3. Linux内核模块编程
在嵌入式Linux驱动开发中,内核模块是动态加载到内核中的代码片段,无需重新编译整个内核即可扩展功能。触摸屏驱动通常以内核模块形式存在,通过module_init和module_exit宏注册入口/出口函数,通过许可证声明满足GPL合规要求,通过模块参数传递配置信息(如I2C地址、中断号、轮询间隔)。
3.1 模块基本框架
每个内核模块必须包含两个核心宏:module_init()指定模块加载时调用的初始化函数,module_exit()指定模块卸载时调用的清理函数。这两个宏展开后会将函数指针放入特定的ELF段,由内核在模块生命周期中自动调用。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init touch_init(void)
{
printk(KERN_INFO "Touch driver initialized\n");
return 0;
}
static void __exit touch_exit(void)
{
printk(KERN_INFO "Touch driver exited\n");
}
module_init(touch_init);
module_exit(touch_exit);
关键点:
__init标记:初始化函数在运行后会被内核释放,回收内存。__exit标记:卸载函数在非模块编译时会被忽略,减少内核体积。- 初始化函数返回0表示成功,负值表示失败,模块加载将被中止。
3.2 许可证声明
Linux内核要求所有模块明确声明许可证,以维护GPL协议的合规性。未声明或声明非GPL兼容许可证的模块,将无法使用内核导出的GPL-only符号(如EXPORT_SYMBOL_GPL导出的函数)。
| 宏 | 许可证类型 | 能否使用GPL-only符号 | 典型场景 |
|---|---|---|---|
MODULE_LICENSE("GPL") |
GNU General Public License v2 | 是 | 开源驱动、内核主线提交 |
MODULE_LICENSE("GPL v2") |
GPL v2 | 是 | 与GPL v2严格兼容 |
MODULE_LICENSE("Dual BSD/GPL") |
BSD或GPL双许可 | 是 | BSD项目兼容GPL |
MODULE_LICENSE("Proprietary") |
专有许可证 | 否 | 闭源商业驱动 |
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Driver Team <dev@example.com>");
MODULE_DESCRIPTION("Touchscreen driver module");
MODULE_VERSION("1.0");
避坑提示:触摸屏驱动通常需要调用input子系统核心函数(如
input_allocate_device、input_register_device),这些函数大多以EXPORT_SYMBOL_GPL导出。如果模块声明为"Proprietary",将无法链接这些符号,导致驱动加载失败。因此,嵌入式触摸屏驱动必须声明GPL兼容许可证。
3.3 模块参数传递
模块参数允许在加载时动态传递配置值,无需重新编译。触摸屏驱动常用参数包括:I2C从设备地址、中断GPIO号、轮询间隔(毫秒)、最大支持触摸点数等。
3.3.1 参数声明与描述
#include <linux/moduleparam.h>
static int irq_gpio = 17;
static int poll_interval_ms = 10;
static bool enable_debug = false;
module_param(irq_gpio, int, 0644);
module_param(poll_interval_ms, int, 0644);
module_param(enable_debug, bool, 0644);
MODULE_PARM_DESC(irq_gpio, "Interrupt GPIO number (default: 17)");
MODULE_PARM_DESC(poll_interval_ms, "Polling interval in ms (default: 10)");
MODULE_PARM_DESC(enable_debug, "Enable debug output (default: false)");
3.3.2 参数类型与权限
| 类型 | module_param类型标识 |
示例值 | 说明 |
|---|---|---|---|
| 整型 | int |
irq_gpio=17 |
有符号32位整数 |
| 无符号整型 | uint |
poll_interval_ms=10 |
无符号32位整数 |
| 布尔型 | bool |
enable_debug=1 |
0/1或y/n |
| 字符串 | charp |
i2c_bus="i2c-1" |
字符指针,需注意内存管理 |
| 短整型 | short |
threshold=100 |
有符号16位整数 |
权限参数(如0644)控制/sys/module/<name>/parameters/下对应文件的访问权限:
0:不创建sysfs文件,参数仅在加载时可用。0444:只读,用户空间可查看但不能修改。0644:所有者可读写,其他人只读,运行时可通过sysfs动态修改。
3.3.3 加载时传递参数
# 使用insmod加载并传递参数
sudo insmod touch_drv.ko irq_gpio=23 poll_interval_ms=5 enable_debug=1
# 使用modprobe加载(参数放在/etc/modprobe.d/配置文件中)
sudo modprobe touch_drv irq_gpio=23 poll_interval_ms=5
# 运行时通过sysfs修改(需权限0644)
echo 10 > /sys/module/touch_drv/parameters/poll_interval_ms
实践建议:触摸屏驱动中,将I2C地址、中断号、轮询间隔作为模块参数,可以在不同硬件平台间复用同一驱动二进制文件。设备树(Device Tree)成熟后,这些参数通常由设备树节点属性提供,但模块参数仍适用于快速原型验证和调试场景。
3.4 完整模块示例:触摸屏参数化驱动骨架
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
static int irq_gpio = 17;
static int i2c_addr = 0x38;
static int poll_ms = 10;
static bool debug = false;
module_param(irq_gpio, int, 0644);
module_param(i2c_addr, int, 0444);
module_param(poll_ms, int, 0644);
module_param(debug, bool, 0644);
MODULE_PARM_DESC(irq_gpio, "Touch IRQ GPIO number");
MODULE_PARM_DESC(i2c_addr, "I2C slave address (hex)");
MODULE_PARM_DESC(poll_ms, "Polling interval in ms");
MODULE_PARM_DESC(debug, "Enable debug prints");
static int __init touch_probe_init(void)
{
if (debug)
printk(KERN_DEBUG "touch: irq=%d, addr=0x%02x, poll=%dms\n",
irq_gpio, i2c_addr, poll_ms);
/* 实际驱动中:注册I2C/SPI设备、分配input_dev、设置事件类型 */
printk(KERN_INFO "touch: driver loaded successfully\n");
return 0;
}
static void __exit touch_remove_exit(void)
{
/* 实际驱动中:注销input_dev、释放中断、销毁设备 */
printk(KERN_INFO "touch: driver unloaded\n");
}
module_init(touch_probe_init);
module_exit(touch_remove_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Linux Course");
MODULE_DESCRIPTION("Touchscreen driver skeleton with module params");
MODULE_VERSION("1.0");
3.5 模块生命周期与内核交互流程
下图展示了内核模块从加载到卸载的完整生命周期,以及module_init/module_exit在其中的位置:
3.6 常见错误与调试
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
insmod: ERROR: could not insert module |
初始化函数返回负值 | 检查dmesg查看具体错误码;确认硬件资源可用 |
Unknown symbol 或 module license 'Proprietary' taints kernel |
许可证不兼容,无法使用GPL-only符号 | 将MODULE_LICENSE改为"GPL"或"GPL v2" |
| 模块参数未生效 | 参数名拼写错误或类型不匹配 | 检查module_param类型标识;查看/sys/module/<name>/parameters/ |
rmmod: ERROR: Module is in use |
有进程打开模块相关设备文件 | 关闭占用设备文件的进程;使用lsof查找 |
调试命令速查:
dmesg | tail -20— 查看内核日志,定位模块加载/卸载错误。lsmod | grep touch— 确认模块是否已加载。cat /sys/module/touch_drv/parameters/*— 查看当前模块参数值。modinfo touch_drv.ko— 查看模块的许可证、作者、参数描述等信息。
3.7 本章小结
- module_init/module_exit:内核模块的入口和出口,分别对应加载和卸载时的回调函数。
- 许可证声明:触摸屏驱动必须使用GPL兼容许可证,否则无法调用input子系统的核心函数。
- 模块参数:通过
module_param和MODULE_PARM_DESC实现运行时配置,支持int、bool、charp等多种类型,权限控制sysfs访问。 - 生命周期:从用户空间加载→初始化→运行→卸载→清理,每个阶段都有对应的内核回调。