﻿# PCIe 内核枚举与拓扑构建

> [pcie-firmware-enum.md](./pcie-firmware-enum.md) 讲的是固件阶段——ECAM 窗口打通、Bus 0 暴力扫描、给引导设备分配临时 BAR、写 ACPI 表交接。本篇讲内核接管后的工作：**深度优先递归扫描整棵树、分配 Bus 号、探测 BAR 大小并分配物理地址、匹配驱动**。这是 PCIe 四阶段时间线中的阶段③。

## 一、枚举要做什么

固件交接后，内核手上有 ECAM 基址和 Bus 0 设备列表，但 Switch 下游（Bus > 0）的拓扑完全未知，固件的 BAR 分配也是临时凑合的。内核要做三件事：

1. **完整建树**——从 Bus 0 出发，遇到 Switch 就递归扫下游，直到发现所有设备
2. **全局统一分配资源**——BAR 地址、Bus 号、MSI/MSI-X 中断，统一从全局地址空间规划
3. **匹配驱动并 probe**——让每个设备有对应的驱动接管，最终在 `/dev/` 下可用

```plantuml
@startuml
skinparam shadowing false
skinparam DefaultFontSize 11
skinparam NoteBackgroundColor #FFF9C4

participant "固件\n(UEFI/BIOS)" as FW
participant "ACPI 表\n(MCFG+DSDT)" as ACPI
participant "Linux 内核\n(pci_scan_root_bus)" as KERNEL
participant "PCIe 总线" as BUS
participant "驱动" as DRV

== Phase 1: 固件交接 ==

FW -> ACPI : 写入 MCFG + DSDT/SSDT
ACPI --> KERNEL : 内核读取 ACPI 表

note over KERNEL
  已知: ECAM 基址, Bus 0 设备列表
  未知: Switch 下游拓扑, 全局 BAR 布局
end note

== Phase 2: 深度优先递归扫描 + Bus 号分配 + BAR 探测 ==

KERNEL -> BUS : Bus 0 逐 Device 扫 VID/DID
BUS --> KERNEL : Dev 0=>SATA(EP), Dev 1=>USB(EP)
KERNEL -> BUS : Dev 2 => VID 有效, 读 Header Type
BUS --> KERNEL : Header Type=0x01 (Switch!)
KERNEL -> BUS : 写 Pri=0, Sec=1, Sub=255 (武装 Switch)
note over BUS
  Switch 成为 Bus 1 路由器
  此后 Bus=1 的 Config TLP 自动向下游转发
end note

KERNEL -> BUS : 进入 Bus 1, Dev 0 => NVMe (EP)
KERNEL -> BUS : 探测 BAR → 16KB → 分配 0xF8000000

KERNEL -> BUS : Bus 1 Dev 1~31 全部 0xFFFF → 终止
KERNEL -> BUS : 回填 Switch Sub=1 (之前暂写 255)
KERNEL -> BUS : 回到 Bus 0, Dev 3~31 全空

== Phase 3: 驱动匹配 ==

KERNEL -> KERNEL : 建立 sysfs 节点 + pci_match_id()
KERNEL -> DRV : 调用 probe() → ioremap(BAR) → request_irq()
DRV --> KERNEL : 设备就绪, /dev/ 下可用

@enduml
```

**流程中的两个关键动作：**

- **发现 Switch → 立即武装**（写 Pri/Sec/Sub），之后 Bus=1 的 Config TLP 才能穿过 Switch 到达下游设备。这是递归的前提——不写 Bus 号寄存器，ECAM 窗口再大也穿不过 Switch。
- **发现 Endpoint → 探测 BAR 并停止递归**。内核通过写全 1 读回的方式测出 BAR 大小、从 MMIO 地址池分配物理地址、写入 BAR 寄存器。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 | 硅片固化 |

```bash
lspci -t -vv    # 树形拓扑
lspci           # BDF 列表
```

```bash
输出示例:
00:00.0 Host bridge → Root Complex
00:01.0 PCI bridge   → Switch 上游 (Bus 0 → Bus 1)
01:00.0 NVMe         → Bus 1 Endpoint
```

### 2.2 配置空间：Type 0 vs Type 1

内核扫描过程中不断读写的正是设备配置空间，Header 分两种：

```bash
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 往下挖：

```bash
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 核心调用链

```bash
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 的完整递归过程

```plantuml
@startuml
skinparam shadowing false
skinparam DefaultFontSize 11

participant "内核" as KERN
participant "Bus 0" as B0
participant "Switch\nB0:D2:F0" as S0 #FFCCBC
participant "Bus 1" as B1
participant "Switch\nB1:D1:F0" as S1 #FFCCBC
participant "Bus 2" as B2
participant "GPU EP\nB2:D0:F0" as GPU #A5D6A7

== 第一层: 发现 Switch B0:D2:F0 ==

KERN -> B0 : [1] Bus 0 扫描, Dev 2 => Switch
KERN -> S0 : [2] 写 Pri=0, Sec=1, Sub=255

note over S0
  Sec=1, Sub=255
  转发 Bus 1 的 TLP 到下游
end note

== 第二层: 递归 Bus 1, 又发现 Switch ==

KERN -> B1 : [3] 进入 Bus 1, Dev 1 => Switch!
KERN -> S1 : [4] 写 Pri=1, Sec=2, Sub=255

== 第三层: Bus 2 到达叶子 ==

KERN -> B2 : [5] 进入 Bus 2, Dev 0 => GPU EP (停止递归)
B2 --> KERN : Dev 1~31 全部 0xFFFF, 最深 Bus = 2

== 回溯: 自底向上回填 Subordinate ==

KERN -> S1 : [6] 回填 Sub=2 (从 255 收敛)
KERN -> S0 : [7] 回填 Sub=2
KERN -> KERN : [8] 回到 Bus 0 继续 Dev 3~31

@enduml
```

> **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.md](./pcie.md)。

---

## 四、BAR 探测与地址分配

> 先澄清最容易误解的三个概念：

| 概念 | 说明 |
|------|------|
| **"大小"是谁定的** | 设备硬件定死。芯片设计时决定了地址比较器旁路多少条低位地址线——旁路 16 位 → 64KB 窗口，旁路 12 位 → 4KB 窗口。BAR 声明的是"我的寄存器文件需要占多大物理地址空间"，不是内存条容量。 |
| **物理基址谁决定** | **内核分配，不是设备定死的。** 设备刚上电时 BAR 通常是 `0x00000000`。内核探测出 size 后从 MMIO 地址池分配空闲地址，**写进 BAR 寄存器**。设备只能决定"我需多大房子"，不能决定"我住哪条街"。 |
| **映射到内存吗** | **不。** BAR 映射到设备内部寄存器空间，不消耗 DRAM。CPU 发出 `mov [BAR地址]` 后，RC 截获并转成 Memory TLP 发到设备，而不是访问 DRAM 控制器。MMIO 占用的物理地址段在 ACPI 表中标记，`memblock` 会跳过它不分配给 RAM。 |

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

```plantuml
@startuml
skinparam shadowing false
skinparam DefaultFontSize 11

participant "内核" as KERN
participant "Endpoint\nBAR 寄存器" as EP

== Step 1-2: 读原始值(记住现场) ==

KERN -> EP : Config Read → BAR0
EP --> KERN : 0x00000000

== Step 3-6: 写全 1 探测大小 ==

KERN -> EP : Config Write → BAR0, 写入 0xFFFFFFFF
note over EP
  bit 15:0 硬连线为 0 → 写不进去
  bit 16+ 可写
end note
EP --> KERN : 读回 0xFFFF0000

KERN -> KERN : Size = ~(0xFFFF0000 & ~0xF) + 1\n    = 0x00010000 = 64KB

== Step 7-9: 恢复 + 分配 + 写入基址 ==

KERN -> EP : Config Write → BAR0, 写回 0x00000000 (恢复)
KERN -> KERN : 从 iomem_resource 分配 0xF8000000 (64KB 对齐)
KERN -> EP : Config Write → BAR0, 写入 0xF8000000

note over EP
  BAR0 = 0xF8000000
  此后 CPU 可通过 MMIO 访问设备寄存器
end note

@enduml
```

**读回值为什么不是 `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 访问

```bash
CPU 执行:  mov [0xF8000400], 42

拆开 32-bit 地址:
┌──────────────────┬──────────────────────┐
│   bit 31:16      │      bit 15:0        │
│   0xF800         │      0x0400          │
│   ↑ 送比较器      │      ↑ 旁路,不比较    │
│   与 BAR 高位比较  │   直接当设备内部偏移用  │
└──────────────────┴──────────────────────┘
```

```plantuml
@startuml
skinparam shadowing false
skinparam DefaultFontSize 10

rectangle "  CPU 发出的物理地址\n0xF8000400  " as ADDR #E3F2FD
rectangle "  BAR0 寄存器\n(base = 0xF8000000)  " as BARREG #FFF8E1

rectangle "  <b>地址比较器</b> (设备端硬件)  " as CMP #C8E6C9 {
  rectangle "比较 bit 31:16\n0xF800 == 0xF800 ?" as HI_CMP
  rectangle "bit 15:0 不参与比较\n旁路 → 内部偏移" as LO_BYPASS
}

rectangle "  <b>设备内部寄存器</b>\n(共 64KB)  " as DEV #FFCCBC {
  rectangle "偏移 0x0000\nDoorbell\n\n偏移 0x0400  ← 命中!\n偏移 0x0404\n...\n偏移 0xFFFF" as REGS
}

ADDR -down-> CMP : 地址同时进比较器
HI_CMP -down-> LO_BYPASS : bit 31:16 匹配\n地址属于本设备
LO_BYPASS -down-> REGS : bit 15:0 = 0x0400\n→ 写内部偏移 0x0400 的寄存器

note bottom of CMP
  <b>窗口大小 = 2^(旁路位数)</b>
  旁路 16 位 → 64KB 窗口
  旁路 12 位 → 4KB 窗口
  旁路 28 位 → 256MB 窗口
  旁路位数由芯片设计固定
  跟 BAR 的 32-bit 位宽无关
end note

@enduml
```

**三句话记住：**
- 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 = 可选额外接口——最灵活，无惯例 |

```bash
lspci -s 01:00.0 -vv    # 查看单设备完整 BAR 布局
# 输出: Region 0: Memory at f7000000 (64-bit, non-prefetchable) [size=16K]
```

### 4.5 全局 MMIO 资源池：`struct resource` 树

内核在资源树中统一管理所有物理地址空间：

```bash
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 ~ 0x7FFFFFFF
```

MMIO 范围由 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 分配过程序列图

```plantuml
@startuml
skinparam shadowing false
skinparam DefaultFontSize 11

participant "内核\npci_bus_assign_resources" as KERN
participant "资源树\niomem_resource" as RES
participant "Bridge\n(Switch 上游端口)" as BR
participant "Endpoint\nBAR 寄存器" as EP

== Step 1: 收集设备 BAR 总需求 ==

KERN -> KERN : 遍历所有 BAR: BAR0=64KB, BAR1=4KB

== Step 2: 按 size=align 逐个分配 ==

KERN -> RES : allocate_resource(size=64KB, align=64KB, <4GB)
RES --> KERN : 0xF8000000 ~ 0xF800FFFF (64KB)
KERN -> RES : allocate_resource(size=4KB, align=4KB)
RES --> KERN : 0xF8010000 ~ 0xF8010FFF (4KB)

== Step 3: 计算 Bridge 下游窗口范围 ==

KERN -> KERN : 收集下游所有 BAR 范围\niomem_resource 中标记 Union
KERN -> KERN : 为 32-bit/64-bit 各建一个窗口

== Step 4: 写入 Bridge + Device 寄存器 ==

KERN -> BR : Config Write → Bridge\nMemory Base=0xF800, Limit=0xF801 (高 12 位, 粒度 1MB)
KERN -> EP : Config Write → BAR0 = 0xF8000000
KERN -> EP : Config Write → BAR1 = 0xF8010000
KERN -> EP : Config Write → Command Register\nMemory Space Enable = 1

== Step 5: 递归向上合并 ==

KERN -> KERN : 父 Bridge 窗口 = 所有子 Bridge 窗口的并集\n递归向上调整直到 Root Bus

@enduml
```

关键约束：

| 约束 | 说明 |
|------|------|
| **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 窗口的并集 |

内核代码入口：

```c
// 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.md](./pcie-bar-activation.md)。

---

## 五、驱动匹配与 probe

建树完成、BAR 分配完毕后，内核用 `pci_match_id()` 对比设备 VID/DID 和已注册驱动的 `pci_device_id` 表：

```c
// 驱动侧声明
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 下有一个节点：

```bash
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.md](./pcie.md) | 物理基础——拓扑/Lane/三层协议/MMIO；路由机制详见该篇 |
| [pcie-link-training.md](./pcie-link-training.md) | 阶段①——链路训练，枚举的物理前提 |
| [pcie-pre-enumeration.md](./pcie-pre-enumeration.md) | RC/BDF/BAR 硬件原理，枚举依赖的底层机制 |
| [pcie-firmware-enum.md](./pcie-firmware-enum.md) | 阶段②——固件枚举，内核建树的前置阶段 |
| [pcie-bar-activation.md](./pcie-bar-activation.md) | 阶段④——BAR 激活 + 驱动 probe 的完整 MMIO 路径 |
| [io-addressing.md](../io-addressing.md) | MMIO/PMIO 编址基础 |
| [dma.md](../dma.md) | DMA 依赖 BAR 分配和地址路由 |

---

## 七、一句话总结

> **内核 PCIe 枚举的本质是深度优先递归建树——从 Bus 0 出发，遇 Switch 就分配 Bus 号、写 Pri/Sec/Sub、递归进入子总线，遇 Endpoint 则探测 BAR 大小、分配物理地址并写入 BAR，整棵树建完后匹配驱动 probe，最终每个设备在 `/sys/bus/pci/devices/BDF/` 下可见。**

