PCIe配置空间与BAR归纳总结
配置空间,说白了就是每个PCIe设备自带的一个“身份证”。里面记录了设备的厂家、型号、需要的资源大小,以及当前分配给它的地址。没有它,CPU根本不知道设备长什么样,更别提DMA了。
我个人习惯把配置空间想象成一个“通信簿”。系统启动时,BIOS或操作系统会挨个翻每个设备的通信簿,然后根据需求分配地址。BAR寄存器就是这个簿子里最关键的一页——它告诉系统:“我需要这么大一块内存空间,你得给我安排上。”
配置空间的结构
PCIe设备必须有256字节的标准配置空间,这是从PCI时代继承下来的。除此之外,还有个4096字节的扩展配置空间,里面装了PCIe特有的功能,比如AER、VC、SR-IOV等。但咱们做DMA驱动,先吃透前面256字节就够了。
这256字节又分两部分:前64字节是Header,后面192字节是Capabilities链表。Header根据设备类型有两种模板:
- Type 0 —— 用在Endpoint(端点设备)上,比如我们常见的网卡、SSD、DMA加速卡。
- Type 1 —— 用在Bridge(桥设备)上,比如PCIe switch的上行口和下接口。
下面这个表格把两种Header的布局差异列出来了,我项目里经常对照着看,避免搞混:
| 偏移 | Type 0 关键字段 | Type 1 关键字段 |
|---|---|---|
| 0x00 | Vendor ID, Device ID | 同左 |
| 0x04 | Command, Status | 同左 |
| 0x08 | Revision ID, Class Code | 同左 |
| 0x0C | Cache Line, Latency Timer, Header Type, BIST | 同左 |
| 0x10~0x27 | BAR0~BAR5 (每个4字节) | BAR0~BAR1,其余为 Bus Numbers, Subordinate, I/O Limit等 |
| 0x28 | Cardbus CIS Pointer | Secondary Status, I/O和Memory限值 |
| 0x2C~0x3F | Subsystem Vendor/Device ID, Expansion ROM Base, Capability Pointer | Expansion ROM Base, Bridge Control, Capability Pointer |
BAR —— 基地址寄存器
BAR是驱动开发里打交道最多的寄存器之一。每个BAR的宽度是32位,但如果设备需要64位地址空间,可以占两个连续的BAR(比如BAR0和BAR1组合成一个64位寄存器)。
BAR的位定义如下:
- 位0 —— 空间类型:0表示Memory空间,1表示I/O空间(现代PCIe设备基本不用I/O了)。
- 位2:1 —— 对于Memory BAR:00表示32位地址空间,10表示64位地址空间。
- 位3 —— 可预取标志(Prefetchable)。如果置1,表示这个空间可以缓存,读取不会产生副作用。
- 高位 —— 基地址(由系统固件写入)。
怎么知道一个BAR需要多大空间?嗯,这里有个经典套路:先全写1到这个BAR,再读回来,低位的0就代表了需要的大小。举个例子:如果你写0xFFFFFFFF到BAR0,读回来是0xFFFF0000,那么低16位都是0,说明需要2^16 = 64KB空间。注意读回来的值要取反再对齐。
/* 伪代码:探测BAR空间大小 */
uint32_t bar_orig = pci_read_config(dev, offset);
pci_write_config(dev, offset, 0xFFFFFFFF);
uint32_t bar_size_raw = pci_read_config(dev, offset);
pci_write_config(dev, offset, bar_orig); // 恢复原值
uint32_t size = ~(bar_size_raw & 0xFFFFFFF0) + 1; // 对齐到2的幂
/* 注意:如果bit0为0(Memory BAR),需要屏蔽低4位;如果为1(I/O BAR),屏蔽低2位 */
地址映射机制
当系统枚举完成后,BAR里存放的就是物理地址了。但CPU要访问这些地址,还需要通过MMU建立页表映射(对于用户态驱动)或直接使用高版本内核的ioremap(内核态驱动)。
映射完成后,驱动就可以像访问普通内存一样读写设备的内存空间了。不过要注意,PCIe事务层会把这种访问转成Memory Read/Write TLP,设备收到后根据地址命中哪个BAR来响应。
下面这张图总结了配置空间的结构以及BAR在整个枚举流程中的位置:
常见陷阱与避坑指南
- BAR对齐要求: 写入BAR的基地址必须与BAR本身的大小对齐。比如BAR需要1MB空间,那么基地址的低20位必须为0。我遇到过硬件工程师设计的BAR大小不是2的幂次方,导致系统无法分配,后来改成了2的幂才解决。
- 64位BAR的读写顺序: 如果设备使用64位BAR(比如BAR0+BAR1),必须先写低位BAR再写高位BAR,否则设备行为可能是未定义的。有些设备会直接忽略高位写入。
- 可预取与不可预取: 可预取BAR支持合并写和推测读,性能更好。但若设备寄存器有副作用(比如读自清零状态寄存器),则不能设置为可预取。
总结要点
- 配置空间是PCIe设备的软接口,驱动通过它了解设备需求并建立地址映射。
- BAR是配置空间里最核心的寄存器,它告诉系统设备需要什么类型的地址空间以及多大。
- 探测BAR大小的“写1再读”方法是标准做法,但必须在设备未启用时执行。
- 地址映射完成后,驱动通过MMIO访问BAR对应的物理地址,本质上是发起PCIe Memory TLP。
- 实战中要特别注意BAR的对齐、64位处理、以及可预取属性的正确使用。
嗯,这些知识看起来多,实际上你亲手调试一次PCIe设备,把BAR的值读出来、映射、然后写个寄存器看看能否控制LED,马上就全通了。下一块我们会实际动手,写最简单的驱动读取配置空间,到时候你就明白了。