Device Operations in coreboot Firmware
Introduction
The device_operations
structure is a cornerstone of coreboot’s
hardware abstraction layer. It represents a set of function pointers
defining how the firmware interacts with a specific hardware device
during various boot stages. This structure lets the coreboot
architecture establish a consistent interface for device initialization,
configuration, resource allocation, and ACPI table generation, while
allowing device-specific implementations behind this common interface.
At its core, device_operations
applies the strategy pattern in
systems programming. It decouples algorithms (device operations) from
the core boot sequence, allowing devices to define their own behavior
while the boot process follows a predictable flow. This pattern enables
coreboot to support a wide range of hardware platforms with minimal
changes to the core boot sequence code.
Structure Definition
The device_operations
structure, defined in
src/include/device/device.h
, consists of several function pointers,
each representing a specific operation performed on a device during
boot:
struct device_operations {
void (*read_resources)(struct device *dev);
void (*set_resources)(struct device *dev);
void (*enable_resources)(struct device *dev);
void (*init)(struct device *dev);
void (*final)(struct device *dev);
void (*scan_bus)(struct device *bus);
void (*enable)(struct device *dev);
void (*vga_disable)(struct device *dev);
void (*reset_bus)(struct bus *bus);
int (*get_smbios_data)(struct device *dev, int *handle,
unsigned long *current);
void (*get_smbios_strings)(struct device *dev, struct smbios_type11 *t);
unsigned long (*write_acpi_tables)(const struct device *dev,
unsigned long start, struct acpi_rsdp *rsdp);
void (*acpi_fill_ssdt)(const struct device *dev);
const char *(*acpi_name)(const struct device *dev);
const char *(*acpi_hid)(const struct device *dev);
const struct pci_operations *ops_pci;
const struct i2c_bus_operations *ops_i2c_bus;
const struct spi_bus_operations *ops_spi_bus;
const struct smbus_bus_operations *ops_smbus_bus;
const struct pnp_mode_ops *ops_pnp_mode;
const struct gpio_operations *ops_gpio;
const struct mdio_bus_operations *ops_mdio;
};
Core Resource Management Functions
read_resources
: Discovers and collects resources required by the device (I/O ports, memory regions, IRQs, etc.). This typically involves reading configuration registers or static device tree information.set_resources
: Assigns finalized resources to the device after the resource allocation phase. This writes the assigned base addresses, lengths, and other parameters back to the device structure, but not necessarily to hardware registers yet.enable_resources
: Activates the assigned resources in the device’s hardware registers (e.g., writing to PCI BARs, enabling memory decoding).
Device Lifecycle Functions
init
: Performs device-specific initialization, often requiring access to the device’s assigned resources. This is called after resources have been enabled.final
: Executes final setup or cleanup operations before the payload is loaded. This is useful for tasks that depend on other devices being initialized.enable
: Enables the device in the hardware, often setting bits in configuration registers to make the device active. Called afterenable_resources
.
Bus Management Functions
scan_bus
: Enumerates child devices on a bus device (e.g., scanning a PCI bus for devices, probing I2C devices). Only applicable to devices that act as buses.reset_bus
: Resets all devices on a specific bus.vga_disable
: Disables VGA decoding on a PCI device when another VGA device is active. Used to manage legacy VGA resources.
System Table Generation Functions
get_smbios_data
: Provides SMBIOS data specific to the device for Type 9 (System Slots) or Type 41 (Onboard Devices Extended Information).get_smbios_strings
: Supplies string information for SMBIOS tables, often related to the data provided byget_smbios_data
.write_acpi_tables
: Generates device-specific ACPI tables (like SSDTs) or contributes data to system-wide tables.acpi_fill_ssdt
: Adds device-specific objects (scopes, methods, data) to the Secondary System Description Table (SSDT).acpi_name
: Returns the ACPI name for the device (e.g.,\_SB.PCI0.GFX0
). This defines the device’s path in the ACPI namespace.acpi_hid
: Returns the ACPI Hardware ID (HID) for the device (e.g.,PNP0A08
). Used by the OS to match drivers.
Bus-Specific Operation Pointers
These fields point to bus-specific operation structures when a device functions as a bus controller (or exposes bus-like functionality). See the “Bus-Specific Operations” section for details.
ops_pci
: Operations for PCI configuration space access.ops_i2c_bus
: Operations for I2C bus transactions (read, write, transfer).ops_spi_bus
: Operations for SPI bus transactions.ops_smbus_bus
: Operations for SMBus transactions.ops_pnp_mode
: Operations for Plug-and-Play device configuration.ops_gpio
: Operations for GPIO control (get, set, configure direction/pulls).ops_mdio
: Operations for MDIO (Management Data Input/Output) bus access, used for Ethernet PHYs.
Device Lifecycle in coreboot
The function pointers in device_operations
are called at specific
stages during the boot process, following a sequence defined in
coreboot’s boot state machine (src/lib/hardwaremain.c
). Understanding
this lifecycle helps developers implement appropriate behavior for each
function pointer.
Boot Sequence and Device Operations
coreboot’s main device initialization sequence involves these boot states:
BS_DEV_INIT_CHIPS (
dev_initialize_chips()
): Initializes chip drivers (chip_operations
).BS_DEV_ENUMERATE (
dev_enumerate()
): Discovers and enumerates devices.Calls
scan_bus()
for each bus to detect child devices.
BS_DEV_RESOURCES (
dev_configure()
): Allocates resources across all enumerated devices.Calls
read_resources()
for each device to discover required resources.Calls
set_resources()
for each device to assign allocated resources back to thestruct device
.
BS_DEV_ENABLE (
dev_enable()
): Enables devices and their resources.Calls
enable_resources()
for each device to activate assigned resources in hardware.Calls
enable()
for each device to perform general hardware enablement.
BS_DEV_INIT (
dev_initialize()
): Initializes devices.Calls
init()
for each device to perform device-specific setup.
BS_POST_DEVICE (
dev_finalize()
): Finalizes devices before payload loading.Calls
final()
for each device for any final cleanup or setup.
The sequence is primarily driven by the boot_states
array in
src/lib/hardwaremain.c
:
static struct boot_state boot_states[] = {
/* ... other states ... */
BS_INIT_ENTRY(BS_PRE_DEVICE, bs_pre_device),
BS_INIT_ENTRY(BS_DEV_INIT_CHIPS, bs_dev_init_chips),
BS_INIT_ENTRY(BS_DEV_ENUMERATE, bs_dev_enumerate),
BS_INIT_ENTRY(BS_DEV_RESOURCES, bs_dev_resources),
BS_INIT_ENTRY(BS_DEV_ENABLE, bs_dev_enable),
BS_INIT_ENTRY(BS_DEV_INIT, bs_dev_init),
BS_INIT_ENTRY(BS_POST_DEVICE, bs_post_device),
/* ... other states ... */
};
Later stages include ACPI and SMBIOS table generation, where functions
like write_acpi_tables()
, acpi_fill_ssdt()
, get_smbios_data()
, and
get_smbios_strings()
are invoked as part of the table construction
process.
Inheritance and Code Reuse Patterns
The device_operations
structure enables several patterns for code
reuse:
1. Default Implementations
coreboot provides default implementations for common device types (like root devices, PCI devices, PCI bridges), which can be used directly or extended. Chip or mainboard code often assigns these defaults if no specific driver is found.
/* From src/device/root_device.c */
struct device_operations default_dev_ops_root = {
.read_resources = noop_read_resources,
.set_resources = noop_set_resources,
.scan_bus = scan_static_bus,
.reset_bus = root_dev_reset,
#if CONFIG(HAVE_ACPI_TABLES)
.acpi_name = root_dev_acpi_name,
#endif
};
2. No-op Functions
Simple shim functions (often static inline) are provided for cases where a device doesn’t need to implement specific operations. Using these avoids leaving function pointers NULL.
/* From src/include/device/device.h */
static inline void noop_read_resources(struct device *dev) {}
static inline void noop_set_resources(struct device *dev) {}
3. Chain of Responsibility / Delegation
Some implementations delegate to parent devices or use helper functions when they can’t handle an operation themselves or when common logic can be shared. For example, ACPI name generation often traverses up the device tree.
/* Simplified example logic */
const char *acpi_device_name(const struct device *dev)
{
const char *name = NULL;
const struct device *pdev = dev;
/* Check for device specific handler */
if (dev->ops && dev->ops->acpi_name) {
name = dev->ops->acpi_name(dev);
if (name)
return name; /* Device handled it */
}
/* Walk up the tree to find if any parent can provide a name */
while (pdev->upstream && pdev->upstream->dev) {
pdev = pdev->upstream->dev;
if (pdev->ops && pdev->ops->acpi_name) {
/* Note: Parent's acpi_name might handle the original child 'dev' */
name = pdev->ops->acpi_name(dev);
if (name)
return name; /* Parent handled it */
}
}
/* Fallback or default logic if needed */
return NULL;
}
This pattern allows parent devices (like buses) to provide default behavior or naming schemes if a child device doesn’t specify its own.
Implementation Examples
These examples show typical device_operations
assignments. Actual
implementations might involve more conditional compilation based on
Kconfig options.
PCI Device Operations (Default)
/* From src/device/pci_device.c */
struct device_operations default_pci_ops_dev = {
.read_resources = pci_dev_read_resources,
.set_resources = pci_dev_set_resources,
.enable_resources = pci_dev_enable_resources,
#if CONFIG(HAVE_ACPI_TABLES)
.write_acpi_tables = pci_rom_write_acpi_tables,
.acpi_fill_ssdt = pci_rom_ssdt,
#endif
.init = pci_dev_init,
/* Assigns PCI-specific operations */
.ops_pci = &pci_dev_ops_pci,
};
CPU Cluster Operations
/* From src/soc/intel/alderlake/chip.c (representative example) */
static struct device_operations cpu_bus_ops = {
.read_resources = noop_read_resources,
.set_resources = noop_set_resources,
.enable_resources = cpu_set_north_irqs,
#if CONFIG(HAVE_ACPI_TABLES)
.acpi_fill_ssdt = cpu_fill_ssdt,
#endif
/* CPU clusters often don't need scan_bus, init, etc. */
};
GPIO Controller Operations
/* From src/soc/intel/common/block/gpio/gpio_dev.c */
static struct gpio_operations gpio_ops = {
.get = gpio_get,
.set = gpio_set,
/* ... other GPIO functions ... */
};
struct device_operations block_gpio_ops = {
.read_resources = noop_read_resources,
.set_resources = noop_set_resources,
/* Assigns GPIO-specific operations */
.ops_gpio = &gpio_ops,
};
Registration and Discovery
How are device_operations
structures associated with struct device
instances?
1. Static Assignment (via chip_operations
)
For devices known at build time (defined in devicetree.cb), the
device_operations
structure is often assigned in the SOC’s or
mainboard’s chip_operations->enable_dev()
function based on the
device path type or other properties.
/* Example from src/soc/intel/alderlake/chip.c */
static void soc_enable(struct device *dev)
{
/* Assign ops based on the device's role in the tree */
if (dev->path.type == DEVICE_PATH_DOMAIN)
dev->ops = &pci_domain_ops; /* Handles PCI domain resources */
else if (dev->path.type == DEVICE_PATH_CPU_CLUSTER)
dev->ops = &cpu_bus_ops; /* Handles CPU cluster setup */
else if (dev->path.type == DEVICE_PATH_GPIO)
block_gpio_enable(dev); /* Assigns block_gpio_ops */
/* ... other assignments for specific PCI devices, etc. ... */
}
The enable_dev
function is part of struct chip_operations
, which
handles broader chip-level initialization (see “Relationship with
chip_operations
” section).
2. Dynamic Detection (PCI Drivers)
For PCI devices discovered during bus scanning (scan_bus
), coreboot
looks through a list of registered PCI drivers (_pci_drivers
array)
to find one matching the device’s vendor and device IDs.
/* Logic from src/device/pci_device.c::set_pci_ops() */
static void set_pci_ops(struct device *dev)
{
struct pci_driver *driver;
/* Check if ops already assigned (e.g., by chip_ops->enable_dev) */
if (dev->ops)
return;
/* Look through registered PCI drivers */
for (driver = &_pci_drivers[0]; driver != &_epci_drivers[0]; driver++) {
if ((driver->vendor == dev->vendor) &&
device_id_match(driver, dev->device)) {
/* Found a matching driver, assign its ops */
dev->ops = (struct device_operations *)driver->ops;
printk(BIOS_SPEW, "%s: Assigned ops from driver for %04x:%04x\n",
dev_path(dev), driver->vendor, driver->device);
return; /* Stop searching */
}
}
/* Fall back to default operations if no specific driver found */
if (!dev->ops) {
if ((dev->hdr_type & 0x7f) == PCI_HEADER_TYPE_BRIDGE) {
dev->ops = get_pci_bridge_ops(dev); /* Special ops for bridges */
} else {
dev->ops = &default_pci_ops_dev; /* Default for normal devices */
}
printk(BIOS_SPEW, "%s: Assigned default PCI ops\n", dev_path(dev));
}
}
Build Process Integration
The device_operations
structures are integrated into the coreboot
build process:
Static Device Tree: The mainboard’s
devicetree.cb
defines the initial device hierarchy. The build process converts this into C code (static.c
) containingstruct device
instances.PCI Driver Registration: PCI drivers (containing their own
device_operations
) register themselves using the__pci_driver
linker set macro./* Example pattern */ struct pci_driver example_pci_driver __pci_driver = { .ops = &example_device_ops, /* Pointer to device_operations */ .vendor = VENDOR_ID, .device = DEVICE_ID, /* Or .devices for a list */ };
Linking: The build system collects all structures placed in the
__pci_driver
section and creates the_pci_drivers
array used byset_pci_ops()
. It ensures all necessary code (default ops, driver ops, core device functions) is linked into the final firmware image.
Relationship with chip_operations
It’s important to distinguish device_operations
from
chip_operations
(defined in src/include/chip.h
).
chip_operations
: Defines operations related to the overall chipset or mainboard logic. It includes functions called earlier in the boot process, likeenable_dev
,init
, andfinal
.chip_operations->enable_dev()
is crucial as it often performs initial setup for static devices and is the primary place wheredevice_operations
pointers are assigned for non-PCI devices based on their path or type.chip_operations->init()
runs duringBS_DEV_INIT_CHIPS
, before mostdevice_operations
functions.
device_operations
: Defines operations for individual devices within the device tree. These are called after the correspondingchip_operations
stage and operate on a specificstruct device
.
Essentially, chip_operations
sets the stage at the SoC/mainboard level,
including assigning the correct device_operations
to static devices,
while device_operations
handles the specific actions for each device
later in the boot process.
Bus-Specific Operations
The ops_*
pointers within struct device_operations
(e.g., ops_pci
,
ops_i2c_bus
, ops_spi_bus
, ops_gpio
) provide a way for devices that
act as bus controllers or expose bus-like interfaces to offer
standardized access methods.
Purpose: They abstract the low-level details of interacting with that specific bus type. For example, a PCI host bridge device will implement
struct pci_operations
via itsops_pci
pointer, allowing other code to perform PCI config reads/writes through it without knowing the exact hardware mechanism. Similarly, an I2C controller device implementsstruct i2c_bus_operations
viaops_i2c_bus
to provide standardread
,write
, andtransfer
functions for that bus segment.Usage: Code needing to interact with a bus first finds the controller
struct device
in the tree, then accesses the relevant bus operations through the appropriateops_*
pointer, passing the target address or parameters. For instance, to talk to an I2C device at address0x50
on the bus controlled byi2c_controller_dev
, one might call:i2c_controller_dev->ops->ops_i2c_bus->transfer(...)
. Helper functions often wrap this access pattern.Implementation: The structures like
struct pci_operations
,struct i2c_bus_operations
, etc., are defined in corresponding header files (e.g.,src/include/device/pci_ops.h
,src/include/drivers/i2c/i2c_bus.h
). Devices acting as controllers provide concrete implementations of these functions, tailored to their hardware.
This mechanism allows coreboot to manage diverse bus types using a consistent device model, where the controller device itself exposes the necessary functions for interacting with devices on its bus.
Best Practices
When implementing device_operations
:
Leverage Defaults/No-ops: Use default or no-op implementations whenever possible. Only override functions that require custom behavior for your specific device.
Error Handling: Check return values from functions called within your ops implementations and handle errors gracefully (e.g., log an error, return an error code if applicable).
Resource Management: In
read_resources
, accurately declare all resources (MMIO, I/O ports, IRQs) your device needs, specifying flags like fixed vs. alignment, or bridge vs. standard device. Incorrect resource declaration is a common source of issues.Initialization Order: Be mindful of dependencies in
init
. If your device relies on another device being fully initialized, consider deferring that part of the initialization to thefinal
callback, which runs later.Minimal Implementation: Only implement the functions relevant to your device type. A simple MMIO device might only need
read_resources
,set_resources
,enable_resources
, and perhaps ACPI functions. A bus device additionally needsscan_bus
.Bus Operations: If implementing a bus controller, correctly implement the corresponding bus operations structure (e.g.,
struct pci_operations
,struct i2c_bus_operations
) and assign it to the appropriateops_*
field.ACPI/SMBIOS: If the device needs OS visibility via ACPI or SMBIOS, implement the relevant functions (
acpi_name
,acpi_hid
,acpi_fill_ssdt
,get_smbios_data
, etc.). Ensure ACPI names and HIDs are correct according to specifications and platform needs.
Logging and Debugging
Use coreboot’s logging facilities (printk
) within your device_operations
functions to provide visibility during development and debugging. Use
appropriate log levels (e.g., BIOS_DEBUG
, BIOS_INFO
, BIOS_ERR
).
static void example_device_init(struct device *dev)
{
printk(BIOS_DEBUG, "%s: Initializing device at %s\n", __func__,
dev_path(dev));
/* ... Device initialization code ... */
if (/* some condition */) {
printk(BIOS_SPEW, "%s: Condition met, applying setting X\n",
dev_path(dev));
/* ... */
}
if (/* error condition */) {
printk(BIOS_ERR, "%s: Failed to initialize feature Y!\n",
dev_path(dev));
/* Handle error */
}
printk(BIOS_DEBUG, "%s: Initialization complete for %s\n", __func__,
dev_path(dev));
}
Consistent logging helps trace the boot process and pinpoint where issues occur.
Common Troubleshooting
Missing Resource Declarations:
Problem: Device fails to function, or conflicts arise because a required resource (MMIO range, I/O port, IRQ) was not declared in
read_resources
. The resource allocator is unaware of the need.Solution: Verify that
read_resources
correctly calls functions likepci_dev_read_resources
or manually adds all necessary resources using functions likemmio_resource()
,io_resource()
, etc. Check PCI BARs or device datasheets.
Initialization Order Issues:
Problem:
init()
fails because it depends on another device that hasn’t been fully initialized yet (e.g., accessing a shared resource like SMBus before the SMBus controller is ready).Solution: Move the dependent initialization code to the
final
callback if possible. Alternatively, ensure the dependency is met by careful ordering in the device tree or using boot state callbacks if necessary for complex scenarios.
Resource Conflicts:
Problem: Boot fails during resource allocation, or devices misbehave because multiple devices requested the same non-sharable resource (e.g., conflicting fixed MMIO regions).
Solution: Review resource declarations in
read_resources
across all relevant devices. Ensure fixed resources don’t overlap. Check if bridge windows are correctly defined and large enough. Use coreboot’s resource reporting logs to identify overlaps.
ACPI Table Generation Errors:
Problem: The operating system fails to recognize the device, assigns the wrong driver, or the device doesn’t function correctly (e.g., power management issues).
Solution: Double-check the
acpi_name
,acpi_hid
,_CRS
(generated from assigned resources), andacpi_fill_ssdt
implementations. Verify names match the ACPI hierarchy and HIDs match expected driver bindings. Ensure SSDT methods correctly access hardware. Use OS debugging tools (e.g.,acpidump
, Device Manager errors) to diagnose.
Incorrect
ops
Pointer Assigned:Problem: Device behaves incorrectly because the wrong
device_operations
structure was assigned (e.g., default PCI ops assigned to a device needing a specific driver’s ops).Solution: Check the logic in
chip_operations->enable_dev
(for static devices) or the PCI driver registration (__pci_driver
macro andset_pci_ops
fallback logic) to ensure the correctops
structure is being selected and assigned based on device type, path, or PCI ID. Add debug prints to verify whichops
structure is assigned.
Advanced Usage
Complex Device Hierarchies
For devices with non-standard interactions or complex initialization,
custom device_operations
can be created, often inheriting from defaults
but overriding specific functions.
static void advanced_device_init(struct device *dev)
{
/* First, perform standard PCI init */
pci_dev_init(dev);
/* Then, add custom initialization steps */
printk(BIOS_DEBUG, "%s: Performing advanced init\n", dev_path(dev));
/* ... custom register writes, configuration ... */
}
static const char *advanced_device_acpi_name(const struct device *dev)
{
/* Provide a custom ACPI name based on some property */
if (/* condition */)
return "ADV0001";
else
return "ADV0002";
}
/* Combine default and custom operations */
static struct device_operations advanced_device_ops = {
/* Inherit resource handling from default PCI ops */
.read_resources = pci_dev_read_resources,
.set_resources = pci_dev_set_resources,
.enable_resources = pci_dev_enable_resources,
/* Override init */
.init = advanced_device_init,
/* Override ACPI naming */
.acpi_name = advanced_device_acpi_name,
/* Other functions might use defaults or no-ops */
};
Dynamic Configuration based on Probing
Some init
or other op implementations might probe the device’s
capabilities or read configuration data (e.g., from SPD, VPD, or straps)
and alter their behavior accordingly.
static void conditional_device_init(struct device *dev)
{
uint8_t feature_flags;
/* Read capability register from the device */
feature_flags = pci_read_config8(dev, EXAMPLE_CAP_REG);
printk(BIOS_DEBUG, "%s: Feature flags: 0x%02x\n", dev_path(dev),
feature_flags);
/* Conditional initialization based on detected features */
if (feature_flags & FEATURE_X_ENABLED) {
printk(BIOS_INFO, "%s: Initializing Feature X\n", dev_path(dev));
init_feature_x(dev);
}
if (feature_flags & FEATURE_Y_ENABLED) {
printk(BIOS_INFO, "%s: Initializing Feature Y\n", dev_path(dev));
init_feature_y(dev);
}
}
Conclusion
The device_operations
structure is a powerful abstraction mechanism in
coreboot. It enables consistent handling of diverse hardware while
allowing for device-specific behavior. By providing a standard interface
for device operations throughout the boot process, it simplifies the
codebase, enhances maintainability, and provides the extensibility needed
to support new hardware platforms.
Understanding this structure, its relationship with chip_operations
,
and its role in the boot process is essential for coreboot developers,
particularly when adding support for new devices or debugging hardware
initialization issues. By following the patterns and best practices
outlined here, developers can create robust and reusable device driver
implementations that integrate smoothly into the coreboot architecture.