2、SMMU固件架构:ATF、UEFI、OPTEE中的SMMU驱动框架,固件与内核的交互模型

这一章我们来聊聊固件侧的SMMU。很多人觉得SMMU是内核管的事,跟固件没关系。其实不然。我做过几个项目,都是因为固件侧SMMU配置没搞对,导致内核一启动就卡死,连log都看不到。说白了,固件才是SMMU的"第一责任人"。

为什么这么说?因为系统上电后,内核还没跑起来,SMMU就得先初始化好。谁来做?ATF、UEFI、OPTEE这些固件。它们负责把SMMU的全局配置、StreamID映射、安全态隔离这些基础工作做完,然后才把接力棒交给内核。

核心观点:固件是SMMU的"初始化者",内核是"运行时管理者"。两者分工明确,但交互模型必须设计好,否则性能和安全都会出问题。

2.1 三大固件中的SMMU驱动框架

先看一张整体架构图,帮你建立全局认知。

固件与内核的SMMU交互架构 固件层(EL3 / S-EL1 / S-EL2) ATF (ARM Trusted Firmware) SMMU全局初始化 StreamID配置 安全态隔离 UEFI ACPI IORT表生成 DMA保护配置 Boot Service DMA OPTEE 安全世界SMMU配置 SPMC SMMU管理 FF-A接口交互 内核层(EL1 / EL2) Linux SMMU驱动 (drivers/iommu/arm-smmu-v3.c) Stream Table / CD Table / STE / 页表 / 中断处理 / TLB维护 固件与内核的交互接口 SMC/HVC调用 · 共享内存 · IORT表 · 设备树 FF-A · SMMUv3 Native Command Queue

2.2 ATF中的SMMU驱动

ATF运行在EL3,是系统里权限最高的固件。它负责SMMU的早期初始化。我习惯把ATF的SMMU工作分成三个阶段:

  1. 冷启动初始化:配置SMMU全局寄存器,比如SMMU_CR0、SMMU_CR1、SMMU_CR2。设置StreamID的位数、队列基地址等。
  2. 安全态隔离:把安全世界的StreamID和非安全世界的StreamID分开。防止非安全世界通过DMA访问安全内存。
  3. 运行时服务:通过SMC接口响应内核的SMMU配置请求。
我的经验:ATF里SMMU初始化最坑的是StreamID的位数配置。如果芯片设计用了20位StreamID,但你只配了16位,高4位会被截断。我曾经因为这个bug查了三天,最后发现是ATF代码里一个宏定义写错了。

ATF中SMMU相关的关键代码路径:

/* ATF SMMU驱动核心接口 */
void smmu_init(uintptr_t base_addr)
{
    /* 1. 复位SMMU */
    mmio_write_32(base_addr + SMMU_CR0, 0);
    
    /* 2. 配置StreamID映射表基地址 */
    mmio_write_64(base_addr + SMMU_STRTAB_BASE, strtab_pa);
    mmio_write_32(base_addr + SMMU_STRTAB_BASE_CFG, cfg);
    
    /* 3. 使能SMMU */
    mmio_setbits_32(base_addr + SMMU_CR0, SMMU_CR0_SMMUEN);
    
    /* 4. 等待使能完成 */
    while (!(mmio_read_32(base_addr + SMMU_CR0) & SMMU_CR0_SMMUEN));
}

2.3 UEFI中的SMMU配置

UEFI的角色跟ATF不一样。它不直接操作SMMU寄存器(除非做DMA保护),而是负责生成描述SMMU拓扑的ACPI表。最关键的就是IORT表。

IORT表里描述了:

  • SMMU硬件实例的数量和基地址
  • 每个SMMU支持的StreamID范围
  • 设备到SMMU的映射关系
  • 中断信息(比如EVENTQ、PRIQ、CMDQSync中断)

内核启动时,通过IORT表知道SMMU长什么样、怎么用。如果IORT表写错了,内核要么找不到SMMU,要么配错中断号。嗯,我遇到过IORT表里中断号写反了,结果SMMU报错内核收不到中断,DMA事务全挂。

IORT表节点类型 描述 常见配置错误
ITS Group节点 描述GIC ITS信息 ITS ID与硬件不匹配
SMMUv3节点 SMMU硬件实例信息 基地址错误、中断号错误
Named Component节点 设备到SMMU的映射 StreamID映射关系写错
Root Complex节点 PCIe RC到SMMU的映射 RID范围配置错误
避坑指南:UEFI里配置IORT表时,一定要确认SMMU的Revision ID。SMMUv3.1和SMMUv3.2的IORT表字段有差异。我曾经因为Revision ID写错,导致内核无法识别SMMU的PRI功能。

2.4 OPTEE中的SMMU管理

OPTEE运行在安全世界(S-EL1或S-EL2)。它有自己的SMMU配置需求。比如安全应用(TA)要访问硬件外设,就需要通过OPTEE配置SMMU。

OPTEE的SMMU管理有几个特点:

  • SPMC(Secure Partition Manager Core):在FF-A架构下,SPMC负责管理安全分区的SMMU配置。
  • 内存隔离:安全世界的DMA必须走独立的SMMU Stream,不能和非安全世界混用。
  • FF-A接口:通过FF-A的Memory Management Descriptor来传递SMMU配置信息。

我记得有个项目,安全摄像头要通过OPTEE访问DDR。一开始没配SMMU,结果安全摄像头DMA直接写穿了非安全内存。后来在OPTEE里加了SMMU配置,把安全摄像头的StreamID映射到安全内存区域,问题才解决。

2.5 固件与内核的交互模型

固件和内核怎么配合?我总结了几种交互方式:

交互方式 调用方 用途 性能开销
SMC调用 内核 → ATF SMMU全局配置、安全态切换 高(约5000 cycles)
HVC调用 内核 → Hypervisor 虚拟化SMMU配置 中(约2000 cycles)
共享内存 双向 传递Stream Table、CD Table 低(约100 cycles)
Command Queue 内核 → SMMU硬件 TLB维护、STE配置 低(约50 cycles)

这里有个关键点:内核尽量不走SMC。为什么?因为SMC要陷入EL3,开销太大。内核应该直接操作SMMU的Command Queue来维护TLB和配置STE。只有在需要改全局配置或切换安全态时,才走SMC。

性能建议:我一般要求内核侧SMMU驱动,每秒钟的SMC调用不超过100次。超过这个数,TLB维护的开销就会明显影响IO性能。如果发现SMC调用频繁,优先考虑用Command Queue的CMD_TLBI和CMD_CFGI_STE替代。

2.6 交互模型的实际案例

拿一个实际场景来说。假设系统里有个NVMe SSD,通过SMMU做IOMMU。整个交互流程是这样的:

  1. 上电阶段:ATF初始化SMMU,配置全局寄存器,设置StreamID映射。
  2. UEFI阶段:UEFI生成IORT表,描述NVMe控制器到SMMU的映射关系。
  3. 内核启动:内核解析IORT表,找到SMMU实例和NVMe控制器的StreamID。
  4. 驱动加载:内核SMMU驱动分配STE和CD,通过Command Queue配置到硬件。
  5. 运行时:NVMe驱动发起DMA,SMMU自动完成地址翻译。TLB miss时,内核通过Command Queue做TLB维护。

这个流程里,固件只参与前两步。后面全是内核和SMMU硬件直接交互。为什么这样设计?说白了,就是性能。EL3的SMC调用太慢了,能不走就不走。

我的习惯:调试SMMU交互问题时,我一般先在ATF里加log,确认SMMU初始化成功。然后在UEFI里dump IORT表,看设备映射对不对。最后在内核里开SMMU的debugfs,看STE和CD的配置。三步走完,90%的问题都能定位。

嗯,这一章的内容就到这里。固件侧的SMMU配置,说白了就是"初始化+描述+交接"。初始化交给ATF,描述交给UEFI,安全隔离交给OPTEE,运行时管理交给内核。各司其职,才能让SMMU高效工作。