[PATCH i2c-tools v4 7/8] decode-dimms: Decode DDR5 error log

Stephen Horvath <[email protected]> Thu, 23 Jul 2026 13:40:16 +0000
Newsgroups org.kernel.vger.linux-i2c
Message-ID <[email protected]>
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 <[email protected]>
---
 eeprom/decode-dimms | 147 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 147 insertions(+)

diff --git a/eeprom/decode-dimms b/eeprom/decode-dimms
index 7b8dbb8..9ddbacf 100755
--- a/eeprom/decode-dimms
+++ b/eeprom/decode-dimms
@@ -2726,6 +2726,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($)
 {
@@ -3287,6 +3431,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