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。

配置Bus Number时,有个规矩:Secondary Bus Number必须大于Primary Bus Number,Subordinate Bus Number必须大于等于Secondary Bus Number。软件(BIOS/OS)在枚举时会自动填上这些值。

我个人的经验是,如果你在做模拟器或者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位),那么这个地址就会命中这个窗口。桥梁就会放行。

注意:对于64位地址的Memory空间,需要使用Memory Base Upper 32 和 Memory Limit Upper 32 这两个扩展寄存器。它们位于0x24和0x26。是否支持64位地址由Capability列表中的某个特性决定。千万别搞混了。

预取与非预取:一个微妙的区别

在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);

其实很多BIOS/OS里的PCI枚举代码,就是在按照这个思路,遍历所有Bridge,一层层地分配这些Base/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的集合体),它的配置空间里塞满了这些东西。

Type 1 Bridge 配置空间核心逻辑 Root Complex (RC) Bus 0 Bridge (Type 1) Primary Bus = 0 Secondary Bus = 1 Downstream Bus 1 Bridge (Type 1) Primary Bus = 1 Secondary Bus = 2 Downstream Bus 2 配置逻辑 1. Bridge 1 的 Subordinate Bus = 2 2. Bridge 2 的 Subordinate Bus = 2 关键字段 (Bridge 1) Primary Bus Number = 0 Secondary Bus Number = 1 Subordinate Bus Number = 2 Memory Base / Limit IO Base / Limit