2. SMMU固件初始化流程:固件启动阶段、Stream Table配置、Context Bank初始化、默认安全配置

SMMU的初始化,说白了就是给这个地址翻译引擎“上电开窍”。你想想看,一个刚上电的SMMU,内部寄存器全是默认值,Stream Table指针指向垃圾地址,Context Bank里全是随机数。这时候CPU发个DMA请求过来,SMMU根本不知道该怎么翻译,大概率直接给你来个translation fault。

所以固件初始化这件事,做对了是性能的起点,做错了就是系统崩溃的起点。我个人习惯把初始化拆成四个阶段:启动准备 → Stream Table建表 → Context Bank配置 → 安全加固。下面我一个一个讲。

2.1 固件启动阶段:SMMU从复位到就绪

SMMU上电后,首先进入的是复位状态。这时候所有配置寄存器都是复位默认值,TLB里空空如也,Stream Table指针寄存器(SMMU_SMRx、SMMU_S2CRx、SMMU_CBARx等)全是0。

固件要做的第一件事,就是确认SMMU硬件版本和特性集。我记得有一次在项目里,芯片手册说支持SMMUv3.1,结果读ID寄存器发现Stage-2翻译根本没实现。嗯,从那以后我每次初始化前都会先读一遍ID寄存器,不依赖文档。

我的习惯:初始化第一步,读取SMMU_IDR0~IDR5,确认支持的翻译阶段(Stage-1/Stage-2)、地址位宽、队列深度等关键参数。这一步能避免后续配置踩坑。

确认硬件能力后,固件需要做以下几步:

  1. 关闭SMMU全局使能:写SMMU_CR0寄存器,清除SMMU_EN位。确保配置期间不会有任何翻译请求进来。
  2. 清空TLB和缓存:写SMMU_TLBIVMID或SMMU_TLBIALL,确保所有残留的翻译缓存被刷掉。我见过一个案例,固件没做TLB invalidation,结果旧配置的残留条目和新配置冲突,导致随机性fault。
  3. 配置基础地址寄存器:设置SMMU_CBAR(Context Bank基地址)、SMMU_SMR(Stream Match寄存器基地址)、SMMU_S2CR(Stream to Context寄存器基地址)。这些指针决定了后续Stream Table和Context Bank在内存中的位置。
  4. 设置队列基地址:如果使用Event Queue、PRI Queue或CMD Queue,需要配置对应的基地址和队列大小。

这一步完成后,SMMU处于“半初始化”状态——硬件知道去哪里找配置表了,但表里还是空的。

2.2 Stream Table配置:设备到SMMU的“门牌号”

Stream Table是SMMU的核心数据结构。每个连接到SMMU的设备(或者说每个Stream ID)都在Stream Table里有一条记录。这条记录告诉SMMU:这个设备用哪个Context Bank、用哪个翻译规则。

Stream Table的结构有两种:线性Stream Table和2级Stream Table。我个人在项目中更常用2级结构,因为线性表在Stream ID范围很大时太浪费内存。

特性 线性Stream Table 2级Stream Table
内存占用 连续大块内存,Stream ID范围大时浪费 按需分配,节省内存
访问速度 直接索引,一次内存访问 两级索引,两次内存访问
配置复杂度 简单,一个基地址搞定 需要配置L1和L2描述符
适用场景 Stream ID范围小(< 256) Stream ID范围大(> 1024)

配置Stream Table时,每个Stream Table Entry(STE)包含以下关键字段:

  • Config字段:决定该Stream是否启用翻译、使用哪个Stage、是否bypass。
  • Context Bank指针:指向该Stream使用的Context Bank描述符。
  • Stream ID掩码和匹配规则:用于多Stream ID共享一个STE的场景。
避坑指南:我曾经在配置STE时,把Config字段写成了0x0(表示Stream不使能),结果设备发起的DMA请求全部被SMMU忽略,系统直接hang住。排查了整整两天才发现是Config没配对。所以我的建议是:每个STE配置完后,读回验证一遍。

2.3 Context Bank初始化:翻译上下文的“工作台”

Context Bank是SMMU执行地址翻译时的工作上下文。每个Context Bank对应一个独立的地址空间,包含页表基地址、ASID(Address Space ID)、翻译控制位等信息。

Context Bank的初始化流程如下:

  1. 分配Context Bank内存:每个Context Bank描述符占用64字节(SMMUv3),需要对齐到64字节边界。
  2. 配置页表基地址:写入TTBR0和TTBR1(如果支持),指向Stage-1和Stage-2的页表。
  3. 设置翻译控制寄存器:包括页表格式(4K/16K/64K页)、地址位宽、缓存策略等。
  4. 配置ASID和VMID:ASID用于区分不同进程的TLB条目,VMID用于虚拟机间的隔离。
  5. 设置安全状态:标记该Context Bank是安全世界还是非安全世界使用。

这里有个细节:Context Bank的配置和Stream Table的配置是联动的。Stream Table里的STE指向Context Bank,而Context Bank里的页表基地址决定了翻译结果。如果STE指向了一个未初始化的Context Bank,SMMU访问时就会触发异常。

注意:Context Bank的数量是有限的(由SMMU硬件决定,通常8~64个)。不要为每个设备都分配独立的Context Bank,否则很快会用完。我的做法是:把共享相同地址空间的设备映射到同一个Context Bank,通过ASID区分进程。

2.4 默认安全配置:别让SMMU成为后门

SMMU的安全配置,说白了就是防止非安全世界的设备访问安全世界的内存。ARM架构里,安全世界和非安全世界是严格隔离的,SMMU必须强制执行这个隔离。

默认安全配置包括以下几个方面:

配置项 说明 我的建议
SMMU_CR0.NSAIDEN 非安全Stream ID使能 默认关闭,按需开启
SMMU_CR0.NSATOS 非安全访问安全内存控制 必须关闭,防止绕过安全隔离
Stream Table安全属性 每个STE的安全/非安全标记 安全设备配安全STE,非安全设备配非安全STE
Context Bank安全属性 每个Context Bank的安全/非安全标记 与STE保持一致,避免交叉访问

我遇到过最典型的安全问题是:固件在初始化时把所有Stream Table和Context Bank都配成了非安全属性,结果安全世界的设备(比如安全摄像头)的DMA请求被SMMU拒绝,因为SMMU认为它是非安全设备。反过来,如果非安全设备的STE指向了安全Context Bank,那就更危险了——非安全世界可能通过DMA读取安全内存。

我的经验:初始化时先配安全世界的Stream Table和Context Bank,再配非安全世界的。这样即使非安全配置出错,也不会影响安全世界的隔离。另外,配完后用安全监控软件扫描一遍所有STE和Context Bank的安全属性,确保没有交叉。

2.5 初始化流程图:一张图看懂全过程

下面这张图是我自己画的SMMU初始化流程图,涵盖了从复位到就绪的完整路径。你可以把它贴在工位上,每次配SMMU时对照着走一遍。

SMMU固件初始化流程图 SMMU固件初始化流程图 SMMU上电复位 读取IDR0~IDR5确认能力 关闭SMMU全局使能 清空TLB和缓存 配置CBAR/SMR/S2CR基地址 配置Stream Table条目 初始化Context Bank 配置安全/非安全属性 开启SMMU全局使能 提示:每一步配置后建议读回验证,避免寄存器写入失败导致初始化异常

这张图里我特别标注了“关闭全局使能”和“开启全局使能”两个关键节点。中间的所有配置,都必须在SMMU关闭的状态下完成。如果你在SMMU使能状态下修改Stream Table或Context Bank,轻则配置不生效,重则触发unexpected fault。

2.6 初始化后的验证检查

初始化完成后,别急着跑业务。我习惯做一轮快速验证:

  • 读SMMU_CR0:确认SMMU_EN位已置1,SMMU处于使能状态。
  • 读SMMU_CBAR:确认Context Bank基地址与配置值一致。
  • 读SMMU_SMR和SMMU_S2CR:确认Stream Table条目已正确加载。
  • 触发一次测试翻译:用软件模拟一个Stream ID的翻译请求,检查返回的物理地址是否正确。
血的教训:我曾经在一个项目里,初始化后没做验证就直接跑业务,结果发现所有DMA请求都返回translation fault。排查后发现是SMMU_CBAR寄存器写入时地址没对齐(低4位不是0),硬件直接忽略了写入值。从那以后,我每次初始化后都会读回所有关键寄存器,确保写入生效。

好了,SMMU固件初始化的核心流程就这些。说白了就是:先关使能,配表,配上下文,配安全,再开使能。每一步都有坑,但只要你按流程走,读回验证,基本不会出大问题。