FreeRTOS 临界区——精炼笔记
2026/9/13 4:24:43 网站建设 项目流程

FreeRTOS临界区详解:原理、任务优先级区别及STM32使用注意事项

前言

在学习 FreeRTOS 时,很容易把“临界区”和“提高任务优先级”混为一谈。

例如:Task0负责创建 Task1、Task2、Task3。为了避免创建 Task1 后立即切换到高优先级任务,是否应该使用临界区?

答案是:

临界区确实可以暂时阻止任务切换,但它的主要用途是保护短小的共享数据操作,而不是控制多个任务的创建顺序。

本文介绍临界区的作用、实现机制、适用场景,以及它与任务优先级、互斥锁之间的区别。


一、什么是临界区

临界区(Critical Section)是一段不能被其他并发执行单元干扰的代码。

在 FreeRTOS 任务中,使用下面两个宏进入和退出临界区:

taskENTER_CRITICAL();/* 需要保护的代码 */taskEXIT_CRITICAL();

进入临界区后,当前任务不会因为普通的抢占调度而切换到其他任务。退出临界区后,调度器重新选择当前最高优先级的就绪任务运行。

需要注意,临界区不是:

  • 等待其他任务执行完成;
  • 控制任务启动顺序;
  • 让其他任务进入阻塞状态;
  • 可以长时间独占CPU的代码区域。

可以简单理解为:

临界区的目标是保证一小段代码连续执行,而不是保证整个任务连续执行。


二、为什么需要临界区

1. 保护“读—修改—写”操作

假设任务和中断都会修改同一个变量:

sharedCount++;

虽然只有一行C代码,但底层可能需要执行三个步骤:

读取 sharedCount ↓ 数值加1 ↓ 写回 sharedCount

如果程序执行到中间时被打断,另一个任务或中断也修改了该变量,就可能发生数据更新丢失。

可以使用临界区保护:

taskENTER_CRITICAL();sharedCount++;taskEXIT_CRITICAL();

2. 保持多个变量的一致性

假设传感器数据由多个成员组成:

typedefstruct{inttemperature;inthumidity;TickType_t timestamp;}SensorData_t;

更新数据时,如果被其他任务或中断打断,读取方可能看到一组不完整的数据。

推荐先在临界区外完成耗时操作,然后进入临界区快速更新:

TickType_t now=xTaskGetTickCount();taskENTER_CRITICAL();sensorData.temperature=newTemperature;sensorData.humidity=newHumidity;sensorData.timestamp=now;taskEXIT_CRITICAL();

这样可以保证其他代码不会读取到只更新了一部分的传感器数据。


三、volatile不能代替临界区

下面的写法不能保证并发安全:

volatileuint32_tsharedCount;

volatile的作用是告诉编译器:

每次访问这个变量时,都应真正读取或写入内存,不要擅自省略或缓存这个操作。

但是,volatile不能保证下面的操作不可被打断:

sharedCount++;

因此需要区分:

机制作用
volatile防止编译器省略或缓存内存访问
临界区防止一段代码被相关任务或中断并发干扰
互斥锁协调多个任务对共享资源的访问
原子操作保证特定操作不可分割

四、临界区与任务优先级的区别

假设系统中存在以下任务:

Task0:优先级4,负责系统初始化 Task1:优先级1 Task2:优先级2 Task3:优先级3

Task0创建其他任务:

staticvoidTask0(void*argument){configASSERT(xTaskCreate(Task1,"Task1",128,NULL,1,NULL)==pdPASS);configASSERT(xTaskCreate(Task2,"Task2",128,NULL,2,NULL)==pdPASS);configASSERT(xTaskCreate(Task3,"Task3",128,NULL,3,NULL)==pdPASS);/* 初始化任务完成使命后删除自己 */vTaskDelete(NULL);}

由于 Task0 的优先级高于 Task1~3,新创建的任务虽然进入就绪状态,但不能抢占 Task0。

执行过程大致如下:

Task0开始运行 ↓ 创建Task1,Task1进入就绪状态 ↓ 创建Task2,Task2进入就绪状态 ↓ 创建Task3,Task3进入就绪状态 ↓ Task0删除自己 ↓ Task3作为最高优先级就绪任务开始运行

这属于任务调度设计,不属于临界区保护。

两种方式的区别如下:

对比项提高Task0优先级使用临界区
低优先级任务能否抢占不能不能
更高优先级任务能否抢占通常不能
是否影响中断不影响会屏蔽全部或部分中断
主要用途安排任务执行顺序保护极短的共享操作
能否长时间使用任务阻塞合理时可以不可以

因此,如果需求只是:

创建完 Task1~3后再让它们正常运行。

优先考虑以下方法:

  1. 在启动调度器之前创建全部任务;
  2. 使用高优先级初始化任务,完成后删除自己;
  3. 使用事件组、信号量或任务通知作为启动同步机制。

一般不应该为了创建多个任务而设置一个较长的临界区。


五、为什么临界区必须尽可能短

临界区会屏蔽全部或部分中断,并阻止正常的任务抢占。

如果临界区运行时间过长,会造成:

  • 串口接收数据丢失;
  • 定时器中断响应延迟;
  • ADC、DMA等外设处理不及时;
  • 高优先级任务无法按时运行;
  • 系统实时性下降;
  • 严重时触发看门狗复位。

下面的代码不适合放在临界区中:

taskENTER_CRITICAL();HAL_UART_Transmit(&huart1,data,length,1000);printf("processing...\r\n");for(uint32_ti=0;i<1000000;i++){/* 长时间循环 */}taskEXIT_CRITICAL();

串口发送、格式化输出和长循环的执行时间都可能很长。


六、临界区中禁止或应避免的操作

临界区内部不应进行可能阻塞、等待或主动触发调度的操作,例如:

vTaskDelay(...);xQueueReceive(queue,&data,portMAX_DELAY);xSemaphoreTake(mutex,portMAX_DELAY);ulTaskNotifyTake(pdTRUE,portMAX_DELAY);

也应避免放入:

  • printf()
  • 长时间串口发送;
  • 动态内存分配;
  • xTaskCreate()
  • 文件或Flash写入;
  • 复杂计算;
  • 长循环;
  • 执行时间不确定的函数。

特别不能在临界区中等待中断改变标志:

taskENTER_CRITICAL();while(uartFinished==0){/* 错误:相关中断可能已经被临界区屏蔽 */}taskEXIT_CRITICAL();

这可能形成死锁:任务在等待中断,但负责修改标志的中断无法执行。


七、任务与中断使用不同的临界区接口

1. 在任务中使用

taskENTER_CRITICAL();sharedValue++;taskEXIT_CRITICAL();

2. 在中断服务函数中使用

中断中不能直接使用普通任务版本,应使用带有_FROM_ISR的接口:

voidUSART_IRQHandler(void){UBaseType_t savedInterruptStatus;savedInterruptStatus=taskENTER_CRITICAL_FROM_ISR();sharedValue++;taskEXIT_CRITICAL_FROM_ISR(savedInterruptStatus);}

taskENTER_CRITICAL_FROM_ISR()会返回进入临界区前的中断屏蔽状态。

这个返回值必须传递给对应的:

taskEXIT_CRITICAL_FROM_ISR(savedInterruptStatus);

不能随意填写,也不能丢弃。


八、STM32 Cortex-M中的临界区机制

在常见的 Cortex-M3、Cortex-M4和Cortex-M7端口中,FreeRTOS通常使用BASEPRI寄存器和下面的配置屏蔽一定范围的中断:

configMAX_SYSCALL_INTERRUPT_PRIORITY

因此,进入临界区不一定意味着所有硬件中断都被关闭。

一般情况下:

  • 受 FreeRTOS 管理的较低紧迫度中断会被屏蔽;
  • 高于阈值的高紧迫度中断仍然可以运行;
  • 不受屏蔽的高紧迫度中断不能调用 FreeRTOS API。

特别注意优先级数字方向

FreeRTOS任务优先级:

数字越大,任务优先级越高

Cortex-M的NVIC中断优先级通常是:

数值越小,中断逻辑优先级越高

例如:

任务优先级3高于任务优先级1 但是: 中断优先级1高于中断优先级3

任务优先级和中断优先级属于两个不同的系统,不能直接比较。


九、临界区支持嵌套

FreeRTOS临界区通常支持嵌套:

taskENTER_CRITICAL();/* 嵌套深度:1 */taskENTER_CRITICAL();/* 嵌套深度:2 *//* 仍然处于临界区 */taskEXIT_CRITICAL();/* 嵌套深度:1,尚未真正退出 */taskEXIT_CRITICAL();/* 嵌套深度:0,真正退出 */

进入和退出必须严格配对。

如果少调用一次taskEXIT_CRITICAL(),系统可能长期无法恢复正常中断响应和任务调度。

推荐保持结构清晰:

taskENTER_CRITICAL();/* 简短的受保护操作 */taskEXIT_CRITICAL();

尽量不要在中间直接return

taskENTER_CRITICAL();if(error){return;/* 错误:没有退出临界区 */}taskEXIT_CRITICAL();

十、临界区、互斥锁和队列如何选择

可以按照下面的思路选择:

是否存在并发访问? │ ├── 否:不需要保护 │ └── 是 │ ├── 只有任务访问,操作时间较长 │ └── 使用互斥锁 │ ├── 任务和ISR访问,操作非常短 │ └── 考虑临界区或原子操作 │ ├── 需要传递数据 │ └── 使用队列 │ ├── 只需要通知事件发生 │ └── 使用任务通知或信号量 │ └── 需要控制任务启动顺序 └── 使用初始化任务或启动同步机制

简单对比如下:

需求推荐机制
修改几个任务与ISR共享的变量临界区或原子操作
多个任务共享串口、I2C、SPI互斥锁
ISR向任务传递数据队列或任务通知
等待某个事件发生信号量、事件组或任务通知
控制多个任务何时开始工作事件组或启动信号
创建完所有任务后再运行调度器启动前创建,或高优先级初始化任务

十一、使用临界区前的检查清单

使用临界区前,可以检查以下问题:

  • 是否确实存在共享数据?
  • 哪些任务或中断会访问该数据?
  • 是否可以改用队列、任务通知或互斥锁?
  • 临界区中的代码是否足够短?
  • 执行时间是否固定且可预测?
  • 是否包含延时、等待、串口发送或动态分配?
  • 进入和退出是否严格配对?
  • ISR是否使用了正确的_FROM_ISR版本?
  • STM32中断优先级是否配置正确?
  • 是否启用了configASSERT()帮助发现配置错误?

总结

临界区可以概括为一句话:

临界区用于保护极短且不能被并发打断的操作,而不是保护整个任务,也不是控制任务的创建和启动顺序。

对于 Task0 创建 Task1~3的场景,优先使用高优先级初始化任务,并在初始化完成后删除 Task0:

staticvoidTask0(void*argument){/* 创建任务和完成初始化 */vTaskDelete(NULL);}

只有在保护共享变量、状态更新或极短硬件操作时,才应考虑使用临界区:

taskENTER_CRITICAL();sharedData++;taskEXIT_CRITICAL();

临界区使用原则是:

能不用就不用,必须使用时尽可能短。


参考资料

  • FreeRTOS:taskENTER_CRITICAL()与taskEXIT_CRITICAL()
  • FreeRTOS:ISR临界区接口
  • FreeRTOS:Cortex-M中断优先级说明

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询