4、内核模块开发入门:模块编写、编译、加载卸载、printk调试与模块参数传递
在Linux驱动开发中,内核模块(Kernel Module)是扩展内核功能的主要手段,尤其对于RK3588这类复杂的ARM SoC,外设驱动的开发几乎都以模块形式进行。模块机制允许驱动代码在系统运行期间动态加载到内核地址空间,无需重新编译整个内核或重启设备,这极大提升了开发与调试效率。本章将围绕模块的编写规范、交叉编译流程、加载与卸载操作、printk调试机制以及模块参数传递展开,帮助读者建立驱动开发的基本工程能力。
4.1 内核模块的基本结构与编写规范
一个标准的内核模块至少包含两个核心函数:模块加载入口(module_init)和模块卸载出口(module_exit)。在RK3588平台上,模块代码运行在ARMv8-A架构的内核态,拥有完全的内核地址空间访问权限。模块的编写必须遵循内核编码风格,并且只能使用内核导出的API,不能链接用户态的标准C库。
以下是一个最小可用的模块源码示例,该模块在加载时打印一条信息,卸载时打印另一条信息:
// hello.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
static int __init hello_init(void)
{
printk(KERN_INFO "[Hello] RK3588 module loaded.\n");
return 0; // 返回0表示加载成功
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "[Hello] RK3588 module unloaded.\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("RK3588 Driver Course");
MODULE_DESCRIPTION("A simple hello module for RK3588");
MODULE_VERSION("1.0");
在编写模块时,__init 和 __exit 宏会将函数放入特定的代码段,加载完成后这些内存可以被释放以节省空间。对于RK3588这种内存资源相对丰富的平台,这一机制仍然有效,但更重要的是养成正确的代码段使用习惯。
4.2 交叉编译环境配置与Makefile编写
RK3588是ARM64架构,开发主机通常是x86_64,因此必须使用交叉编译工具链。在瑞芯微官方SDK中,通常已经预置了工具链路径,例如prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-linux-gnu/bin。编译模块前,需要确保目标板的内核源码树已经配置并生成了Module.symvers文件。
一个典型的模块Makefile如下:
# Makefile
# 指定交叉编译工具链前缀
CROSS_COMPILE ?= aarch64-linux-gnu-
# 指定内核源码树路径(RK3588 SDK中的内核目录)
KDIR ?= /home/user/rk3588_sdk/kernel
# 当前目录
PWD := $(shell pwd)
obj-m := hello.o
all:
$(MAKE) ARCH=arm64 CROSS_COMPILE=$(CROSS_COMPILE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) ARCH=arm64 CROSS_COMPILE=$(CROSS_COMPILE) -C $(KDIR) M=$(PWD) clean
编译时执行make,会生成hello.ko文件。使用file hello.ko可以验证架构是否为ARM aarch64。如果误用了x86的gcc,生成的模块在加载时会报invalid module format错误。
| 编译要素 | 要求 | 错误示例 |
|---|---|---|
| 工具链 | aarch64-linux-gnu-gcc | gcc(x86原生) |
| 内核源码 | 与目标板运行内核版本一致 | 版本不匹配导致 vermagic 错误 |
| 架构参数 | ARCH=arm64 | ARCH=x86_64 |
| 模块目标 | obj-m := xxx.o | obj-y(编入内核) |
4.3 模块的加载、卸载与状态管理
将编译好的.ko文件拷贝到RK3588目标板后,使用insmod命令加载模块,使用rmmod卸载模块。加载时,内核会执行模块的init函数;卸载时执行exit函数。常用命令如下:
# 加载模块
insmod hello.ko
# 查看已加载模块
lsmod | grep hello
# 查看模块详细信息
modinfo hello.ko
# 卸载模块
rmmod hello
# 查看内核日志
dmesg | tail -20
在开发调试阶段,modprobe命令也可以使用,但它会尝试解决模块依赖,对于简单模块两者效果相同。需要注意的是,如果模块的init函数返回非0值,加载会失败,且内核会调用cleanup函数进行清理。
mem=内核参数限制内存范围,或使用QEMU模拟环境进行初步验证。另外,不要在生产环境中随意加载未经验证的模块。
4.4 printk调试机制与日志级别
printk是内核态最常用的调试工具,其用法类似用户态的printf,但增加了日志级别控制。日志级别决定了消息是否立即显示在控制台上,以及记录到内核缓冲区的方式。RK3588的调试串口默认输出所有级别低于console_loglevel的消息。
printk的日志级别定义在<linux/kernel.h>中,常用级别如下:
KERN_EMERG "0" // 紧急事件,系统崩溃
KERN_ALERT "1" // 必须立即处理
KERN_CRIT "2" // 严重错误
KERN_ERR "3" // 错误
KERN_WARNING "4" // 警告
KERN_NOTICE "5" // 正常但重要
KERN_INFO "6" // 信息
KERN_DEBUG "7" // 调试信息
使用示例:
printk(KERN_ERR "GPIO %d config failed, ret=%d\n", gpio_num, ret);
printk(KERN_DEBUG "Register value: 0x%08x\n", reg_val);
在RK3588开发中,建议在驱动初始化、中断处理、IO操作等关键路径添加适当的printk。但要注意,在中断上下文或原子操作中,printk可能导致睡眠,应使用printk_ratelimited或pr_err等变体。
echo 8 > /proc/sys/kernel/printk将控制台日志级别设为8,使所有printk信息都输出到串口。但生产环境应恢复默认级别(通常为4),避免日志刷屏影响系统性能。
4.5 模块参数传递与sysfs接口
模块参数允许在加载时向模块传递配置值,这对于驱动开发非常实用。例如,可以指定GPIO编号、中断号、设备名称等。使用module_param宏声明参数,并指定权限位。
// param_demo.c
#include <linux/moduleparam.h>
static int gpio_num = 100;
static char *dev_name = "rk3588_led";
static int debug_level = 0;
module_param(gpio_num, int, 0644);
module_param(dev_name, charp, 0644);
module_param(debug_level, int, 0644);
MODULE_PARM_DESC(gpio_num, "GPIO number for LED control");
MODULE_PARM_DESC(dev_name, "Device name for misc device");
MODULE_PARM_DESC(debug_level, "Debug level (0-3)");
加载时传递参数:
insmod param_demo.ko gpio_num=88 dev_name="rk3588_led0" debug_level=2
如果权限位设为0644,模块加载后会在/sys/module/param_demo/parameters/目录下生成对应的参数文件,可以在运行时通过echo修改参数值(前提是模块内没有使用const修饰)。
| 参数类型 | module_param类型标识 | 用户态对应 |
|---|---|---|
| 整型 | int, uint, long, ulong | echo 100 > /sys/.../gpio_num |
| 布尔 | bool, invbool | echo 1 > /sys/.../enable |
| 字符指针 | charp | echo "name" > /sys/.../dev_name |
| 数组 | array of int | insmod xxx.ko arr=1,2,3,4 |
4.6 实战案例:RK3588 GPIO控制模块开发
下面通过一个完整的GPIO控制模块案例,串联本章所有知识点。该模块实现一个简单的LED控制功能,使用模块参数指定GPIO编号,并通过printk输出调试信息。
场景背景:在RK3588 EVB板上,需要控制一个连接在GPIO4_B5(对应GPIO编号149)的LED灯。要求模块加载时初始化GPIO为输出并点亮LED,卸载时关闭LED并释放GPIO。
关键实现:
// gpio_led.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/delay.h>
static int gpio_led_num = 149; // 默认GPIO4_B5
static bool active_high = true;
module_param(gpio_led_num, int, 0644);
module_param(active_high, bool, 0644);
MODULE_PARM_DESC(gpio_led_num, "GPIO number for LED");
MODULE_PARM_DESC(active_high, "Active high polarity");
static int __init gpio_led_init(void)
{
int ret;
if (!gpio_is_valid(gpio_led_num)) {
pr_err("Invalid GPIO %d\n", gpio_led_num);
return -EINVAL;
}
ret = gpio_request(gpio_led_num, "rk3588_led");
if (ret) {
pr_err("GPIO %d request failed: %d\n", gpio_led_num, ret);
return ret;
}
ret = gpio_direction_output(gpio_led_num, active_high ? 1 : 0);
if (ret) {
pr_err("GPIO %d direction set failed\n", gpio_led_num);
gpio_free(gpio_led_num);
return ret;
}
gpio_set_value(gpio_led_num, active_high ? 1 : 0);
pr_info("LED on GPIO %d initialized (active_high=%d)\n",
gpio_led_num, active_high);
return 0;
}
static void __exit gpio_led_exit(void)
{
gpio_set_value(gpio_led_num, active_high ? 0 : 1);
gpio_free(gpio_led_num);
pr_info("LED on GPIO %d released\n", gpio_led_num);
}
module_init(gpio_led_init);
module_exit(gpio_led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("RK3588 Driver Course");
MODULE_DESCRIPTION("GPIO LED control module for RK3588");
编译与测试流程:
- 编写Makefile,指定KDIR为RK3588 SDK内核路径。
- 执行
make生成gpio_led.ko。 - 拷贝到目标板,执行
insmod gpio_led.ko gpio_led_num=149 active_high=1。 - 观察LED状态,使用
dmesg查看日志确认初始化信息。 - 执行
rmmod gpio_led,确认LED熄灭且日志正常。
结果与复盘:该模块在RK3588上测试通过,加载后LED点亮,卸载后熄灭。调试过程中发现,如果GPIO编号错误,gpio_request会返回-EINVAL,通过printk可以快速定位问题。此外,在设备树已占用该GPIO的情况下,gpio_request会失败,此时需要检查设备树配置。
4.7 总结与工程建议
内核模块开发是RK3588驱动开发的基础技能。本章涵盖了模块结构、交叉编译、加载卸载、printk调试和参数传递五大核心内容。在实际项目中,建议遵循以下工程规范:
- 所有模块必须声明MODULE_LICENSE,推荐使用"GPL"以避免内核符号导出限制。
- 在模块加载和卸载路径中,添加详细的printk日志,便于现场问题定位。
- 模块参数尽量提供默认值,并设置合理的权限位(如0644),方便运行时调整。
- 使用
modinfo验证模块元数据,确保版本和描述信息完整。 - 在RK3588平台上,优先使用设备树描述硬件资源,模块中通过
of_系列API获取资源,而非硬编码。