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 进去再读回来。
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。这就是设备要求的最小地址窗口大小。
| 写入 0xFFFFFFFF 后读回值 | 翻转后掩码 | 地址范围大小 | 对齐要求 |
|---|---|---|---|
| 0xFFFFFFF0 | 0x0000000F | 16 字节 | 16 B 对齐 |
| 0xFFFF0000 | 0x0000FFFF | 64 KB | 64 KB 对齐 |
| 0xFF000000 | 0x00FFFFFF | 16 MB | 16 MB 对齐 |
| 0xF0000000 | 0x0FFFFFFF | 256 MB | 256 MB 对齐 |
4.2 对齐要求:硬件不是随便放的
地址范围算出来之后,你分配给 BAR 的基地址必须满足对齐要求。说白了,起始地址必须是地址窗口大小的整数倍。
举个例子:某设备 BAR2 需要 4 KB 的窗口,那基地址末 12 位必须是 0。也就是说,基地址必须 4 KB 对齐。如果你把基地址设在 0x1000,没问题;但如果你设在 0x1004,设备会忽略低 12 位的值——因为硬件只看 bit 31–12。
下表列出常见对齐粒度与对应的掩码:
| 对齐大小 | 低位掩码 | 地址示例(基址 + 偏移) |
|---|---|---|
| 16 B | 0x0F | 0x1000 – 0x100F |
| 256 B | 0xFF | 0x1200 – 0x12FF |
| 4 KB | 0xFFF | 0x4000 – 0x4FFF |
| 1 MB | 0xFFFFF | 0x100000 – 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 读回来的值 取反 得到的。嗯,不能搞反。如果你把掩码用成了非掩码,结果会全错。
0xFFF00000,那实际地址掩码就是 0x000FFFFF。在代码里直接取反即可。
4.4 再捋一遍整个解码流程
下面这张图能帮你把整体逻辑串起来。从读取 BAR 寄存器开始,到最终得到可用的物理地址区间,一共四步。
1. 读回 BAR 原始值(写全 1 法)
2. 清属性位得到基地址
3. 计算掩码 = ~(原始值 & ~0x0F)
4. 基地址 + 掩码 → 得到对齐窗口大小和可用区间
说白了,BAR 解码就是这四步的循环。你在写驱动时,只要记住把命令寄存器的对应 bit 先关了,再写全 1,基本不会翻车。嗯,今天就到这儿,剩下的你带着这个思路去翻 PCIe Spec 就知道每段描述其实都在说什么了。