- D-6 目录规范化:docs 根目录散落文档归入 UI设计/子系统设计/架构设计/审查记录/ 环境与部署/网络 等子目录,去掉目录名空格,重命名 嵌入式C语言代码规范_V1.0.md - D-7 三档一致性门禁:新增 scripts/check_doc_links.py(链接存在性)、 scripts/gen_task_table.py(FreeRTOS 任务表数值)、scripts/check_mem_numbers.py (内存/Flash 体积),均支持 --check/--write/--diff,pre-commit 钩子同步注册 - D-5 文档索引:新建 docs/README.md,新建 docs/文档规范.md 约束目录命名与链接写法 - D-1 芯片手册瘦身:删除 1069 张无引用孤儿图片(77.06 MB), docs/芯片手册 120 MB → 41 MB,已引用图片零断链 - 同步 C 源码注释中的文档路径引用(D-6 重组后),构建 0 错误 0 警告
16 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 + SYS_EVT_STORAGE_READY(osFlagsWaitAll | osFlagsNoClear) |
start_test_task(testTask) |
统一测试入口,按 TEST_SUITE_* 分发 |
NET 模式下 osEventFlagsWait(SYS_EVT_NET_READY, osFlagsWaitAny | osFlagsNoClear, osWaitForever) 阻塞等待 |
timeSyncTask |
墙钟时间同步 | osEventFlagsWait(SYS_EVT_NET_READY, … | osFlagsNoClear) 后开始同步 |
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,无需改动任何已有任务的代码。 - 就绪位是"状态",不是"事件":消费者必须带
osFlagsNoClear等待,否则先到的消费者会把位清掉,后面的消费者(尤其osFlagsWaitAll组合)永远等不到——2026-09-21 实测即由此导致ftpTask永久阻塞、FTP 21 端口从未监听(详见 §3.2 注与 §6 第 7 条)。
这与 CubeMX"一次性创建全部任务"的模式完全兼容:任务仍由 MX_FREERTOS_Init 统一创建,但各自在入口 wait,相当于"创建即挂起、就绪才放行"。
上图:左 = 生产者(set)· 中 = 事件组
sys_evtHandle· 右 = 消费者(wait);任务卡与事件位对齐同一行,bit1 → ftpTask的额外依赖用 All 汇合点(osFlagsWaitAll)表达。 源码tools/arch_diagram/gen_barrier_svg.py(改数据重跑即可),PNG 预览本地渲染、不入库。
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:首次时间同步完成 */
#define SYS_EVT_NET_RECONFIG (1u << 4) /* 配置保存成功:请求立即应用新网口配置(netTask 消费后重建网络) */
#define SYS_EVT_CFG_SAVE (1u << 5) /* UI:网口配置保存请求(defaultTask = 配置引擎属主,消费并写盘) */
/* 后续新增子系统继续左移分配 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(卷挂载 + 网络配置加载完成后) |
依赖文件系统的任务/测试套件(netTask 组网、STORAGE/GD5F 等) |
SYS_EVT_FTP_READY |
ftpTask(lftpd_start 成功后) |
无(示例,预留给需要 FTP 就绪的后续功能) |
SYS_EVT_TIMESYNC_READY |
(预留,暂无生产者置位) | 可选:对时精度敏感的业务 |
SYS_EVT_NET_RECONFIG |
配置保存成功后(配置引擎属主 defaultTask 写盘后经 net_cfg 应用回调置位) |
netTask 主循环:消费后 net_stack_bring_up() 重建网络(新网口参数立即生效),并递增网络代次号 |
SYS_EVT_CFG_SAVE |
UI 网口设置页保存行(经 net_cfg 保存交接回调置位) |
defaultTask 主循环:立即唤醒执行 net_cfg_save_run()(写盘;成功后置 SYS_EVT_NET_RECONFIG 触发应用) |
各消费点的等待选项(2026-09-21 修正,务必照此写):
- 状态位(
NET_READY/STORAGE_READY):osFlagsWaitAny | osFlagsNoClear(单依赖)或osFlagsWaitAll | osFlagsNoClear(组合依赖,如ftpTask)——任何消费者都不得消费它;- 命令位(
NET_RECONFIG/CFG_SAVE):消费型等待(不加osFlagsNoClear)——必须恰好消费一次,否则会被反复执行(重复写盘 / 反复重建网络)。 判据:这个位表达的是"环境已具备"(状态)还是"请做一件事"(事件)。
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 挂载 + 网络配置加载(net_cfg_load,含首启旧档迁移;失败回退工厂默认) 后:
osEventFlagsSet(sys_evtHandle, SYS_EVT_STORAGE_READY);
屏障语义 = "卷挂载 + 配置就绪":
STORAGE_READY之后,netTask组网时 网络配置 RAM 副本恒有效(无需再读盘)。
Step 4 ftpTask 改 wait(消费者)
将 Src/freertos.c 中 StartFtpTask(__weak)的:
osDelay(7000); /* 旧:魔法延时 */
lftpd_start("/", 21, &ftp);
改为(网络 且 存储都就绪才 listen):
/* 等网络 + 存储都真正就绪后再 listen,取代拍脑袋的 7s 魔法延时。
* osFlagsNoClear 必须带:就绪位是状态,被别的消费者清掉后本任务的
* WaitAll(两位) 将永不成立(2026-09-21 实测:FTP 21 端口从未监听) */
if (sys_evtHandle != NULL) {
(void)osEventFlagsWait(sys_evtHandle,
SYS_EVT_NET_READY | SYS_EVT_STORAGE_READY,
osFlagsWaitAll | osFlagsNoClear, 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 | osFlagsNoClear, osWaitForever);
若测试套件同时需要文件系统,可组合等待:
uint32_t need = SYS_EVT_NET_READY | SYS_EVT_STORAGE_READY;
osEventFlagsWait(sys_evtHandle, need, osFlagsWaitAll | osFlagsNoClear, osWaitForever);
Step 6 timeSyncTask 必须等网络就绪(已实现)
timeSyncTask 依赖网络,App/time_sync.c 中已在入口 wait SYS_EVT_NET_READY(osFlagsWaitAny | osFlagsNoClear, osWaitForever)后才开始时间同步,取代原先 8s 魔法延时。代码已落地:
/* 等网络栈真正就绪,确保 netTask 已开始处理消息(NoClear:状态位不得被消费) */
if (sys_evtHandle != NULL) {
(void)osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY,
osFlagsWaitAny | osFlagsNoClear, osWaitForever);
}
注:
SYS_EVT_TIMESYNC_READY目前仅定义、暂无生产者置位(预留给对时精度敏感的业务),故其生产者列为"可选/预留",消费者亦暂不存在。
5. 迁移前后对照
| 场景 | 改造前 | 改造后 |
|---|---|---|
ftpTask 等网络 |
osDelay(7000) 硬等 |
osEventFlagsWait(NET_READY+STORAGE_READY, osFlagsWaitAll+osFlagsNoClear, …) 就绪即放行 |
start_test_task 等网络 |
while(!g_net_ready) osDelay(100) 每 100ms 空转 |
osEventFlagsWait(NET_READY, osFlagsWaitAny+osFlagsNoClear, …) 零占用阻塞 |
| "网络且存储都就绪" | 无法实现 | osEventFlagsWait(NET_READY+STORAGE_READY, osFlagsWaitAll+osFlagsNoClear, …) |
| 新增子系统 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加有限超时 + 错误日志,便于定位(另见第 7 条:位被别的消费者吃掉同样造成永久阻塞,而且更隐蔽):/* 状态位必须带 osFlagsNoClear,否则这条带超时的探测会把 NET_READY 吃掉, * 后面的消费者(含 ftpTask 的 WaitAll)就再也等不到了 */ if (osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY, osFlagsWaitAny | osFlagsNoClear, 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(基础线程)入口自挂起(不 bring_up、不轮询、不广播SYS_EVT_NET_READY),start_test_task自己net_stack_bring_up()独占 CH395F。此时无人置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 等待),仅保留事件标志表达就绪。- ⚠ 就绪位被消费 → 永久阻塞(2026-09-21 实测,最隐蔽的一类):
osEventFlagsWait默认xClearOnExit = pdTRUE(CMSIS 包装见cmsis_os2.c:1243-1249),唤醒时就把满足条件的位清掉。修前netTask等STORAGE_READY(Any,消费 bit1)、uiTask/timeSyncTask等NET_READY(Any,消费 bit0),于是ftpTask的WaitAll(bit0|bit1)需要"两位同时为 1"的条件永不可能成立(bit1 在 netTask 醒来时消失、bit0 在 uiTask/timeSync 醒来时消失)→ 任务永久阻塞,无日志、无错误码、无超时(osWaitForever+(void)丢返回值 = 完全静默),FTP 21 端口从未监听。- 修法:所有状态位等待加
osFlagsNoClear(已改:Src/freertos.c的 ftpTask、App/task/ui_task.c、App/time_sync.c、App/task/net_task.c、test/ch395f_test_task.c);命令位(CFG_SAVE/NET_RECONFIG)保持消费型等待。 - 工程化防线:① 关键消费者加"启动超时告警"(如 ftpTask 10s 内未到 listen 就
DBG_ERROR);② 用scripts/stack_report.py看任务栈水位——某任务水位异常低 = 它根本没干活(本次正是靠 ftpTask 只有 156B 才发现)。
- 修法:所有状态位等待加
7. 总结
不要靠"等多少毫秒"来赌依赖就绪,而要靠"对方声明自己就绪了"来驱动启动——Event Flags 把隐式的时间耦合变成显式的事件依赖,启动顺序自然清晰、健壮、易扩展。