4. SMMU编程模型:寄存器概览、Command Queue与Event Queue、配置流程、中断处理
好,咱们今天来聊聊SMMU的编程模型。说实话,这部分是真正上手操作SMMU的核心。你光知道SMMU能干嘛还不够,得知道怎么让它干活。我刚开始接触SMMU时,面对那一堆寄存器,说实话有点懵。但摸清楚套路后,发现其实就那么几块东西。
4.1 SMMU寄存器概览:你只需要记住这几类
SMMU的寄存器,说白了就是它的“控制面板”。ARM把寄存器分成了几个组,每个组管一摊事。我个人习惯把它们分成三大类:
- 全局控制寄存器:比如SMMU_CR0、SMMU_CR1这些,控制SMMU的全局行为,像使能、错误处理模式等。
- Stream Table相关寄存器:SMMU_SMRn、SMMU_S2CRn,用来配置每个Stream ID的映射规则和安全属性。
- 队列与中断寄存器:Command Queue、Event Queue、PRI Queue的控制和状态寄存器,以及中断状态寄存器。
嗯,这里要注意,不同版本的SMMU(比如SMMUv2和SMMUv3)寄存器布局差异很大。SMMUv3的寄存器模型更简洁,很多配置都移到了内存中。但不管怎么变,核心逻辑是一样的。
核心要点:SMMU的寄存器访问,必须遵循ARM的“先写后读”原则。你写一个配置寄存器后,最好读回来确认一下。我在项目中遇到过,因为总线缓冲导致写操作没生效,结果设备直接访问了错误的内存区域,查了半天。
4.2 Command Queue:给SMMU下指令的通道
Command Queue,简称CQ。它是软件和SMMU之间的“命令通道”。你想让SMMU做点事,比如刷新TLB、更新配置,就往这个队列里塞命令。
CQ本质上是一个环形缓冲区,在内存中。SMMU内部有一个生产者指针(PROD)和一个消费者指针(CONS)。软件写命令到队列,然后更新PROD指针。SMMU从队列里取命令执行,然后更新CONS指针。
我建议你画个图理解一下:
你看,PROD和CONS之间的区域就是待处理的命令。当PROD追上CONS时,队列就满了。这时候软件必须等待,或者丢弃命令。
避坑指南:我曾经在调试时,发现SMMU不响应命令。查了半天,原来是Command Queue的基地址没对齐。ARM要求队列基地址必须按队列大小对齐,比如64KB的队列,基地址必须是64KB对齐的。这个细节很容易忽略。
4.3 Event Queue:SMMU的“告状”通道
Event Queue,简称EQ。它是SMMU向软件报告事件的通道。比如发生了地址翻译错误、设备访问违规、或者TLB刷新完成等,SMMU都会往EQ里塞一个事件记录。
EQ的结构和CQ类似,也是环形缓冲区。但方向是反的:SMMU是生产者,软件是消费者。
事件记录的结构是固定的,每个记录包含:
- Event ID:标识事件类型,比如F_TRANSLATION_FAULT、F_PERMISSION_FAULT等。
- Stream ID:出问题的设备是谁。
- Substream ID:如果是PCIe设备,就是Function Number。
- 错误地址:访问的虚拟地址或物理地址。
- 其他信息:比如访问权限、大小等。
你想想看,如果没有Event Queue,SMMU出错了怎么办?难道轮询寄存器?那效率太低了。Event Queue的设计,让SMMU可以异步地通知软件,软件只需要在中断处理程序里读取队列即可。
4.4 配置流程:从复位到正常运行
配置SMMU,说白了就是三步走:初始化、建表、使能。我总结了一个标准流程:
- 复位状态检查:读取SMMU_IDR寄存器,确认SMMU的版本和特性。不同版本的SMMU,支持的配置项不一样。
- 配置Stream Table:设置SMR和S2CR,定义每个Stream ID的翻译规则。这一步是关键,决定了哪些设备走SMMU,哪些不走。
- 初始化队列:分配Command Queue和Event Queue的内存,设置基地址和大小寄存器。
- 配置中断:设置中断使能寄存器,注册中断处理函数。
- 使能SMMU:设置SMMU_CR0的使能位。注意,使能后SMMU就开始工作了。
嗯,这里有个细节。使能SMMU之前,最好先发一个“空命令”到Command Queue,确保队列机制正常。我习惯在使能前,先写一个SYNC命令,然后等待它完成。这样可以验证CQ通路是否畅通。
警告:千万不要在SMMU使能后,再去修改Stream Table的配置。除非你先发一个TLB invalidation命令,让SMMU刷新缓存。否则,SMMU可能还在使用旧的配置,导致访问错误。
4.5 中断处理:别让SMMU“喊破喉咙”
SMMU的中断,主要来自Event Queue和PRI Queue。当队列中有新事件时,SMMU会触发中断。软件在中断处理程序里,需要读取队列,处理事件,然后更新CONS指针。
中断处理的关键点:
- 中断类型:SMMU支持MSI和Line中断。MSI更适合高性能场景,Line中断更简单。
- 中断聚合:SMMU可以把多个事件合并到一个中断里,减少中断频率。但要注意,聚合太多会导致事件处理延迟。
- 错误处理:对于翻译错误,软件可以选择“终止”或“重试”。终止就是直接返回错误给设备,重试则需要软件修复问题后重新发起访问。
我曾经遇到一个案例:某个设备频繁触发翻译错误,导致SMMU中断风暴。查下来,原来是设备驱动里配置的Stream ID不对。嗯,这种问题,说白了就是软件配置和硬件不匹配。
4.6 实战:一个简单的配置示例
下面是一个简化的SMMU配置代码片段,基于SMMUv2。注意,这只是演示逻辑,实际代码要复杂得多。
// 1. 检查SMMU版本
uint32_t idr = readl(SMMU_BASE + SMMU_IDR0);
if ((idr & 0xFF) != 0x10) {
// 不是SMMUv2,报错
return -ENODEV;
}
// 2. 配置Stream Table
// 假设设备Stream ID为0,走bypass
writel(0x0, SMMU_BASE + SMMU_SMR0); // Stream ID匹配规则
writel(0x1, SMMU_BASE + SMMU_S2CR0); // Bypass模式
// 3. 初始化Command Queue
// 分配4KB内存,基地址对齐
void *cq_base = alloc_aligned(4096, 4096);
writel((uintptr_t)cq_base, SMMU_BASE + SMMU_CQBASE);
writel(0x100, SMMU_BASE + SMMU_CQPROD); // 初始生产者指针
// 4. 初始化Event Queue
void *eq_base = alloc_aligned(4096, 4096);
writel((uintptr_t)eq_base, SMMU_BASE + SMMU_EQBASE);
writel(0x0, SMMU_BASE + SMMU_EQPROD); // 初始消费者指针
// 5. 使能SMMU
writel(SMMU_CR0_ENABLE, SMMU_BASE + SMMU_CR0);
// 6. 发送一个SYNC命令,验证队列
struct smmu_cmd sync_cmd = {
.opcode = CMD_SYNC,
.sync = { .context = 0x1 }
};
smmu_send_cmd(&sync_cmd);
你看,代码其实不复杂。但每个步骤都有坑。比如分配队列内存时,必须保证物理地址连续,而且对齐。我建议你用DMA分配函数,而不是普通的kmalloc。
4.7 小结
SMMU的编程模型,核心就是三件事:配置寄存器、操作队列、处理中断。你只要把这三块搞明白,剩下的就是细节了。
我个人觉得,学习SMMU最好的方法,就是动手写一个简单的驱动。不用太复杂,能配置一个设备走bypass就行。然后逐步增加功能,比如配置翻译规则、处理错误事件。纸上得来终觉浅,绝知此事要躬行。
最后提醒一句:SMMU的寄存器访问,一定要用内存屏障。ARM架构下,外设寄存器的访问顺序可能被CPU重排。我习惯在每次读写寄存器前后,加上dsb()指令,确保顺序正确。