3. PCIe配置空间:布局、Capability结构与BAR空间
配置空间,是咱们和PCIe设备打交道的第一步。
说白了,就是设备上电后,系统软件(比如BIOS或者Linux内核)通过读写它,来发现设备、分配资源、配置工作模式。你想想看,如果连配置都读不对,那链路训练跑得再欢实,数据传输也没戏。
我个人习惯,先把前256字节的“头”和“身子”摸清楚,再看后面的Capability链表,最后搞定BAR空间——这是从基础到进阶的路径,我们一步步来。
3.1 配置空间布局(前256字节)
每个PCIe功能(Function)都有独立的配置空间,大小为4KB。但真正让系统识别的,是前256字节的“配置头”。这部分由PCIe规范定义,所有设备都必须实现。
这256字节又分成两部分:前64字节是Header,后192字节是Capability结构。
Header有3种类型:
- Type 0:非桥设备(比如Endpoint、NVMe SSD、GPU)。
- Type 1:PCIe桥(比如Root Port、Switch的Upstream/Downstream Port)。
- Type 2:CardBus桥(PCI时代的老古董,现在基本见不到了)。
来看Type 0 Header的布局,这是最常用的:
| 偏移地址(字节) | 寄存器名称 | 位数 | 个人备注 |
|---|---|---|---|
| 0x00 - 0x01 | Vendor ID | 16 | 厂商编号,比如0x8086是Intel |
| 0x02 - 0x03 | Device ID | 16 | 设备型号,比如0x10EC是某个网卡 |
| 0x04 - 0x05 | Command | 16 | 控制I/O、内存、DMA、中断等使能 |
| 0x06 - 0x07 | Status | 16 | 中断状态、能力列表、错误状态 |
| 0x08 | Revision ID | 8 | 硬件修订版本 |
| 0x09 - 0x0B | Class Code | 24 | 设备类别,如0x0108是NVMe控制器 |
| 0x0C | Cache Line Size | 8 | 一般用默认值,调试时很少动 |
| 0x0D | Latency Timer | 8 | PCI时代的旧物,PCIe基本不用 |
| 0x0E | Header Type | 8 | 0x00表示Type 0,0x01表示Type 1 |
| 0x0F | BIST | 8 | 内建自测试,一般不用 |
| 0x10 - 0x27 | BAR 0 - BAR 5 | 96 | 最关键的资源分配区域,后面重点讲 |
| 0x28 | Cardbus CIS Pointer | 32 | 旧设备用,现忽略 |
| 0x2C - 0x2F | Subsystem Vendor ID / Subsystem ID | 32 | 子系统标识,用于更细粒度的驱动匹配 |
| 0x30 | Expansion ROM Base Address | 32 | 扩展ROM,用于Option ROM启动 |
| 0x34 - 0x3B | Capabilities Pointer | 8+保留 | 指向第一个Capability结构的指针 |
| 0x3C - 0x3F | Interrupt相关 | 32 | 中断线、中断管脚等 |
3.2 Capability结构:链表的魅力
前64字节之后,从0x34处的Capabilities Pointer开始,就是一片新天地。
PCIe用链表(Linked List)来组织这些能力。每个Capability结构都包含:
- 一个字节的Capability ID(标识这是什么能力)
- 一个字节的Next Capability Pointer(指向下一个结构的偏移)
- 后面跟着该能力专有的配置数据
链表遍历的方法是:从Header的0x34处拿到第一个指针,然后跳转过去。读这个结构的第一个字节是ID,第二个字节是Next Pointer。如果Next Pointer为0x00,表示链表结束。就是这么朴实无华。
PCIe必须实现的几个关键Capability:
- PCI Power Management (Cap ID = 0x01):管理设备的电源状态,比如D0、D3热、D3冷等。
- MSI/MSI-X (Cap ID = 0x05 / 0x11):消息信号中断。这东西比传统的INTx中断高效多了。尤其MSI-X,每个中断向量可以独立配置,大型NVMe和网卡都在用。
- PCI Express Capability (Cap ID = 0x10):PCIe核心能力。这里面包含了设备/端口的类型、版本(Gen1/Gen2/Gen3等)、链路控制、链路状态、设备控制和状态等。
- Advanced Error Reporting (AER, Cap ID = 0x01但由Extended Capability实现):高级错误报告。我强烈建议调试时打开AER,它能告诉你到底是“链路不可恢复错误”还是“接收端溢出”。
举个例子,读一个NVMe SSD的Capability链表。先读到Power Management,然后Next指到PCI Express Capability。再往下可能是MSI-X。就这样一级一级走完。
3.3 BAR空间:设备的地图
BAR(Base Address Register),说白了就是设备告诉系统:“我想让你把我的哪些内部寄存器、内存、I/O端口映射到你的物理地址空间里来。”
系统软件(比如BIOS、UEFI、Linux内核)读取BAR,然后分配一段不冲突的物理地址,再写回BAR。设备就知道:“哦,我的寄存器在A地址,我的数据缓冲区在B地址。”
Type 0 Header有6个BAR,从偏移0x10到0x27,每个BAR 4字节(32位)或8字节(64位)。
但注意:64位BAR是成对使用的。比如BAR0和BAR1组成一个64位地址空间,那么BAR1寄存器就不能单独用了,它的空间会被占掉。所以6个BAR实际支持的地址空间数量取决于使用了几对64位BAR。
如何判断BAR的类型和大小?
- 读BAR的原始值。
- 往BAR写全1(0xFFFFFFFF),再读回来。
- 读回来的值低4位表示类型:
0b0000 -> 32位内存映射空间
0b0010 -> 64位内存映射空间
0b0001 -> I/O空间(现在很少用了,PCIe推荐用内存映射) - 高位里连续为0的位数,就是该空间的大小。比如读回来是0xFFFF0000,说明低16位用了,空间大小为64KB。
下面这张SVG图,可以把配置空间的逻辑结构串起来:
这张图里,我刻意把BAR空间和Capability链表画在平行线上。意思很清楚:系统软件既要解析BAR分配地址,又要遍历Capability链表配置能力。两个任务同时进行,互不干扰,但都基于配置头。
实战:怎么看一个设备的配置空间?
在Linux下,用 lspci -s 01:00.0 -xxx 可以导出完整的配置空间。但调试时我更推荐用 setpci 命令逐字节读,或者写一个小的C程序用 sysfs 的 /sys/bus/pci/devices/ 来操作。
比如,我想看NVMe SSD的BAR0是什么:
// 伪代码示意
int fd = open("/sys/bus/pci/devices/0000:01:00.0/config", O_RDWR);
uint32_t bar0;
pread(fd, &bar0, 4, 0x10); // 读偏移0x10处的4字节
printf("BAR0 = 0x%08X\n", bar0);
close(fd);
读回来如果是0xFFFF0000,说明设备要求64KB空间,且是32位内存映射。如果BAR0低4位是0b0010,那就得把BAR0和BAR1合起来读,组成一个64位地址。