1. PCIe 地址空间基础:配置空间、MMIO 空间、IO 空间与 BAR 寄存器

在深入 ioremap 的实战陷阱之前,必须首先建立对 PCIe 地址空间的清晰认知。PCIe 设备通过三种独立的地址空间与主机通信,而 BAR(基地址寄存器) 则是连接这些空间与驱动代码的桥梁。理解这些基础概念,是避免后续映射错误的第一步。

1.1 PCIe 三大地址空间概览

PCIe 总线继承了 PCI 的地址空间模型,将设备资源划分为三个逻辑上独立的区域。驱动开发者必须明确区分它们,因为 ioremap 只适用于其中一种。

地址空间 访问方式 大小限制 驱动映射方式 典型用途
配置空间 通过 CFG 读写(I/O 或内存映射) 256B / 4KB(ECAM) 不直接映射,通过 pci_read_config_* 访问 设备枚举、BAR 读取、中断配置
MMIO 空间 内存映射读写 最大 64 位地址范围 ioremap() / pci_iomap() 设备寄存器、数据缓冲区、描述符环
IO 空间 通过 IN/OUT 指令 最大 64KB(x86) ioport_map()(非 ioremap) 传统设备控制寄存器(x86 平台)
实战要点: 绝大多数现代 PCIe 设备使用 MMIO 空间 暴露寄存器。IO 空间在 x86 以外的平台(ARM、RISC-V)上可能不存在或需要特殊处理。配置空间由内核 PCI 子系统管理,驱动不应直接 ioremap 配置空间。

1.2 BAR 寄存器:设备资源的“门牌号”

每个 PCIe 设备功能(Function)最多可拥有 6 个 BAR(Base Address Register),位于配置空间的偏移 0x10 至 0x24。BAR 存储了设备请求的地址空间类型、大小和基地址。系统固件(BIOS/UEFI)或操作系统在枚举时,会为每个 BAR 分配一个物理地址。

BAR 的类型与编码

BAR 寄存器的低比特位决定了其类型,这是驱动解析时必须检查的关键字段:

  • Bit 0 = 0:MMIO 空间(Memory Space)
  • Bit 0 = 1:IO 空间(I/O Space)
  • Bit 2:1(仅 MMIO 时有效):
    • 00:32 位地址(BAR 为 32 位宽)
    • 10:64 位地址(BAR 占用连续两个 BAR 槽位,共 64 位)
  • Bit 3(仅 MMIO 时有效):Prefetchable 标志,表示该区域是否支持预取。
关键陷阱: 64 位 MMIO BAR 会占用 两个连续的 BAR 索引(例如 BAR0 和 BAR1)。驱动在遍历 BAR 时,如果检测到 64 位类型,必须跳过下一个 BAR 槽位,否则会读到错误的地址。

1.3 从 BAR 到物理地址:驱动必须经历的步骤

驱动获取设备 MMIO 物理地址的标准流程如下,这也是后续 ioremap 的输入来源:

  1. 读取 BAR 原始值:调用 pci_read_config_dword(pdev, PCI_BASE_ADDRESS_0, &bar_raw)。
  2. 解析类型与掩码:检查 bar_raw & 0x01 判断 MMIO/IO;对于 MMIO,使用 bar_raw & PCI_BASE_ADDRESS_MEM_MASK(即 ~0xF)得到对齐后的基地址。
  3. 获取物理地址:对于 64 位 BAR,需要读取下一个 BAR 寄存器作为高 32 位,组合成 phys_addr_t。
  4. 获取资源大小:内核已通过 pci_resource_start() 和 pci_resource_len() 封装了上述过程,驱动应优先使用这些 API。
// 推荐做法:使用内核封装 API 获取 MMIO 物理地址和长度
struct pci_dev *pdev = ...;  // 从 pci_get_device 或 probe 获得
int bar_index = 0;

unsigned long phys_addr = pci_resource_start(pdev, bar_index);
unsigned long region_len = pci_resource_len(pdev, bar_index);
unsigned long flags      = pci_resource_flags(pdev, bar_index);

if (!(flags & IORESOURCE_MEM)) {
    dev_err(&pdev->dev, "BAR %d is not MMIO!\n", bar_index);
    return -EINVAL;
}

// 检查是否为 64 位 BAR(可选,用于调试)
if (flags & IORESOURCE_MEM_64)
    dev_info(&pdev->dev, "BAR %d is 64-bit MMIO\n", bar_index);

dev_info(&pdev->dev, "BAR%d: phys=0x%lx, len=%lu\n",
         bar_index, phys_addr, region_len);

1.4 地址空间关系图:从 PCIe 设备到 CPU 视角

下图展示了 PCIe 设备内部的地址空间如何通过 BAR 映射到 CPU 的物理地址空间,以及驱动最终通过 ioremap 建立虚拟地址映射的完整链路。

PCIe 地址空间映射关系 PCIe 地址空间映射链路 PCIe 设备 (Endpoint) 配置空间 (256B) VID/DID/BAR/Class MMIO 空间 寄存器 / 数据缓冲区 IO 空间 (可选) 传统控制寄存器 BAR 寄存器 BAR0: MMIO 32-bit BAR1: MMIO 64-bit (高) BAR2: IO PCIe 总线 & RC 地址转换 / 路由 TLP 事务层 系统固件分配 物理地址映射 CPU 物理地址空间 MMIO 区域 0xFE00_0000 - ... IO 端口区域 0x1000 - 0x10FF DRAM (系统内存) 0x0000_0000 - ... ioremap 映射 物理 → 虚拟地址 页表建立 Cache 属性设置 BAR 地址请求 分配物理地址 驱动通过 pci_resource_start() 获取物理地址

1.5 常见陷阱与检查清单

⚠️ 本节核心陷阱: 驱动开发者在处理 BAR 时最容易犯的三个错误:
  • 混淆 IO 空间与 MMIO 空间:对 IO 空间使用 ioremap 会导致映射失败或访问异常。必须通过 pci_resource_flags() & IORESOURCE_IO 判断。
  • 忽略 64 位 BAR 的槽位占用:遍历 BAR 时未跳过被 64 位 BAR 占用的下一个索引,导致读取到无效地址。
  • 直接使用 BAR 原始值:未掩码低 4 位(类型/属性位),导致地址错位。

驱动开发者的 BAR 处理检查清单

步骤 检查项 正确做法
1 确认 BAR 类型 使用 pci_resource_flags() 检查 IORESOURCE_MEM 或 IORESOURCE_IO
2 确认 BAR 宽度 检查 IORESOURCE_MEM_64 标志,若为 64 位则跳过下一个 BAR 索引
3 获取物理地址 使用 pci_resource_start(),不要手动解析 BAR 原始值
4 获取区域长度 使用 pci_resource_len(),确保映射范围不越界
5 检查地址非零 如果 phys_addr == 0,说明 BAR 未分配或设备未使能

1.6 小结:本节知识图谱

本节建立了 PCIe 地址空间的基础框架,这是后续所有 ioremap 实战讨论的基石。请确保你已掌握以下核心概念:

  • PCIe 的 三种地址空间(配置、MMIO、IO)及其访问方式差异。
  • BAR 寄存器 作为设备资源声明和系统地址分配的核心机制。
  • 驱动获取 MMIO 物理地址的 标准 API 流程(pci_resource_start/len/flags)。
  • 64 位 BAR 的 槽位占用规则 和 IO 空间的 映射方法区别。