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 a memdev=<link<memory-backend>> property to the smbus-eeprom device. Stock QEMU 10.x rejects the argument.

  • SmbusHcShim.efi, which publishes EFI_I2C_MASTER_PROTOCOL on 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 out is populated; on failure out is 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 cap allows 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_module and mfg_code_dram are 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.