[PATCH i2c-tools v4 3/8] decode-dimms: Decode timings and other data for DDR5

Stephen Horvath <[email protected]> Thu, 23 Jul 2026 13:40:13 +0000
Newsgroups org.kernel.vger.linux-i2c
Message-ID <[email protected]>
Decode size, timings, and other data for DDR5 based on the JEDEC
JESD400-5B specification.

Signed-off-by: Stephen Horvath <[email protected]>
---
 eeprom/decode-dimms | 281 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 281 insertions(+)

diff --git a/eeprom/decode-dimms b/eeprom/decode-dimms
index 61bfd57..cd0e2fd 100755
--- a/eeprom/decode-dimms
+++ b/eeprom/decode-dimms
@@ -2128,6 +2128,282 @@ sub decode_ddr4_sdram($)
 	}
 }
 
+# DDR5 Rounding Algorithms
+sub ddr5_min_round($$)
+{
+	my ($tck_ps, $ps) = @_;
+	my $correction = 3;  # 0.30% per the rounding algorithm
+	return int((int($ps) * (1000 - $correction) / int($tck_ps) + 1000) / 1000);
+}
+
+sub ddr5_round_twr($$)
+{
+	my ($tck, $twr) = @_;
+	return $twr / ddr5_min_round($tck, $twr);
+}
+
+# Rounded per DDR5 specifications
+sub ddr5_core_timings($$$$$)
+{
+	my ($cas, $ctime, $trcd, $trp, $tras) = @_;
+
+	$cas += $cas % 2;  # Round up to next even number
+	return $cas . "-" . ddr5_min_round($ctime, $trcd) .
+		      "-" . ddr5_min_round($ctime, $trp) .
+		      "-" . ddr5_min_round($ctime, $tras);
+}
+
+# Return 16-bit value from two bytes
+sub ddr5_16($$)
+{
+	my ($bytes, $index) = @_;
+	return (($bytes->[$index + 1] << 8) | $bytes->[$index]);
+}
+
+# Return 16-bit ps value in ns
+sub ddr5_ns($$)
+{
+	my ($bytes, $index) = @_;
+	return (ddr5_16($bytes, $index) / 1000);
+}
+
+# Parameter: EEPROM bytes 0-639 (using 1-255)
+sub decode_ddr5_sdram($)
+{
+	my $bytes = shift;
+	my ($ctime, $ctime_max);
+	my $ii;
+
+	my @module_types = (
+		{ type => "Reserved (0x00)",	},
+		{ type => "RDIMM",		},
+		{ type => "UDIMM",		},
+		{ type => "SODIMM",		},
+		{ type => "LRDIMM",		},
+		{ type => "CUDIMM",		},
+		{ type => "CSOUDIMM",		},
+		{ type => "MRDIMM",		},
+		{ type => "CAMM2",		},
+		{ type => "Reserved (0x09)",	},
+		{ type => "DDIMM",		},
+		{ type => "Solder down",	},
+		{ type => "Reserved (0x0C)",	},
+		{ type => "Reserved (0x0D)",	},
+		{ type => "Reserved (0x0E)",	},
+		{ type => "Reserved (0x0F)",	},
+	);
+
+# SPD revision
+	printl("SPD Revision", ($bytes->[1] >> 4) . "." . ($bytes->[1] & 0xf));
+
+	my $raw_type = $bytes->[3];
+	my $type = $raw_type & 0x0f;
+	printl("Module Type", $module_types[$type]->{type});
+
+# time bases
+	if (($bytes->[19] & 0x03) != 0x00 || ($bytes->[19] & 0xc0) != 0x00) {
+		print STDERR "Unknown time base values, can't decode\n";
+		return;
+	}
+
+	my $twr = ddr5_16($bytes, 40);
+
+# speed
+	prints("Memory Characteristics");
+
+	$ctime = ddr5_16($bytes, 20);
+	$ctime = ddr5_round_twr($ctime, $twr);
+	$ctime_max = ddr5_16($bytes, 22);
+	$ctime_max = ddr5_round_twr($ctime_max, $twr);
+
+	my $ddrclk = 2 * (1000 / $ctime) * 1000;
+	my $tbits = 8 << ($bytes->[235] & 7);
+	my $pcclk = int ($ddrclk * $tbits / 8);
+	# Round down to comply with Jedec
+	$pcclk = ($pcclk - ($pcclk % 100)) * 2;
+	$ddrclk = int ($ddrclk);
+	printl("Maximum module speed", "$ddrclk MT/s (PC5-${pcclk})");
+
+# Size computation
+	my $rank_mix = $bytes->[234] & 0x40;
+	my $sdram_width0 = 4 << (($bytes->[6] >> 5) & 0x07);
+	my $sdram_width1 = 4 << (($bytes->[10] >> 5) & 0x07);
+	my $bus_width = 8 << ($bytes->[235] & 0x07);
+	my $ranks = (($bytes->[234] >> 3) & 0x07) + 1;
+	my $subchannels = 1 << (($bytes->[235] >> 5) & 0x07);
+
+	my $die_count0 = (($bytes->[4] >> 5) & 0x07) + 1;
+	my $die_3ds0 = $die_count0 > 2;
+	if ($die_3ds0) { $die_count0 >>= 1; }
+
+	my $die_count1 = (($bytes->[8] >> 5) & 0x07) + 1;
+	my $die_3ds1 = $die_count1 > 2;
+	if ($die_3ds1) { $die_count1 >>= 1; }
+
+	my $die_count = $die_count0 + $die_count1;
+
+	my @density_list = (0, 4, 8, 12, 16, 24, 32, 48, 64);
+	my $density0 = $density_list[$bytes->[4] & 0x1f];
+	my $density1 = $density_list[$bytes->[8] & 0x1f];
+
+	my $cap0 = $subchannels * ($bus_width / $sdram_width0) * $die_count0 * ($density0 / 8) * $ranks;
+	my $cap1 = $subchannels * ($bus_width / $sdram_width1) * $die_count1 * ($density1 / 8) * $ranks;
+	my $cap = $cap0 + $cap1;
+
+	printl("Size", $cap . " GB");
+
+	printl("Banks x Rows x Columns x Bits" . ($rank_mix ? " (Even Rank)" : ""),
+	       join(' x ', (1 << (($bytes->[7] >> 5) & 0x07)) * (1 << ($bytes->[7] & 0x07)),
+			   (( $bytes->[5]       & 0x1f) + 16),
+			   ((($bytes->[5] >> 5) & 0x05) + 10),
+			   (8 << ($bytes->[235] & 0x07))));
+
+	printl_cond($rank_mix, "Banks x Rows x Columns x Bits (Odd Rank)",
+		    join(' x ', (1 << (($bytes->[11] >> 5) & 0x07)) * (1 << ($bytes->[11] & 0x07)),
+			        (( $bytes->[9]       & 0x1f) + 16),
+			        ((($bytes->[9] >> 5) & 0x05) + 10),
+			        (8 << ($bytes->[235] & 0x07))));
+
+	printl("SDRAM Device Width" . ($rank_mix ? " (Even Rank)" : ""), "$sdram_width0 bits");
+	printl_cond($rank_mix, "SDRAM Device Width (Odd Rank)", "$sdram_width1 bits");
+
+	printl("Ranks", $ranks);
+	printl_cond($ranks > 1, "Rank Mix",
+		    $rank_mix ? "Asymmetrical" : "Symmetrical");
+	printl("Primary Bus Width", (8 << ($bytes->[235] & 7))." bits");
+	printl_cond($bytes->[235] & 0x18, "Bus Width Extension", (($bytes->[235] & 0x18) >> 1) ." bits");
+
+	my $taa;
+	my $trcd;
+	my $trp;
+	my $tras;
+
+	$taa  = ddr5_16($bytes, 30);
+	$trcd = ddr5_16($bytes, 32);
+	$trp  = ddr5_16($bytes, 34);
+	$tras = ddr5_16($bytes, 36);
+
+	printl("CL-RCD-RP-RAS (cycles)",
+	       ddr5_core_timings(ddr5_min_round($ctime, $taa),
+						$ctime, $trcd, $trp, $tras));
+
+# latencies
+	my %cas;
+	my $cas_sup = ($bytes->[28] << 32) + ($bytes->[27] << 24) +
+		      ($bytes->[26] << 16) + ($bytes->[25] << 8) + $bytes->[24];
+	my $base_cas = 20;
+
+	for ($ii = 0; $ii < 40; $ii++) {
+		if ($cas_sup & (1 << $ii)) {
+			$cas{$base_cas + ($ii * 2)}++;
+		}
+	}
+	printl("Supported CAS Latencies", cas_latencies(keys %cas));
+
+# standard DDR5 speeds
+	prints("Timings at Standard Speeds");
+	foreach my $ctime_at_speed (5/23, 5/22, 5/21, 5/20, 5/19, 5/18, 5/17, 5/16,
+				    5/15, 5/14, 5/13, 5/12, 5/11, 5/10, 5/9, 5/8) {
+		my $best_cas = 0;
+
+		$ctime_at_speed *= 1000;  # Convert to ps for calculations
+
+		# Find min CAS latency at this speed
+		for ($ii = 39; $ii >= 0; $ii--) {
+			next unless ($cas_sup & (1 << $ii));
+			if (ddr5_min_round($ctime_at_speed, $taa) < $base_cas + ($ii * 2)) {
+				$best_cas = $base_cas + ($ii * 2);
+			}
+		}
+
+		printl_cond($best_cas && $ctime_at_speed >= $ctime
+				      && $ctime_at_speed <= $ctime_max,
+			    "CL-RCD-RP-RAS (cycles)" . as_ddr(5, $ctime_at_speed / 1000),
+			    ddr5_core_timings($best_cas, $ctime_at_speed, $trcd, $trp, $tras));
+	}
+
+# more timing information
+	prints("Timing Parameters");
+
+	printl("Minimum Cycle Time (tCKmin)", tns3($ctime / 1000));
+	printl("Maximum Cycle Time (tCKmax)", tns3($ctime_max / 1000));
+	printl("Minimum CAS Latency Time (tAA)", tns3($taa / 1000));
+	printl("Minimum Active to Read/Write Delay (tRCD)", tns3($trcd / 1000));
+	printl("Minimum Row Precharge Delay (tRP)", tns3($trp / 1000));
+	printl("Minimum Active to Precharge Delay (tRAS)", tns3($tras / 1000));
+	printl("Minimum Active to Auto-Refresh Delay (tRC)", tns3(ddr5_ns($bytes, 38)));
+	printl("Minimum Write Recovery Time (tWR)", tns3($twr / 1000));
+	printl("Minimum Normal Recovery Delay (tRFC1)", tns3(ddr5_16($bytes, 42)));
+	printl("Minimum Fine Recovery Delay (tRFC2)", tns3(ddr5_16($bytes, 44)));
+	printl("Minimum Same Bank Recovery Delay (tRFCsb)", tns3(ddr5_16($bytes, 46)));
+	printl("Minimum Row Active to Row Active Delay (tRRD_L)",
+	       tns3(ddr5_ns($bytes, 70)) . " / " . $bytes->[72] . " cycles");
+	printl("Minimum Read to Read Delay (tCCD_L)",
+	       tns3(ddr5_ns($bytes, 73)) . " / " . $bytes->[75] . " cycles");
+	printl("Minimum Write to Write Delay (tCCD_L_WR)",
+	       tns3(ddr5_ns($bytes, 76)) . " / " . $bytes->[78] . " cycles");
+	printl("Minimum 2nd Write to Write Delay (tCCD_L_WR2)",
+	       tns3(ddr5_ns($bytes, 79)) . " / " . $bytes->[81] . " cycles");
+	printl("Minimum Four Activate Window Delay (tFAW)",
+	       tns3(ddr5_ns($bytes, 82)) . " / " . $bytes->[84] . " cycles");
+	printl("Minimum Write to Read Delay for Same Bank Group (tCCD_L_WTR)",
+	       tns3(ddr5_ns($bytes, 85)) . " / " . $bytes->[87] . " cycles");
+	printl("Minimum Write to Read Delay for Different Bank Group (tCCD_S_WTR)",
+	       tns3(ddr5_ns($bytes, 88)) . " / " . $bytes->[90] . " cycles");
+	printl("Minimum Read to Precharge Delay (tRTP)",
+	       tns3(ddr5_ns($bytes, 91)) . " / " . $bytes->[93] . " cycles");
+
+	# Optional? Some modules set these to 0, which is 'Reserved' per the spec
+	printl_cond($bytes->[96], "Minimum Read to Read Delay Delay for Different Bank in Group (tCCD_M)",
+		    tns3(ddr5_ns($bytes, 94)) . " / " . $bytes->[96] . " cycles");
+	printl_cond($bytes->[99], "Minimum Write to Write Delay for Different Bank in Group (tCCD_M_WR)",
+		    tns3(ddr5_ns($bytes, 97)) . " / " . $bytes->[99] . " cycles");
+	printl_cond($bytes->[102], "Minimum Write to Read Delay for Different Bank in Group (tCCD_M_WTR)",
+		    tns3(ddr5_ns($bytes, 100)) . " / " . $bytes->[102] . " cycles");
+
+# miscellaneous stuff
+	prints("Other Information");
+
+	my $package_type0 = $die_3ds0 ? "3DS" :
+			    $die_count0 > 1 ? "Dual-die package" :
+			    $die_count0 == 1 ? "Monolithic" : "Unknown";
+	$package_type0 .= sprintf(" (%u dies)", $die_count0) if $die_count0 >= 2;
+	printl("Package Type" . ($rank_mix ? " (Even Rank)" : ""), $package_type0);
+
+	my $package_type1 = $die_3ds1 ? "3DS" :
+			    $die_count1 > 1 ? "Dual-die package" :
+			    $die_count1 == 1 ? "Monolithic" : "Unknown";
+	$package_type1 .= sprintf(" (%u dies)", $die_count1) if $die_count1 >= 2;
+	printl_cond($rank_mix, "Package Type (Odd Rank)", $package_type1);
+
+	my $ppr = $bytes->[12] >> 7;
+	printl("Post Package Repair",
+	       $ppr == 0x00 ? "One row per bank group" :
+	       $ppr == 0x01 ? "One row per bank" : "Unknown");
+	printl("Soft PPR Undo/Lock", $bytes->[12] & 0x20 ?
+	       "Supported" : "Not Supported");
+	printl("MBIST PPR", $bytes->[12] & 0x02 ?
+	       "Supported" : "Not Supported");
+
+	printl("Module Nominal Voltage (VDD)",
+	       ($bytes->[16] & 0xf0) == 0x00 ? "1.1 V" :
+	       ($bytes->[16] & 0x0c) == 0x00 ? "Unknown (1.1 V operable)" :
+	       ($bytes->[16] & 0x03) == 0x00 ? "Unknown (1.1 V endurant)" : "Unknown");
+
+	printl("Module Nominal Voltage (VDDQ)",
+	       ($bytes->[17] & 0xf0) == 0x00 ? "1.1 V" :
+	       ($bytes->[17] & 0x0c) == 0x00 ? "Unknown (1.1 V operable)" :
+	       ($bytes->[17] & 0x03) == 0x00 ? "Unknown (1.1 V endurant)" : "Unknown");
+
+	printl("Module Nominal Voltage (VPP)",
+	       ($bytes->[18] & 0xf0) == 0x00 ? "1.8 V" :
+	       ($bytes->[18] & 0x0c) == 0x00 ? "Unknown (1.8 V operable)" :
+	       ($bytes->[18] & 0x03) == 0x00 ? "Unknown (1.8 V endurant)" : "Unknown");
+
+	printl("Thermal Sensor",
+	       $bytes->[14] & 0x08 ? "Supported" : "No");
+}
+
 # Parameter: EEPROM bytes 0-127 (using 4-5)
 sub decode_direct_rambus($)
 {
@@ -2178,6 +2454,10 @@ sub decode_rambus($)
 	"DDR4E SDRAM"	=> \&decode_ddr4_sdram,
 	"LPDDR4 SDRAM"	=> \&decode_ddr4_sdram,
 	"LPDDR4X SDRAM"	=> \&decode_ddr4_sdram,
+	"DDR5 SDRAM"	=> \&decode_ddr5_sdram,
+	"LPDDR5 SDRAM"	=> \&decode_ddr5_sdram,
+	"DDR5 NVDIMM-P"	=> \&decode_ddr5_sdram,
+	"LPDDR5X SDRAM"	=> \&decode_ddr5_sdram,
 	"Direct Rambus"	=> \&decode_direct_rambus,
 	"Rambus"	=> \&decode_rambus,
 );
@@ -2844,6 +3124,7 @@ for $current (0 .. $#dimm) {
 			"DDR4E SDRAM", "LPDDR3 SDRAM",	# 14, 15
 			"LPDDR4 SDRAM", "LPDDR4X SDRAM", # 16, 17
 			"DDR5 SDRAM", "LPDDR5 SDRAM",	# 18, 19
+			"DDR5 NVDIMM-P", "LPDDR5X SDRAM", # 20, 21
 		);
 		if ($bytes[2] < @type_list) {
 			$type = $type_list[$bytes[2]];

-- 
2.53.0