【CMSIS RTOS】线程间通信实战:信号量、事件标志与消息队列如何选型
【CMSIS RTOS】线程间通信实战信号量、事件标志与消息队列如何选型摘要上一篇我们梳理了 CMSIS RTOS 的主要函数本文聚焦线程间通信IPC三大机制——信号量osSemaphore、事件标志osEventFlags与消息队列osMessageQueue从原理、API 到 STM32 FreeRTOS 下的实战代码讲清楚什么场景该用哪一个。一、为什么需要三种不同的 IPC 机制嵌入式系统里线程之间打交道无非三类需求这个资源我在用你等等—— 互斥与资源管控 → 信号量 / 互斥量某件事发生了相关线程该动起来了—— 纯事件通知可多条件组合 → 事件标志把这段数据交给你—— 数据传递 → 消息队列一句话概括信号量回答还有多少资源可用事件标志回答发生了什么消息队列回答数据是什么。二、信号量资源计数与同步信号量本质上是一个令牌计数器。获取acquire消耗一个令牌释放release归还一个。令牌数为 1 且具备优先级继承时就是我们常说的互斥量。核心 APIosSemaphoreId_t osSemaphoreNew(uint32_t max_count, uint32_t initial_count, const osSemaphoreAttr_t *attr); osStatus_t osSemaphoreAcquire(osSemaphoreId_t sem_id, uint32_t timeout); osStatus_t osSemaphoreRelease(osSemaphoreId_t sem_id); osStatus_t osSemaphoreDelete(osSemaphoreId_t sem_id);典型用法 1保护共享资源资源池osSemaphoreId_t uart_sem; void app_init(void) { /* 最多 3 个线程同时使用缓冲池 */ uart_sem osSemaphoreNew(3U, 3U, NULL); } void worker_thread(void *arg) { (void)arg; for (;;) { osSemaphoreAcquire(uart_sem, osWaitForever); /* 拿不到就阻塞 */ use_buffer_pool(); osSemaphoreRelease(uart_sem); } }典型用法 2中断 → 线程同步中断里给信号线程里等信号是最常见的生产者-消费者同步方式void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { osSemaphoreRelease(uart_rx_sem); /* ISR 中只做释放 */ } void parser_thread(void *arg) { for (;;) { osSemaphoreAcquire(uart_rx_sem, osWaitForever); parse_rx_buffer(); } }注意CMSIS-RTOS2 统一了线程/中断调用场景底层如 CMSIS-FreeRTOS 封装会自动区分任务级与 ISR 级调用不再需要像原生 FreeRTOS 那样区分xSemaphoreTake/xSemaphoreTakeFromISR。三、事件标志一组发生了什么的位事件标志是一个 32 位标志集合每个位代表一个事件。线程可以按AND / OR组合等待多个事件这是它相比信号量最大的优势。核心 APIosEventFlagsId_t osEventFlagsNew(const osEventFlagsAttr_t *attr); uint32_t osEventFlagsSet(osEventFlagsId_t ef_id, uint32_t flags); uint32_t osEventFlagsClear(osEventFlagsId_t ef_id, uint32_t flags); uint32_t osEventFlagsWait(osEventFlagsId_t ef_id, uint32_t flags, uint32_t options, uint32_t timeout);典型用法多条件组合触发比如数据采集线程需要同时等ADC 数据就绪和配置完成#define EVT_ADC_READY (1U 0) #define EVT_CFG_DONE (1U 1) osEventFlagsId_t evt; void adc_isr(void) { osEventFlagsSet(evt, EVT_ADC_READY); } void config_thread(void *arg) { load_config(); osEventFlagsSet(evt, EVT_CFG_DONE); } void collect_thread(void *arg) { for (;;) { /* AND 模式两个事件都到齐才继续 */ osEventFlagsWait(evt, EVT_ADC_READY | EVT_CFG_DONE, osFlagsWaitAll, osWaitForever); process_sample(); osEventFlagsClear(evt, EVT_ADC_READY | EVT_CFG_DONE); } }options传osFlagsWaitAll表示全部到齐传osFlagsWaitAny表示任意一个即唤醒再加osFlagsNoClear可以控制是否在等待成功后自动清除标志。四、消息队列带数据的传递前两者只传信号消息队列按值拷贝传递数据天然解耦生产者与消费者的执行节奏。核心 APIosMessageQueueId_t osMessageQueueNew(uint32_t msg_count, uint32_t msg_size, const osMessageQueueAttr_t *attr); osStatus_t osMessageQueuePut(osMessageQueueId_t mq_id, const void *msg_ptr, uint8_t msg_prio, uint32_t timeout); osStatus_t osMessageQueueGet(osMessageQueueId_t mq_id, void *msg_ptr, uint8_t *msg_prio, uint32_t timeout); uint32_t osMessageQueueGetCount(osMessageQueueId_t mq_id); uint32_t osMessageQueueGetSpace(osMessageQueueId_t mq_id);要点msg_size是单条消息的字节数队列创建后固定消息是按值拷贝不是指针小数据直接传结构体大数据传指向缓冲区的指针 长度这类小结构体注意缓冲区生命周期msg_prio支持消息优先级同队列内高优先级消息先被取出不指定mq_mem/cb_mem时控制块与存储区从 RTOS 堆中动态分配产品环境建议静态分配避免运行期堆碎片。典型用法传感器数据上报typedef struct { uint8_t ch; uint16_t raw; uint32_t tick; } sample_msg_t; osMessageQueueId_t sample_mq; void app_init(void) { /* 16 条消息每条 sizeof(sample_msg_t) 字节 */ sample_mq osMessageQueueNew(16U, sizeof(sample_msg_t), NULL); } void sensor_thread(void *arg) { sample_msg_t m {0}; for (;;) { m.ch read_channel(); m.raw read_adc(); m.tick osKernelGetTickCount(); /* 队列满则丢掉最旧数据前先尝试 0 超时写入 */ if (osMessageQueuePut(sample_mq, m, 0U, 0U) ! osOK) { sample_msg_t drop; osMessageQueueGet(sample_mq, drop, NULL, 0U); /* 丢最旧 */ osMessageQueuePut(sample_mq, m, 0U, 0U); } osDelay(10U); } } void upload_thread(void *arg) { sample_msg_t m; for (;;) { osMessageQueueGet(sample_mq, m, NULL, osWaitForever); upload(m); } }五、三者对比与选型速查维度信号量事件标志消息队列本质令牌计数器位标志集合按值拷贝的 FIFO是否消耗获取一次消耗一个读标志不消耗取出一条少一条传递内容无纯计数无事件位任意固定长度数据组合等待不支持支持 AND/OR 掩码不支持ISR 中使用Acquire/Release 均可*Set/Clear/Wait 均可*Put/Get 均可*0 超时典型场景互斥、资源池、中断同步多条件触发、一对多通知传感器数据流、命令分发*CMSIS-RTOS2 层面允许线程与 ISR 统一调用具体约束取决于底层封装RTX5 / CMSIS-FreeRTOSISR 中建议使用 0 超时。选型口诀只需要通知一下 →事件标志多条件用osFlagsWaitAll/Any需要占坑/资源配额 →信号量互斥场景优先用带优先级继承的osMutexNew需要把数据交给别人 →消息队列六、常见踩坑ISR 中使用osWaitForever中断里绝不能无限阻塞超时一律传 0。消息队列传指针后缓冲区被复用按值拷贝只拷贝msg_size字节若消息里放的是指针必须保证消费者取走前缓冲区有效静态双缓冲或内存池配合。信号量当互斥用却没开优先级继承纯粹的计数信号量没有优先级继承互斥场景应使用osMutexNew否则可能出现优先级反转。队列容量拍脑袋定用osMessageQueueGetSpace()在调试阶段观察峰值占用再定容量避免偶发丢数据。忘记osEventFlagsClear非自动清除模式下标志不清会导致WaitAll直接被旧标志穿透。七、小结CMSIS-RTOS2 把各家 RTOS 的 IPC 机制统一成一套 API在 STM32CubeIDE 里切换 FreeRTOS / RTX5 几乎不用改应用代码。掌握信号量管资源、事件标志管通知、消息队列管数据这条主线绝大多数线程间通信的选型问题都能迎刃而解。参考资料CMSIS-RTOS2 官方文档Message QueueCMSIS-RTOS2 官方文档SemaphoresFreeRTOS 官方队列文档中文野火 FreeRTOS 消息队列章节事件和信号量对比与应用场景CSDN