4. ARM平台PCIe配置空间访问:ARM ECAM机制与MMIO映射配置空间

ARM平台与x86在PCIe配置空间访问上存在根本性差异。x86依赖CF8h/CFCh I/O端口或MMCONFIG,而ARM架构从设计上取消了I/O空间,完全采用内存映射I/O(MMIO)方式访问PCIe配置空间。ARM通过ECAM(Enhanced Configuration Access Mechanism)机制,将PCIe总线上的所有配置空间统一映射到系统内存地址空间中,实现了对配置空间的直接、高效访问。

核心要点: ARM平台没有I/O空间,PCIe配置空间访问完全依赖MMIO。ECAM机制将整个PCIe域(最多256条总线、32个设备、8个功能)的配置空间线性映射到一段连续的物理地址区域。

4.1 ECAM机制原理

ECAM是ARM服务器和嵌入式平台访问PCIe配置空间的标准方法。它将PCIe配置空间映射到系统内存的特定区域,CPU通过普通的load/store指令即可读写配置寄存器。

ECAM地址映射公式:

配置空间地址 = ECAM基地址 + (Bus_Number << 20) | (Device_Number << 15) | (Function_Number << 12) | Register_Offset

其中:

  • ECAM基地址:由系统固件(如UEFI、ATF)在启动时分配,通常位于高地址区域(如0x40000000以上)。
  • Bus_Number:8位,范围0~255,对应PCIe总线号。
  • Device_Number:5位,范围0~31,对应设备号。
  • Function_Number:3位,范围0~7,对应功能号。
  • Register_Offset:12位,范围0~4095,对应配置空间内的寄存器偏移。

每个PCIe功能(Function)占用4KB配置空间,与PCIe规范定义的扩展配置空间(256字节传统配置 + 扩展配置)完全对应。

ARM vs x86 配置空间访问对比: x86的CF8h/CFCh方法需要两次I/O操作(先写地址端口,再读写数据端口),而ARM的ECAM只需一次内存访问,延迟更低,更适合高性能场景。

4.2 ECAM配置空间内存布局

ECAM将整个PCIe总线拓扑的配置空间组织为连续的内存块。下图展示了ECAM地址空间的层次结构:

ARM ECAM配置空间内存布局 ECAM基地址(物理内存起始地址) 总线0(Bus 0) — 1MB 总线1(Bus 1) — 1MB ⋮ 总线255(Bus 255) — 1MB 每条总线内部:32个设备 × 8个功能 × 4KB配置空间 = 1MB 设备号[4:0] + 功能号[2:0] + 寄存器偏移[11:0] 构成内部偏移

ECAM地址空间总大小为:256条总线 × 1MB/总线 = 256MB。系统固件必须在物理地址空间中预留这段连续区域,并确保没有其他外设或内存与之重叠。

4.3 ARM ECAM访问流程

在ARM平台上,访问PCIe配置空间的典型步骤如下:

  1. 获取ECAM基地址:从设备树(Device Tree)或ACPI表中读取ECAM配置区域。
  2. 计算目标地址:根据总线号、设备号、功能号和寄存器偏移,按公式生成物理地址。
  3. 建立页表映射:如果运行在虚拟地址空间(如Linux内核),需要将ECAM物理地址映射到内核虚拟地址空间。
  4. 执行load/store指令:使用LDR/STR指令直接读写映射后的虚拟地址。
  5. 内存屏障(可选):在需要严格顺序的场景下,插入DSB/DMB指令保证访问完成。
注意事项: ARM架构对MMIO访问的缓存属性要求严格。ECAM区域必须映射为非缓存(Device-nGnRnE)属性,禁止使用Cache,否则会导致配置空间读取数据不一致或写入丢失。

4.4 设备树中的ECAM描述

在ARM嵌入式系统中,PCIe控制器及其ECAM配置通常在设备树中描述。以下是一个典型的PCIe节点示例:

pcie@40000000 {
    compatible = "arm,pcie-ecam";
    reg = <0x0 0x40000000 0x0 0x10000000>;  // ECAM基地址和大小(256MB)
    bus-range = <0x0 0xff>;                 // 总线号范围
    #address-cells = <3>;
    #size-cells = <2>;
    device_type = "pci";
    ranges = <0x02000000 0x0 0x50000000 0x0 0x50000000 0x0 0x10000000>;
    // 配置空间访问方式:ECAM
    msi-parent = <&its>;
    status = "okay";
};

设备树中的关键字段说明:

字段 含义 ARM平台典型值
reg ECAM配置空间的物理基地址和大小 0x40000000,大小256MB
bus-range 该PCIe控制器管理的总线号范围 <0x0 0xff>(全部256条总线)
ranges PCIe地址空间到CPU物理地址的映射 定义MMIO和I/O(ARM通常不使用I/O)
compatible 驱动匹配字符串 "arm,pcie-ecam"

4.5 ARM ECAM与x86 MMCONFIG的异同

ARM的ECAM与x86的MMCONFIG(Memory-Mapped Config)在原理上非常相似,但存在关键差异:

对比维度 ARM ECAM x86 MMCONFIG
地址空间来源 设备树/ACPI静态分配 BIOS/UEFI在启动时分配
配置空间大小 固定256MB(256总线 × 1MB) 通常256MB,但可配置
访问方式 仅MMIO(无I/O空间) MMIO + 传统I/O(CF8h/CFCh)
缓存属性 必须Device-nGnRnE 必须Uncacheable(UC)
页表映射 需要显式建立页表 早期使用I/O端口,MMCONFIG需映射
热插拔支持 依赖固件动态更新ECAM范围 通过ACPI _OSC协商
开发建议: 在ARM平台编写PCIe驱动时,不要假设配置空间基地址固定。始终从设备树或ACPI表中动态获取ECAM基地址,并正确设置MMIO的缓存属性。使用ioremap()时指定MT_DEVICE或pgprot_device()。

4.6 ECAM访问的软件实现示例

以下是在ARM Linux内核中通过ECAM访问PCIe配置空间的典型代码片段:

#include <linux/io.h>
#include <linux/pci.h>

/* ECAM基地址(从设备树获取) */
static void __iomem *ecam_base;

/* 初始化ECAM映射 */
int pci_ecam_init(phys_addr_t base, size_t size)
{
    ecam_base = ioremap(base, size);
    if (!ecam_base)
        return -ENOMEM;
    /* 设置内存属性为Device-nGnRnE */
    set_memory_uc((unsigned long)ecam_base, size >> PAGE_SHIFT);
    return 0;
}

/* 读取配置空间32位寄存器 */
u32 pci_ecam_read_config(struct pci_bus *bus, unsigned int devfn,
                         int where, int size)
{
    u8 bus_num = bus->number;
    u8 dev_num = PCI_SLOT(devfn);
    u8 func_num = PCI_FUNC(devfn);
    void __iomem *addr;

    /* 计算ECAM地址 */
    addr = ecam_base + (bus_num << 20) | (dev_num << 15) |
           (func_num << 12) | (where & 0xfff);

    /* 按大小读取 */
    switch (size) {
    case 1:
        return readb(addr);
    case 2:
        return readw(addr);
    case 4:
        return readl(addr);
    default:
        return 0xFFFFFFFF;
    }
}

上述代码展示了ECAM地址计算和MMIO读取的核心逻辑。实际内核实现(如drivers/pci/ecam.c)还包含总线扫描、错误处理和多域支持。

4.7 ARM ECAM的局限性及应对

ECAM机制虽然高效,但在实际应用中存在以下限制:

  • 地址空间碎片化:256MB连续物理地址在嵌入式系统中可能难以分配,尤其是内存地址空间紧张的平台。
  • 不支持非标准配置空间大小:ECAM假设每个功能固定4KB,某些专用硬件可能使用更大的配置空间。
  • 热插拔场景的地址更新:当PCIe设备热插拔导致总线拓扑变化时,ECAM映射范围可能需要动态调整。
  • 多PCIe域支持:每个PCIe域需要独立的ECAM区域,系统需要管理多个256MB窗口。
应对策略: 对于地址空间紧张的平台,可以使用分段ECAM(Segmented ECAM),只映射实际存在的总线范围。Linux内核通过pci_ecam_create()支持动态创建ECAM映射,避免预先分配全部256MB。