3. BAR空间原理:Base Address Register (BAR) 的类型、地址映射与内存/IO空间选择
BAR(Base Address Register)是PCIe设备与主机软件交互的第一道桥梁。设备通过BAR向系统声明自己需要多少地址空间、支持哪种访问类型(Memory或I/O),而系统固件(BIOS/UEFI)则负责为这些BAR分配物理地址,使CPU能够通过Load/Store指令直接访问设备内部的寄存器或内存区域。理解BAR的编码机制、类型选择和映射策略,是后续DMA性能调优和描述符设计的基础。
3.1 BAR的硬件结构与类型编码
每个PCIe Function最多支持6个BAR(Type 0配置空间),每个BAR为32位或64位宽度。BAR的低位用于标识类型和属性,高位用于存放基地址。系统软件通过向BAR写入全1再回读的方式,探测设备需要的地址空间大小和对齐要求。
| 位域 | 宽度 | 含义 |
|---|---|---|
| Bit 0 | 1 | 空间类型:0 = Memory Space,1 = I/O Space |
| Bit 2:1 | 2 | Memory类型:00 = 32-bit,10 = 64-bit,01/11 = 保留 |
| Bit 3 | 1 | Prefetchable:1 = 支持预取,0 = 不可预取 |
| Bit 31:4 | 28 | 基地址(32-bit BAR),实际有效位由对齐大小决定 |
3.2 Memory空间 vs I/O空间
PCIe规范虽然保留了I/O空间的支持,但在现代高性能系统中,强烈建议使用Memory空间。I/O空间存在地址范围小(x86上仅64KB)、访问延迟高、不支持预取和合并写等严重限制。
| 对比维度 | Memory空间 | I/O空间 |
|---|---|---|
| 地址范围 | 32-bit或64-bit,可达TB级 | 16-bit,仅64KB |
| 访问指令 | Load/Store(MOV, LDR/STR) | 专用IN/OUT指令 |
| 预取能力 | 支持(Prefetchable BAR) | 不支持 |
| 写合并 | 支持(WC内存类型) | 不支持 |
| 性能 | 高,可被CPU缓存 | 低,每次访问需总线事务 |
| 推荐度 | 强烈推荐 | 仅用于兼容旧设备 |
3.3 64-bit BAR与地址映射
当设备需要超过4GB的地址空间(如大容量显存、多描述符环),必须使用64-bit BAR。64-bit BAR占用两个连续的BAR槽位(如BAR0和BAR1),低BAR存放低32位基地址,高BAR存放高32位基地址。
- 枚举阶段:系统软件读取每个Function的Vendor ID/Device ID,开始配置扫描。
- 大小探测:向BAR写入全1,回读后解析类型和大小。
- 地址分配:BIOS/UEFI根据系统拓扑和MMIO空闲区域,为每个BAR分配物理基地址。
- 写入生效:将分配的基地址写入BAR寄存器,设备开始响应这些地址上的访问。
- ioremap:驱动调用
pci_ioremap_bar()将物理地址映射到内核虚拟地址空间。
3.4 Prefetchable属性与性能影响
Prefetchable位(BAR Bit 3)决定了该地址空间是否允许CPU和PCIe桥进行推测性预取和写合并。对于只包含读不影响状态(如FIFO、状态寄存器)的区域,必须设置为Non-Prefetchable;而对于RAM类存储(描述符环、数据缓冲区),应设置为Prefetchable。
| 场景 | 推荐BAR类型 | 原因 |
|---|---|---|
| 控制/状态寄存器 | 32-bit Non-Prefetchable | 避免预取导致寄存器状态误读 |
| 描述符环(Descriptor Ring) | 64-bit Prefetchable | 支持CPU写合并,减少MMIO写入次数 |
| 数据缓冲区(Data Buffer) | 64-bit Prefetchable | 支持预取和缓存,提升读吞吐 |
| 门铃寄存器(Doorbell) | 32-bit Non-Prefetchable | 每次写入必须立即生效,不可合并 |
3.5 驱动中的BAR操作示例
以下代码展示了Linux驱动中典型的BAR探测、映射和访问流程:
#include <linux/pci.h>
#include <linux/io.h>
static int my_pcie_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct my_dev *dev;
unsigned long mmio_start, mmio_len;
int ret;
// 1. 启用PCIe设备
ret = pci_enable_device(pdev);
if (ret) return ret;
// 2. 设置DMA掩码(64-bit优先)
ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
if (ret) {
ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32));
if (ret) goto err_disable;
}
// 3. 获取BAR0资源信息
mmio_start = pci_resource_start(pdev, 0);
mmio_len = pci_resource_len(pdev, 0);
// 4. 检查BAR类型(Memory vs I/O)
if (!(pci_resource_flags(pdev, 0) & IORESOURCE_MEM)) {
dev_err(&pdev->dev, "BAR0 is not Memory space!\n");
ret = -EINVAL;
goto err_disable;
}
// 5. 将BAR物理地址映射到内核虚拟地址
dev->regs = pci_ioremap_bar(pdev, 0);
if (!dev->regs) {
ret = -ENOMEM;
goto err_disable;
}
// 6. 通过虚拟地址访问设备寄存器
iowrite32(0x1, dev->regs + DOORBELL_OFFSET);
val = ioread32(dev->regs + STATUS_OFFSET);
pci_set_drvdata(pdev, dev);
return 0;
err_disable:
pci_disable_device(pdev);
return ret;
}
iowrite32()配合wmb()写屏障确保顺序;对于描述符环批量更新,优先使用memcpy_toio()或ioread32_rep()/iowrite32_rep()系列函数,利用CPU的批量传输指令减少总线事务数量。
3.6 BAR空间设计检查清单
- 类型选择:所有新设计优先使用Memory空间,避免I/O空间。
- 宽度选择:如果设备内存 > 4GB,必须使用64-bit BAR;即使小于4GB,也建议使用64-bit BAR以兼容大地址空间系统。
- Prefetchable:RAM类存储必须设置为Prefetchable;寄存器类必须设置为Non-Prefetchable。
- 对齐要求:BAR大小必须是2的幂,基地址必须按大小对齐(如8MB BAR必须对齐到8MB边界)。
- 多BAR规划:将控制寄存器和数据缓冲区分离到不同BAR,便于驱动独立映射和管理。
- 大小优化:不要申请比实际需要更大的BAR空间,避免浪费系统MMIO资源。
BAR空间的设计直接决定了CPU访问设备的效率。一个合理的BAR规划,能够为后续的DMA描述符设计提供低延迟、高带宽的MMIO通道,是PCIe驱动性能调优的基石。