ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

FreeRTOS任务通信详解:队列、信号量、互斥量怎么选

FreeRTOS任务通信详解:队列、信号量、互斥量怎么选

上一篇学会了创建任务,但多个任务怎么传数据、怎么同步?
传感器任务采到的数据怎么给上报任务?中断来了怎么通知任务处理?
这篇讲透FreeRTOS的5种通信机制,附选择决策树和智慧农业项目的真实架构。


一、为什么需要任务通信?

1.1 裸机思维的问题

新手第一反应是用全局变量传数据:

/* 错误做法:全局变量 */volatileuint8_tsensor_data;volatileuint8_tdata_ready;voidsensor_task(void*arg){for(;;){sensor_data=read_sensor();data_ready=1;vTaskDelay(pdMS_TO_TICKS(100));}}voidupload_task(void*arg){for(;;){if(data_ready){upload(sensor_data);data_ready=0;}vTaskDelay(pdMS_TO_TICKS(10));}}

问题

  1. upload_task每10ms轮询一次data_ready,99%是空转,浪费CPU
  2. 如果传感器采集很快,data_ready还没被清零就又写了新数据——数据覆盖
  3. 多个任务读写同一个变量,可能读到"半新半旧"的数据——数据撕裂(所谓数据撕裂,就是一个32位变量,高16位被改了、低16位还没来得及改,你就读走了,读到一个不存在的中间值)

1.2 RTOS的正确做法

FreeRTOS提供专门的通信机制,解决三个问题:

问题解决方案
任务等待事件,不想空转信号量(阻塞等待,不占CPU)
任务间传数据,不丢失队列(FIFO缓存)
保护共享资源,防止冲突互斥量(加锁解锁)
等待多个事件组合事件组(位运算)

二、队列(Queue):任务间的数据管道

2.1 队列是什么

队列是一个先进先出(FIFO)的数据结构,任务A往里放数据,任务B从里取数据。

任务A(生产者) 任务B(消费者) │ │ ▼ ▼ [发] → [数据1][数据2][数据3] → [收] ←─── FIFO队列 ───→ (先放的数据先被取走)

特点

  • 有容量限制(创建时指定)
  • 满了再发会阻塞或超时返回
  • 空了再收会阻塞或超时返回
  • 线程安全(线程安全=多任务同时读写不会出问题,队列内部已经做了保护),多任务读写不需要额外加锁

2.2 创建和使用队列

#include"FreeRTOS.h"#include"queue.h"/* 定义数据结构 */typedefstruct{floattemperature;floathumidity;}sensor_data_t;QueueHandle_t sensor_queue;/* 创建队列:容量8,每个元素是sensor_data_t大小 */voidqueue_init(void){sensor_queue=xQueueCreate(8,sizeof(sensor_data_t));}/* 生产者任务:采集数据,发送到队列 */voidsensor_task(void*arg){sensor_data_tdata;for(;;){data.temperature=read_temp();data.humidity=read_humi();/* 发送到队列,超时10ms */xQueueSend(sensor_queue,&data,pdMS_TO_TICKS(10));vTaskDelay(pdMS_TO_TICKS(100));}}/* 消费者任务:从队列取数据,上报 */voidupload_task(void*arg){sensor_data_tdata;for(;;){/* 阻塞等待队列有数据,一直等 */if(xQueueReceive(sensor_queue,&data,portMAX_DELAY)==pdTRUE){upload_to_cloud(&data);}/* 不需要vTaskDelay!没数据时xQueueReceive会阻塞,不占CPU */}}

2.3 队列的关键参数

xQueueSend(queue,&data,timeout);/* │ │ │ *//* │ │ └─ 超时:pdMS_TO_TICKS(10)=等10ms,portMAX_DELAY=死等 *//* │ └─ 数据指针(注意传地址!) *//* └─ 队列句柄 */

坑点xQueueSend传的是数据指针,函数内部会拷贝一份数据到队列。所以传局部变量地址是安全的(拷贝完成后局部变量就可以释放了)。

2.4 队列满了怎么办

队列容量8,已经放了8个数据: [1][2][3][4][5][6][7][8] 生产者再发第9个 → 取决于超时设置: portMAX_DELAY → 阻塞等待,直到消费者取走一个 pdMS_TO_TICKS(10) → 等10ms,超时返回失败 0 → 立即返回失败(不阻塞)

策略:生产速度 > 消费速度时,队列会满。要么加大队列容量,要么丢弃旧数据(用xQueueOverwrite)。


三、信号量(Semaphore):事件通知

3.1 信号量是什么

信号量像是一个"计数器",用于通知事件发生

二值信号量:只有0和1,像一个"标志位" 任务A give → 计数变1 任务B take → 计数变0,同时任务B被唤醒 计数信号量:可以大于1,像"停车场车位" 多个生产者give → 计数累加 消费者take → 计数减1

3.2 二值信号量:中断通知任务

最经典用法——ISR通知任务处理事件

#include"semphr.h"SemaphoreHandle_t uart_sem;/* 创建二值信号量 */voidsem_init(void){uart_sem=xSemaphoreCreateBinary();}/* ISR:UART收到数据,释放信号量 */voidUSART1_IRQHandler(void){BaseType_t xHigherPriorityTaskWoken=pdFALSE;if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE)){__HAL_UART_CLEAR_IDLEFLAG(&huart1);/* 释放信号量,通知任务 */xSemaphoreGiveFromISR(uart_sem,&xHigherPriorityTaskWoken);}/* 必要时触发任务切换 */portYIELD_FROM_ISR(xHigherPriorityTaskWoken);}/* 任务:阻塞等待信号量 */voiduart_task(void*arg){for(;;){/* 阻塞等待信号量,不占CPU */xSemaphoreTake(uart_sem,portMAX_DELAY);/* 收到信号量,处理UART数据 */process_uart_data();}}

优势:任务平时阻塞睡眠,不占CPU。中断来了一给信号量,任务立即唤醒处理。比轮询高效100倍。

3.3 计数信号量:计事件次数

计数信号量可以累计事件次数,适合"多个事件可能同时发生"的场景:

SemaphoreHandle_t event_sem;/* 创建计数信号量:最大计数10,初始0 */event_sem=xSemaphoreCreateCounting(10,0);/* 多个中断源都能give */voidISR_button(void){xSemaphoreGiveFromISR(event_sem,&woken);}voidISR_timer(void){xSemaphoreGiveFromISR(event_sem,&woken);}/* 任务每次take处理一个事件 */voidevent_task(void*arg){for(;;){xSemaphoreTake(event_sem,portMAX_DELAY);handle_event();/* 处理一个事件 *//* 如果give了3次,这里会连续执行3次 */}}

二值vs计数:二值只能记"有没有",计数能记"有几次"。如果事件可能堆积,用计数信号量。


四、互斥量(Mutex):保护共享资源

4.1 共享资源冲突

两个任务同时操作同一个硬件(如UART),会数据错乱:

/* 错误:两个任务同时用UART打印 */voidtask_a(void*arg){for(;;){printf("任务A:温度=%d\r\n",temp);/* 可能打印到一半被切走 */vTaskDelay(pdMS_TO_TICKS(100));}}voidtask_b(void*arg){for(;;){printf("任务B:湿度=%d\r\n",humi);/* 插进来打印,输出交错 */vTaskDelay(pdMS_TO_TICKS(100));}}/* 实际输出可能变成: 任务任务BA::温度湿== 30 55 */

4.2 互斥量解决

SemaphoreHandle_t uart_mutex;/* 创建互斥量 */uart_mutex=xSemaphoreCreateMutex();voidtask_a(void*arg){for(;;){xSemaphoreTake(uart_mutex,portMAX_DELAY);/* 加锁 */printf("任务A:温度=%d\r\n",temp);xSemaphoreGive(uart_mutex);/* 解锁 */vTaskDelay(pdMS_TO_TICKS(100));}}voidtask_b(void*arg){for(;;){xSemaphoreTake(uart_mutex,portMAX_DELAY);/* 加锁(等A解锁) */printf("任务B:湿度=%d\r\n",humi);xSemaphoreGive(uart_mutex);/* 解锁 */vTaskDelay(pdMS_TO_TICKS(100));}}

4.3 互斥量 vs 二值信号量

对比二值信号量互斥量
优先级继承❌ 无✅ 有
谁都能give✅ 是❌ 只有take者能give
适用中断通知任务保护共享资源

优先级继承是互斥量的核心特性,下面详细讲。


五、优先级翻转与优先级继承(面试必考)

5.1 什么是优先级翻转

高优先级任务被低优先级任务间接阻塞的现象:

时间轴 → 低优先级任务C:███持有Mutex███ ███继续███ 中优先级任务B: ████运行████████████ 高优先级任务A: ███等Mutex(被C阻塞)███ 1. C先运行,拿了Mutex 2. A就绪,抢占C,但A也要Mutex → A阻塞,等C释放 3. B就绪,优先级比C高,抢占C 4. B跑完,C才能继续,C释放Mutex,A才恢复 结果:A(最高)被B(中等)间接阻塞了!违反优先级原则!

5.2 优先级继承解决

互斥量支持优先级继承:当高优先级任务等Mutex时,持有Mutex的低优先级任务会临时继承高优先级,尽快执行完释放资源:

低优先级任务C:███持Mutex(继承A的优先级)███ 中优先级任务B: ███运行███ ← 被C压制 高优先级任务A: ███等Mutex███恢复运行███ 1. C持有Mutex,A等Mutex 2. C继承A的高优先级 → B无法抢占C 3. C快速执行完,释放Mutex,恢复原优先级 4. A获得Mutex,恢复运行

结论:保护共享资源必须用Mutex,不能用二值信号量。Mutex有优先级继承,能避免翻转。


六、事件组(Event Group):等多个事件

6.1 场景

需要"传感器就绪WiFi连接成功"才上报数据。用信号量要等两个,事件组可以一次等多个:

#include"event_groups.h"#defineBIT_SENSOR_READY(1<<0)#defineBIT_WIFI_READY(1<<1)EventGroupHandle_t system_events;/* 创建事件组 */system_events=xEventGroupCreate();/* 传感器任务:采集完置位 */voidsensor_task(void*arg){for(;;){read_sensor();xEventGroupSetBits(system_events,BIT_SENSOR_READY);vTaskDelay(pdMS_TO_TICKS(100));}}/* WiFi任务:连接成功置位 */voidwifi_task(void*arg){for(;;){if(wifi_connect()==OK){xEventGroupSetBits(system_events,BIT_WIFI_READY);break;}vTaskDelay(pdMS_TO_TICKS(1000));}}/* 上报任务:等两个事件都置位 */voidupload_task(void*arg){for(;;){/* 等待两个bit都为1,参数1=要等全部 */xEventGroupWaitBits(system_events,BIT_SENSOR_READY|BIT_WIFI_READY,pdTRUE,/* 退出后清除bit */pdTRUE,/* pdTRUE=等全部(AND), pdFALSE=等任一(OR) */portMAX_DELAY);upload_to_cloud();}}

优势:一个API等多个事件,不用创建多个信号量。适合"多条件满足才执行"的场景。


七、5种机制对比与选择

机制本质典型场景
队列数据管道(FIFO)任务间传数据
二值信号量0/1标志ISR通知任务
计数信号量计数器计事件次数、资源池
互斥量带优先级继承的锁保护共享资源
事件组24位标志位等多个事件组合
任务通知轻量级通知一对一通知(最省内存)

选择决策树

要传数据? ├─ 是 → 数据量大吗? │ ├─ 单个变量/结构体 → 队列 │ └─ 大块数据 → 队列传指针(注意内存管理) │ └─ 否 → 中断通知任务? ├─ 是 → 二值信号量 或 任务通知 │ └─ 否 → 保护共享资源? ├─ 是 → 互斥量 │ └─ 否 → 等多个事件? ├─ 是 → 事件组 └─ 否 → 计数信号量

任务通知(Task Notification):FreeRTOS V8.2+新增,比信号量更轻量(不需要单独分配对象),适合一对一通知。ESP8266中断→解析任务这种场景,用任务通知比信号量更好。


八、智慧农业项目的通信架构

这是我实际项目里用到的,展示完整的多任务通信设计:

┌─────────────────┐ │ 按键中断ISR │ └────────┬────────┘ │ Give ▼ ┌──────────┐ Send ┌──────────────┐ Receive ┌──────────┐ │ 传感器 │────────►│ 传感器队列 │─────────►│ 上报任务 │ │ 采集任务 │ │ (容量8) │ │ (MQTT) │ └──────────┘ └──────────────┘ └──────────┘ │ │ Mutex ▼ ┌──────────┐ ┌──────────────┐ ┌──────────┐ │ 按键任务 │◄────────│ 按键信号量 │ │ UART │ │ │ Take │ (二值) │ │ (共享) │ └──────────┘ └──────────────┘ └──────────┘ ▲ │ Mutex ┌──────────┐ ┌──────────────┐ │ │ MQTT任务 │◄────────│ 事件组 │ │ │ │ │ (网络就绪位) │─────────────┘ └──────────┘ └──────────────┘
通信机制用途
传感器队列采集任务→上报任务传数据
按键信号量按键ISR→按键任务通知
UART互斥量上报任务和调试任务共享UART
事件组等WiFi就绪+MQTT连接成功才上报

九、ISR里的FromISR规则(铁律)

中断里调用FreeRTOS API必须用FromISR版本,且要处理任务切换:

voidISR(void){BaseType_t xHigherPriorityTaskWoken=pdFALSE;/* 必须用FromISR版本 */xQueueSendFromISR(queue,&data,&xHigherPriorityTaskWoken);xSemaphoreGiveFromISR(sem,&xHigherPriorityTaskWoken);/* 检查是否唤醒了更高优先级任务,如果是则触发切换 */portYIELD_FROM_ISR(xHigherPriorityTaskWoken);}

为什么不能用普通版本?

  1. 普通版本会调用调度器,ISR里不能调度
  2. 普通版本可能阻塞,ISR里不能阻塞
  3. 内部临界区实现不同,用错会导致未定义行为

铁律:ISR里只用带FromISR的API,普通API在ISR里一律禁用。


十、常见踩坑

坑1:队列传数据成了传指针

/* 错误:传指针,数据可能被覆盖 */sensor_data_t*ptr=&data;xQueueSend(queue,&ptr,0);/* 传的是指针的指针 *//* 正确:传数据本身 */xQueueSend(queue,&data,0);/* 队列内部拷贝数据 */

队列是值拷贝,传&data会把data的内容拷进队列。如果传指针,多个任务访问同一块内存,还是会冲突。


坑2:互斥量在中断里使用

/* 错误:Mutex不能在ISR里用! */voidISR(void){xSemaphoreTake(mutex,0);/* 崩溃 */}/* 正确:中断里只能用二值信号量 */xSemaphoreGiveFromISR(sem,&woken);

Mutex有优先级继承机制,依赖任务调度,不能在ISR里用。ISR里只能用FromISR版本的信号量。


坑3:信号量创建后忘记give,任务永远阻塞

sem=xSemaphoreCreateBinary();/* 二值信号量创建后初始值是0!必须先give才能take */voidtask(void*arg){xSemaphoreTake(sem,portMAX_DELAY);/* 永远阻塞! */}

解决:创建后手动give一次,或用xSemaphoreCreateMutex()(初始可用)。


坑4:死锁

/* 任务A */Take(Mutex1);Take(Mutex2);/* 等任务B释放Mutex2 *//* 任务B */Take(Mutex2);Take(Mutex1);/* 等任务A释放Mutex1 *//* A等B,B等A,死锁! */

解决:多个互斥量时,所有任务按相同顺序获取(如都先锁Mutex1再锁Mutex2)。


坑5:队列内存不足创建失败

queue=xQueueCreate(100,sizeof(big_struct_t));/* 可能返回NULL */if(queue==NULL){/* 创建失败!堆内存不够 */}

解决:检查返回值;调大configTOTAL_HEAP_SIZE;或减少队列容量。


十一、总结

要点内容
队列任务间传数据,FIFO,值拷贝
二值信号量ISR→任务通知,不占CPU
计数信号量累计事件次数
互斥量保护共享资源,带优先级继承
事件组等多个事件组合
FromISR中断里必须用FromISR版本
优先级翻转用Mutex的优先级继承解决

一句话总结:任务通信的核心是"传数据用队列,等事件用信号量,护资源用互斥量",记住这句口诀,90%的场景都能搞定。


下一篇预告:《FreeRTOS进阶:任务通知与内存管理》

如果这篇文章对你有帮助,点赞 + 收藏 + 关注,这是我持续更新的动力!
有问题欢迎评论区交流,我会逐条回复。

作者:嵌入式阿蔡

返回列表