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开发板上的按键,电路原理图如下:
按键 KEY0 是连接到 I.MX6U 的 UART1_CTS 这个 IO 上的 ,我们查一下这个IO是哪个,这里其实我没看懂,但是根据教程描述,这个引脚应该是这个:
这个引脚的默认功能是UART1_CTS_B,但是可以复用为GPIO1_IO18,这里就相当于接在了GPIO1_IO18上边。
二、IMX6ULL 的GPIO中断寄存器简介
1. GPIO interrupt configuration register1(GPIOx_ICR1)
GPIO 中断配置寄存器 1,用来配置 GPIO 中断 1~15 的触发类型。
位域 | 名 | 读写 | 描述 |
---|---|---|---|
[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 的触发类型。
与 GPIOx_ICR1 类似, ICR15 ~ ICR31 对应 GPIO interrupt 16 ~ 31。
3. GPIO interrupt mask register (GPIOx_IMR)
GPIO 中断屏蔽寄存器,用来屏蔽或使能某个 GPIO 中断。
位域 | 名称 | 读写 | 描述 |
---|---|---|---|
[n] | IMR | R/W | 每一位对应一个 GPIO 中断, 0:中断被屏蔽 1:中断使能,未被屏蔽 |
4. GPIO interrupt status register (GPIOx_ISR)
GPIO 中断状态寄存器,表示某个 GPIO 中断是否发生了。
位域 | 名称 | 读写 | 描述 |
---|---|---|---|
[n] | ISR | R/W | 每一位对应一个 GPIO 中断,跟 GPIO_IMR 无关,就是说即使屏 蔽了某个中断,还是可以在本寄存器中观察它的状态。 读: 0:中断未发生; 1:中断已发生。 写:某位写入 1 时,清零该位。 |
5. GPIO edge select register (GPIOx_EDGE_SEL)
GPIO 中断边沿选择寄存器,它可以用来覆盖 GPIOx_ICR1/2 中的配置值。
每一位对应一个 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管脚对应的中断号:
可以看到GPIO1的0 -15管脚使用的是66,16 - 31使用的是67,这里只是IRQ的编号,对应到 GIC 的 SPI中断号需要在此编号基础上加上 32,所以这里的按键中断号实际为99(67+32)。这个其实在我们之前移植的SDK包里边就有定义,我们打开 MCIMX6Y2.h 文件,有如下内容:
这里其实已经为我们定义好了中断号的枚举类型。上边我们知道多个GPIO引脚都会产生这个中断号,所以我们需要注意:当发生 GIC 99 号中断时,表示发生了 GPIO1 中 interrupt 0 ~15,然后需要进一步细分出是 GPIO1 里的哪一个中断。
3. GIC 控制器基地址的获取方法
直接查数据手册 《i.MX 6ULL Applications ProcessorReference Manual 》的Table 2-1. System memory map,可以知道 gic 的基地址是 0xA0000,如下图:
对于 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 |
1.2 重新编写 start.S
完整的可以看这里:07_EXIT/project/start.S · sumumm/imx6ull-bare-demo - 码云 - 开源中国 (gitee.com),这里只写一部分重要的内容:
1 | /* IRQ中断!重点!!!!! */ |
IRQ 中断服务函数主要的工作就是区分当前发生的什么中断,也就是获取发生中断的中断号。然后针对不同的外部中断做出不同的处理。
1.2.1 保存现场
1 | push {lr} /* 保存lr地址 */ |
1.2.2 获取当前中断号
1 | mrc p15, 4, r1, c15, c0, 0 /* 从CP15的C0寄存器内的值到R1寄存器中 |
经过这一步,中断号被保存到了 r0 寄存器中。
1.2.3 调用中断处理函数
1 | push {r0, r1} /* 保存r0,r1 */ |
这里先讲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 | pop {lr} /* 执行完C语言中断服务函数,lr出栈 */ |
最后向 GICC_EOIR 寄存器写入刚刚处理完成的中断号, 当一个中断处理完成以后必须向 GICC_EOIR 寄存器写入其中断号表示中断处理完成。
1.2.5 恢复现场
1 | pop {r0} |
1.2.6 跳回原来的地方运行
1 | subs pc, lr, #4 /* 将lr-4赋给pc */ |
中断处理完成以后就要重新返回到曾经被中断打断的地方运行,这里为什么要将lr-4 然后赋给 pc 呢?而不是直接将 lr 赋值给 pc? ARM 的指令是三级流水线:取指、译指、执行, pc 指向的是正在取值的地址,这就是很多书上说的 pc=当前执行指令地址+8。比如下面代码示例:
1 | 0X2000 MOV R1, R0 ;执行 |
上面示例代码中,左侧一列是地址,中间是指令,最右边是流水线。当前正在执行 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):
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 | void gic_init(void) |
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 |
|
注意:
(1)执行 Reset_Handler 时, CPU 处于 IRQ 模式,用的是 IRQ 模式下的栈,需要先在 Reset_Handler 里设置好 IRQ 模式的栈,这样在中断模式里才可以使用栈,才能调用 C 函数。
(2)在 Reset_Handler 里调用“ cpsie i”打开中断,这是把 CPSR 中的 I 位清零。
(3)在 Reset_Handler 里使用如下两条指令设置异常向量的基地址,。
1 | ldr r0, =_vector_table |
2.3 中断异常处理 C 函数部分
gic.c文件中handle_irq_c 函数功能简述如下:
① 获取到 gic 的基地址;
② 读取 GICC_IAR 获得中断号;
③ 根据中断号调用对应中断号的 irq_handler 函数,该函数是用户通过request_irq 注册的中断处理函数;
④ 然后往 GICC_EOIR 写入中断号清除掉中断。
1 | void handle_irq_c(void) |
谁调用 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 | void key_irq_init(void) |
还是以 KEY1 为例说明处理函数 key_gpio5_handle_irq(main.c中),它的功能如下
① 读取 GPIO_DR 寄存器,根据 GPIO5_01 的状态打印信息、操作 LED ;
② 在 GPIO 模块内部清除中断 。
1 | void key_gpio5_handle_irq(void) |
2.5 特定中断号的中断使能和禁止
设置好一切之后,就是使能中断了。对于GIC,程序里使用gic.c 中的 gic_enable_irq,它的功能为:
- 根据中断号找到对应的 GICD_ISENABLERn 寄存器;
- 往相应位中写入 1,即可使能中断。
要关闭中断时 ,操作是类似的 ,函数是 gic_disable_irq ,通过往GICD_ICENABLERn 对应的位写入 1 来禁止中断。
1 | void gic_enable_irq(IRQn_Type nr) |
2.6 修改 CPSR 使能中断
在 start.S 中,可以看到如下代码,它把 CP15 中 SCTRL 的值读出后,把I bit 清零,再写入。这就是在 CPU 核中使能 IRQ 中断。
1 | Reset_Handler: |
2.7 完整代码
完整代码看这里:07_EXIT/02_my_exit · sumumm/imx6ull-bare-demo - 码云 - 开源中国 (gitee.com)