ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FreeRTOS 临界区——精炼笔记

FreeRTOS 临界区——精炼笔记 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 nowxTaskGetTickCount();taskENTER_CRITICAL();sensorData.temperaturenewTemperature;sensorData.humiditynewHumidity;sensorData.timestampnow;taskEXIT_CRITICAL();这样可以保证其他代码不会读取到只更新了一部分的传感器数据。三、volatile不能代替临界区下面的写法不能保证并发安全volatileuint32_tsharedCount;volatile的作用是告诉编译器每次访问这个变量时都应真正读取或写入内存不要擅自省略或缓存这个操作。但是volatile不能保证下面的操作不可被打断sharedCount;因此需要区分机制作用volatile防止编译器省略或缓存内存访问临界区防止一段代码被相关任务或中断并发干扰互斥锁协调多个任务对共享资源的访问原子操作保证特定操作不可分割四、临界区与任务优先级的区别假设系统中存在以下任务Task0优先级4负责系统初始化 Task1优先级1 Task2优先级2 Task3优先级3Task0创建其他任务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 的优先级高于 Task13新创建的任务虽然进入就绪状态但不能抢占 Task0。执行过程大致如下Task0开始运行 ↓ 创建Task1Task1进入就绪状态 ↓ 创建Task2Task2进入就绪状态 ↓ 创建Task3Task3进入就绪状态 ↓ Task0删除自己 ↓ Task3作为最高优先级就绪任务开始运行这属于任务调度设计不属于临界区保护。两种方式的区别如下对比项提高Task0优先级使用临界区低优先级任务能否抢占不能不能更高优先级任务能否抢占能通常不能是否影响中断不影响会屏蔽全部或部分中断主要用途安排任务执行顺序保护极短的共享操作能否长时间使用任务阻塞合理时可以不可以因此如果需求只是创建完 Task13后再让它们正常运行。优先考虑以下方法在启动调度器之前创建全部任务使用高优先级初始化任务完成后删除自己使用事件组、信号量或任务通知作为启动同步机制。一般不应该为了创建多个任务而设置一个较长的临界区。五、为什么临界区必须尽可能短临界区会屏蔽全部或部分中断并阻止正常的任务抢占。如果临界区运行时间过长会造成串口接收数据丢失定时器中断响应延迟ADC、DMA等外设处理不及时高优先级任务无法按时运行系统实时性下降严重时触发看门狗复位。下面的代码不适合放在临界区中taskENTER_CRITICAL();HAL_UART_Transmit(huart1,data,length,1000);printf(processing...\r\n);for(uint32_ti0;i1000000;i){/* 长时间循环 */}taskEXIT_CRITICAL();串口发送、格式化输出和长循环的执行时间都可能很长。六、临界区中禁止或应避免的操作临界区内部不应进行可能阻塞、等待或主动触发调度的操作例如vTaskDelay(...);xQueueReceive(queue,data,portMAX_DELAY);xSemaphoreTake(mutex,portMAX_DELAY);ulTaskNotifyTake(pdTRUE,portMAX_DELAY);也应避免放入printf()长时间串口发送动态内存分配xTaskCreate()文件或Flash写入复杂计算长循环执行时间不确定的函数。特别不能在临界区中等待中断改变标志taskENTER_CRITICAL();while(uartFinished0){/* 错误相关中断可能已经被临界区屏蔽 */}taskEXIT_CRITICAL();这可能形成死锁任务在等待中断但负责修改标志的中断无法执行。七、任务与中断使用不同的临界区接口1. 在任务中使用taskENTER_CRITICAL();sharedValue;taskEXIT_CRITICAL();2. 在中断服务函数中使用中断中不能直接使用普通任务版本应使用带有_FROM_ISR的接口voidUSART_IRQHandler(void){UBaseType_t savedInterruptStatus;savedInterruptStatustaskENTER_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();尽量不要在中间直接returntaskENTER_CRITICAL();if(error){return;/* 错误没有退出临界区 */}taskEXIT_CRITICAL();十、临界区、互斥锁和队列如何选择可以按照下面的思路选择是否存在并发访问 │ ├── 否不需要保护 │ └── 是 │ ├── 只有任务访问操作时间较长 │ └── 使用互斥锁 │ ├── 任务和ISR访问操作非常短 │ └── 考虑临界区或原子操作 │ ├── 需要传递数据 │ └── 使用队列 │ ├── 只需要通知事件发生 │ └── 使用任务通知或信号量 │ └── 需要控制任务启动顺序 └── 使用初始化任务或启动同步机制简单对比如下需求推荐机制修改几个任务与ISR共享的变量临界区或原子操作多个任务共享串口、I2C、SPI互斥锁ISR向任务传递数据队列或任务通知等待某个事件发生信号量、事件组或任务通知控制多个任务何时开始工作事件组或启动信号创建完所有任务后再运行调度器启动前创建或高优先级初始化任务十一、使用临界区前的检查清单使用临界区前可以检查以下问题是否确实存在共享数据哪些任务或中断会访问该数据是否可以改用队列、任务通知或互斥锁临界区中的代码是否足够短执行时间是否固定且可预测是否包含延时、等待、串口发送或动态分配进入和退出是否严格配对ISR是否使用了正确的_FROM_ISR版本STM32中断优先级是否配置正确是否启用了configASSERT()帮助发现配置错误总结临界区可以概括为一句话临界区用于保护极短且不能被并发打断的操作而不是保护整个任务也不是控制任务的创建和启动顺序。对于 Task0 创建 Task13的场景优先使用高优先级初始化任务并在初始化完成后删除 Task0staticvoidTask0(void*argument){/* 创建任务和完成初始化 */vTaskDelete(NULL);}只有在保护共享变量、状态更新或极短硬件操作时才应考虑使用临界区taskENTER_CRITICAL();sharedData;taskEXIT_CRITICAL();临界区使用原则是能不用就不用必须使用时尽可能短。参考资料FreeRTOStaskENTER_CRITICAL()与taskEXIT_CRITICAL()FreeRTOSISR临界区接口FreeRTOSCortex-M中断优先级说明
返回列表