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 模块的基本组成

一个标准的内核模块至少包含以下四个要素:

  1. 模块许可证声明 —— 使用 MODULE_LICENSE("GPL") 告知内核该模块遵循 GPL 协议。
  2. 模块作者与描述 —— MODULE_AUTHOR() 和 MODULE_DESCRIPTION() 提供元信息。
  3. 初始化入口 —— 通过 module_init() 宏注册加载时调用的函数。
  4. 清理出口 —— 通过 module_exit() 宏注册卸载时调用的函数。
提示: 即使是最简单的 "Hello World" 模块,也必须包含 module_init 和 module_exit,否则内核无法正确管理模块生命周期。
内核模块基本结构 内核模块 .ko 文件 模块元信息 MODULE_LICENSE("GPL") MODULE_AUTHOR / MODULE_DESCRIPTION 初始化入口 static int __init my_init(void) module_init(my_init); 清理出口 static void __exit my_exit(void) module_exit(my_exit); 功能实现代码 GPIO 操作 / 文件操作 / 中断处理

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 加载与卸载的完整流程

  1. 编译模块:通过 Makefile 调用内核构建系统,生成 .ko 文件。
  2. 加载模块:sudo insmod my_module.ko,内核调用 module_init 注册的函数。
  3. 验证加载:lsmod 查看模块列表,dmesg | tail 查看内核日志。
  4. 卸载模块:sudo rmmod my_module,内核调用 module_exit 注册的函数。
  5. 确认卸载:再次 lsmod 确认模块已从列表中移除。
注意: 如果模块正在被其他模块或进程引用,rmmod 会失败并返回 "Module is in use"。此时需要先解除依赖关系,或使用 modprobe -r 递归卸载。
模块加载与卸载生命周期 用户态(Shell 命令) 内核态(Kernel Space) insmod 加载 .ko 文件 module_init() 执行初始化 设备注册 GPIO/字符设备 运行中 提供服务 module_exit() 清理资源 设备注销 释放 GPIO/内存 rmmod 卸载模块

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 引脚重新编译。
模块参数传递流程 insmod 命令行 gpio_pin=23 gpio_label="led" 内核参数解析 module_param 宏处理 模块内部使用 gpio_pin = 23 sysfs 接口 /sys/module/.../parameters/ 运行时修改 echo 25 > gpio_pin

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
编译与测试步骤:
  1. 将上述 Makefile 和 gpio_demo.c 放在同一目录。
  2. 执行 make 编译,生成 gpio_demo.ko。
  3. 执行 make install 加载模块,观察 dmesg 输出。
  4. 执行 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 开启所有日志。