尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

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

FreeRTOS任务通信详解:队列、信号量、互斥量怎么选 上一篇学会了创建任务但多个任务怎么传数据、怎么同步传感器任务采到的数据怎么给上报任务中断来了怎么通知任务处理这篇讲透FreeRTOS的5种通信机制附选择决策树和智慧农业项目的真实架构。一、为什么需要任务通信1.1 裸机思维的问题新手第一反应是用全局变量传数据/* 错误做法全局变量 */volatileuint8_tsensor_data;volatileuint8_tdata_ready;voidsensor_task(void*arg){for(;;){sensor_dataread_sensor();data_ready1;vTaskDelay(pdMS_TO_TICKS(100));}}voidupload_task(void*arg){for(;;){if(data_ready){upload(sensor_data);data_ready0;}vTaskDelay(pdMS_TO_TICKS(10));}}问题upload_task每10ms轮询一次data_ready99%是空转浪费CPU如果传感器采集很快data_ready还没被清零就又写了新数据——数据覆盖多个任务读写同一个变量可能读到半新半旧的数据——数据撕裂所谓数据撕裂就是一个32位变量高16位被改了、低16位还没来得及改你就读走了读到一个不存在的中间值1.2 RTOS的正确做法FreeRTOS提供专门的通信机制解决三个问题问题解决方案任务等待事件不想空转信号量阻塞等待不占CPU任务间传数据不丢失队列FIFO缓存保护共享资源防止冲突互斥量加锁解锁等待多个事件组合事件组位运算二、队列Queue任务间的数据管道2.1 队列是什么队列是一个先进先出(FIFO)的数据结构任务A往里放数据任务B从里取数据。任务A(生产者) 任务B(消费者) │ │ ▼ ▼ [发] → [数据1][数据2][数据3] → [收] ←─── FIFO队列 ───→ 先放的数据先被取走特点有容量限制创建时指定满了再发会阻塞或超时返回空了再收会阻塞或超时返回线程安全线程安全多任务同时读写不会出问题队列内部已经做了保护多任务读写不需要额外加锁2.2 创建和使用队列#includeFreeRTOS.h#includequeue.h/* 定义数据结构 */typedefstruct{floattemperature;floathumidity;}sensor_data_t;QueueHandle_t sensor_queue;/* 创建队列容量8每个元素是sensor_data_t大小 */voidqueue_init(void){sensor_queuexQueueCreate(8,sizeof(sensor_data_t));}/* 生产者任务采集数据发送到队列 */voidsensor_task(void*arg){sensor_data_tdata;for(;;){data.temperatureread_temp();data.humidityread_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)等10msportMAX_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 → 计数减13.2 二值信号量中断通知任务最经典用法——ISR通知任务处理事件#includesemphr.hSemaphoreHandle_t uart_sem;/* 创建二值信号量 */voidsem_init(void){uart_semxSemaphoreCreateBinary();}/* ISRUART收到数据释放信号量 */voidUSART1_IRQHandler(void){BaseType_t xHigherPriorityTaskWokenpdFALSE;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_semxSemaphoreCreateCounting(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_mutexxSemaphoreCreateMutex();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释放MutexA才恢复 结果A(最高)被B(中等)间接阻塞了违反优先级原则5.2 优先级继承解决互斥量支持优先级继承当高优先级任务等Mutex时持有Mutex的低优先级任务会临时继承高优先级尽快执行完释放资源低优先级任务C███持Mutex(继承A的优先级)███ 中优先级任务B ███运行███ ← 被C压制 高优先级任务A ███等Mutex███恢复运行███ 1. C持有MutexA等Mutex 2. C继承A的高优先级 → B无法抢占C 3. C快速执行完释放Mutex恢复原优先级 4. A获得Mutex恢复运行结论保护共享资源必须用Mutex不能用二值信号量。Mutex有优先级继承能避免翻转。六、事件组Event Group等多个事件6.1 场景需要传感器就绪且WiFi连接成功才上报数据。用信号量要等两个事件组可以一次等多个#includeevent_groups.h#defineBIT_SENSOR_READY(10)#defineBIT_WIFI_READY(11)EventGroupHandle_t system_events;/* 创建事件组 */system_eventsxEventGroupCreate();/* 传感器任务采集完置位 */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 xHigherPriorityTaskWokenpdFALSE;/* 必须用FromISR版本 */xQueueSendFromISR(queue,data,xHigherPriorityTaskWoken);xSemaphoreGiveFromISR(sem,xHigherPriorityTaskWoken);/* 检查是否唤醒了更高优先级任务如果是则触发切换 */portYIELD_FROM_ISR(xHigherPriorityTaskWoken);}为什么不能用普通版本普通版本会调用调度器ISR里不能调度普通版本可能阻塞ISR里不能阻塞内部临界区实现不同用错会导致未定义行为铁律ISR里只用带FromISR的API普通API在ISR里一律禁用。十、常见踩坑坑1队列传数据成了传指针/* 错误传指针数据可能被覆盖 */sensor_data_t*ptrdata;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任务永远阻塞semxSemaphoreCreateBinary();/* 二值信号量创建后初始值是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等BB等A死锁 */解决多个互斥量时所有任务按相同顺序获取如都先锁Mutex1再锁Mutex2。坑5队列内存不足创建失败queuexQueueCreate(100,sizeof(big_struct_t));/* 可能返回NULL */if(queueNULL){/* 创建失败堆内存不够 */}解决检查返回值调大configTOTAL_HEAP_SIZE或减少队列容量。十一、总结要点内容队列任务间传数据FIFO值拷贝二值信号量ISR→任务通知不占CPU计数信号量累计事件次数互斥量保护共享资源带优先级继承事件组等多个事件组合FromISR中断里必须用FromISR版本优先级翻转用Mutex的优先级继承解决一句话总结任务通信的核心是传数据用队列等事件用信号量护资源用互斥量记住这句口诀90%的场景都能搞定。下一篇预告《FreeRTOS进阶任务通知与内存管理》如果这篇文章对你有帮助点赞 收藏 关注这是我持续更新的动力有问题欢迎评论区交流我会逐条回复。作者嵌入式阿蔡
返回列表