写在前面
本文主要是对于 FreeRTOS 中临界段的保护的详细解释,代码大部分参考了野火 FreeRTOS 教程配套源码,作了一小部分修改。
一、什么是临界段
临界段就是一段在执行的时候不能被中断的代码段。
**临界段(Critical Section)**是指在多任务或多线程环境下,一段代码或一组代码,在执行期间对共享资源进行访问或操作的临界区域。临界段中的代码只能被一个任务或线程单独执行,以确保对共享资源的访问是正确和一致的。
二、为什么要保护临界段
在进入临界段之前,任务或线程需要获得对应的同步机制(如互斥锁、信号量等)的控制权。一旦获得控制权,任务或线程可以安全地访问临界段中的共享资源。在临界段执行完成后,任务或线程释放同步机制的控制权,让其他任务或线程可以进入临界段执行。
三、要保护临界段,我们需要做什么?
1. 了解临界段什么时候会被打断
- 系统调度
- 外部中断
- 因此,对临界段的保护的实质——对中断的开和关的控制。
2. 了解Cortex-M内核中断指令的开关指令
①一些缩写的解释
- NMI:Non-Maskable Interrupt(不可屏蔽中断)
- CPSID:Change Program Status and Interrupt Disable(修改程序状态并禁用中断)
- CPSID 后面跟着的 I 或者 F 分别表示 Interrupt(普通的可屏蔽中断)和 FAULT Interrupt(异常中断)
- Primask是一个特殊的寄存器,全称是 “PrIMask”,其含义是 “Priority Mask”,即优先级屏蔽
- basepri"代表的是"Base Priority",即基本优先级
②中断操作指令
CPSID I ;PRIMASK=1 ;关中断
CPSIE I ;PRIMASK=0 ;开中断
CPSID F ;FAULTMASK=1 ;关异常
CPSIE F ;FAULTMASK=0 ;开异常
③寄存器说明
3. 可嵌套的中断开启与关闭函数对保护临界区的作用
野火的教程在这方面语焉不详,笔者自行理解了一下:
所谓可嵌套的中断操作函数,指的是该中断函数有返回值,返回该函数操作前的中断屏蔽等级,便于其他函数使用该返回值恢复现场。
对于可嵌套的中断操作函数在保护临界区方面有如下作用:
进入临界区时,关闭中断,此时可嵌套的关闭中断函数可以保存关闭前的屏蔽等级,在退出临界区可以使用该保存的屏蔽等级进行屏蔽等级的恢复设置。
代码例程如下:
// 全局共享资源
volatile int counter = 0;
// 中断处理程序
void interrupt_handler()
{
// 进入临界区(关闭中断)
Interruption_Mask_Level = disable_interrupt();
// 访问共享资源
counter++;
// 退出临界区(打开中断)
enable_interrupt(Interruption_Mask_Level);
}
// 主程序
int main()
{
// 启用中断
while (1)
{
// 执行其他任务
}
return 0;
}
四、临界段相关操作函数详解
主要分为三类函数:
- 关中断函数
- 开中断函数
- 进出临界段函数
其中开关中断函数被进出临界段函数调用。
1. 关中断函数
- 宏定义
//关中断函数
//不带返回值的关中断函数,不能嵌套,不能在中断里面使用
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
//带返回值的关中断函数,可以嵌套,可以在中断里面使用
#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI()
//中断屏蔽等级的设置
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 191 /* 高四位有效,即等于0xb0,或者是11 */
- 不带返回值的关中断函数,原理就是改写 BASEPRI 寄存器的值屏蔽指定等级的中断
//不带返回值,不能嵌套
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 //宏定义中 basepri = 11,也就是优先级大于11的中断不被响应
dsb
isb
}
}
- 带返回值的关中断函数,原理就是先记录 BASEPRI 寄存器的值作为函数返回值,然后再改写 BASEPRI 寄存器的值屏蔽指定等级的中断
static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void )
{
uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm
{
/* Set BASEPRI to the max syscall priority to effect a critical
section. */
mrs ulReturn, basepri //先把basepri中的值保存在返回值中
msr basepri, ulNewBASEPRI //再设置basepri的值
dsb
isb
}
return ulReturn;
}
2. 开中断函数
- 宏定义
- 不可嵌套的开中断函数就是将 BASEPRI 寄存器的值设置为 0,使任何一个中断都不被屏蔽
- 可嵌套的开中断函数就是将 BASEPRI 寄存器的值设置为 可嵌套的关中断函数的返回值,使中断屏蔽等级在进出临界区后和之前保持一致
//开中断函数
// 不可嵌套
#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 )
// 可嵌套
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x)
- 具体函数实现
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
}
}
3. 进出临界段函数
进出临界段函数抽象了好几层,如下:
task.h
可以看到,不带中断保护版本多抽象了一层,而带中断保护版本直接对开关中断进行操作
/* ==========进入临界段,不带中断保护版本,不能嵌套=============== */
#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 )
portmacro.h
- 下面是不带中断保护版本的进出临界段函数的抽象:
//进出临界段
#define portENTER_CRITICAL() vPortEnterCritical()
#define portEXIT_CRITICAL() vPortExitCritical()
展示不带中断保护版本的进出临界段函数代码:
vPortEnterCritical(): 这个函数的作用是进入临界区。它首先调用portDISABLE_INTERRUPTS()函数来禁用中断,阻止其他中断干扰。然后,它将uxCriticalNesting计数器加一,表示进入了一个新的临界区。如果uxCriticalNesting计数器的值为1,即当前进入的是第一个临界区,那么可以使用configASSERT()来进行断言检查,确保该函数不是在中断上下文中调用。
vPortExitCritical(): 这个函数的作用是退出临界区。它首先检查uxCriticalNesting计数器的值,确保当前确实在一个临界区中。然后,它将uxCriticalNesting计数器减一,表示退出当前的临界区。如果uxCriticalNesting计数器的值减为0,即没有进入任何临界区了,那么可以调用portENABLE_INTERRUPTS()函数来重新启用中断,允许其他中断恢复执行。
//进入临界段
void vPortEnterCritical( void )
{
portDISABLE_INTERRUPTS();
uxCriticalNesting++;
/* This is not the interrupt safe version of the enter critical function so
assert() if it is being called from an interrupt context. Only API
functions that end in "FromISR" can be used in an interrupt. Only assert if
the critical nesting count is 1 to protect against recursive calls if the
assert function also uses a critical section. */
if( uxCriticalNesting == 1 )
{
//configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
}
}
//退出临界段
void vPortExitCritical( void )
{
//configASSERT( uxCriticalNesting );
uxCriticalNesting--;
if( uxCriticalNesting == 0 )
{
portENABLE_INTERRUPTS();
}
}
- 而带中断保护版本的就是可嵌套开关中断函数:
//进出临界段
//带返回值的关中断函数,可以嵌套,可以在中断里面使用
#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI()
//可嵌套开中断函数
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x)
五、临界段操作函数如何使用
1. 中断场合
2. 非中断场合
后记
如果您觉得本文写得不错,可以点个赞激励一下作者!
如果您发现本文的问题,欢迎在评论区或者私信共同探讨!
共勉!