ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RK3568 NPU驱动深度排查:从内核模块到设备树的完整解决路径

RK3568 NPU驱动深度排查:从内核模块到设备树的完整解决路径

在嵌入式设备上跑AI推理,我相信很多朋友最先碰到的往往不是模型精度不够,而是NPU根本不工作。就拿RK3568这块板子来说,它内置的NPU算力不大,但胜在能跑常见的分类、检测模型,功耗又低,非常适合放在边缘设备上做视频分析、OCR预识别或者简单的人形检测。可每次拿到一块新板子,大概率都会在NPU驱动这个环节卡一下:要么rknpu.ko加载失败,要么/dev/rknpu节点不存在,要么驱动装上了但用户态rknn_init一调用就返回-5。这篇文章就把我在RK3568上处理NPU驱动问题的完整思路、命令和解决办法整理出来,从内核态驱动、设备树、用户态运行时三个层面逐个拆,给正在调板的你一个可以直接照着做的排查路径。

1. RK3568 NPU驱动的整体链路拆解

1.1 三层结构:硬件、内核模块、用户态运行时

先说清楚RK3568的NPU是怎么被用起来的。它并不是一个简单的IP核,光靠CPU访问寄存器是跑不了神经网络的。整套东西分三层:

最底层的NPU硬件本身,在RK3568里占据了独立的内存映射地址空间,有自己的时钟、电源域、IOMMU,甚至还有独立的SRAM区域。它需要内核态程序去管理,这一步交给内核驱动模块rknpu.ko。

中间一层就是rknpu.ko。它在内核里注册成一个misc设备,对外暴露一个设备节点,一般是/dev/rknpu。这个模块的工作包括:控制NPU的电源域和时钟、配置IOMMU做内存地址映射、把NPU从复位状态释放出来、以及接收用户态发过来的消息。可以把它理解成一座桥,桥的一头是NPU硬件,另一头是用户态程序。

最上面一层是用户态运行时库,也就是大家熟悉的RKNN系列工具链里带的librknnrt.so。你的应用程序调用rknn_init、rknn_run这类API时,实际是运行时库通过open("/dev/rknpu")和ioctl与内核驱动打交道,把计算图下发到NPU上执行。

很多人会混淆“驱动”和“运行时库”。如果只是替换librknnrt.so,那不算解决驱动问题,只是换了用户态一端;真正的“NPU驱动”一般指rknpu.ko这个内核模块,以及和设备树里NPU节点相关的电源、内存配置。换句话说,驱动解决的是“设备能不能被系统识别、能不能被访问”的问题,运行时库解决的是“模型能不能在设备上跑起来”的问题。

1.2 问题分层:先定位再动手,别一头扎进代码里

围绕这个结构,我把平时遇到的NPU问题粗暴地分成四类:

  • 驱动模块层:rknpu.ko没有加载,或者加载时报错,比如version magic mismatch、Unknown symbol。
  • 设备节点层:/dev/rknpu不存在,或者权限不对,导致用户态打不开。
  • 运行时库层:驱动装好了,节点也在,但librknnrt.so与驱动版本不匹配,或者依赖的库缺失,rknn_init失败。
  • 资源限制层:驱动和库都正常,但内存不够、CMA分配失败、电源域没起来,导致NPU运行报错或者性能异常。

实际排查时我的顺序是:先看内核模块在不在,再看设备节点在不在,然后看内核日志有没有报错,最后才轮到用户态问题。如果你一上来就盯着应用程序的报错代码看,很容易绕弯路。因为rknn_init返回-5这个错误,根因可能是驱动没加载,也可能是权限不对,还可能是运行时库和驱动版本不一致,不先看内核态,很难判断。

2. 环境排查:驱动装没装、能不能用的五个检查命令

2.1 检查内核版本与驱动模块加载状态

拿到一块RK3568板子,先别急着编译任何东西,先用三条命令确认当前环境:

uname -a lsmod | grep rknpu find /lib/modules/$(uname -r) -name "*rknpu*" 2>/dev/null

uname -a相当重要,后面的所有版本匹配都要以它为准。比如内核版本是4.19.219,那驱动模块必须是用4.19.219同版本内核源码编译出来的,否则modprobe大概率会报version magic不匹配。

lsmod如果输出了rknpu,说明模块已经加载。如果没有任何输出,也先别慌,可能是模块文件不在/lib/modules目录下,或者depmod没有生成依赖索引。find命令就是用来确认模块文件存不存在的。这里有个坑:很多SDK会把内核模块放在板卡根文件系统的/lib/modules目录里,但有些精简rootfs只放了驱动,没放modules.dep,导致modprobe找不到设备名。遇到这个情况,可以用depmod -a重新生成依赖。

2.2 确认NPU设备节点和内核日志

模块加载之后,还要确认设备节点有没有自动创建:

ls -l /dev/rknpu*

正常情况应该有一个主设备号10、次设备号120的字符设备。如果没有,最常见的原因是驱动注册misc设备失败,或者根文件系统里的udev/mdev规则没有覆盖到这个设备。

此时要立刻刷内核日志:

dmesg | grep -iE "rknpu|npu"

看到类似“rknpu: probe success”或者“rk-npu: registered”这样的日志,说明驱动加载和设备注册都正常。如果什么都没看到,先查模块是否真的加载了;如果有报错,比如“version magic”“Unknown symbol”或者“Failed to register misc device”,记下来,后面逐一拆解。

另一个容易忽视的点是权限。很多开发阶段直接用root用户跑,没问题;但如果你用普通用户跑推理程序,/dev/rknpu没有0666权限就会出现Permission denied。这种问题用udev规则解决,在/etc/udev/rules.d/里加一条:

KERNEL=="rknpu", MODE="0666"

2.3 用户态运行库版本核对

确认内核态没毛病之后,再到应用目录下检查运行时库:

ldd ./your_infer_app ls -l /usr/lib/librknnrt.so strings /usr/lib/librknnrt.so | grep -iE "version|rknn" | head -20

重点看两件事:第一,你的程序链接到的librknnrt.so是不是实际存在于设备上;第二,库版本是否和驱动配套。官方RKNN套件的1.x版本和2.x版本在设备端运行时库的接口上差异很大,驱动和库混搭会出现“编译时能过、运行时莫名失败”的诡异问题。

我自己的经验是:拿到板子之后,先把板厂或者原厂SDK里自带的librknnrt.so原封不动地放到/usr/lib里,不要因为自己编译了新版教程里的库就去替换,除非你已经确认内核驱动也能跟上新版接口。驱动和运行时库,永远是成对升级的。

2.4 设备树里NPU节点是否存在

最后一步检查设备树,这一步经常被跳过去,但很多诡异问题都是在这埋下的:

ls /proc/device-tree | grep -i npu cat /proc/device-tree/npu@fde40000/status 2>/dev/null

设备树节点里如果status字段是“disabled”,或者整个npu节点都不存在,那驱动probe时找不到对应platform_device,内核模块就会静默成功加载,但不创建设备节点。出现这种情况,不要纠结驱动代码,直接检查板级dts文件,确认npu节点、电源域、IOMMU都已经使能。

3. 解决驱动加载失败的三条主流路径

3.1 最稳妥的方案:使用SDK配套的预编译驱动

如果手上有完整的Linux SDK,里面带kernel源码和预编译镜像,那最稳的办法是直接用SDK里的内核和模块,不要自己东拼西凑。原厂SDK发布时,内核、设备树、驱动模块、用户态工具链是同一套代码里出来的,版本锁定,编译选项一致,基本不会出现vermagic冲突。

操作思路大概是:

  1. 把SDK里编译好的boot.img或者内核镜像、dtb文件烧录到板卡。
  2. 把SDK rootfs里/lib/modules/整个目录同步到板子根文件系统。
  3. 重启后执行depmod -a,再modprobe rknpu。

这个方案没什么技术含量,但它能最大程度排除人为干扰。如果你拿到一块别人调过的板子,不知道之前刷过什么内核、什么驱动,不要犹豫,先重刷整套镜像,这是最快的复位手段。

我在实际项目中吃过大亏:某块板卡为了修别的外设bug,同事手动替换了设备树,结果NPU消失了两天。最后对比SDK原始dtb才发现npu节点的power-domains被误删了。所以每次动设备树之前,先备份原始dtb,养成习惯。

3.2 自己编译内核模块:从源码到加载全流程

当板卡的存量内核版本和SDK不同,或者你需要给自定义内核集成NPU驱动时,就得自己编译rknpu模块。这里有几个必须满足的前置条件:

  • 运行板卡的内核版本与编译设备上用的内核源码版本严格一致。
  • 交叉编译工具链能够编译该内核版本,一般用aarch64-linux-gnu-gcc。
  • 内核源码里的配置项CONFIG_ROCKCHIP_RKNPU要确认,另外相关的IOMMU、devfreq、DMA CMA能力都要开启。

编译时可以只编这个模块,不需要编整个内核镜像:

export ARCH=arm64 export CROSS_COMPILE=aarch64-linux-gnu- make menuconfig # 确认CONFIG_ROCKCHIP_RKNPU为y或m # 只编译模块 make M=drivers/rknpu modules

编译完成之后,把rknpu.ko拷贝到目标板:

scp drivers/rknpu/rknpu.ko root@<板卡IP>:/lib/modules/$(uname -r)/extra/ ssh root@<板卡IP> "depmod -a && modprobe rknpu"

这里想强调的是vermagic问题。每个内核模块都带一个版本魔法,由内核版本、编译时SMP/PREEMPT等配置决定。板卡上运行的内核和编译模块的内核只要有一点差异,比如打开了PREEMPT_RT,模块就加载不起来。最快排查方法是:

modinfo rknpu.ko | grep vermagic cat /proc/version

两者里的字符串必须一致。如果实在不想因为这个原因反复调,可以把内核重新编译成和模块匹配的版本,但这属于改内核了,工作量会大许多。更聪明的做法是直接使用板卡运行内核对应的内核源码,不要用MAINLINE内核去编厂商SDK的驱动。

3.3 DKMS:应对内核升级后的驱动失效

还有一种场景,你的板卡升级rootfs之后内核也随之升级,比如从4.19.x升到4.19.y,原来手动扔到/lib/modules里的rknpu.ko在新内核下彻底失效。这时候可以引入DKMS机制,让系统在新内核安装时自动重新编译驱动模块。

在编译机上创建/usr/src/rknpu-2.0/目录,放入驱动源码和dkms.conf:

PACKAGE_NAME="rknpu" PACKAGE_VERSION="2.0" BUILT_MODULE_NAME[0]="rknpu" DEST_MODULE_LOCATION[0]="/kernel/drivers/rknpu" AUTOINSTALL="yes"

然后执行:

dkms add -m rknpu -v 2.0 dkms build -m rknpu -v 2.0 dkms install -m rknpu -v 2.0

DKMS会在当前内核编译一次驱动并安装到/lib/modules/$(uname -r)/updates/dkms/下,还要运行depmod。之后每换一个内核,DKMS的hook都会自动对新内核重新编译。这个方案适合长期维护的板卡,尤其适合那些定期更新内核安全补丁的ARM64设备。

不过DKMS也不是万能的,前提是运行内核里有对应的内核头文件包,而且交叉编译环境能正确匹配。如果你的内核是板厂魔改过的不标准内核,DKMS build那一步可能还是会失败,这时候老老实实用SDK路线更靠谱。

3.4 设备树补充:电源域、IOMMU与内存预留

RK3568的NPU驱动能否probe成功,和设备树节点的完整程度关系极大。常见的dts片段会包含:

  • 寄存器地址与中断:比如reg = <0x0 0xfde40000 0x0 0x40000>。
  • 电源域:引用power controller里的npu对应电源域。
  • 时钟:引用npu core和bus时钟。
  • 内存预留:在reserved-memory节点里给NPU预留一段连续物理内存,比如no-map属性。
  • IOMMU节点:让NPU能通过IOMMU访问外部内存。

如果只缺了最基础的中断、时钟,驱动通常会在probe时直接返回错误;如果缺了内存预留或IOMMU,可能出现insmod成功但NPU异常跑不动或者内存分配失败。

检查设备树时,我习惯用dtc反编译现有dtb:

dtc -I fs -O dts /proc/device-tree -o current.dts grep -n "npu" current.dts

注意,/proc/device-tree是运行时展开的fdt,uboot实际上也可能对dtb做过修改。如果发现npu节点status是disabled,可以直接在dts源码里改成okay,重新编译dtb烧录。如果发现整个节点都不存在,那就得回到SDK的板级dts找基础dtsi,确认是否 A 引用了公共的rk3568-npu.dtsi。

4. 编译安装和验证的完整实操记录

4.1 一次真实的驱动排障过程

用一个典型场景来走一遍完整流程。假设手里有一块RK3568板卡,刷了一个精简版Debian系统,内核是4.19.219,但SDK内核源码也是4.19.219,驱动源码在SDK kernel/drivers/rknpu目录下。

先做状态登记:

uname -r # 输出 4.19.219 lsmod | grep rknpu # 无输出 ls -l /dev/rknpu* # 无此类设备文件 dmesg | grep -i rknpu # 无输出

这说明要么模块没装,要么装了也没加载成功。再查模块文件:

find /lib/modules/$(uname -r) -name "*rknpu*"

没有任何结果,代表根文件系统里就没有驱动模块。这种情况下,别怨系统,老实去SDK里编译。

4.2 交叉编译并安装模块

我的编译主机是x86_64 Ubuntu环境,工具链用的是aarch64-linux-gnu-gcc。进入SDK的kernel目录:

export ARCH=arm64 export CROSS_COMPILE=aarch64-linux-gnu- # 确认config cat .config | grep RKNPU # CONFIG_ROCKCHIP_RKNPU=m

看到CONFIG_ROCKCHIP_RKNPU=m,说明驱动编译为模块,符合预期。然后单独编译这个目录下的模块:

make M=drivers/rknpu modules ls drivers/rknpu/rknpu.ko

编译产出rknpu.ko后,在板上创建标准模块目录,拷贝并加载:

mkdir -p /lib/modules/$(uname -r)/extra cp rknpu.ko /lib/modules/$(uname -r)/extra/ depmod -a modprobe rknpu

如果运气好,dmesg会多出驱动probe的日志:

dmesg | grep -i rknpu # rknpu: ver=1.0 # rknpu: 0x10000000,0x10000000 # rknpu: regulator_enable done # rknpu: clk_enable done # rknpu: rknpu probe success

再查看设备节点:

ls -l /dev/rknpu # crw-rw---- 1 root root 10, 120 Jan 1 08:00 /dev/rknpu

到这里,驱动层面的问题已经解决了。

4.3 让模块开机自动加载

开发阶段手动modprobe没问题,但产品化之后必须开机自动加载。最简单的办法:

echo rknpu > /etc/modules-load.d/rknpu.conf

这个文件里的内容是一个模块名,initramfs或systemd会在启动阶段自动加载。如果你用的是传统init脚本,记得确保depmod已经生成依赖,否则启动时仍然找不到模块。

4.4 用推理程序验证驱动是否全程可用

驱动加载成功不意味着整条链路没问题,还得跑一个简单的推理demo验证。我用官方RKNN套件里的分类示例,把模型文件和图片放到板子上,然后执行:

./rknn_classify_demo model.rknn test.jpg

第一次跑起来的时候,看到模型加载耗时几十毫秒,推理耗时十几毫秒,心里才踏实。如果在这里碰到rknn_init返回-5,通常不是驱动问题,而是库里没有打开/dev/rknpu的权限,或者库与驱动不匹配。可以用strace看一下:

strace -f -e trace=openat ./rknn_classify_demo model.rknn test.jpg 2>&1 | grep rknpu

能找到open("/dev/rknpu")失败的记录,就说明权限或者节点路径不对,按2.2的udev规则处理。

5. 常见问题速查表与避坑心得

5.1 问题速查表

我在不同板卡上碰到过的问题基本都集中在下面这张表里。每一行里的排查方法都是实际用过的,可以直接照抄。

问题现象可能原因排查与解决
modprobe rknpu提示 module not found模块不在/lib/modules目录,或depmod索引未生成find检查文件,depmod -a后重试
insmod rknpu.ko报version magic mismatch模块与运行内核编译版本不一致modinfo vermagic与/proc/version对比,换配套源码重编
insmod报Unknown symbol驱动依赖的内核符号缺失,或依赖模块未加载dmesg看符号名,先加载依赖模块或重编内核
/dev/rknpu设备节点不存在驱动注册misc设备失败、设备树节点disableddmesg查misc注册失败,检查设备树npu节点状态
/dev/rknpu存在但权限不足用户态非root访问被拒加udev规则MODE=0666
rknn_init返回-5驱动未加载、权限不足、运行时库不匹配、节点打不开用strace跟踪openat,检查驱动和库版本
rknn_init成功但rknn_run报内存错误CMA连续内存不足或IOMMU未配置调整cmdline的cma大小,dmesg查NPU内存申请日志
驱动加载成功但NPU性能极低频率锁在最低档、散热降频、电源域异常查看/sys/class/devfreq下NPU频率,检查供电和温度
内核升级后驱动失效模块与新版内核不匹配使用DKMS,或直接使用同一版本SDK重新编译模块

5.2 避坑心得

先说版本匹配。我有一次为了一个很小的算子支持,把librknnrt.so换成了较新的版本,结果驱动还是旧的,rknn_init直接报“version mismatch”。后来老老实实把整个SDK里配套的驱动和库一起换,一次就过了。这件事给我的教训是:在RK3568这种半封闭的NPU生态里,没必要追新,要追求版本一致性。芯片原厂发布SDK时,内部所有版本已经经过验证,你单独升级任何一个环节都是在给自己挖坑。

再看内存问题。RK3568的NPU对内存连续性有要求,尤其是在跑稍微大一些的检测模型时。SDK默认cmdline里可能已经留了CMA区域,但如果你的板卡rootfs修改过cmdline,或者配置了较大的GPU显存内存,NPU就可能在运行中途报分配失败。解决办法是给kernel cmdline加上cma大小:

cma=128M

但修改内核启动参数一定要谨慎,加太大容易影响系统其他内存使用,加太小又不够NPU模型切分。一个比较稳的调整建议是128M起步,通过dmesg里NPU的拿内存日志反复试,找到稳定的边界值。

然后是设备树。很多开发者拿到新板卡,第一件事就是刷一个通用Ubuntu镜像,然后发现NPU怎么都不工作。你仔细查dmesg,发现驱动压根没有probe,再一看/proc/device-tree,npu节点status是disabled甚至不存在。这不是驱动问题,是设备树和内核镜像根本不配套。所以每次刷系统时,内核镜像、dtb、rknpu模块、librknnrt.so四个东西一定是同一套SDK编译产物,这是最快的排障方式。

最后想分享一个小技巧:在长期维护的多块板卡上,我会写一个简单的诊断脚本,放到/usr/local/sbin/下,名字就叫rknpu_check.sh。内容就是前面2.1到2.4命令的组合:

#!/bin/bash echo "== kernel ==" uname -r echo "== module ==" lsmod | grep rknpu || echo "module not loaded" echo "== node ==" ls -l /dev/rknpu 2>/dev/null || echo "node missing" echo "== dmesg ==" dmesg | grep -iE "rknpu|npu" | tail -20 echo "== library ==" ls -l /usr/lib/librknnrt.so 2>/dev/null || echo "runtime lib missing"

每次遇到问题先把脚本跑一遍,输出的信息已经能覆盖80%的驱动故障。这个脚本帮我在好几块板子上快速锁定了问题,省掉了大量反复敲命令的时间。

回到开头说的那个场景:RK3568的NPU驱动问题,绝大多数都不是真的“硬件坏了”,而是版本、设备树、模块路径、权限这些系统工程问题。只要把链路拆开,一层一层验证,解决起来其实很快。希望这篇整理能帮你少走弯路,把更多时间花在NPU应用本身。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进