12 KiB
项目启动顺序管理(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 之前的历史做法,仅作背景记录,当前代码已不再使用。
- 全局标志轮询:
netTask就绪后置g_net_ready = 1,start_test_task用while (!g_net_ready) { osDelay(100); }忙等。 - 魔法延时:
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 中配置事件组(推荐,避免手写创建代码):
- 打开
STM32F407-Demo.ioc→Middleware/Software Components→FREERTOS→Configuration, 确认Mode为 CMSIS_V2(本项目即用此接口)。 - 在 FREERTOS 配置树中找到 Events(事件标志) 分类,点击
Add新建一个 Event Flags 对象。 - 命名(Object Name)填
sys_evt(必须与此一致,代码生成器会据此产生句柄sys_evtHandle)。 - 其余参数默认,保存并 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. 注意事项 / 潜在坑
- 事件组必须先建后用:
sys_evtHandle由 CubeMX(FREERTOS Events 对象sys_evt)在MX_FREERTOS_Init的任务创建之前自动创建,因此早于所有wait。若句柄异常为NULL,osEventFlagsSet/Wait会返回osError且不阻塞——消费者会直接越过等待往下跑,重新引入竞态;各等待点已用if (sys_evtHandle != NULL)防御。 osWaitForever死锁风险:若某生产者永远不置位(如初始化失败提前return),消费者会永久阻塞。建议给关键wait加有限超时 + 错误日志,便于定位:if (osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY, osFlagsWaitAny, 30000U) == 0) { DBG_ERROR("wait NET_READY timeout"); /* 后续可走降级/出错处理 */ }wait只能在任务上下文:osEventFlagsWait不能在中断中调用;本项目所有等待点都在任务入口,安全。osEventFlagsSet可在任务或中断中调用。- bit 上限:事件标志为 32 位,最高位(1<<31)被 CMSIS-RTOS 保留,可用 bit 0~30。
- 测试构建(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)。 g_net_ready去留:已完成淘汰。g_net_ready原全局标志已从代码中彻底移除(定义、置位、所有消费方均改为 Event Flags 等待),仅保留事件标志表达就绪。
7. 总结
不要靠"等多少毫秒"来赌依赖就绪,而要靠"对方声明自己就绪了"来驱动启动——Event Flags 把隐式的时间耦合变成显式的事件依赖,启动顺序自然清晰、健壮、易扩展。