2525c2d948
用户裁定两条:① 剩余裸写端口的地方加「工厂默认」限定词,不改固件行为; ② 生效语义按需求口径「保存即生效」定,现实现(端口下次重 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 与凭证同码:本笔只碰注释与配图文字,无固件行为变更。
276 lines
23 KiB
Markdown
276 lines
23 KiB
Markdown
# 项目启动顺序管理(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`,相当于"创建即挂起、就绪才放行"。
|
||
|
||

|
||
|
||
> 上图:**左 = 生产者(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 把隐式的时间耦合变成显式的事件依赖,启动顺序自然清晰、健壮、易扩展。
|