Cortex-A系列中斷

蘑菇王國大聰明發表於2021-11-24

1. 回顧STM32系統

1.1 中斷向量表

ARM晶片衝0x00000000,在程式開始的地方存放中斷向量表,按下中斷時,就相當於告訴CPU進入的函式。描述很多箇中斷服務函式的表。
image
對於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,中斷向量表需要使用者自己去定義
image
image
這裡面用pc因為pc執行完就進入下一個+0x04,在前面就定義各個中斷
GIC V2是cortex7-A使用的,最多支援8個核,V3和V4是給64位晶片使用的
image

  • 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

image
image

c1

image
image

c12

image

c15

image
image

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);
}

中斷驅動

image

  • 我們首先要設定GPIO的中斷觸發方式,也就是ICR1和ICR2暫存器。觸發方式有低電平高電平上升沿下降沿,對於本例程來說,按鍵是屬於下降沿觸發
    image
  • IMR暫存器使能GPIO對應的中斷
  • edge暫存器是設定邊沿觸發的
    image
  • ISR暫存器,每一個IO都有一個,處理完中斷之後要清除中斷標誌位,也就是清除ISR,清除不是寫0,ISR是寫1清除

GIC配置

  • 使能相應的中斷id,比如key是GPIO1——IO18,就要找到它的中斷id,由圖可知是67
    注意這裡面雖然是67,但是別忘了多核中斷的32個,所以要67+32
    image
    image
  • 設定相應的相應優先順序
  • 註冊中斷處理函式
    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 錯誤解決方案

image
這種情況一般是沒有在makefile新增搜尋路徑

按下按鍵後,沒有反應並且卡死

image
從04開始必須是中斷向量表,在連結檔案中可以看到,中斷並不是從04開始的,被bss段佔了
第一種解決方案,就是中斷向量偏移改成8開始,但是預設其低5位必須是0
第二種是遮蔽清理bss段 或者把它移動到清理之前的位置

相關文章