207 lines
12 KiB
Markdown
207 lines
12 KiB
Markdown
# 项目启动顺序管理(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 把隐式的时间耦合变成显式的事件依赖,启动顺序自然清晰、健壮、易扩展。
|