1. 前言
这两天做项目用到中断,发现以前学的那点东西都忘光了,索性写个笔记回忆加强一下记忆!
首先,中断分为硬中断和软中断。而软中断又细分为软中断、tasklet、工作队列;每种中断都有其优缺点,以及适用场景,具体情况看如下分析。
2. 中断的概念
2.1 中断的上半部和下半部
要讲中断就要先讲清楚它工作的区域,中断的上半部和下半部。上半部是指中断处理程序;下半部是指一些虽然与中断有相关性但是可以延后执行的任务。(这里的上半部和下半部其实对应的就是硬中断和软中断,且下半部是由上半部调用来执行延时耗时操作的)。
两者的主要区别在于:中断上半部不能被相同类型的中断打断,而下半部依然可以被中断打断;中断上半部对于时间非常敏感,而下半部基本上都是一些可以延迟的工作。由于二者的这种区别,所以对于一个工作是放在上半部还是放在下半部去执行,可以参考下面4条:
如果一个任务对时间非常敏感,将其放在中断上半部处理程序中执行。
如果一个任务和硬件相关,将其放在中断上半部处理程序中执行。
如果一个任务要保证不被其他中断(特别是相同的中断)打断,将其放在中断上半部处理程序中执行。
其他所有任务,考虑放在下半部去执行。
2.2 中断的触发流程
2.2.1 硬件层面
1.中断请求:
外部设备(如键盘、网卡、触摸屏等)发生某些事件(键盘按下、数据到达、触摸屏被触摸)时,会通过中断请求线向cpu发出中断请求信号。
中断控制器(如APIC、PIC)接收到中断请求信号,并将其排队处理。
2.中断控制器处理:
中断控制器会根据优先级选择一个中断请求,将中断信号发给CPU。
中断控制器会发送中断向量(中断类型编号)给CPU,告知是哪种中断。
3.CPU响应:
CPU完成当前指令后,保存当前的程序计算器和处理器状态寄存器,进入中断处理模式。
CPU禁用中断(或设置中断屏蔽)以避免嵌套中断,获取中断向量,判断中断类型
2.2.2 软件层面
1.中断向量表:
根据中断向量,CPU查找中断向量表(IVT),找到对应的中断服务程序(ISR)的入口地址。
2.中断服务程序(ISR)执行:
CPU跳转到中断服务程序入口,开始执行ISR。
ISR会处理硬件设备的中断请求,如读取数据、清除中断标志等。
3.下半部处理:
如果ISR执行时间较长或需要执行复杂操作,会将这些操作延迟到下半部处理。
下半部处理机制包括软中断、tasklets和工作队列(workqueues),用于处理延迟任务。
4.恢复执行:
ISR完成后,恢复之前保存的处理器状态和程序计数器。
CPU重新启用中断(如果之前禁用了中断),继续执行被中断的程序。
2.2.3 流程图
[外部设备事件] --> [中断控制器收到中断请求] --> [中断控制器发送中断信号给CPU] --> [CPU保存状态并禁用中断] --> [CPU根据中断向量查找ISR] --> [ISR处理中断请求] --> [ISR延迟任务到下半部处理] --> [ISR完成后恢复状态] --> [CPU重新启用中断] --> [CPU继续执行被中断的程序]
3 中断的应用
3.1 硬中断
从上文可知硬中断就是中断的上半部,用于执行不耗时的操作。
在实际使用中我们申请的中断号(用reques_irq申请),注册中断绑定的中断函数都是硬中断。如下代码所示,触发中断后执行绑定的中断函数,整个过程就是硬中断的调用。
3.1.1 接口
struct device_node *of_find_compatible_node(struct device_node *from,
const char *type, const char *compat);
功能:通过compatible属性查找指定节点
参数:
@from - 指向开始路径的节点,如果为NULL,则从根节点开始
@type - device_type设备类型,可以为NULL
@compat - 指向节点的compatible属性的值(字符串)的⾸地址
返回值:成功:得到节点的⾸地址;失败:NULL
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
功能:解析设备树并映射产生软中断号
参数:
@dev:节点指针
@index:interrupts后写的中断的描述的下标
返回值:成功返回软中断号,失败返回0
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
功能:注册中断
参数;
@irq:软中断号 (以后在内核中使用的中断号都是软中断号)
@handler:中断处理函数的指针
irqreturn_t irq_handle(int irq, void *dev)
{
//中断处理函数,中断处理函数中不能做延时,耗时,甚至休眠的操作
//return IRQ_NONE; //中断没有处理完成
return IRQ_HANDLED;//中断执行成功,中断处理完成了
}
@flags:中断触发方式
IRQF_TRIGGER_RISING //上升沿触发
IRQF_TRIGGER_FALLING //下降沿触发
IRQF_TRIGGER_HIGH //高电平触发
IRQF_TRIGGER_LOW //低电平触发
IRQF_SHARED //共享中断
@name:中断的名字
cat /proc/interrupts
@dev:向中断处理函数传递的参数
返回值:成功返回0,失败返回错误码
const void *free_irq(unsigned int irq, void *dev_id)
功能:释放中断
参数:
@irq:软中断号
@dev_id:注册中断时候的第5个参数
返回值:返回devname
interrupt-parent = <&gpio3>;
interrupts = ;
这是因为设备树的interrupts节点的第一个参数即是它的引脚编号也是它的中断号,既是103号中断(332+(08+7)转换方式看我文章)。
通过request_irq函数,将设备树的中断号映射到内核获取到软中断号,再与硬中断的函数绑定。
3.1.2示例
#include
#include
#include
#include
#include
/*
myirq{
compatible = "aaaa,myirq";
interrupt-parent = <&gpiof>;
interrupts = <9 0>;
};
*/
struct device_node *node;
unsigned int irqno;
#define name "key1";
irqreturn_t key_irq_handle(int irq, void* dev)
{
printk("执行中断逻辑\n");
return IRQ_HANDLED;
}
static int __init myirq_init(void)
{
int i, ret;
// 1.获取设备树中的节点
node = of_find_compatible_node(NULL, NULL, "aaaa,myirq");
if (node == NULL) {
printk("get node error\n");
return -EINVAL;
}
// 2.映射得到软中断号
irqno = irq_of_parse_and_map(node, 0);
if (irqno == 0) {
printk("get irqno error\n");
return -EAGAIN;
}
// 3.注册中断
ret = request_irq(irqno, key_irq_handle,
IRQF_TRIGGER_FALLING, name, 0);
if (ret) {
printk("request irq error\n");
return ret;
}
}
return 0;
}
static void __exit myirq_exit(void)
{
free_irq(irqno, 0);
}
module_init(myirq_init);
module_exit(myirq_exit);
MODULE_LICENSE("GPL");
3.2 软中断
软中断是一组静态定义的下半部接口,可以在所有处理器上同时执行,即使两个类型相同也可以。
但一个软中断不会抢占另一个软中断,唯一可以抢占软中断的是硬中断。
目前Linux系统最多支持32个软中断,系统已经定义使用了10个,剩下的用户可以自己指定,但是看一下前面的说明!避免自己创建软中断,如果不是需要高频率的线程工作调度,一般来说系统提供的软中断已经够我们使用了,我们在日常开发中最好还是遵循系统给出的指导建议,避免出现异常,日常的学习调试,我们可以创建自己的软中断,加深对这方面知识的理解。上面列出的软中断类型越靠前优先级越高,其中有两个需要关注一下,就是HI_SOFTIRQ和TASKLET_SOFTIRQ,系统已经帮我们初始化好了,tasklet就是基于这两个软中断去实现的。
这里简介一下软中断的添加流程,实际应用在还是以tasklet和工作队列为主。
1.添加我们自己的软中断
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
MY_SOFTIRQ, /*我自己添加的软中断*/
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
2.在kernel/softirq.c中定义自己的软中断处理函数
//我自己定义的软中断处理函数
static void my_softirq_action(struct softirq_action *a)
{
...
}
3.初始化
void __init softirq_init(void)
{
int cpu;
for_each_possible_cpu(cpu) {
per_cpu(tasklet_vec, cpu).tail =
&per_cpu(tasklet_vec, cpu).head;
per_cpu(tasklet_hi_vec, cpu).tail =
&per_cpu(tasklet_hi_vec, cpu).head;
}
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
open_softirq(MY_SOFTIRQ, tasklet_hi_action);//我自己定义的软中断
}
4.激活
raise_softirq(MY_SOFTIRQ);
定义了软中断,那跟系统自带的软中断,它们在什么时候得到执行呢?我们来看一下 do_softirq函数:
文件路径\:kernel/softirq.c
asmlinkage void do_softirq(void)
{
__u32 pending;
unsigned long flags;
if (in_interrupt())//判断当前是否处于中断状态
return;
local_irq_save(flags);//保存中断标记
pending = local_softirq_pending();
if (pending)//循环处理已经注册的软中断
__do_softirq();
local_irq_restore(flags);
}
这种方式我没有使用过,虽说效率最高,但调用流程比较复杂,推荐使用其他的软中断。
3.3 tasklet中断
tasklet是基于软中断的机制实现的
3.3.1 接口
1.分配对象
struct tasklet_struct
{
struct tasklet_struct *next; //tasklet的链表
unsigned long state; //是否需要触发底半部的状态
atomic_t count; //触发的次数
bool use_callback; //true使用callback ,false使用func
union {
void (*func)(unsigned long data); //旧版本
void (*callback)(struct tasklet_struct *t); //新版本
};
unsigned long data; //传递参数
};
struct tasklet_struct tasklet;
2.对象初始化
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data) //旧版本的初始化
void tasklet_setup(struct tasklet_struct *t,
void (*callback)(struct tasklet_struct *)) //新版本的初始化
3.调用执行
void tasklet_schedule(struct tasklet_struct *t)
3.3.2 示例
#include
#include
#include
#include
#include
#include
/*
myirq{
compatible = "aaaa,myirq";
interrupt-parent = <&gpiof>;
interrupts = <9 0>;
};
*/
struct device_node *node;
unsigned int irqno;
#define name "key1";
struct tasklet_struct tasklet;
//底半部处理函数
void irq_tasklet_bottom(struct tasklet_struct* tasklet)
{
//5.执行延时操作逻辑
}
//中断顶半部
irqreturn_t key_irq_handle(int irq, void* dev)
{
//4.开始底半部
tasklet_schedule(&tasklet);
return IRQ_HANDLED;
}
static int __init myirq_init(void)
{
int i, ret;
// 0.tasklet_setup初始化
tasklet_setup(&tasklet, irq_tasklet_bottom);
// 1.获取设备树中的节点
node = of_find_compatible_node(NULL, NULL, "aaaa,myirq");
if (node == NULL) {
printk("get node error\n");
return -EINVAL;
}
// 2.映射得到软中断号
irqno = irq_of_parse_and_map(node, 0);
if (irqno == 0) {
printk("get irqno error\n");
return -EAGAIN;
}
// 3.注册中断
ret = request_irq(irqno, key_irq_handle,
IRQF_TRIGGER_FALLING, name, 0;
if (ret) {
printk("request irq error\n");
return ret;
}
}
return 0;
}
static void __exit myirq_exit(void)
{
free_irq(irqno, 0);
}
module_init(myirq_init);
module_exit(myirq_exit);
MODULE_LICENSE("GPL");
定义tasklet变量,实现软中断处理函数,初始化,调度,以上这些就是tasklet的使用步骤了,内核帮我们省略了很多麻烦的实现,所以使用起来比较简单。
3.4 工作队列(workqueue)
前面已经讲了软中断还有tasklet了,那这里的工作队列和它们有什么区别呢?为什么会存在工作队列机制?
存在即是合理,既然存在那肯定是用来弥补前两者的缺陷的,所以我们先来分析看看前两者有什么缺点。
软中断和tasklet是运行于中断上下文的,它们属于内核态没有进程的切换,因此在执行过程中不能休眠,不能阻塞,一旦休眠或者阻塞,则系统直接挂死。比如我调试驱动时候,曾经在中断处理函数中调用spi同步数据的函数,系统直接挂死了,后来看代码的说明才明白,不能在中断中调用休眠,阻塞的函数。因此软中断和tasklet是有一定的使用局限性的,工作队列的出现正是用在软中断和tasklet不能使用的场合,比如需要调用一个具有可延迟函数的特质,但是这个函数又有可能引起休眠、阻塞。
3.4.1 接口
1.分配对象
struct work_struct {
atomic_long_t data; //可以向底半部处理函数传递数据
struct list_head entry; //构成队列项
work_func_t func; //底半部处理函数
};
struct work_struct work;
2.对象初始化
void mywork_func(struct work_struct *work)
{
//工作队列的底半部处理函数
}
INIT_WORK(&work, mywork_func)
3.调用执行
bool schedule_work(struct work_struct *work)
4.保证底半部执行结束在卸载驱动防止Oops的空指针错误
cancel_work_sync(&work);
3.4.2 示例
#include
#include
#include
#include
#include
#include
#include
/*
myirq{
compatible = "aaaa,myirq";
interrupt-parent = <&gpiof>;
interrupts = <9 0>;
};
*/
struct device_node *node;
unsigned int irqno;
#define name "key1";
struct work_struct work;
//底半部处理函数
void irq_work_func(struct work_struct* mwork)
{
//5.执行延时操作逻辑
}
//中断顶半部
irqreturn_t key_irq_handle(int irq, void* dev)
{
//4.调用工作队列
schedule_work(&work);
return IRQ_HANDLED;
}
static int __init myirq_init(void)
{
int i, ret;
// 0.tasklet_setup初始化
INIT_WORK(&work, irq_work_func);
// 1.获取设备树中的节点
node = of_find_compatible_node(NULL, NULL, "aaaa,myirq");
if (node == NULL) {
printk("get node error\n");
return -EINVAL;
}
// 2.映射得到软中断号
irqno = irq_of_parse_and_map(node, 0);
if (irqno == 0) {
printk("get irqno error\n");
return -EAGAIN;
}
// 3.注册中断
ret = request_irq(irqno, key_irq_handle,
IRQF_TRIGGER_FALLING, name, 0);
if (ret) {
printk("request irq error\n");
return ret;
}
}
return 0;
}
static void __exit myirq_exit(void)
{
int i;
cancel_work_sync(&work);
free_irq(irqno,0);
}
module_init(myirq_init);
module_exit(myirq_exit);
MODULE_LICENSE("GPL");
3.5 应用实例
工作队列(workqueue)和tasklet的使用思路是差不多的,各有局限性,实际的使用需要根据自身情况来选择。且工作队列还有其他的用法,例如创建一个固定周期调度的工作队列,这个是tasklet无法做到的。
在这里我引用别人总结的比较直观的一句话:我们在做驱动的时候,关于这三个下半部(也就是以上的三种机制)实现,需要考虑两点:首先,是不是需要一个可调度的实体来执行需要推后完成的工作(即休眠的需要),如果有,工作队列就是唯一的选择,否则最好用tasklet。性能如果是最重要的,那还是软中断吧。
光看理论没有实践还是不懂,这是我在lcd屏幕中触摸屏的中断代码,展示了在实际项目中对中断得使用。
3.5.1 设备树
为防止歧义,我只保留了中断相关的设备树信息。
&spi0 {
#address-cells = <1>;
#size-cells = <0>;
tsc2046@0 {
pinctrl-names = "default";
pinctrl-0 = <&tp_irq>;
status = "okay";
compatible = "ti,tsc2046";
vcc-supply = <&ads7846reg1v8>;
spi-max-frequency = <1500000>;
reg = <0>; /* CS0 */
interrupt-parent = <&gpio3>;
interrupts = ; /* GPIO */
pendown-gpio = <&gpio3 RK_PA7 GPIO_ACTIVE_HIGH>;
};
};
&pinctrl {
spi0 {
tp_irq:tp-irg {
rockchip,pins = <3 RK_PA7 RK_FUNC_GPIO &pcfg_pull_none>;
};
};
};
3.5.2 驱动源码
如下是我得中断代码。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define my_irq_name "my_irq"
struct device_node *node;
unsigned int irqno;
struct my_touch_data
{
struct spi_device *spi;
struct work_struct work;
void *private_data; /* 私有数据 */
};
uint16_t touch_buf = 0;
uint16_t value_re;
struct my_touch_data *data;
void read_spi(struct my_touch_data *data, uint8_t tcmd)
{
/**************************************************************/
int ret;
unsigned char txdata;
struct spi_message m;
struct spi_transfer *t;
struct spi_device *spi = (struct spi_device *)data->private_data;
t = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL); /* 申请内存 */
/* 第1次,发送要读取的寄存地址 */
txdata = tcmd;
t->speed_hz = 5000000;
t->tx_buf = &txdata; /* 要发送的数据 */
t->rx_buf = &touch_buf; /* 读取到的数据 */
t->len = 1; /* 1个字节 */
spi_message_init(&m); /* 初始化spi_message */
spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
ret = spi_sync(spi, &m); /* 同步发送 */
mdelay(1);
/* 第2次,读取数据 */
txdata = 0x00; /* 随便一个值,此处无意义 */
t->tx_buf = &txdata;
t->rx_buf = &touch_buf; /* 读取到的数据 */
t->len = 1; /* 要读取的数据长度 */
spi_message_init(&m); /* 初始化spi_message */
spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
ret = spi_sync(spi, &m); /* 同步发送 */
value_re = touch_buf<<8;
/* 第3次,读取数据 */
txdata = 0x00; /* 随便一个值,此处无意义 */
t->tx_buf = &txdata;
t->rx_buf = &touch_buf; /* 读取到的数据 */
t->len = 1; /* 要读取的数据长度 */
spi_message_init(&m); /* 初始化spi_message */
spi_message_add_tail(t, &m);/* 将spi_transfer添加到spi_message队列 */
ret = spi_sync(spi, &m); /* 同步发送 */
value_re |= touch_buf;
value_re >>= 3;
udelay(200);
kfree(t); /* 释放内存 */
return value_re;
}
// 底半部处理函数
void irq_work_func(struct work_struct *mwork)
{
printk("进入软中断,工作队列\n");
disable_irq_nosync(irqno);
read_spi(data, 0xd0);
enable_irq(irqno);
disable_irq_nosync(irqno);
read_spi(data, 0x90);
enable_irq(irqno);
}
// 中断顶半部
irqreturn_t key_irq_handle(int irq, void *dev)
{
printk("进入中断上半部\n");
if (schedule_work(&data->work))
{
printk("调用中断下半部完成\n");
}else{
printk("调用中断下半部失败\n");
}
return IRQ_HANDLED;
}
int my_touch_probe(struct spi_device *spi)
{
int ret;
data = devm_kzalloc(&spi->dev, sizeof(struct my_touch_data), GFP_KERNEL);
if (!data)
return -ENOMEM;
/*******************************************************************/
// 0.tasklet_setup初始化
spi->mode = SPI_MODE_0; /*MODE0,CPOL=0,CPHA=0*/
spi->max_speed_hz = 5000000;
spi_setup(spi);
data->private_data = spi; /* 设置私有数据 */
INIT_WORK(&data->work, irq_work_func);
/*******************注册中断********************************/
// 1.获取设备树中的节点
node = of_find_compatible_node(NULL, NULL, "ti,tsc2046");
if (node == NULL)
{
printk("get node error\n");
return -EINVAL;
}
printk("匹配设备树成功\n");
// 2.映射得到软中断号
irqno = irq_of_parse_and_map(node, 0);
if (irqno == 0)
{
printk("get irqno error\n");
return -EAGAIN;
}
printk("获取中断号成功\n");
// 3.注册中断
ret = devm_request_irq(&spi->dev, irqno, key_irq_handle,
IRQF_TRIGGER_FALLING, my_irq_name, (void *)data);
if (ret)
{
printk("request irq error\n");
return ret;
}
printk("注册中断成功\n");
/******************中断注册完成*******************************/
return 0;
}
int my_touch_remove(struct spi_device *spi)
{
cancel_work_sync(&data->work);
free_irq(irqno, 0);
return 0;
}
const struct of_device_id of_match[] = {
{
.compatible = "ti,tsc2046",
},
{},
};
MODULE_DEVICE_TABLE(of, of_match);
struct spi_driver my_touch = {
.probe = my_touch_probe,
.remove = my_touch_remove,
.driver = {
.name = "my_touch",
.of_match_table = of_match,
},
};
module_spi_driver(my_touch);
MODULE_LICENSE("GPL");
3.5.3 注意事项
1.硬中断中与spi_sync的冲突
在实际开发中遇到了很多问题。如在硬中断中读取spi数据会导致内核崩溃;后来研究发现原来硬中断中不能阻塞等待,而spi的spi_sync函数会阻塞等待数据返回,这二者一叠加直接导致内核崩溃。
如明明整个中断函数写的没问题,但就是不能从硬中断调用软中断成功;这是因为软中断中得调用函数有问题导致得(我这里是读取spi数据得函数协议不对导致得)。
中断调用失败的问题很尴尬,但凡调用的函数有任何错误都会导致中断调用失败。这种情况和以往的经验不同,即使失败起码错误之前的函数执行完成应该打印出信息吧!但中断特有的机制导致,不会有任何打印信息出来,只会显示调用失败。没搞清楚这个问题之前,一点信息不打印一度让我很懵逼。
2.中断与spi通信的冲突
在上述问题都解决后,让人懵逼的是不返回正确数据,恒定的返回0xff、0x00、0xf8;但各个方面都没问题,对比内核的ads7846.c和网上的xpt2046的驱动发现也没区别,但就是不返回有效数据;后面尝试用线程发送读取数据,就可正常返回。挠破头皮也没发现问题在哪,机缘巧合下给中断触发加了下面的锁才正常返回数据。
disable_irq_nosync(irqno);
read_spi(data, 0x90);
enable_irq(irqno);
分析觉得问题应该是中断不断的触发,而工作队列的中断是创建一组线程执行中断内的程序。二者叠加导致一直向触摸屏发送读取命令,所以才时序错乱无法返回有效数据的。
3.适配官方的ads7846.c驱动的问题
在自己的驱动初步跑通后,就准备适配官方的驱动;结果发现怎么测试都卡在中断和spi_sync函数上了,通篇查下来代码匹配没任何问题。后面找了个xpt2046的驱动代码也是卡在这里,没办法逐行读代码分析问题原因;最后分析出原因应该就是代码中对数据处理的问题,导致给触摸屏发送的数据不正常,且spi的协议也有一定的问题才导致适配官方驱动不成功的。考虑到这种数据处理问题需要消耗的精力和时常,还是用自己写的触摸驱动性价比高一些。
例如:
使用spi子系统传输时设置返回两个字节就会发生错误,我只能老老实实的返回一个字节单独处理;我看各种例程都是随意设置返回字节数的,所以我怀疑是luckfox的开发板自身的问题。
req->xfer[1].rx_buf = &req->sample;
req->xfer[1].len = 2;
spi_message_add_tail(&req->xfer[1], &req->msg);
4 总结
至此关于中断的理论与实践就都研究的差不多了,整体下来我觉得在项目中简单得使用中断是没任何问题了,想再深入研究就要分析内核源码了,查看中断得底层实现。写完了才发现定时器中断忘提了,有需要得话,后续再专门写一篇。还有我对项目中遇到得问题都只是基于现有知识得判断,如果有大佬能给出确切得结论还望不吝赐教!!!!
太赞了!!!
嘎嘎帅!!
啊啊啊啊啊啊