3· BAR寄存器结构与类型 —— Memory BAR、I/O BAR、64位BAR、Prefetchable BAR的区别

讲完了 BAR 空间怎么划分,我们得聊聊 BAR 自己的「性格」。

BAR 寄存器里存的不只是地址,它还带着一堆属性位。硬件的同学可能觉得这些属性是留给软件看的,但我在项目里栽过跟头之后才发现——硬件不懂这些属性,总线直接罢工。

3.1 四种 BAR 类型,你搞清楚了吗?

PCIe 规范定义了四种 BAR 类型,说白了就是告诉系统:「我这块地址空间要怎么用。」

我个人习惯把它们分成两组:Memory 系和 I/O 系。Memory 系又分出 64 位和 Prefetchable 两个变种。

类型 地址位宽 是否支持预取 典型用途
Memory BAR (32-bit) 32位 否 普通寄存器、FIFO
I/O BAR 32位 否 遗留设备 I/O 端口
64-bit Memory BAR 64位 通常否 大容量内存、DMA 缓冲区
Prefetchable BAR 32或64位 是 显存、大块读缓存

3.2 Memory BAR —— 最常见,也最容易被忽略

Memory BAR 是我们用的最多的类型。基本上所有性能敏感的设备,都会把自己的地址空间映射成 Memory BAR。

为什么?因为 CPU 访问 Memory 空间比访问 I/O 空间快得多。CPU 可以直接用 load/store 指令操作 Memory BAR,甚至可以用 cache 做预取。

关键点: Memory BAR 必须支持读写。如果某个寄存器是只读的,你也要能响应“写入但不生效”。否则,系统在探测 BAR 大小的时候会读到错误值,直接导致资源分配失败。

我遇到过一位开发者,他把一个只读寄存器用 Memory BAR 映射出来,结果系统枚举的时候发现写入不回读正确值,直接把设备标记为故障。折腾了两天,最后加了个 dummy 写寄存器才解决。

3.3 I/O BAR —— 能不用就别用

I/O BAR 是为了兼容老设备保留的。PCIe 协议虽然支持 I/O 事务,但很多现代 Root Complex 干脆不支持 I/O 空间了。

我记得有一次项目,芯片里面用了一个 I/O BAR 做配置接口。在 x86 平台没问题,到了 ARM 平台上,系统直接找不到这个 BAR。查了三天,才发现是 ARM 的 RC 把 I/O 空间给禁用了。

避坑指南: 新设计千万不要用 I/O BAR。除非你的设备只能用在老式 x86 系统上,否则 I/O BAR 只会给移植和调试带来无穷无尽的麻烦。我曾经因为一个 I/O BAR 被客户投诉“只支持 Intel 平台”,后来全部改成 Memory BAR 才解决。

I/O BAR 还有一个缺点:访问开销大。每次 I/O 操作都需要走 PCIe 的 I/O 事务层包(TLP),效率比 Memory 空间差一个数量级。你想想看,谁会愿意用自己的性能瓶颈去换取“兼容性”这种虚无缥缈的东西?

3.4 64-bit BAR —— 大内存时代的标配

32 位地址空间只有 4 GB。如果在 64 位系统上,你的设备需要映射超过 4 GB 的物理地址空间,64-bit BAR 就是唯一选择。

实现 64-bit BAR 并不复杂:BAR0 和 BAR1 配对使用,BAR0 存低 32 位,BAR1 存高 32 位。系统枚举时发现 BAR0 的最高位为 1,就知道这是个 64-bit BAR,然后自动读取 BAR1 作为高地址。

// 伪代码:配置 64-bit BAR
// BAR0 的 bit 1-0 设为 00 (Memory),bit 3 设为 1 (64-bit)
bar0_val = (address & 0xFFFFFFF0) | 0x04;  // 低位地址 + 64-bit 标志
bar1_val = (address >> 32) & 0xFFFFFFFF;   // 高位地址

write_config(dev, BAR0_offset, bar0_val);
write_config(dev, BAR1_offset, bar1_val);

建议: 如果你的设备用不了 4 GB 以上地址,但还是用了 64-bit BAR,那纯属浪费资源。64-bit BAR 占用两个连续的配置空间槽位,可能影响其他设备的资源分配。我通常只在分配大于 2 GB 的空间时才用 64-bit BAR。

3.5 Prefetchable BAR —— 把预取的权利交给 CPU

Prefetchable 是 BAR 属性里最有意思的一个。如果 BAR 设置了 Prefetchable 位,系统就知道这块空间可以“预取”——也就是 CPU 可以在不知道你实际需要多少数据的情况下,提前读一大块数据。

这有什么好处?性能。对于显存、大块缓冲区、查找表这类内容,CPU 预取可以大幅减少访问延迟。

但要注意:Prefetchable 要求你的设备读操作不能有副作用(side effect)。说白了,我读一次和读一万次,状态必须一样。如果读 FIFO 会清空数据,或者读某一个寄存器会改变设备状态,那你不能设 Prefetchable。

常见误区: 很多人以为 Prefetchable 只跟性能有关。不,它还会影响系统对 BAR 空间的对齐要求。Prefetchable BAR 可以不被系统严格按 4 KB 对齐(有些系统会合并相邻的 Prefetchable 区域),而非 Prefetchable 的 Memory BAR 必须严格按 4 KB 对齐。这点在资源紧张的多设备拓扑里很关键。

我在调试一个视频采集卡的时候遇到过一个 Bug:采集卡把视频帧缓冲设成了 Prefetchable。结果 CPU 预取读了多余的数据,导致驱动里读取的帧数据总是错位,画面出现撕裂。检查了两周才发现是 BAR 属性问题,改回 Non-Prefetchable 就好了。

3.6 四种 BAR 类型的核心区别

BAR 寄存器类型框架图 四种 BAR 类型核心区别 BAR 属性决策节点 Memory BAR 32-bit, 无预取 I/O BAR 遗留接口,慎用 64-bit Memory BAR 高位地址扩展 Prefetchable BAR 允许CPU预取 选型优先级 Memory BAR > Prefetchable BAR > 64-bit BAR > I/O BAR 新设计优先 Non-Prefetchable Memory BAR

3.7 实战中的选型建议

说了这么多理论,到底怎么选?我给出一个实际的决策思路:

  1. 能用 Memory BAR 就别用 I/O BAR。 哪怕你的设备是 PCIe 转并口这种老掉牙的东西,也请用 Memory BAR 做地址映射。
  2. 地址空间大于 2 GB 时必须用 64-bit BAR。 这是硬性约束。对了,32-bit 的 Prefetchable BAR 也支持大于 2 GB,但很多操作系统有自己的一套对齐规则,直接 64-bit 更省心。
  3. 只有纯读取、无副作用的设备才设 Prefetchable。 我通常只在显存、查找表、代码存储器上用 Prefetchable。其他一律 Non-Prefetchable。

核心原则: 能用 Memory BAR 的地方不要用 I/O BAR;能用 32-bit 的地方不要用 64-bit;能用 Non-Prefetchable 的地方不要用 Prefetchable。过度设计不是好事,尤其是在资源紧张的 64-bit 地址空间里。