﻿# PCIe 阶段④：BAR 激活 → 地址解码器生效 → 设备真正可用

> 内核 `pci_scan_root_bus()` 完成后，所有设备的 BAR 已被分配了物理地址——但设备**还没真正响应 MMIO**。为什么？因为 BAR 寄存器里写入了基址只是第一步，设备的地址解码器需要这个基址来激活。而地址解码器激活后，CPU 的 `mov [BAR_addr]` 穿过 Store Buffer、缓存层级、RC，最终变成 TLP 的过程才是 MMIO 真正工作的时刻。最后，内核 `pci_match_id()` 匹配驱动 → `probe()` → `ioremap()` → 设备从"被发现"变成"可用"。本篇拆解这最后一公里的全过程。

## 零、总览：BAR 激活在时间线上的位置

```bash
按下电源键
    │
    ▼
┌──────────────────────────────────────────────────────────────┐
│ ① 硬件链路训练 → L0 就绪                                    │
├──────────────────────────────────────────────────────────────┤
│ ② 固件扫描 Bus 0 → 分配临时 BAR → ACPI 表交接              │
├──────────────────────────────────────────────────────────────┤
│ ③ 内核深度优先枚举 → 建树 → 读配置空间 → 分配 BAR 地址      │
│    此时 BAR 寄存器里已经有值了，但...                         │
├──────────────────────────────────────────────────────────────┤
│ ④ BAR 激活 + 地址解码器生效 + 驱动 probe()                  │
│    · BAR 写入 → 地址解码器激活                               │
│    · MMIO 路径：CPU mov → Store Buffer → RC → TLP            │
│    · 驱动匹配 + ioremap + probe()                            │
│    · 设备从"被发现"到"可用"                                  │
│    ← 本篇                                                      │
└──────────────────────────────────────────────────────────────┘
```

| BAR 激活完成意味着 | 没激活意味着 |
|-------------------|-------------|
| CPU 的 `mov [BAR_addr]` 能变成 Memory TLP 到达设备 | 读写 BAR 地址 → 无响应 / 全 F 返回 |
| 驱动可以 `ioremap` + 读写设备寄存器 | `ioremap` 成功但读写无意义 |
| DMA 可以发起（设备能发 Memory Read/Write TLP 到主机内存） | 设备不能主动发起 DMA |

---

## 一、核心要点

- **BAR 写入基址 ≠ 设备立刻响应 MMIO**：地址解码器需要基址来激活，但中间还有 Store Buffer / WC Buffer / RC 多级路径
- **MMIO 读的延迟远高于 MMIO 写**：写是 Posted（CPU 发出不管），读是 Non-Posted（CPU 等 Completion）
- **驱动匹配是最后一步**：`pci_match_id()` → `pci_driver.probe()` → `ioremap(BAR)` → `request_irq(MSI)`
- **probe 成功后设备才真正可用**：用户态 `/dev/` 节点出现，应用程序可以 open/read/write

---

## 二、BAR 激活：地址解码器的工作原理

### 2.1 地址解码器的硬件设计

每个 PCIe Endpoint 内部有一个**地址解码器（Address Decoder）**，它是一组硬件比较器：

```bash
Endpoint 内部:

        BAR0 = 0xF8000000 (64KB, 由内核写入)
        BAR1 = 0xF8010000 (4KB,  由内核写入)
        BAR2 = 未使用 (0x00000000)
        ...

TLP 到达 ──→ ┌─────────────────────────────┐
             │  地址解码器                    │
             │                              │
             │  if addr in [0xF8000000,      │
             │              0xF800FFFF]:     │
             │    → 路由到 BAR0 对应的寄存器组│
             │                              │
             │  elif addr in [0xF8010000,    │
             │                0xF8010FFF]:   │
             │    → 路由到 BAR1 对应的寄存器组│
             │                              │
             │  else:                        │
             │    → Unsupported Request (UR) │
             └─────────────────────────────┘
                      ↓
              设备内部逻辑 (控制寄存器 / 数据 FIFO / DMA 引擎)
```

> BAR 写入基址后，地址解码器的比较器窗口就生效了。在此之前（BAR=0），没有任何地址落在 [0, size-1] 内（除非 OS 把物理地址 0 分配给了设备——这不会发生），所以设备不响应任何 MMIO。

### 2.2 BAR 写入 vs 地址解码器激活的时序

```bash
OS 写入 BAR:
  1. Config Write TLP: 目标=BDF, Reg=BAR0, Data=0xF8000000
  2. 设备接收 TLP, BAR0 寄存器更新为 0xF8000000
  3. 硬件自动: BAR0 的值输出到地址解码器的比较器输入
  4. 地址解码器激活: 窗口 [0xF8000000, 0xF800FFFF] 生效

  → 整个过程的延迟: 一个 Config Write TLP 的往返时间 (~几百 ns)
  → 不需要软件再做什么, 硬件自动完成
```

### 2.3 内核分配 BAR 地址的策略

```c
// Linux 内核分配 BAR 地址的核心逻辑 (简化)

// 1. 收集所有设备的 BAR size
for each device in bus:
    for each BAR in device:
        read BAR size (写全1读回 trick)

// 2. 从全局 MMIO 地址池分配
//    内核维护一个 MMIO 地址分配器，按 size 对齐分配
for each device in bus (按照"先大后小"的策略，减少碎片):
    for each BAR in device:
        addr = allocate_aligned(mmio_pool, BAR_size, BAR_alignment);
        write BAR register = addr;
        // 此时设备的地址解码器激活
```

> **关键设计**：内核按 BAR size 从大到小分配（大的先占连续空间），避免小 BAR 碎片堵住大 BAR 的对齐要求。

---

## 三、MMIO 路径：CPU `mov [BAR_addr]` → 设备收到 TLP

### 3.1 完整的 MMIO 写路径

```plantuml
@startuml
skinparam shadowing false
skinparam ParticipantPadding 70
skinparam BoxPadding 15

participant "CPU Core" as CPU
participant "Store Buffer" as SB
participant "Root Complex" as RC
participant "PCIe Bus" as BUS
participant "Endpoint" as EP

CPU -> SB : ① mov [BAR_addr], eax (MMIO Write)
SB -> RC : ② MMIO 窗口 (UC/WC)\n不进 cache, 直接转发到 RC
RC -> RC : ③ RC 地址解码:\n匹配 BAR 窗口 → Memory Write TLP
RC -> BUS : ④ Memory Write TLP\nAddr=BAR_addr, Data=eax
BUS -> EP : ⑤ TLP 经 Switch 地址路由 → Endpoint
EP -> EP : ⑥ 地址解码器匹配 → 写入寄存器

note over CPU, EP
  <b>Memory Write TLP 是 Posted</b>
  CPU 发出后不等待完成, 延迟 ~几百 ns
  但 CPU 不知道设备是否收到 (无 Completion)
end note

@enduml
```

### 3.2 完整的 MMIO 读路径

```plantuml
@startuml
skinparam shadowing false
skinparam ParticipantPadding 70
skinparam BoxPadding 15

participant "CPU Core" as CPU
participant "Root Complex" as RC
participant "PCIe Bus" as BUS
participant "Endpoint" as EP

CPU -> RC : ① mov eax, [BAR_addr] (MMIO Read)
RC -> RC : ② 地址解码: MMIO 窗口 → Memory Read TLP
RC -> BUS : ③ Memory Read TLP (Addr=BAR_addr)
BUS -> EP : ④ TLP 到达 Endpoint
EP -> EP : ⑤ 地址解码器匹配 → 读寄存器值
EP -> BUS : ⑥ Completion TLP (Data=寄存器值)
BUS -> RC : ⑦ Completion 返回 RC
RC -> CPU : ⑧ RAX = 寄存器值

note over CPU, EP
  <b>Memory Read TLP 是 Non-Posted</b>
  CPU 必须等 Completion 返回, 延迟 ~500ns~1μs
  MMIO 读比写慢数倍的根本原因
end note

@enduml
```

### 3.3 MMIO Write vs Read 延迟对比

| | MMIO Write (Posted) | MMIO Read (Non-Posted) |
|------|:---:|:---:|
| CPU 发出去后 | 立刻返回，继续执行 | **暂停**，等 Completion |
| TLP 类型 | Memory Write | Memory Read |
| 是否阻塞 CPU | 不阻塞（Store Buffer 吸收） | **阻塞**（in-order 核心会 stall） |
| 典型延迟 | ~100-200ns | ~500ns-1μs |
| 可靠性 | 不保证设备收到（PCIe 链路层保证但不保证设备处理） | 保证（拿到数据才返回） |
| 对 CPU pipeline 的影响 | 几乎无 | 显著（out-of-order 核心可部分缓解） |

> **工程建议**：高性能驱动避免在热路径做 MMIO Read。用 MMIO Write 发命令，用 MSI-X 中断或轮询内存标志位（DMA 写入）来感知完成。

---

## 四、驱动匹配与 probe()：设备的"最后一步"

### 4.1 驱动匹配流程

```c
// Linux PCI 驱动匹配流程 (简化)

// 内核枚举完所有设备后，调用 pci_bus_add_devices()
// → 为每个设备触发驱动匹配

pci_bus_add_devices()
  └─ for each pci_dev:
       device_add(&pci_dev->dev)        // 注册到 Linux 设备模型
         └─ bus_probe_device()
              └─ pci_device_probe()
                   ├─ 遍历 pci_bus_type 的驱动链表
                   ├─ pci_match_id(driver->id_table, pci_dev)
                   │    // 比对 VID/DID/SubVID/SubDID/Class
                   │    if 匹配:
                   │        driver->probe(pci_dev)
                   │          ├─ pci_enable_device()          // 使能设备
                   │          ├─ pci_request_regions()        // 独占 BAR 区域
                   │          ├─ ioremap(BAR0, size)          // 物理地址→内核虚拟地址
                   │          ├─ pci_alloc_irq_vectors()      // 分配 MSI/MSI-X
                   │          ├─ request_irq()                // 注册中断 handler
                   │          ├─ 初始化设备硬件寄存器          // 通过 ioremap 的地址写 BAR
                   │          └─ 注册 /dev/ 节点 / sysfs / char device
                   │
                   └─ 无匹配 → 设备保持"未绑定"状态
```

### 4.2 probe() 里的关键动作

```c
// 一个典型的 NVMe 驱动 probe() 示例 (极度简化)

static int nvme_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    // 1. 使能设备 (设置 PCI_COMMAND 寄存器: Bus Master + MMIO + MSI)
    pci_enable_device_mem(pdev);
    pci_set_master(pdev);  // 允许设备发起 DMA (Bus Mastering)

    // 2. 独占 BAR 区域 (防止其他驱动也 map 同一块)
    pci_request_regions(pdev, "nvme");

    // 3. ioremap: 把 BAR0 的物理地址映射到内核虚拟地址
    //    之后就能用 writel() / readl() 访问设备寄存器了
    void __iomem *bar0 = pci_ioremap_bar(pdev, 0);
    // bar0 现在指向 NVMe 控制器的寄存器 (SQ/CQ 门铃等)

    // 4. 分配 MSI-X 中断向量
    int nvecs = pci_alloc_irq_vectors(pdev, 1, 16, PCI_IRQ_MSIX);
    // 每个 IO 队列一个中断向量, 分散到不同 CPU 核

    // 5. 注册中断 handler
    request_irq(pci_irq_vector(pdev, 0), nvme_irq, 0, "nvme0", queue);

    // 6. 初始化硬件
    writel(0, bar0 + NVME_REG_CC);       // 禁用控制器
    writel(1, bar0 + NVME_REG_AQA);      // 设置 Admin Queue 深度
    // ... 设置 Admin SQ/CQ 的物理地址 (DMA 地址)
    writel(1, bar0 + NVME_REG_CC);       // 使能控制器

    // 7. 注册块设备 /dev/nvme0n1
    //    用户态现在可以 open/read/write 了

    return 0;
}
```

### 4.3 为什么 BAR 分配和驱动匹配要分开

| 阶段 | 做什么 | 为什么不能合并 |
|------|--------|--------------|
| **枚举 (pci_scan_root_bus)** | 读 VID/DID、探测 BAR size、分配 BAR 地址 | 需要全局视野：知道所有设备的 BAR size 后才能做最优地址分配 |
| **激活 (pci_bus_add_devices)** | 触发驱动匹配、probe() | 此时所有 BAR 地址已分配好，驱动直接 ioremap 即可 |

> 如果每个设备发现时立刻 probe，地址分配就无法全局优化——先来的设备可能抢走大块连续空间，后来的大 BAR 设备找不到对齐的地址。

---

## 五、设备从"被发现"到"可用"的完整状态变化

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

state "断电" as OFF
state "L0 就绪\n(链路训练完成)" as L0
state "VID/DID 已读取\n身份已知" as ID
state "BAR size 已探测\n资源需求已知" as SIZE
state "BAR 地址已分配\n地址解码器激活" as BAR
state "驱动已匹配\nprobe() 已调用" as PROBE
state "设备可用\n用户态可访问" as READY

OFF --> L0 : 硬件自动 (链路训练)
L0 --> ID : Config Read (读 VID/DID)
ID --> SIZE : Config R/W (写全1读回)
SIZE --> BAR : Config Write (写 BAR 基址)
BAR --> PROBE : pci_match_id() → probe()
PROBE --> READY : ioremap + request_irq + 初始化

note right of L0 : 物理层就绪, 可传 TLP\n但软件还不知道设备存在
note right of ID : 系统知道"你是谁"\n但不知道"需要什么资源"
note right of BAR : 地址解码器激活, MMIO 可用\n但还没有驱动管理设备
note right of READY : /dev/nvme0n1 出现\n应用程序可读写

@enduml
```

---

## 六、常见问题：BAR 分配后的坑

| 问题 | 症状 | 原因 |
|------|------|------|
| **ioremap 成功但读写全是 0xFF** | 驱动读 BAR 寄存器返回全 F | BAR 写入失败、设备未正确复位、地址解码器未激活 |
| **MMIO 写不生效** | 写 BAR 寄存器但设备行为不变 | Store Buffer 未刷出（需 `wmb()` + `readl()` 强制刷新） |
| **DMA 失败** | 设备发 Memory TLP 但主机收不到 | Bus Mastering 未使能（`pci_set_master()` 忘了调） |
| **中断收不到** | probe 成功但无中断触发 | MSI-X 未分配或 `request_irq()` 的 CPU 亲和性设错 |
| **跨 NUMA 性能差** | 设备插在 Node 0 但 DMA buffer 在 Node 1 | DMA 跨 NUMA 节点 → 内存带宽减半、延迟加倍 |

---

## 七、与前面阶段的衔接

- 阶段① [pcie-link-training.md](./pcie-link-training.md)：链路训练（L0 是 BAR 激活的物理前提）
- 阶段② [pcie-firmware-enum.md](./pcie-firmware-enum.md)：固件分配了临时 BAR（被内核覆盖）
- 阶段③ [pcie-enumeration.md](./pcie-enumeration.md) §四：内核探测 BAR size + 分配 BAR 地址（激活的前提）
- 本篇：BAR 写入 → 地址解码器生效 → 驱动 probe → 设备可用

---

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

- [pcie.md](./pcie.md) §三 — MMIO 与 BAR 空间（BAR 的物理基础）
- [pcie-enumeration.md](./pcie-enumeration.md) §四 — BAR 大小探测与地址分配（激活的前序步骤）
- [dma.md](../dma.md) — BAR 激活后 DMA 才可用（设备发 Memory Read/Write TLP）
- [pcie.md](./pcie.md) §四.1 — DMA 控制面与数据面的区别：BAR 寄存的是控制指令，数据传输不经过 BAR
- [io-addressing.md](../io-addressing.md) — `mov [BAR]` 如何穿过 Store Buffer/RC 变成 TLP（MMIO 路径的细节）

---

## 九、一句话总结

> **BAR 激活是 PCIe 设备从"被发现"到"可用"的最后一公里——内核把物理基址写入 BAR 寄存器后，设备地址解码器自动激活，MMIO 读写路径打通（Write 走 Posted 不阻塞 CPU，Read 走 Non-Posted 阻塞等 Completion）。驱动通过 `pci_match_id()` 匹配后，probe() 里 `ioremap` + `request_irq` + 硬件初始化，设备才算真正上线——用户态终于能看到 `/dev/nvme0n1`。**

