Files
STM32F4-Base/docs/项目启动顺序管理.md
T
2026-08-29 11:10:08 +08:00

12 KiB
Raw Blame History

项目启动顺序管理(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
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,每位表示一个子系统"就绪":

/* 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() 成功返回后(已退出失败死等分支)置位:

/* 广播"网络就绪",唤醒等待它的 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 挂载成功后:

osEventFlagsSet(sys_evtHandle, SYS_EVT_STORAGE_READY);

Step 4 ftpTask 改 wait(消费者)

将 Src/freertos.c 中 StartFtpTask(__weak)的:

osDelay(7000);                 /* 旧:魔法延时 */
lftpd_start("/", 21, &ftp);

改为(网络 且 存储都就绪才 listen):

/* 等网络 + 存储都真正就绪后再 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 模式的:

while (!g_net_ready) {         /* 旧:忙等轮询 */
    osDelay(100);
}

改为:

(void)osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY, osFlagsWaitAny, osWaitForever);

若测试套件同时需要文件系统,可组合等待:

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 魔法延时。代码已落地:

/* 等网络栈真正就绪,确保 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
新增子系统 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 加有限超时 + 错误日志,便于定位:
    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 把隐式的时间耦合变成显式的事件依赖,启动顺序自然清晰、健壮、易扩展。