4. BAR 地址解码:地址范围计算、对齐要求、地址掩码

说真的,刚开始接触 PCI/PCIe 设备的 BAR 解码时,我也被一堆 bit 位搞晕过。但搞明白之后你会发现,内核无非是拿 BAR 寄存器的值做几次位运算,就能把物理地址范围算出来。今天咱们就一个坑一个坑地踩过去。

4.1 地址范围计算:从寄存器值到区间

每个 BAR 寄存器(32 位为例)的低 4 位是属性位。bit 0 决定是 I/O 空间还是内存空间,bit 1–2 决定内存类型(32 位/64 位/预取等),bit 3 是预取标志。真正存放基地址的部分从 bit 4 开始往上走。

我个人习惯是先写个辅助宏把属性位清掉:

#define BAR_BASE_MASK  (~0x0FULL)
uint32_t bar_val = pci_conf_read(dev, base_addr_reg);
uint32_t base = bar_val & BAR_BASE_MASK;

但光读出来是不够的。你要算这 BAR 占多大空间,得用另一个经典套路——写全 1 进去再读回来。

📍 关键步骤: 往 BAR 写入 0xFFFFFFFF,再读取得到的值,然后用掩码取低 32/64 位,翻转位宽就能得到地址掩码。

比如我写过的一个网络控制器,往 BAR0 写全 1 后读到 0xFFF00000。这说明有效地址位只有高 12 位(bit 31–20),低 20 位全是 0。那么该 BAR 的地址范围就是 base ~ base + (1 << 20) - 1,也就是 1 MB 对齐的连续区间。

为什么是 1 MB?你算算:bit 19–0 全为 0,说明地址范围是 220 = 1 MB。这就是设备要求的最小地址窗口大小。

地址范围计算实例(32 位 BAR)
写入 0xFFFFFFFF 后读回值翻转后掩码地址范围大小对齐要求
0xFFFFFFF00x0000000F16 字节16 B 对齐
0xFFFF00000x0000FFFF64 KB64 KB 对齐
0xFF0000000x00FFFFFF16 MB16 MB 对齐
0xF00000000x0FFFFFFF256 MB256 MB 对齐
⚠️ 我曾经踩过的坑: 写全 1 前先把命令寄存器的“内存空间使能”和“I/O 空间使能”位清掉,否则设备可能响应写操作把总线锁死。我在调试一块 FPGA 网卡时就被锁过一次,最后只能按复位键。

4.2 对齐要求:硬件不是随便放的

地址范围算出来之后,你分配给 BAR 的基地址必须满足对齐要求。说白了,起始地址必须是地址窗口大小的整数倍。

举个例子:某设备 BAR2 需要 4 KB 的窗口,那基地址末 12 位必须是 0。也就是说,基地址必须 4 KB 对齐。如果你把基地址设在 0x1000,没问题;但如果你设在 0x1004,设备会忽略低 12 位的值——因为硬件只看 bit 31–12。

💡 我个人习惯: 在 BIOS/UEFI 的 PCI 枚举代码里,我会把每个 BAR 的对齐值单独算出来,然后再按从大到小的顺序分配地址。这么做能最大限度减少碎片。

下表列出常见对齐粒度与对应的掩码:

对齐粒度与地址掩码对照
对齐大小低位掩码地址示例(基址 + 偏移)
16 B0x0F0x1000 – 0x100F
256 B0xFF0x1200 – 0x12FF
4 KB0xFFF0x4000 – 0x4FFF
1 MB0xFFFFF0x100000 – 0x1FFFFF

你可能会问:为什么非要这么对齐?因为地址译码逻辑在硬件内部就是一个比较器,只比较高位。如果你不对齐,设备可能同时响应多个地址区段,那不乱套了?

4.3 地址掩码:硬件如何决定“这是我家的地盘”

掩码的本质是一个“忽略位”的指示器。硬件拿到基地址后,会把地址总线上送来的地址和掩码进行一次 AND 操作(实际上是判断高位是否相等)。

举个例子:BAR 基地址 = 0x10000000,掩码 = 0xFFFF0000。那么凡是落在 0x10000000 ~ 0x1000FFFF 的地址,设备都会响应。硬件的判断逻辑其实就一句话:

if ((addr & BAR_MASK) == (base_addr & BAR_MASK))
    // 设备响应

我在公司做 PCIe 交换芯片验证时,就写过这么一段模拟代码:

uint32_t bar_base = 0x10000000;
uint32_t bar_mask = 0xFFFF0000;  // 64 KB 窗口
uint32_t test_addr = 0x10001234;
if ((test_addr & bar_mask) == bar_base) {
    printf("Hit! Target address 0x%08X\n", test_addr);
}

你会发现,这里 bar_mask 就是我们在 4.1 节里通过写全 1 读回来的值 取反 得到的。嗯,不能搞反。如果你把掩码用成了非掩码,结果会全错。

💡 一个小窍门: 如果你读到的 BAR 掩码(原始值)是 0xFFF00000,那实际地址掩码就是 0x000FFFFF。在代码里直接取反即可。

4.4 再捋一遍整个解码流程

下面这张图能帮你把整体逻辑串起来。从读取 BAR 寄存器开始,到最终得到可用的物理地址区间,一共四步。

BAR 地址解码流程 读取 BAR 寄存器 (写 0xFFFFFFFF 后读回) 提取基地址 清低 4 位属性位 计算地址掩码 & 对齐要求 掩码 = ~读回值 | 0x0F 得出地址区间 基址 ~ 基址 + 窗口大小 - 1 常用技巧: 写全 1 前先失能命令寄存器
🗺️ 核心逻辑总结:
1. 读回 BAR 原始值(写全 1 法)
2. 清属性位得到基地址
3. 计算掩码 = ~(原始值 & ~0x0F)
4. 基地址 + 掩码 → 得到对齐窗口大小和可用区间

说白了,BAR 解码就是这四步的循环。你在写驱动时,只要记住把命令寄存器的对应 bit 先关了,再写全 1,基本不会翻车。嗯,今天就到这儿,剩下的你带着这个思路去翻 PCIe Spec 就知道每段描述其实都在说什么了。