PCIe 阶段④:BAR 激活 → 地址解码器生效 → 设备真正可用
内核
pci_scan_root_bus()完成后,所有设备的 BAR 已被分配了物理地址——但设备还没真正响应 MMIO。为什么?因为 BAR 寄存器里写入了基址只是第一步,设备的地址解码器需要这个基址来激活。而地址解码器激活后,CPU 的mov [BAR_addr]穿过 Store Buffer、缓存层级、RC,最终变成 TLP 的过程才是 MMIO 真正工作的时刻。最后,内核pci_match_id()匹配驱动 →probe()→ioremap()→ 设备从"被发现"变成"可用"。本篇拆解这最后一公里的全过程。
零、总览:BAR 激活在时间线上的位置
按下电源键
│
▼
┌──────────────────────────────────────────────────────────────┐
│ ① 硬件链路训练 → 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),它是一组硬件比较器:
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 地址解码器激活的时序
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 地址的策略
// 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 写路径

3.2 完整的 MMIO 读路径

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 驱动匹配流程
// 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() 里的关键动作
// 一个典型的 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 设备找不到对齐的地址。
五、设备从"被发现"到"可用"的完整状态变化

六、常见问题: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:链路训练(L0 是 BAR 激活的物理前提)
- 阶段② pcie-firmware-enum:固件分配了临时 BAR(被内核覆盖)
- 阶段③ pcie-enumeration §四:内核探测 BAR size + 分配 BAR 地址(激活的前提)
- 本篇:BAR 写入 → 地址解码器生效 → 驱动 probe → 设备可用
八、与现有文档的关系
- pcie §三 — MMIO 与 BAR 空间(BAR 的物理基础)
- pcie-enumeration §四 — BAR 大小探测与地址分配(激活的前序步骤)
- dma — BAR 激活后 DMA 才可用(设备发 Memory Read/Write TLP)
- pcie §四.1 — DMA 控制面与数据面的区别:BAR 寄存的是控制指令,数据传输不经过 BAR
- io-addressing —
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。