3. BAR 基础:寄存器定义、类型与属性

PCIe 设备要和 CPU 打交道,靠的就是 BAR——Base Address Register。说白了,BAR 就是设备向系统声明「我需要多少地址空间、我要哪种地址空间、我的地址空间有什么脾气」的一组寄存器。我刚开始接触 PCIe 时,总觉得 BAR 就是个内存地址,后来踩了几个坑才发现,细节远比想象的多。

3.1 什么是 BAR 寄存器?

每个 PCIe 功能(Function)最多可以有 6 个 BAR(Type 0 配置空间),Type 1(桥)只有 2 个。它们位于配置空间偏移 0x10~0x24 的位置,每个 BAR 占 4 字节。

BAR 的核心作用只有一个:让系统软件(BIOS 或 OS)知道设备需要多大的地址窗口,然后给设备分配一个基地址。分配完成后,软件和硬件就通过这个基地址 + 内部偏移来通信。

我个人习惯把 BAR 理解成设备的「家庭住址申请表」——设备先填好需要多大面积,系统给你划块地皮,你就在那上面盖房子。房子怎么盖、盖成啥样,就看后面三种类型了。

3.2 Memory 与 IO —— 两种 BAR 类型

BAR 可以声明为两种类型:Memory BAR 和 IO BAR。我遇到过不少新手分不清这两者,甚至有人觉得 IO BAR 在现代系统里还有用。其实,两者的区别非常本质。

Memory BAR:映射到系统的内存地址空间。CPU 可以用普通的 load/store 指令直接读写设备内部的寄存器或 FIFO。性能高、延迟低,是主流方式。绝大部分 PCIe 设备(NVMe、GPU、网卡)都使用 Memory BAR。

IO BAR:映射到 x86 特有的 IO 地址空间。CPU 要用专门的 in/out 指令访问,速度慢,且非 x86 平台(ARM、RISC-V)甚至根本不支持 IO 空间。我建议:新设计除非万不得已,不要用 IO BAR。

核心结论:Memory BAR 是现代 PCIe 设备的标配;IO BAR 是历史遗留物,仅用于兼容老旧设备。

来看一张对比表:

对比项 Memory BAR IO BAR
访问指令 load/store(MOV 等) in/out 专用指令
地址空间 系统内存地址空间(32/64 位) IO 地址空间(16 位)
性能 高,可缓存、可预取 低,非缓存、不可预取
平台兼容性 所有体系结构 仅 x86 传统模式
推荐场景 几乎所有设备 不推荐

注意:在 64 位系统中,IO 空间仍然存在,但很多芯片组已经不再对 IO BAR 分配空间。曾经有个项目用 IO BAR 控制 GPIO,结果在新款主板上直接分配的基址为 0,设备根本用不了。后来我全改成了 Memory BAR,一劳永逸。

3.3 BAR 的关键属性

光知道类型还不够。每个 Memory BAR 还有三个重要的属性位,它们在寄存器的最低位或高位里藏着。我们一个一个说。

3.3.1 Prefetchable(预取)

Prefetchable 字面意思是「可预取」。如果设了这个标志,就表示这块地址空间是不包含副作用的。什么意思呢?你读某个地址,它不会导致硬件状态改变。这样的话,CPU 或 DMA 就可以放心地超前读取(预取)多次,合并读写,甚至做缓存。

举个我实际碰到的例子:一个 FPGA 实现的帧缓冲区,它内部就是一块双端口 RAM。读它不会修改内部指针,所以完全可以设为 Prefetchable。CPU 读起来很快,因为 cache 会帮你合并。但如果是控制状态寄存器(比如写一位触发中断),读它会导致清状态位,那不能设 Prefetchable,否则预取会破坏状态。

经验法则:纯粹的数据缓冲区(FIFO 读端、帧缓存、寄存器文件)设 Prefetchable;带有读后清零、读会改变状态的寄存器,一定不要设。

3.3.2 64-bit(64 位地址)

每个 BAR 寄存器是 32 位的。如果设备需要访问的内存地址超过 4GB,就需要使用 64 位 BAR。具体做法是把连续的两个 BAR 寄存器合并成一个 64 位 BAR(低位存低 32 位,高位存高 32 位)。

在我设计的某款高速数据采集卡中,板载 16GB DDR,如果只用 32 位 BAR,系统永远访问不到全部内存。嗯,所以必须用 64 位 BAR。注意:64 位 BAR 必须是 Prefetchable 的(PCIe 规范要求),它同时会吃掉两个 BAR 寄存器。

3.3.3 类型编码与位域结构

BAR 寄存器的最低位(bit 0)用来区分 Memory BAR(0)和 IO BAR(1)。对于 Memory BAR,bit 2:1 编码内存类型:

  • 00:32 位,非 Prefetchable
  • 01:保留
  • 10:64 位,Prefetchable
  • 11:32 位,Prefetchable

你看,其实 bit 2:1 就同时编码了地址宽度和 Prefetchable 属性。设计硬件时,可别弄反了。我曾经把一个应该设 32 位 Prefetchable 的 BAR 写成了 64 位 Prefetchable,结果在 32 位系统上分配基址失败,排查了一整天……

IO BAR 就简单了:bit 0=1,bit 1 保留,其他位用来表示大小和基址。

3.4 知识结构图

下面这张图概括了 BAR 的核心体系,能帮你一眼看清类型、属性与位域的关系:

BAR 寄存器知识结构 BAR 寄存器 类型 类型 Memory BAR IO BAR 32-bit / 64-bit Prefetchable 位域解读 bit 0:类型选择 bit 2:1:Memory类型 系统分配基址 → 访问

3.5 如何确定 BAR 的大小?—— 软件探测机制

这是个经典面试题,也是实际开发种最实用的技能。系统是怎么知道你的 BAR 需要多大地址空间的?

方法很简单:软件先往 BAR 寄存器写全 1(0xFFFFFFFF),然后再读回来。根据读回的值就能算出大小。我写个伪代码示意:

// 假设访问配置空间函数 pci_config_read/write
uint32_t original = pci_config_read(bus, dev, func, bar_offset);
pci_config_write(bus, dev, func, bar_offset, 0xFFFFFFFF);
uint32_t mask = pci_config_read(bus, dev, func, bar_offset);
pci_config_write(bus, dev, func, bar_offset, original); // 恢复原始值

// 计算大小:取反后 +1,再考虑低位属性位
uint32_t size = (~(mask & 0xFFFFFFF0)) + 1;

原理是:硬件 BAR 寄存器里的可写位越多,说明它能用的地址越多。写全 1 后,那些被硬件固定为 0 的位就是「地址不可用」区域,这些 0 位的个数就决定了大小。说白了,BAR 的大小必须是 2 的幂次,且对齐到自身大小。

注意:读回的值包含了最低 4 位(类型和属性位),计算前必须把它们清掉(& 0xFFFFFFF0)。我刚才说过,IO BAR 的写法略有不同,但核心思想一样。

3.6 实战中的避坑总结

最后分享一下我多年的经验,总结成几条简单规则:

  • 选 Memory BAR,别碰 IO BAR——除非你要兼容 20 年前的设备。
  • 64 位 BAR 一定要配合 Prefetchable——规范要求,非设不可。
  • 大小务必对齐——比如申请 16KB,BAR 的基址必须 16KB 对齐,否则分配会失败。
  • 使用前一定要恢复 BAR 原始值——探测大小那段代码,如果忘记恢复,设备可能直接罢工。
  • 硬件设计时,BAR 的默认值不能全 0——最好给个非零的默认基址,便于调试。

嗯,以上就是 BAR 基础的全部核心内容。理解了这些,你就能看懂 PCIe 设备的「户口本」了。剩下的,我们在后面的章节里结合具体分配流程来深入。记住:BAR 是设备与系统之间握手的第一步,这步走对了,后面就顺了。