在学习本章节之前,有必须先去了解学习下列知识,并结合韦东山老师的IMX6ULL裸机开发手册学习
此外:值得注意的是需要了解向量表,以及中断程序的处理流程
一、GPIO中断介绍(通用概念)
中断也属于一种异常
CPU在运行过程中,也会被各种异常打断。这些异常有
① 指令未定义
② 指令、数据访问有问题
③SWI(软中断)
④ 快中断
⑤ 中断
① 按键
② 定时器
③ ADC转换完成
④ UART发生完数据、接收数据等等
这些众多的中断源,汇集于中断管理器,由中断管理器选择优先级最高的中断并通知CPU。CPU会根据中断的类型到跳转到不同的地址处理中断。发生中断后,CPU并不是随便跳到一个地址处理中断,而是根据异常向量表,跳转到对应的地址处理中断。
1.1 GPIO 中断
GPIO中断,指由GPIO模块产生的中断,有边沿触发中断或者电平翻转中断。GPIO模块能检测到引脚上电平的变化,并向中断控制器(GIC)发出中断信号,GIC再向CPU发出中断信号。框架如下图:
CPU在每执行完了条指令时会检查是否发生了中断,若是则会跳转到中断处理地址进行中断处理。为了避免破坏主任务数据,CPU会处理保存当前相关寄存器(保存现场)并进入中断服务函数,执行完中断服务函数后,CPU会恢复相关寄存器(恢复现场),回到主任务继续执行程序。
程序发生GPIO中断后会根据异常向量表强制跳转到0x18(IRQ中断地址)。如下图:
常向量表并不总是从0地址开始,IMX6ULL可以设置vector base寄存器,指定向量表在其他位置,比如设置 vector base 为 0x80000000,指定为 DDR 的某个地址。但是表中的各个异常向量的偏移地址,是固定的:复位向量偏移地址是 0,中断是 0x18。
1.2 GIC中断控制器功能概述(IMX6ULL)
IMX6ULL是Cortex-A7内核,采用GIC V2(Generic Interrupt Controller)中断控制器。在这里只简单的介绍一下GIC,具体可以参考16节中GIC的阐述
如上图所示,硬件中断信号发送到GIC(Generic Interrupt Controller),GIC产生一个FIQ或IRQ信号给CPU。
在初始化中断时,要初始化这4部分:产生中断的源头(GPIO模块或UART模块等)、GIC(内部有Distributor或CPU interface)、CPU本身(设置CPSR寄存器),如下图:
GIC的主要作用可以归结为接受硬件中断信号,并进行简单的处理,按照一定的设置策略,分给对应的CPU处理。如下图:
GIC可以通过四个信号向CPU核汇报中断情况:VIRQ(虚拟快速IRQ)、VFIQ(虚拟快速FIQ)、IRQ、FIQ。VIRQ、VFIQ是针对虚拟化,剩下就是IRQ和FIQ。GPIO中断属于IRQ中断,所以在本次实验中GIC上报IRQ信号给ARM内核。
1.2.1 GIC的内部过程
中断源分为SPI(Shared Peripheral Interrupt)、PPI(Private Peripheral Interrupt)、SGI request(Software-generated Interrupt)。外部中断都属于SPI中断源。
GIC控制器包括分发器(Distributor)和CPU接口端(CPU interface)。
中断信号先到达分发器,分发器根据该中断所设定的CPU,把中断发送到CPU对应的CPUinterface上;在CPUinterface里判断该中断的优先级是否足够高,能否抢断或打断当前的中端处理,如果可以,CPU interface就会发送一个物理的signa到CPU的IRQ线上;CPU接收到中断信号,转到中断处理模式进行处理。
1.3 GIC中断寄存器(IMX6ULL)
GIC寄存器分为Distributor register和CPU interface register。寄存器数目较多,这里介绍本次实验中需要我们设置的寄存器。
Distributor的寄存器名字中有“GICD_”前缀,CPU interface的寄存器名字中有“GICC_”前缀。
1.3.1 GICC_IAR寄存器
GICC_IAR寄存器属于CPU interface register,作用是:保存中断ID,读取GICC_IAR寄存器可以获得中断ID,这个过程可以当作对中断的确认。
位域 | 名 | 读写 | 描述 |
[31:13] | - | | 保留 |
[12:10] | CPUID | R | 对于SGI类中断,它表示谁发出了中断。例如,值为3表示该请求是通过对CPU interface 3上的GICD_SGIR的写操作生成的。 |
[9:0] | Interrupt ID | R | 中断ID |
1.3.2 GICC_EOIR寄存器
写此寄存器,表示某中断已经处理完毕。GICC_IAR的值表示当前在处理的中断,把GICC_IAR的值写入GICC_EOIR就表示中断处理完了。
位域 | 名 | 读写 | 描述 |
[31:13] | - | | 保留 |
[12:10] | CPUID | W | 对于SGI类中断,它的值跟GICD_IAR. CPUID的相同。 |
[9:0] | EOIINTID | W | 中断ID,它的值跟GICD_IAR里的中断ID相同 |
1.4 CP15协处理器及相关寄存器
这部分的指令,我们一般用现成的即可。此处了解该类知识
CP15协处理器介绍
在基于ARM的嵌入式系统中,存储系统通常是协处理器CP15完成的。ARM处理器使用协处理器指令MCR和MRC来读写寄存器,控制cache、MMU、配置时钟(在bootloader时钟初始化时会用到)等。CP15包含16个32位寄存器,编号为0~15。
在本次实验中,需要设置的寄存器有:SCTLR(System Control Register)寄存器,VBAR(Vector Base Address)寄存器。
1.4.1 SCTLR(System Control Register)寄存器
设置SCTLR寄存器可以控制cache、MMU等。
把上面的英文注释翻译成中文,如下表:
位域 | 名 | 读写 | 描述 |
[13] | V | R/W | 向量位,用来设置向量表的基地址。 0:Low exception vectors,基地址为0;如果有VBAR寄存器,则使用它来指定向量表基地址; 1:High exception vectors,向量表基地址为0xFFFF0000 |
[12] | I | R/W | 指令cache使能位 0:指令cache禁止,这是默认值 1:指令cache使能 |
[11] | Z | RAO/WI | 分支预测使能位,当MMU使能时该位自动使能 |
[2] | C | R/W | 数据cache使能位 0:数据cache禁止,这是默认值 1:数据cache使能 |
[1] | A | R/W | 字节对齐设置位, 0:地址对齐检查禁止,这是默认值 1:地址对齐检查使能 |
[0] | M | R/W | MMU使能位, 0:MMU禁止,这是默认值; 1:MMU使能 |
可以使用以下指令读写SCTLR寄存器:
MRC p15, 0, <Rt>, c1, c0, 0 ;把SCTLR寄存器的值读到ARM寄存器Rt中。
MRC p15, 0, <Rt>, c1, c0, 0 ;把ARM寄存器Rt的值写入SCTLR寄存器。
1.4.2 VBAR(Vector Base Address)寄存器
通过VBAR寄存器,可以设置异常向量表的映射地址。如果不把异常向量表的映射地址告诉CPU,在发生异常时,CPU就找不到异常向量表,就无法处理异常。
位域 | 名 | 读写 | 描述 |
[31:5] | Vector_Base_Address | R/W | 向量表基地址的b[31:5],这也意味着这个地址的低5位全为0 |
二、GPIO中断寄存器介绍(针对IMX6ULL)
2.1 GPIOx_ICR1(GPIO interrupt configuration register1)
GPIO中断配置寄存器1,用来配置GPIO中断1~15的触发类型。
位域 | 名 | 读写 | 描述 |
[2n+1:2n] | ICRn | R/W | 用来设置GPIO中断的触发类型, 00:低电平触发; 01:高电平触发; 10:上升沿触发; 11:下降沿触发 |
ICR0~ICR15对应GPIO interrupt 0-15。
2.2 GPIOx_ICR2(GPIO interrupt configuration register2)
GPIO中断配置寄存器2,用来配置GPIO中断16~31的触发类型。
与GPIOx_ICR1类似,ICR15~ICR31对应GPIO interrupt 16-31。
2.3 GPIOx_IMR( GPIO interrupt mask register)
GPIO中断屏蔽寄存器,用来屏蔽或使能某个GPIO中断。
位域 | 名 | 读写 | 描述 |
[n] | IMR | R/W | 每一位对应一个GPIO中断, 0:中断被屏蔽 1:中断使能,未被屏蔽 |
2.4 GPIOx_ISR( GPIO interrupt status register)
GPIO中断状态寄存器,表示某个GPIO中断是否发生了。
位域 | 名 | 读写 | 描述 |
[n] | ISR | R/W | 每一位对应一个GPIO中断,跟GPIO_IMR无关,就是说即使屏蔽了某个中断,还是可以在本寄存器中观察它的状态。 读: 0:中断未发生; 1:中断已发生。 写:某位写入1时,清零该位。 |
2.5 GPIOx_EDGE_SEL(GPIO edge select register)
GPIO中断边沿选择寄存器,它可以用来覆盖GPIOx_ICR1/2中的配置值。
每一位对应一个GPIO中断,一旦设置了GPIO_EDGE_SEL[n]时,GPIO会忽略ICR [n]设置,GPIO interrupt n的触发类型就是双边沿触发。
三、按键中断程序编程流程及示例
100ASK_IMX6ULL有2个按键,本节程序将设置它们的中断处理函数,在按键被按下或松开时进行打印;另外,还可以用KEY1来操作LED。
程序的总体流程是:
① 在中断向量中,保存现场,调用处理函数,恢复现场;
② 初始化:为KEY1、KEY2设置处理函数;初使化GPIO模块、初始化GIC;
③ 准备好一切之后,使能中断。
阅读代码时,建议按照下图来理解:
3.1 管脚设置与中断号查询
从上面的电路图可见KEY1接在GPIO5_1(SNVS_TAMPER1pad,ALT5)上,KEY4接在GPIO4_14(NAND_CE1_Bpad,ALT5)上。
程序中使用IOMUXC_SetPinMux函数设置这两个引脚为GPIO模式。
如何获取这两个GPIO的中断号呢?查阅数据手册的《chapter3,CORTEX A7interrupts》章节,这两个GPIO的中断号如下表所示。对应到GIC的SPI中断号需要在此编号基础上加上32,所以KEY1对应的GIC interrupt ID为(74+ 32= 106),KEY2对应的GIC interrupt ID为(72+ 32= 104)。
当发生GIC 104号中断时,表示发生了GPIO4中interrupt 0~15,需要进一步细分出是GPIO4里的哪一个中断。
当发生GIC 106号中断时,表示发生了GPIO5中interrupt 0~15,需要进一步细分出是GPIO5里的哪一个中断。
3.2 GIC控制器基地址的获取方法
直接查数据手册Table 2-1. System memory map,可以知道gic的基地址是0xA0000,如下图:
对于GIC基地址,还可以通过 CP15查询,下面指令将GIC的基地址读到r0寄存器:
mrc p15, 4, r0, c15, c0, 0
3.3 GIC初始化
gic_init函数实现了如下功能:
①通过CP15获取GIC的基地址,
②读取GICD_TYPER寄存器获得中断的数目,
③ 往GICD_ ICENABLERn寄存器写入0xFFFFFFFF禁用所有的SGI,PPI和SPI;
④通过GICC_PMR设置优先级等级,设置为0xF8;
⑤将GICC_BPR设置为2,这允许各个优先级进行抢占;
⑥ 最后使能group0的distributor和CPU interface。
void gic_init(void)
{
u32 i, irq_num;
GIC_Type *gic = get_gic_base();
/* the maximum number of interrupt IDs that the GIC supports */
irq_num = (gic->D_TYPER & 0x1F) + 1;
/* On POR, all SPI is in group 0, level-sensitive and using 1-N model */
/* Disable all PPI, SGI and SPI */
for (i = 0; i < irq_num; i++)
gic->D_ICENABLER[i] = 0xFFFFFFFFUL;
/* The priority mask level for the CPU interface. If the priority of an
* interrupt is higher than the value indicated by this field,
* the interface signals the interrupt to the processor.
*/
gic->C_PMR = (0xFFUL << (8 - 5)) & 0xFFUL;
/* No subpriority, all priority level allows preemption */
gic->C_BPR = 7 - 5;
/* Enables the forwarding of pending interrupts from the Distributor to the CPU interfaces.
* Enable group0 distribution
*/
gic->D_CTLR = 1UL;
/* Enables the signaling of interrupts by the CPU interface to the connected processor
* Enable group0 signaling
*/
gic->C_CTLR = 1UL;
3.4 中断异常处理汇编部分
start.S中对于中断的处理,概括如下:
① 在异常向量表偏移为0x18的地方使用“ldr pc, =IRQ_Handler”跳转;
② IRQ_Handler标号的处理可以简单分为:保存现场,执行C函数,恢复现场:
在IRQ_Handler标号,处理器处于中断模式,“lr_irq - 4”就是被中断的、尚未执行的指令的地址,我们将r0-r12和“lr-4”都保存在栈上。
然后调用C函数handle_irq_c来处理中断。
函数返回来后,执行“ldmia sp!, {r0-r12, pc}^”,这条指令做的事情可多了:
这样,被中断的程序就继续运行了。
start.s代码如下
.text
.global _start, _vector_table
_start:
_vector_table:
ldr pc, =Reset_Handler /* Reset */
ldr pc, =Undefined_Handler /* Undefined instructions */
ldr pc, =SVC_Handler /* Supervisor Call */
b halt//ldr pc, =PrefAbort_Handler /* Prefetch abort */
b halt//ldr pc, =DataAbort_Handler /* Data abort */
.word 0 /* RESERVED */
ldr pc, =IRQ_Handler /* IRQ interrupt */
b halt//ldr pc, =FIQ_Handler /* FIQ interrupt */
………
.align 2 /*
告诉汇编程序,本伪指令下面的内存变量必须从下一个能被2
整除的地址开始分配*/
IRQ_Handler:
/* 执行到这里之前:
* 1.
lr_irq保存有被中断模式中的下一条即将执行的指令的地址
* 2. SPSR_irq保存有被中断模式的CPSR
* 3. CPSR中的M4-M0被设置为10010, 进入到irq模式
* 4. 跳到0x18的地方执行程序 (向量表的偏移地址确定)
*/
/* 保存现场 */
/* 在irq异常处理函数中有可能会修改r0-r12, 所以先保存 */
/* lr-4是异常处理完后的返回地址, 也要保存 */
sub lr, lr, #4
stmdb sp!, {r0-r12, lr}
/* 处理irq异常 */
bl handle_irq_c
/* 恢复现场 */
ldmia sp!, {r0-r12, pc}^ /* ^会把spsr_irq的值恢复到cpsr里 */
.align 2
Reset_Handler:
/* Reset SCTlr Settings */
mrc p15, 0, r0, c1, c0, 0 /* read SCTRL, Read CP15 System Control
register */
bic r0, r0, #(0x1 << 13) /* Clear V bit 13 to use normal exception
vectors */
bic r0, r0, #(0x1 << 12) /* Clear I bit 12 to disable I Cache */
bic r0, r0, #(0x1 << 2) /* Clear C bit 2 to disable D Cache */
bic r0, r0, #(0x1 << 2) /* Clear A bit 1 to disable strict alignment */
bic r0, r0, #(0x1 << 11) /* Clear Z bit 11 to disable branch prediction */
bic r0, r0, #0x1 /* Clear M bit 0 to disable MMU */
mcr p15, 0, r0, c1, c0, 0 /* write SCTRL, CP15 System Control register */
cps #0x1B /* Enter undef mode */
ldr sp, =0x80300000 /* Set up undef mode stack */
cps #0x12 /* Enter irq mode */
ldr sp, =0x80400000 /* Set up irq mode stack */
cps #0x13 /* Enter Supervisor mode */
ldr sp, =0x80200000 /* Set up Supervisor Mode stack */
/* 设置异常向量表基地址 : VBAR */
ldr r0, =_vector_table
mcr p15, 0, r0, c12, c0, 0 /* set VBAR, Vector Base Address Register*/
//mrc p15, 0, r0, c12, c0, 0 //read VBAR
bl clean_bss
bl system_init
cpsie i /* Unmask interrupts */
bl main
halt:
b halt
clean_bss:
/* 清除BSS段 */
ldr r1, =__bss_start
ldr r2, =__bss_end
mov r3, #0
clean:
cmp r1, r2
strlt r3, [r1]
add r1, r1, #4
blt clean
mov pc, lr
注意:执行Reset_Handler时,CPU处于IRQ模式,用的是IRQ模式下的栈,需要先在Reset_Handler里设置好IRQ模式的栈,这样在中断模式里才可以使用栈,才能调用C函数。
注意:在Reset_Handler里调用“cpsie i”打开中断,这是把CPSR中的I位清零。
注意:在Reset_Handler里使用如下两条指令设置异常向量的基地址
ldr r0, =_vector_tablemcr p15, 0, r0, c12, c0, 0 /* set VBAR, Vector Base Address Register*/
3.5 中断异常处理C函数部分
handle_irq_c函数功能简述如下:
①获取到gic的基地址;
②读取GICC_IAR获得中断号;
③根据中断号调用对应中断号的irq_handler函数,该函数是用户通过request_irq注册的中断处理函数,
④ 然后往GICC_EOIR写入中断号清除掉中断。
gic代码如下:
void handle_irq_c(void)
{
int 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;
printf("irq %d is happened\r\n", nr);
irq_table[nr].irq_handler(nr, irq_table[nr].param);
/* write GICC_EOIR inform the CPU interface that it has completed
* the processing of the specified interrupt
*/
gic->C_EOIR = nr;
}
谁调用reqeust_irq设置了irq_table[nr].irq_handler?请看下一节。
3.6 GPIO中断初始化与安装中断处理函数
先看看初始化函数key_irq_init,功能如下(以KEY1为例):
① 对于KEY1,对应的引脚是GPIO5_01,通过EDGE_SEL设置成双边沿触发;
②设置IMR使能中断;
③ 为了防止误触发,先将ISR对应位写1清除掉中断;
④ 调用request_irq注册对应中断的中断处理函数,就是设置irq_table数组中某一项,设置函数指针:
对于GPIO5_01,处理函数是key_gpio5_handle_irq;对于GPIO4_14,处理函数是key_gpio4_handle_irq。
初始化代码如下:
void key_irq_init(void)
{
/* if set detects any edge on the corresponding input signal*/
GPIO5->EDGE_SEL |= (1 << 1);
/* if set 1, unmasked, Interrupt n is enabled */
GPIO5->IMR |= (1 << 1);
/* clear interrupt first to avoid unexpected event */
GPIO5->ISR |= (1 << 1);
GPIO4->EDGE_SEL |= (1 << 14);
GPIO4->IMR |= (1 << 14);
GPIO4->ISR |= (1 << 14);
request_irq(GPIO5_Combined_0_15_IRQn, (irq_handler_t)key_gpio5_handle_irq, NULL);
request_irq(GPIO4_Combined_0_15_IRQn, (irq_handler_t)key_gpio4_handle_irq, NULL);
}
还是以KEY1为例讲解处理函数key_gpio5_handle_irq,它的功能如下:
①读取GPIO_DR寄存器,根据GPIO5_01的状态打印信息、操作LED
② 在GPIO模块内部清除中断
代码如下:
void key_gpio5_handle_irq(void)
{
/* read GPIO5_DR to get GPIO5_IO01 status*/
if((GPIO5->DR >> 1 ) & 0x1) {
printf("key 1 is release\r\n");
/* led off, set GPIO5_DR to configure GPIO5_IO03 output 1 */
GPIO5->DR |= (1<<3); //led on
} else {
printf("key 1 is press\r\n");
/* led on, set GPIO5_DR to configure GPIO5_IO03 output 0 */
GPIO5->DR &= ~(1<<3); //led off
}
/* write 1 to clear GPIO5_IO03 interrput status*/
GPIO5->ISR |= (1 << 1);
}
void key_gpio4_handle_irq(void)
{
/* read GPIO4_DR to get GPIO4_IO014 status*/
if((GPIO4->DR >> 14 ) & 0x1)
printf("key 2 is release\r\n");
else
printf("key 2 is press\r\n");
/* write 1 to clear GPIO4_IO014 interrput status*/
GPIO4->ISR |= (1 << 14);
}
3.7 特定中断号的中断使能与禁止
设置好一切之后,就是使能中断了。
对于GIC,程序里使用gic_enable_irq,它的功能为:
① 根据中断号找到对应的GICD_ISENABLERn寄存器;
② 往相应位中写入1,即可使能中断。
要关闭中断时,操作是类似的,函数是gic_disable_irq,通过往GICD_ICENABLERn对应的位写入1来禁止中断。
void gic_enable_irq(IRQn_Type 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));
}
void gic_disable_irq(IRQn_Type 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));
}
3.8 修改CPSR使能中断
在start.S中,可以看到如下代码,它把CP15中SCTRL的值读出后,把I bit清零,再写入。这就是在CPU核中使能IRQ中断。
代码如下:
Reset_Handler:
/* Reset SCTRL Settings */
mrc p15, 0, r0, c1, c0, 0 /* read SCTRL, Read CP15 System Control register */
bic r0, r0, #(0x1 << 13) /* Clear V bit 13 to use normal exception vectors */
bic r0, r0, #(0x1 << 12) /* Clear I bit 12 to disable I Cache */
bic r0, r0, #(0x1 << 2) /* Clear C bit 2 to disable D Cache */
bic r0, r0, #(0x1 << 2) /* Clear A bit 1 to disable strict alignment */
bic r0, r0, #(0x1 << 11) /* Clear Z bit 11 to disable branch prediction */
bic r0, r0, #0x1 /* Clear M bit 0 to disable MMU */
mcr p15, 0, r0, c1, c0, 0 /* write SCTRL, CP15 System Control register */
3.9 主函数调用
main函数调用system_init进行系统初始化,system_init做了这些事情:
①调用system_init_irq_table初始化中断跳转表;
②调用key_irq_init初始化按键中断:配置GPIO、注册中断处理函数;
③调用gic_init初始化GIC控制器;
④ 最后通过gic_enable_irq使能中断。
void system_init()
{
init_pins();
led_gpio_init();
led_ctl(0);//turn off led
boot_clk_gate_init();
boot_clk_init();
uart1_init();
puts("hello world\r\n");
system_init_irq_table(); //初始化中断跳转表
key_irq_init(); //初始化按键中断、配置GPIO、注册中断处理函数
gic_init(); //初始化GIC控制器
gic_enable_irq(GPIO5_Combined_0_15_IRQn);//使能中断
gic_enable_irq(GPIO4_Combined_0_15_IRQn);//使能中断
}
3.10 程序编译烧录运行