﻿# 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 拓扑与固件枚举的边界

```plantuml
@startuml
skinparam shadowing false
skinparam DefaultFontSize 11
skinparam packageBorderColor #555555

package "CPU 封装" #F8F8F8 {
  package "Root Complex (RC)" #EEEEEE {
    component "Root Port ①" as RP1
    component "Root Port ②" as RP2
    component "Root Port" as RP3
  }
}

rectangle "<color:green><b>固件可见 → Bus 0</b></color>" as VISIBLE #F0FFF0 {
  component "iGPU\nB0:D0:F0\nVID=8086" as IGPU
  component "板载 NVMe\nB0:D1:F0\nVID=144D" as NVME
  component "PCIe Switch\nB0:D2:F0\n(Type 1 Header)" as SWITCH
}

rectangle "<color:red><b>固件不可见 → Bus > 0</b></color>" as INVISIBLE #FFF0F0 {
  component "独显 GPU\nB1:D0:F0\n(x16 插槽)" as DGPU
  component "NVMe 扩展卡\nB1:D1:F0\n(M.2 转接)" as NVME2
}

RP1 --> IGPU
RP2 --> NVME
RP3 --> SWITCH
SWITCH --> DGPU
SWITCH --> NVME2

note bottom of INVISIBLE
  <b>Switch 自身在 Bus 0, 固件可枚举</b>
  但下游 Bus 号存在 Switch 的 Type 1 配置空间里
  固件不递归解析, <color:red>留白给内核阶段③</color>
end note

@enduml
```

**链路训练完成后，固件面对的问题：**

| 已知 | 未知 |
|------|------|
| 物理链路已 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 的硬件级时序
- 四章（交接）讲固件→内核的信息传递，五章总结产物

## 零、总览：固件枚举在时间线上的位置

```bash
按下电源键
    │
    ▼
┌──────────────────────────────────────────────────────────────┐
│ ① 硬件链路训练 (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 读 |

```bash
┌─────────────────────┐
│ 芯片组 (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"。

```bash
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)
└────────────────────────────────────┘
```

```bash
# 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 -20
```

### 2.2 ECAM 地址计算公式

固件拿到 MCFG 基址后，ECAM 地址的计算公式就是 BDF 的直译：

```bash
ECAM_addr = MCFG_Base + (Bus << 20 | Device << 15 | Function << 12) + Register_Offset
```

每个 PCIe 设备的 4KB 配置空间被完整映射到一段连续的 MMIO 地址中：

```bash
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 的地址解码器

```c
// 伪代码：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.md](./pcie-pre-enumeration.md) 反复强调"RC 是固定功能块"——但固定功能块也需要**可编程窗口**来告诉它"ECAM 基址在哪"。固件就是这个窗口的配置者。

```plantuml
@startuml
skinparam shadowing false
skinparam DefaultFontSize 11
skinparam participant {
  Padding 50
}

participant "平台寄存器\n(芯片组固化)" as PLAT
participant "固件\n(UEFI/BIOS)" as FW
participant "ACPI 表\n(RAM)" as ACPI
participant "Root Complex\n(地址解码器)" as RC
participant "CPU" as CPU

== ① 获取 ECAM 基址 ==

FW -> PLAT : 读取 ECAM 基址寄存器
PLAT --> FW : ECAM_Base = 0xE0000000
note right of PLAT : 硬件固化的值\nIntel 平台常见 0xE0000000

== ② 写入 ACPI MCFG 表 ==

FW -> ACPI : 写入 MCFG 表 Entry 0\nBase=0xE0000000, Seg=0\nBus Start=0, Bus End=255
note right of ACPI : 内核启动时\n从这读 ECAM 基址

== ③ 编程 RC 地址解码器 ==

FW -> RC : 写 RC_ECAM_BASE = 0xE0000000
FW -> RC : 写 RC_ECAM_LIMIT = 0xE0000000 + 256MB - 1
FW -> RC : 写 RC_ECAM_BUS_START = 0
FW -> RC : 写 RC_ECAM_BUS_END = 255

note over RC
  <b>RC 地址解码器已编程</b>
  · 物理地址 0xE0000000 ~ 0xEFFFFFFF
    → 翻译为 Config TLP
  · 之前: 全 0, 任何 ECAM mov 都当非法地址丢弃
  · 现在: ECAM 窗口 <color:green>生效</color>
end note

== ④ 验证 ECAM 窗口 ==

CPU -> RC : mov eax, [0xE0000000]\n(Bus=0, Dev=0, Func=0, Reg=0)
RC -> RC : 地址落在 ECAM 窗口内\n→ 提取 B=0, D=0, F=0, R=0\n→ 生成 Config Read TLP
RC --> CPU : TLP 发出, 等待 Completion

note over FW, CPU
  <b>ECAM 窗口打通!</b>
  · 此后固件遍历 Bus 0 的 Device 0~31
  · 每个 mov [ECAM + offset] 都触发一次 Config TLP
  · MCFG 表保留在 RAM, 内核启动时读取
end note

@enduml
```

---

## 三、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 的硬件时序

```plantuml
@startuml
skinparam shadowing false
skinparam ParticipantPadding 80
skinparam BoxPadding 20

participant "UEFI 固件" as FW
participant "CPU Core" as CPU
participant "Root Complex" as RC
participant "Bus 0" as BUS
participant "Endpoint\n(NVMe, Dev 1)" as EP

== 链路训练已完成, L0 就绪 ==

FW -> CPU : ① mov eax, [ECAM + Dev1 VID/DID]
CPU -> RC : ② 物理地址落入 RC 外部总线
RC -> RC : ③ RC 地址解码: ECAM 窗口内\n→ 提取 B=0,D=1,F=0,R=0
RC -> BUS : ④ Config Read TLP (Type 0)\nBus=0 Dev=1 Func=0 Reg=0
BUS -> EP : ⑤ TLP 到达 Dev 1, BDF 匹配
EP -> BUS : ⑥ Completion TLP\nData=0x144D_A808 (VID=三星,DID=NVMe)
BUS -> RC : ⑦ Completion 返回
RC -> CPU : ⑧ RAX = 设备 VID/DID
CPU -> FW : ⑨ VID != 0xFFFF → 设备存在!

@enduml
```

### 3.3 固件拿到 VID/DID 后做什么

VID/DID 只告诉你"设备存在"，接下来固件要回答三个问题：**这是什么设备？需要启动它吗？怎么启动？**

```plantuml
@startuml
skinparam shadowing false
skinparam DefaultFontSize 11
skinparam participant {
  Padding 50
}

participant "固件" as FW
participant "设备\n(NVMe, Dev 1)" as DEV
participant "Option ROM\n(设备自带)" as ROM
participant "ACPI 表\n(RAM)" as ACPI

== ① 读 Class Code, 判断设备类型 ==

FW -> DEV : Config Read TLP, 偏移 0x08\n读 Class Code + Subclass
DEV --> FW : Class Code = 0x0108\n(大容量存储 → NVMe)
note right of FW : Class Code 0x01=存储\\n0x02=网络 0x03=显示\\n0x06=桥设备

== ② 判断: 是否引导必需? ==

FW -> FW : 对比 Boot Order:\nNVMe = 系统盘 → YES
note right of FW : 不是 Boot 必需 →\\n只记 VID/DID 到 ACPI,\\n不分配 BAR, 留给内核

== ③ 分配临时 BAR ==

FW -> FW : 从固件 MMIO 地址池\n取一段: 0xFC000000~0xFC003FFF (16KB)
FW -> DEV : Config Write TLP, BAR0\n写入 0xFC000000
FW -> DEV : Config Write TLP, Command Register\nbit 1 (Memory Space) = 1
note over DEV
  <b>设备内存空间已使能</b>
  BAR0 = 0xFC000000
  此后 CPU 可 MMIO 访问 NVMe 寄存器
end note

== ④ 执行 Option ROM (如有) ==

FW -> ROM : 读 Expansion ROM BAR\n映射 ROM 到内存
FW -> ROM : 执行 Option ROM 初始化代码\n(x86 实模式)
ROM --> FW : 初始化完成\n· NVMe 固件自检通过\n· OpRegion 注册\n· 返回 Legacy/EFI 入口点

== ⑤ 安装 EFI 协议 ==

FW -> FW : 安装 NVM Express PassThru Protocol
note over FW
  <b>设备已就绪:</b>
  · GOP (显卡) → 屏幕亮
  · NVMe PassThru → 可读引导扇区
  · SNP (网卡) → 可 PXE 启动
end note

== ⑥ 记录到 ACPI ==

FW -> ACPI : 写入 DSDT/SSDT\n· _ADR = BDF\n· _HID / _CID 设备标识\n· BAR 地址 + 中断路由 (_PRT)

@enduml
```

**三条关键决策路径**：

| 设备角色 | 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 内核的启动交接

```c
// 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 子系统更成熟，有更好的地址空间分配策略和冲突处理 |

---

## 五、固件枚举的关键产物

```plantuml
@startuml
skinparam shadowing false
skinparam BoxPadding 15
skinparam DefaultFontSize 12

rectangle "固件枚举产物" as OUTPUT {
  rectangle "ECAM 窗口已映射" as ECAM
  rectangle "ACPI 表已构建" as ACPI
  rectangle "Boot 设备可用" as BOOT
  rectangle "Bus 0 设备列表" as BUS0
}

ECAM --> ACPI : MCFG 表记录 ECAM 基址
ACPI --> BOOT : DSDT/SSDT 描述\nBoot 设备 BAR/中断
BOOT --> BUS0 : 显卡/NVMe/USB\n已初始化
BUS0 --> ECAM : 后续内核通过\nECAM 发 Config TLP

@enduml
```

| 产物 | 对内核的作用 |
|------|------------|
| **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.md](./pcie-enumeration.md)（阶段③：内核深度优先枚举 + 建树）

---

## 七、与现有文档的关系

- [pcie-link-training.md](./pcie-link-training.md) — 阶段①：链路训练（固件枚举的物理前提）
- [pcie-pre-enumeration.md](./pcie-pre-enumeration.md) — RC/BDF/BAR 硬件原理（固件发 Config TLP 的底层机制）
- [pcie-enumeration.md](./pcie-enumeration.md) — 阶段③：内核接管后的完整枚举（固件的后续）

---

## 八、一句话总结

> **固件枚举是 PCIe 设备发现的"轻量第一扫"——从 MCFG 表拿 ECAM 基址，遍历 Bus 0 发 Config TLP 读 VID/DID，只为显卡/NVMe/键盘等引导必需的设备分配临时 BAR 并初始化，然后通过 ACPI 表把拓扑信息交给内核。内核不信任固件的 BAR 分配，接管后全量重扫。**

