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全局使能:写SMMU_CR0寄存器,清除SMMU_EN位。确保配置期间不会有任何翻译请求进来。
- 清空TLB和缓存:写SMMU_TLBIVMID或SMMU_TLBIALL,确保所有残留的翻译缓存被刷掉。我见过一个案例,固件没做TLB invalidation,结果旧配置的残留条目和新配置冲突,导致随机性fault。
- 配置基础地址寄存器:设置SMMU_CBAR(Context Bank基地址)、SMMU_SMR(Stream Match寄存器基地址)、SMMU_S2CR(Stream to Context寄存器基地址)。这些指针决定了后续Stream Table和Context Bank在内存中的位置。
- 设置队列基地址:如果使用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的场景。
2.3 Context Bank初始化:翻译上下文的“工作台”
Context Bank是SMMU执行地址翻译时的工作上下文。每个Context Bank对应一个独立的地址空间,包含页表基地址、ASID(Address Space ID)、翻译控制位等信息。
Context Bank的初始化流程如下:
- 分配Context Bank内存:每个Context Bank描述符占用64字节(SMMUv3),需要对齐到64字节边界。
- 配置页表基地址:写入TTBR0和TTBR1(如果支持),指向Stage-1和Stage-2的页表。
- 设置翻译控制寄存器:包括页表格式(4K/16K/64K页)、地址位宽、缓存策略等。
- 配置ASID和VMID:ASID用于区分不同进程的TLB条目,VMID用于虚拟机间的隔离。
- 设置安全状态:标记该Context Bank是安全世界还是非安全世界使用。
这里有个细节:Context Bank的配置和Stream Table的配置是联动的。Stream Table里的STE指向Context Bank,而Context Bank里的页表基地址决定了翻译结果。如果STE指向了一个未初始化的Context Bank,SMMU访问时就会触发异常。
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读取安全内存。
2.5 初始化流程图:一张图看懂全过程
下面这张图是我自己画的SMMU初始化流程图,涵盖了从复位到就绪的完整路径。你可以把它贴在工位上,每次配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的翻译请求,检查返回的物理地址是否正确。
好了,SMMU固件初始化的核心流程就这些。说白了就是:先关使能,配表,配上下文,配安全,再开使能。每一步都有坑,但只要你按流程走,读回验证,基本不会出大问题。