作者:彭东林
QQ: 405728433
以前一直有个疑问,在U-boot下到底能不能使用中断,为了验证这个问题,于是乎,昨天晚上我在自己的 TQ2440开发板上进行了uboot环境下的按键中断实验,这次使用的我刚移植的最新版Uboot,版本是 u-boot-2014-04,验证的结论是:
U-boot完全能够支持中断
下面就以u-boot-2014-04为例,介绍一下按键中断的实现。
这里分为几部分介绍:
1、异常向量表 ------ 由u-boot完成
2、通用中断处理函数 ------ 由u-boot完成
3、u-boot自己完成的中断初始化部分 ----- 由u-boot完成
4、用户按键中断中断初始化 ------ 由用户完成
5、用户自定义中断处理函数 ------ 由用户完成
这里有两篇我在网络上搜集的关于S3C2440中断的文章:
https://files.cnblogs.com/pengdonglin137/S3C2440%E7%B3%BB%E7%BB%9F%E4%B8%AD%E6%96%AD.rar
其中介绍了如何使用S3C2440的中断功能以及ARM处理器异常处理。
先简单介绍一下几个知识点:
- ARM状态下的寄存器组织
在系统上电时,也就是RESET后,处于SVC特权模式
- ARM状态寄存器
关于状态寄存器的介绍可以参考:
http://www.cnblogs.com/pengdonglin137/p/3819546.html
Control Bits的含义:
Mode bits的含义:
- 异常向量表
异常向量表是一段特定内存地址空间,每种ARM异常对应一个字长空间(4Bytes),正好是一条32位指令长度,当异常发生时,CPU强制将PC的值设置为当前异常对应的固定内存地址。如表3-4所示是S3C2440的异常向量表。
我们一般都是用的是IRQ异常。下面的按键产生IRQ异常。
- 异常发生的硬件操作
在异常发生后,ARM内核会自动做以下工作:
保存执行状态:将CPSR复制到发生的异常模式下SPSR中;
(以按键中断为例,uboot环境下处于SVC模式,中断后,处于irq模式,所以这步完成的动作是:CPSR ----> SPSR_irq)
模式切换:将CPSR模式位强制设置为与异常类型相对应的值,同时处理器进入到ARM执行模式,禁止所有IRQ中断,当进入FIQ快速中断模式时禁止FIQ中断;
(以按键中断为例,将CPSR的mode bits设置为0x12,将I位置为1,屏蔽IRQ中断,将T位置为1,进入ARM状态)
保存返回地址:将下一条指令的地址(被打断程序)保存在LR(异常模式下LR_excep)中。
(一条指令的执行分为:取指,译码,执行三个主要阶段, CPU由于使用流水线技术,造成当前执行指令的地址应该是PC – 8(32位机一条指令四个字节),那么执行指令的下条指令应该是PC – 4。在异常发生时,CPU自动会将将PC – 4 的值保存到LR里,但是该值是否正确还要看异常类型才能决定。
快速中断请求和一般中断请求返回处理是一样的。通常处理器执行完当前指令后,查询FIQ/IRQ中断引脚,并查看是否允许FIQ/IRQ中断,如果 某个中断引脚有效,并且系统允许该中断产生,处理器将产生FIQ/IRQ异常中断,当FIQ/IRQ异常中断产生时,程序计数器pc的值已经更新,它指向 当前指令后面第3条指令(对于ARM指令,它指向当前指令地址加12字节的位置;对于Thumb指令,它指向当前指令地址加6字节的位置),当 FIQ/IRQ异常中断产生时,处理器将值(pc-4)保存到FIQ/IRQ异常模式下的寄存器lr_irq/lr_irq中,它指向当前指令之后的第2 条指令,因此正确返回地址可以通过下面指令算出:
SUBS PC,LR_irq,#4 ; 一般中断
SUBS PC,LR_fiq,#4 ; 快速中断
注:LR_irq/LR_fiq分别为一般中断和快速中断异常模式下LR,并不存在LR_xxx寄存器,为方便读者理解加上_xxx)
跳入异常向量表:强制设置PC的值为相应异常向量地址,跳转到异常处理程序中。
(以按键中断为例,将PC强制设置为0x18)
- 保存执行现场
异常处理程序最开始,要保存被打断程序的执行现场,程序的执行现场无非就是保存当前操作寄存器里的数据,可以通过下面的栈操作指令实现保存现场:
STMFD SP_excep!, {R0 – R12, LR_excep}
注:LR_abt,SP_excep分别为对应异常模式下LR和SP,为方便读者理解加上_abt
需要注意的是,在跳转到异常处理程序入口时,已经切换到对应异常模式下了,因此这里的SP是异常模式下的SP_excep了,所以被打断程序现场 (寄存器数据)是保存在异常模式下的栈里,上述指令将R0~R12全部都保存到了异常模式栈,最后将修改完的被打断程序返回地址入栈保存,之所以保存该返 回地址就是将来可以通过类似:MOV PC, LR的指令,返回用户程序继续执行。
异常发生后,要针对异常类型进行处理,因此,每种异常都有自己的异常处理程序,异常处理过程通过下节的系统中断处理来进行分析。
- 异常处理的返回
异常处理完成之后,返回被打断程序继续执行,具体操作如下:
恢复被打断程序运行时寄存器数据
恢复程序运行时状态CPSR
通过进入异常时保存的返回地址,返回到被打断程序继续执行
异常发生后,进入异常处理程序时,将用户程序寄存器R0~R12里的数据保存在了异常模式下栈里面,异常处理完返回时,要将栈里保存的的数据再恢复 回原先R0~R12里,毫无疑问在异常处理过程中必须要保证异常处理入口和出口时栈指针SP_excep要一样,否则恢复到R0~R12里的数据不正确, 返回被打断程序时执行现场不一致,出现问题,虽然将执行现场恢复了,但是此时还是在异常模式下,CPSR里的状态是异常模式下状态,因此要恢复 SPSR_excep里的保存状态到CPSR里,SPSR_excep是被打断程序执行时的状态,在恢复SPSR_excep到CPSR的同时,CPU的 模式和状态从异常模式切换回了被打断程序执行时的模式和状态。此刻程序现场恢复了,状态也恢复了,但PC里的值仍然指向异常模式下的地址空间,我们要让 CPU继续执行被打断程序,因此要再手动改变PC的值为进入异常时的返回地址,该地址在异常处理入口时已经计算好,直接将PC = LR_excep即可。
上述操作可以一步一步实现,但是通常我们可以通过一条指令实现上述全部操作:
LDMFD SP_excp!, {r0-r12, pc}^
注:SP_excep为对应异常模式下SP,^符号表示恢复SPSR_excep到CPSR
以上操作可以用下图来描述
接下来分析u-boot代码。
让u-boot支持中断,首先需要在配置文件中定义几个宏,我在我的板子的配置文件include/configs/smdk2440.h中定义了如下几个宏(少定义了在编译时会报错,可以根据出错信息判断少定义了那些宏):
#define CONFIG_USE_IRQ
#define CONFIG_STACKSIZE_IRQ (4*1024) /* IRQ的栈大小*/
#define CONFIG_STACKSIZE_FIQ (4*1024) /* FIQ的栈大小*/
异常向量表
首先分析一下arch/arm/cpu/arm920t/start.S
1: .globl _start 指令链接地址 指令的运行地址
2: _start: b start_code 0x33f00000 0x00000000
3: ldr pc, _undefined_instruction
0x33f00004 0x00000004
4: ldr pc, _software_interrupt
0x33f00008 0x00000008
5: ldr pc, _prefetch_abort
0x33f0000c 0x0000000c
6: ldr pc, _data_abort
0x33f00010 0x00000010
7: ldr pc, _not_used
0x33f00014 0x00000014
8: ldr pc, _irq
0x33f00018 0x00000018
9: ldr pc, _fiq
0x33f0001c 0x0000001c
10:
11: _undefined_instruction: .word undefined_instruction
0x33f00020 0x00000020
12: _software_interrupt: .word software_interrupt
0x33f00024 0x00000024
13: _prefetch_abort: .word prefetch_abort
0x33f00028 0x00000028
14: _data_abort: .word data_abort
0x33f0002c 0x0000002c
15: _not_used: .word not_used
0x33f00030 0x00000030
16: _irq: .word irq
0x33f00034 0x00000034
17: _fiq: .word fiq
0x33f00038 0x00000038
18:
19: .balignl 16,0xdeadbeef
上面就是建立异常向量表,其中b start_code指令的地址对应的就是复位异常发生时要赋给PC的值,b 是一条相对跳转指令。其中,我们要关注的是IRQ异常: ldr pc, _irq ,这条语句的作用是将_irq中存放的数据放入pc中,可以将_irq看做变量名或者一个*p,而其中存放的是内容就是irq,即中断处理的入口地址(链接地址)。
即当发生按键动作是,pc会指向“ldr pc, _irq”所在的地址,执行这条指令(会被解释成ldr pc, [pc, #offset]),这条指令完成了将irq的地址(链接地址)赋给了pc,从而从异常向量表中直接跳入了中断处理程序(链接时确定的地址处)。
这里需要解释一下,指令的运行地址和链接地址。链接地址是在编译连接时编译器确定的地址,运行地址是实际运行这条指令时,去哪个物理地址去取这条指令,这两个地址一般相同。如果设备支持程序在Flash中运行,那么这两个地址相同,但是对于从NandFlash启动时,他们就不同了,以S3C2440为例,系统会先把NandFlash的前4KB的内容读到SRAM(sram会被映射到物理地址0开始的地方),然后运行这4KB的程序,这段4KB的程序负责把整个程序从NandFlash读到他们的链接地址处(一般在物理内存的末端,S3C2440的物理内存起始地址是0x30000000)。那么对于刚才运行在SRAM中的那4KB程序来说,他们的运行地址(sram中,起始地址0)跟链接地址(内存中,起始地址0x30000000)就不相同了。ARM架构下的异常向量表默认应该存放在0地址处,即要想使用异常,物理地址0处应该存放正确完整的异常向量表。对于从NorFlash启动,自然不是问题,此时NorFlash会被映射到物理地址0开始的地方,NorFlash的中存放的uboot开头便是异常向量表。对于从NandFlash启动时,SRAM被映射到了物理地址0开始的地方,并且前面已经说过,SRAM中的代码来自NandFlash的前4KB,这4KB也就是uboot的前4KB,自然含有异常向量表,也不会出问题,如果你故意在u-boot中通过使用命令mw破坏SRAM中的异常向量表,当发生异常时,u-boot就跑飞了。这里还要提一下被重定向到内存中的u-boot,其中也含有异常向量表,但是异常产生时系统用不到。
通用中断处理函数
通用中断处理函数在u-boot中的实现,还是在start.S中(我做了修改):
1: .align 5
2: irq:
3: sub lr, lr, #4 @ the return address
4: ldr sp, IRQ_STACK_START @ the stack for irq
5: stmdb sp!, { r0-r12,lr } @ save registers
6:
7: ldr lr, =int_return @ set the return addr
8: ldr pc, =do_irq @ call the isr
9: int_return:
10: ldmia sp!, { r0-r12,pc }^ @ return from interrupt
11:
解释:
“sub lr, lr, #4”的原因在上面已经解释过了。
“ldr sp, IRQ_STACK_START”
这条指令中的sp已经是irq模式下的sp,即r13_irq,意思是将IRQ_STACK_START中存放的数据放入sp,即初始化irq模式下的栈指针。IRQ_STACK_START在什么地方赋值呢?一会儿分析。
“stmdb sp!, { r0-r12,lr }”
这条指令负责保存现场,r0~r12是svc模式和irq模式共用的寄存器,同时由于下面在调用用C实现的do_irq时会用到,所以这里要保存。由于lr会被赋予新的值,这里也要保存。
“ldr lr, =int_return”
将int_return的链接地址放入lr中,因为在用C实现的do_irq执行结束是会执行 ldr pc, lr 的操作,正好执行到下面将要说的指令。
“ldmia sp!, { r0-r12,pc }^”
恢复现场,其中 ^ 表示将SPSR_irq赋值给CPSR。
u-boot自己完成的中断初始化部分
这里u-boot替用户完成的部分有:
1、中断栈的分配
2、设置CPSR的相关位,是I位清零,即使IRQ有效
中断栈的分配
IRQ_STACK_START的定义:
在arch/arm/cpu/arm920t/start.S中:
可以看到,IRQ_STACK_START的初始值给的是0x0badc0de,将来重定向到内存中后,会修改这个值。这里还要明确的是将来会在内存中u-boot的链接地址附近和4G空间的开始4KB内各有一个IRQ_STACK_START,重定向后,u-boot看到的是内存中u-boot链接地址起始地址附近的那个IRQ_STACK_START。也就是说,重定向后,4G空间前4KB处仅仅完成了发生异常后,依赖异常向量表跳转到异常处理程序的链接起始地址(在内存中)处。
IRQ_STACK_START的赋值:
对于u-boot-2014-04是在arch/arm/lib/board.c中的board_init_f函数中完成的:
1: unsigned int board_init_f(ulong bootflag)
2: {
3: bd_t *bd;
4: init_fnc_t **init_fnc_ptr;
5: gd_t *id;
6: ulong addr, addr_sp;
7: void *new_fdt = NULL;
8: size_t fdt_size = 0;
9:
10: memset((void *)gd, 0, sizeof(gd_t));
11:
12: gd->mon_len = (ulong)&__bss_end - (ulong)_start;
13: /* Allow the early environment to override the fdt address */
14: gd->fdt_blob = (void *)getenv_ulong("fdtcontroladdr", 16,
15: (uintptr_t)gd->fdt_blob);
16:
17: for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
18: if ((*init_fnc_ptr)() != 0) {
19: hang ();
20: }
21: }
22:
23: debug("monitor len: %08lX\n", gd->mon_len);
24: /*
25: * Ram is setup, size stored in gd !!
26: */
27: debug("ramsize: %08lX\n", gd->ram_size);
28:
29: addr = CONFIG_SYS_SDRAM_BASE + get_effective_memsize(); // addr = 0x30000000 + 0x4000000
30:
31: /* round down to next 4 kB limit */
32: addr &= ~(4096 - 1);
33: debug("Top of RAM usable for U-Boot at: %08lx\n", addr);
34:
35: /*
36: * reserve memory for U-Boot code, data & bss
37: * round down to next 4 kB limit
38: */
39:
40: addr = CONFIG_SYS_TEXT_BASE; // addr = 0x33f00000
41: addr &= ~(4096 - 1);
42:
43: debug("Reserving %ldk for U-Boot at: %08lx\n", gd->mon_len >> 10, addr);
44:
45: /*
46: * reserve memory for malloc() arena
47: */
48: addr_sp = addr - TOTAL_MALLOC_LEN;
49: debug("Reserving %dk for malloc() at: %08lx\n",
50: TOTAL_MALLOC_LEN >> 10, addr_sp);
51: /*
52: * (permanently) allocate a Board Info struct
53: * and a permanent copy of the "global" data
54: */
55: addr_sp -= sizeof (bd_t);
56: bd = (bd_t *) addr_sp;
57: gd->bd = bd;
58: debug("Reserving %zu Bytes for Board Info at: %08lx\n",
59: sizeof (bd_t), addr_sp);
60:
61:
addr_sp -= sizeof (gd_t);
62: id = (gd_t *) addr_sp;
63: debug("Reserving %zu Bytes for Global Data at: %08lx\n",
64: sizeof (gd_t), addr_sp);
65:
66: /* setup stackpointer for exeptions */
67: gd->irq_sp = addr_sp;
68: #ifdef CONFIG_USE_IRQ
69:
addr_sp -= (CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ); // 在smdk2440.h中这俩个宏都定义为了4K
70: debug("Reserving %zu Bytes for IRQ stack at: %08lx\n",
71: CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ, addr_sp);
72: #endif
73: /* leave 3 words for abort-stack */
74: addr_sp -= 12;
75:
76: /* 8-byte alignment for ABI compliance */
77: addr_sp &= ~0x07;
78: debug("New Stack Pointer is: %08lx\n", addr_sp);
79:
80: gd->bd->bi_baudrate = gd->baudrate;
81: /* Ram ist board specific, so move it to board code ... */
82: dram_init_banksize();
83: display_dram_config(); /* and display it */
84:
85: gd->relocaddr = addr;
86: gd->start_addr_sp = addr_sp;
87: gd->reloc_off = addr - (ulong)&_start;
88: debug("relocation Offset is: %08lx\n", gd->reloc_off);
89: if (new_fdt) {
90: memcpy(new_fdt, gd->fdt_blob, fdt_size);
91: gd->fdt_blob = new_fdt;
92: }
93: memcpy(id, (void *)gd, sizeof(gd_t));
94:
95: return (unsigned int)id;
96: }
从上面的代码可以看出u-boot的内存分布图大致如下:
从图中可以看到中断栈的位置。ARM使用的栈是向下增长的,中断栈的栈底地址存放在gd的irq_sp中。
上面只是分配了,还没有赋值给IRQ_STACK_START,它的赋值在arch/arm/lib/board.c中的board_init_r函数中:
void board_init_r(gd_t *id, ulong dest_addr)
{
......
/* set up exceptions */
interrupt_init();
/* enable exceptions */
enable_interrupts();
.......
for (;;) {
main_loop();
}
}
其中,在interrupt_init中给IRQ_STACK_START赋值,在enable_interrupts中设置CPSR相关位。这两个函数在
interrupts.c (arch\arm\lib) 中实现。
#ifdef CONFIG_USE_IRQ
int interrupt_init (void)
{
/*
* setup up stacks if necessary
*/
IRQ_STACK_START = gd->irq_sp - 4;
IRQ_STACK_START_IN = gd->irq_sp + 8;
FIQ_STACK_START = IRQ_STACK_START - CONFIG_STACKSIZE_IRQ;
//return arch_interrupt_init();
return 0;
}
/* enable IRQ interrupts */
void enable_interrupts (void)
{
unsigned long temp;
__asm__ __volatile__("mrs %0, cpsr\n"
"bic %0, %0, #0x80\n" // 清除I位,使能IRQ
"msr cpsr_c, %0"
: "=r" (temp)
:
: "memory");
}
/*
* disable IRQ/FIQ interrupts
* returns true if interrupts had been enabled before we disabled them
*/
int disable_interrupts (void)
{
unsigned long old,temp;
__asm__ __volatile__("mrs %0, cpsr\n"
"orr %1, %0, #0xc0\n" // 将I和F置位,屏蔽FIQ和RIQ
"msr cpsr_c, %1"
: "=r" (old), "=r" (temp)
:
: "memory");
return (old & 0x80) == 0;
}
#else
......
#endif
用户按键中断中断初始化
这部分由用户自己完成,我们要实现的是按键中断,然后再在中断处理函数中点亮某个LED灯,关闭其他的LED灯。这部分应该放在u-boot已经完成了系统的初始化工作,这里我把它放在了执行main_loop之前。
void board_init_r(gd_t *id, ulong dest_addr)
{
......
{
#define GPBCON (*(volatile unsigned long *)0x56000010)
#define GPFCON (*(volatile unsigned long *)0x56000050)
#define EINTMASK (*(volatile unsigned long *)0x560000a4)
#define EXTINT0 (*(volatile unsigned long *)0x56000088)
#define INTMSK (*(volatile unsigned long *)0x4A000008)
/*
* LED1,LED2,LED3,LED4 分别对应 GPB5、 GPB6、 GPB7、 GPB8
*/
#define GPB5_out (1<<(5*2))
#define GPB6_out (1<<(6*2))
#define GPB7_out (1<<(7*2))
#define GPB8_out (1<<(8*2))
#define GPB5_msk (3<<(5*2))
#define GPB6_msk (3<<(6*2))
#define GPB7_msk (3<<(7*2))
#define GPB8_msk (3<<(8*2))
/*
* 按键S1,S2,S3,S4 分别对应GPF0、GPF2、GPF0、GPF4
*/
#define GPF0_eint (0x2<<(0*2))
#define GPF1_eint (0x2<<(1*2))
#define GPF2_eint (0x2<<(2*2))
#define GPF4_eint (0x2<<(4*2))
#define GPF0_msk (3<<(0*2))
#define GPF1_msk (3<<(1*2))
#define GPF2_msk (3<<(2*2))
#define GPF4_msk (3<<(4*2))
//将控制LED的引脚设置为输出
GPBCON &= ~(GPB5_msk | GPB6_msk | GPB7_msk | GPB8_msk);
GPBCON |= GPB5_out | GPB6_out | GPB7_out | GPB8_out;
// 将按键部分的引脚设置为外部中断模式
GPFCON &= ~(GPF0_msk | GPF2_msk | GPF1_msk | GPF4_msk);
GPFCON |= GPF0_eint | GPF2_eint | GPF1_eint | GPF4_eint;
// 外部中断4到7共用一个中断EINT4_7,将外部中断4对应的屏蔽位清除
EINTMASK &= ~(1<<4);
// EINT0、EINT2 EINT1、EINT4_7 清除屏蔽位 0 2 1 4
INTMSK &= (~(1<<0)) & (~(1<<2)) & (~(1<<1) & (~(1<<4)));
//设置为下降沿触发
EXTINT0 &= ~0xffff;
EXTINT0 |= ((2<<0) | (2<<4) | (2<<8) | (2<<16));
}
/* main_loop() can return to retry autoboot, if so just run it again. */
for (;;) {
main_loop();
}
}
用户自定义中断处理函数
这部分是用户自己实现的。完成当按键中断发生后,用户期望完成的功能。我们所要的功能是:点亮某个LED灯,关闭其他的LED灯。
这部分的实现我放在了interrupts.c (arch\arm\cpu\arm920t\s3c24x0)中。
void do_irq (struct pt_regs *pt_regs)
{
#define INTOFFSET (*(volatile unsigned long *)0x4A000014)
#define GPBDAT (*(volatile unsigned long *)0x56000014)
#define EINTPEND (*(volatile unsigned long *)0x560000a8)
#define SRCPND (*(volatile unsigned long *)0x4A000000)
#define INTPND (*(volatile unsigned long *)0x4A000010)
unsigned long oft = INTOFFSET;
switch( oft )
{
// 按键0
case 0:
{
GPBDAT |= (0xf<<5); // 熄灭所有LED
GPBDAT &= ~(1<<5); // LED1亮
printf("EINT0\n");
break;
}
// 按键1
case 1:
{
GPBDAT |= (0xff<<5); // 熄灭所有LED
GPBDAT &= ~(1<<7); // LED2亮
printf("EINT1\n");
break;
}
// 按键2
case 2:
{
GPBDAT |= (0xff<<5); // 熄灭所有LED
GPBDAT &= ~(1<<6); // LED3亮
printf("EINT2\n");
break;
}
//按键3
case 4:
{
GPBDAT |= (0xff<<5); // 熄灭所有LED
GPBDAT &= ~(1<<8); // LED4亮
printf("EINT4\n");
break;
}
default:
break;
}
//清中断
if( oft == 4 )
EINTPEND = (1<<4); // EINT4_7合用IRQ4
SRCPND = 1<<oft; // 对应位写1
INTPND = 1<<oft; // 对应位写1
}
至此,u-boot下就实现了按键中断。无论从NorFlash还是NandFlash启动都可以,大家还可以验证一下,当从NandFlash启动后,手动将SRAM(物理起始地址从0开始的4KB空间)全部清零,然后再按键,看看现象,此时u-boot肯定跑飞了。
完!!