Files
STM32F4-Base/docs/架构设计/项目启动顺序管理.md
T
wandering1 2525c2d948 docs(104): 端口口径统一成「工厂默认」,登记「改后立即重 bind」待办
用户裁定两条:① 剩余裸写端口的地方加「工厂默认」限定词,不改固件行为;
② 生效语义按需求口径「保存即生效」定,现实现(端口下次重 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
与凭证同码:本笔只碰注释与配图文字,无固件行为变更。
2026-10-30 15:40:27 +08:00

276 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 项目启动顺序管理(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)三栏依赖图](../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 把隐式的时间耦合变成显式的事件依赖,启动顺序自然清晰、健壮、易扩展。