3. BAR空间基础:BAR的作用、宽度与地址空间类型
基地址寄存器(Base Address Register,BAR)是PCI/PCIe设备配置空间中最关键的寄存器组之一。驱动程序通过读取BAR来获知设备在系统中的物理地址映射,从而实现对设备寄存器和内存的访问。本章深入剖析BAR的核心作用、宽度差异以及地址空间类型,为后续的映射与枚举打下基础。
3.1 BAR的核心作用
每个PCIe Function最多可拥有6个BAR(Type 0配置头),或2个BAR(Type 1配置头,用于Bridge)。BAR的本质是设备向系统报告其所需地址空间大小的“声明寄存器”,同时也是系统软件(BIOS/OS)分配实际物理地址的“写入目标”。
关键理解: BAR不是存储数据的普通寄存器,而是一种“可编程地址解码器”。设备通过BAR告诉系统:“我需要一块连续的地址空间,大小是X,类型是Y”。系统软件在枚举时写入基地址,设备随后对该地址范围内的访问进行解码响应。
BAR在驱动开发中的具体作用包括:
- 资源发现: 驱动读取BAR的初始值(全F或特定值),通过回写测试确定设备需要的地址空间大小。
- 地址分配: 系统固件或操作系统将分配好的物理基地址写入BAR,设备据此响应总线事务。
- 类型识别: 通过BAR的低位比特位,驱动可判断该BAR映射的是内存空间还是I/O空间,以及是32位还是64位地址。
- 驱动映射: 驱动从BAR中读出已分配的物理地址,调用ioremap()或MmMapIoSpace()等接口建立虚拟地址到物理地址的映射,从而访问设备寄存器。
3.2 BAR的宽度:32位与64位
PCIe设备支持两种宽度的BAR:32位BAR和64位BAR。宽度决定了设备能够请求的地址空间范围。
| 特性 | 32位BAR | 64位BAR |
|---|---|---|
| 地址范围 | 0x00000000 ~ 0xFFFFFFFF(4GB) | 0x00000000_00000000 ~ 0xFFFFFFFF_FFFFFFFF(16EB) |
| 占用BAR槽位 | 1个(如BAR0) | 2个连续槽位(如BAR0 + BAR1) |
| 适用场景 | 小容量寄存器组、传统设备 | 大容量显存、高带宽缓冲区、FPGA大地址空间 |
| 驱动处理 | 读取单个BAR即可获得完整地址 | 需将高低两个BAR组合为一个64位地址 |
驱动开发提示: 当检测到BAR的低4位为
0b0100(即bit[2:1] = 10)时,表示该BAR为64位内存空间。此时必须将下一个BAR(BARn+1)视为高32位部分,驱动应一次性读取两个BAR并拼接为64位地址。
下图展示了32位BAR与64位BAR在配置空间中的占用方式:
3.3 BAR的地址空间类型:内存空间与I/O空间
PCIe规范定义了两种地址空间类型,BAR通过其最低位(bit 0)来标识:
- bit 0 = 0: 映射到内存空间(Memory Space)
- bit 0 = 1: 映射到I/O空间(I/O Space)
两种空间类型在驱动开发中的差异显著,下表总结了关键区别:
| 对比维度 | 内存空间(Memory Space) | I/O空间(I/O Space) |
|---|---|---|
| 访问指令 | 普通内存读写(MOV、STR、LDR) | 专用I/O指令(IN、OUT,x86架构) |
| 地址范围 | 系统物理地址空间的一部分 | 独立的64KB I/O地址空间(x86) |
| 性能 | 高,支持CPU缓存、预取、合并写 | 低,每次访问需通过I/O控制器,不可缓存 |
| 驱动映射方式 | ioremap() / MmMapIoSpace() | ioport_map() / 直接使用IN/OUT指令 |
| 现代设备趋势 | 主流,几乎所有高性能设备使用 | 逐渐淘汰,仅用于兼容旧设备或控制寄存器 |
| PCIe规范支持 | 完全支持,推荐使用 | 可选支持,部分Root Complex可能不支持 |
重要提醒: 在x86以外的架构(如ARM、RISC-V)中,I/O空间可能不存在或通过内存映射方式模拟。因此,新开发的PCIe驱动应优先使用内存空间BAR,以保证跨平台兼容性。如果设备同时提供两种类型的BAR,驱动应优先选择内存空间BAR进行映射。
3.4 BAR的编码格式与类型识别
BAR寄存器的低4位(bit[3:0])包含了类型和编码信息,驱动通过读取这些位即可判断BAR的属性。下图展示了BAR的位布局:
驱动在枚举过程中,通过以下伪代码逻辑识别BAR类型:
/* 伪代码:BAR类型识别 */
uint32_t bar_value = pci_config_readl(dev, offset);
if (bar_value & 0x1) {
/* bit[0] = 1 → I/O空间BAR */
bar_type = PCI_BAR_TYPE_IO;
bar_size = 计算I/O空间大小();
} else {
/* bit[0] = 0 → 内存空间BAR */
if ((bar_value & 0x6) == 0x4) {
/* bit[2:1] = 10 → 64位内存BAR */
bar_type = PCI_BAR_TYPE_MEM64;
/* 需要读取下一个BAR作为高32位 */
bar_addr_hi = pci_config_readl(dev, offset + 4);
bar_addr = ((uint64_t)bar_addr_hi << 32) | (bar_value & ~0xF);
} else {
/* bit[2:1] = 00 → 32位内存BAR */
bar_type = PCI_BAR_TYPE_MEM32;
bar_addr = bar_value & ~0xF;
}
}
3.5 BAR空间大小探测方法
驱动开发中一个经典操作是“探测BAR大小”。其原理是:向BAR写入全1值,然后回读,根据保留为0的位确定地址范围。具体步骤如下:
- 保存原始值: 读取BAR当前值并保存。
- 写入全1: 向BAR写入
0xFFFFFFFF(32位)或0xFFFFFFFF_FFFFFFFF(64位)。 - 回读并取反: 读取BAR值,对低有效位取反加1,得到地址空间大小。
- 恢复原始值: 将保存的原始值写回BAR。
示例: 假设BAR0原始值为
0xFE000000(已分配地址)。写入 0xFFFFFFFF 后回读得到 0xFF000000。取反低24位得到 0x00FFFFFF,加1得到 0x01000000,即该BAR大小为16MB。
下表总结了不同大小BAR的探测结果特征:
| BAR大小 | 写入全1后回读值 | 地址位宽 | 典型应用 |
|---|---|---|---|
| 4KB | 0xFFFFF000 | 12位 | 小型控制寄存器组 |
| 1MB | 0xFFF00000 | 20位 | 中等大小缓冲区 |
| 16MB | 0xFF000000 | 24位 | 帧缓冲、DMA区域 |
| 256MB | 0xF0000000 | 28位 | 大容量显存 |
| 4GB(64位BAR) | 0x00000000_FFFFFFFF | 32位 | 超大地址空间设备 |
3.6 本章小结
- BAR是PCIe设备向系统声明地址空间需求的机制,驱动通过BAR获取设备的物理地址映射。
- 32位BAR支持最大4GB地址空间,64位BAR支持16EB,但需占用两个连续BAR槽位。
- 内存空间BAR(bit[0]=0)是现代PCIe设备的主流选择,性能优于I/O空间BAR。
- 驱动通过BAR的低4位编码识别类型,并通过“写全1回读”方法探测BAR大小。
- 理解BAR的宽度和类型是后续实现ioremap映射和DMA缓冲区分配的前提。