PCIe配置空间(Type1):Bridge的“户口本”与“地址地图”
好,我们接着聊PCIe配置空间。上一章我们把Type0(Endpoint)的配置空间翻了个底朝天。这一章,咱们把目光转向Bridge,也就是Type1 Header。
Bridge在PCIe里到底扮演什么角色?说白了,就是“中间人”。它连接着上一级总线(Primary Bus)和下一级总线(Secondary Bus),负责把事务在两个总线之间搬来搬去。嗯,这听起来有点像网络里的路由器。
那它的“户口本”——Type1配置空间,自然就得记录两边的地址、编号、甚至一些状态信息。我个人习惯把Type1 Headers看成是“总线间的桥梁说明书”。你想想看,如果没有它,CPU怎么知道它发出去的数据该往哪条路上转发?
Bridge配置空间的独特之处
Type0和Type1的前16个字节(0x00-0x0F)长得几乎一模一样——Vendor ID、Device ID、Command、Status等等。真正的差异从0x10开始。
- Base Address Registers (BARs): Type1至少有2个BAR(可以更多),但用途和Type0完全不同。Bridge的BAR不是给它自己用的,而是用于“穿透”访问的。比如,想让CPU看到一个挂在Bridge下面的Endpoint的某个BAR空间,就得在Bridge的BAR里做文章。
- Bus Numbers: 这是Type1最核心的字段,用来标识它管的“地盘”。
- Memory/IO Base & Limit: 定义它“管辖”的地址范围,好比一张过滤表。
- Bridge Control & Secondary Status: 用来控制和管理它这一侧的总线。
我曾经在一个项目里,因为没配好Bridge的BAR,导致Endpoint无法被上层软件访问。那次调试花了我一整天,最后发现就是Bridge的BAR空间被遗漏了。所以,别小看这玩意儿。
Primary / Secondary / Subordinate Bus Number:桥梁的“三围”
这是Bridge最关键的三个寄存器。它们定义了这个Bridge在PCIe总线拓扑中的“物理位置”。
| 字段 | 偏移量 | 描述 |
|---|---|---|
| Primary Bus Number | 0x18 | Bridge本身所连接的上游总线(Higher-Level Bus)编号。 |
| Secondary Bus Number | 0x19 | Bridge下游侧的总线编号(Lower-Level Bus)。 |
| Subordinate Bus Number | 0x1A | 这个Bridge下游所能到达的最远(最大)总线编号。 |
举个例子:
假设Root Complex(RC)的总线号是0。第一个Switch(Upstream Port)的Primary Bus是0,Secondary Bus是1。那这个Bridge的配置就是:
- Primary Bus Number = 0
- Secondary Bus Number = 1
- Subordinate Bus Number = ?
假如这个Switch下面级联了另一个Switch,那个Switch的Secondary Bus号是2。那么,第一个Bridge的Subordinate Bus Number就应该设为2。因为它下游能到达的最远总线号是2。
我个人的经验是,如果你在做模拟器或者FPGA原型,这些数字一定要算清楚。我曾经见过一个设计,Subordinate Bus Number写错了,导致所有穿越这个Bridge的事务都发不出去。
Memory Base / Limit 与 IO Base / Limit:Bridge的“地址过滤网”
光有Bus Number还不够,总线上的设备怎么知道哪些地址范围是应该被Bridge转发出去的?这就靠Memory Base/Limit和IO Base/Limit了。
这些寄存器定义了一段连续的地址空间。当Bridge看到下游发来的事务,它会检查目标地址是否落在自己Primary侧的Memory/IO Base和Limit之间?如果是,那就转发(Pass Through)。如果不是,嗯,就不管。
逻辑判断很简单:
- Memory空间: 使用 Memory Base 和 Memory Limit 寄存器。它们都是32位寄存器,但只使用高16位(位[31:16])来比较。低16位对齐到1MB边界。所以,实际上定义的是一个以1MB为单位的地址窗口。
- IO空间: 使用 IO Base 和 IO Limit 寄存器。同样只使用高8位(位[15:8]),对齐到4KB边界。
如果一个事务的地址是0x80000000,而Memory Base是0x8000,Memory Limit是0x8FFF(实际比较的是地址的高16位),那么这个地址就会命中这个窗口。桥梁就会放行。
预取与非预取:一个微妙的区别
在Memory Base/Limit字段中,还有一个位叫“Prefetchable”(位[3])。这个位标记了这段Memory地址空间是否支持“预取”读。说白了,就是当CPU读这段地址时,可以先不等待数据返回,预期后面还会有连续的读请求。一般用在有比较大缓冲区的设备上,比如显卡的Frame Buffer。
非预取(Non-Prefetchable)就是常规的读,必须等到数据回来,CPU才能继续。通常用于配置空间、控制寄存器等。
配置步骤(以MCU视角):
// 假设已经枚举出这个Bridge
// 我们要分配它下游的Memory空间
// 先配置Memory Base/Limit
uint32_t memory_base = 0x8000; // 对应地址 0x80000000
uint32_t memory_limit = 0x8FFF; // 对应地址 0x8FFF0000
// 写入Bridge的配置空间
write_config_dword(bus, dev, func, 0x20, memory_base << 16 | memory_limit);
为什么需要Primary/Secondary/Subordinate Bus Number + Memory/IO Base/Limit?
这两个机制配合起来,才能构成完整的转发决策。
- Bus Number 决定了“你是谁”(拓扑位置)。
- Base/Limit 决定了“你要去哪”(地址过滤)。
当一个事务从Primary侧过来,Bridge会先看目的地Bus Number。如果这个Bus Number在Secondary Bus和Subordinate Bus之间,说明目的地在这个Bridge的管辖范围内。然后它再检查地址,如果地址也在Base/Limit窗口内,就转发。如果不是,就忽略。这就像一个高效的路由表。
我过去在设计一个复杂的PCIe Switch时,经常需要手动绘制一张地址分配表,把每个Bridge的Base/Limit标注好,才能确保所有设备都能相互通信。一旦疏忽,就可能出现“死胡同”或者“回环”。
Type 1 配置空间与 Type 0 的关键区别总结
| 特性 | Type 0 (Endpoint) | Type 1 (Bridge/Switch) |
|---|---|---|
| BARs数量 | 最多6个(功能寄存器) | 至少2个(用于穿透访问) |
| Bus Numbers | 无(自己不参与总线管理) | Primary/Secondary/Subordinate |
| Memory/IO Base & Limit | 无 | 有(定义地址过滤窗口) |
| Capabilities Pointer | 有(0x34) | 有(0x34) |
| Expansion ROM Base Addr | 可选(0x30) | 可选(0x38) |
所以,当你看到一个PCIe设备时,通过读取Header Type字段(0x0C寄存器,位[7:0]),就能马上区分它是Type0(0x00)还是Type1(0x01)。有了这些基础知识,你就能理解为什么在拓扑中发现一个Switch(其实就是多个Bridge的集合体),它的配置空间里塞满了这些东西。