0 前言
在MCU中,临界段保护是指在多任务或多线程环境中,确保某段代码在执行时不会被其他任务或中断打断,从而避免数据竞争或不一致的问题。临界段保护通常用于共享资源的访问,如全局变量、硬件寄存器等。
我们有一些常用的临界段保护方法,但他们在任何场景下都可靠吗?
1 常用的临界段保护方法
PRIMASK
是1bit的中断屏蔽寄存器,在置位时会阻止除不可屏蔽中断和HardFault异常外的所有中断。
在CMSIS库中,可以直接用__disable_irq()
和__enable_irq()
来操作PRIMASK
寄存器。
使用汇编指令MSR
和MRS
可以访问该寄存器。
- 直接关掉中断
void main()
{
/* 代码段1 */
__disable_irq(); // 关闭全局中断
/* 需要临界段保护的代码 */
__enable_irq(); // 打开全局中断
/* 代码段2 */
}
在执行临界段前后,中断使能状态应该保持一致。
而这段代码如果在__disable_irq()
的时候全局中断是关闭的,那么在后面调用__enable_irq()
的时候就会打开全局中断,改变了系统的中断屏蔽状态。
- 临界段后,恢复中断状态
void main()
{
/* 代码段1 */
uint32_t intFlag = __get_PRIMASK(); // 获取当前中断状态
__disable_irq(); // 关闭全局中断
/* 需要临界段保护的代码 */
__set_PRIMASK( intFlag ); // 恢复之前的中断状态
/* 代码段2 */
}
这段代码先保存了当前的中断屏蔽状态,在执行临界段后恢复了之前的中断屏蔽状态,保证了执行临界段前后中断屏蔽状态的一致性。
但这样是否会也会存在一些问题呢?
2 问题:上面方法恢复的中断状态可靠吗?
在上面的代码中:
(1)使用了一个变量intFlag
保存当前的中断屏蔽状态
(2)使用了__disable_irq()
关闭全局中断
(3)执行临界段
(4)使用__set_PRIMASK( intFlag )
恢复中断。
如果在步骤(1)和步骤(2)之间,一个中断被响应,并且在该中断中关闭了全局中断,那么在步骤(4)恢复中断的时候,就会错误的将中断开启。
这么做当然很蠢,因为如果一个中断希望获取临界段保护,那么在它关闭中断后应该恢复之前的状态。
但我想知道的是:如果有一个中断,他的目标就是触发的时候禁止全局中断,这样会导致临界段保护恢复一个错误的中断状态吗?
上述情况的前提:
1.在临界段之前全局中断是使能的
——否则也不会响应中断,更不会关掉全局中断了
.
2.Cortex-M内核
在处理器接受了一个异常后,寄存器组中的一些寄存器(R0
、R1
、R2
、R3
、R12
、R14
)、返回地址(PC
)以及程序状态寄存器(xPSR
)会被自动压入当前栈空间里,以便在退出中断后恢复运行环境,使被中断的程序正确执行。
.
而保存中断屏蔽状态的PRIMASK
寄存器中是没有被压栈的。因此,若在中断处理期间修改了PRIMASK寄存器,从而改变了中断屏蔽状态,那么在退出中断后,中断屏蔽状态将保持与中断处理期间修改后的状态一致。
.
而在ARM7TDMI内核中,中断屏蔽状态保存在其CPSR
寄存器中,而该寄存器在处理器接受异常后会被压栈,因此在退出异常后CPSR
寄存器会被恢复为中断处理前的状态。
由此可见,在Cortex-M处理器中,若某个中断的意图是关闭全局中断,则可能导致临界段保护机制恢复至错误的中断状态。
为了进一步分析,我想看一下成熟的代码是怎么处理该问题的。
3 FreeRTOS的临界段保护操作
注:这里用的是FreeRTOS在Cortex M0/M0+上的接口文件,Cortex M3及以上内核的有不同的实现方法。
3.1 不可在中断中使用的临界段保护
taskENTER_CRITIAL()
进入临界段保护
void vPortEnterCritial( void )
{
portDISABLE_INTERRUPTS(); // __disable_irq(),即关闭全局中断
uxCriticalNesting++; // 支持嵌套使用,调用一次计数变量+1
__dsb( portSY_FULL_READ_WRITE ); // portSY_FULL_READ_WRITE = 15
__isb( portSY_FULL_READ_WRITE ); // portSY_FULL_READ_WRITE = 15
}
DSB
:数据同步屏障。确保下一条指令执行前,所有的存储器访问都已完成。
例如对存储器映射切换寄存器变成后,为了确保写操作完成并且存储器配置已经得到更新,在进行下一步以前,应该使用DSB
命令。
ISB
:清除流水线。确保在执行新指令前,之前的所有指令都已完成。
例如在使用MSR
指令改变CONTROL
寄存器的值之后,应该使用ISB
,以确保接下来的操作使用已经更新的设置。
taskEXIT_CRITIAL()
退出临界段保护
void vPortExitCritial( void )
{
configASSERT( uxCriticalNesting );
uxCriticalNesting--; // 支持嵌套使用,调用一次计数变量-1
if( uxCriticalNesting == 0 ) // 支持嵌套使用,在计数变量为0时打开终端
{
portENABLE_INTERRUPTS(); // __enable_irq(),即打开全局中断
}
}
3.2 可在中断中使用的临界段保护
taskENTER_CRITIAL_FROM_ISR()
进入临界段保护(可在中断中使用)
__asm uint32_t ulSetInterruptMaskFromISR( void )
{
mrs r0, PRIMASK // 将PRIMASK寄存器的值保存到r0寄存器中(即返回值)
cpsid i // 关闭中断
bx lr // 将LR中的返回地址装入PC,即返回到调用程序
}
注:C语言在ARM内核上函数参数的传递通常遵循ARM Architecture Procedure Call Standard (AAPCS),该标准要求第一个参数通过R0
寄存器传递。
taskEXIT_CRITIAL_FROM_ISR()
进入临界段保护(可在中断中使用)
__asm uint32_t vClearInterruptMaskFromISR( uint32_t ulMask )
{
msr PRIMASK, r0 // 将r0寄存器的值(即ulMask)赋给PRIMASK寄存器
bx lr // 将LR中的返回地址装入PC,即返回到调用程序
}
注:AAPCS标准要求返回值通过R0
寄存器传递。
MRS
:将特殊寄存器送到寄存器
MSR
:将寄存器送到特殊寄存器
看来FreeRTOS也是用的这种方式来实现临界段保护的,即先保存当前状态,再禁止中断,再恢复状态。
FreeRTOS不可能没考虑到这个问题,因此看看ARM对MSR
这个指令是怎么说的,有没有可能是从内核上保证了这个问题呢?
4 ARM官方对MRS指令的描述
developer.arm.com / M33 User Guide - MRS
翻译过来就是:
结合使用MRS
和MSR
作为读-修改-写序列的一部分,用于更新PSR
,例如清除Q
标志。
在进程交换代码中,被换出进程的程序员模型状态必须被保存,包括相关的PSR
内容,同样,被换入进程的状态也必须被恢复。 这些操作在状态保存指令序列中使用MRS
,在状态恢复指令序列中使用MSR
。
显然,ARM要求MRS
和MSR
必须成对使用。
因此我认为前文所说的 “在中断中屏蔽全局中断,而不恢复调用中断前的屏蔽状态就退出中断” 的使用方式是不被ARM允许的,原因之一就是会破坏临界段保护。
5 参考资料
- 《ARM Cortex-M0/M0+ 权威指南 第二版》
- Arm Developer