Linux内核SMMUv3驱动深度解析:从设备树到硬件探测的全流程指南
在嵌入式系统和服务器领域,内存管理单元(MMU)对于CPU的内存访问至关重要,而系统内存管理单元(SMMU)则为设备提供了类似的功能。本文将带您深入探索Linux内核中SMMUv3驱动的初始化过程,揭示从设备树解析到硬件探测的完整技术细节。
1. SMMUv3架构概览与核心概念
SMMUv3(System Memory Management Unit version 3)是ARM架构中用于管理设备直接内存访问(DMA)的关键组件。它实现了IOMMU(输入输出内存管理单元)的功能,为系统中的外设提供了地址转换和访问控制能力。
SMMUv3的核心功能模块:
- 地址转换:将设备发出的虚拟地址(VA)转换为物理地址(PA)
- 访问控制:检查设备是否有权限访问特定内存区域
- 中断处理:管理来自SMMU硬件的事件和错误中断
- 队列管理:处理命令队列(CMD Queue)、事件队列(Event Queue)等
与传统的MMU不同,SMMU需要支持多个设备同时访问,因此引入了Stream ID(SID)和Substream ID(SSID)的概念:
| 概念 | 描述 | 作用 |
|---|---|---|
| Stream ID | 设备标识符 | 区分不同设备的地址空间 |
| Substream ID | 进程标识符 | 支持设备内多进程的地址隔离 |
SMMUv3的典型工作流程如下:
- 设备发起DMA请求,携带Stream ID和Substream ID
- SMMU根据Stream ID查找Stream Table Entry(STE)
- 从STE中获取Context Descriptor(CD)表的信息
- 使用Substream ID索引CD表,获取页表基地址
- 完成地址转换并检查访问权限
2. 设备树解析:SMMU的硬件描述
Linux内核通过设备树(Device Tree)获取SMMU硬件的配置信息。典型的SMMUv3设备树节点如下:
smmu@2b400000 { compatible = "arm,smmu-v3"; reg = <0x0 0x2b400000 0x0 0x20000>; interrupts = <GIC_SPI 74 IRQ_TYPE_EDGE_RISING>, <GIC_SPI 75 IRQ_TYPE_EDGE_RISING>, <GIC_SPI 77 IRQ_TYPE_EDGE_RISING>, <GIC_SPI 79 IRQ_TYPE_EDGE_RISING>; interrupt-names = "eventq", "priq", "cmdq-sync", "gerror"; dma-coherent; #iommu-cells = <1>; msi-parent = <&its 0xff0000>; };关键属性解析:
compatible:用于匹配SMMU驱动,必须包含"arm,smmu-v3"reg:SMMU寄存器的物理地址和大小interrupts:定义四个关键中断源#iommu-cells:必须设置为1,表示每个设备需要一个Stream IDdma-coherent:指示SMMU是否支持缓存一致性
驱动通过arm_smmu_device_dt_probe()函数解析这些属性:
static int arm_smmu_device_dt_probe(struct platform_device *pdev, struct arm_smmu_device *smmu) { if (of_property_read_u32(dev->of_node, "#iommu-cells", &cells)) dev_err(dev, "missing #iommu-cells property\n"); else if (cells != 1) dev_err(dev, "invalid #iommu-cells value (%d)\n", cells); parse_driver_options(smmu); if (of_dma_is_coherent(dev->of_node)) smmu->features |= ARM_SMMU_FEAT_COHERENCY; return 0; }3. 硬件探测与寄存器分析
SMMUv3驱动通过读取三个关键识别寄存器(IDR0、IDR1和IDR5)来探测硬件能力。这些寄存器提供了SMMU实现的具体特性信息。
IDR0寄存器关键字段分析:
| 位域 | 名称 | 描述 |
|---|---|---|
| 27 | ST_LEVEL | Stream表格式(0=线性,1=两级) |
| 25:24 | TERM_MODEL | 错误处理模式 |
| 22:21 | TTENDIAN | 转换表字节序支持 |
| 19 | CD2L | 是否支持两级CD表 |
| 16 | PRI | 是否支持Page Request Interface |
| 10 | ATS | 是否支持Address Translation Service |
| 1 | S1P | 是否支持Stage1转换 |
| 0 | S2P | 是否支持Stage2转换 |
驱动通过arm_smmu_device_hw_probe()函数读取并解析这些寄存器:
static int arm_smmu_device_hw_probe(struct arm_smmu_device *smmu) { u32 reg; reg = readl_relaxed(smmu->base + ARM_SMMU_IDR0); if (FIELD_GET(IDR0_ST_LVL_MASK, reg) == IDR0_ST_LVL_2LVL) smmu->features |= ARM_SMMU_FEAT_2_LVL_STRTAB; if (reg & IDR0_CD2L) smmu->features |= ARM_SMMU_FEAT_2_LVL_CDTAB; // 其他特性探测... }IDR1寄存器主要提供队列和标识符大小的信息:
- CMDQ、EVENTQ和PRIQ的最大条目数(log2值)
- Stream ID和Substream ID支持的位数
- 表格基地址是否固定
IDR5寄存器则包含:
- 支持的地址大小(VAX字段)
- 支持的转换粒度(4K/16K/64K)
- 输出地址大小(OAS)
4. 数据结构初始化:STE、CD与队列
SMMUv3驱动需要初始化多个关键数据结构,包括Stream表、Context描述符表和各种队列。
4.1 Stream表初始化
Stream表是SMMUv3的核心数据结构,每个条目(STE)大小为64字节,包含一个设备的所有转换配置。驱动根据IDR0.ST_LEVEL决定使用线性表还是两级表结构。
线性Stream表初始化流程:
- 计算表大小:
(1 << sid_bits) * 64 - 分配连续DMA内存
- 初始化所有STE为bypass模式
- 配置STRTAB_BASE和STRTAB_BASE_CFG寄存器
static int arm_smmu_init_strtab_linear(struct arm_smmu_device *smmu) { size_t size = (1 << smmu->sid_bits) * (STRTAB_STE_DWORDS << 3); void *strtab = dmam_alloc_coherent(smmu->dev, size, &cfg->strtab_dma, GFP_KERNEL); reg = FIELD_PREP(STRTAB_BASE_CFG_FMT, STRTAB_BASE_CFG_FMT_LINEAR); reg |= FIELD_PREP(STRTAB_BASE_CFG_LOG2SIZE, smmu->sid_bits); arm_smmu_init_bypass_stes(strtab, cfg->num_l1_ents); }两级Stream表则更为复杂,需要额外初始化L1描述符表:
- 计算L1表大小:
(1 << (log2size - split)) * 8 - 分配L1表内存
- 为每个L1描述符分配L2表
- 初始化所有STE
4.2 队列初始化
SMMUv3使用三种主要队列与软件交互:
- 命令队列(CMD Queue):软件发送命令给SMMU
- 事件队列(Event Queue):SMMU报告错误和事件
- PRI队列:处理页请求接口(可选)
队列初始化在arm_smmu_init_queues()中完成:
static int arm_smmu_init_queues(struct arm_smmu_device *smmu) { ret = arm_smmu_init_one_queue(smmu, &smmu->cmdq.q, ARM_SMMU_CMDQ_PROD, ARM_SMMU_CMDQ_CONS, CMDQ_ENT_DWORDS, "cmdq"); ret = arm_smmu_init_one_queue(smmu, &smmu->evtq.q, ARM_SMMU_EVTQ_PROD, ARM_SMMU_EVTQ_CONS, EVTQ_ENT_DWORDS, "evtq"); if (smmu->features & ARM_SMMU_FEAT_PRI) ret = arm_smmu_init_one_queue(smmu, &smmu->priq.q, ARM_SMMU_PRIQ_PROD, ARM_SMMU_PRIQ_CONS, PRIQ_ENT_DWORDS, "priq"); return ret; }每个队列都有生产者和消费者指针,SMMU硬件从队列中取命令或向队列写入事件。
5. 硬件配置与中断设置
完成数据结构初始化后,驱动需要配置SMMU硬件寄存器并设置中断处理。
关键硬件配置步骤:
- 禁用SMMU(清除CR0寄存器)
- 配置队列基地址寄存器(CMDQ_BASE、EVTQ_BASE等)
- 配置Stream表基地址寄存器(STRTAB_BASE)
- 启用SMMU(设置CR0寄存器)
static int arm_smmu_device_reset(struct arm_smmu_device *smmu) { /* 禁用SMMU */ arm_smmu_device_disable(smmu); /* 配置队列寄存器 */ writeq_relaxed(smmu->cmdq.q.q_base, smmu->base + ARM_SMMU_CMDQ_BASE); writel_relaxed(smmu->cmdq.q.llq.prod, smmu->base + ARM_SMMU_CMDQ_PROD); writel_relaxed(smmu->cmdq.q.llq.cons, smmu->base + ARM_SMMU_CMDQ_CONS); /* 配置Stream表 */ writeq_relaxed(smmu->strtab_cfg.strtab_base, smmu->base + ARM_SMMU_STRTAB_BASE); writel_relaxed(smmu->strtab_cfg.strtab_base_cfg, smmu->base + ARM_SMMU_STRTAB_BASE_CFG); /* 启用SMMU */ enables = CR0_CMDQEN; arm_smmu_write_reg_sync(smmu, enables, ARM_SMMU_CR0, ARM_SMMU_CR0ACK); }中断处理设置:
SMMUv3支持多种中断类型,驱动需要为每种中断注册处理函数:
- 事件队列中断:处理SMMU事件
- 命令队列同步中断:处理命令完成通知
- 全局错误中断:处理全局错误条件
static int arm_smmu_setup_irqs(struct arm_smmu_device *smmu) { /* 全局错误中断 */ ret = devm_request_irq(smmu->dev, smmu->gerr_irq, arm_smmu_gerror_handler, 0, "arm-smmu-v3-gerror", smmu); /* 事件队列中断 */ ret = devm_request_irq(smmu->dev, smmu->evtq.q.irq, arm_smmu_evtq_handler, 0, "arm-smmu-v3-evtq", smmu); /* 其他中断... */ }典型的中断处理流程包括:
- 读取中断状态寄存器
- 根据中断类型处理相应事件
- 清除中断状态位
6. 设备连接与地址空间管理
SMMUv3驱动最后需要注册到Linux IOMMU框架,使设备能够连接到SMMU实例。
关键操作结构体:
static struct iommu_ops arm_smmu_ops = { .capable = arm_smmu_capable, .domain_alloc = arm_smmu_domain_alloc, .domain_free = arm_smmu_domain_free, .attach_dev = arm_smmu_attach_dev, .map = arm_smmu_map, .unmap = arm_smmu_unmap, .iova_to_phys = arm_smmu_iova_to_phys, .add_device = arm_smmu_add_device, .remove_device = arm_smmu_remove_device, };设备连接流程:
- 总线发现设备并调用
add_device回调 - 为设备创建或查找IOMMU group
- 分配IOMMU domain
- 调用
attach_dev将设备连接到domain
在arm_smmu_attach_dev中,驱动会:
- 为设备分配ASID(地址空间ID)
- 初始化CD表(Context Descriptor)
- 配置STE指向CD表
- 建立页表转换结构
static int arm_smmu_attach_dev(struct iommu_domain *domain, struct device *dev) { /* 获取SMMU domain */ smmu_domain = to_smmu_domain(domain); /* 分配ASID */ asids = arm_smmu_bitmap_alloc(); /* 根据转换阶段初始化 */ if (smmu_domain->stage == ARM_SMMU_DOMAIN_S1) { ret = arm_smmu_domain_finalise_s1(domain, master); } else { ret = arm_smmu_domain_finalise_s2(domain, master); } /* 配置STE */ arm_smmu_write_ctx_desc(master, 0, &smmu_domain->s1_cfg.cdcfg); }地址空间隔离:
SMMUv3通过Stream ID和Substream ID实现设备间的地址空间隔离:
- 每个设备有唯一的Stream ID
- 设备内不同进程使用不同Substream ID
- 每个Substream ID对应独立的CD和页表
这种设计使得SMMU能够同时支持多个设备的DMA操作,同时保持地址空间的隔离性和安全性。