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在其中的位置:

Linux内核模块生命周期与module_init/module_exit 用户空间 insmod / modprobe 内核模块加载 module_init() 调用 初始化执行 注册设备/中断/input_dev 模块运行中 处理触摸事件 / 与input子系统交互 / 响应sysfs参数变更 模块卸载 module_exit() 调用 清理执行 注销设备/释放内存/销毁input_dev rmmod 完成 模块参数传递 insmod touch.ko irq_gpio=23 poll_ms=5 debug=1

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访问。
  • 生命周期:从用户空间加载→初始化→运行→卸载→清理,每个阶段都有对应的内核回调。