前面的按鍵實驗是通過死循環一直讀取按鈕電平值來判斷是否有按下按鈕,接下來將使用另外一個更優雅的方式實驗按鍵按下功能-中斷。
CPU在正常處理指令的時候會遇到外設打斷當前執行邏輯,我們稱為異常中斷。一系列中斷處理集中在一起管理,我們稱為異常中斷向量表。
中斷向量表
Coretex-A系列的中斷向量表就是存放在程序起始位置(鏈接起始地址)的一組由4字節組成的一組數據,Coretex-A 32位處理器每一條指令長度就是4個字節,所以本質上這個中斷向量表是一組固定地址的指令。Coretext-A系統CPU總共支持8個中斷:
這8個中斷里面需要特別注意也是需要開發的主要是復位中斷
與IRQ中斷
。復位中斷
在上電或者按下Reset按鈕后硬件加載程序同時PC
寄存器位置重置為0x0或者鏈接起始地址時觸發,IRQ中斷
則是外設觸發。每一個中斷發生時PC
寄存器會被設置成一個固定的地址,而這個地址則對應中斷向量表中一條指令。
中斷向量表添加到匯編最開始的位置:
/* 從鏈接起始地址開始,8條4字節的指令組成了ARM的中斷向量表 */
/* 中斷向量表放在最開始的位置,每一條指令對應了具體的中斷處理 */
/* 當發生對應中斷時,硬件會把對應的地址設置到pc寄存器,從而執行對應的中斷服務函數 */
ldr pc, =Reset_Handler /* 0x00: 復位中斷 */
ldr pc, =Undefine_Instruction_Handler /* 0x04: 未定義中斷指令 */
ldr pc, =Software_Interrupt_Handler /* 0x08: 軟中斷, SVC特權模式 */
ldr pc, =Prefetch_Abort_Handler /* 0x0c: 指令預取中止中斷 */
ldr pc, =Data_Abort_Handler /* 0x10: 數據訪問中止中斷 */
ldr pc, =Not_Used_Handler /* 0x14: 未使用的中斷 */
ldr pc, =IRQ_Handler /* 0x18: 外部設備中斷 */
ldr pc, =FIQ_Handler /* 0x1c: 快速中斷 */
復位中斷服務函數
上電后第一個要觸發的則是復位中斷
,通過向量表中定義的指令可以將程序切換到Reset_Handler
處開始執行
- 關閉IRQ
- 關閉I,D Cache,以及MMU
- 設置中斷的起始地址,即設置成鏈接起始地址(因為程序是從鏈接起始地址開始運行的)
- 設置IRQ,SVC以及SYS模式下C語言的運行環境(C語言的SP指針棧頂)
- 打開IRQ
- 調轉到C語言的main函數開始運行
cpsid i /* 關閉IRQ, 此時IRQ還沒有配置完成,所以關閉*/
/*
在設備上電啟動時,執行的代碼訪問的外設都是實際地址,
mmu與cache此時的意義不大,
這個時候為了防止cache與mmu可能導致的問題會先將mmu與cache關閉
*/
/* CP15: SCTLR */
/* 關閉 I-Cache, D-Cache, MMU */
MRC p15, 0, r0, c1, c0, 0 /* 將SCTLR寄存器讀取到r0寄存器 */
bic r0, r0, #(1 << 0) /* 關閉MMU */
bic r0, r0, #(1 << 1) /* 關閉對齊 */
bic r0, r0, #(1 << 11) /* 關閉分支預測 */
bic r0, r0, #(1 << 12) /* 關閉i-cache */
bic r0, r0, #(1 << 2) /* 關閉d-cache */
MCR p15, 0, r0, c1, c0, 0 /* 將r0寄存器數據寫入到SCTLR寄存器 */
/* 設置中斷向量偏移,在發生中斷之前設置即可,也可以在C語言中設置 */
ldr r0, =0x87800000 /* 將0x87800000這個立即數寫入到 r0寄存器, 也就是鏈接起始地址*/
dsb /* 這里涉及到了改變讀取內存的地址起始地址,需要使用內存屏障指令進隔斷,保證前后讀取指令都是正常的地址 */
isb /* 這里涉及到了改變讀取內存的地址起始地址,需要使用內存屏障指令進隔斷,保證前后讀取指令都是正常的地址 */
MCR p15, 0, r0, c12, c0, 0 /* 將r0的數據寫入到VBAR寄存器中,表示向量偏移地址是0x87800000 */
dsb /* 這里涉及到了改變讀取內存的地址起始地址,需要使用內存屏障指令進隔斷,保證前后讀取指令都是正常的地址 */
isb /* 這里涉及到了改變讀取內存的地址起始地址,需要使用內存屏障指令進隔斷,保證前后讀取指令都是正常的地址 */
/* 設置不同模式下的sp指針,每一個模式的sp對應不同的物理地址,當進入不同工作模式時C語言會在不同的物理sp指針指向的棧內存上工作 */
/* 進入IRQ模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 將r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x12 /* r0或上0x12,表示使用IRQ模式 */
msr cpsr, r0 /* 將r0 的數據寫入到cpsr_c中 */
ldr sp, =0x80600000 /* 設置棧指針 */
/* 進入SYS模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 將r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x1f /* r0或上0x1f,表示使用SYS模式 */
msr cpsr, r0 /* 將r0 的數據寫入到cpsr_c中 */
ldr sp, =0x80400000 /* 設置棧指針 */
/* 進入SVC模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 將r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x13 /* r0或上0x13,表示使用SVC模式 */
msr cpsr, r0 /* 將r0 的數據寫入到cpsr_c中 */
ldr sp, =0x80200000 /* 設置棧指針 */
cpsie i /* 打開IRQ */
b main /* 跳轉到C語言main函數 */
IRQ外設中斷服務函數
當一個外設觸發中斷(比如按鍵按下后)會執行IRQ_Handler
函數。
- 中斷發生是首先保護現場(lr, r0-r12寄存器, 保存spsr寄存器數據)
- 讀取GIC寄存器組的起始地址
- 通過對GIC寄存器組基地址偏移得到CPU Interface寄存器組
- 通過對CPU Interface基地址進行偏移得到GICC_IAR寄存器,它保存了觸發中斷的CPU號與中斷號
- 讀取中斷號(目前只有一個CPU內核,可以不管CPU號)放入r0寄存器,調用對應的C語言函數執行中斷
- 在執行中斷前,首先需要將模式切換到SVC,這樣在執行中斷的時候可以允許新的IRQ中斷觸發
- 執行C語言的中斷邏輯后切換到IRQ模式,繼續完成中斷收尾工作
- 恢復spsr寄存器數據
- 恢復中斷執行前的現場(lr, r0-r12)
- 將lr地址減4字節再給到pc寄存器,恢復中斷前的執行指令
/*
中斷發生時, IRQ模式下的lr(LR_svc物理)寄存器保存中斷時刻的PC寄存器
通過使用push命令將lr的值壓入棧,這樣的目的是為了在執行完當前中斷服務函數
后可以順利的返回到中斷前的執行位置,因為在執行中斷服務函數的時候lr里面的值可能發生變化
比如: 在內部使用了blx調用其它函數,新的IRQ中斷進入
*/
push {lr}
/*
保存中斷發生時的執行現場(r0-r12)
從User/Sys模式切換到IRQ模式,r0-r12寄存器是通用的,所以需要將這些寄存器都壓入棧保存起來,
由于在執行IRQ中斷函數時模式已經切換,此時的sp指針已經是IRQ模式下的棧地址了,所以r0-r12保存到了
IRQ對應的棧空間中,恢復現場的時候只需要入棧即可
*/
push {r0-r12}
// push {r0-r3, r12}
/*
將spsr(SPSR_irq)寄存器的值壓入棧,spsr保存了中斷發生時的cpsr寄存器的值,
中斷執行完成之后需要恢復
*/
mrs r0, spsr
push {r0}
/* GIC寄存器組的基地址(起始地址,通過起始地址可以訪問得所有的GIC寄存器) */
/* 將GIC基地址讀取到r1寄存器中 */
MRC p15, 4, r1, c15, c0, 0 // Read Configuration Base Address Register
/* 0x2000 - 0x3FFF 是GIC中CPU Interface的范圍 */
/* 將r1中保存的GIC基地址偏移0x2000再保存到r1中 */
/* 此時r1中保存的是CPU Interface的基地址 */
add r1, r1, #0x2000
/*
r1(CPU Interface基地址)偏移0x0c得到GICC_IAR寄存器地址,
將GICC_IAR寄存器的值讀取到r0中,
GICC_IAR保存了IRQ中斷的中斷號與CPU號(多核時使用),
通過中斷號即可明確具體的中斷來源并對中斷進行響應
*/
ldr r0, [r1, #0x0c]
/*
由于要進入到SVC模式了,需要將r0, r1兩個通用寄存器的數據保存到棧里,
防止在SVC模式下后r0,r1數據丟失
此時r0, r1保存到的是IRQ模式下的棧空間,
*/
push {r0, r1}
/*
將CPSR寄存器的M[4:0]值改成10011, 讓CPU進入到SVC模式,
進行SVC模式之后,當我們處理當前中斷時,
系統可以再次響應IRQ中斷
*/
cps #0x13 // 進入到SVC
/*
進入到svc模式后先將lr的數據壓入棧,執行完后再恢復
因為接下來要使用blx調用C語言函數,會改變lr寄存器的數據
*/
push {lr}
ldr r2, =system_irq_handler // 將C語言寫的中斷服務函數的地址加載到r2寄存器
blx r2 // 調用C語言的中斷處理函數, r0為函數參數
pop {lr} // 調用完具體中斷處理函數后,lr恢復
cps #0x12 // 進入到IRQ,執行完中斷服務函數后進入IRQ不能再次響應IRQ中斷,直到當前的IRQ中斷完成
pop {r0, r1} // 恢復IRQ模式下r0,r1寄存器的數據
/*
此時r0保存的是GICC_IAR寄存器的數據,
將GICC_IAR的數據寫入到GICC_EOIR寄存器,表示當前IRQ中斷處理完成
*/
str r0, [r1, #0x10]
pop {r0} // 將棧頂的數據(此時棧頂保存的是spsr寄存器的值)出棧到r0寄存器
/// spsr_cxsf其中(cxsf表示4個不同的8bit位數據,后續表示此次命令影響的數據位), spsr_cxsf等于spsr
msr spsr_cxsf, r0 // 恢復spsr寄存器數據
pop {r0-r12} // 恢復r0-r12寄存器的數據
pop {lr} // 恢復lr寄存器的數據
subs pc, lr, #4 // 將lr - 4字節賦值給lc, 恢復中斷前的執行命令繼續執行
IRQ中斷服務通用邏輯處理函數
我們需要編寫一個通用的中斷處理函數,從參數(r0寄存器中的GICC_IAR寄存器的數據)中提取中斷號,根據對應的中斷號再調用注冊進來的具體的中斷函數,比如: 按鍵中斷函數
void system_irq_handler(unsigned int gicciar)
{
uint32_t irqNum = gicciar & 0x3FF;
if (irqNum >= NUMBER_OF_INT_VECTORS)
return;
Interrupt_Irq_Count++;
Interrupt_Irq_Data iid = _irqInterruptTables[irqNum];
iid.handler(irqNum, iid.context);
Interrupt_Irq_Count--;
}
外設中斷驅動
- GPIO復用以及配置電氣屬性
- 配置GPIO的輸入與輸出
- 初始化GPIO中斷
void GPIO_Init_Interrupt(GPIO_Type *base, int pin, GPIO_INTERRUPT_MODE mode)
{
/// 首先將GPIO的edge_sel寄存器對應pin位清0,如果為1則會使ICR寄存器的配置無效
base->EDGE_SEL &= ~(1 << pin);
/// 對應ICR的索引(按2位為一個單元)
int icrOffset = pin;
/// 具體的icr寄存器地址
__IO uint32_t *p_icr = NULL;
if (pin < 16)
{
p_icr = &(base->ICR1);
}
else
{
p_icr = &(base->ICR2);
icrOffset -= 16;
}
switch (mode)
{
case GPIO_INTERRUPT_MODE_NO_INTERRUPT:
break;
case GPIO_INTERRUPT_MODE_LOW:
*p_icr &= ~(3 << (2 * icrOffset));
break;
case GPIO_INTERRUPT_MODE_HIGH:
*p_icr &= ~(3 << (2 * icrOffset));
*p_icr |= 1 << (2 * icrOffset);
break;
case GPIO_INTERRUPT_MODE_RISING_EDGE:
*p_icr &= ~(3 << (2 * icrOffset));
*p_icr |= 2 << (2 * icrOffset);
break;
case GPIO_INTERRUPT_MODE_FALLING_EDGE:
*p_icr &= ~(3 << (2 * icrOffset));
*p_icr |= 3 << (2 * icrOffset);
break;
case GPIO_INTERRUPT_MODE_RISING_AND_RALLING_EDGE:
*p_icr &= ~(3 << (2 * icrOffset));
base->EDGE_SEL |= (1 << pin);
break;
}
}
- I.MX6ULL的GIC使能對應中斷ID的中斷
/// 使用GPIO1的IO18對應的IRQ中斷ID(GPIO1_Combined_16_31_IRQn)
GIC_EnableIRQ(GPIO1_Combined_16_31_IRQn);
- 注冊對應中斷ID的中斷服務處理函數
/// 注冊對應IRQ中斷號的中斷服務函數
Interrupt_Irq_Handler_Register(GPIO1_Combined_16_31_IRQn, (Interrupt_Irq_Handler)Key0_Interrupt_Irq_Handler, NULL);
/// 使用GPIO01_IO18中斷
- GPIO使能中斷
/// 使用GPIO01_IO18中斷
GPIO_Enable_Interrupt(GPIO1, 18);
- 在中斷服務處理函數中處理中斷
void Key0_Interrupt_Irq_Handler(unsigned int gicciar, void *context)
{
/// 中斷服務函數要求快進快出,這里沒有定時器
/// 為了處理抖動暫時使用Delay來解決
/// 以后使用定時器來處理
Delay(10);
if (GPIO_ReadValue(GPIO1, 18) == 0)
{
Beep_On();
Led_On();
Delay(350);
Beep_Off();
Led_Off();
}
/// 中斷處理完成后,清楚中斷標志位
GPIO_Clean_Interrupt_Flag(GPIO1, 18);
}
發布評論請先 登錄
相關推薦
評論