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 motivating
consumer use case (Linux-side dump-memory parity on UEFI) is
DDR4/DDR5 server fleets. DDR3 is a small additional codec when
a consumer asks for it.
Platform limitations — when AxlSpd doesn’t deliver
Direct SPD reads work only when the platform’s SMBus exposes the
DIMM EEPROMs to non-firmware software. On many server boards this
isn’t true and axl_spd_next returns NULL on first call. Always
have an SMBIOS Type 17 fallback ready (see tools/memspd.c for
the reference pattern).
Confirmed-affected platforms:
OEM-CPLD-routed DIMMs on some AMD server boards (FCH 1022:790b family). DDR5 SPDs are NOT on the FCH AUX SMBus on these platforms. They’re physically routed through an OEM-specific CPLD (“FPGA hub PLD”) behind a vendor UEFI protocol; BIOS reads them there and publishes the results via SMBIOS Type 17. The FCH AUX SMBus controller is electrically present but empty, returning the chipset- default “all-ACK + zero-data” pattern Linux warns about in
drivers/i2c/busses/i2c-piix4.clines 968-974. Linux’s spd5118 driver fails to bind for the same reason — verified on an AMD-EPYC server with kernel 6.19.10.Platforms where the SPD-carrying SMBus isn’t published as
EFI_SMBUS_HC_PROTOCOLorEFI_I2C_MASTER_PROTOCOL. AxlSpd’s auto-detect can’t find a transport. The AMD FCH PIIX4 direct-I/O fallback inaxl-smbus-piix4.ccovers boards where firmware is opinionated about which controller it advertises, but the chipset still has to deliver real bytes (see #1 above).
The cases where AxlSpd genuinely beats SMBIOS Type 17 are rare: live timing parameters (tCL, tRCD, tRP, tRAS) that aren’t in Type 17, and platforms where the BIOS publishes a stale or partial Type 17 (uncommon on modern firmware).
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). For human-readable rendering, the
axl_spd_ids_* API loads a curated JSON5 sidecar (jedec.json5)
into a process-global table and exposes lookup / format helpers
parallel to axl_pci_ids_load and axl_pci_format_name:
if (axl_spd_ids_load(NULL) == AXL_SIDECAR_OK) {
const char *name = axl_spd_vendor_name(info.mfg_code_module);
/* NULL-safe; consumers fall back to numeric IDs */
}
char buf[AXL_SPD_NAME_COMPOSED_MAX];
axl_spd_format_name(info.mfg_code_module, buf, sizeof(buf));
/* known → "Micron Technology"; unknown → "0xCCCC" */
The handle API (axl_spd_ids_open / _open_from_buffer /
_close / _vendor_name / _foreach_vendor / _format_name)
mirrors AxlPciIds for consumers that want layered databases
(public + private overlay). Schema 1 only — JEDEC has no
subsystem dimension that motivated AxlPciIds’s v1/v2 split.
tools/memspd.c is the reference consumer: at startup it calls
axl_spd_ids_load(--jedec-file or NULL) and renders manufacturer
fields via axl_spd_vendor_name and axl_spd_format_name.
share/jedec.json5 carries ~30 common server vendors; the
file is hand-curated (no auto-converter — JEDEC publishes
JEP-106 as PDF, not a canonical text database).
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
axl_spd_ids_load’s autodiscovery (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). For human-readable rendering, the axl_spd_ids_* API loads a curated JSON5 sidecar (jedec.json5) into a process- global table and exposes lookup / format helpers parallel to axl_pci_ids_load and axl_pci_format_name. Consumers that want a different DB (private OEM sheet, vendor-restricted list) layer their own handle on top via axl_spd_ids_open_from_buffer — same shape as AxlPciIds.
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.
Platform limitations
Direct SPD reads work only when the platform’s SMBus exposes the DIMM EEPROMs to non-firmware software. On many server boards this isn’t true and the API returns 0 populated slots. Known affected classes (verified empirically):
OEM-CPLD-routed DIMMs on some AMD server boards (FCH 1022:790b family) — DDR5 SPDs are NOT on the FCH AUX SMBus on these platforms. They’re physically routed through an OEM-specific CPLD (“FPGA hub PLD”) behind a vendor UEFI protocol; BIOS reads them there and publishes the results via SMBIOS Type 17. The FCH AUX SMBus is electrically present but empty, returning the chipset-default “all-ACK + zero-data” pattern Linux warns about in
drivers/i2c/busses/i2c-piix4.clines 968-974. Linux’s spd5118 driver fails to bind for the same reason — verified on an AMD-EPYC server with kernel 6.19.10.Platforms where the SMBus exposing DIMM SPDs isn’t published as or — AxlSpd’s auto-detect can’t find a transport. The PIIX4 direct-I/O fallback (when AMD FCH SMBus PCI device is present) helps on boards where firmware is opinionated about which controller it advertises, but the chipset still has to deliver real bytes (see #1).
DDR3 / pre-2014 modules — codec coverage is DDR4 + DDR5 only. SPD3 EEPROMs respond but
axl_spd_decodereturnsAxlSpdInfo{ ddr_generation = 0 }for them.
Recommended consumer pattern
Treat AxlSpd as opportunistic and always have an SMBIOS Type 17 fallback ready. Per-DIMM SMBIOS data — manufacturer, part number, capacity, configured speed, ECC bits, slot locator — is BIOS-populated on every system that ships SMBIOS, including all the platforms where AxlSpd silently finds nothing. See tools/memspd.c for the reference “try AxlSpd first, fall back to SMBIOS Type 17” pattern.
The cases where AxlSpd genuinely beats SMBIOS Type 17 are rare: live timing parameters (tCL, tRCD, tRP, tRAS) that aren’t exposed via Type 17, and platforms where the BIOS publishes a stale or partial Type 17 (uncommon on modern firmware).
Database iteration
Walk every vendor entry. Non-zero callback return stops iteration and propagates as the iter rc.
-
typedef int (*AxlSpdIdsVendorFn)(uint16_t code, const char *name, void *ctx)
-
int axl_spd_ids_foreach_vendor(const AxlSpdIds *ids, AxlSpdIdsVendorFn fn, void *ctx)
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).
-
AXL_SPD_VENDOR_NAME_MAX
Maximum bytes (including NUL) any vendor-name lookup can return. Sized for the longest real JEP-106 entry plus headroom.
-
AXL_SPD_NAME_COMPOSED_MAX
Maximum bytes (including NUL) for axl_spd_ids_format_name output. Vendor name plus the “<unknown>” → “0xCCCC” numeric fallback fit.
Typedefs
-
typedef struct AxlSpdIds AxlSpdIds
Opaque handle to a loaded JEDEC vendor-name database.
Created by axl_spd_ids_open or axl_spd_ids_open_from_buffer; destroyed by axl_spd_ids_close. Multiple handles can coexist — a consumer that ships an internal OEM sheet on top of the public set loads two handles and queries them in priority order, mirroring AxlPciIds.
The process-global API (axl_spd_ids_load and friends) wraps a single internal handle for the common case.
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)
out – receives the decoded SPD on success
- Returns:
AXL_OK on success, AXL_ERR 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:
AXL_OK on success, AXL_ERR 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:
AXL_OK on success, AXL_ERR if the memory type is unsupported or the buffer is too short for the detected generation.
-
AxlSidecarStatus axl_spd_ids_open(const char *path, AxlSpdIds **out)
Open a JEDEC vendor database from a JSON5 file.
- Returns:
AXL_SIDECAR_OKon success (handle returned viaout),AXL_SIDECAR_FILE_MISSINGifpathdoes not exist,AXL_SIDECAR_PARSE_ERRORon JSON5 / schema rejection.
-
AxlSidecarStatus axl_spd_ids_open_from_buffer(const char *json5, size_t len, AxlSpdIds **out)
Open a JEDEC vendor database from an in-memory JSON5 buffer.
- Returns:
AXL_SIDECAR_OKon success,AXL_SIDECAR_PARSE_ERRORon parse / schema error. (NoFILE_MISSING— the buffer is the input.)
-
const char *axl_spd_ids_vendor_name(const AxlSpdIds *ids, uint16_t code)
Vendor lookup against an explicit handle.
- Parameters:
ids – handle from axl_spd_ids_load
code – packed JEP-106 (bank<<8 | id)
- Returns:
database-owned string or NULL if unknown / handle empty.
-
int axl_spd_ids_format_name(const AxlSpdIds *ids, uint16_t code, char *buf, size_t buflen)
Compose a “vendor name or numeric fallback” display string.
Centralizes the rendering convention so every consumer prints the same string for the same JEP-106 code:
vendor known →
"<vendor>"vendor unknown →
"0xCCCC"(uppercase 4-digit hex)
- Returns:
number of bytes written excluding NUL (snprintf shape), or -1 on bad arguments.
-
AxlSidecarStatus axl_spd_ids_load(const char *override_path)
Load the curated JEDEC database into the process-global slot.
Two modes selected by
override_path:Explicit (
override_pathnon-NULL): use exactly that path.Autodiscover (
override_pathNULL): tryjedec.json5next to the running .efi, then in the current working directory.
Idempotent: a successful load is a no-op on subsequent calls. On the first successful load, registers an axl_atexit cleanup so the parsed table is freed at runtime cleanup automatically.
-
void axl_spd_ids_free(void)
Free the loaded database. Safe to call when none is loaded.
-
const char *axl_spd_vendor_name(uint16_t code)
Singleton-backed vendor lookup.
- Returns:
database-owned string or NULL if no database loaded or
codeis not present.
-
int axl_spd_format_name(uint16_t code, char *buf, size_t buflen)
Singleton-backed convenience wrapper for axl_spd_ids_format_name.
-
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