第四章: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 位数。

具体步骤如下:

  1. 主机把设备所要求的空间大小对齐到 2 的幂次(比如 4KB、1MB)。
  2. 在系统地址空间中找一块空闲区域,长度等于该大小。
  3. 将该区域的基地址写入 BAR 寄存器。
  4. 设备从此就“住”在系统物理地址空间的这个范围里了。

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 的核心逻辑串起来。你跟着箭头的方向看:

BAR 寄存器核心知识结构 BAR 寄存器知识结构 BAR 寄存器 作用 提供地址映射信息 类型 Memory / I/O 告诉主机所需空间大小 (全1读回掩码) 声明地址类型(预取等) (位3 预取位) 32位 / 64位 (位1-2 编码) I/O 空间 vs 内存空间 (位0 区分) 地址映射流程 1. 枚举时读 BAR 原始值 2. 写全 1 读掩码计算大小 3. 分配基址写入 BAR 64位 BAR 需组合 两个连续 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 需要特别小心组合逻辑。这些知识点消化后,后面讲配置空间其他寄存器和中断就会轻松很多。