正点原子 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 开发板的硬件特性,兼顾功能正确性与实操性。

相关推荐
小码吃趴菜2 小时前
进程间通信 管道-信号量
linux
AndyHeee2 小时前
【瑞芯微rk3576刷ubuntu根文件系统容量不足问题解决】
linux·数据库·ubuntu
李昊哲小课2 小时前
Ubuntu 24.04 在线安装 Redis 8.x 完整教程
linux·redis·ubuntu
sao.hk2 小时前
ubuntu2404,vbox,全屏显示
linux·运维·服务器
危笑ioi2 小时前
linux配置nfs在ubuntu22.04
linux·运维·服务器
社会零时工2 小时前
【ROS2】海康相机ROS2设备服务节点开发
linux·c++·相机·ros2
东城绝神2 小时前
《Linux运维总结:Ubuntu 22.04配置chrony时间同步服务》
linux·运维·ubuntu·chrony
刘程佳2 小时前
Ubuntu 系统没有识别 Pixel 6 的 USB 设备权限
linux·运维·ubuntu
wa的一声哭了3 小时前
矩阵分析 单元函数矩阵微积分和多元向量值的导数
linux·c语言·c++·线性代数·算法·矩阵·云计算