3. QNX PCI子系统架构:pci-server、pci-tool、libpci接口、PCI设备枚举流程
QNX的PCI子系统采用经典的客户端-服务器架构,将硬件枚举、资源分配与中断路由集中管理,为驱动开发者提供统一的访问接口。理解这一架构是编写正确PCI驱动的前提。
3.1 架构总览:三层分离模型
整个PCI子系统分为三个逻辑层,每一层职责清晰,通过标准IPC机制通信:
核心设计思想: 驱动开发者无需直接操作PCI配置空间,所有对PCI配置空间的读写请求都通过消息传递委托给 pci-server 处理,从而保证系统级的资源冲突检测与原子性。
3.2 核心组件详解
3.2.1 pci-server (io-pci)
pci-server 是PCI子系统的唯一仲裁者,在系统启动时由进程管理器自动启动。它负责以下关键任务:
- 总线枚举: 扫描PCI总线树,发现所有设备与桥接器。
- 资源分配: 为每个设备分配内存基址(BAR)、I/O端口、中断向量。
- 配置空间代理: 接收客户端请求,读写PCI配置空间寄存器。
- 中断路由: 管理MSI/MSI-X与传统INTx中断的映射。
- 热插拔事件: 监控PCIe热插拔状态变化并通知注册的驱动。
启动确认: 在目标系统上执行
pidin | grep pci 可确认 pci-server 是否运行。若未运行,驱动将无法通过 libpci 获取设备信息。
3.2.2 pci-tool (命令行工具)
pci-tool 是调试PCI子系统的首选工具,直接与 pci-server 通信,无需编写代码即可查看设备拓扑与配置空间。
| 命令 | 功能 | 典型输出 |
|---|---|---|
pci -v |
列出所有PCI设备及详细信息 | Bus/Dev/Func、Vendor ID、Device ID、Class Code |
pci -b 0 -d 0 -f 0 |
查看指定BDF设备的配置空间 | 256字节配置空间十六进制转储 |
pci -r |
显示BAR地址与大小 | BAR0=0xFE000000 (size=1MB) |
pci -i |
显示中断路由信息 | INTx#、MSI向量、IRQ编号 |
3.2.3 libpci 接口
libpci 是驱动开发者直接使用的客户端库,封装了与 pci-server 的IPC通信细节。核心API如下:
#include <hw/pci.h>
/* 1. 建立与 pci-server 的连接 */
pci_handle_t pci_attach(int flags);
/* 2. 枚举设备:遍历PCI总线树 */
int pci_find_device(pci_handle_t handle, uint16_t vid, uint16_t did, int index, struct pci_dev_info *info);
/* 3. 读取配置空间寄存器 */
int pci_read_config32(pci_handle_t handle, struct pci_dev_info *info, int offset, uint32_t *value);
/* 4. 写入配置空间寄存器 */
int pci_write_config32(pci_handle_t handle, struct pci_dev_info *info, int offset, uint32_t value);
/* 5. 获取BAR物理地址 */
int pci_get_bar_addr(pci_handle_t handle, struct pci_dev_info *info, int barnum, paddr_t *addr, size_t *size);
/* 6. 中断相关:绑定中断处理 */
int pci_attach_interrupt(pci_handle_t handle, struct pci_dev_info *info, int vector, struct sigevent *event, int flags);
/* 7. 断开连接 */
void pci_detach(pci_handle_t handle);
注意: 所有 libpci 调用都是阻塞的,因为底层通过
MsgSend() 向 pci-server 发送同步消息。在中断服务例程(ISR)中禁止调用 libpci 函数,否则会导致死锁。
3.3 PCI设备枚举流程
设备枚举是驱动初始化的第一步,典型流程如下:
- 调用 pci_attach():建立与 pci-server 的IPC通道,返回句柄。
- 循环调用 pci_find_device():按 Vendor ID + Device ID 匹配目标设备,每次调用返回一个匹配项。
- 读取配置空间:通过 pci_read_config32() 获取 Class Code、Revision ID、Subsystem ID 等。
- 获取BAR资源:调用 pci_get_bar_addr() 获取每个BAR的物理地址与大小。
- 注册中断:调用 pci_attach_interrupt() 绑定中断事件。
- 调用 pci_detach():驱动卸载时释放连接。
3.4 关键数据结构:pci_dev_info
pci_find_device() 填充的 struct pci_dev_info 是驱动获取设备信息的核心结构体:
struct pci_dev_info {
uint16_t VendorId; /* 厂商ID */
uint16_t DeviceId; /* 设备ID */
uint16_t SubVendorId; /* 子系统厂商ID */
uint16_t SubDeviceId; /* 子系统设备ID */
uint8_t RevisionId; /* 修订版本 */
uint8_t ClassCode; /* 基类代码 (如 0x02 表示网络控制器) */
uint8_t SubClass; /* 子类代码 */
uint8_t ProgIf; /* 编程接口 */
uint8_t Bus; /* 总线号 */
uint8_t DevFunc; /* 设备号(高5位) + 功能号(低3位) */
uint16_t Command; /* 命令寄存器当前值 */
uint16_t Status; /* 状态寄存器当前值 */
paddr_t BaseAddress[6]; /* BAR0-BAR5 物理地址 */
size_t BaseAddressSize[6]; /* 每个BAR的大小 */
uint8_t Irq; /* 中断线 (INTx) */
uint8_t MinGnt; /* 突发传输周期 */
uint8_t MaxLat; /* 最大延迟 */
};
3.5 典型驱动初始化代码片段
以下代码展示了如何在驱动中完成PCI设备枚举与资源获取:
#include <hw/pci.h>
#include <stdio.h>
int pci_driver_init(pci_handle_t *handle, struct pci_dev_info *info) {
int ret;
int index = 0;
/* 1. 连接 pci-server */
*handle = pci_attach(0);
if (*handle == NULL) {
fprintf(stderr, "pci_attach failed\n");
return -1;
}
/* 2. 枚举设备:匹配 Vendor=0x10EE, Device=0x9034 */
while ((ret = pci_find_device(*handle, 0x10EE, 0x9034, index, info)) == PCI_SUCCESS) {
printf("Found device at BDF %02x:%02x.%x\n",
info->Bus, info->DevFunc >> 3, info->DevFunc & 0x07);
/* 3. 打印BAR0信息 */
printf(" BAR0 addr=0x%llx size=%zu\n",
info->BaseAddress[0], info->BaseAddressSize[0]);
/* 4. 检查中断线 */
if (info->Irq != 0) {
printf(" INTx IRQ=%d\n", info->Irq);
}
index++;
}
if (index == 0) {
fprintf(stderr, "No matching device found\n");
pci_detach(*handle);
return -1;
}
return 0;
}
调试技巧: 在调用 pci_find_device() 之前,先用
pci -v | grep 10EE 确认目标设备是否已被 pci-server 正确枚举。如果设备未出现,检查硬件连接或 PCIe 链路状态。
3.6 架构设计要点总结
| 组件 | 角色 | 驱动开发者关注点 |
|---|---|---|
| pci-server | 系统服务,管理全局PCI资源 | 确保其运行;不直接交互 |
| pci-tool | 调试工具,查询设备信息 | 开发阶段频繁使用 |
| libpci | 客户端API库 | 驱动初始化/卸载时调用 |
| pci_dev_info | 设备信息结构体 | 获取BAR、IRQ、BDF等关键参数 |