PCIe 阶段②:固件枚举 —— 第一笔 Config TLP 到 Bus 0 就绪
链路训练完成后,物理层可以传 TLP 了——但 CPU 怎么知道总线上有什么设备?答案是固件(UEFI/BIOS)主动"敲门":通过 RC 发 Config TLP 到 Bus 0 上的每个 Device,读 VID/DID。非
0xFFFF说明设备存在,然后给引导必需的设备(显卡/NVMe/键盘)分配临时 BAR。本篇聚焦这个阶段:固件怎么拿到 ECAM 窗口地址、怎么发第一笔 Config TLP、为什么只扫描 Bus 0、以及固件和内核之间怎么"交接"PCIe 设备信息。
典型 PCIe 拓扑与固件枚举的边界
链路训练完成后,固件面对的问题:
| 已知 | 未知 |
|---|---|
| 物理链路已 up——Lane 数、速率协商完毕 | Bus 0 上到底挂了多少个 Device? |
| RC 的 Root Port 端口数(硬件固定) | 每个 Device 是什么(VID/DID = ?) |
| ECAM 基址(从平台寄存器读出) | 有没有 PCIe Switch?下游 Bus 号是多少? |
| — | Switch 后面挂了多少设备? |
换句话说:物理层通了,但拓扑层一片黑。固件枚举的本质就是——从 RC 出发,逐 Device 敲门问"你是谁",把能直接看到的 Bus 0 设备全部登记入册。Switch 下游的设备固件无能力访问(缺少 Type 1 → Bus 号的映射),留白给内核。
缩写表
| 缩写 | 全称 | 含义 |
|---|---|---|
| VID | Vendor ID | 16 位制造商编号,PCI-SIG 分配(如 0x8086 = Intel, 0x144D = Samsung) |
| DID | Device ID | 16 位设备型号编号,厂商自定。VID + DID 组成 32 位标识符,存于配置空间偏移 0x00。读回 0xFFFFFFFF = 空槽位 |
| ECAM | Enhanced Configuration Access Mechanism | 增强型配置访问机制——用一段物理地址直接映射 PCIe 配置空间(每个设备 4KB,全 Bus 覆盖合计 256MB),一个 mov [ECAM+BDF] 就触发一次 Config TLP |
| MCFG | Memory-mapped Configuration | ACPI 表,记录 ECAM 窗口的物理基址和覆盖的 Bus 范围 |
| ACPI | Advanced Configuration and Power Interface | 高级配置与电源接口——固件与 OS 之间传递硬件拓扑信息的标准格式(含 MCFG / DSDT / DMAR 等子表) |
| DSDT | Differentiated System Description Table | ACPI 主表,描述平台上的固定硬件设备拓扑 |
| SSDT | Secondary System Description Table | ACPI 附表,描述可插拔/可选设备 |
| DMAR | DMA Remapping Table | ACPI 表,描述 IOMMU 的 DMA 地址翻译与设备隔离配置 |
| BAR | Base Address Register | 基址寄存器——设备配置空间里的寄存器,存 MMIO/PIO 窗口的起始地址。树构建前全为 0(或固件临时值) |
| BDF | Bus:Device.Function | PCIe 三层地址:Bus(0~255):Device(0~31):Function(0~7),唯一标识一个 PCIe 功能单元 |
| RC | Root Complex | 根复合体——CPU 与 PCIe 总线之间的硬件桥,负责把 CPU 的 MMIO mov 翻译成 TLP |
| TLP | Transaction Layer Packet | 事务层包——PCIe 协议中承载读写操作的数据包,Config TLP 访问配置空间,Memory TLP 访问 MMIO |
| UEFI | Unified Extensible Firmware Interface | 统一可扩展固件接口——现代 BIOS 标准,PEI(Pre-EFI)和 DXE(Driver Execution Environment)两阶段完成设备枚举 |
| GOP | Graphics Output Protocol | UEFI 图形输出协议——替代传统 VBIOS,让显卡在没有 OS 驱动的情况下也能显示画面 |
本阶段目标
让引导必需的设备可用,其余交给内核。
具体来说:固件枚举只做三件事。
| # | 目标 | 如何做到 |
|---|---|---|
| 1 | 打通 ECAM 窗口 | 从平台寄存器读 ECAM 基址 → 写入 RC 地址解码器 → CPU 的 mov [ECAM] 被翻译为 Config TLP |
| 2 | 发现 Bus 0 上的设备 | 遍历 Device 0~31, 发 Config Read TLP (ID 路由) 读 VID/DID → ≠ 0xFFFF = 存在 |
| 3 | 初始化引导设备 | 仅为显卡 (VBIOS/GOP) 和 NVMe (系统盘) 分配临时 BAR + 执行 Option ROM → 屏幕亮 + 有启动盘 |
固件不做的:不扫描 Switch 下游、不递归枚举、不加载非引导设备驱动——这些交给内核阶段③。
阅读建议
- 一/二章(MCFG/ECAM)是基础,理解后才能看懂三章的扫描原理
- 三章(Bus 0 扫描)是核心,包含第一笔 Config TLP 的硬件级时序
- 四章(交接)讲固件→内核的信息传递,五章总结产物
零、总览:固件枚举在时间线上的位置
按下电源键
│
▼
┌──────────────────────────────────────────────────────────────┐
│ ① 硬件链路训练 (0~100ms) │
│ LTSSM: Detect → Polling → Configuration → L0 │
├──────────────────────────────────────────────────────────────┤
│ ② 固件扫描 Bus 0 + 分配临时 BAR (UEFI/BIOS) │
│ · 读 MCFG/ACPI 表获取 ECAM 基址 │
│ · 遍历 Bus 0 Device 0~31, 读 VID/DID │
│ · 给 Boot 设备分配临时 BAR/中断 │
│ · 构建 ACPI DSDT/SSDT 中的设备描述 │
│ ← 本篇 │
├──────────────────────────────────────────────────────────────┤
│ ③ 内核深度优先枚举 + 建树 (pci_scan_root_bus) │
├──────────────────────────────────────────────────────────────┤
│ ④ BAR 激活 + 地址解码器生效 + 驱动 probe() │
└──────────────────────────────────────────────────────────────┘| 固件枚举完成意味着 | 固件枚举没做意味着 |
|---|---|
| 显卡能输出画面(VBIOS/GOP 已初始化) | 黑屏,看不到 BIOS 界面 |
| NVMe/硬盘能读引导扇区 | 找不到启动设备 |
| ACPI 表里有了设备描述 | 内核不知道 PCIe 拓扑的起点在哪 |
一、核心要点
- 固件枚举是"够用就好":只为引导必需的设备(显卡、NVMe、USB 键盘)分配临时 BAR,不扫描 Switch 下游
- ECAM 窗口是固件设的:UEFI 从 MCFG 表读取 ECAM 基址,写入 RC 的地址解码寄存器
- 第一笔 Config TLP 就是读 VID/DID:Bus 0, Dev 0~31, Func 0, Reg 0 — 返回
0xFFFF就是空槽位 - 固件和内核的交接:通过 ACPI 表(MCFG/DSDT/SSDT)传递 PCIe 拓扑信息,内核在此基础上重扫
- Bus 0 是固件唯一的入口:Switch 下游的 Bus 号还没分配,固件只看到 RC 直连的设备
二、MCFG 表与 ECAM 窗口:固件的"敲门砖"
本章目标:打通 ECAM 窗口——让 CPU 的
mov [地址]指令能被 RC 翻译成 Config TLP,真正触达 PCIe 设备。没有这个窗口,后续的 Bus 扫描、BAR 读写全都没法做。
前置概念:平台寄存器
平台寄存器(Platform Configuration Registers) 是芯片组(PCH / FCH)内部的一组硬件寄存器,出厂时由芯片设计者固化在硅片里——不归 OS 管、不归固件写,只读。
它解决一个先有鸡还是先有蛋的问题:固件刚启动时,还没枚举任何 PCIe 设备,不知道总线上有什么、不知道内存布局——但它必须先把 ECAM 窗口配好才能开始扫描。那么"ECAM 基址放哪"这个信息从哪来?答案就是芯片组自带的平台寄存器。
平台寄存器不止存 ECAM 基址,它是芯片组的硬件自描述层:
| 寄存器类别 | 存什么 | 为什么需要 |
|---|---|---|
| 地址映射 | ECAM 基址、DRAM 范围、MMIO 窗口边界 | 固件必须知道哪些物理地址已经"占坑",才能避开冲突 |
| 能力报告 | 最大 PCIe Lane 数、支持的 Gen 版本、Root Port 数量 | 固件据此配置 RC 的链路参数和端口使能 |
| 错误状态 | PCIe AER、Machine Check 等硬件错误记录 | 固件/OS 读取以判断硬件是否健康 |
| 电源/热管理 | TDP 限制、温度阈值、电源状态 | 固件据此初始化风扇曲线和功耗预算 |
一句话:平台寄存器是芯片组焊在硅片上的"配置手册"——固件不是凭空编造地址,而是先读它,再照章办事。没有这组寄存器,固件连"我是谁、我在哪、我能干什么"都不知道。
ECAM 基址就是其中之一。Intel 平台叫 PCIEXBAR(PCI Express Base Address Register),AMD 平台类似机制。固件做的事情不是"决定" ECAM 放哪,而是从平台寄存器读出硬件给定的基址,再把它写入两处:
| 写入位置 | 目的 |
|---|---|
| RC 地址解码器 | 告诉 RC:"这个物理地址范围要走 PCIe,翻译成 Config TLP" |
| ACPI MCFG 表(留在 RAM) | 告诉内核:"ECAM 窗口在这里",内核启动时直接从 MCFG 读 |
┌─────────────────────┐
│ 芯片组 (PCH) │
│ 平台寄存器: PCIEXBAR │ ← 硬件固化, 0xE0000000
│ (只读, RC 内部) │
└─────────┬───────────┘
│ 固件读取
▼
┌──────┴──────┐
│ │
▼ ▼
┌──────┐ ┌──────────┐
│ RC │ │ MCFG 表 │
│地址解│ │ (RAM) │
│码器 │ │ │
└──────┘ └──────────┘
硬件生效 软件可读
TLP 翻译 内核读取为什么不让固件自己定基址? ECAM 窗口占 256MB 物理地址空间,必须避开 DRAM、APIC、HPET、MMIO 等已有映射。让芯片组厂商在硅片设计阶段统一安排,比分发固件到处"猜地址"可靠得多。这也是
0xE0000000在 Intel 平台几乎雷打不动的原因。 平台寄存器的范围限制:它只描述芯片组内部固化连接的设备——比如 RC 有几个 Root Port、每个 Port 的 Lane 数和 Gen 上限、板载 SATA/USB 控制器的 BDF。外接设备(插在 PCIe 插槽的独显、NVMe 扩展卡等)不在平台寄存器里——它们的 BDF 由 Switch 动态分配,必须通过 Config TLP 主动探测才能发现。换句话说,平台寄存器告诉你"主板长什么样",不告诉你"用户插了什么"。
2.1 MCFG 表是什么
MCFG(Memory-mapped Configuration,也叫 PCI Express Enhanced Configuration Mechanism)是 ACPI 规范定义的一张表,告诉 OS/固件:"ECAM 窗口的物理基址在这里,涵盖的 Bus 范围是从 X 到 Y"。
ACPI 表结构 (简化的 MCFG):
┌────────────────────────────────────┐
│ ACPI Header │
│ Signature: "MCFG" │
│ Length: ... │
├────────────────────────────────────┤
│ Reserved (8 bytes) │
├────────────────────────────────────┤
│ Configuration Space Base Address │ ← ECAM 基址 (64-bit)
│ Allocation Structure Entry 0: │
│ Base Address: 0xE0000000 │ e.g. Intel 平台常见值
│ PCI Segment Group: 0 │ (Domain 0000)
│ Start Bus Number: 0 │ 这个窗口覆盖 Bus 0~255
│ End Bus Number: 255 │
├────────────────────────────────────┤
│ Allocation Structure Entry 1: │ ← 第二个 ECAM 段 (如果有)
│ ... │ e.g. VMD 域 (Domain 10000)
└────────────────────────────────────┘# Linux 上查看 MCFG 内容
dmesg | grep -i "MCFG\|ECAM\|mmconfig"
# 示例输出: PCI: MMCONFIG for domain 0000 [bus 00-ff] at [mem 0xe0000000-0xefffffff]
# 或者从 sysfs 看
cat /sys/firmware/acpi/tables/MCFG | xxd | head -202.2 ECAM 地址计算公式
固件拿到 MCFG 基址后,ECAM 地址的计算公式就是 BDF 的直译:
ECAM_addr = MCFG_Base + (Bus << 20 | Device << 15 | Function << 12) + Register_Offset每个 PCIe 设备的 4KB 配置空间被完整映射到一段连续的 MMIO 地址中:
MCFG_Base = 0xE0000000
Bus 0, Dev 0, Func 0 → 0xE0000000 ~ 0xE0000FFF (4KB)
Bus 0, Dev 1, Func 0 → 0xE0008000 ~ 0xE0008FFF (4KB)
Bus 0, Dev 1, Func 1 → 0xE0009000 ~ 0xE0009FFF (4KB)
Bus 1, Dev 0, Func 0 → 0xE0100000 ~ 0xE0100FFF (4KB)
...
Bus 255, Dev 31, Func 7 → 0xEFFFFFF000 ~ 0xEFFFFFFF (4KB)关键:ECAM 窗口里的 256MB(Bus 0~255 × 32 Device × 8 Func × 4KB)是预留的映射空间,不是真的用了 256MB 物理内存。大部分 BDF 对应的是空槽位,读回来是
0xFFFFFFFF。 那怎么知道哪些 BDF 有设备? 没有"设备列表"可查——固件只能暴力探测:逐 Device 号发 Config Read TLP,读偏移 0x00 的 VID/DID。非0xFFFF= 设备存在,0xFFFF= 空槽位。这是 PCIe 协议层面的约定,不是硬件自动报告的。 外接设备(插在 PCIe Switch 下游)呢? Switch 下游的设备在 Bus > 0 上(例如插槽后面的显卡、NVMe 扩展卡),不在 Bus 0。固件枚举阶段看不到这些设备——RC 内部没有它们的 Bus 号→下游端口映射信息,必须等内核阶段③先读到 Switch 的 Type 1 配置空间,获取 Secondary/Subordinate Bus 号,才能递归扫描下游。简单说:固件只看 Bus 0,Switch 后面的交由内核发现。
2.3 固件如何初始化 RC 的地址解码器
// 伪代码:UEFI 固件在 PEI/DXE 阶段做的事
// 1. 从 ACPI MCFG 表读 ECAM 基址
MCFG_TABLE *mcfg = (MCFG_TABLE *)FindAcpiTable("MCFG");
uint64_t ecam_base = mcfg->ConfigBaseAddress;
// 2. 写 RC 内部寄存器,建立 ECAM 地址窗口
// (RC 的地址解码器是一组可编程寄存器,定义哪些物理地址走 PCIe)
// 具体寄存器因厂商而异(Intel/AMD/ARM 不同),但概念相同
RootComplex_WriteRegister(RC_ECAM_BASE, ecam_base);
RootComplex_WriteRegister(RC_ECAM_LIMIT, ecam_base + (256 << 20) - 1); // 256MB
RootComplex_WriteRegister(RC_ECAM_BUS_START, 0);
RootComplex_WriteRegister(RC_ECAM_BUS_END, 255);
// 3. 此时 CPU 执行 mov [ecam_base + offset] 就会被 RC 翻译成 Config TLP这也是为什么 pcie-pre-enumeration 反复强调"RC 是固定功能块"——但固定功能块也需要可编程窗口来告诉它"ECAM 基址在哪"。固件就是这个窗口的配置者。

三、Bus 0 扫描:发现设备的第一笔 Config TLP
本章目标:ECAM 窗口打通后,固件终于能"敲门"了——从 Bus 0 的 Device 0 开始,逐个发 Config Read TLP 读 VID/DID,把每个真实存在的设备登记入册。
ECAM 窗口只是铺好了路,路上有没有"房子"(设备)仍然未知。第一笔 Config TLP 的核心目的就一个——读配置空间偏移 0x00 的 VID/DID 寄存器:
| 读到什么 | 含义 | 下一步 |
|---|---|---|
VID=0x8086, DID=0x... | 设备存在(Intel 的某个 PCIe 设备) | 记录 BDF,读 Header Type,继续 BAR 发现 |
0xFFFFFFFF | 空槽位 | 跳过,下一 Device |
| Master Abort(RC 收不到 Completion) | Device 号超出 RC 硬件端口数 | 固件可以提前终止该 Bus 扫描 |
为什么先读 VID/DID 而不是 BAR? VID/DID 是设备的"身份证"——先确认有人,再问他要多大房间(BAR)。对一个不存在的 Device 发 BAR 读请求,只会白白等一个 Master Abort 超时。
3.1 扫描策略
固件对 Bus 0 的扫描是一个暴力穷举过程,没有捷径:
第一步:逐 Device 读 VID/DID。 Device 号从 0 到 31(一条 Bus 最多 32 个 Device),对每个 Device 用 ECAM 公式算出地址,发一次 Config Read TLP 读偏移 0x00。读回来的 32 位值就是 VID(低 16 位)和 DID(高 16 位)。如果读回 0xFFFFFFFF——空槽位,直接跳下一个 Device。
第二步:读 Header Type 区分角色。 如果 VID/DID 有效,说明设备存在,接着读配置空间偏移 0x0E 的 Header Type 寄存器:
| Header Type (bit 6:0) | 设备类型 | 固件行为 |
|---|---|---|
0x00 | Type 0 — Endpoint(iGPU / NVMe / USB 控制器等终端设备) | 继续 3.3 节的初始化流程 |
0x01 | Type 1 — PCIe Switch / Bridge | 记录存在,但不分配新 Bus 号、不递归扫描下游——留给内核 |
| bit 7 = 1 | 多 Function 设备 | 额外扫描 Function 1~7(一个物理插槽可能封装多个逻辑设备) |
为什么固件看到 Switch 也不递归? 要扫描 Switch 下游,必须先写 Switch 的 Type 1 配置空间分配 Secondary Bus 号——这等于固件要实现完整的 Bus 号管理器。但固件没有全局地址空间视图(不知道哪些 Bus 号会被内核后续用到的热插拔设备占用),贸然分配反而可能和内核冲突。所以固件的策略很清晰:认出 Switch 有下游 → 记录 → 跳过 → 让内核在阶段③统一分配。
3.2 第一笔 Config TLP 的硬件时序

3.3 固件拿到 VID/DID 后做什么
VID/DID 只告诉你"设备存在",接下来固件要回答三个问题:这是什么设备?需要启动它吗?怎么启动?

三条关键决策路径:
| 设备角色 | Class Code 示例 | 固件行为 |
|---|---|---|
| 引导显卡 | 0x03(显示控制器) | 分配 BAR → 执行 VBIOS/GOP ROM → 屏幕亮,安装 GOP |
| 系统盘 | 0x0108(NVMe) | 分配 BAR → 安装 NVM Express PassThru → 可读取引导扇区 |
| 引导网卡 | 0x02(网络控制器) | 分配 BAR → 执行 PXE ROM → 安装 SNP → 可 PXE 网络启动 |
| 非引导设备 | 任意 | 只记 VID/DID/Class Code → ACPI,不分配 BAR,驱程留给内核 |
为什么固件只给引导设备分配 BAR? 这里的"MMIO 地址池"指的是 CPU 物理地址空间中可用于映射 PCIe 设备 BAR 的区域,跟芯片组的 DRAM 大小无关——BAR 消耗的是地址线可寻址的范围,不是 RAM。 为什么可用的物理地址空间也有限? 固件阶段 CPU 跑在传统模式(16/32-bit),只能操作低 4GB 地址空间。而这 4GB 早已被瓜分殆尽:0-640KB 是传统内存、640KB-1MB 是 VGA/BIOS 固件区间、1MB~某处是 DRAM 实际地址、加上 APIC/HPET/ECAM 本身等硬件固定映射……真正能给 BAR 分配的连续空白区间通常只有几十 MB。给所有设备(USB 声卡、Thunderbolt 控制器等)都分配的话,不仅地址空间不够用,还会把低 4GB 地址彻底碎片化——后面 OS 内核想给大 BAR 设备(如 GPU 的 256MB BAR)找连续空间时就没位置了。 此外固件的 BAR 分配逻辑本身就是"够用就行":顺序分配、不做碎片整理、不预留扩展。固件的原则:够亮屏、够找到盘,其余不碰。
四、固件如何把信息传给内核
4.1 ACPI 表传递
固件枚举结束后,把 PCIe 设备信息写入 ACPI 表,内核启动时从 ACPI 读取:
| ACPI 表 | 传递了什么 |
|---|---|
| MCFG | ECAM 窗口基址 + 覆盖的 Bus 范围 → 内核知道往哪发 Config TLP |
| DSDT / SSDT | 设备树描述,每个设备一个 _HID/_CID/_ADR 节点,含 BDF 和资源分配(BAR 地址/中断线) |
| DMAR | DMA Remapping 表 → IOMMU 配置,DMA 地址翻译 |
| HPET / APIC | 定时器和中断控制器信息 |
4.2 内核的启动交接
// Linux 内核启动路径 (简化)
start_kernel()
└─ setup_arch()
├─ acpi_boot_table_init() // 解析 ACPI 表 (含 MCFG)
│ └─ 拿到 ECAM 基址
│
└─ pci_acpi_init() // 从 ACPI _ADR 节点获取固件的设备描述
└─ pci_acpi_scan_root()
└─ pci_scan_root_bus() // ← 内核重新扫描!
// 内核不信任固件的 BAR 分配
// 完全重新枚举 + 重新分配 BAR内核不信任固件:即使 UEFI 已经分配了临时 BAR,Linux 也会用
pci_scan_root_bus()全量重扫、重新分配 BAR。唯一保留的是固件的 MCFG 表(ECAM 窗口信息)和 ACPI 里的中断路由描述。
4.3 为什么固件不完整扫描
固件枚举只到 Bus 0,原因:
| 原因 | 说明 |
|---|---|
| 时间 | 全树递归扫描 + BAR 分配太慢,开机速度是固件的核心 KPI |
| 目的 | 固件只需要让用户看到 BIOS 界面 + 找到启动盘。Switch 下游的 FPGA/网卡/第二块 GPU 不影响引导 |
| 复杂度 | Switch 下游可能有几十个设备,固件做完整的资源分配意味着固件需要实现完整的 PCIe 资源管理器——和内核重复 |
| 交给内核 | 内核的 PCI 子系统更成熟,有更好的地址空间分配策略和冲突处理 |
五、固件枚举的关键产物
| 产物 | 对内核的作用 |
|---|---|
| MCFG 表 | 覆盖所有设备——ECAM 基址是全局基础设施,Bus 0~255 全部可访问,不管是 Bus 0 的板载设备还是 Switch 下游的外接设备 |
| DSDT/SSDT 设备描述 | 仅覆盖Bus 0 上已探测到的设备——固件暴力扫描过的那些 BDF。Boot 必需设备写了临时 BAR + 中断,非 Boot 设备只写 VID/DID/Class Code。Switch 下游的外接设备不在此列,等内核阶段③自己发现 |
| 已初始化的 Boot 设备 | 显卡能显示内核启动日志(printk 可见)、NVMe 能读内核镜像 |
| 中断路由描述 | _PRT(PCI Routing Table)告诉内核每个设备的 INTx 引脚连到哪个 IOAPIC 输入 |
六、与后续阶段的衔接
固件把 MCFG 表和设备描述交给内核后,内核开始全面重扫:
→ 进入 pcie-enumeration(阶段③:内核深度优先枚举 + 建树)
七、与现有文档的关系
- pcie-link-training — 阶段①:链路训练(固件枚举的物理前提)
- pcie-pre-enumeration — RC/BDF/BAR 硬件原理(固件发 Config TLP 的底层机制)
- pcie-enumeration — 阶段③:内核接管后的完整枚举(固件的后续)
八、一句话总结
固件枚举是 PCIe 设备发现的"轻量第一扫"——从 MCFG 表拿 ECAM 基址,遍历 Bus 0 发 Config TLP 读 VID/DID,只为显卡/NVMe/键盘等引导必需的设备分配临时 BAR 并初始化,然后通过 ACPI 表把拓扑信息交给内核。内核不信任固件的 BAR 分配,接管后全量重扫。