1. 项目概述
最近在调试瑞芯微RK3566开发板时,需要为内核添加一个简单的驱动模块。虽然官方SDK提供了丰富的驱动支持,但在实际开发中我们经常需要根据业务需求编写自定义模块。本文将详细介绍从零开始编写、编译到加载测试一个基础Linux内核模块的全过程。
这个名为hello_drv的模块虽然功能简单(仅在内核日志中打印信息),但完整呈现了Linux内核模块开发的核心流程。对于嵌入式开发者而言,掌握这套工作流至关重要,因为它是后续开发更复杂驱动的基础。通过这个案例,你将学会:
- 模块代码的基本结构
- 交叉编译环境的配置
- 模块的加载/卸载操作
- 调试信息的查看方法
2. 开发环境准备
2.1 硬件设备清单
进行RK3566模块开发需要准备以下硬件:
- 瑞芯微RK3566开发板(或类似ARM64架构设备)
- 调试串口线(推荐USB转TTL模块)
- 网线(用于网络调试)
- 电源适配器(5V/2A以上)
2.2 软件工具链
开发主机建议使用Ubuntu 20.04 LTS系统,需要安装以下组件:
- 交叉编译工具链:
bash复制sudo apt install gcc-aarch64-linux-gnu
- 内核头文件(可选):
bash复制sudo apt install linux-headers-$(uname -r)
- 串口调试工具:
bash复制sudo apt install minicom
2.3 内核源码获取
从瑞芯微官方获取对应开发板的Linux SDK:
bash复制git clone https://github.com/rockchip-linux/kernel.git -b develop-5.10
注意:不同板型的内核分支可能不同,请根据实际硬件选择正确的分支版本
3. 模块代码解析
3.1 模块基本结构
创建hello_drv.c文件,包含以下核心组件:
c复制#include <linux/init.h>
#include <linux/module.h>
// 模块初始化函数
static int __init hello_drv_init(void)
{
printk(KERN_INFO "---------- ^_^ %s-------------\n",__FUNCTION__);
return 0;
}
// 模块退出函数
static void __exit hello_drv_exit(void)
{
printk(KERN_INFO "---------- ^_^ %s-------------\n",__FUNCTION__);
}
// 注册模块入口/出口
module_init(hello_drv_init);
module_exit(hello_drv_exit);
// 模块元信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple hello world driver");
关键点说明:
__init和__exit宏标记函数生命周期printk是内核态的输出函数(不同于用户态的printf)KERN_INFO定义日志级别(建议始终明确指定)
3.2 Makefile配置
创建Makefile文件,适配RK3566的交叉编译环境:
makefile复制# 内核源码路径(根据实际位置修改)
KERN_DIR = /path/to/rk356x_linux_sdk/kernel
# 交叉编译工具配置
CROSS_COMPILE = aarch64-linux-gnu-
ARCH = arm64
# 当前目录
CUR_DIR = $(shell pwd)
# 编译目标
obj-m = hello_drv.o
# 编译规则
all:
make -C $(KERN_DIR) \
ARCH=$(ARCH) \
CROSS_COMPILE=$(CROSS_COMPILE) \
M=$(CUR_DIR) \
modules
clean:
make -C $(KERN_DIR) \
ARCH=$(ARCH) \
CROSS_COMPILE=$(CROSS_COMPILE) \
M=$(CUR_DIR) \
clean
install:
sudo cp *.ko /path/to/rootfs/lib/modules/$(shell uname -r)
重要参数说明:
KERN_DIR:必须指向完整的内核源码目录M=$(CUR_DIR):告知内核构建系统模块源码的位置obj-m:指定要构建的模块对象
4. 编译与部署
4.1 交叉编译流程
- 设置环境变量(可选):
bash复制export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
- 执行编译:
bash复制make -j$(nproc)
提示:-j参数指定并行编译任务数,可显著加快编译速度
- 检查生成文件:
bash复制file hello_drv.ko
正常输出应显示:
code复制hello_drv.ko: ELF 64-bit LSB relocatable, ARM aarch64, version 1 (SYSV), BuildID[sha1]=..., not stripped
4.2 模块部署到开发板
- 将ko文件传输到开发板:
bash复制scp hello_drv.ko root@开发板IP:/lib/modules/
- 加载模块:
bash复制insmod hello_drv.ko
- 验证加载:
bash复制lsmod | grep hello_drv
应看到类似输出:
code复制hello_drv 16384 0
- 查看内核日志:
bash复制dmesg | tail -n 5
预期输出:
code复制[ 1234.567890] ---------- ^_^ hello_drv_init-------------
5. 调试与问题排查
5.1 常见编译错误
- 内核版本不匹配:
code复制ERROR: could not insert module hello_drv.ko: Invalid module format
解决方案:
- 确保开发板运行的内核与编译使用的内核版本一致
- 执行
uname -r查看开发板内核版本
- 架构不匹配:
code复制file format not recognized
解决方案:
- 确认Makefile中
ARCH和CROSS_COMPILE配置正确 - 检查工具链是否安装完整
5.2 运行时问题
- printk不输出:
- 检查日志级别:
cat /proc/sys/kernel/printk - 确保printk使用
KERN_INFO或更高优先级
- 模块无法卸载:
code复制rmmod: ERROR: Module hello_drv is in use
解决方案:
- 检查模块引用计数:
lsmod | grep hello_drv - 确保没有其他模块或进程依赖该模块
6. 进阶开发建议
6.1 添加模块参数
增强模块灵活性,支持运行时配置:
c复制static char *name = "world";
module_param(name, charp, 0644);
MODULE_PARM_DESC(name, "The name to print");
static int __init hello_drv_init(void)
{
printk(KERN_INFO "Hello %s!\n", name);
return 0;
}
使用方式:
bash复制insmod hello_drv.ko name="rk3566"
6.2 符号导出
允许其他模块调用本模块的函数:
c复制void my_exported_func(void)
{
printk(KERN_INFO "Function called\n");
}
EXPORT_SYMBOL(my_exported_func);
6.3 调试技巧
- 使用
CONFIG_DYNAMIC_DEBUG:
c复制pr_debug("Debug message: %d\n", value);
运行时控制:
bash复制echo 'file hello_drv.c +p' > /sys/kernel/debug/dynamic_debug/control
- 内核Oops分析:
- 安装crash工具
- 收集vmcore文件
- 使用
addr2line定位问题代码
7. 性能优化
7.1 减小模块体积
- 去除调试符号:
bash复制aarch64-linux-gnu-strip --strip-debug hello_drv.ko
- 编译器优化选项:
makefile复制EXTRA_CFLAGS += -Os
7.2 内存管理
- 合理使用
__init和__exit:
__init标记的代码在初始化后会被释放__exit标记的代码在非模块卸载场景不会被编译
- 避免内存泄漏:
- 确保所有
kmalloc都有对应的kfree - 使用
devres系列API管理资源
8. 实际项目经验
在RK3566上开发工业控制器时,我们遇到几个典型问题:
- 时钟同步问题:
- 模块依赖的时钟源未正确初始化
- 解决方案:在init函数中添加时钟获取和使能逻辑
- DMA缓存一致性:
- ARM64需要处理缓存一致性
- 使用
dma_alloc_coherent替代kmalloc
- 中断处理优化:
- 快速路径使用
request_irq的IRQF_NO_THREAD标志 - 耗时操作使用工作队列或线程化中断
关键提示:在嵌入式开发中,建议每个功能模块都实现为独立ko文件,便于单独调试和更新
