第2章 PCIe配置空间概述:用途、结构、Type0 vs Type1

好,我们正式开始进入PCIe配置空间的世界。你可能听过这么一句话:「PCIe的灵魂在配置空间。」我个人觉得这话一点不夸张。

你有没有想过,CPU怎么知道插在PCIe槽上的设备是什么?是个显卡还是NVMe SSD?该分配多少内存地址给它?其实,这些信息都藏在配置空间里——一块仅有256字节(或4KB,取决于是否支持Extended Configuration Space)的只读/可写寄存器区域。

今天这一章,我们就把配置空间的用途、内部布局,以及最重要的Type0和Type1搞明白。

2.1 配置空间的用途

为什么需要配置空间?说白了,就是让系统软件(BIOS/UEFI、OS内核)能枚举、识别、初始化PCIe设备。没有它,CPU连设备是啥都不知道。

  • 设备识别:读取Vendor ID、Device ID就知道是哪家厂商的什么型号。
  • 资源分配:系统通过配置空间里的Base Address Register(BAR)来分配MMIO内存地址。
  • 中断配置:Interrupt Pin / Line设定中断路由。
  • 错误报告:Advanced Error Reporting(AER)全靠扩展配置空间里的寄存器。
  • 链路管理:Link Control、Link Status等寄存器让你知道链路宽度、速率。

核心观点:配置空间就是PCIe设备的「身份证+资源清单+控制面板」。

我记得在某个项目中,新来的同事直接给BAR0写了一个错误的地址,结果系统直接挂死。后来大家才意识到——配置空间里的任何一个bit,都不能随便乱动。

2.2 配置空间的结构

标准配置空间的大小是256字节,0x00~0xFF。在PCI Express中,又扩展到了4KB(0x000~0xFFF),多出来的部分叫Extended Configuration Space。不过基础的0~0xFF部分兼容PCI。

我们主要看前64字节,也就是Header部分。这里分成两类:Type 0 和 Type 1。你只要看Header Type寄存器的值就知道——0x0是Type0,0x1是Type1。

偏移 寄存器名 说明
0x00Vendor ID厂商ID(如0x8086为Intel)
0x02Device ID设备型号
0x04Command控制I/O、Memory、Bus Master等使能
0x06Status状态位,如Interrupt Status
0x08Revision ID硅版本
0x09Class Code类型(如0x030000为显示控制器)
0x0CHeader Type0x0: Type0, 0x1: Type1
0x0DBIST内建自测试(一般不常用)

嗯,这里要注意:Header Type寄存器中的bit7(0x80)表示该设备是多功能设备。我经常看到有人读出来是0x80就以为Type0,其实是0x00+bit7==0x80,仍然是Type0。

2.3 Type0 vs Type1——核心区别

这一节很重要。你想想看:一个普通的PCIe设备(比如网卡、显卡)和一个PCIe桥(用于扩展总线)的配置空间,能一样吗?不能。Type0给Endpoint(终点设备)用,Type1给Bridge(桥)用。

我当初刚学的时候,总是混淆这两个结构。直到自己写了一个PCIe设备驱动,发现Type1里有Primary Bus Number、Secondary Bus Number、Subordinate Bus Number这些与总线枚举相关的字段,而Type0里根本没有,这才彻底搞清楚了。

2.3.1 结构差异一览

特性 Type0(Endpoint) Type1(Bridge)
Header长度64字节(0x00~0x3F)64字节(0x00~0x3F)
Base Address Registers最多6个BAR(0x10~0x24)最多2个BAR(0x10~0x14)
Bus Number寄存器无Primary/Secondary/Subordinate Bus Number
I/O Base/Limit无I/O Base & I/O Limit(16位或32位)
Memory Base/Limit无Memory Base & Memory Limit
Prefetchable Base/Limit无Prefetchable Base & Limit
Expansion ROM BAR可选(0x30)可选(0x38)
Interrupt Pin & Line有(0x3C~0x3D)有(0x3C~0x3D)
Bridge Control无0x3E~0x3F

如果你看到这还在疑惑,别急。我习惯把Type1比作「路口转接板」——它把上游(Primary Bus)的请求转发到下游(Secondary Bus),因此需要记录下游的总线编号和地址范围。而Endpoint是末端设备,只需要知道自己的资源在哪里。

2.3.2 枚举中的角色

系统软件是如何通过Type1来遍历整棵PCIe树的?

  • Root Complex内部有一个虚拟的Bridge(Type1),它的SecBus=1, SubBus=最多
  • 扫描Secondary Bus上的设备:如果读到的是Type0,那就是Endpoint;如果读到Type1,那说明这个设备又是一个桥,需要递归进入。
  • 每个Type1里的Secondary Bus Number和Subordinate Bus Number定义了桥所管辖的总线范围。

避坑:我曾经见过一个桥芯片,固件写错了Subordinate Bus Number,导致Linux内核在一级枚举时检测不到后面的设备。后来用PCIe分析仪抓包才发现——桥的配置空间里Subordinate写入了一个比Secondary还小的值。嗯,这种Bug查起来特别耗时间。

2.4 配置空间访问方式

配置空间不是直接读写内存,需要通过特殊的访问机制。在x86上,传统方法就是I/O端口0xCF8/0xCFC(PCI配置空间访问)。PCIe引入了MMIO方式,通过Memory Mapped Configuration Space(MMCFG),把4KB配置空间映射到内存地址段。通常Linux里会放在物理地址0xE0000000附近。

// 经典PCI配置访问示例(x86 I/O端口)
uint32_t pci_read_config_dword(uint8_t bus, uint8_t dev, uint8_t func, uint8_t offset) {
    outl(0x80000000 | (bus << 16) | (dev << 11) | (func << 8) | (offset & 0xFC), 0xCF8);
    return inl(0xCFC);
}

但PCIe推荐使用MMCFG,因为I/O端口慢且麻烦。比如在Linux驱动里,你直接使用 pci_read_config_dword(pdev, offset) 就自动适配了。

2.5 本章知识体系图

PCIe配置空间Header结构对比(Type0 vs Type1) PCIe配置空间Header结构(T0 vs T1) Type0 (Endpoint) Type1 (Bridge) Vendor ID / Device ID / Command / Status / Rev ID / Class Code / HdrType/BIST 同左 BAR0 ~ BAR5 (6个) 每个BAR 32位,可组合成64位 BAR0 ~ BAR1 (2个) Primary Bus Number (0x18) Secondary Bus Number (0x19) Subordinate Bus Number (0x1A) I/O Base/Limit, Memory Base/Limit (0x1C~0x24) Expansion ROM BAR (0x30) Expansion ROM BAR (0x38) Interrupt Line / Pin (0x3C~0x3D) Interrupt Line / Pin (0x3C~0x3D) 预留 / 可选寄存器 Bridge Control (0x3E~0x3F) 公共字段布局相似 Bridge负责 总线编号/地址转发

⚠️ 常见错误:很多初学者以为Type0和Type1的配置空间完全一样,于是读取Type1的BAR2~5(其实不存在),读出来的永远是0xFFFFFFFF。我在一个项目中就遇到过——自己写工具读某个桥的BAR3,结果总是全1,还以为坏了,查了半天才发现是Header Type搞错了。

2.6 小结与建议

这一章我们从三个角度拆解了PCIe配置空间:用途(枚举、资源分配)、结构(前64字节Header)、Type0 vs Type1的核心差异。

我个人建议你,在正式动手操作之前,先通过lspci -xxx或lspci -vvv把实际机器上的设备配置空间导出看看,对照文档印证一遍。只有看熟了结构图,后面写代码才不会心虚。