用户裁定两条:① 剩余裸写端口的地方加「工厂默认」限定词,不改固件行为; ② 生效语义按需求口径「保存即生效」定,现实现(端口下次重 bind 才生效)是 要改的一侧 → 建任务登记,本笔不动代码行为。 限定词(24 处 / 17 文件):AGENTS 数据主线、App/protocol/README、根 README 文档索引、Src/freertos.c 三处任务注释、docs/README、项目启动顺序管理、存储 架构说明、软件架构与运行时规格(2)、RAM 使用分析、产品需求设计文档(5)、 项目架构总览(3)、test/host/README、iec104_master 流程注释、IEC104 上行设 计说明、继电保护功能复现规划。配图两张同批重跑:gen_arch_svg.py 与 gen_memory_svg.py(脚本与产物同一笔)。 两处陈旧计数(都做过双源核证): - 测试开发标准流程 像素层「60 内容页」→ 内容页黄金参考 66 张(64 页); 双源 = simulator/tests/ref/shot_*.bin 实数 66 与 check_content_pages.py 的 PAGES 66 条(含两个 _p2)。 - .pre-commit-config.yaml 的 uinav-selftest 注释「18 张黄金参考」→ 写成 「接线当时 18 张、现 66 张」:git log -S 定到 6a904c1b(2026-10-25),那 天 ref 目录确实 18 张,写时为真,但它描述的是钩子现有行为,属当前态。 口径修正与登记: - 页面扩充详细方案「通讯设置 = 无配置模型(端口与 CA 都是 #define)」→ 标 「当时」,并写明现已在 /config.ini 的 [comm] 段可配、UI 通讯设置页可改。 - 产品需求 §通讯配置:删掉「改后需重启生效」这句把两件事混为一谈的话,改成 需求要求「保存即生效」+ 现实现(公共地址重启、端口下次重 bind)+ 指向登记簿。 - docs/任务规划/登记簿.md §1 新增「104 端口改后立刻重 bind」:含隐性耦合 —— .serialtest/config.json 的必检锚点写死 listening on 2404,端口一改串口 回归必假失败;改行为时要么锚点只查 listening on,要么由脚本注入端口。 复扫(旧形态归零判据)剩 20 处未带限定词,逐条定性为刻意不改:默认值定义处、 出厂模板与断言、usage 与 argparse default、UI 黄金参考显示值、台架主站地址 (同数不同义)、串口锚点、叙事句、引用关系图坐标。逐条理由见一次性台账。 门禁:check_doc_links --sections-strict 对 16 篇改动 .md 逐个 rc=0; check_mermaid --all 31 文件 62 围栏新发 0;check_nested_comment --all 449 文件新发 0;check_bom_drift --all 99 个名单内文件无漂移; check_generated_fresh PASS(25 生成器 / 21 一致 / 4 只减不增基线);字节体检 无新增 BOM / CR / U+FFFD / 行尾空格(三连空行与 HEAD 逐项相同 = 存量)。 /keil-build 0 错 0 警,Program Size Code=246720 RO=29836 RW=2052 ZI=149708 与凭证同码:本笔只碰注释与配图文字,无固件行为变更。
23 KiB
项目启动顺序管理(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 之前的历史做法,仅作背景记录,当前代码已不再使用。
- 全局标志轮询:
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-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,相当于"创建即挂起、就绪才放行"。
上图:左 = 生产者(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 中配置事件组(推荐,避免手写创建代码):
- 打开
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;旧档 /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. 注意事项 / 潜在坑
-
事件组必须先建后用:
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/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 才发现)。
- 修法:所有状态位等待加
-
启动段(
osKernelStart之前)死机的判读法(2026-10-02 落地):app_main_init全程运行在内核启动前——此时没有任务、没有看门狗保护,一旦卡死就是"无输出 + LED 不闪"(立项时明示的代价)。得益于那次整改补齐的可观测性,现在每一步都有结果日志,判读规则很简单:开机日志的最后一行 = 卡住的那一步(该步的"结果日志"没打出来)。
启动进度轨迹(按顺序,每条都是"结果"而非"进入前"锚点):
# 日志行 对应步骤 卡此步的含义 1 boot: syslog init OKsyslog_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/TIMEOUTtpafe5160_init()TPAFE5160 SPI 通信 9 ADC input layer init: OKadc_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: OKfreq_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):
- 纯寄存器 / HAL 轮询操作:启动段初始化只走不依赖内核的裸驱动路径;
- 无锁静态缓冲:内核前产生的日志先暂存到
s_preboot[8](SYSLOG_QUEUE_MAX = 8,纯 BSS 数组,不碰任何内核对象;RTOS 前单生产者、后单消费者,故免锁)。投递点syslog_post按内核状态(osKernelGetState())自动分流:内核未运行 → 写s_preboot;已运行 → 走真队列; - 内核启动后惰性创建真队列:真队列推迟到任务上下文首次使用时才创建——见
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 把隐式的时间耦合变成显式的事件依赖,启动顺序自然清晰、健壮、易扩展。