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_OKon success (idempotent on the second call)AXL_SIDECAR_FILE_MISSINGif no candidate file existsAXL_SIDECAR_PARSE_ERRORif 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:
trueif 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 pullof the JSON5 sidecar without rebuilding every consumer.The overlay lives in the
classes[]section ofshare/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.
-
enumerator AXL_PCI_HEADER_TYPE_NORMAL
-
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)
-
enumerator AXL_PCI_CLASS_FMT_FULL
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 0seg: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
outleft 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.Fform.Always emits the 4-digit segment — round-trips with axl_pci_addr_parse. NUL-terminates
bufwhenbuflen>= 13.- Parameters:
addr – address to format
buf – destination buffer
buflen – capacity of
buf
- Returns:
number of bytes written excluding NUL, or -1 if
buflenis 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
bytesof PCI configuration space intobuf.Walks the function’s config space in 32-bit ECAM-natural chunks (little-endian on the wire, packed into
bufat offsets 0..bytes).bytesis 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_readtracks 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 bufloop.- 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
bufis 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_ifform — 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
& 0x7Fmasking and& 0x80bit 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
typeis 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/sdiduntouched.- 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.idsdatabase) 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
bufis NULL orbuflenis 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_FULLis identical to axl_pci_class_string.FMT_SUBCLASSemits 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_BASEemits 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
nthfunction 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
nthmatch exists.
-
int axl_pci_find_by_class(uint32_t class_code, uint16_t nth, AxlPciAddr *out)
Find the
nthfunction with a matching class triplet.The 24-bit class is
(base_class << 16) | (subclass << 8) | prog_if, matching howlspci -vvvprints it. Pass0xFFFFFFto 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
nthmatch exists.
-
int axl_pci_bridge_info(AxlPciAddr addr, AxlPciBridge *out)
Read the bridge bus tuple, if
addris 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 guaranteesaddris 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
addris 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 asAXL_DP_MAX_NODESfor 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_nextinternally and they share a static cursor. The callback must not callaxl_pci_next(the tree walk itself usesaxl_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_offto 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 passout_offfrom 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
buflenbytes of its data intobuf.- Parameters:
addr – target function
keyword – 2-char ASCII keyword (NOT nul-terminated)
buf – destination buffer
buflen – capacity of
bufout_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_lenis set to the keyword’s actual data length (which may exceedbuflen— 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
cbis 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 fromcbstops 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_ERRORsplit 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 viaout),AXL_SIDECAR_FILE_MISSINGifpathdoes not exist or is unreadable,AXL_SIDECAR_PARSE_ERRORif 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_OKon success,AXL_SIDECAR_PARSE_ERRORon parse / schema error. (NoFILE_MISSINGreturn — 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_*_namelookups 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
idsorfnis 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_pathnon-NULL): use exactly that path. ReturnsAXL_SIDECAR_FILE_MISSINGif the file is missing,AXL_SIDECAR_PARSE_ERRORif found but malformed. No fallback — explicit means explicit, so the error code reflects what the user asked for.Autodiscover (
override_pathNULL): trypci-ids.json5next to the running .efi (companion path), then in the current working directory. ReturnsAXL_SIDECAR_FILE_MISSINGif neither candidate exists,AXL_SIDECAR_PARSE_ERRORif 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 usescripts/pci-ids-to-json5.pyto 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
vidis 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
dbis 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_pathis authoritative; NULL autodiscovers viapci-ids.json5next to the running .efi, then in cwd. Loader reads only theclasses[]section, ignoringvendors[].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_OKon success (idempotent on second call),AXL_SIDECAR_FILE_MISSINGif missing,AXL_SIDECAR_PARSE_ERRORon 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.
-
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.