一、实验核心功能解释
本实验基于 Linux 内核的中断上下半部机制,实现 "按键触发→中断响应→工作队列传参→参数解析打印" 的完整流程,核心功能拆解如下:
- 中断上半部:按键按下触发 GPIO 中断,快速响应(仅调度工作队列,不做复杂处理),避免阻塞中断。
- 工作队列传参:通过自定义结构体封装参数(如按键 ID、触发次数),将参数随工作项提交到工作队列,解决中断上下半部数据传递问题。
- 中断下半部 :工作线程执行工作函数,通过
container_of宏解析自定义结构体,提取参数并打印,同时加入延迟消抖,过滤按键机械抖动导致的误触发。 - 硬件适配:基于 ATK-DLRK3568 开发板的 GPIO 资源,确保硬件接线与内核 GPIO 编号匹配,避免资源冲突。
二、实验完整版流程(分 4 大阶段)
阶段 1:实验准备(硬件 + 软件环境)
1.1 硬件材料清单
| 材料 | 数量 | 用途说明 |
|---|---|---|
| ATK-DLRK3568 开发板 | 1 | 核心硬件平台,提供 GPIO、电源、调试串口 |
| 外部按键(轻触开关) | 1 | 触发中断的输入设备 |
| 杜邦线(母对母) | 2 | 连接按键与开发板 GPIO / 地 |
| 12V 开发板配套电源 | 1 | 给开发板供电(必用配套电源,避免功率不足) |
| USB-TypeC 线 | 1 | 连接开发板调试串口(CH340C 芯片),查看日志 |
1.2 硬件接线步骤(关键!)
开发板选用GPIO0_A4(内核 GPIO 编号 = 4) 作为按键中断引脚(该引脚属于用户扩展 IO,无默认外设占用,安全可用),接线如下:
- 按键的一端引脚 → 开发板 "用户扩展 IO" 的
GPIO0_A4引脚(查看开发板丝印,对应 JP11 排针的 "GPIO0_A4"); - 按键的另一端引脚 → 开发板 "GND" 引脚(任意公共地,如 JP11 排针的 GND);
- (可选)若开发板 GPIO 无内部上拉(ATK-DLRK3568 默认内部上拉),需在
GPIO0_A4与3.3V之间接 10KΩ 上拉电阻(实际可省略)。
注意:开发板 GPIO 工作电压为3.3V,切勿接 5V,避免烧毁 GPIO!
1.3 软件环境准备
- 内核源码:获取与开发板匹配的 Linux 内核源码(推荐 4.19.232 版本,与开发板出厂内核一致,避免驱动兼容性问题);
- 交叉编译器 :安装
arm-linux-gnueabihf-gcc(开发板为 ARM 架构,推荐版本 6.3.1,与内核编译工具链一致); - 调试工具:PC 端安装串口调试软件(如 SecureCRT、Putty),波特率设为 115200(开发板调试串口默认波特率)。
阶段 2:软件开发(驱动代码 + 编译脚本)
2.1 驱动代码编写(key_workqueue_param.c)
核心逻辑:自定义工作项结构体(含参数)→ 申请 GPIO / 中断 → 初始化工作队列 → 中断触发调度工作 → 工作函数解析参数打印。
cpp
#include <linux/init.h>
#include <linux/module.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>
#include <linux/delay.h>
// 1. 自定义工作项结构体:封装要传递的参数(按键ID、触发次数)
struct work_data {
struct work_struct test_work; // 必须包含work_struct,作为工作项核心
int key_id; // 参数1:按键ID(区分多个按键)
int trigger_cnt; // 参数2:按键触发次数
};
// 全局变量:工作队列、工作项、GPIO/中断号
struct workqueue_struct *test_workqueue;
struct work_data test_work_item;
int gpio_num = 4; // 开发板GPIO0_A4对应的内核编号(关键!)
int irq_num;
int trigger_count = 0; // 记录按键总触发次数
// 3. 工作函数(中断下半部):解析参数并处理
void test_work_handler(struct work_struct *work) {
// 通过container_of宏,从work_struct指针反向获取自定义结构体指针
struct work_data *pdata = container_of(work, struct work_data, test_work);
// 消抖:延迟10ms(避开按键机械抖动窗口期)
msleep(10);
// 读取GPIO电平,确认按键真按下(避免误触发)
int key_level = gpio_get_value(gpio_num);
if (key_level == 0) { // 低电平表示按键按下(内部上拉)
pdata->trigger_cnt++; // 更新触发次数
// 打印参数(内核日志,通过dmesg查看)
printk("[Key Workqueue] 按键ID:%d,当前触发次数:%d,按键电平:%d\n",
pdata->key_id, pdata->trigger_cnt, key_level);
trigger_count = pdata->trigger_cnt; // 同步全局计数
}
}
// 2. 中断处理函数(中断上半部):快速响应,调度工作队列
irqreturn_t key_interrupt_handler(int irq, void *dev_id) {
printk("[Key Interrupt] 检测到按键触发,调度工作队列...\n");
// 给工作项赋值(每次触发更新参数)
test_work_item.key_id = 1; // 单个按键,ID设为1
test_work_item.trigger_cnt = trigger_count;
// 提交工作项到工作队列(触发下半部执行)
queue_work(test_workqueue, &test_work_item.test_work);
return IRQ_HANDLED; // 中断处理完成
}
// 4. 驱动初始化函数(模块加载时执行)
static int __init key_workqueue_init(void) {
int ret;
// ① 申请GPIO(避免资源冲突)
ret = gpio_request(gpio_num, "key_gpio_0_a4");
if (ret < 0) {
printk("GPIO%d申请失败!错误码:%d\n", gpio_num, ret);
return ret;
}
// ② 配置GPIO为输入模式(按键为输入设备)
ret = gpio_direction_input(gpio_num);
if (ret < 0) {
printk("GPIO%d配置输入失败!错误码:%d\n", gpio_num, ret);
goto err_gpio_free; // 失败则释放已申请的GPIO
}
// ③ 将GPIO映射为中断号(开发板GPIO0_A4对应中断号由内核自动分配)
irq_num = gpio_to_irq(gpio_num);
printk("GPIO%d映射的中断号:%d\n", gpio_num, irq_num);
// ④ 申请中断(触发方式:下降沿→按键按下时电平从1→0)
ret = request_irq(irq_num, key_interrupt_handler,
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"key_irq", NULL);
if (ret < 0) {
printk("中断%d申请失败!错误码:%d\n", irq_num, ret);
goto err_gpio_free;
}
// ⑤ 创建工作队列(自定义队列,避免与系统队列冲突)
test_workqueue = create_workqueue("key_workqueue");
if (!test_workqueue) {
printk("工作队列创建失败!\n");
ret = -ENOMEM;
goto err_irq_free;
}
// ⑥ 初始化工作项(绑定工作函数)
INIT_WORK(&test_work_item.test_work, test_work_handler);
// ⑦ 初始化工作项参数
test_work_item.key_id = 1;
test_work_item.trigger_cnt = 0;
trigger_count = 0;
printk("按键中断+工作队列传参驱动加载成功!\n");
return 0;
// 错误处理:资源回滚
err_irq_free:
free_irq(irq_num, NULL);
err_gpio_free:
gpio_free(gpio_num);
return ret;
}
// 5. 驱动退出函数(模块卸载时执行)
static void __exit key_workqueue_exit(void) {
// 释放资源:工作队列→中断→GPIO
cancel_work_sync(&test_work_item.test_work); // 同步取消工作项
destroy_workqueue(test_workqueue); // 销毁工作队列
free_irq(irq_num, NULL); // 释放中断
gpio_free(gpio_num); // 释放GPIO
printk("按键中断+工作队列传参驱动卸载成功!\n");
}
// 模块加载/卸载入口
module_init(key_workqueue_init);
module_exit(key_workqueue_exit);
MODULE_LICENSE("GPL"); // 必须声明GPL协议,否则内核拒绝加载
MODULE_DESCRIPTION("Key Interrupt + Workqueue Param Demo for ATK-DLRK3568");
2.2 编译脚本编写(Makefile)
需指定开发板内核源码路径、交叉编译器,确保编译出适配 ARM 架构的驱动模块:
makefile
# 内核源码路径:替换为你的RK3568内核源码绝对路径
KERNELDIR ?= /home/user/rk3568_kernel_4.19.232
# 当前驱动代码路径(自动获取)
PWD := $(shell pwd)
# 目标模块名(与驱动文件名一致,不含.c)
obj-m += key_workqueue_param.o
# 编译指令:ARM架构,交叉编译器为arm-linux-gnueabihf-
all:
make -C $(KERNELDIR) M=$(PWD) modules \
ARCH=arm \
CROSS_COMPILE=arm-linux-gnueabihf-
# 清理编译产物
clean:
make -C $(KERNELDIR) M=$(PWD) clean
阶段 3:编译与测试(开发板实操)
3.1 编译驱动模块
-
将
key_workqueue_param.c和Makefile放入同一文件夹; -
打开终端,进入该文件夹,执行编译命令:
make # 若内核路径或编译器错误,需修改Makefile后重新编译 -
编译成功后,文件夹会生成
key_workqueue_param.ko(驱动模块文件)。
3.2 开发板测试步骤
-
连接开发板:
- 用 12V 电源给开发板供电,打开电源开关(蓝色电源灯 PWR1 点亮);
- 用 USB-TypeC 线连接开发板 "调试串口" 与 PC,打开串口软件(波特率 115200,无校验位)。
-
传输驱动模块:
- 通过 U 盘 / SSH 将
key_workqueue_param.ko传到开发板(如/userdata目录)。
- 通过 U 盘 / SSH 将
-
加载驱动并测试:
-
开发板终端执行以下命令:
# 1. 切换到驱动所在目录 cd /userdata # 2. 加载驱动模块 insmod key_workqueue_param.ko # 3. 实时查看内核日志(观察按键触发效果) dmesg -w -
触发按键 :按下连接在 GPIO0_A4 上的外部按键,串口日志会输出:
plaintext
[Key Interrupt] 检测到按键触发,调度工作队列... [Key Workqueue] 按键ID:1,当前触发次数:1,按键电平:0 [Key Interrupt] 检测到按键触发,调度工作队列... [Key Workqueue] 按键ID:1,当前触发次数:2,按键电平:0
-
-
卸载驱动(测试完成后):
-
新打开一个开发板终端,执行卸载命令:
rmmod key_workqueue_param -
串口日志会输出 "驱动卸载成功",确认资源已释放。
-
阶段 4:常见问题排查
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 驱动加载失败(insmod 报错) | 内核版本不匹配 / 编译器架构错误 | 确认内核路径正确,交叉编译器为 arm-linux-gnueabihf- |
| 按键无响应(dmesg 无日志) | GPIO 接线错误 / 中断号映射失败 | 重新检查接线(GPIO0_A4→GND),用gpio_to_irq(4)确认中断号 |
| 多次触发(一次按键打印多条) | 未消抖 / 触发方式错误 | 在工作函数加msleep(10),确认中断触发方式为IRQF_TRIGGER_FALLING |
| 内核报错 "GPIO 申请失败" | GPIO 被其他外设占用(如用户按键 ADC) | 更换未占用的 GPIO(如 GPIO0_B3,内核编号 11) |
三、实验关键模块详解
-
自定义工作项结构体(
struct work_data):- 必须包含
struct work_struct(内核工作项的核心),其他为自定义参数; - 作用:将中断上下文的参数(如按键 ID、触发次数)传递到工作队列下半部。
- 必须包含
-
container_of宏的作用:- 内核常用宏,通过 "结构体成员指针" 反向获取 "整个结构体指针";
- 本实验中,从
work_struct *work获取struct work_data *pdata,从而读取key_id和trigger_cnt。
-
中断上下半部分工:
- 上半部(
key_interrupt_handler):仅做 "调度工作队列",耗时 < 1ms,避免阻塞中断; - 下半部(
test_work_handler):做 "参数解析 + 打印 + 消抖",允许耗时操作(如msleep)。
- 上半部(
-
开发板适配关键点:
- GPIO 编号:需根据开发板手册映射(GPIO0_A4→内核编号 4),不可随意填写;
- 中断触发方式:内部上拉 GPIO 默认高电平,按键按下为低电平,故用
IRQF_TRIGGER_FALLING(下降沿触发)。
通过以上流程,可完整实现 "按键中断 + 工作队列传参" 功能,同时适配正点原子 ATK-DLRK3568 开发板的硬件特性,兼顾功能正确性与实操性。