* [PATCH i2c-tools v3 0/8] decode-dimms: Implement DDR5 decoding
@ 2026-06-29 7:09 Stephen Horvath
2026-06-29 7:10 ` [PATCH i2c-tools v3 2/8] decode-dimms: Decode DDR5 Manufacturer Data Stephen Horvath
` (8 more replies)
0 siblings, 9 replies; 10+ messages in thread
From: Stephen Horvath @ 2026-06-29 7:09 UTC (permalink / raw)
To: Wolfram Sang, Jean Delvare, linux-i2c@vger.kernel.org
Cc: Stephen Horvath, Guenter Roeck, Kamil Aronowski
Hi, this series of patches adds DDR5 support to decode-dimms.
I'm not too experienced with perl or the JEDEC specs, so there's
probably going to be some questionable choices here, but I'd love to
hear feedback.
The first 4 patches (1, 2, 3, 4) add the essential DDR5 information to
decode-dimms.
The patches 5 & 7 haven't been tested on hardware implementations so I'm
happy for them to be dropped if they're not useful. However they add
type specific information and error log support.
Patch 8 adds XMP and EXPO support for DDR5, though I don't have access
to the datasheet for either, so I'm fine if they get dropped as well.
Though I may also implement XMP for DDR3 and DDR4 in another series if
these are well received.
Patch 6 just adds a new function `prints2`, to print a subheading,
used for the error logs, XMP and EXPO.
V2 has been rebased onto master, and patch 1 has been updated to check
byte 2 for DDR5 identification, not just byte 0, and the commit
messages have been made more formal.
V3 has again been rebased onto master, as well as:
- The density calculation has been fixed so it no longer provides
incorrect results for >48GB modules
- tCCD_L_WR, tCCD_L_WR2, tRTP, tCCD_M, tCCD_M_WR, tCCD_M_WTR timings
have been added
- VDDQ and VPP voltages added
- Usages of DDR4 rounding has been replaced with correct DDR5 rounding
- Added a standard speed (9200) found in JESD400-5D.01
- The timings have been rearranged in order of address
- The text for the timings has been updated to more accurately reflect
the JEDEC spec
- Patches 6 and 8 have been added (`prints2` + XMP/EXPO support)
- Error log support (previously patch 6, now 7) has been improved
- Comments in patch 1 have been updated with DDR5 info
- Some patch messages have been changed slightly
Signed-off-by: Stephen Horvath <s.horvath@outlook.com.au>
---
Stephen Horvath (8):
decode-dimms: Implement DDR5 checksum parsing
decode-dimms: Decode DDR5 Manufacturer Data
decode-dimms: Decode timings and other data for DDR5
decode-dimms: Decode DDR5 common module information
decode-dimms: Add basic decoding of type specific information for DDR5
decode-dimms: Add second level separator
decode-dimms: Decode DDR5 error log
decode-dimms: Add DDR5 XMP 3.0 and EXPO decoding
eeprom/decode-dimms | 839 +++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 833 insertions(+), 6 deletions(-)
---
base-commit: 5852f5375eea6ca9dabf31f4772cb8b8ecc905d8
change-id: 20260624-ddr5-8f35700a6a50
Best regards,
--
Stephen Horvath <s.horvath@outlook.com.au>
^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH i2c-tools v3 1/8] decode-dimms: Implement DDR5 checksum parsing
2026-06-29 7:09 [PATCH i2c-tools v3 0/8] decode-dimms: Implement DDR5 decoding Stephen Horvath
2026-06-29 7:10 ` [PATCH i2c-tools v3 2/8] decode-dimms: Decode DDR5 Manufacturer Data Stephen Horvath
@ 2026-06-29 7:10 ` Stephen Horvath
2026-06-29 7:10 ` [PATCH i2c-tools v3 3/8] decode-dimms: Decode timings and other data for DDR5 Stephen Horvath
` (6 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Stephen Horvath @ 2026-06-29 7:10 UTC (permalink / raw)
To: Wolfram Sang, Jean Delvare, linux-i2c@vger.kernel.org
Cc: Stephen Horvath, Guenter Roeck, Kamil Aronowski
The CRC for the DDR5 SPD is located at bytes 510-511 of the SPD data.
There is now functionality to read upto byte 512 and calculate the CRC16
if byte 2 indicates DDR5 memory and byte 0 indicates at least 512 bytes.
This patch is based off of a patch by Guenter Roeck, linked below.
Link: https://lore.kernel.org/linux-hwmon/efb77b37-30e5-48a8-b4af-eb9995a2882b@roeck-us.net/
Cc: Guenter Roeck <linux@roeck-us.net>
Signed-off-by: Stephen Horvath <s.horvath@outlook.com.au>
---
eeprom/decode-dimms | 32 ++++++++++++++++++++++++++------
1 file changed, 26 insertions(+), 6 deletions(-)
diff --git a/eeprom/decode-dimms b/eeprom/decode-dimms
index 7682345..d694f41 100755
--- a/eeprom/decode-dimms
+++ b/eeprom/decode-dimms
@@ -22,7 +22,8 @@
# the legacy eeprom driver (in the kernel tree since v2.6.0). For kernels
# older than 2.6.0, the eeprom driver can be found in the lm-sensors 2
# package. For DDR4, you need the ee1004 driver (in the kernel tree since
-# kernel v4.20).
+# kernel v4.20). For DDR5, you need the spd5118 driver (in the kernel tree
+# since v6.11).
#
# References:
# PC SDRAM Serial Presence
@@ -2402,7 +2403,12 @@ sub spd_sizes($)
my $bytes = shift;
my $type = $bytes->[2];
- if ($type == 12 || $type == 14 || $type == 16 || $type == 17) {
+ if ($type == 18 || $type == 19 || $type == 20 || $type == 21) {
+ # DDR5
+ my $spd_len = 256 * ((($bytes->[0] >> 4) & 7) + 1);
+ my $used = $spd_len;
+ return ($spd_len, $used);
+ } elsif ($type == 12 || $type == 14 || $type == 16 || $type == 17) {
# DDR4
my $spd_len = 256 * (($bytes->[0] >> 4) & 7);
my $used = 128 * ($bytes->[0] & 15);
@@ -2511,10 +2517,16 @@ sub calculate_crc($$$)
sub check_crc($)
{
my $bytes = shift;
+ my $is_ddr5 = ($bytes->[0] & 0x70) == 0x30;
my $crc_cover = $bytes->[0] & 0x80 ? 116 : 125;
+ my $crc_start = 126;
+ if ($is_ddr5) {
+ $crc_cover = 509;
+ $crc_start = 510;
+ }
my $crc = calculate_crc($bytes, 0, $crc_cover + 1);
- my $dimm_crc = ($bytes->[127] << 8) | $bytes->[126];
+ my $dimm_crc = ($bytes->[$crc_start + 1] << 8) | $bytes->[$crc_start];
return ("EEPROM CRC of bytes 0-$crc_cover",
($dimm_crc == $crc) ? 1 : 0,
sprintf("0x%04X", $dimm_crc),
@@ -2617,7 +2629,8 @@ sub get_dimm_list
if ($use_sysfs) {
@drivers = ('eeprom',
'at24',
- 'ee1004'); # DDR4
+ 'ee1004', # DDR4
+ 'spd5118'); # DDR5
} else {
@drivers = ('eeprom');
$dir = '/proc/sys/dev/sensors';
@@ -2642,7 +2655,8 @@ sub get_dimm_list
next unless defined $attr &&
($attr eq "eeprom" ||
$attr eq "spd" ||
- $attr eq "ee1004"); # DDR4
+ $attr eq "ee1004" || # DDR4
+ $attr eq "spd5118"); # DDR5
} else {
next unless $file =~ /^eeprom-/;
}
@@ -2654,7 +2668,7 @@ sub get_dimm_list
}
if (!$opened) {
- print STDERR "No EEPROM found, try loading the eeprom, at24 or ee1004 module\n";
+ print STDERR "No EEPROM found, try loading the eeprom, at24, ee1004 or spd5118 module\n";
exit;
}
@@ -2684,6 +2698,12 @@ for my $i (0 .. $#dimm) {
$dimm[$i]->{chk_spd}, $dimm[$i]->{chk_calc}) =
checksum(\@bytes);
} else {
+
+ # Check for DDR5 protocol type 18, 19, 20, 21 and size >= 512
+ if ($bytes[2] >= 18 && $bytes[2] <= 21 && ($bytes[0] & 0x70) >= 0x30) {
+ # DDR5's checksum is at 510-511
+ push(@bytes, readspd(@bytes, 512, $dimm[$i]->{file}));
+ }
($dimm[$i]->{chk_label}, $dimm[$i]->{chk_valid},
$dimm[$i]->{chk_spd}, $dimm[$i]->{chk_calc}) =
check_crc(\@bytes);
--
2.53.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH i2c-tools v3 2/8] decode-dimms: Decode DDR5 Manufacturer Data
2026-06-29 7:09 [PATCH i2c-tools v3 0/8] decode-dimms: Implement DDR5 decoding Stephen Horvath
@ 2026-06-29 7:10 ` Stephen Horvath
2026-06-29 7:10 ` [PATCH i2c-tools v3 1/8] decode-dimms: Implement DDR5 checksum parsing Stephen Horvath
` (7 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Stephen Horvath @ 2026-06-29 7:10 UTC (permalink / raw)
To: Wolfram Sang, Jean Delvare, linux-i2c@vger.kernel.org
Cc: Stephen Horvath, Guenter Roeck, Kamil Aronowski
Decode the DRAM's manufacturer data for DDR5.
There are more manufacturers in the common and type specific sections,
but only the 'Manufacturing Information' section of the eeprom has been
implemented.
Signed-off-by: Stephen Horvath <s.horvath@outlook.com.au>
---
eeprom/decode-dimms | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 51 insertions(+)
diff --git a/eeprom/decode-dimms b/eeprom/decode-dimms
index d694f41..dd03d2c 100755
--- a/eeprom/decode-dimms
+++ b/eeprom/decode-dimms
@@ -2279,6 +2279,48 @@ sub decode_ddr4_mfg_data($)
sprintf("0x%02X", $bytes->[349]));
}
+# Parameter: EEPROM bytes 0-639 (using 512-554)
+sub decode_ddr5_mfg_data($)
+{
+ my $bytes = shift;
+
+ prints("Manufacturer Data");
+
+ printl("Module Manufacturer",
+ manufacturer_ddr3($bytes->[512], $bytes->[513]));
+
+ printl_cond(spd_written(@{$bytes}[552..553]),
+ "DRAM Manufacturer",
+ manufacturer_ddr3($bytes->[552], $bytes->[553]));
+
+ printl_mfg_location_code($bytes->[514]);
+
+ printl_cond(spd_written(@{$bytes}[515..516]),
+ "Manufacturing Date",
+ manufacture_date($bytes->[515], $bytes->[516]));
+
+ printl_mfg_assembly_serial(@{$bytes}[517..520]);
+
+ printl("Part Number", part_number(@{$bytes}[521..550]));
+
+ printl_cond(spd_written(@{$bytes}[551]),
+ "Revision Code",
+ sprintf("0x%02X", $bytes->[551]));
+
+ if ($bytes->[554] != 0xff) {
+ # DRAM Stepping may be a number or an uppercase ASCII letter
+ # 0x00-0xfe is valid, 0xff is invalid
+ my $stepping = $bytes->[554];
+ if ($stepping < 0x41 || $stepping > 0x5a) {
+ printl("DRAM Stepping",
+ sprintf("0x%02X", $stepping));
+ } else {
+ printl("DRAM Stepping",
+ sprintf("%c", $stepping));
+ }
+ }
+}
+
# Parameter: EEPROM bytes 0-127 (using 64-98)
sub decode_manufacturing_information($)
{
@@ -2832,6 +2874,15 @@ for $current (0 .. $#dimm) {
} elsif (!$use_hexdump && $dimm[$current]->{driver} ne "ee1004") {
print STDERR "HINT: You should be using the ee1004 driver instead of the $dimm[$current]->{driver} driver\n";
}
+ } elsif ($type eq "DDR5 SDRAM" ||
+ $type eq "LPDDR5 SDRAM" ||
+ $type eq "DDR5 NVDIMM-P" ||
+ $type eq "LPDDR5X SDRAM") {
+ if (@bytes >= 640) {
+ # Decode DDR5-specific manufacturing data in bytes
+ # 512-639
+ decode_ddr5_mfg_data(\@bytes);
+ }
} else {
# Decode next 35 bytes (64-98, common to most
# memory types)
--
2.53.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH i2c-tools v3 3/8] decode-dimms: Decode timings and other data for DDR5
2026-06-29 7:09 [PATCH i2c-tools v3 0/8] decode-dimms: Implement DDR5 decoding Stephen Horvath
2026-06-29 7:10 ` [PATCH i2c-tools v3 2/8] decode-dimms: Decode DDR5 Manufacturer Data Stephen Horvath
2026-06-29 7:10 ` [PATCH i2c-tools v3 1/8] decode-dimms: Implement DDR5 checksum parsing Stephen Horvath
@ 2026-06-29 7:10 ` Stephen Horvath
2026-06-29 7:10 ` [PATCH i2c-tools v3 5/8] decode-dimms: Add basic decoding of type specific information " Stephen Horvath
` (5 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Stephen Horvath @ 2026-06-29 7:10 UTC (permalink / raw)
To: Wolfram Sang, Jean Delvare, linux-i2c@vger.kernel.org
Cc: Stephen Horvath, Guenter Roeck, Kamil Aronowski
Decode size, timings, and other data for DDR5 based on the JEDEC
JESD400-5B specification.
Signed-off-by: Stephen Horvath <s.horvath@outlook.com.au>
---
eeprom/decode-dimms | 279 ++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 279 insertions(+)
diff --git a/eeprom/decode-dimms b/eeprom/decode-dimms
index dd03d2c..4f9c4f0 100755
--- a/eeprom/decode-dimms
+++ b/eeprom/decode-dimms
@@ -2128,6 +2128,280 @@ sub decode_ddr4_sdram($)
}
}
+# DDR5 Rounding Algorithms
+sub ddr5_min_round($$)
+{
+ my ($tck, $ps) = @_;
+ my $correction = 3; # 0.30% per the rounding algorithm
+ return int(($ps * (1000 - $correction) / $tck + 1000) / 1000);
+}
+
+sub ddr5_round_twr($$)
+{
+ my ($tck, $twr) = @_;
+ my $correction = 3; # 0.30% per the rounding algorithm
+ my $new_twr = $twr * (1000 - $correction);
+ $tck = ($new_twr / $tck) + 1000;
+
+ return $twr / int($tck / 1000);
+}
+
+# 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 combined time in ns
+sub ddr5_ns($$)
+{
+ my ($bytes, $index) = @_;
+
+ return (($bytes->[$index + 1] << 8) | $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_ns($bytes, 40);
+
+# speed
+ prints("Memory Characteristics");
+
+ $ctime = ddr5_ns($bytes, 20);
+ $ctime = ddr5_round_twr($ctime, $twr);
+ $ctime_max = ddr5_ns($bytes, 22);
+ $ctime_max = ddr5_round_twr($ctime_max, $twr);
+
+ my $ddrclk = 2 * (1000 / $ctime);
+ 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_ns($bytes, 30);
+ $trcd = ddr5_ns($bytes, 32);
+ $trp = ddr5_ns($bytes, 34);
+ $tras = ddr5_ns($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;
+
+
+ # Find min CAS latency at this speed
+ for ($ii = 39; $ii >= 0; $ii--) {
+ next unless ($cas_sup & (1 << $ii));
+ if (ceil(($taa * 997 / $ctime_at_speed + 1000) / 1000) <= $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),
+ ddr5_core_timings($best_cas, $ctime_at_speed,
+ $trcd, $trp, $tras));
+ }
+
+# more timing information
+ prints("Timing Parameters");
+
+ printl("Minimum Cycle Time (tCKmin)", tns3($ctime));
+ printl("Maximum Cycle Time (tCKmax)", tns3($ctime_max));
+ printl("Minimum CAS Latency Time (tAA)", tns3($taa));
+ printl("Minimum Active to Read/Write Delay (tRCD)", tns3($trcd));
+ printl("Minimum Row Precharge Delay (tRP)", tns3($trp));
+ printl("Minimum Active to Precharge Delay (tRAS)", tns3($tras));
+ printl("Minimum Active to Auto-Refresh Delay (tRC)", tns3(ddr5_ns($bytes, 38)));
+ printl("Minimum Write Recovery Time (tWR)", tns3($twr));
+ printl("Minimum Normal Recovery Delay (tRFC1)", tns3(ddr5_ns($bytes, 42) * 1000));
+ printl("Minimum Fine Recovery Delay (tRFC2)", tns3(ddr5_ns($bytes, 44) * 1000));
+ printl("Minimum Same Bank Recovery Delay (tRFCsb)", tns3(ddr5_ns($bytes, 46) * 1000));
+ 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? Not stated, but not present in my modules.
+ 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.1 V" :
+ ($bytes->[18] & 0x0c) == 0x00 ? "Unknown (1.1 V operable)" :
+ ($bytes->[18] & 0x03) == 0x00 ? "Unknown (1.1 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 +2452,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,
);
@@ -2845,6 +3123,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
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH i2c-tools v3 5/8] decode-dimms: Add basic decoding of type specific information for DDR5
2026-06-29 7:09 [PATCH i2c-tools v3 0/8] decode-dimms: Implement DDR5 decoding Stephen Horvath
` (2 preceding siblings ...)
2026-06-29 7:10 ` [PATCH i2c-tools v3 3/8] decode-dimms: Decode timings and other data for DDR5 Stephen Horvath
@ 2026-06-29 7:10 ` Stephen Horvath
2026-06-29 7:10 ` [PATCH i2c-tools v3 4/8] decode-dimms: Decode DDR5 common module information Stephen Horvath
` (4 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Stephen Horvath @ 2026-06-29 7:10 UTC (permalink / raw)
To: Wolfram Sang, Jean Delvare, linux-i2c@vger.kernel.org
Cc: Stephen Horvath, Guenter Roeck, Kamil Aronowski
Decode more manufacturer information, but only the info that is specific
to certain types of DDR5 memory modules, and not common to all DDR5
modules.
This is completely untested since my modules are SPD revision 1.0 for
module information and don't expose this. I also only have UDIMMs.
Signed-off-by: Stephen Horvath <s.horvath@outlook.com.au>
---
eeprom/decode-dimms | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 71 insertions(+)
diff --git a/eeprom/decode-dimms b/eeprom/decode-dimms
index 250ff84..dbaec97 100755
--- a/eeprom/decode-dimms
+++ b/eeprom/decode-dimms
@@ -2436,6 +2436,77 @@ sub decode_ddr5_sdram($)
(($bytes->[231] >> 4) & 15) + 1));
printl("Module Reference Card",
ddr3_reference_card($bytes->[232], $bytes->[232]));
+
+# type-specific settings
+ if ($spd_info_rev == 0x11 &&
+ ($type == 0x02 || # UDIMM
+ $type == 0x03 || # SODIMM
+ $type == 0x05 || # CUDIMM
+ $type == 0x06)) { # CSODIMM
+ prints("Unbuffered Memory Module");
+
+ printl("Clock driver manufacturer",
+ manufacturer_ddr3($bytes->[240], $bytes->[241]));
+ }
+
+ if ($spd_info_rev == 0x11 &&
+ ($type == 0x01 || # RDIMM
+ $type == 0x04)) { # LRDIMM
+ prints("Registered Memory Module");
+
+ printl("Clock driver manufacturer",
+ manufacturer_ddr3($bytes->[240], $bytes->[241]));
+ printl("Data buffer manufacturer",
+ manufacturer_ddr3($bytes->[244], $bytes->[245]));
+ }
+
+ if ($spd_info_rev == 0x11 &&
+ ($type == 0x07)) { # MRDIMM
+ prints("Multiplexed Rank Memory Module");
+
+ printl("Clock driver manufacturer",
+ manufacturer_ddr3($bytes->[240], $bytes->[241]));
+ printl("Data buffer manufacturer",
+ manufacturer_ddr3($bytes->[244], $bytes->[245]));
+ }
+
+ if ($spd_info_rev == 0x10 &&
+ ($type == 0x0A)) { # DDIMM
+ prints("Differential Memory Module");
+
+ printl("Memory buffer manufacturer",
+ manufacturer_ddr3($bytes->[240], $bytes->[241]));
+ }
+
+ if ($spd_info_rev == 0x01 &&
+ ($raw_type & 0xf0 == 0x90)) { # NVDIMM-N
+ prints("Non-Volatile Memory Module");
+
+ printl("Clock driver manufacturer",
+ manufacturer_ddr3($bytes->[240], $bytes->[241]));
+ printl("Data buffer manufacturer",
+ manufacturer_ddr3($bytes->[244], $bytes->[245]));
+ }
+
+ if ($spd_info_rev == 0x10 &&
+ ($raw_type & 0xf0 == 0xA0)) { # NVDIMM-P
+ prints("Non-Volatile Memory Module");
+
+ printl("Clock driver manufacturer",
+ manufacturer_ddr3($bytes->[240], $bytes->[241]));
+ printl("Data buffer manufacturer",
+ manufacturer_ddr3($bytes->[244], $bytes->[245]));
+ }
+
+ if ($spd_info_rev == 0x10 &&
+ ($type == 0x08)) { # CAMM2
+ prints("Compression Attached Memory Module");
+
+ printl("Clock driver 0 manufacturer",
+ manufacturer_ddr3($bytes->[240], $bytes->[241]));
+ printl("Clock driver 1 manufacturer",
+ manufacturer_ddr3($bytes->[244], $bytes->[245]));
+ }
}
# Parameter: EEPROM bytes 0-127 (using 4-5)
--
2.53.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH i2c-tools v3 4/8] decode-dimms: Decode DDR5 common module information
2026-06-29 7:09 [PATCH i2c-tools v3 0/8] decode-dimms: Implement DDR5 decoding Stephen Horvath
` (3 preceding siblings ...)
2026-06-29 7:10 ` [PATCH i2c-tools v3 5/8] decode-dimms: Add basic decoding of type specific information " Stephen Horvath
@ 2026-06-29 7:10 ` Stephen Horvath
2026-06-29 7:10 ` [PATCH i2c-tools v3 6/8] decode-dimms: Add second level separator Stephen Horvath
` (3 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Stephen Horvath @ 2026-06-29 7:10 UTC (permalink / raw)
To: Wolfram Sang, Jean Delvare, linux-i2c@vger.kernel.org
Cc: Stephen Horvath, Guenter Roeck, Kamil Aronowski
Decode the extra manufacturer information that was mentioned in a
previous patch, but only the info that is common between all DDR5 memory
modules.
The physical size is now implemented too.
Signed-off-by: Stephen Horvath <s.horvath@outlook.com.au>
---
eeprom/decode-dimms | 36 ++++++++++++++++++++++++++++++++++++
1 file changed, 36 insertions(+)
diff --git a/eeprom/decode-dimms b/eeprom/decode-dimms
index 4f9c4f0..250ff84 100755
--- a/eeprom/decode-dimms
+++ b/eeprom/decode-dimms
@@ -2400,6 +2400,42 @@ sub decode_ddr5_sdram($)
printl("Thermal Sensor",
$bytes->[14] & 0x08 ? "Supported" : "No");
+
+# common module information
+ prints("Common Information");
+
+ my $spd_info_rev = $bytes->[192];
+ printl("SPD Module Info Revision", ($spd_info_rev >> 4) . "." . ($spd_info_rev & 0xf));
+
+ printl_cond($bytes->[196] & 0x80,
+ "SPD Manufacturer",
+ manufacturer_ddr3($bytes->[194], $bytes->[195]));
+ printl_cond($bytes->[200] & 0x80,
+ "PMIC0 Manufacturer",
+ manufacturer_ddr3($bytes->[198], $bytes->[199]));
+ printl_cond($bytes->[204] & 0x80,
+ "PMIC1 Manufacturer",
+ manufacturer_ddr3($bytes->[202], $bytes->[203]));
+ printl_cond($bytes->[208] & 0x80,
+ "PMIC2 Manufacturer",
+ manufacturer_ddr3($bytes->[206], $bytes->[207]));
+ printl_cond($bytes->[212] & 0xC0,
+ "Thermal Sensors Manufacturer",
+ manufacturer_ddr3($bytes->[210], $bytes->[211]));
+
+ prints("Physical Characteristics");
+
+ my $height = $bytes->[230] & 0x1f;
+ printl("Module Height",
+ $height == 0x00 ? "15 mm or less" :
+ $height == 0x1f ? "more than 45 mm" :
+ sprintf("%u mm", $height + 15));
+ printl("Module Thickness",
+ sprintf("%d mm front, %d mm back",
+ ($bytes->[231] & 0x0f) + 1,
+ (($bytes->[231] >> 4) & 15) + 1));
+ printl("Module Reference Card",
+ ddr3_reference_card($bytes->[232], $bytes->[232]));
}
# Parameter: EEPROM bytes 0-127 (using 4-5)
--
2.53.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH i2c-tools v3 6/8] decode-dimms: Add second level separator
2026-06-29 7:09 [PATCH i2c-tools v3 0/8] decode-dimms: Implement DDR5 decoding Stephen Horvath
` (4 preceding siblings ...)
2026-06-29 7:10 ` [PATCH i2c-tools v3 4/8] decode-dimms: Decode DDR5 common module information Stephen Horvath
@ 2026-06-29 7:10 ` Stephen Horvath
2026-06-29 7:10 ` [PATCH i2c-tools v3 7/8] decode-dimms: Decode DDR5 error log Stephen Horvath
` (2 subsequent siblings)
8 siblings, 0 replies; 10+ messages in thread
From: Stephen Horvath @ 2026-06-29 7:10 UTC (permalink / raw)
To: Wolfram Sang, Jean Delvare, linux-i2c@vger.kernel.org
Cc: Stephen Horvath, Guenter Roeck, Kamil Aronowski
Adds `prints2` for a second level of separation useful for listing
subsections for XMP & EXPO profiles, or error log entries.
Signed-off-by: Stephen Horvath <s.horvath@outlook.com.au>
---
eeprom/decode-dimms | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/eeprom/decode-dimms b/eeprom/decode-dimms
index dbaec97..c72ceb1 100755
--- a/eeprom/decode-dimms
+++ b/eeprom/decode-dimms
@@ -712,6 +712,18 @@ sub real_prints($) # print separator w/ given text
}
}
+sub real_prints2($) # print second level separator w/ given text
+{
+ my ($label, $ncol) = @_;
+ $ncol = 1 unless $ncol;
+ if ($opt_html) {
+ $label = html_encode($label);
+ print "<tr><td style=\"font-weight: normal; text-align: center;\" colspan=\"".(1+$ncol)."\">$label</td></tr>\n";
+ } else {
+ print "\n--== $label ==--\n";
+ }
+}
+
sub printh($$) # print header w/ given text
{
my ($header, $sub) = @_;
@@ -759,6 +771,12 @@ sub prints($) # print separator w/ given text
push @{$dimm[$current]->{output}}, \@output;
}
+sub prints2($) # print second level separator w/ given text
+{
+ my @output = (\&real_prints2, @_);
+ push @{$dimm[$current]->{output}}, \@output;
+}
+
# Helper functions
sub tns1($) # print a time in ns, with 1 decimal digit
--
2.53.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH i2c-tools v3 7/8] decode-dimms: Decode DDR5 error log
2026-06-29 7:09 [PATCH i2c-tools v3 0/8] decode-dimms: Implement DDR5 decoding Stephen Horvath
` (5 preceding siblings ...)
2026-06-29 7:10 ` [PATCH i2c-tools v3 6/8] decode-dimms: Add second level separator Stephen Horvath
@ 2026-06-29 7:10 ` Stephen Horvath
2026-06-29 7:10 ` [PATCH i2c-tools v3 8/8] decode-dimms: Add DDR5 XMP 3.0 and EXPO decoding Stephen Horvath
2026-06-29 9:05 ` [PATCH i2c-tools v3 0/8] decode-dimms: Implement DDR5 decoding Wolfram Sang
8 siblings, 0 replies; 10+ messages in thread
From: Stephen Horvath @ 2026-06-29 7:10 UTC (permalink / raw)
To: Wolfram Sang, Jean Delvare, linux-i2c@vger.kernel.org
Cc: Stephen Horvath, Guenter Roeck, Kamil Aronowski
JESD400-5B specifies that an error log can be written to anywhere in the
end user programmable eeprom section, following a specific format. Code
to find and read this error log for DDR5 dimms has been added.
This is also completely untested on actual hardware implementations,
only tested by reading some manually constructed files.
Signed-off-by: Stephen Horvath <s.horvath@outlook.com.au>
---
eeprom/decode-dimms | 147 ++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 147 insertions(+)
diff --git a/eeprom/decode-dimms b/eeprom/decode-dimms
index c72ceb1..458a6b7 100755
--- a/eeprom/decode-dimms
+++ b/eeprom/decode-dimms
@@ -2724,6 +2724,150 @@ sub decode_ddr5_mfg_data($)
}
}
+# Parameter: EEPROM bytes 0-1023 (using 640-1023)
+sub decode_ddr5_error_data($)
+{
+ my $bytes = shift;
+
+ # Zero or more error logs may appear anywhere in any End User Programmable blocks of the SPD,
+ # including over SPD Block boundaries. They may be found by searching for a four byte anchor string.
+
+ my $errors = [];
+
+ my $size = scalar @{$bytes} < 1023 ? scalar @{$bytes} : 1023;
+
+ for (my $ii = 0; $ii < $size - 640 - 23; $ii++) {
+ if (join('', @{$bytes}[640 + $ii .. 640 + $ii + 3]) eq "95707695") {
+ push @{$errors}, [@{$bytes}[640 + $ii .. 640 + $ii + 23]];
+ $ii += 23;
+ }
+ }
+
+ if (@{$errors} == 0) {
+ # No error logs found
+ return;
+ }
+
+ prints("Error Log");
+
+ printl("Error Log Count", scalar @{$errors});
+
+ for (my $ii = 0; $ii < scalar @{$errors}; $ii++) {
+ my $error = @{$errors}[$ii];
+
+ prints2("Error $ii");
+
+ # error location
+ printl_cond($error->[4] & (1 << 0), "Error Type", "DRAM Uncorrectable Error");
+ printl_cond($error->[4] & (1 << 1), "Error Type", "DRAM Correctable Error");
+ printl_cond($error->[4] & (1 << 2), "Error Type", "DRAM ECS Error");
+ printl_cond($error->[4] & (1 << 3), "Error Type", "hPPR Was Required");
+ printl_cond($error->[4] & (1 << 4), "Error Type", "hPPR Resource Error");
+
+ printl("Error Location CPU", ($error->[5] >> 3) & 0x07);
+ printl("Error Location CPUMC", (($error->[5] & 3) << 2) | ($error->[6] >> 6));
+ printl("Error Location DIMM", ($error->[6] >> 4) & 0x01);
+
+ # these are active low
+ printl_cond(~$error->[6] & (1 << 3), "Error Location Rank", "0 (sub-channel A)");
+ printl_cond(~$error->[6] & (1 << 2), "Error Location Rank", "1 (sub-channel A)");
+ printl_cond(~$error->[6] & (1 << 1), "Error Location Rank", "0 (sub-channel B)");
+ printl_cond(~$error->[6] & (1 << 0), "Error Location Rank", "1 (sub-channel B)");
+
+ printl("Error Location Pseudo-channel", ($error->[7] >> 7) & 0x01);
+ printl("Error Location Parity", ($error->[7] >> 6) & 0x01);
+
+ if (($error->[7] >> 5) & 1) {
+ # chip identifier?
+ printl("Error Location Chip", ($error->[7] >> 2) & 0x07);
+ } else {
+ # row address?
+ printl("Error Location Bank Group",
+ (($error->[7] & 0x03) << 1) | (($error->[8] & 0x80) >> 7));
+ printl("Error Location Bank Address",
+ ($error->[8] >> 5) & 0x03);
+ printl("Error Location Row Address",
+ (($error->[8] & 0x1f) << 12) | ($error->[9] << 4) | ($error->[10] >> 4));
+ printl("Error Location Column Address",
+ (($error->[10] & 0x0f) << 7) | (($error->[11] & 0xf0) >> 1));
+ }
+
+ # also active low
+ printl_cond(~$error->[11] & (1 << 0), "Error Location Device", "DQS6A");
+ printl_cond(~$error->[11] & (1 << 1), "Error Location Device", "DQS7A");
+ printl_cond(~$error->[11] & (1 << 2), "Error Location Device", "DQS8A");
+ printl_cond(~$error->[11] & (1 << 3), "Error Location Device", "DQS9A");
+
+ printl_cond(~$error->[12] & (1 << 0), "Error Location Device", "DQS8B");
+ printl_cond(~$error->[12] & (1 << 1), "Error Location Device", "DQS9B");
+ printl_cond(~$error->[12] & (1 << 2), "Error Location Device", "DQS0A");
+ printl_cond(~$error->[12] & (1 << 3), "Error Location Device", "DQS1A");
+ printl_cond(~$error->[12] & (1 << 4), "Error Location Device", "DQS2A");
+ printl_cond(~$error->[12] & (1 << 5), "Error Location Device", "DQS3A");
+ printl_cond(~$error->[12] & (1 << 6), "Error Location Device", "DQS4A");
+ printl_cond(~$error->[12] & (1 << 7), "Error Location Device", "DQS5A");
+
+ printl_cond(~$error->[12] & (1 << 0), "Error Location Device", "DQS0B");
+ printl_cond(~$error->[12] & (1 << 1), "Error Location Device", "DQS1B");
+ printl_cond(~$error->[12] & (1 << 2), "Error Location Device", "DQS2B");
+ printl_cond(~$error->[12] & (1 << 3), "Error Location Device", "DQS3B");
+ printl_cond(~$error->[12] & (1 << 4), "Error Location Device", "DQS4B");
+ printl_cond(~$error->[12] & (1 << 5), "Error Location Device", "DQS5B");
+ printl_cond(~$error->[12] & (1 << 6), "Error Location Device", "DQS6B");
+ printl_cond(~$error->[12] & (1 << 7), "Error Location Device", "DQS7B");
+
+ # timestamp
+ my $year = ($error->[14] >> 2) + 2020;
+ my $month = (($error->[14] & 0x03) << 2) | ($error->[15] >> 6);
+ my $day = ($error->[15] & 0x3e) >> 1;
+ my $hour = (($error->[15] & 0x01) << 4) | ($error->[16] >> 4);
+ my $minute = (($error->[16] & 0x0f) << 2) | ($error->[17] >> 6);
+ my $second = $error->[17] & 0x3f;
+ printl("Timestamp", sprintf("%04d-%02d-%02d %02d:%02d:%02d",
+ $year, $month, $day, $hour, $minute, $second));
+
+ # DRAM refresh settings
+ if ($error->[18]) {
+ my $refresh_rate = ($error->[18] & 0x07);
+ my $wide_temp = ($error->[18] >> 5) & 0x01;
+ printl_cond($refresh_rate == 0 && $wide_temp == 1,
+ "DRAM Refresh Settings", "1x Refresh Rate, Temp <= 75°C");
+ printl_cond($refresh_rate == 1 && $wide_temp == 0,
+ "DRAM Refresh Settings", "1x Refresh Rate, Temp <= 80°C");
+ printl_cond($refresh_rate == 1 && $wide_temp == 1,
+ "DRAM Refresh Settings", "1x Refresh Rate, Temp 75°C - 80°C");
+ printl_cond($refresh_rate == 2,
+ "DRAM Refresh Settings", "1x Refresh Rate, Temp 80°C - 85°C");
+ printl_cond($refresh_rate == 3,
+ "DRAM Refresh Settings", "2x Refresh Rate, Temp 85°C - 90°C");
+ printl_cond($refresh_rate == 4,
+ "DRAM Refresh Settings", "2x Refresh Rate, Temp 90°C - 95°C");
+ printl_cond($refresh_rate == 5 && $wide_temp == 1,
+ "DRAM Refresh Settings", "2x Refresh Rate, Temp 95°C - 100°C");
+ printl_cond($refresh_rate == 5 && $wide_temp == 0,
+ "DRAM Refresh Settings", "2x Refresh Rate, Temp > 95°C");
+ printl_cond($refresh_rate == 6 && $wide_temp == 1,
+ "DRAM Refresh Settings", "2x Refresh Rate, Temp > 100°C");
+ }
+
+ # measured temperature
+ sub print_temp($$) {
+ my ($label, $temp) = @_;
+ if ($temp == 1) {
+ printl("$label Temperature", "<= 75°C");
+ } elsif ($temp == 0x1f) {
+ printl("$label Temperature", ">= 105°C");
+ } elsif ($temp != 0) {
+ printl("$label Temperature", sprintf("%d°C", $temp + 74));
+ }
+ }
+
+ print_temp("SPD", $error->[19] & 0x1f);
+ print_temp("TS0", $error->[19] >> 5 | ($error->[20] & 0x03) << 3);
+ print_temp("TS1", ($error->[20] >> 2) & 0x1f);
+ }
+}
+
# Parameter: EEPROM bytes 0-127 (using 64-98)
sub decode_manufacturing_information($)
{
@@ -3286,6 +3430,9 @@ for $current (0 .. $#dimm) {
# 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);
}
} else {
# Decode next 35 bytes (64-98, common to most
--
2.53.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH i2c-tools v3 8/8] decode-dimms: Add DDR5 XMP 3.0 and EXPO decoding
2026-06-29 7:09 [PATCH i2c-tools v3 0/8] decode-dimms: Implement DDR5 decoding Stephen Horvath
` (6 preceding siblings ...)
2026-06-29 7:10 ` [PATCH i2c-tools v3 7/8] decode-dimms: Decode DDR5 error log Stephen Horvath
@ 2026-06-29 7:10 ` Stephen Horvath
2026-06-29 9:05 ` [PATCH i2c-tools v3 0/8] decode-dimms: Implement DDR5 decoding Wolfram Sang
8 siblings, 0 replies; 10+ messages in thread
From: Stephen Horvath @ 2026-06-29 7:10 UTC (permalink / raw)
To: Wolfram Sang, Jean Delvare, linux-i2c@vger.kernel.org
Cc: Stephen Horvath, Guenter Roeck, Kamil Aronowski
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 <s.horvath@outlook.com.au>
---
eeprom/decode-dimms | 207 +++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 206 insertions(+), 1 deletion(-)
diff --git a/eeprom/decode-dimms b/eeprom/decode-dimms
index 458a6b7..37b39fb 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($)
{
@@ -2527,6 +2535,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_ns($bytes, $offset + 23);
+ my $ctime = ddr5_ns($bytes, $offset + 5);
+ $ctime = ddr5_round_twr($ctime, $twr);
+
+ my $ddrclk = 2 * (1000 / $ctime);
+ 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_ns($bytes, $offset + 13);
+ my $trcd = ddr5_ns($bytes, $offset + 15);
+ my $trp = ddr5_ns($bytes, $offset + 17);
+ my $tras = ddr5_ns($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));
+ printl("Minimum CAS Latency Time (tAA)", tns3($taa));
+ printl("Minimum Active to Read/Write Delay (tRCD)", tns3($trcd));
+ printl("Minimum Row Precharge Delay (tRP)", tns3($trp));
+ printl("Minimum Active to Precharge Delay (tRAS)", tns3($tras));
+ printl("Minimum Active to Auto-Refresh Delay (tRC)", tns3(ddr5_ns($bytes, $offset + 21)));
+ printl("Minimum Write Recovery Time (tWR)", tns3($twr));
+ printl("Minimum Normal Recovery Delay (tRFC1)", tns3(ddr5_ns($bytes, $offset + 25) * 1000));
+ printl("Minimum Fine Recovery Delay (tRFC2)", tns3(ddr5_ns($bytes, $offset + 27) * 1000));
+ printl("Minimum Same Bank Recovery Delay (tRFCsb)", tns3(ddr5_ns($bytes, $offset + 29) * 1000));
+ # 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_ns($bytes, $offset + 16);
+ my $ctime = ddr5_ns($bytes, $offset + 4);
+ $ctime = ddr5_round_twr($ctime, $twr);
+
+ my $ddrclk = 2 * (1000 / $ctime);
+ 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_ns($bytes, $offset + 6);
+ my $trcd = ddr5_ns($bytes, $offset + 8);
+ my $trp = ddr5_ns($bytes, $offset + 10);
+ my $tras = ddr5_ns($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));
+ printl("Minimum CAS Latency Time (tAA)", tns3($taa));
+ printl("Minimum Active to Read/Write Delay (tRCD)", tns3($trcd));
+ printl("Minimum Row Precharge Delay (tRP)", tns3($trp));
+ printl("Minimum Active to Precharge Delay (tRAS)", tns3($tras));
+ printl("Minimum Active to Auto-Refresh Delay (tRC)", tns3(ddr5_ns($bytes, $offset + 14)));
+ printl("Minimum Write Recovery Time (tWR)", tns3($twr));
+ printl("Minimum Normal Recovery Delay (tRFC1)", tns3(ddr5_ns($bytes, $offset + 18) * 1000));
+ printl("Minimum Fine Recovery Delay (tRFC2)", tns3(ddr5_ns($bytes, $offset + 20) * 1000));
+ printl("Minimum Same Bank Recovery Delay (tRFCsb)", tns3(ddr5_ns($bytes, $offset + 22) * 1000));
+ # 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($)
{
@@ -3122,6 +3312,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') {
@@ -3426,13 +3628,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
^ permalink raw reply related [flat|nested] 10+ messages in thread
* Re: [PATCH i2c-tools v3 0/8] decode-dimms: Implement DDR5 decoding
2026-06-29 7:09 [PATCH i2c-tools v3 0/8] decode-dimms: Implement DDR5 decoding Stephen Horvath
` (7 preceding siblings ...)
2026-06-29 7:10 ` [PATCH i2c-tools v3 8/8] decode-dimms: Add DDR5 XMP 3.0 and EXPO decoding Stephen Horvath
@ 2026-06-29 9:05 ` Wolfram Sang
8 siblings, 0 replies; 10+ messages in thread
From: Wolfram Sang @ 2026-06-29 9:05 UTC (permalink / raw)
To: Stephen Horvath
Cc: Jean Delvare, linux-i2c@vger.kernel.org, Guenter Roeck,
Kamil Aronowski
Hi Stephen,
> Hi, this series of patches adds DDR5 support to decode-dimms.
Awesome, thank you for doing that!
> I'm not too experienced with perl or the JEDEC specs, so there's
> probably going to be some questionable choices here, but I'd love to
> hear feedback.
I am not the one who can help here because this is so not my realm. But
if you can already point out some 'questionable choices', it will help
other reviewers to chose their starting point ;)
Thanks again,
Wolfram
^ permalink raw reply [flat|nested] 10+ messages in thread
end of thread, other threads:[~2026-06-29 9:05 UTC | newest]
Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-29 7:09 [PATCH i2c-tools v3 0/8] decode-dimms: Implement DDR5 decoding Stephen Horvath
2026-06-29 7:10 ` [PATCH i2c-tools v3 2/8] decode-dimms: Decode DDR5 Manufacturer Data Stephen Horvath
2026-06-29 7:10 ` [PATCH i2c-tools v3 1/8] decode-dimms: Implement DDR5 checksum parsing Stephen Horvath
2026-06-29 7:10 ` [PATCH i2c-tools v3 3/8] decode-dimms: Decode timings and other data for DDR5 Stephen Horvath
2026-06-29 7:10 ` [PATCH i2c-tools v3 5/8] decode-dimms: Add basic decoding of type specific information " Stephen Horvath
2026-06-29 7:10 ` [PATCH i2c-tools v3 4/8] decode-dimms: Decode DDR5 common module information Stephen Horvath
2026-06-29 7:10 ` [PATCH i2c-tools v3 6/8] decode-dimms: Add second level separator Stephen Horvath
2026-06-29 7:10 ` [PATCH i2c-tools v3 7/8] decode-dimms: Decode DDR5 error log Stephen Horvath
2026-06-29 7:10 ` [PATCH i2c-tools v3 8/8] decode-dimms: Add DDR5 XMP 3.0 and EXPO decoding Stephen Horvath
2026-06-29 9:05 ` [PATCH i2c-tools v3 0/8] decode-dimms: Implement DDR5 decoding Wolfram Sang
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.