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

Two boilerplate-killer wrappers around the standard config-space header offsets — they fold the “is this function absent?” and “unpack the 24-bit class triplet” patterns into one call:

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 class24;  /* (base << 16) | (sub << 8) | prog_if */
axl_pci_get_class24(addr, &class24);

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. class24 matches the shape consumed by axl_pci_find_by_class.

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) {
    if (id == 0x10) {
        /* PCIe Capability — PCI Express link/device control */
    }
}

off = 0;
while (axl_pci_ext_cap_next(addr, off, &off, &id) == 0) {
    if (id == 0x0002) {
        /* Virtual Channel */
    }
}

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);
    // ...
}

Defines

AXL_PCI_ADDR_STR_MAX

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

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:

0 on success, -1 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:

0 on success, -1 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_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:

0 on success (both fields populated), -1 if the function is absent or any bus error is encountered.

int axl_pci_get_class24(AxlPciAddr addr, uint32_t *class24)

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

  • class24 – [out] 24-bit class code

Returns:

0 on success, -1 on bus error.

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 (vendor ID 0xFFFF) are skipped. Single-function devices are detected via the header-type byte and their functions 1–7 are skipped.

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).

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:

0 on success, -1 if no nth match exists.

int axl_pci_find_by_class(uint32_t class24, 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:
  • class24 – 24-bit class code

  • nth – 0-based match index

  • out – [out] address of the matching function

Returns:

0 on success, -1 if no nth match exists.

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:

0 on success (capability found), -1 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:

0 on success, -1 when the chain ends or no extended caps are present.

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:

0 on success, -1 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

  • 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.

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)