PCIe 内核枚举与拓扑构建
pcie-firmware-enum 讲的是固件阶段——ECAM 窗口打通、Bus 0 暴力扫描、给引导设备分配临时 BAR、写 ACPI 表交接。本篇讲内核接管后的工作:深度优先递归扫描整棵树、分配 Bus 号、探测 BAR 大小并分配物理地址、匹配驱动。这是 PCIe 四阶段时间线中的阶段③。
一、枚举要做什么
固件交接后,内核手上有 ECAM 基址和 Bus 0 设备列表,但 Switch 下游(Bus > 0)的拓扑完全未知,固件的 BAR 分配也是临时凑合的。内核要做三件事:
- 完整建树——从 Bus 0 出发,遇到 Switch 就递归扫下游,直到发现所有设备
- 全局统一分配资源——BAR 地址、Bus 号、MSI/MSI-X 中断,统一从全局地址空间规划
- 匹配驱动并 probe——让每个设备有对应的驱动接管,最终在
/dev/下可用

流程中的两个关键动作:
- 发现 Switch → 立即武装(写 Pri/Sec/Sub),之后 Bus=1 的 Config TLP 才能穿过 Switch 到达下游设备。这是递归的前提——不写 Bus 号寄存器,ECAM 窗口再大也穿不过 Switch。
- 发现 Endpoint → 探测 BAR 并停止递归。内核通过写全 1 读回的方式测出 BAR 大小、从 MMIO 地址池分配物理地址、写入 BAR 寄存器。BAR 存的是内核分配的基址,大小由设备硬件定死——详见第四章。
二、BDF 与配置空间
2.1 BDF 地址
每个 PCIe 设备有唯一三元组——BDF(Bus : Device . Function):
| 字段 | 位数 | 范围 | 来源 |
|---|---|---|---|
| Bus | 8-bit | 0~255 | 枚举时动态分配。Bus 0 是 RC 直连总线;Switch 下游 Bus 号由内核写入 Secondary/Subordinate 寄存器 |
| Device | 5-bit | 0~31 | 物理连线决定——RC 内部 Device 号硅片固化,Switch 下游端口由硬件端口号固定 |
| Function | 3-bit | 0~7 | 硅片固化 |
lspci -t -vv # 树形拓扑
lspci # BDF 列表输出示例:
00:00.0 Host bridge → Root Complex
00:01.0 PCI bridge → Switch 上游 (Bus 0 → Bus 1)
01:00.0 NVMe → Bus 1 Endpoint2.2 配置空间:Type 0 vs Type 1
内核扫描过程中不断读写的正是设备配置空间,Header 分两种:
Type 0: Endpoint Type 1: Bridge/Switch
┌──────────────────────────┐ ┌──────────────────────────┐
│ VID/DID │ Status/Cmd │ │ VID/DID │ Status/Cmd │
│ Class │ Revision │ │ Class │ Revision │
│ BIST │ HdrType=0x00 │ │ BIST │ HdrType=0x01 │
│ BAR0 │ │ BAR0-1 (桥自身, 很少用) │
│ BAR1 │ │ Primary Bus │ Sec Bus │
│ BAR2 │ │ Sub Bus │ Sec Stat │
│ BAR3 │ │ Memory Base / Limit │
│ BAR4 │ │ Prefetchable Mem Base/Lim│
│ BAR5 │ │ I/O Base / Limit │
│ Subsys VID/DID │ │ ... │
│ Capabilities Pointer │ └──────────────────────────┘
│ Int Pin / Line │
└──────────────────────────┘| 对比 | Type 0 (Endpoint) | Type 1 (Bridge/Switch) |
|---|---|---|
| Header Type | 0x00 | 0x01 |
| 内核关注的关键字段 | BAR0~5(MMIO 窗口)、Class Code(匹配驱动) | Pri/Sec/Sub Bus(Bus 号路由)、Mem Base/Limit(地址路由) |
| 内核写什么 | BAR 基址、Command 使能位 | Bus 号、MMIO 下游窗口 |
枚举时内核先读 VID/DID 确认设备存在,再读 Header Type(偏移 0x0E)决定后续动作:0x00→ 探测 BAR 后停止,0x01→ 分配 Bus 号后递归进入子总线。
三、深度优先扫描与 Bus 号分配
3.1 为什么是深度优先
内核不线性穷举 256×32×8 = 65536 个 BDF,而是顺着 Switch 往下挖:
Bus 0
Dev 0 → 空, Dev 1 → 空
Dev 2 → Switch → 分配 Bus 1, 立刻下去
Bus 1
Dev 0 → NVMe (EP)
Dev 1 → GPU (EP)
Dev 2 → Switch → 分配 Bus 2, 再下去
Bus 2
Dev 0 → FPGA (EP)
Dev 3 → 空 ...好处:每发现一个 Switch 就探明它整个下游子树,再回来继续扫当前 Bus,不会遗漏任何级联设备。
3.2 核心调用链
pci_scan_root_bus()
└─ pci_scan_child_bus() ← 扫描一条总线上每个 Device
├─ pci_scan_slot() ← 逐 Device 读 VID/DID
│ └─ pci_scan_device() ← 读 VID/DID
│ └─ pci_setup_device() ← 读完整配置空间, 确定 Type 0/1
│ └─ pci_device_add() ← 注册到内核设备模型
│
└─ 如果是 Type 1:
├─ pci_alloc_child_bus() ← 分配新 Bus 号, 写 Pri/Sec/Sub
└─ pci_scan_child_bus(new_bus) ← 递归!3.3 Type 1 Header 的三个 Bus 号寄存器
每个 Switch 端口就是一个 Type 1 Bridge,其配置空间里有三个 Bus 号域——这是拓扑构建的核心机制:
| 寄存器 | 含义 | 谁写 |
|---|---|---|
| Primary Bus | 上游总线号 | 发现 Switch 时填当前扫描的 Bus 号 |
| Secondary Bus | 下游第一条总线号 | 分配一个新 Bus 号 |
| Subordinate Bus | 下游最深总线号 | 递归扫描完毕后回填(因为分配时不知道下游有几层) |
3.4 嵌套 Switch 的完整递归过程

Subordinate 必须先写大值再回填——分配 Bus 号时你不知道下游还会不会有 Switch、有几层。先写 255 让 Config TLP 能穿过,扫完子总线后回来写真实值。这也是为什么固件不敢递归:固件没有全局 Bus 号池,不知道哪些号会被内核后续的热插拔设备占用。
3.5 Switch 硬件如何判断路由方向
三个 Bus 号写入后,每条 Config TLP 经过 Switch 时按以下逻辑决定路由:
| 条件 | 动作 | 含义 |
|---|---|---|
Bus == Secondary | 转为 Type 0 Config TLP | 目标是本 Switch 直连下游的 EP |
Sec < Bus <= Sub | 保持 Type 1, 继续转发 | 目标在更深层,透传给下游 Switch |
Bus > Sub 或 Bus < Pri | 不向下转发 | 不在本桥管辖范围 |
关于 ID 路由和地址路由的完整机制,详见 pcie。
四、BAR 探测与地址分配
先澄清最容易误解的三个概念:
| 概念 | 说明 |
|---|---|
| "大小"是谁定的 | 设备硬件定死。芯片设计时决定了地址比较器旁路多少条低位地址线——旁路 16 位 → 64KB 窗口,旁路 12 位 → 4KB 窗口。BAR 声明的是"我的寄存器文件需要占多大物理地址空间",不是内存条容量。 |
| 物理基址谁决定 | 内核分配,不是设备定死的。 设备刚上电时 BAR 通常是 0x00000000。内核探测出 size 后从 MMIO 地址池分配空闲地址,写进 BAR 寄存器。设备只能决定"我需多大房子",不能决定"我住哪条街"。 |
| 映射到内存吗 | 不。 BAR 映射到设备内部寄存器空间,不消耗 DRAM。CPU 发出 mov [BAR地址] 后,RC 截获并转成 Memory TLP 发到设备,而不是访问 DRAM 控制器。MMIO 占用的物理地址段在 ACPI 表中标记,memblock 会跳过它不分配给 RAM。 |
CPU 发出 mov [0xF8000000], 42
→ RC 查表: 该地址属于 Bus 1 Dev 0 的 BAR
→ 生成 Memory Write TLP → Switch → NVMe
→ NVMe 的 BAR0 硬件解码: "这是写我的 Doorbell"
→ 写 Doorbell 寄存器, 通知控制器有新命令4.1 探测原理:写全 1 读回
BAR 是 Type 0 配置空间中的一组寄存器(BAR0~BAR5),每个 32-bit。低几位是只读 flag bits(IO/Memory 类型、32/64-bit、可预取),高位可写。两个 BAR 槽位可以合并成 64-bit BAR。

读回值为什么不是 0xFFFFFFFF? BAR 寄存器中,地址比较器旁路的低位在硬件上硬连线到地(GND),写操作对这些位无效,读操作永远返回 0。写 0xFFFFFFFF 后读回 0xFFFF0000,低 16 位为 0 → 这些位对应的地址线根本没接到比较器上 → 窗口大小 2^16 = 64KB。
为什么用 ~x + 1 算 size? 这是求二进制补码,本质是"找到最低一个可写位对应的权重"。0xFFFF0000 的 ~x + 1 = 0x00010000 = 64KB,正好是 bit 16 的权重。& ~0xF 是去掉低 4 位 flag bits 的干扰——它们也是只读的但不代表地址空间大小。
为什么必须先恢复原始值? 0xFFFF0000 是一个无效物理地址,不恢复会干扰后续对 BAR 的任何读取。
一句话总结 BAR 探测:① 设备通过硬连线低位声明所需地址窗口大小 → ② 内核从 MMIO 地址池分配物理地址并写入 BAR → ③ CPU 用普通
mov指令直通设备寄存器(RC 自动截获转成 Memory TLP,无需特殊 IO 指令或系统调用)。
4.2 地址比较器:硬件如何解码 MMIO 访问
CPU 执行: mov [0xF8000400], 42
拆开 32-bit 地址:
┌──────────────────┬──────────────────────┐
│ bit 31:16 │ bit 15:0 │
│ 0xF800 │ 0x0400 │
│ ↑ 送比较器 │ ↑ 旁路,不比较 │
│ 与 BAR 高位比较 │ 直接当设备内部偏移用 │
└──────────────────┴──────────────────────┘三句话记住:
- BAR 寄存器存的是基地址(门牌号),不是设备内部寄存器的位图
- 窗口大小由硬件比较器的旁路位数决定,旁路 16 位 → 64KB
- "探测大小"本质上就是探测多少位被旁路了——写全 1 读回 0 的位 = 旁路位
4.3 BAR 类型识别
| BAR 低几位 | 含义 |
|---|---|
| bit 0 = 0 | Memory BAR(MMIO,最常用) |
| bit 0 = 1 | I/O BAR(PMIO,PCIe 基本不用) |
| bit 2-1 = 00 | 32-bit BAR,可映射到 4GB 以下 |
| bit 2-1 = 10 | 64-bit BAR,占用连续两个槽位(如 BAR0+BAR1) |
| bit 3 = 1 | Prefetchable(允许预取,用于显卡 VRAM 等;控制寄存器绝不能标此位——读操作可能有副作用) |
4.4 常见设备的 BAR 布局惯例
BAR 没有强制标准,但行业中有常见模式:
| 设备类型 | 典型 BAR 布局 |
|---|---|
| NVMe SSD | BAR0(64-bit,BAR0+BAR1),通常 16KB——控制器只暴露一组 Doorbell + Admin Queue 寄存器 |
| 显卡 GPU | BAR0 = 控制寄存器(小),BAR2 = 显存 VRAM(64-bit, BAR2+BAR3, Prefetchable, 数 GB)。BAR1 常闲置——BAR2 是 64-bit 吃掉两个槽位,BAR0 独立使用后中间 BAR1 自然空出 |
| 网卡 NIC | BAR0 = 主寄存器块,BAR2/BAR4 = 可选 SR-IOV VF 映射 |
| AHCI SATA | BAR5 = ABAR(AHCI spec 规定偏移 0x24,刚好对应 BAR5,少数有固定位置的例外) |
| xHCI USB | BAR0 = MMIO 运行寄存器 |
| FPGA / 智能网卡 | BAR0 = CSR 寄存器,BAR2 = 大块 DMA 缓冲区,BAR4 = 可选额外接口——最灵活,无惯例 |
lspci -s 01:00.0 -vv # 查看单设备完整 BAR 布局
# 输出: Region 0: Memory at f7000000 (64-bit, non-prefetchable) [size=16K]4.5 全局 MMIO 资源池:struct resource 树
内核在资源树中统一管理所有物理地址空间:
iomem_resource (0x0 ~ 0xFFFFFFFFFFFFFFFF)
├── PCI Bus 0000:00 MMIO 窗口: 0x80000000 ~ 0xFFFFFFFF
│ ├── BAR: B0:D0:F0 SATA 0xF8000000 ~ 0xF8000FFF (4KB)
│ ├── BAR: B0:D1:F0 USB 0xF8001000 ~ 0xF8001FFF (4KB)
│ └── BAR: B1:D0:F0 NVMe 0xF8100000 ~ 0xF8103FFF (16KB)
└── system RAM: 0x0 ~ 0x7FFFFFFFMMIO 范围由 ACPI 表(_CRS)或设备树在启动时确定,物理内存管理器(memblock)会跳过它不分配给 RAM。分配时内核调用 pci_bus_alloc_resource() 在父窗口中搜索满足对齐和大小的空闲区间。
4.6 对齐约束
BAR 起始地址必须按自身大小对齐——这不是内核额外要求,是硬件决定的:BAR 寄存器只有高位可写,低位硬连线为 0,你写 0xF8001000 给 64KB BAR,硬件实际收到的是 0xF8000000。
| BAR 类型 | 分配范围 | 原因 |
|---|---|---|
| 32-bit BAR | 只能 < 4GB | 32 位地址空间无法表达 4GB 以上地址;DMA 时设备发不出正确地址 |
| 64-bit BAR | 优先 > 4GB | 把 4GB 以下宝贵空间留给 32-bit 设备 |
4.7 分配过程序列图

关键约束:
| 约束 | 说明 |
|---|---|
| size = alignment | BAR 的 size 和 alignment 相同,由硬件低位只读保证 |
| 32-bit BAR < 4GB | 内核指定 limit = 0xFFFFFFFF |
| Bridge 窗口必须覆盖下游 | Memory Base ≤ 所有下游 BAR 且 Memory Limit ≥ 所有下游 BAR |
| Bridge 粒度 1MB | Memory Base/Limit 只用 bit 31:20(12 位) |
| 向上递归合并 | 父 Bridge 窗口 = 所有子 Bridge 窗口的并集 |
内核代码入口:
// drivers/pci/setup-bus.c
void pci_assign_unassigned_resources(void) {
// 对每个 root bus:
// __pci_bus_assign_resources(bus)
// ├── pbus_size_mem() // Pass 1: 计算 bridge 下游总大小
// ├── pbus_size_io()
// ├── __pci_bus_assign_resources_sorted()
// │ └── pci_bus_alloc_resource() // Pass 2: 分配 + 写入 BAR
// └── pci_bus_dump_resources()
}BAR 激活后 CPU 如何通过
mov [BAR]发送 Memory TLP 到设备,详见 pcie-bar-activation。
五、驱动匹配与 probe
建树完成、BAR 分配完毕后,内核用 pci_match_id() 对比设备 VID/DID 和已注册驱动的 pci_device_id 表:
// 驱动侧声明
static const struct pci_device_id mydrv_ids[] = {
{ PCI_DEVICE(0x144D, 0xA808) }, // 三星 NVMe
{ 0, }
};
MODULE_DEVICE_TABLE(pci, mydrv_ids);
// 内核侧自动匹配 → 调用 probe()
static int mydrv_probe(struct pci_dev *pdev, const struct pci_device_id *id) {
// ioremap(BAR) → 设备寄存器可 MMIO 访问
// request_irq() → 中断可用
// 注册块设备/字符设备 → /dev/ 下可见
}每个枚举到的设备在 sysfs 下有一个节点:
ls /sys/bus/pci/devices/
# 0000:00:00.0 0000:00:01.0 0000:01:00.0
cat /sys/bus/pci/devices/0000\:01\:00.0/resource
# BAR0: 0xf8000000-0xf8003fff (MMIO 16KB)六、与现有文档的关系
| 文档 | 关系 |
|---|---|
| pcie | 物理基础——拓扑/Lane/三层协议/MMIO;路由机制详见该篇 |
| pcie-link-training | 阶段①——链路训练,枚举的物理前提 |
| pcie-pre-enumeration | RC/BDF/BAR 硬件原理,枚举依赖的底层机制 |
| pcie-firmware-enum | 阶段②——固件枚举,内核建树的前置阶段 |
| pcie-bar-activation | 阶段④——BAR 激活 + 驱动 probe 的完整 MMIO 路径 |
| io-addressing | MMIO/PMIO 编址基础 |
| dma | DMA 依赖 BAR 分配和地址路由 |
七、一句话总结
内核 PCIe 枚举的本质是深度优先递归建树——从 Bus 0 出发,遇 Switch 就分配 Bus 号、写 Pri/Sec/Sub、递归进入子总线,遇 Endpoint 则探测 BAR 大小、分配物理地址并写入 BAR,整棵树建完后匹配驱动 probe,最终每个设备在
/sys/bus/pci/devices/BDF/下可见。