正点原子 ATK-DLRK3568 开发板:按键中断 + 工作队列传参实验(完整版流程 + 功能解释)

一、实验核心功能解释

本实验基于 Linux 内核的中断上下半部机制,实现 "按键触发→中断响应→工作队列传参→参数解析打印" 的完整流程,核心功能拆解如下:

  1. 中断上半部:按键按下触发 GPIO 中断,快速响应(仅调度工作队列,不做复杂处理),避免阻塞中断。
  2. 工作队列传参:通过自定义结构体封装参数(如按键 ID、触发次数),将参数随工作项提交到工作队列,解决中断上下半部数据传递问题。
  3. 中断下半部 :工作线程执行工作函数,通过container_of宏解析自定义结构体,提取参数并打印,同时加入延迟消抖,过滤按键机械抖动导致的误触发。
  4. 硬件适配:基于 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,无默认外设占用,安全可用),接线如下:

  1. 按键的一端引脚 → 开发板 "用户扩展 IO" 的GPIO0_A4引脚(查看开发板丝印,对应 JP11 排针的 "GPIO0_A4");
  2. 按键的另一端引脚 → 开发板 "GND" 引脚(任意公共地,如 JP11 排针的 GND);
  3. (可选)若开发板 GPIO 无内部上拉(ATK-DLRK3568 默认内部上拉),需在GPIO0_A43.3V之间接 10KΩ 上拉电阻(实际可省略)。

注意:开发板 GPIO 工作电压为3.3V,切勿接 5V,避免烧毁 GPIO!

1.3 软件环境准备
  1. 内核源码:获取与开发板匹配的 Linux 内核源码(推荐 4.19.232 版本,与开发板出厂内核一致,避免驱动兼容性问题);
  2. 交叉编译器 :安装arm-linux-gnueabihf-gcc(开发板为 ARM 架构,推荐版本 6.3.1,与内核编译工具链一致);
  3. 调试工具: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 编译驱动模块
  1. key_workqueue_param.cMakefile放入同一文件夹;

  2. 打开终端,进入该文件夹,执行编译命令:

    复制代码
    make  # 若内核路径或编译器错误,需修改Makefile后重新编译
  3. 编译成功后,文件夹会生成key_workqueue_param.ko(驱动模块文件)。

3.2 开发板测试步骤
  1. 连接开发板

    • 用 12V 电源给开发板供电,打开电源开关(蓝色电源灯 PWR1 点亮);
    • 用 USB-TypeC 线连接开发板 "调试串口" 与 PC,打开串口软件(波特率 115200,无校验位)。
  2. 传输驱动模块

    • 通过 U 盘 / SSH 将key_workqueue_param.ko传到开发板(如/userdata目录)。
  3. 加载驱动并测试

    • 开发板终端执行以下命令:

      复制代码
      # 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
  4. 卸载驱动(测试完成后)

    • 新打开一个开发板终端,执行卸载命令:

      复制代码
      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)

三、实验关键模块详解

  1. 自定义工作项结构体(struct work_data

    • 必须包含struct work_struct(内核工作项的核心),其他为自定义参数;
    • 作用:将中断上下文的参数(如按键 ID、触发次数)传递到工作队列下半部。
  2. container_of宏的作用

    • 内核常用宏,通过 "结构体成员指针" 反向获取 "整个结构体指针";
    • 本实验中,从work_struct *work获取struct work_data *pdata,从而读取key_idtrigger_cnt
  3. 中断上下半部分工

    • 上半部(key_interrupt_handler):仅做 "调度工作队列",耗时 < 1ms,避免阻塞中断;
    • 下半部(test_work_handler):做 "参数解析 + 打印 + 消抖",允许耗时操作(如msleep)。
  4. 开发板适配关键点

    • GPIO 编号:需根据开发板手册映射(GPIO0_A4→内核编号 4),不可随意填写;
    • 中断触发方式:内部上拉 GPIO 默认高电平,按键按下为低电平,故用IRQF_TRIGGER_FALLING(下降沿触发)。

通过以上流程,可完整实现 "按键中断 + 工作队列传参" 功能,同时适配正点原子 ATK-DLRK3568 开发板的硬件特性,兼顾功能正确性与实操性。

相关推荐
HLC++15 小时前
Linux的进程间通信
android·linux·服务器
华清远见IT开放实验室16 小时前
实验室建设案例 | 石家庄科技信息职业学院嵌入式实验室——从底层硬件到系统应用,一所应用型高校的嵌入式人才培养这样落地
linux·arm开发·stm32·嵌入式硬件·高校·实验室建设
groundhappy19 小时前
idalib安装和codex ida-mcp配置
linux·开发语言·python
通信小小昕19 小时前
Ubuntu 26.04 中文输入法安装
linux·运维·ubuntu
张小姐的猫21 小时前
【Linux】网络编程 —— HTTP协议(上)
linux·运维·服务器·网络·http·单例模式·策略模式
栩栩云生21 小时前
AI 写代码犯的错,早被写进了错题集
linux·安全·ai编程
imc.111 天前
linux基础IO
linux·运维·服务器
BelongPanda1 天前
Linux Nginx 纯手动 Let‘s Encrypt 泛域名证书配置教程
linux·nginx
酷可达拉斯1 天前
Linux操作系统-shell编程(0)
linux·运维·服务器·python·云计算
2301_777998341 天前
Linux中断机制:操作系统如何高效运行
linux·运维·服务器