多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

RK3588上ARM SMMUv3 IOMMU环境搭建与调试实战

RK3588上ARM SMMUv3 IOMMU环境搭建与调试实战 RK3588这颗芯片今年在嵌入式社区的存在感确实强八核A76A55、6T算力的NPU、8K视频编解码几乎是一块自带流量的开发板核心。但很多人拿到手玩了一阵子就会发现裸奔的外设访问内存其实挺危险的某个驱动写越界、某个DMA踩了别的模块的地址轻则花屏死机重则直接整板挂掉。这也是我写这篇东西的初衷就是带着你在RK3588上把ARM SMMUv3这套IOMMU环境从设备树开始一步步搭起来让外设的内存访问变得可控、可隔离、可追踪。这篇内容不是教科书式地罗列SMMU规范而是我实际在RK3588上调试、配置、踩坑的记录适合正在搞RK3588底层驱动、也在为内存隔离和DMA安全性头疼的人。无论你是刚接触设备树的新手还是已经会改dts但没碰过IOMMU的老手照着这套思路走一遍基本能把SMMUv3在RK3588上的配置路径摸清楚。1. 内容整体设计与思路拆解1.1 为什么RK3588需要SMMU它到底解决什么问题先打个比方。没有SMMU的时候系统中的每个外设GPU、VPU、ISP、PCIe控制器就像没有门禁的访客只要拿到了物理地址就能直接读写整片DDR。这种设计在单任务裸机时代没问题但在跑Linux、跑多进程、跑虚拟化的今天就是灾难。一个驱动程序里的野指针、一次DMA长度算错就可能把内核关键数据结构甚至别的进程内存冲掉。SMMUSystem Memory Management Unit就是给这些“访客”装了一道门禁核心作用有两个地址翻译和权限控制。外设发起的DMA访问先经过SMMU由SMMU把设备看到的“设备地址”IOVA翻译成真实的物理地址PA同时检查这次访问是否有权限、访问长度是否越界。这样每个外设都有一张独立的“地址映射表”谁也看不见谁谁也踩不到谁。SMMUv3相比v2的最大区别是把很多原本软件要干的事情下沉到了硬件。比如页表遍历、TLB缓存管理、命令队列、事件队列这些v3架构都通过硬件队列机制实现了更高的吞吐和更低的延迟。对RK3588这种多外设、高带宽的SoC来说SMMUv3几乎是标配级别的存在GPU、VPU、ISP这些大流量设备都挂在SMMU后面。从我实际调试的体会来说SMMU带来的最直接好处是当某个驱动的DMA出错时系统能够把错误定位到具体的设备和访问地址而不是让整块板子突然挂掉。这种“可控的失败”在嵌入式产品开发里太重要了。我见过太多因为没有SMMU保护、驱动DMA越界导致整机开机后跑几分钟就随机关机的案例排查起来非常痛苦。1.2 配置方案选型设备树、内核配置与驱动配合的关系要在RK3588上把SMMU用起来涉及三个层面的工作缺一不可。第一层是设备树配置。设备树负责描述“硬件拓扑”哪个SMMU实例管哪几个外设外设通过什么StreamID连接SMMUSMMU自身的中断、寄存器基地址等信息都写在dts/dtsi里。Linux内核启动时SMMU驱动会解析设备树节点初始化硬件并把外设和对应的SMMU实例绑定起来。第二层是内核配置。SMMUv3驱动、IOMMU子系统、DMA-IOMMU粘合层这些模块需要在内核编译时选上。设备树写得再对内核没编对应的选项也是白搭。第三层是驱动配合。设备驱动在申请DMA内存、映射DMA地址时要经由DMA API走IOMMU的映射流程。幸运的是Linux内核的dma-mapping框架已经把这些封装好了大多数驱动只需要调用标准DMA API如dma_alloc_coherent、dma_map_single就能自动完成IOVA分配和映射不需要针对SMMU写额外代码。我在方案设计上的建议是先以最小系统验证SMMU通路再逐步扩大保护范围。也就是先给一个外设比如VPU挂上SMMU确认设备能正常分配IOVA、正常做DMA然后再把GPU、ISP这些大设备加进来。上来就一股脑把所有SMMU都使能一旦出问题根本分不清是哪个设备配置错了。2. 核心细节解析与实操要点2.1 设备树里SMMU节点的基本结构在RK3588的设备树源码里SMMU节点的定义一般在SoC的dtsi文件中。不同Linux SDK版本可能略有差异但节点结构基本遵循ARM SMMUv3的binding规范大致长这样smmu0: iommufc900000 { compatible arm,smmu-v3; reg 0x0 0xfc900000 0x0 0x20000; interrupts GIC_SPI 127 IRQ_TYPE_LEVEL_HIGH, GIC_SPI 128 IRQ_TYPE_LEVEL_HIGH, GIC_SPI 129 IRQ_TYPE_LEVEL_HIGH, GIC_SPI 130 IRQ_TYPE_LEVEL_HIGH; interrupt-names eventq, gerror, priq, cmdq-sync; #iommu-cells 1; dma-coherent; status okay; };我来逐个属性拆一下这些经常被误解compatible arm,smmu-v3告诉内核这个节点由ARM SMMUv3驱动接管这个字符串不能写错否则驱动匹配不上。regSMMU控制寄存器的物理地址和大小。RK3588的SMMU寄存器空间通常映射为一段连续的MMIO区域大小一般是0x20000128KB。interruptsSMMUv3有多个中断源eventq事件队列、gerror全局错误、priq页请求队列、cmdq-sync命令队列同步都会产生中断。这些中断在设备树里用interrupt-names区分配置时必须确保数量和顺序一一对应。#iommu-cells 1这个属性决定了设备节点在引用SMMU时需要传几个参数通常是StreamID。RK3588一般取1也就是设备侧写法是iommus smmu0 0x0。dma-coherent表示SMMU和DDR之间是缓存一致性的。RK3588 CPU和DMA外设访问DDR时保持一致性加上这个属性后DMA API就不再做额外的cache维护操作性能会好很多。2.2 外设节点如何挂到SMMU后面设备要接受SMMU管控需要在设备节点中增加iommus属性。以把VPU视频编解码单元挂到SMMU为例vpu_combo: vpu-combofdb50000 { compatible rockchip,vpu-combo; reg 0x0 0xfdb50000 0x0 0x500; interrupts GIC_SPI 119 IRQ_TYPE_LEVEL_HIGH; iommus smmu0 0x0; status okay; };这里iommus属性的第一个参数是SMMU节点的phandle即smmu0第二个参数是设备使用的StreamID。StreamID是设备侧唯一标识符SMMU通过它找到设备对应的页表。如果不填或者填错StreamIDSMMU会认为这个设备不认识直接拒绝访问或者转发到错误的配置。比较特殊的一类是PCIe设备。PCIe设备挂在SMMU后面通常不是用iommus而是用iommu-map把PCIe的Request ID映射到StreamID。RK3588如果要用PCIe SATA卡、NVMe盘这一块配置也很关键。格式大致是pcie2x1l0: pciefd8c0000 { compatible rockchip,rk3588-pcie; ... iommu-map 0x0 smmu_pcie 0x0 0x1000; };iommu-map的含义是PCIe Request ID 0x0~0xFFF范围内的设备映射到smmu_pcie对应的StreamID 0x0~0xFFF。这种写法允许一个PCIe控制器下的多个设备共享同一个SMMU域每个PCIe设备仍然有自己的StreamID。2.3 StreamID与SMMU实例的对应关系在RK3588上SMMU不是一个单一大模块而是分散在SoC各处的多个实例。我最初调试时踩过的最大一个坑就是没有搞清楚VPU到底挂在哪个SMMU实例上结果把StreamID填给了一个完全不相干的SMMU节点。一般经验是RK3588的SMMU实例大致分为几组一组服务视频相关模块VPU、VEPU、VDPU一组服务显示相关模块DP、HDMI一组服务GPU和NPU还有PCIe控制器自带SMMU。具体的实例编号和StreamID分配要到Rockchip发布的TRM技术参考手册里查。不同SDK版本StreamID的定义也可能微调千万别照着旧版本的资料照抄。我的建议是在确定某个设备的StreamID之前先去内核源码里搜索该设备名相关的dts配置同时对照TRM的IOMMU章节。实际项目中见过好几次有人把GPU的StreamID配给了VPU结果VPU一启动GPU那边开始报页错误两个模块都崩。这个对应关系直接决定了设备树配得对不对也是排查问题的第一步。你可以在后面的调试环节用/sys/kernel/debug/iommu/下的信息来反推设备实际使用的StreamID这个我们后面详细说。3. 实操过程与核心环节实现3.1 从零搭建环境准备、内核配置与设备树修改先说环境。我用来做这套验证的环境是RK3588开发板、Linux 5.10内核SDK默认版本、aarch64交叉编译工具链。如果你用的是Ubuntu主机装好交叉工具链后确认aarch64-linux-gnu-gcc能正常执行即可。第一步是准备内核源码和编译环境。Rockchip的SDK通常会带一份完整的内核源码目录结构里kernel/arch/arm64/boot/dts/rockchip/下存放着RK3588各板型的设备树源文件比如rk3588-evb.dts、rk3588s-evb.dts等。拿到源码后先找到SoC级dtsi文件比如rk3588s.dtsi这个文件里定义了SMMU节点和外设节点注意多数板级dts都会通过#include包含它。第二步是确认内核配置。配置项是否打开直接决定SMMU驱动编不编得进去。用menuconfig检查下面几个关键的配置项make ARCHarm64 menuconfig需要确认如下选项为y或mCONFIG_IOMMU_SUPPORTIOMMU子系统总开关。CONFIG_IOMMU_IO_PGTABLEIOMMU页表分配和管理ARM SMMUv3依赖它。CONFIG_ARM_SMMU_V3ARM SMMUv3驱动。CONFIG_IOMMU_DMADMA-IOMMU粘合层让DMA API能自动走IOMMU。CONFIG_ARM_SMMU_V3_SVA如果后续想做Sub-Virtual Address共享虚拟地址可以选上但基础环境可以先不开。我个人实际编译内核时还建议把CONFIG_IOMMU_DEBUGFS打开后面调试能看到设备与StreamID的绑定关系排查问题会方便很多。第三步是修改设备树。假设我要给VPU模块接上SMMU保护那就找到vpu_combo节点确认它里面有iommus属性。如果SDK默认已经配好了那你可以先不改直接编译跑起来观察SMMU是否生效。如果SDK里这个设备没配SMMU那就要手动加上并确保StreamID和SMMU实例匹配。设备树文件里还有一种常见写法是status disabled有些外设默认是关闭的。如果你想验证SMMU但对应的设备没开设备树里即使写了iommus也不会生效因为设备自身没probe。这点要检查清楚。3.2 编译、烧录与验证SMMU是否生效设备树修改后也要编译才能生效。设备树的编译方式和内核镜像略有不同需要先编译dtbs然后生成boot.img或者单独更新dtb分区。不同SDK的烧录方式不一样有些支持fastboot单独烧dtb有些则需要重新打包boot镜像。我常用的流程是这样编译内核和dtbmake ARCHarm64 rk3588-evb.img -j$(nproc) make ARCHarm64 dtbs把生成的rk3588-evb.dtb提取出来用SDK提供的打包脚本重新生成boot.img。烧录boot.img到开发板重启。系统起来后第一件事看dmesg输出里有没有SMMU驱动的初始化日志dmesg | grep -i smmu正常情况下你会看到类似这样的输出arm-smmu-v3 fc900000.iommu: SMMUv3 with model 0x0, impl 0x0, version 0x1 arm-smmu-v3 fc900000.iommu: Event queue populated arm-smmu-v3 fc900000.iommu: Cmd queue populated如果什么都没打印说明驱动没匹配上优先检查compatible字符串和内核配置。如果看到报错比如“Failed to initialize”或者“Unknown SMMU type”则多半是reg地址或中断配置错误。确认SMMU驱动正常后再确认设备是否成功绑定到SMMU。我一般是这样操作cat /sys/kernel/debug/iommu/arm-smmu-v3 cat /proc/iommu cat /proc/interrupts | grep smmu这三个命令能看到当前系统里有哪些SMMU实例、哪些域domain被建立、哪些设备已经attach到SMMU域里。如果VPU节点设备树配好了你应该能在输出里找到VPU对应的StreamID和domain信息。随后给VPU设备写一个简单的DMA测试让它申请一块DMA缓冲区然后读取返回的dma_addr即IOVA跟物理地址对比你会看到IOVA和PA并不是同一个值。这就能直观说明SMMU的地址翻译已经在起作用了。3.3 最简单的DMA测试示例验证地址翻译链路这里我写一个简单到不能再简单的内核模块用来申请DMA buffer并打印物理地址和DMA地址的差异。这个模块不依赖具体硬件只需要在VPU或者其他已挂SMMU的设备驱动里跑类似代码即可。核心逻辑dma_addr_t dma_handle; void *vaddr; vaddr dma_alloc_coherent(dev, size, dma_handle, GFP_KERNEL); if (!vaddr) return -ENOMEM; dev_info(dev, vaddr%px phys%pa dma%pad\n, vaddr, virt_to_phys(vaddr), dma_handle);运行后你会看到vaddr对应的物理地址和dma_handle是不同的值dma_handle就是设备看到的地址IOVA它和物理地址之间的映射关系由SMMU页表维护。如果dma_handle和phys相等那说明这个设备其实没有走SMMU可能它所在的节点没配对iommus或者DMA API在no-iommu模式下退化了。我实测时的数据大概是vaddr0xffff0000xxxxphys0x300xxxxxdma0xffffffc000xxxxxx。看到这种高位地址空间范围的dma地址就对了SMMUv3的IOVA空间很充裕。3.4 多设备共享SMMU时的IOVA分配与内存布局影响当多个设备挂到同一个SMMU实例时每个设备默认会有自己的IOVA地址空间互不干扰。上面提到的DMA API在分配IOVA时会从高地址往低地址找一段空闲区间这个区间的大小由dma-iommu代码管理并不一定跟实际物理内存大小一致。我遇到过一次比较有意思的情况两个设备共享同一个SMMU实例但其中一个设备的IOVA空间特别小DMA分配多块缓冲区后出现IOVA耗尽驱动在dma_map_single时返回错误。这种问题通常不是SMMU本身配置有问题而是IOMMU domain的geometry限制。解决方案可以是适当扩大IOVA空间或者检查设备驱动是否没有及时unmap已分配的DMA缓冲区。在RK3588上GPU、NPU这些设备其实是高度依赖DMA缓冲区的如果它们不挂SMMU内存碎片化和越界访问问题会很突出。反过来如果挂了SMMU但IOVA空间分配不合理可能导致性能下降甚至分配失败。我的经验是先不调整默认的IOVA范围跑一通基准测试看有没有问题再根据实际负载来微调。4. 常见问题与排查技巧实录4.1 问题速查表下面把这些年我在RK3588上遇到的SMMU相关问题和排查方向整理成一张速查表按可能性从高到低排列。这里给出的解决办法只针对“设备树SMMU”场景不涉及硬件损坏这种极端情况。现象可能原因排查与解决启动后dmesg无SMMU驱动日志内核没开SMMU驱动compatible不匹配检查CONFIG_ARM_SMMU_V3确认dtsi节点compatible是arm,smmu-v3SMMU驱动报eventq timeoutSMMU寄存器基址错误时钟/电源未使能确认reg地址与TRM一致检查SMMU节点clocks和power-domains配置设备驱动probe失败提示iommu attach失败iommus属性的StreamID不匹配设备挂在错误的SMMU实例核对设备树StreamID与TRM/内核源码换用调试fs查看实际绑定DMA操作时报page fault或CFG_ERR中断设备访问了未映射的IOVADMA长度越界查看dmesg中的SMMU事件检查设备驱动DMA映射是否遗漏确认IOVA分配足够挂上SMMU后设备性能明显下降IOVA分配/回收频繁TLB未命中率高cache维护多余检查dma-coherent属性用perf统计SMMU TLB行为考虑增大IOVA分配粒度开机后设备随机访问错误复位后恢复设备未挂SMMUDMA越界StreamID错误但偶然可用建议把所有高带宽外设都纳入SMMU严格按TRM确认StreamIDPCIe设备枚举失败或DMA出错iommu-map配置错误PCIe Request ID映射不对检查iommu-map属性对照内核日志确认PCIe设备Request ID4.2 深度排查一SMMU事件中断与“interrupt storm”问题SMMUv3的事件队列event queue是用来上报设备和SMMU自身异常的关键通道。常见事件类型包括F_TRANSLATION设备访问了一个没有页表映射的IOVA。F_PERMISSION设备访问了没有权限的页面。F_ADDR_SIZE设备访问的地址大小不在允许范围内。F_CD_HIT上下文描述符不匹配。F_WALK_EABT页表遍历时发生abort可能是页表受损或映射被意外释放。当这些事件发生且没有被正确处理SMMU会一直上报导致CPU被中断淹没系统的现象就是重启后卡死或者持续打印“Unhandled SMMU event”。我用实际经历说下排查思路第一次在RK3588上给VPU挂SMMU时一跑视频解码就死机串口狂刷arm-smmu-v3 fc900000.iommu: event 0x02 received。0x02对应的大概率是translation fault。后来我认真查了VPU的DMA请求逻辑发现VPU固件默认访问的是一个固定物理地址而这个地址没有映射到SMMU页表。最终解决方法是让驱动在加载固件前用DMA API给这块固件区域建立一个合法的IOVA映射。这种问题不是SMMU配置错误而是设备驱动没有完全遵守DMA API规则。排查这类事件时建议按下述流程来dmesg里看到event上报时先记录完整的event数据包括设备StreamID、IOVA地址、event type。用/sys/kernel/debug/iommu/下的信息反查该StreamID对应的设备和domain。根据event地址判断访问方向这个地址是驱动声明的DMA缓冲区还是设备固件里硬编码的地址。如果是硬编码地址检查驱动是否有机会在DMA前注册映射如果设备行为不可控可能需要设备固件配合修改。比较常见的一种“中断风暴”其实是中断号配置错位。SMMUv3节点在设备树里有多个interrupt和interrupt-names如果eventq和gerror中断顺序写反了驱动注册的中断回调会处理不匹配的中断导致既看不到事件内容也清不掉中断pending位。检查方法是看驱动源码里对interrupt-names的解析顺序确保和dts完全一致。4.3 深度排查二配置了iommus但设备没绑定上domain还有种情况是设备树里写了iommus smmu0 0x0但/sys/kernel/debug/iommu/arm-smmu-v3的输出里找不到这个设备。多半原因在于设备的驱动在probe时没有调用iommu_attach_device或DMA API的相关接口或者该设备是platform bus却使用了非标准DMA API路径。可以用以下方式验证设备是否已关联到IOMMU domainls -l /sys/kernel/debug/iommu/ cat /sys/kernel/debug/iommu/arm-smmu-v3如果发现设备没有domain检查设备驱动代码里是否调用了of_dma_configure或of_iommu_configure。正常情况下platform device的probe流程里内核会通过of_dma_configure自动解析iommus属性并建立绑定关系。但如果设备驱动自己用of_platform_populate动态创建子设备就可能漏掉这一步。我在调试RK3588的ISP驱动时遇到过类似问题。ISP的主设备设备树里确实配了iommus但它的子设备以platform device形式动态创建没有继承IOMMU配置。后来在子设备创建后手动调用of_dma_configure(dev, node, true)解决。4.4 深度排查三时钟与电源域没配导致SMMU初始化报错这是一个看上去跟设备树没太大关系、实际关系却极大的坑。SMMU本身也是硬件模块它的MMIO寄存器需要时钟和电源。RK3588的设备树里SMMU节点往往还带clocks和power-domains属性smmu0: iommufc900000 { ... clocks cru CLK_SMMU0; clock-names smmu; power-domains power RK3588_PD_VPU; };忘记配时钟或者时钟源不对SMMU驱动在初始化时可能直接读回全0xFF的寄存器值然后报device not accessible或者SMMU not present。这种问题靠纯看dmesg有时很难判断我建议直接用devmem读SMMU的ID寄存器devmem 0xfc900000正常SMMUv3的ID寄存器是有特定值的如果读到0xFFFFFFFF大概率是时钟或电源域没起来。这个方法和内核日志一对照基本能立刻定位。同理如果SMMU自身时钟有了但所服务设备的时钟没开也会出现设备DMA不工作的情况。RK3588的VPU在运行前必须确保对应power domain已经上电否则SMMU能初始化VPU访问内存照样报错。因此配置SMMU时一定要把目标设备自己的power-domains、clocks一并检查到位。5. 进阶实践与性能调优心得5.1 如何验证SMMU的隔离效果主动制造一次越界访问配置都通了之后我建议主动做一次“破坏性测试”验证SMMU隔离是否真的生效。方法不难临时在内核模块里申请一个较小的DMA缓冲区然后故意在设备驱动里多写一段越界的地址触发一次非法的DMA访问。举例而言驱动通过dma_alloc_coherent分配了4KB缓冲区返回dma_handle和vaddr。我们手动让设备去读写dma_handle 0x10000这个地址。在没挂SMMU的设备上这个写操作很可能直接改写了物理内存里的某个随机数据破坏性不可预估但挂了SMMU之后它会触发translation fault事件日志里会记录一个F_TRANSLATION的event而系统其他部分丝毫未受影响。这个测试我强烈建议在开发阶段做一次目的是确认SMMU的base-bound功能是起作用的。做过这个测试后你再跑压力测试、稳定性测试心里会踏实很多。否则设备树配得对不对、保护范围全不全你根本不知道。5.2 TLB与缓存一致性性能调优的几个思路SMMUv3自身有TLBTranslation Lookaside Buffer缓存频繁的地址映射和TLB miss会带来一定性能损失。在RK3588这种高吞吐平台上性能调优主要围绕以下几点合理设置#iommu-cells和IOVA分配粒度如果设备频繁做小粒度DMA映射TLB miss率会偏高。可以针对设备驱动采用“批量映射”策略即一次映射一大块DMA缓冲区而不是反复map/unmap小块。确认dma-coherent属性是否合理。RK3588的DMA路径在大多数情况下是硬件一致性的加上dma-coherent能避免不必要的cache flush操作。如果某个设备并不是硬件一致性这个属性反而会导致数据不同步需要结合SoC的实际情况来定。减少IOVA空间碎片化。我看到不少驱动喜欢频繁dma_alloc_coherent又释放时间一长IOVA空间就碎成渣。建议高频使用的缓冲区在初始化时统一分配、复用。性能调优没有统一公式必须结合真实业务负载来观察。我一般会用perf统计SMMU相关事件再用/sys/kernel/debug/iommu/的输出看每个domain的map/unmap次数。发现map次数特别多时就先优化DMA映射模式大多数场景能立竿见影。5.3 调试接口和工具推荐最后分享几个我常用的排障命令收藏起来能省不少时间dmesg | grep -i smmu看SMMU初始化和运行日志。cat /proc/iommu看系统中有哪些IOMMU实例和domain。ls /sys/kernel/debug/iommu/arm-smmu-v3*看SMMU调试接口。devmem SMMU基址直接访问SMMU寄存器确认硬件状态。cat /proc/interrupts | grep smmu看SMMU中断是否频繁触发。/sys/kernel/debug/tracing里可以开着iommu相关tracepoint跟踪IOVA分配情况如果内核版本支持的话。这套工具组合下来绝大多数SMMU相关问题都能快速定位到是设备树配错、驱动映射漏了还是硬件访问越界。就我个人的经验来说SMMU配置这件事真正难的不是读懂设备树语法而是建立“设备StreamIDdomainIOVA”这条完整的链路认知。你把这条链路在自己脑子里打通了再回来看dts里每个属性几乎不用背自然就明白了。建议第一次上手时专门拿一块不重要的板子和一个非关键外设来做实验像我这样主动制造几次错误把各种异常日志都见一遍比看一个月文档都管用。后续如果再折腾GPU、NPU、PCIe的SMMU场景你会觉得豁然开朗。
返回列表