# 项目启动顺序管理(Event Flags 就绪屏障 · 已落地) > 已落地:用 CMSIS-RTOS v2 的 **Event Flags** 实现"就绪屏障",取代原先的全局标志轮询 + 魔法延时来管理任务启动顺序,消除脆弱的时间耦合。事件组句柄 `sys_evtHandle` 由 CubeMX(FREERTOS Events 对象 `sys_evt`)生成。 --- ## 1. 背景与现状问题 ### 1.1 现有任务 | 任务 | 职责 | 当前启动依赖 | |------|------|--------------| | `defaultTask` | FTL + FatFs 存储 bring-up + 配置引擎属主 + syslog 唯一写者(一次性后转循环) | 无(最先跑) | | `netTask` | `net_stack_bring_up()` → `net_poll()` 周期轮询 + `net_process_messages()` | 等 `SYS_EVT_STORAGE_READY`(配置已由 defaultTask 屏障前加载,本任务只读 RAM 副本) | | `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)` 后开始同步 | | `uiTask` | LVGL UI(`ui_port_init` → `ui_menu_init` → `ui_net_sink` :7003 → 10ms 刷新) | `osEventFlagsWait(SYS_EVT_NET_READY, osFlagsWaitAny \| osFlagsNoClear, osWaitForever)` | | `iec104Task` | IEC 60870-5-104 上行(工厂默认 :2404 监听 + link 状态机,IEC 104 上行) | `osEventFlagsWait(SYS_EVT_NET_READY, … \| osFlagsNoClear)` | | `adcTask` | TIM3 1kHz 采样节拍**纯消费者**(`adc_input_poll()`,语义取数与有效性/全周傅氏相量取数,不做保护运算) | **无事件位依赖**:TIM3 ISR 内 `adc_tick_irq_notify()` 直接唤醒(TIM3 比调度器早 ≈100ms 启动,ISR 侧只能走该通知函数) | | `protectTask` | 保护判据 20ms 节拍(`prot_elm_run()`,保护判据,保护任务) | 无(节拍自持,不依赖网络) | | `rs485_task` | RS-485 通信(任务体已实现) | **未在 `freertos.c` 创建**(RS-485 采集链路 暂缓,见 `docs/任务规划/暂缓-RS485.md`) | ### 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-NET-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`,相当于"创建即挂起、就绪才放行"。 ![就绪屏障(Event Flags)三栏依赖图](../images/就绪屏障.svg) > 上图:**左 = 生产者(set)· 中 = 事件组 `sys_evtHandle` · 右 = 消费者(wait)**;任务卡与事件位对齐同一行,`bit1 → ftpTask` 的额外依赖用 **All 汇合点**(`osFlagsWaitAll`)表达。 > 源码 `tools/arch_diagram/gen_barrier_svg.py`(改数据重跑即可),PNG 预览本地渲染、不入库。 --- ## 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:首次时间同步完成 */ #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`、`uiTask`、`iec104Task` | | `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/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 挂载 **+ 网络配置加载(`net_cfg_load`;旧档 `/cfg/netcfg.bin` 不读、不迁移,失败回退工厂默认)** 后: ```c osEventFlagsSet(sys_evtHandle, SYS_EVT_STORAGE_READY); ``` > 屏障语义 = "卷挂载 + 配置就绪":`STORAGE_READY` 之后,`netTask` 组网时 > 网络配置 RAM 副本恒有效(无需再读盘)。 ### Step 4 ftpTask 改 wait(消费者) 将 `Src/freertos.c` 中 `StartFtpTask`(`__weak`)的: ```c osDelay(7000); /* 旧:魔法延时 */ lftpd_start("/", 21, &ftp); ``` 改为(网络 **且** 存储都就绪才 listen): ```c /* 等网络 + 存储都真正就绪后再 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/board/ch395f_test_task.c` 中 NET 模式的: ```c while (!g_net_ready) { /* 旧:忙等轮询 */ osDelay(100); } ``` 改为: ```c (void)osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY, osFlagsWaitAny | osFlagsNoClear, osWaitForever); ``` 若测试套件同时需要文件系统,可组合等待: ```c uint32_t need = SYS_EVT_NET_READY | SYS_EVT_STORAGE_READY; osEventFlagsWait(sys_evtHandle, need, osFlagsWaitAll | osFlagsNoClear, osWaitForever); ``` ### Step 6 timeSyncTask 必须等网络就绪(已实现) `timeSyncTask` **依赖网络**,`App/sync/time_sync.c` 中已在入口 `wait SYS_EVT_NET_READY`(`osFlagsWaitAny | osFlagsNoClear, osWaitForever`)后才开始时间同步,取代原先 8s 魔法延时。代码已落地: ```c /* 等网络栈真正就绪,确保 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. 注意事项 / 潜在坑 1. **事件组必须先建后用**:`sys_evtHandle` 由 CubeMX(FREERTOS Events 对象 `sys_evt`)在 `MX_FREERTOS_Init` 的任务创建之前自动创建,因此早于所有 `wait`。若句柄异常为 `NULL`,`osEventFlagsSet/Wait` 会返回 `osError` 且不阻塞——消费者会直接越过等待往下跑,重新引入竞态;各等待点已用 `if (sys_evtHandle != NULL)` 防御。 2. **`osWaitForever` 死锁风险**:若某生产者永远不置位(如初始化失败提前 `return`),消费者会永久阻塞。建议给关键 `wait` 加有限超时 + 错误日志,便于定位(另见第 7 条:**位被别的消费者吃掉**同样造成永久阻塞,而且更隐蔽): ```c /* 状态位必须带 osFlagsNoClear,否则这条带超时的探测会把 NET_READY 吃掉, * 后面的消费者(含 ftpTask 的 WaitAll)就再也等不到了 */ if (osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY, osFlagsWaitAny | osFlagsNoClear, 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`(基础线程)入口自挂起(不 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)`。 6. **`g_net_ready` 去留**:**已完成淘汰**。`g_net_ready` 原全局标志已从代码中彻底移除(定义、置位、所有消费方均改为 Event Flags 等待),仅保留事件标志表达就绪。 7. **⚠ 就绪位被消费 → 永久阻塞(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/sync/time_sync.c`、`App/task/net_task.c`、`test/board/ch395f_test_task.c`);**命令位**(`CFG_SAVE`/`NET_RECONFIG`)保持消费型等待。 - **工程化防线**:① 关键消费者加"启动超时告警"(如 ftpTask 10s 内未到 listen 就 `DBG_ERROR`);② 用 `scripts/stack_report.py` 看任务栈水位——**某任务水位异常低 = 它根本没干活**(本次正是靠 ftpTask 只有 156B 才发现)。 8. **启动段(`osKernelStart` 之前)死机的判读法(2026-10-02 落地)**:`app_main_init` 全程运行在**内核启动前**——此时没有任务、没有看门狗保护,一旦卡死就是"无输出 + LED 不闪"(立项时明示的代价)。得益于那次整改补齐的可观测性,现在**每一步都有结果日志**,判读规则很简单: > **开机日志的最后一行 = 卡住的那一步**(该步的"结果日志"没打出来)。 启动进度轨迹(按顺序,每条都是"结果"而非"进入前"锚点): | # | 日志行 | 对应步骤 | 卡此步的含义 | |---|---|---|---| | 1 | `boot: syslog init OK` | `syslog_mcu_init()` | (纯 RAM,不会卡) | | 2 | `fw build: <日期 时间> <变体>` | 固件身份戳(构建变体串) | (纯打印,不会卡) | | 3 | `reset cause: ... (CSR=...)` | 复位原因翻译 | (纯计算,不会卡) | | 4 | `SD2506 init: OK/FAILED (ret=N)` | `sd2506_init()` | **I2C1 不通**(PB6/PB7、上拉、供电)——失败时其内部还会先打 `sd2506_init: ... FAIL` 指明具体寄存器与错误码 | | 5 | `[CLK] sys_clock_init: ...` | `sys_clock_init()` | RTC 读/写异常(降级时会打 `RTC abnormal, fallback to default time`) | | 6 | `RS485 init: OK (UART4, DE=PA15)` | `rs485_init()` | UART4 / DE 引脚配置 | | 7 | `NAND init: OK/...` | `gd5f2gq5ue_init()` | **BBT 全片扫描**——启动段最耗时的一步,卡住多为此处(SPI2 与片选) | | 8 | `ADC init: OK/TIMEOUT` | `tpafe5160_init()` | TPAFE5160 SPI 通信 | | 9 | `ADC input layer init: OK` | `adc_input_init()`(语义取数与有效性层的语义映射 + ADC1 DMA 武装) | 映射自检 FAILED / DMA arm FAILED(两条 `DBG_ERROR` 即此步;失败时无 OK 行) | | 10 | `phasor layer init: OK (inst=N)` | `phasor_init()` | 纯 RAM(不会卡;实例数不符 = 相量层配置问题) | | 11 | `freq layer init: OK` | `freq_init()` | 纯 RAM(不会卡) | | 12 | `protection init: OK (set=N elm=N)` | `prot_set_init()` + `prot_elm_init()` | 纯 RAM(不会卡) | | 13 | `SD2506 ID/time/temp/battery` 共 5 行 | SD2506 诊断 | I2C1(同第 4 步);缺行 = 诊断段卡死 | | 14 | `App init OK` | 全部完成(其后启动 TIM3 1kHz 采样节拍 `HAL_TIM_Base_Start_IT(&htim3)`,见 AGENTS.md 铁律) | 此后问题属任务/调度层,不再是启动段 | - **若一行日志都没有**:问题在 `fault_handler_enable()` 之前(`HAL_Init` / 时钟树 / `system_stm32f4xx.c`),或 USART1 本身未初始化(`MX_USART1_UART_Init` 在 `main.c` 的 CubeMX 初始化段)。 - **若卡在某步且无该步结果日志**:看该步**内部**是否还有更细的日志(如 `sd2506_init` 的 `ID read FAIL` / `charge reg read FAIL` 区分了 I2C 失败与寄存器校验失败)。 - **配套手段**:`sd2506_init` / `gd5f2gq5ue_init` 内部的失败日志已细化到错误码;`sys_clock_init` 的降级路径写 syslog 自检(`clock chip fault`)。**注意日志本身有成本**——`DBG_*` 单条约 90 B Flash + 192 B 栈(见根目录 `AGENTS.md`),故只补真正静默的黑洞,不加冗余的"进入前/完成后"重复锚点。 --- ## 7. 内核启动前(`osKernelStart` 之前)的编码模式 > 本节是根目录 `AGENTS.md`「平台约束」中这条铁律的完整细节迁入处(2026-10-11,AGENTS.md 瘦身,不再当事实存放地)。 **禁令:`osKernelStart` 之前禁止调用任何 FreeRTOS / CMSIS-RTOS 创建类 API**——`osMessageQueueNew` / `osMessageQueuePut` / `osMutexNew` / `osSemaphoreNew` 等在内核启动前调用会**直接杀死启动**。 **2026-09-20 启动失败实证根因**:`syslog_mcu_init` 在内核启动前调用 `osMessageQueueNew`——这就是 09/19~09/20 "每次必死" 的代码层根因(修复提交 `82411cf`)。机理(见 `App/log/sys_log_mcu.c` 中 `syslog_mcu_init` 的函数注释):`app_main_init()` 运行在 `osKernelInitialize()` **之前**(`Src/main.c` 顺序 = `app_main_init` → `osKernelInitialize` → `MX_FREERTOS_Init` → `osKernelStart`),创建类 API 会在这一窗口提前拉起内核,其后的任务创建落在未完成初始化的内核状态上执行 → 启动失败。 **内核启动前代码的标准模式三件套**(现成参照:`App/log/sys_log_mcu.c`): 1. **纯寄存器 / HAL 轮询操作**:启动段初始化只走不依赖内核的裸驱动路径; 2. **无锁静态缓冲**:内核前产生的日志先暂存到 `s_preboot[8]`(`SYSLOG_QUEUE_MAX = 8`,纯 BSS 数组,不碰任何内核对象;RTOS 前单生产者、后单消费者,故免锁)。投递点 `syslog_post` 按内核状态(`osKernelGetState()`)自动分流:内核未运行 → 写 `s_preboot`;已运行 → 走真队列; 3. **内核启动后惰性创建真队列**:真队列推迟到**任务上下文首次使用时**才创建——见 `syslog_drain`(仅 defaultTask 调用,调度器已在运行,`s_queue == NULL` 时才 `osMessageQueueNew`)。 **推论**:凡在内核启动前执行的初始化函数里,**不得创建任何 CMSIS 对象**。落地效果:`syslog_mcu_init` 现为"内核启动前安全空操作"(直接 `return 0`),真实初始化全部后移到运行期。 > **与 §3.3 的边界(勿混读)**:§3.3 说"CMSIS-RTOS v2 允许在 `osKernelInitialize()` 之后、`osKernelStart()` 之前创建内核对象"——那是 **CubeMX 生成器**在 `MX_FREERTOS_Init` 里创建事件组 / 任务的专用窗口;**手写代码**(尤其 `app_main_init` 这条早于 `osKernelInitialize` 的链)不享受该窗口,一律按本节禁令执行。 **配套判读**:违反本禁令的后果是启动段死机(无输出 + LED 不闪,此时没有任务、没有看门狗保护),定位方法见 §6 第 8 条「启动段(`osKernelStart` 之前)死机的判读法」——开机日志的最后一行 = 卡住的那一步。 --- ## 8. 总结 **不要靠"等多少毫秒"来赌依赖就绪,而要靠"对方声明自己就绪了"来驱动启动**——Event Flags 把隐式的时间耦合变成显式的事件依赖,启动顺序自然清晰、健壮、易扩展。