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的上行口和下接口。
关键区别: Type 0最多有6个BAR(BAR0~BAR5),而Type 1只有2个BAR(BAR0~BAR1),并且多了Primary/Secondary/Subordinate Bus Number等用于路由的字段。

下面这个表格把两种Header的布局差异列出来了,我项目里经常对照着看,避免搞混:

偏移Type 0 关键字段Type 1 关键字段
0x00Vendor ID, Device ID同左
0x04Command, Status同左
0x08Revision ID, Class Code同左
0x0CCache Line, Latency Timer, Header Type, BIST同左
0x10~0x27BAR0~BAR5 (每个4字节)BAR0~BAR1,其余为 Bus Numbers, Subordinate, I/O Limit等
0x28Cardbus CIS PointerSecondary Status, I/O和Memory限值
0x2C~0x3FSubsystem Vendor/Device ID, Expansion ROM Base, Capability PointerExpansion ROM Base, Bridge Control, Capability Pointer
小技巧: 调试时可以读Header Type(偏移0x0E的位6)判断设备是Type 0还是Type 1。值为0表示Type 0,值为1表示Type 1。

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位 */
注意: 这种探测操作必须在设备未启用(Command寄存器的I/O或Memory Enable位为0)时进行,否则会导致总线错误。我曾经在调试一个FPGA加速卡时,忘记关enable就读BAR,结果系统直接挂住,不得不重启。

地址映射机制

当系统枚举完成后,BAR里存放的就是物理地址了。但CPU要访问这些地址,还需要通过MMU建立页表映射(对于用户态驱动)或直接使用高版本内核的ioremap(内核态驱动)。

映射完成后,驱动就可以像访问普通内存一样读写设备的内存空间了。不过要注意,PCIe事务层会把这种访问转成Memory Read/Write TLP,设备收到后根据地址命中哪个BAR来响应。

下面这张图总结了配置空间的结构以及BAR在整个枚举流程中的位置:

PCIe配置空间与BAR工作流程 PCIe配置空间 Header (64字节) Type0: BAR0~BAR5 Capabilities (192字节) PME, MSI, MSI-X... 扩展配置空间 (4KB) AER, VC, FLR... 系统枚举 读取BAR初始值 探测BAR大小 分配物理地址 写入BAR(程序结束) 最终结果 CPU通过MMIO访问BAR映射的地址 设备接收Memory TLP并响应 驱动开发关注点 • 在Command寄存器的Memory Enable置1前完成BAR探测 • 64位BAR需要同时处理相邻两个寄存器,高位BAR可能为0 • I/O BAR已基本废弃,大部分设备只用Memory BAR

常见陷阱与避坑指南

  • BAR对齐要求: 写入BAR的基地址必须与BAR本身的大小对齐。比如BAR需要1MB空间,那么基地址的低20位必须为0。我遇到过硬件工程师设计的BAR大小不是2的幂次方,导致系统无法分配,后来改成了2的幂才解决。
  • 64位BAR的读写顺序: 如果设备使用64位BAR(比如BAR0+BAR1),必须先写低位BAR再写高位BAR,否则设备行为可能是未定义的。有些设备会直接忽略高位写入。
  • 可预取与不可预取: 可预取BAR支持合并写和推测读,性能更好。但若设备寄存器有副作用(比如读自清零状态寄存器),则不能设置为可预取。
我曾经踩过的坑: 有一款DMA控制器,BAR0声明为64位可预取内存空间。我在驱动里用readl/writel正常读写,后来为了优化用了memcpy,结果某些寄存器读自清零导致数据丢失。从那时起,我规定:所有控制状态寄存器相关的BAR必须标记为不可预取,数据缓冲区相关的BAR才用可预取。

总结要点

  1. 配置空间是PCIe设备的软接口,驱动通过它了解设备需求并建立地址映射。
  2. BAR是配置空间里最核心的寄存器,它告诉系统设备需要什么类型的地址空间以及多大。
  3. 探测BAR大小的“写1再读”方法是标准做法,但必须在设备未启用时执行。
  4. 地址映射完成后,驱动通过MMIO访问BAR对应的物理地址,本质上是发起PCIe Memory TLP。
  5. 实战中要特别注意BAR的对齐、64位处理、以及可预取属性的正确使用。

嗯,这些知识看起来多,实际上你亲手调试一次PCIe设备,把BAR的值读出来、映射、然后写个寄存器看看能否控制LED,马上就全通了。下一块我们会实际动手,写最简单的驱动读取配置空间,到时候你就明白了。