第四章:BAR 寄存器详解
好,咱们今天聊聊 BAR。Base Address Register,基地址寄存器。这玩意儿是 PCIe 设备跟主机“握手”的关键一步。我刚开始做驱动时,光看手册以为它就是个地址,后来踩了坑才明白——它背后藏着整套内存映射的玩法。
4.1 它是谁?
每个 PCIe 设备(除了某些特殊类型)都有 6 个 BAR,编号 0 到 5。它们藏在设备的配置空间里,从偏移 0x10 开始,每个 4 字节。你可以把它理解成设备的“门牌号”:主机往这个门牌号写东西,设备就能收到。
核心作用:告诉主机“我有多大的地址空间,我想要哪种类型(内存还是 I/O),你把它映射到系统的哪里”。主机拿到信息后,再通过软件配 BAR,把对应地址写进去,设备就“安家”了。
我个人觉得,BAR 就是设备和主机之间的桥梁。没有它,你没法访问设备里的寄存器、FIFO,甚至控制逻辑。
4.2 两种类型:Memory 和 I/O
你看到的 BAR 寄存器最低位,决定了它是 Memory 型还是 I/O 型。这是硬编码的,设备出厂就定好了。
| 类型 | Bit0 值 | 特点 | 使用场景 |
|---|---|---|---|
| Memory BAR | 0 | 支持 32/64 位地址,可预取,性能高 | 绝大多数现代设备,如网卡、GPU |
| I/O BAR | 1 | 仅 32 位地址,不可预取,有端口限制 | 老旧设备或特殊 I/O 需求 |
你可能会问:“现在谁还用 I/O?” 嗯,我遇到过一些嵌入式平台上的简单外设,比如串口控制器,仍然用 I/O BAR。但实际项目中,Memory BAR 占压倒性多数。
个人技巧:写驱动前先用 lspci -v 看一眼 BAR 的类型。如果看到 [size=256] (64-bit, prefetchable) 之类的描述,那基本就是 Memory BAR。
4.3 地址映射是怎么玩的?
主机启动时,BIOS 或内核会枚举所有 PCIe 设备。对每个 BAR,它先读一次(得到原始值),然后往 BAR 里写全 1,再读回来。这个读回来的值揭示了设备要多大空间。
我拿个例子讲清楚:
- 假设 BAR0 读回来是
0xFFFFFF00。低 4 位是类型字段,忽略。剩下的高 28 位是 1。这意味着设备需要 212 = 4KB 空间。 - 如果是 64 位 BAR,那就需要连续读两个 BAR:BAR0 和 BAR1 组合成一个 64 位数。
具体步骤如下:
- 主机把设备所要求的空间大小对齐到 2 的幂次(比如 4KB、1MB)。
- 在系统地址空间中找一块空闲区域,长度等于该大小。
- 将该区域的基地址写入 BAR 寄存器。
- 设备从此就“住”在系统物理地址空间的这个范围里了。
CPU 之后访问这块地址,就像访问普通内存一样。你写一句 *(volatile uint32_t *)addr = value;,其实就是通过 BAR 映射访问设备内部的寄存器。
// 典型的 BAR 操作流程(伪代码)
uint32_t bar_raw = pci_read(dev, 0x10); // 读 BAR0
bar_raw |= 0xF; // 保留低4位
pci_write(dev, 0x10, 0xFFFFFFFF); // 写全1
uint32_t bar_mask = pci_read(dev, 0x10); // 读回掩码
uint32_t size = ~(bar_mask & ~0xF) + 1; // 计算大小
// 分配地址基址 base,然后写入
pci_write(dev, 0x10, base | (bar_raw & 0xF));
避坑:写全 1 读掩码时,如果设备是 64 位 BAR,你不能只写一个 BAR,必须把连续两个 BAR 都写全 1 再分别读回,然后组合成 64 位掩码。我曾经在这上面吃过亏,只写了一个 BAR,读回来发现掩码不对,浪费了半天时间。
4.4 64 位 BAR 的处理
32 位地址空间越来越不够用,所以现代设备经常用 64 位 BAR。具体怎么做呢?
- 两个连续 BAR 组成一组,比如 BAR0 + BAR1,BAR2 + BAR3 等。
- 第一个 BAR 的 Bit2 标记为 64 位(值为 00b 表示 32 位,10b 表示 64 位)。
- 第二个 BAR 被“借用”作为高 32 位地址。
举例:64 位 BAR 的编码
BAR0 (偏移0x10): [低32位地址 + 类型位 + 64位标记]
BAR1 (偏移0x14): [高32位地址]
注意:第二个 BAR 不再作为独立 BAR 使用。如果设备声明了 64 位 BAR,那么它实际占用的 BAR 数量会减少。比如原来 6 个 BAR,用了 64 位后可能只剩 4 个。
我有个习惯:读设备手册时特别留意 BAR 的位宽说明。有些设备手册写得不清楚,我就直接在代码里打印 BAR 的 Bit2 和 Bit3:
uint32_t bar0 = pci_read(dev, 0x10);
int type = (bar0 >> 1) & 0x3; // 位1-2
if (type == 0x2) {
printf("64-bit BAR detected\n");
}
这样就不会把 64 位 BAR 误当作两个独立的 32 位 BAR 用了。
4.5 知识结构总览
下面我用一张图把 BAR 的核心逻辑串起来。你跟着箭头的方向看:
4.6 一些实战中必须注意的事
- 对齐要求:设备请求的大小必须是对齐的。比如需要 4KB,基址必须 4KB 对齐。主机分配时一般会自动对齐,但如果自己手写分配器,一定要检查。
- 预取位(Prefetchable):BAR 的 Bit3 表示该区域是否可预取。可预取意味着没有任何副作用,可以缓存。写驱动的同学要注意:对于可预取的 Memory BAR,CPU 可能会乱序访问,需要使用 Memory Barrier。
- I/O BAR 的限制:x86 平台上 I/O 空间只有 64KB,且很多已经被占用。我建议新设计尽量不用 I/O BAR。
我私藏的一个小工具:用 setpci 命令可以实时查看/修改 BAR 值。比如 setpci -s 01:00.0 BASE_ADDRESS0.l 就能看到 BAR0 当前值。调试时特别好用。
又一个提醒:千万别在设备驱动里直接往 BAR 寄存器写随机数。我曾经看到一个新人同事,为了“测试”,往 BAR 写了个 0x12345678,结果系统直接挂死。因为主机用了那个地址,而设备根本没准备对应空间。正确的做法是通过操作系统提供的 pci_resource_start() 之类的 API 获取已分配好的基址。
好了,关于 BAR 我们就讲到这里。你只需要记住:它是设备的门牌,主机通过它找到设备。类型、大小、预取属性都藏在 BAR 的位里。64 位 BAR 需要特别小心组合逻辑。这些知识点消化后,后面讲配置空间其他寄存器和中断就会轻松很多。