# 项目启动顺序管理(Event Flags 就绪屏障 · 已落地) > 已落地:用 CMSIS-RTOS v2 的 **Event Flags** 实现"就绪屏障",取代原先的全局标志轮询 + 魔法延时来管理任务启动顺序,消除脆弱的时间耦合。事件组句柄 `sys_evtHandle` 由 CubeMX(FREERTOS Events 对象 `sys_evt`)生成。 --- ## 1. 背景与现状问题 ### 1.1 现有任务 | 任务 | 职责 | 当前启动依赖 | |------|------|--------------| | `defaultTask` | FTL + FatFS 存储挂载/测试(一次性) | 无(最先跑) | | `netTask` | `net_init()` → `net_poll()` 周期轮询 | 无 | | `ftpTask` | `lftpd_start("/", 21, &ftp)` | 等 `SYS_EVT_NET_READY | SYS_EVT_STORAGE_READY`(`osFlagsWaitAll`) | | `start_test_task`(testTask) | 统一测试入口,按 `TEST_SUITE_*` 分发 | NET 模式下 `osEventFlagsWait(SYS_EVT_NET_READY, osFlagsWaitAny, osWaitForever)` 阻塞等待 | | `timeSyncTask` | 墙钟时间同步 | `osEventFlagsWait(SYS_EVT_NET_READY, …)` 后开始同步 | ### 1.2 改造前(已废弃)的依赖管理手段 > 以下为引入 Event Flags 之前的历史做法,仅作背景记录,当前代码已不再使用。 1. **全局标志轮询**:`netTask` 就绪后置 `g_net_ready = 1`,`start_test_task` 用 `while (!g_net_ready) { osDelay(100); }` 忙等。 2. **魔法延时**:`ftpTask` 用 `osDelay(7000)` 硬等 PHY/网络稳定。 ### 1.3 改造前的痛点(即引入 Event Flags 的动机) - **魔法延时脆弱**:PHY 协商、DHCP、首轮 `net_poll()` 的耗时随硬件/环境波动;7s 是拍脑袋定值,一旦网络就绪变慢就失败。这正是 `docs/问题记录/CH395F_Trap_Records.md` 中记录的坑(Trap 09 子类:上电 PHY 未稳定即 listen)。 - **轮询浪费节拍**:`start_test_task` 每 100ms 被唤醒一次去读一个全局变量,纯属空转。 - **依赖隐式且分散**:谁依赖谁、何时就绪,散落在多个任务的延时与 `if` 里,没有统一视图,新增子系统极易遗漏同步。 - **不可组合**:原全局标志只能表达"一个"条件;若某任务需"网络 **且** 存储都就绪"就无从表达。 --- ## 2. 核心思想:就绪屏障(Readiness Barrier) **把"启动顺序"抽象为"子系统就绪事件",而不是"时间先后"。** - 每个子系统(网络、存储、FTP 服务等)在**真正准备好对外服务**的那一刻,广播一个"我好了"的事件标志。 - 任何依赖该子系统的任务,在入口处**阻塞等待**对应事件标志,收到才继续;此前不占 CPU、不被调度唤醒。 - 生产者与消费者**互相不知道对方存在**,只通过事件组解耦——新增任务只需在入口 `wait` 它需要的 bit,无需改动任何已有任务的代码。 这与 CubeMX"一次性创建全部任务"的模式完全兼容:任务仍由 `MX_FREERTOS_Init` 统一创建,但各自在入口 `wait`,相当于"创建即挂起、就绪才放行"。 ### 为什么用 Event Flags 而非全局变量 | 维度 | 全局标志 `g_net_ready` | Event Flags | |------|----------------------|-------------| | 等待方式 | 忙等轮询(浪费 tick) | 阻塞等待(零占用) | | 多依赖组合 | 不支持 | `osFlagsWaitAll` / `osFlagsWaitAny` | | 类型/作用域 | 裸 `volatile`,易误用 | 内核对象,API 受控 | | 清空语义 | 手动维护 | `osFlagsNoClear` 可配 | | 调试可见性 | 不可观测 | 可在 IDE 观测事件组值 | --- ## 3. 方案设计 ### 3.1 事件组与 bit 定义 全项目共用一个事件组 `sys_evtHandle`,每位表示一个子系统"就绪": ```c /* sys_events.h(新建,或并入 net_task.h) */ #include "cmsis_os2.h" extern osEventFlagsId_t sys_evtHandle; #define SYS_EVT_NET_READY (1u << 0) /* netTask:net_init + PHY 稳定完成 */ #define SYS_EVT_STORAGE_READY (1u << 1) /* defaultTask:FTL/FatFS 挂载完成 */ #define SYS_EVT_FTP_READY (1u << 2) /* ftpTask:lftpd_start 完成 */ #define SYS_EVT_TIMESYNC_READY (1u << 3) /* timeSyncTask:首次时间同步完成 */ /* 后续新增子系统继续左移分配 bit,32 位上限内即可 */ ``` ### 3.2 生产者 / 消费者矩阵 | 事件 bit | 生产者(置位处) | 消费者(等待处) | |----------|------------------|------------------| | `SYS_EVT_NET_READY` | `netTask`(`net_task_func` 中 `net_stack_bring_up()` 成功后置位) | `ftpTask`、`start_test_task`(NET 模式)、`timeSyncTask` | | `SYS_EVT_STORAGE_READY` | `defaultTask`(挂载成功后) | 依赖文件系统的测试套件(如 STORAGE/GD5F) | | `SYS_EVT_FTP_READY` | `ftpTask`(`lftpd_start` 成功后) | 无(示例,预留给需要 FTP 就绪的后续功能) | | `SYS_EVT_TIMESYNC_READY` | (预留,暂无生产者置位) | 可选:对时精度敏感的业务 | ### 3.3 事件组创建时机 `sys_evtHandle` 必须在**任何任务 `wait` 之前**存在。`main.c` 调用顺序为: ``` osKernelInitialize(); MX_FREERTOS_Init(); /* CubeMX 在此创建事件组并创建任务 */ osKernelStart(); ``` 事件组**由 CubeMX 代码生成器创建**(见第 4 节 Step 1 的 Events 对象),生成器会在 `MX_FREERTOS_Init` 的任务创建之前插入 `sys_evtHandle = osEventFlagsNew(&sys_evt_attributes);`, 因此无需手写创建代码。(CMSIS-RTOS v2 允许在 `osKernelInitialize()` 之后、 `osKernelStart()` 之前创建内核对象。) --- ## 4. 实施步骤 ### Step 1 定义事件组 - 新建 `App/sys_events.h`,放入第 3.1 节的 `extern osEventFlagsId_t sys_evtHandle;` 声明与 bit 宏。 - 在 **CubeMX** 中配置事件组(推荐,避免手写创建代码): 1. 打开 `STM32F407-Demo.ioc` → `Middleware` / `Software Components` → `FREERTOS` → `Configuration`, 确认 `Mode` 为 **CMSIS_V2**(本项目即用此接口)。 2. 在 FREERTOS 配置树中找到 **Events(事件标志)** 分类,点击 `Add` 新建一个 Event Flags 对象。 3. 命名(Object Name)填 **`sys_evt`**(必须与此一致,代码生成器会据此产生句柄 `sys_evtHandle`)。 4. 其余参数默认,保存并 **Generate Code**。 - 代码生成器自动在 `freertos.c` 生成: - 句柄声明 `osEventFlagsId_t sys_evtHandle;` - 属性 `const osEventFlagsAttr_t sys_evt_attributes = { .name = "sys_evt" };` - 创建 `sys_evtHandle = osEventFlagsNew(&sys_evt_attributes);`(位于 `MX_FREERTOS_Init` 任务创建之前) → 因此 `net_task.c` 不再需要手动定义/创建事件组,仅 `extern` 引用即可。 ### Step 2 netTask 置位(生产者) 在 `App/task/net_task.c` 的 `net_task_func()` 中,`net_stack_bring_up()` 成功返回后(已退出失败死等分支)置位: ```c /* 广播"网络就绪",唤醒等待它的 ftpTask / start_test_task / timeSyncTask 等 */ if (sys_evtHandle != NULL) { osEventFlagsSet(sys_evtHandle, SYS_EVT_NET_READY); } ``` > 注意:置位点放在 `net_task_func` 而非 `net_stack_bring_up` 内部。`net_stack_bring_up` > 是 NET 层 / 硬件层两种模式共用的纯初始化函数(硬件层由 testTask 调用并独占 CH395F), > 若把置位塞进它内部,硬件层模式会误广播 NET_READY,导致 ftpTask 起 FTP 服务与硬件测试抢 CH395F。 > 因此只在 netTask 上下文广播。 ### Step 3 defaultTask 置位存储就绪(生产者) 在 `defaultTask` 完成 FTL/FatFS 挂载成功后: ```c osEventFlagsSet(sys_evtHandle, SYS_EVT_STORAGE_READY); ``` ### Step 4 ftpTask 改 wait(消费者) 将 `Src/freertos.c` 中 `StartFtpTask`(`__weak`)的: ```c osDelay(7000); /* 旧:魔法延时 */ lftpd_start("/", 21, &ftp); ``` 改为(网络 **且** 存储都就绪才 listen): ```c /* 等网络 + 存储都真正就绪后再 listen,取代拍脑袋的 7s 魔法延时 */ if (sys_evtHandle != NULL) { (void)osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY | SYS_EVT_STORAGE_READY, osFlagsWaitAll, osWaitForever); } lftpd_start("/", 21, &ftp); osEventFlagsSet(sys_evtHandle, SYS_EVT_FTP_READY); /* 自身就绪也广播(可选) */ ``` ### Step 5 start_test_task 改 wait(消费者) 将 `test/ch395f_test_task.c` 中 NET 模式的: ```c while (!g_net_ready) { /* 旧:忙等轮询 */ osDelay(100); } ``` 改为: ```c (void)osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY, osFlagsWaitAny, osWaitForever); ``` 若测试套件同时需要文件系统,可组合等待: ```c uint32_t need = SYS_EVT_NET_READY | SYS_EVT_STORAGE_READY; osEventFlagsWait(sys_evtHandle, need, osFlagsWaitAll, osWaitForever); ``` ### Step 6 timeSyncTask 必须等网络就绪(已实现) `timeSyncTask` **依赖网络**,`App/time_sync.c` 中已在入口 `wait SYS_EVT_NET_READY`(`osFlagsWaitAny, osWaitForever`)后才开始时间同步,取代原先 8s 魔法延时。代码已落地: ```c /* 等网络栈真正就绪,确保 netTask 已开始处理消息 */ if (sys_evtHandle != NULL) { (void)osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY, osFlagsWaitAny, osWaitForever); } ``` > 注:`SYS_EVT_TIMESYNC_READY` 目前仅定义、**暂无生产者置位**(预留给对时精度敏感的业务),故其生产者列为"可选/预留",消费者亦暂不存在。 --- ## 5. 迁移前后对照 | 场景 | 改造前 | 改造后 | |------|------|--------| | `ftpTask` 等网络 | `osDelay(7000)` 硬等 | `osEventFlagsWait(NET_READY, …, osWaitForever)` 就绪即放行 | | `start_test_task` 等网络 | `while(!g_net_ready) osDelay(100)` 每 100ms 空转 | `osEventFlagsWait(NET_READY, …, osWaitForever)` 零占用阻塞 | | "网络且存储都就绪" | 无法实现 | `osEventFlagsWait(NET_READY|STORAGE_READY, osFlagsWaitAll, …)` | | 新增子系统 X | 手动在依赖任务里加延时/轮询 | 定义 `SYS_EVT_X`,生产者置位、消费者 `wait`,互不改动 | --- ## 6. 注意事项 / 潜在坑 1. **事件组必须先建后用**:`sys_evtHandle` 由 CubeMX(FREERTOS Events 对象 `sys_evt`)在 `MX_FREERTOS_Init` 的任务创建之前自动创建,因此早于所有 `wait`。若句柄异常为 `NULL`,`osEventFlagsSet/Wait` 会返回 `osError` 且不阻塞——消费者会直接越过等待往下跑,重新引入竞态;各等待点已用 `if (sys_evtHandle != NULL)` 防御。 2. **`osWaitForever` 死锁风险**:若某生产者永远不置位(如初始化失败提前 `return`),消费者会永久阻塞。建议给关键 `wait` 加有限超时 + 错误日志,便于定位: ```c if (osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY, osFlagsWaitAny, 30000U) == 0) { DBG_ERROR("wait NET_READY timeout"); /* 后续可走降级/出错处理 */ } ``` 3. **`wait` 只能在任务上下文**:`osEventFlagsWait` 不能在中断中调用;本项目所有等待点都在任务入口,安全。`osEventFlagsSet` 可在任务或中断中调用。 4. **bit 上限**:事件标志为 32 位,最高位(1<<31)被 CMSIS-RTOS 保留,可用 bit 0~30。 5. **测试构建(HW 层模式)的特殊性**:`TEST_SUITE_CH395F` 硬件层模式下 `netTask` 不创建,`start_test_task` 自己 `net_stack_bring_up()` 独占 CH395F。此时**没有 netTask 来置 `SYS_EVT_NET_READY`**(置位点只在 `net_task_func` 内)。结果:`ftpTask` / `timeSyncTask` 会一直阻塞在 NET_READY 上——这正符合"硬件层模式不需要 FTP / 网络时间同步、CH395F 被 testTask 独占"的设计,属预期行为,不构成冲突。若将来需要在硬件层模式也起 FTP,可让 `start_test_task` 在 bring-up 成功后自行 `osEventFlagsSet(NET_READY)`。 6. **`g_net_ready` 去留**:**已完成淘汰**。`g_net_ready` 原全局标志已从代码中彻底移除(定义、置位、所有消费方均改为 Event Flags 等待),仅保留事件标志表达就绪。 --- ## 7. 总结 **不要靠"等多少毫秒"来赌依赖就绪,而要靠"对方声明自己就绪了"来驱动启动**——Event Flags 把隐式的时间耦合变成显式的事件依赖,启动顺序自然清晰、健壮、易扩展。