LINUX2.4 for S3C2410 的中断问题
%A 作者:崔兴旺 Email
zbcxw@sina.com qq:364749107
%A 1、 LINUX中,中断的安装和使用:
%A 当中断系统硬件产生一个中断信号,LINUX的中断处理系统将根据从硬件获得的中断号调用用户编写的中断处理程序,这个处理程序根据需要可以分为中断上半部和中断下半部(bottom half),一般将需要马上处理的动作安置在上半部中处理,这时是关中断运行。可以在稍后处理的动作安置在下半部处理,这时是开中断运行,可以允许其他中断进入。下半部根据情况可以要也可以不要。
%A A、因此首先要定义一个中断上半部处理程序,原型为:
%A #include <linux/intrrupt.h>
%A #include <linux/sched.h>
%A void Mydev_interrupt_handle(int irq,void *dev_id,struct pt_reg *regs)
%A 其中irq为设备的中断号,这是操作系统传来的,也许你认为这多余,因为系统根据中断号来查找哪个函数处理设备请求,本函数只管进行数据处理就是,管它中断号干什么?这就是你只知其一不知其二了:linux允许多个中断号对应一个设备处理程序,这样你的中断处理函数就可以根据中断号来判断你的设备中那个引脚产生的这次请求,是不是灵活性很大?
%A *dev_id是设备标识符,据说是为了允许一个中断处理程序服务于几个同类型的设备,大概和第一个参数的作用差不多。下一个*regs参数指向中断发生后保存的各个寄存器内容的内核态堆栈区,我想一般程序员不会动它的。当然,最简单的情况下(也是大多数情况下)这几个参数都可以不用,虽然不用,你的中断处理程序也得按这个形式声明,否则在编译阶段就会遇到麻烦。
%A B、下面该定义一个下半部处理程序了:
%A void mydev_bh_interrupt(void)
%A 必须这么简单,没有参数,没有返回值。
%A C、注册下半部。linux系统在一个数组中存储所有的下半部处理程序指针,你必须将你的下半部处理程序的指针插入这个数组中的一个位置,并记住这个位置编号以备在合适的时候告诉系统,这个位置指向的程序就是我需要的。将你的下半部处理程序插入这个数组中的操作叫做“注册下半部”,使用的函数在kernel/softirq.c中的
%A void init_bh(int nr, void (*routine)(void))
%A {
%A bh_base[nr] = routine;
%A mb();
%A }
%A 参数nr为数组中的位置,有资料说其值最大为31,即linux允许最多有32个下半部同时存在。routine为你的下半部处理程序。仔细看void (*routine)(void)和void mydev_bh_interrupt(void)有什么不同?有经验的程序员很快可以看出这是两个形式完全相同的函数定义。对!*routine是形参,而mydev_bh_interrupt可以作为实参,实参和形参的数据类型必须相同。
%A 你还可以看出,bh_base[]就是存储下半部处理程序指针的那个数组。
%A 如果参考别人的一些代码,你会发现比如init_bh(TIMER_BH, timer_bh)一类的语句,这里的timer_bh肯定是一个中断下半部(望文生义可以知道是时钟中断),让人费解的是TIMER_BH是那个所谓数组中的那个位置?你可以在include/linux/intrrupt.h中找到一个枚举类型的定义:
%A enum {
%A TIMER_BH = 0,
%A TQUEUE_BH,
%A DIGI_BH,
%A SERIAL_BH,
%A RISCOM8_BH,
%A SPECIALIX_BH,
%A AURORA_BH,
%A ESP_BH,
%A SCSI_BH,
%A IMMEDIATE_BH,
%A CYCLADES_BH,
%A CM206_BH,
%A JS_BH,
%A MACSERIAL_BH,
%A ISICOM_BH
%A };
%A 这个枚举类型没有名字,因为linux只使用它的枚举元素,其中每一个符号均为一个有着固定值得常量,这里TIMER_BH = 0,SERIAL_BH=3。知道你找到的别人的代码中的TIMER_BH是什么了吧?就是0而已。当然这里定义的这些符号都是系统常用的一些中断处理程序,如果你自己做了一个实验板,让他占用一个空闲的中断,给它写的驱动程序最好不要使用这些已有含义的符号(如果你不怕别扭硬是使用也无不可),要么修改这个枚举类型定义,加一个自己喜欢的符号(你得重新编译linux),要么就使用一个数字(要大于已定义的最大数字)。
%A 说来说去就是:调用init_bh()注册下半部。
%A D、安装中断。以上的工作都做好了,那么万事俱备,只欠东风了,安装好中断,你的中断程序就会立即工作。
%A 调用request_irq()函数安装中断。原型是:
%A int request_irq(unsigned int irq, void (*handler)(int, void *, struct pt_regs *),unsigned long irq_flags, const char * devname, void *dev_id)
%A 是不是感到很繁琐?别急,其实只有几个是重要的,irq,就是你的设备占用的中断号, 它的确定是本文讨论的难点,在此先认为有高人已经告诉你了,不须你操心。*handler,是你的中断上半部处理程序的指针,就是函数名。irq_flags,顾名思义,一些标志,先记住SA_INTERRUPT和SA_SHIRQ ,他们起什么作用可以上网查一下,对于更深的认识中断处理有帮助,对于练习基本使用中断,不了解问题不大。Devname,随便给你的设备取个名字,注意一定要是个字符串或者字符串的指针,最后一个*dev_id,如果不明白就使用NULL,就是告诉系统,我不知道。
%A 如果你不想使用下半部程序了,可以使用remove_bh()主销。
%A 如果你不想使用这个中断了,可以使用free_irq()释放这个中断。
%A 注意,中断处理程序必须在内核模式下运行,即在驱动程序中运行,如果你在用户态的代码中使用上述函数,可能连编译都不会通过。所以如果看不懂我以上写的是什么玩艺,最好先学习一下如何编写linux下的驱动程序。
%A 好了,你的中断程序开始工作了,想办法让你的设备产生一个中断信号试试!怎么样?死机了吧?我的经验是:开发设备驱动程序而从没有让机器死掉,这种情况是很少见的。
%A
%A 2、 中断号的确定
%A 上面在说到安装中断的时候说过,调用request_irq()时的参数中irq的确定是个难题,为什么?
%A 你如果到网络上查一下关于linux的资料,十有八九是关于i386体系结构上的,但linux是可以运行在多种cpu上的,比如采用arm内核的s3c2410,在i386体系上的经验在这里可以用么?我们试验一下:硬件准备,使用s3c2410的EINT0引脚作中断测试,为它编写一个中断驱动程序,最后将面临中断的安装,调用int request_irq(unsigned int irq, void (*handler)(int, void *, struct pt_regs *),unsigned long irq_flags, const char * devname, void *dev_id),其余都好说,irq取什么值?很容易查到类似信息:在i386体系结构中,LINUX内核对中断向量号的使用规定如下:
%A 0-31号中断向量对应于异常类型,32-47号中断向量分配给可屏蔽硬件中断,48-255号分配给软件中断。所以中断控制器上的IRQ0对应于中断向量32。
%A 在ARM的手册上,所有的中断都叫异常,在S3C2410的手册上,其中断控制器的外部中断都叫EINTn,看起来EINT0就像是IRQ0 。
%A 那我们不如试试吧,按照经验,取irq=32, request_irq返回一个值,乃是-EINVAL,意思是说irq的取值非法,要么试试其他值,反正都不能正常工作。
%A 怎么办?让我们寻根求源吧!
%A 文章开头说过:“当中断系统硬件产生一个中断信号,LINUX的中断处理系统将根据从硬件获得的中断号调用用户编写的中断处理程序”,我们看一下linux是如何取得中断号的过程,从中寻觅硬件的中断号如何对应到中断安装的系统调用中的参数的数值。中断产生后cpu进入中断向量指向的总的中断入口程序,对于arm的cpu来说,这个程序在arch/arm/kernel/ entry-armv.S中的这部分:
%A vector_IRQ: @
%A @ save mode specific registers
%A @
%A ldr r13, .LCsirq
%A sub lr, lr, #4
%A str lr, [r13] @ save lr_IRQ
%A mrs lr, spsr
%A str lr, [r13, #4] @ save spsr_IRQ
%A @
%A @ now branch to the relevent MODE handling routine
%A @
%A mov r13, #I_BIT | MODE_SVC
%A msr spsr_c, r13 @ switch to SVC_32 mode
%A
%A and lr, lr, #15
%A ldr lr, [pc, lr, lsl #2]
%A movs pc, lr @ Changes mode and branches
%A
%A .LCtab_irq: .word __irq_usr @ 0 (USR_26 / USR_32)
%A .word __irq_invalid @ 1 (FIQ_26 / FIQ_32)
%A .word __irq_invalid @ 2 (IRQ_26 / IRQ_32)
%A .word __irq_svc @ 3 (SVC_26 / SVC_32)
%A .word __irq_invalid @ 4
%A .word __irq_invalid @ 5
%A .word __irq_invalid @ 6
%A .word __irq_invalid @ 7
%A .word __irq_invalid @ 8
%A .word __irq_invalid @ 9
%A .word __irq_invalid @ a
%A .word __irq_invalid @ b
%A .word __irq_invalid @ c
%A .word __irq_invalid @ d
%A .word __irq_invalid @ e
%A .word __irq_invalid @ f
%A
%A .align 5
%A 根据注释并仔细研究这部分可知,中断产生后,程序保存一些于中断相关的特殊寄存器,改变cpu模式为svc模式并转到__irq_usr,这部分代码如下:
%A __irq_usr: sub sp, sp, #S_FRAME_SIZE
%A stmia sp, {r0 - r12} @ save r0 - r12
%A ldr r4, .LCirq
%A add r8, sp, #S_PC
%A ldmia r4, {r5 - r7} @ get saved PC, SPSR
%A stmia r8, {r5 - r7} @ save pc, psr, old_r0
%A stmdb r8, {sp, lr}^
%A alignment_trap r4, r7, __temp_irq
%A zero_fp
%A 1: get_irqnr_and_base r0, r6, r5, lr
%A movne r1, sp
%A adrsvc ne, lr, 1b
%A @
%A @ routine called with r0 = irq number, r1 = struct pt_regs *
%A @
%A bne do_IRQ
%A mov why, #0
%A get_current_task tsk
%A b ret_to_user
%A 注意get_irqnr_and_base r0, r6, r5, lr一句,这是一个宏,顾名思义,它将取得中断号,将这个宏展开,对于s3c2410来说,是其中的这部分代码:
%A #elif defined(CONFIG_ARCH_S3C2410)
%A #include <asm/hardware.h>
%A
%A .macro disable_fiq
%A .endm
%A
%A .macro get_irqnr_and_base, irqnr, irqstat, base, tmp
%A mov r4, #INTBASE @ virtual address of IRQ registers
%A
%A ldr \irqnr, [r4, #0x8] @ read INTMSK
%A ldr \irqstat, [r4, #0x10] @ read INTPND
%A
%A bics \irqstat, \irqstat, \irqnr
%A bics \irqstat, \irqstat, \irqnr
%A beq 1002f
%A
%A mov \irqnr, #0
%A 1001: tst \irqstat, #1
%A bne 1002f @ found IRQ
%A add \irqnr, \irqnr, #1
%A mov \irqstat, \irqstat, lsr #1
%A cmp \irqnr, #32
%A bcc 1001b
%A 1002:
%A .endm
%A
%A .macro irq_prio_table
%A .endm
%A 分析这段代码可知,这个宏取得中断悬挂寄存器的值,将它转换为中断位对应的数字值即EINTn对应n并返回到第一个参数中,比如EINT0上发生中断,在宏的第一个参数中返回0,EINT1上发生中断,在宏的第一个参数中返回1。
%A 再返回头来看__irq_usr的代码,根据注释和分析可知,代码在r0中存储irq号,r1指向一个包含发生中断时各种寄存器值的结构pt_regs,然后专向执行do_IRQ,函数do_IRQ()的代码在arch/arm/kernel/irq.c中:
%A asmlinkage void do_IRQ(int irq, struct pt_regs * regs)
%A {
%A struct irqdesc * desc;
%A struct irqaction * action;
%A int cpu;
%A
%A irq = fixup_irq(irq);
%A
%A ……
%A ……
%A }
%A 注意这个函数中的irq = fixup_irq(irq);一句,他对刚才取得的irq值进行了修正,对于s3c2410,这个动作在arch\arm\mach-s3c2410\irq.c中:
%A unsigned int fixup_irq(int irq) {
%A unsigned int ret;
%A unsigned long sub_mask, ext_mask;
%A
%A if (irq == OS_TIMER)
%A return irq;
%A
%A switch (irq) {
%A case IRQ_UART0:
%A sub_mask = SUBSRCPND & ~INTSUBMSK;
%A ret = get_subIRQ(sub_mask, 0, 2, irq);
%A break;
%A case IRQ_UART1:
%A sub_mask = SUBSRCPND & ~INTSUBMSK;
%A ret = get_subIRQ(sub_mask, 3, 5, irq);
%A break;
%A case IRQ_UART2:
%A sub_mask = SUBSRCPND & ~INTSUBMSK;
%A ret = get_subIRQ(sub_mask, 6, 8, irq);
%A break;
%A case IRQ_ADCTC:
%A sub_mask = SUBSRCPND & ~INTSUBMSK;
%A ret = get_subIRQ(sub_mask, 9, 10, irq);
%A break;
%A case IRQ_EINT4_7:
%A ext_mask = EINTPEND & ~EINTMASK;
%A ret = get_extIRQ(ext_mask, 4, 7, irq);
%A break;
%A case IRQ_EINT8_23:
%A ext_mask = EINTPEND & ~EINTMASK;
%A ret = get_extIRQ(ext_mask, 8, 23, irq);
%A break;
%A default:
%A ret = irq;
%A }
%A
%A return ret;
%A }
%A 可见,对于EINT0,并不进行修正,而只对几个特殊中断号和多个中断脚复用一个中断号的情况进行修正。由此,我们可以得出S3C2410中,外部中断脚和中断号的对应关系是:
%A 中断脚位 中断号
%A INT_ADC [31]
%A INT_RTC [30]
%A INT_SPI1 [29]
%A INT_UART0 [28]
%A INT_IIC [27]
%A INT_USBH [26]
%A INT_USBD [25]
%A Reserved [24]
%A INT_UART1 [23]
%A INT_SPI0 [22]
%A INT_SDI [21]
%A INT_DMA3 [20]
%A INT_DMA2 [19]
%A INT_DMA1 [18]
%A INT_DMA0 [17]
%A INT_LCD [16]
%A INT_UART2 [15]
%A INT_TIMER4 [14]
%A INT_TIMER3 [13]
%A INT_TIMER2 [12]
%A INT_TIMER1 [11]
%A INT_TIMER0 [10]
%A INT_WDT [9]
%A INT_TICK [8]
%A nBATT_FLT [7]
%A Reserved [6]
%A EINT8_23 [5]
%A EINT4_7 [4]
%A EINT3 [3]
%A EINT2 [2]
%A EINT1 [1]
%A EINT0 [0]
%A
%A 那么,在前面的例子中,我们使用irq=0申请中断为什么也不行呢?我们来看一下系统是如何初始化中断数据的,对于s3c2410,系统在初始化中要调用下面的函数:
%A
%A void __init s3c2410_init_irq(void) {
%A int irq;
%A
%A request_resource(&iomem_resource, &irq_resource);
%A request_resource(&iomem_resource, &eint_resource);
%A
%A /* disable all IRQs */
%A INTMSK = 0xffffffff;
%A INTSUBMSK = 0x7ff;
%A EINTMASK = 0x00fffff0;
%A
%A /* all IRQs are IRQ, not FIQ
%A 0 : IRQ mode
%A 1 : FIQ mode
%A */
%A INTMOD = 0x00000000;
%A
%A /* clear Source/Interrupt Pending Register */
%A SRCPND = 0xffffffff;
%A INTPND = 0xffffffff;
%A SUBSRCPND = 0x7ff;
%A EINTPEND = 0x00fffff0;
%A
%A /* Define irq handler */
%A for (irq=0; irq < NORMAL_IRQ_OFFSET; irq++) {
%A irq_desc[irq].valid = 1;
%A irq_desc[irq].probe_ok = 1;
%A irq_desc[irq].mask_ack = s3c2410_mask_ack_irq;
%A irq_desc[irq].mask = s3c2410_mask_irq;
%A irq_desc[irq].unmask = s3c2410_unmask_irq;
%A }
%A
%A irq_desc[IRQ_RESERVED6].valid = 0;
%A irq_desc[IRQ_RESERVED24].valid = 0;
%A
%A irq_desc[IRQ_EINT4_7].valid = 0;
%A irq_desc[IRQ_EINT8_23].valid = 0;
%A
%A irq_desc[IRQ_EINT0].valid = 0;
%A irq_desc[IRQ_EINT1].valid = 0;
%A irq_desc[IRQ_EINT2].valid = 0;
%A irq_desc[IRQ_EINT3].valid = 0;
%A
%A for (irq=NORMAL_IRQ_OFFSET; irq < EXT_IRQ_OFFSET; irq++) {
%A irq_desc[irq].valid = 0;
%A irq_desc[irq].probe_ok = 1;
%A irq_desc[irq].mask_ack = EINT4_23mask_ack_irq;
%A irq_desc[irq].mask = EINT4_23mask_irq;
%A irq_desc[irq].unmask = EINT4_23unmask_irq;
%A }
%A
%A for (irq=EXT_IRQ_OFFSET; irq < SUB_IRQ_OFFSET; irq++) {
%A irq_desc[irq].valid = 1;
%A irq_desc[irq].probe_ok = 1;
%A irq_desc[irq].mask_ack = SUB_mask_ack_irq;
%A irq_desc[irq].mask = SUB_mask_irq;
%A irq_desc[irq].unmask = SUB_unmask_irq;
%A }
%A }
%A 可以看出,初始化中断描述符表时,对EINT0、EINT1、EINT2、EINT3、EINT4_7、EINT8_23等的.valid元素复位为0,表示这几个中断不可用。结合request_irq()源码可以看出,在注册时就会出现这几个中断不能成功注册的现象,本来这几个值是1的,我真不知道开发者为何这样做!不过还得感谢他们,是他们做的这个手脚促使我学得了这么多原来不知道的东东。
%A 现在将这促人学习的几句注释掉,重新编译这个LINUX,重新启动这个新的内核,重新运行你的测试程序,一切都重新来,你将会兴奋的说:OK!
%A [IMG][/IMG]