74、IMX6ULL按键驱动开发:Platform总线+中断顶底半部+阻塞式读取

IMX6ULL按键驱动开发:Platform总线+中断顶底半部+阻塞式读取

在嵌入式Linux驱动开发中,按键驱动是基础且典型的外设驱动场景,核心需要解决中断触发耗时任务解耦用户层无轮询读取 三个问题。本次基于IMX6ULL开发板,结合Linux内核的GPIO子系统、中断子系统、Platform总线框架,分别实现taskletworkqueue 两种中断底半部的按键驱动,并采用阻塞式读取实现用户层的高效按键状态获取,同时理解中断顶/底半部、进程/中断上下文的核心差异。

一、核心知识点回顾

在解析代码前,先回顾本次驱动开发用到的Linux内核核心子系统和概念,也是嵌入式驱动开发的高频考点:

1. 中断顶/底半部

Linux中断处理采用顶半部(上半部)+底半部 的拆分设计,核心原则是顶半部快进快出,底半部处理非紧急耗时任务

  • 顶半部 :即中断服务程序(ISR),运行在中断上下文 ,处理最紧急的工作(如标记中断、调度底半部),不能阻塞、不能休眠、不能被打断
  • 底半部 :运行在软中断/tasklet/workqueue ,处理非紧急、可能耗时的工作,tasklet/软中断仍在中断上下文(不可阻塞),workqueue运行在进程上下文(可阻塞、可休眠)

2. 关键内核子系统函数

子系统 核心函数 功能说明
GPIO子系统 of_get_named_gpio/gpio_free 从设备树获取GPIO/释放GPIO
中断子系统 request_irq/free_irq/disable_irq 申请/释放/关闭中断
中断底半部 tasklet_init/tasklet_schedule 初始化/调度tasklet
INIT_WORK/schedule_work 初始化/调度workqueue
阻塞实现 init_waitqueue_head 初始化等待队列头
wait_event_interruptible/wake_up_interruptible 阻塞等待/唤醒队列
Platform总线 of_device_id/ platform_driver_register 设备树匹配/注册平台驱动
杂项设备 misc_register/misc_deregister 注册/注销杂项设备(主设备号10)

3. 阻塞式读取

通过等待队列 实现,当无按键中断时,用户层的read操作会被阻塞,直到按键触发中断、底半部唤醒等待队列后,read才会返回按键状态,避免用户层轮询占用CPU资源,是嵌入式驱动中优化用户层调用的常用方式。

4. 杂项设备(miscdevice)

属于简化的字符设备,主设备号固定为10 ,无需手动申请设备号,通过MISC_DYNAMIC_MINOR动态分配次设备号,大幅简化字符设备的注册流程,适合按键、LED等简单外设。

二、驱动整体架构

本次提供的两段按键驱动代码,核心架构完全一致 ,仅中断底半部的实现分别采用taskletworkqueue,整体架构如下:

复制代码
Platform驱动注册 → 设备树匹配成功 → probe函数执行 → 注册misc设备 → 解析设备树GPIO/中断 → 申请中断(顶半部)→ 初始化等待队列/底半部
→ 按键触发中断 → 顶半部调度底半部 → 底半部修改唤醒条件 → 唤醒等待队列 → 用户层read解除阻塞并获取按键状态

两段代码的共性模块:

  1. 头文件引入:包含内核驱动、Platform、GPIO、中断、等待队列等核心头文件;
  2. 全局变量定义:GPIO号、中断号、等待队列头、唤醒条件(condition);
  3. 字符设备操作集:file_operations实现open/read/closewrite暂未实现;
  4. 杂项设备定义:指定设备名、操作集,动态分配次设备号;
  5. Platform驱动:定义probe/remove函数、设备树匹配表(compatible="pt-key");
  6. 模块入口/出口:注册/注销Platform驱动,遵循GPL协议。

以下先解析共性核心代码 ,再单独分析taskletworkqueue的差异实现。

三、共性核心代码解析

1. 全局变量与设备定义

c 复制代码
#define DEV_NAME "key"
static int key_gpio;        // 按键GPIO号
static int key_irq;         // 按键中断号
static wait_queue_head_t wq;// 等待队列头
static int condition;       // 阻塞唤醒条件(0:阻塞,1:唤醒)
static struct miscdevice misc_dev = { // 杂项设备定义
    .minor = MISC_DYNAMIC_MINOR, // 动态分配次设备号
    .name = DEV_NAME,            // 设备名,/dev/key
    .fops = &fops                // 字符设备操作集
};
// 设备树匹配表,与设备树的compatible匹配
static struct of_device_id key_table[] = {
    {.compatible = "pt-key"},
    {} // 空元素结束,必须加
};
// Platform驱动定义
static struct platform_driver pdrv = {
    .probe = probe,
    .remove = remove,
    .driver = {
        .name = DEV_NAME,
        .of_match_table = key_table // 设备树匹配
    }
};

关键说明condition是阻塞唤醒的核心标志,用户层read时会将其置0并阻塞,底半部处理时置1并唤醒等待队列。

2. 字符设备操作集

实现open/read/close,核心是read阻塞式读取

c 复制代码
static struct file_operations fops = {
    .owner = THIS_MODULE, // 归属本模块,防止模块卸载时被引用
    .open = open,
    .read = read,
    .write = write,
    .release = close      // close对应release
};
// open函数:仅打印日志,无实际逻辑
static int open(struct inode * inode, struct file * file) {
    printk("key  open\n");
    return 0;
}
// read函数:阻塞式读取核心
static ssize_t read(struct file * file, char __user * buf, size_t size, loff_t * loff) {
    int ret = 0;
    int status = 0;
    printk("key  read\n");
    condition = 0; // 置0,准备阻塞
    // 阻塞等待:当condition为1时解除阻塞,支持信号中断
    wait_event_interruptible(wq, condition);
    status = 1; // 按键触发,状态置1
    // 将按键状态拷贝到用户层,__user表示用户层地址
    ret = copy_to_user(buf, &status, sizeof(status));
    return sizeof(status); // 返回读取的字节数
}
// close函数:仅打印日志
static int close(struct inode * inode, struct file * file) {
    printk("key  close\n");
    return 0;
}

关键说明

  • wait_event_interruptible(wq, condition):核心阻塞函数,第一个参数是等待队列头,第二个参数是唤醒条件,仅当condition为真时解除阻塞,且支持被信号(如Ctrl+C)中断;
  • copy_to_user:内核空间向用户空间拷贝数据,是驱动与用户层交互的标准函数,禁止直接访问用户层地址。

3. probe函数(Platform驱动核心)

当Platform总线匹配设备树成功后,自动执行probe函数,完成驱动初始化的所有核心工作,是本次驱动的核心函数,流程如下:

复制代码
注册misc设备 → 查找设备树节点 → 获取GPIO → 解析中断号 → 申请中断 → 初始化等待队列 → 初始化底半部

代码解析:

c 复制代码
static int arg = 100; // 中断传递的参数,用于中断顶半部校验
static int probe(struct platform_device * pdev) {
    struct device_node * pdts; // 设备树节点指针
    int ret = misc_register(&misc_dev); // 注册杂项设备
    if(ret) goto err_misc_register; // 错误处理,跳转到对应标签
    
    // 1. 查找设备树节点:路径为/ptkey
    pdts = of_find_node_by_path("/ptkey");
    if(NULL == pdts) {
        ret = PTR_ERR(pdts);
        goto err_of_find;
    }
    // 2. 从设备树获取GPIO:属性名ptkey-gpio,索引0
    key_gpio = of_get_named_gpio(pdts, "ptkey-gpio", 0);
    if(key_gpio < 0) {
        ret = key_gpio;
        goto err_of_find;
    }
    // 3. 从设备树解析中断号并映射
    key_irq = irq_of_parse_and_map(pdts, 0);
    if(key_irq < 0) {
        ret = key_irq;
        goto err_irq_map;
    }
    // 4. 申请中断:中断号、顶半部处理函数、下降沿触发、中断名、传递参数
    ret = request_irq(key_irq, key_irq_handler, IRQF_TRIGGER_FALLING, "key0_irq", &arg);
    if(ret < 0) goto err_request_irq;
    
    // 5. 初始化等待队列头
    init_waitqueue_head(&wq);
    // 6. 初始化底半部(tasklet/workqueue,两段代码差异点)
    // ... 底半部初始化代码
    
    printk("#########################  key_driver  probe\n");
    return 0;

    // 错误处理:从后往前释放资源,核心原则
err_request_irq:
    printk("#########################  request irq\n");
free_irq(key_irq, &arg);
err_irq_map:
    printk("#########################  key_driver irq map\n");
err_of_find:
    printk("#########################  key_driver find node failed\n");
err_misc_register:
    misc_deregister(&misc_dev);
    printk("#########################  key_driver misc register ret = %d\n", ret);
    return ret;
}

关键说明

  1. 错误处理采用标签跳转,从后往前释放已申请的资源,避免内存泄漏/资源占用;
  2. irq_of_parse_and_map:从设备树解析中断号并完成中断映射,是设备树时代的标准用法;
  3. request_irq参数:IRQF_TRIGGER_FALLING表示下降沿触发中断 (按键按下时电平从高到低,触发中断),最后一个参数&arg是传递给中断顶半部的参数;
  4. 设备树节点路径为/ptkey,需在开发板设备树中手动添加该节点。

4. 中断顶半部处理函数

c 复制代码
static irqreturn_t key_irq_handler(int irq, void * dev) {
    int arg = *(int *)dev;
    // 参数校验,仅处理参数为100的中断
    if(100 != arg)
        return IRQ_NONE;
    // 调度底半部(tasklet/workqueue,差异点)
    // ... 底半部调度代码
    printk("irq = %d dev = %d\n", irq, arg);
    return IRQ_HANDLED; // 表示中断已正确处理
}

关键说明

  • 中断顶半部严格遵循快进快出 原则,仅做参数校验底半部调度,无其他耗时操作;
  • 返回值:IRQ_NONE表示不是本驱动的中断,IRQ_HANDLED表示正确处理中断;
  • 运行在中断上下文,禁止调用任何可能阻塞/休眠的函数。

5. remove函数与模块入口/出口

(1)remove函数

当Platform驱动卸载时执行,释放所有已申请的资源

c 复制代码
static int remove(struct platform_device * pdev) {
    disable_irq(key_irq); // 关闭中断
    free_irq(key_irq, &arg); // 释放中断
    gpio_free(key_gpio); // 释放GPIO
    misc_deregister(&misc_dev); // 注销杂项设备
    printk("#########################  key_driver  remove\n");
    return 0;
}
(2)模块入口/出口

遵循Linux内核模块的标准写法,注册/注销Platform驱动:

c 复制代码
static int __init key_driver_init(void) {
    int ret = platform_driver_register(&pdrv);
    if(ret)
        goto err_platform_driver_register;
    printk("key platform_driver_register success\n");
    return 0;
err_platform_driver_register:
    printk("key platform_driver_register failed\n");
    return ret;
}
static void __exit key_driver_exit(void) {
    platform_driver_unregister(&pdrv);
    printk("key platofrm_driver_unregister\n");
}
// 模块入口出口宏
module_init(key_driver_init);
module_exit(key_driver_exit);
MODULE_LICENSE("GPL"); // 必须声明GPL协议,否则模块加载失败

四、两种底半部实现差异解析

1. Tasklet版底半部实现

Tasklet是基于软中断实现的底半部机制 ,运行在中断上下文不可阻塞、不可休眠 ,适合处理短时间、非阻塞的底半部任务,是中断底半部的常用选择。

(1)全局变量与底半部初始化
c 复制代码
// 定义tasklet结构体
static struct tasklet_struct tsk;
// probe函数中初始化tasklet:参数为tasklet结构体、处理函数、传递的参数
tasklet_init(&tsk, key_tasklet_handler, 100);
(2)tasklet处理函数
c 复制代码
static void key_tasklet_handler(unsigned long arg) {
    condition = 1; // 修改唤醒条件为1
    wake_up_interruptible(&wq); // 唤醒等待队列,解除用户层read阻塞
    printk("key_tasklet_handler arg = %ld\n", arg);
}
(3)中断顶半部调度tasklet
c 复制代码
// 在key_irq_handler中添加:调度tasklet
tasklet_schedule(&tsk);

关键说明

  • tasklet_schedule:将tasklet加入内核的软中断调度队列,内核会在合适的时机执行其处理函数;
  • tasklet处理函数仍运行在中断上下文禁止调用ssleep、kmalloc(GFP_KERNEL) 等可能阻塞的函数;
  • 支持传递参数(unsigned long类型),灵活性高。

2. Workqueue版底半部实现

Workqueue是基于内核线程的底半部机制 ,运行在进程上下文可阻塞、可休眠 ,适合处理耗时、需要休眠的底半部任务,是唯一支持休眠的底半部机制。

(1)全局变量与底半部初始化
c 复制代码
// 定义work结构体
static struct work_struct work;
// probe函数中初始化work:参数为work结构体、处理函数
INIT_WORK(&work, key_work_func);
(2)work处理函数
c 复制代码
static void key_work_func(struct work_struct *work) {
    ssleep(1); // 休眠1秒,体现workqueue可阻塞的特性
    condition = 1; // 修改唤醒条件为1
    wake_up_interruptible(&wq); // 唤醒等待队列
    printk("key_work_func ..\n");
}
(3)中断顶半部调度workqueue
c 复制代码
// 在key_irq_handler中添加:调度workqueue
schedule_work(&work);

关键说明

  • schedule_work:将work加入内核的工作队列,内核线程会执行其处理函数;
  • work处理函数运行在进程上下文支持休眠、阻塞 ,代码中加入ssleep(1)验证了这一特性,而tasklet中禁止使用该函数;
  • 是处理耗时中断底半部任务的唯一选择,如需要访问硬件、延时处理的场景。

3. Tasklet与Workqueue核心差异

特性 Tasklet Workqueue
运行上下文 中断上下文 进程上下文
阻塞/休眠 禁止 支持
实现基础 基于软中断 基于内核线程
调度方式 tasklet_schedule schedule_work
适用场景 短时间、非阻塞的底半部任务 耗时、需要休眠的底半部任务
中断屏蔽 屏蔽当前中断线 不屏蔽任何中断

五、设备树配置

本次驱动采用设备树描述硬件资源 ,需在IMX6ULL的设备树文件(如imx6ull-alientek-emmc.dts)中添加/ptkey节点,指定按键的GPIO、中断、兼容属性,代码如下:

dts 复制代码
ptkey {
    compatible = "pt-key"; // 与驱动的of_device_id匹配,必须一致
    ptkey-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; // 按键GPIO,根据实际硬件修改
    interrupt-parent = <&gpio1>; // 中断父节点为GPIO1
    interrupts = <18 IRQ_TYPE_EDGE_FALLING>; // 18号GPIO,下降沿触发
    status = "okay"; // 启用该节点
};

配置说明

  1. compatible:必须与驱动中的of_device_id一致(pt-key),是Platform总线匹配的核心;
  2. ptkey-gpio:根据实际硬件的按键GPIO引脚修改,格式为<&GPIO组 GPIO号 电平属性>
  3. interrupts:中断配置,与request_irqIRQF_TRIGGER_FALLING对应,为下降沿触发;
  4. 设备树修改后,需重新编译设备树并烧录到开发板。

六、驱动编译与测试

1. 驱动编译

编写Makefile,指定交叉编译工具链和Linux内核源码路径(IMX6ULL适配的4.1.15内核),Makefile代码如下:

makefile 复制代码
# 针对tasklet版,obj-m += key_tasklet.o
# 针对workqueue版,obj-m += key_workqueue.o
obj-m += key_tasklet.o
# 内核源码路径,根据自己的开发环境修改
KERNELDIR := /home/linux/imx6ull/linux-imx-rel_imx_4.1.15_2.1.0_ga
# 当前目录
PWD := $(shell pwd)
# 交叉编译工具链,IMX6ULL为arm-linux-gnueabihf-
CROSS_COMPILE := arm-linux-gnueabihf-
ARCH := arm

all:
    make -C $(KERNELDIR) M=$(PWD) modules ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE)
clean:
    make -C $(KERNELDIR) M=$(PWD) clean

编译命令

bash 复制代码
make -j4 # -j4根据CPU核心数调整,加速编译

编译成功后,生成key_tasklet.kokey_workqueue.ko驱动模块。

2. 用户层测试代码

编写C语言测试程序,实现打开设备→阻塞式读取按键状态→打印结果 ,代码如下(key_test.c):

c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    int fd;
    int status = 0;
    // 打开杂项设备,与驱动的DEV_NAME一致
    fd = open("/dev/key", O_RDWR);
    if(fd < 0) {
        perror("open fail");
        return -1;
    }
    printf("open /dev/key success\n");
    while(1) {
        // 阻塞式读取按键状态
        read(fd, &status, sizeof(status));
        if(status == 1) {
            printf("key press !!!\n");
            status = 0;
        }
    }
    close(fd);
    return 0;
}

交叉编译测试程序

bash 复制代码
arm-linux-gnueabihf-gcc key_test.c -o key_test

将编译后的key_test拷贝到开发板根文件系统。

3. 开发板测试步骤

  1. 加载驱动模块 :将ko文件拷贝到开发板,执行加载命令:

    bash 复制代码
    insmod key_tasklet.ko # 加载tasklet版
    # 或insmod key_workqueue.ko # 加载workqueue版
    lsmod # 查看已加载模块
    ls /dev/key # 查看杂项设备是否创建成功
  2. 查看内核打印 :通过dmesg查看驱动初始化日志,确认probe函数执行成功:

    bash 复制代码
    dmesg | grep key
  3. 运行测试程序 :后台运行测试程序,按下按键触发中断:

    bash 复制代码
    ./key_test &

    按下开发板的按键,终端会打印key press !!!,表示驱动工作正常。

  4. 卸载驱动模块 :测试完成后,执行卸载命令:

    bash 复制代码
    rmmod key_tasklet # 无需加.ko后缀

七、开发总结与注意事项

1. 开发总结

本次按键驱动是嵌入式Linux驱动开发的经典案例,涵盖了多个核心知识点,重点掌握:

  1. Platform总线 :实现硬件与驱动分离,通过设备树描述硬件,驱动无需修改代码即可适配不同硬件,是现代Linux驱动的标准设计;
  2. 中断顶底半部 :拆分中断处理任务,顶半部快进快出,底半部处理非紧急任务,根据场景选择tasklet(短时间)或workqueue(耗时/休眠);
  3. 阻塞式读取:通过等待队列实现,避免用户层轮询,优化CPU资源占用,是嵌入式驱动的必备优化手段;
  4. 杂项设备:简化字符设备开发,适合简单外设,无需手动申请设备号;
  5. 资源管理 :驱动开发的核心原则,申请的资源必须释放,错误处理需从后往前释放资源,避免内存泄漏。

2. 关键注意事项

  1. 中断上下文禁止阻塞 :tasklet、软中断、中断顶半部均运行在中断上下文,禁止调用ssleepkmalloc(GFP_KERNEL)copy_from_user等可能阻塞的函数;
  2. 设备树匹配一致性compatible属性必须驱动与设备树完全一致,否则Platform总线无法匹配,probe函数不会执行;
  3. GPL协议声明 :内核模块必须添加MODULE_LICENSE("GPL"),否则模块加载失败;
  4. 用户层与内核层数据交互 :必须使用copy_to_user/copy_from_user,禁止直接访问用户层地址,避免内存访问错误;
  5. 中断触发方式 :根据硬件电路选择IRQF_TRIGGER_FALLING(下降沿)、IRQF_TRIGGER_RISING(上升沿)或IRQF_TRIGGER_BOTH(双边沿)。

八、拓展延伸

本次驱动仅实现了按键的按下触发,可在此基础上进行拓展:

  1. 实现按键消抖:在workqueue中添加延时消抖,避免按键机械抖动导致的多次中断;
  2. 实现按键抬起触发:修改中断触发方式为双边沿,区分按键按下和抬起;
  3. 实现多个按键驱动:修改设备树和驱动,支持多个按键的中断处理;
  4. 实现ioctl接口:在驱动中添加ioctl函数,支持用户层配置按键的中断触发方式。
相关推荐
安科士andxe4 小时前
深入解析|安科士1.25G CWDM SFP光模块核心技术,破解中长距离传输痛点
服务器·网络·5g
小高不会迪斯科6 小时前
CMU 15445学习心得(二) 内存管理及数据移动--数据库系统如何玩转内存
数据库·oracle
YJlio6 小时前
1.7 通过 Sysinternals Live 在线运行工具:不下载也能用的“云端工具箱”
c语言·网络·python·数码相机·ios·django·iphone
e***8907 小时前
MySQL 8.0版本JDBC驱动Jar包
数据库·mysql·jar
l1t7 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
CTRA王大大7 小时前
【网络】FRP实战之frpc全套配置 - fnos飞牛os内网穿透(全网最通俗易懂)
网络
小白同学_C7 小时前
Lab4-Lab: traps && MIT6.1810操作系统工程【持续更新】 _
linux·c/c++·操作系统os
今天只学一颗糖7 小时前
1、《深入理解计算机系统》--计算机系统介绍
linux·笔记·学习·系统架构
儒雅的晴天7 小时前
大模型幻觉问题
运维·服务器
testpassportcn7 小时前
AWS DOP-C02 認證完整解析|AWS DevOps Engineer Professional 考試
网络·学习·改行学it