[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