ACPI Device Enumeration

The Advanced Configuration and Power Interface (ACPI) is the firmware standard that, on x86 and modern Arm servers, tells the operating system what hardware exists on a board that cannot announce itself — the embedded controller, the LPC/legacy peripherals, the platform sensors, the I2C and SPI devices soldered to the mainboard. Where a PCI card advertises itself in configuration space and a USB device through its descriptors, this non-discoverable hardware must be described by firmware. ACPI does this by exposing a tree of objects — the ACPI namespace — built from bytecode tables the BIOS/UEFI firmware hands to the kernel at boot. The Linux ACPI core under drivers/acpi/ walks that namespace, and for every Device object it finds it creates a struct acpi_device, reads the device’s identifiers (_HID, _CID, _ADR), its resources (_CRS), and its device-specific properties (_DSD), and — for the majority of devices — mints a [[Platform Devices and Drivers|struct platform_device]] that the driver core then matches to a driver. This note explains where ACPI tables come from, how the namespace is structured, how the kernel scans it, and how the result plugs into the unified device model. It is grounded in the Linux 6.12 LTS source tree (the 6.18 LTS ACPI core is structurally the same; see the version note).

Mental Model

The single most useful idea: ACPI is to x86/server platforms what the Device Tree is to embedded ARM — a firmware-supplied description of non-discoverable hardware. Both answer the question “the bus can’t find these devices; who tells the kernel they exist?” The difference is form. Device Tree is a static, declarative data structure (a flattened tree of nodes and properties). ACPI is richer and stranger: its tables contain bytecode — ACPI Machine Language (AML) — that the kernel must interpret at runtime to build a tree of objects and to call methods (_CRS, _STA, _DSD) that can compute their answers dynamically rather than returning fixed data. This is the deep reason ACPI needs an in-kernel interpreter (ACPICA) while Device Tree needs only a parser.

flowchart TB
  subgraph FW["Firmware tables (handed to kernel at boot)"]
    RSDP["RSDP<br/>(Root System Description Pointer)<br/>found via UEFI config table / BIOS region"]
    XSDT["XSDT / RSDT<br/>(list of pointers to all other tables)"]
    FADT["FADT<br/>(Fixed ACPI Description Table)<br/>points to the DSDT"]
    DSDT["DSDT + SSDTs<br/>(AML bytecode)"]
    RSDP --> XSDT --> FADT --> DSDT
  end
  DSDT -->|"ACPICA interprets AML"| NS
  subgraph NS["ACPI namespace (in-kernel object tree)"]
    SB["\\_SB (System Bus scope)"]
    D1["Device (PNP0C0C)<br/>_HID, _CRS, _STA, _DSD"]
    D2["Device (INT33FC)<br/>_HID, _CRS, _DSD"]
    SB --> D1
    SB --> D2
  end
  NS -->|"acpi_bus_scan walks tree"| CORE
  subgraph CORE["drivers/acpi/ scan core"]
    ADEV["struct acpi_device<br/>(pnp.ids = _HID + _CIDs)"]
    PDEV["struct platform_device<br/>(or i2c_client / spi_device)"]
    ADEV --> PDEV
  end
  PDEV -->|"acpi_match_table by HID"| DRV["driver .probe()"]

From firmware tables to a bound driver. What it shows: the kernel finds the RSDP, follows it through the table chain (XSDT → FADT → DSDT), and an interpreter turns the DSDT/SSDT bytecode into a namespace of objects; the scan core walks that namespace, creates an acpi_device per Device, and promotes most of them to platform devices keyed by their _HID. The insight to take: there is a clean two-stage pipeline — firmware tables → namespace (interpretation), then namespace → device model (the scan). The first stage is ACPI-specific machinery; the second stage funnels into exactly the same [[The Linux Device Model|struct device]] graph everything else lands in.

The Table Chain — Where the Namespace Comes From

Before any device exists, the kernel must locate ACPI’s tables. The root is the Root System Description Pointer (RSDP), a small structure with the signature "RSD PTR ". On legacy BIOS systems it lives in a well-known low-memory region (the Extended BIOS Data Area or the 0xE0000–0xFFFFF BIOS area); on UEFI systems the firmware hands its physical address to the OS through the EFI configuration table, so no scanning is needed (Wikipedia: ACPI, which states “The Root System Description Pointer (RSDP) is located in a platform-dependent manner, and describes the rest of the tables”).

The RSDP points to a top-level index table. On ACPI 1.0 this was the Root System Description Table (RSDT), an array of 32-bit physical pointers to every other table. ACPI 2.0 added the Extended System Description Table (XSDT), the same idea with 64-bit pointers; the RSDP’s revision field tells the OS which to use, and modern kernels prefer the XSDT. Among the pointers in the XSDT is the Fixed ACPI Description Table (FADT), which holds fixed hardware register addresses (power management, the SCI interrupt) and — crucially for enumeration — a pointer to the Differentiated System Description Table (DSDT). The DSDT, together with any number of Secondary System Description Tables (SSDTs), contains the AML bytecode that defines the namespace (Wikipedia: ACPI: “The FADT (Fixed ACPI Description Table) points to the main system description, while the DSDT (Differentiated System Description Table) contains the primary AML code”).

Uncertain

Verify: the precise memory regions for legacy-BIOS RSDP discovery (EBDA pointer at 0x40E, then the 0xE0000–0xFFFFF scan on a 16-byte boundary) and the exact RSDP revision values that select RSDT vs XSDT. Reason: the authoritative ACPI specification page on uefi.org returned HTTP 403 during this research and the OSDev wiki RSDT page was also blocked, so the table-discovery details rest on Wikipedia’s secondary summary rather than the primary spec. To resolve: read §5.2 (“ACPI System Description Tables”) of the ACPI Specification (currently 6.6, May 2025, UEFI Forum) directly. uncertain

The two languages involved are worth naming precisely. ACPI Source Language (ASL) is the human-readable source a firmware engineer writes; the iASL compiler turns it into ACPI Machine Language (AML), the bytecode actually stored in the DSDT/SSDT. Linux carries ACPICA (ACPI Component Architecture), “an open-source platform-independent reference implementation of the operating system–related ACPI code” (Wikipedia: ACPI) — the same interpreter used by FreeBSD and others. ACPICA parses the tables and executes AML methods on the kernel’s behalf; the Linux-specific glue lives in drivers/acpi/. ACPI was originally developed by Intel, Microsoft, and Toshiba; in October 2013 its assets were transferred to the UEFI Forum, which now stewards the specification.

The Namespace and Its Identifier Objects

The namespace is a tree of named objects rooted at \. Standard scopes include \_SB (the System Bus, where most enumerable devices hang), \_PR (processors, legacy), \_TZ (thermal zones), and \_GPE (general-purpose events). Inside \_SB, firmware declares Device (...) objects, each of which may contain named methods and data objects beginning with an underscore — the “control methods” of the ACPI spec.

For enumeration, a handful of these objects matter:

  • _HID (Hardware ID) — the primary identifier, mandatory for any device on a non-enumerable bus. It is either a PNP ID (e.g. PNP0501 for a 16550 UART) or an ACPI ID (a four-letter vendor code plus hex, e.g. INT33FC). The kernel matches drivers against this.
  • _CID (Compatible ID) — one or more fallback IDs, analogous to listing several compatible strings in Device Tree. A driver can match a _CID when no driver claims the more specific _HID.
  • _ADR (Address) — used instead of _HID for devices on an enumerable bus that ACPI still describes (e.g. a specific PCI device/function pair, encoded as (device << 16) | function). _ADR locates the device within its parent’s address space rather than naming a driver-matchable ID.
  • _STA (Status) — a method returning presence/enabled/functional bits; the kernel uses it to decide whether a device is actually present.
  • _CRS (Current Resource Settings) — a method returning a resource template: the memory ranges, IRQs, GPIO lines, and serial-bus connectors the device uses.
  • _DSD (Device Specific Data) — the ACPI analog of Device-Tree properties: arbitrary key/value pairs (covered below).

Mechanical Walk-Through — The Scan

The entry point is acpi_bus_scan(), documented in the source as adding “ACPI device node objects in a given namespace scope” (scan.c, v6.12). It runs in two passes to cope with dependencies between devices. The first pass calls acpi_walk_namespace(ACPI_TYPE_ANY, handle, ACPI_UINT32_MAX, acpi_bus_check_add_1, ...), which invokes acpi_bus_check_add() with first_pass = true on every namespace node. This pass deliberately postpones any device whose _DEP (dependency) method names a supplier that has not yet been enumerated — the scan records these on an acpi_dep_list via acpi_scan_check_dep(). The second pass, acpi_scan_postponed(), then enumerates the devices that were waiting. The two-pass design exists so that, for example, a device that depends on a GPIO or clock controller is not created before its supplier.

For each qualifying node, acpi_add_single_object() does the real work (scan.c, v6.12): it allocates a struct acpi_device, calls acpi_init_device_object(device, handle, type, acpi_device_release) to wire up the embedded struct device and the acpi_handle back-pointer, evaluates _STA via acpi_bus_get_status(), and registers it with acpi_device_add(). Presence is checked by acpi_device_is_present(), which the source defines as testing “adev->status.present || adev->status.functional”.

Identifier extraction is the heart of matching. acpi_set_pnp_ids() calls ACPICA’s acpi_get_object_info() and then copies whatever the firmware declared into the device’s ID list. The verbatim code shows exactly which _xxx objects feed struct acpi_device_pnp (scan.c, v6.12):

if (info->valid & ACPI_VALID_HID) {
    acpi_add_id(pnp, info->hardware_id.string);   /* _HID → first id */
    pnp->type.platform_id = 1;
}
if (info->valid & ACPI_VALID_CID) {
    cid_list = &info->compatible_id_list;
    for (i = 0; i < cid_list->count; i++)
        acpi_add_id(pnp, cid_list->ids[i].string); /* every _CID appended */
}
if (info->valid & ACPI_VALID_ADR) {
    pnp->bus_address = info->address;              /* _ADR */
    pnp->type.bus_address = 1;
}
if (info->valid & ACPI_VALID_UID)
    pnp->unique_id = kstrdup(info->unique_id.string, GFP_KERNEL); /* _UID */

Line by line: the _HID becomes the first entry on pnp->ids and sets platform_id (this is the ID drivers match against first); each _CID is appended in order, so they act as fallbacks; _ADR is stored as a bus address (not a matchable string); _UID distinguishes multiple instances of the same _HID. The kernel also notes that “Some devices don’t reliably have _HIDs & _CIDs, so add synthetic HIDs to make sure drivers can find them” (scan.c, v6.12). The accessor acpi_device_hid() returns the first ID or, if the list is somehow empty, a dummy_hid sentinel.

const char *acpi_device_hid(struct acpi_device *device)
{
    struct acpi_hardware_id *hid;
    hid = list_first_entry_or_null(&device->pnp.ids,
                                   struct acpi_hardware_id, list);
    if (!hid)
        return dummy_hid;
    return hid->id;
}

Promotion to a platform_device

Most ACPI devices are not driven by binding to the struct acpi_device directly. Instead acpi_default_enumeration() calls acpi_create_platform_device() (in acpi_platform.c) to wrap the device in a [[Platform Devices and Drivers|struct platform_device]] — the same object an embedded SoC peripheral gets. The kernel doc is explicit: “the core ACPI device enumeration code creates struct platform_device objects for the majority of devices that are discovered and enumerated with the help of the platform firmware” (enumeration, v6.12). Drivers should therefore be platform_drivers that also carry an acpi_match_table, not acpi_drivers — the latter exists but is reserved for special cases.

acpi_create_platform_device() is documented as: “Check if the given @adev can be represented as a platform device and, if that’s the case, create and register a platform device, populate its common resources and returns a pointer to it” (acpi_platform.c, v6.12). It gathers resources by calling acpi_dev_get_resources(adev, &resource_list, NULL, NULL) — which evaluates _CRS — and fills a struct platform_device_info whose name is dev_name(&adev->dev), id is PLATFORM_DEVID_NONE, fwnode is the ACPI firmware node, and dma_mask is DMA_BIT_MASK(32) when DMA is supported. Crucially it consults a forbidden_id_list so that core platform infrastructure is never turned into a platform device:

{"ACPI0009", 0},   /* IOxAPIC */
{"ACPI000A", 0},   /* IOAPIC  */
{"PNP0000",  0},   /* PIC     */
{"PNP0100",  0},   /* Timer   */
{"PNP0200",  0},   /* AT DMA Controller */

The IRQ controllers, the legacy PIC, and the system timer are handled by dedicated kernel subsystems, not by generic platform-device probing, so they are excluded.

Resource translation — _CRS

_CRS returns an AML ResourceTemplate listing memory ranges, I/O ports, IRQs, GPIO connections, and serial-bus connectors. The ACPI core parses these into the kernel’s generic struct resource array attached to the platform device, so a driver reaches them with the same helpers a Device-Tree-described platform device uses — platform_get_resource(), platform_get_irq(). For named interrupts, firmware adds an interrupt-names array in _DSD, and a driver calls fwnode_irq_get_byname() with the fwnode and the name. This is the fwnode abstraction at work: the driver never sees ACPI-specific resource code.

_DSD — The ACPI Analog of Device-Tree Properties

The biggest historical gap between ACPI and Device Tree was that ACPI had no general key/value property mechanism — firmware could only express what the spec pre-defined. _DSD (Device Specific Data), added in ACPI 5.1, closed it. A _DSD returns a package whose first element is a UUID selecting the format, and whose second element is the data. The format used for generic properties is the Device Properties UUID daffd814-6eba-4d8c-8a91-bc9bbf4aa301, which the kernel source documents as “the ACPI _DSD device properties GUID [1]” (property.c, v6.12). Under that UUID the data is a list of Package () { "name", value } pairs — exactly the shape of Device-Tree properties.

Name (_DSD, Package () {
    ToUUID("daffd814-6eba-4d8c-8a91-bc9bbf4aa301"),
    Package () {
        Package () { "size",          0x80000 },
        Package () { "pagesize",      32       },
        Package () { "address-width", 16       },
    }
})

A driver reads these with the firmware-agnostic API — device_property_read_u32(dev, "size", &size) — the very same call it would use on a Device-Tree system. The at25 EEPROM driver is the canonical example in the kernel docs (enumeration, v6.12). On the kernel side, acpi_init_properties() evaluates _DSD, and acpi_extract_properties() validates the package and, finding the device-properties GUID, records each pair so the [[The Unified Device Property Interface fwnode|device_property_*]] machinery can serve it (property.c, v6.12). The unification was introduced by Mika Westerberg’s 2014 patch set, “Add ACPI _DSD and unified device properties support” (LWN, 2014).

Driver Matching and a Realistic Example

A platform driver advertises the ACPI IDs it supports through an acpi_device_id table installed in driver.acpi_match_table:

static const struct acpi_device_id mydrv_acpi_match[] = {
    { "INT33FC", 0 },     /* exact _HID this driver claims */
    { }                   /* terminator */
};
MODULE_DEVICE_TABLE(acpi, mydrv_acpi_match);
 
static struct platform_driver my_driver = {
    .probe = my_probe,
    .driver = {
        .name = "my_driver",
        .acpi_match_table = mydrv_acpi_match,
    },
};
module_platform_driver(my_driver);

MODULE_DEVICE_TABLE(acpi, ...) exports the table into the module’s metadata so userspace modprobe/udev can autoload the driver when a matching device appears; .acpi_match_table is what the platform bus’s .match() compares against each device’s _HID/_CID list at match time. Inside .probe(), device_get_match_data(dev) returns the per-entry driver_data from the matched table row — letting one driver handle several closely-related parts.

Serial-bus devices (I2C/SPI) get enumerated automatically

A peripheral hanging off an I2C or SPI controller declares its connector inside _CRS. After the controller’s adapter registers, the I2C or SPI core walks the controller’s ACPI children, reads those connectors, and creates a struct i2c_client or struct spi_device automatically — the slave driver “only need[s] to add the ACPI IDs like with the platform and SPI drivers” (enumeration.rst, v6.12). An example I2C connector in _CRS:

I2cSerialBusV2 (0x48, ControllerInitiated, 400000,
    AddressingMode7Bit, "\\_SB.PCI0.I2C1", 0x00,
    ResourceConsumer, , Exclusive,)

This says: a 7-bit address 0x48 device at 400 kHz behind the controller named \_SB.PCI0.I2C1. The driver author never parses this string; the I2C core does.

PRP0001 — borrowing Device Tree’s compatible strings

A clever bridge lets ACPI reuse the enormous catalog of Device-Tree compatible strings. If a device’s _HID (or _CID) is the special value PRP0001, “the ACPI subsystem will look for the ‘compatible’ property in the device object’s _DSD and will use the value of that property to identify the corresponding device” (enumeration.rst, v6.12). This lets a driver that only ships an of_match_table bind on an ACPI system without adding an acpi_match_table — the firmware author just sets _HID to PRP0001 and adds a compatible property to _DSD.

Failure Modes and How to Diagnose Them

The classic symptom is a device that exists in firmware but whose driver never probes. The diagnostic order:

  • Is the device present per _STA? If _STA returns “not present,” acpi_device_is_present() is false and no platform device is created. Check the DSDT. You can dump and disassemble the live tables with acpidump | acpixtract then iasl -d to read the ASL.
  • Does the _HID/_CID actually match the driver’s acpi_match_table? A single wrong character (vendor IDs like INT33FC vs INT3FFC) silently prevents matching. Look under /sys/bus/acpi/devices/ — each acpi_device exposes its hid, modalias, and status there.
  • Is it stuck behind a _DEP? If the device depends on a supplier that itself failed to enumerate, the second-pass scan leaves it unbound. dmesg ACPI lines and /sys/bus/acpi/devices/<dev>/physical_node (or its absence) reveal this. The generic deferred-probe machinery covers the driver-level version of the same dependency problem.
  • Was it excluded by forbidden_id_list? Timers, PICs, IOAPICs intentionally never become platform devices; that is correct, not a bug.

Uncertain

Verify: the exact ACPI version that introduced _DSD (commonly cited as ACPI 5.1, ~2014) and the version that introduced serial-bus connection resources (commonly cited as ACPI 5.0). Reason: these version attributions come from the kernel docs and secondary summaries; the primary ACPI specification page (uefi.org) was blocked (HTTP 403) during this research, so the exact spec revisions were not confirmed against the standard itself. To resolve: check the change history / §6.2 of the ACPI Specification on uefi.org. uncertain

A subtler trap is binding to the wrong object. Drivers should bind to the platform_device (or i2c_client/spi_device), not write a raw acpi_driver. Inside such a driver ACPI_HANDLE(dev) (equivalently ACPI_COMPANION(dev)) returns the namespace handle so the driver can evaluate extra methods; the kernel docs note that when “ACPI_HANDLE(dev) returns non-NULL the device was enumerated from ACPI namespace” (enumeration, v6.12).

Alternatives and When ACPI Applies

The sibling firmware description is the Device Tree, dominant on ARM/embedded/RISC-V. The practical division: ACPI on x86 PCs and servers and on Arm server-class systems (Arm’s SBSA/SBBR specs mandate ACPI for servers), Device Tree on embedded and mobile. ACPI’s advantage is that it abstracts power management and platform quirks behind firmware-provided methods, so one OS image boots across many boards without per-board data; its cost is the AML interpreter and the opacity of bytecode firmware. Device Tree is simpler and fully inspectable but pushes more knowledge into the kernel. The _DSD mechanism and PRP0001 exist precisely to let the same driver serve both worlds via the unified property API — the reason a modern driver rarely needs to know which firmware described its device.

Production Notes

On any x86 laptop or server, ls /sys/bus/acpi/devices/ shows the enumerated namespace; cat /sys/firmware/acpi/tables/DSDT > dsdt.aml; iasl -d dsdt.aml reverse-engineers the firmware’s ASL — invaluable when a vendor’s _DSD properties are undocumented. The acpi= and acpi_osi= boot parameters, and overriding the DSDT with a patched table, are common field workarounds for buggy firmware. Intel’s Bay Trail / Cherry Trail tablet era drove much of the _DSD and GPIO/serial-bus enumeration work, because those SoCs described I2C touchscreens, sensors, and PMICs entirely through ACPI rather than PCI (LWN, 2014).

Uncertain

Verify: that the Arm SBSA/SBBR server specifications mandate ACPI (rather than merely permit it). Reason: this is stated from general knowledge of Arm’s server boot standards and was not confirmed against an Arm primary document during this task. To resolve: check the Arm Base Boot Requirements (BBR) specification. uncertain

See Also