﻿# 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 就已经在工作了。

```plantuml
@startuml
skinparam shadowing false

rectangle "CPU Core\n(用户态/内核态)" as CPU
rectangle "Root Complex\n=======\n· 地址解码器\n· TLP 生成器\n· TLP 解析器\n· 中断路由" as RC
rectangle "PCIe 总线" as BUS

CPU -down-> RC : ① mov [ECAM_addr], val\n或 in/out 0xCF8/0xCFC
note right of RC
  地址解码器判断:
  · 地址落在 ECAM 窗口?
    → 生成 Config TLP
  · 地址落在 BAR 窗口?
    → 生成 Memory Read/Write TLP
  · 落在 IO 端口 0xCF8/0xCFC?
    → 生成 Config TLP (传统方式)
end note
RC -down-> BUS : ② 发 TLP 到总线上
BUS -up-> RC : ③ Completion TLP 返回
RC -up-> CPU : ④ 结果放入 RAX/目标寄存器
@enduml
```

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 内部有一组硬连线（或可编程寄存器）定义了几段地址窗口：

```bash
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。

```bash
          Root Complex
    ┌─────────┼─────────┐
   Port 0    Port 1    Port 2  ...   ← RC 的物理端口号  = Bus 0 的 Device 号
    ↓         ↓         ↓
  [空]     [NVMe]    [空]
Dev=00    Dev=01    Dev=02
```

> **Bus 0 的 Device 号在枚举前就已经"有了"**——它是 RC 硬件端口物理决定的。固件/OS 枚举只是去**读**这些端口，看对应 Device 是否存在（VID != 0xFFFF）。

### 2.2 BDF → ECAM 地址 → Config TLP

在树构建前，BDF 的唯一作用就是**算 ECAM 地址**：

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

示例：读 Bus 0, Dev 1, Func 0 的 VID/DID（偏移 0x00），设 MCFG_Base = 0xE0000000：

```bash
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 上
```

**整个流程**：

```bash
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 不是一个普通的内存单元——它是一个**有特殊行为的硬件寄存器**：

```bash
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 空间大小**不直接存在某个寄存器里**——但可以通过硬件行为反推：

```bash
探测流程:

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 的状态变化

```bash
时间线:

上电复位
  │  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

```plantuml
@startuml
skinparam shadowing false

concise "硬件\n物理层" as HW
concise "RC\n(硬件)" as RC
concise "固件\nUEFI/BIOS" as FW
concise "内核\nLinux" as KERNEL
concise "PCIe\n设备" as DEV

@0
HW is "断电"
RC is "断电"
FW is "未运行"
KERNEL is "未运行"
DEV is "断电"

@1
HW is "上电\n链路训练"

DEV is "L0 就绪"
RC is "就绪"

@2
FW is "读取 MCFG 表\n设置 ECAM 窗口"
RC is "ECAM 窗口已映射"

@3
FW is "扫描 Bus 0\n读 VID/DID"
RC is "生成 Config TLP"
DEV is "返回 VID/DID"

@4
FW is "分配临时 BAR\n(仅Boot设备)"

@5
KERNEL is "接管\npci_scan_root_bus()"

@6
KERNEL is "深度优先扫描\n· 读 VID/DID\n· 探测 BAR size\n· 分配 BAR 地址\n· 分配 Bus 号(桥)"
RC is "持续生成\nConfig TLP"
DEV is "响应 Config TLP\n返回配置空间"

@7
KERNEL is "建立 sysfs\n匹配驱动 probe()"
DEV is "MMIO 可用\n设备就绪"

@enduml
```

### 4.1 第一步：硬件链路训练（0~100ms）

不需要任何软件。PCIe 物理层的 LTSSM（Link Training and Status State Machine）自动运行：

```bash
Detect → Polling → Configuration → L0
  ↑                                    │
  └──────────── Recovery ←─────────────┘  (出错时)
```

**L0 状态**意味着：两端可以交换 TLP 了。但此时软件还不知道设备存在。

### 4.2 第二步：固件发出第一笔 Config TLP

固件（UEFI/BIOS）通过两种方式之一发出 Config Read TLP：

**方式一：ECAM（现代）**

```c
// 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）**

```c
// 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 TLP
```

### 4.3 第三步：内核递归扫描

内核拿到控制权后，重复"读 VID/DID → 判断类型 → 如果是桥就分配新 Bus 号 → 递归"直到整棵树被发现。详细过程见 [pcie-enumeration.md](./pcie-enumeration.md) §三。

---

## 五、关键机制的对照总结

### 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.md](./pcie-link-training.md) —— 阶段①：链路训练（本篇所有机制的物理前提——L0 就绪才能发 TLP）
- [pcie.md](./pcie.md) —— 拓扑/Lane/三层协议/MMIO，本篇的"前置知识"
- [pcie-firmware-enum.md](./pcie-firmware-enum.md) —— 阶段②：固件枚举（ECAM窗口/MCFG表/第一笔Config TLP，本篇机制的软件应用）
- [pcie-enumeration.md](./pcie-enumeration.md) —— 阶段③：内核完整枚举 + 路由（本篇的"后续"）
- [io-addressing.md](../io-addressing.md) —— PMIO vs MMIO 编址，`mov [addr]` 如何穿过 RC 变成 TLP

## 七、一句话总结

> **树构建前，系统靠三样东西就能发现设备：① 链路训练（硬件自动，无需软件）→ ② RC（CPU 侧的固定功能 TLP 引擎，上电即工作）→ ③ Config TLP（通过 ECAM/IO 端口发出，不依赖 BAR）。BDF 在这个阶段只是 ECAM 地址的计算参数，BAR 只是一个被读写的配置空间寄存器——用来反问硬件"你需要多大 MMIO 空间？"。直到 OS 把物理基址写入 BAR，设备地址解码器才激活，MMIO 才真正可用。**

