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段 或者把它移動到清理之前的位置