freeRtos学习笔(3)临界区管理

freeRtos学习笔记

freeRtos临界区管理

freeRtos临界区

代码的临界段也称为临界区,一旦这部分代码开始执行,则不允许任何中断打断。为确保临界段代码的执行不被中断,在进入临界段之前须关中断,而临界段代码执行完毕后,要立即开中断。

FreeRTOS 的源码中有多处临界段的地方, 临界段虽然保护了关键代码的执行不被打断, 但也会影响系统的实时性。比如此时某个任务正在调用系统 API 函数,而且此时中断正好关闭了,也就是进入到了临界区中,这个时候如果有一个紧急的中断事件被触发,这个中断就不能得到及时执行,必须等到中断开启才可以得到执行, 如果关中断时间超过了紧急中断能够容忍的限度, 危害是可想而知的。
所以,操作系统的中断在某些时候会有适当的中断延迟,因此调用中断屏蔽函数进入临界段的时候,也需快进快出。 当然 FreeRTOS 也能允许一些高优先级的中断不被屏蔽掉,能够及时做出响应,不过这些中断就不受系统管理,也不允许调用 FreeRTOS 中与中断相关的任何 API 函数接口。
FreeRTOS 源码中就有多处临界段的处理, 跟 FreeRTOS 一样, uCOS-II 和 uCOS-III 源码中都是有临界段的, 而 RTX操作系统 的源码中不存在临界段。 另外, 除了 FreeRTOS 操作系统源码所带的临界段以外,用户写应用的时候也有临界段的问题,比如以下两种:

  • 读取或者修改变量(特别是用于任务间通信的全局变量)的代码,一般来说这是最常见的临界代码。
  • 硬件资源或者调用公共函数的代码,特别是不可重入的函数(函数使用了全局变量或者局部静态变量),如果多个任务都访问这个函数,结果是可想而知的。总之, 对于临界段要做到执行时间越短越好, 否则会影响系统的实时性。

临界区处理方法

进入临界段前操作寄存器 basepri 关闭了所有小于等于宏定义 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
所定义的中断优先级, 这样临界段代码就不会被中断干扰到, 而且实现任务切换功能的 PendSV 中断和滴答定时器中断是最低优先级中断, 所以此任务在执行临界段代码期间是不会被其它高优先级任务打断的。
退出临界段时重新操作 basepri 寄存器,即打开被关闭的中断.大于configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
所定义的中断优先级的中断不受freertos管理,在临界区仍然可以响应,但是中断服务函数中不可以调用freertos的API。

taskENTER_CRITICAL()/* 进入临界区 */
/* 临界段代码 */
taskEXIT_CRITICAL();/* 退出临界区 */

如果使用单纯的开关中断,则在以下临界区嵌套情况下,FunctionC()函数本意是要临界保护的,但是在FunctionB()中已退出临界区,和设计本意不同。

void FunctionB()
{
    taskENTER_CRITICAL()/* 进入临界区 */
    /* 临界段代码 */
    taskEXIT_CRITICAL();/* 退出临界区 */
}

void FunctionA()
{
    taskENTER_CRITICAL(); /* 进入临界区 */
    FunctionB();          /* 临界段代码--调用函数 B */
    FunctionC();          /* 临界段代码--调用函数 C */
    taskEXIT_CRITICAL();  /* 退出临界区 */C
}

因此为了避免这种情况,在taskENTER_CRITICAL()和taskEXIT_CRITICAL()函数中存在一个全局变量,记录嵌套次数,因此需要注意进入和退出临界区函数必须要成对出现。

#define taskENTER_CRITICAL() portENTER_CRITICAL()
#define taskEXIT_CRITICAL() portEXIT_CRITICAL()
#define portENTER_CRITICAL() vPortEnterCritical()
#define portEXIT_CRITICAL() vPortExitCritical()
void vPortEnterCritical( void )
{
    portDISABLE_INTERRUPTS();
    uxCriticalNesting++;
    if( uxCriticalNesting == 1 )
    {
        configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
    }
}
/*-----------------------------------------------------------*/
void vPortExitCritical( void )
{
    configASSERT( uxCriticalNesting );
    uxCriticalNesting--;
    if( uxCriticalNesting == 0 )
    {
        portENABLE_INTERRUPTS();
    }
}

一般在使用freertos时,NVIC建议配置为只有抢占优先级,中断是可以嵌套的,但是进入临界区后就关闭了freeRtos可以管理的中断,中断服务函数中还需要记录临界区嵌套次数吗?
是需要的,如果在中断服务函数中调用其他函数,其他函数中也有可能会有临界区保护,因此一样需要记录临界区嵌套次数。freertos中中断服务函数中的API一般都是FROM_ISR结尾的,临界区管理也是这样的。

UBaseType_t uxSavedInterruptStatus;
uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR(); /* 进入临界区 */
/* 临界区代码 */
taskEXIT_CRITICAL_FROM_ISR( uxSavedInterruptStatus ); /* 退出临界区 */

注意和taskENTER_CRITICAL()和taskEXIT_CRITICAL()函数中存在一个全局变量,记录嵌套次数不同,taskENTER_CRITICAL_FROM_ISR()和taskEXIT_CRITICAL_FROM_ISR( uxSavedInterruptStatus )为了提高执行速度,这里采用了另一种方式,使用一个局部变量记录进入临界区时basepri寄存器的值,在退出临界区时,恢复寄存器basepri,从而也达到了嵌套管理的目的,因此进入临界区和退出临界区函数也必须要成对出现。

例子1 调用不可重入函数需进入临界区

/*!
  * @brief    LED闪烁任务
  *
  * @param    pvParame 
  *
  * @return   无
  *
  * @note     
  *
  * @see      
  */
void vTaskLED(void* pvParame)
{
    while(1)
    {
        HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
        /* 进入临界区 */
        taskENTER_CRITICAL();
        printf("LED TASK IS RUNING\r\n");
        /* 退出临界区 */
        taskEXIT_CRITICAL();
        vTaskDelay(50);
    }
}

/*!
  * @brief    printf打印任务
  *
  * @param    pvParame 
  *
  * @return   无
  *
  * @note     
  *
  * @see      
  */
void vTaskPrintf(void* pvParame)
{
    while(1)
    {
        /* 进入临界区 */
        taskENTER_CRITICAL();
        printf("Printf task is runing\r\n");
        printf("Printf task is runing\r\n");
        printf("Printf task is runing\r\n");
        printf("Printf task is runing\r\n");
        printf("Printf task is runing\r\n");
        /* 退出临界区 */
        taskEXIT_CRITICAL();
        vTaskDelay(100);
    }
}



/* 任务句柄 */
static TaskHandle_t ledTaskHandle;
static TaskHandle_t printfTaskHandle;
    
/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  HAL_Init();

  SystemClock_Config();

  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART1_UART_Init();
  MX_CRC_Init();
  MX_RTC_Init();
  USART1->SR;
    
  /* 初始化 Event Recorder 组件 方便MDK调试 */
  EvrFreeRTOSSetup(1);

  
  BaseType_t err;
  
  /* 创建LED闪烁任务 */
  err = xTaskCreate(vTaskLED, "LED TASK", 128, 0, 10, &ledTaskHandle);
  
  if(pdTRUE != err)
  {
    
  }

  /* 创建printf打印任务 */
  err = xTaskCreate(vTaskPrintf, "PRINTF TASK", 128, 0, 9, &printfTaskHandle);
  
  if(err != pdTRUE)
  {
  
  }
  
  /* 启动任务调度器 */
  vTaskStartScheduler();

  while (1)
  {

  }

}
  

上述例子中创建了两个任务,LED闪烁和printf打印任务,两个任务都调用了printf函数,printf函数为不可重入函数,如果不对printf进行临界段保护,则打印结果就会如下所示错乱。

Printf task iLED TASK IS RUNING
s runing
LED TASK IS RUNING
LED TASK IS RUNING
Printf task is runing
Printf task is runing
Printf task is runing
Printf task is runing
Printf task is runing

正常打印结果

LED TASK IS RUNING
Printf task is runing
Printf task is runing
Printf task is runing
Printf task is runing
Printf task is runing
LED TASK IS RUNING
不可重入函数

不可重入函数即 使用了全局变量或局部静态变量并对该变量进行了写操作的函数都为不可重入函数,假设一个函数使用了局部静态变量,任务A调用了这个函数修改了该局部静态变量,接着任务B运行,任务B也调用了这个函数修改了该局部静态变量,当任务A重新运行时,该局部静态变量的值已经发送了变化,如果该函数需要根据该局部静态变量的值进行一些处理,则可能会造成任务A异常。

保护全局变量进入临界区

原子操作

在上一个世纪,人们认为原子是组成物质的最小颗粒 ,在计算机领域引用了这个术语,原子操作即不可分割的,在执行完毕之前不会被任何其它任务或事件中断

在cortex-M中,32位的变量读和写操作都是原子操作,也就是只要一条指令就可以了。如果对全局变量都是原子操作,还进什么临界区啊!
读和写都是原子操作,但是在程序中经常会出现先读后写的情况,两个原子操作加一起就不是原子操作了。
在这里插入图片描述
上述为stm32f103 在mdk -o3 中一个变量自减操作,会编译成4行汇编,如果第二行汇编执行完后,进入了中断,在中断中修改了这个变量,然后退出中断后,接着执行后两行汇编,就会导致中断中的修改无效。在裸机情况下,为了避免该情况,也应该关中断进临界区(有些小伙伴就说,自己从来都没进临界区,代码不照样跑的好好的。虽然上述情况出现的概率很小,但是不是不会发生的,要尽量避免)。裸机的情况下都需要进入临界区,使用RTOS时就更需要注意了,RTOS任务间也会进行抢占,上述情况的发生概率一般会比裸机高许多。

为什么对全局变量进行临界区保护?除了上面的原因,最为常见的是逻辑上,我们认为是原子的,但是实际用代码表达时,却必须用多条语句变成了非原子操作。例如在任务A中逐字符接收数据并放入缓冲区,缓冲区满后对之前数据进行覆盖;在任务B中检查接收缓冲区满后将数据全部发送出去。任务B将全部数据发送出去在逻辑上就是一个原子操作,但是在代码中却需要很多条语句,如果不进入临界区,发送一半时,缓冲区又通过任务A接收到了很多数据,覆盖了老数据,就会造成任务B发送了一半老数据又发送了一半新数据。

《安富莱 STM32-V6 开发板 FreeRTOS 教程》
本文参考 freertos官方文档 https://freertos.org/a00110.html

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值