1. STM32中断系统概述
-
处理器中的中断 在处理器中,中断是一个过程,即CPU在正常执行程序的过程中,遇到外部/内部的紧急事件需要处理,暂时中止当前程序的执行,转而去为处理紧急的事件,待处理完毕后再返回被打断的程序处继续往下执行。中断在计算机多任务处理,尤其是即时系统中尤为重要。比如uCOS,FreeRTOS等。 -
意义 中断能提高CPU的效率,同时能对突发事件做出实时处理。实现程序的并行化,实现嵌入式系统进程之间的切换 -
中断处理过程 简单说,当主程序发现中断发生,就通过中断向量指向执行中断程序,执行完中断程序后返回到原来的主程序,继续运行。如果我们要顺利返回到主程序,我们就需要做现场保护,把状态、参数、下一次执行的程序地址等信息压栈来保存,返回的时候只要出栈就可以顺利回到主程序。
2. NVIC的主要功能
STM32分成两部分,内核和片内外设,其中内核中的NVIC用来管理内外中断。NVIC主要有三个功能:中断管理、支持异常及中断向量处理 和 支持嵌套中断。
2.1 中断管理
Cortex-M4 内核支持 256 个中断,其中包含了 16 个内核中断和 240 个外部中断,并且具有256 级的可编程中断优先级设置。但 STM32F407 并没有使用 Cortex-M4 内核的全部东西(只使用了10个内部中断和82个外部中断)。在Cortex-M4处理器中,每一个外部中断都可以被使能或者禁止,并且可以被设置为挂起状态或者清除状态。这也是中断管理的五个功能:中断优先级设置、使能中断、禁止中断、设置挂起状态和清楚状态。
- 中断优先级设置:当有很多中断同时发生,我们需要设置优先级判断先执行哪一个中断
- 使能中断:让这个中断能用
- 清除使能中断:让这个中断不能用
- 设置挂起状态:假设有两个中断,有优先级更高的中断在执行的时候,另一个中断就被挂起,稍后执行
- 设置清楚状态:当优先级更高的中断执行结束的时候,另一个被挂起的中断就被执行了,这个时候我们要清除这个挂起状态,不要一直让这个中断在挂起状态中,以免反复执行
2.2 嵌套向量控制器NVIC
NVIC是一个中断管理的部件,其内部还是由一系列寄存器构成,下面是一些寄存器的介绍。
寄存器 | 功能 |
---|
ISER | 使能中断 | ICER | 清除中断使能,为了禁止这个中断使用,在这里我们不是直接在使能寄存器 上把使能的寄存器复位来禁止,而是在对应的清除使能寄存器上置位。 | ISPR | 挂起中断,若中断产生但没有立即执行,它就会被挂起 | ICPR | 清除挂起,中断处理完成后应该清除挂起,表示已处理 | IABR | 每个外部中断都有一个活跃状态位,当处理器正在处理这个中断时,该位会被置1 | IP | 用于设置中断的优先级 |
这里解释下为什么很多寄存器后面都有 [0] - [7]。前面我们有提到Cortex-M4一共有256个中断,那么我们要想要去使能这么多中断,我们就需要256位,然而一个寄存器只有32位,因此我们需要 0 - 7 ,一共8个寄存器来控制这么多中断。
2.3 中断及异常向量表
CM4 内核支持 256 个中断,其中包含了 16 个内核中断和 240 个外部中断。STM32F407实际上只使用了10个内部异常和82个外部中断。当异常或中断发生时,处理器会把PC设置为一个特定地址,这一地址就称为异常向量。每一类异常源都对应一个特定的入口地址,这些地址按照优先级排列以后就组成一张异常向量表。
?
\vdots
?
上图就是中断向量表,灰色部分是内部中断,也就是异常;从7-88是外部中断。0x0000 0000 开头的四个字节存放的是栈的地址,后面的四个字节就是复位程序,这在之前stm32的启动流程里有提到过。这样向量化处理中断的好处是采用向量表处理异常,处理器会从存储器的向量表中,自动定位异常的程序入口。从发生异常到异常的处理中间的时间被缩减。
在说一下中断和异常的区别:中断是微处理器外部发送的,通过中断通道送入处理器内部,一般是硬件引起的,比如串口接收中断;而异常通常是微处理器内部发生的,大多是软件引起的,比如除法出错异常,特权调用异常等待。不管是中断还是异常,微处理器通常都有相应的中断/异常服务程序
2.4 嵌套中断
简单来说,嵌套中断就是在我们执行一个中断的时候有另一个优先级特别高的中断发生,这个时候我们中断当前执行的中断,去执行那个优先级更高的中断。下面主要说两个问题。优先级是怎么定的?是不是优先级高的中断一定就会发生中断的嵌套?
- 那么优先级是怎么定的呢?
首先先记住一个原则,数字越小优先级越高。在中断向量表中前三个的优先级类型是固定的,而且是负数,他们的优先级是最高的三个,不能改变。除了这三个异常,其他的中断都是可以设置的。设置优先级是通过 NVIC 的中断优先级寄存器 NVIC_IPRx(在 F407 中,x=0…81)来配置外部中断的优先级,IPR宽度为 8bit,原则上每个外部中断可配置的优先级为0~255,数值越小,优先级越高。但是绝大多数 CM4芯片都会精简设计,以致实际上支持的优先级数减少,在 F407 中,只使用了高 4bit,也就是优先级只有16级 但是我们不是直接拿这四位来定优先级的,而是要对这四位分成两部分,抢占优先级和响应优先级。而这两个优先级各占几个位又要根据 SCB->AIRCR 中的中断分组设置来决定。这里简单介绍一下 STM32F4 的中断分组:STM32F4 将中断分为 5 个组,组 0 - 4。该分组的设置是由 SCB->AIRCR 寄存器的 bit10~8 来定义的。 比较优先级:两个中断,抢占优先级小的那个优先级高;当抢占优先级一样的时候,响应优先级高的那个中断优先级更高。 - 是不是优先级高的中断一定就会发生中断的嵌套?
前面提到了抢占优先级和响应优先级,如果一个中断程序执行的时候,另个中断发生了,如果另个中断的抢占优先级更高,那这个时候发生嵌套中断,也就是暂停当前中断,去执行另个优先级更高的中断。然而,如果另个中断的抢占优先级一样,只是响应优先级更高,那这个时候不发生嵌套中断。
总结:
- 抢占优先级的级别高于响应优先级。而数值越小所代表的优先级就越高。同一时刻发生的中断,优先处理优先级较高的中断。
- 抢占优先级相同就看响应优先级,同样数值越小优先级越高。
- 如果两个中断的抢占优先级和响应优先级都是一样的话,则看哪个中断先发生就先执行哪个。如果同时发生则优先处理编号较小的那个。
- 高优先级的抢占优先级是可以打断正在进行的低抢占优先级中断的。而抢占优先级相同的中断,高优先级的响应优先级不可以打断低响应优先级的中断。
3. 外部中断控制器EXTI
3.1 外部中断控制器EXTI
在片内外设中,基本上所有的外设都可以产生中断,对普通的GPIO来说,EXTI就是用来管理外部GPIO因电平变化而产生的中断。 我们知道,STM32外部有上百个管脚,那EXTI怎么来管理这么多管脚呢?在这里一共有16个外部中断/事件线。其中每一组编号相同的接到同一个事件线,比如PA0、PB0、…、PI0都由EXTI0来管理,以此类推。那具体PAx, …, PIx中的哪一个管脚触发的中断,这是由一个另一个寄存器SYSCFG来控制的。
3.2 系统配置控制器SYSCFG
它和NVIC有一点类似,都有一组配置寄存器(SYSCFG_EXTICR1-4)。系统配置控制器的主要用途如下
- 重映射存储器到代码起始区域。
- 管理连接到 GPIO 口的外部中断。
- 管理系统的可靠性特性。
这个寄存器只有0-15位有效,每四位分成一组,以0-3位为例,这里表示的是EXTI0,如果是0000,那么就是PA0引脚作为EXTI0外部中断的源输入。一个寄存器只能配置4个EXTI,我们一共有16个EXTI,因此我们需要4个寄存器
3.3 外部中断/事件框图
这张图只针对外部中断,外部管脚在1处输入,然后通过边缘检测电路检测在什么时候触发中断(上升沿或下降沿),一旦符合条件能放行到3,这是一个或门,不管是事件或是外部的电平信号,都能通过3。如果是事件变化,就会去到5,通过一个脉冲发生器来触发其他硬件。如果是一个电平变化而导致的中断,就走上面,如果内部没有被屏蔽,也就是说使能了这个中断,就可以进入与门(4),就可以挂起这个中断,等按照优先级排到这个中断的时候就可以把信号发送给NVIC中断控制器来触发中断。
4. 按键中断实例
4.1 简介与项目的建立
上图是按键原理图,在这次实例中,我们按下KEY1后,通过中断与USART,在屏幕上显示按键被按下。 首先,在CubeMX上新建一个项目,通过下图设置中断。
在PI9的配置中设置GPIO mode为External Interrupt Mode with Falling edge trigger detection。也就是做个下降沿检测,按下按钮的时候触发中断。与此同时,我们还要设置NVIC,使能中断,具体看下图。
先设置Priority Group为2位抢占优先级和2位响应优先级,然后,使能EXTI line[9:5] interrupt,同时设置Preemption Priority 和 sub priority都为1,为什么这么设置的原因在后面会说。这时候就可以导出工程了。至于为什么是使能EXTI line[9:5] interrupt,我们可以通过查看中断向量表找到原因。
我们知道外部中断都是用EXTI0-EXTI15来控制的,我们可以通过EXTI9来控制PI9,通过查表知道EXTI9需要用EXTI9_5来控制。
4.2 代码
下面是main.c 的代码,因为我们想要用printf 来输出,所以我们需要覆盖fputc ,具体解释可以看上一篇笔记。然后我们可以看一下MX_GPIO_Init() 里写了什么
#include "main.h"
#include "usart.h"
#include "gpio.h"
void SystemClock_Config(void);
int fputc(int ch, FILE *p)
{
while(!(USART1->SR & (1<<7)));
USART1->DR = ch;
return ch;
}
oid)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
printf("This is KEY test\n");
while (1)
{
}
}
下面是gpio.c ,包含MX_GPIO_Init 和中断回调函数void HAL_GPIO_EXTI_Callback
#include "gpio.h"
void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOI_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOI, &GPIO_InitStruct);
HAL_NVIC_SetPriority(EXTI9_5_IRQn, 1, 1);
HAL_NVIC_EnableIRQ(EXTI9_5_IRQn);
}
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_9)
{
HAL_Delay(20);
if (HAL_GPIO_ReadPin(GPIOI, GPIO_PIN_9) == GPIO_PIN_RESET)
{
printf("Key1\n");
}
}
}
然后我们可以看一下stm32f4xx_it.c ,这里有所有中断的回调函数,我们可以看到很多中断回调函数都是空的或者死循环,在这里我们主要要看EXTI9_5_IRQHandler
#include "main.h"
#include "stm32f4xx_it.h"
void NMI_Handler(void) {while (1){}}
void HardFault_Handler(void) {while (1){}}
void MemManage_Handler(void) {while (1){}}
void BusFault_Handler(void) {while (1){}}
void UsageFault_Handler(void) {while (1){}}
void SVC_Handler(void){}
void DebugMon_Handler(void){}
void PendSV_Handler(void){}
void SysTick_Handler(void)
{
HAL_IncTick();
}
void EXTI9_5_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_9);
}
我们可以继续看一下HAL_GPIO_EXTI_IRQHandler 的定义
void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin)
{
if(__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != RESET)
{
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin);
HAL_GPIO_EXTI_Callback(GPIO_Pin);
}
}
这个回调函数需要我们自己定义,因此我们最好在看一下HAL_GPIO_EXTI_Callback 的定义,__weak 在这里证明这是个弱定义,也就是说我们在别的地方定义的话会自动覆盖这里的函数
__weak void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
UNUSED(GPIO_Pin);
}
因此我们在gpio.c 里定义了这个函数,函数如下。在这里有一点需要关注,我们用了HAL_Delay ,如上图所示,它也是Time base: System tick timer 来控制的。我们知道,在中断中调用另一个中断是嵌套中断。如果两个中断优先级一样,这样就会导致系统死在那里,因此我们需要把GPIO的外部中断的Preemption Priority设置为1,让抢占优先级为0的Hal_Delay 可以发生嵌套中断,以此来顺利运行。
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_9)
{
HAL_Delay(20);
if (HAL_GPIO_ReadPin(GPIOI, GPIO_PIN_9) == GPIO_PIN_RESET)
{
printf("Key1\n");
}
}
}
至此就可以上传到板子上运行程序了。
5. USART中断实例
设置USART与其对应的中断,然后导出项目。USART有两个功能,发送和接收。之前我们有用过两个函数HAL_UART_Transmit 和 HAL_UART_Receive ,但那两个不能用作中断的发送和接收,要用HAL_UART_Transmit_IT 和 HAL_UART_Receive_IT 。
5.1 发送数据
先说HAL_UART_Transmit_IT
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size){}
下面是一个例子
HAL_UART_Transmit_IT(&huart1, (uint8_t *)"UART is sending message\n", 24);
但到这里还没有体现中断,我们举一个例子,如果要在发送完成以后产生一个中断,打印一句Message has sent ,那我们就需要设置控制这个中断的回调函数。我们先打开startup_stm32f407xx.s ,找到里面的中断向量表。
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
__Vectors DCD __initial_sp ; Top of Stack // 异常
.
.
.
DCD SysTick_Handler ; SysTick Handler
; External Interrupts // 外部中断
DCD WWDG_IRQHandler ; Window WatchDog
DCD PVD_IRQHandler ; PVD through EXTI Line detection
DCD TAMP_STAMP_IRQHandler ; Tamper and TimeStamps through the EXTI line
.
.
.
DCD USART1_IRQHandler ; USART1 // USART1中断的回调函数的地址
DCD USART2_IRQHandler ; USART2
DCD USART3_IRQHandler ; USART3
.
.
.
DCD HASH_RNG_IRQHandler ; Hash and Rng
DCD FPU_IRQHandler ; FPU
__Vectors_End
查看USART1_IRQHandler 的定义
void USART1_IRQHandler(void)
{
HAL_UART_IRQHandler(&huart1);
}
继续追查到HAL_UART_IRQHandler ,我们可以把这个函数分成四部分,UART在接收模式、有报错的情况、发送模式 和 发送模式结束的时候
void HAL_UART_IRQHandler(UART_HandleTypeDef *huart)
{
uint32_t isrflags = READ_REG(huart->Instance->SR);
uint32_t cr1its = READ_REG(huart->Instance->CR1);
uint32_t cr3its = READ_REG(huart->Instance->CR3);
uint32_t errorflags = 0x00U;
uint32_t dmarequest = 0x00U;
errorflags = (isrflags & (uint32_t)(USART_SR_PE | USART_SR_FE | USART_SR_ORE | USART_SR_NE));
if (errorflags == RESET)
{
if (((isrflags & USART_SR_RXNE) != RESET) && ((cr1its & USART_CR1_RXNEIE) != RESET))
{
UART_Receive_IT(huart);
return;
}
}
if ((errorflags != RESET) && (((cr3its & USART_CR3_EIE) != RESET) || ((cr1its & (USART_CR1_RXNEIE | USART_CR1_PEIE)) != RESET)))
{ }
if (((isrflags & USART_SR_TXE) != RESET) && ((cr1its & USART_CR1_TXEIE) != RESET))
{
UART_Transmit_IT(huart);
return;
}
if (((isrflags & USART_SR_TC) != RESET) && ((cr1its & USART_CR1_TCIE) != RESET))
{
UART_EndTransmit_IT(huart);
return;
}
}
按照我们之前的需求,是发送完成的时候要产生一个中断,然后发送一句message has sent ,这里我们就需要继续看UART_EndTransmit_IT 这个函数的定义
static HAL_StatusTypeDef UART_EndTransmit_IT(UART_HandleTypeDef *huart)
{
__HAL_UART_DISABLE_IT(huart, UART_IT_TC);
huart->gState = HAL_UART_STATE_READY;
#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
huart->TxCpltCallback(huart);
#else
HAL_UART_TxCpltCallback(huart);
#endif
return HAL_OK;
}
这里HAL_UART_TxCpltCallback ,就是我们修改的回调函数。我们可以在去追一下定义
__weak void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
UNUSED(huart);
}
我们可以在usart.c 里重新定义一下这个函数
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart){
if (huart->Instance == USART1)
{
printf("Message has sent\n");
}
}
我们可以在写一下main.c 函数
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
printf("Uart test\n");
HAL_UART_Transmit_IT(&huart1, (uint8_t *)"UART is sending message\n", 24);
while (1){}
}
上传到板子上后,我们通过uart assistant 上设置好配置,连接后会显示以下Data log
[2022-08-01 05:03:37.709]# RECV ASCII>
Uart test
UART is sending message
Message has sent
5.2 接收数据
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size){}
举个例子
HAL_UART_Receive_IT(&huart1, REV, 2);
要设置中断,还是需要看之前的继续追查到的HAL_UART_IRQHandler 里面的接收模式,
void HAL_UART_IRQHandler(UART_HandleTypeDef *huart)
{
uint32_t isrflags = READ_REG(huart->Instance->SR);
uint32_t cr1its = READ_REG(huart->Instance->CR1);
uint32_t cr3its = READ_REG(huart->Instance->CR3);
uint32_t errorflags = 0x00U;
uint32_t dmarequest = 0x00U;
errorflags = (isrflags & (uint32_t)(USART_SR_PE | USART_SR_FE | USART_SR_ORE | USART_SR_NE));
if (errorflags == RESET)
{
if (((isrflags & USART_SR_RXNE) != RESET) && ((cr1its & USART_CR1_RXNEIE) != RESET))
{
UART_Receive_IT(huart);
return;
}
}
if ((errorflags != RESET) && (((cr3its & USART_CR3_EIE) != RESET) || ((cr1its & (USART_CR1_RXNEIE | USART_CR1_PEIE)) != RESET)))
{ }
if (((isrflags & USART_SR_TXE) != RESET) && ((cr1its & USART_CR1_TXEIE) != RESET))
{
UART_Transmit_IT(huart);
return;
}
if (((isrflags & USART_SR_TC) != RESET) && ((cr1its & USART_CR1_TCIE) != RESET))
{
UART_EndTransmit_IT(huart);
return;
}
}
这里我们继续追查UART_Receive_IT 的定义
static HAL_StatusTypeDef UART_Receive_IT(UART_HandleTypeDef *huart)
{
uint16_t *tmp;
.
.
.
#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
huart->RxCpltCallback(huart);
#else
HAL_UART_RxCpltCallback(huart);
#endif
return HAL_OK;
}
return HAL_OK;
}
else
{
return HAL_BUSY;
}
}
这里我们看到了HAL_UART_RxCpltCallback ,追查下去看下它的定义,也就是我们需要修改的回调函数
__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
UNUSED(huart);
}
我们修改回调函数如下,当接收到指定长度的数据的时候,会发生中断,调用回调函数HAL_UART_RxCpltCallback
extern uint8_t REV[2];
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
printf("REV DATA: REV[0]=%x, REV[1]=%x\n", REV[0], REV[1]);
}
}
这时候我们修改下main 函数
#include "main.h"
#include "usart.h"
#include "gpio.h"
uint8_t REV[2] = {0};
void SystemClock_Config(void);
int fputc(int ch, FILE *p)
{
while (!(USART1->SR & (1<<7)));
USART1->DR = ch;
return ch;
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
HAL_UART_Receive_IT(&huart1, REV, 2);
while (1)
{}
}
下载到板子,在串口助手里输入12 ,会返回
[2022-08-01 05:03:41.491]# RECV ASCII>
REV DATA: REV[0]=1, REV[1]=2
|