Linux GPIO驱动开发与中断外设控制 - 第1节
1.1 内核模块的概念与结构
Linux 内核模块(Kernel Module)是一段可以在运行时动态加载到内核或从内核卸载的代码片段。它允许在不重新编译整个内核、不重启系统的情况下扩展内核功能。对于驱动开发者而言,模块是承载设备驱动逻辑的基本单元。
核心优势: 动态加载/卸载、独立编译、按需启用硬件支持、减少内核镜像体积。
1.1.1 模块与应用程序的对比
| 对比维度 | 内核模块 | 用户态应用程序 |
|---|---|---|
| 运行空间 | 内核空间(Ring 0) | 用户空间(Ring 3) |
| 入口函数 | module_init() |
main() |
| 退出函数 | module_exit() |
进程终止 |
| 内存访问 | 直接访问物理地址 | 虚拟地址,受 MMU 保护 |
| 标准库支持 | 无(不能使用 libc) | 完整 libc 支持 |
| 调试难度 | 高(崩溃即内核 panic) | 低(可 gdb 调试) |
1.1.2 模块的基本组成
一个标准的内核模块至少包含以下四个要素:
- 模块许可证声明 —— 使用
MODULE_LICENSE("GPL")告知内核该模块遵循 GPL 协议。 - 模块作者与描述 ——
MODULE_AUTHOR()和MODULE_DESCRIPTION()提供元信息。 - 初始化入口 —— 通过
module_init()宏注册加载时调用的函数。 - 清理出口 —— 通过
module_exit()宏注册卸载时调用的函数。
提示: 即使是最简单的 "Hello World" 模块,也必须包含
module_init 和 module_exit,否则内核无法正确管理模块生命周期。
1.2 模块的加载与卸载
Linux 提供了一组用户态工具来管理内核模块的生命周期。最常用的命令是 insmod、rmmod 和 modprobe。
1.2.1 核心命令速查
| 命令 | 功能 | 示例 |
|---|---|---|
insmod |
加载指定 .ko 文件到内核 | insmod gpio_driver.ko |
rmmod |
卸载指定名称的模块 | rmmod gpio_driver |
modprobe |
智能加载(自动处理依赖) | modprobe gpio_driver |
lsmod |
列出当前已加载的所有模块 | lsmod | grep gpio |
modinfo |
查看模块的元信息 | modinfo gpio_driver.ko |
1.2.2 加载与卸载的完整流程
- 编译模块:通过 Makefile 调用内核构建系统,生成
.ko文件。 - 加载模块:
sudo insmod my_module.ko,内核调用module_init注册的函数。 - 验证加载:
lsmod查看模块列表,dmesg | tail查看内核日志。 - 卸载模块:
sudo rmmod my_module,内核调用module_exit注册的函数。 - 确认卸载:再次
lsmod确认模块已从列表中移除。
注意: 如果模块正在被其他模块或进程引用,
rmmod 会失败并返回 "Module is in use"。此时需要先解除依赖关系,或使用 modprobe -r 递归卸载。
1.3 模块参数传递
内核模块支持在加载时通过命令行传递参数,从而在不重新编译模块的情况下改变其行为。这对于 GPIO 驱动开发尤为重要——例如在加载时指定要控制的 GPIO 引脚编号。
1.3.1 参数声明宏
| 宏 | 数据类型 | 示例 |
|---|---|---|
module_param(name, type, perm) |
基本类型(int, long, charp 等) | module_param(gpio_num, int, 0644); |
module_param_array(name, type, num, perm) |
数组类型 | module_param_array(pins, int, &count, 0444); |
module_param_string(name, buf, len, perm) |
字符串类型 | module_param_string(dev_name, buf, 32, 0); |
perm 参数说明: 表示 sysfs 文件系统中的访问权限。0 表示不在 sysfs 中创建对应文件;0644 表示所有用户可读,仅 root 可写;0444 表示只读。
1.3.2 实战示例:带参数的 GPIO 模块
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
/* 模块参数声明 */
static int gpio_pin = 17; /* 默认使用 GPIO 17 */
static char *gpio_label = "my_led"; /* 默认标签 */
module_param(gpio_pin, int, 0644);
MODULE_PARM_DESC(gpio_pin, "GPIO pin number to control (default: 17)");
module_param(gpio_label, charp, 0444);
MODULE_PARM_DESC(gpio_label, "Descriptive label for the GPIO (default: my_led)");
static int __init gpio_demo_init(void)
{
printk(KERN_INFO "GPIO Demo: Initializing with pin=%d, label=%s\n",
gpio_pin, gpio_label);
/* 此处将调用 gpio_request() 和 gpio_direction_output() */
return 0;
}
static void __exit gpio_demo_exit(void)
{
printk(KERN_INFO "GPIO Demo: Exiting, releasing pin %d\n", gpio_pin);
/* 此处将调用 gpio_free() */
}
module_init(gpio_demo_init);
module_exit(gpio_demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux Driver Expert");
MODULE_DESCRIPTION("A simple GPIO demo module with parameters");
1.3.3 加载时传递参数
# 使用默认参数加载
sudo insmod gpio_demo.ko
# 自定义参数加载
sudo insmod gpio_demo.ko gpio_pin=23 gpio_label="green_led"
# 查看已加载模块的参数值
cat /sys/module/gpio_demo/parameters/gpio_pin
cat /sys/module/gpio_demo/parameters/gpio_label
# 运行时修改参数(需 perm 包含写权限)
echo 25 | sudo tee /sys/module/gpio_demo/parameters/gpio_pin
最佳实践: 在 GPIO 驱动中,始终将引脚编号设计为模块参数。这样同一份驱动代码可以灵活适配不同的硬件板卡,无需为每个 GPIO 引脚重新编译。
1.4 模块的 Makefile 与编译
内核模块不能像普通 C 程序那样直接用 gcc 编译,必须借助内核的构建系统(Kbuild)。以下是一个标准的模块 Makefile 模板:
# Makefile for GPIO demo module
# 内核源码树路径(根据实际环境修改)
KDIR := /lib/modules/$(shell uname -r)/build
# 当前目录
PWD := $(shell pwd)
# 模块名称
obj-m := gpio_demo.o
# 如果模块由多个源文件组成,使用如下语法:
# gpio_demo-objs := main.o gpio_ops.o
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
install:
sudo insmod gpio_demo.ko gpio_pin=23
remove:
sudo rmmod gpio_demo
# 查看模块信息
info:
modinfo gpio_demo.ko
编译与测试步骤:
- 将上述 Makefile 和
gpio_demo.c放在同一目录。 - 执行
make编译,生成gpio_demo.ko。 - 执行
make install加载模块,观察dmesg输出。 - 执行
make remove卸载模块,确认日志输出。
1.5 本章小结
| 知识点 | 关键要点 |
|---|---|
| 模块概念 | 动态加载/卸载的内核扩展单元,驱动开发的基本载体 |
| 模块结构 | module_init + module_exit + 许可证声明 + 元信息 |
| 加载卸载 | insmod / rmmod / modprobe / lsmod |
| 参数传递 | module_param 宏,支持 int/charp/array,可在加载时或运行时修改 |
| 编译系统 | 使用内核 Kbuild 系统,Makefile 中指定 obj-m |
常见错误排查:
- "Unknown symbol" —— 模块依赖的其他模块未加载,或内核版本不匹配。
- "Operation not permitted" —— 未使用
sudo或内核开启了模块签名验证。 - "Module is in use" —— 有其他进程或模块正在引用当前模块。
- 加载后无日志 —— 检查
printk级别,使用dmesg -n 7开启所有日志。