Files
STM32F4-Base/docs/架构设计/项目启动顺序管理.md
T
wandering1 593c5d0c84 refactor(编号): 采集链分层 L1/L1.5/L2/L2.5/L3/L4/L5 → 语义名(批次 1)
用户裁定(2026-10-28):L1/L1.5/L2/L2.5/L3 这套层号「谁能看懂」,改用语义名。
映射(同一批定稿,含 L4/L5):
  L1   → 语义取数与有效性
  L1.5 → 标定折算
  L2   → 全周傅氏相量
  L2.5 → 测频与遥测快照
  L3   → 保护判据
  L4   → 出口与记录
  L5   → 动作留痕

实测规模:384 站点 / 62 文件(App/algo、App/task、test/host 六个套件、docs/算法、
产品需求、架构总览、启动顺序、ADC 采样节拍、IEC104 上行、采集短板评测、内存管理、
AGENTS.md、README.md、docs/README.md、.pre-commit-config.yaml 与 scripts 里的注释)。

改法(不是全局替换,逐模式有上下文规则):
- 先吃同位语再落裸词:L1 有效性校验 → 语义取数与有效性校验、L2 相量层 → 全周傅氏
  相量层、L3 保护 → 保护判据、L2.5 遥测快照 → 测频与遥测快照,避免「保护判据保护判据」
  这类重复(第一版就是这么坏掉的,已整体回滚重做)。
- .c/.h 只改注释:先把字符串字面量切出去(引号数为奇数的跨行注释按整行处理),
  C 字符串里的 L2-in ring 之类原样保留——AGENTS 铁律「字符串字面量必须纯 ASCII」。
- 层内条目号(L1-3 / L2-3 这类)**本批不动**,属批次 2;另外三族(UI 菜单层级
  L0~L3、链路延迟分段 L1~L8、CubeMX 防丢失三层)也不动,属批次 3。
- 顺手修的措辞:范围写法 L1~L3 → 「分层(语义取数与有效性 → 保护判据)」、
  CI 段「不含 L1/L2/L3」→「不含采集链分层」、目录地图里同一处并列表述。

验证:@build 0 错 0 警(Code=236456,与改前逐字节同尺寸——纯注释改动);
内存布局配图已按新脚本重生成(自检通过);doc-links --all --sections-strict 通过
(195 文档);mojibake / bom-guard / coding-standard / 新编号门禁自查 全 rc=0;
git diff --check 干净。
2026-10-28 13:04:02 +08:00

23 KiB
Raw Blame History

项目启动顺序管理(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 状态机,B4) 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(),保护判据,B5) 无(节拍自持,不依赖网络)
rs485_task RS-485 通信(任务体已实现) 未在 freertos.c 创建(B2 暂缓,见 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)三栏依赖图

上图:左 = 生产者(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、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() 成功返回后(已退出失败死等分支)置位:

/* 广播"网络就绪",唤醒等待它的 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 不读、不迁移,失败回退工厂默认) 后:

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/board/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/sync/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. 注意事项 / 潜在坑

  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 条:位被别的消费者吃掉同样造成永久阻塞,而且更隐蔽):

    /* 状态位必须带 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 把隐式的时间耦合变成显式的事件依赖,启动顺序自然清晰、健壮、易扩展。