1. 回顧STM32系統
1.1 中斷向量表
ARM晶片衝0x00000000,在程式開始的地方存放中斷向量表,按下中斷時,就相當於告訴CPU進入的函式。描述很多箇中斷服務函式的表。
對於STM32來說,程式碼最開始存放棧頂指標(0x80000000),然後是Reset_Handler(0x80000004復位中斷),以此類推
1.2 中斷向量偏移
一般ARM是從0x00000000,32是從0x80000000,I.MX是0x87800000,所以要設定中斷向量偏移,32中設定SCB的VTOR暫存器為新的中斷向量表起始地址即可
1.3 nvic中斷控制器,使能和關閉中斷,設定中斷優先順序
1.4 中斷服務函式編寫
2. Cortex-A中斷
主要是IRQ中斷 0x18
2.1 Cortex-A 中斷向量表
Cortex-A 中斷向量有8箇中斷,其中重點關注IRQ,中斷向量表需要使用者自己去定義
這裡面用pc因為pc執行完就進入下一個+0x04,在前面就定義各個中斷
GIC V2是cortex7-A使用的,最多支援8個核,V3和V4是給64位晶片使用的
- SPI:共享中斷,那些外部中斷都屬於SPI
- PPI:私有中斷,GIC是多核的,每個核都有自己的私有中斷
- SGI:軟體中斷,由軟體觸發引起的中斷,通過寫入暫存器來完成,系統會使用它完成多核中斷
2.2 中斷號
為了區分不同的中斷,引入了中斷號,1020箇中斷號。
0-15是給SGI
15-31是給PPI。
剩下的給SPI,但在I.MX6U只用到了160個,SPI是128箇中斷,CortexA7有128箇中斷。
中斷ID的作用的是讓IRQ認識到是哪個中斷。
2.3 中斷服務函式
一個是IRQ中斷服務函式的編寫,另一個是在IRQ中斷服務函式裡面去查詢並執行的具體的外設中斷服務函式。
3. 程式碼(編寫按鍵中斷例程)
由原理圖可知,Key0使用UARTA1_CTS這個IO
3.1 修改彙編檔案
編寫復位中斷函式
- 關閉ID Cache和MMU
- 設定處理器9種模式下的對應sp指標,要使用中斷必須設定IRQ模式下的sp指標,直接設定所有的sp指標
- 清除bss段
- 進入main
···
···
3.2 cp15協處理器
c0
c1
c12
c15
3.3 程式碼編寫
IRQ中斷服務函式編寫,根據上面介紹的暫存器,要編寫彙編程式碼和systemIRQhandler函式,具體過程已經寫好。(彙編的介紹在中斷棧分析裡面講過)
彙編程式碼
.global _start /* 全域性標號 */
/*
* 描述: _start函式,程式從此函式開始執行,此函式主要功能是設定C
* 執行環境。
*/
_start:
/* 編寫中斷向量表 */
ldr pc,= Reset_Handler /* 復位中斷函式,名字可以隨便寫,下面對上*/
ldr pc,= Undefined_Handler /* 未定義指令中斷 */
ldr pc,= SVC_Handler /* SVC中斷 */
ldr pc,= PrefAbort_Handler /* 預取中止 */
ldr pc,= DataAbort_Handler /* 資料中止 */
ldr pc,= NotUsed_Handler /* 沒用中斷 */
ldr pc,= IRQ_Handler /* 中斷 */
ldr pc,= FIQ_Handler /* 快速中斷 */
/* 復位中斷 */
Reset_Handler:
cpsid i /* 關閉全域性中斷 */
/* 關閉 I,DCache 和 MMU
* 採取讀-改-寫的方式。
*/
mrc p15, 0, r0, c1, c0, 0 /* 讀取 CP15 的 C1 暫存器到 R0 中 */
bic r0, r0, #(0x1 << 12) /* 清除 C1 的 I 位,關閉 I Cache */
bic r0, r0, #(0x1 << 2) /* 清除 C1 的 C 位,關閉 D Cache */
bic r0, r0, #0x2 /* 清除 C1 的 A 位,關閉對齊檢查 */
bic r0, r0, #(0x1 << 11) /* 清除 C1 的 Z 位,關閉分支預測 */
bic r0, r0, #0x1 /* 清除 C1 的 M 位,關閉 MMU */
mcr p15, 0, r0, c1, c0, 0 /* 將 r0 的值寫入到 CP15 的 C1 中 */
#if 0
/* 彙編版本設定中斷向量表偏移 */ @也可以在c語言裡面做
ldr r0, =0X87800000
dsb @同步指令 一個是資料同步一個是指令同步
isb
mcr p15, 0, r0, c12, c0, 0
dsb
isb
#endif
/* 清理bss段的程式碼 */
.global _bss_start
_bss_start:
.word _bss_start /* 相當於定義了一個段,類似宣告一個數,word是一個word長度 */
.global _bss_end
_bss_end:
.word _bss_end
/* 清除BSS段 */
ldr r0, _bss_start
ldr r1, _bss_end
mov r2, #0
bss_loop:
stmia r0!, {r2} @將r2裡面的資料轉存到r0
cmp r0, r1 @比較兩個暫存器裡面的值
ble bss_loop @如果小於等於r1,繼續清楚bss段
/* 設定各個模式下的棧指標,
* 注意:IMX6UL 的堆疊是向下增長的!
* 堆疊指標地址一定要是 4 位元組地址對齊的!!!
* DDR 範圍:0X80000000~0X9FFFFFFF 或者 0X8FFFFFFF
*/
/* 進入 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 中 */
ldr sp, =0x80600000 /* IRQ 模式棧首地址為 0X80600000,大小為 2MB */
/* 進入 SYS 模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 將 r0 的低 5 位清零,也就是 cpsr 的 M0~M4 */
orr r0, r0, #0x1f /* r0 或上 0x13,表示使用 SYS 模式 */
msr cpsr, r0 /* 將 r0 的資料寫入到 cpsr 中 */
ldr sp, =0x80400000 /* SYS 模式棧首地址為 0X80400000,大小為 2MB */
/* 進入 SVC 模式 最後設定這個直接進入C語言*/
mrs r0, cpsr
bic r0, r0, #0x1f /* 將 r0 的低 5 位清零,也就是 cpsr 的 M0~M4 */
orr r0, r0, #0x13 /* r0 或上 0x13,表示使用 SVC 模式 */
msr cpsr, r0 /* 將 r0 的資料寫入到 cpsr 中 */
ldr sp, =0X80200000 /* SVC 模式棧首地址為 0X80200000,大小為 2MB */
cpsie i /* 開啟全域性中斷 */
#if 0
/* 使能 IRQ 中斷 和cpsie i衝突啦,可以不屑*/
mrs r0, cpsr /* 讀取 cpsr 暫存器值到 r0 中 */
bic r0, r0, #0x80 /* 將 r0 暫存器中 bit7 清零,也就是 CPSR 中
* 的 I 位清零,表示允許 IRQ 中斷
*/
msr cpsr, r0 /* 將 r0 重新寫入到 cpsr 中 */
#endif
b main /* 跳轉到 main 函式
/* 未定義中斷 */
Undefined_Handler:
ldr r0, =Undefined_Handler
bx r0
/* SVC 中斷 */
SVC_Handler:
ldr r0, =SVC_Handler
bx r0
/* 預取終止中斷 */
PrefAbort_Handler:
ldr r0, =PrefAbort_Handler
bx r0
/* 資料終止中斷 */
DataAbort_Handler:
ldr r0, =DataAbort_Handler
bx r0
/* 未使用的中斷 */
NotUsed_Handler:
ldr r0, =NotUsed_Handler
bx r0
/* IRQ 中斷!重點!!!!! */
IRQ_Handler:
push {lr} /* 儲存 lr 地址 */
push {r0-r3, r12} /* 儲存 r0-r3,r12 暫存器 */
mrs r0, spsr /* 讀取 spsr 暫存器 */
push {r0} /* 儲存 spsr 暫存器 */
mrc p15, 4, r1, c15, c0, 0 /* 將 CP15 的 C0 內的值到 R1 暫存器中
* 參考文件 ARM Cortex-A(armV7)程式設計手冊 V4.0.pdf P49
* Cortex-A7 Technical ReferenceManua.pdf P68 P138
*/
add r1, r1, #0X2000 /* GIC 基地址加 0X2000,得到 CPU 介面端基地址 */
ldr r0, [r1, #0XC] /* CPU 介面端基地址加 0X0C 就是 GICC_IAR 暫存器,
* GICC_IAR 儲存著當前發生中斷的中斷號,我們要根據
* 這個中斷號來絕對呼叫哪個中斷服務函式
* 這樣就通過R0傳參啦
*/
push {r0, r1} /* 儲存 r0,r1 */
@ 中斷來了必須再SVC模式下處理, R0,R1,R2傳參
cps #0x13 /* 進入 SVC 模式,允許其他中斷再次進去 */
push {lr} /* 儲存 SVC 模式的 lr 暫存器 */
@ 必須用r2因為只有它能用
ldr r2, =system_irqhandler /* 載入 C 語言中斷處理函式到 r2 暫存器中, 預設用r0傳參*/
blx r2 /* 執行 C 語言中斷處理函式,帶有一個引數 */
pop {lr} /* 執行完 C 語言中斷服務函式,lr 出棧 */
@ 處理完畢後返回IRQ模
cps #0x12 /* 進入 IRQ 模式 */
pop {r0, r1}
@ 處理完之後,將IAR寫入到EOIR裡面
str r0, [r1, #0X10] /* 中斷執行完成,寫 EOIR */
pop {r0}
msr spsr_cxsf, r0 /* 恢復 spsr */
pop {r0-r3, r12} /* r0-r3,r12 出棧 */
pop {lr} /* lr 出棧 */
subs pc, lr, #4 /* 將 lr-4 賦給 pc */
/* FIQ 中斷 */
FIQ_Handler:
ldr r0, =FIQ_Handler
bx r0
在bsp資料夾裡面建立int資料夾,來初始化中斷服務函式。
bsp_int.h
#ifndef __BSP_INT_H
#define __BSP_INT_H
#include "imx6ul.h"
/* 定義中斷處理函式的形式 */
/* 函式指標的形式定義
* gicciar: 中斷號
* param: 中斷傳過來的引數
*/
typedef void (*system_irq_handler_t)(unsigned int gicciar,
void *param);
/* 中斷處理函式結構體,IMX6UL具有160箇中斷 */
typedef struct _sys_irq_handle
{
system_irq_handler_t irq_handler; /* 中斷處理函式 */
void *userParam; /* 中斷處理函式的引數, 這個和上面的param是一個 */
}sys_irq_handle_t;
void int_init();
void default_irqhanler(unsigned int gicciar, void *param);
void system_register_irqhandler(IRQn_Type irq, system_irq_handler_t handler, void *userParam);
void system_irqhandler(unsigned int giccIar);
#endif // !__INT_H
bsp_int.c
#include "bsp_int.h"
static unsigned int irqNesting; //中斷巢狀計數
/* 中斷處理函式結構體 */
/* NUMBER_OF_INT_VECTORS有定義,160
* 並在下面定義了列舉型別,表示各個中斷號
*/
static sys_irq_handle_t irqTable[NUMBER_OF_INT_VECTORS];
/* 既然有了函式表,就初始化中斷處理函式表 */
/* 給這個表的每個函式都寫上預設值 */
void system_irqtable_init(void)
{
irqNesting = 0; /* 初始化的時候清零中斷巢狀,
* 每進入system的時候,
* +1 */
unsigned int i = 0;
for (i = 0; i < NUMBER_OF_INT_VECTORS; ++i)
{
/* code */
irqTable[i].irq_handler = default_irqhanler;
irqTable[i].userParam = NULL;
}
}
/* 註冊中斷處理函式,每個中斷都要註冊,如果要用到這個中斷 */
/* irq:列舉型別的中斷號
* handler:中斷處理函式
* userParam:引數
*/
void system_register_irqhandler(IRQn_Type irq, system_irq_handler_t handler, void *userParam)
{
irqTable[irq].irq_handler = handler;
irqTable[irq].userParam = userParam;
}
/* 中斷初始化 */
void int_init()
{
/* GIC初始化,這個core_ca7裡面已經定義好了 */
GIC_Init();
/* 中斷表初始化 */
system_irqtable_init();
/* 中斷向量偏移設定 , 也是core_ca7裡面的函式*/
__set_VBAR(0x87800000);
}
/* 具體的中斷處理函式,IRQ_HANDLer會呼叫次函式 */
void system_irqhandler(unsigned int giccIar)
{
/* giccIar是彙編中傳進來的中斷號,和前10位與一下可以得到中斷號具體值 */
/* 這裡面的intNum是中斷號,1023最大 */
uint32_t intNum = giccIar & 0x3FF;
/* 檢查中斷id */
if(intNum == 1023 || intNum >= NUMBER_OF_INT_VECTORS)
{
return;
}
/* 根據中斷id號,讀取中斷處理函式 */
++irqNesting;
irqTable[intNum].irq_handler(intNum, irqTable[intNum].userParam);
--irqNesting; //處理完之後減1
}
/* 預設中斷處理函式 */
void default_irqhanler(unsigned int gicciar, void *param)
{
while(1);
}
中斷驅動
- 我們首先要設定GPIO的中斷觸發方式,也就是ICR1和ICR2暫存器。觸發方式有低電平高電平上升沿下降沿,對於本例程來說,按鍵是屬於下降沿觸發
- IMR暫存器使能GPIO對應的中斷
- edge暫存器是設定邊沿觸發的
- ISR暫存器,每一個IO都有一個,處理完中斷之後要清除中斷標誌位,也就是清除ISR,清除不是寫0,ISR是寫1清除
GIC配置
- 使能相應的中斷id,比如key是GPIO1——IO18,就要找到它的中斷id,由圖可知是67
注意這裡面雖然是67,但是別忘了多核中斷的32個,所以要67+32
- 設定相應的相應優先順序
- 註冊中斷處理函式
gpio.h
/* 為了方便gpio的驅動編寫,編寫一個gpio驅動檔案 */
#ifndef __BSP_GPIO
#define __BSP_GPIO
#include "imx6ul.h"
/* 列舉型別,用於描述中斷觸發型別 */
typedef enum _gpio_interrupt_mode
{
/* 建議和暫存器裡面的值對應 */
kGPIO_NoIntMode = 0U, /* 無觸發 */
kGPIO_IntLowLevel = 1U, /* 低電平觸發 */
kGPIO_IntHighLevel = 2U, /* 高電平觸發 */
kGPIO_IntRisingEdge = 3U, /* 上升沿觸發 */
kGPIO_IntFallingEdge = 4U, /* 下降沿觸發 */
kGPIO_IntRisingOrFallingEdge = 5U, /* 上升沿和下降沿都觸發 */
} gpio_interrupt_mode_t;
/* 列舉型別和結構體定義 */
typedef enum _gpio_pin_direction
{
// 0U和1U是無符號整型的0和1
kGPIO_DigitalInput = 0U, /* 輸入 */
kGPIO_DigitalOutput = 1U, /* 輸出 */
} gpio_pin_direction_t;
/* GPIO 配置結構體 */
typedef struct _gpio_pin_config
{
gpio_pin_direction_t direction; /* GPIO 方向:輸入還是輸出 */
uint8_t outputLogic; /* 如果是輸出的話,預設輸出電平 */
gpio_interrupt_mode_t interruptMode; /* 中斷型別 */
} gpio_pin_config_t;
/* 函式宣告 */
void gpio_init(GPIO_Type *base, int pin, gpio_pin_config_t *config);
int gpio_pinread(GPIO_Type *base, int pin);
void gpio_pinwrite(GPIO_Type *base, int pin, int value);
/* 與中斷有關的函式宣告 */
void gpio_enable(GPIO_Type *base, int pin);
void gpio_disable(GPIO_Type *base, int pin);
void gpio_clearIntFlags(GPIO_Type *base, int pin);
void gpio_intConfig(GPIO_Type *base, int pin, gpio_interrupt_mode_t interruptMode);
#endif // !__BSP_GPIO
gpio.c
#include "bsp_gpio.h"
/*
* @description : GPIO 初始化。
* @param - base : 要初始化的 GPIO 組。
* @param - pin : 要初始化 GPIO 在組內的編號。
* @param - config : GPIO 配置結構體。
* @return : 無
*/
void gpio_init(GPIO_Type *base, int pin, gpio_pin_config_t *config)
{
// base是GPIO的型別,比如DR,GDIR等
// 我們一般操作用這兩個比較多,比如配置輸入輸出
// pin是第幾個腳
if(config->direction == kGPIO_DigitalInput) /* 輸入 */
{
base->GDIR &= ~( 1 << pin);
}else {/* 輸出 */
base->GDIR |= 1 << pin;
gpio_pinwrite(base, pin, config->outputLogic);/* 預設輸出電平 */
}
/* 配置中斷 */
gpio_intConfig(base, pin, config->interruptMode);
}
/*
* @description : 讀取指定 GPIO 的電平值 。
* @param – base : 要讀取的 GPIO 組。
* @param - pin : 要讀取的 GPIO 腳號。
* @return : 無
*/
int gpio_pinread(GPIO_Type *base, int pin)
{
return (((base->DR) >> pin) & 0x1);
}
/*
* @description : 指定 GPIO 輸出高或者低電平 。
* @param – base : 要輸出的的 GPIO 組。
* @param - pin : 要輸出的 GPIO 腳號。
* @param – value : 要輸出的電平,1 輸出高電平, 0 輸出低低電平
* @return : 無
*/
void gpio_pinwrite(GPIO_Type *base, int pin, int value)
{
if (value == 0U)
{
base->DR &= ~(1U << pin); /* 輸出低電平 */
}else{
base->DR |= (1U << pin); /* 輸出高電平 */
}
}
/* 使能指定IO中斷 */
void gpio_enable(GPIO_Type *base, int pin)
{
base->IMR |= (1U << pin); /* 1使能 */
}
/* 關閉指定IO中斷 */
void gpio_disable(GPIO_Type *base, int pin)
{
base->IMR &= ~(1U << pin); /* 0關閉 */
}
/* 清楚中斷標誌位 */
void gpio_clearIntFlags(GPIO_Type *base, int pin)
{
base->ISR |= (1U << pin); /* 1使能 */
}
/* GPIO中斷初始化函式 */
void gpio_intConfig(GPIO_Type *base, int pin,
gpio_interrupt_mode_t interruptMode)
{
/* 定義一個指標 */
/* 用這個來表示具體使用的哪一個ICR暫存器 , 因為有兩個暫存器!*/
volatile uint32_t *icr; //資料類吆喝base裡面的一樣
uint32_t icrShift;
icrShift = pin;
/* 先清零,如果它置1,ICR就無效啦 */
base->EDGE_SEL &= ~(1 << pin);
if(pin < 16)
{
icr = &(base->ICR1);
}else{
icr = &(base->ICR2);
//注意是高位的話,ICR2從0開始,也就是pin是16,但是用ICR2就是0
icrShift -= 16;
}
switch (interruptMode)
{
/* 00 : 低電平觸發
* 01 :高點平觸發
* 10 :上升沿
* 11 :下降沿
* 邊沿觸發:EDGE_SEL為1
*/
case(kGPIO_IntLowLevel):
*icr &= ~(3U << (2 * icrShift));
break;
case(kGPIO_IntHighLevel):
//先清0
*icr = (*icr & (~(3U << (2 * icrShift)))) | (1U << (2 * icrShift));
break;
case(kGPIO_IntRisingEdge):
*icr = (*icr & (~(3U << (2 * icrShift)))) | (2U << (2 * icrShift));
break;
case(kGPIO_IntFallingEdge):
*icr |= (3U << (2 * icrShift));
break;
case(kGPIO_IntRisingOrFallingEdge):
base->EDGE_SEL |= (1U << pin);
break;
default:
break;
}
}
外部中斷
exit.h
#ifndef __BSP_EXIT_H
#define __BSP_EXIT_H
#include "imx6ul.h"
void exit_init();
void gpio1_io18_irqhandler(uint32_t giccIar, void *param);
#endif // !__BSP_EXIT_H
exit.c
#include "bsp_exit.h"
#include "bsp_gpio.h"
#include "bsp_int.h"
#include "bsp_delay.h"
#include "bsp_beep.h"
void exit_init()
{
gpio_pin_config_t key_config;
/* 初始化IO複用,為GPIO */
IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0);
/* 2、、配置 UART1_CTS_B 的 IO 屬性
*bit 16:0 HYS 關閉
*bit [15:14]: 11 預設 22K 上拉
*bit [13]: 1 pull 功能
*bit [12]: 1 pull/keeper 使能
*bit [11]: 0 關閉開路輸出
*bit [7:6]: 10 速度 100Mhz
*bit [5:3]: 000 關閉輸出
*bit [0]: 0 低轉換率
*/
IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18, 0xF080);
/* 初始化這個GPIO為輸入 */
key_config.outputLogic = kGPIO_DigitalInput;
key_config.interruptMode = kGPIO_IntFallingEdge;
gpio_init(GPIO1, 18, &key_config);
/* GIC使能,已經定義好啦 */
GIC_EnableIRQ(GPIO1_Combined_16_31_IRQn);
/* 註冊中斷服務函式 */
system_register_irqhandler(GPIO1_Combined_16_31_IRQn,
gpio1_io18_irqhandler ,NULL);
/* gpio使能 */
gpio_enable(GPIO1, 18);
}
/* 中斷處理函式格式 */
void gpio1_io18_irqhandler(uint32_t giccIar, void *param)
{
static unsigned char state = 0;
/*
*採用延時消抖,中斷服務函式中禁止使用延時函式!因為中斷服務需要
*快進快出!!這裡為了演示所以採用了延時函式進行消抖,後面我們會講解
*定時器中斷消抖法!!!
*/
delay(10);
if(gpio_pinread(GPIO1, 18) == 0) /* 按鍵按下了 */
{
state = !state;
beep_switch(state);
}
gpio_clearIntFlags(GPIO1, 18); /* 清除中斷標誌位 */
}
3.4 錯誤解決方案
這種情況一般是沒有在makefile新增搜尋路徑
按下按鍵後,沒有反應並且卡死
從04開始必須是中斷向量表,在連結檔案中可以看到,中斷並不是從04開始的,被bss段佔了
第一種解決方案,就是中斷向量偏移改成8開始,但是預設其低5位必須是0
第二種是遮蔽清理bss段 或者把它移動到清理之前的位置