作者:彭東林
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中斷的文章:
http://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肯定跑飛了。
完!!