IMX6ULL裸机学习(11)— 中断和GIC中断控制器
一、什么是GIC
GIC即通用中断控制器( Generic Interrupt Controller),ARM 体系结构定义了通用中断控制器(GIC),该控制器包括一组用于管理单核或多核系统中的中断的硬件资源。 GIC 提供了内存映射寄存器,可用于管理中断源和行为,以及用于将中断路由到各个 CPU 核。它使软件能够屏蔽,启用和禁用来自各个中断源的中断,以对各个中断源进行优先级排序和生成软件触发中断。
如下图所示,对于各种中断都是通过GIC到达CPU的。另外,所有的中断都使用同一个异常向量,即无论发生什么中断,CPU都是跳转到异常向量表的第七项IRQ异常处,对于不同的中断,都需要我们在软件中区分。
二、中断的分类
如下图所示,为GIC控制器的逻辑结构。我们知道,中断源是有很多的,所以为了区分这些不同的中断源GIC会给他们分配一个唯一 ID,这些 ID 就是中断 ID。每一个 CPU 最多支持 1020 个中断 ID,中断 ID 号为 ID0~ID1019。这 1020 个 ID 分为三类:
-
SGI:软件触发中断(Software Generated Interrupt)中断号0-15
这是由软件通过写入专用仲裁单元的寄存器即软件触发中断寄存器(GICD_SGIR)显式生成的。它最常用于CPU核间通信。SGI既可以发给所有的核,也可以发送给系统中选定的一组核心。中断号0-15保留用于SGI的中断号。用于通信的确切中断号由软件决定。 -
PPI:私有外设中断(Private Peripheral Interrupt)中断号16-31
我们知道,GIC 是支持多核的,每个核肯定有自己独有的中断。这些独有的中断肯定是要指定的核心处理,因此这些中断就叫做私有中断。PPI的中断号为16-31,它们标识CPU核私有的中断源,并且独立于另一个内核上的相同中断源,比如,每个核的计时器。 -
SPI:共享外设中断(Shared Peripheral Interrupt)中断号32-1020
共享中断,顾名思义,所有 Core 共享的中断,这个是最常见的,比如按键中断、串口中断等等,这些中断所有的 Core 都可以处理,不限定特定 Core。
三、IMX6ULL的GPIO中断
对于IMX6ULL来说,共享中断总共使用了128个中断 ID,即ID号32~159,这128个中断 在《I.MX6ULL 参考手册》的【3.2 Cortex A7 interrupts】小节的【Table 3-1】中列出来了。我们可以从中查找到我们所需要的GPIO1_IO18的中断ID为99(=67+32),并且可以看到GPIO1_IO16~IO31 这 16 个 IO 共用 ID 99。
四、IRQ中断编程
首先,修改start.s
文件,添加IRQ异常的处理和允许中断触发
.text /* .text段保存代码,是只读和可执行的,后面的指令都属于.text段。 */
.global _start /* .global表示_start是一个全局符号,会在链接器链接时用到 */
_start: /* 标签_start,汇编程序的默认入口是_start */
b reset /* 复位异常,复位后从这里开始执行 */
ldr pc,=_undefined_instruction /* 未定义的指令异常,当cpu执行到不认识的指令时,会跳转到这里 */
ldr pc,=_software_interrupt /* 软中断异常,当cpu执行svc指令时,会跳转到这里 */
.word 0 /* ldr pc,=_prefetch_abort /* 预取中止异常,处理器预取指令的地址不存在,或不允许当前访问时 */
.word 0 /* ldr pc,=_data_abort /* 数据中止异常,处理器数据访问指令的地址不存在,或不允许当前访问时 */
.word 0 /* ldr pc,=_not_used /* 未使用,预留的地址 */
ldr pc,=_irq /* 中断请求有效,且CPSR中的I位(即中断屏蔽位)为0时,产生IRQ异常 */
.word 0 /* ldr pc,=_fiq /* 快速中断请求有效,且CPSR中的F位(即快中断屏蔽位)为0时,产生FIQ异常 */
reset:
/* 1、设置栈 */
ldr sp, =(0x80000000+0x100000) /* 设置栈顶地址 */
/* 2、设置异常向量表基地址 : VBAR */
ldr r0, =_start
mcr p15, 0, r0, c12, c0, 0
/* 3、清除bss段 */
ldr r1, =__bss_start /* 将bss段开始地址存入r1寄存器 */
ldr r2, =__bss_end /* 将bss段结束地址存入r2寄存器 */
b clean_bss
clean:
mov r3, #0 /* 将0存入r3寄存器 */
str r3, [r1], #4 /* 将r3中的值存到r1中的值所指向的地址中, 同时r1中的值加4 */
clean_bss:
cmp r1, r2 /* 比较r1和r2内的值是否相等 */
bne clean /* 如果不相等则跳转到clean标签处,如果相等则往下执行 */
/* 4、调用系统初始化函数,在异常处理函数中需要用到uart功能,需要先初始化uart */
bl SystemInit
/* 5、获取当前CPSR的值存入r1中,r1会作为C函数的参数进行传递 */
mrs r0, cpsr
bl print_cpsr
/* 6、写入一个未定义指令 */
.word 0xffffffff
/* 7、触发一个SVC异常 */
SVC #1
/* 8、允许中断触发,清除I位,使能中断*/
CPSIE I //
/* 9、跳转到led函数 */
bl main
/* 10、原地循环 */
b .
_undefined_instruction:
/* 1、设置Undefined模式下的栈 */
ldr sp, =(0x80000000+0x50000)
/* 2、保存现场 */
stmdb sp!, {r0-r12,lr}
/* 3、调用处理函数 */
bl do_undefined_c
/* 4、恢复现场 */
ldmia sp!, {r0-r12,pc}^
_software_interrupt:
/* 1、设置SVC模式下的栈 */
ldr sp, =(0x80000000+0x40000)
/* 2、保存现场 */
stmdb sp!, {r0-r12,lr}
/* 3、调用处理函数 */
bl do_svc_c
/* 4、恢复现场 */
ldmia sp!, {r0-r12,pc}^
_irq:
/* 1、设置IRQ模式下的栈 */
ldr sp, =(0x80000000+0x30000)
/* 2、保存现场 */
subs lr, lr, #4
stmdb sp!, {r0-r12,lr}
/* 3、调用处理函数 */
bl do_irq_c
/* 4、恢复现场 */
ldmia sp!, {r0-r12,pc}^
然后新建gic.h和gic.c函数,实现gic控制器的初始化和irq处理函数do_irq_c()
首先,我们可以在《I.MX6ULL 参考手册》的【System memory map】表中查到GIC控制器的基地址为0X00A00000
,也可以通过 CP15 查询,指令如下,将 GIC 的基地址读到 r0 寄存器:
mrc p15, 4, r0, c15, c0, 0
接下来我们要将所有的中断禁用,以免再初始化的过程中出现意外,先通过GICD_TYPER
寄存器获取中断个数,在【ARM® Generic Interrupt Controller】手册中可以查到该寄存器
然后对 GICD_ICENABLER
寄存器写1来禁止中断,注意 GICD_ICENABLER有一组寄存器,每个寄存器控制着32个中断号
然后GICD_IGROUPR
寄存器来设置中断的分发,将中断全部分发给group0,(关于Group 0 和Group 1是与安全相关的,暂时不去深究),同样的,每个寄存器控制着32个中断号。
另外,对于多核CPU,需要设置将中断发送给哪一个CPU,通过GICD_ITARGETSR
寄存器来实现,因为IMX6ULL为单核CPU,所以我们将所有的共享中断都发送给CPU interface 0,不同于上面两个寄存器,每个GICD_ITARGETSR寄存器只控制4个中断号,且GICD_ITARGETSR0~ 7 是只读的,对应的就是中断号0~31(软件触发中断和私有外设中断)。
然后GICD_ICFGR
用来设置中断触发类型,每个GICD_ICFGR寄存器控制16个中断号
关于中断类型的区别如下
寄存器GICC_PMR
是一个中断优先级过滤器,对于IMX6ULL来说,其有32个优先级,【7:3】为优先级设置有效位,所以我们将GICC_PMR的【7:3】设置为1用来通过这32个优先级,将【2:0】设为0。
接下来是抢占优先级和子优先级的设置,我们将【7:3】这5位都设置位抢占优先级,即没有子优先级, GICC_BPR
在设置为2,如下所示
编写gic_init()
如下所示
/* 描述:GIC控制器初始化
* 参数:无
* 返回值:无 */
void gic_init(void)
{
u32 i, irq_num;
/* 1、获取GIC控制器的基地址 */
GIC_Type *gic = get_gic_base();
/* 2、获取imxull的中断个数,注意: 中断个数 = irq_num * 32*/
irq_num = (gic->D_TYPER & 0x1F) + 1;
/* 3、禁止所有的PPI、SIG、SPI中断,每个ICENABLER寄存器对应32个中断号 */
for (i = 0; i < irq_num; i++)
gic->D_ICENABLER[i] = 0xFFFFFFFFUL;
/* 4、把所有的中断都发给group0,每个IGROUPR寄存器对应32个中断号 */
for (i = 0; i < irq_num; i++)
gic->D_IGROUPR[i] = 0x0UL;
/* 5、所有的SPI中断都发给cpu interface 0,D_ITARGETSR定义为8位数据类型,只对应一个中断号 */
for (i = 32; i < (irq_num * 32); i++)
gic->D_ITARGETSR[i] = 0x01UL;
/* 6、设置所有的SPI中断触发类型,0-level, 1-edge */
for (i = 2; i < irq_num << 1; i++)
gic->D_ICFGR[i] = 0x01010101UL;
/* 7、优先级过滤设置,允许imx6ull所有的32个优先级 */
gic->C_PMR = (0xFFUL << (8 - 5)) & 0xFFUL;
/* 8、设置优先级组,[7:3]五位都为抢占优先级,没有子优先级 */
gic->C_BPR = 7 - 5;
/* 9、使能group0给CPU interface分发中断 */
gic->D_CTLR = 1UL;
/* 10、使能CPU interface给processor分发中断 */
gic->C_CTLR = 1UL;
}
/* 描述:使能中断号为nr的中断
* 参数:中断ID
* 返回值:无 */
void gic_enable_irq(uint32_t nr)
{
GIC_Type *gic = get_gic_base();
/* The GICD_ISENABLERs provide a Set-enable bit for each interrupt supported by the GIC.
* Writing 1 to a Set-enable bit enables forwarding of the corresponding interrupt from the
* Distributor to the CPU interfaces. Reading a bit identifies whether the interrupt is enabled.
*/
gic->D_ISENABLER[nr >> 5] = (uint32_t)(1UL << (nr & 0x1FUL));
}
然后我们再实现如下几个函数
/* 描述:使能中断号为nr的中断
* 参数:中断ID
* 返回值:无 */
void gic_enable_irq(uint32_t nr)
{
GIC_Type *gic = get_gic_base();
/* The GICD_ISENABLERs provide a Set-enable bit for each interrupt supported by the GIC.
* Writing 1 to a Set-enable bit enables forwarding of the corresponding interrupt from the
* Distributor to the CPU interfaces. Reading a bit identifies whether the interrupt is enabled.
*/
gic->D_ISENABLER[nr >> 5] = (uint32_t)(1UL << (nr & 0x1FUL));
}
/* 描述:禁止中断号为nr的中断
* 参数:中断ID
* 返回值:无 */
void gic_disable_irq(uint32_t nr)
{
GIC_Type *gic = get_gic_base();
/* The GICD_ICENABLERs provide a Clear-enable bit for each interrupt supported by the
* GIC. Writing 1 to a Clear-enable bit disables forwarding of the corresponding interrupt from
* the Distributor to the CPU interfaces. Reading a bit identifies whether the interrupt is enabled.
*/
gic->D_ICENABLER[nr >> 5] = (uint32_t)(1UL << (nr & 0x1FUL));
}
/* 描述:获得当前中断的 interrtup ID。
* 参数:无
* 返回值:中断ID */
uint32_t get_gic_irq(void)
{
uint32_t nr;
GIC_Type *gic = get_gic_base();
/* The processor reads GICC_IAR to obtain the interrupt ID of the
* signaled interrupt. This read acts as an acknowledge for the interrupt
*/
nr = gic->C_IAR;
return nr;
}
/* 描述:清除中断,表示该中断已经处理完毕
* 参数:中断ID
* 返回值:无 */
void clear_gic_irq(int nr)
{
GIC_Type *gic = get_gic_base();
/* write GICC_EOIR inform the CPU interface that it has completed
* the processing of the specified interrupt
*/
gic->C_EOIR = nr;
}
/* 描述:IRQ中断处理函数
* 参数:无
* 返回值:无 */
void do_irq_c(void)
{
int irq;
/* 1. 分辨中断,获取当前中断号 */
irq = get_gic_irq();
/* 2. 调用处理函数 */
if (irq == (67 + 32))
{
irq_hander_gpio1_16_32();
}
/* 3. 清除中断 */
clear_gic_irq(irq);
}
五、GPIO编程
新建key.h
和key.c
文件,编写GPIO初始化以及中断处理函数
#include "gic.h"
#include "key.h"
#include "uart.h"
#define IRQ_GPIO1_16_32 (67 + 32)
void key_init(void)
{
volatile unsigned int *pRegKey;
GPIO_Type *gpio1 = (GPIO_Type *)0x0209c000;
/* 1、把GPIO1_18设置为GPIO功能 */
pRegKey = (volatile unsigned int *)(0x020e0000 + 0x8c);
*pRegKey &= ~(0xf);
*pRegKey |= (0x5);
/* 2、把GPIO1_18设置为输入引脚 */
gpio1->GDIR &= ~(1<<18);
/* 3、设置中断触发方式:双边沿触发 */
gpio1->EDGE_SEL |= (1<<18);
/* 4、为避免发生错误的中断, 先清除IO18中断标志位和GPIO1中断 */
gpio1->ISR |= (1<<18);
clear_gic_irq(IRQ_GPIO1_16_32);
/* 5、使能IO14中断,使能GPIO4中断 */
gpio1->IMR |= (1<<18);
gic_enable_irq(IRQ_GPIO1_16_32);
}
void irq_hander_gpio1_16_32(void)
{
GPIO_Type *gpio1 = (GPIO_Type *)0x0209c000;
if ((gpio1->ISR & (1<<18)) != 0)
{
if (gpio1->DR & (1<<18))
{
putstring("KEY released!\r\n");
}
else
{
putstring("KEY pressed!\r\n");
}
gpio1->ISR |= (1<<18);
}
}
六、编译
修改makefile,添加gic.c和key.c到源文件目录中,
C_SOURCES = src/main.c \
src/uart.c \
src/led.c \
src/gic.c \
src/key.c
在main函数中加入gic和key的初始化
#include "uart.h"
#include "led.h"
#include "gic.h"
#include "key.h"
void SystemInit(void)
{
uart_init();
}
int main(void)
{
led_init();
gic_init();
key_init();
putstring("imx6ull\r\n");
while(1)
{
putstring("led on\r\n");
led_on();
delay(1000000);
putstring("led off\r\n");
led_off();
delay(1000000);
}
}
执行make编译,
六、烧录运行
接下来烧录到开发板,按下按键,可以看到中断已经触发
七、附录
上一篇:IMX6ULL裸机学习(10)— 未定义异常和SVC异常实例
下一篇:
代码存放:https://gitee.com/william_william/imx6ull_noos/tree/master/05_gpio_int