1)实验平台:正点原子新起点V2开发板
2)平台购买地址:https://detail.tmall.com/item.htm?id=609758951113
2)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-300792-1-1.html
3)对正点原子FPGA感兴趣的同学可以加群讨论:994244016
4)关注正点原子公众号,获取最新资料更新
在前面几章的学习中,我们掌握了STM32MP1的IO口最基本的操作方法,在上一章节的实验中,我们通过按键扫描的方式来检测按键的状态,本章节,我们将通过中断的方式来检测按键的状态。通过本章节的学习,我们可以掌握STM32MP157的IO口作为外部中断输入来使用的方法。
本章将分为如下几个小节:
13.1、STM32MP157中断控制器;
12.2、硬件设计;
12.3、软件设计;
12.4、章节小结;
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-M4内核的组合,属于多核异构,其中,Cortex-A内核的中断管理机构叫做GIC(general interrupt controller),即通用中断控制器。Cortex-M内核的中断管理机构叫NVIC(Nested Vectored Interrupt Controller),即嵌套向量中断控制器。关于Cortex-A内核的中断我们会在A7相关例程里介绍,这里,我们重点介绍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. 1参考手册中的中断向量汇总表
我们在分析startup_stm32mp15xx.s启动文件的时候有讲解过M4内核的中断向量表是位于RETRAM(64kB)中的,地址从0x00000000开始,详见第6.3.5小节。如下图是启动文件中Cortex M4的最小中断向量表:
图13.1.2. 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来定义的。具体的分配关系如下表所示:
表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和中断线映射关系表
- 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的中断触发模式,其中:
GPIO的中断触发模式
Extemal Interrupt Mode with Rising edge trigger detection
具有上升沿触发检测功能的外部中断模式
External Interrupt Mode with Falling edge trigger detection
具有下降沿触发检测的外部中断模式
External Interrupt Mode with Rising/Falling edge trigger detection
具有上升/下降沿触发检测的外部中断模式
External Event Mode with Rising edge trigger detection
具有上升沿触发检测的外部事件模式
External Event Mode with Falling edge trigger detection
具有下降沿触发检测的外部事件模式
External Event Mode with Rising/Falling edge trigger detection
具有上升/下降沿触发检测的外部事件模式
表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小节有分析)。外设的中断服务函数也可以编写在其他文件中,比如各个硬件的驱动文件里面,不一定只能写在stm32mp1xx_it.c文件里,看个人喜欢。文件中生成的函数,是下图中配置有了才生成的:
图13.3.3. 3默认的代码生成配置
我们来看看此文件的代码:
stm32mp1xx_it.c文件代码
1 #include "main.h"
2 #include "stm32mp1xx_it.h"
3 /*********************************/
4 /* Cortex-M4处理器中断和异常处理程序 */
5 /*********************************/
6 /* 此函数处理不可屏蔽的中断 */
7 void NMI_Handler(void)
8 {
9
10 }
11 /* 此功能处理硬故障中断 */
12 void HardFault_Handler(void)
13 {
14 while (1)
15 {
16
17 }
18 }
19 /* 此功能处理内存管理故障 */
20 void MemManage_Handler(void)
21 {
22 while (1)
23 {
24
25 }
26 }
27 /* 此函数处理预取错误,内存访问错误 */
28 void BusFault_Handler(void)
29 {
30 while (1)
31 {
32
33 }
34 }
35 /* 此函数处理未定义的指令或非法状态 */
36 void UsageFault_Handler(void)
37 {
38 while (1)
39 {
40
41 }
42 }
43 /* 该功能通过SWI指令处理系统服务调用 */
44 void SVC_Handler(void)
45 {
46
47 }
48 /* 此功能处理调试监视器 */
49 void DebugMon_Handler(void)
50 {
51
52 }
53 /* 此功能处理可挂起的系统服务请求 */
54 void PendSV_Handler(void)
55 {
56
57 }
58 /* 此功能处理系统滴答计时器 */
59 void SysTick_Handler(void)
60 {
61 HAL_IncTick();
62 }
63
64/*****************************************************************/
65 /* STM32MP1xx外围设备中断处理程序 */
66 /* 在此处添加所用外围设备的中断处理程序 */
67 /* 有关可用的外设中断处理程序名称,请参阅启动文件(startup_stm32mp1xx.s)*/
68/*****************************************************************/
69
70 /* 此函数处理EXTI line0中断 */
71 void EXTI0_IRQHandler(void)
72 {
73 HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
74 }
75
76 /* 此函数处理EXTI line3中断 */
77 void EXTI3_IRQHandler(void)
78 {
79 HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_3);
80 }
81
82 /* 此函数处理EXTI line7中断 */
83 void EXTI7_IRQHandler(void)
84 {
85 HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_7);
86 }
这一部分是STM32CubeIDE生成的中断服务函数,例如第71行,EXTI0_IRQHandler是中断服务函数的名字,里边则调用第73行的中断请求函数。
上面中断服务程序函数的名字是有规律的,名字要和启动文件startup_stm32mp1xx.s中的中断服务函数名字一致。我们在分析启动文件的时候有讲解过,在startup_stm32mp1xx.s启动文件中有对中断服务函数做了弱(weak)定义,所以用户才可以在其它文件中重新定义中断服务函数。下图是启动文件中的中断服务函数,可以看到每个中断服务函数都是弱定义,每个弱定义的中断服务函数都定义了一个别名Default_Handler,这个Default_Handler就是执行无限死循环。可想而知,如果开启了中断,但是用户没有编写中断服务函数的话,上电运行以后,如果进入中断,那么程序就进入了Default_Handler死循环了(详见第6.3.5小节)。所以,在开启中断以后,我们一定要正确编写中断服务函数,而这一部分的操作STM32CubeIDE已经帮我们做了,以上的代码就是。
图13.3.3. 4启动文件中中断服务函数的部分截图
前面第3~63行,主要是Cortex-M4处理器中断和异常处理服务函数,例如非屏蔽中断NMI,硬件故障中断HardFault、内存管理中断MemManage等。
第59~62,SysTick中断服务函数,调用HAL_IncTick来实现SysTick每1ms的周期产生一次中断,因为Systick会给其它函数作为时基源,例如我们会用到的延时函数HAL_Delay也是通过Systick来计时的。
第70~86行,是STM32MP1xx外围设备中断服务函数,我们前面配置了GPIO的3个外部中断,分别连在EXTI0、EXTI3和EXTI7中断线上。
我们以EXTI0中断为例,如果发生中断,则进入中断服务函数EXTI0_IRQHandler,EXTI0_IRQHandler调用了stm32mp1xx_hal_gpio.c文件的HAL_GPIO_EXTI_IRQHandler函数,用于处理EXTI中断请求,代码如下:
stm32mp1xx_hal_gpio.c代码
1 /*
2 * @brief 处理EXTI中断请求。
3 * @param GPIO_Pin指定连接到相应EXTI线的GPIO端口引脚。
4 * @retval 无
5 */
6 void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
7 {
8 /* 检测到EXTI线路是上升沿触发的中断 */
9 if (__HAL_GPIO_EXTI_GET_RISING_IT(GPIO_Pin) != RESET)
10 {
11 /* 清除中断标识位 */
12 __HAL_GPIO_EXTI_CLEAR_RISING_IT(GPIO_Pin);
13 HAL_GPIO_EXTI_Rising_Callback(GPIO_Pin);/* 调用EXTI检测回调函数 */
14 }
15 /* 检测到EXTI线路是下降沿触发的中断 */
16 if (__HAL_GPIO_EXTI_GET_FALLING_IT(GPIO_Pin) != RESET)
17 {
18 /* 清除中断标识位 */
19 __HAL_GPIO_EXTI_CLEAR_FALLING_IT(GPIO_Pin);
20 HAL_GPIO_EXTI_Falling_Callback(GPIO_Pin);/* 调用EXTI检测回调函数 */
21 }
22 }
该函数实现的作用非常简单,通过入口参数GPIO_Pin判断中断来自哪个IO口,是上升沿还是下降沿,然后清除相应的中断标志位,最后调用回调函数HAL_GPIO_EXTI_Rising_Callback或者HAL_GPIO_EXTI_Falling_Callback实现控制逻辑。在所有的外部中断服务函数中直接调用外部中断共用处理函数HAL_GPIO_EXTI_IRQHandler,然后在回调函数中通过判断中断是来自哪个IO口来编写相应的中断服务控制逻辑。
以EXTI7为例,第9~14行,RESET的值为0,通过读取RPR1寄存器来检查EXTI0中断线是否为上升沿,如果是上升沿触发中断,则先清除EXTI0中断的挂起位,再处理回调函数。第12行是通过对RPR1寄存器的第0位RPIF0写1来清除中断标志位的。
以EXTI7为例,第16~21行,同样地,当读取FPR1寄存器的第7位FPIF7为1的时候,表示来了一个下降沿中断,先对FPIF7写1来清除中断标志位以后,再处理回调函数。
判断是否有中断,是通过读取中断标志位来实现的,读取的中断标志位为1,表示发生中断,读取的中断标志位为0,表示没有发生中断。在调用回调函数前(也可以在回调函数之后)先清理了中断标志位,如果不清理,当中断返回以后,还是会重新进入中断,程序会一直卡死在中断里,回不到主函数。
我们来看看回调函数,函数在stm32mp1xx_hal_gpio.c文件中,代码如下
stm32mp1xx_hal_gpio.c文件代码
1 /*
2 * EXTI中断检测回调函数(上升沿)
3 * GPIO_Pin指定连接到相应EXTI线的端口引脚
4 */
5 __weak void HAL_GPIO_EXTI_Rising_Callback(uint16_t GPIO_Pin)
6 {
7 /* 防止未使用的参数编译警告 */
8 UNUSED(GPIO_Pin);
9 /* 注意:不应修改此函数,当需要回调时,可以在用户
10 文件中实现HAL_GPIO_EXTI_Rising_Callback */
11 }
12 /*
13 * EXTI中断线检测回调函数(下降沿)
14 * GPIO_Pin指定连接到相应EXTI线的端口引脚
15 */
16 __weak void HAL_GPIO_EXTI_Falling_Callback(uint16_t GPIO_Pin)
17 {
18 /* 防止未使用的参数编译警告 */
19 UNUSED(GPIO_Pin);
20 /* 注意:不应修改此函数,当需要回调时,可以在用户
21 文件中实现HAL_GPIO_EXTI_Rising_Callback */
22 }
第5行和第16行,回调函数是weak弱定义,弱就是表示此函数可以被用户进行重写(重新定义),但是不能直接修改此弱定义函数。如果用户在其它地方重新定义了一个同名函数,最终编译器编译的时候,就会选择用户定义的函数,后面我们会自己编写回调函数。这里,宏UNUSED是为了避免gcc/g++编译器告警,在stm32mp1xx_hal_def.h文件中有定义:
#define UNUSED(X) (void)X
这里(void)X表示空类型的X,表示什么也不做。因为如果形参在函数中未使用,编译的时候会报错类似“定义的局部变量/函数形参 x 未使用”的告警。通过UNUSED(X)来调用了函数的形参,编译器在编译的时候会忽略掉此空类型的X。
4. stm32mp1xx_hal_msp.c文件
HAL_MspInit函数在stm32mp1xx_hal.c文件里面被做了弱定义:
stm32mp1xx_hal.c文件代码
__weak void HAL_MspInit(void)
{
/* 注意:这个函数不应该修改,当需要回调时,HAL_MspInit可以在用户文件中实现* /
}
HAL_MspInit函数前有一个weak,也是可以被用户重新定义的,而STM32CubeIDE自动为我们做了这步,stm32mp1xx_hal_msp.c文件中重新定义HAL_MspInit函数如下:
stm32mp1xx_hal_msp.c文件代码
1 #include "main.h"
2
3 void HAL_MspInit(void)
4 {
5 __HAL_RCC_HSEM_CLK_ENABLE(); /* 开启HSEM时钟,表示使能HSEM */
6 /* 设置NVIC的中断优先级分组为2 */
7 HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);
8 }
HAL_MspInit函数用于初始化底层硬件,其中,第5行用于开启HSEM外设时钟,HSEM是集成在STM32中的硬件信号量,用于管理一些共享资源的访问权限和同步,保护GPIO和外部中断EXTI配置免受并发访问。
第7行,设置NVIC的中断优先级分组为2。其实在HAL_Init函数中,有初始化NVIC的中断优先级分组为4,然后再调用被重新定义的HAL_MspInit函数再次初始化中断优先级分组,最终中断优先级分组为2。
stm32mp1xx_hal.c文件代码
HAL_StatusTypeDef HAL_Init(void)
{
/* 设置中断组优先级 */
#if defined (CORE_CM4)
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
#endif
/* 更新SystemCoreClock全局变量 */
SystemCoreClock = HAL_RCC_GetSystemCoreClockFreq();
/* 使用systick作为时基源并配置1ms滴答(复位后的默认时钟为HSI)*/
if(HAL_InitTick(TICK_INT_PRIORITY) != HAL_OK)
{
return HAL_ERROR;
}
/* 初始化底层硬件 */
HAL_MspInit();
return HAL_OK;
}
13.3.4 用户逻辑代码实现
- 新建用户文件
和前面章节一样,我们复制上一章节工程中的BSP文件夹到本CM4工程的Core/Src下,如下图:
图13.3.4. 1生成的工程
2. 添加用户驱动代码
经过前面对工程的分析:
main.c完成了GPIO的时钟初始化;stm32mp1xx_hal_msp.c文件设置了NVIC的中断优先级分组为2;gpio.c文件完成了GPIO时钟使能、gpio初始化以及gpio所对应的中断线的抢占优先级和子优先级的设置;stm32mp1xx_it.c文件中生成了EXTI中断的中断服务函数框架,但是回调函数还需要我们重新定义,整个中断处理过程才完整。
接下来,我们需要做的是编写回调函数,然后中断服务函数会调用我们编写的回调函数来实现中断控制逻辑。回调函数我们可以在stm32mp1xx_hal_msp.c文件中定义,也可以在其它文件中定义,看个人喜欢,这里直接在上一章节的key.c文件中定义,最后的key.c文件内容如下:
key.c文件代码
1 #include "./Include/key.h"
2 #include "./Include/led.h"
3 #include "./Include/beep.h"
4
5 void delay_short(volatile unsigned int n)
6 {
7 while(n--){}
8 }
9 void delay(volatile unsigned int n)
10 {
11 while(n--)
12 {
13 delay_short(0x7fff);
14 }
15 }
16
17 /*
18 * @brief GPIO上升沿回调函数
19 * @param GPIO_Pin: 中断引脚号
20 * @note 在HAL库中所有的外部中断服务函数都会调用此函数
21 * @retval 无
22 */
23 void HAL_GPIO_EXTI_Rising_Callback(uint16_t GPIO_Pin)
24 {
25 /* 消抖,此处为了方便,使用了延时函数,实际代码中禁止
26 * 在中断服务函数中调用任何HAL_Delay之类的延时函数!!!
27 */
28 HAL_Delay(10);
29 //for(long n = 1;n<72000;n++){}; //消除抖动
30 //delay(2); //消除抖动
31 if (WKUP == 1) /* WK_UP中断 */
32 {
33 BEEP_TOGGLE(); /* 蜂鸣器 状态取反 */
34 }
35 }
36
37 /*
38 * @brief GPIO下降沿回调函
39 * @param GPIO_Pin: 中断引脚
40 * @note 在HAL库中所有的外部中断服务函数都会调用此函数
41 * @retval 无
42 */
43 void HAL_GPIO_EXTI_Falling_Callback(uint16_t GPIO_Pin)
44 {
45 /* 消抖,此处为了方便,使用了延时函数,实际代码中禁止
46 *在中断服务函数中调用任何HAL_Delay之类的延时函数!!!
47 */
48 HAL_Delay(10);
49 //for(long n = 1;n<72000;n++){}; //消除抖动
50 //delay(2); //消除抖动
51 if (KEY0 == 0) /* KEY0中断 */
52 {
53 LED0_TOGGLE(); /* LED0状态取反 */
54 }
55 else if(KEY1 == 0) /* KEY1中断 */
56 {
57 LED1_TOGGLE(); /* LED01状态取反 */
58 }
59 }
我们简单分析一下以上代码。
如果是上升沿中断,则调用HAL_GPIO_EXTI_Rising_Callback回调函数,先使用HAL_Delay(10)延时10ms进行消抖,然后再判断按键WKUP的电平,如果为高电平,表示WKUP按键按下,LED1状态翻转。
如果是下降沿中断,先延时10ms进行消抖,然后再判断按键的电平,如果KEY0电平为0,表示KY0按下,LED0状态取反。如果KEY1电平为0,表示KEY1按下,LED1状态取反。
这里提一下,HAL_Delay(10)主要是用于按键消抖的,避免按键按下以后因为机械抖动而引起LED灯或者蜂鸣器多次翻转,不稳定,所以加了10ms的消抖延时。不过,如果要在中断服务函数中使用HAL_Delay函数的话,那么中断的优先级在设置的时候就要注意,不能设置比SysTick的中断优先级比高或者同级,否则中断服务函数一直得不到执行,从而卡死在这里。
也可以采用其他方式来达到延时的效果,例如上述代码的第29行,使用for循环来达到一定的延时时间,第30行,使用while循环来达到延时,这两种方式里就没有用到HAL_Delay函数了,那么设置按键中断的抢占优先级为0的时候就不会出现程序卡死的情况了。
修改后的main.c代码如下,while循环中加了延时函数,当然也可以不加:
main.c文件代码
1 #include "main.h"
2 #include "gpio.h"
3
4 #include "./BSP/Include/beep.h"
5 #include "./BSP/Include/led.h"
6 #include "./BSP/Include/key.h"
7
8 void SystemClock_Config(void);
9
10 int main(void)
11 {
12 /* HAL库初始化 */
13 HAL_Init();
14
15 if(IS_ENGINEERING_BOOT_MODE())
16 {
17 SystemClock_Config(); /* 配置系统时钟 */
18 }
19
20 MX_GPIO_Init(); /* 初始化已经配置了的外设 */
21 led_init(); /* 关闭 LED0,打开LED1 */
22 beep_init(); /* 关闭BEEP */
23
24 while (1)
25 {
26 HAL_Delay(1000); /* 延时1000ms */
27 }
28 }
29
30 void SystemClock_Config(void)
31 {
32 /* 此处省略时钟初始化代码 */
33 }
34 void Error_Handler(void)
35 {
36
37 }
13.3.5 编译和调试
保存修改后点击工具栏的小锤子进行编译,编译无报错后,按照第4.1.6小节连接好开发板和ST-Link,进入Debug模式。进入Debug以后,点击继续运行按钮来运行调试,此时LED1灯亮,LED0灭,蜂鸣器不响。当按下KEY1的时候,LED1灯灭,再按下KEY1的时候LED1又亮,实现LED1灯翻转。LED1和蜂鸣器也是类似,按下KEY0,LED0翻转,按下WKUP,蜂鸣器翻转。
13.4 章节小结
中断在裸机开发中很常用,学好中断相当重要。本小节,我们来总结和中断有关的几个重要文件以及实验的设计过程。(虽然在前面我们都有提到过,不过知识点分散在各个部分了,为了把它们串起来,这里还是有必要总结一下。)
13.4.1 几个重要的文件
core_cm4.h
core_cm4.h文件有内核外设相关定义,例如SysTick和NVIC内核外设寄存器定义,还有NVIC函数定义,用于管理Cortex-M内核所有中断和事件,包括中断的使能和除能,中断的优先级等, NVIC函数会被重新封装到stm32mp1xx_hal_cortex.c文件中。
startup_stm32mp157daax.s
startup_stm32mp157daax.s启动文件中有定义中断向量表,并且预先为每个中断都写了一个中断服务函数,只是这些中断服务函数都是weak弱定义,且为空函数,目的是为了初始化中断向量表,并且为这些弱定义的中断服务函数设置了一个别名Default_Handler,如果用户开启了中断,并没有编写中断服务函数,就默认执行Default_Handler函数,不会造成程序崩溃。如果用户有重新定义一个中断服务函数,那么就会执行用户定义的中断服务函数。在STM32CubeIDE中,如果配置好了中断,就会在stm32mp1xx_it.c文件中生成中断服务函数。
stm32mp157dxx_cm4.h
stm32mp157dxx_cm4.h文件主要就是对STM32MP157dxx系列器件的Cortex-M处理器和外设的设备资源定义,例如外设中断号定义、外设寄存器结构体声明、外设寄存器位定义和寄存器的操作的宏定义以及外围设备内存映射等等。
其中,定义的中断号和中断向量表的映射关系如下,CPU就是根据这个中断号来找到启动文件中的中断服务程序的入口地址的:
图13.4.1. 1中断号和中断向量表的映射关系
找到中断服务程序的入口地址以后,就跳到中断服务程序中,执行中断服务程序。这里注意,在启动文件中有定义了中断服务程序,不过这些中断服务程序均是弱(weak)定义,弱,就意味着用户可以在别的地方定义,然后执行的是用户定义的中断服务函数。那如果用户没有正确定义中断服务函数(例如,中断服务函数名字写错),进入中断以后,就默认执行启动文件startup_stm32mp157daax.s中弱定义的中断服务函数,也就是执行Default_Handler,进入无限死循环(我们在第6.3.5小节,讲解启动文件的时候分析过) 。
下图是中断向量表和中断服务函数的对应关系:
图13.4.1. 2中断向量表和中断服务函数的映射关系
13.4.2 本章实验设计过程
①配置GPIO工作模式配置GPIO的外部中断模式设置GPIO的中断优先级保持默认的HSI时钟生成工程。
②生成的工程中:
stm32mp1xx_hal.c
stm32mp1xx_hal.c文件初始化HAL库,并调用HAL_MspInit函数完成中断优先级分组。
stm32mp1xx_hal_gpio.c
stm32mp1xx_hal_gpio.c文件有GPIO初始化相关函数,包括GPIO模式初始化和EXTI(外部中断)模式初始化,并定义了GPIO外部中断请求函数和弱定义了外部中断回调函数。GPIO外部中断请求函数通过调用清除中断标志位函数和外部中断回调函数来完成中断请求功能,外部中断回调函数需要用户去编写。
gpio.c
gpio.c用于开启GPIO时钟、设置中断触发模式、初始化GPIO,通过中断号来设置对应中断的抢占优先级、子优先级以及开启中断。最后也是通过此中断号来运行中断服务程序的,我们前面有分析了中断号的作用,如果想自己手动编写这部分代码,中断号千万不要写错!直接对照stm32mp157dxx_cm4.h文件里定义好的中断号来写。
stm32mp1xx_hal_msp.c
stm32mp1xx_hal_msp.c文件通过HAL_MspInit函数开启HSEM时钟,重定义HAL_MspInit函数来设置中断优先级分组,重定义的HAL_MspInit函数最后被stm32mp1xx_hal.c文件中的HAL_Init调用了,这样才完成中断优先级分组的最终初始化。
stm32mp1xx_it.c
stm32mp1xx_it.c文件中生成中断服务函数。这里要注意:
①如果要自己手动编写中断服务函数的话,中断服务函数的函数名必须跟启动文件里面预先设置的名字一样,如果函数名写错了,或者开启了中断却没写中断服务函数,当发生中断时,系统就默认执行启动文件中预先写好的Default_Handler空函数,并且在里面无限空循环,实现不了中断。
②另外,中断服务函数中,必须软件清除中断标志位,如果不清除,在中断返回以后,还是会不断地重新进入中断,程序卡在中断中,无法返回主程序。所以,如果是自己编写的中断服务函数代码,当程序异常的时候,记得检查中断标志位是否已经清除了。
回调函数
外部中断的回调函数是被外部中断请求函数调用了,外部中断的中断回调函数我们可以在stm32mp1xx_hal_msp.c文件中编写,也可以在其它地方编写,但不能在弱(weak)定义的函数中直接改写,这是不允许的。要注意的是,如果中断中有用到HAL_Delay函数的话,要格外小心,所设置的外部中断优先级不能高于或者等于SysTick的中断优先级,否则会出现程序无法正常进入中断,程序卡死。
我们将整个过程用一个简单的图来表示,下图中只是简单列出部分重要文件间的关系。结合上面的文字描述以及图的表达,我们能更好地梳理本章实验。
图13.4.2. 1几个文件的关系