1. 中断管理简介
1.1 什么是中断?
简介:让 CPU 打断正常运行的程序,转而去处理紧急的事件(程序),就叫中断。
中断执行机制,可简单概括为三步:
- 中断请求。外设产生中断请求(GPIO 外部中断、定时器中断等)
- 响应中断。CPU停止执行当前程序,转而去执行中断处理程序(ISR)
- 退出中断。执行完毕,返回被打断的程序处,继续往下执行
说到中断,那必不可少就提到我们的中断优先级,因为中断它说到底其实就是一个比较紧急的事件,我们要优先去处理紧急事件,但是你在处理的过程中难免有可能会遇到更加紧急的事情,那这时候咋办?那这时候你就要去处理更加紧急的事情。那这个对应到中断,就是它中断优先级要更高。
也就是说中断优先级高的可以抢占优先级低的中断去执行,也就是中断嵌套。
1.2 中断优先级分组设置
ARM Cortex-M 使用了 8 位宽 的寄存器来配置中断的优先等级,这个寄存器就是中断优先级配置寄存器。
注意:ARM Cortex-M 的中断优先级配置寄存器是一个 8 位宽的,所以它最大就是 28=256,所以说 ARM Cortex-M 这个内核它最大能支持到 256 中断优先级等级,也就是 0~255 这么一个范围了。
那这个范围其实很大的了,对于一般的芯片厂商来说,它并不需要用到这么多等级,没必要。就比如我们的 ST。ST 它也属于 ARM Cortex-M 内核的,那它就没有用 8 位这么多了。
STM32 只用了中断优先级配置寄存器的高4位 [7 : 4],所以 STM32 提供了最大 16 级的中断优先等级。
STM32 的中断优先级可以分为抢占优先级和子优先级:
- 抢占优先级: 抢占优先级高的中断可以打断正在执行但抢占优先级低的中断。
- 子优先级:
- 当同时发生具有相同抢占优先级的两个中断时,子优先级数值小的优先执行。
- 那如果不同时呢?子优先级数值小的中断是不会抢占正在执行的子优先级数值大的中断。
注意:中断优先级数值越小越优先
STM32 使用了中断优先级配置寄存器的高4位 [7 : 4],那这 4 个位是怎么分配给抢占优先级和子优先级呢?就由我们的中断优先级分组来设置了。
一共有 5 种分配方式,对应着中断优先级分组的 5 个组:
优先级分组 | 抢占优先级 | 子优先级 | 优先级配置寄存器高 4 位 |
---|---|---|---|
NVIC_PriorityGroup_0 | 0 级抢占优先级 | 0-15 级子优先级 | 0bit 用于抢占优先级、4bit 用于子优先级 |
NVIC_PriorityGroup_1 | 0-1 级抢占优先级 | 0-7 级子优先级 | 1bit 用于抢占优先级、3bit 用于子优先级 |
NVIC_PriorityGroup_2 | 0-3 级抢占优先级 | 0-3 级子优先级 | 2bit 用于抢占优先级、2bit 用于子优先级 |
NVIC_PriorityGroup_3 | 0-7 级抢占优先级 | 0-1 级子优先级 | 3bit 用于抢占优先级、1bit 用于子优先级 |
NVIC_PriorityGroup_4 | 0-15 级抢占优先级 | 0 级子优先级 | 4bit 用于抢占优先级、0bit 用于子优先级 |
在上一章节我们有提到过 FreeRTOS 它为了方便管理使用的就是 NVIC_PriorityGroup_4。
因为它全部用作抢占式优先级位,大家想象一下,我用作抢占式我就很容易区分哪个优先级高,然后高的去打断低的;但是如果你用作子优先级,我知道你优先级高,但是我不能实现打断,只要你抢占优先级相同,你子优先级高的或低的你都不能打断。
通过调用函数 HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
即可完成设置。(在 HAL_Init 中设置)
FreeRTOS官网关于中断说明:https://www.freertos.org/RTOS-Cortex-M3-M4.html
特点:
- 低于 configMAX_SYSCALL_INTERRUPT_PRIORITY 优先级的中断里才允许调用 FreeRTOS 的 API 函数
- 建议将所有优先级位指定为抢占优先级位,方便 FreeRTOS 管理
- 中断优先级数值越小越优先,任务优先级数值越大越优先
1.3 中断相关寄存器
中断管理说到底其实就是控制的它的寄存器。
首先先来介绍三个系统中断优先级配置寄存器,分别为 SHPR1、 SHPR2、 SHPR3
SHPR1 寄存器地址:0xE000ED18
SHPR2 寄存器地址:0xE000ED1C
SHPR3 寄存器地址:0xE000ED20
表出自:《Cortex M3 权威指南(中文)》第 286 页
那这里面有什么特殊的,需要我们特别关注的?
- PendSV 的优先级设置
- Systick(滴答定时器)的优先级设置
- Systick(滴答定时器):给我们的系统提供心跳节拍。
- PendSV :任务切换、任务调度都是在 PendSV 中断里面实现的。
所以这两个的优先级设置是非常重要的。
这里它一个地址是 8 个位,然后一个寄存器是 32 位的。也就是说 8个、8个、8个、8个 组成一个 32 位的寄存器,SHPR1 寄存器起始地址就是 0xE000ED18;接着下面这 4 个,它地址是 0xE000ED1C,也就是 SHPR2 寄存器,它也是 32 位;最后一个就是地址是 0xE000ED20 的SHPR3 寄存器。
然后每一个地址 8 个位,那么我们比如要设置这个 PendSV 它的一个优先级,怎么设置?从 0xE000ED20 这个地址偏移 16 位,就可以设置 0xE000ED22 这个地址;如果你要设置滴答定时器的优先级,从 0xE000ED20 这个地址偏移 24 位,就可以设置到滴答定时器的优先级。
从 0xE000ED20 这个地址偏移就行了。那这个地址就是 SHPR3 寄存器。
那么来看一下,在 FreeRTOS 中,我们是如何配置 PendSV 和 Systick 中断优先级:
- 宏:(SHPR3 寄存器、PendSV 中断优先级、 Systick 中断优先级)
- SHPR3 寄存器宏定义
- PendSV 中断优先级宏定义、 Systick 中断优先级宏定义
为什么左移 16 位、24 位?因为SHPR3 寄存器有 4 个 8 位,PendSV 在次高 8 位,Systick 在高 8 位,那我要操作 PendSV 是不是要左移 16 位,然后操作 Systick 是不是要左移 24 位。
- 中断优先级配置
把 15 左移 4 位。15 代表中断最低优先级,然后左移 4 位,因为 STM32 的优先级配置低 4 位是没用的,只用到前面的 bit4~bit7,所以要左移 4 位。
所以经过这一大段设置,最终把 PendSV 和 SysTick 设置成 15 这个最低中断优先级了。
设置最低:保证系统任务切换不会阻塞系统其他中断的响应。
- 也就是说中断可以打断任务,但任务不能打断中断。因为中断是一个紧急的事情,所以中断是可以打断任意的任务,但是任务不能打断中断,就这么一个概念。
- PendSV 中断里面就是来处理任务切换的,所以设置成最低。
接着继续介绍另外三个寄存器:三个中断屏蔽寄存器,分别为 PRIMASK、 FAULTMASK 和BASEPRI。
FreeRTOS 就是利用的 BASEPRI 这个寄存器来管理中断的。
BASEPRI:屏蔽优先级低于某一个阈值的中断。
- 比如: BASEPRI 设置为 0x50(5 << 4),代表中断优先级在 5~15 内的均被屏蔽(关闭),不能中断,触发了也不会响应;0~4 的中断优先级正常执行,因为不在 BASEPRI 的管理范围内。
关中断程序示例:
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI() /* 关中断函数 */
static portFORCE_INLINE void vPortRaiseBASEPRI( void )
{
uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY; /* 0x50 */
__asm
{
msr basepri, ulNewBASEPRI /* 汇编,将 ulNewBASEPRI 赋值给 basepri 寄存器 */
dsb
isb
}
}
#define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5 /* FreeRTOS可管理的最高中断优先级 */
原理:往 BASEPRI 寄存器写入一个值。0 是开中断;其他值是关中断的阈值。
此时中断优先级在 5 ~ 15 的全部被关闭(禁止)。
所以我们的 FreeRTOS 的最大的管理范围就是 5~15,这是我们能关闭的,能禁止的,0~4 FreeRTOS 是操作不了的。
当 BASEPRI 设置为 0x50 时:
此时优先级在 5~15 之间的这些中断是 FreeRTOS 可以管理的,FreeRTOS 想禁止的时候就可以禁止,而这些被 FreeRTOS 管理的中断在它的中断服务函数里面是可以调用带有 “FromISR” 结尾的 API 函数,也就是说只有被 FreeRTOS 管理的中断服务函数里面才可以调用 FreeRTOS 的 API 函数。
然后不在 FreeRTOS 管理的优先级在 0~4 之间的这些中断不会被 FreeRTOS 禁止,但是不能调用 FreeRTOS 的 API 函数。
总结:
在中断服务函数中调度 FreeRTOS 的 API 函数需注意:
- 中断服务函数的优先级需在 FreeRTOS 所管理的范围内
- 在中断服务函数里边需调用 FreeRTOS 的 API 函数,必须使用带“FromISR”后缀的函数
- 中断优先级分组一定要设置成 NVIC_PriorityGroup_4。把全部的位设置成抢占式优先级位。
开中断程序示例:
#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 ) /* 开中断函数 */
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
__asm
{
msr basepri, ulBASEPRI /* 汇编,将 basepri 寄存器设置成 0 */
}
}
原理:当 BASEPRI 寄存器设置为 0 时,则不关闭任何中断,也就是开中断。
其实我们主要管理的就是 BASEPRI 寄存器的值,所以 FreeRTOS 的中断管理还是比较简单的。
总结:FreeRTOS中断管理就是利用 BASEPRI 寄存器实现的
2. FreeRTOS中断管理实验
- 实验目的:学会使用FreeRTOS的中断管理!
本实验会使用两个定时器,一个优先级为 4,一个优先级为 6,注意:FreeRTOS 我们设置的管理的优先级范围:5~15。那很明显,6 这个优先级在 FreeRTOS 管理范围内,所以 FreeRTOS 可以管理它;但是这个 4 在 FreeRTOS 管理范围之外了,所以 FreeRTOS 就管理不了它。
所以我们会实现这么一个现象:
- 两个定时器每 1 s,打印一段字符串。
- 当 FreeRTOS 关中断时,在 FreeRTOS 管理范围内的 6 就会被关闭,停止打印;当 FreeRTOS 开中断时,它才会继续打印,这是优先级为 6 的定时器的现象;
- 那优先级为 4 的定时器,它不受 FreeRTOS 管理,当 FreeRTOS 关中断时禁止不了它,它照样打印;当 FreeRTOS 开中断时也跟它没关系,它还是照样打印。所以它在FreeRTOS 关中断期间依旧是持续不断的打印。
- 实验设计:将设计2个任务:start_task、task1
2个任务的功能如下:
- start_task:用来创建 task1 任务
- task1:中断测试任务,任务中将调用关中断和开中断函数来体现对中断的管理作用!
当然我们定时器的一个驱动代码还是要另外实现的,只是我们这个任务的内容就比较简单,其实就是调用关中断以及开中断两个函数而已。
2.1 定时器驱动实现
btim.h
#ifndef __BTIM_H
#define __BTIM_H
#include "./SYSTEM/sys/sys.h"
/******************************************************************************************/
/* 基本定时器 定义 */
/* TIMX 中断定义
* 默认是针对TIM6/TIM7
* 注意: 通过修改这4个宏定义,可以支持TIM1~TIM8任意一个定时器.
*/
#define BTIM_TIMX_INT TIM6 /* 定时器 */
#define BTIM_TIMX_INT_IRQn TIM6_DAC_IRQn /* 中断线 */
#define BTIM_TIMX_INT_IRQHandler TIM6_DAC_IRQHandler /* 中断服务函数 */
#define BTIM_TIMX_INT_CLK_ENABLE() do{ __HAL_RCC_TIM6_CLK_ENABLE(); }while(0) /* TIM6 时钟使能 */
#define BTIM_TIM7_INT TIM7
#define BTIM_TIM7_INT_IRQn TIM7_IRQn
#define BTIM_TIM7_INT_IRQHandler TIM7_IRQHandler
#define BTIM_TIM7_INT_CLK_ENABLE() do{ __HAL_RCC_TIM7_CLK_ENABLE(); }while(0) /* TIM6 时钟使能 */
/******************************************************************************************/
void btim_timx_int_init(uint16_t arr, uint16_t psc); /* 基本定时器 定时中断初始化函数 */
void btim_tim7_int_init(uint16_t arr, uint16_t psc); /* 基本定时器 定时中断初始化函数 */
#endif
#include "./BSP/LED/led.h"
#include "./BSP/TIMER/btim.h"
#include "./SYSTEM/usart/usart.h"
TIM_HandleTypeDef g_timx_handle; /* 定时器参数句柄 */
TIM_HandleTypeDef g_tim7_handle; /* 定时器参数句柄 */
/**
* @brief 基本定时器TIMX定时中断初始化函数
* @note
* 基本定时器的时钟来自APB1,当PPRE1 ≥ 2分频的时候
* 基本定时器的时钟为APB1时钟的2倍, 而APB1为45M, 所以定时器时钟 = 90Mhz
* 定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
* Ft=定时器工作频率,单位:Mhz
*
* @param arr : 自动重装值。
* @param psc : 时钟预分频数
* @retval 无
*/
void btim_timx_int_init(uint16_t arr, uint16_t psc)
{
g_timx_handle.Instance = BTIM_TIMX_INT; /* 定时器x */
g_timx_handle.Init.Prescaler = psc; /* 分频 */
g_timx_handle.Init.CounterMode = TIM_COUNTERMODE_UP; /* 递增计数模式 */
g_timx_handle.Init.Period = arr; /* 自动装载值 */
HAL_TIM_Base_Init(&g_timx_handle);
HAL_TIM_Base_Start_IT(&g_timx_handle); /* 使能定时器x和定时器更新中断 */
}
/* TIM7初始化函数 */
void btim_tim7_int_init(uint16_t arr, uint16_t psc)
{
g_tim7_handle.Instance = BTIM_TIM7_INT; /* 定时器x */
g_tim7_handle.Init.Prescaler = psc; /* 分频 */
g_tim7_handle.Init.CounterMode = TIM_COUNTERMODE_UP; /* 递增计数模式 */
g_tim7_handle.Init.Period = arr; /* 自动装载值 */
HAL_TIM_Base_Init(&g_tim7_handle);
HAL_TIM_Base_Start_IT(&g_tim7_handle); /* 使能定时器x和定时器更新中断 */
}
/**
* @brief 定时器底层驱动,开启时钟,设置中断优先级
此函数会被HAL_TIM_Base_Init()函数调用
* @param 无
* @retval 无
*/
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
{
if (htim->Instance == BTIM_TIMX_INT)
{
BTIM_TIMX_INT_CLK_ENABLE(); /* 使能TIMx时钟 */
HAL_NVIC_SetPriority(BTIM_TIMX_INT_IRQn, 6, 0); /* 抢占6,子优先级0 */
HAL_NVIC_EnableIRQ(BTIM_TIMX_INT_IRQn); /* 开启ITMx中断 */
}
if(htim->Instance == BTIM_TIM7_INT)
{
BTIM_TIM7_INT_CLK_ENABLE(); /* 使能TIM7时钟 */
HAL_NVIC_SetPriority(BTIM_TIM7_INT_IRQn, 4, 0); /* 抢占4,子优先级0 */
HAL_NVIC_EnableIRQ(BTIM_TIM7_INT_IRQn); /* 开启ITM7中断 */
}
}
/**
* @brief 基本定时器TIMX中断服务函数
* @param 无
* @retval 无
*/
void BTIM_TIMX_INT_IRQHandler(void)
{
HAL_TIM_IRQHandler(&g_timx_handle); /* 定时器回调函数 */
}
/* TIM7中断服务函数 */
void BTIM_TIM7_INT_IRQHandler(void)
{
HAL_TIM_IRQHandler(&g_tim7_handle); /* 定时器回调函数 */
}
/**
* @brief 回调函数,定时器中断服务函数调用
* @param 无
* @retval 无
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == BTIM_TIMX_INT)
{
printf("TIM6优先级为6的正在运行!!!\r\n");
}else if(htim->Instance == BTIM_TIM7_INT)
{
printf("TIM7优先级为4的正在运行!!!!!\r\n");
}
}
main.c
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/KEY/key.h"
#include "./BSP/SDRAM/sdram.h"
#include "./MALLOC/malloc.h"
#include "freertos_demo.h"
#include "./BSP/TIMER/btim.h" /* 定时器驱动头文件 */
int main(void)
{
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(360, 25, 2, 8); /* 设置主频时钟,180Mhz */
delay_init(180); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
led_init(); /* 初始化LED */
key_init(); /* 初始化按键 */
sdram_init(); /* SRAM初始化 */
lcd_init(); /* 初始化LCD */
btim_timx_int_init(10000 - 1 ,9000 - 1); /* 1s中断一次 */
btim_tim7_int_init(10000 - 1 ,9000 - 1); /* 1s中断一次 */
my_mem_init(SRAMIN); /* 初始化内部内存池 */
my_mem_init(SRAMEX); /* 初始化外部内存池 */
my_mem_init(SRAMCCM); /* 初始化CCM内存池 */
freertos_demo();
}
调用定时器的初始化函数来实现定时器 1s 中断一次。因为我们是以 429 为例的,429 的主频时钟是 180M,然后 TIM6 和 7 的时钟是主频时钟的一半,也就是 90M,那么这里要设置为 1s,所以定时器的分频就可以设置为 9000 - 1,然后重装载值要设置成 10000 - 1,那这样才是 1s 中断一次。(90M/9000/10000 = 1)
注意:不同的系列的基本定时器的时钟是不一样的。
我们现在就可以验证一下这两个基本定时器的驱动有没有问题。没有问题的话,它就会每一秒打印一次。
2.2 编写中断测试任务
/* 任务一,中断测试任务 */
void task1( void * pvParameters )
{
uint8_t task1_num = 0;
while(1)
{
if(++task1_num == 5)
{
task1_num = 0;
printf("关中断!!\r\n");
portDISABLE_INTERRUPTS(); /* 关中断 */
delay_ms(5000);
printf("开中断!!!\r\n");
portENABLE_INTERRUPTS(); /* 开中断 */
}
vTaskDelay(1000);
}
}
为什么只能用 delay_ms(5000);
(死延时) 而不能用 vTaskDelay(5000);
(系统延时)?
vTaskDelay();
的内部实现会调用恢复任务调度器(xTaskResumeAll();
),恢复任务调度器内部会调用到临界区,临界区它里面操作是什么?我们这里提前给大家说一下,退出临界区它里面也会调用到开中断(portENABLE_INTERRUPTS();
)。所以 vTaskDelay();
这个函数是会调用到临界区的,但它一调用到临界区,那它直接调用到开中断(portENABLE_INTERRUPTS();
)函数,所以此时你这里如果调用的是 vTaskDelay();
,那么是不是你关了中断,它直接给你开了,那你关中断是不是没用啊,你关了立马它又给你开起来了,所以你就达不到关中断这么一个效果。我们这里为了演示,所以调用 delay_ms(5000);
这个死延时。