FreeRTOS内核详解(1) —— 临界段保护原理

什么是临界段

临界段用一句话概括就是一段在执行的时候不能被中断的代码段。 在 FreeRTOS 里面,这个临界段最常出现的就是对全局变量的操作,由于不同任务间可以切换运行,当一个任务在访问某个全局变量时,这时其他任务切入,改变了该全局变量,再回到上个任务时访问该变量已经不是当时的值,这种情况可能会导致不可意料的后果。

FreeRTOS 源码中就有多处临界段的处理,跟 FreeRTOS 一样,uCOS-II 和 uCOS-III 源码中都是有临界段的,而 RTX 的源码中不存在临界段。另外,除了 FreeRTOS 操作系统源码所带的临界段以外,用户写应用的时候也有临界段的问题,比如以下两种:
读取或者修改变量(尤其是任务间通信的全局变量)的代码。
调用公共函数的代码,特别是不可重入函数,如果多个任务都访问这个函数,结果是可想而知的。
注意:由于临界段中需要关闭中断,因此会影响某些紧急中断的处理速度,同时由于临界段中关闭了任务调度,也会影响系统实时性,总之临界段的执行时间要求越短越好。

Cortex-M 内核快速关中断指令

为了快速地开关中断, Cortex-M 内核专门设置了一条 CPS 指令,有 4 种用法

CPSID I ;PRIMASK=1 ;关中断
CPSIE I ;PRIMASK=0 ;开中断
CPSID F ;FAULTMASK=1 ;关异常
CPSIE F ;FAULTMASK=0 ;开异常

PRIMASKFAULTMAST 是 Cortex-M 内核 里面三个中断屏蔽寄存器中的两个,还有一个是 BASEPRI,有关这三个寄存器的详细用法
在这里插入图片描述
在 FreeRTOS 中,对中断的开和关是通过操作 BASEPRI 寄存器来实现的,即大于等于 BASEPRI 的值的中断会被屏蔽,小于 BASEPRI 的值的中断则不会被屏蔽,不受FreeRTOS 管理。用户可以设置 BASEPRI 的值来选择性的给一些非常紧急的中断留一条后路。

进出临界段源码分析

进入和退出临界段的宏在 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 )

进入和退出临界段的宏分中断保护版本和非中断版本, 但最终都是通过开/关中断来实现。有关开/光中断的底层代码我们已经讲解,那么接下来的退出和进入临界段的代码配套注释来理解即可。

进入临界段

进入临界段,不带中断保护版本且不能嵌套的代码实现

1. 不带中断保护版本

/* ==========进入临界段, 不带中断保护版本,不能嵌套=============== */
/* 在 task.h 中定义 */
#define taskENTER_CRITICAL() portENTER_CRITICAL()

/* 在 portmacro.h 中定义 */
#define portENTER_CRITICAL() vPortEnterCritical()

/* 在 port.c 中定义 */
void vPortEnterCritical( void )
{
	portDISABLE_INTERRUPTS();
	uxCriticalNesting++; 
	
	if ( uxCriticalNesting == 1 ) 
	{
		configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
	}
}

/* 在 portmacro.h 中定义 */
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()

/* 在 portmacro.h 中定义 */
static portFORCE_INLINE void vPortRaiseBASEPRI( void )
{
	uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
	
	__asm
	{
		msr basepri, ulNewBASEPRI
		dsb
		isb
	}
}

//FreeRTOS_config.h中
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY			15                      //中断最低优先级
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY	10                       //系统可管理的最高中断优先级
#define configKERNEL_INTERRUPT_PRIORITY 		( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 	( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )

uxCriticalNesting 是在 port.c 中定义的静态变量,表示临界段嵌套计数器 , 默 认 初 始 化 为 0xaaaaaaaa , 在 调 度 器 启 动 时 会 被 重 新 初 始 化 为 0 :vTaskStartScheduler()->xPortStartScheduler()->uxCriticalNesting = 0。

如果 uxCriticalNesting 等于 1,即一层嵌套,要确保当前没有中断活跃,即内核外设 SCB 中的中断和控制寄存器 SCB_ICSR 的低 8 位要等于 0。

uxCriticalNesting这个变量比较重要,用于临界段的嵌套计数。初学的同学也许会问这里直接的开关中断不就可以了吗,为什么还要做一个嵌套计数呢?主要是因为直接的开关中断方式不支持在开关中断之间的代码里再次执行开关中断的嵌套处理,假如当前们的代码是关闭中断的,嵌套了一个含有开关中断的临界区代码后,退出时中断就成开的了,这样就出问题了。通过嵌套计数就有效地防止了用户嵌套调用函数taskENTER_CRITICAL 和 taskEXIT_CRITICAL 时出错。
在这里插入图片描述

2. 带中断保护版本

/* ==========进入临界段,带中断保护版本,可以嵌套=============== */
/* 在 task.h 中定义 */
#define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR()

/* 在 portmacro.h 中定义 */
#define portSET_INTERRUPT_MASK_FROM_ISR()
ulPortRaiseBASEPRI()

/* 在 portmacro.h 中定义 */
static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void )
{
	uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
	
	__asm
	{
		mrs ulReturn, basepri
		msr basepri, ulNewBASEPRI
		dsb
		isb
	}
	
	return ulReturn;
}

通过上面的源码实现可以看出,FreeRTOS 的开关全局中断是通过操作寄存器 basepri 实现的。初学的同学也许会问,这里怎么没有中断嵌套计数了呢?是的,这里换了另外一种实现方法,通过保存和恢复寄存器 basepri 的数值就可以实现嵌套使用。如果大家研究过 uCOS-II 或者 III 的源码,跟这里的实现方式是一样的。

/*===========================================可屏蔽的中断优先级配置====================================================*/
/*
 * 用于配置STM32的特殊寄存器basepri寄存器的值,用于屏蔽中断,当大于basepri值的优先级的中断将被全部屏蔽。basepri只有4bit有效,
 * 默认只为0,即全部中断都没有被屏蔽。configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY配置为:5,意思就是中断优先级大于5的中断都被屏蔽。
 * 当把配置好的优先级写到寄存器的时候,是按照8bit来写的,所以真正写的时候需要经过转换,公式为:
 * ((priority << (8 - __NVIC_PRIO_BITS)) & 0xff),其中的priority就是我们配置的真正的优先级。经过这个公式之后得到的是下面的这个宏:
 * configMAX_SYSCALL_INTERRUPT_PRIORITY
 *
 * 在FreeRTOS中,关中断是通过配置basepri寄存器来实现的,关掉的中断由配置的basepri的值决定,小于basepri值的
 * 中断FreeRTOS是关不掉的,这样做的好处是系统设计者可以人为的控制那些非常重要的中断不能被关闭,在紧要的关头必须被响应。
 * 而在UCOS中,关中断是通过控制PRIMASK来实现的,PRIMASK是一个单1的二进制位,写1则除能除了NMI和硬 fault的所有中断。当UCOS关闭
 * 中断之后,即使是你在系统中设计的非常紧急的中断来了都不能马上响应,这加大了中断延迟的时间,如果是性命攸关的场合,那后果估计挺严重。
 * 相比UCOS的关中断的设计,FreeRTOS的设计则显得人性化很多。
 *
 */
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY    2
#define configMAX_SYSCALL_INTERRUPT_PRIORITY     ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )

退出临界段

退出临界段,不带中断保护版本且不能嵌套的代码实现

1. 不带中断保护的版本

/* ==========退出临界段,不带中断保护版本,不能嵌套=============== */
/* 在 task.h 中定义 */
#define taskEXIT_CRITICAL() portEXIT_CRITICAL()

/* 在 portmacro.h 中定义 */
#define portEXIT_CRITICAL() vPortExitCritical()

/* 在 port.c 中定义 */
void vPortExitCritical( void )
{
	configASSERT( uxCriticalNesting );
	uxCriticalNesting--;
	if ( uxCriticalNesting == 0 )
	{
		portENABLE_INTERRUPTS();
	}
}

/* 在 portmacro.h 中定义 */
#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 )

/* 在 portmacro.h 中定义 */
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
	__asm
	{
		msr basepri, ulBASEPRI
	}
}

2. 带中断保护的版本

/* ==========退出临界段,带中断保护版本,可以嵌套=============== */
/* 在 task.h 中定义 */
#define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x )

/* 在 portmacro.h 中定义 */
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x)

/* 在 portmacro.h 中定义 */
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
	__asm
	{
		msr basepri, ulBASEPRI
	}
}

应用

在 FreeRTOS 中,对临界段的保护出现在两种场合,一种是在中断场合一种是在非中断场合

/* 中断场合*/
{
	uint32_t ulReturn;
	/* 进入临界段 */
	ulReturn = taskENTER_CRITICAL_FROM_ISR();
	
	/* 临界段代码 */
	
	/* 退出临界段 */
	taskEXIT_CRITICAL_FROM_ISR( ulReturn );
}

/* 非中断场合 */
{
	/* 进入临界段 */
	taskENTER_CRITICAL();
	
	/* 临界段代码 */
	
	/* 退出临界段*/
	taskEXIT_CRITICAL();
}

参考鸣谢:
https://www.itdaan.com/blog/2017/07/21/24a1a4f2e6d7df68fbedf353e7096082.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吾爱技术圈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值