☰
RK3588 SMMUv3设备树配置指南:IOMMU环境搭建与调试
2026/10/3 2:14:06 网站建设 项目流程

前阵子帮朋友调一块RK3588的板子,遇到一个特别典型的问题:GPU跑分看着正常,但一旦把显示分辨率拉高,或者同时跑NPU和视频编解码,整个系统就卡成幻灯片。排查了一圈,发现根子根本不在GPU频率、DDR带宽或者电源管理,而是内核里压根没把SMMUv3用起来,所有外设DMA都走的是不做地址翻译的bypass模式,大量无效的cache维护把总线带宽白白耗干,设备之间还互相踩内存。说白了,就是设备树里IOMMU环境没搭对。

这篇文章我会把为RK3588搭建SMMUv3/IOMMU环境的完整思路走一遍,重点放在设备树配置上。从RK3588的SMMUv3硬件实例分布,到节点里每个字段的实际含义,再到外设怎么绑定IOMMU、驱动侧需要配合什么、以及最后怎么验证配置真的生效。适合正在做RK3588、RK3568等Rockchip平台底层开发,被IOMMU、DMA一致性、设备树这几个词来回折腾的工程师。也适合刚开始接触ARM设备树,想搞明白SMMUv3到底是怎么接入Linux IOMMU框架的开发者。

1. RK3588上的SMMUv3实例:先搞清楚硬件上有哪些IOMMU

1.1 从RK3399到RK3588:IOMMU方案的代差

老Rockchip平台的开发者对RK3399那些SMMU应该不陌生,RK3399用的是比较早期的rockchip,iommu方案,compatible字段基本是rockchip,iommu,内部实现接近SMMUv1/v2的思路,寄存器直接暴露,中断上报也比较简单。到了RK3568其实还是这套老方案。但RK3588这一代不一样,它换用了ARM官方的SMMUv3实现,设备树里看到的compatible变成了arm,smmu-v3。

这个变化不能简单理解成“寄存器地址变了”,SMMUv3在架构上是重写的。它的操作方式从“操作一堆寄存器”变成了“驱动分配内存队列,再通过写命令队列来下发操作”,比如TLBI命令、STE配置命令都是走命令队列的。中断方面,事件上报走event queue,全局错误走gerror中断,轻量级故障恢复还能用PRI机制。这套设计和PCIe SAS、NVMe这些高性能设备是同一代架构,对大量并发DMA请求的处理能力比老方案强得多。

1.2 RK3588典型SMMU实例分布

RK3588的SMMU不是一颗独立的芯片,而是以多个SMMU实例的形式分散在SoC内部,每个电源域、每种加速器都挂着自己的SMMU。以Rockchip各版本SDK的设备树来看,常见的实例大概有这些:

实例服务对象典型用途
GPU SMMUMali-G610 GPU图形渲染、Compute Shader
NPU SMMURKNN NPUAI推理、神经网络算子
VPU SMMU视频编解码单元H.264/H.265/VP9编解码
VOP SMMU显示控制器图层扫描、显示链路
ISP SMMU图像信号处理器Camera图像处理
PCIe SMMUPCIe控制器NVMe SSD、PCIe网卡等

每个SMMU实例在设备树中都是一个独立的iommu节点,有自己独立的reg寄存器空间和中断号。RK3588的SDK里,这些SMMU节点默认很多是status = "disabled"状态,需要板级配置去打开。这就是为什么很多人拿到开发板,一开始看/sys/kernel/iommu_groups/下面空空如也,或者只有PCIe产生的几个group。

1.3 不开SMMU会怎样:不只是安全问题

对很多应用开发者来说,IOMMU看起来是个“安全隔离”功能,好像是军工级嵌入式系统才需要的东西。但实际上在RK3588这个级别的平台上,不开SMMU首先砸的是性能。

原因在于cache一致性维护。没有SMMU做地址翻译时,CPU和外设共享物理内存,而CPU有L1/L2/L3缓存,设备直接DMA读写内存的话,可能读到的是CPU缓存里的旧数据,或者CPU缓存了设备刚写入内存的数据但自己不知道。为了保证数据一致,Linux DMA API会做大量的cache clean/invalidate操作。GPU和NPU这种高带宽外设,每一次大批量DMA都做一轮cache维护,带宽损耗非常恐怖。开了SMMU之后,DMA操作可以直接命中IOVA映射好的内存区域,很多场景下cache维护的开销能省掉一大截。

另外还有内存连续性问题。不开SMMU,驱动申请DMA buffer必须找物理连续的内存,这在长时间运行的设备上很容易导致内存碎片化,分配失败率节节攀升。开了SMMU之后,IOVA可以映射到物理上分散的页,对设备来说看到的是一段连续的地址空间,内存管理压力小很多。

1.4 什么时候需要手动改设备树

SDK自带的defconfig和设备树,通常已经给官方开发板配置好了SMMU。但实战中遇到的问题是另一种情况:

  • 自己画的核心板,某个外设没有使用默认的SMMU实例,需要改绑定的SMMU。
  • 裁剪系统时把某些SMMU实例裁掉了,现在又要加回来。
  • 新加了一个FPGA或自定义IP作为PCIe外设,需要调整iommu-map映射。
  • 发现某个外设驱动一直报DMA错误,排查下来是SMMU状态和实际硬件不匹配。

所以“从零配置”不是一个理论练习,而是嵌入式开发中真实会碰到的工作。

2. 设备树里SMMUv3节点长什么样:逐字段拆解

2.1 一个典型的SMMUv3节点

先给一个RK3588平台上常见的SMMUv3节点示例。不同SDK版本节点名称和地址会有差异,但节点内部结构基本一致:

gpu_smmu: iommu@fd000000 { compatible = "arm,smmu-v3"; reg = <0x0 0xfd000000 0x0 0x20000>; interrupts = <GIC_SPI 77 IRQ_TYPE_LEVEL_HIGH>, <GIC_SPI 78 IRQ_TYPE_LEVEL_HIGH>; interrupt-names = "eventq", "gerror"; #iommu-cells = <1>; clocks = <&cru CLK_GPU>; clock-names = "clk"; power-domains = <&power RK3588_PD_GPU>; status = "disabled"; };

我见过很多新手拿到这个节点之后,直接照抄到自己的板级dts里,然后发现SMMU没起来。原因很简单,status = "disabled"还在,光复制节点是不行的。但除了status之外,这个节点里还有几个字段值得逐个搞清楚。

2.2 compatible、reg与SMMUv3的硬件视图

compatible = "arm,smmu-v3"表示这是一个ARM标准SMMUv3设备,Linux内核里对应的驱动是drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c。这个compatible值最好不要自己发明,比如不要写什么rockchip,smmu-v3之类,除非瑞芯微官方在某个BSP里明确定义了这类compatible,否则内核驱动匹配不上。

reg定义的是SMMU控制器的寄存器空间。在RK3588这种64位SoC上,设备树通常设置#address-cells = <2>,所以reg里的地址和长度各占两个cell。<0x0 0xfd000000 0x0 0x20000>表示基地址0xfd000000,大小0x20000。SMMUv3的寄存器本来就比较多,包括STRTAB_BASE、CMD队列基址寄存器、EVTQ基址寄存器、GERROR寄存器等等。这里的大小一般不需要开发者算太细,SDK里给的通常就是正确的。

有一个容易忽略的点:SMMUv3标准驱动在probe时还要确认硬件是否支持某些特性,比如HTTU(硬件加速的页表更新)、PRI、PASID等。这些能力不是设备树能直接开启的,而是硬件实现里烧死的。设备树能做的,是确保interrupts和power-domains配置正确,让驱动能正常访问到这些寄存器。

2.3 中断命名:eventq和gerror不能搞混

SMMUv3的中断体系中,最核心的两个中断是:

  • eventq:事件队列中断。当DMA请求触发了翻译错误(比如访问未映射的IOVA)、访问权限错误、STE没配置等,SMMU会往事件队列写一条事件记录,然后触发此中断。
  • gerror:全局错误中断。用于报告SMMU自身的错误,比如命令队列下溢、STE表配置错误等。

这两个中断在设备树里通过interrupt-names来区分。很多Rockchip SDK里SMMU节点缺省只有一个eventq中断,gerror和eventq共用一个中断号,这也是允许的,驱动会自己识别。

实战中我踩过的一个坑是,从别的平台移植设备树时,中断号抄错了一位,结果eventq中断实际指到了另一个外设的中断。表面看SMMU初始化一切正常,但一旦真的发生DMA翻译错误,SMMU的中断没有被正确响应,系统直接卡死或者只有日志没有任何中断上报。所以拿到设备树后,一定要对照SoC的TRM,逐个确认SMMU实例对应的GIC中断号。

2.4 #iommu-cells与StreamID的对应逻辑

#iommu-cells = <1>这个字段决定了一个设备在引用该SMMU时,iommus属性需要带几个cell。对于标准SMMUv3,这1个cell就是StreamID。

StreamID是SMMU世界里最核心的一个ID,它用来标识“是哪个master在发起DMA”。每个挂在SMMU下面的外设,必须有一个独一无二的StreamID。这个ID由硬件设计决定,通常可以在SoC的memory map或SMMU集成手册里找到。设备树中通过如下方式把这个ID告诉内核:

&gpu { iommus = <&gpu_smmu 0x0>; };

意思是,GPU这个外设的StreamID是0x0,当GPU发起DMA时,SMMU根据StreamID去STE表里找对应的翻译上下文,然后按上下文配置做地址翻译。

如果写的是:

&some_device { iommus = <&gpu_smmu 0x80>; };

那就是告诉内核,某个外设的StreamID是0x80,它挂在gpu_smmu这个SMMU下。如果这个StreamID和实际硬件设计不一致,SMMU收到DMA请求后去STE里查表,查不到有效表项,直接就产生一个事件并拒绝访问,设备驱动就会得到一堆DMA超时或page fault错误。

2.5 外设侧iommus属性与PCIe的iommu-map

普通平台设备通过iommus属性绑定IOMMU,例如:

&vpu { iommus = <&vpu_smmu 0x100>; status = "okay"; };

对于PCIe控制器,情况又不一样。PCIe下的每个设备(Bus/Device/Function)天然有一个RID(Requestor ID),设备树里通过iommu-map来建立RID和StreamID的映射关系:

&pcie2x1l2 { iommu-map = <0x0 &pcie_smmu 0x0 0x1000>; status = "okay"; };

这段的意思是,从PCIe RID 0x0开始,一共0x1000个RID,映射到pcie_smmu的StreamID 0x0到0xFFF。为什么要单独写这样一个映射?因为PCIe设备的数量是动态的,插入什么卡不确定,RID范围固定但对应哪个StreamID,需要平台设计者给出规则。这个规则不同SoC差异很大,RK3588的PCIe SMMU映射也需要查TRM确认。

顺便说一句,如果你只是给GPU、NPU、VPU这些SoC内部设备配置SMMU,iommus就够了,完全不用碰iommu-map。

3. 从零配置RK3588设备树的完整流程

3.1 准备工作:拿到正确的SDK和工具链

动手改设备树之前,先确认环境。我建议用Rockchip官方发布的Linux SDK,或者你自己vendor定制的内核源码树。交叉编译工具链用aarch64-linux-gnu-gcc即可。热搜里常看到“arm交叉编译”这个词,但这里要明确,RK3588跑的是64位ARMv8,必须用aarch64工具链,用arm-linux-gnueabihf那种32位工具链编译出的内核是跑不起来的。

设备树编译本身不依赖交叉编译器,用内核仓库自带的dtc工具就能完成:

make ARCH=arm64 rockchip_linux_defconfig make ARCH=arm64 dtbs

如果只想单独编译某个dtb:

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- dtbs # 或者手动用dtc ./scripts/dtc/dtc -I dts -O dtb -o rk3588.dtb arch/arm64/boot/dts/rockchip/rk3588-evb1-lp4-v10.dts

3.2 在SDK里找出可用的SMMU节点

第一步永远是搜索,而不是硬写。打开内核源码目录,在arch/arm64/boot/dts/rockchip/下面找rk3588相关的dtsi文件:

grep -n "arm,smmu-v3" arch/arm64/boot/dts/rockchip/rk3588*.dtsi

正常情况下你会看到多个SMMU节点,每个对应一个硬件实例。你要做的不是去重新发明这些节点,而是确认它们是否被正确使能,以及要给自己想启用IOMMU的设备加上iommus属性。

搜索之后,对照你的板级dts,看这些SMMU节点的status是否已经是okay。SDK默认状态不一定,比如官方EVB板子的设备树可能所有SMMU都开了,但一个小批量定制板卡的BSP可能把它们全关了,因为厂商觉得“用不到IOMMU,还能省点启动时间”。

3.3 使能SMMU节点和绑定外设

在板级dts(比如你自己的rk3588-custom-board.dts)中,做两件事:

第一,打开SMMU节点:

&gpu_smmu { status = "okay"; };

第二,给目标外设绑定IOMMU:

&gpu { iommus = <&gpu_smmu 0x0>; status = "okay"; };

这里建议把iommus绑定放在板级dts里而不要改dtsi的公共部分,原因很简单:板级dts是你们项目自己的配置,公共dtsi是给所有板卡共用的,改了会影响其他人。

如果你能把每个SMMU实例的stream ID都从TRM里查出来,那就更稳妥了。比如NPU的SMMU可能需要:

&rknpu { iommus = <&rknpu_smmu 0x200>; status = "okay"; };

这里0x200只是举例,实际StreamID要按TRM填写。

3.4 内核config和启动参数的安排

设备树配好了,内核config不对也不行。在arm64平台上,以下config是必须确认的:

CONFIG_IOMMU_SUPPORT=y CONFIG_IOMMU_DMA=y CONFIG_ARM_SMMU_V3=y

这三个就是SMMUv3设备树生效的基本盘。如果用的是vendor SDK,可能还有老式的CONFIG_ROCKCHIP_IOMMU之类,但那是给老平台用的,RK3588走的是CONFIG_ARM_SMMU_V3。

启动参数方面,有两个cmdline参数影响IOMMU行为:

  • iommu.passthrough=0/1:默认是0,表示所有设备都走地址翻译模式。设为1表示全部走passthrough,等于把SMMU当透明桥用,通常不推荐这样全局设置,因为就失去意义了。
  • iommu.strict=1/0:strict模式表示IOVA和页表的unmap是同步释放的,性能可能受影响但内存管理简单。默认值在不同内核版本里不太一样,需要按实际业务选。

如果你只是想让某个外设绕过SMMU,不要用cmdline全局设置,而是在设备树里不给那个外设写iommus属性,或者让对应的SMMU实例保持disabled状态。

3.5 编译烧录与常见假象

设备树改好之后,编译、打包、烧录,这些流程SDK文档里都有,我就不重复了。但我想说一个很容易骗到人的细节:你有没有确认你烧录进去的dtb,真的是你改的那份?

Rockchip平台的内核镜像通常是boot.img,里面包含了kernel和dtb。有的SDK打包脚本会把dtb直接塞进kernel image里,有的会单独打包成一个resource.img。如果你只改了dts,但打包脚本没把新dtb打进去,或者打进去的顺序不对,你看到的设备树行为还是老样子。这种情况我遇到过不止一次。

烧录后第一时间验证设备树内容:

# 在板子上查看实际生效的设备树 cat /proc/device-tree/gpu_smmu/status ls /proc/device-tree/iommu@fd000000/

如果读到的是okay,说明你改的dtb生效了;如果还是disabled,回去查打包链路,大概率是烧录的不是你编译产物。

4. 驱动侧不配合,SMMU配了也白配:DMA mask与内存分配

4.1 设备驱动其实不用直接碰SMMU

Linux对IOMMU的抽象做得比较彻底。设备侧驱动只要写好了DMA API,IOMMU框架会自动帮你完成地址翻译和页表管理。也就是说,你的驱动里不需要调用任何SMMU专用API,不需要读SMMU寄存器,也不需要手动设置STE表项。内核的IOMMU框架会在设备probe时,通过设备树中的iommus属性找到对应的IOMMU域,然后给这个设备建立dma地址空间。

这个设计对驱动开发者非常友好,但同时也意味着:如果你的驱动DMA API用得不对,问题可能被掩盖成各种奇怪的现象,而不是直接报“IOMMU失败”。

4.2 dma_mask:最容易忽略的隐性炸弹

一个最常见的配合问题是dma_mask设置不当。设备树里配好了SMMU,但驱动在probe时没有正确设置dma_mask和coherent_dma_mask,内核DMA层会用一个极保守的默认值,导致dma_alloc_coherent分配失败,或者分配的地址设备根本访问不到。

标准驱动代码里应该有类似这样的初始化:

static int my_device_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; int ret; ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(40)); if (ret) { dev_err(dev, "failed to set DMA mask: %d\n", ret); return ret; } ... }

为什么是40位?因为SMMU的IOVA地址宽度和物理地址宽度不一定相同,RK3588的SMMU通常支持40位以上的地址空间,但具体是多少要看TRM里的SMMU地址位宽。设置一个过高的DMA mask而不确认硬件支持,可能导致IOVA分配时出错。设置得过低,又会限制IOVA空间容量。这个数字最好以硬件手册为准。

如果你写的是平台驱动,但用的是旧式platform_get_resource(pdev, IORESOURCE_MEM, 0)加ioremap这一套,也没有设置DMA mask,那在SMMU开启后,DMA操作经常会“时而正常时而失败”。因为有时映射到的物理内存恰好是DMA能访问的,有时不是。

4.3 DMA API使用的几个细节

在驱动里分配DMA内存,推荐用标准API:

dma_addr_t dma_handle; void *cpu_addr; cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);

使用dma_alloc_coherent的好处是,它拿到的内存同时满足cache一致性和IOVA映射两个条件。SMMU会为这块分配的内存建立页表映射,返回的dma_handle是一个IOVA地址,设备用这个地址发起DMA完全没问题。

另外一些需要注意的点:

  • 如果你在驱动里自己分配了内存(比如kmalloc或alloc_pages),想把这个内存的物理地址直接给设备用,在SMMU场景下是行不通的。设备看的是IOVA,不是物理地址。必须通过dma_map_single()/dma_map_page()这类API把内存映射到IOVA空间,告诉设备使用返回的dma_addr。
  • 映射方向DMA_TO_DEVICE/DMA_FROM_DEVICE/DMA_BIDIRECTIONAL要写对,尤其是双向传输的场景,写错会导致数据错乱。
  • 取消映射时一定要对应成对使用dma_unmap_single()/dma_free_coherent()。漏unmap会造成IOVA泄漏,长时间运行后IOVA耗尽,DMA分配失败。

4.4 coherent与non-coherent的内存模型差异

设备树里可能出现dma-coherent属性,这表示设备的DMA访问与CPU缓存是保持一致的。如果不加这个属性,内核会认为设备是非coherent的,在DMA操作前后需要做cache clean/invalidate,性能损失明显。

但在RK3588上,不能简单认为“加了dma-coherent就一定好”。这个属性必须符合硬件的真实行为。如果设备实际是non-coherent,你却声明了coherent,那设备DMA写入的数据在CPU缓存里可能看不到,表现出来就是数据莫名丢失、视频花屏、GPU输出错乱。反过来,设备实际是coherent却没声明,性能下降但至少数据不坏。

所以在改设备树时,SMMU节点本身有dma-coherent,不代表它服务的外设也带dma-coherent。每个外设节点要按各自的硬件手册单独确认。设备节点的dma-coherent属性,SMMU只是负责地址翻译,不管cache一致性。

5. 怎么确认IOMMU真的生效了:日志、sysfs与一次故意的crash

5.1 从启动日志里读出SMMU初始化状态

配置完设备树,重启板子,第一件事是看内核日志:

dmesg | grep -iE "smmu|iommu"

正常情况下,你能看到类似这样的输出:

[ 1.234567] arm-smmu-v3 fd000000.iommu: probing hardware configuration... [ 1.234567] arm-smmu-v3 fd000000.iommu: SMMUv3 with (0x10) features: ... [ 1.234567] iommu: Default domain type: Translated

如果你看到的是arm-smmu-v3: probe of fd000000.iommu failed with error -16,那一般是中断号冲突或者power domain没起来,要回头查设备树资源。

更直接的一个方法,是启动后检查/sys/kernel/iommu_groups/目录:

ls /sys/kernel/iommu_groups/

每出现一个数字编号的目录,就代表内核建立了一个IOMMU group。查看某个group下挂了哪些设备:

ls /sys/kernel/iommu_groups/0/devices/

如果GPU、NPU或你想配置的外设出现在某个group下,说明设备已经成功attach到了IOMMU域。如果设备树配了iommus但这里没有对应设备,问题大概率出在驱动probe时没有走of_dma_configure路径,或者驱动的probe直接失败了。

5.2 用debugfs看IOVA分配和页表情况

内核开启CONFIG_IOMMU_DEBUGFS后,可以查看更详细的IOMMU状态:

ls /sys/kernel/debug/iommu/

这里能看到每个iommu domain的页表信息、IOVA分配范围等。对调试IOVA耗尽问题特别有帮助。不过有些SDK内核没开debugfs,或者挂载权限限制,先用mount -t debugfs none /sys/kernel/debug挂载一下。

5.3 制造一次IO page fault:最靠谱的验证方式

配置完SMMU之后,怎么知道翻译功能真的在工作?一个非常有效的方法是“主动触发一次翻译错误”。

方法很简单:写一个临时的字符驱动,在probe里用dma_alloc_coherent分配一块缓冲,但故意把dma_handle改写掉,比如dma_handle |= 0x80000000,然后让设备往这个错误地址写数据。如果SMMU在工作,设备发起的DMA会触发IO_PAGE_FAULT,内核日志里出现:

arm-smmu-v3 fd000000.iommu: event 0x10 received: arm-smmu-v3 fd000000.iommu: 0x00000000f0100000 arm-smmu-v3 fd000000.iommu: 0x0000000000000080 arm-smmu-v3 fd000000.iommu: 0x0000000000000000 arm-smmu-v3 fd000000.iommu: 0x0000000000000000

事件类型0x10表示F_TRANSLATION,即地址翻译失败。看到这条日志,基本可以确定SMMU已经接管了设备的DMA地址翻译。

如果你改错地址后,设备居然也能正常访问内存,没有任何事件上报,那就要怀疑SMMU是不是没在翻译模式,而是走了bypass。

5.4 性能对比:验证IOMMU带来的收益

除了看日志和sysfs,也可以通过实际性能测试来验证配置效果。最简单粗放的方法是,对比开启SMMU前后,同一个GPU或NPU benchmark的吞吐量,以及dmesg里cache维护相关日志的频率。

更专业的做法是用perf统计TLB miss或者用bus tracer看总线上cache维护事务的多少。但对于大多数嵌入式团队来说,帧率、FPS、推理耗时、编解码fps这几个指标足够说明问题。如果发现开启SMMU后性能反而明显降低,请跳到第6.4节看strict模式的坑。

6. 实际调试记录:遇到的坑和排查套路

6.1 eventq中断风暴:一路刷屏停不下来

有次调试RK3588的VPU视频解码,SMMU打开后,板子直接刷屏:

arm-smmu-v3 vpu_smmu: event 0x10 received: ...

每秒钟刷出几十条翻译错误事件。第一反应是StreamID配错了。后来查TRM发现,VPU模块内部有多个子模块(decode、encode、post-processor),它们各自有不同的StreamID,而设备树里给整个VPU节点只配置了一个StreamID。子模块发DMA时用了自己没有映射的StreamID,SMMU自然拦截。

解决办法不是在驱动里强行塞死子模块,而是去查TRM里VPU各子模块对应的StreamID,在设备树里给每个子节点配正确的iommus。如果内核里VPU是作为一个复合设备出现,可能还需要看驱动的iommu attach逻辑,确保每个子设备都正确绑定了SMMU。

6.2 外设iommus属性cells数量和SMMU不匹配

另一个高频问题:SMMU节点写的是#iommu-cells = <1>,但外设节点里写了两个cell:

&rknpu { iommus = <&rknpu_smmu 0x0 0x1>; status = "okay"; };

内核解析时直接报错,设备DMA功能整个失效。这种错误其实很容易查,dmesg | grep iommu就能看到parsing failed之类。但为什么会出现?很多开发者从老平台移植设备树,老平台rockchip,iommu可能允许带一个附加标识cell,到了SMMUv3标准绑定里就只认一个StreamID。移植时没有仔细核对bindings文档,就踩进去了。

规范的做法是参考内核里的Documentation/devicetree/bindings/iommu/arm,smmu-v3.yaml,这个文件定义了这个compatible下所有属性的合法写法。

6.3 power-domain没打开:SMMU寄存器读到全F

如果你在调试时发现SMMU驱动probe失败,现象是访问寄存器时读回的全是0xFFFFFFFF,或者系统直接触发external abort,那不用急着怀疑SMMU节点本身的reg写错了,先确认SMMU所在的电源域和时钟有没有被使能。

RK3588的SMMU实例通常跟随服务对象的power domain,比如gpu_smmu挂在GPU的power domain下。如果GPU的power domain本身是关闭状态,CPU去访问SMMU寄存器就会失败。设备树里通常通过power-domains = <&power RK3588_PD_GPU>来关联。如果你在板级dts里改了外设的电源域,别忘了检查对应的SMMU节点是否也需要同样的关联。

时钟也一样。有的SMMU节点带clocks属性,如果对应时钟被关闭或者设置了错误的clock parent,SMMU也会无法正常工作。这类问题在启动日志里不一定有明确报错,更多时候表现为SMMU驱动“静默失败”,然后外设DMA各种超时。

6.4 strict模式与性能下争论:一个取舍问题

有次用户反馈,GPU开启IOMMU之后性能还不如不开。查下来是内核默认把iommu.strict设成了1,所有IOVA和页表在unmap时立刻释放。GPU这种高频创建销毁DMA映射的外设,频繁的TLBI和页表更新操作成了热点。

试了几组参数之后,把iommu.strict=0加进cmdline,让IOVA和页表异步释放,GPU帧率提升明显。但strict=0也有代价——内存和IOVA的释放会有延迟,极端场景下可能出现IOVA分配不出去的假象。所以这个参数要结合业务取舍,不是无脑开成non-strict。

6.5 cache一致性问题:SMMU管不了的事

SMMU只负责地址翻译和访问权限控制,不负责cache一致性。有次调试一个自定义FPGA通过PCIe挂在RK3588下,发现FPGA读到的数据总是差一拍,明显是cache stale。一开始怀疑SMMU配置问题,但仔细看事件队列,没有翻译错误,sysfs里也正常。

后来查FPGA的AXI属性,发现它没有走可以自动维护cache一致性的通路,设备侧也没有声明coherent。加上设备树中给PCIe外设的描述里本来就没有dma-coherent,驱动里又用了dma_alloc_coherent,但FPGA侧读的时候并没有让CPU做cache invalidate。这种情况下,SMMU无能为力,得靠驱动在正确的时机做cache操作,或者确认硬件互连是否支持coherent请求,不能把所有锅都甩给IOMMU设备树配置。

这些坑回过头看,每一个都有清晰的排查路线。设备树配置SMMUv3的难点其实不在于“写几行属性”,而在于对硬件细节的理解足够细:StreamID从哪来、中断号对不对、电源时钟是否齐全、驱动写的DMA API是否规范。把这些链路都打通之后,IOMMU在RK3588上的价值才会真正体现出来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询