
如果你的内核日志里刷出DMA_CHANNEL_NPRIV error这一行紧接着 DMA 传输就失败不用怀疑这不是随机故障而是 DMA 通道在权限或配置层面被卡住了。这种错误在嵌入式 Linux 开发里出现频率很高尤其做 BSP 适配、外设驱动移植、或者从原厂 SDK 往新板子搬代码的时候几乎每个人都会遇到一两次。我自己调试过的几个平台里不同厂商给这个报错起的名字还不太一样但实质都指向同一类问题驱动想用某条 DMA 通道但通道当前的状态不允许它这么用。下面我会以一次典型的调试过程为线索先讲清楚 DMA 控制器里“私有通道”到底是怎么回事再完整走一遍从设备树到内核驱动的通道申请链路最后给出可以照着做的排查步骤和修复方法。这里面的思路不挑平台不管你是用 Rockchip、NXP、ST 还是全志的芯片只要看到类似DMA_CHANNEL_NPRIV的报错都能用同一套逻辑去定位。1. 先搞清楚DMA 通道的“私有”属性到底是什么1.1 从硬件角度看 DMA 通道资源DMADirect Memory Access控制器的核心是一组硬件通道常见的有 8 条、16 条、甚至 32 条。每条通道可以独立配置源地址、目的地址、传输宽度、突发长度和传输方向。它存在的意义就是替 CPU 搬数据把 CPU 从重复的 memcpy 里解放出来。比如 SPI 接了一个 ADCADC 采完一批数据后通过 DMA 直接放到内存缓冲区CPU 只需要在处理完中断后去缓冲区里拿结果不用在中断里一个字节一个字节地读。但通道并不是“谁想用就能用”的。很多 SoC 在设计 DMA 控制器时会把一部分通道标记成私有private或特权privileged。这种标记从硬件层面就把通道的使用权限定在特定主机或特定安全域内。举个例子一个芯片里同时有 Cortex-A 核心和 Cortex-M 核心DMA 控制器两边都能访问为了避免 M 核的固件把 A 核的内存数据胡乱搬走硬件工程师会规定某几条通道只能由安全侧访问或者某几条通道只能由 M 核的固件使用。普通 Linux 驱动跑在 A 核的非安全侧去请求这些被保留的通道时硬件会直接拒绝驱动如果捕获到了这个状态就可能把错误信息打印成DMA_CHANNEL_NPRIV。NPRIV这个缩写字面上是 Not Privileged非特权。不同的芯片手册对它定义略有不同有些表示“当前访问是非特权模式”有些表示“这条通道不允许非特权访问”还有的干脆就是厂商 BSP 里自定义的一个状态位宏名。我甚至见过某个驱动里直接写了#define DMA_CHANNEL_NPRIV 0x01它用来标记一条通道当前处于非特权保护状态。大家记住一点就够了这个报错本质是访问权限检查没过不是数据搬错了也不是地址计算错。1.2 Linux DMA engine 如何管理通道Linux 内核里的 DMA engine 子系统抽象出了两套核心结构体一套描述控制器struct dma_device一套描述通道struct dma_chan。struct dma_device代表一个 DMA 控制器实例里面有一个通道链表保存着该控制器下所有可用的通道还有一个能力位图cap_mask表示这个控制器支持哪些能力比如DMA_SLAVE、DMA_CYCLIC、DMA_MEMCPY等等。struct dma_chan代表一条具体的 DMA 通道里面有通道编号、所属控制器、当前被谁使用、以及一个private字段。这个private字段是一个万能指针常被驱动用来挂一些自定义的权限标记或配置信息。客户端驱动比如 SPI、UART 的驱动申请通道时调用的核心 API 是dma_request_chan()老一点的内核里是dma_request_slave_channel()。在设备树模式下这个 API 会走到of_dma_request_slave_channel()解析设备树节点里的dmas和dma-names属性找到对应的 DMA 控制器然后调用控制器驱动注册的of_xlate回调把设备树里的通道参数翻译成一个具体的dma_chan。翻译成功后还会继续调用控制器驱动的device_alloc_chan_resources回调让控制器驱动为这条通道准备传输资源。私有权限检查就可以发生在of_xlate阶段也可以发生在device_alloc_chan_resources阶段。如果控制器驱动在检查时发现请求者不是特权实体返回一个-EPERM或-EACCES上层 API 就会让这次通道请求失败。具体到日志有的 BSP 驱动会在dev_err里直接打一条DMA_CHANNEL_NPRIV error有的则只默默返回错误码。所以这个错误名并不是 Linux 内核通用代码里的标准字符串而是厂商驱动里对某个错误分支的命名。1.3 为什么叫 NPRIV 而不是 PRIV很多初学者会有疑问报错明明说“非特权”为什么不是叫PRIV这一点确实容易绕晕。我个人的理解是DMA_CHANNEL_NPRIV这个名字更多是在描述“非特权访问”这件事本身而不是描述通道属性。驱动检测到当前请求来自非特权侧或者通道不允许非特权访问就把这个状态记录成NPRIV然后带着这个状态去打印错误。打个比方电梯里写着“员工专用”你按了楼层没反应门禁系统上报一条“non-employee access error”你看到的是“非员工”这个元凶而不是“员工”这个词。所以遇到这个报错不要纠结名字是 PRIV 还是 NPRIV重点是要去查这条通道被谁保护了、为什么保护。下一步我们顺着 DMA 通道请求链路看错误到底在哪里被抛出来。2. 从设备树到驱动的完整请求链路2.1 设备树里的 DMA 描述在设备树里一个需要使用 DMA 的外设节点通常会这样描述spi2 { dmas dma0 0, dma0 1; dma-names tx, rx; status okay; };这段的意思是SPI2 控制器一共有两条 DMA 通道一条命名成tx使用 dma0 控制器的第 0 条通道另一条命名成rx使用 dma0 控制器的第 1 条通道。dma0 0中的第一个参数是控制器 phandle后面的数字会被传给 dma0 的of_xlate回调由控制器驱动自己解释。对应的 DMA 控制器节点长这样dma0: dmaxxxxxxx { compatible vendor,xxx-dma; reg 0x0 0x10000000 0x0 0x1000; interrupts 0 30 IRQ_TYPE_LEVEL_HIGH; #dma-cells 1; dma-channels 8; clocks clk_dma; status okay; };#dma-cells 1表示dmas里每个描述由一个 cell 组成也就是一个参数。如果控制器驱动需要更复杂的描述比如 DMA 请求信号编号加上通道优先级就会定义成#dma-cells 2两边对齐即可。很多芯片的 DMA 控制器支持不同通道具备不同属性但又不想在设备树里写死所有细节时驱动会引入额外的属性比如dma-channel-mask、dma-channel-reserved这类非标准属性。如果你的 BSP 设备树里存在这些属性出现DMA_CHANNEL_NPRIV error时就要第一时间去查这些属性因为它们很可能就是把通道“私有化”的开关。2.2 内核里 DMA 通道的申请流程从客户端驱动发起请求到真正拿到通道完整的调用链大致如下dma_request_chan() - dma_request_chan_by_mask() - of_dma_request_slave_channel() - of_dma_xlate_by_chan_id() 或者 dma_spec-of_xlate() - dma_get_slave_channel() - device_alloc_chan_resources()当设备树里的dma-cells结构和某个控制器驱动匹配后控制器驱动的of_xlate回调会被调用。这个回调的职责是把设备树里的args参数转换成该控制器内部的一个dma_chan指针。举个例子static struct dma_chan *xxx_dma_of_xlate(struct of_phandle_args *dma_spec, struct of_dma *ofdma) { struct xxx_dma_device *d ofdma-of_dma_data; u32 chan_id dma_spec-args[0]; if (chan_id d-dma_dev.chancnt) { dev_err(d-dev, invalid channel id: %u\n, chan_id); return NULL; } if (d-chans[chan_id].flags DMA_CHANNEL_NPRIV) { dev_err(d-dev, DMA_CHANNEL_NPRIV error: channel %u is reserved\n, chan_id); return NULL; } return dma_get_slave_channel(d-chans[chan_id].chan); }这段代码是我根据几个平台驱动整理出来的通用形态不是某一个内核版本的源码。它的逻辑很清晰先校验通道号是否合法再检查该通道是否设置了私有权标志如果设置了就直接返回失败。dma_get_slave_channel()这个函数内部还会做一步确认比如检查通道是否已经被其他客户端占用如果被占用也会失败。真正开始分配通道资源时进入device_alloc_chan_resources回调。控制器驱动在这里会做一些准备工作例如分配描述符内存、清零硬件寄存器状态、申请中断。如果之前of_xlate阶段没有检查通道权限这里也是一个检查的合适位置。2.3 错误到底在哪里被抛出你可以用两种方式定位错误抛出的位置。第一种是看 dmesg 上下文。在出现DMA_CHANNEL_NPRIV error的前后几行通常有更详细的信息比如设备树节点名称、DMA 控制器名称、通道编号[ 12.345678] xxx-dma 10000000.dma: DMA_CHANNEL_NPRIV error: channel 3 access denied [ 12.345690] dmaengine: dma_request_chan: failed to get channel: -13-13 就是-EPERM表示操作权限不允许。这个错误码能帮你做初步判断。如果返回的是 -16-EBUSY说明通道被占用如果返回 -22-EINVAL说明参数不对如果返回 -19-ENODEV说明设备不存在或者通道号越界。第二种是直接在源码里搜字符串。拿到 BSP 的源码包后执行grep -rn DMA_CHANNEL_NPRIV --include*.c --include*.h找到所有引用点看它是通过什么条件进入错误分支的。这是最直接、最不会猜错的方法。很多厂商的 BSP 驱动喜欢把错误码含义用宏包装一层打印出来的字符串往往和宏名一致一搜就能找到源头。3. 一次完整的排查与修复实战3.1 场景复现与日志定位我在某个项目里遇到过类似情况。主控芯片内部有一个 SPI 控制器接了一颗工业 ADC数据量比较大所以必须开 DMA。设备树里配置了 SPI2 的tx和rx通道但内核启动后SPI2 的驱动一直 probe 失败dmesg 里反复出现DMA_CHANNEL_NPRIV error。第一步先把日志完整抓下来dmesg | grep -iE dma|spi2|npriv输出显示请求通道 0 失败。接着我看设备树spi2 { dmas dma0 0, dma0 1; dma-names tx, rx; };从日志和配置来看tx请求的是 dma0 的第 0 条通道。但我在 DMA 控制器的驱动源码里发现它把第 0 到第 3 条通道都通过一个数组标记成了保留通道只有第 4 到第 7 条通道允许普通 Linux 驱动使用。问题一下子就清楚了不是驱动写错而是我选的通道刚好落在保留区间。3.2 三路并行排查遇到这类问题不要只盯着一个方向查。我习惯同时从三个方向下手哪个先找到证据就从哪里突破。第一路设备树检查。用dtc把当前系统实际生效的设备树反编译出来确认设备树没有因为 overlay 或者 bootloader 修改而和你编辑的源码不一致dtc -I fs -O dts /sys/firmware/devicetree/base/ -o extract.dts然后看 SPI2 节点里的dmas属性到底写的是什么。很多时候你以为自己改了设备树但实际 boot 用的是另一个 dtb这种低级错误在项目里非常常见。第二路内核配置检查。确认 DMA 控制器驱动真的编译进内核或者模块加载成功了。可以在/sys/class/dma/下看到系统中的 DMA 通道ls -l /sys/class/dma/如果目录是空的或者找不到对应控制器的通道那问题就不是私有权限而是 DMA 控制器根本没 probe 成功。这时候应该去查 DMA 控制器的时钟、电源、复位信号而不是纠结NPRIV错误名。第三路驱动代码检查。定位DMA_CHANNEL_NPRIV宏定义的引用位置看它是在probe时通过寄存器读取设置的还是通过设备树属性设置的。如果是寄存器读取还要去芯片手册里找对应寄存器的权限位说明确认是不是安全配置把这些通道锁住了。3.3 修复两种常见改法根据上面的定位修复通常有两种方案。方案 A修改设备树换一条可用的普通通道。这个方法最干净风险最小。我的平台里 DMA 控制器提供 8 条通道其中第 0 到第 3 条是保留的第 4 到第 7 条可以给普通外设用。于是我把 SPI2 的 dmas 改成了spi2 { dmas dma0 4, dma0 5; dma-names tx, rx; };然后重新编译设备树覆盖掉 boot 分区的 dtb重启验证。DMA 请求成功SPI 通信正常。方案 B如果硬件上所有普通通道都已经用完确实需要用保留通道那么需要评估这个通道是否真的可以被非安全侧使用。有些芯片的保留通道只是软件策略性质Bootloader 里会配置 DMA 通道的“安全位”这部分安全位是可以重新配置的。如果确认开发板上没有安全固件用到这些通道可以通过修改 DMA 控制器驱动里的通道注册逻辑把这个通道从保留列表里去掉。比如驱动里原来这么写if (i RESERVED_CHANNEL_COUNT) { ch-flags | DMA_CHANNEL_NPRIV; }那就看是否可以把RESERVED_CHANNEL_COUNT调小或者通过设备树属性来控制保留通道的数量。但我不建议直接硬编码去掉这个标志因为生产固件里如果存在安全侧功能强行放开保留通道会带来严重的内存取越权风险甚至导致系统崩溃。改代码之前一定先确认安全和硬件隔离要求允许。3.4 验证结果修改完设备树后重新启动检查 dmesgdmesg | grep -i dma可以看到 SPI2 成功请求到了tx和rx通道。然后用实际业务跑一遍数据传输比如通过 SPI 连续读 ADC 数据 10 分钟确认没有数据传输错误、没有 DMA 超时中断。如果传输期间系统变卡顿或者 dmesg 出现 DMA 传输错误要回头检查通道冲突。我那次修改后SPI 吞吐率从原来 CPU 轮询模式下的不到一半提升到了硬件 DMA 模式的满带宽问题彻底解决。4. 常见问题与排查技巧实录4.1 问题速查表我把平时遇到过的DMA_CHANNEL_NPRIV error相关情况整理成了一张表方便大家对照排查症状可能原因解决办法请求特定通道时报错其他通道正常该通道被保留或安全配置锁定换通道或调整设备树确认安全固件是否占用任意通道请求都报错DMA 控制器 probe 失败或驱动没有正确注册检查时钟、电源、复位、dts 节点状态错误码是 -EPERM权限不足访问被拒绝检查通道私有标志、安全配置错误码是 -EBUSY通道已被其他驱动占用查找占用者排查驱动加载顺序或通道冲突错误码是 -ENODEV设备树通道号无效或控制器不存在核对 dmas 属性和 #dma-cells申请成功但传输无数据方向配置错误、地址未映射、IOMMU 问题检查 dma_slave_config 参数和内存区域表里的错误码对应关系在不同 Linux 内核版本和 BSP 驱动里可能略有差异但大方向一致。看到具体错误码后先判断是哪一类再决定下一步动作。4.2 独家避坑技巧第一遇到未知错误名第一件事是搜源码。直接在工程里全局搜DMA_CHANNEL_NPRIV找到它的定义和打印点。很多时候 BSP 驱动的打印里会带__func__或dev_err能直接看到是哪个文件哪一行抛出来的。这个动作可以帮你节省至少半小时的猜测时间。第二用 ftrace 跟踪dma_request_chan的调用链。在调试阶段可以在内核启动参数里加trace_eventfunction_graph或者在运行时挂上 ftraceecho dma_request_chan /sys/kernel/debug/tracing/set_graph_function echo function_graph /sys/kernel/debug/tracing/current_tracer cat /sys/kernel/debug/tracing/trace这样能看到是哪一层调用触发了失败以及返回路径如何。不过 ftrace 输出的信息量很大建议先把不必要的追踪关掉集中看目标函数。第三验证实际生效的设备树。不要只看你自己修改的.dts源文件因为 Bootloader 可能对设备树做了二次修改。用dtc反编译当前运行的设备树再和源文件对比能避免很多“明明改了为什么没生效”的问题。第四检查 DMA 通道之前先确认 DMA 控制器的基础环境。比如时钟没开时读寄存器可能全返回 0xFF驱动会把异常状态误判成权限问题。先把控制器节点拉起来确认寄存器可读、中断正常再查通道权限。4.3 一个容易被忽略的细节DMA 通道的复用与并发DMA 通道是稀缺资源一个dma_chan在同一时刻只能被一个客户端持有。两个不同驱动同时去请求同一条通道后一个请求会失败典型错误码是-EBUSY。但在某些 BSP 驱动的实现里这个失败状态会被统一包装成自定义错误甚至打印成DMA_CHANNEL_NPRIV error。这就有迷惑性了你可能以为是权限问题实际上是被别的驱动占了。怎么快速验证看dmesg里有没有其他驱动在同一时间段请求过 DMA 通道或者通过sysfs查看通道的in_use状态具体路径因内核版本而异。也可以在客户端驱动里临时加一段打印struct dma_chan *chan dma_request_chan(pdev-dev, tx); if (IS_ERR(chan)) { dev_err(pdev-dev, dma request failed: %ld\n, PTR_ERR(chan)); return PTR_ERR(chan); }PTR_ERR打印出来的错误码会清清楚楚告诉你是权限、占用还是参数问题这一步能把排查范围缩小一大半。提示看到DMA_CHANNEL_NPRIV error时先看打印点发生在哪个阶段。如果是系统启动阶段多半是设备树或驱动初始化顺序问题如果是运行阶段偶尔出现多半是资源竞争或通道复用问题。我在实际调试中踩过不少次类似的坑最大的体会是DMA 通道报错九成不是驱动算法问题而是设备树里通道选得不对、或者通道被硬件安全策略锁住了。真正把DMA_CHANNEL_NPRIV这个宏名写进日志的驱动其实已经帮你做了大量检查顺着它背后的判断条件去追往往比你想的要快。最后再分享一个小技巧以后遇到任何“读不懂的内核错误字符串”先别问搜索引擎先在本地源码里搜一遍。很多看起来高深的错误宏其实就是厂商驱动里一个很简单判断分支的别名找到它问题就解决了一半。