AxlSpd — JEDEC SPD reader (DDR4/DDR5)
JEDEC Serial Presence Detect (SPD) reader for DDR4/DDR5 DIMMs.
Header: <axl/axl-spd.h>. Lazy on first call: AxlSpd opens an
@ref AxlSmbus session against whatever controller the platform
exposes (HC protocol, then I2C Master), probes the eight standard
SPD addresses (0x50..0x57), and hands off to the codec selected
by the memory-type byte at SPD offset 2.
DDR3 is intentionally out of scope for v1 — the consumer use
case (Dell delldiagslinux’s dump-memory parity on UEFI) is
DDR4/DDR5 server fleets. DDR3 is a small additional codec when
a consumer asks for it.
uint8_t *slot = NULL;
while ((slot = axl_spd_next(slot)) != NULL) {
AxlSpdInfo info;
if (axl_spd_read(*slot, &info) == 0 && info.ddr_generation != 0) {
axl_printf("Slot 0x%02X DDR%u %u MT/s %llu bytes ECC=%s\n",
*slot, info.ddr_generation, info.speed_mts,
(unsigned long long)info.capacity_bytes,
info.has_ecc ? "yes" : "no");
}
}
Cursor iteration follows the established pattern (matches
axl_smbios_find_next, axl_acpi_find_next, axl_pci_next).
The cursor is module-global — UEFI is single-threaded so two
overlapping walks would corrupt each other; treat the iterator
as exclusive.
Decoded info
typedef struct {
uint8_t ddr_generation; /* 4, 5; 0 = unknown */
uint16_t mfg_code_module; /* JEP-106 (bank<<8) | id; 0 if unset */
uint16_t mfg_code_dram;
uint8_t mfg_location;
uint16_t mfg_year; /* 2000 + BCD year byte */
uint8_t mfg_week; /* 1..53 */
uint32_t serial; /* 4 bytes BE */
char part_number[31]; /* trimmed ASCII */
uint64_t capacity_bytes;
uint16_t speed_mts; /* JEDEC speed grade in MT/s */
bool has_ecc;
bool registered;
} AxlSpdInfo;
Manufacturer fields are exposed as raw 16-bit JEP-106 codes — high byte is the continuation-bank index, low byte is the position within that bank as physically stored on the SPD (parity bit included). The library deliberately does not embed a vendor-name table — consumers do that lookup at the tool layer. The codes themselves are uncopyrightable factual data; the mapping to human names is policy that should be data-driven and mutable without rebuilding.
tools/memspd.c ships the JSON-sidecar pattern: at startup it
loads jedec.json5 from the binary’s directory (or --jedec-file <path>) and resolves mfg_code_* to vendor names. The stub
share/jedec.json5 carries 15 common server vendors; consumers
can extend or replace it freely.
Wire-protocol notes
DDR4 (key byte 0x0C) — EE1004 hub. Lower 256 bytes are accessed
directly. The upper 256 bytes (Module Manufacturing Information at
offsets 320..511) are reached after a Set Page Address (SPA) write
to the dedicated pseudo-slaves 0x36 (lower) / 0x37 (upper). AxlSpd
attempts SPA via axl_smbus_write_byte; standards-compliant EE1004
devices ignore the data byte. SPA failure is non-fatal — the codec
falls back to “lower page only” silently and the manufacturer fields
remain at zero. QEMU’s smbus-eeprom doesn’t model SPA, so wire-path
QEMU coverage is lower-page only.
DDR5 (key byte 0x12) — SPD5118 hub. The 1024-byte address space
is divided into eight 128-byte pages (0..7); each page is read at
hub-relative offsets 0x80..0xFF after writing the page index to
MR11 (register 0x0B). AxlSpd handles paging internally and
restores page 0 after every read — including mid-read failures
— so subsequent consumers see a predictable hub state.
Pure-decoder API
axl_spd_decode(buf, len, *out) runs the same codec on a captured
buffer with no SMBus involvement. Useful for offline analysis
(decoded fields out of a raw blob captured on real hardware) and
for cross-arch unit testing — the AxlPlatform suite’s DDR4/DDR5
decode tests use this entry point so coverage works the same on
x86 (where QEMU has SMBus) and AArch64 (where it doesn’t).
Pair with axl_spd_dump_raw(addr, *buf, cap, *len) to capture a
DIMM’s SPD bytes for off-box analysis.
tools/memspd.c
memspd list — one-line summary per populated slot
memspd show <slot> — decoded fields for one slot
memspd decode <slot> — raw hex dump + decoded fields
Common flag: --jedec-file <path> overrides the default sidecar
discovery (binary’s directory → ./jedec.json5). When no sidecar
loads, manufacturer fields print as raw hex codes — the
information still reaches the user, just unresolved.
Testing
Pure decoder coverage runs in AxlTestPlatform against canned
blobs synthesised by test/data/gen-spd.py — 21 tests, balanced
across x86 and AArch64.
Wire-path coverage lives in test/integration/test-spd-qemu.sh
(auxiliary, opt-out of the test-axl.sh ratchet). It depends on:
A locally-patched QEMU built from
scripts/qemu-patches/0001-smbus-eeprom-add-memdev-link.patch, which adds amemdev=<link<memory-backend>>property to thesmbus-eepromdevice. Stock QEMU 10.x rejects the argument.SmbusHcShim.efi, which publishesEFI_I2C_MASTER_PROTOCOLon top of QEMU’s ICH9 SMBus controller (OVMF doesn’t ship a SMBUS HC driver).
The test attaches the canned DDR4 blob to slot 0x50 and verifies
memspd.efi decodes it to “DDR4 / 8 GB / 2400 MT/s / ECC: yes”.
API Reference
JEDEC Serial Presence Detect (SPD) reader for DDR4/DDR5 DIMMs.
SPD EEPROMs sit on the platform SMBus at addresses 0x50–0x57 (one per DIMM slot) and carry the JEDEC-defined module-identification block written by the DIMM vendor at manufacture time. AxlSpd talks to them over AxlSmbus (so HC and I2C-Master transports both work), branches the codec on the memory-type byte at offset 2, and surfaces a decoded AxlSpdInfo struct.
Manufacturer fields are exposed as raw 16-bit JEP-106 codes (high byte = continuation-bank index, low byte = position in bank). The library deliberately does not embed a vendor name table — consumers do that lookup at the tool layer (see tools/memspd.c for the JSON-sidecar pattern). The spec calls these codes facts; lookup tables are JEDEC publications. Splitting the concern keeps the library tiny and the decode policy mutable without rebuilding.
DDR3 is intentionally out of scope for v1. DDR5 modules use the SPD5118 hub protocol: the lower 128 bytes of each 128-byte page are addressed directly; the upper window is paged via MR11 (register 0x0B). AxlSpd handles the page selection internally.
Iteration mirrors the established cursor pattern (see axl_smbios_find_next, axl_pci_next):
uint8_t *slot = NULL;
while ((slot = axl_spd_next(slot)) != NULL) {
AxlSpdInfo info;
if (axl_spd_read(*slot, &info) == 0) {
// ... decoded fields available in `info`
}
}
There is no axl_spd_init() entry point — first call to a public function lazily opens an AxlSmbus session.
Defines
-
AXL_SPD_ADDR_FIRST
First SMBus address scanned for an SPD EEPROM.
-
AXL_SPD_ADDR_LAST
Last SMBus address scanned for an SPD EEPROM (inclusive).
-
AXL_SPD_PART_NUMBER_MAX
Maximum part-number length across DDR3/4/5 (DDR5 is 30 ASCII chars).
-
AXL_SPD_RAW_MAX
Maximum raw SPD payload size (DDR5 = 1024 bytes; DDR3/4 = 256/512).
Functions
-
uint8_t *axl_spd_next(uint8_t *prev)
Walk populated SPD slots on the platform SMBus.
Probes 0x50..0x57 in order and returns each address that responds with a valid memory-type byte (DDR4=0x0C, DDR5=0x12). The returned pointer references an internal static cursor; pass NULL to restart, or the previous non-NULL return value to advance. The caller never owns the cursor’s storage.
Lazy: opens an AxlSmbus session on first call.
- Parameters:
prev – previous result, or NULL to start
- Returns:
pointer to the next populated SMBus address, or NULL when enumeration is complete (or no SMBus controller is available).
-
int axl_spd_read(uint8_t addr, AxlSpdInfo *out)
Read and decode the SPD at a specific SMBus address.
Issues the right sequence of SMBus byte reads for the device’s generation (auto-detected from the memory-type byte at offset 2), including SPD5118 page selection for DDR5 modules. On success
outis populated; on failureoutis left in an unspecified state.- Parameters:
addr – 7-bit SMBus address (0x50..0x57)
- Returns:
0 on success, -1 if the slot is empty / unsupported / I/O error.
-
int axl_spd_dump_raw(uint8_t addr, uint8_t *buf, size_t cap, size_t *len)
Read raw SPD bytes for offline analysis.
For DDR4 reads the lower 256 bytes (or 512 if
capallows and the device supports it). For DDR5 reads up to 1024 bytes across all eight 128-byte pages, switching pages via MR11 as needed.The buffer can later be fed to axl_spd_decode to obtain the same decoded view as axl_spd_read — useful for capturing SPDs on real hardware and decoding them off-box.
- Parameters:
addr – 7-bit SMBus address.
buf – output buffer.
cap – buffer capacity in bytes.
len – (out) bytes actually written.
- Returns:
0 on success, -1 on transport error or empty slot.
-
int axl_spd_decode(const uint8_t *buf, size_t len, AxlSpdInfo *out)
Decode an SPD buffer captured from a real DIMM.
Pure function — no SMBus, no allocations. Branches on
buf[2] (the memory-type byte) to select the DDR4 or DDR5 codec.- Parameters:
buf – raw SPD bytes (256+ for DDR4, 1024 for full DDR5).
len – bytes available in
buf.out – (out) decoded info; zero-initialised by this call.
- Returns:
0 on success, -1 if the memory type is unsupported or the buffer is too short for the detected generation.
-
struct AxlSpdInfo
- #include <axl-spd.h>
Decoded module identification block.
Populated by axl_spd_read or axl_spd_decode. Caller-owned (no allocations); zero-initialise before passing in.
mfg_code_moduleandmfg_code_dramare packed JEP-106 codes: high byte is the continuation-bank index (0-based), low byte is the position within that bank. Look up the human-readable name via a vendor table at the tool layer.Public Members
-
uint8_t ddr_generation
4 = DDR4, 5 = DDR5; 0 = unknown
-
uint16_t mfg_code_module
JEP-106 (bank<<8 | id) for module manufacturer; 0 if unset.
-
uint16_t mfg_code_dram
JEP-106 for DRAM die manufacturer; 0 if unset.
-
uint8_t mfg_location
Vendor-defined site code.
-
uint16_t mfg_year
Four-digit year (2000 + BCD year byte); 0 if unset.
-
uint8_t mfg_week
ISO week (1..53) decoded from BCD.
-
uint32_t serial
4 bytes, big-endian on the wire
-
char part_number[31]
ASCII, NUL-terminated, trimmed.
-
uint64_t capacity_bytes
Module capacity in bytes; 0 if not decodable.
-
uint16_t speed_mts
JEDEC speed grade (MT/s); 0 if not decodable.
-
bool has_ecc
True if the module exposes ECC bits.
-
bool registered
True for RDIMM / LRDIMM.
-
uint8_t ddr_generation