ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32 FreeRTOS任务间通信:订阅通知组件架构与实现

STM32 FreeRTOS任务间通信:订阅通知组件架构与实现 在STM32上跑FreeRTOS的项目十有八九会遇到同一个问题任务之间的数据到底怎么传。队列、信号量、事件组这些原语都很好用但任务一多“谁把消息发给谁”这件事很快就会变成一团乱麻。我这两年一直在用一套自己写的订阅通知组件让任务之间只认事件、不认对象代码清爽很多。这篇文章就把这套组件的架构设计、核心实现和一整套实测经验完整讲一遍适合手里已经能用FreeRTOS跑通基础任务的开发者尤其是正在做多任务STM32项目、想进一步优化代码结构的工程师。先说明白这套东西能解决什么它做的事情很简单任何一个任务或中断都可以发布一个“事件”其他任务按照自己的兴趣“订阅”对应事件组件负责把事件路由到订阅者的队列里。发布者不需要知道谁在听订阅者不需要知道谁在喊。我把它用在温湿度采集、报警器、串口上报这类典型嵌入式场景里代码之间的耦合肉眼可见地降下来了。下面直接进入正题。1. 为什么在STM32FreeRTOS里需要“订阅通知”这套架构1.1 裸机时代的两种典型组织方式为什么撑不住很多老项目在裸机阶段会走两条路。第一条是全局标志位轮询所有模块共用一个状态机或者一堆全局变量主循环里挨个检查“有没有新事件”。这种写法最简单但事件一多就分不清谁消费了、谁还没消费而且不同模块的轮询周期不一致某个模块可能延迟好几轮才处理到最新事件实时性全看主循环脸色。第二条是函数回调按键模块收到按键后直接调用display_update()、buzzer_beep()、upload_report()。新加一个功能模块按键模块就要多调一个函数接口越来越臃肿。更麻烦的是回调函数的执行上下文——如果按键是中断触发的回调里绝对不能做耗时操作不能调阻塞式API否则整个系统时序全乱。我当年在项目里吃过这个亏一个回调里加了一行串口打印结果传感器读取时序被拉长数据直接周期性漂移。轮询和回调本质上都是“生产者直接认识消费者”的模型。系统规模小的时候还能忍一旦模块数量上到四五个新增任何一个功能都要去翻旧代码、改调用关系这几十行代码改起来不难但改完很容易把别的模块影响得莫名其妙。1.2 FreeRTOS 并不等于解耦任务间通信更需要架构约束很多人把裸机代码改成FreeRTOS之后常见的操作是把while(1)里互相调用的逻辑拆成几个任务然后任务之间直接用队列句柄互相发消息。比如任务A创建了一个队列任务B、C、D都拿这个句柄去发数据。这比裸机好但本质还是任务之间互相握着对方的“联系方式”耦合依然存在。更隐蔽的问题是全局变量和二值信号量的滥用。两个任务共享一个传感器数据缓冲区A写、B读谁先谁后全靠约定。短时间没问题时间一长谁在某处多加了一条分支竞态就出来了。我自己见过不止一次现场设备跑几个月突然某天报警器失效查到最后就是共享缓冲区被覆盖了没有任何机制拦住这种覆盖。FreeRTOS本身提供的队列、信号量、事件组都是很底层的通信原语它们解决的是“怎么安全地传数据”而不是“上层模块之间怎么组织关系”。订阅通知组件做的事情是在这些原语之上包一层事件路由让数据从“任务A发给任务B”变成“事件X被路由给所有关心X的消费者”。1.3 订阅通知组件解决的问题与适用边界这套架构最核心的价值有三点。第一是解耦发布者只调sn_publish(event_id, data)它不知道也不关心消费者是谁第二是可扩展新加一个订阅者只需要创建队列、调sn_subscribe注册发布者代码一行都不用动第三是可观测所有事件都从组件经过可以顺手统计每个订阅者的队列丢包数、订阅关系表排查问题的时候非常好用。但我也得说句公道话不是所有场景都适合上这套东西。如果系统只有两三个任务、事件类型只有两三种直接用队列对接反而更透明。订阅通知适合的是“同一事件被多个模块消费”和“发布者与消费者数量都不太确定”的场景。后面我会专门写一节什么时候应该放弃这个组件避免被架构反噬。2. 组件架构设计与选型考量2.1 底层用队列广播而不是回调或任务通知这一步是整个组件的根基选错了后面全别扭。当时我列了三种候选方案实测对比过这里直接给结论。方案延迟RAM开销是否支持缓冲适用场景回调函数最低最低不支持中断里快速通知、短小处理FreeRTOS队列广播中高队列内存支持事件带数据、消费者独立处理任务通知 xTaskNotify接近最低低有限24位标志简单事件标志、唤醒任务回调方案误区最多。看起来回调最轻量但回调在发布者的上下文里执行里面不能阻塞、不能做长任务。而嵌入式里大多数事件都伴随数据比如温度值、按键编码、网络帧光发一个“事件ID”往往不够还得带数据。任务通知虽然快但它本质是给单个任务发标志带大量数据不方便而且多个消费者场景下需要扩展成事件组使用起来绕。我的选择是FreeRTOS消息队列广播。每个订阅者自己创建一个队列发布者在组件内部遍历订阅表把事件消息拷贝后发送到所有订阅者的队列。订阅者任务在自己的任务上下文里阻塞读取处理耗时完全不影响发布者。代价就是RAM开销每个队列都要预分配内存但这在STM32上可以通过合理配置解决。2.2 订阅表用静态二维数组避免动态内存与链表订阅表是整个组件的数据核心它记录“事件ID - 哪些队列订阅了”。我第一版用的是链表动态分配节点结果在STM32上踩了坑系统跑几天之后频繁订阅退订导致堆碎片新节点分配不出来整机死机。后来痛定思痛全部改成静态二维数组。#define SN_MAX_EVENT_TYPES 16 #define SN_MAX_SUBSCRIBERS_PER_EVENT 4 typedef struct { QueueHandle_t hq; uint8_t in_use; uint32_t dropped_cnt; } sn_sub_cell_t; static sn_sub_cell_t s_sub_tab[SN_MAX_EVENT_TYPES][SN_MAX_SUBSCRIBERS_PER_EVENT];这个表格的组织方式是按事件分行每行最多4个订阅者。发布时只需要遍历对应事件的那一行最多4次循环最坏执行时间一目了然。从RAM开销看sn_sub_cell_t在32位Cortex-M上大约12字节整张表16412768字节不到1KB大部分STM32芯片都能接受。为什么不用“按订阅者为中心”的记录表那种方式发布时要遍历所有订阅者逐个比对订阅者感不感兴趣事件种类一多效率就下来而且记录条目的数量很难估算。按事件分行的二维数组增减订阅者都只在对应行内操作逻辑最直观。2.3 消息结构内联缓冲拷贝模式怎么定消息结构决定了事件怎么从发布者手里到订阅者手里。两个选择指针模式和拷贝模式。指针模式最省内存sn_msg_t里只放data_ptr和data_len发布者和订阅者共享一块内存区域。但它的前提是数据生命周期要由调用双方自己保证一旦发布者传的是局部变量函数退出后内存被回收订阅者拿到的就是悬空指针。这个坑我踩过不止一次。所以我最终选择了内联拷贝模式。消息结构里直接放一个固定大小的缓冲区#define SN_INLINE_DATA_MAX 32 typedef struct { uint16_t event_id; uint16_t data_len; uint8_t data[SN_INLINE_DATA_MAX]; } sn_msg_t;发布时组件内部将数据memcpy到这个内联缓冲区然后通过队列发送。订阅者拿到消息后data里的值是完整拷贝不依赖发布者后续怎么做。对这种嵌入式场景32字节足够覆盖8字节的传感器数据、4字节的整数状态、协议帧头等大多数情况。代价是队列元素变大但如果数据确实超过32字节我建议设计成另一个专门的大数据通道而不是让这条通用通道背上负担。2.4 FreeRTOS配置裁剪与内存预算在STM32上跑这套组件FreeRTOS的FreeRTOSConfig.h有几个宏需要调整。第一个是configTOTAL_HEAP_SIZE因为队列内存和任务栈都从这里划建议用heap_4.c它能把相邻空闲块合并碎片化低于heap_2。第二个是configCHECK_FOR_STACK_OVERFLOW开发期我强烈建议设为2配合钩子函数能尽早抓到栈溢出。第三个是configUSE_16_BIT_TICKSSTM32上完全是32位环境直接设0。内存预算我可以给个参考值。一个深度为4、元素大小sizeof(sn_msg_t)的队列在Cortex-M4上由于结构体对齐到4字节每个元素实际占用40字节整个队列约160字节。订阅者任务栈建议256字即1KB其中要留足printf和memcpy的递归调用空间实测偏小20%会偶发HardFault。整个订阅通知组件加上4个任务、5个队列在F103这种72MHz的主控上完全跑得动。3. 组件完整实现sub_notify 源码级拆解3.1 头文件设计接口、参数、注意事项先放出头文件所有接口都在这里用起来只需包含这一个头文件。// sub_notify.h #ifndef SUB_NOTIFY_H #define SUB_NOTIFY_H #include FreeRTOS.h #include queue.h #include stdint.h #include string.h #define SN_MAX_EVENT_TYPES 16 #define SN_MAX_SUBSCRIBERS_PER_EVENT 4 #define SN_INLINE_DATA_MAX 32 typedef struct { uint16_t event_id; uint16_t data_len; /* 实际数据长度 SN_INLINE_DATA_MAX */ uint8_t data[SN_INLINE_DATA_MAX]; } sn_msg_t; int sn_init(void); int sn_subscribe(uint16_t event_id, QueueHandle_t hq); int sn_unsubscribe(uint16_t event_id, QueueHandle_t hq); int sn_publish(uint16_t event_id, const void *data, uint16_t len); int sn_publish_from_isr(uint16_t event_id, const void *data, uint16_t len); void sn_print_table(void); uint32_t sn_get_dropped_count(uint16_t event_id, uint8_t sub_index); #endif几个关键点先说清楚。第一SN_MAX_EVENT_TYPES是事件类型总数事件ID必须是从0开始的连续整数不要用随机数字。第二SN_MAX_SUBSCRIBERS_PER_EVENT决定一个事件最多被几个任务订阅4个基本覆盖绝大多数场景不够就调大RAM开销线性增长。第三所有订阅者需要自己创建队列组件只负责往队列里投递不负责队列生命周期这样组件的复杂度才控制得住。3.2 订阅与退订临界区保护与重复订阅处理订阅函数要做的事情就是在对应事件行里找一个空槽位把队列句柄放进去。// sub_notify.c static sn_sub_cell_t s_sub_tab[SN_MAX_EVENT_TYPES][SN_MAX_SUBSCRIBERS_PER_EVENT]; static uint8_t s_inited 0; int sn_subscribe(uint16_t event_id, QueueHandle_t hq) { if (event_id SN_MAX_EVENT_TYPES || hq NULL) { return -1; } /* 重复订阅同一个队列、同一个事件直接返回成功 */ for (int i 0; i SN_MAX_SUBSCRIBERS_PER_EVENT; i) { if (s_sub_tab[event_id][i].in_use s_sub_tab[event_id][i].hq hq) { return 0; } } taskENTER_CRITICAL(); for (int i 0; i SN_MAX_SUBSCRIBERS_PER_EVENT; i) { if (!s_sub_tab[event_id][i].in_use) { s_sub_tab[event_id][i].hq hq; s_sub_tab[event_id][i].dropped_cnt 0; s_sub_tab[event_id][i].in_use 1; taskEXIT_CRITICAL(); return 0; } } taskEXIT_CRITICAL(); return -2; /* 该事件订阅者已满 */ }重复订阅检测很重要。我第一次写的时候没有这个逻辑初始化代码不小心调了两次sn_subscribe事件发出后同一个队列收到两份拷贝显示任务明明只该刷一次屏幕结果一秒钟刷了两次看起来像闪烁。加上检测后这个问题就消失了。退订函数类似对应槽位的in_use置0即可。注意一点如果发布正在进行退订可能导致一个事件多发到已经被退订的队列但订阅者的队列还活着就不会出错所以要求退订前别急着把队列删掉。3.3 发布与分发普通版本与中断版本的实现发布函数是整个组件的核心。它做的事很简单构造sn_msg_t遍历二维数组里的对应行往每个hq队列发送。这里我用了0超时非阻塞发送队列满就直接计数丢弃绝对不能让发布者被一个处理不过来的订阅者拖住。int sn_publish(uint16_t event_id, const void *data, uint16_t len) { sn_msg_t msg; uint16_t copy_len; if (event_id SN_MAX_EVENT_TYPES) { return -1; } copy_len (len SN_INLINE_DATA_MAX) ? len : SN_INLINE_DATA_MAX; msg.event_id event_id; msg.data_len copy_len; memset(msg.data, 0, sizeof(msg.data)); if (data copy_len) { memcpy(msg.data, data, copy_len); } for (int i 0; i SN_MAX_SUBSCRIBERS_PER_EVENT; i) { if (!s_sub_tab[event_id][i].in_use) { continue; } if (xQueueSend(s_sub_tab[event_id][i].hq, msg, 0) ! pdPASS) { s_sub_tab[event_id][i].dropped_cnt; } } return 0; }如果发布者是中断上下文必须用xQueueSendFromISR并且要处理上下文切换请求int sn_publish_from_isr(uint16_t event_id, const void *data, uint16_t len) { sn_msg_t msg; uint16_t copy_len; BaseType_t xHigherPriorityTaskWoken pdFALSE; if (event_id SN_MAX_EVENT_TYPES) { return -1; } copy_len (len SN_INLINE_DATA_MAX) ? len : SN_INLINE_DATA_MAX; msg.event_id event_id; msg.data_len copy_len; memset(msg.data, 0, sizeof(msg.data)); if (data copy_len) { memcpy(msg.data, data, copy_len); } for (int i 0; i SN_MAX_SUBSCRIBERS_PER_EVENT; i) { if (!s_sub_tab[event_id][i].in_use) { continue; } if (xQueueSendFromISR(s_sub_tab[event_id][i].hq, msg, xHigherPriorityTaskWoken) ! pdPASS) { s_sub_tab[event_id][i].dropped_cnt; } } portYIELD_FROM_ISR(xHigherPriorityTaskWoken); return 0; }很多人会忽略一个关键前提发布函数里遍历订阅表时订阅表不能被其他任务同时修改。我在订阅和退订函数用了taskENTER_CRITICAL()保护写操作而发布函数不做什么额外保护——发布前订阅表结构固定即便发布时另一个任务退订最坏情况也只是多发送一次到旧队列不会炸系统。如果想让极端情况更稳可以在发布函数外面加一层短的临界区但实测开销增加明显我这里就不加了逻辑上也能自洽。3.4 调试辅助订阅表打印与丢弃计数嵌入式调试靠printf是常态组件能不能快速把状态打出来很关键。我在组件里实现了一个sn_print_table()把当前所有有订阅记录的事件、订阅者队列、丢包计数打出来。void sn_print_table(void) { for (int e 0; e SN_MAX_EVENT_TYPES; e) { for (int s 0; s SN_MAX_SUBSCRIBERS_PER_EVENT; s) { if (s_sub_tab[e][s].in_use) { printf(evt%d sub[%d] q%p dropped%lu\r\n, e, s, (void *)s_sub_tab[e][s].hq, (unsigned long)s_sub_tab[e][s].dropped_cnt); } } } }加上sn_get_dropped_count(event_id, sub_index)可以精准查到某个订阅者丢了多少次。我自己排查问题时最常用的操作是现象异常时先打印订阅表确认订阅关系没被改坏再打印丢弃计数判断是发布频率过高还是订阅者处理太慢。有了这两个数据80%的问题都能定位。3.5 订阅者任务的标准写法订阅者侧我通常建议写成“休眠唤醒”模式任务启动后阻塞在队列接收上收到消息再处理。模板如下。void subscriber_task(void *arg) { QueueHandle_t q (QueueHandle_t)arg; sn_msg_t msg; while (1) { if (xQueueReceive(q, msg, portMAX_DELAY) pdPASS) { handle_event(msg); } } }注意msg是任务栈上的局部变量每次xQueueReceive从队列里拷出一份完整消息处理完之后变量自动失效不会串台。这种模式下订阅者任务平时零CPU占用既不空转也不抢调度实时性靠队列缓冲和任务优先级来调节。4. 实战温湿度计报警器项目如何用这套组件架构组织代码4.1 项目需求与任务划分我用一个很常见的“数字温湿度计与报警器”项目来演示。硬件是STM32F103C8T6最小系统板、DHT11温湿度传感器、0.96寸OLED屏、一个无源蜂鸣器、两个按键。功能需求很简单每1秒采集一次温湿度并显示温度超过设定阈值时报警按键可以调整阈值。按照订阅通知架构我把系统拆成五个任务任务之间没有直接的联系全靠事件路由。任务名职能优先级行为sensor_task读DHT11发布温湿度事件3周期1秒oled_task订阅温湿度事件刷新显示1阻塞等待alarm_task订阅温湿度阈值事件控制蜂鸣器2阻塞等待key_task扫描按键发布阈值调整事件2周期20ms传感器任务不认识显示任务不认识报警任务也不认识串口上报任务它只需要调用sn_publish(EVT_TEMP_HUM, sensor_data, sizeof(sensor_data))。后续要加一个云端上报功能只需新建一个任务订阅EVT_TEMP_HUM传感器任务一行代码不用动。4.2 事件定义与订阅关系表事件ID我这样定义enum { EVT_TEMP_HUM 0, EVT_SET_THRESHOLD, EVT_COUNT };订阅关系如下EVT_TEMP_HUM被oled_queue、alarm_queue订阅。EVT_SET_THRESHOLD被alarm_queue订阅。这里注意一个设计细节EVT_TEMP_HUM同时被两个队列订阅而alarm_task又同时订阅了两个事件。同一个队列可以订阅多个事件组件往队列里发送的消息都带event_id订阅者拿到消息后可以用switch分支区分处理。4.3 关键代码初始化、发布端、订阅端初始化逻辑集中在app_init()里先创建队列再注册订阅关系最后才创建任务。顺序很重要如果任务先跑起来、队列还没创建好订阅就会失败。static QueueHandle_t g_sensor_q; static QueueHandle_t g_oled_q; static QueueHandle_t g_alarm_q; void app_init(void) { sn_init(); g_sensor_q xQueueCreate(2, sizeof(sn_msg_t)); /* 传感器任务不订阅这个队列其实没用示例保留 */ g_oled_q xQueueCreate(4, sizeof(sn_msg_t)); g_alarm_q xQueueCreate(4, sizeof(sn_msg_t)); if (sn_subscribe(EVT_TEMP_HUM, g_oled_q) ! 0) { /* 打印错误 */ } if (sn_subscribe(EVT_TEMP_HUM, g_alarm_q) ! 0) { /* 打印错误 */ } if (sn_subscribe(EVT_SET_THRESHOLD, g_alarm_q) ! 0) { /* 打印错误 */ } xTaskCreate(sensor_task, sensor, 256, NULL, 3, NULL); xTaskCreate(oled_task, oled, 256, g_oled_q, 1, NULL); xTaskCreate(alarm_task, alarm, 256, g_alarm_q, 2, NULL); xTaskCreate(key_task, key, 256, NULL, 2, NULL); vTaskStartScheduler(); }发布端传感器任务读取DHT11后直接发布。这里的数据是本地结构体但因为组件内部做了内联拷贝函数返回后局部变量销毁完全不影响订阅者。typedef struct { int16_t temp_c; uint8_t humi; } env_data_t; void sensor_task(void *arg) { env_data_t env; while (1) { vTaskDelay(pdMS_TO_TICKS(1000)); read_dht11(env.temp_c, env.humi); /* 读取温湿度 */ sn_publish(EVT_TEMP_HUM, env, sizeof(env)); } }订阅端报警任务的核心逻辑如下。它同时处理两个事件收到EVT_TEMP_HUM就判断阈值收到EVT_SET_THRESHOLD就更新阈值。void alarm_task(void *arg) { QueueHandle_t q (QueueHandle_t)arg; sn_msg_t msg; int16_t threshold 280; /* 28.0度 */ uint8_t alarm_on 0; while (1) { if (xQueueReceive(q, msg, portMAX_DELAY) ! pdPASS) { continue; } if (msg.event_id EVT_TEMP_HUM) { env_data_t *env (env_data_t *)msg.data; if (env-temp_c threshold !alarm_on) { buzzer_on(); alarm_on 1; } else if (env-temp_c threshold alarm_on) { buzzer_off(); alarm_on 0; } } else if (msg.event_id EVT_SET_THRESHOLD) { threshold *(int16_t *)msg.data; } } }按键任务检测到按键后发布一个阈值事件void key_task(void *arg) { int16_t new_threshold; while (1) { vTaskDelay(pdMS_TO_TICKS(20)); if (key_up_pressed()) { new_threshold read_threshold_from_nvm() 5; sn_publish(EVT_SET_THRESHOLD, new_threshold, sizeof(new_threshold)); } } }整套代码跑起来传感器任务只管读、只管发OLED任务、报警任务各自在自己的队列上等数据。发布端没有引用任何订阅端的函数或句柄订阅端之间也完全不认识。新增一个“数据记录”功能模块无非就是再写一个任务、再sn_subscribe(EVT_TEMP_HUM, new_queue)原有任务全部不动。4.4 实际运行效果与性能开销在F103 72MHz上实测一次EVT_TEMP_HUM发布包含32字节数据拷贝加两次队列发送耗时在10~30微秒级别。1秒一次的传感器事件这个开销完全可忽略。整个系统运行稳定没有出现队列积压和事件丢失订阅表打印出来干净清晰。性能评估这类组件重点不是单次发布耗时而是最坏情况下的确定性。因为订阅表按事件分行一个事件最多4个订阅者发布时最多遍历4个槽位执行时间与总任务数量无关这是这套架构在嵌入式场景里最大的底气。5. 常见问题与排查技巧实录5.1 事件丢了先算队列深度再看丢弃计数最典型的“丢事件”场景是订阅者处理慢、发布者频率高。我建议先把丢弃计数打出来确认到底是哪个订阅者丢的然后计算队列深度。公式很简单队列深度 高峰发布速率 × 订阅者最坏处理时间然后至少留2到3倍余量。举个例子传感器1秒发布1次报警任务最坏处理时间1毫秒包括判断、写蜂鸣器IO那么深度2都嫌少推荐直接给4。为什么不是1因为系统启动瞬间多个模块同时初始化事件可能在订阅者任务开始调度前短时间涌入深度1的队列会立刻满掉。我自己的项目里统一要求队列深度不低于2启动瞬间事件井喷的问题再也没出现过。5.2 data指针悬空与数据错乱如果直接把组件改成指针模式或者发布者传入的是局部变量订阅者拿到data_ptr后访问到的可能是已经失效的栈内存。这个问题最恶心的地方在于偶发性发布者在函数返回前把数据填好订阅者过了一个调度周期才去取期间发布者函数的栈空间可能已经被其他函数覆盖。解决思路两条一是坚持用组件自带的32字节内联缓冲区发布时把数据完整拷进去二是如果必须走指针模式发布者要保证数据生命周期比如用static修饰的全局缓冲或专用消息池。我在项目里统一用内联拷贝彻底从机制上消灭这类问题。5.3 中断里发布事件和优先级问题中断里发布事件必须用sn_publish_from_isr不能用普通版本这是红线。普通版本里xQueueSend在没有空闲空间时会等待而中断里不允许阻塞一旦队列满就会触发断言甚至死机。另外要注意中断里不能调用printf包括sn_print_table调试输出只能通过任务来做。优先级方面订阅者任务的优先级决定了事件处理的实时性。报警任务优先级给了2高于OLED显示任务的1所以温度越限事件能优先被处理。发布者任务的优先级相对低原因是我们用了非阻塞发送发布者不会被队列阻塞优先级高低只影响事件到达队列的时机影响远小于订阅侧。5.4 问题速查表现象可能原因解决方法订阅者收不到事件事件ID越界/未订阅检查sn_subscribe返回值打印订阅表事件收到但数据不对data生命周期问题改用内联拷贝模式系统跑几天后异常动态内存碎片/栈溢出改静态分配开栈溢出检测中断里调用发布会死机在中断里用了普通版本改用sn_publish_from_isr订阅者任务一直不执行优先级太低被高优先级任务饿死检查优先级确保订阅者任务有时间片串口打印卡死在中断里打印/临界区过长打印移到任务里缩短临界区5.5 别硬套组件什么时候应该放弃订阅通知订阅通知不是银弹。事件频率非常高、数据量非常大的场景比如高速ADC采集后要实时处理队列拷贝的开销就会成为瓶颈这时更合适的是共享内存加信号量或者直接任务通知。任务之间本来就是一对一的私有通信没必要绕到组件里来一层壳。系统规模只有两个任务的时候直接xQueueSend对接反而干净。我的建议是任务数量三四个以下事件类型单一老老实实用基础队列任务数量上到四五个、事件类型超过三种、同一事件被多个模块消费才值得引入订阅通知。架构是为系统规模服务的不是为了炫技。最后分享一点个人经验。组件第一版的时候我用的是回调方案结果在中断、高优先级任务、耗时处理这几座大山之间反复碰壁才下决心改成队列广播版本。这套代码前前后后用了两年从F103到GD32到STM32H7都平稳运行。如果你正在一个任务关系逐渐失控的FreeRTOS项目里花半天时间把订阅通知组件搭起来后面能省下不止一个通宵的排查时间。
返回列表