1、简介
中断是指CPU在执行程序的过程中,出现了某种突发事件,使得CPU不得不暂停当前正在处理的事务,转而去处理突发事务,处理完毕后CPU又返回原程序被中断的位置继续执行。
中断根据分类,又可以分为内部中断和外部中断、可屏蔽中断和不可屏蔽中断、向量中断和非向量中断。
内部中断:中断源来自CPU内部(软件中断指令、溢出、除法错误等)。例如操作系统从用户态切换到内核态需借助CPU内部的软件中断。
外部中断:中断源来自CPU外部,由外设提出请求。
可屏蔽中断:可以通过屏蔽字被屏蔽,屏蔽后,该中断不再得到响应。
不可屏蔽中断:不能被屏蔽。
向量中断:采用向量中断的CPU通常为不同的中断分配不同的中断号,当检测到某个中断号的中断到来后,就自动跳转到与该中断号对应的地址执行。不同的中断号有不同的入口地址。向量中断由硬件提供中断服务程序入口地址。
非向量中断:多个中断共享一个入口地址,进入该入口地址后再通过软件判断中断标志来识别具体是哪个中断。非向量中断由软件提供中断服务程序入口地址。
2、linux的中断处理
由于中断服务程序的执行并不存在于进程的上下文,因此,要求中断处理不能嵌套,中断的处理过程越快越好。但实际情况是,中断处理程序并不能保证处理时延,有的中断处理程序需要处理很复杂的工作,由于中断无法打断无法嵌套的缘故,这段时间就会全部分配给中断处理程序,而无法进行其他工作了,这是不可接受的。因此Linux就将中断处理程序拆分成了两个部分:上半部(top half)和下半部(bottom half)。
上半部:完成尽可能少的比较紧急的工作,往往是简单读取寄存器中的中断状态并清除中断标志后就进行中断登记的工作:将下半部处理程序挂到该设备的下半部执行队列中去。这样上半部执行的速度就会很快,可以服务更多的中断请求。往往被设计为不可被中断。
下半部:完成中断事件的绝大多数任务。下半部几乎做了中断处理程序的所有事情,而且下半部可以被新的中断打断。下半部相对来说并不是非常紧急的,而且相对比较耗时,不在硬件中断服务程序中执行。
3、Linux中断编程
在linux设备驱动中使用request_irq()申请中断号,使用free_irq()释放中断号。例如想要注册一个在信号下降沿触发的GPIO中断:
irq_handler_t irq_ioctl_ptr = irq_ioctl;
for(i=0;i<count;i++)
{
gpio_request(gpios[i].gpio,"tmc_irq_test");//请求使用编号为 gpios[i].gpio的GPIO
gpio_direction_input(gpios[i].gpio);//将指定的GPIO配置为输入模式
gpios[i].irq = gpio_to_irq(gpios[i].gpio);//将GPIO转换为相应的中断号
ret = request_irq(gpios[i].irq,irq_ioctl_ptr,IRQF_TRIGGER_FALLING,"tmc_irq_test",&gpios[i]);//注册中断处理函数
init_waitqueue_head(&irq_wq);
}
gpio_request(gpios[i].gpio,"tmc_irq_test"):请求一个GPIO引脚,第一个参数是要申请的引脚编号,第二个参数是该引脚的标签描述。
gpio_direction_input():配置GPIO引脚的方向。一个参数,要配置的引脚编号。
gpio_to_irq():将GPIO编号转换为中断请求号。一个参数,待转换的引脚编号。后续代码可以使用这个中断请求号来注册中断处理函数或进行其他相关操作。
request_irq():注册中断处理函数。第一个参数为中断请求号。第二个参数handler为中断处理函数的指针。当指定的中断发生时,这个函数会被调用。第三个参数flags用于指定中断的触发条件,此处IRQF_TRIGGER_FALLING表示下降沿触发。第四个参数为中断请求的名称。第五个参数在中断共享时会用到,一般设置为这个设备的设备结构体或者NULL。
中断处理程序irq_ioctl如下所示,实现逻辑很简单,先延时80ms消除抖动,随后判断是否仍是低电平,是的话就说明这是一个人为操作的下降沿,输出打印信息(完成中断处理任务)。延时使用delay进行延时,使用sleep延时系统会挂掉(不知道为什么)。不过最好还是不要在中断处理函数内使用延时函数。
enum irqreturn irq_ioctl (int irq, void *dev_id)
{
mdelay(80);//不能用msleep,会出问题,但是可以用mdelay延时
if(gpio_get_value(gpios[0].gpio)!=0)
{
return IRQ_NONE;
}
flag=1;
wake_up_interruptible(&irq_wq);
printk("detect low level\n");
return IRQ_HANDLED;
}
4、下半部实现机制
事实上可以把irq_ioctl()函数里除了wake_upinterruptible()的其它功能看作中断的上半部。在这里msleep其实是不合适的,应该放在被等待队列唤醒的阻塞进程中去执行。即:
ssize_t irq_read (struct file *filp, char __user *buf, size_t size, loff_t *off)
{
char tmp_buf[10];
int ret;
printk("irq_read: wait flag==1, now flag=%d\n",flag);
wait_event_interruptible(irq_wq,flag==1);
printk("irq_read: get flag==1!, now flag=%d\n",flag);
msleep(8000);
if(gpio_get_value(gpios[0].gpio)!=0)
{
return IRQ_NONE;
}
flag=0;
tmp_buf[0]=1;
size=1;
ret = copy_to_user(buf,tmp_buf,size);
if(ret<0)
{
printk("copy_to_user error!\n");
return -1;
}
return size;
}
enum irqreturn irq_ioctl (int irq, void *dev_id)
{
flag=1;
wake_up_interruptible(&irq_wq);
printk("detect low level\n");
return IRQ_HANDLED;
}
这种唤醒机制从严格意义上而言,不算一种下半部的机制。正儿八经实现下半部的机制有:tasklet、工作队列、软中断三种。
以tasklet为例,只需定义tasklet及其处理函数并将两者关联即可。例如:
void irq_tasklet_func(unsigned long); DECLARE_TASKLET(irq_tasklet,irq_task_let_func,data); tasklet_schedule(&irq_tasklet);
代码定义了实现名称为irq_tasklet的tasklet并将其与irq_tasklet_func函数绑定,传入data参数。在需要调度tasklet的时候,使用tasklet_schedule()函数就能使系统在适当的时候进行调度运行。例如:
struct tasklet_struct irq_tasklet; void irq_tasklet_func(struct tasklet_struct *unused) { msleep_interruptible(80); if(gpio_get_value(gpios[0].gpio)!=0) { return; } flag=1; printk("detect low level\n"); } DECLARE_TASKLET(irq_tasklet,irq_tasklet_func); enum irqreturn irq_ioctl (int irq, void *dev_id) { // wake_up_interruptible(&irq_wq); tasklet_schedule(&irq_tasklet); return IRQ_HANDLED; }