4. 设备枚举原理:BDF编码与配置读写机制
PCIe设备枚举是驱动开发的基础环节,其核心在于理解总线号、设备号、功能号(BDF)的编码规则,以及配置空间的读写机制。本章将深入剖析这两部分内容,为后续的BAR空间映射和驱动初始化打下坚实基础。
4.1 BDF编码规则
在PCIe拓扑中,每个端点设备(Endpoint)或桥设备(Bridge)都由一个唯一的BDF(Bus:Device.Function)三元组标识。该标识用于配置空间访问和中断路由。
- 总线号(Bus Number):8位,范围0-255。根复合体(Root Complex)通常位于总线0,通过桥设备扩展出次级总线。
- 设备号(Device Number):5位,范围0-31。挂载在同一总线上的物理设备编号。
- 功能号(Function Number):3位,范围0-7。一个物理设备最多可支持8个独立功能(如多功能网卡)。
关键点: BDF组合形成一个16位的标识符,在配置读写操作中作为目标地址的一部分。例如,BDF = 0x00:1F.0 表示总线0、设备31、功能0。
4.1.1 BDF编码结构图
4.2 配置读写机制
PCIe配置空间是设备初始化与资源分配的核心。系统软件通过两种方式访问配置空间:I/O端口方式(传统方式)和内存映射方式(MMIO方式)。
4.2.1 两种访问方式对比
| 特性 | I/O端口方式 (CF8/CFC) | 内存映射方式 (MMIO) |
|---|---|---|
| 访问端口/地址 | 0xCF8 (地址端口) / 0xCFC (数据端口) | 通过MCFG表映射到物理内存 |
| 地址编码 | BDF + 寄存器偏移 (8位) | 物理地址 = 基址 + (BDF << 12) + 偏移 |
| 性能 | 慢 (需要两次I/O操作) | 快 (直接内存访问) |
| 适用范围 | PCI/PCI-X 传统设备 | PCIe 设备 (必需) |
| 并发性 | 不支持多线程并发访问 | 支持 (每个设备独立地址空间) |
4.2.2 I/O端口方式 (CF8/CFC)
传统PCI配置访问使用两个I/O端口:
- 0xCF8:配置地址端口(CONFIG_ADDRESS),32位寄存器,用于指定目标BDF和寄存器偏移。
- 0xCFC:配置数据端口(CONFIG_DATA),32位寄存器,用于读写配置数据。
访问流程如下:
- 将目标BDF和寄存器偏移编码写入0xCF8端口。
- 从0xCFC端口读取或写入32位数据。
注意: 0xCF8端口的位31必须置1(Enable位),表示配置空间访问有效。否则写入被忽略。
配置地址寄存器格式
I/O端口方式代码示例
// 通过I/O端口读取配置空间DWORD
uint32_t pci_config_read_dword(uint8_t bus, uint8_t dev, uint8_t func, uint8_t offset) {
uint32_t address;
uint32_t data;
// 构造配置地址
address = (1 << 31) | // Enable位
((uint32_t)bus << 16) | // 总线号
((uint32_t)dev << 11) | // 设备号
((uint32_t)func << 8) | // 功能号
(offset & 0xFC); // 寄存器偏移(低2位清零)
// 写入地址端口
outl(0xCF8, address);
// 读取数据端口
data = inl(0xCFC);
return data;
}
// 通过I/O端口写入配置空间DWORD
void pci_config_write_dword(uint8_t bus, uint8_t dev, uint8_t func,
uint8_t offset, uint32_t value) {
uint32_t address;
address = (1 << 31) |
((uint32_t)bus << 16) |
((uint32_t)dev << 11) |
((uint32_t)func << 8) |
(offset & 0xFC);
outl(0xCF8, address);
outl(0xCFC, value);
}
4.2.3 内存映射方式 (MMIO)
PCIe规范要求所有操作系统必须支持内存映射配置访问(ECAM,Enhanced Configuration Access Mechanism)。系统通过ACPI的MCFG表获取ECAM基地址,将配置空间映射到物理内存。
ECAM地址计算公式:
物理地址 = ECAM基地址 + (bus << 20) | (device << 15) | (function << 12) | offset
- 每个设备占用4KB配置空间(4096字节)。
- 每个总线最多支持32个设备 × 8个功能 = 256个功能。
- ECAM基地址通常位于高物理内存区域(如0xE0000000以上)。
ECAM地址映射结构图
MMIO方式代码示例
// 通过MMIO读取配置空间DWORD
uint32_t pci_mmio_read_dword(void *ecam_base, uint8_t bus,
uint8_t dev, uint8_t func, uint8_t offset) {
uint32_t *reg_addr;
// 计算寄存器地址
reg_addr = (uint32_t *)((uint8_t *)ecam_base +
((uint32_t)bus << 20) |
((uint32_t)dev << 15) |
((uint32_t)func << 12) |
(offset & 0xFC));
return *reg_addr;
}
// 通过MMIO写入配置空间DWORD
void pci_mmio_write_dword(void *ecam_base, uint8_t bus,
uint8_t dev, uint8_t func,
uint8_t offset, uint32_t value) {
uint32_t *reg_addr;
reg_addr = (uint32_t *)((uint8_t *)ecam_base +
((uint32_t)bus << 20) |
((uint32_t)dev << 15) |
((uint32_t)func << 12) |
(offset & 0xFC));
*reg_addr = value;
}
4.3 设备枚举流程
系统启动时,BIOS/UEFI或操作系统通过深度优先遍历PCIe树,完成设备枚举。核心步骤如下:
- 扫描总线0:从总线0的设备0开始,读取每个设备的Vendor ID和Device ID。
- 检测桥设备:如果设备是PCI-to-PCI桥(Class Code = 0x060400),则读取其次级总线号和 subordinate 总线号。
- 递归扫描:对每个桥设备的次级总线重复步骤1-2,直到所有总线被扫描完毕。
- 分配BDF:为每个发现的设备分配唯一的BDF。
常见问题: 如果设备Vendor ID读取为0xFFFFFFFF,表示该设备不存在或未连接。这是判断设备是否存在的标准方法。
4.3.1 设备枚举流程图
4.4 本章小结
| 概念 | 关键点 |
|---|---|
| BDF编码 | 总线号(8位) + 设备号(5位) + 功能号(3位) = 16位标识符 |
| I/O端口方式 | 使用0xCF8/0xCFC端口,适用于传统PCI设备 |
| MMIO方式 | 通过ECAM基地址直接内存访问,性能更优 |
| 设备枚举 | 深度优先遍历PCIe树,读取Vendor ID判断设备存在性 |
实战建议: 在Linux内核中,设备枚举由
pci_scan_bus() 和 pci_scan_slot() 函数完成。驱动开发者通常不需要手动枚举,但理解BDF和配置访问机制对调试BAR映射和中断分配至关重要。