Files
wandering1 4a4ad6bc5f chore(全仓): @build/@test 旧文档别名清退为技能调用名
现状口径:/keil-build 与 /serial-boot-test 是 DSH 会话的技能调用名,
不是 shell 命令;「名 + 命令行参数」的命令形态改为可执行体
run_boot_test.py 的真实调用。

- AGENTS.md:别名说明重写为「技能调用名不是 shell 命令」,两条目改
  「编译(/keil-build)」「串口回归(/serial-boot-test)」
- keil-build / serial-boot-test 两 skill 调用名与示例同步;task-worktree
  方案文档去掉 @build/@test 惯例从句;@task 保留(不在点名范围)
- 123 文件现状陈述全改;台架捕获、变更记录/时间线、rename 映射输入
  等 41 行历史证据保留旧名
- gen_memory_svg.py 6 处措辞随改,3 张 SVG 产物同批重跑
- 顺手补并行方案同行漏网计数 51→52 钩子

验证:提交期快钩子对 123 文件实跑全 Passed;check_doc_links --all
rc=0(3038 处、无法判定 0);check_mermaid --all rc=0;YAML 解析 52
钩子不变;skill-size rc=0;py_compile 全过;字节卫生零漂移。
2026-10-30 13:03:12 +08:00

853 lines
46 KiB
C
Raw Permalink 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.
/*
* 模块名称:Default Application Task
* 模块功能:主控任务(defaultTask)任务体实现:
* ① 启动段:存储 bring-up(挂载 + 空卷自动建卷 + 卷健康探针两击 +
* 自愈重格式化,挂载失败后主循环 3×2s 有界重试)+ 网络配置加载
* + STORAGE_READY 屏障 + "保存网络配置"回调注册
* ② 控制台单写者:dbg_console_writer_attach 注册 + 主循环 dbg_console_drain
* 排空(串口唯一发送者,见 App/log/console_log.c 设计说明)
* ③ 主循环:配置保存执行 + 2s 配置轮询 + LED 心跳 + syslog 落盘
* + 栈水位观测([STACK] 报告)
* 适用平台:STM32F407ZGTx + FreeRTOS(CMSIS-RTOS V2)
* 作者:王建锋
* 创建日期:2026-09-25
* 修改记录:
* v1.0 2026-09-25 王建锋 自 Src/freertos.c 迁出 StartDefaultTask 任务体及
* stack_probe_poll / storage_bring_up / 保存通知回调
* (CubeMX 生成文件只留任务创建接线与崩溃钩子)
* v1.1 2026-10-02 王建锋 存储 bring-up 首行创建卷锁(vol_lock_init,幂等):
* FTP 文件操作与 syslog/配置的卷访问自此串行化
* (2026-10-02 台架:并发 FTL 写 → journal 竞争 →
* 本任务主循环喂狗点饥饿 → IWDG 误复位)
* v1.2 2026-10-10 王建锋 任务耗时测量接线:主循环周期上报各任务最坏
* 耗时(task_prof_report),并在**心跳位图出现 1→0 边沿**
* 的瞬间留一份现场快照(task_prof_report_raw,裸 UART);
* 另给控制台排空与 syslog 落盘各加一个耗时探针
* v1.3 2026-10-25 DTU 项目组 写回缓存空闲 flush 接线:主循环 syslog 排空
* 之后调 vol_idle_sync_poll()(卷空闲周期 CTRL_SYNC 兜底,
* 模块 = App/fs/vol_idle_sync.[ch],道F 批 2)
* v1.4 2026-10-29 DTU 项目组 故障录波落盘道(task-2):vol_idle_sync_poll 之后
* 新增 wave_store_drain()——一次心跳整段搬完(lead 裁③:
* 一次 try 锁内 pack_next 256 B 分片 + 完整 open...close),
* 持锁耗时 >50 ms 补一条 WARN、峰值 ms 记本层静态;
* 轮转号自持(启动扫 /wave 重建,成功 ACK 才推进,lead 裁(b));
* 失败 = 先 unlink 半成品再 write_done(-1)(lead 裁①);
* 卷未就绪 / 环未封存静默跳过(§5.1 所有权 + §9 一条线约束)
* v1.5 2026-10-29 DTU 项目组 Lead 复核补第四道并发陷阱(本道与 adcTask 的抢占窗):
* 搬段中途 adcTask 可能正好走到 SEALED 滞留自弃(2 s)甚至
* 已封出下一段——CRC 是对"读到的字节"算的,半新半旧的段照样
* 自洽,只能靠写完关档后复检环侧身份(握手量 + 段首 t0/帧序)
* 判废:判废即 unlink、不 ACK、号不推进,失败按既有单条留痕
*/
#include "default_task.h"
#include "test_config.h"
#include "cmsis_os.h"
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "ff.h"
#include "nand_ftl.h"
#include "net_cfg.h"
#include "cfg_file.h"
#include "run_par.h" /* 运行参数段落盘(「数据可信」这一项,配置引擎属主 = 本任务) */
#include "prot_set_store.h"
#include "prot_set.h" /* task-10:prot_set_dirty_peek(区分"无待写"与"真落盘") */
#include "iec104_comm.h" /* 道G task-9:[comm] 段落盘(脏位轮询同款,通讯设置页) */
#include "sys_log.h"
#include "sys_events.h"
/* 深调用链模块(本任务 = 存储 bring-up + 配置引擎属主 + syslog 唯一写者,
* 全项目最深链在此):日志行缓冲收到 128 B(默认 192),本模块最长行渲染后
* < 128 → 不截断 */
#define DBG_LINE_BUF_MAX 128U
#include "dbg_log.h"
#include "task_watchdog.h"
#include "task_prof.h" /* 各任务最坏耗时上报 */
#include "vol_lock.h"
#include "vol_idle_sync.h" /* 写回缓存空闲 flush 兜底(App/fs/vol_idle_sync.h) */
#include "wave_ring.h" /* 录波环消费侧:sealed/sealed_meta/read_cb/write_done(task-2) */
#include "wave_store.h" /* 段打包游标 + 段号轮转纯函数(落盘道本体,task-2) */
#include "task_handles.h" /* 任务句柄与属性块的引用契约(定义处仍是 Src/freertos.c) */
/* Private defines -----------------------------------------------------------*/
/* 栈水位观测节奏:首次报告须晚于 bring-up 深链(mount/mkfs/探针/cfg/syslog),
* 之后每周期只判定"是否出现更深峰值",无新峰值不打印(保持串口静默窗口) */
#define STACK_PROBE_FIRST_MS 5000u
#define STACK_PROBE_PERIOD_MS 2000u
/* S11(2026-10-03):堆低水位告警阈值(B)。
* 依据:2026-10-03 上板两次开机的 min_ever 实测下限 3,712 B(当时 configTOTAL_HEAP_SIZE
* = 48KB)——低于此值说明堆余量 <8%,新增任务/缓冲随时可能分配失败(隐性风险)。
* 阈值取 3,072 B 留出运行间 ~600B 波动余量,避免把正常波动误报为故障。
* 2026-10-12(B5):堆扩至 56KB(.ioc configTOTAL_HEAP_SIZE=57344,CCM 腾给
* protectTask/iec104Task 栈),阈值**不随堆缩放**——它是绝对字节安全线。 */
#define HEAP_LOW_WATER_B 3072u
/* 存储自愈策略(2026-09-21 审计 P0 修复):启动期存储挂载失败后,
* 主循环按 2s 周期重跑完整 bring-up 的次数上限;耗尽则维持无存储降级
* (自检已上报、卷保留、不重格式化),等下次上电重新 resume */
#define STORAGE_BRINGUP_RETRIES 3u
/* 录波段落盘持锁耗时告警线(lead 裁③i):一次心跳整段 5250 B(约 21 次 256 B
* f_write),设计 §4.2 预估落盘窗「几十 ms」;越过本线说明本段在占用 defaultTask
* 喂狗链前的最慢一段,按事件补一条 WARN(不是周期行),并把历史峰值留在本层
* 静态里供收工报告核喂狗余量(vol_lock.h 铁律「等锁即毁喂狗链」的前例在案) */
#define WAVE_DRAIN_HELD_WARN_MS 50u
/* 搬运缓冲(§7 的 256 B;wave_store_pack_next 头阶段要求 cap >= 128,lead 转告(d)) */
#define WAVE_DRAIN_CHUNK_B 256u
/* Private typedef -------------------------------------------------------------*/
typedef struct {
const char *p_name;
const osThreadId_t *p_id;
const osThreadAttr_t *p_attr; /* 配置栈大小来源(stack_size) */
} stack_probe_task_t;
static const stack_probe_task_t s_stack_probe[] = {
{ "default", &defaultTaskHandle, &defaultTask_attributes },
{ "net", &netTaskHandle, &netTask_attributes },
{ "ftp", &ftpTaskHandle, &ftpTask_attributes },
{ "test", &testTaskHandle, &testTask_attributes },
{ "ui", &uiTaskHandle, &uiTask_attributes },
{ "timesync", &timeSyncTaskHandle, &timeSyncTask_attributes },
{ "adc", &adcTaskHandle, &adcTask_attributes },
{ "iec104", &iec104TaskHandle, &iec104Task_attributes },
{ "protect", &protectTaskHandle, &protectTask_attributes },
};
#define STACK_PROBE_COUNT (sizeof(s_stack_probe) / sizeof(s_stack_probe[0]))
static void stack_probe_poll(void);
#if !defined(TEST_SUITE_STORAGE)
static void default_task_net_cfg_save_notify(void);
static FRESULT storage_bring_up(FATFS *fs);
static void wave_store_drain(void);
#endif
/*
* 函数功能:defaultTask 任务体(2026-09-25 自 Src/freertos.c StartDefaultTask 迁出,
* 逻辑零改动)
* 入口参数:argument - 未用
* 返回值:无(任务不返回)
* 限定条件:仅由 defaultTask 调用(CubeMX 接线 StartDefaultTask 转调);本任务
* 只在内核运行期被调度,启动段允许使用 CMSIS 对象
* 函数说明:启动段 = 存储 bring-up + 保存回调注册 + 控制台写者注册;
* 主循环 = 事件等待(配置保存/控制台待发,50ms 兜底)→ 控制台排空
* → 配置保存执行 → 2s 配置轮询(含存储自愈有界重试)→ LED 心跳
* → syslog 落盘 → 栈水位观测
*/
void default_task_func(void *argument) {
(void)argument;
task_wd_bind(TASK_WD_DEFAULT); /* FTL 心跳路由:登记本任务句柄(入口一次) */
/* 系统启动期挂载 NAND 上的 FatFS 卷,供 FTP 及其它模块访问。
* f_mount(forced=1) 会触发 disk_initialize() → nand_ftl_init()
* (dhara_map_resume)。此前默认构建从未挂载,导致 FTP 的
* LIST/STOR/RETR 全部返回 550(f_opendir 失败)。
* 完整 bring-up 序列(挂载 + 空卷自动建卷 + 健康探针 + 自愈重格式化
* + 日志挂载 + 配置加载 + 就绪屏障)实现于 storage_bring_up()。
*
* 存储自愈(2026-09-21 审计,P0 修复):dhara_map_resume 失败不再被当
* "新卷"静默重格式化(扫描中任何一次瞬态毛刺都会销毁全部用户数据)——
* nand_ftl 仅把 DHARA_E_TOO_BAD(真空白)判为新卷,其余失败使
* disk_initialize 失败 → f_mount 失败 → 下方自检上报并保留卷;
* 瞬态失败由主循环有界重试(STORAGE_BRINGUP_RETRIES 次 × 2s),
* 成功后补跑"存储就绪"序列(日志挂载/配置加载/屏障置位,均幂等)。
* 探针/重格式化自愈策略仍在 storage_bring_up 内(2026-09-20 修复,
* 小写两击确认、FR_INVALID_NAME 不入重格式化集、写保护类只上报,
* 详见该函数注释)。 */
#if !defined(TEST_SUITE_STORAGE)
/* 存储自愈:存储 bring-up 剩余重试次数(0 = 已就绪或已耗尽);
* 须在启动块之前声明(启动块失败路径写入、主循环读取) */
uint32_t storage_retry = 0u;
#endif
#if !defined(TEST_SUITE_STORAGE)
/* STORAGE 套件构建下 defaultTask 完全跳过存储 bring-up(挂载/配置/日志盘写),
* 卷由 testTask 的 storage_test_run 独占——测试含 nand_ftl_deinit 掉电模拟,
* 与任何后台文件写入并发都会互相踩坏 FTL/FatFs 状态(2026-09-20 台架实证:
* 双写并发导致 mkfs 半途冻结 + BBT 被误写) */
static FATFS default_fatfs;
FRESULT fr = storage_bring_up(&default_fatfs);
if (fr != FR_OK) {
/* 存储不可写:自检故障仅能滞留待发队列(串口已告警) */
syslog_post(SYSLOG_SELFTEST, "filesystem fault");
/* 存储自愈:启动主循环中的有界重试(每 2s 一次) */
storage_retry = STORAGE_BRINGUP_RETRIES;
}
/* 注册"保存网络配置"请求通知:UI(uiTask)投递 → 置事件 → 本任务立即执行写盘
* (配置引擎属主 = 本任务;UI 保存不再直接触碰配置文件引擎) */
net_cfg_set_save_req_cb(default_task_net_cfg_save_notify);
#else
/* STORAGE 套件构建:挂载/配置引擎/日志盘写全部跳过,存储由 testTask 独占 */
DBG_INFO("[FS] mount/cfg skipped (STORAGE suite owns storage)");
#endif
/* 配置 poll 周期计数(每 40 次循环 = 2s 轮询一次,与引擎文档/模拟器一致。
* 循环基准 50ms:控制台"待发"事件即时唤醒,无事件时 50ms 兜底,日志时延 <=50ms) */
uint32_t cfg_poll_cnt = 0u;
uint32_t led_tick = HAL_GetTick();
/* 注册控制台写者:本任务此后是串口唯一发送者(其余任务只写环形缓冲)。
* 见 App/log/console_log.c "控制台单写者日志"——消除并发写造成的日志逐字符交错,
* 并让脏指针无法再进入 HAL_UART_Transmit 的缓冲遍历。 */
dbg_console_writer_attach();
/* 启动看门狗(延后启动):此处位于全部长耗时初始化之后——存储 bring-up 深链
* (mount/mkfs/FAT 探针/syslog 挂载/配置加载)已完成,监视一开始即处于稳态。
* 这正是"启动段不喂狗、主循环段才喂"的落点(见 docs/子系统设计/看门狗设计说明.md
* §4.1):MX_IWDG_Init() 只填参数不启动,启动延后到此时。
* 代价(显式声明):从复位到此处约数秒的窗口无看门狗保护,该段死机表现为
* "无任何输出 + LED 不闪",可人工辨识。 */
task_wd_start();
for (;;) {
/* 第 8 轮取证:进段打点——每步前记录段号到 .noinit 槽(复位不丢)。
* 误复位后开机 `PREVRUN seg=N` 即"上一轮主循环死在哪个调用点"
* (第 7 轮初判"poller 自身停摆"经第 8b 轮更正:真因是 IWDG 超时被静默
* 配成复位默认值,见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录)。 */
task_wd_seg(TASK_WD_SEG_POLL);
/* 心跳抽样与喂狗判定(内部节流至 100ms;全员到位才喂,否则累计停喂) */
task_wd_poll();
/* 耗时测量:某位由"到过"变为"缺席"的瞬间留一份现场快照。
* 走裸 UART 直发——缺席窗口内环形缓冲可能积压,这两行必须出去。
*
* ⚠ 判据用**位图边沿**,不能用 miss 累计数:`task_wd_miss_count()` 返回的是
* 单调累计的 s_miss_total(永不清零),开机 bring-up 阶段任务尚未全部启动时
* 就已经 >0 → 若以"归零"判恢复,则首次缺席之后再也不会触发。
* 位图边沿(某位 1→0)天然"每个 episode 只报一次":此后 prev 也不含该位。 */
{
static uint32_t s_prof_prev_mask;
const uint32_t mask = task_wd_last_mask();
if ((s_prof_prev_mask != 0u) && ((s_prof_prev_mask & ~mask) != 0u)) {
task_prof_report_raw();
}
s_prof_prev_mask = mask;
}
/* 本任务自己的心跳 */
task_wd_beat(TASK_WD_DEFAULT);
/* 等待"保存网络配置"或"控制台待发":任一即醒;无事件 50ms 后返回
* (日志时延 = 事件唤醒,实测无丢失;50ms 兜底仅防事件位异常) */
task_wd_seg(TASK_WD_SEG_EVTWAIT);
if (sys_evtHandle != NULL) {
(void)osEventFlagsWait(sys_evtHandle,
SYS_EVT_CFG_SAVE | SYS_EVT_CONSOLE_TX
| SYS_EVT_PROT_SET_SAVE | SYS_EVT_RUN_PAR_SAVE,
osFlagsWaitAny, 50u);
}
/* 控制台唯一发送点:把环形缓冲中的日志按块发出(无待发时立即返回) */
task_wd_seg(TASK_WD_SEG_DRAIN);
/* P2 探针:控制台排空单独计时。它内部**忙等** UART TXE(单次预算
* DBG_CON_DRAIN_BUDGET_MS=250ms),且本任务为 osPriorityNormal →
* 是"某个 Normal 任务连续占用 CPU 把 UI 饿死"的头号候选 */
task_prof_enter(TASK_PROF_DEF_DRAIN);
dbg_console_drain();
task_prof_exit(TASK_PROF_DEF_DRAIN);
/* 控制台 TX 故障留痕:由本任务(syslog 唯一写者)在 drain **之外**补记 run.log。
* console_log 内部刻意不调 syslog_post —— 那会构成日志调用图成环,
* 让 armlink 静态栈深在每个任务上都多叠一条日志尾巴(见 console_log.c) */
if (dbg_console_tx_fault_log_take() != 0U) {
syslog_post(SYSLOG_RUN, "console tx fault, uart reinit");
}
/* 执行待处理保存(配置引擎属主:写盘 + 成功后立即应用);无请求 = 空操作 */
#if !defined(TEST_SUITE_STORAGE)
task_wd_seg(TASK_WD_SEG_CFGSAVE);
net_cfg_save_run();
/* 保护定值落盘(B7):内部消费脏标志,无改动 = 空操作;
* 写失败脏回置 → iec104Task 下轮 poll 再投递事件 = 天然重试。
* task-10「操作记录」写入点之一:⚠ 不能按 prot_set_store_save() 的返回值
* 记——它对"无待写"同样返 0,照返回值记就是每 50 ms 刷一条**没发生过**的
* 操作(正是"不许用静态演示数据充数"的反面)。先 peek 脏位(非消费,
* iec104_serv.c 通知属主用的就是同一枚),只有"确实有待写且这趟写成功"
* 才算一次整定操作。失败不记(判据 = 动作成功后写一条),脏位保留、
* 下轮重试成功后才留痕。 */
if ((prot_set_dirty_peek() != 0u) && (prot_set_store_save() == 0)) {
syslog_post(SYSLOG_OPER, "settings saved");
}
/* 运行参数段落盘(「数据可信」这一项):同款脏语义;save = 引擎整档外科写
* ([run_par] 9 键 + 被 adc_cal_set 联动改写的 [adc_cal] 两段一趟落) */
(void)run_par_store_save();
/* [comm] 段落盘(道G task-9 通讯设置页):同款脏位、每轮无条件,
* 无事件位(task-8 D5 口径——ca 改后重启生效,落盘只保断电不丢) */
(void)iec104_comm_store_save();
#endif
/* 配置文件变更轮询(2s 周期;沉降去抖在引擎内) */
if ((cfg_poll_cnt++ % 40u) == 0u) {
#if !defined(TEST_SUITE_STORAGE)
task_wd_seg(TASK_WD_SEG_CFGPOLL);
/* 存储自愈:启动期挂载失败时,每 2s 重跑完整 bring-up——
* 瞬态毛刺(如 dhara resume 扫描的瞬时 SPI 故障)通常第二次
* 即恢复;成功后由 storage_bring_up 补跑启动时跳过的
* "存储就绪"序列(日志挂载/配置加载/屏障置位,均幂等);
* 持续性故障耗尽重试则维持无存储降级(自检已在启动/耗尽时
* 上报,卷保留、不重格式化,等下次上电重新 resume) */
if (storage_retry > 0u) {
FRESULT rfr = storage_bring_up(&default_fatfs);
if (rfr == FR_OK) {
storage_retry = 0u;
DBG_INFO("[FS] storage bring-up OK on retry");
} else if (--storage_retry == 0u) {
DBG_ERROR("[FS] storage retries exhausted (fr=%d), "
"storage disabled", (int)rfr);
syslog_post(SYSLOG_SELFTEST, "filesystem fault");
}
}
/* 卷已就绪(STORAGE_READY)但日志后端未就绪(建 /log 曾失败,
* s_fs_ready 恒 0 → syslog_drain 永久 no-op)→ 2s 周期重试(幂等;
* 就绪后 syslog_mcu_mounted 立即返回,仅一次 volatile 读)。 */
if (sys_evtHandle != NULL
&& (osEventFlagsGet(sys_evtHandle) & SYS_EVT_STORAGE_READY) != 0u) {
syslog_mcu_mounted();
}
cfg_file_poll();
#endif
}
/* LED 心跳:500ms 翻转一次(与旧 500ms 循环节奏等效;循环基准改 50ms 后
* 不能再每轮翻转,否则变成 10Hz 闪烁) */
task_wd_seg(TASK_WD_SEG_LED);
if ((HAL_GetTick() - led_tick) >= 500u) {
led_tick = HAL_GetTick();
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
HAL_GPIO_TogglePin(LED3_GPIO_Port, LED3_Pin);
}
#if !defined(TEST_SUITE_STORAGE)
/* 排空运行期日志(上电/重启分类与自检故障落盘) */
task_wd_seg(TASK_WD_SEG_SYSLOG);
/* P2 探针:syslog 落盘(NAND/FTL 写,最坏数百 ms)单独计时 */
task_prof_enter(TASK_PROF_DEF_SYSLOG);
syslog_drain();
task_prof_exit(TASK_PROF_DEF_SYSLOG);
/* 写回缓存空闲 flush 兜底:卷空闲(try 锁)+ 周期节流 + 就绪门控全在
* 模块内(App/fs/vol_idle_sync.h);clean 态零 NAND 触点,脏态最坏 =
* 一次 f_close 同型的同步段。放在 drain 之后:本拍若写过日志,close
* 已同步过,这里随即 clean 短路——漏网脏页的驻留窗由周期封顶 */
vol_idle_sync_poll();
/* 录波段落盘(task-2):卷空闲 + 环已封存才搬,就绪门/try 锁全在函数内;
* 一次心跳最多一段(lead 裁③)。放在 idle sync 之后:本拍若刚写过录波段,
* 下一拍空闲 flush 会兜住残留脏页,语义与 syslog 排空同位 */
wave_store_drain();
#endif
/* 栈水位观测:首次 5s 后报告,此后每 2s 采样、出现更深峰值才打印 */
task_wd_seg(TASK_WD_SEG_STACK);
stack_probe_poll();
/* 各任务最坏耗时 / 次数 / "仍在段内"快照上报。
* 内部节流(首次 5s,此后每 2s);判据:
* uisn 持续增长 → uiTask 卡在 ui_net_sink_poll 的往返里(阻塞)
* uipn 持续增长而 uisn 为 0 → 被调度饿死(本轮迟迟跑不完)
* uiw 大 → uiTask 自身渲染拖长
* [PROF2] 的 defd/netp/adcp 谁在同一窗口抬起来 → 谁就是占用者 */
task_prof_report();
}
}
/*
* 函数功能:栈水位周期观测(可观测性,2026-09-21 自 Src/freertos.c 迁出,
* 逻辑零改动)
* 入口参数:无
* 返回值:无
* 限定条件:仅 defaultTask 主循环调用
* 函数说明:
* 数据源:任务栈"历史最小剩余"——内核按特征值填充法统计(与
* configCHECK_FOR_STACK_OVERFLOW=2 同一机制),osThreadGetStackSpace() 取剩余
* 字节,配置字节取各任务 *_attributes.stack_size(唯一定义处,避免常量重复;
* 本版 CMSIS 包装只有 GetStackSpace 实现,无 osThreadGetStackSize)。
* 峰值占用 = 配置 - 剩余。
* 定栈规则(docs/内存管理/RAM使用分析与优化方案.md):新栈 = 峰值*1.5 + 1KB ISR
* 余量,并要求 free_min >= 新栈*30% 才算合格。
* 节奏:首次 STACK_PROBE_FIRST_MS 后报告(bring-up 深链已跑过),此后每
* STACK_PROBE_PERIOD_MS 采样一次,只在出现更深峰值时打印——既不改串口静默
* 窗口,也不漏掉之后才走到的更深分支(如卷修复/配置保存路径)。
* 输出(纯 ASCII,便于 /serial-boot-test 采集):
* [STACK] <task> peak_used=<B>/<cfg> free_min=<B> (<pct>%)
* [STACK] heap free=<B> min_ever=<B>
* 未覆盖 idle/timer 任务(内核静态小栈、纯循环,未取句柄)。
*/
static void stack_probe_poll(void) {
static uint32_t s_free_min[STACK_PROBE_COUNT]; /* 历史最小剩余(B),0=未采样 */
static uint32_t s_last_tick;
static uint8_t s_reported;
const TickType_t now = xTaskGetTickCount();
uint32_t i;
uint8_t deeper = 0u;
if (s_reported == 0u) {
if (now < pdMS_TO_TICKS(STACK_PROBE_FIRST_MS)) {
return;
}
} else if ((uint32_t)(now - s_last_tick) < (uint32_t)pdMS_TO_TICKS(STACK_PROBE_PERIOD_MS)) {
return;
}
s_last_tick = (uint32_t)now;
/* S11 堆低水位告警(2026-10-03):min_ever 跌破阈值 → 串口告警 + 自检留痕。
* 独立于下方"出现更深栈峰值才打印"的抑制:堆水位与栈峰值无关,须每 2s 采样。
* xPortGetMinimumEverFreeHeapSize() 单调不增 → 一旦跌破即持续成立,故只报一次
* (latch),既不刷屏,也不会漏(s_heap_alarm 置位后不再复报)。 */
{
static uint8_t s_heap_alarm;
const uint32_t min_ever = (uint32_t)xPortGetMinimumEverFreeHeapSize();
if ((s_heap_alarm == 0u) && (min_ever < HEAP_LOW_WATER_B)) {
s_heap_alarm = 1u;
DBG_ERROR("[HEAP] low water: %lu B (< %lu B)",
(unsigned long)min_ever, (unsigned long)HEAP_LOW_WATER_B);
(void)syslog_post(SYSLOG_SELFTEST, "heap low water");
}
}
for (i = 0u; i < (uint32_t)STACK_PROBE_COUNT; i++) {
const osThreadId_t th = *(s_stack_probe[i].p_id);
uint32_t free_now;
if (th == NULL) {
continue; /* 任务未创建 */
}
free_now = osThreadGetStackSpace(th);
if (free_now == 0u) {
continue; /* 句柄不可用:跳过 */
}
if ((s_free_min[i] == 0u) || (free_now < s_free_min[i])) {
s_free_min[i] = free_now; /* 刷新历史最小(= 最深峰值) */
deeper = 1u;
}
}
if ((s_reported != 0u) && (deeper == 0u)) {
return; /* 无新峰值:静默 */
}
s_reported = 1u;
for (i = 0u; i < (uint32_t)STACK_PROBE_COUNT; i++) {
const uint32_t free_min = s_free_min[i];
const uint32_t cfg = (uint32_t)s_stack_probe[i].p_attr->stack_size;
if (free_min == 0u) {
continue;
}
if (cfg == 0u) {
continue;
}
DBG_INFO("[STACK] %s peak_used=%lu/%lu free_min=%lu (%lu%%)",
s_stack_probe[i].p_name,
(unsigned long)(cfg - free_min), (unsigned long)cfg,
(unsigned long)free_min,
(unsigned long)((free_min * 100u) / cfg));
}
DBG_INFO("[STACK] heap free=%lu min_ever=%lu",
(unsigned long)xPortGetFreeHeapSize(),
(unsigned long)xPortGetMinimumEverFreeHeapSize());
}
/*
* 函数功能:存储 bring-up 完整序列(挂载 + 空卷自动建卷 + 卷健康探针两击 +
* 自愈重格式化 + 日志挂载 + 配置加载 + "存储就绪"屏障;
* 2026-09-25 自 Src/freertos.c 迁出,逻辑零改动)
* 入口参数:fs - 调用方持有的 FATFS 实例(defaultTask 局部 static)
* 返回值:FRESULT - FR_OK = 就绪(含屏障已置位);其它 = 失败(卷保留)
* 限定条件:仅 defaultTask 调用(配置引擎/日志盘写属主);STORAGE 套件构建
* 下整体不参与编译(卷由 testTask 独占)
* 函数说明:
* 由 defaultTask 调用:启动期一次;若失败,主循环每 2s 重试至多
* STORAGE_BRINGUP_RETRIES 次(存储自愈策略,2026-09-21):dhara_map_resume
* 失败分类修复后(nand_ftl.c 仅真空白判新卷,其余失败保留卷并返回
* disk_initialize 失败),重试让瞬态毛刺(SPI 瞬时故障/扫描窗口抖动)
* 在数秒内免重启恢复,成功时补跑启动期跳过的就绪序列(日志挂载/配置
* 加载/屏障置位,三者均幂等、可重跑);持续故障耗尽重试后进入无存储
* 降级态(自检已上报、卷保留、不重格式化,等下次上电重新 resume)。
*
* 探针/重格式化策略(2026-09-20 台架实证修复,原启动块注释迁入):
* 小写两击确认防 FTL 瞬态抖动误触发;仅结构性错误集合(已排除
* FR_INVALID_NAME)触发自愈重格式化;写保护/卷锁/参数类只上报不重格式化
* (防对写锁卷整卷擦除)。
*/
#if !defined(TEST_SUITE_STORAGE)
static FRESULT storage_bring_up(FATFS *fs) {
FRESULT fr;
/* 卷锁创建(幂等):必须早于本卷任何跨任务使用者——ftpTask 的 lftpd 文件
* 操作受 STORAGE_READY 屏障门控,屏障在本函数末尾才置位,时序天然安全 */
(void)vol_lock_init();
fr = f_mount(fs, "", 1);
if (fr == FR_NO_FILESYSTEM) {
/* 卷为空(如恢复/坏块重建后清空):自动格式化建卷
* (MKFS_PARM 与存储测试一致:FM_FAT32 / 无分区表) */
DBG_INFO("[FS] no FAT volume, formatting NAND...");
static uint8_t fs_work[FF_MAX_SS];
MKFS_PARM opts = { FM_FAT32, 0, 0, 0, 0 };
/* 格式化前清空 FTL map 树:新卷从空树开始写,journal
* 不与旧卷残留共存(结构卫生)。注:这不是"无限重格式化"的根因——
* 真根因是 FatFs 探针文件名非法(见下方探针注释)。 */
(void)nand_ftl_format();
fr = f_mkfs("", &opts, fs_work, sizeof(fs_work));
if (fr == FR_OK) {
fr = f_mount(fs, "", 1);
} else {
DBG_ERROR("[FS] f_mkfs failed (fr=%d)", (int)fr);
}
}
if (fr == FR_OK) {
/* 探针:/fschk.tmp 小写一次,验证 FAT 链真实可用。本移植
* f_write 簇分配失败返回 FR_OK+短写 → 必须校验写回字节数;短写按
* FR_DENIED 归一化(落入下方结构性集合触发重格式化)。
* ⚠ 事故教训(2026-09-20 台架实证,"无限重格式化"真根因):本移植
* FF_USE_LFN=0,ff.c create_short 把 ".fschk" 点后的 FSCHK 当扩展域
* 解析(5 字符 > 3 上限)→ f_open 恒返 FR_INVALID_NAME(6)(纯 RAM
* 判定、不碰盘)→ 曾误入下方结构性错误集合 → 每 boot 假失败 →
* 无限重格式化。探针名必须严格 8.3:名≤8、扩展≤3、不以点开头。 */
#define FS_VOL_PROBE(h) do { \
static FIL __chk; /* static:FF_FS_TINY=0 → 局部 FIL 吃 528 B 栈;探针只在 \
* defaultTask 启动/自愈路径跑,无并发 */ \
static const uint8_t __chkbuf[8] = { 0 }; \
UINT __bw = 0u; \
FRESULT __wr; \
(h) = f_open(&__chk, "/fschk.tmp", FA_WRITE | FA_OPEN_ALWAYS); \
if ((h) == FR_OK) { \
__wr = f_write(&__chk, __chkbuf, (UINT)sizeof(__chkbuf), &__bw); \
(void)f_close(&__chk); \
if (__wr == FR_OK && __bw == (UINT)sizeof(__chkbuf)) { \
(h) = FR_OK; \
(void)f_unlink("/fschk.tmp"); \
} else { \
(h) = (__wr != FR_OK) ? __wr : FR_DENIED; \
} \
} \
} while (0)
FRESULT hfr;
FS_VOL_PROBE(hfr);
if (hfr != FR_OK) {
FS_VOL_PROBE(hfr); /* 两击确认 */
}
if (hfr != FR_OK) {
/* ⚠ FR_INVALID_NAME 绝不可入本集合:它是路径名语法错误(纯 RAM 判定、
* 不碰盘),与卷健康无关(曾误入 → "无限重格式化"事故) */
if (hfr == FR_DENIED || hfr == FR_INVALID_OBJECT ||
hfr == FR_INT_ERR || hfr == FR_DISK_ERR ||
hfr == FR_NO_FILE || hfr == FR_NO_PATH ||
hfr == FR_NO_FILESYSTEM) {
DBG_INFO("[FS] volume FAT invalid (fr=%d), reformatting...",
(int)hfr);
f_mount(NULL, "", 0);
/* 格式化前清空 FTL map 树(同首启路径:新卷从空树写起) */
(void)nand_ftl_format();
{
static uint8_t fs_work2[FF_MAX_SS];
MKFS_PARM opts2 = { FM_FAT32, 0, 0, 0, 0 };
fr = f_mkfs("", &opts2, fs_work2, sizeof(fs_work2));
if (fr == FR_OK) {
fr = f_mount(fs, "", 1);
}
}
if (fr == FR_OK) {
DBG_INFO("[FS] volume reformatted OK");
} else {
DBG_ERROR("[FS] volume repair failed (fr=%d)", (int)fr);
syslog_post(SYSLOG_SELFTEST, "storage fault");
}
} else {
/* 其余错误(写保护/卷锁/参数类)不重格式化——只上报,
* 防对写锁卷整卷擦除(FTL 磨损最大操作) */
DBG_ERROR("[FS] volume probe failed (fr=%d)", (int)hfr);
syslog_post(SYSLOG_SELFTEST, "storage fault");
fr = FR_INT_ERR;
}
}
#undef FS_VOL_PROBE
if (fr == FR_OK) {
DBG_INFO("[FS] FATFS mounted");
/* 日志引擎挂载后处理:建 /log 目录 + 写"上电/重启"分类记录(唯一写者) */
syslog_mcu_mounted();
/* 加载网络配置进 RAM 副本(配置引擎属主 = 本任务;只读 INI,
* 文件缺失/损坏 = 工厂默认,不阻塞组网)。须在置位 STORAGE_READY 前,
* 使"存储就绪"屏障蕴含"配置已就绪",netTask 组网时配置恒有效 */
(void)net_cfg_load();
/* 装载保护定值持久化档(B7):文件缺失 = 出厂默认(首启正常,
* 静默);blob 损坏/越界 = 整表拒绝保持默认 + 自检留痕
* (all-or-nothing,prot_set_store_parse 保证一字不半装载)。
* 须在 STORAGE_READY 前——netTask 等此屏障,104 定值写必在装载后 */
{
int ps_rc = prot_set_store_load();
if (ps_rc == 0) {
DBG_INFO("[CFG] prot set loaded (/prot_set.bin)");
} else if (ps_rc < 0) {
DBG_ERROR("[CFG] prot set blob CORRUPT, defaults in use");
syslog_post(SYSLOG_SELFTEST, "prot set corrupt");
}
}
/* 广播"存储就绪"(= 卷挂载 + 配置加载完成),唤醒依赖文件系统的任务 */
if (sys_evtHandle != NULL) {
osEventFlagsSet(sys_evtHandle, SYS_EVT_STORAGE_READY);
}
}
} else {
DBG_ERROR("[FS] f_mount failed (fr=%d)", (int)fr);
}
return fr;
}
#endif /* !TEST_SUITE_STORAGE */
/*
* 函数功能:故障录波落盘道——把已封存的 wave_ring 段整段搬进 /wave(task-2)
* 入口参数:无 void
* 返回值:无 void
* 限定条件:仅 defaultTask 主循环调用(vol_idle_sync_poll 之后);内部绝不阻塞——
* try 锁 + 有界段长(5250 B / 256 B ≈ 21 次 f_write,设计 §4.2 预估几十 ms),
* 绝不把喂狗点做成等待态(vol_lock.h 使用约定:defaultTask 一律 try)
* 函数说明:所有权按 docs/子系统设计/故障录波设计说明.md §5.1——环的生产者是
* adcTask,本道是唯一落盘消费者;跨任务握手只认 wave_ring_sealed()。
* 本道承担五条口径:
* ① 打开一律 FA_WRITE|FA_OPEN_APPEND:本移植 FA_OPEN_APPEND 不含 FA_WRITE
* (docs/架构设计/STM32F4-Base存储架构说明.md §10.1 陷阱一),少写位
* f_write 恒 FR_DENIED;
* ② 每次 f_write 比对写回字节数:本移植簇分配失败返 FR_OK+短写无错误码
* (同处陷阱二),短写一律判失败;
* ③ 轮转号由本道自持(lead 转告(b)):ring.seg_idx 只是本 boot 本地计数,
* 盘上权威下标 = 首次持锁扫 /wave 用 wave_store_idx_from_name() 重建
* (取已存在最大号 +1,空目录 = 0,陌生名字不入账),写成功才
* wave_store_next_idx() 推进;失败不推进、重试同号;
* ④ 成败都必须 wave_ring_write_done() ACK——ACK 前环一直冻结;err 账
* 由 ring 的 err_cnt 单点收口(§9),本道不数第二份(ACK 前环已
* 超时自弃的那次例外:skip 掉 ACK,不去补第二笔账);
* ⑤ 锁跨越一次完整 open...close(vol_lock.h 铁律),持锁耗时超
* WAVE_DRAIN_HELD_WARN_MS 按事件补一条 WARN(§9 唯一周期行不在本道),
* 历史峰值 s_wheld_ms_max 供核 defaultTask 喂狗余量(lead 裁③)。
* 失败处置(lead 裁①):开过文件才 unlink 半成品(未开成的失败不动盘,
* 绝不拿未初始化的路径误删旧段);unlink 失败照旧 ACK(-1)——最坏留
* 一个坏段文件,PC 侧被 CRC 判废,不会画成证据。
* 静默跳过(不刷屏):卷未就绪 / 环未封存 / try 锁没到(下轮再来,
* 环侧 2 s 超时自弃是兜底账,本道不为此打日志)。
* (上面五条口径就是本函数功能「把已封存段整段搬进 /wave」的逐条展开,
* 细则出处 docs/子系统设计/故障录波设计说明.md §5.1 与 §10)
*/
static void wave_store_drain(void) {
static FIL s_wfil; /* static:FF_FS_TINY=0 时 FIL ~528 B,
* 绝不上栈(FS_VOL_PROBE 同一理由) */
static uint8_t s_wbuf[WAVE_DRAIN_CHUNK_B];
static wave_pack_ctx_t s_wpack;
static wave_seg_meta_t s_wmeta;
static char s_wpath[WAVE_ST_PATH_MAX];
static DIR s_wdir;
static FILINFO s_wfno;
static uint8_t s_wnext_idx; /* 本道自持的下一个段号 0..99 */
static uint8_t s_wscan_done; /* 1 = 本 boot 已扫过 /wave */
static uint8_t s_wdir_ready; /* 1 = /wave 已建或已存在 */
static uint8_t s_wcrc_state; /* 0 未验 1 过 2 败(败=永久自弃,不烧垃圾段) */
static uint32_t s_wheld_ms_max; /* 持锁搬段历史峰值 ms(收工报告核余量用) */
uint32_t t0_ms;
uint32_t held_ms;
uint32_t written = 0u;
uint32_t len = 0u;
uint32_t iter;
uint8_t opened = 0u;
uint8_t skip_ack = 0u; /* 1 = 环已自行了断本段,本道不 ACK 不补账 */
wave_ring_stat_t wst2; /* 关档后复检环侧身份用(v1.5) */
const char *why = NULL;
FRESULT fr;
UINT bw;
int rc;
/* 就绪门(同 vol_idle_sync_poll 的四级门风格) */
if (sys_evtHandle == NULL) {
return;
}
if ((osEventFlagsGet(sys_evtHandle) & SYS_EVT_STORAGE_READY) == 0u) {
return;
}
/* 封存门:只搬已封存段(唯一跨任务握手量,§5.1) */
if (wave_ring_sealed() == 0u) {
return;
}
/* 空闲门:try 锁;卷被 syslog/配置/FTP 占住 → 本轮放弃,不排队不打日志 */
if (vol_lock_wait(0u) == 0u) {
return;
}
t0_ms = (uint32_t)xTaskGetTickCount();
/* CRC 锚点自检每 boot 一次(落盘前失败 = 整族失败,永久自弃免得白烧写次数) */
if (s_wcrc_state == 0u) {
s_wcrc_state = (wave_store_crc_selftest() == WAVE_ST_OK) ? 1u : 2u;
}
if (s_wcrc_state == 2u) {
why = "crc selftest FAIL";
}
if ((why == NULL) && (s_wdir_ready == 0u)) {
fr = f_mkdir(WAVE_ST_DIR);
if ((fr == FR_OK) || (fr == FR_EXIST)) {
s_wdir_ready = 1u;
} else {
why = "mkdir /wave";
}
}
if ((why == NULL) && (s_wscan_done == 0u)) {
FRESULT rr;
fr = f_opendir(&s_wdir, WAVE_ST_DIR);
if (fr == FR_OK) {
uint8_t found = 0u;
uint8_t mx = 0u;
for (;;) {
uint8_t ix;
rr = f_readdir(&s_wdir, &s_wfno);
if ((rr != FR_OK) || (s_wfno.fname[0] == 0)) {
break;
}
if (wave_store_idx_from_name(s_wfno.fname, &ix) == WAVE_ST_OK) {
if ((found == 0u) || (ix > mx)) {
mx = ix;
}
found = 1u;
}
}
(void)f_closedir(&s_wdir);
if (rr == FR_OK) {
/* max+1 即下一写号;回绕号复用由 PC 侧 t0/frame_seq 对账兜底,
* 目录里没有的事实不去猜更多(陌生名字一律不入账) */
s_wnext_idx = (found != 0u) ? wave_store_next_idx(mx) : 0u;
s_wscan_done = 1u;
} else {
why = "readdir /wave";
}
} else {
why = "opendir /wave";
}
}
if ((why == NULL) && (s_wscan_done != 0u)) {
if (wave_ring_sealed_meta(&s_wmeta) != WAVE_ST_OK) {
/* sealed 检查与此读之间环 2 s 超时自弃(err 已由环计过):
* 本道不 ACK、不补账、不打日志,放锁走人 */
skip_ack = 1u;
}
}
if ((why == NULL) && (skip_ack == 0u)) {
/* lead 转告(e):E_FORMAT/E_SIZE = 坏段头,一律当落盘失败丢段计 err,
* 不当参数错吞掉 */
rc = wave_store_pack_start(&s_wpack, &s_wmeta, wave_ring_read_cb, NULL);
if (rc != WAVE_ST_OK) {
why = "pack_start";
}
}
if ((why == NULL) && (skip_ack == 0u)) {
if (wave_store_seg_path(s_wpath, (uint32_t)sizeof(s_wpath),
s_wnext_idx) <= 0) {
why = "seg_path";
} else {
fr = f_open(&s_wfil, s_wpath, FA_WRITE | FA_OPEN_APPEND);
if (fr != FR_OK) {
why = "f_open";
} else {
opened = 1u;
}
}
}
iter = 0u;
while ((why == NULL) && (opened != 0u)) {
rc = wave_store_pack_next(&s_wpack, s_wbuf,
(uint32_t)sizeof(s_wbuf), &len);
if ((rc != WAVE_ST_OK) && (rc != WAVE_ST_DONE)) {
why = "pack_next";
break;
}
if (len != 0u) {
bw = 0u;
fr = f_write(&s_wfil, s_wbuf, (UINT)len, &bw);
if ((fr != FR_OK) || (bw != (UINT)len)) {
why = "f_write short"; /* ②:FR_OK+短写 = 本移植独有失败形态 */
break;
}
written += len;
}
if (rc == WAVE_ST_DONE) {
break;
}
if (++iter > 32u) { /* 5250/256 = 21 片;游标卡死时的有界保险 */
why = "pack stall";
break;
}
}
if (opened != 0u) {
fr = f_close(&s_wfil);
if ((why == NULL) && (skip_ack == 0u) && (fr != FR_OK)) {
why = "f_close";
}
}
if ((why == NULL) && (skip_ack == 0u) && (opened != 0u)) {
if (written != wave_store_pack_total(&s_wpack)) {
why = "len mismatch"; /* 长度等式(§6.3):半截文件骗不过这条 */
}
}
/* v1.5(lead 复核):adcTask 优先级高于本道,搬段中途它可能正好把滞留 2 s 的本段
* 自弃(甚至已经封出下一段)。数据区的 CRC 是对"本道读到的字节"算的,半新半旧的
* 段一样自洽 → 关档后必须复检:握手量没了、或段首身份不是本段,一律判废。
* 判废走既有失败路:unlink 本道开过的文件 + write_done(-1)(此时多半回 E_STATE,
* 环侧的 err 已由自弃计过,这里不重复补账)+ 段号不推进(同号重写覆盖)。 */
if ((why == NULL) && (skip_ack == 0u)) {
if ((wave_ring_sealed() == 0u) || (wave_ring_get_stat(&wst2) != WAVE_ST_OK)
|| (wst2.last_t0_ms != s_wmeta.t0_ms) || (wst2.last_seq != s_wmeta.frame_seq)) {
why = "ring abandoned";
}
}
if (skip_ack == 0u) {
/* 失败留痕边沿锁存(搬进本块,lead 裁方案 A):
* ① 它是 static,初值只在编译期发生一次、不是每次进块重置;
* ② 它是"上一拍是否处于失败态"的边沿锁存,所以必须跨调用存活;
* ③ 单独放这里而不在函数入口那 12 连排台账里,因为 variableScope
* 检查要求把局部量收进真正覆盖全部使用点的最小块(本仓刻意开了 style)。 */
static uint8_t s_wfail_latch; /* 一个 episode 只留一条痕 */
if (why == NULL) {
(void)wave_ring_write_done(0);
s_wnext_idx = wave_store_next_idx(s_wnext_idx); /* 成功才推进(转告(b)) */
s_wfail_latch = 0u;
} else {
if (opened != 0u) {
(void)f_unlink(s_wpath); /* 只清本道开过的文件;unlink 失败照旧 ACK */
}
(void)wave_ring_write_done(-1);
if (s_wfail_latch == 0u) {
s_wfail_latch = 1u; /* 持续失败不刷屏;成功一次重新武装 */
DBG_ERROR("[WAVE] seg drop: %s (idx=%u written=%lu)", why,
(unsigned)s_wnext_idx, (unsigned long)written);
}
}
}
/* 持锁耗时台账(lead 裁③i/ii):超线按事件补一条 WARN(原先这里静默占锁,
* 就是「等锁即毁喂狗链」前例的同族黑洞);峰值进静态供收工报告 */
held_ms = (uint32_t)xTaskGetTickCount() - t0_ms;
if (held_ms > s_wheld_ms_max) {
s_wheld_ms_max = held_ms;
}
if ((skip_ack == 0u) && (held_ms > (uint32_t)WAVE_DRAIN_HELD_WARN_MS)) {
/* 本仓 dbg_log 无 WARN 级(宏只有 ERROR/INFO/DEBUG,ERR 级自动带 [ERR] = /serial-boot-test
* 禁用标记):[WARN] 按 console_log.c 先例进正文,慢不是错(真失败另有 seg drop) */
DBG_INFO("[WAVE][WARN] seg held %lu ms (idx=%u)",
(unsigned long)held_ms, (unsigned)s_wnext_idx);
}
vol_lock_release(); /* 成败/跳过都放锁:锁泄漏比丢一段录波重一百倍 */
}
/*
* 函数功能:"保存网络配置"请求通知(注册给 net_cfg 的保存交接;
* 2026-09-25 自 Src/freertos.c 迁出,逻辑零改动)
* 入口参数:无
* 返回值:无
* 限定条件:仅由 uiTask 上下文经 net_cfg 回调调用(只置事件位,无内核对象依赖)
* 函数说明:uiTask 投递保存请求 → 置 SYS_EVT_CFG_SAVE → defaultTask 循环
* 立即唤醒执行写盘
*/
#if !defined(TEST_SUITE_STORAGE)
static void default_task_net_cfg_save_notify(void) {
if (sys_evtHandle != NULL) {
(void)osEventFlagsSet(sys_evtHandle, SYS_EVT_CFG_SAVE);
}
}
#endif