FreeRTOS双执行循环间共享数据一致性保障方案问询
更优的FreeRTOS同步方案:告别全局互斥变量
嘿,这个实时控制的时序同步场景太典型了!你的初始思路(用状态标识保护共享数据)方向完全没问题,但全局变量确实容易让代码耦合度上升,尤其是项目规模变大后维护起来头疼。下面给你几个基于FreeRTOS原生特性的替代方案,都能避开全局变量的问题:
方案1:用单元素队列传递最新数据
队列是FreeRTOS里最常用的同步工具,刚好适配你这种“只需要最新一次有效数据”的场景:
- 给SL任务创建一个长度为1的队列,队列元素类型就是你的共享数据结构体(比如
typedef struct { ... } SharedData_t;) - FL每次执行完,如果是第4次循环,就把当前最新的共享数据发送到队列里(用
xQueueOverwrite(),直接覆盖队列里的旧数据,确保SL拿到的永远是第4次FL的结果) - SL任务每次被触发时,直接从队列里取数据(用
xQueueReceive(),超时设为portMAX_DELAY,保证能拿到有效数据)
示例代码片段:
// 在SL任务初始化时创建队列(可以把队列句柄作为任务参数传递,不用全局) SharedData_t xSharedData; QueueHandle_t xDataQueue = xQueueCreate(1, sizeof(SharedData_t)); // FL任务中的逻辑(每4次执行时发送数据) static uint8_t uFLCount = 0; void vFastLoopTask(void *pvParameters) { while(1) { // 等待ADC转换完成中断触发 ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // 采集并处理数据到本地变量 SharedData_t xCurrentData; xCurrentData.adc_val = HAL_ADC_GetValue(&hadc1); // ...其他数据处理 uFLCount++; if(uFLCount >= 4) { // 发送最新数据到队列,覆盖旧数据 xQueueOverwrite(xDataQueue, &xCurrentData); uFLCount = 0; } } } // SL任务中的逻辑 void vSlowLoopTask(void *pvParameters) { QueueHandle_t xQueue = (QueueHandle_t)pvParameters; SharedData_t xReceivedData; while(1) { // 等待队列有数据(或者用定时器触发,结合队列取数) xQueueReceive(xQueue, &xReceivedData, portMAX_DELAY); // 使用第4次FL的有效数据进行计算 processSlowLoopData(&xReceivedData); } }
这个方案的好处是完全不需要全局变量,队列句柄可以通过任务参数传递,数据传递和同步逻辑完全由FreeRTOS管理,线程安全有保障。
方案2:用任务通知传递数据指针(开销最小)
FreeRTOS的任务通知是比队列更轻量的同步方式,适合这种一对一的任务同步场景:
- 把共享数据封装成FL任务的静态结构体(只在FL任务内部可见,避免全局)
- 每4次FL执行完成后,给SL任务发送任务通知,把静态结构体的指针作为通知值传递
- SL任务收到通知后,直接使用这个指针指向的数据(注意:如果FL后续会修改这个结构体,最好加个简单的同步,或者用双缓冲)
示例代码片段:
void vFastLoopTask(void *pvParameters) { // 静态共享数据,仅FL任务可见 static SharedData_t xStaticSharedData; static uint8_t uFLCount = 0; TaskHandle_t xSLTaskHandle = (TaskHandle_t)pvParameters; while(1) { // 等待ADC中断触发 ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // 更新静态数据 xStaticSharedData.adc_val = HAL_ADC_GetValue(&hadc1); // ...其他处理 uFLCount++; if(uFLCount >= 4) { // 给SL任务发送通知,传递数据指针 xTaskNotify(xSLTaskHandle, (uint32_t)&xStaticSharedData, eSetValueWithOverwrite); uFLCount = 0; } } } void vSlowLoopTask(void *pvParameters) { while(1) { // 等待FL任务的通知 uint32_t ulNotificationValue; ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // 获取数据指针 SharedData_t *pData = (SharedData_t *)ulNotificationValue; // 使用数据(如果FL在SL处理时不会修改,直接用就行;如果会修改,要加同步) processSlowLoopData(pData); } }
这个方案的优势是开销极小,不需要创建额外的队列对象,数据也不需要拷贝,适合资源紧张的MCU场景。
方案3:双缓冲+事件组(适合大数据量场景)
如果你的共享数据量比较大,拷贝数据开销高,可以用双缓冲+事件组的方式:
- FL任务维护两个静态缓冲(
bufA和bufB),每次FL更新其中一个缓冲 - 每4次FL完成后,通过事件组通知SL“当前可用的缓冲是哪一个”
- SL任务收到通知后,读取对应缓冲的数据,此时FL可以继续更新另一个缓冲,互不干扰
示例代码片段:
#define BUF_A_READY (1 << 0) #define BUF_B_READY (1 << 1) void vFastLoopTask(void *pvParameters) { static SharedData_t bufA, bufB; static uint8_t uCurrentBuf = 0; // 0: bufA, 1: bufB static uint8_t uFLCount = 0; EventGroupHandle_t xEventGroup = (EventGroupHandle_t)pvParameters; while(1) { ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // 更新当前缓冲的数据 if(uCurrentBuf == 0) { bufA.adc_val = HAL_ADC_GetValue(&hadc1); } else { bufB.adc_val = HAL_ADC_GetValue(&hadc1); } uFLCount++; if(uFLCount >= 4) { // 通知SL当前缓冲可用 xEventGroupSetBits(xEventGroup, uCurrentBuf == 0 ? BUF_A_READY : BUF_B_READY); // 切换缓冲,下次FL更新另一个 uCurrentBuf = !uCurrentBuf; uFLCount = 0; } } } void vSlowLoopTask(void *pvParameters) { EventGroupHandle_t xEventGroup = (EventGroupHandle_t)pvParameters; // 这里可以把缓冲指针作为任务参数传递,避免全局访问 SharedData_t *pBufA = ((SharedData_t**)pvParameters)[0]; SharedData_t *pBufB = ((SharedData_t**)pvParameters)[1]; SharedData_t *pData; while(1) { EventBits_t uxBits = xEventGroupWaitBits(xEventGroup, BUF_A_READY | BUF_B_READY, pdTRUE, pdFALSE, portMAX_DELAY); // 确定要读取的缓冲 if(uxBits & BUF_A_READY) { pData = pBufA; } else { pData = pBufB; } processSlowLoopData(pData); } }
这个方案避免了数据拷贝,适合大数据量的同步场景,而且两个任务的操作完全并行,不会互相阻塞。
选择建议
- 如果数据量小、逻辑简单:优先用队列方案,最通用,代码可读性最高
- 如果追求最小开销:选任务通知方案,资源占用最少
- 如果数据量大、需要并行处理:用双缓冲+事件组方案
内容的提问来源于stack exchange,提问作者Steve
相关产品推荐
相关产品推荐

