AxlPci — PCI/PCIe config-space access

PCI / PCIe configuration-space access via ECAM.

Header: <axl/axl-pci.h>. Lazy on first call: AxlPci consults AxlAcpi for the MCFG table to find each segment’s ECAM base address, then computes register offsets directly. Never falls back to legacy 0xCF8/0xCFC port pair; on platforms without MCFG (rare on modern hardware), every axl_pci_* call returns -1.

Cursor-style enumeration matches axl_smbios_find_next and axl_acpi_find_next:

AxlPciAddr *p = NULL;
while ((p = axl_pci_next(p)) != NULL) {
    uint16_t vid, did;
    axl_pci_read_config_16(*p, 0x00, &vid);
    axl_pci_read_config_16(*p, 0x02, &did);
    axl_printf("%04x:%02x:%02x.%u  %04x:%04x\n",
               p->seg, p->bus, p->dev, p->func, vid, did);
}

axl_pci_next skips empty slots (vendor ID 0xFFFF) and honours the multi-function header bit, so single-function devices’ phantom funcs 1–7 don’t appear in the walk.

Address tuple

typedef struct {
    uint16_t  seg;    ///< PCI segment group
    uint8_t   bus;
    uint8_t   dev;
    uint8_t   func;
} AxlPciAddr;

uint16_t seg matches the UEFI MCFG / PCIe spec’s segment-group width — multi-segment platforms (large servers, some ARM SoCs) are addressable directly through every axl_pci_* API. Single- segment systems leave it at 0.

axl_pci_addr_parse and axl_pci_addr_format round-trip an AxlPciAddr through the canonical lower-hex SSSS:BB:DD.F form (same shape as lspci). Parse accepts both 3-component (bus:dev.func, segment defaults to 0) and 4-component (seg:bus:dev.func) variants, with bounded-range checks at parse time:

AxlPciAddr a;
if (axl_pci_addr_parse(argv[1], &a) != 0) { /* malformed */ }

char buf[AXL_PCI_ADDR_STR_MAX];
axl_pci_addr_format(a, buf, sizeof(buf));
axl_printf("device %s\n", buf);

Common header reads

Boilerplate-killer wrappers around the standard config-space header offsets. They fold the “is this function absent?”, “unpack the 24-bit class triplet”, “split header type from multi-function bit”, and “Type-0-only fields” patterns into single calls:

uint16_t vid, did;
if (axl_pci_get_vid_did(addr, &vid, &did) == 0) {
    /* function is present (vid != 0xFFFF) and both fields read OK */
}

uint32_t class_code;  /* (base << 16) | (sub << 8) | prog_if */
axl_pci_get_class_code(addr, &class_code);

AxlPciHeaderType  hdr;
bool              is_multi_function;
axl_pci_get_header_type(addr, &hdr, &is_multi_function);
/* hdr is one of NORMAL (0x00) / BRIDGE (0x01) / CARDBUS (0x02);
   either out param may be NULL. */

uint16_t svid, sdid;
if (axl_pci_get_subsystem(addr, &svid, &sdid) == 0) {
    /* function is Type 0 (regular endpoint) and SVID/SDID read OK;
       Type 1 / Type 2 functions return -1 — those bytes are
       repurposed in PCI-PCI and CardBus bridges. */
}

axl_pci_get_vid_did returns -1 when the function is absent (vid reads as 0xFFFF), so callers don’t have to special-case the sentinel. axl_pci_get_header_type and axl_pci_get_subsystem share the same absent-function check internally — their -1 return covers “function not present” alongside the type-specific rejection cases. class_code matches the shape consumed by axl_pci_find_by_class.

axl_pci_class_string decodes the 24-bit class triplet into a human-readable form per the PCI Code and ID Assignment Spec:

uint32_t class_code;
char     cls[80];
axl_pci_get_class_code(addr, &class_code);
axl_pci_class_string(class_code, cls, sizeof(cls));
axl_printf("Class %06X (%s)\n", class_code, cls);
// Class 0C0330 (Serial bus controller / USB / xHCI)

Vendor/device-name lookup is opt-in via a curated JSON5 sidecar — see “Vendor/device name database” below. The full canonical pci.ids text database (~6 MB) is intentionally out of scope as a shipped artifact; consumers convert it to the JSON5 schema with scripts/pci-ids-to-json5.py if they need the long tail.

Config-space dump

axl_pci_dump reads up to 4096 bytes (the PCIe ECAM extent) of a function’s config space in 32-bit ECAM-natural chunks. Folds the endian-pack, absent-detection (VID == 0xFFFF), and ECAM cap into one call:

uint8_t  buf[256] = { 0 };
size_t   ok       = 0;
if (axl_pci_dump(addr, buf, sizeof(buf), &ok) == 0) {
    axl_hexdump(buf, ok, 0);    // dump only the bytes that read OK
}

Returns -1 on absent functions (no buffer mutation past zeroed unread bytes); *out_read reports how many bytes the caller can safely consume after a partial dump.

Lookups

AxlPciAddr nic;
if (axl_pci_find_by_vid_did(0x8086, 0x100E, 0, &nic) == 0) {
    /* Intel 82540EM e1000 */
}

AxlPciAddr usb_xhci;
/* class 0x0C 0x03 0x30 = serial bus, USB controller, xHCI prog-if */
axl_pci_find_by_class(0x0C0330, 0, &usb_xhci);

Capabilities

Two cursors — legacy (chain at 0x34) and PCIe extended (chain at 0x100):

uint16_t off = 0;
uint16_t id;
while (axl_pci_cap_next(addr, off, &off, &id) == 0) {
    axl_printf("  [%02x] %s\n", off, axl_pci_cap_id_str((uint8_t)id));
}

off = 0;
while (axl_pci_ext_cap_next(addr, off, &off, &id) == 0) {
    axl_printf("  [%03x] %s\n", off, axl_pci_ext_cap_id_str(id));
}

axl_pci_cap_id_str and axl_pci_ext_cap_id_str decode the standard capability IDs from the PCI Local Bus Spec (PM, MSI, MSI-X, PCIe, VPD, SATA, …) and PCIe Base Spec (AER, VC, SR-IOV, ATS, DPC, …) respectively. Unknown IDs return "<unknown>"; both lookups are always non-NULL.

Both walks are bounded against malformed/absent-device cap chains: axl_pci_cap_next does a vendor-ID precheck on the entry call (absent BDF → terminates immediately) and rejects back-pointers (next <= prev_off) at every step. axl_pci_ext_cap_next enforces the same forward-progress guard. Without these, ECAM all-1s reads on absent BDFs would feed a synthetic header at offset 0xFC whose next byte is 0xFF, looping forever — see commit 8b90954.

Bridges and topology

axl_pci_bridge_info reads the bus-number tuple (primary / secondary / subordinate) from a PCI-PCI bridge’s header — returns -1 cleanly if the function is a type-0 endpoint or type-2 CardBus rather than a type-1 bridge:

AxlPciBridge br;
if (axl_pci_bridge_info(rp, &br) == 0) {
    /* rp is a PCI-PCI bridge; rp's downstream side is bus br.secondary */
}

axl_pci_tree_for_each walks the topology in tree order (depth-first per segment), invoking a callback for every responding function with its depth and “is this a bridge” flag:

static int print_node(AxlPciAddr a, unsigned depth, bool is_bridge, void *ctx) {
    (void)ctx;
    for (unsigned i = 0; i < depth; i++) axl_print("  ");
    char buf[AXL_PCI_ADDR_STR_MAX];
    axl_pci_addr_format(a, buf, sizeof(buf));
    axl_printf("%s%s\n", buf, is_bridge ? " (bridge)" : "");
    return 0;
}

axl_pci_tree_for_each(print_node, NULL);

Bridges are visited immediately before their children, so a renderer can emit the box-drawing connector without lookahead. Cycle detection (per-segment visited-bus bitmap) plus a recursion-depth cap (AXL_PCI_TREE_MAX_DEPTH = 16) keep the walker safe against malformed firmware or hostile bridge configurations — same defense-in-depth posture as the cap-walk monotonic guard from commit 8b90954.

Vendor / device / subsystem name database

axl_pci_ids_load(override_path) loads share/pci-ids.json5 — a curated JSON5 sidecar that pairs vendors[] (read here) with classes[] (read by axl_pci_class_load) in one schema-2 file. When override_path is non-NULL it is used authoritatively (no fallback — explicit means explicit); when NULL the loader autodiscovers pci-ids.json5 companion to the running .efi or in cwd. axl-sdk ships a starter set covering QEMU emulated devices, common server NICs, NVMe, and GPUs — extend or replace as your fleet requires.

Both loaders read the same file. Each ignores the section it doesn’t care about (axl_pci_class_load skips vendors[], axl_pci_ids_load skips classes[]), so a tool that only needs class names doesn’t pay for the much larger vendor table.

if (axl_pci_ids_load(NULL) == 0) {
    const char *vendor = axl_pci_vendor_name(0x8086);
    const char *device = axl_pci_device_name(0x8086, 0x29C0);
    const char *card   = axl_pci_subsys_name(0x1028, 0x1FCA);
    /* all NULL-safe; consumers fall back to numeric IDs */
}

axl_pci_ids_load returns an AxlSidecarStatus (defined in <axl/axl-sidecar.h> and shared with AxlSpdIds, AxlUsbIds, and AxlPciClassDb):

  • AXL_SIDECAR_OK on success (idempotent on the second call)

  • AXL_SIDECAR_FILE_MISSING if no candidate file exists

  • AXL_SIDECAR_PARSE_ERROR if a candidate was found but failed to parse

The split lets tools log differently — “no database shipped” is a deployment problem (numeric fallback is fine), while “parse error” is an authoring problem worth being loud about. Numeric values match the legacy 0/-1/-2 ABI, so legacy callers using if (rc != 0) still compile and run; new code uses the named constants.

Two schema versions supported:

  • Schema 2 (default for new files) — hierarchical: devices nest under their parent vendor, subsystems nest under their parent device. Locality of related rows is the win when maintaining thousands of entries by hand. The loader also accepts a top-level subsystems[] block for orphan entries the maintainer doesn’t know which device to nest under.

  • Schema 1 (legacy) — flat: three independent top-level arrays (vendors[], devices[], subsystems[]), each entry self-contained. Cheap to parse and easy to generate; awkward to hand-maintain at scale.

Both populate the same internal hash tables — lookups are global on the respective key regardless of which form the file used. The loader pivots on the schema field; an unrecognized schema number returns AXL_SIDECAR_PARSE_ERROR rather than silently misparsing.

Subsystem entries identify the OEM card built around a piece of silicon; the (svid, sdid) pair lives at config-space offsets 0x2C / 0x2E on header-type-0 functions. For the long tail, scripts/pci-ids-to-json5.py converts canonical pci.ids text to this schema (default schema 2; --schema 1 opts into the flat layout if you need it; --vendors-only filters to a curated subset).

Composed-name helper

axl_pci_format_name(vid, did, buf, buflen) centralizes the “vendor + device + numeric tail” rendering convention so every consumer prints the same string for the same (vid, did) pair:

char buf[AXL_PCI_NAME_COMPOSED_MAX];
axl_pci_format_name(0x8086, 0x29C0, buf, sizeof(buf));
// → "Intel Corporation Q35 Host Bridge"

Vendor-known + device-unknown produces "<vendor> Device <DID hex>"; vendor-unknown short-circuits to "<VID>:<DID>" regardless of whether the device entry happens to exist.

Layered databases (handle API)

For consumers that want a “public + private” overlay (private DB shadows public on (svid, sdid) collisions), the handle API lets you load and query multiple databases:

AxlPciIds *pub  = NULL;
AxlPciIds *priv = NULL;
axl_pci_ids_open("pci-ids.json5",         &pub);
axl_pci_ids_open("private-pci-ids.json5", &priv);

const char *s = axl_pci_ids_subsys_name(priv, svid, sdid);
if (s == NULL) s = axl_pci_ids_subsys_name(pub, svid, sdid);

axl_pci_ids_close(priv);
axl_pci_ids_close(pub);

axl_pci_ids_format_name(handle, vid, did, buf, buflen) is the handle-aware equivalent of axl_pci_format_name.

For “show me everything in this overlay” (debug dumps, validators, text exports), use the iterator API — axl_pci_ids_foreach_vendor / _device / _subsys walks the loaded entries and propagates a non-zero callback return as an early stop.

Per-name length contracts

AXL_PCI_VENDOR_NAME_MAX     = 128 bytes
AXL_PCI_DEVICE_NAME_MAX     = 192 bytes
AXL_PCI_SUBSYS_NAME_MAX     = 192 bytes
AXL_PCI_CLASS_NAME_MAX      = 128 bytes
AXL_PCI_NAME_COMPOSED_MAX   = 384 bytes

Sized to comfortably hold real pci.ids entries; loader silently truncates over-cap names. Pin char buf[AXL_PCI_NAME_COMPOSED_MAX] on the stack and never have to worry about formatter overflow.

Class-name overlay (optional)

For decoding the class triplet itself, the compiled-in tables in src/pci/axl-pci.c are the bootstrap default. The classes[] section of pci-ids.json5 overlays per-tier names — consulted before the compiled-in table — so new triplets (CXL Memory Expanders, future PCIe class assignments, …) can ship via a git pull of the sidecar instead of rebuilding every consumer:

axl_pci_class_load(NULL);  /* opt-in opportunistic load */
char buf[AXL_PCI_CLASS_NAME_MAX];
axl_pci_class_string_fmt(0x060000, AXL_PCI_CLASS_FMT_FULL,
                         buf, sizeof(buf));
/* overlay consulted first per-tier; compiled-in falls through */

Same -1/-2 distinction and override-authoritative semantics as axl_pci_ids_load. Both schemas parse:

  • Schema 2 (current) — hierarchical: subclasses nest under bases, programming interfaces nest under subclasses. Locality matches the vendors[] convention.

  • Schema 1 (legacy) — flat: each entry pins any subset of (base, sub, prog). Useful for hand-edited overrides where the hierarchy adds friction over a one-line addition.

Both populate the same internal hash tables; lookups are global on the composite key regardless of file shape.

AxlPciClassFmt selects the output shape:

  • FMT_FULL"Bridge / Host bridge / <prog>" (default)

  • FMT_SUBCLASS"Host bridge" (Linux lspci shape; collapses to base when sub unknown, then numeric)

  • FMT_BASE"Bridge" (collapses to numeric when unknown)

VPD

Vital Product Data (PCI 3.0 §6.4) — keyword-tagged inventory data exposed by some NICs and storage controllers. AxlPci handles the F-bit handshake on the address register, the dword-aligned data window, and the Read-Only / Read-Write tag walk:

uint8_t buf[64];
size_t  len = 0;
if (axl_pci_vpd_read(nic, "PN", buf, sizeof(buf), &len) == 0) {
    /* `len` is the actual on-device length; buf was filled with
       up to sizeof(buf) bytes of it. */
    axl_printf("Part number: %.*s\n", (int)len, buf);
}

For “show me everything that’s there” — vendor-specific V0..V9 / Y0..Y9 keywords included — use axl_pci_vpd_iter and dispatch through a callback:

static int dump_cb(const char keyword[2], const uint8_t *data,
                   size_t len, void *ctx) {
    (void)ctx;
    axl_printf("  %c%c (%zu): %.*s\n",
               keyword[0], keyword[1], len, (int)len, data);
    return 0;  /* return non-zero to stop the walk early */
}

axl_pci_vpd_iter(nic, dump_cb, NULL);

axl_pci_vpd_iter and axl_pci_vpd_read share the same VPD walker — one cap-list lookup, one tag walk — so calling either reflects the same on-device state. The callback’s data pointer is only valid for the duration of the call; copy any bytes you want to retain.

API Reference

PCI/PCIe configuration-space access via ECAM.

Configuration space is reached through the MCFG-described Enhanced Configuration Access Mechanism (ECAM) on both x86 and AArch64 — never via the legacy 0xCF8/0xCFC port pair. The MCFG discovery is lazy on first access; if the firmware did not publish an MCFG table, all axl_pci_* calls fail with -1 rather than silently falling back to legacy ports.

Cursor-style iteration mirrors axl_smbios_find_next and axl_acpi_find_next:

AxlPciAddr *p = NULL;
while ((p = axl_pci_next(p)) != NULL) {
    uint16_t vid;
    axl_pci_read_config_16(*p, 0x00, &vid);
    // ...
}

Per-name length contracts

Maximum bytes (including NUL) any database lookup can return.

Documented caps so consumers can stack-allocate buffers at compile time. The loader truncates over-cap entries on the way in (silent — axl_json_get_string does the truncation), so lookup return values are always within bounds. Forward-looking: the curated share/pci-ids.json5 is well under these numbers today.

AXL_PCI_VENDOR_NAME_MAX

vendor entry max bytes

AXL_PCI_DEVICE_NAME_MAX

device entry max bytes

AXL_PCI_SUBSYS_NAME_MAX

subsystem entry max bytes

AXL_PCI_CLASS_NAME_MAX

class entry max bytes

AXL_PCI_NAME_COMPOSED_MAX

axl_pci_format_name output max

Database iteration callbacks

Non-zero return stops the walk; the value propagates.

typedef int (*AxlPciIdsVendorFn)(uint16_t vid, const char *name, void *ctx)
typedef int (*AxlPciIdsDeviceFn)(uint16_t vid, uint16_t did, const char *name, void *ctx)
typedef int (*AxlPciIdsSubsysFn)(uint16_t svid, uint16_t sdid, const char *name, void *ctx)

Defines

AXL_PCI_ADDR_STR_MAX

Buffer size that fits the canonical “SSSS:BB:DD.F” form plus NUL.

AXL_PCI_CONFIG_SPACE_MAX

Capacity of a function’s full PCIe ECAM config space.

AXL_PCI_TREE_MAX_DEPTH

depth backstop for tree walks

Typedefs

typedef int (*AxlPciTreeFn)(AxlPciAddr addr, unsigned depth, bool is_bridge, void *ctx)

Per-node callback for axl_pci_tree_for_each.

Param addr:

function being visited

Param depth:

0 for root-bus devices, 1 for first-level bridge children, etc.

Param is_bridge:

true if the function is a PCI-PCI bridge whose secondary bus is about to be descended into

Param ctx:

caller’s opaque context

Return:

non-zero to stop the walk early; the value becomes the return of axl_pci_tree_for_each. Return 0 to continue.

typedef struct AxlPciIds AxlPciIds

Opaque handle to a loaded vendor/device/subsystem database.

Created by axl_pci_ids_open or axl_pci_ids_open_from_buffer, destroyed by axl_pci_ids_close. Multiple handles can coexist — a consumer that wants a “public + private” overlay loads two handles and queries them in priority order, so internal/OEM names shadow the public set on collisions.

The process-global API (axl_pci_ids_load and friends) wraps a single internal handle for the common case.

typedef struct AxlPciClassDb AxlPciClassDb

Opaque handle to a loaded PCI class-code name overlay.

Parallel to AxlPciIds but for class triplet decoding. The compiled-in tables in axl-pci.c stay as the bootstrap default; a loaded overlay is consulted first per-tier (base, sub, prog), with the compiled-in table as the fallback. New class triplets (CXL Memory Expanders, future PCIe class assignments, …) can land via a git pull of the JSON5 sidecar without rebuilding every consumer.

The overlay lives in the classes[] section of share/pci-ids.json5

. Schema 2 nests subclasses under bases and progs under subclasses; schema 1 (legacy flat) lets each entry pin any subset of (base, sub, prog) — base only for “all subclasses

of this base”, base+sub for a subclass, base+sub+prog for a specific prog_if.

Enums

enum AxlPciHeaderType

PCI configuration-space header type, decoded from the low 7 bits of offset 0x0E. The high bit (0x80) is the multi-function flag and is exposed separately by axl_pci_get_header_type.

Values:

enumerator AXL_PCI_HEADER_TYPE_NORMAL

Type 0: regular function (BARs, SVID/SDID, etc.)

enumerator AXL_PCI_HEADER_TYPE_BRIDGE

Type 1: PCI-PCI bridge.

enumerator AXL_PCI_HEADER_TYPE_CARDBUS

Type 2: CardBus bridge.

enum AxlPciClassFmt

Output shape selector for axl_pci_class_string_fmt.

Different consumers want different verbosity from the same class code. Verbose tools want the full slash-joined triplet; row-oriented tools where the class column blows out the right margin want the subclass alone (matches Linux lspci’s output shape); coarse categorization wants just the base.

Values:

enumerator AXL_PCI_CLASS_FMT_FULL

“Bridge / Host bridge” (axl_pci_class_string default)

enumerator AXL_PCI_CLASS_FMT_SUBCLASS

“Host bridge” (subclass tier alone)

enumerator AXL_PCI_CLASS_FMT_BASE

“Bridge” (base tier alone)

Functions

int axl_pci_addr_parse(const char *s, AxlPciAddr *out)

Parse a textual PCI address into an AxlPciAddr.

Accepts two hex-only formats:

  • bus:dev.func — segment defaults to 0

  • seg:bus:dev.func — explicit segment

Components are bounded at parse time (bus 0..0xFF, dev 0..0x1F, func 0..0x07, seg 0..0xFFFF); out-of-range or malformed input returns -1 with out left unmodified.

Parameters:
  • s – input string (NUL-terminated, hex digits + : + .)

  • out – [out] parsed address (untouched on error)

Returns:

AXL_OK on success, AXL_ERR on malformed input.

int axl_pci_addr_format(AxlPciAddr addr, char *buf, size_t buflen)

Write an AxlPciAddr in canonical SSSS:BB:DD.F form.

Always emits the 4-digit segment — round-trips with axl_pci_addr_parse. NUL-terminates buf when buflen >= 13.

Parameters:
  • addr – address to format

  • buf – destination buffer

  • buflen – capacity of buf

Returns:

number of bytes written excluding NUL, or -1 if buflen is too small (need >= AXL_PCI_ADDR_STR_MAX).

int axl_pci_read_config_8(AxlPciAddr addr, uint16_t reg, uint8_t *out)

Read a byte from PCI configuration space.

Parameters:
  • addr – target function

  • reg – register offset (0..4095 for ECAM)

  • out – [out] receives the value

Returns:

AXL_OK on success, AXL_ERR if the address is outside any MCFG segment or MCFG isn’t available.

int axl_pci_read_config_16(AxlPciAddr addr, uint16_t reg, uint16_t *out)

16-bit variant of axl_pci_read_config_8. Register should be 16-bit aligned.

int axl_pci_read_config_32(AxlPciAddr addr, uint16_t reg, uint32_t *out)

32-bit variant of axl_pci_read_config_8. Register should be 32-bit aligned.

int axl_pci_write_config_8(AxlPciAddr addr, uint16_t reg, uint8_t value)

Write counterpart to axl_pci_read_config_8.

int axl_pci_write_config_16(AxlPciAddr addr, uint16_t reg, uint16_t value)

Write counterpart to axl_pci_read_config_16.

int axl_pci_write_config_32(AxlPciAddr addr, uint16_t reg, uint32_t value)

Write counterpart to axl_pci_read_config_32.

int axl_pci_dump(AxlPciAddr addr, uint8_t *buf, size_t bytes, size_t *out_read)

Read up to bytes of PCI configuration space into buf.

Walks the function’s config space in 32-bit ECAM-natural chunks (little-endian on the wire, packed into buf at offsets 0..bytes). bytes is rounded down to a multiple of 4 and capped at AXL_PCI_CONFIG_SPACE_MAX. The function is treated as absent if zero successful reads occur (vendor ID at offset 0x00 reads as 0xFFFFFFFF) — returns -1 with *out_read = 0. On a partial dump (some reads succeeded, some failed), *out_read tracks how many bytes the caller can safely consume; the remaining buffer bytes are zeroed.

Replaces the per-tool hand-rolled for (reg = 0; reg + 4 <= bytes; reg += 4) read32; pack into buf loop.

Parameters:
  • addr – target function

  • buf – destination buffer

  • bytes – capacity (rounded down to 4, capped at AXL_PCI_CONFIG_SPACE_MAX)

  • out_read – [out, optional] bytes successfully populated

Returns:

AXL_OK on success (one or more successful reads), AXL_ERR if the function is absent or buf is NULL or MCFG isn’t available.

int axl_pci_get_vid_did(AxlPciAddr addr, uint16_t *vid, uint16_t *did)

Read vendor ID and device ID from a function’s standard header.

Reads offsets 0x00 and 0x02. The “function absent” sentinel (vendor ID == 0xFFFF) is folded into the return code so callers don’t have to special-case it.

Parameters:
  • addr – target function

  • vid – [out] vendor ID

  • did – [out] device ID

Returns:

AXL_OK on success (both fields populated), AXL_ERR if the function is absent or any bus error is encountered.

int axl_pci_get_class_code(AxlPciAddr addr, uint32_t *class_code)

Read the 24-bit class code from a function’s standard header.

Folds the three bytes at offsets 0x09 (programming interface), 0x0A (subclass), and 0x0B (base class) into the canonical (base << 16) | (sub << 8) | prog_if form — same shape consumed by axl_pci_find_by_class.

Parameters:
  • addr – target function

  • class_code – [out] 24-bit class code

Returns:

AXL_OK on success, AXL_ERR on bus error.

int axl_pci_get_header_type(AxlPciAddr addr, AxlPciHeaderType *type, bool *is_multi_function)

Read the configuration-space header type and multi-function bit.

Splits the byte at offset 0x0E into the type enum (low 7 bits) and the multi-function flag (bit 7). Eliminates the manual & 0x7F masking and & 0x80 bit test that every consumer rolling its own type detection writes. Either out parameter may be NULL.

If the firmware reports a header-type byte the spec doesn’t define (anything outside 0x00..0x02 in the low 7 bits) the call still returns 0 and type is set to the raw value cast through the enum — callers can compare against the named constants and treat unknown values as opaque. Bus error returns -1.

Parameters:
  • addr – target function

  • type – [out] header type (NULL allowed)

  • is_multi_function – [out] bit 7 of offset 0x0E (NULL allowed)

Returns:

AXL_OK on success, AXL_ERR on bus error.

int axl_pci_get_subsystem(AxlPciAddr addr, uint16_t *svid, uint16_t *sdid)

Read a Type 0 function’s Subsystem Vendor ID and Subsystem ID.

Only Type 0 functions (regular endpoints) carry SVID/SDID at config offsets 0x2C / 0x2E; Type 1 (PCI-PCI bridge) and Type 2 (CardBus) use those bytes for other purposes. The header-type check is baked in: a non-zero header type returns -1 with svid / sdid untouched.

Parameters:
  • addr – target function (must be header-type 0)

  • svid – [out] subsystem vendor ID

  • sdid – [out] subsystem device ID

Returns:

AXL_OK on success (both fields populated), AXL_ERR if the function is absent, has a non-Type-0 header, or any bus error is encountered.

int axl_pci_class_string(uint32_t class_code, char *buf, size_t buflen)

Format a 24-bit PCI class code as a human-readable string.

Decodes per the PCI Code and ID Assignment Specification — up to three tiers: base class ((class_code >> 16) & 0xFF), subclass ((class_code >> 8) & 0xFF), and programming interface (class_code & 0xFF). Tiers with no spec-defined name are omitted rather than rendered as <unknown> placeholders. This mirrors Linux lspci’s posture (no placeholder noise) but not its output shape — lspci collapses the triplet to a single subclass string (“Host bridge”), while AXL keeps the slash-joined triplet so the base class stays visible. Output shapes:

  • All known: "<base> / <sub> / <prog>" (e.g. "Display controller / VGA-compatible / standard")

  • Known base+sub, unknown prog: "<base> / <sub>" (e.g. "Bridge / Host bridge")

  • Known base, unknown sub: "<base>" (e.g. "Bridge")

  • Wholly unknown class: "Class XXXXXX" (numeric hex), in the spirit of lspci’s numeric fallback for unidentified classes.

Always NUL-terminates buf (snprintf-shape).

Vendor/device-name lookup (the pci.ids database) is intentionally out of scope — too large for AXL, and consumers grep their own.

Parameters:
  • class_code – 24-bit class code

  • buf – destination buffer

  • buflen – capacity of buf

Returns:

number of bytes written excluding NUL, or -1 if buf is NULL or buflen is 0.

int axl_pci_class_string_fmt(uint32_t class_code, AxlPciClassFmt fmt, char *buf, size_t buflen)

Format a class code with a chosen output shape.

Behavior in each mode follows the same “omit unknown tiers,

fall back to numeric `Class XXXXXX` when wholly unknown” posture as axl_pci_class_string:

  • FMT_FULL is identical to axl_pci_class_string.

  • FMT_SUBCLASS emits just the subclass name. If the subclass isn’t in the table, falls back to the base name; if the base is also unknown, falls back to numeric.

  • FMT_BASE emits just the base name. If unknown, numeric fallback.

Returns:

number of bytes written excluding NUL, or -1 on bad args or unknown fmt.

AxlPciAddr *axl_pci_next(AxlPciAddr *prev)

Iterate every responding PCI function across all MCFG segments.

Returns a pointer to a static internal cursor; the storage is reused across calls and is invalidated by the next call. Pass NULL to start the walk fresh, or the previous non-NULL return value to advance — passing any other pointer (including a caller-allocated AxlPciAddr) restarts iteration silently. The caller never owns the cursor’s storage.

Empty slots are skipped: both vendor ID 0xFFFF (the bus “no

device” sentinel) and 0x0000 (a reserved vendor ID — some chipsets return all-zero config reads for disconnected slots, producing “phantom” 0000:0000 devices). Single-function devices are detected via the header-type byte and their functions 1–7 are skipped.

Use axl_pci_next_unfiltered if you need to see 0x0000 slots.

Parameters:
  • prev – previous result, or NULL to start

Returns:

pointer to the next populated function, or NULL when enumeration is complete (or MCFG is unavailable).

AxlPciAddr *axl_pci_next_unfiltered(AxlPciAddr *prev)

Like axl_pci_next, but does NOT skip 0x0000 phantom slots (only 0xFFFF absent slots are skipped).

Opt-in for the rare consumer that must enumerate raw config space including slots a quirky chipset reports as 0000:0000. Most callers want axl_pci_next, which filters phantoms by default. Shares the same static cursor as axl_pci_next — do not interleave the two within a single walk.

Parameters:
  • prev – previous result, or NULL to start

Returns:

pointer to the next responding function (vendor ID != 0xFFFF), or NULL when enumeration is complete.

int axl_pci_find_by_vid_did(uint16_t vid, uint16_t did, uint16_t nth, AxlPciAddr *out)

Find the nth function with matching vendor+device IDs.

Parameters:
  • vid – vendor ID

  • did – device ID

  • nth – 0-based match index

  • out – [out] address of the matching function

Returns:

AXL_OK on success, AXL_ERR if no nth match exists.

int axl_pci_find_by_class(uint32_t class_code, uint16_t nth, AxlPciAddr *out)

Find the nth function with a matching class triplet.

The 24-bit class is (base_class << 16) | (subclass << 8) | prog_if, matching how lspci -vvv prints it. Pass 0xFFFFFF to match any.

Parameters:
  • class_code – 24-bit class code

  • nth – 0-based match index

  • out – [out] address of the matching function

Returns:

AXL_OK on success, AXL_ERR if no nth match exists.

int axl_pci_bridge_info(AxlPciAddr addr, AxlPciBridge *out)

Read the bridge bus tuple, if addr is a PCI-PCI bridge.

Reads the header-type byte first; on a non-bridge function (type 0 endpoint or type 2 CardBus), returns -1 without touching out. Successful return guarantees addr is header type 1 and the three bus-number bytes are populated.

Parameters:
  • addr – target function

  • out – [out] primary/secondary/subordinate

Returns:

AXL_OK on success, AXL_ERR if addr is not a PCI-PCI bridge or any bus error is encountered.

int axl_pci_tree_for_each(AxlPciTreeFn fn, void *ctx)

Walk the PCI topology in tree order, depth-first per segment.

Builds an in-memory model of the topology by enumerating every responding function once via axl_pci_next, then identifying root buses per segment (any bus that’s not the secondary bus of some bridge) and recursing through bridges. Functions on the same bus are visited in (dev, func) order; bridge children are visited immediately after their bridge.

Multi-segment platforms are walked one segment at a time; segments are visited in MCFG-table order.

Defensive against malformed or hostile topologies: per-segment visited-bus bitmaps detect cycles, and a recursion-depth cap (AXL_PCI_TREE_MAX_DEPTH) backstops pathological chains. Same posture as AXL_DP_MAX_NODES for device-path iteration and the cap-walk self-loop / offset-range guards (which allow descending cap chains but still terminate on malformed ones).

Not reentrant against axl_pci_next — the walker drives axl_pci_next internally and they share a static cursor. The callback must not call axl_pci_next (the tree walk itself uses axl_pci_* config-space reads, which are fine).

Parameters:
  • fn – per-node callback (must not be NULL)

  • ctx – opaque context forwarded to fn

Returns:

0 on a clean walk, the callback’s first non-zero return if it stopped early, or -1 if MCFG is unavailable / any internal allocation fails.

int axl_pci_cap_next(AxlPciAddr addr, uint16_t prev_off, uint16_t *out_off, uint16_t *out_id)

Iterate the standard PCI capability list.

Pass 0 in prev_off to start (the function’s capability list pointer at 0x34 is consulted automatically). On the first call the function returns the first capability; on subsequent calls pass out_off from the previous return value to advance.

Parameters:
  • addr – target function

  • prev_off – previous offset, or 0 to start

  • out_off – [out] offset of the next capability

  • out_id – [out] capability ID

Returns:

AXL_OK on success (capability found), AXL_ERR when no more capabilities exist or the function has no capabilities.

int axl_pci_ext_cap_next(AxlPciAddr addr, uint16_t prev_off, uint16_t *out_off, uint16_t *out_id)

Iterate the PCIe extended capability list (offsets 0x100..).

Same conventions as axl_pci_cap_next, but operates on the PCIe extended capability chain. Returns -1 if the device is not PCIe (no extended caps).

Parameters:
  • addr – target function

  • prev_off – previous offset, or 0 to start

  • out_off – [out] offset of the next capability

  • out_id – [out] extended capability ID

Returns:

AXL_OK on success, AXL_ERR when the chain ends or no extended caps are present.

const char *axl_pci_cap_id_str(uint8_t cap_id)

Look up a human-readable name for a legacy PCI capability ID.

Covers the standard IDs from the PCI Local Bus Specification (PM, AGP, VPD, Slot ID, MSI, CompactPCI HotSwap, PCI-X, HyperTransport, Vendor-Specific, Debug, CompactPCI Resource, PCI HotPlug, Bridge Subsystem ID, AGP 8x, Secure, PCI Express, MSI-X, SATA, Advanced Features, Enhanced Allocation, FPB).

Parameters:
  • cap_id – legacy capability ID (8 bits)

Returns:

A pointer to a static string. Always non-NULL — unknown IDs return “<unknown>”.

const char *axl_pci_ext_cap_id_str(uint16_t cap_id)

Look up a human-readable name for a PCIe extended capability ID.

Covers the standard IDs from PCIe Base Specification — AER, Virtual Channel, Serial Number, Power Budgeting, ACS, ATS, SR-IOV, MR-IOV, Multicast, Resizable BAR, DPA, TPH, LTR, Secondary PCIe, PMUX, PASID, LNR, DPC, L1 PM Substates, PTM, Frame Capability, ReadyToReset, Designated Vendor-Specific, VF Resizable BAR, Data Link Feature, Physical Layer 16/32 GT/s, Lane Margining, Hierarchy ID, NPEM, etc.

Parameters:
  • cap_id – extended capability ID (16 bits)

Returns:

A pointer to a static string. Always non-NULL — unknown IDs return “<unknown>”.

int axl_pci_vpd_read(AxlPciAddr addr, const char keyword[2], uint8_t *buf, size_t buflen, size_t *out_len)

Read a VPD keyword from a function’s Vital Product Data area.

Walks the VPD capability (PCI 3.0 §6.4) — keyword-tagged blocks inside the Read-Only and Read/Write resource sections. Keyword is exactly 2 ASCII characters (e.g. “PN” for part number, “EC” for engineering change, “SN” for serial). The function locates the matching keyword in either RO or RW area and copies up to buflen bytes of its data into buf.

Parameters:
  • addr – target function

  • keyword – 2-char ASCII keyword (NOT nul-terminated)

  • buf – destination buffer

  • buflen – capacity of buf

  • out_len – [out] keyword’s actual length

Returns:

AXL_OK on success, AXL_ERR if VPD is unsupported, the keyword is not present, or any bus error is encountered. On success, *out_len is set to the keyword’s actual data length (which may exceed buflen — in which case the buffer was truncated).

int axl_pci_vpd_iter(AxlPciAddr addr, int (*cb)(const char keyword[2], const uint8_t *data, size_t len, void *ctx), void *ctx)

Walk every keyword in a function’s VPD area and dispatch to a callback.

Complements axl_pci_vpd_read for tools that want “show me

everything that’s there” rather than “fetch this specific

keyword.” Visits both the Read-Only (PN/EC/SN/MN/RV/V0..V9/…) and Read-Write (Y0..Y9/RW/…) resource sections in document order. Vendor-specific keywords (V0..V9, Y0..Y9) reach the callback alongside the standard ones.

The data buffer passed to cb is owned by the implementation and is only valid for the duration of the call — the callback must copy bytes it wants to retain. Returning non-zero from cb stops iteration; that value becomes the iter return.

Parameters:
  • addr – target function

  • cb – per-keyword callback. Receives keyword (2-char ASCII), data (impl-owned bytes), len, and ctx.

  • ctx – opaque context forwarded to cb

Returns:

0 if iteration completed without the callback stopping it, the callback’s non-zero return if it stopped early, or -1 if VPD is unsupported or any bus error is encountered.

AxlSidecarStatus axl_pci_ids_open(const char *path, AxlPciIds **out)

Open a database handle by reading a JSON5 file at path.

The FILE_MISSING / PARSE_ERROR split lets tools log differently — “no database shipped” is a deployment problem (numeric fallback is fine), while “parse error” is an authoring problem that should be loud.

Parameters:
  • path – path to JSON5 file

  • out – [out] handle on success

Returns:

AXL_SIDECAR_OK (handle returned via out), AXL_SIDECAR_FILE_MISSING if path does not exist or is unreadable, AXL_SIDECAR_PARSE_ERROR if the file was found but JSON5 parsing or schema validation failed.

AxlSidecarStatus axl_pci_ids_open_from_buffer(const char *json5, size_t len, AxlPciIds **out)

Open a database handle from an in-memory JSON5 buffer.

Identical semantics to axl_pci_ids_open but reads from a caller-owned buffer instead of a file. Useful for embedded or test fixtures that ship the database compiled in.

Parameters:
  • json5 – JSON5 source (no NUL required)

  • len – buffer length in bytes

  • out – [out] handle on success

Returns:

AXL_SIDECAR_OK on success, AXL_SIDECAR_PARSE_ERROR on parse / schema error. (No FILE_MISSING return — the buffer is the input, so “not found” doesn’t apply.)

void axl_pci_ids_close(AxlPciIds *ids)

Free a database handle.

NULL-safe. After calling, every pointer previously returned by the axl_pci_ids_*_name lookups against this handle is invalid.

Parameters:
  • ids – handle (NULL-safe)

const char *axl_pci_ids_vendor_name(const AxlPciIds *ids, uint16_t vid)

Vendor lookup against an explicit handle.

Returns:

database-owned string or NULL if unknown / handle empty.

const char *axl_pci_ids_device_name(const AxlPciIds *ids, uint16_t vid, uint16_t did)

Device lookup against an explicit handle.

Returns:

database-owned string or NULL if (vid, did) is unknown.

const char *axl_pci_ids_subsys_name(const AxlPciIds *ids, uint16_t svid, uint16_t sdid)

Subsystem lookup against an explicit handle.

Subsystem IDs identify the OEM card built around a piece of silicon — a server-vendor rebadged NIC’s (svid, sdid) decodes to the OEM SKU name even though the underlying device’s (vid, did) reports the silicon vendor. The (svid, sdid) pair lives at config offsets 0x2C / 0x2E on header-type-0 functions.

Returns:

database-owned string or NULL if (svid, sdid) is unknown.

int axl_pci_ids_foreach_vendor(const AxlPciIds *ids, AxlPciIdsVendorFn fn, void *ctx)

Iterate every vendor entry in a database.

Useful for debug dumps (“show me everything in this overlay”), validators (“does my private DB shadow these public entries?”), and code that needs to materialize the database into a different representation (sorted list, text export, …).

Iteration order is hash-table-internal — do not rely on it.

Returns:

0 if the walk completed without the callback stopping it, the callback’s first non-zero return if it stopped early, or -1 if ids or fn is NULL.

int axl_pci_ids_foreach_device(const AxlPciIds *ids, AxlPciIdsDeviceFn fn, void *ctx)

Iterate every (vid, did) device entry. See axl_pci_ids_foreach_vendor.

int axl_pci_ids_foreach_subsys(const AxlPciIds *ids, AxlPciIdsSubsysFn fn, void *ctx)

Iterate every (svid, sdid) subsystem entry. See axl_pci_ids_foreach_vendor.

AxlSidecarStatus axl_pci_ids_load(const char *override_path)

Load a curated PCI vendor/device/subsystem name database.

Two modes selected by override_path:

  • Explicit (override_path non-NULL): use exactly that path. Returns AXL_SIDECAR_FILE_MISSING if the file is missing, AXL_SIDECAR_PARSE_ERROR if found but malformed. No fallback — explicit means explicit, so the error code reflects what the user asked for.

  • Autodiscover (override_path NULL): try pci-ids.json5 next to the running .efi (companion path), then in the current working directory. Returns AXL_SIDECAR_FILE_MISSING if neither candidate exists, AXL_SIDECAR_PARSE_ERROR if a candidate was found but failed to parse.

The file format is the JSON5 schema axl-sdk ships in share/pci-ids.json5 — vendor entries { id, name }, device entries { vid, did, name }, optional subsystem entries { svid, sdid, name }. Only IDs explicitly listed are decoded; for the long tail use scripts/pci-ids-to-json5.py to generate a custom database from the canonical pci.ids text file.

Idempotent: a successful load is a no-op on subsequent calls.

On a successful first load, the singleton registers an axl_atexit cleanup so the parsed hash tables are freed at runtime cleanup automatically. Calling axl_pci_ids_free explicitly is still fine (it unregisters the trampoline) and worth doing for consumers that want to drop the database before exit, but it’s no longer required for leak-free shutdown.

Parameters:
  • override_path – explicit path, or NULL to auto-discover

void axl_pci_ids_free(void)

Free the loaded vendor/device database.

Safe to call when no database is loaded. After calling, the pointers previously returned from axl_pci_vendor_name and axl_pci_device_name are no longer valid.

Optional — axl_pci_ids_load registers an atexit cleanup automatically. Call this only when you want to drop the database before runtime cleanup runs (e.g. memory-pressure reclaim).

const char *axl_pci_vendor_name(uint16_t vid)

Look up a vendor name by 16-bit vendor ID.

Parameters:
  • vid – 16-bit vendor ID

Returns:

pointer to the vendor name (database-owned, valid until axl_pci_ids_free), or NULL if no database is loaded or vid is not present in the loaded set.

const char *axl_pci_device_name(uint16_t vid, uint16_t did)

Look up a device name by (vid, did) pair.

Does not fall back to the vendor name when the device is unknown — callers compose their own “vendor name + numeric device ID” fallback (or use axl_pci_format_name).

Parameters:
  • vid – 16-bit vendor ID

  • did – 16-bit device ID

Returns:

pointer to the device name (database-owned), or NULL if no database is loaded or the pair isn’t in the loaded set.

const char *axl_pci_subsys_name(uint16_t svid, uint16_t sdid)

Look up a subsystem (OEM card) name by (svid, sdid) pair.

See axl_pci_ids_subsys_name for the rationale (OEM-rebadged silicon needs OEM SKU decoding). Same fallback semantics as the other singleton helpers — NULL when no database is loaded or the pair is unknown.

Parameters:
  • svid – 16-bit subsystem vendor ID

  • sdid – 16-bit subsystem device ID

int axl_pci_ids_format_name(const AxlPciIds *ids, uint16_t vid, uint16_t did, char *buf, size_t buflen)

Compose a “vendor + device” display string against a handle.

Centralizes the rendering convention every consumer would otherwise reinvent — the goal is that every tool prints the same string for the same (vid, did) pair. Output:

  • vendor known + device known → "<vendor> <device>"

  • vendor known + device unknown → "<vendor> Device <DID hex>"

  • vendor unknown → "<VID>:<DID>"

Hex literals in the output are lowercase, 4-wide, zero-padded (matching Linux lspci convention).

Vendor-unknown short-circuits regardless of device-name presence: without a verified vendor a device-name hit is ambiguous provenance, so the fallback is always all-numeric.

Output never exceeds AXL_PCI_NAME_COMPOSED_MAX bytes — pin char buf[AXL_PCI_NAME_COMPOSED_MAX] and the formatter is truncation-safe.

Returns:

number of bytes written excluding NUL (snprintf-shape), or -1 on bad arguments.

int axl_pci_format_name(uint16_t vid, uint16_t did, char *buf, size_t buflen)

Singleton-backed convenience wrapper for axl_pci_ids_format_name.

Equivalent to axl_pci_ids_format_name(<process-global handle>, ...). Layered-DB consumers should call the handle form directly with their own priority chain.

AxlSidecarStatus axl_pci_class_open(const char *path, AxlPciClassDb **out)

Open a class-overlay handle from a JSON5 file.

Returns:

AXL_SIDECAR_OK / AXL_SIDECAR_FILE_MISSING / AXL_SIDECAR_PARSE_ERROR.

AxlSidecarStatus axl_pci_class_open_from_buffer(const char *json5, size_t len, AxlPciClassDb **out)

Open a class-overlay handle from an in-memory buffer.

Returns:

AXL_SIDECAR_OK / AXL_SIDECAR_PARSE_ERROR.

void axl_pci_class_close(AxlPciClassDb *db)

Free a class-overlay handle. NULL-safe.

const char *axl_pci_class_db_base_name(const AxlPciClassDb *db, uint8_t base)

Per-tier overlay lookups against an explicit handle.

Only the overlay is consulted — these do NOT fall back to the compiled-in tables. Consumers that want “overlay first, then

compiled-in” should use axl_pci_class_string_fmt, which internally walks the singleton overlay then the built-in tables.

Returns:

database-owned string or NULL if db is NULL or the tier has no override entry for this code.

const char *axl_pci_class_db_sub_name(const AxlPciClassDb *db, uint8_t base, uint8_t sub)
const char *axl_pci_class_db_prog_name(const AxlPciClassDb *db, uint8_t base, uint8_t sub, uint8_t prog)
AxlSidecarStatus axl_pci_class_load(const char *override_path)

Load the process-global class-name overlay.

Same lookup semantics as axl_pci_ids_load — explicit override_path is authoritative; NULL autodiscovers via pci-ids.json5 next to the running .efi, then in cwd. Loader reads only the classes[] section, ignoring vendors[].

Once loaded, every axl_pci_class_string and axl_pci_class_string_fmt call consults the overlay before the compiled-in tables. The compiled-in tables stay as the bootstrap so axl-sdk works without a sidecar at all.

Like axl_pci_ids_load, this registers an atexit cleanup on a successful first load so the overlay is freed at runtime cleanup automatically. Calling axl_pci_class_free explicitly is optional (used to drop the overlay early).

Returns:

AXL_SIDECAR_OK on success (idempotent on second call), AXL_SIDECAR_FILE_MISSING if missing, AXL_SIDECAR_PARSE_ERROR on parse error.

void axl_pci_class_free(void)

Free the process-global class-name overlay.

After calling, lookups fall back exclusively to the compiled-in tables.

struct AxlPciAddr
#include <axl-pci.h>

A PCI configuration-space address (segment:bus:dev:func).

16-bit segment matches the UEFI MCFG / PCIe spec width so multi- segment platforms are addressable directly — every lookup, walk, and find helper takes segment as part of the address tuple. Single-segment systems leave it at 0. Bus / dev / func are the standard 8/5/3-bit fields.

Public Members

uint16_t seg

PCI segment group.

uint8_t bus

bus number (0..255)

uint8_t dev

device number (0..31)

uint8_t func

function number (0..7)

struct AxlPciBridge
#include <axl-pci.h>

Per-bridge bus-number tuple.

For a PCI-PCI bridge function (header type 1), these three bytes live at config-space offsets 0x18 / 0x19 / 0x1A. The bridge claims config-space transactions for buses in the inclusive range [secondary, subordinate] and forwards them downstream.

Public Members

uint8_t primary

upstream bus the bridge sits on

uint8_t secondary

first bus on the downstream side

uint8_t subordinate

highest bus number behind this bridge