ARM杂谈——临界段保护恢复的中断状态可靠吗

发布于:2025-05-15 ⋅ 阅读:(14) ⋅ 点赞:(0)

0 前言

在MCU中,临界段保护是指在多任务或多线程环境中,确保某段代码在执行时不会被其他任务或中断打断,从而避免数据竞争或不一致的问题。临界段保护通常用于共享资源的访问,如全局变量、硬件寄存器等。

我们有一些常用的临界段保护方法,但他们在任何场景下都可靠吗?

1 常用的临界段保护方法

PRIMASK是1bit的中断屏蔽寄存器,在置位时会阻止除不可屏蔽中断和HardFault异常外的所有中断。

在CMSIS库中,可以直接用__disable_irq()__enable_irq()来操作PRIMASK寄存器。
使用汇编指令MSRMRS可以访问该寄存器。

  • 直接关掉中断
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内核
在处理器接受了一个异常后,寄存器组中的一些寄存器(R0R1R2R3R12R14)、返回地址(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
在这里插入图片描述

翻译过来就是:
结合使用 MRSMSR 作为读-修改-写序列的一部分,用于更新 PSR,例如清除 Q 标志。
在进程交换代码中,被换出进程的程序员模型状态必须被保存,包括相关的 PSR 内容,同样,被换入进程的状态也必须被恢复。 这些操作在状态保存指令序列中使用 MRS,在状态恢复指令序列中使用 MSR

显然,ARM要求MRSMSR必须成对使用。

因此我认为前文所说的 “在中断中屏蔽全局中断,而不恢复调用中断前的屏蔽状态就退出中断” 的使用方式是不被ARM允许的,原因之一就是会破坏临界段保护。

5 参考资料


网站公告

今日签到

点亮在社区的每一天
去签到