自制工业控制系统:PCIe总线枚举实战——ECAM配置空间、BAR探测与MSI配置
Contents
起因
自研的 GD32F4+FPGA 运动控制卡和 NVMe 固态盘都挂在 PCIe 总线上,用之前得先把它们找出来。PCIe 本身就是"被逼出来"的总线:1992 年的 PCI 是共享并行总线,设备一多频率就上不去,2003 年 PCI-SIG 用串行点对点加交换结构把它整个重做——今天枚举的这棵树就是那场重做的遗产。枚举用的两件工具也各有来历:老配置空间访问走 CF8/CFC 端口,天生 256 条总线的天花板,PCI 固件规范为此补了内存映射的 ECAM;MSI 是 PCI 2.2 引入的消息式中断,设备写一笔内存就算发中断,比拉物理中断线省引脚也好扩展。这篇做 PCIe 枚举:从 MCFG 拿 ECAM 基址、深度优先走拓扑、探测 BAR、使能设备、配好 MSI。枚举产物是一张设备清单,后面 NVMe 驱动和运动控制卡驱动都从这里起步。
一、配置空间怎么访问:ECAM
现代 UEFI 平台走 ECAM(内存映射配置空间)。基址不是猜的——从 ACPI MCFG 表读:移交的 RSDP → XSDT → 找签名 "MCFG" 的表,表体是若干条目,每条给 (段号, 起始总线, 结束总线, 基址)。
ECAM 的地址拼法一句话记牢:
|
|
每个设备功能分到 4KB 的配置空间窗口。配置空间常用偏移:
| 偏移 | 字段 | 说明 |
|---|---|---|
| 0x00 | Vendor ID(16 位) | 0xFFFF = 空槽,枚举的判据 |
| 0x02 | Device ID(16 位) | 具体型号;自研卡的 Vendor/Device 对是驱动匹配的记号 |
| 0x04 | Command | bit1 内存空间使能,bit2 总线主控(DMA 必开) |
| 0x08 | Revision/Class Code | 类码区分 NVMe 控制器(0x010802)等 |
| 0x0E | Header Type | 低 7 位:0=端点,1=PCI-PCI 桥;bit7=多功能 |
| 0x10–0x24 | BAR0–BAR5 | 基址寄存器 |
| 0x34 | Capabilities 指针 | MSI/MSI-X 能力链的入口 |
二、枚举:深度优先走桥
从总线 0 开始,每总线 32 设备 × 8 功能,判空靠 Vendor ID:
|
|
深度优先是为了桥的下游先编好号再回填。桥的总线号寄存器有三个:0x18 Primary(上游)、0x19 Secondary(下游)、0x1A Subordinate(下游最大总线号)——必须先把 Secondary/Subordinate 开出来,下游设备的配置空间才映射得进来;扫完下游把 Subordinate 收紧到实际最大值,不然桥会替不存在的总线转发配置访问。工控板拓扑简单(根口直挂 NVMe + 运动卡,最多一层桥),但代码按通用写法走,QEMU 和真机行为一致。
三、BAR 探测:写入全 1 读回
BAR 的尺寸不能问,只能探:保存原值 → 写 0xFFFFFFFF → 读回 → 按类型清掉低位标志位 → 取反加一就是尺寸 → 恢复原值。
|
|
三个实战坑:恢复原值是纪律(枚举完设备就不可用了,固件分的基址全乱);64 位 BAR 占两个寄存器要连着探;固件已经分好基址了,我们读出来登记就行,不重新分配——专用系统信固件的地基。
identity 分页下 MMIO 物理地址可以直接访问,驱动拿到 (BAR 基址, 尺寸) 就能 as *mut u32 开始干活。最后开 Command 的 bit1|bit2(内存空间 + 总线主控),DMA 设备不开 bus master 发不出任何传输。
四、MSI:中断的接线
设备能力链从 0x34 的指针开始,每个能力有"下一指针"串成链,按能力 ID 找:0x05 = MSI,0x11 = MSI-X。MSI 能力结构的关键字段:消息地址寄存器(+4 起,32 位)、消息数据寄存器(+8 起,16 位有效)、控制寄存器(+2,bit0 使能,bit7 64 位地址支持)。
接线三步:
|
|
一次普通内存写就变成一个中断——没有共享线、没有电平维持。自研卡值得把 MSI 的多消息能力也用上:控制寄存器 bit4..6 写入要启用的向量个数(log2 计数,1/2/4/8,上限看 bit1..3 的能力位),设备中断时改写消息数据低位选向量——比如运动卡分 4 个:放电完成、状态就绪、故障、门铃,驱动一眼分得清来源,省掉"先读状态寄存器再分派"的来回。向量号从设备区 0x20+ 分(APIC 篇的优先级表),天然低于插补 0xD0——NVMe 完成中断再忙也插不进放电节拍,这个隔离不用写一行代码,优先级硬件自带。
五、DMA 内存的前置约束
- 物理连续 + 可寻址:无 IOMMU 假设下,DMA 缓冲必须物理连续,且落在设备的可寻址范围内(保守做法:全部 4GB 以下)。DMA 池在内存管理器篇已经建好——低 4GB 一刀切出来的那块,NVMe 的 PRP 页、EHCI 的队列头都从这里出
- 缓存一致性白送:x86 平台 PCIe DMA 天然 cache 一致(snoop),不用手动刷缓存——这是相对 ARM 最大的减负
六、枚举清单之后:设备框架的接口
枚举的终点不是一张打印清单,是驱动注册表。系统层把它做成通用框架,不写死任何一块卡:
|
|
NVMe、EHCI、运动控制卡在框架眼里一律平等:注册一个 PciDriver,按 Vendor/Device 或类码匹配,probe 拿到 BAR 元组、MSI 向量和 DMA 池句柄自己开工。这也是两层架构在这篇的落点:框架归系统层,具体的卡——包括 GD32F4+FPGA 运动控制卡——在应用篇的适配层里实现,换卡换机床不动这里一行。
总结
实战清单:MCFG 取 ECAM 基址、bus<<20|dev<<15|fn<<12 拼地址、Vendor 0xFFFF 判空、Header Type 分桥递归(0x18/0x19/0x1A 开窗口、Subordinate 收紧)、写全 1 探 BAR 并恢复原值、Command 开 bit1|bit2、能力链找 MSI 写地址/数据/使能(多消息按 2 的幂启用)、DMA 用内存管理器篇的低 4GB 池、枚举产物挂进 PciDriver 注册表。跑通标志:枚举清单里能看到 NVMe 控制器(类码 0x010802)和自研卡的 Vendor/Device ID,QEMU 里 lspci 对得上号;注册一个空 probe 的测试驱动能被自研卡的 ID 命中。下一篇啃 USB:EHCI 主机控制器驱动。
参考链接
- OSDev Wiki: PCI(配置空间/BAR/能力链)/ PCI Express / MSI
- 本系列 APIC 篇:APIC定时器与系统时间实战(向量布局与 MSI 机制的另一半)
- 本系列第一篇:自制工业控制系统:从UEFI到内核
Author 软件开发大郭
LastMod 2026-09-11