中断处理模型
要想弄清楚 desc->handle_irq(irq, desc)和我们注册的中断有什么关联,就要
了解中断处理模型了。
1.中断处理模型结构
①NR_IRQS:最大的中断号include/asm/arch/irq.h
②irq_desc[]:是一个指向 irq_desc_t 结构的数组, irq_desc_t 结构是各个设备中断服务例程的描述符。Irq_desc_t 结构体中的成员 action 指向该中断号对应的 irqaction 结构体链表。Irqaction 结构体定义在include/linux/interrupt.h 中,如下:
struct irqaction {
irq_handler_t handler; //中断处理函数,注册时提供
unsigned long flags; //中断标志,注册时提供
cpumask_t mask; //中断掩码
const char *name; //中断名称
void *dev_id; //设备 id,本文后面部分介绍中断共享时会详细说明
这个参数的作用
struct irqaction *next; //如果有中断共享,则继续执行,
int irq; //中断号,注册时提供
struct proc_dir_entry *dir; //指向 IRQn 相关的/proc/irq/n 目录
的描述符
};
当我们注册中断号为 irq 的中断服务程序时,系统会根据注册参数封装相应的irqaction 结构体。并把中断号为 irq 的 irqaction 结构体写入 irq_desc[irq]->action(中断号用来搜索位置的,数组的下标)。这样就把设备的中断请求号与该设备的中断服务例程irqaction 联系在一起了。样当 CPU 接收到中断请求后,就可以根据中断号通过 irq_desc []找到该设备的中断服务程序。
2.中断共享的处理模型
共享中断的不同设备的 iqraction 结构体都会添加进该中断号对应的 irq_desc结构体的 action 成员所指向的 irqaction 链表内。当内核发生中断时,它会依次调用该链表内所有的 handler (irqaction里的)函数。因此,若驱动程序需要使用共享中 断机制,其中断处理函数必须有能力识别是否是自己的硬件产生了中断。通常是通过读取该硬件设备提供的中断 flag 标志位进行判断。也就是说不是任何设备都 可以做为中断共享源的, 它必须能够通过的它的中断 flag 判断出是否发生了中断。中断共享的注册方法是:
int request_irq(unsigned int irq, irq_handler_t handler,IRQF_SHARED, const char *devname, void *dev_id)
很多权威资料中都提到,中断共享注册时的注册函数中的 dev_id 参数是必不可少的,并且 dev_id 的值必须唯一。那么这里提供唯一的 dev_id 值的究竟是做什么用的?
根据我们前面中断模型的知识,可以看出发生中断时,内核并不判断究竟是共享中断线上的哪个设备产生了中断,它会循环 执行所有该中断线上注册的中断处理函数(即 irqaction->handler 函数)。因此 irqaction->handler 函数有责 任识别出是否是自己的硬件设备产生了中断,然后再执行该中断处理函数。通常是通过读取该硬件设备提供的中断 flag 标志位进行判断。那既然 kernel 循 环执行该中断线上注册的所有 irqaction->handler 函数,把识别究竟是哪个硬件设备产生了中断这件事交给中断处理函数本身去做,那 request_irq 的 dev_id 参数究竟是做什么用的?
很多资料中都建议将设备结构指针作为 dev_id 参数。在中断到来时,迅速地根据硬件寄存器中的信息比照传入的 dev_id 参数判断是否是本设备的中断,若不是,应迅速返回。这样的说法没有问题,也是我们编程时都遵循的方法。但事实上并不能够说明为什么中断共享必 须要设置 dev_id。
下面解释一下 dev_id 参数为什么必须的,而且是必须唯一的。当调用 free_irq 注销中断处理函数时(通常卸载驱动时其中断处理函数也会被注销掉),因为 dev_id 是唯一 的,所以可以通过它来判断从共享中断线上的多个中断处理程序中删除指定的一个。如果没有这个参数,那么 kernel 不可能知道给定的中断线上到底要删除哪 一个处理程序。
注销函数定义在 Kernel/irq/manage.c 中定义:
void free_irq(unsigned int irq, void *dev_id)
3.子中断注册的实现
①问题的提出
参看 3.5 节中判断中断号的方法,可以看到只是通过 S3C2410 中断控制器中的INTOFFSET 寄存器来判断的。对于 INTPND 中的 EINT4_7、EINT8_23、INT_UART0、INT_ADC 等带有子中断的向量,INTOFFSET 无法判断出具体的中断号。平台留给我们的注册方法如下:
在 include/asm/arch/irqs.h 中有类似如下定义:
/* interrupts generated from the external interrupts sources */
#define IRQ_EINT4 S3C2410_IRQ(32) /* 48 */
#define IRQ_EINT5 S3C2410_IRQ(33)
#define IRQ_EINT6 S3C2410_IRQ(34)
#define IRQ_EINT7 S3C2410_IRQ(35)
#define IRQ_EINT8 S3C2410_IRQ(36)
#define IRQ_EINT9 S3C2410_IRQ(37)
#define IRQ_EINT10 S3C2410_IRQ(38)
#define IRQ_EINT11 S3C2410_IRQ(39)
#define IRQ_EINT12 S3C2410_IRQ(40)
#define IRQ_EINT13 S3C2410_IRQ(41)
#define IRQ_EINT14 S3C2410_IRQ(42)
#define IRQ_EINT15 S3C2410_IRQ(43)
#define IRQ_EINT16 S3C2410_IRQ(44)
#define IRQ_EINT17 S3C2410_IRQ(45)
#define IRQ_EINT18 S3C2410_IRQ(46)
#define IRQ_EINT19 S3C2410_IRQ(47)
#define IRQ_EINT20 S3C2410_IRQ(48) /* 64 */
#define IRQ_EINT21 S3C2410_IRQ(49)
#define IRQ_EINT22 S3C2410_IRQ(50)
#define IRQ_EINT23 S3C2410_IRQ(51)
可以看到平台为每种子中断都定义了中断号, 如果你想实现 EINT10 的中断注册,直接按照 IRQ_EINT10 这个中断号注册都可以了。那么平台代码是如何实现这部分中断注册的呢?
②注册问题的解决
/*arch/arm/plat-s3c24xx/irq.c*/
void __init s3c24xx_init_irq(void)
{ ……
set_irq_chained_handler(IRQ_EINT4t7, s3c_irq_demux_extint4t7);
set_irq_chained_handler(IRQ_EINT8t23, s3c_irq_demux_extint8);
set_irq_chained_handler(IRQ_UART0, s3c_irq_demux_uart0);
set_irq_chained_handler(IRQ_UART1, s3c_irq_demux_uart1);
set_irq_chained_handler(IRQ_UART2, s3c_irq_demux_uart2);
set_irq_chained_handler(IRQ_ADCPARENT, s3c_irq_demux_adc);
……
}
平台在初始化时会调用到 s3c24xx_init_irq,在此函数中实现了对 EINT4_7、EINT8_23、INT_UART0、INT_ADC 等中断的注册。下面看看这些带有子中断的中断号对应的处理函数的内容。以 IRQ_EINT4t7 为例,其它情况类似。
/*arch/arm/plat-s3c24xx/irq.c*/
s3c_irq_demux_extint4t7(unsigned int irq,struct irq_desc *desc)
{
unsigned long eintpnd = __raw_readl(S3C24XX_EINTPEND);
unsigned long eintmsk = __raw_readl(S3C24XX_EINTMASK);
eintpnd &= ~eintmsk;
eintpnd &= 0xff; /* only lower irqs */
/* eintpnd 中可以有多个位同时置 1,这一点和 intpnd 的只能有 1 个位置
1 是不一样的 */
while (eintpnd) { //循环执行所有置位的子中断
irq = __ffs(eintpnd); //算出第一个不为 0 的位,类似 arm v5后的 clz 前导 0 的作用
eintpnd &= ~(1<<irq); //清除相应的位
irq += (IRQ_EINT4 - 4); //算出对应的中断号
desc_handle_irq(irq, irq_desc + irq); //执行对应子中断的注
册函数
}
}
从上面的函数可以看出子中断是如何注册及被调用到的。有人可能会问为何不在include/asm/arch-s3c2410/entry-macro.s 文件中 get_irqnr_and_base 函数判断中断号时,直接算出对应的子中断号,就可以直接找到子中断处理了呢?原因是: get_irqnr_and_base 是平台给系统提供的函数,对于多个子中断同时置位的情况无法通过一个值返回(因为子中断中,如 eintpnd 是可以多个位同时置位的))。而 intpnd 则没有这个问题。