limingth 发表于 2005-6-13 14:35:34

Learn lumit Step 15 : 中断按钮实验

Learn lumit Step 15 : 中断按钮实验
++++++++++++++++++++++++++++++++++++++++++++++++++++++

    所有前面例子中提到的实验,无论是输入设备或者是输出设备,都是采用顺序执行的
方式,没有涉及到程序的异步执行。但即使是单片机也有中断,导致程序需要对中断进行
处理,因此就不得不涉及到 ARM 处理器的一些体系结构方面的知识。

    这一节我们以板上的 int0 中断按钮为例,介绍一下如何处理和实现系统设备的中断。
首先需要对 ARM 的中断处理流程做一个简单介绍。

    ARM 分为多个处理器状态,其中最常用的是 SVC 和 IRQ 状态,每种状态的区别主要
在于有一些寄存器是只有自己可见,而其他状态不可见的。SVC 可以理解成就是系统态,
我们前面所介绍的程序都是在 SVC 态执行的,而 IRQ 就是系统发生中断后,自动跳转到
IRQ 状态,同时 CPU 会自动到 0x18 地址处执行这里的处理指令。

    我们以 int0 例子中的代码为例,具体说明这个过程。相比以前的例子这里多了一个
startup.s 作为进入 main 之前需要首先执行的启动代码。它所要做的事情主要有如下的
这五个工作:

1) 建立 IRQ 态的堆栈指针 sp_irq
2) 建立 SVC 态的堆栈指针 sp_svc
3) 保存 __main 的入口到 lr 寄存器
4) CPU 进入 SVC 状态,同时开中断。
5) 跳转到 __main 入口,执行 C 语言代码

; startup.s
; **********************************************************************
; * Set up the stack pointer to point
; **********************************************************************

        ;set up irq stack
        mov         r0, #0xd2               ; make irq mode with all irqs disabled
        msr         cpsr_cxsf, r0
        MOV sp, #0x70000
       
        ;set up svc stack
            mov        r0, #0xd3                ; make svc mode with all irqs disabled
        msr        cpsr_cxsf, r0               
        MOV sp, #0x80000
       
; **********************************************************************
; * Get the address of the C entry point.
; **********************************************************************

        LDR lr, =__main
       
; **********************************************************************
; * Enable the interrupt while staying in the supervisor mode
; **********************************************************************
       
        MOV        r0, #Mode_Svc:OR:F_Bit
        MSR        cpsr_c, r0

        MOV         pc, lr

    对比上面的代码,这几个工作为系统支持 IRQ 中断状态做了必要的准备,其中最重要的
就是为 IRQ 状态下的 SP 堆栈寄存器做了赋值,建立了 IRQ 中断态的堆栈空间。

    进入 main 函数后,系统主要执行了如下代码:

int main( void )
{       
        int i = 1;
       
        install_irq_handler( irq_handler );

        led_init();
       
        int0_install_irq_hooker( int0_hooker );
       
        // should be after hooker installation
        int0_init();
               
        while(i++)
        {
                led_one_light(i%3);
                led_delay( 100 );
                led_one_dark(i%3);
        }
       
        return 0;
}

    可以看到,程序结构还是很清晰的,从函数命名上基本就能理解大致的执行流程。
1) 为系统注册一个中断处理函数 irq_handler ,是在汇编文件 startup.s 中实现的。
2) 为 int0 设备的中断处理安装一个用户来实现的钩子函数 int0_hooker 。
3) 初始化 led 设备,便于程序执行过程中有一个简单的输出结果显示。
4) 初始化 int0 设备,开始允许 int0 中断。
5) 进入主程序流程中,在一个无限循环中,依次点亮 led 0, led 1, led 2 三个灯。
   
    在 main.c 主文件中,irq 中断的管理和注册是通过一个中断向量数组来实现的。
这个函数指针的数组,代表了 S3C4510 CPU 的 21 个中断源的中断处理函数的入口。
系统在中断发生后,首先进入到 irq_handler 中,对 cpu 寄存器上下文状态做必要的
保存(堆栈操作)后,调用 C 语言级的 do_irq 函数。在这个函数中,根据当前中断源
的中断号 irq_source,来查询系统维护的中断向量数组,看是否已经有了某个设备注册
了该中断号的中断处理函数。如果有则跳转过去执行该中断处理函数。

-----------------------------------------------------------------------------------
; startup.s
        IMPORTdo_irq
        EXPORTirq_handler
irq_handler
        SUB        lr, lr, #4            
        STMFD        sp!, {r0-r12, lr}      ; push r0-r12 register file and lr( pc return address )

        MRS         r4, spsr      
        STMFD         sp!, {r4}                ; push current spsr_cxsf_irq ( =cpsr_svc )

        BL        do_irq                       ; goto C handler
       
        LDMFD         sp!, {r4}                ; get cpsr_svc from stack
        MSR   spsr_cxsf, r4               ; prepare spsr_cxsf to return svc mode       

        LDMFD        sp!, {r0-r12, pc}^       ; recover r0-r12 and pc from stack, cpsr also


-----------------------------------------------------------------------------------
/* main.c */
void (*device_irq_handler)(int irq);

void do_irq( void )
{
        void (* current_pc)();
        int irq_source;
        int i;
       
        // get irq number from INTPND       
        irq_source = INTPND;
       
        // get current device irq handler to current_pc
        for( i = 0; i < IRQ_SOURCE_NUM; i++ )
        {
                if( irq_source & ( 1 << i ) )
                {        // here is an interrupt at source i       
                       
                        if( device_irq_handler )
                        {        // if this interrupt has an registered handler
                                // then get this handler address to current_pc                       
                                current_pc = device_irq_handler;
                                // call registered device irq handler to do_irq
                                ((void (*)(void))(current_pc))(); /* thanks, STheobald */       
                        }
                }       
        }
       
        return;       
}

    关于中断处理函数的注册和释放,主要都是参考了 linux 的实现机制,尤其是对于设备驱动
的处理,也是做了一些简化,但基本思路上是类似的。在 int0_driver 这个文件里面,照例还是
实现了 open, read, write, ioctl, release 这 5 个底层接口。而中断处理函数的实现,中断号
的申请和释放,以及用户安装 hooker 函数的实现都放在了上层 int0_api 这个文件里面,这有点
类似于 linux 里面的内核模块。

/* main.c */
int request_irq( unsigned int irq, void (*handler)(int irq) )
{
        if( device_irq_handler )
                return -1;        // fail to request, free this irq first
               
        // fill the device_irq_handler vector
        device_irq_handler = handler;
       
        return 0;       
}
   
int free_irq( unsigned int irq, void (*handler)(int irq) )
{
        if( !device_irq_handler )
                return -1;        // fail to request, free this irq first
               
        // free the device_irq_handler vector
        device_irq_handler = 0;
       
        return 0;       
}

    在 int0_api 这一层,主要是实现 int0_irq_handler 的中断处理程序,在该程序中,
必要的话,就调用用户的 hooker 函数。同时为用户安装 hooker 函数提供接口。
   
/* int0_api.c */
extern int request_irq( unsigned int irq, void (*handler)(void) );

static void (*int0_irq_hooker)(void) = 0;

void int0_irq_handler( void )
{
        // here we add some user code for int0_irq
        if( int0_irq_hooker )
                int0_irq_hooker();

        // here we call low-level int0_irq_handler
        int0_ioctl( INT0_CLEAR_INTERRUPT, 0 );
       
        return;
}

void int0_install_irq_hooker( void (*handler)(void) )
{
        int0_irq_hooker = handler;
}
   

   在 int0_init 初始化流程中,主要是完成申请注册一个中断号以及和它对应的
中断处理函数。同时设置启动中断的一些参数,例如中断触发方式为上升沿触发等。
   
/* set int0 related gpio */
int int0_init( void )
{
        // External interrupt 0 source number is 0
        request_irq( 0, int0_irq_handler );

        int0_open();

        // set int0 interrupt edge detect
        int0_ioctl( INT0_RISING_EDGE_INTERRUPT, 0 );
       
        // enable int0 interrupt
        int0_ioctl( INT0_ENABLE_INTERRUPT, 0 );

        // set active high
        int0_ioctl( INT0_ACTIVE_HIGH, 0 );
       
        return 0;
}
   
    在 int0_driver 底层驱动接口中,最重要的是实现 int0_ioctl 控制接口。这个 ioctl
在前面的几个例子里,都没有很实际的应用,但对于 int0 这个设备,就体现出其很重要的
作用,可以对比以前的例子,这些设置参数是无法通过设备 读/写 的接口来实现的。

int int0_ioctl( unsigned int cmd, unsigned long arg )
{
        switch( cmd )
        {
                // Enable interrupt request and Unmask PIO8 interrupt
                case        INT0_ENABLE_INTERRUPT:               
                        IOPCON         |= INT0_IO_ENABLE;
                        break;

                // clear int 0 pending interrupts
                case        INT0_CLEAR_INTERRUPT:
                        INTPND |= INT0_MASK;
                        break;
                                       
                // set rising edge interrupt
                case        INT0_RISING_EDGE_INTERRUPT:               
                          IOPCON        |= INT0_IO_RISING_EDGE;               
                        break;
               
                // set falling edge interrupt
                case        INT0_FALLING_EDGE_INTERRUPT:       
                          IOPCON        |= INT0_IO_FALLING_EDGE;
                        break;
               
                // set both edge interrupt
                case        INT0_BOTH_EDGE_INTERRUPT:               
                          IOPCON        |= INT0_IO_BOTH_EDGE;
                        break;
                                         
                // set as active high
                case        INT0_ACTIVE_HIGH:
                        IOPCON        |= INT0_IO_ACTIVE_HIGH;
                        break;
                       
                default:
                        break;
        }       
       
        return 0;
}

    在学习 linux 设备驱动程序的过程中,我很想做的一件事情,就是基于 linux 的机制,
实现一个简化的中断处理和设备驱动模型,应用在一些相对简单的领域。在 lumit4510 平台
上做的这个例子,多少也体现了这样一个思路。

bouncer_chen 发表于 2005-6-13 15:30:20

这段例程非常好,感谢。

limingth 发表于 2005-6-13 18:21:00

:lol:

wenbbo 发表于 2005-6-15 11:10:32

实验思想非常好,但可否把源码下载的链接做上,以方便下载源码。

limingth 发表于 2005-6-15 11:12:59

:?:   上面第一个帖子的后面跟着的 Learn-lumit-Step-15.中断按钮实验.zip链接不就是么?
每一个帖子后面都带有例子的全部源码和 readme ,看仔细哦    :twisted:

millionwood 发表于 2005-6-28 10:02:30

大侠请指导一下编译过程好吗?我直接make不行啊,不懂怎么编译,puke :oops:

limingth 发表于 2005-6-28 10:05:29

别着急,先试试下载 Step 1 里面的例子看看 make 行不行?

viking00000 发表于 2006-4-5 19:56:39

* this is isr entry address, could be another address like 0x3c, 0x58... */
        unsigned int * isr_entry_addr = ( unsigned int * ) 0x38;

0x38的地址是怎么来的啊,could be another address ?

* make an instruction: it is machine-code for "ldrpc, "*/
        instruction = ((unsigned int) isr_entry_addr- (unsigned int)irq_vec_addr - 0x08) | 0xe59ff000;

0xe59ff000的地址是怎么得出的啊?

limingth 发表于 2006-4-6 18:03:26

0xe59ff000 不是一个地址,而是一条机器指令的某些二进位,具体这些位是什么含义,可以查看arm汇编指令相关资料。 :P
页: [1]
查看完整版本: Learn lumit Step 15 : 中断按钮实验