
🎬 渡水无言:个人主页渡水无言
❄专栏传送门: 《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》
❄专栏传送门: 《freertos专栏》 《STM32 HAL库专栏》《linux裸机开发专栏》
❄专栏传送门:《产品测评专栏》
⭐️流水不争先,争的是滔滔不绝📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生
| 省级优秀毕业生获得者 | csdn新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生
在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连
目录
四、按键中断驱动程序编写(完整代码 + 逐行解析,零基础入门实战)
前言
裸机开发中使用中断需要手动配置寄存器、映射中断向量表,步骤繁琐;而 Linux 内核提供了完善的中断框架,开发者只需申请中断 + 注册中断处理函数,无需关注底层寄存器操作,极大简化了开发流程。
一、Linux 中断简介
先来回顾一下裸机实验里面中断的处理方法:
①、使能中断,初始化相应的寄存器。
②、注册中断服务函数,也就是向 irqTable 数组的指定标号处写入中断服务函数。
②、中断发生以后进入 IRQ 中断服务函数,在 IRQ 中断服务函数在数组 irqTable 里面查找具体的中断处理函数,找到以后执行相应的中断处理函数。
在 Linux 内核中也提供了大量的中断相关的 API 函数,我们来看一下这些跟中断有关的API 函数。
1.1 核心中断 API 函数
1.1.1 中断号
1.1.2 中断申请与释放
| 函数 | 作用 | 关键参数说明 |
|---|---|---|
request_irq(irq, handler, flags, name, dev) | 申请并使能中断 |
dev:一般情况下将 dev 设置为设备结构体,dev 会传递给中断处理函数 irq_handler_t 的第二个参数。 |
free_irq(irq, dev) | 释放中断 | 共享中断需通过dev区分不同设备 |
常用中断标准如下所示:

1.1.3 中断处理函数
irqreturn_t (*irq_handler_t) (int irq, void *dev_id)
// 返回值:IRQ_RETVAL(IRQ_HANDLED) 表示中断已处理
第一个参数是要中断处理函数要相应的中断号。
第二个参数是一个指向 void 的指针,也就是个通用指针。
需要与 request_irq 函数的 dev 参数保持一致。用于区分共享中断的不同设备, dev 也可以指向设备数据结构。
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、程序编写
4.2.1、修改设备树文件
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;
}
五、运行测试
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 所示:

rmmod imx6uirq.ko
总结
完成了I.MX6U Linux 驱动中断实验(按键中断 + 定时器消抖 + 设备树配置实战教程)。
转载自CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/weixin_61186812/article/details/159004404



