PCIe 树构建前的通信机制:RC、BDF、BAR 的工作原理
内核 pci_scan_root_bus() 跑起来之前,系统处于什么状态?CPU 凭什么能跟一个还没分配地址的设备对话?BAR 寄存器里最初存的是什么?Root Complex 在这一切里扮演什么角色?本篇聚焦树构建之前的硬件阶段——从系统上电到链路训练完成,再到第一笔 Config TLP 发出去把设备 VID/DID 读回来。
零、总览:树构建之前,系统"有什么"
| 有什么 | 说明 |
|---|---|
| ✅ 物理链路 | 链路训练(硬件自动完成),L0 就绪,可以传 TLP |
| ✅ Root Complex | CPU 侧的固定功能块,一直在线。CPU 通过 MMIO 地址空间和 RC 对话 |
| ✅ ECAM 窗口 | 固件(UEFI/BIOS)在 MCFG 表里写好了 ECAM 基址,CPU 可以直接用 mov 发 Config TLP |
| ✅ Bus 0 的设备 | 固件已在 MCFG 里描述了 Bus 0 上的设备(至少保证显卡/键盘/启动盘可用) |
| ❌ BAR | 未分配——BAR 寄存器里存的要么是硬件复位值 0x00000000,要么是固件临时分配的值 |
| ❌ Bus 1+ | 未扫描——Switch 下游的 Bus 号还没分配,设备不可见 |
| ❌ 驱动 | 未加载——设备 VID/DID 还没被匹配到任何驱动 |
核心结论:树构建前,系统不是一张白纸。硬件物理层已经就绪(链路训练),RC 一直在运行,固件已经把 ECAM 窗口映射好了。软件要做的只是"走过去敲门"——通过 Config TLP 读配置空间、识别设备、分配资源。
一、Root Complex(RC)—— CPU 进入 PCIe 世界的唯一门户
核心要点:
- RC 是一块硬件固定功能块,不做软件初始化也能工作的 PCIe 接口
- RC = 地址翻译器:把 CPU 的 MMIO
mov [addr]翻译成 PCIe TLP,反之亦然 - RC 不经过 BAR——它自身在 CPU 物理地址空间里有固定的 MMIO 窗口(ECAM)
1.1 RC 是什么
Root Complex 是 CPU 芯片内部(或北桥/芯片组)的一块固定功能硬件。它不是 PCIe Endpoint,不需要驱动程序——从系统上电那一刻起,RC 就已经在工作了。
RC 在硬件上做的事:
| 功能 | 说明 |
|---|---|
| 地址解码 | 判断 CPU 发出的物理地址属于哪个窗口:ECAM?MMIO(BAR)?DRAM? |
| TLP 生成 | 把 CPU 的 mov / in/out 翻译成标准 PCIe TLP 格式 |
| TLP 解析 | 接收总线返回的 Completion TLP,提取数据还给 CPU |
| 中断路由 | 把 PCIe MSI/MSI-X 中断翻译成 LAPIC 能识别的中断消息 |
| 排序规则 | 保证 Posted/Non-Posted/Completion 三种 TLPs 的 PCIe 排序模型正确执行 |
1.2 RC 怎么知道发 Config TLP 还是 Memory TLP?
看地址。RC 内部有一组硬连线(或可编程寄存器)定义了几段地址窗口:
CPU 物理地址空间:
┌──────────────────────┐ 0x00000000
│ DRAM │
│ (内存控制器处理) │
├──────────────────────┤ 0x80000000 ← 例子
│ ECAM 窗口 │ ← 固件从 MCFG 表设好
│ (所有 BDF 配置空间) │ CPU 访问这里 → RC 生成 Config TLP
├──────────────────────┤ 0xA0000000
│ MMIO 窗口 (BAR) │ ← OS 枚举后分配
│ (设备寄存器映射) │ CPU 访问这里 → RC 生成 Memory Read/Write TLP
├──────────────────────┤ 0xC0000000
│ ... │
└──────────────────────┘关键:ECAM 窗口的基址是固件在 ACPI MCFG 表里写好的,不是 OS 枚举时才设的。所以树构建前 CPU 就能通过 ECAM 发出 Config TLP。
二、BDF 在枚举前如何起作用
核心要点:
- 链路训练完成后,Bus 0 上的 Device 号由物理连接位置隐式确定(Slot 0 → Dev 0)
- 枚举前只有 Bus 0 可访问——Switch 下游的 Bus 号还没分配,设备不可见
- BDF 的直接作用:作为 ECAM 地址的计算输入,
ECAM_base + (Bus<<20 | Dev<<15 | Func<<12)
2.1 枚举前,Bus 号和 Device 号从哪来
链路训练完成时,硬件不关心"你叫什么名字"。Bus/Device 号不是设备自带的,而是:
- Bus 0:永远是 RC 直连的总线。RC 的每个下游端口物理上固定对应 Bus 0 上的一个 Device 号。
- Device 号:由 RC 内部的下游端口编号决定。物理上插在第 N 个端口 = Bus 0 上的 Device N。
Root Complex
┌─────────┼─────────┐
Port 0 Port 1 Port 2 ... ← RC 的物理端口号 = Bus 0 的 Device 号
↓ ↓ ↓
[空] [NVMe] [空]
Dev=00 Dev=01 Dev=02Bus 0 的 Device 号在枚举前就已经"有了"——它是 RC 硬件端口物理决定的。固件/OS 枚举只是去读这些端口,看对应 Device 是否存在(VID != 0xFFFF)。
2.2 BDF → ECAM 地址 → Config TLP
在树构建前,BDF 的唯一作用就是算 ECAM 地址:
ECAM 地址 = MCFG_Base + (Bus << 20 | Device << 15 | Function << 12) + Register_Offset示例:读 Bus 0, Dev 1, Func 0 的 VID/DID(偏移 0x00),设 MCFG_Base = 0xE0000000:
ECAM 地址 = 0xE0000000 + (0 << 20 | 1 << 15 | 0 << 12) + 0x00
= 0xE0000000 + 0x00008000 + 0x00
= 0xE0008000
CPU 执行: mov eax, [0xE0008000]
RC 解码: 地址落在 ECAM 窗口 → 提取 Bus=0, Dev=1, Func=0, Reg=0
→ 生成 Config Read TLP, 目标 BDF = 00:01.0
→ 发到 Bus 0 上整个流程:
CPU RC PCIe Bus Endpoint
│ │ │ │
│ mov eax, [0xE0008000] │ │ │
│ ─────────────────────────→ │ │ │
│ │ 地址解码: ECAM 窗口 │ │
│ │ Bus=0 Dev=1 Func=0 │ │
│ │ │ │
│ │ Config Read TLP ───────────→│ │
│ │ (Bus=0, Dev=1, Reg=0) │ │
│ │ │ │
│ │ │ ← 设备 01.0 匹配 BDF │
│ │ │ 返回 VID/DID │
│ │ │ ───────────────────────→│
│ │ │ │
│ │ Completion TLP ←───────────│ │
│ │ (数据 = 0x144D_A808) │ │
│ │ │ │
│ RAX = 0x144D_A808 │ │ │
│ ←───────────────────────── │ │ │2.3 为什么枚举前看不到 Bus 1+ 的设备
一开始 BIOS/内核只知道 Bus 0 的存在。原因很简单:
- RC 只有 Bus 0 的端口。要发现 Switch 下游的设备,必须先读到 Switch 的 Type 1 配置头,然后给它分配 Secondary/Subordinate Bus 号。
- 在分配之前,Switch 的下游端口不在任何已知的 Bus 号范围内——RC 不知道该往哪发 Config TLP。
这就是深度优先扫描的必要性:先发现 Bus 0 上的桥 → 分配 Bus 1 → 才能进入 Bus 1 扫描 → 发现 Bus 1 上的桥 → 分配 Bus 2 → ...
三、BAR 如何在树构建前起作用
核心要点:
- BAR 本质是一个硬件寄存器:低位是只读 flag(IO/Memory/64bit/可预取),高位是可写的地址字段
- 复位后的 BAR =
0x00000000(或固件临时值),设备不会响应 MMIO - BAR 大小探测利用了硬件行为:向 BAR 写全 1 → 硬件只保留能写入的位 → 低位保持不变 → 读回后算大小
- 树构建前 BAR 的唯一作用:通过 Config TLP 读写 BAR 寄存器本身,探测设备需要多少 MMIO 空间
3.1 BAR 寄存器的硬件设计
BAR 不是一个普通的内存单元——它是一个有特殊行为的硬件寄存器:
BAR 寄存器 (32-bit 示例):
Bit 31 ......................... Bit 4 │ Bit 3 │ Bit 2-1 │ Bit 0
┌──────────────────────────────────────┼────────┼──────────┼────────┐
│ 可读写的地址字段 (高位) │Prefetch│ Type │ IO/Mem │
│ 软件向这里写物理基址 │ able │ 00=32bit │ Space │
│ │ │ 10=64bit │ │
└──────────────────────────────────────┴────────┴──────────┴────────┘
↑ ↑ ↑
可写 只读 只读关键硬件特性:
| 特性 | 硬件行为 |
|---|---|
| 地址字段可写 | 软件写入物理基址后,BAR 输出到地址解码器,设备开始响应对应地址范围的 MMIO |
| 低位 flag 只读 | bit 0, bit 2-1, bit 3 由硬件固定,软件写什么都不会改变它们 |
| 地址字段按 size 对齐 | 如果设备需要 64KB MMIO,则 bit 15-0 在地址字段里不可写(硬件固定为 0) |
| 复位值 | 上电后 BAR = 0x00000000(所有可写位清零),设备不响应任何 MMIO 地址 |
3.2 BAR 大小探测原理(写全 1 读回)
设备需要的 MMIO 空间大小不直接存在某个寄存器里——但可以通过硬件行为反推:
探测流程:
1. 读 BAR 当前值 → 保存原始值 (original)
e.g. 读回 0x00000000 (bit 0=0 → Memory BAR, bit 2-1=00 → 32-bit)
2. 向 BAR 写 0xFFFFFFFF (全 1)
┌─────────────────────────────────────────────────────────┐
│ 硬件行为: │
│ · 低位 flag (bit 0, bit 2-1, bit 3): 全 1 写入 → 忽略 │
│ → 保持原始值 (硬件只读) │
│ · 高位地址字段: 从最低的可写位到最高位 → 接受 1 │
│ 但低于 size 边界的位 → 硬件固定为 0 (不可写) │
│ · 举例: 设备需要 64KB (bit 15-0 不可写) │
│ 写 0xFFFF_FFFF → 实际写入 0xFFFF_0000 │
└─────────────────────────────────────────────────────────┘
3. 读 BAR → 得到写后值 (readback)
e.g. 读回 0xFFFF0000 (bit 15-0 都是 0, bit 31-16 都是 1)
4. 计算 size:
有效值 = readback & ~0xF # 去掉 flag 位 → 0xFFFF0000
size = ~有效值 + 1 # 取反加一
= ~0xFFFF0000 + 1
= 0x0000FFFF + 1
= 0x00010000 # = 64KB
5. 恢复原始值: 向 BAR 写回 original为什么这个 trick 能工作:
硬件设计上,BAR 地址字段中低于 size 边界的位在硅片上就不存在——它们对应的触发器/锁存器根本没被实现。写 1 进去,读回来永远是 0。所以 "写全 1 → 读回 → 看哪些位变 1 了" 就能反推出 size。
3.3 树构建前 BAR 的状态变化
时间线:
上电复位
│ BAR = 0x00000000 ← 硬件默认值
│ 设备不响应任何 MMIO 地址
│
▼
固件枚举 (可选)
│ UEFI 可能给显卡/Boot设备分配临时 BAR 值
│ 此时设备可以响应 MMIO (如果固件做了)
│
▼
Linux 内核接管
│ pci_scan_device() → 读 VID/DID (不碰 BAR)
│ pci_setup_device() → 读 BAR 原始值,保存
│
▼
BAR 大小探测
│ 写全 1 → 读回 → 计算 size → 恢复原始值
│ BAR 的最终值仍未确定
│
▼
BAR 地址分配
│ OS 从全局 MMIO 地址池分配一段对齐的物理地址
│ 写入 BAR 寄存器 → 设备地址解码器激活
│ 从此设备可以响应 MMIO 读写
│
▼
驱动 probe()
│ ioremap(BAR0) → 拿到内核虚拟地址
│ 可以读写设备寄存器了树构建前,BAR 没有"起作用"——它只是被读取(获取 size)和被写入(探测/临时赋值)。真正"起作用"是分配完成后:设备地址解码器开始匹配 BAR 中的基址范围,响应 MMIO。
四、设备发现的完整时序:从硬件到软件
核心要点:
- 第一步永远不是软件——是硬件:链路训练自动完成,L0 就绪
- 第二步才是固件:通过 RC 的 ECAM/IO 端口发出 Config TLP,读 VID/DID
- 第三步内核接管:深度优先递归扫描,每发现一个桥就分配新 Bus 号、递归进入
- 整个过程的关键前提:RC 一直在运行,ECAM 窗口固件已设好,Config TLP 不需要 BAR

4.1 第一步:硬件链路训练(0~100ms)
不需要任何软件。PCIe 物理层的 LTSSM(Link Training and Status State Machine)自动运行:
Detect → Polling → Configuration → L0
↑ │
└──────────── Recovery ←─────────────┘ (出错时)L0 状态意味着:两端可以交换 TLP 了。但此时软件还不知道设备存在。
4.2 第二步:固件发出第一笔 Config TLP
固件(UEFI/BIOS)通过两种方式之一发出 Config Read TLP:
方式一:ECAM(现代)
// MCFG 表已经告诉了固件 ECAM 基址
uint32_t ecam_base = mcfg->base_address; // e.g. 0xE0000000
uint32_t *config_space = (uint32_t *)(ecam_base + (0 << 20 | 1 << 15 | 0 << 12)); // Bus 0, Dev 1
uint32_t vid_did = *config_space; // 触发 RC 生成 Config Read TLP
if ((vid_did & 0xFFFF) != 0xFFFF) { // VID != 0xFFFF → 设备存在
// 找到了!
}方式二:IO 端口 0xCF8/0xCFC(传统 x86)
// Bus=0, Dev=1, Func=0, Reg=0 (VID/DID)
outl(0xCF8, (1 << 31) | (0 << 16) | (1 << 11) | (0 << 8) | 0);
uint32_t vid_did = inl(0xCFC); // RC 生成 Config Read TLP4.3 第三步:内核递归扫描
内核拿到控制权后,重复"读 VID/DID → 判断类型 → 如果是桥就分配新 Bus 号 → 递归"直到整棵树被发现。详细过程见 pcie-enumeration §三。
五、关键机制的对照总结
5.1 RC vs Switch vs Endpoint 的"自知之明"
| 组件 | 上电后"知道自己是谁"吗 | 需要什么才能工作 |
|---|---|---|
| RC | ✅ 始终知道——它是固定功能硬件 | 固件设好 ECAM 窗口基址即可 |
| Switch | ❌ 不知道——Type 1 Header 里的 Pri/Sec/Sub Bus 都是 0 | 需要 OS 枚举时写入 Bus 号和地址窗口寄存器 |
| Endpoint | ❌ 不知道——BAR 是 0,不响应 MMIO | 需要 OS 枚举时写入 BAR 地址 |
5.2 BDF vs BAR 在树构建前的作用域
| BDF | BAR | |
|---|---|---|
| 树构建前的作用 | 作为 ECAM 地址计算输入,发出 Config TLP 定位设备 | 仅作为配置空间中的一个寄存器被读写(探测 size) |
| 树构建前不做什么 | Bus 1+ 不可访问(Switch 还没被配置) | 不响应 MMIO(地址解码器未激活) |
| 第一次"生效"的时机 | Bus 0 上链路训练完成即可用 | OS 写入物理基址后,地址解码器激活 |
| 谁在维系 | RC 的端口 → Device 号的物理映射 | 设备上 BAR 寄存器的硬件行为(只读低位 + 写全 1 trick) |
5.3 Config TLP vs Memory TLP 在树构建前
| Config TLP | Memory TLP (MMIO) | |
|---|---|---|
| 树构建前能用? | ✅ 链路训练完成后即可 | ❌ BAR 未分配,设备不响应 |
| 发出去的前提 | ECAM 窗口已映射(固件设) | BAR 已写入物理基址(OS 枚举后) |
| 路由方式 | ID 路由(Bus 号) | 地址路由(物理地址 → MemBase/Limit 匹配) |
| 用途 | 发现设备、读配置空间、探测 BAR、分配 BAR | 驱动读写设备寄存器、DMA |
六、与现有文档的关系
- pcie-link-training —— 阶段①:链路训练(本篇所有机制的物理前提——L0 就绪才能发 TLP)
- pcie —— 拓扑/Lane/三层协议/MMIO,本篇的"前置知识"
- pcie-firmware-enum —— 阶段②:固件枚举(ECAM窗口/MCFG表/第一笔Config TLP,本篇机制的软件应用)
- pcie-enumeration —— 阶段③:内核完整枚举 + 路由(本篇的"后续")
- io-addressing —— PMIO vs MMIO 编址,
mov [addr]如何穿过 RC 变成 TLP
七、一句话总结
树构建前,系统靠三样东西就能发现设备:① 链路训练(硬件自动,无需软件)→ ② RC(CPU 侧的固定功能 TLP 引擎,上电即工作)→ ③ Config TLP(通过 ECAM/IO 端口发出,不依赖 BAR)。BDF 在这个阶段只是 ECAM 地址的计算参数,BAR 只是一个被读写的配置空间寄存器——用来反问硬件"你需要多大 MMIO 空间?"。直到 OS 把物理基址写入 BAR,设备地址解码器才激活,MMIO 才真正可用。