
之前有个项目一个传感器采集任务要给显示任务传数据就十几个字节。我一开始想都没想直接上了队列写完跑了几天觉得好像哪里不对——这RAM占用也太夸张了吧后来翻FreeRTOS的配置手册才发现我踩了一个经典的新手坑。FreeRTOS每个队列最少要消耗几十个字节的TCB再加上你定义的队列深度和消息大小乘起来就很可观了。而且如果只是简单通知数据准备好了这种场景队列完全是杀鸡用牛刀。// 之前用队列的写法 QueueHandle_t sensorQueue; sensorQueue xQueueCreate(10, sizeof(SensorData_t)); // 一个队列吃掉 10 * sizeof(SensorData_t) QueueTCB开销 // SensorData_t 如果定义得大一点分分钟上百字节我那个SensorData_t定义了个float数组加上时间戳64个字节队列深度设了10再加上Queue自身的管理结构体光这一个队列就吃掉将近700字节的RAM。在STM32F103这种只有20K RAM的芯片上这可不是小数目。后来同事跟我说你试试任务通知啊。// 用任务通知改写之后 // 完全不需要创建任何队列直接调用API static void sensor_task(void *pvParameters) { uint32_t sensor_value; while (1) { sensor_value read_sensor(); xTaskNotify(display_task_handle, sensor_value, eSetValueWithOverwrite); vTaskDelay(pdMS_TO_TICKS(100)); } } static void display_task(void *pvParameters) { uint32_t received_value; while (1) { xTaskNotifyWait(0x00, ULONG_MAX, received_value, portMAX_DELAY); update_display(received_value); } }就这就这。没有队列创建没有队列句柄传递几行代码搞定。FreeRTOS的任务通知本质上每个任务自带一个32位的通知值而且是在任务TCB内部自带的不需要额外分配内存。对大多数传一个uint32_t或者一个指针的场景来说完全够用。当然任务通知也有它的局限。我最头疼的就是有的场景需要多个任务同时等待同一个通知——任务通知做不到它是点对点的只能从一个任务通知另一个。而且如果你需要等好几种不同类型的消息用一个32位的值来编码也挺痛苦的得搞位掩码。所以我现在的选择逻辑一般是这么分的传几十个字节以上的结构体 → 用队列传单个数值或指针 → 用任务通知需要广播给多个任务 → 用队列或者事件组一个任务等多种信号 → 用事件组或者用通知值做位掩码说到事件组我之前也一直回避用。后来在做一个多传感器同步采集的时候发现事件组真他妈好用。一个EventGroupHandle可以定义很多个bit每个bit代表一个事件多个任务可以同时对事件组做wait操作。EventGroupHandle_t eventGroup; eventGroup xEventGroupCreate(); // 传感器1完成任务后set bit0 xEventGroupSetBits(eventGroup, BIT_0); // 传感器2完成任务后set bit1 xEventGroupSetBits(eventGroup, BIT_1); // 主任务等待两个传感器都完成 xEventGroupWaitBits( eventGroup, BIT_0 | BIT_1, pdTRUE, pdTRUE, portMAX_DELAY );在调试这些RTOS机制的时候我踩过的另一个坑是vTaskDelay和vTaskDelayUntil的区别。// 死循环里如果这么写每次执行完再delay // 实际上delay的时间是固定的但整体周期会漂移 void vTaskFunction(void *pvParameters) { for (;;) { do_something(); vTaskDelay(pdMS_TO_TICKS(50)); } } // 用vTaskDelayUntil才能保证固定周期 void vTaskFunction(void *pvParameters) { TickType_t xLastWakeTime xTaskGetTickCount(); for (;;) { do_something(); vTaskDelayUntil(xLastWakeTime, pdMS_TO_TICKS(50)); } }有次客户跟我说他们的设备采样周期不稳定我查了半天发现就是这个原因。前一次执行的时间delay了后面每次都会累加误差越来越大。回到任务通知这个话题。我记得FreeRTOS的文档里提到过任务通知比队列快大概45%左右而且省内存。但我自己实际测下来在小数据量的场景下速度差异感知不强内存节省倒是实实在在的。对于小资源芯片来说能省一点是一点。// 实际项目中我经常用任务通知来传指针 // 这样既不占额外内存又不限制数据类型 void *pData malloc(sizeof(SomeStruct)); fill_data(pData); xTaskNotify(consumer_task, (uint32_t)pData, eSetValueWithOverwrite); // 接收方 uint32_t notify_value; xTaskNotifyWait(0, 0, notify_value, portMAX_DELAY); SomeStruct *pReceived (SomeStruct *)notify_value; use_data(pReceived); free(pReceived);注意这里有个细节——用eSetValueWithOverwrite会把之前没取走的值直接覆盖掉。如果发送频率高于接收处理频率数据就丢了。所以在发送方和接收方速率不匹配的场景用eSetValueWithoutOverwrite加超时重试更稳妥。说实话我之前做裸机开发的时候对这种覆盖还是排队的问题根本不敏感。裸机一个循环数据来了就处理处理完了再取下一个。上了RTOS以后任务之间通信的设计直接决定了系统的可靠性。多任务通信这东西做嵌入式的基本绕不过去。不管是队列、任务通知还是事件组、信号量每个都有自己的脾气。用一阵子熟悉了会发现选对了通信方式代码写得顺手ram占用也合理我现在写新模块的时候首先会想想这个数据的消费者是谁有几个消费者数据的生命周期多长如果都想清楚了用什么IPC基本就一目了然了。至于队列嘛——我现在只在大数据量或者需要缓冲的场景才用。小数据传个值任务通知足够好了。