### BAR 寄存器详解:格式、标志与属性 #### 3.1 到底什么是 BAR 寄存器? 咱们调试 PCIe 设备,绕不开的一个东西就是 **BAR 寄存器**。英文全称是 Base Address Register,直译就是“基地址寄存器”。 这玩意儿到底有什么用呢?其实很简单。 你想想看,一个 PCIe 设备插到主板上,CPU 怎么跟它通信?CPU 不认识设备,设备也不认识 CPU。它们需要一个“中间人”—— 系统软件(BIOS 或者操作系统)来分配地址。 BAR 寄存器就是设备对系统说的一句话:“嘿,我需要多大的地址空间?我要放内存还是 IO?我能不能预取?” 我打个比方。BAR 就像设备提交的一张“需求单”。系统看到这个单子,就知道该给设备预留多少“地盘”,然后把这个地盘的起始地址写回 BAR。这样一来,CPU 就能通过访问这些地址来操作你的设备硬件了。 所以,搞懂 BAR 的格式,是调试地址冲突的第一步。你连设备要啥你都不知道,怎么去查冲突呢? #### 3.2 BAR 的比特位结构 每个 BAR 寄存器是 32 位宽的,但并不是所有位都用来存地址。它里面有各种标志位,告诉系统这个设备的“脾气秉性”。 下面我拆解一下这 32 个比特位。这是最基础的,但你得烂熟于心。 | Bit 位置 | 名称 | 功能描述 | 我的备注 | | :--- | :--- | :--- | :--- | | **Bit 0** | **空间类型标志** | `0` 表示映射到内存空间;`1` 表示映射到 IO 空间。 | 这个位最关键,决定了你的设备是走内存总线还是 IO 总线。 | | **Bit 2:1** | **内存空间类型** | `00` 表示 32 位地址空间;`10` 表示 64 位地址空间。 | 64 位表示该设备需要占用两个连续的 32 位 BAR 寄存器。 | | **Bit 3** | **预取标志** | `0` 表示不可预取;`1` 表示可预取。 | 可预取意味着该区域没有读副作用,CPU 可以提前往里面扔数据。 | | **Bit 31:4** | **基地址** | 实际映射的地址。这部分由系统软件写入,告诉设备你的地盘在哪。 | 注意,硬件复位时这些位通常为 `0`,或者只读固定值。 | > 对了,你可能注意到,表中没有提到 Bit 4 以上。这是因为地址对齐的要求,低 4 位只能用于标志,不能用于地址。 ##### 3.2.1 内存空间与 IO 空间标志 **Bit 0** 是第一个分水岭。它告诉系统,设备需要的是 **内存空间** 还是 **IO 空间**。 - **内存空间 (Bit 0 = 0)**:这是最常见的。设备被映射到 CPU 的物理内存地址空间里。也就是 CPU 可以直接用 `mov` 指令读写这些地址。速度快,效率高。大部分高性能设备(显卡、网卡、NVMe 固态盘)都用内存空间。 - **IO 空间 (Bit 0 = 1)**:这是 x86 架构特有的遗留产物。设备被映射到 CPU 的 IO 地址空间里。你需要用 `in` / `out` 指令才能访问。 我个人建议,除非你是在维护老古董设备,否则尽量别用 IO 空间。为什么?因为 IO 空间很小,只有 64K 字节(16 位地址),而且访问速度慢。在现代系统上,IO 空间已经基本被淘汰了。我在调试一个工控板卡时就遇到过这个问题,板卡死活不工作,读 BAR 发现它配了 IO 空间,而 BIOS 没给它分配。后来改成内存映射就好了。 ##### 3.2.2 预取属性 (Prefetchable) **Bit 3** 控制的是 **预取属性**。这个属性挺有意思。 可预取区域(Bit 3 = 1)有以下特点: 1. **读操作无副作用**:你读这个地址,不会改变设备的状态。你可以放心大胆地反复读。 2. **允许合并写**:CPU 可以把对小地址的多次写操作合并成一次对连续地址的宽位写操作,提高性能。 3. **允许推测读**:CPU 可以猜测程序将要读这个地址,提前把它加载到缓存里。 4. **必须映射为 uncacheable (UC)** 或 **write-combining (WC)** 类型的内存。 **不可预取区域** (Bit 3 = 0) 则相反: 1. **读操作可能有副作用**:比如你读一个 FIFO 的数据,读一次数据就没了。 2. **禁止合并写和推测读**。每次访问必须严格按程序顺序执行。 3. **必须映射为 uncacheable (UC)** 类型的内存。 > 搞明白了吗?如果你的设备是纯粹的存储类(比如显存),设成可预取能获得最佳性能。但如果是控制寄存器、FIFO、状态寄存器,千万别设成可预取!否则 CPU 乱序执行,你的设备逻辑会乱掉。我曾经在一个网络加速卡上看到工程师把控制寄存器配成了可预取,结果驱动一加载就死机,排查了好几天才找到这个坑。 ##### 3.2.3 64 位标志 **Bit 2:1** 是专门针对 64 位地址空间的标志。 如果这两个位是 `10`,表示这个设备需要一个 **64 位** 的地址空间。这意味着它需要连续占用两个 32 位的 BAR 寄存器(比如 BAR0 和 BAR1)。这两个 BAR 要配对使用,**低 32 位 BAR** 里存的是地址的低 32 位,**高 32 位 BAR** 里存的是地址的高 32 位。 64 位标志的设置非常简单: - **Bit 2 = 0**:表示 32 位地址空间。 - **Bit 2 = 1, Bit 1 = 0**:表示 64 位地址空间。 在实际硬件设计上,如果你把一个 BAR 配成 64 位的,那么它必须是连续的。比如你不能把 BAR0 和 BAR2 配对,这会导致系统无法正确分配地址,直接报错。 嗯,这里要注意,在 32 位系统上,BIOS 可能无法为 64 位 BAR 分配高于 4G 的地址。这时候驱动需要能降级处理,否则设备就没法用了。 #### 3.3 地址对齐要求 读写 BAR 时,有一个隐藏的坑:**地址对齐**。 系统软件在给设备分配地址时,会先读出 BAR 中的所有位(硬件设计)来算设备需要多大空间。怎么算的呢? 设备会先发一个“需求单”:它把所有高地址位(Bit 31:4)写成 `1`,然后回读。系统软件发现低某些位读回来是 `0`,就知道设备需要多大的空间。这个“低某些位为 0”的部分,就是设备要求的对齐大小。 比如说,一个设备需要 4K 字节的内存空间。那么它的 BAR 中 `Bit 11:4` 必须硬件设计成只读 `0`。`Bit 31:12` 才用来放地址。 **为什么需要对齐?** 因为系统给设备分配的地址,必须是 2^N 的整数倍。N 就是低 `0` 的位数。4K 空间的对齐要求是 4K 的整数倍。你给它分配一个 0xF000 的地址,它没问题。你给它分配一个 0xF001 的地址,它就不认了。 所以,你在写硬件逻辑时,**必须保证 BAR 的最低 N 个比特位是只读 0 的**。不满足这个条件,设备在 BIOS 阶段就会被系统忽略。 #### 3.4 一张图看懂 BAR 结构 下面我画了一张图,总结了 BAR 寄存器的核心知识体系。你可以把它当成一个框架图来复习。
BAR 寄存器核心逻辑结构图 BAR 寄存器 核心结构 BAR 寄存器 (32-bit) Bit 0: 空间类型标志 0: 内存空间 1: IO 空间 Bit 2:1: 地址宽度 00: 32-bit 10: 64-bit Bit 3: 预取属性 0: 不可预取 1: 可预取 Bit 31:4: 基地址 对齐要求 最低 N 位必须为 0 (N 由所需空间大小决定)
这张图帮你把 BAR 寄存器各比特位的职责分清了。记住,**Bit 0, Bit 2:1, Bit 3** 这三个标志位的组合,决定了你的硬件逻辑应该怎么写。
**调试小贴士**:如果你发现设备在枚举阶段没有被分配地址,或者分配的地址跟你预期的不一样。第一件事就是用 `lspci -vvv` 或者 `pciutils` 工具读一下该设备的 BAR 寄存器的原始值。看哪个比特位被硬件写死了,跟你的设计是否一致。
**千万别踩的坑**:千万不要在软件里通过写 BAR 来修改设备需要的地址空间大小。BAR 寄存器的高位是软件可写的(用来放分配好的基地址),但那些标志位(Bit 0, Bit 2:1, Bit 3)以及低位地址预留位,都是硬件决定的,你写了也没用。我曾经见过一个菜鸟,试图通过写 BAR0 来把设备映射到 IO 空间,结果导致系统直接蓝屏。