LV04-07-中断与异常-05-IMX6ULL按键中断实例

本文主要是中断与异常——IMX6ULL按键中断实例的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows版本 windows11
Ubuntu版本 Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
Win32DiskImager Win32DiskImager v1.0
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
https://elixir.bootlin.com/linux/latest/source 在线阅读linux kernel源码
nxp-imx/linux-imx/releases/tag/rel_imx_4.1.15_2.1.0_ga NXP linux内核仓库tags中的rel_imx_4.1.15_2.1.0_ga
nxp-imx/uboot-imx/releases/tag/rel_imx_4.1.15_2.1.0_ga NXP u-boot仓库tags中的rel_imx_4.1.15_2.1.0_ga
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)
ARM Cortex-A7 MPCore Technical Reference Manual Cortex-A7 MPCore技术参考手册
ARM Architecture Reference Manual ARMv7-A and ARMv7-R edition ARM架构参考手册ARMv7-A和ARMv7-R版
Arm Generic Interrupt Controller Architecture Specification- version 3 and version 4 Arm通用中断控制器架构规范-版本3和版本4
ARM Generic Interrupt Controller Architecture Specification - Version 2.0 Arm通用中断控制器架构规范-版本2.0
ARM Cortex-A Series Programmer's Guide for ARMv7-A Cortex-A系列ARMv7-A编程指南

一、硬件连接

我们用到的就是ALPHA开发板上的按键,电路原理图如下:

image-20230910131613053

按键 KEY0 是连接到 I.MX6U 的 UART1_CTS 这个 IO 上的 ,我们查一下这个IO是哪个,这里其实我没看懂,但是根据教程描述,这个引脚应该是这个:

image-20230910132414351

这个引脚的默认功能是UART1_CTS_B,但是可以复用为GPIO1_IO18,这里就相当于接在了GPIO1_IO18上边。

二、IMX6ULL 的GPIO中断寄存器简介

1. GPIO interrupt configuration register1(GPIOx_ICR1)

GPIO 中断配置寄存器 1,用来配置 GPIO 中断 1~15 的触发类型。

image-20240121150737954
位域 读写 描述
[2n+1:2n] ICRn R/W 用来设置 GPIO 中断的触发类型, 00:低电平触发; 01:高电平触发; 10:上升沿触发; 11:下降沿触发

ICR0~ICR15 对应 GPIO interrupt 0 ~ 15。

2. GPIO interrupt configuration register2(GPIOx_ICR2)

GPIO 中断配置寄存器 2,用来配置 GPIO 中断 16 ~ 31 的触发类型。

image-20240121150853139

与 GPIOx_ICR1 类似, ICR15 ~ ICR31 对应 GPIO interrupt 16 ~ 31。

3. GPIO interrupt mask register (GPIOx_IMR)

GPIO 中断屏蔽寄存器,用来屏蔽或使能某个 GPIO 中断。

image-20240121151048035
位域 名称 读写 描述
[n] IMR R/W 每一位对应一个 GPIO 中断, 0:中断被屏蔽 1:中断使能,未被屏蔽

4. GPIO interrupt status register (GPIOx_ISR)

GPIO 中断状态寄存器,表示某个 GPIO 中断是否发生了。

image-20240121151146225
位域 名称 读写 描述
[n] ISR R/W 每一位对应一个 GPIO 中断,跟 GPIO_IMR 无关,就是说即使屏 蔽了某个中断,还是可以在本寄存器中观察它的状态。 读: 0:中断未发生; 1:中断已发生。 写:某位写入 1 时,清零该位。

5. GPIO edge select register (GPIOx_EDGE_SEL)

GPIO 中断边沿选择寄存器,它可以用来覆盖 GPIOx_ICR1/2 中的配置值。

image-20240121151310604

每一位对应一个 GPIO 中断,一旦设置了 GPIO_EDGE_SEL[n]时, GPIO 会忽略ICR [n]设置, GPIO interrupt n 的触发类型就是双边沿触发。

三、中断程序编程示例

1. 总体编程流程

我使用的开发板IMX6ULL 有 1 个按键,本节程序将设置它的中断处理函数,实现GPIO的外部中断。整体的编程流程是:

① 在中断向量中,保存现场,调用处理函数,恢复现场;

② 初始化:为 KEY0 设置处理函数;初使化 GPIO 模块、初始化 GIC;

③ 准备好一切之后,使能中断。

2. 如何确认中断号

我们要知道发生了什么中断,需要确认中断发生得到时候的中断号,那么怎么确认中断号?我们前边知道了按键接在了GPIO1_IO18上边,我们可以查看《I.MX6UL参考手册》的3.2 Cortex A7 interrupts一节,找到这个GPIO管脚对应的中断号:

image-20230910133047305

可以看到GPIO1的0 -15管脚使用的是66,16 - 31使用的是67,这里只是IRQ的编号,对应到 GIC 的 SPI中断号需要在此编号基础上加上 32,所以这里的按键中断号实际为99(67+32)。这个其实在我们之前移植的SDK包里边就有定义,我们打开 MCIMX6Y2.h 文件,有如下内容:

image-20230910133331052

这里其实已经为我们定义好了中断号的枚举类型。上边我们知道多个GPIO引脚都会产生这个中断号,所以我们需要注意:当发生 GIC 99 号中断时,表示发生了 GPIO1 中 interrupt 0 ~15,然后需要进一步细分出是 GPIO1 里的哪一个中断。

3. GIC 控制器基地址的获取方法

直接查数据手册 《i.MX 6ULL Applications ProcessorReference Manual 》的Table 2-1. System memory map,可以知道 gic 的基地址是 0xA0000,如下图:

image-20240121152007337

对于 GIC 基地址,还可以通过 CP15 查询,下面指令将 GIC 的基地址读到 r0 寄存器:

1
mrc p15, 4, r0, c15, c0, 0

四、软件设计

1. 使用NXP官方SDK实现

1.1 移植 SDK 包中断相关文件

将 SDK 包中的文件 core_ca7.h 拷贝到工程中的“imx6ul”文件夹中,并需要进行修改进行修改。主要留下和 GIC 相关的内容,我们重点是需要core_ca7.h 中的 10 个 API 函数,这 10 个函数如表所示:

函数 描述
GIC_Init 初始化 GIC。
GIC_EnableIRQ 使能指定的外设中断。
GIC_DisableIRQ 关闭指定的外设中断。
GIC_AcknowledgeIRQ 返回中断号。
GIC_DeactivateIRQ 无效化指定中断。
GIC_GetRunningPriority 获取当前正在运行的中断优先级。
GIC_SetPriorityGrouping 设置抢占优先级位数。
GIC_GetPriorityGrouping 获取抢占优先级位数。
GIC_SetPriority 设置指定中断的优先级。
GIC_GetPriority 获取指定中断的优先级。

移植好 core_ca7.h 以后,修改文件 imx6ul.h,在里面加上如下一行代码:

1
#include "core_ca7.h"

1.2 重新编写 start.S

完整的可以看这里:07_EXIT/project/start.S · sumumm/imx6ull-bare-demo - 码云 - 开源中国 (gitee.com),这里只写一部分重要的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/* 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,也就是GIC的CPU接口端基地址 */
ldr r0, [r1, #0XC] /* GIC的CPU接口端基地址加0X0C就是GICC_IAR寄存器,
* GICC_IAR寄存器保存这当前发生中断的中断号,我们要根据
* 这个中断号来绝对调用哪个中断服务函数
*/
push {r0, r1} /* 保存r0,r1 */

cps #0x13 /* 进入SVC模式,允许其他中断再次进去 */

push {lr} /* 保存SVC模式的lr寄存器 */
ldr r2, =system_irqhandler /* 加载C语言中断处理函数到r2寄存器中*/
blx r2 /* 运行C语言中断处理函数,带有一个参数,保存在R0寄存器中 */

pop {lr} /* 执行完C语言中断服务函数,lr出栈 */
cps #0x12 /* 进入IRQ模式 */
pop {r0, r1}
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 */

IRQ 中断服务函数主要的工作就是区分当前发生的什么中断,也就是获取发生中断的中断号。然后针对不同的外部中断做出不同的处理。

1.2.1 保存现场

1
2
3
4
5
push {lr}					/* 保存lr地址 */
push {r0-r3, r12} /* 保存r0-r3,r12寄存器 */

mrs r0, spsr /* 读取spsr寄存器 */
push {r0} /* 保存spsr寄存器 */

1.2.2 获取当前中断号

1
2
3
4
5
6
7
8
9
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,也就是GIC的CPU接口端基地址 */
ldr r0, [r1, #0XC] /* GIC的CPU接口端基地址加0X0C就是GICC_IAR寄存器,
* GICC_IAR寄存器保存这当前发生中断的中断号,我们要根据
* 这个中断号来绝对调用哪个中断服务函数
*/

经过这一步,中断号被保存到了 r0 寄存器中。

1.2.3 调用中断处理函数

1
2
3
4
5
6
7
push {r0, r1}				/* 保存r0,r1 */

cps #0x13 /* 进入SVC模式,允许其他中断再次进去 */

push {lr} /* 保存SVC模式的lr寄存器 */
ldr r2, =system_irqhandler /* 加载C语言中断处理函数到r2寄存器中*/
blx r2 /* 运行C语言中断处理函数,带有一个参数,保存在R0寄存器中 */

这里先讲R0和R1中的数据入栈,随后调用了函数 system_irqhandler,函数 system_irqhandler 是一个 C 语言函数,此函数有一个参数,这个参数就是中断号,所以我们需要传递一个参数。汇编中调用 C 函数如何实现参数传递呢?根据 ATPCS(ARM-Thumb Procedure Call Standard)定义的函数参数传递规则,在汇编调用 C 函数的时候建议形参不要超过 4 个, 形参可以由 r0~r3 这四个寄存器来传递,如果形参大于 4 个, 那么大于 4 个的部分要使用堆栈进行传递。 所以给 r0 寄存器写入中断号就可以了函数 system_irqhandler 的参数传递,前边已经向 r0 寄存器写入了中断号了。中断的真正处理过程其实是在函数 system_irqhandler 中完成,稍后需要编写函数 system_irqhandler。

1.2.4 中断处理完成

1
2
3
4
pop {lr}					/* 执行完C语言中断服务函数,lr出栈 */
cps #0x12 /* 进入IRQ模式 */
pop {r0, r1}
str r0, [r1, #0X10] /* 中断执行完成,写EOIR */

最后向 GICC_EOIR 寄存器写入刚刚处理完成的中断号, 当一个中断处理完成以后必须向 GICC_EOIR 寄存器写入其中断号表示中断处理完成。

1.2.5 恢复现场

1
2
3
4
5
pop {r0}						
msr spsr_cxsf, r0 /* 恢复spsr */

pop {r0-r3, r12} /* r0-r3,r12出栈 */
pop {lr} /* lr出栈 */

1.2.6 跳回原来的地方运行

1
subs pc, lr, #4				/* 将lr-4赋给pc */		

中断处理完成以后就要重新返回到曾经被中断打断的地方运行,这里为什么要将lr-4 然后赋给 pc 呢?而不是直接将 lr 赋值给 pc? ARM 的指令是三级流水线:取指、译指、执行, pc 指向的是正在取值的地址,这就是很多书上说的 pc=当前执行指令地址+8。比如下面代码示例:

1
2
3
0X2000 MOV R1, R0 ;执行
0X2004 MOV R2, R3 ;译指
0X2008 MOV R4, R5 ;取值 PC

上面示例代码中,左侧一列是地址,中间是指令,最右边是流水线。当前正在执行 0X2000地址处的指令“MOV R1, R0”,但是 PC 里面已经保存了 0X2008 地址处的指令“MOV R4, R5”。假设此时发生了中断,中断发生的时候保存在 lr 中的是 pc 的值,也就是地址 0X2008。当中断处理完成以后肯定需要回到被中断点接着执行,如果直接跳转到 lr 里面保存的地址处(0X2008)开始运行,那么就有一个指令没有执行,那就是地址 0X2004 处的指令“MOV R2, R3”,显然这是一个很严重的错误!所以就需要将 lr-4 赋值给 pc,也就是 pc=0X2004,从指令“MOV R2,R3”开始执行。

1.3 通用中断驱动文件

在 start.S 文件中我们在中断服务函数 IRQ_Handler 中调用了 C 函数 system_irqhandler 来处理具体的中断。此函数有一个参数,参数是中断号,但是函数 system_irqhandler 的具体内容还没有实现,所以需要实现函数 system_irqhandler 的具体内容。不同的中断源对应不同的中断处理函数, I.MX6U 有 160 个中断源,所以需要 160 个中断处理函数,我们可以将这些中断处理函数放到一个数组里面,中断处理函数在数组中的标号就是其对应的中断号。当中断发生以后函数 system_irqhandler 根据中断号从中断处理函数数组中找到对应的中断处理函数并执行即可。

在 bsp 目录下新建名为“int”的文件夹,在 bsp/int 文件夹里面创建 bsp_int.c 和 bsp_int.h 这两个文件。 07_EXIT/01_nxp_sdk_exit/bsp/int · sumumm/imx6ull-bare-demo - 码云 - 开源中国 (gitee.com)

image-20240118222334443

1.4 GPIO驱动文件

前面的实验中我们只是使用到了 GPIO 最基本的输入输出功能,本章我们需要使用GPIO 的中断功能。所以需要修改文件 GPIO 的驱动文件bsp_gpio.c 和 bsp_gpio.h,加上中断相关函数。07_EXIT/01_nxp_sdk_exit/bsp/gpio · sumumm/imx6ull-bare-demo - 码云 - 开源中国 (gitee.com)

1.5 按键中断驱动文件

本实验的目的是以中断的方式编写 KEY 按键驱动,当按下 KEY 以后触发 GPIO 中断,然后在中断服务函数里面控制蜂鸣器的开关。所以接下来是要编写按键 KEY 对应的UART1_CTS 这个 IO 的中断驱动,在 bsp 文件夹里面新建名为“exit”的文件夹,然后在 bsp/exit里面新建 bsp_exit.c 和 bsp_exit.h 两个文件。 可以看这里:07_EXIT/01_nxp_sdk_exit/bsp/exit · sumumm/imx6ull-bare-demo - 码云 - 开源中国 (gitee.com)

1.6 完整代码

可以看这里:07_EXIT · sumumm/imx6ull-bare-demo - 码云 - 开源中国 (gitee.com)

2. 自己写一个?

上面是要用到NXP提供的SDK的,为了加深理解,这里我们其实可以自己试下实现GIC的一些控制操作,这里参考的是韦东山裸机开发教程的相关章节。这里我没有调试,直接参考了下教程,学习一下思路而已。

2.1 GIC初始化

gic.c文件中gic_init 函数实现了如下功能:

① 通过 CP15 获取 GIC 的基地址;

② 读取 GICD_TYPER 寄存器获得中断的数目 ;

③ 往 GICD_ ICENABLERn 寄存器写入 0xFFFFFFFF 禁用所有的 SGI, PPI 和SPI;

④ 通过 GICC_PMR 设置优先级等级,设置为 0xF8;

⑤ 将 GICC_BPR 设置为 2,这允许各个优先级进行抢占;

⑥ 最后使能 group0 的 distributor 和 CPU interface。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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;
}

2.2 中断异常处理汇编部分

start.S 中对于中断的处理,概括如下:

  • 在异常向量表偏移为 0x18 的地方使用“ ldr pc, =IRQ_Handler”跳转;
  • IRQ_Handler 标号的处理可以简单分为:保存现场,执行 C 函数,恢复现场:

在 IRQ_Handler 标号,处理器处于中断模式,“ lr_irq - 4”就是被中断的、尚未执行的指令的地址,我们将 r0-r12 和“ lr-4”都保存在栈上。然后调用C 函数 handle_irq_c 来处理中断。C 函数返回来后,执行 ldmia sp!, {r0-r12, pc}^,这条指令做的事情可多了:

  • 把保存在栈上的值恢复到 r0-r12,把之前保存的“ lr_irq - 4”恢复到PC。
  • 把 SPSR 中保存的被中断状态的 CPSR,恢复到 CPSR(指令后的“ ^”号表示这个操作) 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
	
.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
Undefined_Handler:
/* 执行到这里之前:
* 1. lr_und保存有被中断模式中的下一条即将执行的指令的地址
* 2. SPSR_und保存有被中断模式的CPSR
* 3. CPSR中的M4-M0被设置为11011, 进入到und模式
* 4. 跳到0x4的地方执行程序
*/

/* 在und异常处理函数中有可能会修改r0-r12, 所以先保存 */
/* lr是异常处理完后的返回地址, 也要保存 */
stmdb sp!, {r0-r12, lr}

/* 保存现场 */
/* 处理und异常 */
mrs r0, cpsr
ldr r1, =und_string
bl printException

/* 恢复现场 */
ldmia sp!, {r0-r12, pc}^ /* ^会把spsr的值恢复到cpsr里 */

und_string:
.string "undefined instruction exception"

.align 2
SVC_Handler:
/* 执行到这里之前:
* 1. lr_svc保存有被中断模式中的下一条即将执行的指令的地址
* 2. SPSR_svc保存有被中断模式的CPSR
* 3. CPSR中的M4-M0被设置为10011, 进入到svc模式
* 4. 跳到0x08的地方执行程序
*/

/* 保存现场 */
/* 在swi异常处理函数中有可能会修改r0-r12, 所以先保存 */
/* lr是异常处理完后的返回地址, 也要保存 */
stmdb sp!, {r0-r12, lr}

mov r4, lr

/* 处理swi异常 */
mrs r0, cpsr
ldr r1, =swi_string
bl printException

sub r0, r4, #4
bl printSWIVal

/* 恢复现场 */
ldmia sp!, {r0-r12, pc}^ /* ^会把spsr的值恢复到cpsr里 */

swi_string:
.string "swi exception"

.align 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, Write to 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 */

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

注意

(1)执行 Reset_Handler 时, CPU 处于 IRQ 模式,用的是 IRQ 模式下的栈,需要先在 Reset_Handler 里设置好 IRQ 模式的栈,这样在中断模式里才可以使用栈,才能调用 C 函数。

(2)在 Reset_Handler 里调用“ cpsie i”打开中断,这是把 CPSR 中的 I 位清零。

(3)在 Reset_Handler 里使用如下两条指令设置异常向量的基地址,。

1
2
ldr r0, =_vector_table
mcr p15, 0, r0, c12, c0, 0 /* set VBAR, Vector Base Address Register*/

2.3 中断异常处理 C 函数部分

gic.c文件中handle_irq_c 函数功能简述如下:

① 获取到 gic 的基地址;

② 读取 GICC_IAR 获得中断号;

③ 根据中断号调用对应中断号的 irq_handler 函数,该函数是用户通过request_irq 注册的中断处理函数;

④ 然后往 GICC_EOIR 写入中断号清除掉中断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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?后面会有说明。

2.4 GPIO 中断初始化和注册中断处理程序

先看看main.c中初始化函数 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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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(main.c中),它的功能如下

① 读取 GPIO_DR 寄存器,根据 GPIO5_01 的状态打印信息、操作 LED ;

② 在 GPIO 模块内部清除中断 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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);
}

2.5 特定中断号的中断使能和禁止

设置好一切之后,就是使能中断了。对于GIC,程序里使用gic.c 中的 gic_enable_irq,它的功能为:

  • 根据中断号找到对应的 GICD_ISENABLERn 寄存器;
  • 往相应位中写入 1,即可使能中断。

要关闭中断时 ,操作是类似的 ,函数是 gic_disable_irq ,通过往GICD_ICENABLERn 对应的位写入 1 来禁止中断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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));
}

2.6 修改 CPSR 使能中断

在 start.S 中,可以看到如下代码,它把 CP15 中 SCTRL 的值读出后,把I bit 清零,再写入。这就是在 CPU 核中使能 IRQ 中断。

1
2
3
4
5
6
7
8
9
10
11
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, Write to CP15 System Control register */
/* ...... */

2.7 完整代码

完整代码看这里:07_EXIT/02_my_exit · sumumm/imx6ull-bare-demo - 码云 - 开源中国 (gitee.com)