FreeRTOS中断配置和临界段
前言
你是否在FreeRTOS开发中遭遇过这样的魔幻场景?——明明设置了高优先级中断,却在任务切换时莫名失效;临界段代码执行时,低优先级中断突然闯入导致系统崩溃。这些诡异现象的背后,藏着Cortex-M内核与FreeRTOS调度器之间最容易被忽视的「中断屏蔽暗箱」:当BASEPRI寄存器被设置为5时,究竟哪些中断会被屏蔽?PendSV和SysTick为何被刻意设为最低优先级?临界段保护函数里的_FROM_ISR
后缀又藏着怎样的底层玄机?
这篇文章将带你穿透中断管理的迷雾:从异常优先级阵列的寻址机制(4个异常统一成32BIT字寻址的底层设计),到BASEPRI、PRIMASK、FAULTMASK三大寄存器的「屏蔽金字塔」(BASEPRI按优先级阈值屏蔽,PRIMASK全局禁中断(除了 NMI 和 HardFault),FAULTMASK连HardFault都不放过),再到临界段保护的黄金法则(任务级与中断级临界区的混用禁忌)。我们会用STM32的实测案例拆解configMAX_SYSCALL_INTERRUPT_PRIORITY
的配置逻辑——为什么TIM3中断优先级设为4就不会被屏蔽,而TIM5设为5就会失效?为什么PendSV必须通过左移16BIT写入0xE000ED20地址?
无论你是被中断屏蔽规则绕到头晕的新手,还是想优化实时性的资深开发者,这里都有你需要的「避坑手册」:从寄存器底层操作到临界段代码模板,从portDISABLE_INTERRUPTS()
的本质到中断实验的现象解析。文末更有中断屏蔽优先级对照表、临界段保护使用禁忌清单等硬核干货,建议收藏备用——毕竟,真正懂FreeRTOS中断管理的开发者,才能让MCU在中断与任务的博弈中稳如泰山!
为什么这篇文章值得收藏?
- ❶ 深度解析BASEPRI/PRIMASK/FAULTMASK的底层机制,附Cortex-M内核手册对照;
- ❷ 拆解PendSV/SysTick优先级设置的「左移魔法」,揭秘0xE000ED20地址的寻址奥秘;
- ❸ 提供STM32中断屏蔽实验全流程,含TIM3/TIM5优先级配置与现象解析的实战案例。
本文从FreeRTOS的中断配置和临界段的角度出发,深入探讨了Cortex-M内核的中断屏蔽机制、FreeRTOS系统对中断的屏蔽理念、临界段的含义和种类及实现方式。最后通过测试FreeRTOS关闭中断和打开中断前后的实验现象,进一步理解FreeRTOS对中断的管理。全文一万多字,耐心看完,收获满满。如果对大家有帮助,可以请点个赞吗?谢谢啦,我是那种看到点赞就高兴一整天的那种,哈哈哈!
异常优先级阵列的寻址方式
下图是系统异常优先级阵列的一部分,需要表达的是,每个异常的优先级占8个BIT,也就是1个字节。(而这一个字节中也是高4BIT有效。)对于优先级阵列中,连续4个异常会统一成32BIT的字,统一寻址。也即是说,访问PendSV或者SysTick也是从0xE000 ED20处寻址,之后将要写的值左移16BIT或者24BIT。
PendSV和SysTick的中断优先级中断优先级的设置函数
BaseType_t xPortStartScheduler( void )
{
/* Make PendSV, CallSV and SysTick the same priroity as the kernel. */
*(portNVIC_SYSPRI2) |= portNVIC_PENDSV_PRI;
*(portNVIC_SYSPRI2) |= portNVIC_SYSTICK_PRI;
/* Start the timer that generates the tick ISR. Interrupts are disabled
here already. */
prvSetupTimerInterrupt();
/* Initialise the critical nesting count ready for the first task. */
uxCriticalNesting = 0;
/* Start the first task. */
prvPortStartFirstTask();
/* Should not get here! */
return 0;
}
代码中有两行需要注意:
*(portNVIC_SYSPRI2) |= portNVIC_PENDSV_PRI;
*(portNVIC_SYSPRI2) |= portNVIC_SYSTICK_PRI;
而代码中的左边地址:
#define portNVIC_SYSPRI2 ( ( volatile uint32_t *) 0xe000ed20 )
看到这个宏的地址就是我们前面说的0xE000 ED20。这两个中断在FreeRTOS中优先级最低。
用于屏蔽中断的寄存器
BASEPRI:
功能:BASEPRI(Base Priority Register)用于动态屏蔽优先级低于某个阈值的中断。它是一个 8 位寄存器,但在 Cortex-M3/M4/M7 中通常只使用低 3~4 位(取决于芯片实现)。
工作原理:当BASEPRI被设置为非零值N时,所有优先级数值大于等于N的中断都会被屏蔽(优先级数值越大,实际优先级越低)。优先级数值小于N的中断则不受影响。
PRIMASK:
功能:全局屏蔽所有可屏蔽中断(除了 NMI 和 HardFault)。
特点:比BASEPRI更激进,不区分中断优先级。
FAULTMASK:
功能:比PRIMASK更狠,连HardFault也屏蔽了,但NMI没有屏蔽。
只有 Reset 信号(硬件复位)和NMI能打破 FAULTMASK 的屏蔽。
中断配置宏
#define configPRIO_BITS 4 /* 15 priority levels */
位于FreeRTOSConfig.h,定义了MCU使用几位优先级,当然STM32使用的是4位。
/* The lowest interrupt priority that can be used in a call to a "set priority"
function. */
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 0xf
同样位于FreeRTOSConfig.h,最低优先级是15。很容易理解,一共4个BIT表示优先级,那就是说最低优先级是15。
/* Interrupt priorities used by the kernel port layer itself. These are generic
to all Cortex-M ports, and do not rely on any particular library functions. */
#define configKERNEL_INTERRUPT_PRIORITY ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
同样位于FreeRTOSConfig.h,设置内核中断优先级,前面说了,高4BIT有效,所以就左移4位。
#define portNVIC_PENDSV_PRI ( portMIN_INTERRUPT_PRIORITY << 16UL )
#define portNVIC_SYSTICK_PRI ( portMIN_INTERRUPT_PRIORITY << 24UL )
位于port.c,设置PendSV和SysTick的中断优先级。前面说,寻址的话从0xE000 ED20处寻址,之后将要写的值左移16BIT或者24BIT。就是在这里实现的。
/* The highest interrupt priority that can be used by any interrupt service
routine that makes calls to interrupt safe FreeRTOS API functions. DO NOT CALL
INTERRUPT SAFE FREERTOS API FUNCTIONS FROM ANY INTERRUPT THAT HAS A HIGHER
PRIORITY THAN THIS! (higher priorities are lower numeric values. */
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5
位于FreeRTOSConfig.h,设置FreeRTOS可以管理的最大优先级,也即是BASEPRI寄存器管理的阈值优先级,可以自由定义。这里的5表示,优先级数小于5的不归FreeRTOS管理,具体比方说Reset、NMI这些优先级很高的中断,是FreeRTOS不能管理的。
/* !!!! configMAX_SYSCALL_INTERRUPT_PRIORITY must not be set to zero !!!!
See http://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html. */
#define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
位于FreeRTOSConfig.h,configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY左移4BIT而来。低于此优先级的中断可以调用FreeRTOS的API函数,高于此优先级的中断不能调用,FreeRTOS也无权禁止。
在内核中的SCB->AIRCR寄存器中有定义中断优先级分组的字段。在STM32中定义了5个中断优先级分组,即NVIC_Priority_Group_0、NVIC_Priority_Group_1、NVIC_Priority_Group_2、NVIC_Priority_Group_3、NVIC_Priority_Group_4。在FreeRTOS中,我们使用NVIC_Priority_Group_4,也就是4BIT全部是抢占优先级,共有16个优先级。
由于STM32中,中断优先级分组定义为NVIC_Priority_Group_4,所以0是最高优先级,15是最低优先级。其中优先级0-4的中断不接收FreeRTOS管理,也不会因为执行FreeRTOS内核而超时;优先级在5-15的中断可以调用”FromISR“结尾的API函数,并且可以中断嵌套。
FreeRTOS的开关中断
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 )
下面看一下后面的函数。
static portFORCE_INLINE void vPortRaiseBASEPRI( void )
{
uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm
{
/* Set BASEPRI to the max syscall priority to effect a critical
section. */
msr basepri, ulNewBASEPRI
dsb
isb
}
}
上面代码中可以看到把configMAX_SYSCALL_INTERRUPT_PRIORITY宏的值写进了BASEPRI寄存器,也就是说,优先级低于这个等级的全部屏蔽。
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
__asm
{
/* Barrier instructions are not used as this function is only used to
lower the BASEPRI value. */
msr basepri, ulBASEPRI
}
}
入参为0,把0写入BASEPRI寄存器,就是把中断打开。
临界段代码
临界段代码也叫临界区,指的是必须完整运行不允许被打断的代码段。进入临界区之前,关闭中断,处理完临界段代码后打开中断。FreeRTOS与临界段保护的函数有以下4个。
#define taskENTER_CRITICAL() portENTER_CRITICAL()
#define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR()
#define taskEXIT_CRITICAL() portEXIT_CRITICAL()
#define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x )
没有_FROM_ISR
字段的就是任务级的临界段保护,带有_FROM_ISR
的是中断级临界段保护。二者是成对使用的。
任务级临界段保护
#define taskENTER_CRITICAL() portENTER_CRITICAL()
#define taskEXIT_CRITICAL() portEXIT_CRITICAL()
这两个成对使用。
#define portENTER_CRITICAL() vPortEnterCritical()
#define portEXIT_CRITICAL() vPortExitCritical()
中断级临界段保护
中断的优先级数必须要高于5,换句话说,优先级要低于5,这要分清楚。
#define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR()
#define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x )
接着看一下后面的宏函数
#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI()
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x)
vPortSetBASEPRI(x)是给BASEPRI设置一个值。
ulPortRaiseBASEPRI()的具体内容如下:
下面介绍一下中断临界段保护的使用方法。在一个中断服务函数中,可以写下面的代码:
staVal = taskENTER_CRITICAL_FROM_ISR();
/*巴拉巴拉,处理一些其他代码*/
taskEXIT_CRITICAL_FROM_ISR(staVal);
第一行是进入临界区,第二行是退出临界区。
测试中断实验
实验代码补充
使用打开和关闭中断的宏函数来测试中断打开和关闭后的不同现象,以此更深理解”FreeRTOS开关中断“。
代码贴在下面,大家直接复制就行。
这里是main.c的代码。
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "FreeRTOS.h"
#include "task.h"
#include "timer.h"
#include "stdio.h"
#define START_TASK_PRIO 1//任务优先级
#define START_STK_SIZE 128//任务堆栈大小
TaskHandle_t StartTask_Handler;//任务句柄
void start_task(void * pvParameters);//任务函数
#define INTERRUPT_TASK_PRIO 4//任务优先级
#define INTERRUPT_STK_SIZE 128//任务堆栈大小
TaskHandle_t INTERRUPTTask_Handler;//任务句柄
void interrupt_task(void * p_arg);//任务函数
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
delay_init(168);
uart_init(115200);
LED_Init();
TIM3_Int_Init(10000-1, 8400-1);
TIM5_Int_Init(10000-1, 8400-1);
//创建开始任务
xTaskCreate((TaskFunction_t) start_task,
(const char*) "start_task",
(uint16_t) START_STK_SIZE,
(void*) NULL,
(UBaseType_t)START_TASK_PRIO,
(TaskHandle_t*) &StartTask_Handler);
vTaskStartScheduler();
}
//开始任务函数
void start_task(void * pxParameters)
{
taskENTER_CRITICAL();//进入临界区
//创建interrupt任务
xTaskCreate((TaskFunction_t) interrupt_task,
(const char*) "interrupt_task",
(uint16_t) INTERRUPT_STK_SIZE,
(void*) NULL,
(UBaseType_t)INTERRUPT_TASK_PRIO,
(TaskHandle_t*) &INTERRUPTTask_Handler);
vTaskDelete(StartTask_Handler);//删除开始任务
taskEXIT_CRITICAL();//推出临界区
}
//中断测试任务函数
void interrupt_task(void * p_arg)
{
static u32 total_num = 0;
while(1)
{
total_num += 1;
if(total_num == 5)
{
printf("关闭中断……\r\n");
portDISABLE_INTERRUPTS();
delay_xms(5000);
printf("打开中断……\r\n");
portENABLE_INTERRUPTS();
}
LED1=!LED1;//DS1翻转
vTaskDelay(1000);
}
}
下面是timer.c的代码。
#include "timer.h"
#include "led.h"
#include "stdio.h"
//通用定时器3中断初始化
//arr:自动重装值。
//psc:时钟预分频数
//定时器溢出时间计算方法:Tout=((arr+1)*(psc+1))/Ft us.
//Ft=定时器工作频率,单位:Mhz
//这里使用的是定时器3!
void TIM3_Int_Init(u16 arr,u16 psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE); ///使能TIM3时钟
TIM_TimeBaseInitStructure.TIM_Period = arr; //自动重装载值
TIM_TimeBaseInitStructure.TIM_Prescaler=psc; //定时器分频
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up; //向上计数模式
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);//初始化TIM3
TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE); //允许定时器3更新中断
TIM_Cmd(TIM3,ENABLE); //使能定时器3
NVIC_InitStructure.NVIC_IRQChannel=TIM3_IRQn; //定时器3中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0x04; //抢占优先级1
NVIC_InitStructure.NVIC_IRQChannelSubPriority=0x00; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
//通用定时器5中断初始化
//arr:自动重装值。
//psc:时钟预分频数
//定时器溢出时间计算方法:Tout=((arr+1)*(psc+1))/Ft us.
//Ft=定时器工作频率,单位:Mhz
//这里使用的是定时器5!
void TIM5_Int_Init(u16 arr,u16 psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5,ENABLE); ///使能TIM5时钟
TIM_TimeBaseInitStructure.TIM_Period = arr; //自动重装载值
TIM_TimeBaseInitStructure.TIM_Prescaler=psc; //定时器分频
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up; //向上计数模式
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM5,&TIM_TimeBaseInitStructure);//初始化TIM5
TIM_ITConfig(TIM5,TIM_IT_Update,ENABLE); //允许定时器5更新中断
TIM_Cmd(TIM5,ENABLE); //使能定时器5
NVIC_InitStructure.NVIC_IRQChannel=TIM5_IRQn; //定时器5中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0x05; //抢占优先级1
NVIC_InitStructure.NVIC_IRQChannelSubPriority=0x00; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
//定时器3中断服务函数
void TIM3_IRQHandler(void)
{
if(TIM_GetITStatus(TIM3,TIM_IT_Update)==SET) //溢出中断
{
printf("TIM3输出……\r\n");
}
TIM_ClearITPendingBit(TIM3,TIM_IT_Update); //清除中断标志位
}
//定时器5中断服务函数
void TIM5_IRQHandler(void)
{
if(TIM_GetITStatus(TIM5,TIM_IT_Update)==SET) //溢出中断
{
printf("TIM5输出……\r\n");
}
TIM_ClearITPendingBit(TIM5,TIM_IT_Update); //清除中断标志位
}
下面是timer.h的代码
#ifndef _TIMER_H
#define _TIMER_H
#include "sys.h"
void TIM3_Int_Init(u16 arr,u16 psc);
void TIM5_Int_Init(u16 arr,u16 psc);
#endif
至于其余的代码,在我上一篇FreeRTOS的文章里有,大家可以去看看。
实现现象
LED等每隔1秒亮一次。但5秒后,LED熄灭。过了5秒,LED又开始亮灭闪烁,间隔1秒。
串口助手上的现象如下:
而就是在关闭中断期间,LED熄灭了5秒。
从实验现象理解前面的内容
回顾前面说过的内容:
BASEPRI的工作原理:当BASEPRI被设置为非零值N时,所有优先级数值大于等于N的中断都会被屏蔽(优先级数值越大,实际优先级越低)。优先级数值小于N的中断则不受影响。
FreeRTOS将N设置成5,所以TIM3优先级是4,自然不受影响;而TIM5的中断优先级是5,就会被屏蔽掉。
全文到此结束,欢迎点赞、收藏、转发!