4. 配置空间头部(Type 1):桥设备的寄存器归纳总结
说完了 Type 0 的 Endpoint,咱们来看看桥设备。PCIe 里的桥,说白了就是用来扩展总线层级的。我最早调试 switch 的时候就发现,很多人把 Type 1 头和 Type 0 记混,结果配置空间读回来一堆错误。
桥设备的配置空间头部是 64 字节,前 16 字节跟 Type 0 几乎一样,但从偏移 0x10 开始就完全不同了。你想想看,桥要管总线号、地址窗口、还有下游设备的枚举,这些信息全塞在头部里。
4.1 头部的总体布局
我们先看一张直观的图,把 Type 1 头部的寄存器排布理清楚。我个人习惯在做桥初始化之前,对着这张图过一遍,不容易漏。
4.2 必须掌握的寄存器——桥的门面
桥设备最关键的几个寄存器,我按重要性给你捋一遍。
4.2.1 总线号寄存器(偏移 0x18)
| 偏移 | 寄存器 | 位域 | 作用 |
|---|---|---|---|
| 0x18 | Primary Bus Number | [7:0] | 上游总线号,桥收到配置请求时据此判断是否命中本桥 |
| 0x19 | Secondary Bus Number | [7:0] | 下游总线号,桥响应配置请求时使用的目标总线 |
| 0x1A | Subordinate Bus Number | [7:0] | 下级最大总线号,桥用它决定是否转发配置事务 |
| 0x1B | Secondary Latency Timer | [7:0] | 下游总线上的延迟计时器(PCI 兼容,PCIe 通常忽略) |
这三个总线号,是桥的灵魂。我记得有一次搞服务器 PCIe 拓扑,总线上挂了好几级 switch,结果某一块卡死活枚举不出来。最后读 Secondary Bus Number 发现软件写成了和 Primary 相同,导致配置请求被桥自己吞掉,根本没转发下去。
4.2.2 I/O 与 Memory 窗口寄存器
桥必须告诉上层:下游有多少地址空间需要访问。窗口寄存器就干这个事。
| 偏移 | 寄存器对 | 粒度 | 说明 |
|---|---|---|---|
| 0x1C | I/O Base / I/O Limit | 4KB 对齐 | 定义下游 I/O 空间的范围(若支持 I/O) |
| 0x20 | Memory Base / Memory Limit | 1MB 对齐 | 非预取内存窗口,最常用 |
| 0x24 | Prefetchable Memory Base / Limit | 1MB 对齐 | 可预取内存窗口,通常用于视频帧缓冲等高带宽场景 |
| 0x28 | Prefetchable Base Upper 32 / Limit Upper 32 | 64位扩展 | 支持 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) | 0x00 | 0x01 |
| 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 枚举流程中桥的作用
咱们简单串一下桥在枚举里的角色:
- 主机通过 Type 0 配置请求读取根桥的配置空间,发现它是桥(Header Type = 1)。
- 写 Primary Bus Number = 0,Secondary Bus Number = 1,Subordinate Bus Number 先设一个临时值(比如 255)。
- 向 Secondary Bus 发 Type 0 配置请求,枚举下游设备。
- 每发现一个桥,递归下去,分配新总线号,更新上游 Subordinate Bus Number。
- 枚举完一轮后,根据下游设备需要的地址空间,设置桥的 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 桥设备的时候,打开配置空间,定位到对应的偏移,你就能看明白硬件到底在干什么。