3、SMMU寄存器详解:SMMUv3关键寄存器配置实战
说实话,SMMUv3的寄存器数量不少,但真正在固件开发中天天打交道的,就那么几个。我刚开始接触SMMU时,面对几百页的Spec也是一头雾水。后来踩了几个坑才明白——抓住SMMU_CR0、SMMU_SMRn、SMMU_CBn这三类寄存器,你就抓住了SMMU配置的命脉。
今天咱们就一个一个掰开揉碎了讲。我会结合我实际项目中的配置经验,告诉你每个寄存器该怎么配、配错了会出什么妖蛾子。
3.1 SMMU_CR0:全局控制寄存器
SMMU_CR0是整个SMMU的“总开关”。你想想看,如果这个寄存器没配好,后面配再多都是白搭。我见过一个同事,调试了三天SMMU不工作,最后发现是SMMU_CR0的SMMU_EN位没置1——这种低级错误,说出来都是泪。
SMMU_CR0的关键位域:
| 位域 | 名称 | 说明 | 我的建议 |
|---|---|---|---|
| [0] | SMMU_EN | SMMU全局使能,0=禁用,1=启用 | 最后一步再置1,先配好其他寄存器 |
| [1] | PTM_EN | 页表内存访问使能 | 必须置1,否则页表走不了 |
| [13] | EVTGEN_EN | 事件生成使能 | 调试阶段建议开启 |
| [16] | ATS_EN | ATS(地址转换服务)使能 | PCIe设备需要时开启 |
| [19] | MSI_EN | MSI中断使能 | 建议开启,用于错误上报 |
实际代码中,我一般这样配:
// 伪代码:SMMU_CR0 配置示例
uint32_t cr0_val = 0;
// 先关掉SMMU,防止配置过程中有流量乱入
cr0_val &= ~(1 << 0); // SMMU_EN = 0
// 配置其他功能位
cr0_val |= (1 << 1); // PTM_EN = 1
cr0_val |= (1 << 13); // EVTGEN_EN = 1
cr0_val |= (1 << 19); // MSI_EN = 1
// 写入寄存器
mmio_write_32(SMMU_CR0_BASE + 0x00, cr0_val);
// 确认写入成功
uint32_t readback = mmio_read_32(SMMU_CR0_BASE + 0x00);
assert(readback == cr0_val);
// 最后再使能SMMU
cr0_val |= (1 << 0); // SMMU_EN = 1
mmio_write_32(SMMU_CR0_BASE + 0x00, cr0_val);
3.2 SMMU_SMRn:流匹配寄存器组
SMMU_SMRn的作用,说白了就是“谁的话我听,谁的话我不听”。每个SMRn寄存器对应一条流匹配规则,决定哪些设备发出的请求需要经过SMMU转换。
SMMU_SMRn的结构:
| 位域 | 名称 | 说明 |
|---|---|---|
| [31:16] | VMID | 虚拟机ID,用于虚拟化场景 |
| [15:0] | SID | Stream ID,设备标识 |
每个SMRn寄存器还配有一个SMMU_S2CRn(Stream to Context Register),用来指定匹配到的流该走哪个上下文(Context Bank)。
我个人的习惯是:先规划好SID分配表,再动手配寄存器。比如:
// SID分配规划示例
// SID 0x0000 - 0x00FF: 保留给内部设备
// SID 0x0100 - 0x01FF: PCIe RC (Root Complex)
// SID 0x0200 - 0x02FF: 显示控制器
// SID 0x0300 - 0x03FF: 多媒体引擎
// 配置SMR0:匹配PCIe设备,映射到Context Bank 0
mmio_write_32(SMMU_SMR_BASE + 0x00 * 8, 0x00000100); // SMR0: SID=0x0100, mask=0xFF00
mmio_write_32(SMMU_S2CR_BASE + 0x00 * 8, 0x00000001); // S2CR0: 指向CB0
// 配置SMR1:匹配显示控制器,映射到Context Bank 1
mmio_write_32(SMMU_SMR_BASE + 0x01 * 8, 0x00000200); // SMR1: SID=0x0200, mask=0xFF00
mmio_write_32(SMMU_S2CR_BASE + 0x01 * 8, 0x00000002); // S2CR1: 指向CB1
3.3 SMMU_CBn:上下文寄存器组
SMMU_CBn是真正干活的寄存器。每个Context Bank包含一组寄存器,用来描述一个地址空间的转换规则。包括页表基址、ASID(Address Space ID)、缓存策略、异常处理等。
一个Context Bank的关键寄存器:
| 寄存器 | 偏移 | 说明 |
|---|---|---|
| CBARn | 0x00 | Context Bank属性寄存器,配置VA大小、页表格式等 |
| TTBR0n | 0x08 | 转换表基址寄存器0(Stage 1页表) |
| TTBR1n | 0x10 | 转换表基址寄存器1(Stage 1页表,高地址空间) |
| ACTLRn | 0x18 | 辅助控制寄存器,TLB预取、缓存策略等 |
| MAIRn | 0x20 | 内存属性间接寄存器,定义页表中的属性编码 |
配Context Bank时,我最关注的是TTBR0n和ACTLRn。TTBR0n决定了页表在哪,ACTLRn决定了TLB怎么玩。
// 配置Context Bank 0
// 假设页表已经准备好,物理地址为0x8000_0000
// 1. 配置CBAR:VA_SIZE=48bit,页表格式为4KB粒度
mmio_write_32(SMMU_CB0_BASE + 0x00, 0x0000002F); // CBAR: VA=48bit, TG=4KB
// 2. 配置TTBR0:页表基址
mmio_write_64(SMMU_CB0_BASE + 0x08, 0x0000000080000000ULL); // TTBR0
// 3. 配置MAIR:定义内存属性
// Attr0: Normal Memory, Outer/Inner Write-Back
// Attr1: Device-nGnRE
mmio_write_32(SMMU_CB0_BASE + 0x20, 0x000004FF); // MAIR
// 4. 配置ACTLR:开启TLB预取,使能合并写
mmio_write_32(SMMU_CB0_BASE + 0x18, 0x00000003); // ACTLR: TLB_prefetch=1, merge_write=1
3.4 实战:完整配置一个设备流
好了,理论说完了,咱们来一个完整的配置流程。假设我们要为一个PCIe网卡配置SMMU:
- Step 1:读取设备SID(从PCIe配置空间或设备树中获取)
- Step 2:分配一个空闲的SMRn和Context Bank
- Step 3:配置SMMU_CR0(先关SMMU_EN)
- Step 4:配置SMRn和S2CRn(绑定SID到Context Bank)
- Step 5:配置Context Bank(页表、ASID、缓存策略)
- Step 6:使能SMMU_CR0(置SMMU_EN=1)
- Step 7:验证配置(发送测试请求,检查是否正常转换)