4. 配置空间头部(Type 1):桥设备的寄存器归纳总结

说完了 Type 0 的 Endpoint,咱们来看看桥设备。PCIe 里的桥,说白了就是用来扩展总线层级的。我最早调试 switch 的时候就发现,很多人把 Type 1 头和 Type 0 记混,结果配置空间读回来一堆错误。

桥设备的配置空间头部是 64 字节,前 16 字节跟 Type 0 几乎一样,但从偏移 0x10 开始就完全不同了。你想想看,桥要管总线号、地址窗口、还有下游设备的枚举,这些信息全塞在头部里。

4.1 头部的总体布局

我们先看一张直观的图,把 Type 1 头部的寄存器排布理清楚。我个人习惯在做桥初始化之前,对着这张图过一遍,不容易漏。

PCIe Type 1 配置空间头部(64字节)布局 0x00 Device ID Vendor ID 0x04 Status Command ... 0x08 Class Code Revision ID 0x0C BIST Header Type Latency Timer Cache Line Size 0x10 Base Address Register 0 (BAR0) — 32位 0x14 0x14 Base Address Register 1 (BAR1) — 32位 0x18 0x18 Subordinate Bus Number Secondary Bus Number Primary Bus Number 0x1C 0x1C Secondary Latency Timer I/O Limit I/O Base ... 0x20 0x20 Memory Base Memory Limit 0x24 Prefetchable Memory Base Prefetchable Mem Limit 0x28 Prefetchable Base Upper 32 bits 0x2C Prefetchable Limit Upper 32 bits 0x30 Reserved 0x34 Expansion ROM Base Address (可选) 0x38 Reserved Interrupt Pin Interrupt Line Bridge Control 0x3C~0x3F 说明:各寄存器宽度为 8/16/32 位不等,图中以 32 位对齐显示。偏移地址均为十六进制。
小提示:我建议你在调试时,先把这张图打印出来或者贴到屏幕边。曾经有一个项目,我在写 switch 驱动时,就是靠这张图快速定位了 Secondary Bus Number 配置错误导致的下游设备不可见问题。

4.2 必须掌握的寄存器——桥的门面

桥设备最关键的几个寄存器,我按重要性给你捋一遍。

4.2.1 总线号寄存器(偏移 0x18)

偏移寄存器位域作用
0x18Primary Bus Number[7:0]上游总线号,桥收到配置请求时据此判断是否命中本桥
0x19Secondary Bus Number[7:0]下游总线号,桥响应配置请求时使用的目标总线
0x1ASubordinate Bus Number[7:0]下级最大总线号,桥用它决定是否转发配置事务
0x1BSecondary Latency Timer[7:0]下游总线上的延迟计时器(PCI 兼容,PCIe 通常忽略)

这三个总线号,是桥的灵魂。我记得有一次搞服务器 PCIe 拓扑,总线上挂了好几级 switch,结果某一块卡死活枚举不出来。最后读 Secondary Bus Number 发现软件写成了和 Primary 相同,导致配置请求被桥自己吞掉,根本没转发下去。

注意:Primary Bus Number 由固件或操作系统在枚举时写入,一旦设定,桥就会用它进行地址过滤。软件在写之前必须确保下游总线还没被分配。

4.2.2 I/O 与 Memory 窗口寄存器

桥必须告诉上层:下游有多少地址空间需要访问。窗口寄存器就干这个事。

偏移寄存器对粒度说明
0x1CI/O Base / I/O Limit4KB 对齐定义下游 I/O 空间的范围(若支持 I/O)
0x20Memory Base / Memory Limit1MB 对齐非预取内存窗口,最常用
0x24Prefetchable Memory Base / Limit1MB 对齐可预取内存窗口,通常用于视频帧缓冲等高带宽场景
0x28Prefetchable Base Upper 32 / Limit Upper 3264位扩展支持 64 位地址时使用

看,每个窗口都有 Base 和 Limit。桥在收到上游的内存请求时,会拿地址跟这些窗口比一比。如果落在窗口内,就转发到下游;否则忽略——这是硬件做的,不需要软件干预。我个人习惯是在固件里先把这些窗口设成全 0,等枚举真正分配完地址空间再写进去,避免过早开放导致地址冲突。

4.2.3 Bridge Control 寄存器(偏移 0x3C)

这个 16 位寄存器是桥的行为总开关。我重点说几个实用位:

  • Bit 0 – Secondary Bus Reset:置 1 就会把下游总线拉复位。我用来在热插拔后复位异常设备。
  • Bit 1 – Fast Back-to-Back Enable:PCI 时代的文物,PCIe 下不要管。
  • Bit 4 – Parity Error Response Enable:控制下游的奇偶错误是否上报。
  • Bit 6 – SERR# Enable:允许桥上报系统严重错误。
  • Bit 7 – Discard Timer SERR# Enable:事务被丢弃时是否触发 SERR。
  • Bits 10,11 – VGA Enable / ISA Enable:用于 legacy 设备兼容,不是 PCIe 主角。

经验之谈:我在调试 PCIe switch 驱动时,发现热插拔后下游设备功能异常,但总线枚举一切正常。最后发现是 Secondary Bus Reset 在复位完成后没有及时清掉,桥一直处于复位状态。所以操作复位位后一定要记得清回 0,并等待复位完成(通常要求至少 1ms)。

4.3 与 Type 0 不同的地方——千万别搞混

很多人问我,能不能用读 Type 0 的方式读桥?不行。我给你列个表,你一眼就能看出差别。

特征Type 0 (Endpoint)Type 1 (Bridge)
Header Type (偏移 0x0E)0x000x01
BAR 数量最多 6 个 (0x10~0x24)最多 2 个 (0x10~0x14)
总线号寄存器无有 (0x18~0x1B)
窗口寄存器无有 (0x1C~0x2F)
Expansion ROM偏移 0x30偏移 0x34
Bridge Control无偏移 0x3C 的低16位

你看,最明显的区别就是偏移 0x0E 的 Header Type。枚举软件就是靠这个位区分 Endpoint 还是桥的。我曾经见过硬件人员拿错配置空间版本,把 Type 1 当 Type 0 初始化,结果 BAR 读到后面全是 0,吓一跳——其实是因为 Type 1 的 BAR 只到偏移 0x14。

4.4 枚举流程中桥的作用

咱们简单串一下桥在枚举里的角色:

  1. 主机通过 Type 0 配置请求读取根桥的配置空间,发现它是桥(Header Type = 1)。
  2. 写 Primary Bus Number = 0,Secondary Bus Number = 1,Subordinate Bus Number 先设一个临时值(比如 255)。
  3. 向 Secondary Bus 发 Type 0 配置请求,枚举下游设备。
  4. 每发现一个桥,递归下去,分配新总线号,更新上游 Subordinate Bus Number。
  5. 枚举完一轮后,根据下游设备需要的地址空间,设置桥的 I/O 和 Memory 窗口寄存器。

这个过程有点像地铁线路图。每个桥是一个换乘站,你要告诉它“哪些站归你管”。我曾经在一个 8 级深度的拓扑里,排查为什么第三级桥下的设备能访问,但第四级桥下的设备发请求超时。最后发现是第三级桥的 Subordinate Bus Number 设得太小,没有把第四级包进去。硬件真的不撒谎——设错了,它就不转发。

调试小技巧:可以用lspci -vvv查看 Linux 下每个桥的总线号窗口,再和硬件逻辑分析仪抓到的配置请求对比。我常用的三板斧:读 Bridge Control 确认复位位没置、读 Secondary Bus Number 确认不等于 Primary、读 Memory Base/Limit 确认窗口不为零。

4.5 总结一下

Type 1 配置空间头部,说白了就是桥的“地址路由表”。它告诉上游:我管哪个总线号,我的下游有哪些内存和 I/O 范围。你只要掌握总线号寄存器、窗口寄存器、Bridge Control 这三个核心部件,桥的配置就不会出大毛病。

嗯,咱们今天不急着往下讲。你先消化掉这张寄存器布局图,下次 debug 桥设备的时候,打开配置空间,定位到对应的偏移,你就能看明白硬件到底在干什么。