3. BAR空间原理
BAR(Base Address Register,基地址寄存器)是PCIe设备配置空间中最关键的寄存器组,它决定了主机CPU如何访问设备内部的寄存器和内存缓冲区。理解BAR的工作原理是进行MMIO编程的基础。
3.1 BAR寄存器的作用
每个PCIe设备在配置空间中拥有最多6个BAR寄存器(Function 0通常有6个,其他Function可能较少)。每个BAR描述了一段连续的地址空间,用于以下目的:
- 设备资源声明:设备通过BAR向系统声明它需要多大的地址空间(内存或I/O)。
- 基地址分配:系统固件(BIOS/UEFI)或操作系统在枚举过程中,将物理地址写入BAR,确定设备资源在系统地址空间中的位置。
- 访问接口:CPU通过BAR中保存的基地址加上内部偏移,直接读写设备内部的寄存器或内存缓冲区。
3.2 BAR地址映射机制
BAR地址映射是PCIe枚举过程中的核心步骤,分为两个阶段:大小探测和基地址分配。
3.2.1 大小探测(Size Detection)
系统软件通过向BAR写入全1(0xFFFFFFFF),然后回读,来确定设备需要多大的地址空间。设备硬件会将不关心的位(即地址空间大小对应的低位)强制返回0,从而让软件计算出所需空间大小。
// 伪代码:探测BAR0所需内存大小
uint32_t bar_original = pci_read_config_dword(dev, 0x10); // 保存原始值
pci_write_config_dword(dev, 0x10, 0xFFFFFFFF); // 写入全1
uint32_t bar_probe = pci_read_config_dword(dev, 0x10); // 回读
uint32_t bar_size = ~(bar_probe & 0xFFFFFFF0) + 1; // 计算大小
pci_write_config_dword(dev, 0x10, bar_original); // 恢复原始值
3.2.2 基地址分配(Base Address Assignment)
系统软件根据探测到的大小,在系统物理地址空间中分配一块连续的、未使用的区域,并将起始地址写入BAR。写入时,软件必须保证地址与BAR大小对齐(即地址必须是大小的整数倍)。
3.2.3 映射关系示意图
下图展示了从CPU虚拟地址到PCIe设备内部寄存器的完整映射路径:
3.3 内存空间与IO空间的区别
PCIe规范支持两种地址空间类型:内存空间(Memory Space)和IO空间(I/O Space)。BAR寄存器的第0位(即最低位)用于区分类型:0表示内存空间,1表示IO空间。
| 对比维度 | 内存空间(Memory Space) | IO空间(I/O Space) |
|---|---|---|
| BAR位0值 | 0 | 1 |
| 地址宽度 | 32位或64位(支持64位BAR) | 32位(x86实际使用16位地址) |
| 访问指令 | 普通内存访问指令(MOV/STR/LDR) | 专用IO指令(IN/OUT on x86) |
| 访问粒度 | 字节、字、双字、四字 | 通常为字节或字 |
| 缓存行为 | 可配置为Uncacheable(强烈建议) | 总是非缓存、非合并 |
| 地址空间大小 | 整个系统物理地址空间(可达TB级) | 64KB(x86传统限制) |
| 性能 | 高(可利用CPU缓存一致性协议) | 低(每次访问需通过IO控制器) |
| 现代设备使用 | 绝大多数PCIe设备(NVMe、GPU、网卡) | 极少使用(仅遗留设备) |
3.3.1 内存空间详解
内存空间是PCIe设备最常用的地址空间类型。当BAR被配置为内存空间时,设备内部的寄存器被映射到系统的物理地址空间中,CPU可以直接使用内存访问指令进行读写。
- Prefetchable vs Non-Prefetchable:BAR的第3位表示是否可预取(Prefetchable)。可预取空间允许CPU推测性读取和合并写入,适用于不含读副作用的缓冲区(如帧缓冲)。不可预取空间用于包含状态寄存器的区域,每次读取必须精确执行。
- 64位BAR:当BAR的第2:1位为10时,表示该BAR是64位的,占用连续的两个BAR寄存器(如BAR0和BAR1),提供更大的地址空间。
ioremap()将物理地址映射到内核虚拟地址空间,然后通过readl()/writel()等函数访问。对于可预取空间,可以使用memremap()并指定MEMREMAP_WB标志。
3.3.2 IO空间详解
IO空间是x86架构遗留的地址空间概念,通过专用的IN/OUT指令访问。在PCIe时代,IO空间的使用已经大幅减少:
- 地址限制:x86 IO空间只有64KB(0x0000-0xFFFF),且大部分已被系统板载设备占用。
- 性能瓶颈:每次IO访问都需要通过IO控制器,无法利用CPU缓存,延迟远高于内存访问。
- 非x86架构:ARM、RISC-V等架构通常不支持IO空间,所有外设访问都通过内存映射实现。
3.3.3 空间类型选择决策流程
下图帮助开发者在设计或配置PCIe设备时选择合适的地址空间类型:
3.4 BAR寄存器格式详解
BAR寄存器的位定义因类型而异,理解这些位是正确配置BAR的前提。
3.4.1 32位内存空间BAR格式
位 31:4 — 基地址(Base Address),必须与大小对齐
位 3 — Prefetchable(可预取):1=可预取,0=不可预取
位 2:1 — 类型:00=32位,10=64位(占用两个BAR)
位 0 — 空间类型:0=内存空间
3.4.2 IO空间BAR格式
位 31:2 — 基地址(Base Address),必须与大小对齐
位 1 — 保留(必须为0)
位 0 — 空间类型:1=IO空间
3.5 Linux内核中的BAR操作
在Linux驱动开发中,通过PCI核心API操作BAR,无需直接读写配置空间:
#include <linux/pci.h>
struct pci_dev *dev = pci_get_device(0x10EE, 0x1234, NULL);
if (!dev) {
pr_err("Device not found\n");
return -ENODEV;
}
// 获取BAR0的物理地址和大小
resource_size_t bar0_start = pci_resource_start(dev, 0);
resource_size_t bar0_end = pci_resource_end(dev, 0);
resource_size_t bar0_len = pci_resource_len(dev, 0);
unsigned long bar0_flags = pci_resource_flags(dev, 0);
// 检查BAR类型
if (bar0_flags & IORESOURCE_MEM) {
pr_info("BAR0 is Memory Space\n");
if (bar0_flags & IORESOURCE_PREFETCH)
pr_info("BAR0 is Prefetchable\n");
} else if (bar0_flags & IORESOURCE_IO) {
pr_info("BAR0 is I/O Space\n");
}
// 将BAR0映射到内核虚拟地址空间
void __iomem *bar0_virt = pci_iomap(dev, 0, bar0_len);
if (!bar0_virt) {
pr_err("pci_iomap failed\n");
return -ENOMEM;
}
// 通过MMIO访问设备寄存器
u32 val = ioread32(bar0_virt + 0x1000); // 读取偏移0x1000的寄存器
iowrite32(0x1, bar0_virt + 0x1004); // 写入偏移0x1004的寄存器
// 解除映射
pci_iounmap(dev, bar0_virt);
pci_resource_start():返回BAR的物理基地址(系统分配后的值)。pci_resource_len():返回BAR的大小(通过探测获得)。pci_resource_flags():返回BAR的类型标志(IORESOURCE_MEM/IORESOURCE_IO等)。pci_iomap():将BAR物理地址映射到内核虚拟地址空间,返回void __iomem *指针。
3.6 本章小结
| 知识点 | 核心要点 |
|---|---|
| BAR寄存器作用 | 声明设备所需地址空间大小,保存系统分配的基地址,提供CPU访问设备的入口 |
| 地址映射机制 | 系统通过写入全1探测大小,然后分配对齐的基地址写入BAR,建立物理地址到设备内部的映射 |
| 内存空间 vs IO空间 | 内存空间是现代PCIe设备的主流选择,支持64位地址、高性能、非x86架构兼容;IO空间仅用于遗留设备 |
| BAR格式 | 位0决定空间类型,位2:1决定32/64位,位3决定可预取性;IO空间格式更简单 |
| Linux操作 | 使用pci_resource_*系列函数获取BAR信息,pci_iomap建立MMIO映射,ioread/iowrite访问寄存器 |