在嵌入式系统开发中,中断是十分重要的知识点,在大部分单片机构建的应用产品中,基本都是以前后台方式(大循环加中断)的方式来实现功能。在中断中处理外部的触发信号,并在主循环或者线程中处理应用;基于RTOS构建的系统应用,一般也使用Systick时钟中断作为核心滴答时钟。在嵌入式Linux应用来说,中断也是处理CPU的突发事件的主要方法,主要实现以下功能。
理解中断的背后执行逻辑,对于单片机和嵌入式Linux的开发都有重要意义,这里以Cortex-A7内核讲解嵌入式Linux的中断处理机制。
对于Linux内核中的中断系统,由三部分参与。
上述就是简化的Linux中断处理流程,不过事实上Linux系统的中断机制远复杂于此,
0x00 复位中断(Reset) 特权(Supervisor)
0x04 未定义指令中断(Undefined Instruction) 未定义指令(undef)
0x08 软中断(Software Interrupt, SWI) 特权(Supervisor)
0x0C 指令预取中止中断(Prefetch Abort) 中止(Abort)
0x10 数据访问中止中断(Data Abort) 中止(Abort)
0x14 未使用(No Used) /
0x18 IRQ中断(IRQ Interrupt) 外部中断(IRQ)
0x1C FIQ中断(FIQ Interrupt) 快速中断(FIQ)
上述列出的就是Cortex-A7芯片支持的全部中断类型,其中0x00~0x10为系统中断,当芯片执行出现异常,或者由SWI指令主动触发时,就会执行这些中断,一般由Linux内核管理,详细理解如下。
基于对单片机的了解,芯片都是支持多个外设的,特别是对于Cortex-A系统芯片,往往更加复杂,那么仅依靠7个中断如何支持外部中断的需求?这就要提到IRQ和FIQ了, 对于Cortex-A系列芯片来说,任意一个外部中断如IO_Interrupt, Legacy_Interrupt等,都会触发IRQ或FIQ中断,进入对应的中断函数。并在此函数中通过软件读取寄存器的值来判断具体发生了什么中断,这样多个中断可以使用同一个入口线号,如IRQ_Interrupt_handler。其中管理外部信号到中断触发的器件被称为GIC(Generic Interrupt Controller),其框架如下所示。
参考上图,中断根据源头的不同,其说明如下所示。
另外对于Cortex-A7芯片来说,中断使能包含IRQ或FIQ总中断使能,以及ID0-ID1019可选的中断源使能两部分;优先级为32bit的数据,支持抢占优先级和子优先级,这部分和Cortex-M系列基本是一致的。
在理解Cortex-A7内核处理机制之前,需要了解到芯片内核状态分类如下:
user mode: 用户模式,用户空间AP执行所处于的模式。
superiver mode: 超级模式,或者SVC模式,大部分Linux内核执行代码处于该模式下。
IRQ mode: 中断模式,触发中断后,处理器进入的模式。
Abort mode: 用来处理上面提到的Data Abort和prefetch Abort异常
和单片机的流程类似,当有外部触发信号到达,并且所有中断相关的使能都打开的情况下,中断控制器GIC就会根据配置好的硬件信息,将IRQ(或FIQ)的中断触发信息告知指定的Core。处理器感知到该信号后到达时,对于进行irq模式前的系统状态值如cpsr寄存器值,PC指针进行保存(分别保存到SPSR和LR寄存器中),置位相应的中断状态标志位。计算中断向量的入口位置,将PC设置为该值并跳转,总结下来,在中断发生时,内核的硬件处理包含置位中断信息,保存中断前关键状态,进入IRQ模式,然后在跳转到中断向量的入口,后续就由软件接口进行后续处理。
对于软件部分的处理,则包含irq模式,svc/usr模式处理和应用代码处理。irq模式主要进行了r0,lr以及cpsr的保存,压栈处理(Irq模式下的堆栈设定为12字节),根据进入中断前的系统模式,维护中断处理表格,进行后续的中断处理,根据中断前模式的不同分别执行不同的入口函数_irq_usr(用户模式入口函数)和irq_svc(superior模式入口函数),同时将系统模式切换到SVC模式。以用户模式为例,主要实现流程如下。
对于内核中,涉及到中断访问的接口主要由中断申请和释放的函数组成。
//gpio号映射到irq号
int gpio_to_irq(unsigned gpio);
//gpiod结构映射到irq编号
int gpiod_to_irq(const struct gpio_desc *desc);
//从设备树中解析获取interrupt属性对应的irq编号
unsigned int irq_of_parse_and_map(struct device_node *node, int index);
//从内核中申请irq对象
//handler为中断上半部,内部不能有延时和让系统进入休眠状态的代码
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);
//释放申请的irq对象
void free_irq(unsigned int irq, void *dev);
//带内核管理申请irq对象接口
//request_irq的系统管理版本,支持驱动移除时自动释放
static inline int __must_check
devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler,
unsigned long irqflags, const char *devname, void *dev_id)
//申请线程化的irq对象,其中handler为内核态执行函数,thread_fn为线程化的用户态执行函数
//hander,中断上半部
//thread_fn,中断下半部,线程化的中断执行函数,支持休眠和用户态的程序
//irqflags,定义的中断类型,需要与设备树中中断信息一致(或为0,则使用设备树定义的中断信息),否则重新加载驱动时会报错(第一次以驱动为准)
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)
//中断回调函数
static irqreturn_t key0_handler(int irq, void *dev_id)
{
//中断的具体实现
//...
return IRQ_RETVAL(IRQ_HANDLED);
}
当然,也可以通过devm_request_irq来申请中断,可以在驱动卸载时不用主动调用free_irq显示释放中断请求。此外,也可以通过disable_irq(非中断函数中),disable_irq_nosync(中断函数中)和enable_irq来管理中断的开关。
在单片机中,我们了解到中断的执行会打断其它应用的执行,所以中断的动作应该尽可能的短,如果具体的操作过长,会把代码分为两部分,其中时间相关比较紧要的在中断中执行,而非必要的则移动到主循环中执行,如UART通讯中,数据接收往往通过中断或者DMA获取,而协议解析和应用处理则在主循环执行,那么对于嵌入式Linux来说,在驱动中也采用类似的机制,把中断的应用拆成两部分执行,一般称为顶半部和底半部机制执行。
其中顶半部即为上述的中断回调函数key0_handler,其在内核模式下调用,会影响到系统的执行;因此只能进行简单的事件触发、状态修改动作,不能够执行导致挂起,休眠的行为,否则会出错。具体的应用则在底半部执行,实现具体的功能,底半部可以执行挂起或者休眠动作,不影响系统功能。当然,在顶半部也可以使用timer软件定时器,tasklet工作队列等方式,模拟系统底半部的执行,单独即可实现功能,当然使用devm_request_threaded_irq接口可以在单个接口中实现上述应用。
理解到这,我们对嵌入式Linux中的中断触发和在内核的应用已经有了初步的了解,那么我们是如何获取外设对应的硬件中断号,这就需要涉及设备树中的中断信息节点,以i.MX6Ull为例,在<3.2 Cortex A7 interrupts>章节中,定义了内部的中断ID,以按键对应的GPIO1_18引脚中断为例,其属性gpio1控制器,对应中断就为GIC_SPI_BASE+66和GIC_SPI_BASE+67,隶属于gpio1下的引脚,中断触发时检测和执行即如下所示。
关于中断在设备树中的应用如下所示。
//定义中断控制器节点
gpio1: gpio@0209c000 {
//其它已经注释
interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>;
interrupt-controller;
#interrupt-cells = <2>;
};
//具体的中断节点
key {
interrupt-parent = <&gpio1>;
interrupts = <18 IRQ_TYPE_EDGE_BOTH>;
};
//中断线号带两个参数类型,表示中断控制器为gpio1,对应gpio1的9号中断
interrput-parent = <&gpio1>;
interrupts = <9 IRQ_TYPE_LEVEL_LOW>;
//中断线号带三个参数类型
//interrupts = <中断类型,中断号偏移值,中断触发类型>
//中断类型
//GPI_SGI: software generated interrupt,中断号:0~15
//GIC_PPI: peripheral processer interrupt, 中断号:16~31
//GIC_SPI:shared processer interrupt, 中断号:32~160
interrupts = <GIC_SPI 23 1> //对应中断线号32+23=55, 上升沿触发
中断触发条件在设备树中的定义如下。
IRQ_TYPE_EDGE_BOTH 双边沿触发中断,等于(IRQ_TYPE_EDGE_RISING | IRQ_TYPE_EDGE_FALLING) |
在具体的代码实现中,即可通过函数irq_of_parse_and_map找到引脚对应的中断线号,用于后续中断回调函数的申请。
上面描述了中断引脚对应的设备树,下面描述中断使用的接口,具体如下。
在Linux内核中,中断处理分为上半部(top half)和下半部(bottom half)。上半部处理紧急的、对时间要求严格的任务,而下半部处理相对不紧急的任务。下半部可以通过多种机制实现,包括软中断(softirq)、tasklet和工作队列(workqueue)
软中断是一种在中断上下文执行的下半部机制。软中断可以在多个CPU上并行执行,因此适用于对响应时间要求较高的任务。软中断的处理函数必须是可重入的,因为它们可能在多个CPU上同时执行。
注册软中断
void softirq_handler(struct softirq_action *action)
{
// 处理软中断的代码
}
open_softirq(TIMER_SOFTIRQ, softirq_handler);
/**
* @brief 软中断类型枚举
*
* 定义了Linux内核中各种软中断的类型。软中断是一种在中断上下文执行的下半部机制,
* 用于处理相对不紧急的任务。软中断可以在多个CPU上并行执行,因此适用于对响应时间要求较高的任务。
* 软中断的处理函数必须是可重入的,因为它们可能在多个CPU上同时执行。
*/
enum
{
/**
* @brief 高优先级软中断
*
* 用于处理最高优先级的软中断任务。
*/
HI_SOFTIRQ = 0,
/**
* @brief 定时器软中断
*
* 用于处理定时器相关的软中断任务。
*/
TIMER_SOFTIRQ,
/**
* @brief 网络发送软中断
*
* 用于处理网络发送相关的软中断任务。
*/
NET_TX_SOFTIRQ,
/**
* @brief 网络接收软中断
*
* 用于处理网络接收相关的软中断任务。
*/
NET_RX_SOFTIRQ,
/**
* @brief 块设备软中断
*
* 用于处理块设备相关的软中断任务。
*/
BLOCK_SOFTIRQ,
/**
* @brief 中断轮询软中断
*
* 用于处理中断轮询相关的软中断任务。
*/
IRQ_POLL_SOFTIRQ,
/**
* @brief 任务队列软中断
*
* 用于处理任务队列相关的软中断任务。
*/
TASKLET_SOFTIRQ,
/**
* @brief 调度软中断
*
* 用于处理调度相关的软中断任务。
*/
SCHED_SOFTIRQ,
/**
* @brief 高精度定时器软中断
*
* 用于处理高精度定时器相关的软中断任务。
*/
HRTIMER_SOFTIRQ,
/**
* @brief RCU软中断
*
* 用于处理RCU(Read-Copy Update)相关的软中断任务。
* RCU是一种用于实现高效的读-写同步的机制。
*/
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
/**
* @brief 软中断总数
*
* 定义了软中断的总数,用于表示软中断类型的范围。
*/
NR_SOFTIRQS
};
触发软中断
raise_softirq(TIMER_SOFTIRQ);
在Linux内核中,中断处理分为上半部(top half)和下半部(bottom half)。上半部处理紧急的、对时间要求严格的任务,而下半部处理相对不紧急的任务。下半部可以通过多种机制实现,包括软中断(softirq)、tasklet和工作队列(workqueue)
//使用tasklet管理中断
//底半部应用部分执行
static void tasklet_do_func(unsigned long data)
{
printk(KERN_INFO"key interrupt tasklet do:%ld!\r\n", data);
}
DECLARE_TASKLET(tasklet_func, tasklet_do_func, 0);
//顶半部中断向量执行
static irqreturn_t key0_handler(int irq, void *dev_id)
{
/*触发事件*/
tasklet_schedule(&tasklet_func);
return IRQ_RETVAL(IRQ_HANDLED);
}
int key_probe(platform_device *pdev)
{
//申请中断
devm_request_irq(&pdev->dev, chip->irq, key0_handler,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
"key0",
(void *)chip);
}
工作队列是一种在进程上下文执行的下半部机制。工作队列适用于需要睡眠的任务,因为它们在进程上下文中执行,可以安全地调用睡眠函数。
定义和初始化工作队列
#include <linux/workqueue.h>
void work_handler(struct work_struct *work)
{
// 处理工作队列的代码
}
DECLARE_WORK(work, work_handler);
调度工作队列
schedule_work(&my_work);
内核定时器基于硬件定时器实现,它使用一个全局的定时器链表来管理所有的定时器。当一个定时器被创建并启动后,它会被插入到定时器链表中,并根据其到期时间进行排序。当硬件定时器触发中断时,内核会检查定时器链表,找出所有到期的定时器,并执行相应的处理函数。
struct key_data
{
struct timer_list timer;
};
//使用timer管理中断
//底半部应用部分执行
static void timer_function(struct timer_list *arg)
{
struct integration_data *chip = container_of(arg, struct integration_data, timer);
chip->timer.expires = jiffies + 10;
add_timer(&chip->timer);
printk(KERN_INFO"timer function:%d!\r\n", chip->value);
}
//顶半部中断向量执行
static irqreturn_t key0_handler(int irq, void *dev_id)
{
struct key_data *chip = (struct key_data *)dev_id;
chip->timer.expires = jiffies + 10;
add_timer(&chip->timer);
return IRQ_RETVAL(IRQ_HANDLED);
}
int key_probe(platform_device *pdev)
{
key_data *chip;
//.....
//申请中断
devm_request_irq(&pdev->dev, chip->irq, key0_handler,
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
"key0",
(void *)chip);
//初始化定时器
timer_setup(&chip->timer, timer_function, 0);
chip->timer.expires = jiffies + 10;
add_timer(&chip->timer);
}
static irqreturn_t key_handler(int irq, void *data)
{
struct key_data* chip = (struct key_data*)data;
printk(KERN_INFO"key_handler:%d!\r\n", chip->data);
}
int key_probe(platform_device *pdev)
{
key_data *chip;
//.....
//申请中断
chip->irq = irq_of_parse_and_map(nd, 0);
ret = devm_request_threaded_irq(&pdev->dev,
chip->irq,
NULL, key_handler,
IRQF_SHARED | IRQF_ONESHOT | IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
"key0",
(void *)chip);
}
至此,我们对嵌入式应用中的中断机制进行了解读,当然这些都是理论知识的说明,对于嵌入式应用中,如何结合实际情况,配置合适的中断优先级,并实现应用从而满足产品的需求才是最重要的部分。这部分经验是需要实践积累和总结的,不过理解了中断实现的背后机制,在实践中知其然也知其所以然,以理论配合应用来学习,也是嵌入式开发的最佳提升之道。
直接开始下一节说明: i2c设备和驱动管理框架