BAR 基础概念:地址空间的“门牌号”

大家好,我是老李。说实话,最开始接触 PCIe 调试那会儿,我就被 BAR 这个概念绕晕过。你说它是个寄存器吧,它又像个地址转换器。后来一个老前辈跟我说了一句大白话——BAR 就是板卡在系统地址空间里租的门面房号。嗯,从那以后我再也没糊涂过。

这一章咱们就把 BAR 这个概念彻底搞清楚。你想想看,一个 PCIe 设备想要跟 CPU 通信,要么通过 IO 端口,要么通过内存映射。但怎么让 CPU 知道你在哪?怎么分配地址给你?答案就藏在每个设备的配置空间里——那些叫 Base Address Register 的寄存器。

什么是 BAR?

每个 PCI/PCIe 功能(Function)最多可以有 6 个 BAR(Type 0 配置空间),或者 2 个 BAR(Type 1 用于桥)。它们按照规范分布在配置空间偏移量 0x10 到 0x24 的位置。每个 BAR 本质上是一个 32 位的寄存器,但也有 64 位 BAR 的说法——其实就是连续占两个 BAR 的位置。

BAR 里存什么?存的是基地址,但更重要的是它的低几位包含了类型信息:

  • Bit 0:指示是 Memory Space 还是 I/O Space
  • Bit 2:1:对于 Memory BAR,指示 32 位还是 64 位地址
  • Bit 3:Prefetchable 位,告诉 CPU 这段内存能不能投机预取

我个人习惯把 BAR 的低位叫“属性位”,高位叫“可编程位”。为什么叫可编程?因为系统固件会通过写入全 1 然后读回的方式探测 BAR 需要的地址范围大小,再回写实际的基地址。这就是经典的“BAR 大小探测”流程。

核心要点:BAR 不是固定的地址,它是系统启动时由 PCI 枚举器分配的。你在驱动里读到的 BAR 值,是经过桥转译后的最终访问地址。

BAR 的历史:从 PCI 到 PCIe

PCI 总线在 90 年代刚出来时,设备地址分配是硬跳线的,后来才引入了配置空间和 BAR 的概念。PCIe 作为 PCI 的进化版,完全继承了 BAR 的机制——说实话,PCIe 的物理层和事务层改得面目全非,但配置空间这块,几乎没动。

我记得有一次做服务器项目,用的还是 PCI-X 的 RAID 卡。因为桥接芯片的 BAR 分配出了问题,导致重启后系统找不到设备。查来查去,最后发现是 PCI 到 PCIe 桥的 Type 1 配置空间里,Subordinate Bus Number 设置错了,间接影响了 BAR 的 Decode 范围。从那以后我对 BAR 的“桥片传播”特性特别留心。

BAR 在 PCIe 中的作用

BAR 的核心作用有三点:

  1. 告诉系统:我的地址范围有多大(通过写全 1 读回检测)
  2. 告诉系统:我的地址空间类型(Memory / I/O / 64-bit / Prefetchable)
  3. 作为 CPU 访问设备的门户——读写 BAR 对应的起始地址,就等于跟设备寄存器通信

举个例子,你有一张网卡,它的 BAR0 映射了 4KB 的 MMIO 空间。系统给它分配基址 0xF0000000。那么 CPU 要读网卡的发送描述符,直接读 0xF0000000 + 偏移 0x20 就行了。这个地址会经 PCIe 的地址译码逻辑路由到网卡内部。

我的建议:在调试 BAR 问题前,先用 lspci -vvv(Linux)或 devcon(Windows)把当前设备的 BAR 值 dump 出来。对照配置空间的偏移量手动检查一遍,往往能发现地址重叠或者类型不匹配的问题。

BAR 的初始化流程(5 步搞定)

系统固件(UEFI/BIOS)在枚举 PCIe 设备时,会按照以下步骤配置每个 BAR:

步骤 操作 我遇到的坑
1 读取 BAR 原值(确定是否可编程) 有的设备 BAR 是 hardwired(只读的),不能写
2 写入 0xFFFFFFFF,再读回 注意要保存原值,不然设备可能失效
3 根据读回的低位提取大小(取反加 1) 我曾经忽略过 64 位 BAR 的高 32 位也要写全 1
4 分配对齐后的基地址 必须按大小对齐,否则译码会出错
5 写回基地址,并清除属性位(确保 bit 0 正确) Memory BAR 的 bit 0 必须写 0,I/O BAR 写 1

警告:BAR 的大小一定是对齐到 2 的幂次,且最小为 16 字节(I/O BAR 最少 4 字节)。如果一个 BAR 要求 8KB,但你在固件里分配了不对齐的地址,设备会静默忽略你的访问——这是最难查的故障之一。

常见误解与避坑

  • 误解:BAR 的值就是物理地址。 实际上经过 IOMMU 或 ATS 转换后,CPU 看到的是虚拟地址,设备看到的才是 IOVA。但裸机下没有 IOMMU 时,BAR 值就是物理地址。
  • 误解:64 位 BAR 就是两个 32 位寄存器。 确切地说,它是连续的两个 BAR 位置,但高 32 位寄存器在偏移量 +4 的地方。探测时需要把两个寄存器都写全 1,然后读回合并。
  • 避坑:我曾经在调试 FPGA 板卡时发现 BAR0 读出来全是 0。查了半天才发现是配置空间的 Type 选错了——Type 0 和 Type 1 的 BAR 数量不同,桥片只允许 2 个 BAR。

一句话总结:BAR 是沟通 CPU 地址域和设备内部寄存器空间的桥梁。弄懂它,你就能看懂系统启动日志里那堆 BAR=0x...,size=... 的信息,也就能快速定位地址冲突了。

PCIe 设备 BAR 结构与地址映射流程 PCIe 配置空间 (Type 0 设备) 偏移 0x10 : BAR0 偏移 0x14 : BAR1 偏移 0x18 : BAR2 偏移 0x1C : BAR3 偏移 0x20 : BAR4 偏移 0x24 : BAR5 其他寄存器 Class Code / Rev ID ... 读取 单个 BAR 寄存器 高位 [31:4] : 基地址 (可编程,由系统写入) 低位 [3:0] : 属性 bit0=0 Mem / =1 IO bit2:1=00: 32位 bit2:1=10: 64位 bit3=1 : Prefetchable (允许缓存预取) 大小 = 2^N 字节 映射 系统地址空间 其他设备 (网卡、GPU 等) 设备 MMIO BAR0 基址 BAR1 BAR2 PCIe 桥片 转发区间 IO 空间 (可选)

看图说话:左列是配置空间中的 6 个 BAR 寄存器,中间展示了单个 BAR 的位域结构,右列表示这些 BAR 最终映射到的系统地址空间。注意 64 位 BAR 会占用两个连续 BAR 位置,映射时得到一个 64 位的连续区域。

最后一句话:BAR 这东西,说白了就是“设备说:我需要这么大一片地址,你给我安排个地方”。系统说“好”,然后写进去一个地址。你如果发现设备不好使,别急着怀疑硬件,先拿 lsaspi 或者内存查看工具看看 BAR 有没有被正确分配——九成的地址冲突问题都能在这步解决。