嵌入式Linux下DMA_CHANNEL_NPRIV错误定位与修复

发布时间:2026/8/30 15:27:46
嵌入式Linux下DMA_CHANNEL_NPRIV错误定位与修复 前一阵调一块嵌入式板卡外设数据通过 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_COMPLETE、DMA_IN_PROGRESS、DMA_PAUSED、DMA_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_ERR或AXI_PROT_ERR底层寄存器位含义类似但搜索引擎上只能命中某一个关键词很容易一开始就搜错方向。因此我建议遇到这种报错第一步不是去翻驱动代码而是先搞明白“这个字符串到底是谁打印出来的”。可以在内核源码根目录直接搜索字符串grep -r DMA_CHANNEL_NPRIV --include*.c --include*.h /path/to/kernel如果官方内核搜不到就去厂商提供的 BSP 包里搜。找到打印位置后顺藤摸瓜就能看到它读的是哪个状态寄存器这一步能把排查范围迅速从“整个内核”缩小到“某个 DMA 控制器的状态位”。2. 从日志开始定位先分清是 CPU 侧还是设备侧拒绝2.1 抓日志时必备的信息定位这种问题最忌讳只拿一行 dmesg 就开猜。我建议至少先把以下信息收集齐完整内核日志启动时加ignore_loglevel和dma_debug1避免日志被限流同时让 DMA API 检查尽量多跑一轮。设备树里 DMA 控制器和外设的完整节点包括dma-coherent、iommus、memory-region、dma-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_debug1跑过一次虽然没有直接抓到 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 channeldma0chan0 iterations100 timeout500 cat /sys/kernel/debug/dmatest/summary如果dmatest同样报错问题就不再是业务驱动的问题而是更底层的 DMA 通道配置问题。2.3 判断拒绝发生在哪一层拿到错误后我的判断顺序是先确认 CPU 侧有没有访问异常。如果 dmesg 里有关于 SMMU、IOMMU、pagetable 的错误说明问题出在地址翻译或者权限检查阶段。再确认 DMA 控制器本身有没有把错误状态寄存器记录下来。很多 SoC 在 DMA 中断状态寄存器里会有SEC_ERR、NPRIV_ERR、BUS_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_poola0000000 { compatible shared-dma-pool; no-map; reg 0x0 0xa0000000 0x0 0x100000; }; }; dma_controller { memory-region dma_pool; dma-coherent; };检查后发现 memory-region 引用和dma-coherent都在而且dma_debug1也没有报 buffer map/unmap 不匹配。这个方向很快排除。不过我还是把no-map这段内存单独和错误地址比对过最终确认报错的地址不在这段保留区内。这个绕路其实不算白走因为我确认了“内存区域没有越界”就把排查重点从“地址映射”移到了“访问权限”。如果你也遇到类似问题建议先看一眼错误地址是否落在保留区。是的话大概率是权限或预留内存配置问题不是的话再往下查通道属性。3.2 第二层怀疑SMMU/IOMMU 的 StreamID 配错ARM SoC 里DMA 请求由主设备发起会带上一个 StreamIDSMMU 根据 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 channeldma0chan0 iterations100000 timeout2000 cat /sys/kernel/debug/dmatest/summary确认 summary 里test_result为 0CRC 错误计数也为 0。这里有个很容易忽略的细节dmatest默认会在所有注册通道上跑如果它自动去测被固件锁住的通道还是会报错所以需要用channel参数指定通道或者把 CPU 亲和性配好。然后在真实业务里做数据回环测试用 SPI 采集一批波形数据通过 DMA 搬运到内存再从内存搬回 SPI 发送上位机对比原始数据跑 10 万次循环看 CRC 是否有误码。最后做 24 小时压力测试。我写了个脚本每小时记录一次 dmesg 和 DMA 控制器状态寄存器重点关注NPRIV_ERR和SEC_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我第一件事就是比对寄存器快照而不是从头开始猜。这个方法不一定最优但确实能让你在复杂的嵌入式环境下少走很长的弯路。