RV1126内核驱动入门:交叉编译到烧写的完整实战
1. 为什么零基础入门建议从RV1126内核驱动开始RV1126内核驱动编译与烧写听起来像是需要啃半年内核源码才能碰的东西但实际拆开看就是一套可以照着做的流程。我第一次接触这块芯片的时候连交叉编译工具链都没配明白照着网上的文章一步步试最后总算把第一个hello.ko跑起来。这篇文章就是想按我实测过的路径把这个过程完整讲清楚适合完全零基础、但有一点点C语言和Linux命令行常识的读者。先说为什么推荐用RV1126入门而不是直接在PC机上学。RV1126属于带NPU和ISP的视觉处理芯片广泛用在智能摄像头、边缘计算盒子这类产品上。它的SDK通常已经帮你整理好了内核源码、交叉编译工具链、文件系统和烧写脚本你不需要从零搭建整个系统只需要把注意力集中在内核驱动本身。这个特性对新手极其友好你遇到问题的时候能区分问题是出在环境配置还是驱动代码而不是所有环节都糊在一起。另外芯片架构是ARM Cortex-A732位内核版本基于Linux 4.x/5.x和大多数嵌入式教材、网上的驱动资料高度匹配。你在这套平台上学到的知识换到其他ARM Linux板子比如树莓派、各种国产开发板逻辑几乎完全一样。所以我认为这是性价比很高的入门路径。那到底需要准备什么我按最低配置列一下一台x86主机装Ubuntu 20.04或18.04 LTS最好有8GB以上内存磁盘空闲空间尽量留大一些内核编译经常需要20-30GB临时空间。开发板和相关电源、串口线、USB烧写线。串口是必须的因为看日志、观察模块加载输出都靠它。SDK源码包一般解压后里面包含kernel目录也就是内核源码树。一个能联网的终端用于安装依赖包和交叉编译工具链。这套东西准备好后续所有操作都在Linux终端里完成。如果你连终端都没用过建议先花半天熟悉一下cd、ls、make、export这些基础命令否则后面会手忙脚乱。整个流程可以压缩成四句话配置交叉编译环境、编写驱动源码、编译生成.ko模块或boot镜像、通过烧写工具部署到开发板验证。接下来我按实际操作的顺序一步步展开。2. 搭建交叉编译环境工具链版本与目录结构里的门道2.1 交叉编译工具链选择与安装交叉编译工具链是这门课的第一个大坑。你在PC上写代码用PC自带gcc编译出来的程序只能在PC上跑而开发板是ARM架构必须用能生成ARM指令的编译器。RV1126的SDK里通常会预置一份工具链路径一般在toolchain/目录下或者SDK初始化的时候已经帮你装好了。我的建议是第一优先使用SDK自带工具链不要自己另外下载因为内核编译对编译器版本非常敏感。如果SDK里没带也可以自己从发行版软件源安装。这里有个关键点RV1126是32位ARM平台所以要装arm-linux-gnueabihf工具链而不是aarch64-linux-gnu。很多新手看到厂商文档里有“armhf”字样却装成了aarch64结果编译内核时出现一堆无法识别的选项。可以这样确认arm-linux-gnueabihf-gcc --version如果命令能输出版本号说明工具链已经可用。否则需要手动安装sudo apt update sudo apt install gcc-arm-linux-gnueabihf这里要注意如果系统是Ubuntu 20.04及以上软件源里的交叉编译器版本可能偏新。对于Linux 4.19/Cortex-A7这个组合来说gcc 9/10问题不大但如果编译时报内联汇编错误或unknown option优先回退到SDK自带工具链。2.2 内核源码树结构认知解压SDK后你会看到一堆目录别慌。我们需要关注的只有kernel目录下面的部分典型结构kernel/ ├── arch/ │ └── arm/ │ ├── configs/ # 平台默认配置文件 │ ├── boot/ │ └── mach-xxx/ # 板级支持代码 ├── drivers/ │ └── xxx # 各类驱动源码 ├── include/ ├── scripts/ ├── Makefile └── Kconfigarch/arm/configs下通常有对应开发板的默认配置文件文件名带平台型号比如某型号的defconfig。编译内核的第一步就是把平台默认配置加载成当前配置。这步相当于告诉内核“你需要支持哪些功能和驱动”配置文件就是一张功能清单。drivers目录下面按类型分了很多子目录gpio、i2c、spi、net、video等等。我们接下来要添加的hello驱动既可以直接放在某个子目录里也可以单独放在内核源码树外的目录中用模块方式编译。新手建议从独立目录开始不影响内核原有代码。2.3 用一次完整编译验证环境是否正常环境配置完不能拍脑袋说没问题一定要做一次完整的原厂内核编译。这一步有两个目的一是验证工具链和源码能配合工作二是生成后面编译模块时需要的“内核编译产物”。因为外部模块编译时必须依赖内核源码树里已经生成好的头文件和符号表没有这些你会遇到autoconf.h缺失之类的报错。在kernel目录下执行export ARCHarm export CROSS_COMPILEarm-linux-gnueabihf- make xxx_defconfig # 用SDK文档里对应的配置名 make -j4 zImage-j4表示用4线程并行编译速度会快不少。这里如果报错先检查依赖库是否装全。常见缺少的包有sudo apt install libncurses5-dev libssl-dev bc flex bison首次编译内核可能需要20分钟到1小时视机器性能而定。编译成功后在arch/arm/boot/下能看到zImage文件这时候环境才算真正通了。我没见过谁能跳过这步直接编模块成功的所以这一小时别省。3. 第一个驱动模块从hello代码到ko文件的全过程3.1 驱动源码骨架编写驱动模块虽然挂在Linux内核里但本质上也是一段C程序只不过它的入口不是main()而是module_init和module_exit指定的函数。内核模块的加载入口主要负责初始化资源卸载入口负责释放。我们先写一个最简版本先不要管设备树、硬件寄存器这些复杂概念。新建一个目录这里假设路径是/home/用户名/hello_driver在里面创建hello.c#include linux/init.h #include linux/module.h #include linux/kernel.h static int __init hello_init(void) { printk(KERN_INFO Hello RV1126 kernel module\n); return 0; } static void __exit hello_exit(void) { printk(KERN_INFO Goodbye RV1126 kernel module\n); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE(GPL); MODULE_DESCRIPTION(A simple hello module);printk就是内核空间的打印函数作用类似用户空间的printf。这里用KERN_INFO表示普通信息级别后面在dmesg里能看到输出。MODULE_LICENSE(GPL)不是可有可无的没有它编译会报警告而且某些内核功能可能对非GPL模块拒绝加载。__init和__exit是两个宏作用是告诉内核这些函数只在初始化/退出时存在正常运行时可以释放节省内存。3.2 Makefile的组成与变量解析模块能编译出来靠的是Makefile里那些看起来绕的参数。我的做法是复制一份通用模板然后修改路径即可。下面这份是经过多次试验的obj-m : hello.o KERNEL_DIR : /path/to/kernel PWD : $(shell pwd) all: $(MAKE) -C $(KERNEL_DIR) M$(PWD) ARCHarm CROSS_COMPILEarm-linux-gnueabihf- modules clean: $(MAKE) -C $(KERNEL_DIR) M$(PWD) ARCHarm CROSS_COMPILEarm-linux-gnueabihf- cleanobj-m : hello.o表示我们希望生成一个名为hello的内核模块后面连接到hello.c。中间的-C选项是让make先切换到内核源码目录读那里的Makefile然后利用M$(PWD)回到当前目录编译模块。ARCHarm和CROSS_COMPILEarm-linux-gnueabihf-不能漏漏了就会用PC的gcc编x86模块拿上板子直接格式错误。这里有个容易被忽略的细节obj-m后面的目标名必须和源码名一致如果你把目标改成myhello.o就得再写一条myhello-objs : hello.o不如直接同名省事。3.3 编译并检查ko文件在hello_driver目录下执行make顺利的话目录下会生成hello.ko同时伴随一堆中间文件。可以用下列命令验证file hello.ko输出里如果有ARM或EABI字样说明架构正确。如果看到x86-64说明交叉编译没有生效回去检查Makefile里的CROSS_COMPILE。再用modinfo查看模块信息arm-linux-gnueabihf-modinfo hello.ko可以看到模块名、描述、许可证等。检查完后用make clean清掉中间文件只保留hello.c和Makefile方便后面打包。到这一步你已经跨过了“能生成板子架构模块文件”这道坎。4. 把驱动编进内核Kconfig与menuconfig的配置链路4.1 模块方式与内建方式的差异前面我们编译的是外部可加载模块module即.ko文件。它的好处是灵活可以随意insmod/rmmod坏了也不影响系统启动。但有些驱动必须在系统启动早期就存在比如存储控制、串口初始化这时候外部模块就来不及了必须编进zImage也就是内建built-in。模块和内建在Kconfig里对应两个选项m表示模块y表示内建n表示不编。明白这三态后面配置内核就心里有底了。设备驱动开发中最常用的套路是先配成模块调试功能稳定后再改成内建或者用init脚本加载。4.2 修改Kconfig添加自己的配置项为了让自己的驱动能出现在menuconfig配置界面里需要给它配一个Kconfig选项。这里我演示一种最小做法在drivers/Kconfig末尾追加内容不太推荐容易污染原厂配置更好的方式是建一个独立目录。不过零基础阶段我建议先在内核源码树外维护源码只使用内核已有的内核模块编译框架。如果你想体验“打开菜单开关驱动”可以在某个驱动子目录下创建一个Kconfig片段。比如在drivers/misc/目录下已有Kconfig中追加config HELLO_MODULE tristate Hello module support default n help This is a demo module for RV1126 driver learning.然后确认drivers/Kconfig里通过source引用了drivers/misc/Kconfig这样菜单上就会出现“Hello module support”。选M保存后退出再在hello_driver目录下执行make内核会根据配置自动编译这个模块。实际上如果你只想编译自己的模块不需要动Kconfig直接在外部Makefile里指定obj-m就足够了。但理解Kconfig的价值在于将来你想炮制一个驱动目录或者把自己的驱动集成进SDK就必须走这套流程。4.3 配置内核并编译生成boot.img在SDK场景下配置内核最常用命令是make menuconfig界面里用方向键浏览按空格切换[ ]、[M]、[*]三种状态。保存退出后.config文件就更新了。接下来要重新生成zImage和对应的设备树文件。设备树DTB描述了硬件连接关系内核启动时用它来识别串口、GPIO、I2C等资源。RV1126这种芯片通常需要把zImage和DTB打包成一个boot.img才能被U-Boot引导。打包方式每个SDK略有差别但原理一样SDK里会有一个mkbootimg类似的脚本输入是zImage和.dtb文件输出是boot.img。你可以先确认SDK文档里是否有现成的build boot.img脚本没有就找“打包boot”相关的Make目标。把boot.img烧写到boot分区后内核才会按新配置启动。5. 烧写到开发板分区表、打包流程与fastboot实操5.1 理解开发板的启动分区RV1126方案常见的内存布局是U-Boot放在起始地址接着是boot分区、rootfs分区等。烧写前必须先搞清楚开发板固件分了几块每块是干嘛的。虽然理论上你可以直接往裸flash里写整片镜像但那会抹掉引导程序非常容易变砖。先用串口连接开发板启动时按提示进入bootloader或升级模式。一个通用的做法是在系统起来后执行adb reboot bootloader或者按开发板上的物理按键触发升级模式。不同板子的进入方式不同但最终目的都是让U-Boot进入等待烧写的状态。下面是常见分区表具体分区名和大小以你的SDK文档为准分区名内容烧写必要性ubootU-Boot引导程序通常不动bootzImage DTB本文需要烧写rootfs根文件系统存放ko模块、应用程序misc升级标识等一般不手动烧写5.2 构建镜像并使用烧写工具我们在第4节已经得到了boot.img。现在把它烧进boot分区。以fastboot为例如果你的SDK用专用图形工具操作逻辑类似都要选择分区和镜像文件fastboot flash boot boot.img执行前先用fastboot devices确认能看到设备ID。如果提示no permissions大概率是udev权限没配用sudo fastboot即可。烧写完成后还要把hello.ko放到板子的文件系统里。有两种方式一是把ko放进rootfs镜像再整体烧写适合发布固件二是用adb push临时推送到板的/data目录适合调试阶段。新手强烈建议用第二种不用重刷整个文件系统。具体命令adb push hello.ko /data/然后重启开发板进入系统后切换目录用insmod加载cd /data insmod hello.ko没有报错说明加载成功再用lsmod可以看到hello模块出现在列表里。卸载时使用rmmod hello注意rmmod后面是模块名不是ko文件名。5.3 验证驱动是否真正工作加载完毕后第一件事是看内核日志。串口下执行dmesg | tail能搜到Hello RV1126 kernel module输出说明模块的init函数确实被调用了。卸载后再执行dmesg | tail应该多出Goodbye那行。我见过不少新人在这里卡住原因是板子的串口波特率或控制台配置不对导致根本看不到内核日志。建议把串口工具波特率设为1500000或115200具体以SDK文档为准。如果dmesg没有输出先检查模块是否加载成功再检查打印级别后面会说。6. 踩坑记录交叉编译版本、模块加载失败与日志排查6.1 编译阶段的常见报错与对策我整理了一张排查表按频率排序报错现象原因解决思路gcc: error: unrecognized command line option工具链版本或架构不匹配切换为SDK自带工具链确认CROSS_COMPILE前缀kernel source tree not foundMakefile中KERNEL_DIR路径不对用绝对路径指向内核源码目录确认该目录下有Makefileinclude/generated/autoconf.h: No such file内核没做过配置回到kernel目录执行make xxx_defconfigNo rule to make target modules当前内核源码树不完整或版本过老重新解压SDK原版内核源码不要用git子模块缺失的仓库工具链版本不匹配是最隐蔽的。我遇到过编译器版本太新导致asm内联汇编语法不兼容的情况报错位置根本不在你的源码里而在内核头文件里。这时候不要去改内核源码先换工具链。SDK预置工具链是最好的避坑方式。6.2 insmod失败的几种典型原因模块编译出来了也推到板子上了一insmod却报错。常见的错误消息有insmod: cant open /data/hello.ko文件不在或路径写错。检查目录和文件名。module: invalid module format最经典说明模块编译时的内核配置和当前运行的内核不一致。这种情况多半是你改了内核配置后用新的boot.img启动但模块是用旧配置编译的或者反过来。解决方法是确保编译模块时使用的内核源码树和板子里运行的内核是同一份配置、同一份源码。Unknown symbol模块引用了其他模块导出的函数但那个模块没加载。需要先加载依赖模块。missing MODULE_LICENSE()编译时用的是旧内核规范新内核要求显示声明许可证回到hello.c加上MODULE_LICENSE(GPL)。还有一种情况值得单独说你用了modprobe而不是insmod。modprobe会先查找依赖并自动加载但它需要depmod生成的模块依赖文件。在嵌入式文件系统里如果你没有运行过depmod -amodprobe hello就会报not found。这是正常的不代表模块有问题直接用insmod即可。6.3 printk日志级别与dmesg调试技巧驱动里最常用的调试手段就是printk但它有级别。数字越小优先级越高dmesg默认只显示低于当前console_loglevel的日志。如果看不到自己的打印可以先用dmesg -c清空日志再加载模块减少干扰。观察输出时优先使用dmesg | grep -i hello为了更直观也可以在代码里临时提高级别printk(KERN_ALERT Hello RV1126 kernel module\n);级别高的信息通常不会被过滤。但正式交付代码时要把调试信息恢复到KERN_INFO或KERN_DEBUG否则会被同事骂刷屏。另外一个实用技巧是给模块传递参数。比如在hello.c里声明一个变量static char *who world; module_param(who, charp, 0644); MODULE_PARM_DESC(who, who to greet);加载时用insmod hello.ko whoRV1126就能在不改代码的情况下改变行为。这个做法在调试真实设备时非常有用你可以用它控制调试级别或者指定设备节点。最后分享一个小习惯我在调试RV1126驱动时会同时开三个终端一个看串口输出一个用dmesg -w跟踪内核日志一个用来敲编译命令。每次改代码后按顺序执行make、adb push、insmod、dmesg四步形成肌肉记忆。等你跑通第一个hello模块后面去啃GPIO、I2C这些真实驱动时流程还是这套只是硬件操作逻辑变复杂而已。