PCI Subsystem: A Technical Deep Dive
[ English | 简体中文 ]
I. PCI Subsystem Framework Overview
The PCI (Peripheral Component Interconnect) subsystem in the openvela operating system closely follows the mature model of the Linux PCI subsystem. Therefore, developers familiar with the Linux kernel can quickly grasp its core concepts.
The openvela PCI framework is mainly composed of the following three core parts:
- PCI Controller Initialization and Bus Enumeration: Responsible for scanning the PCI bus at system startup, discovering all connected devices, and allocating resources.
- PCI RC (Root Complex) Driver Registration: Manages drivers for RC-facing devices, handling the matching and initialization of devices and drivers.
- PCI EPC (Endpoint Controller) Framework: Supports configuring the system as a PCI Endpoint device and manages its functions.
II. PCI Controller Initialization and Bus Enumeration
1. Initialization Process
After the system powers on, the PCI Host Bridge driver calls the pci_register_controller() function to start the initialization of the PCI subsystem. The entire process follows the standard PCI Bus Enumeration procedure.

The detailed steps are as follows:
-
Initialize the root bus:
The system requests resources for the root bus (Bus 0) and initializes the relevant list structures. This includes adding the bus node to the global bus list
g_pci_busesand initializing itschildren(subordinate buses) anddeviceslists. -
Depth-first traversal of devices:
This includes PCI-to-PCI bridges and Endpoint devices. The system calls
pci_alloc_deviceto request a correspondingstruct pci_device_sfor each device and populates its data structure members.-
For a PCI-to-PCI bridge device: A new PCI subordinate bus (the
child_busof the original bus) is requested and added to the current bus'schildrenlist. Next, the scan continues downward from the bridge device to itschildbusto complete the traversal of the subordinate bus. The bridge device's configuration space is then populated, which mainly includes three parts: the I/Obaseandlimit, the memory addressbaseandstart, and the prefetchable memory addressstartandlimit. -
For a PCI Endpoint device: The system calls
pci_alloc_device()to allocate astruct pci_device_sinstance for it and fills in information such as the BDF (Bus/Device/Function) number. Subsequently, the system allocates a corresponding PCI domain address for it based on the BAR (Base Address Register) space type (I/O, prefetchable memory, non-prefetchable memory). Finally, the device is added to thedeviceslist of its bus, preparing it for the subsequent driver matching process.
-
-
Registering Devices with the System:
After traversing and allocating resources for the PCI subsystem, the
pci_register_bus_devices(bus)function is called. This function iterates through thedevicesandchildrenlists on the bus and callspci_register_device(dev)to register the devices from these lists with the system. If a matching driver has already been registered, the system will automatically call that driver'sprobecallback function; otherwise, the device will only be registered on the bus, awaiting the future loading of a driver.
2. Core Data Structures
struct pci_controller_s
This structure defines the resource pool and operations of a PCI controller.
/* PCI controller resource configuration data */
struct pci_controller_s
{
struct pci_resource_s io; /* I/O type PCI resources */
struct pci_resource_s mem; /* Non-prefetchable Mem type PCI resources */
struct pci_resource_s mem_pref; /* Prefetchable Mem type PCI resources */
FAR const struct pci_ops_s *ops;
FAR struct pci_bus_s *bus; /* Points to the root bus */
uint8_t busno; /* PCI root bus number */
struct list_node node; /* Node in the global list of PCI controllers */
};
struct pci_ops_s
This structure defines the hook functions that the low-level hardware controller needs to implement to abstract hardware operations.
/* Hook functions for the PCI controller */
struct pci_ops_s
{
CODE int (*read)(FAR struct pci_bus_s *bus, uint32_t devfn, int where,
int size, FAR uint32_t *val);
CODE int (*write)(FAR struct pci_bus_s *bus, uint32_t devfn, int where,
int size, uint32_t val);
/* Return memory address for pci resource */
CODE uintptr_t (*map)(FAR struct pci_bus_s *bus, uintptr_t start,
uintptr_t end);
CODE int (*read_io)(FAR struct pci_bus_s *bus, uintptr_t addr,
int size, FAR uint32_t *val);
CODE int (*write_io)(FAR struct pci_bus_s *bus, uintptr_t addr,
int size, uint32_t val);
/* Get interrupt number associated with a given INTx line */
CODE int (*get_irq)(FAR struct pci_bus_s *bus, uint32_t devfn,
uint8_t line, uint8_t pin);
/* Allocate interrupt for MSI/MSI-X */
CODE int (*alloc_irq)(FAR struct pci_bus_s *bus, uint32_t devfn,
FAR int *irq, int num);
CODE void (*release_irq)(FAR struct pci_bus_s *bus, FAR int *irq, int num);
/* Connect interrupt for MSI/MSI-X */
CODE int (*connect_irq)(FAR struct pci_bus_s *bus, FAR int *irq,
int num, FAR uintptr_t *mar, FAR uint32_t *mdr);
};
struct pci_bus_s
This structure represents a PCI bus.
/* PCI bus data structure */
struct pci_bus_s
{
FAR struct pci_controller_s *ctrl; /* Associated host controller */
FAR struct pci_bus_s *parent_bus; /* Parent bus */
struct list_node node; /* Node in list of buses */
struct list_node children; /* List of child buses */
struct list_node devices; /* List of devices on this bus */
uint8_t number; /* Bus number */
};
3. Core APIs
The core of PCI subsystem initialization is registering the PCI controller:
| Function Prototype | Description |
|---|---|
int pci_register_controller(FAR struct pci_controller_s *ctrl) |
Registers a PCI controller and starts the bus enumeration process. |
int pci_register_device(FAR struct pci_device_s *dev) |
Registers a PCI device with the PCI core layer. |
III. PCI RC (Root Complex) Device Driver Registration
After PCI devices and their drivers are registered with the PCI bus, the PCI core layer attempts to match each device with a suitable driver. Upon a successful match, the system calls the driver's provided probe callback function to initiate device initialization.
Using the PCI EDU (Educational) virtual device driver as an example, the standard process is as follows:

-
Register the driver:
The driver developer implements
struct pci_driver_s, fills in the device ID table (id_table) and callback functions likeprobe, and then callspci_register_driver()to register the driver with the global device list. -
Match the device and driver:
When a device in the device list is successfully matched with a driver, the driver's
probefunction is called for initial configuration. -
Execute the
probefunction:After a successful match, the system calls the driver's
probefunction, passing apci_device_sinstance as a parameter. The driver performs device initialization within this function, which mainly includes:- Calling
pci_enable_device()to enable the device. - Calling
pci_set_master()to set up the configuration space, allowing the device to act as a bus master to initiate I/O and memory access. - Using
pci_map_bar()to map the BAR (Base Address Register) space, obtaining a virtual address accessible by the CPU.
- Calling
-
Configure interrupts:
- Legacy INTx Interrupts: Call
pci_get_irq()to get the interrupt number, then register an Interrupt Service Routine (ISR) and enable the interrupt. - Message Signaled Interrupts (MSI/MSI-X): Call
pci_alloc_irq()to request interrupt resources, then configure the Capability register viapci_connect_irq(). Similarly, after obtaining the interrupt number, an ISR must be registered and the interrupt enabled.
- Legacy INTx Interrupts: Call
1. Core Data Structures
struct pci_driver_s
This structure defines a PCI device driver.
/* PCI driver data members */
struct pci_driver_s
{
FAR const struct pci_device_id_s *id_table; // PCI device ID table
/* New device inserted */
CODE int (*probe)(FAR struct pci_device_s *dev); // probe hook function
/* Device removed (NULL if not a hot-plug capable driver) */
CODE void (*remove)(FAR struct pci_device_s *dev); // remove hook function
struct list_node node; // Node in the global list of drivers
};
struct pci_device_s
This structure represents a PCI device.
/* PCI device data members */
struct pci_device_s
{
struct list_node node; /* Node in the global pci_device list */
struct list_node bus_list; /* Node in per-bus list, added to dev->bus->devices list */
FAR struct pci_bus_s *bus; /* Bus this device is on */
FAR struct pci_bus_s *subordinate; /* Bus this device bridges to (points to subordinate bus) */
uint32_t devfn; /* Encoded device & function index */
uint16_t vendor; /* Vendor id */
uint16_t device; /* Device id */
uint16_t subsystem_vendor;
uint16_t subsystem_device;
uint32_t class; /* 3 bytes: (base,sub,prog-if) */
uint8_t revision; /* PCI revision, low byte of class word */
uint8_t hdr_type; /* PCI header type ('multi' flag masked out) */
/* I/O and memory regions + expansion ROMs */
struct pci_resource_s resource[PCI_NUM_RESOURCES];
FAR struct pci_driver_s *drv;
FAR void *priv; /* Used by pci driver */
};
2. Core APIs
| Function Prototype | Description |
|---|---|
int pci_register_driver(FAR struct pci_driver_s *drv) |
Registers a PCI device driver. |
int pci_enable_device(FAR struct pci_device_s *dev) |
Enables a PCI device and allocates I/O and memory resources. |
void pci_set_master(FAR struct pci_device_s *dev) |
Enables the PCI device's control over memory and I/O read/write requests. |
void *pci_map_bar(FAR struct pci_device_s *dev, int bar) |
Maps a specified BAR space to the CPU address space. |
int pci_get_irq(FAR struct pci_device_s *dev) |
Gets the PCI Legacy interrupt number used by the device. |
void pci_enable_irq(FAR struct pci_device_s *dev, int irq) |
Enables the specified PCI Legacy interrupt number. |
void pci_disable_irq(FAR struct pci_device_s *dev) |
Disables the PCI Legacy interrupt. |
int pci_alloc_irq(FAR struct pci_device_s *dev, FAR int *irq, int num) |
Allocates MSI/MSI-X interrupts. |
void pci_release_irq(FAR struct pci_device_s *dev, FAR int *irq, int num) |
Releases allocated MSI/MSI-X interrupts. |
int pci_connect_irq(FAR struct pci_device_s *dev, FAR int *irq, int num) |
Configures the MSI/MSI-X interrupt Capability register. |
3. Hands-on: Booting a PCI EDU Device with QEMU
The EDU (Educational) device is a virtual PCI device provided by QEMU, making it ideal for learning and debugging PCI driver development. You can easily test your drivers in a QEMU environment.
Example Boot Command:
sudo qemu-system-aarch64 \
-m 32g \
-cpu cortex-a53 \
-machine virt,virtualization=on,gic-version=2 \
-kernel /path/to/your/nuttx \
-nographic \
-chardev stdio,id=con,mux=on \
-serial chardev:con \
-mon chardev=con,mode=readline \
-device edu \
-D ./nuttx_edu.log
Note:
Please modify the local path
/path/to/your/nuttxin the command according to your actual environment.
IV. PCI EPC (Endpoint Controller) Framework
The PCI EPC (Endpoint Controller) allows a system to act as an Endpoint device on the PCI bus. Its design is also inspired by Linux's three-layer model, which, from top to bottom, consists of:
- Endpoint Function (EPF) Driver Layer: The driver that implements the specific functionality of the device.
- Endpoint Core Framework Layer: Provides a unified interface to connect function drivers with controller drivers.
- Endpoint Controller (EPC) Driver Layer: The low-level driver that interacts directly with the hardware.

1. EPF (Endpoint Function) Driver Flow
-
Create the EPC device:
The EP Controller is a PCI device. The EPC driver acts as a standard PCI device driver. When the system detects the EP Controller hardware, its
probefunction is called. In this function, in addition to performing regular PCI initialization,pci_epc_create()is called to create an EPC device, which is then added to the global listg_pci_epc_device_list. -
Register the EPF device and driver:
A PCI EPF device is registered via
pci_epf_device_register(), and a PCI EPF driver is registered viapci_epf_register_driver(). After a successful match, the corresponding driver'sprobefunction is called. -
Bind the EPF to the EPC:
When an EPF device and driver are successfully matched, the core layer calls
pci_epf_bind(). This function triggers the driver'sbindcallback. In thebindcallback, the EPF driver associates itself with a specific EPC device and initializes the configuration space and BAR spaces. -
Start the EPF:
After the
bindis complete, an upper-level application can initiate astartrequest, which ultimately calls the EPC driver'sstartcallback function. -
Configure the hardware:
In the
startcallback, the EPC driver configures relevant hardware registers based on the EPF's features (such as MSI/MSI-X support) and enables the PCI Link.
2. Core Data Structures
struct pci_epc_ctrl_s
This structure represents an Endpoint Controller.
/* PCI EPC control data members */
struct pci_epc_ctrl_s
{
struct list_node epf; // List of EP functions; EPF will be added to this list
FAR const struct pci_epc_ops_s *ops;
FAR struct pci_epc_mem_s *mem; // EPC address space
unsigned int num_windows; // Number of outbound address maps
uint8_t max_functions; // Max functions (8)
struct list_node node; // Node added to the global pci_epc_device list
/* Mutex to protect against concurrent access of EP controller */
mutex_t lock;
unsigned long funcno_map; // Bitmap for function numbers
FAR void *priv;
char name[0];
};
/* Hook functions defined by epc_ops */
static const struct pci_epc_ops_s g_qemu_epc_ops =
{
.write_header = qemu_epc_write_header,
.set_bar = qemu_epc_set_bar,
.clear_bar = qemu_epc_clear_bar,
.map_addr = qemu_epc_map_addr,
.unmap_addr = qemu_epc_unmap_addr,
.raise_irq = qemu_epc_raise_irq,
.start = qemu_epc_start,
.get_features = qemu_epc_get_features,
.set_msi = qemu_epc_set_msi,
.get_msi = qemu_epc_get_msi,
.set_msix = qemu_epc_set_msix,
.get_msix = qemu_epc_get_msix,
};
struct pci_epf_driver_s
This structure defines an Endpoint Function driver.
/* PCI EPF driver data members */
struct pci_epf_driver_s
{
CODE int (*probe)(FAR struct pci_epf_device_s *epf);
CODE void (*remove)(FAR struct pci_epf_device_s *epf);
struct list_node node; // Node in the global pci_epf_device list
FAR struct pci_epf_ops_s *ops; // See g_pci_epf_test_ops below
FAR const struct pci_epf_device_id_s *id_table; // See g_pci_epf_test_id_table below
};
static const struct pci_epf_ops_s g_pci_epf_test_ops =
{
.unbind = pci_epf_test_unbind,
.bind = pci_epf_test_bind,
};
static const struct pci_epf_device_id_s g_pci_epf_test_id_table[] =
{
{.name = "pci_epf_test_0", },
{.name = "pci_epf_test_1", },
{}
};
3. Core APIs
| Function Prototype | Description |
|---|---|
FAR struct pci_epc_ctrl_s *pci_epc_create(...) |
Creates an Endpoint Controller device. |
void pci_epc_destroy(FAR struct pci_epc_ctrl_s *epc) |
Unregisters and releases an Endpoint Controller device. |
int pci_epc_start(FAR struct pci_epc_ctrl_s *epc) |
Starts the EPC and enables the PCI Link. |
void pci_epc_stop(FAR struct pci_epc_ctrl_s *epc) |
Stops the EPC and disables the PCI Link. |
int pci_epc_raise_irq(...) |
Raises an interrupt (INTx, MSI, MSI-X) via the EPC. |
int pci_epc_set_bar(...) |
Configures the BAR space for a specified Function. |
int pci_epc_add_epf(...) |
Associates an EPF device with an EPC. |
int pci_epf_device_register(...) |
Registers an EPF device. |
int pci_epf_register_driver(...) |
Registers an EPF driver. |
V. Debugging Tools: pciutils
- Code Path:
external/pciutils - Compilation: Enable
CONFIG_PCIUTILS=yin menuconfig.
Usage Example (lspci):


VI. Testing Tools: pcitest
pcitest is a PCI Endpoint test application integrated into the apps/testing directory. It needs to be used in conjunction with the pci_ep_test.c driver to verify communication between the RC and the EP.
-
Application Path:
apps/testing/pcitest -
Build Configuration:
- Enable
CONFIG_TESTING_PCITEST=yto compile thepcitestapplication. - Enable
CONFIG_EP_TEST=yto compile thepci_ep_test.cdriver.
- Enable
Execution Example:
-
pcitestoutput on the RC side:
-
Test output on the EP side: