I.MX6U Linux 驱动开发篇---零基础必看!中断实验(按键中断 + 定时器消抖 + 设备树配置实战教程)--- Ubuntu20.04

I.MX6U Linux 驱动开发篇---零基础必看!中断实验(按键中断 + 定时器消抖 + 设备树配置实战教程)--- Ubuntu20.04
🎬 渡水无言个人主页渡水无言

专栏传送门: 《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》

专栏传送门: 《freertos专栏》《STM32 HAL库专栏》《linux裸机开发专栏

专栏传送门《产品测评专栏
⭐️流水不争先,争的是滔滔不绝

 📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生

| 省级优秀毕业生获得者 | ZEEKLOG新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生

在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连

目录

前言

一、Linux 中断简介

1.1 核心中断 API 函数

1.1.1 中断号

1.1.2 中断申请与释放

1.1.3 中断处理函数

1.1.4 中断使能 / 禁止(入门必备工具)

1.2 中断上半部与下半部(核心优化思路,零基础也能懂)

1.2.1、上半部

1.2.2、下半部

软中断(静态注册的轻量级下半部)

tasklet(推荐优先使用的下半部)

工作队列(支持睡眠的下半部)

二、设备树中断配置(KEY0 为例,零基础手把手教学)

2.1 中断控制器节点(GIC 控制器示例)

2.2 GPIO 作为中断控制器(GPIO5 节点示例)

2.3 设备中断节点(以 fxls8471 磁力计为例)

2.4、设备树中断属性总结

三、获取中断号(驱动开发必备)

3.1 从设备树解析中断号

3.2 从 GPIO 获取中断号

四、按键中断驱动程序编写(完整代码 + 逐行解析,零基础入门实战)

4.1、硬件原理图

4.2、程序编写

4.2.1、修改设备树文件

4.2.2、驱动代码编写(imx6uirq.c)

4.2.3、驱动代码分段分析

结构体定义(33-58 行)

中断处理函数(66-74 行)

定时器消抖函数(81-99 行)

按键 IO 初始化函数(106-155 行)

字符设备操作函数(164-210 行)

驱动入口 / 出口函数(217-272 行)

4.2.3、测试 APP 代码

五、运行测试

5.1、编译驱动程序和测试 APP

 5.2、运行测试

总结


前言

裸机开发中使用中断需要手动配置寄存器、映射中断向量表,步骤繁琐;而 Linux 内核提供了完善的中断框架,开发者只需申请中断 + 注册中断处理函数,无需关注底层寄存器操作,极大简化了开发流程。


一、Linux 中断简介

先来回顾一下裸机实验里面中断的处理方法:

①、使能中断,初始化相应的寄存器。

②、注册中断服务函数,也就是向 irqTable 数组的指定标号处写入中断服务函数。

②、中断发生以后进入 IRQ 中断服务函数,在 IRQ 中断服务函数在数组 irqTable 里面查找具体的中断处理函数,找到以后执行相应的中断处理函数。

在 Linux 内核中也提供了大量的中断相关的 API 函数,我们来看一下这些跟中断有关的API 函数。

1.1 核心中断 API 函数

1.1.1 中断号

每个中断都有一个中断号,通过中断号即可区分不同的中断,有的资料也把中断号叫做中断线。在 Linux 内核中使用一个 int 变量表示中断号。

1.1.2 中断申请与释放

在 Linux 内核中要想使用某个中断是需要申请的,request_irq 函数用于申请中断

函数作用关键参数说明
request_irq(irq, handler, flags, name, dev)申请并使能中断

irq:中断号

handler:中断处理函数。当中断发生以后就会执行此中断处理函数。

flags:中断标志(触发方式)

name:中断名(/proc/interrupts 可查)

dev:一般情况下将 dev 设置为设备结构体,dev 会传递给中断处理函数 irq_handler_t 的第二个参数。

free_irq(irq, dev)释放中断共享中断需通过dev区分不同设备

常用中断标准如下所示:

1.1.3 中断处理函数

使用 request_irq 函数申请中断的时候需要设置中断处理函数

irqreturn_t (*irq_handler_t) (int irq, void *dev_id) // 返回值:IRQ_RETVAL(IRQ_HANDLED) 表示中断已处理

第一个参数是要中断处理函数要相应的中断号。

第二个参数是一个指向 void 的指针,也就是个通用指针。

需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备, dev 也可以指向设备数据结构。

中断处理函数的返回值为 irqreturn_t 类型。

enum irqreturn { IRQ_NONE = (0 << 0), // 中断不是本设备触发 IRQ_HANDLED = (1 << 0), // 中断已被本设备处理 IRQ_WAKE_THREAD = (1 << 1), // 唤醒中断线程(线程化中断场景) }; typedef enum irqreturn irqreturn_t;

1.1.4 中断使能 / 禁止(入门必备工具)

Linux 内核提供了两类中断控制函数:

单中断控制:enable_irq() / disable_irq() / disable_irq_nosync(),使能/禁止指定的单个中断;
全局中断控制:local_irq_enable() / local_irq_disable(),用于开启 / 关闭当前处理器的整个中断系统。

注意:⚠️ 全局中断的坑(零基础一定要看!)

直接使用local_irq_disable+local_irq_enable会引发严重问题:

假设 A 任务调用local_irq_disable关闭全局中断 10 秒,执行到第 2 秒时 B 任务开始运行,B 也调用local_irq_disable关闭 3 秒。3 秒后 B 调用local_irq_enable直接打开全局中断,此时仅过去 2+3=5 秒,A 任务要关闭 10 秒的愿望就破灭了,可能导致系统崩溃!

为了避免这种 “互相干扰” 的问题,必须使用状态保存 + 恢复的成对函数:

local_irq_save(flags) // 禁止中断,并将当前中断状态保存到flags中 local_irq_restore(flags) // 恢复中断到flags保存的状态
local_irq_save:不仅关闭中断,还会把当前 CPU 的中断状态(是否已关闭)存到flags变量里;
local_irq_restore:不是简单打开中断,而是把中断状态恢复到flags记录的样子 —— 如果之前是关闭的,就保持关闭;如果之前是打开的,才打开。

✅ 最佳实践:永远用local_irq_save/local_irq_restore替代local_irq_disable/local_irq_enable,保证多任务环境下中断状态的安全,零基础也能写出稳定代码!

总结:

函数作用适用场景
enable_irq/ disable_irq使能 / 禁止指定中断
irq 就是要禁止的中断号
单个中断控制
local_irq_enable/ local_irq_disable开启 / 关闭全局中断临界区保护
local_irq_save/ local_irq_restore保存 / 恢复中断状态避免全局中断被误修改

1.2 中断上半部与下半部(核心优化思路,零基础也能懂)

中断处理的核心要求是快进快出,因此 Linux 将中断分为两部分:

上半部:中断处理函数(必须短、快),仅处理时间敏感的操作(如清除中断标志、触发下半部);
下半部:处理耗时操作(如数据解析、硬件交互),内核提供 3 种实现方式:
软中断:静态注册,适用于高频场景(如网络、定时器);
tasklet:基于软中断实现,推荐优先使用(接口简单);
工作队列:运行在进程上下文,支持睡眠(如 IIC/SPI 数据读取)。

1.2.1、上半部

主要为:中断处理函数(必须短、快),仅处理时间敏感的操作(如清除中断标志、触发下半部);

1.2.2、下半部

软中断(静态注册的轻量级下半部)

早期 Linux 内核使用 “bottom half(BH)” 实现下半部,后被软中断和 tasklet 替代(2.5 版本后 BH 已被废弃)。

Linux 内核用softirq_action结构体表示软中断:

struct softirq_action { void (*action)(struct softirq_action *); // 软中断服务函数 };

内核中定义了 10 个全局软中断(softirq_vec[NR_SOFTIRQS]),NR_SOFTIRQS是枚举类型:

enum { HI_SOFTIRQ=0, /* 高优先级软中断 */ TIMER_SOFTIRQ, /* 定时器软中断 */ NET_TX_SOFTIRQ, /* 网络数据发送软中断 */ NET_RX_SOFTIRQ, /* 网络数据接收软中断 */ BLOCK_SOFTIRQ, BLOCK_IOPOLL_SOFTIRQ, TASKLET_SOFTIRQ, /* tasklet软中断 */ SCHED_SOFTIRQ, /* 调度软中断 */ HRTIMER_SOFTIRQ, /* 高精度定时器软中断 */ RCU_SOFTIRQ, /* RCU软中断 */ NR_SOFTIRQS };

使用软中断的核心步骤:

注册软中断:open_softirq(nr, action),指定软中断类型和处理函数;
触发软中断:raise_softirq(nr),在中断上半部中调用;
内核初始化:softirq_init()会默认打开TASKLET_SOFTIRQ和HI_SOFTIRQ。

 注意:软中断必须编译时静态注册,适合高频、轻量的耗时操作。

tasklet(推荐优先使用的下半部)

tasklet 是基于软中断实现的下半部机制,接口更简单,零基础推荐优先使用

Linux 内核用tasklet_struct结构体表示 tasklet:

struct tasklet_struct { struct tasklet_struct *next; // 下一个tasklet unsigned long state; // tasklet状态 atomic_t count; // 引用计数 void (*func)(unsigned long); // tasklet执行函数 unsigned long data; // 传递给func的参数 };

使用步骤:

定义 + 初始化 tasklet:
方式 1:先定义struct tasklet_struct,再用tasklet_init()初始化;
方式 2:用宏DECLARE_TASKLET(name, func, data)一次性完成定义和初始化;
调度 tasklet:在中断上半部调用tasklet_schedule(&tasklet),让内核在合适时间执行;

示例代码

// 定义tasklet struct tasklet_struct testtasklet; // tasklet处理函数 void testtasklet_func(unsigned long data) { /* 耗时操作放在这里,比如数据解析、IIC读取 */ } // 中断处理函数(上半部) irqreturn_t test_handler(int irq, void *dev_id) { tasklet_schedule(&testtasklet); // 触发下半部 return IRQ_RETVAL(IRQ_HANDLED); } // 驱动入口初始化 static int __init xxxx_init(void) { tasklet_init(&testtasklet, testtasklet_func, data); request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev); return 0; }
工作队列(支持睡眠的下半部)

工作队列是唯一运行在进程上下文的下半部机制,允许睡眠 / 重调度,适合需要阻塞的耗时操作(如 IIC/SPI 慢速设备读取)。

Linux 内核用work_struct表示一个工作:

struct work_struct { atomic_long_t data; struct list_head entry; work_func_t func; // 工作队列处理函数 };
使用步骤:
定义 + 初始化工作:
方式 1:先定义struct work_struct,再用INIT_WORK(work, func)初始化;
方式 2:用宏DECLARE_WORK(n, f)一次性完成创建和初始化;
调度工作:调用schedule_work(&work),让内核线程处理;

示例代码

// 定义工作 struct work_struct testwork; // work处理函数 void testwork_func_t(struct work_struct *work) { /* 可睡眠的耗时操作,比如SPI读取Flash数据 */ } // 中断处理函数(上半部) irqreturn_t test_handler(int irq, void *dev_id) { /* 调度 work */ schedule_work(&testwork); // 触发下半部 return IRQ_RETVAL(IRQ_HANDLED); } // 驱动入口初始化 static int __init xxxx_init(void) { /* 初始化 work */ INIT_WORK(&testwork, testwork_func_t); /* 注册中断处理函数 */ request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev); return 0; }

二、设备树中断配置(KEY0 为例,零基础手把手教学)

使用设备树时,需要在设备树中配置中断相关属性,Linux 内核会自动读取并配置中断。其中的 intc 节点就是 I.MX6ULL 的中断控制器节点。

2.1 中断控制器节点(GIC 控制器示例)

I.MX6ULL 的 GIC 中断控制器节点(imx6ull.dtsi中的intc节点)定义了中断解析规则:

intc: interrupt-controller@00a01000 { compatible = "arm,cortex-a7-gic"; #interrupt-cells = <3>; // 每个中断源用3个cells描述 interrupt-controller; // 表示当前节点是中断控制器 reg = <0x00a01000 0x1000>, <0x00a02000 0x100>; };
compatible = "arm,cortex-a7-gic":匹配 GIC 中断控制器驱动;
#interrupt-cells = <3>:表示此控制器下的中断源需要 3 个 cell 来描述:
第 1 个 cell:中断类型(0=SPI,1=PPI);
第 2 个 cell:中断号(SPI:0~987;PPI:0~15);
第 3 个 cell:触发类型 + PPI CPU 掩码(bit [3:0]:1 = 上升沿,2 = 下降沿,4 = 高电平,8 = 低电平);
interrupt-controller:空属性,标记当前节点为中断控制器。

2.2 GPIO 作为中断控制器(GPIO5 节点示例)

GPIO 节点也可作为中断控制器,以imx6ull.dtsi中的gpio5节点为例:

gpio5: gpio@020ac000 { compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio"; reg = <0x020ac000 0x4000>; interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>, <GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>; gpio-controller; #gpio-cells = <2>; interrupt-controller; #interrupt-cells = <2>; };
interrupts:包含两条中断信息,均为 SPI 类型、高电平触发,中断号分别为 74、75;
74 对应 GPIO5_IO00~GPIO5_IO15;
75 对应 GPIO5_IO16~GPIO5_IO31;
interrupt-controller:标记gpio5为中断控制器,管理其所有 IO 的中断;
#interrupt-cells = <2>:表示此控制器下的中断源用 2 个 cell 描述(GPIO 编号 + 触发类型)。

2.3 设备中断节点(以 fxls8471 磁力计为例)

以 NXP 官方开发板的磁力计芯片fxls8471为例,其设备节点配置:

fxls8471@1e { compatible = "fsl,fxls8471"; reg = <0x1e>; position = <0>; interrupt-parent = <&gpio5>; // 指定父中断控制器为gpio5 interrupts = <0 8>; // 0=GPIO5_IO00,8=低电平触发 };
关键属性解析:
interrupt-parent:指定中断控制器(这里是gpio5);
interrupts:中断信息,0对应 GPIO5_IO00,8对应低电平触发(IRQ_TYPE_LEVEL_LOW)。

2.4、设备树中断属性总结

属性名含义
#interrupt-cells指定中断源的信息 cell 个数
interrupt-controller标记当前节点为中断控制器
interrupts指定中断号、触发方式等信息
interrupt-parent指定父中断(即中断控制器)

三、获取中断号(驱动开发必备)

中断信息已写入设备树,驱动中可通过以下函数获取中断号:

3.1 从设备树解析中断号

irq_of_parse_and_map():从设备节点的interrupts属性中提取中断号:

unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
dev:设备节点指针;
index:interrupts属性可能包含多条中断信息,用index指定要获取的条目;
返回值:对应的中断号。

3.2 从 GPIO 获取中断号

gpio_to_irq():将 GPIO 编号转换为对应的中断号(GPIO 作为中断时使用):

int gpio_to_irq(unsigned int gpio)
gpio:要获取的 GPIO 编号;
返回值:GPIO 对应的中断号。

四、按键中断驱动程序编写(完整代码 + 逐行解析,零基础入门实战)

4.1、硬件原理图

按键 KEY0 的原理图如下:

图中可以看出,按键 KEY0 是连接到 I.MX6U 的 UART1_CTS 这个 IO 上的,KEY0接了一个 10K 的上拉电阻,因此 KEY0 没有按下的时候 UART1_CTS 应该是高电平,当 KEY0按下以后 UART1_CTS 就是低电平。

4.2、程序编写

本章实验我们驱动 I.MX6U-ALPHA 开发板上的 KEY0 按键,不过我们采用中断的方式, 并且采用定时器来实现按键消抖,应用程序读取按键值并且通过终端打印出来。

4.2.1、修改设备树文件

实验使用到了按键 KEY0,按键 KEY0 使用中断模式,因此需要在“key”节点下添加 中断相关属性,添加完成以后的“key”节点内容如下所示:

key { #address-cells = <1>; #size-cells = <1>; compatible = "atkalpha-key"; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_key>; key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; /* KEY0 */ interrupt-parent = <&gpio1>; /* 中断控制器为gpio1 */ interrupts = <18 IRQ_TYPE_EDGE_BOTH>; /* 上升沿+下降沿触发 */ status = "okay"; };
key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>:指定 KEY0 为 GPIO1_IO18,低电平有效;
interrupt-parent = <&gpio1>:KEY0 的中断控制器为gpio1;
interrupts = <18 IRQ_TYPE_EDGE_BOTH>:
18:GPIO1 组的 18 号 IO(即 GPIO1_IO18);
IRQ_TYPE_EDGE_BOTH:上升沿和下降沿同时触发(KEY0 按下和释放都会触发中断)。

4.2.2、驱动代码编写(imx6uirq.c)

#include <linux/types.h> #include <linux/kernel.h> #include <linux/delay.h> #include <linux/ide.h> #include <linux/init.h> #include <linux/module.h> #include <linux/errno.h> #include <linux/gpio.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/of.h> #include <linux/of_address.h> #include <linux/of_gpio.h> #include <linux/semaphore.h> #include <linux/timer.h> #include <linux/of_irq.h> #include <linux/irq.h> #include <asm/mach/map.h> #include <asm/uaccess.h> #include <asm/io.h> /*************************************************************** Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved. 文件名 : imx6uirq.c 描述 : Linux中断驱动实验 其他 : 无 ***************************************************************/ #define IMX6UIRQ_CNT 1 /* 设备号个数 */ #define IMX6UIRQ_NAME "imx6uirq" /* 名字 */ #define KEY0VALUE 0X01 /* KEY0按键值 */ #define INVAKEY 0XFF /* 无效的按键值 */ #define KEY_NUM 1 /* 按键数量 */ /* 中断IO描述结构体 */ struct irq_keydesc { int gpio; /* gpio */ int irqnum; /* 中断号 */ unsigned char value; /* 按键对应的键值 */ char name[10]; /* 名字 */ irqreturn_t (*handler)(int, void *); /* 中断服务函数 */ }; /* imx6uirq设备结构体 */ struct imx6uirq_dev{ dev_t devid; /* 设备号 */ struct cdev cdev; /* cdev */ struct class *class; /* 类 */ struct device *device; /* 设备 */ int major; /* 主设备号 */ int minor; /* 次设备号 */ struct device_node *nd; /* 设备节点 */ atomic_t keyvalue; /* 有效的按键键值 */ atomic_t releasekey; /* 标记是否完成一次完成的按键,包括按下和释放 */ struct timer_list timer;/* 定义一个定时器*/ struct irq_keydesc irqkeydesc[KEY_NUM]; /* 按键描述数组 */ unsigned char curkeynum; /* 当前的按键号 */ }; struct imx6uirq_dev imx6uirq; /* irq设备 */ /* @description : 中断服务函数,开启定时器,延时10ms, * 定时器用于按键消抖。 * @param - irq : 中断号 * @param - dev_id : 设备结构。 * @return : 中断执行结果 */ static irqreturn_t key0_handler(int irq, void *dev_id) { struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id; dev->curkeynum = 0; dev->timer.data = (volatile long)dev_id; mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10)); /* 10ms定时 */ return IRQ_RETVAL(IRQ_HANDLED); } /* @description : 定时器服务函数,用于按键消抖,定时器到了以后 * 再次读取按键值,如果按键还是处于按下状态就表示按键有效。 * @param - arg : 设备结构变量 * @return : 无 */ void timer_function(unsigned long arg) { unsigned char value; unsigned char num; struct irq_keydesc *keydesc; struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg; num = dev->curkeynum; keydesc = &dev->irqkeydesc[num]; value = gpio_get_value(keydesc->gpio); /* 读取IO值 */ if(value == 0){ /* 按下按键 */ atomic_set(&dev->keyvalue, keydesc->value); } else{ /* 按键松开 */ atomic_set(&dev->keyvalue, 0x80 | keydesc->value); atomic_set(&dev->releasekey, 1); /* 标记松开按键,即完成一次完整的按键过程 */ } } /* * @description : 按键IO初始化 * @param : 无 * @return : 无 */ static int keyio_init(void) { unsigned char i = 0; int ret = 0; imx6uirq.nd = of_find_node_by_path("/key"); if (imx6uirq.nd== NULL){ printk("key node not find!\r\n"); return -EINVAL; } /* 提取GPIO */ for (i = 0; i < KEY_NUM; i++) { imx6uirq.irqkeydesc[i].gpio = of_get_named_gpio(imx6uirq.nd ,"key-gpio", i); if (imx6uirq.irqkeydesc[i].gpio < 0) { printk("can't get key%d\r\n", i); } } /* 初始化key所使用的IO,并且设置成中断模式 */ for (i = 0; i < KEY_NUM; i++) { memset(imx6uirq.irqkeydesc[i].name, 0, sizeof(imx6uirq.irqkeydesc[i].name)); /* 缓冲区清零 */ sprintf(imx6uirq.irqkeydesc[i].name, "KEY%d", i); /* 组合名字 */ gpio_request(imx6uirq.irqkeydesc[i].gpio, imx6uirq.irqkeydesc[i].name); gpio_direction_input(imx6uirq.irqkeydesc[i].gpio); imx6uirq.irqkeydesc[i].irqnum = irq_of_parse_and_map(imx6uirq.nd, i); #if 0 imx6uirq.irqkeydesc[i].irqnum = gpio_to_irq(imx6uirq.irqkeydesc[i].gpio); #endif printk("key%d:gpio=%d, irqnum=%d\r\n",i, imx6uirq.irqkeydesc[i].gpio, imx6uirq.irqkeydesc[i].irqnum); } /* 申请中断 */ imx6uirq.irqkeydesc[0].handler = key0_handler; imx6uirq.irqkeydesc[0].value = KEY0VALUE; for (i = 0; i < KEY_NUM; i++) { ret = request_irq(imx6uirq.irqkeydesc[i].irqnum, imx6uirq.irqkeydesc[i].handler, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, imx6uirq.irqkeydesc[i].name, &imx6uirq); if(ret < 0){ printk("irq %d request failed!\r\n", imx6uirq.irqkeydesc[i].irqnum); return -EFAULT; } } /* 创建定时器 */ init_timer(&imx6uirq.timer); imx6uirq.timer.function = timer_function; return 0; } /* * @description : 打开设备 * @param - inode : 传递给驱动的inode * @param - filp : 设备文件,file结构体有个叫做private_data的成员变量 * 一般在open的时候将private_data指向设备结构体。 * @return : 0 成功;其他 失败 */ static int imx6uirq_open(struct inode *inode, struct file *filp) { filp->private_data = &imx6uirq; /* 设置私有数据 */ return 0; } /* * @description : 从设备读取数据 * @param - filp : 要打开的设备文件(文件描述符) * @param - buf : 返回给用户空间的数据缓冲区 * @param - cnt : 要读取的数据长度 * @param - offt : 相对于文件首地址的偏移 * @return : 读取的字节数,如果为负值,表示读取失败 */ static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) { int ret = 0; unsigned char keyvalue = 0; unsigned char releasekey = 0; struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data; keyvalue = atomic_read(&dev->keyvalue); releasekey = atomic_read(&dev->releasekey); if (releasekey) { /* 有按键按下 */ if (keyvalue & 0x80) { keyvalue &= ~0x80; ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue)); } else { goto data_error; } atomic_set(&dev->releasekey, 0);/* 按下标志清零 */ } else { goto data_error; } return 0; data_error: return -EINVAL; } /* 设备操作函数 */ static struct file_operations imx6uirq_fops = { .owner = THIS_MODULE, .open = imx6uirq_open, .read = imx6uirq_read, }; /* * @description : 驱动入口函数 * @param : 无 * @return : 无 */ static int __init imx6uirq_init(void) { /* 1、构建设备号 */ if (imx6uirq.major) { imx6uirq.devid = MKDEV(imx6uirq.major, 0); register_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT, IMX6UIRQ_NAME); } else { alloc_chrdev_region(&imx6uirq.devid, 0, IMX6UIRQ_CNT, IMX6UIRQ_NAME); imx6uirq.major = MAJOR(imx6uirq.devid); imx6uirq.minor = MINOR(imx6uirq.devid); } /* 2、注册字符设备 */ cdev_init(&imx6uirq.cdev, &imx6uirq_fops); cdev_add(&imx6uirq.cdev, imx6uirq.devid, IMX6UIRQ_CNT); /* 3、创建类 */ imx6uirq.class = class_create(THIS_MODULE, IMX6UIRQ_NAME); if (IS_ERR(imx6uirq.class)) { return PTR_ERR(imx6uirq.class); } /* 4、创建设备 */ imx6uirq.device = device_create(imx6uirq.class, NULL, imx6uirq.devid, NULL, IMX6UIRQ_NAME); if (IS_ERR(imx6uirq.device)) { return PTR_ERR(imx6uirq.device); } /* 5、初始化按键 */ atomic_set(&imx6uirq.keyvalue, INVAKEY); atomic_set(&imx6uirq.releasekey, 0); keyio_init(); return 0; } /* * @description : 驱动出口函数 * @param : 无 * @return : 无 */ static void __exit imx6uirq_exit(void) { unsigned int i = 0; /* 删除定时器 */ del_timer_sync(&imx6uirq.timer); /* 删除定时器 */ /* 释放中断 */ for (i = 0; i < KEY_NUM; i++) { free_irq(imx6uirq.irqkeydesc[i].irqnum, &imx6uirq); gpio_free(imx6uirq.irqkeydesc[i].gpio); } cdev_del(&imx6uirq.cdev); unregister_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT); device_destroy(imx6uirq.class, imx6uirq.devid); class_destroy(imx6uirq.class); } module_init(imx6uirq_init); module_exit(imx6uirq_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("duan"); 

4.2.3、驱动代码分段分析

结构体定义(33-58 行)
33 /* 中断IO描述结构体 */ 34 struct irq_keydesc { 35 int gpio; /* gpio */ 36 int irqnum; /* 中断号 */ 37 unsigned char value; /* 按键对应的键值 */ 38 char name[10]; /* 名字 */ 39 irqreturn_t (*handler)(int, void *); /* 中断服务函数 */ 40 }; 42 /* imx6uirq设备结构体 */ 43 struct imx6uirq_dev{ 44 dev_t devid; /* 设备号 */ 45 struct cdev cdev; /* cdev */ 46 struct class *class; /* 类 */ 47 struct device *device; /* 设备 */ 48 int major; /* 主设备号 */ 49 int minor; /* 次设备号 */ 50 struct device_node *nd; /* 设备节点 */ 51 atomic_t keyvalue; /* 有效的按键键值 */ 52 atomic_t releasekey; /* 标记是否完成一次完成的按键,包括按下和释放 */ 53 struct timer_list timer;/* 定义一个定时器*/ 54 struct irq_keydesc irqkeydesc[KEY_NUM]; /* 按键描述数组 */ 55 unsigned char curkeynum; /* 当前的按键号 */ 56 }; 58 struct imx6uirq_dev imx6uirq; /* irq设备 */
irq_keydesc 结构体(34 行):
封装单个按键的所有关键信息,包括 GPIO 号、中断号、键值、中断处理函数;
便于扩展多按键(只需增加数组元素,无需修改核心逻辑)。
imx6uirq_dev 结构体(43 行):
字符设备驱动标准成员(44-49 行):设备号、cdev、类 / 设备节点;
设备树成员(50 行):指向设备树中 key 节点的指针;
原子变量(51-52 行):keyvalue存储按键值,releasekey标记完整按键过程(按下 + 释放),原子变量避免多线程竞态;
定时器成员(53 行):用于按键消抖的定时器;
按键描述数组(54 行):支持多按键扩展;
全局设备实例(58 行):整个驱动的核心数据载体。
中断处理函数(66-74 行)
66 static irqreturn_t key0_handler(int irq, void *dev_id) 67 { 68 struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id; 69 70 dev->curkeynum = 0; 71 dev->timer.data = (volatile long)dev_id; 72 mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10)); /* 10ms定时 */ 73 return IRQ_RETVAL(IRQ_HANDLED); 74 }
函数作用:KEY0 中断触发后执行的上半部处理函数,遵循 “快进快出” 原则;
关键逻辑:
68 行:将dev_id转换为设备结构体指针,获取全局设备实例;
70 行:标记当前触发中断的是第 0 个按键(KEY0);
71 行:设置定时器私有数据为设备结构体指针;
72 行:启动 10ms 定时器(msecs_to_jiffies将毫秒转换为内核节拍数),用于按键消抖;
73 行:标准中断返回值,告知内核中断已处理完成。
定时器消抖函数(81-99 行)
81 void timer_function(unsigned long arg) 82 { 83 unsigned char value; 84 unsigned char num; 85 struct irq_keydesc *keydesc; 86 struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg; 87 88 num = dev->curkeynum; 89 keydesc = &dev->irqkeydesc[num]; 90 91 value = gpio_get_value(keydesc->gpio); /* 读取IO值 */ 92 if(value == 0){ /* 按下按键 */ 93 atomic_set(&dev->keyvalue, keydesc->value); 94 } 95 else{ /* 按键松开 */ 96 atomic_set(&dev->keyvalue, 0x80 | keydesc->value); 97 atomic_set(&dev->releasekey, 1); /* 标记松开按键,即完成一次完整的按键过程 */ 98 } 99 }
函数作用:10ms 定时到期后执行,读取 GPIO 电平确认按键真实状态(消抖核心);
关键逻辑:
86 行:将定时器参数转换为设备结构体指针;
91 行:读取 GPIO 电平(0 = 按下,1 = 松开);
92-94 行:按键按下时,设置keyvalue为 KEY0VALUE(0x01);
95-98 行:按键松开时,keyvalue最高位置 1(0x81),并设置releasekey为 1,标记一次完整按键过程;
原子操作:atomic_set保证多线程下按键状态的原子性,避免竞态。
按键 IO 初始化函数(106-155 行)
106 static int keyio_init(void) 107 { 108 unsigned char i = 0; 109 int ret = 0; 110 111 imx6uirq.nd = of_find_node_by_path("/key"); 112 if (imx6uirq.nd== NULL){ 113 printk("key node not find!\r\n"); 114 return -EINVAL; 115 } 116 117 /* 提取GPIO */ 118 for (i = 0; i < KEY_NUM; i++) { 119 imx6uirq.irqkeydesc[i].gpio = of_get_named_gpio(imx6uirq.nd ,"key-gpio", i); 120 if (imx6uirq.irqkeydesc[i].gpio < 0) { 121 printk("can't get key%d\r\n", i); 122 } 123 } 124 125 /* 初始化key所使用的IO,并且设置成中断模式 */ 126 for (i = 0; i < KEY_NUM; i++) { 127 memset(imx6uirq.irqkeydesc[i].name, 0, sizeof(imx6uirq.irqkeydesc[i].name)); /* 缓冲区清零 */ 128 sprintf(imx6uirq.irqkeydesc[i].name, "KEY%d", i); /* 组合名字 */ 129 gpio_request(imx6uirq.irqkeydesc[i].gpio, imx6uirq.irqkeydesc[i].name); 130 gpio_direction_input(imx6uirq.irqkeydesc[i].gpio); 131 imx6uirq.irqkeydesc[i].irqnum = irq_of_parse_and_map(imx6uirq.nd, i); 132 #if 0 133 imx6uirq.irqkeydesc[i].irqnum = gpio_to_irq(imx6uirq.irqkeydesc[i].gpio); 134 #endif 135 printk("key%d:gpio=%d, irqnum=%d\r\n",i, imx6uirq.irqkeydesc[i].gpio, 136 imx6uirq.irqkeydesc[i].irqnum); 137 } 138 /* 申请中断 */ 139 imx6uirq.irqkeydesc[0].handler = key0_handler; 140 imx6uirq.irqkeydesc[0].value = KEY0VALUE; 141 142 for (i = 0; i < KEY_NUM; i++) { 143 ret = request_irq(imx6uirq.irqkeydesc[i].irqnum, imx6uirq.irqkeydesc[i].handler, 144 IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, imx6uirq.irqkeydesc[i].name, &imx6uirq); 145 if(ret < 0){ 146 printk("irq %d request failed!\r\n", imx6uirq.irqkeydesc[i].irqnum); 147 return -EFAULT; 148 } 149 } 150 151 /* 创建定时器 */ 152 init_timer(&imx6uirq.timer); 153 imx6uirq.timer.function = timer_function; 154 return 0; 155 }
核心解析
设备树节点查找(111 行):
of_find_node_by_path("/key"):查找设备树中路径为/key的节点;
失败则返回-EINVAL,驱动初始化失败。
GPIO 提取(118-123 行):
of_get_named_gpio:从key节点的key-gpio属性中提取 GPIO 编号;
支持多按键扩展(只需修改KEY_NUM和设备树)。
GPIO 初始化(126-137 行):
129 行:申请 GPIO 资源,避免资源冲突;
130 行:设置 GPIO 为输入模式(按键检测);
131 行:从设备树解析中断号(推荐方式);
133 行:备选方案(GPIO 转中断号),适合无设备树场景。
中断申请(142-149 行):
request_irq参数说明:
参数 1:中断号;
参数 2:中断处理函数(key0_handler);
参数 3:触发方式(上升沿 + 下降沿);
参数 4:中断名(/proc/interrupts 可查);
参数 5:传递给中断处理函数的私有数据(设备结构体);
触发方式与设备树中IRQ_TYPE_EDGE_BOTH对应。
定时器初始化(152-153 行):
init_timer:初始化定时器;
设置定时器处理函数为timer_function(消抖核心)。
字符设备操作函数(164-210 行)
164 static int imx6uirq_open(struct inode *inode, struct file *filp) 165 { 166 filp->private_data = &imx6uirq; /* 设置私有数据 */ 167 return 0; 168 } 178 static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) 179 { 180 int ret = 0; 181 unsigned char keyvalue = 0; 182 unsigned char releasekey = 0; 183 struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data; 184 185 keyvalue = atomic_read(&dev->keyvalue); 186 releasekey = atomic_read(&dev->releasekey); 187 188 if (releasekey) { /* 有按键按下 */ 189 if (keyvalue & 0x80) { 190 keyvalue &= ~0x80; 191 ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue)); 192 } else { 193 goto data_error; 194 } 195 atomic_set(&dev->releasekey, 0);/* 按下标志清零 */ 196 } else { 197 goto data_error; 198 } 199 return 0; 200 201 data_error: 202 return -EINVAL; 203 } 205 /* 设备操作函数 */ 206 static struct file_operations imx6uirq_fops = { 207 .owner = THIS_MODULE, 208 .open = imx6uirq_open, 209 .read = imx6uirq_read, 210 };
open 函数(164 行):
将设备结构体指针赋值给filp->private_data,便于后续 read 函数获取设备实例;
字符设备驱动标准操作,无额外逻辑。
read 函数(178 行):
185-186 行:读取原子变量,获取按键值和按键完成标记;
188 行:仅当releasekey=1(完成一次按键过程)时,才向用户空间返回数据;
189-191 行:清除keyvalue最高位(松开标记),通过copy_to_user将按键值拷贝到用户空间;
195 行:清零releasekey,准备下一次按键检测;
核心注意:copy_to_user是内核空间向用户空间拷贝数据的标准函数,不能直接赋值(内存隔离)。
驱动入口 / 出口函数(217-272 行)
217 static int __init imx6uirq_init(void) 218 { 219 /* 1、构建设备号 */ 220 if (imx6uirq.major) { 221 imx6uirq.devid = MKDEV(imx6uirq.major, 0); 222 register_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT, IMX6UIRQ_NAME); 223 } else { 224 alloc_chrdev_region(&imx6uirq.devid, 0, IMX6UIRQ_CNT, IMX6UIRQ_NAME); 225 imx6uirq.major = MAJOR(imx6uirq.devid); 226 imx6uirq.minor = MINOR(imx6uirq.devid); 227 } 228 229 /* 2、注册字符设备 */ 230 cdev_init(&imx6uirq.cdev, &imx6uirq_fops); 231 cdev_add(&imx6uirq.cdev, imx6uirq.devid, IMX6UIRQ_CNT); 232 233 /* 3、创建类 */ 234 imx6uirq.class = class_create(THIS_MODULE, IMX6UIRQ_NAME); 235 if (IS_ERR(imx6uirq.class)) { 236 return PTR_ERR(imx6uirq.class); 237 } 238 239 /* 4、创建设备 */ 240 imx6uirq.device = device_create(imx6uirq.class, NULL, imx6uirq.devid, NULL, IMX6UIRQ_NAME); 241 if (IS_ERR(imx6uirq.device)) { 242 return PTR_ERR(imx6uirq.device); 243 } 244 245 /* 5、初始化按键 */ 246 atomic_set(&imx6uirq.keyvalue, INVAKEY); 247 atomic_set(&imx6uirq.releasekey, 0); 248 keyio_init(); 249 return 0; 250 } 257 static void __exit imx6uirq_exit(void) 258 { 259 unsigned int i = 0; 260 /* 删除定时器 */ 261 del_timer_sync(&imx6uirq.timer); /* 删除定时器 */ 262 263 /* 释放中断 */ 264 for (i = 0; i < KEY_NUM; i++) { 265 free_irq(imx6uirq.irqkeydesc[i].irqnum, &imx6uirq); 266 gpio_free(imx6uirq.irqkeydesc[i].gpio); 267 } 268 cdev_del(&imx6uirq.cdev); 269 unregister_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT); 270 device_destroy(imx6uirq.class, imx6uirq.devid); 271 class_destroy(imx6uirq.class); 272 } 274 module_init(imx6uirq_init); 275 module_exit(imx6uirq_exit); 276 MODULE_LICENSE("GPL"); 277 MODULE_AUTHOR("duan");
入口函数(imx6uirq_init)
设备号处理(220-227 行):
静态指定主设备号:若major不为 0,使用register_chrdev_region注册;
动态分配设备号:若major为 0,使用alloc_chrdev_region分配,自动获取主 / 次设备号;
动态分配是推荐方式,避免设备号冲突。
字符设备注册(230-231 行):
cdev_init:初始化 cdev 结构体,绑定设备操作集;
cdev_add:将 cdev 添加到内核,完成字符设备注册。
类 / 设备创建(234-243 行):
class_create:创建类,用于自动生成设备节点;
device_create:创建设备节点(/dev/imx6uirq);
IS_ERR:检查创建是否失败,避免空指针操作。
按键初始化(246-248 行):
初始化原子变量为默认值;
调用keyio_init完成 GPIO / 中断 / 定时器初始化。
出口函数(imx6uirq_exit)
资源释放顺序:
先释放定时器(261 行):del_timer_sync等待定时器完成,避免竞态;
再释放中断 / GPIO(264-267 行):free_irq释放中断,gpio_free释放 GPIO 资源;
最后释放字符设备资源(268-271 行):注销 cdev、设备号、销毁设备 / 类;
核心原则:与初始化顺序相反,避免资源泄漏。

4.2.3、测试 APP 代码

#include "stdio.h" #include "unistd.h" #include "sys/types.h" #include "sys/stat.h" #include "fcntl.h" #include "stdlib.h" #include "string.h" #include "linux/ioctl.h" int main(int argc, char *argv[]) { int fd; int ret = 0; char *filename; unsigned char data; if (argc != 2) { printf("Error Usage!\r\n"); return -1; } filename = argv[1]; fd = open(filename, O_RDWR); if (fd < 0) { printf("Can't open file %s\r\n", filename); return -1; } while (1) { ret = read(fd, &data, sizeof(data)); if (ret >= 0 && data) { /* 读取到有效按键值 */ printf("key value = %#X\r\n", data); } } close(fd); return ret; }

第 45~53 行的 while 循环用于不断的读取按键值,如果读取到有效的按键值就将其输出到终端上。

五、运行测试

5.1、编译驱动程序和测试 APP

编写 Makefile 文件,本次实验的 Makefile 文件和之前的led实验基本一样,只是将 obj-m 变量的值改为imx6uirq.o,Makefile 内容如下所示:

KERNELDIR := /home/duan/linux/linux-imx-rel_imx_4.1.15_2.1.1_ga_alientek_v2.2 CURRENT_PATH := $(shell pwd) obj-m :=imx6uirq.o build: kernel_modules kernel_modules: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules clean: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean 

第 4 行,设置 obj-m 变量的值为imx6uirq.o。

输入如下命令编译出驱动模块文件:

make -j32

编译成功以后就会生成一个名为“imx6uirq.ko”的驱动模块文件。

输入如下命令编译出驱动模块文件:

make -j32

编译成功以后就会生成一个名为“key.ko”的驱动模块文件。

编译测试 APP

输入如下命令编译测试试imx6uirqApp.c这个测试程序:

arm-linux-gnueabihf-gcc imx6uirqApp.c -o imx6uirqApp

编译成功以后就会生成 keyApp 这个应用程序。

 5.2、运行测试

将上一小节编译出来的imx6uirq.ko 和 imx6uirqApp这两个文件拷贝到 rootfs/lib/modules/4.1.15 目录中。

sudo cp imx6uirq.ko /home/duan/linux/nfs/rootfs/lib/modules/4.1.15/ -f
sudo cp imx6uirqApp /home/duan/linux/nfs/rootfs/lib/modules/4.1.15/ -f

进入到目录 lib/modules/4.1.15 中,输入如下命令加载imx6uirq.ko 驱动模块:

depmod //第一次加载驱动的时候需要运行此命令 modprobe imx6uirq.ko //加载驱动

驱动加载成功可通过查看/proc/interrupts 来检查一下对应的中断有没有被注册上,输入如下命令:

cat /proc/interrupts

接下来使用如下命令来测试中断:

./imx6uirqApp /dev/imx6uirq

按下开发板上的 KEY0 键,终端就会输出按键值,如图 51.4.2.2 所示:

从图 51.4.2.2 可以看出,按键值获取成功,并且不会有按键抖动导致的误判发生,说明按键消抖工作正常。如果要卸载驱动的话输入如下命令即可:

rmmod imx6uirq.ko

总结

完成了I.MX6U Linux 驱动中断实验(按键中断 + 定时器消抖 + 设备树配置实战教程)。

Read more

ESLint 全指南:从原理到实践,构建高质量的 JavaScript/TypeScript 代码

ESLint 全指南:从原理到实践,构建高质量的 JavaScript/TypeScript 代码

文章目录 * 概述 * 一、 核心原理:深入理解 ESLint 的工作机制 * 流程图:ESLint 核心工作流程 * 二、 基础实战:快速上手与配置 * 1. 安装 ESLint * 2. 初始化配置文件 * 3. 配置文件深度解析 * 流程图:ESLint 配置解析与合并 * 4. 运行与忽略 * 三、 进阶之道:构建现代化前端工程规范 * 1. 使用共享配置 * 2. 完美集成 TypeScript * 3. 与 Prettier 和平共处 * 4. 性能优化 * 四、 生态集成:无缝融入开发工作流 * 1. 编辑器集成 * 2. 构建工具集成 * 3. CI/CD 集成

By Ne0inhk
基于Java springboot医院低值耗材管理系统耗材出入库(源码+文档+运行视频+讲解视频)

基于Java springboot医院低值耗材管理系统耗材出入库(源码+文档+运行视频+讲解视频)

文章目录 * 系列文章目录 * 目的 * 前言 * 一、详细视频演示 * 二、项目部分实现截图 * 三、技术栈 * 后端框架springboot * 前端框架vue * 持久层框架MyBaitsPlus * 系统测试 * 四、代码参考 * 源码获取 目的 摘要:医院低值耗材管理是医疗运营的重要环节,传统人工管理模式存在效率低、易出错等问题。本文基于Java Spring Boot框架设计低值耗材出入库管理系统,采用前后端分离架构,前端以Vue.js构建交互界面,后端利用Spring Boot整合MyBatis-Plus实现业务逻辑处理,数据库选用MySQL存储结构化数据。系统涵盖耗材信息录入、入库登记、出库审批、库存盘点等核心功能,通过动态库存预警机制,当库存低于安全阈值时自动触发提醒,确保库存合理。同时,系统支持多级权限管理,不同角色拥有差异化操作权限,保障数据安全。经测试,系统能有效提升低值耗材出入库管理效率,降低人工错误率,为医院精细化管理提供有力支持,对优化医疗资源配置、降低运营成本具有积极意义。 前言 💗博主介绍

By Ne0inhk

Exception in thread “main“ java.lang.NoSuchMethodError: ‘java.lang.String org.junit.platform.engine.

初始化的项目出现junit报错 Exception in thread "main" java.lang.NoSuchMethodError: 'java.lang.String org.junit.platform.engine.discovery.MethodSelector.getMethodParameterTypes()' at com.intellij.junit5.JUnit5TestRunnerUtil.loadMethodByReflection(JUnit5TestRunnerUtil.java:127) at com.intellij.junit5.JUnit5TestRunnerUtil.buildRequest(JUnit5TestRunnerUtil.java:102) at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:43) at

By Ne0inhk
基于 Java 的消息队列选型年度总结:RabbitMQ、RocketMQ、Kafka 实战对比

基于 Java 的消息队列选型年度总结:RabbitMQ、RocketMQ、Kafka 实战对比

文章目录 * 基于 Java 的消息队列选型年度总结:RabbitMQ、RocketMQ、Kafka 实战对比 🚀 * 一、为什么需要消息队列?🤔 * 二、三大消息队列详解 📚 * 1. RabbitMQ * 2. RocketMQ * 3. Apache Kafka * 三、三大消息队列概览 📊 * 四、架构设计对比 🏗️ * 1. RabbitMQ 架构 * 2. RocketMQ 架构 * 3. Kafka 架构 * 五、Java 集成实战 💻 * 1. RabbitMQ + Spring Boot 示例 * 2. RocketMQ + Spring Boot 示例 * 3. Kafka + Spring Boot 示例

By Ne0inhk