1.中断基础
1.1.什么是中断
中断是指在CPU正常运行期间,由于内部事件或外部事件引起的CPU暂时停止正在运行的程序,转去该内部或外部事件的服务程序中去,服务程序执行完毕后再返回断点处继续运行被暂时中断的程序。
在中断发生时被调用的用来处理中断的函数称为中断处理函数。
中断的处理流程如下图所示:
1.2.中断的意义
外设的处理速度远远慢于CPU的处理速度,如果采用轮询方式(CPU一直等待)处理外设,会降低CPU的利用率。如果采用中断方式处理外设,会大大提高CPU的利用率。
以cpu读取串口信息为例:
为了防止串口数据的丢失,不采用中断,采用轮询,CPU只能不停读取串口的信息;
如果采用中断,cpu当发现串口数据不可读时,cpu可以做其他事情,一旦串口数据准备就绪,串口会给CPU发送一个中断信号,告诉cpu,数据准备就绪,请来处理,cpu处理完毕以后,再接着执行原先被打断的任务!
1.3.中断控制器
外设的中断信号并不是直接输送给CPU,而是先给中断控制器,经过中断控制器的判断以后,再决定是否给CPU发送中断信号。然后CPU一旦接收到了外设的中断信号之后,CPU开始进行中断处理。
中断控制器具有以下几个作用:
1.能够屏蔽或者使能某个中断信号;
2.能够设置中断优先级;
3.能够设置外设中断信号的有效触发方式;
4.如果是多核,能够指定中断信号给哪个CPU发送。
Cotex-M系列处理器使用NVIC中断控制器;
Cotex-A/R系列处理器使用GIC中断控制器。
2.内核中断编程
Linux内核中有专门中断子系统,其实现原理非常复杂,但是驱动开发者不需要知道其实现的具体细节,只需要知道如何应用该子系统提供的API函数来编写中断相关驱动代码即可。
2.1.申请中断
在Linux内核中要想使用某个中断是需要申请的,在驱动程序中通过request_irq函数申请中断。
头文件: #include <linux/interrupt.h>
函数原型:int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev)
函数功能:向内核申请硬件中断资源,注册中断处理函数到内核
函数参数:
irq 要申请中断的中断号
handler 中断处理函数,当中断发生后就会执行此中断处理函数
flags 中断标志
name 中断名字,申请中断成功后可在/proc/interrupts文件中看到对应的中断名字
dev 给中断处理函数传递参数,可以为NULL
函数返回值:成功返回0,失败返回负数(-EINVAL表示中断号无效,-EBUSY表示中断已经被占用)
2.2.中断号
硬件中断对应的软件编号,又称中断号。该中断号一般由芯片厂家在内核源码中定义。不同的芯片中断数量不一样,对应的外设也不相同,所以中断号定义一般是存放在具体芯片相关的目录中。一般定义一般在:arch\构架\mach-芯片型号\include\mach\irqs.h头文件中。
对于外部中断,一般不直接使用上面的EXYNOS4_IRQ_EINTX 宏,都是使用内核提供的通用API函数:gpio_to_irq来获取中断号,该函数可以通过 IO 口管脚编号转换成它对应中断编号。IO口管脚编号也是由芯片厂商在内核源码arch/构架/mach-芯片型号/include/mach/gpio.h文件中定义好的。
2.3.中断处理函数
中断处理函数的格式为:irqreturn_t (*irq_handler_t)(int, void *);
第一个参数表示中断号,第二个参数是一个指向void的指针。
在中断处理函数被调用时,这两个参数实际传递的就是request_irq 时的第1个参数(irq)和最后一个参数(dev)。
中断处理函数的返回值是irqreturn_t类型,
irqreturn_t在include/linux/irqreturn.h文件中定义如下:
/*
* enum irqreturn
* @IRQ_NONE interrupt was not from this device
* @IRQ_HANDLED interrupt was handled by this device
* @IRQ_WAKE_THREAD handler requests to wake the handler thread
*/
enum irqreturn {
IRQ_NONE = (0 << 0), //表示中断没有被处理,比较少用,只会在共享中断中出现。
IRQ_HANDLED = (1 << 0), //表示中断被正确处理了,常用。
IRQ_WAKE_THREAD = (1 << 1), //表示去唤醒中断处理者的线程,基本不用。
};
typedef enum irqreturn irqreturn_t;
可以看出irqreturn_t是个枚举类型,所以中断处理函数一共有三种返回值。
注:
内核要求中断处理函数的执行的速度要快,不能做休眠操作以避免中断长时间占用CPU资源,导致系统的并发和响应能力受到影响。在Linux内核中,中断不隶属于任何进程,不参与进程的调度。
2.4.中断标志
中断标志用来描述中断的特征。其值由内核源码定义,可以在文件include/linux/interrupt.h里面查看所有的中断标志。这里介绍几个常用的中断标志:
标志 描述
IRQF_DISABLED 指明该中断是独占中断,只能注册一次
IRQF_SHARED 指明该中断是共享中断,可以注册多次
IRQF_ONESHOT 单次中断,中断执行一次就结束
IRQF_TRIGGER_NONE 无触发,用于内部事件
IRQF_TRIGGER_RISING 上升沿触发
IRQF_TRIGGER_FALLING 下降沿触发
IRQF_TRIGGER_HIGH 高电平触发
IRQF_TRIGGER_LOW 低电平触发
以上这些标志可以通过“|”来实现多种组合。当然IRQF_DISABLED与IRQF_SHARED是不能相组合的。
2.5.共享中断与独占中断
独占中断是指一个中断源单独占据一个中断线,该中断独占一个中断号,该中断号对应一个中断处理函数。
共享中断是指多个中断源共享一根中断线的情况,它们使用同一个中断号,该中断号对应N个中断处理函数。
对于独占中断,同一个中断号只能被request_irq注册一次。
对于共享中断,可以多次调request_irq注册同一个中断号。
中断处理函数模型差异:
使用同一个中断号注册的所有中断处理函数会链接成一个链表,这样当共享中断中的一个外设产生中断时,就要遍历其对应的中断处理程序链表,直到某一个中断服务函数返回时返回IRQ_HANDLED。
如果是共享中断,其中断处理函数模型如下:
irqreturn_t isr_name(int irq,void *dev)
{
if(不是本外设产生中断信号) //一般通过外设的状态寄存器判断
return IRQ_NONE;
…… //中断处理
return IRQ_HANDLED;
}
注意:
当中断产生时判断哪一个外设产生的中断,是通过外设的状态寄存器来判断,而不是通过中断处理函数中的dev参数。
如果是独占中断,其中断处理函数模型如下:
irqreturn_t isr_name(int irq,void *dev)
{
if(外设状态寄存器标志位置1 或 IO口电平满足要求){
……
}
return IRQ_HANDLED;
}
dev参数使用差异:
如果flags指定为独占中断(IRQF_DISABLED),request_irq函数中的dev参数可以指定为NULL。
如果 flag指定为共享中断(IRQF_SHARED),request_irq函数中的dev参数则不能为NULL,并且保证惟一性。
很多人认为指定为共享中断时,dev 用来标识发生中断时具体是哪一个中断。但实际上,这个dev并不具备标识是哪一个中断的能力。这个参数真正的用途是用来注销共享中断中的具体哪一个中断。
当然,不同的中断号可以注册相同的中断处理函数,用dev参数来区分不同的中断处理函数,这一点和在创建N个线程时,可以运行同样的线程函数,通过线程函数参数来进行区分是一个道理。
2.6.释放中断
中断使用完成以后需要进行释放,在驱动程序中通过free_irq函数释放相应的中断。
头文件: #include <linux/interrupt.h>
函数原型:void free_irq(unsigned int irq, void *dev);
函数功能:释放中断资源,删除中断处理函数
函数参数:irq 要释放中断的中断号
dev 给中断处理函数传递的参数,切记一定要和注册时传递的参数一样
函数返回值:无
2.7.中断使能与禁止
void enable_irq(unsigned int irq);
void disable_irq(unsigned int irq);
enable_irq和disable_irq用于使能和禁止指定的中断,irq就是要禁止的中断号。
使用request_irq函数申请中断时,便已激活中断,不需要手动使能中断。
要注意的是,disable_irq函数要等到当前正在执行的中断处理函数执行完才返回。因此使用者需要保证不会产生新的中断,并且确保所有已经开始执行的中断处理程序已经全部退出。在这种情况下,可以使用另外一个中断禁止函数:
void disable_irq_nosync(unsigned int irq);
disable_irq_nosync函数调用以后立即返回,不会等待当前中断处理程序执行完毕。
有时候我们需要关闭当前处理器的整个中断系统,可以使用下面两个函数:
local_irq_enable()
local_irq_disable()
2.7.内核中断编程步骤
1)看硬件原理图,确定使用的中断;
2)确定功能编写中断服务函数;
3)在适当的地方(驱动加载函数)调用 request_irq注册中断;
4)在相反的地方(驱动卸载函数)调用 free_irq 注销中断;
补充:如果在注册过程没有完成整个工作流程,也需要注销已经注册的中断。
3.内核中断编程示例
3.1.按键中断信息结构体
typedef struct{
int gpio_num;
char *irq_name;
int key_num;
}KeyIRQ_Info;
KeyIRQ_Info keysData[4]= {
{EXYNOS4_GPX3(2),"key1",1},
{EXYNOS4_GPX3(3),"key2",2},
{EXYNOS4_GPX3(4),"key3",3},
{EXYNOS4_GPX3(5),"key4",4},
};
3.2.中断处理函数
irqreturn_t keysIRQ_Handler(int irq, void *dev)
{
//printk("*****%s*****\n",__FUNCTION__);
int keynum = *(int *)dev;
if( *keydat & 1<<(keynum+1) ){ //按键松开
printk("key%d is up\n",keynum);
}else{ //按键按下
printk("key%d is press\n",keynum);
}
return IRQ_HANDLED;
}
3.3.驱动加载函数
static int __init keyIRQ_init(void)
{
int i,ret;
unsigned int flag = IRQF_TRIGGER_RISING|IRQF_TRIGGER_FALLING;
printk("*****%s*****\n",__FUNCTION__);
/* 地址映射 */
keycon = ioremap(0x11000C60, 4);
keydat = ioremap(0x11000C64, 4);
/* 配置key对应IO口 */
*keycon &= ~((0xf<<2*4)|(0xf<<3*4)|(0xf<<4*4)|(0xf<<5*4));
/* 申请4个按键中断 */
for(i=0;i<4;i++){
key_irq = gpio_to_irq(keysData[i].gpio_num);
ret = request_irq(key_irq, keysIRQ_Handler,flag,keysData[i].irq_name,&keysData[i].key_num);
if(ret < 0){
break;
}
}
/* 申请中断发生失败,则要把之前申请过的全部释放掉 */
if(ret < 0){
for(i=i-1;i>=0;i--){
key_irq = gpio_to_irq(keysData[i].gpio_num);
free_irq(key_irq, &keysData[i].key_num);
}
return ret;
}
return 0;
}
3.4.驱动卸载函数
static void __exit keyIRQ_exit(void)
{
int i;
printk("*****%s*****\n",__FUNCTION__);
/* 释放4个按键中断 */
for(i=0;i<4;i++){
key_irq = gpio_to_irq(keysData[i].gpio_num);
free_irq(key_irq, &keysData[i].key_num);
}
/* 解除映射 */
iounmap(keycon);
iounmap(keydat);
}