本文转载于微信公众号:嵌入式软件开发交流;原文地址:https://mp.weixin.qq.com/s/vJCoUr8wHanF9Epvi1rKcw
一、前言
中断在驱动中是非常常用的,无论是外部的 GPIO 中断,还是 SPI,I2C 等发送或接收中断,都是必不可少的。所以今天来看看 Linux 中的中断处理。
上面我们根据中断来源,屏蔽方式和中断入口对中断进行了简单的分类。
二、中断控制器
PIC:可编程中断控制器;
GIC:Generic Interrupt Controller,通用中断控制器(常用)。
GIC 是目前最常见的一种中断控制器,它在多核 CPU 中特别常见。它对中断做了细分:
-
SGI:Software Generated Interrupt,软件产生的中断,可以用于多核的核间通信。一个 CPU 可以通过写 GIC 的寄存器给另外一个 CPU 产生中断。(中断号 0~15)
-
PPI:Private Peripheral Interrupt,某个 CPU 私有外设的中断,这类外设的中断只能发给绑定的那个 CPU。(中断号 16~31)
-
SPI:Shared Peripheral Interrupt,共享外设的中断,这类中断可以路由到任何一个 CPU。(中断号 32~1019)
这些更详细的内容可以到内核源码中的 Documentation 中找到更详细的介绍。
三、中断处理框架
为了中断执行时间尽量短和中断处理需完成的工作尽量大之间找到平衡点,Linux 将中断处理程序分解为两个半部:顶半部和底半部。
顶半部用于完成尽量少的比较紧急的功能。底半部完成耗时操作。顶半部不会被中断打断,底半部可以。
如果中断要处理的工作本身很少,则完全可以直接在顶半部全部完成。
四、驱动中断编程
//申请中断
/*
参数说明:
irq:要申请的硬件中断号
handler:中断处理函数,顶半部
flags:中断触发方式
name:中断名称
dev:传递给中断处理函数的私有数据
*/
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler,
unsigned long irqflags, const char *devname, void *dev_id)
//释放中断
const void *free_irq(unsigned int irq, void *dev_id)
//使能和屏蔽某个中断
void disable_irq(unsigned int irq) //需要等待目前中断处理完成
void disable_irq_nosync(unsigned int irq) //不需要等待目前中断处理完成
void enable_irq(unsigned int irq)
//使能和屏蔽所有中断
#define local_irq_enable() do { raw_local_irq_enable(); } while (0)
#define local_irq_disable() do { raw_local_irq_disable(); } while (0)
上面是 Linux 提供的一些接口函数,比较简单。
五、底半部机制
5.1、tasklet
要使用 tasklet 只要使用下面的宏将 tasklet 和其处理函数关联在一起即可。
#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
然后在需要调用 tasklet 的时候调用 tasklet_schedule 函数就能使系统在适当的时候进行调度运行。
static inline void tasklet_schedule(struct tasklet_struct *t);
tasklet 程序模板
//定义tasklet和底半部函数并关联
void xxx_do_tasklet(unsigned long);
DECLARE_TASKLET(xxx_tasklet, xxx_do_tasklet, 0);
//中断处理底半部
void xxx_do_tasklet(unsigned long)
{
......
}
//中断处理顶半部
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
......
tasklet_schedule(&xxx_tasklet);
......
}
//设备驱动模块加载函数
int __init xxx_init(void)
{
//申请中断
result = request_irq(xxx_irq, xxx_interrupt,
IRQF_DISABLED, "xxx", NULL);
......
return IRQ_HANDLED;
}
//设备驱动模块卸载函数
void __exit xxx_exit(void)
{
......
//释放中断
free_irq(xxx_irq, xxx_interrupt);
}
5.2、工作队列
工作队列的使用方法和 tasklet 基本相似。
1)定义一个工作队列和底半部处理函数
struct work_struct my_wq; //工作队列
void my_wq_func(struct work_struct *work); //处理函数
2)初始化工作队列
INIT_WORK(&my_wq, my_wq_func);
3)调用工作队列执行函数
schedule_work(&my_wq); //调度工作队列执行
工作队列程序模板
//定义工作队列和处理函数
struct work_struct xxx_wq;
void xxx_do_work(struct work_struct *work);
//中断处理底半部
void xxx_do_work(struct work_struct *work)
{
.......
}
//中断处理顶半部
irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
.......
schedule_work(&xxx_wq);
.......
return IRQ_HANDLED;
}
//设备驱动模块加载函数
int xxx_init(void)
{
......
//申请中断
result = request_irq(xxx_irq, xxx_interrupt,
0, "XXX", NULL);
//初始化工作队列
INIT_WORK(&xxx_wq, xxx_do_work);
......
}
//设备驱动模块卸载函数
void xxx_exit(void)
{
.......
//释放中断
free_irq(xxx_irq, xxx_interrupt);
.......
}
5.3、软中断
这个我们一般不直接使用,tasklet 就是基于软中断实现的。
5.4、线程 irq
在内核中除了使用 request_irq()、devm_request_irq() 申请中断外,还可以使用 request_threaded_irq() 和 devm_request_threaded_irq() 来申请。
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn, unsigned long irqflags,
const char *devname, void *dev_id)
int devm_request_threaded_irq(struct device *dev, unsigned int irq,
irq_handler_t handler, irq_handler_t thread_fn,
unsigned long irqflags, const char *devname,
void *dev_id)
上面两个函数多了个 thread_fn,用这两个 API 申请中断的时候,内核会为相应的中断号分配一个对应的内核线程。
参数 handler 对应的函数执行于中断上下文、thread_fn 参数对应的函数执行于内核线程。如果 handler 结束时返回值为 IRQ_WAKE_THREAD,内核会调度对应线程执行 thread_fn 对应的函数。