资讯动态

嵌入式从零开始(第九篇):任务间的悄悄话 —— 队列、信号量、互斥量

发布时间:2026/9/29 9:07:10 来源:尧图企业网站定制
前言为什么任务需要“悄悄话”在上一篇文章中我们把裸机的while(1)大循环拆成了多个独立任务每个任务各司其职。但问题随之而来任务之间怎么交换数据怎么同步动作比如传感器采集任务每 100ms 产生一组温湿度数据显示任务需要拿到最新数据刷新屏幕。如果直接用全局变量// 全局变量危险int16_ttemperature;int16_thumidity;voidsensor_task(void*pv){for(;;){temperatureread_temp();humidityread_humid();vTaskDelay(100);}}voiddisplay_task(void*pv){for(;;){// 有可能读到一半被传感器任务打断数据不一致int16_tttemperature;int16_thhumidity;display(t,h);vTaskDelay(500);}}这带来了两个问题数据完整性传感器任务可能在显示任务刚读完温度、还没读湿度时修改了数据导致温度与湿度不匹配“撕裂”。实时性显示任务需要“知道”有新数据而不是靠轮询。RTOS 提供了一整套任务间通信IPCInter-Process Communication机制包括队列、信号量、互斥量、事件组等。它们不仅解决了数据共享的安全问题还能实现任务的同步与阻塞等待。本文将重点介绍最常用的三种队列、信号量、互斥量。一、队列任务间的“信箱”1.1 队列是什么队列Queue是 RTOS 中最基本、最强大的 IPC 工具。它本质上是一个**先进先出FIFO**的缓冲区可以存放固定数量的消息每个消息可以是任意类型整数、结构体、指针等。任务可以从队列尾部发送消息从队列头部接收消息。形象类比队列就像邮局的信箱。发信人任务或中断把信投入信箱收信人任务从信箱取信。如果信箱满了发信人可以选择等待如果信箱空了收信人也可以选择等待。每个信箱有固定的容量且信件按先投先取顺序处理。1.2 队列的核心操作创建xQueueCreate(队列长度, 每个消息的字节大小)发送xQueueSend()/xQueueSendToBack()/xQueueSendToFront()接收xQueueReceive()/xQueuePeek()读取但不移除在中断中发送/接收xQueueSendFromISR()/xQueueReceiveFromISR()1.3 代码示例传感器任务与显示任务通信// 定义消息结构体typedefstruct{int16_ttemperature;int16_thumidity;}SensorData_t;// 全局队列句柄QueueHandle_t xSensorQueue;voidsensor_task(void*pvParameters){SensorData_t data;for(;;){data.temperatureread_temp();data.humidityread_humid();// 发送到队列若队列满则阻塞等待portMAX_DELAY 表示无限等待xQueueSend(xSensorQueue,data,portMAX_DELAY);vTaskDelay(pdMS_TO_TICKS(100));}}voiddisplay_task(void*pvParameters){SensorData_t received;for(;;){// 阻塞等待队列有数据if(xQueueReceive(xSensorQueue,received,portMAX_DELAY)pdPASS){display(received.temperature,received.humidity);}}}intmain(void){// 创建队列最多存放 5 个 SensorData_t 结构体xSensorQueuexQueueCreate(5,sizeof(SensorData_t));xTaskCreate(sensor_task,Sensor,128,NULL,2,NULL);xTaskCreate(display_task,Display,256,NULL,1,NULL);vTaskStartScheduler();}优势数据完整性结构体被整体拷贝到队列中接收时也是整体拷贝不会出现数据撕裂。解耦传感器任务只管放数据显示任务只管取数据彼此不知道对方的存在。阻塞机制显示任务在没有数据时自动阻塞不占用 CPU。1.4 队列在中断中的使用在串口接收中断中通常不能直接处理数据耗时且不可重入但可以把收到的字节放入队列由任务处理QueueHandle_t xUartQueue;voidUSART1_IRQHandler(void){uint8_tbyte;if(USART_GetITStatus(USART1,USART_IT_RXNE)){byteUSART_ReceiveData(USART1);BaseType_t xHigherPriorityTaskWokenpdFALSE;xQueueSendFromISR(xUartQueue,byte,xHigherPriorityTaskWoken);portYIELD_FROM_ISR(xHigherPriorityTaskWoken);}}注意在 ISR 中不能使用不带FromISR后缀的 API因为可能导致阻塞。1.5 队列的变体消息邮箱消息邮箱Mailbox本质上是长度为 1 的队列。它只存放最新的一条消息新的消息会覆盖旧的或根据配置丢弃。在一些 RTOS如 uC/OS中作为独立对象存在在 FreeRTOS 中直接用长度为 1 的队列即可模拟。使用场景传递最新状态不需要历史记录比如 GPS 定位更新。二、信号量任务同步的“旗语”信号量Semaphore不传递数据只传递一个“计数”。它用于两个目的同步事件通知和资源管理。2.1 二值信号量任务同步二值信号量只有 0 和 1 两种状态通常用来通知某个事件发生了。类比一个“旗子”。旗子放下0表示“没有事件”旗子升起1表示“事件已发生”。任务等待旗子升起中断或其他任务升起旗子。示例按键中断唤醒任务SemaphoreHandle_t xButtonSemaphore;voidEXTI0_IRQHandler(void){BaseType_t xHigherPriorityTaskWokenpdFALSE;xSemaphoreGiveFromISR(xButtonSemaphore,xHigherPriorityTaskWoken);portYIELD_FROM_ISR(xHigherPriorityTaskWoken);// 清除中断标志...}voidled_task(void*pv){for(;;){// 等待信号量无限阻塞if(xSemaphoreTake(xButtonSemaphore,portMAX_DELAY)pdTRUE){HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);}}}intmain(void){xButtonSemaphorexSemaphoreCreateBinary();// 创建任务初始化中断...vTaskStartScheduler();}注意二值信号量创建后默认是 0无效状态。如果希望在创建后立即有效可以调用xSemaphoreGive()一次。2.2 计数信号量管理多个资源计数信号量可以持有 0 到某个最大值的计数值用于管理多个相同类型的资源比如多个可用的缓冲区、多台打印机。类比电影院存包柜。有 N 个空柜子每个柜子是一个资源。存包时取走一个柜子计数减 1取包时归还一个柜子计数加 1。如果所有柜子都被占用后来的顾客必须等待。示例管理 5 个串口 DMA 缓冲区#defineBUFFER_COUNT5SemaphoreHandle_t xBufferSemaphore;voidinit(void){xBufferSemaphorexSemaphoreCreateCounting(BUFFER_COUNT,BUFFER_COUNT);}voiddma_irq_handler(void){// 处理完一个缓冲区后释放信号量xSemaphoreGiveFromISR(xBufferSemaphore,NULL);}voidprocess_task(void*pv){for(;;){// 获取一个缓冲区若没有则阻塞xSemaphoreTake(xBufferSemaphore,portMAX_DELAY);// 处理数据...}}2.3 信号量与队列的取舍需要传递数据→ 队列只需通知事件无数据→ 二值信号量管理资源计数→ 计数信号量注意信号量的take/give操作是原子的但不会像队列那样拷贝数据效率更高。三、互斥量保护共享资源的“锁”3.1 为什么需要互斥量当多个任务需要访问同一个共享资源如打印机、EEPROM、全局数据结构时可能出现竞态条件。前面我们用队列传递数据避免了共享但有些场景下必须共享比如一个全局的配置结构体、硬件外设。互斥量MutexMutual Exclusion是一把“锁”。只有获得锁的任务才能访问资源其他任务必须等待锁释放。类比厕所的门锁。一个人进去后锁上门其他人只能在门外排队。等里面的人出来并解锁下一个人才能进入。3.2 互斥量的使用SemaphoreHandle_t xMutex;voidtask_A(void*pv){for(;;){xSemaphoreTake(xMutex,portMAX_DELAY);// 访问共享资源比如写 EEPROMwrite_eeprom();xSemaphoreGive(xMutex);vTaskDelay(100);}}voidtask_B(void*pv){for(;;){xSemaphoreTake(xMutex,portMAX_DELAY);read_eeprom();xSemaphoreGive(xMutex);vTaskDelay(200);}}3.3 优先级反转与优先级继承互斥量有一个致命陷阱优先级反转。假设低优先级任务 L 持有互斥量高优先级任务 H 等待互斥量被阻塞中优先级任务 M不涉及该互斥量开始运行抢占 L结果H 被 M 间接阻塞M 优先级高于 H这就反转了。解决方案FreeRTOS 的互斥量实现了优先级继承机制。当 H 等待 L 持有的互斥量时L 临时继承 H 的优先级从而不会被 M 抢占尽快释放锁。释放后 L 恢复原优先级。注意优先级继承只能缓解反转不能完全消除。最彻底的办法是设计时避免高优先级任务长时间等待低优先级任务的锁。3.4 互斥量与二值信号量的区别特性互斥量二值信号量典型用途保护共享资源事件同步优先级继承支持不支持谁可以解锁只能由持有锁的任务解锁任何任务/中断都可以 give递归获取支持同一任务可多次 take不支持重要绝对不能把互斥量用在中断中因为中断无法被阻塞且优先级继承机制在中断上下文无效。四、其他 IPC 机制简要介绍4.1 事件组事件组Event Group允许任务等待多个事件的任意组合。比如等待“按键按下”或“定时器超时”任一事件发生或者同时等待“串口接收完成”和“DMA 传输完成”两个事件都发生。每个事件用一个 bit 表示最多 24 个事件。4.2 消息邮箱再提一些 RTOS如 uC/OS将消息邮箱作为独立对象本质是单槽队列但可以存放指针。FreeRTOS 直接用长度为 1 的队列模拟即可。4.3 任务通知FreeRTOS 独有的轻量级 IPC每个任务内置一个 32 位值可以用类似信号量的方式通知任务速度更快但功能有限只能一对一通知。五、实践指南如何选择 IPC 机制场景推荐工具传递数据多个数据项队列传递最新状态只关心最新队列长度 1或消息邮箱通知事件发生如按键、中断二值信号量管理资源池如缓冲区、连接池计数信号量保护共享内存/外设互斥量等待多个事件或组合事件组轻量级、一对一同步任务通知高级一个反模式用信号量来保护共享资源而非互斥量。二值信号量不提供优先级继承在高优先级任务等待时可能导致优先级反转。六、常见误区与注意事项在中断中使用不带FromISR的 API→ 导致系统崩溃。忘记检查返回值队列或信号量创建可能失败内存不足不检查就使用会硬故障。互斥量在不同任务中 take/give 不配对互斥量必须由 take 的任务 give否则会破坏所有权机制。在 ISR 中 take 信号量ISR 不能阻塞所以只能xSemaphoreGiveFromISR不能 take。队列消息过大每次拷贝整个结构体若结构体很大 64 字节考虑传递指针注意生命周期。优先级反转使用互斥量而非二值信号量保护共享资源并设计任务优先级合理。七、总结队列传递数据FIFO支持阻塞。是任务间通信的主力。二值信号量事件通知常用于中断与任务同步。计数信号量管理多个同类资源。互斥量保护共享资源支持优先级继承防止优先级反转。选择准则数据用队列同步用信号量资源保护用互斥量。系列导航第一篇嵌入式到底是什么含 ARM/C51/STM32 关系第二篇串口江湖 —— UART、RS-232、RS-485番外篇波特率解析第三篇两线走天下 —— I2C 总线精讲第四篇极速先锋 —— SPI 总线精讲第五篇嵌入式大脑 —— 中断与事件驱动第六篇时间管理大师 —— 定时器与系统滴答第七篇存储与地址 —— 大小端、内存映射、4GB 空间之谜第八篇从裸机到 RTOS —— 任务、调度、FreeRTOS第九篇任务间悄悄话 —— 队列、信号量、互斥量本文第十篇位运算的艺术 —— 、|、^、、预告

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑