1)实验平台:正点原子STM32MP157开发板 2)购买链接:https://item.taobao.com/item.htm?&id=629270721801 3)全套实验源码 手册 视频下载地址:http://www.openedv.com/thread-318813-1-1.html 4)正点原子官方B站:https://space.bilibili.com/394620890 5)正点原子STM32MP157技术交流群:691905614
第十三章 外部中断实验
在前几章的学习中,我们掌握了STM32MP1的IO在上一章的实验中,我们通过扫描按钮来检测按钮的状态。在本章中,我们将通过中断来检测按钮的状态。通过学习本章,我们可以掌握它STM32MP157的IO口腔被用作外部中断输入的方法。 本章将分为以下几个部分: 13.1、STM32MP157中断控制器; 12.二、硬件设计; 12.三、软件设计; 12.四、章节总结; 13.1 STM32MP157中断控制器 13.1.1 中断的概念 为了方便以下原理部分的解释,我们先解释几个重要概念。
- 什么是中断 对于单片机,CPU另一件事发生在执行事件A时B,B比A更紧急,所以B要求CPU优先处理。CPU收到请求后,首先暂停执行事件A,转而执行事件B。事件B执行后,CPU返回之前暂停的事件A的地方继续执行。 事件B在这里被称为中断源。中断源向CPU提出处理的请求称为中断请求。中断时被打断的暂停点称为断点。CPU暂停执行事件A的过程称为中断响应。中断源处理程序称为中断处理程序。CPU执行中断处理程序的过程称为中断处理。返回断点的过程称为中断返回。如下所示,整个过程也可称为中断嵌套:
图13.1.1. 1中断嵌套 2. 中断分类 按不同的分类方法可分为中断: ①可分为硬件中断和软件中断。硬件中断可以是CPU以外的I/O设备中断,每个外设都有自己的IRQ(中断请求),CPU相应的IRQ分发到相应的硬件驱动器上。软件中断是一个CPU指令,用来陷入中断。 ②硬件中断可分为可屏蔽中断和非可屏蔽中断。可屏蔽是CPU可响应中断或不响应中断。非屏蔽是指CPU必须无条件响应,不能通过设置中断屏蔽寄存器来关闭。 ③中断也可分为内部中断和外部中断。外部中断是指原因CPU由算法指令或地址越界引起的外部信号触发的中断、内部中断,也称为软件中断或系统异常中断。 一般参考手册会给出具体的中断属于哪一类。下面将解释中断优先级和中断向量。 13.1.2 NVIC简介 STM32MP1系列是2个Cortex-A7内核和1个Cortex-M四核的组合,属于多核异构,Cortex-A内核中断管理机构称为GIC(general interrupt controller),即通用中断控制器。Cortex-M内核中断管理机构称为NVIC(Nested Vectored Interrupt Controller),即嵌套向量中断控制器。关于Cortex-A我们将中断内核A在这里,我们将重点介绍相关的例程M4内核的NVIC。
- NVIC简介 NVIC即嵌套向量中断控制器Cortex-M内核器件用于管理内核的所有中断和事件,包括中断的能量使用和能量去除、中断的优先级等。因为它属于内核器件,所以更多关于它的描述可以看到内核的相关信息,比如ARM的《Cortex?-M4 Devices Generic User Guide》。 M3/M4/M7内核支持256个中断,包括16个系统中断和240个外部中断,以及256个可编程中断设置。然而,芯片制造商通常不会耗尽所有核心资源,如STM32MP157系统中断10个,外部中断150个。下图为参考手册中截取的中断映射表(也称中断向量表)EXTI event(1)列表中有括号表示EXTI未连接到NVIC,EXTI配置不会影响中断线的状态,如(18),带括号的表示EXTI配置可能会影响中断线的状态,即EXTI中断可能会被屏蔽。
图13.1.2. 参考手册中的中断向量汇总表 我们在分析startup_stm32mp15xx.s启动文件时有解释M4核心的中断向量表位于RETRAM(64kB)地址从0x从一万万开始,详见第六次.3.5小节。如下图所示,启动文件Cortex M4最小中断向量表:
图13.1.2. 启动文件中最小中断向量表 中断向量用于存储中断服务程序的第一个地址,也称为中断服务程序的入口地址,CPU为了让中断号获得中断向量值CPU通过中断号找到相应的中断向量,需要在内存中建立一个查询表,由一系列中断服务程序的入口地址组成,也称为中断向量表。在上图参考手册中的中断向量表中: ①priority 一列表示中断优先级,中断优先级按从高到低的顺序排序。值越小,中断优先级越高; ②Type of priority指固定不变的优先级类型(Fixed),还是可编程的(Settable),这里有特别的Reset、NMI、HardFault和MemManage,其中断优先级不能改变,优先级为负,高于普通中断优先级。 ③Acronym一列中断名称; ④Description说明中断; ⑤Address表示中断的地址,CPU从这个地址了解中断位置在哪里; ⑥最右边是EXTI,即外部中断/事件,是NVIC其中一个。其中包含括号的括号表示EXTI未连接输出NVIC,没有括号括起的表示EXTI输出已连接NVIC,并非所有中断/事件都列在表中。我们稍后将单独分析EXTI。 150个外部中断部分STM32MP157参考手册有详细列表,这里不全部截图。我们来看看中断号。 在采用向量中断方式的中断系统中,中断号是系统分配给每个中断源的代码,CPU中断服务程序的入口地址必须通过中断号找到,从而将程序转移到中断服务程序。STM32MP1xx的中断号在stm32mp157dxx_cm4.h文件中有一个定义,如下图所示,第一列是中断号的名称,与中断向量表的名称相对应,第二列是相应的中断号。关于中断号和中断向量表以及中断服务函数的对应关系,我们后面会详细分析。
图13.1.2. 3中断号定义 2. NVIC寄存器 NVIC可以定义相关寄存器core_cm4.h我们通过程序的定义直接分析文件NVIC定义如下: core_cm4.h文件代码
typedef struct {
__IOM uint32_t ISER[8U]; /* 中断使能寄存器 */ uint32_t RESERVED0[24U];
__IOM uint32_t ICER[8U]; /* 中断除能寄存器 */
uint32_t RSERVED1[24U];
__IOM uint32_t ISPR[8U]; /* 中断使能挂起寄存器 */
uint32_t RESERVED2[24U];
__IOM uint32_t ICPR[8U]; /* 中断解挂寄存器 */
uint32_t RESERVED3[24U];
__IOM uint32_t IABR[8U]; /* 中断有效位寄存器 */
uint32_t RESERVED4[56U];
__IOM uint8_t IP[240U]; /* 中断优先级寄存器(8位宽) */
uint32_t RESERVED5[644U];
__OM uint32_t STIR; /*软件触发中断寄存器 */
} NVIC_Type;
STM32MP157的中断是在这些寄存器的控制下有序的执行的。只有了解这些中断寄存器,才能方便的使用STM32MP157的中断。下面我们重点介绍这几个寄存器: ISER[8] ISER全称是:Interrupt Set Enable Registers,这是一个中断使能寄存器组。 上面说了CM4内核支持256个中断,这里用8个32位寄存器来控制,每个位控制一个中断。但是STM32MP157的可屏蔽中断最多只有150个,所以对我们来说,有用的就是4个(ISER[0~4]]),总共可以表示160个中断,而STM32MP157只用了其中的150个。 ISER[0]设置031号中断的使能,ISER[1]设置3263号中断的使能,其他以此类推,这样总共150个中断就可以分别对应上了。如果要使能某个中断,必须设置相应的ISER位为1,使该中断被使能(这里仅仅是使能,还要配合中断分组、屏蔽、IO口映射等设置才算是一个完整的中断设置)。具体每一位对应哪个中断,请参考stm32mp157dxx_cm4.h里面的第70行到220行,共150个。 该寄存器对应的位写1表示使能位所对应的中断,写0表示无效。 ICER[8] ICER全称是:Interrupt Clear Enable Registers,是一个中断除能寄存器组。该寄存器组与ISER的作用恰好相反,是用来清除某个中断的使能的。ICER寄存器的位对应的中断也和ISER寄存器的位对应的中断一样,ICER[0]设置031号中断除能,ICER[1]设置3263号中断的使能,以此类推。这里要专门设置一个ICER来清除中断位,而不是向ISER写0来清除,是因为NVIC的这些寄存器都是写1有效的,写0是无效的。 ISPR[8] ISPR全称是:Interrupt Set Pending Registers,是一个中断使能挂起控制寄存器组。每个位对应的中断和ISER是一样的。通过置1,可以将正在进行的中断挂起,转而执行同级或更高级别的中断。写1改变中断状态为挂起,写0是无效的。 ICPR[8] ICPR全称是:Interrupt Clear Pending Registers,是一个中断解挂控制寄存器组。其作用与ISPR相反,每个位对应的中断和ISER是一样的。通过设置1,可以将挂起的中断解挂。写0无效。 IABR[8] IABR全称是:Interrupt Active Bit Registers,是一个中断激活标志位寄存器组。对应位所代表的中断和ISER一样,如果为1,则表示该位所对应的中断正在被执行。这是一个只读寄存器,通过读取它可以知道当前在执行的中断是哪一个。在中断执行完了以后由硬件自动清零。 IP[240] IP全称是:Interrupt Priority Registers,是一个中断优先级控制寄存器组。这个寄存器组相当重要!STM32MP157的中断分组与这个寄存器组密切相关。 IP寄存器组由240个8bit的寄存器组成,每个寄存器对应一个中断优先级,每个可屏蔽中断占用8bit,这样总共可以表示240个可屏蔽中断。 实际上STM32MP157只用到了其中的150个。IP[149]IP[0]分别对应中断1490。每个可屏蔽中断占用的8bit并没有全部使用,而是只用了高4位。这4位,又分为抢占优先级和子优先级,抢占优先级在前,子优先级在后。而这两个优先级各占几个位又要根据SCB->AIRCR中的中断分组设置来决定。关于中断优先级控制的寄存器组我们下面再详细讲解。 STIR STIR全称是:Software Trigger Interrupt Register,是软件触发中断寄存器,0~8位表示软件生成的中断编号,写入STIR以生成软件生成的中断(Software Generated Interrupt:SGl)。要写入的值是所需的中断ID。 SGI,范围为0-239(刚好240个)。例如,值Ob000000011指定中断IRQ3。 3. 中断优先级 如果有多个中断一起发生,那么STM32该如何处理中断呢?我们先来了解中断源的优先级分组的概念,ARM的中断源优先级分两种:抢占优先级(Preemption Priority)和响应优先级(Sub Priority),响应优先级也称子优先级,每个中断源都需要被指定这两种优先级。 在NVIC中,由寄存器NVIC_IPR0NVIC_IPR59共60个寄存器控制中断优先级,每个寄存器32位,每8位又分为一组,一个寄存器可以分4组,所以就有了240(4*60)组宽度为8bit的中断优先级控制寄存器。原则上每个外部中断可配置的优先级为0255,数值越小,优先级越高。但是实际上M3 /M4 /M7 芯片为了精简设计,只使用了高四位[7:4],低四位取零,这样以至于最多只有16级中断嵌套,即2^4=16。 对于NVIC的中断优先级分组:STM32MP157将中断分为5个组,分别为组0~4。在stm32mp1xx_hal_cortex.h文件中有定义。 stm32mp1xx_hal_cortex.h文件代码 /* 0位用于抢占优先级,4位响应优先级 */
#define NVIC_PRIORITYGROUP_0 ((uint32_t)0x00000007)
/* 1位抢占优先级,3位响应优先级 */
#define NVIC_PRIORITYGROUP_1 ((uint32_t)0x00000006)
/* 2位抢占优先级,2位响应优先级 */
#define NVIC_PRIORITYGROUP_2 ((uint32_t)0x00000005)
/* 3位抢占优先级,1位响应优先级 */
#define NVIC_PRIORITYGROUP_3 ((uint32_t)0x00000004)
/* 4位抢占优先级,0位响应优先级 */
#define NVIC_PRIORITYGROUP_4 ((uint32_t)0x00000003)
该分组的设置是由SCB->AIRCR寄存器的bit10~8来定义的。具体的分配关系如下表所示: 优先级分组 AIRCR[10:8] bit[7:4]分配情况 分配结果 0 111 0:4 0位抢占优先级,4位响应优先级 1 110 1:3 1位抢占优先级,3位响应优先级 2 101 2:2 2位抢占优先级,2位响应优先级 3 100 3:1 3位抢占优先级,1位响应优先级 4 011 4:0 4位抢占优先级,0位响应优先级 表13.1.2. 1 AIRCR中断分组设置表 通过这个表,我们就可以清楚的看到组04对应的配置关系,例如优先级分组设置为3,那么此时所有的150个中断,每个中断的中断优先寄存器的高四位中的最高3位是抢占优先级,低1位是响应优先级,抢占优先级共有23=8种,子优先级共有21=2种,共有8*2=16级嵌套,每个中断,你可以设置抢占优先级为07,响应优先级为1或0。 抢占优先级的级别高于响应优先级。而数值越小所代表的优先级就越高。优先级编号越小其优先级越高,抢占式优先级和响应优先级对中断控制遵循的原则: ①抢占优先级高的中断可以打断正在执行的抢占优先级低的中断。 ②抢占优先级相同,响应优先级高的中断不能打断响应优先级低的中断。 即两个中断抢占优先级相同,响应优先级低的中断正在执行中,此时来了响应优先级高的中断,它不能打断响应优先级低的中断。 ③当抢占优先级相同时,如果响应优先级高的中断和响应优先级低的中断同时发生,响应优先级高的中断程序先于响应优先级低的中断程序被执行。 ④当两个或者多个中断的抢占式优先级和响应优先级相同时,如果他们同时发生,那么就遵循自然优先级,看中断事件向量表的中断排序,数值越小的优先级越高。 ⑤如果两个中断事件的抢占优先级和响应优先级都相同,先发生的中断事件就先被处理。 ⑥系统中断,如PendSV、SVCall、UsageFault等或者内核外设Systick的中断是不是就比外部的中断要高?这个是不一定的,所有的中断都是在NVIC下面设置的优先级,根据他们的抢占优先级和子优先级来。 结合实例说明一下:假定设置中断优先级分组为2,然后设置: 中断3(RTC_WKUP)的抢占优先级为2,响应优先级为1; 中断6(外部中断0)的抢占优先级为3,响应优先级为0; 中断7(外部中断1)的抢占优先级为2,响应优先级为0。 这三个中断源同时申请中断,那么这3个中断的优先级顺序为:中断7>中断3>中断6。 上面例子中的中断3和中断7都可以打断中断6的中断。而中断7和中断3却不可以相互打断! 4. NVIC相关函数 在core_cm4.h文件中有如下定义,这些函数将被stm32mp1xx_hal_cortex.c文件中的NVIC函数调用。 core_cm4.h文件代码
/* 设置优先级分组 */
#define NVIC_SetPriorityGrouping __NVIC_SetPriorityGrouping
/* 获取优先分组 */
#define NVIC_GetPriorityGrouping __NVIC_GetPriorityGrouping
/* 启用中断 */
#define NVIC_EnableIRQ __NVIC_EnableIRQ
/* 获取中断启用状态 */
#define NVIC_GetEnableIRQ __NVIC_GetEnableIRQ
/* 禁用中断 */
#define NVIC_DisableIRQ __NVIC_DisableIRQ
/* 获取待处理的中断 */
#define NVIC_GetPendingIRQ __NVIC_GetPendingIRQ
/* 设置待处理中断 */
#define NVIC_SetPendingIRQ __NVIC_SetPendingIRQ
/* 清除待处理中断 */
#define NVIC_ClearPendingIRQ __NVIC_ClearPendingIRQ
/* 获取活动中的中断 */
#define NVIC_GetActive __NVIC_GetActive
/* 设置中断优先级 */
#define NVIC_SetPriority __NVIC_SetPriority
/* 获取中断优先级 */
#define NVIC_GetPriority __NVIC_GetPriority
/* 系统重置 */
#define NVIC_SystemReset __NVIC_SystemReset
我们来看stm32mp1xx_hal_cortex.c文件定义的NVIC函数。下面列出我们较为常用的函数,想了解更多其他的函数请自行查阅。 (1)HAL_NVIC_SetPriorityGrouping函数 函数功能:用于设置中断优先级分组(通过操作AIRCR寄存器来实现)。 函数形参: 形参是中断优先级分组号,可以选择范围:NVIC_PRIORITYGROUP_0到NVIC_PRIORITYGROUP_4(共5组),也就是我们上面提到的AIRCR中断分组设置表中的分组。 函数返回值:无 注意事项: 这个函数在一个工程里基本只调用一次,而且是在程序HAL库初始化函数里面已经被调用,后续就不会再调用了。因为当后续调用设置成不同的中断优先级分组时,有可能造成前面设置好的抢占优先级和响应优先级不匹配。 void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup) { /* 检测参数 */ assert_param(IS_NVIC_PRIORITY_GROUP(PriorityGroup));
/* 根据参数值设置PRIGROUP[10:8]位(设定优先级分组)*/ NVIC_SetPriorityGrouping(PriorityGroup); } (2)HAL_NVIC_SetPriority函数 函数描述:用于设置中断的抢占优先级和响应优先级(通过操作IP和SHP寄存器来实现)。 函数形参: 形参1是中断号,用于指定中断源,可以选择范围:IRQn_Type定义的枚举类型,定义在stm32mp157dxx_cm4.h文件中,前面给出的中断号定义截图。 形参2是抢占优先级,可以选择范围:0到15。 形参3是响应优先级,可以选择范围:0到15。 函数返回值:无 void HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority) { uint32_t prioritygroup = 0x00;
/* 检测参数 / assert_param(IS_NVIC_SUB_PRIORITY(SubPriority)); assert_param(IS_NVIC_PREEMPTION_PRIORITY(PreemptPriority)); / 获取中断优先级组 / prioritygroup = NVIC_GetPriorityGrouping(); / 设置优先级 / NVIC_SetPriority(IRQn, NVIC_EncodePriority(prioritygroup, PreemptPriority, SubPriority)); } (3)HAL_NVIC_EnableIRQ函数 函数描述:用于使能中断(通过操作ISER 寄存器来实现)。 函数形参:IRQn是中断号,可以选择范围:IRQn_Type定义的枚举类型,定义在stm32mp157dxx_cm4.h 函数返回值:无 void HAL_NVIC_EnableIRQ(IRQn_Type IRQn) { / 检查参数*/ assert_param(IS_NVIC_DEVICE_IRQ(IRQn));
/* 使能中断 / NVIC_EnableIRQ(IRQn); } (4)HAL_NVIC_DisableIRQ函数 函数描述:用于中断除能(通过操作ICER 寄存器来实现)。 函数形参:无形参 函数返回值:无 void HAL_NVIC_DisableIRQ(IRQn_Type IRQn) { / 检查参数*/ assert_param(IS_NVIC_DEVICE_IRQ(IRQn));
/* 禁用中断 / NVIC_DisableIRQ(IRQn); } (5)HAL_NVIC_SystemReset函数 函数描述:用于软件复位系统(通过操作AIRCR寄存器来实现)。 函数形参:无形参 函数返回值:无 其他的NVIC函数用得较少,我们就不一一列出来了。NVIC的介绍就到这,下面介绍外部中断。 void HAL_NVIC_SystemReset(void) { / 系统重置 */ NVIC_SystemReset(); } NVIC相关的函数我们就介绍到这里,下面我们来了解和GPIO密切相关的EXTI。 13.1.3 EXTI简介
- EXTI框图分析 EXTI是ST公司在其STM32产品上扩展的外中断控制,EXTI(Extended interrupt and event controller),即外部中断/事件控制器,这里包含两个部分,一个是中断,另一个是事件。我们前面阐述中断的概念时说的事件A和事件B就是两个事件,事件可以分为中断事件和非中断事件,能引起发生中断的事件我们叫做中断事件。我们先大概了解EXTI的架构,如下图是EXTI功能框图,EXTI主要由以下4个部分组成: 寄存器模块(Registers) EXTI多路复用器模块(EXTI mux) 事件输入触发模块(Event Trigger) 屏蔽模块(Masking) 其中: ①寄存器模块包含所有EXTI寄存器,通过AHB接口可以访问寄存器模块; ②EXTI多路复用器在EXTI事件的信号上提供IO端口选择,可以通过配置EXTI_EXTICR1EXTI_EXTICR4寄存器来选择对应的IO口,这些IO口可以作为外部中断的输入源。从图中看出,EXTI多路复用器模块可以输出015个可配置事件,我们下文会分析这16个可配置事件“Configurable event”是怎么和GPIO相连以及寄存器怎么配置; ③事件输入触发模块提供事件输入边沿触发逻辑,此逻辑可以是上升沿触发、下降沿触发或者双边沿触发; ④屏蔽模块为不同的唤醒、中断和事件输出及其屏蔽功能提供事件分配。 框图中最左边,输入事件分为两类,一个是可配置事件(来自I / O或能够产生脉冲的外设的信号),一个是直接事件“Direct event”(其它外设的中断和唤醒源,需要在外设中将其清除)。它们的功能和特点如下: 可配置事件:①具有可选的活动触发沿;②中断挂起状态寄存器位与上升沿和下降沿无关;③单独的中断和事件生成掩码,用于调节CPU唤醒、中断和事件生成;④软件触发的可能性。 直接事件:①具有固定的上升沿活动触发器;②EXTI中没有中断等待状态寄存器位(产生事件的外设提供了中断等待状态标志);③单独的中断和事件生成掩码,用于调节CPU唤醒和事件生成;④没有软件触发的可能性。
图13.1.3. 1 EXTI框图 由上面的框图我们可以知道,EXTI负责管理映射到GPIO引脚上的外中断和其它片上外设的中断以及软件中断,EXTI的输出,一个用于唤醒PWR,另外的输出最终被映射到内核的NVIC的相应通道上。 2. GPIO和中断线的映射关系 EXTI支持 76个输入中断/事件请求,包括可配置事件和直接事件。每个中断设有状态位,每个中断/事件都有独立的屏蔽设置,其中有21个可配置的输入事件,48个直接事件,其它的事件是保留的。如下,EXTI线在stm32mp1xx_hal_exti.h文件中有定义: stm32mp1xx_hal_exti.h文件代码
#define EXTI_LINE_0 (EXTI_GPIO | EXTI_EVENT | EXTI_REG1 | 0x00u)
#define EXTI_LINE_1 (EXTI_GPIO | EXTI_EVENT | EXTI_REG1 | 0x01u)
#define EXTI_LINE_2 (EXTI_GPIO | EXTI_EVENT | EXTI_REG1 | 0x02u)
#define EXTI_LINE_3 (EXTI_GPIO | EXTI_EVENT | EXTI_REG1 | 0x03u)
/* 此处省略部分代码 */
#define EXTI_LINE_72 (EXTI_DIRECT | EXTI_REG3 | 0x08u)
#define EXTI_LINE_73 (EXTI_CONFIG | EXTI_REG3 | 0x09u)
#define EXTI_LINE_74 (EXTI_RESERVED | EXTI_REG3 | 0x0Au)
#define EXTI_LINE_75 (EXTI_DIRECT | EXTI_REG3 | 0x0Bu)
EXTI多路复用器可以输出015个可配置事件到事件输入触发模块,这015个事件对应外部IO口的输入中断,标号分别为EXTI[0]~ EXTI[15],共16个外部中断线。如下图是STM32MP157 EXTI事件汇总表格,只截图了表格的一部分,详细的表格信息可以在参考手册中查阅。EXTI[0]~ EXTI[15]可配置,唤醒目标为MPU或者MCU。
图13.1.3. 2 STM32MP157 EXTI事件汇总表 上面的16个中断线每次只能连接到1个IO口上,即STM32MP157供IO口使用的中断线只有16个,但是STM32MP157的IO口却远远不止16个,那么STM32MP157是怎么把16个中断线和IO口一一对应起来的呢?于是STM32就这样设计:GPIO的引脚Px0 ~ Px15(x=A,B,C,D,E,F,G,H,I,J,K,Z)分别映射到了中断线0~15。这样每个中断线对应了最多12个IO口。 以EXTI0线为例,GPIOx.0映射到了EXTI0,即PA0、PB0、PC0、PD0、PE0、PF0、PG0、PH0、PI0、PJ0、PK0、PZ0映射到了EXTI0上。对应关系如下图:
图13.1.3. 3 GPIO和中断线映射关系图 GPIO和中断线的映射关系如下表:
表13.1.3. 1 GPIO和中断线映射关系表 3. EXTI相关寄存器 (1)EXTI外部中断选择寄存器 上面我们了解了GPIO和中断线的映射关系,它是由EXTI外部中断选择寄存器控制的,寄存器是EXTI_EXTICR1~ EXTI_EXTICR4。我们来看看这4个寄存器怎么配置。
图13.1.3. 4 EXTICR1寄存器 EXTI_EXTICR1是EXTI外部中断选择寄存器1,属于32位可读可写寄存器,每8位控制一个16个pin:第07位配置对应的是EXTI0,这些位由软件写入,以选择EXTI0外部中断的输入源。例如,写入0x00表示选择PA0作为EXTI0的外部中断输入源,写0x01表示将PB0作为EXTI0外部中断输入源,依此类推。第8第15位对应的是EXTI1,写入0x00表示选择PA1作为EXTI1的外部中断输入源,写入0x01表示将PB1作为EXTI1外部中断输入源,依此类推。剩下的EXTI_EXTICR2~ EXTI_EXTICR4寄存器的配置方法也类似。即GPIO数字编号相同的引脚(PA0、PB0、PC0……PI0、PZ0)共享一个中断源,和我们上面的GPIO和中断线的映射关系图对应。 这里要注意: ①每个 GPIO 都可以配置成外部中断/事件模式,要选择引脚与 16 个外部中断/事件 EXTI0~EXTI15 中的某根线导通,需要根据寄存器EXTI_EXTICR1~ EXTI_EXTICR4来配置。不过,在HAL库中的HAL_GPIO_Init函数已经为我们做好了这些操作,我们只需要指定对应的引脚就可以将IO端口注册至中断线(将IO口映射到中断线N)。 ③STM32的外部中断不是固定的,是可以映射的,如EXTI0既可以映射到PA0也可以映射到PB0上。如果某根外部中断/事件线配置了多个IO口,例如EXTI0配置了多个引脚作为外部中断输入源,如配置了PA0、PB0和PC0,那么同一个时刻只能有一个可以和EXTI0导通,即如果用了PA0就不能用PB0和PC0,用了PC0就不能用PB0和PA0。 (2)EXTI中断挂起标志寄存器 EXTI上升沿挂起寄存器有:RPR1、RPR2和RPR3,EXTI下降沿挂起寄存器有FPR1、FPR2和FPR3。我们只需要先了解RPR1和FPR1,其它寄存器基本类似。 RPR1是EXTI上升沿挂起寄存器,RPR1仅包含可配置事件的寄存器位,仅低17位(RPIF0~RPIF16)可用,其它位保留,rc_w1 表示此寄存器可读、可清除、可写(写1有效,写0无效)。 如果读取此寄存器某位为0,表示对应的引脚没有发生上升沿触发请求,如果读取此寄存器某位为1,表示对应引脚发生上升沿触发请求。当上升沿事件或EXTI_SWIER软件触发到达可配置事件线时,该位置1。可以通过软件程序将1写入该位来清除该位,写0无影响。
图13.1.3. 5 RPR1寄存器 FPR1是EXTI下降沿挂起寄存器,和RPR1类似是低17位可用。读取某位为0,表示未发生下降沿触发请求,读取某位为1,表示发生上升沿触发请求。当下降沿事件到达可配置事件行时,该位置1。同样的,可通过将1写入该位来清除该位。
图13.1.3. 6 FPR1寄存器 以上的寄存器要注意,中断标志位不会自动清除,在HAL库中,GPIO外部中断处理函数是通过对应位写1来清除中断标识位的,中断的触发是通过读取标志位来进行的。如果中断标志位不清除,那么完成中断处理程序后,程序还是会继续进入中断无法退出,返回不到主函数。后面的实验中要注意这点。 13.2 硬件设计
- 例程功能 通过外部中断的方式让开发板上的三个独立按键控制LED灯和蜂鸣器翻转:KEY0控制LED0翻转,KEY1控制LED1翻转,WKUP控制BEEP翻转。
- 硬件资源
表13.2. 1硬件资源表 3. 原理图 LED和BEEP的原理图我们前面已经涉及,独立按键硬件部分的原理图如下图所示:
图13.2. 1独立按键与STM32MP157连接原理图 这里需要注意的是:按键KEY0和KEY1是低电平有效的,而按键WK_UP是高电平有效的,并且按键KEY0和KEY2接了一个10k的上拉电阻,而按键WK_UP外部没有上下拉电阻。芯片内部有30~50K欧之间上下拉电阻,上电复位后,GPIO端口上拉下拉寄存器PUPDR的复位值为0x0000 0000,表示芯片内部上下拉电阻都不开启,引脚处于浮空模式中,浮空模式下,引脚的电平是不可确定的,那么按键按下以后可能存在引脚电平还是没变化的情况,程序测试结果就不准确,所以,按键WK_UP所对应的引脚一定要开启芯片内部下拉,我们后面的工程配置中会强调这步操作。 13.3 软件设计 本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\11、M4 CubeIDE裸机驱动例程\CubeIDE_project\ 6 EXTI。 13.3.1 程序设计
- EXTI的HAL库驱动 (1)GPIO的EXTI初始化功能 前面我们在讲解HAL_GPIO_Init函数的时候有提到过:HAL库的EXTI外部中断的设置功能是整合到HAL_GPIO_Init函数里面的,而不是单独独立一个文件。所以我们的外部中断的初始化函数也是用HAL_GPIO_Init函数来实现。这里就不分析HAL_GPIO_Init函数了,感兴趣的小伙伴可以自行分析。 (2)GPIO的模式设置 既然是要用到外部中断,所以我们的GPIO的模式要从下面的三个模式中选中一个:
stm32mp1xx_hal_gpio.h文件代码
/* 外部中断,上升沿触发检测 */
#define GPIO_MODE_IT_RISING (uint32_t)0x10110000U)
/* 外部中断,下降沿触发检测 */
#define GPIO_MODE_IT_FALLING ((uint32_t)0x10210000U)
/* 外部中断,上升和下降双沿触发检测 */
#define GPIO_MODE_IT_RISING_FALLING ((uint32_t)0x10310000U)
KEY0和KEY1是低电平有效的,所以我们要选择下降沿触发检测,而KEYUP是高电平有效的,那么就应该选择上升沿触发检测。 2. 外部中断配置步骤 (1)使能IO口时钟 首先,我们要使用IO口作为中断输入,所以我们要使能相应的IO口时钟。 (2)设置IO口模式,中断触发条件,设置IO口与中断线的映射关系 在HAL库中,这部分已经在函数HAL_GPIO_Init中一次性完成了。 (3)配置中断优先级(NVIC),并使能中断 我们设置好中断线和GPIO映射,然后又设置好了中断的触发模式等初始化参数。既然是外部中断,涉及到中断我们当然还要设置NVIC中断优先级。 (4)编写中断服务函数 我们配置完中断优先级之后,接着要做的就是编写中断服务函数,可以在STM32CubeID生成的stm32mp1xx_it.c文件中编写。 (5)编写中断处理回调函数 在使用HAL库的时候,我们也可以跟使用标准库一样,在中断服务函数中编写控制逻辑。但是HAL库为了用户使用方便,提供了一个中断通用入口函数HAL_GPIO_EXTI_IRQHandler,在该函数内部直接调用回调函数HAL_GPIO_EXTI_Rising_Callback和HAL_GPIO_EXTI_Falling_Callback,回调函数需要我们自己编写,从而实现中断控制逻辑。 以上的步骤(1)(5)中,前面的步骤(1)(4)在STM32CubeIDE生成的初始化代码中已经为我们做好了,而步骤(5)需要我们手动去完成。 3. 程序设计流程
图13.3.1. 1程序设计流程图 13.3.2 生成初始化代码
- 创建工程 创建一个新的工程EXTI,(如果不想创建工程,也可以直接在上一章节的KEY按键工程中直接配置)。
- GPIO工作模式配置 在System CoreGPIO选项中配置如下: ①按照前面的配置步骤,配置LED0和LDE1以及BEEP为推挽输出、上拉、Very High模式,User Label名字和前面章节的实验一致,因为我们要用前面工程的驱动代码。 ②按键的User Label名字也和上一章节的保持一致。除了按键WKUP配置为下拉模式,其它按键均配置为上拉模式。这里注意了,如果按键不配置上下拉的话,后面按下按键将会看不到效果。 ③按键WKUP和KEY0以及KEY1分别配置为GPIO_EXTI0、GPIO_EXTI3、GPIO_EXTI7外部中断模式。以上GPIO记得配置给CM4使用,前面章节我们都有强调。 如下图是WKUP按键配置为外部中断模式。
图13.3.2. 1配置为外部中断模式 要配置GPIO的中断触发模式,其中:
表13.3.2. 1 GPIO的中断触发模式 这里,我们配置PA0(WKUP)为Extemal Interrupt Mode with Rising edge trigger detection模式,配置PG3和PH7为External Interrupt Mode with Falling edge trigger detection模式,如下图所示。
图13.3.2. 2 GPIO配置 3. NVIC配置 在System CoreNVIC选项中配置如下: 如下图,在NVIC的NVIC配置项下,配置外部中断的优先级, Preemption Priority是抢占优先级,Sub Priority是响应优先级(子优先级),Enabled全局中断使能选项记得勾选。 EXTI0中断线:抢占优先级为2,响应优先级为0; EXTI3中断线:抢占优先级为2,响应优先级为1; EXTI7中断线:抢占优先级为2,子优先级为2。 这里要注意,如果要在中断服务函数中使用HAL_Delay延时函数的话,那么在设置按键的优先级的时候,就尽量不要设置抢占优先级为0(0是最高优先级),因为HAL_Delay延时函数是通过Systick(滴答定时器)来提供时基的,而Systick的抢占先级和子优先级这里默认设置为0了,如果外部中断的抢占优先级也设置为0,就会出现程序进入外部中断以后,Systick中断服务函数一直未能执行,导致程序运行以后出现卡死的情况,这点我们在分析stm32mp1xx_hal_conf.h文件的时候有说过,详见第7.4.1小节。
图13.3.2. 3配置外部中断的优先级 如下图,在NVIC的Code generation默认配置如下,即保持默认的配置,尽量不要修改。目的就是要在stm32mp1xx_it.c文件中生成EXTI中断服务函数。
图13.3.2. 4生成代码保持默认配置 4. 生成工程 我们采用默认内部高速时钟HSI(64MHz)。同时在Project Manager窗口勾选此项,配置独立生成对应外设的初始化.h和.c 文件:
图13.3.2. 5 配置生成独立文件 配置好以后,按下“Ctrl+S”保存修改,生成初始化代码。
图13.3.2. 6生成的工程 13.3.3 初始化代码分析 生成的工程中,我们分析一下几个重要的文件。
- main.c文件 main.c文件主要就是系统时钟初始化代码,我们前面已经有分析过。因为我们没有配置时钟,所以采用默认的系统时钟频率64MHz,而GPIO挂在AHB4总线上,AHB4的时钟频率也为64MHz。
- gpio.c文件 gpio.c文件主要是gpio初始化代码,包括开启GPIO时钟、配置GPIO工作模式、外部中断优先级初始化,代码如下图: gpio.c文件代码
1 #include "gpio.h"
2
3 void MX_GPIO_Init(void)
4 {
5 GPIO_InitTypeDef GPIO_InitStruct = {
0};
6
7 /* GPIO时钟使能 */
8 __HAL_RCC_GPIOI_CLK_ENABLE(); /* 开启LED0的时钟 */
9 __HAL_RCC_GPIOC_CLK_ENABLE(); /* 开启BEEP的时钟 */
10 __HAL_RCC_GPIOG_CLK_ENABLE(); /* 开启KEY0的时钟 */
11 __HAL_RCC_GPIOF_CLK_ENABLE(); /* 开启LED1的时钟 */
12 __HAL_RCC_GPIOH_CLK_ENABLE(); /* 开启KEY1的时钟 */
13 __HAL_RCC_GPIOA_CLK_ENABLE(); /* 开启WKUP的时钟 */
14
15 /* 配置LED0的工作模式 */
16 GPIO_InitStruct.Pin = LED0_Pin; /* LED0引脚 */
17 GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; /* 输出模式 */
18 GPIO_InitStruct.Pull = GPIO_PULLUP; /* 设置上拉 */
19 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; /* 高速模式 */
20 HAL_GPIO_Init(LED0_GPIO_Port, &GPIO_InitStruct); /* LED0初始化 */
21
22 /* 配置BEEP的工作模式 */
23 GPIO_InitStruct.Pin = BEEP_Pin; /* BEEP引脚 */
24 GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; /* 输出模式 */
25 GPIO_InitStruct.Pull = GPIO_PULLUP; /* 设置上拉 */
26 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; /* 高速模式 */
27 HAL_GPIO_Init(BEEP_GPIO_Port, &GPIO_InitStruct); /* BEEP初始化 */
28
29 /* 配置KEY0 */
30 GPIO_InitStruct.Pin = KEY0_Pin; /* KEY0引脚 */
31 GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; /* 下降沿触发 */
32 GPIO_InitStruct.Pull = GPIO_PULLUP; /* 设置上拉 */
33 HAL_GPIO_Init(KEY0_GPIO_Port, &GPIO_InitStruct); /* KEY0初始化 */
34
35 /* 配置LED1的工作模式 */
36 GPIO_InitStruct.Pin = LED1_Pin; /* LED1引脚 */
37 GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; /* 输出模式 */
38 GPIO_InitStruct.Pull = GPIO_PULLUP; /* 设置上拉 */
39 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; /* 高速模式 */
40 HAL_GPIO_Init(LED1_GPIO_Port, &GPIO_InitStruct); /* LED1初始化 */
41
42 /* 配置KEY1 */
43 GPIO_InitStruct.Pin = KEY1_Pin; /* KEY1引脚 */
44 GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; /* 下降沿触发 */
45 GPIO_InitStruct.Pull = GPIO_PULLUP; /* 设置上拉 */
46 HAL_GPIO_Init(KEY1_GPIO_Port, &GPIO_InitStruct); /* KEY1初始化 */
47
48 /* 配置WKUP */
49 GPIO_InitStruct.Pin = WKUP_Pin; /* WKUP引脚 */
50 GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING; /* 上升沿触发 */
51 GPIO_InitStruct.Pull = GPIO_PULLDOWN; /* 设置下拉 */
52 HAL_GPIO_Init(WKUP_GPIO_Port, &GPIO_InitStruct); /* WKUP初始化 */
53
54 /* EXTI中断优先级设置 */
55
56 /* 设置EXTI0中断抢占优先级为2,子优先级为0 */
57 HAL_NVIC_SetPriority(EXTI0_IRQn, 2, 0);
58 /* 启用EXTI0中断 */
59 HAL_NVIC_EnableIRQ(EXTI0_IRQn);
60 /* 设置EXTI3中断抢占优先级为2,子优先级为1 */
61 HAL_NVIC_SetPriority(EXTI3_IRQn, 2, 1);
62 /* 启用EXTI3中断 */
63 HAL_NVIC_EnableIRQ(EXTI3_IRQn);
64 /* 设置EXTI7中断抢占优先级为2,子优先级为2 */
65 HAL_NVIC_SetPriority(EXTI7_IRQn, 2, 2);
66 /* 启用EXTI7中断 */
67 HAL_NVIC_EnableIRQ(EXTI7_IRQn);
68 }
GPIO初始化的代码大部分我们都已经很熟悉了,其中第57~67行,调用HAL_NVIC_SetPriority函数设置外部中断的抢占优先级和子优先级,并调用中断使能函数HAL_NVIC_EnableIRQ启用对应的中断。
例如第57行和第59行,EXTI0_IRQn是中断号(其中断号为6),2是抢占优先级,0是子优先级。中断号是在stm32mp157dxx_cm4.h文件中定义的,目的就是为了通过此中断号来找到启动文件startup_stm32mp157daax.s里边的中断向量表中对应的中断服务程序入口地址,通过此入口地址可以找到中断服务函数,然后就去执行中断服务函数。外部中断号和外部中断向量表对应关系如下:
图13.3.3. 1中断号和中断向量表的映射关系 中断向量表和中断服务函数对应关系如下,注意,这里中断服务函数是弱定义的:
图13.3.3. 2中断向量表和中断服务函数的映射关系 上述代码中,设置好的中断优先级如下: EXTI0中断抢占优先级为2,子优先级为0; EXTI3中断抢占优先级为2,子优先级为1; EXTI7中断抢占优先级为2,子优先级为2。 中断优先级是EXTI0 > EXTI3> EXTI7,当然也可以随意设置优先级顺序,例如设置中断优先级EXTI0< EXTI3< EXTI7也是可以的。 3. stm32mp1xx_it.c文件 stm32mp1xx_it.c文件是STM32CubeIDE自动生成的,文件包含异常处理程序和外设的中断服务程序,文件中有调用HAL_IncTick函数来保证Systick每隔1ms产生一次中断(我们在7.4.2的第1小节有分析)。外设的中断服