嵌入式Linux下DMA_CHANNEL_NPRIV错误定位与修复
2026/8/30 15:27:40 网站建设 项目流程

前一阵调一块嵌入式板卡,外设数据通过 DMA 搬运时,内核日志里反复刷出DMA_CHANNEL_NPRIV error。第一眼看到这行字我有点懵:Linux 的 dmaengine 框架里并没有一个叫DMA_CHANNEL_NPRIV的标准错误码,而且日志里没带通道号、没带设备地址,直接甩在这就完事了。翻遍整份内核文档也找不到对应条目,GitHub 上能搜到的几处也都是零零散散一句话。后来顺着驱动往下挖,才发现这个错误根本不是内核框架抛的,而是 SoC 的 DMA 控制器固件直接返回的状态位。这篇就把定位过程和最终方案写全,给同样卡在这个错误上的人一条可复现的排查路径。

如果你也遇到了这个报错,不要急着改驱动,先把它当成一个硬件权限事件来处理。

1. 先认识这个报错:DMA_CHANNEL_NPRIV 到底是谁抛出来的

1.1 错误码的出处与大致含义

在标准内核源码include/linux/dmaengine.h里,dmaengine 的状态返回通常只有DMA_COMPLETEDMA_IN_PROGRESSDMA_PAUSEDDMA_ERROR这几类,并没有DMA_CHANNEL_NPRIV这样一个统一字符串。多数 DMA 驱动在底层硬件出错时,会翻译成-EIO-EFAULT或者直接上报DMA_ERROR,能保留原始硬件状态字并转成这种可读字符串的情况很少见。

所以当你看到DMA_CHANNEL_NPRIV时,基本可以断定这是厂商 SDK 驱动或者 DMA 控制器固件自定义的宏。NPRIV 是 Non-Privileged 的缩写,直译过来就是“非特权访问被拒绝”。常见含义是:发起 DMA 请求的主设备,带着一个非特权(Non-secure 或 Non-privileged)的事务属性,去访问了一个只允许特权模式访问的 DMA 通道、缓冲区或寄存器区域,硬件安全模块直接把这个事务掐断了。

这个过程很像小区门禁:DMA 通道是房间,SMMU/TrustZone 这类组件是门禁系统,门禁要求只能管理员卡进入,但这次来的是普通访客卡,门禁连门都不开,直接吐一个 NPRIV 状态位给 DMA 控制器,DMA 控制器再把这个状态上报给 CPU。

1.2 为什么这个报错容易让人走弯路

我这次卡了接近三天,回头想,主要是三个原因:

  • 报错出现的位置和触发点并不同步。DMA 是异步操作,提交请求后 CPU 可以继续跑,等到中断或超时才发现通道已经报错了,这时再回溯是哪一笔请求、哪个外设触发的,已经很困难。
  • 这个错误大多出现在中断上下文或者内核线程里,没有用户态进程上下文可供抓取,perf、strace 都帮不上忙。
  • 不同 SoC 对同一类问题的话术不一样。有的平台叫DMA_CHANNEL_NPRIV,有的叫DMA_SECURE_VIOLATION,还有的叫SEC_ERRAXI_PROT_ERR,底层寄存器位含义类似,但搜索引擎上只能命中某一个关键词,很容易一开始就搜错方向。

因此我建议遇到这种报错,第一步不是去翻驱动代码,而是先搞明白“这个字符串到底是谁打印出来的”。可以在内核源码根目录直接搜索字符串:

grep -r "DMA_CHANNEL_NPRIV" --include="*.c" --include="*.h" /path/to/kernel

如果官方内核搜不到,就去厂商提供的 BSP 包里搜。找到打印位置后,顺藤摸瓜就能看到它读的是哪个状态寄存器,这一步能把排查范围迅速从“整个内核”缩小到“某个 DMA 控制器的状态位”。

2. 从日志开始定位:先分清是 CPU 侧还是设备侧拒绝

2.1 抓日志时必备的信息

定位这种问题,最忌讳只拿一行 dmesg 就开猜。我建议至少先把以下信息收集齐:

  • 完整内核日志,启动时加ignore_logleveldma_debug=1,避免日志被限流,同时让 DMA API 检查尽量多跑一轮。
  • 设备树里 DMA 控制器和外设的完整节点,包括dma-coherentiommusmemory-regiondma-channels这些属性。
  • 驱动里申请 DMA channel 的位置,是用的dma_request_chan还是dma_request_slave_channel,配置的dma_slave_config里 src、dst 地址和方向。
  • 复现时准确的串口时间戳,方便把错误和具体操作对上。

如果平台开了 IOMMU/SMMU,务必把 debugfs 信息也导出来。

ls /sys/kernel/debug/iommu/ ls /sys/kernel/debug/arm-smmu/ cat /sys/kernel/debug/iommu/address

我用dma_debug=1跑过一次,虽然没有直接抓到 NPRIV 的来源,但很快确认不是 dma_map/unmap 层面的问题,因为缓冲区分配和映射的日志都很正常。这一步能帮你排除掉一大批“内存一致性”“地址未映射”类错误。

2.2 做一个最小复现:把干扰项剪掉

如果项目里已经有完整的外设驱动,第一次报错往往夹杂着业务数据流,很难判断是业务操作触发的,还是驱动初始化就触发的。我当时的做法是砍掉所有业务逻辑,只保留一个最基础的 DMA 搬运流程,做成内核测试模块。

核心逻辑大致如下:

static int dma_test_transfer(struct device *dev) { struct dma_chan *chan; struct dma_slave_config cfg = {0}; struct dma_async_tx_descriptor *desc; struct dma_tx_state state; dma_cookie_t cookie; void *src_buf, *dst_buf; int ret; chan = dma_request_chan(dev, "tx"); if (IS_ERR(chan)) return PTR_ERR(chan); cfg.direction = DMA_MEM_TO_MEM; cfg.src_addr = virt_to_phys(src_buf); cfg.dst_addr = virt_to_phys(dst_buf); cfg.src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES; cfg.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES; cfg.src_maxburst = 4; cfg.dst_maxburst = 4; ret = dmaengine_slave_config(chan, &cfg); if (ret) { dma_release_channel(chan); return ret; } desc = dmaengine_prep_slave_single(chan, virt_to_phys(dst_buf), PAGE_SIZE, DMA_MEM_TO_MEM, 0); if (!desc) return -ENOMEM; cookie = dmaengine_submit(desc); dma_async_issue_pending(chan); /* 等待传输完成或超时 */ dma_wait_for_async_tx(desc); dmaengine_tx_status(chan, cookie, &state); dma_release_channel(chan); return 0; }

这个模块跑起来后,如果第一次申请通道、第一次提交就报错,那问题基本锁定在“通道本身不可用”;如果每次都是跑了几轮之后才报错,那就要考虑资源竞争、缓冲区访问越界或状态积累。我这次属于前者:只要加载模块,第一个 DMA 请求就会被拒。

更省事的方法是用内核自带的dmatest模块。它本身就是用来验证 DMA 通道的,能够自动做 buffer 填充和 CRC 校验,不需要写代码。加载方式很简单:

modprobe dmatest channel=dma0chan0 iterations=100 timeout=500 cat /sys/kernel/debug/dmatest/summary

如果dmatest同样报错,问题就不再是业务驱动的问题,而是更底层的 DMA 通道配置问题。

2.3 判断拒绝发生在哪一层

拿到错误后,我的判断顺序是:

  • 先确认 CPU 侧有没有访问异常。如果 dmesg 里有关于 SMMU、IOMMU、pagetable 的错误,说明问题出在地址翻译或者权限检查阶段。
  • 再确认 DMA 控制器本身有没有把错误状态寄存器记录下来。很多 SoC 在 DMA 中断状态寄存器里会有SEC_ERRNPRIV_ERRBUS_ERR这样的独立位,查 TRM 的寄存器描述,能直接定位到具体错误类型。
  • 最后确认当前请求的缓冲区地址。如果错误状态寄存器里附带了出错的物理地址,可以对比一下是不是落在了设备树预留的安全内存区域里。

这一步非常关键。如果是 CPU 侧拒绝,你会看到 SMMU 打印的 fault 信息,里面通常有 master 和 Stream ID;如果是设备侧拒绝,SMMU 那边干干净净,只有 DMA 控制器在报错。本次排查看下来,SMMU 没有任何打印,所以问题被牢牢锁定在 DMA 控制器自身。

3. 我这次踩坑的完整排查链路

3.1 第一层怀疑:设备树配置漏了 dma-coherent

一开始我怀疑是 DMA 缓冲区的一致性配置不对。嵌入式平台上,DMA 需要的缓冲区必须是物理连续的,并且 CPU 缓存和 DMA 控制器看到的视角要一致。设备树里dma-coherent;这行属性,表示该设备和 CPU 共享内存一致性域,不用每次搬运都做 cache sync。如果漏掉它,DMA 会走一致性映射,但这本身不会造成 NPRIV 错误,只可能导致数据不一致。

我当时的设备树片段类似这样:

reserved-memory { #address-cells = <2>; #size-cells = <2>; ranges; dma_pool: dma_pool@a0000000 { compatible = "shared-dma-pool"; no-map; reg = <0x0 0xa0000000 0x0 0x100000>; }; }; &dma_controller { memory-region = <&dma_pool>; dma-coherent; };

检查后发现 memory-region 引用和dma-coherent都在,而且dma_debug=1也没有报 buffer map/unmap 不匹配。这个方向很快排除。不过我还是把no-map这段内存单独和错误地址比对过,最终确认报错的地址不在这段保留区内。

这个绕路其实不算白走,因为我确认了“内存区域没有越界”,就把排查重点从“地址映射”移到了“访问权限”。如果你也遇到类似问题,建议先看一眼错误地址是否落在保留区。是的话,大概率是权限或预留内存配置问题;不是的话,再往下查通道属性。

3.2 第二层怀疑:SMMU/IOMMU 的 StreamID 配错

ARM SoC 里,DMA 请求由主设备发起,会带上一个 StreamID,SMMU 根据 StreamID 找到对应的页表和上下文描述符,再决定这次访问是否合法。如果 StreamID 没有配置,SMMU 可能直接上报 Global Error 或者安全错误。NPRIV 和这类问题看起来很像,都是“硬件不放行”。

排查 StreamID 是比较繁琐的一步:

  • 在 SoC 手册里找到 DMA 控制器对应 Master 的 StreamID 列表。
  • 在设备树里检查每个外设的iommus = <&smmu 0x??>是否和硬件一致。
  • 通过 SMMU debugfs 看实际匹配到的 StreamID。

我本来以为会在这个环节找到问题,但实测下来,这块平台上 SMMU 根本没有使能,DMA 控制器直接和内存相连,没有经过 SMMU 翻译。所以错误不是 SMMU 抛出来的,这一条也排除了。

排除后,我反而不慌了:既然没有 SMMU,还能报 NPRIV,说明这大概率是和 TrustZone/安全属性相关的硬件拦截,也就是固件层的通道分配问题。

3.3 第三层怀疑:固件里 DMA 通道被安全世界独占

这块平台的 DMA 控制器一共有 8 个通道,设备树里只暴露了前 4 个,后 4 个被固件“隐藏”了。驱动申请 dma0chan0 正常,但从 dma0chan1 开始,只要一发请求,就会报DMA_CHANNEL_NPRIV

看了平台固件(ATF/OP-TEE)的初始化代码之后,根因浮出水面:固件在上电阶段把 DMA 控制器的部分通道标记成了 Secure 属性,保留给安全世界使用。普通内核跑在 Non-Secure 世界,非安全事务去访问这些通道时,硬件会在入口直接拦截,并置上 NPRIV 错误位。这和你代码写没写对完全没有关系,纯粹是“这把钥匙打不开这扇门”。

确认方式其实很直接:在固件启动日志里搜索 DMA 相关的初始化信息,或者直接读固件源码里对 channel 安全属性的默认配置。我这边是一条条打印了 DMA 控制器状态寄存器,发现固件给 4~7 号通道设置的 security bit 都为 1,而 Linux 驱动只要访问到这些通道,错误状态位就会被置位。

到这一步,问题的性质就变了:不是内核驱动写错了,而是固件和内核的设备树对通道的理解不一致。内核以为这些通道可用,固件却已经把它们锁进了安全世界。

4. 最终修复与验证

4.1 本次根因与对应修改

根因确认后,有两个修复方向。

第一个方向是改固件,把通道 4~7 的安全属性关掉,让它们开放给非安全世界使用。这个方向最彻底,但需要同步修改 ATF/OP-TEE 里的平台配置,并且要严格评估安全风险:这些通道原本可能是给可信执行环境使用的,一旦开放,安全边界就变了。

第二个方向更快更稳:既然当前方案根本用不到 4~7 号通道,那就从软件侧把内核可见通道数量限制在前 4 个。设备树里增加dma-channels = <4>;,驱动改为读取设备树中的通道数,而不是硬编码 8。这样内核就不会去碰那些被安全世界独占的通道。

我最终选择的是第二个方向,一是业务场景用不到那么多通道,二是不用改动已验证过的安全固件,降低整体风险。关键代码改动很小:

&dma_controller { compatible = "vendor,dma-controller"; dma-channels = <4>; #dma-cells = <1>; };

驱动侧如果原本直接用宏定义死通道数,要改成从 device node 读取:

struct device_node *np = pdev->dev.of_node; int nr_channels = 4; of_property_read_u32(np, "dma-channels", &nr_channels);

这样驱动只会在 0~3 号通道之间创建 dmaengine channel,不会再枚举到 4~7 号通道。加载后,DMA_CHANNEL_NPRIV就再没出现过。

如果你是平台厂商,更建议走第一个方向,因为隐藏通道会让内核实际资源和硬件资源不一致,后续维护很容易踩坑;但如果是产品迭代阶段,第二个方向通常是性价比最高的止血方案。

4.2 验证不再出现错误

修完代码不能只跑一次业务就算完,我用下面的步骤做了完整验证:

先用dmatest做高强度传输:

modprobe dmatest channel=dma0chan0 iterations=100000 timeout=2000 cat /sys/kernel/debug/dmatest/summary

确认 summary 里test_result为 0,CRC 错误计数也为 0。这里有个很容易忽略的细节:dmatest默认会在所有注册通道上跑,如果它自动去测被固件锁住的通道,还是会报错,所以需要用channel参数指定通道,或者把 CPU 亲和性配好。

然后在真实业务里做数据回环测试:用 SPI 采集一批波形数据,通过 DMA 搬运到内存,再从内存搬回 SPI 发送,上位机对比原始数据,跑 10 万次循环,看 CRC 是否有误码。

最后做 24 小时压力测试。我写了个脚本每小时记录一次 dmesg 和 DMA 控制器状态寄存器,重点关注NPRIV_ERRSEC_ERR这两个 bit,确认始终为 0。

我还做了一个负向验证:临时把设备树的dma-channels改回 8,让内核再次枚举到 4~7 号通道,一加载驱动就立刻复现DMA_CHANNEL_NPRIV,再改回 4 又恢复正常。这一正一反,基本可以百分百确定根因就是固件通道安全属性和内核通道枚举不一致。

5. 同类 DMA 报错的快速排查表与习惯建议

5.1 这些容易混淆的错误信号

.代表含义常见原因
DMA_CHANNEL_NPRIV非特权访问被拒绝通道被安全固件占用,或访问了安全属性不匹配的缓冲区
DMA_BUS_ABORT总线访问中止访问了无效物理地址、总线超时、从设备未就绪
DMA_READ_ERROR/DMA_WRITE_ERROR读写数据错误外设数据宽度不匹配、FIFO 配置错误、时钟或电源问题
DMA_TIMEOUT传输超时descriptor 没有正确提交、通道中断没使能、设备时钟门控异常
DMA_ERR/DMA_ERROR通用 DMA 错误需要读具体状态寄存器进一步区分

这些错误在日志里长得像,但排查路径完全不同。DMA_BUS_ABORT我会先去查地址和总线信号,DMA_CHANNEL_NPRIV则会先查权限和安全属性,不要混为一谈。

读错误状态寄存器时,重点看几个通用位:SEC_ERR表示安全错误,NPRIV_ERR表示非特权错误,BUS_ERR表示总线错误,DEC_ERR表示解码错误。每个 bit 对应不同的处理动作,对照 TRM 记录下触发条件,比在源码里瞎翻效率高得多。

5.2 我自己的排错习惯

踩过几次大坑之后,我现在处理 DMA 相关问题基本会保持以下习惯:

第一,任何奇怪的状态码都先当成“硬件返回的原始状态”,而不是直接套内核标准错误。先把打印字符串的出处找出来,再去看它背后的寄存器位,这条路几乎不会跑偏。

第二,尽量用内核自带的dmatest做第一轮验证。它能快速区分是驱动业务问题还是底层通道问题。如果dmatest报错,那基本不用花时间审业务代码了。

第三,设备树、固件、内核的版本组合一定要留记录。DMA 这类问题经常不是代码逻辑错,而是三者的配置发生了错位。比如设备树说有 8 个通道,固件却只开放 4 个,这种隐藏冲突不通过启动日志很难发现。

第四,修完问题之后,我会把启动那一刻 DMA 控制器和 SMMU 相关寄存器的快照保存下来。以后再遇到类似 NPRIV 错误,先比对快照,看通道安全属性有没有变化,能省下大量时间。

就我自己而言,这次之后我建了一个小脚本,把启动后 DMA/SMMU 相关寄存器状态和 key dmesg 打点一并归档。以后再有人报DMA_CHANNEL_NPRIV,我第一件事就是比对寄存器快照,而不是从头开始猜。这个方法不一定最优,但确实能让你在复杂的嵌入式环境下,少走很长的弯路。

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

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

立即咨询