3. SMMU编程模型:寄存器空间、命令队列、事件队列、PRI队列

SMMU的编程模型是系统软件与硬件交互的核心接口。理解其寄存器空间布局、队列机制以及中断处理流程,是进行SMMU初始化和动态管理的基础。本章将深入剖析SMMU的四大编程要素:寄存器空间、命令队列(Command Queue)、事件队列(Event Queue)和PRI队列(Page Request Interface Queue)。

3.1 SMMU寄存器空间

SMMU的寄存器空间分为全局寄存器(Global Registers)和上下文寄存器(Context Registers)两大部分。全局寄存器控制SMMU的整体行为,而上下文寄存器则管理具体的Stream Table Entry(STE)和配置。

寄存器类别 偏移范围 主要功能 关键寄存器示例
全局寄存器 0x0000 - 0x0FFF SMMU全局控制、状态、ID、队列基址 SMMU_CR0, SMMU_CR1, SMMU_IDR0, SMMU_GBPA
上下文寄存器 0x1000 - 0xFFFF 每个Stream的STE配置、CD表基址、TTB STE0_Config, STE0_S1ContextPtr
命令队列寄存器 0x2000 - 0x2FFF 命令队列基址、生产者/消费者指针 SMMU_CMDQ_BASE, SMMU_CMDQ_PROD, SMMU_CMDQ_CONS
事件队列寄存器 0x3000 - 0x3FFF 事件队列基址、生产者/消费者指针 SMMU_EVTQ_BASE, SMMU_EVTQ_PROD, SMMU_EVTQ_CONS
PRI队列寄存器 0x4000 - 0x4FFF PRI队列基址、生产者/消费者指针 SMMU_PRIQ_BASE, SMMU_PRIQ_PROD, SMMU_PRIQ_CONS
编程要点: 全局寄存器通常通过MMIO(Memory-Mapped I/O)访问,且必须遵循ARM架构规定的对齐和访问顺序。在修改控制寄存器(如SMMU_CR0)时,建议先读取、修改、再写回,并轮询状态位确认生效。

3.2 命令队列(Command Queue)

命令队列是软件向SMMU下发控制指令的通道。它是一个环形缓冲区(Ring Buffer),由软件(生产者)写入命令,SMMU硬件(消费者)读取并执行。常见的命令包括:TLBI(TLB无效化)、SYNC(同步屏障)、PREFETCH(预取)等。

命令队列操作流程

  1. 初始化: 设置SMMU_CMDQ_BASE寄存器,指定队列基址和大小。
  2. 写入命令: 软件将命令写入队列的当前生产者指针位置。
  3. 更新生产者指针: 写入SMMU_CMDQ_PROD寄存器,通知硬件有新命令。
  4. 硬件消费: SMMU读取命令并执行,更新消费者指针SMMU_CMDQ_CONS。
  5. 同步等待: 软件通过发送SYNC命令并轮询消费者指针,确保之前命令执行完毕。
关键命令格式(CMD_PREFETCH_CONFIG):
// 命令字格式(128位)
// Word 0: [31:0] 命令类型 & 标志位
// Word 1: [31:0] StreamID
// Word 2: [31:0] SubstreamID
// Word 3: [31:0] 保留

// 示例:发送TLBI_ASID命令
#define CMD_TLBI_ASID      0x3
#define CMD_TLBI_ASID_ASID 0x1

void smmu_send_tlbi_asid(uint32_t asid) {
    uint64_t cmd[2] = {0};
    cmd[0] = CMD_TLBI_ASID | (asid << 12);
    // 写入命令队列并更新PROD指针
    writeq(cmd[0], cmq_base + prod_offset);
    writeq(cmd[1], cmq_base + prod_offset + 8);
    writel(prod + 2, smmu_base + SMMU_CMDQ_PROD);
}
命令队列环形缓冲区操作流程 命令队列环形缓冲区(Ring Buffer) 生产者 (PROD) 消费者 (CONS) 软件写入命令 硬件消费执行 软件通过更新PROD指针通知硬件,硬件执行后更新CONS指针,软件通过SYNC命令确保同步

3.3 事件队列(Event Queue)

事件队列是SMMU向软件报告错误、异常或状态信息的通道。当SMMU检测到配置错误、地址翻译错误(如F_TRANSLATION)、权限错误(如F_PERMISSION)时,会将事件记录写入事件队列,并可选地触发中断。

常见事件类型

  • F_TRANSLATION: 地址翻译失败,通常由无效的STE或CD表引起。
  • F_PERMISSION: 访问权限冲突,如写只读页面。
  • F_ACCESS: 访问了标记为不可访问的页面。
  • F_ADDR_SIZE: 输入地址超出了配置的地址空间范围。
  • C_BAD_STREAMID: 无效的StreamID。
调试建议: 在开发阶段,建议将事件队列中断使能,并在中断处理函数中打印事件记录。通过分析事件队列中的错误码和StreamID,可以快速定位配置错误或设备驱动问题。
事件队列错误处理流程 设备发起DMA SMMU检测错误 写入事件队列 触发中断 中断处理函数读取事件队列 解析错误码、StreamID、地址

3.4 PRI队列(Page Request Interface Queue)

PRI队列是SMMUv3引入的用于支持PCIe PRI(Page Request Interface)协议的通道。当设备访问的页面不在内存中(或未被映射)时,SMMU可以通过PRI队列向软件发送页面请求,软件响应后分配物理页面并更新页表,然后通过命令队列发送响应。

PRI队列工作流程

步骤 发起方 操作描述 关键寄存器/命令
1 设备 发起DMA访问,触发缺页 PCIe PRI Request
2 SMMU 将PRI请求写入PRI队列 SMMU_PRIQ_PROD
3 软件 从PRI队列读取请求,分配内存 SMMU_PRIQ_CONS
4 软件 更新页表,映射物理页面 页表操作
5 软件 发送PRI响应命令 CMD_PRI_RESP
6 SMMU 通知设备页面已就绪 PCIe PRI Response
PRI响应命令格式:
// CMD_PRI_RESP 命令结构
// Word 0: [31:0] 命令类型 (0x4)
// Word 1: [31:0] StreamID
// Word 2: [31:0] SubstreamID
// Word 3: [31:0] 页面请求组ID & 响应状态

// 示例:响应PRI请求,允许访问
void smmu_pri_response(uint32_t sid, uint32_t ssid, uint32_t prg_id) {
    uint64_t cmd[2] = {0};
    cmd[0] = (0x4) | (sid << 32);
    cmd[1] = (ssid << 32) | (prg_id & 0xFFFF) | (0x1 << 16); // 状态=成功
    // 写入命令队列
    writeq(cmd[0], cmq_base + prod_offset);
    writeq(cmd[1], cmq_base + prod_offset + 8);
    writel(prod + 2, smmu_base + SMMU_CMDQ_PROD);
}
PRI队列完整交互流程 PCIe设备 SMMU 软件驱动 物理内存 ① PRI请求 ② 写入PRI队列 ③ 分配页面 ④ 更新页表 ⑤ 发送PRI响应命令 ⑥ PRI响应 PRI队列关键点 • PRI队列与事件队列类似,也是环形缓冲区,但专门用于处理设备缺页请求。 • 软件必须及时处理PRI请求,避免设备超时(PCIe PRI超时通常为10ms-100ms)。 • 响应命令通过命令队列下发,使用CMD_PRI_RESP命令携带StreamID和页面组ID。 • 支持PRI的设备通常用于GPU、NVMe等需要按需分页的高性能场景。

3.5 队列管理与中断机制

SMMU的每个队列(命令、事件、PRI)都支持独立的中断配置。通过SMMU_IRQ_CTRL寄存器可以控制每个队列的中断使能,而SMMU_IRQ_STATUS寄存器则用于查询中断状态。

中断处理最佳实践

  • 命令队列: 通常使用轮询模式(SYNC命令后轮询CONS指针),避免中断开销。
  • 事件队列: 必须使能中断,以便及时捕获错误。
  • PRI队列: 建议使能中断,确保低延迟响应设备缺页请求。
  • 中断亲和性: 将SMMU中断绑定到特定CPU核心,避免中断风暴。
性能优化: 对于命令队列,可以采用批处理模式:一次性写入多个命令,然后统一更新PROD指针,减少MMIO访问次数。对于事件队列,建议在中断处理函数中一次性读取多个事件记录,提高处理效率。