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总线上的所有配置空间统一映射到系统内存地址空间中,实现了对配置空间的直接、高效访问。
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字节传统配置 + 扩展配置)完全对应。
4.2 ECAM配置空间内存布局
ECAM将整个PCIe总线拓扑的配置空间组织为连续的内存块。下图展示了ECAM地址空间的层次结构:
ECAM地址空间总大小为:256条总线 × 1MB/总线 = 256MB。系统固件必须在物理地址空间中预留这段连续区域,并确保没有其他外设或内存与之重叠。
4.3 ARM ECAM访问流程
在ARM平台上,访问PCIe配置空间的典型步骤如下:
- 获取ECAM基地址:从设备树(Device Tree)或ACPI表中读取ECAM配置区域。
- 计算目标地址:根据总线号、设备号、功能号和寄存器偏移,按公式生成物理地址。
- 建立页表映射:如果运行在虚拟地址空间(如Linux内核),需要将ECAM物理地址映射到内核虚拟地址空间。
- 执行load/store指令:使用LDR/STR指令直接读写映射后的虚拟地址。
- 内存屏障(可选):在需要严格顺序的场景下,插入DSB/DMB指令保证访问完成。
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协商 |
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窗口。
pci_ecam_create()支持动态创建ECAM映射,避免预先分配全部256MB。