[PATCH i2c-tools v4 8/8] decode-dimms: Add DDR5 XMP 3.0 and EXPO decoding

Stephen Horvath <[email protected]> Thu, 23 Jul 2026 13:40:17 +0000
Newsgroups org.kernel.vger.linux-i2c
Message-ID <[email protected]>
Implement decoding of up to 3 XMP 3.0 profiles, and 2 EXPO profiles, for
DDR5 DIMMs.

As I don't have the datasheets for XMP 3.0, or EXPO, this is referenced
on code found in edlf/DDRXMPEditor[1],
The-Open-Memory-Initiative-OMI/spdr[2], and prior versions of XMP.

[1] https://github.com/edlf/DDRXMPEditor/blob/main/DDR5SPD/DDR5_SPD.cs
[2] https://github.com/The-Open-Memory-Initiative-OMI/spdr/blob/main/spdr/src/vendor.rs

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

diff --git a/eeprom/decode-dimms b/eeprom/decode-dimms
index 9ddbacf..9cf7fec 100755
--- a/eeprom/decode-dimms
+++ b/eeprom/decode-dimms
@@ -802,6 +802,14 @@ sub value_or_undefined
 	return $value;
 }
 
+# print a voltage encoded in the XMP 1.0 format
+# bits 7:5 are the integer part in volts, bits 4:0 are the fractional part in 50 mV increments
+sub xmp1_voltage($) {
+	my $byte = shift;
+	my $millivolts = (($byte >> 5) * 1000) + (($byte & 0x1f) * 50);
+	return sprintf("%.2f V", $millivolts / 1000);
+}
+
 # Common to SDR, DDR and DDR2 SDRAM
 sub sdram_voltage_interface_level($)
 {
@@ -2529,6 +2537,188 @@ sub decode_ddr5_sdram($)
 	}
 }
 
+# Parameter: EEPROM bytes 0-1023 (using 640-895)
+sub decode_ddr5_xmp3_data($)
+{
+
+	my $bytes = shift;
+	my $block_size = 64;
+
+	# Check for 0x0C4A magic and 0x30 version.
+	if ($bytes->[640] != 0x0C || $bytes->[641] != 0x4A || $bytes->[642] != 0x30) {
+		return;
+	}
+	
+	prints("XMP 3.0 Profiles");
+	my $profile_count = 0;
+	for (my $ii = 0; $ii < 3; $ii++) {
+		if (($bytes->[643] >> $ii) & 1) {
+			$profile_count++;
+		}
+	}
+
+	my $crc_spd = ($bytes->[703] << 8) | $bytes->[702];
+	print_crc_check($bytes, 640, $block_size, $crc_spd);
+
+	for (my $ii = 0; $ii < $profile_count; $ii++) {
+		prints2("Profile " . ($ii + 1));
+
+		# Name offset
+		my $offset = 654 + ($ii * 16);
+		my $name = join('', map { chr($_) } @{$bytes}[$offset .. $offset + 15]);
+		# Strip off trailing nulls/newlines
+		$name =~ s/(\0|\r|\n).*//;
+		printl("Profile Name", $name);
+
+		# Data offset
+		$offset = 704 + ($ii * $block_size);
+
+		my $profile_crc_spd = ($bytes->[$offset + $block_size - 1] << 8) | $bytes->[$offset + $block_size - 2];
+		print_crc_check($bytes, $offset, $block_size, $profile_crc_spd);
+
+		my $twr = ddr5_16($bytes, $offset + 23);
+		my $ctime = ddr5_16($bytes, $offset + 5);
+		$ctime = ddr5_round_twr($ctime, $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})");
+
+		my $taa  = ddr5_16($bytes, $offset + 13);
+		my $trcd = ddr5_16($bytes, $offset + 15);
+		my $trp  = ddr5_16($bytes, $offset + 17);
+		my $tras = ddr5_16($bytes, $offset + 19);
+
+		printl("CL-RCD-RP-RAS (cycles)",
+		ddr5_core_timings(ddr5_min_round($ctime, $taa),
+						 $ctime, $trcd, $trp, $tras));
+
+		my %cas;
+		my $cas_sup = ($bytes->[$offset + 11] << 32) + ($bytes->[$offset + 10] << 24) +
+			($bytes->[$offset + 9] << 16) + ($bytes->[$offset + 8] << 8) + $bytes->[$offset + 7];
+		my $base_cas = 20;
+
+		for (my $bit = 0; $bit < 40; $bit++) {
+			if ($cas_sup & (1 << $bit)) {
+				$cas{$base_cas + ($bit * 2)}++;
+			}
+		}
+		printl("Supported CAS Latencies", cas_latencies(keys %cas));
+
+		printl("Minimum Cycle Time (tCKmin)", tns3($ctime / 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, $offset + 21)));
+		printl("Minimum Write Recovery Time (tWR)", tns3($twr / 1000));
+		printl("Minimum Normal Recovery Delay (tRFC1)", tns3(ddr5_16($bytes, $offset + 25)));
+		printl("Minimum Fine Recovery Delay (tRFC2)", tns3(ddr5_16($bytes, $offset + 27)));
+		printl("Minimum Same Bank Recovery Delay (tRFCsb)", tns3(ddr5_16($bytes, $offset + 29)));
+		# Optional it seems
+		printl_cond($bytes->[$offset + 33], "Minimum Row Active to Row Active Delay (tRRD_L)",
+		            tns3(ddr5_ns($bytes, $offset + 31)) . " / " . $bytes->[$offset + 33] . " cycles");
+		printl_cond($bytes->[$offset + 48], "Minimum Read to Read Delay (tCCD_L)",
+		            tns3(ddr5_ns($bytes, $offset + 46)) . " / " . $bytes->[$offset + 48] . " cycles");
+		printl_cond($bytes->[$offset + 36], "Minimum Write to Write Delay (tCCD_L_WR)",
+		            tns3(ddr5_ns($bytes, $offset + 34)) . " / " . $bytes->[$offset + 36] . " cycles");
+		printl_cond($bytes->[$offset + 39], "Minimum 2nd Write to Write Delay (tCCD_L_WR2)",
+		            tns3(ddr5_ns($bytes, $offset + 37)) . " / " . $bytes->[$offset + 39] . " cycles");
+		printl_cond($bytes->[$offset + 54], "Minimum Four Activate Window Delay (tFAW)",
+		            tns3(ddr5_ns($bytes, $offset + 52)) . " / " . $bytes->[$offset + 54] . " cycles");
+		printl_cond($bytes->[$offset + 42], "Minimum Write to Read Delay for Same Bank Group (tCCD_L_WTR)",
+		            tns3(ddr5_ns($bytes, $offset + 40)) . " / " . $bytes->[$offset + 42] . " cycles");
+		printl_cond($bytes->[$offset + 45], "Minimum Write to Read Delay for Different Bank Group (tCCD_S_WTR)",
+		            tns3(ddr5_ns($bytes, $offset + 43)) . " / " . $bytes->[$offset + 45] . " cycles");
+		printl_cond($bytes->[$offset + 51], "Minimum Read to Precharge Delay (tRTP)",
+		            tns3(ddr5_ns($bytes, $offset + 49)) . " / " . $bytes->[$offset + 51] . " cycles");
+
+		printl("Supply Voltage (VDD)", xmp1_voltage($bytes->[$offset + 1]));
+		printl("I/O Supply Voltage (VDDQ)", xmp1_voltage($bytes->[$offset + 2]));
+		printl("Pump Voltage (VPP)", xmp1_voltage($bytes->[$offset + 0]));
+	}
+}
+
+# Parameter: EEPROM bytes 0-1023 (using 832-959)
+sub decode_ddr5_expo_data($)
+{
+	my $bytes = shift;
+
+	# Check for "EXPO" magic.
+	if ($bytes->[832] != ord('E') || $bytes->[833] != ord('X') || $bytes->[834] != ord('P') || $bytes->[835] != ord('O')) {
+		return;
+	}
+	
+	prints("EXPO Profiles");
+	my $profile_count = 2;
+	my $block_size = 40;
+
+	my $crc_spd = ($bytes->[959] << 8) | $bytes->[958];
+	print_crc_check($bytes, 832, 128, $crc_spd);
+
+	for (my $ii = 0; $ii < $profile_count; $ii++) {
+		prints2("Profile " . ($ii + 1));
+		my $offset = 842 + ($ii * $block_size);
+
+		my $twr = ddr5_16($bytes, $offset + 16);
+		my $ctime = ddr5_16($bytes, $offset + 4);
+		$ctime = ddr5_round_twr($ctime, $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})");
+
+		my $taa  = ddr5_16($bytes, $offset + 6);
+		my $trcd = ddr5_16($bytes, $offset + 8);
+		my $trp  = ddr5_16($bytes, $offset + 10);
+		my $tras = ddr5_16($bytes, $offset + 12);
+
+		printl("CL-RCD-RP-RAS (cycles)",
+		ddr5_core_timings(ddr5_min_round($ctime, $taa),
+						 $ctime, $trcd, $trp, $tras));
+
+		printl("Minimum Cycle Time (tCKmin)", tns3($ctime / 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, $offset + 14)));
+		printl("Minimum Write Recovery Time (tWR)", tns3($twr / 1000));
+		printl("Minimum Normal Recovery Delay (tRFC1)", tns3(ddr5_16($bytes, $offset + 18)));
+		printl("Minimum Fine Recovery Delay (tRFC2)", tns3(ddr5_16($bytes, $offset + 20)));
+		printl("Minimum Same Bank Recovery Delay (tRFCsb)", tns3(ddr5_16($bytes, $offset + 22)));
+		# Optional it seems
+		printl_cond($bytes->[$offset + 24], "Minimum Row Active to Row Active Delay (tRRD_L)",
+			    tns3(ddr5_ns($bytes, $offset + 24)));
+		printl_cond($bytes->[$offset + 26], "Minimum Read to Read Delay (tCCD_L)",
+			    tns3(ddr5_ns($bytes, $offset + 26)));
+		printl_cond($bytes->[$offset + 28], "Minimum Write to Write Delay (tCCD_L_WR)",
+			    tns3(ddr5_ns($bytes, $offset + 28)));
+		printl_cond($bytes->[$offset + 30], "Minimum 2nd Write to Write Delay (tCCD_L_WR2)",
+			    tns3(ddr5_ns($bytes, $offset + 30)));
+		printl_cond($bytes->[$offset + 32], "Minimum Four Activate Window Delay (tFAW)",
+			    tns3(ddr5_ns($bytes, $offset + 32)));
+		printl_cond($bytes->[$offset + 34], "Minimum Write to Read Delay for Same Bank Group (tCCD_L_WTR)",
+			    tns3(ddr5_ns($bytes, $offset + 34)));
+		printl_cond($bytes->[$offset + 36], "Minimum Write to Read Delay for Different Bank Group (tCCD_S_WTR)",
+			    tns3(ddr5_ns($bytes, $offset + 36)));
+		printl_cond($bytes->[$offset + 38], "Minimum Read to Precharge Delay (tRTP)",
+			    tns3(ddr5_ns($bytes, $offset + 38)));
+
+		printl("Supply Voltage (VDD)", xmp1_voltage($bytes->[$offset + 0]));
+		printl("I/O Supply Voltage (VDDQ)", xmp1_voltage($bytes->[$offset + 1]));
+		printl("Pump Voltage (VPP)", xmp1_voltage($bytes->[$offset + 2]));
+	}
+}
+
 # Parameter: EEPROM bytes 0-127 (using 4-5)
 sub decode_direct_rambus($)
 {
@@ -3124,6 +3314,18 @@ sub check_crc($)
 		sprintf("0x%04X", $crc));
 }
 
+sub print_crc_check($$$$)
+{
+	my ($bytes, $offset, $length, $crc_spd) = @_;
+	my $crc_calc = calculate_crc($bytes, $offset, $length - 2);
+	my $crc_ok = $crc_calc == $crc_spd;
+	printl("EEPROM CRC of bytes " . $offset . "-" . ($offset + $length - 1), $crc_ok ?
+		sprintf("OK (0x\%04X)", $crc_calc) :
+		sprintf("Bad\n(found 0x\%04X, calculated 0x\%04X)",
+			$crc_spd, $crc_calc));
+	return $crc_ok;
+}
+
 # Parse command-line
 foreach (@ARGV) {
 	if ($_ eq '-h' || $_ eq '--help') {
@@ -3427,13 +3629,16 @@ for $current (0 .. $#dimm) {
 		 $type eq "LPDDR5 SDRAM" ||
 		 $type eq "DDR5 NVDIMM-P" ||
 		 $type eq "LPDDR5X SDRAM") {
-		if (@bytes >= 640) {
+		if (@bytes >= 1024) {
 			# Decode DDR5-specific manufacturing data in bytes
 			# 512-639
 			decode_ddr5_mfg_data(\@bytes);
 			# Decode DDR5-specific error log
 			# 640-1023 (max)
 			decode_ddr5_error_data(\@bytes);
+			# Decode DDR5-specific memory profiles
+			decode_ddr5_xmp3_data(\@bytes);
+			decode_ddr5_expo_data(\@bytes);
 		}
 	} else {
 		# Decode next 35 bytes (64-98, common to most

-- 
2.53.0