正点原子 发表于 2022-11-3 10:11:18

《STM32MP1 M4裸机HAL库开发指南》第十九章 外部中断实验

本帖最后由 正点原子 于 2022-11-3 10:11 编辑

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口作为外部中断输入来使用的方法。
      本章将分为如下几个小节:
      19.1、STM32MP157中断控制器;
      19.2、硬件设计;
      19.3、程序设计;
      19.4、编译和测试;
      19.5、章节小结;

19.1 STM32MP157中断控制器
19.1.1 中断的概念
      为了方便后面的原理部分内容的讲解,我们这里先说明几个重要的概念。
1. 什么是中断
      对于单片机而言,CPU在执行事件A时,发生了另一件事B,这事件B比事件A要紧急,于是事件B请求CPU优先处理。CPU收到请求以后,先暂停执行事件A,转而去执行事件B。当事件B执行完以后,CPU返回之前被暂停的事件A的地方继续执行。
      这里,事件B称为中断源。中断源向CPU提出处理的请求称为中断请求。发生中断时被打断的暂停点称为断点。CPU暂停执行事件A而转响应中断请求的过程称为中断响应。处理中断源的程序称为中断处理程序。CPU执行中断处理程序的过程称为中断处理。返回断点的过程称为中断返回。中断流程图如下所示,整个过程也可以称之为中断嵌套:
图19.1.1. 1中断嵌套
2. 中断分类
      中断可以按照不同分类方法来分:
      ①如可以分为硬件中断和软件中断。硬件中断可以是CPU以外的I/O设备产生的中断,每个外设都有他自己的IRQ(中断请求),CPU可以将相应的IRQ分发到相应的硬件驱动上。软件中断是一条CPU指令,用以自陷一个中断。
      ②硬件中断可以分为可屏蔽中断和非可屏蔽中断。可屏蔽就是CPU可以响应中断也可以不响应中断。非屏蔽就是指CPU必须无条件响应,无法通过设置中断屏蔽寄存器来将其关闭。
      ③中断也可以分为内部中断和外部中断。外部中断是指由CPU外部信号触发的中断,内部中断,例如算法指令或者地址越界引起的中断,这种也称之为软件中断或者系统异常中断。
      具体某一个中断属于哪一类,一般参考手册上会给出。关于中断优先级以及中断向量,我们下文会进行讲解。
19.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。
1. 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可能会屏蔽该中断。
图19.1.2. 1参考手册中的中断向量汇总表
                我们在分析startup_stm32mp15xx.s启动文件的时候有讲解过M4内核的中断向量表是位于RETRAM(64kB)中的,地址从0x00000000开始。如下图是启动文件中Cortex M4的最小中断向量表:
图19.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文件中有定义,如下图所示,第一列是中断号名称,它和中断向量表的名称有对应,第二列是对应的中断号。关于中断号和中断向量表以及中断服务函数的对应关系,我们后面会详细分析。
图19.1.2. 3中断号定义
2. NVIC寄存器
NVIC相关的寄存器的定义可以在core_cm4.h文件中找到,我们直接通过程序的定义来分析NVIC相关的寄存器,其定义如下:
core_cm4.h文件代码
typedef struct
{
__IOM uint32_t ISER;               /* 中断使能寄存器 */
      uint32_t RESERVED0;
__IOM uint32_t ICER;               /* 中断除能寄存器 */
      uint32_t RSERVED1;
__IOM uint32_t ISPR;               /* 中断使能挂起寄存器 */
      uint32_t RESERVED2;
__IOM uint32_t ICPR;               /* 中断解挂寄存器 */
      uint32_t RESERVED3;
__IOM uint32_t IABR;               /* 中断有效位寄存器 */
      uint32_t RESERVED4;
__IOM uint8_tIP;               /* 中断优先级寄存器(8位宽) */
      uint32_t RESERVED5;
__OMuint32_t STIR;                  /*软件触发中断寄存器 */
}NVIC_Type;

STM32MP157的中断是在这些寄存器的控制下有序的执行的。只有了解这些中断寄存器,才能方便的使用STM32MP157的中断。下面我们重点介绍这几个寄存器:
ISER
ISER全称是:Interrupt Set Enable Registers,这是一个中断使能寄存器组。
上面说了CM4内核支持256个中断,这里用8个32位寄存器来控制,每个位控制一个中断。但是STM32MP157的可屏蔽中断最多只有150个,所以对我们来说,有用的就是4个(ISER]),总共可以表示160个中断,而STM32MP157只用了其中的150个。
ISER设置0~31号中断的使能,ISER设置32~63号中断的使能,其他以此类推,这样总共150个中断就可以分别对应上了。如果要使能某个中断,必须设置相应的ISER位为1,使该中断被使能(这里仅仅是使能,还要配合中断分组、屏蔽、IO口映射等设置才算是一个完整的中断设置)。具体每一位对应哪个中断,请参考stm32mp157dxx_cm4.h里面的第70行到220行,共150个。
该寄存器对应的位写1表示使能位所对应的中断,写0表示无效。
ICER
ICER全称是:Interrupt Clear Enable Registers,是一个中断除能寄存器组。该寄存器组与ISER的作用恰好相反,是用来清除某个中断的使能的。ICER寄存器的位对应的中断也和ISER寄存器的位对应的中断一样,ICER设置0~31号中断除能,ICER设置32~63号中断的使能,以此类推。这里要专门设置一个ICER来清除中断位,而不是向ISER写0来清除,是因为NVIC的这些寄存器都是写1有效的,写0是无效的。
ISPR
ISPR全称是:Interrupt Set Pending Registers,是一个中断使能挂起控制寄存器组。每个位对应的中断和ISER是一样的。通过置1,可以将正在进行的中断挂起,转而执行同级或更高级别的中断。写1改变中断状态为挂起,写0是无效的。
ICPR
ICPR全称是:Interrupt Clear Pending Registers,是一个中断解挂控制寄存器组。其作用与ISPR相反,每个位对应的中断和ISER是一样的。通过设置1,可以将挂起的中断解挂。写0无效。
IABR
IABR全称是:Interrupt Active Bit Registers,是一个中断激活标志位寄存器组。对应位所代表的中断和ISER一样,如果为1,则表示该位所对应的中断正在被执行。这是一个只读寄存器,通过读取它可以知道当前在执行的中断是哪一个。在中断执行完了以后由硬件自动清零。
IP
IP全称是:Interrupt Priority Registers,是一个中断优先级控制寄存器组。这个寄存器组相当重要!STM32MP157的中断分组与这个寄存器组密切相关。
IP寄存器组由240个8bit的寄存器组成,每个寄存器对应一个中断优先级,每个可屏蔽中断占用8bit,这样总共可以表示240个可屏蔽中断。
实际上STM32MP157只用到了其中的150个。IP~IP分别对应中断149~0。每个可屏蔽中断占用的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_IPR0~NVIC_IPR59共60个寄存器控制中断优先级,每个寄存器32位,每8位又分为一组,一个寄存器可以分4组,所以就有了240(4*60)组宽度为8bit的中断优先级控制寄存器。原则上每个外部中断可配置的优先级为0~255,数值越小,优先级越高。但是实际上M3 /M4 /M7 芯片为了精简设计,只使用了高四位,低四位取零,这样以至于最多只有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来定义的。具体的分配关系如下表所示:
表19.1.2. 1 AIRCR中断分组设置表
通过这个表,我们就可以清楚的看到组0~4对应的配置关系,例如优先级分组设置为3,那么此时所有的150个中断,每个中断的中断优先寄存器的高四位中的最高3位是抢占优先级,低1位是响应优先级,抢占优先级共有23=8种,子优先级共有21=2种,共有8*2=16级嵌套,每个中断,你可以设置抢占优先级为0~7,响应优先级为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位(设定优先级分组)*/
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。
19.1.3 EXTI简介
1. 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_EXTICR1~EXTI_EXTICR4寄存器来选择对应的IO口,这些IO口可以作为外部中断的输入源。从图中看出,EXTI多路复用器模块可以输出0~15个可配置事件,我们下文会分析这16个可配置事件“Configurable event”是怎么和GPIO相连以及寄存器怎么配置;      
      ③事件输入触发模块提供事件输入边沿触发逻辑,此逻辑可以是上升沿触发、下降沿触发或者双边沿触发;
      ④屏蔽模块为不同的唤醒、中断和事件输出及其屏蔽功能提供事件分配。
      框图中最左边,输入事件分为两类,一个是可配置事件(来自I / O或能够产生脉冲的外设的信号),一个是直接事件“Direct event”(其它外设的中断和唤醒源,需要在外设中将其清除)。它们的功能和特点如下:
      可配置事件:①具有可选的活动触发沿;②中断挂起状态寄存器位与上升沿和下降沿无关;③单独的中断和事件生成掩码,用于调节CPU唤醒、中断和事件生成;④软件触发的可能性。
      直接事件:①具有固定的上升沿活动触发器;②EXTI中没有中断等待状态寄存器位(产生事件的外设提供了中断等待状态标志);③单独的中断和事件生成掩码,用于调节CPU唤醒和事件生成;④没有软件触发的可能性。
图19.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多路复用器可以输出0~15个可配置事件到事件输入触发模块,这0~15个事件对应外部IO口的输入中断,标号分别为EXTI~ EXTI,共16个外部中断线。如下图是STM32MP157 EXTI事件汇总表格,只截图了表格的一部分,详细的表格信息可以在参考手册中查阅。EXTI~ EXTI可配置,唤醒目标为MPU或者MCU。
图19.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上。对应关系如下图:
图19.1.3. 3 GPIO和中断线映射关系图
      GPIO和中断线的映射关系如下表:
表19.1.3. 1 GPIO和中断线映射关系表
3. EXTI相关寄存器
(1)EXTI外部中断选择寄存器
      上面我们了解了GPIO和中断线的映射关系,它是由EXTI外部中断选择寄存器控制的,寄存器是EXTI_EXTICR1~ EXTI_EXTICR4。我们来看看这4个寄存器怎么配置。
图19.1.3. 4 EXTICR1寄存器
      EXTI_EXTICR1是EXTI外部中断选择寄存器1,属于32位可读可写寄存器,每8位控制一个16个pin:第0~7位配置对应的是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无影响。
图19.1.3. 5 RPR1寄存器
      FPR1是EXTI下降沿挂起寄存器,和RPR1类似是低17位可用。读取某位为0,表示未发生下降沿触发请求,读取某位为1,表示发生上升沿触发请求。当下降沿事件到达可配置事件行时,该位置1。同样的,可通过将1写入该位来清除该位。
图19.1.3. 6 FPR1寄存器
      以上的寄存器要注意,中断标志位不会自动清除,在HAL库中,GPIO外部中断处理函数是通过对应位写1来清除中断标识位的,中断的触发是通过读取标志位来进行的。如果中断标志位不清除,那么完成中断处理程序后,程序还是会继续进入中断无法退出,返回不到主函数。后面的实验中要注意这点。
19.2 硬件设计
1. 例程功能
通过外部中断的方式让开发板上的三个独立按键控制LED灯和蜂鸣器翻转:KEY0控制LED0翻转,KEY1控制LED1翻转,WKUP控制BEEP翻转。
2. 硬件资源
表19.2. 1硬件资源表
3. 原理图
LED和BEEP的原理图我们前面已经涉及,独立按键硬件部分的原理图如下图所示:
图19.2. 1独立按键与STM32MP157连接原理图
这里需要注意的是:按键KEY0和KEY1是低电平有效的,而按键WK_UP是高电平有效的,并且按键KEY0和KEY2接了一个10K欧的上拉电阻,而按键WK_UP外部没有上下拉电阻。芯片内部有30~50K欧之间上下拉电阻,上电复位后,GPIO端口上拉下拉寄存器PUPDR的复位值为0x0000 0000,表示芯片内部上下拉电阻都不开启,引脚处于浮空模式中,浮空模式下,引脚的电平是不可确定的,那么按键按下以后可能存在引脚电平还是没变化的情况,程序测试结果就不准确,所以,按键WK_UP所对应的引脚一定要开启芯片内部下拉。
19.3 程序设计
      本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\3、M4裸机驱动例程\ MP157-M4 HAL库V1.2\实验8 外部中断实验。
19.3.1 程序设计思路
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文件代码
/* 外部中断,上升沿触发检测 */
#defineGPIO_MODE_IT_RISING          (uint32_t)0x10110000U)
/* 外部中断,下降沿触发检测 */
#defineGPIO_MODE_IT_FALLING         ((uint32_t)0x10210000U)
/* 外部中断,上升和下降双沿触发检测 */
#defineGPIO_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)编写中断服务函数
      我们配置完中断优先级之后,接着要做的就是编写中断服务函数,可以在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. 程序设计流程
图19.3.1. 1程序设计流程图
19.3.2 实验代码实现
1. 新建文件
      在工程的Drivers/BSP下新建EXTI文件夹,在该文件夹下新建exti.c和exti.h文件,并将exti.c文件关联到工程中,关于外部中断的初始化和中断服务函数以及回调函数我们均在该文件中添加。
图19.3.2.1 新建和关联exti.c文件
2. 添加exti.h文件代码
      exit.h文件代码主要是实现中断引脚的定义和中断函数的声明,其代码如下:
#ifndef __EXTI_H
#define __EXTI_H

#include "./SYSTEM/sys/sys.h"
/* 按键引脚和中断编号&中断服务函数定义,以及GPIO时钟使能 */
#define KEY0_INT_GPIO_PORT             GPIOG
#define KEY0_INT_GPIO_PIN            GPIO_PIN_3               /* PG3 */
#define KEY0_INT_GPIO_CLK_ENABLE()    do{ __HAL_RCC_GPIOG_CLK_ENABLE(); }while(0)   
#define KEY0_INT_IRQn                   EXTI3_IRQn
#define KEY0_INT_IRQHandler            EXTI3_IRQHandler

#define KEY1_INT_GPIO_PORT             GPIOH
#define KEY1_INT_GPIO_PIN            GPIO_PIN_7               /* PH7 */
#define KEY1_INT_GPIO_CLK_ENABLE()    do{ __HAL_RCC_GPIOH_CLK_ENABLE(); }while(0)
#define KEY1_INT_IRQn                   EXTI7_IRQn
#define KEY1_INT_IRQHandler            EXTI7_IRQHandler

#define WKUP_INT_GPIO_PORT             GPIOA
#define WKUP_INT_GPIO_PIN            GPIO_PIN_0                /* PA0 */
#define WKUP_INT_GPIO_CLK_ENABLE()    do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)
#define WKUP_INT_IRQn                   EXTI0_IRQn
#define WKUP_INT_IRQHandler            EXTI0_IRQHandler

void extix_init(void); /* 外部中断初始化函数声明 */

#endif
由硬件设计小节,我们知道KEY0、KEY1和KEY_UP分别来连接到PG3、PH7和PA0上,即对应了EXTI3、EXTI7和EXTI0这三条外部中断线;EXTI3_IRQn、EXTI7_IRQn和EXTI0_IRQn分别是3条中断线的中断号;EXTI3_IRQHandler、EXTI7_IRQHandler和EXTI0_IRQHandler分别是3条中断线的中断服务函数。
      中断号是在stm32mp157dxx_cm4.h文件中定义的,目的就是为了通过此中断号来找到启动文件startup_stm32mp157daax.s里边的中断向量表中对应的中断服务程序入口地址,通过此入口地址可以找到中断服务函数,然后就去执行中断服务函数。外部中断号和外部中断向量表对应关系如下:
图19.3.2.1中断号和中断向量表的映射关系
3. 添加exti.c文件代码
      实验中添加以下头文件:
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/KEY/key.h"
#include "./BSP/BEEP/beep.h"
#include "./BSP/EXTI/exti.h"
(1)外部中断初始化函数
/**
* @brief       外部中断初始化程序
* @param       无
* @retval      无
*/
void extix_init(void)
{
    GPIO_InitTypeDef gpio_init_struct;
   
    KEY0_INT_GPIO_CLK_ENABLE();         /* KEY0时钟使能 */
    KEY1_INT_GPIO_CLK_ENABLE();         /* KEY1时钟使能 */
    WKUP_INT_GPIO_CLK_ENABLE();         /* WKUP时钟使能 */

    gpio_init_struct.Pin = KEY0_INT_GPIO_PIN;                               /* KEY0引脚 */
    gpio_init_struct.Mode = GPIO_MODE_IT_FALLING;                        /* 下降沿触发 */
    gpio_init_struct.Pull = GPIO_PULLUP;                                     /* 上拉 */
    HAL_GPIO_Init(KEY0_INT_GPIO_PORT, &gpio_init_struct);/* KEY0引脚初始化 */

    gpio_init_struct.Pin = KEY1_INT_GPIO_PIN;               /* KEY1引脚 */
    gpio_init_struct.Mode = GPIO_MODE_IT_FALLING;          /* 下降沿触发 */
    gpio_init_struct.Pull = GPIO_PULLUP;                     /* 上拉 */
    HAL_GPIO_Init(KEY1_INT_GPIO_PORT, &gpio_init_struct); /* KEY1引脚初始化 */
   
    gpio_init_struct.Pin = WKUP_INT_GPIO_PIN;               /* WK_UP引脚 */
    gpio_init_struct.Mode = GPIO_MODE_IT_RISING;         /* 上升沿触发 */
    gpio_init_struct.Pull = GPIO_PULLDOWN;                   /* 下拉 */
    HAL_GPIO_Init(WKUP_INT_GPIO_PORT, &gpio_init_struct); /* WK_UP引脚初始化 */

    HAL_NVIC_SetPriority(WKUP_INT_IRQn, 0, 0);    /* 抢占优先级为0,子优先级为0 */
    HAL_NVIC_EnableIRQ(WKUP_INT_IRQn);            /* 使能中断线0 */
   
    HAL_NVIC_SetPriority(KEY0_INT_IRQn, 0, 1);    /* 抢占优先级为0,子优先级为1 */
    HAL_NVIC_EnableIRQ(KEY0_INT_IRQn);            /* 使能中断线3 */
   
    HAL_NVIC_SetPriority(KEY1_INT_IRQn, 0, 2);    /* 抢占优先级为0,子优先级为2 */
    HAL_NVIC_EnableIRQ(KEY1_INT_IRQn);            /* 使能中断线7 */
}
      以上代码中,先使能按键引脚对应的GPIO时钟,再初始化按键,然后调用HAL_NVIC_SetPriority函数设置外部中断的抢占优先级和子优先级,并调用中断使能函数HAL_NVIC_EnableIRQ启用对应的中断。这里注意的是,我们配置了按键中断的抢占优先级为0,子优先级分别为0、1、2,中断优先级是EXTI0 > EXTI3> EXTI7,当然也可以随意设置优先级顺序,例如设置中断优先级EXTI0< EXTI3< EXTI7也是可以的。
      一般是先确定中断优先级分组再确定中断优先级的,中断优先级有了,那中断优先级分组是哪个呢?中断优先级分组在HAL_Init函数中配置了,如下:
HAL_StatusTypeDef HAL_Init(void)
{
#if defined (CORE_CM4)
/* 设置中断优先级分组为2 */
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);
#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;
}
(3)编写中断服务函数
      以上3个中断,我们编写3个中断服务函数,如下:
/**
* @brief       外部中断服务程序
* @param       无
* @retval      无
*/
void WKUP_INT_IRQHandler(void)
{
    HAL_GPIO_EXTI_IRQHandler(WKUP_INT_GPIO_PIN);/* 调用中断处理公用函数 */
}

void KEY0_INT_IRQHandler(void)
{
    HAL_GPIO_EXTI_IRQHandler(KEY0_INT_GPIO_PIN); /* 调用中断处理公用函数 */
}

void KEY1_INT_IRQHandler(void)
{
    HAL_GPIO_EXTI_IRQHandler(KEY1_INT_GPIO_PIN); /* 调用中断处理公用函数 */
}
      以上3个中断服务函数均调用了中断请求函数HAL_GPIO_EXTI_IRQHandler,实际上该函数是处理所有外部中断的中断请求函数,其通过中断号来区别是哪一个中断。
      中断向量表和中断服务函数对应关系如下,注意,这里中断服务函数实际上是被弱定义的Default_Handler函数给代替了:
图19.3.2.2 中断向量表和中断服务函数的映射关系
      上面中断服务程序函数的名字是有规律的,名字要和启动文件startup_stm32mp1xx.s中的中断服务函数名字一致。我们在分析启动文件的时候有讲解过,在startup_stm32mp1xx.s启动文件中有对中断服务函数做了弱(weak)定义(被弱定义的Default_Handler函数给代替了),所以用户才可以在其它文件中重新定义中断服务函数。Default_Handler函数就是执行无限死循环。可想而知,如果开启了中断,但是用户没有编写中断服务函数或者中断服务函数名字写错了的话,上电运行以后,如果进入中断,那么程序就进入了Default_Handler死循环了(我们前面第八章节有分析过)。所以,在开启中断以后,我们一定要正确编写中断服务函数。
(4)编写回调函数
/**
* @brief       GPIO上升沿回调函数
* @param       GPIO_Pin: 中断引脚号
* @note      在HAL库中所有的外部中断服务函数都会调用此函数
* @retval      无
*/
void HAL_GPIO_EXTI_Rising_Callback(uint16_t GPIO_Pin)
{
/** 消抖,此处为了方便使用了延时函数,实际代码中禁止
   *在中断服务函数中调用任何HAL_Delay之类的延时函数!!!
   */
    delay(2);   
    if (WK_UP == 1)                     /* WK_UP中断 */
    {
      LED1_TOGGLE();      /* LED1 状态取反 */
    }   
}
/**
* @brief       GPIO下降沿回调函数
* @param       GPIO_Pin: 中断引脚号
* @note      在HAL库中所有的外部中断服务函数都会调用此函数
* @retval      无
*/
void HAL_GPIO_EXTI_Falling_Callback(uint16_t GPIO_Pin)
{
/** 消抖,此处为了方便使用了延时函数,实际代码中禁止
   *在中断服务函数中调用任何HAL_Delay之类的延时函数!!!
   */
    delay(2);   
    if (KEY0 == 0)                      /* KEY0中断 */
    {
      BEEP_TOGGLE();                /* 蜂鸣器 状态取反 */
    }
    else if(KEY1 == 0)            /* KEY1中断 */
    {
      LED0_TOGGLE();      /*LED0 状态取反 */
    }
}
      我们简单分析一下以上代码。
      如果是上升沿中断,则调用HAL_GPIO_EXTI_Rising_Callback回调函数,先使用delay (10)延时10ms进行消抖,然后再判断按键WKUP的电平,如果为高电平,表示WKUP按键按下,LED1状态翻转。
      如果是下降沿中断,先延时10ms进行消抖,然后再判断按键的电平,如果KEY0电平为0,表示KY0按下,LED0状态取反。如果KEY1电平为0,表示KEY1按下,LED1状态取反。
      这里提一下,HAL_Delay函数也可以实现延时,我们上面的回调函数中没有使用此函数来实现延时,因为如果要在中断服务函数中使用HAL_Delay函数的话,那么中断的优先级在设置的时候就要注意,不能设置比SysTick的中断优先级比高或者同级,否则中断服务函数一直得不到执行,从而卡死在这里(不信的话,可以试试)。我们本实验中设置了3个按键的抢占优先级都是0,使用HAL_Delay函数来消抖的话,就会出现程序卡死的情况。所以我们使用了之前编写的空循环实现延时函数delay来达到消抖的效果。
4. 编写main.c文件代码
      mian.c文件的代码如下:
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/BEEP/beep.h"
#include "./BSP/KEY/key.h"
#include "./BSP/EXTI/exti.h"
/**
* @brief       主函数
* @param       无
* @retval      无
*/
int main(void)
{
    HAL_Init();   /* 初始化HAL库*/
    /* 初始化M4内核时钟 */
    if(IS_ENGINEERING_BOOT_MODE())
    {
      sys_stm32_clock_init(34, 2, 2, 17, 6826);
    }
    led_init();   /* 初始化LED */
    beep_init();    /* 初始化蜂鸣器 */
    extix_init();   /* 初始化中断 */
    while(1)
    {
      delay(100);/* 延时一定时间 */
      //HAL_Delay(10); /* 也可以使用HAL库自带的HAL_Delay实现延时 */
    }
}
      以上代码中,我们使用上一章节实验的sys_stm32_clock_init来配置MCU的时钟为209MHz。同时初始化LED默认关闭,蜂鸣器不响。另外,延时函数的话,我们也可以使用HAL_Delay来达到延时(注意,这个位置不是在回调函数中,所以不会导致程序卡死)。
19.4 编译和测试
      编译程序无报错后,运行程序,发现LED0和LED1不亮,蜂鸣器也不响。当按下WK_UP的时候,LED1翻转,当按下KEY0的时候,蜂鸣器翻转,当按下KEY1的时候,LED0翻转,实验和我们的程序设计一致。
19.5 章节小结
      中断在裸机开发中很常用,学好中断相当重要。本小节,我们来总结和中断有关的几个重要文件以及实验的设计过程。(虽然在前面我们都有提到过,不过知识点分散在各个部分了,为了把它们串起来,这里还是有必要总结一下。)
19.5.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所替代,
目的是为了初始化中断向量表。值得注意的是,如果用STM32CubeIDE来开发,如果配置好了中断,就会在stm32mp1xx_it.c文件中生成中断服务函数。
stm32mp157dxx_cm4.h
      stm32mp157dxx_cm4.h文件主要就是对STM32MP157dxx系列器件的Cortex-M处理器和外设的设备资源定义,例如外设中断号定义、外设寄存器结构体声明、外设寄存器位定义和寄存器的操作的宏定义以及外围设备内存映射等等。
      其中,定义的中断号和中断向量表的映射关系如下,CPU就是根据这个中断号来找到启动文件中的中断服务程序的入口地址的:
图19.5.1.1中断号和中断向量表的映射关系
      找到中断服务程序的入口地址以后,就跳到中断服务程序中,执行中断服务程序。这里注意,在启动文件中有定义了中断服务程序,不过这些中断服务程序均是弱(weak)定义,弱,就意味着用户可以在别的地方定义,然后执行的是用户定义的中断服务函数。那如果用户没有正确定义中断服务函数(例如,中断服务函数名字写错),进入中断以后,就默认执行启动文件startup_stm32mp157daax.s中弱定义的中断服务函数,也就是执行Default_Handler,进入无限死循环,不会造成程序崩溃。如果用户有正确重新定义一个中断服务函数,那么就会执行用户定义的中断服务函数。
      下图是中断向量表和中断服务函数的对应关系:
图19.5.1.2中断向量表和中断服务函数的映射关系
19.5.2 本章实验设计过程
exti.c和exti.h
      配置GPIO工作模式、配置GPIO的外部中断模式、设置GPIO的中断优先级、使能中断、中断回调函数。
stm32mp1xx_hal.c
      stm32mp1xx_hal.c文件初始化HAL库,并调用HAL_MspInit函数完成中断优先级分组设置。
stm32mp1xx_hal_gpio.c
      stm32mp1xx_hal_gpio.c文件有GPIO初始化相关函数,包括GPIO模式初始化和EXTI(外部中断)模式初始化,并定义了GPIO外部中断请求函数和弱定义了外部中断回调函数。GPIO外部中断请求函数通过调用清除中断标志位函数和外部中断回调函数来完成中断请求功能,外部中断回调函数需要用户去编写。
回调函数
      外部中断的回调函数是被外部中断请求函数调用了,外部中断的中断回调函数在哪个文件中编写没关系,但不能在弱(weak)定义的函数中直接改写,这是不允许的。要注意的是,如果中断中有用到HAL_Delay函数的话,要格外小心,所设置的外部中断优先级不能高于或者等于SysTick的中断优先级,否则会出现程序无法正常进入中断,程序卡死。
      我们将整个过程用一个简单的图来表示,下图中只是简单列出部分重要文件间的关系。结合上面的文字描述以及图的表达,我们能更好地梳理本章实验。
图19.5.2.1几个文件的关系
页: [1]
查看完整版本: 《STM32MP1 M4裸机HAL库开发指南》第十九章 外部中断实验