ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

RV1126内核驱动入门:交叉编译到烧写的完整实战

RV1126内核驱动入门:交叉编译到烧写的完整实战

1. 为什么零基础入门建议从RV1126内核驱动开始

RV1126内核驱动编译与烧写,听起来像是需要啃半年内核源码才能碰的东西,但实际拆开看,就是一套可以照着做的流程。我第一次接触这块芯片的时候,连交叉编译工具链都没配明白,照着网上的文章一步步试,最后总算把第一个hello.ko跑起来。这篇文章就是想按我实测过的路径,把这个过程完整讲清楚,适合完全零基础、但有一点点C语言和Linux命令行常识的读者。

先说为什么推荐用RV1126入门,而不是直接在PC机上学。RV1126属于带NPU和ISP的视觉处理芯片,广泛用在智能摄像头、边缘计算盒子这类产品上。它的SDK通常已经帮你整理好了内核源码、交叉编译工具链、文件系统和烧写脚本,你不需要从零搭建整个系统,只需要把注意力集中在内核驱动本身。这个特性对新手极其友好:你遇到问题的时候,能区分问题是出在环境配置还是驱动代码,而不是所有环节都糊在一起。

另外,芯片架构是ARM Cortex-A7,32位,内核版本基于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 └── Kconfig

arch/arm/configs下通常有对应开发板的默认配置文件,文件名带平台型号,比如某型号的defconfig。编译内核的第一步,就是把平台默认配置加载成当前配置。这步相当于告诉内核“你需要支持哪些功能和驱动”,配置文件就是一张功能清单。

drivers目录下面按类型分了很多子目录:gpio、i2c、spi、net、video等等。我们接下来要添加的hello驱动,既可以直接放在某个子目录里,也可以单独放在内核源码树外的目录中用模块方式编译。新手建议从独立目录开始,不影响内核原有代码。

2.3 用一次完整编译验证环境是否正常

环境配置完不能拍脑袋说没问题,一定要做一次完整的原厂内核编译。这一步有两个目的:一是验证工具链和源码能配合工作,二是生成后面编译模块时需要的“内核编译产物”。因为外部模块编译时,必须依赖内核源码树里已经生成好的头文件和符号表,没有这些,你会遇到autoconf.h缺失之类的报错。

在kernel目录下执行:

export ARCH=arm export CROSS_COMPILE=arm-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) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules clean: $(MAKE) -C $(KERNEL_DIR) M=$(PWD) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- clean

obj-m := hello.o表示我们希望生成一个名为hello的内核模块,后面连接到hello.c。中间的-C选项是让make先切换到内核源码目录读那里的Makefile,然后利用M=$(PWD)回到当前目录编译模块。ARCH=arm和CROSS_COMPILE=arm-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路径不对用绝对路径指向内核源码目录,确认该目录下有Makefile
include/generated/autoconf.h: No such file内核没做过配置回到kernel目录执行make xxx_defconfig
No rule to make target 'modules'当前内核源码树不完整或版本过老重新解压SDK原版内核源码,不要用git子模块缺失的仓库

工具链版本不匹配是最隐蔽的。我遇到过编译器版本太新导致asm内联汇编语法不兼容的情况,报错位置根本不在你的源码里,而在内核头文件里。这时候不要去改内核源码,先换工具链。SDK预置工具链是最好的避坑方式。

6.2 insmod失败的几种典型原因

模块编译出来了,也推到板子上了,一insmod却报错。常见的错误消息有:

  • insmod: can't 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 -a,modprobe 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 who=RV1126,就能在不改代码的情况下改变行为。这个做法在调试真实设备时非常有用,你可以用它控制调试级别或者指定设备节点。

最后分享一个小习惯:我在调试RV1126驱动时会同时开三个终端,一个看串口输出,一个用dmesg -w跟踪内核日志,一个用来敲编译命令。每次改代码后,按顺序执行make、adb push、insmod、dmesg四步,形成肌肉记忆。等你跑通第一个hello模块,后面去啃GPIO、I2C这些真实驱动时,流程还是这套,只是硬件操作逻辑变复杂而已。

RELATED READING

延伸阅读

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