Files
STM32F4-Base/App/task/default_task.c
T
wandering1 8c72698666 feat(运行参数): 卡10 run_par真源收编+104段0x8020~0x8058读写映射(道D M6-①)
新增 App/algo/run_par.c/h(「数据可信」卡 10,lead 四裁落地):
- [run_par] cfg 段:遥测死区 9 键 int32 毫单位(db_ua…db_freq),工厂默认 =
  卡 6 额定 0.5% 档(500mV×4 / 5mA×4 / 500mHz);注册走 adc_cal 同款内核前
  契约(app_main_init),不挂回调 = 引擎副本即生效值;键数 9≤16 不拆段
- 真源收编(裁③):pts 层 s_yc_db 运行时副本删除,COV 每拍实时读 run_par
  (协议→算法向下依赖,同 meas 先例;无投递无缓存 = 顺序/幂等平凡成立);
  iec104_pts_yc_set_deadband()/deadband() 保留为转发壳,壳内保留"成功清
  半截确认计数"语义并由 TC-IEC104-027 追加断言钉住(裁⑤)
- 104 地址映射(裁①):0x8030~0x8038 死区 9 点 / 0x8040~0x8047 k 覆盖 /
  0x8048~0x804F b 覆盖(逐项地址本仓自定——复现规划 §5.4 权威表不在仓,
  送检点表到位后替换 run_par.c 顶部常量段一处;已在 IEC104 §3.4 表头挂账);
  路由顺序 = run_par 表先、未命中落 prot_set——0x8027~0x802A 属 prot_set
  告警段永不覆盖,TC-IEC104-029 双向往返钉死;空隙地址自然 COT14/PN
- 通道系数覆盖 = adc_cal_set 代理(裁②a):合法带/越带 [ERR] 留痕全复用,
  run_par 不存第二份系数(防双真源);单侧写 RMW 用另一侧当前生效值;
  回显 = 生效值视图(文件坏值被哨兵回退 ROM 时主站看到的就是下游吃到的);
  adc_cal 配套:+adc_cal_b_codes() 读取口(回显/RMW 用)+ set() 授权写者
  注记扩至 iec104Task 直通路径(单字原子契约同「数据可信」§4.4)
- 持久化(裁②b):新事件位 SYS_EVT_RUN_PAR_SAVE(1<<8);iec104Task 主循环
  peek 投递 → defaultTask run_par_store_save() 一条事件一趟 cfg_file_save
  整档外科(覆盖 [run_par] 9 键 + 联动 [adc_cal] 两段);失败脏保留 = 天然
  重试(B7 prot_set 同款)。C_SE 现行无"暂停/测试态"门控——文档现行如此
  不造新语义,送检若要求接线点已写进 §3.4 注(裁④)

测试:新建 test/host/run_par(TC-RUNPAR-001~008,67 断言,真实 cfg 引擎 +
真实 adc_cal + 内存虚拟盘同 adc_cal 套件先例;[run_par] 注册契约/落盘往返/
外改文件即副本生效/两段式不提交不置脏/合法带拒+RMW+生效值回显/init 幂等
重注册留痕);iec104 套扩真链 TU(cfg_file+adc_cal+run_par 非桩,141 断言:
+TC-IEC104-029 路由 + 壳清确认计数 3 断言);meas 套静态更名 s_snap*→
s_ms_snap*(与 cfg_file.c s_snap 同 TU 相撞,D1 同款改名法,226 断言不变);
iec104_serv 套补 run_par_dirty_peek 假件。id_registry 建族 TC-HOST-RUNPAR
(游标 008)+ TC-HOST-IEC104 游标 028→029;新钩子 run-par-unit-test +
iec104 钩子触发面随真链扩;uvprojx +run_par.c(合法入库)+ SIM_EXEMPT 登记
(模拟器不编 104 协议,已核实 CMake 面)。

文档:IEC104 §3.4 运行参数段表(含送检替换账/门控未来条件/代理声明)、§7、
§8 第 4 条、变更记录;网络配置说明新增 §3.5 [run_par] 键表;复现规划 §5.4
补运行参数段落实进度。**遗留(交 lead)**:数据可信 §4.4 "引擎 RAM 副本"
一节当时的措辞对象含 pts 侧副本,s_yc_db 删除后该句过时(裁③b 点名,§4.4
在写域外未动)。

gate:@build 0/0(Code=219620 RO=22036 RW=1280 ZI=132864);vs 0cbae7d:
Flash +1912B、RAM(RW+ZI) +8B(37B 新静态被链接器并洞吸收);iec104Task 栈
1456B/margin 43%、defaultTask 3032B 不变;id-check/doc-links/cppcheck/
coding-standard 实跑过;--all-files 全量 45 钩子实跑(43 绿 + 仅 mem-budget /
mem-numbers 两红 = 台账受管区 vs .map 漂移,回填属 lead 单写者区,批 1/道D-1
/D2 同口径):gate: only mem-budget/mem-numbers pending lead ledger backfill
BOM/CR/U+FFFD/字面量 ASCII 逐文件自查通过。
2026-10-25 13:15:41 +08:00

571 lines
30 KiB
C
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.
/*
* 模块名称:Default Application Task
* 模块功能:主控任务(defaultTask)任务体实现:
* ① 启动段:存储 bring-up(挂载 + 空卷自动建卷 + 卷健康探针两击 +
* 自愈重格式化,E-31 失败后主循环 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)
*/
#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" /* 运行参数段落盘(卡 10,配置引擎属主 = 本任务) */
#include "prot_set_store.h"
#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 "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
/* E-31 自愈策略(2026-09-21 审计 P0 修复):启动期存储挂载失败后,
* 主循环按 2s 周期重跑完整 bring-up 的次数上限;耗尽则维持无存储降级
* (自检已上报、卷保留、不重格式化),等下次上电重新 resume */
#define STORAGE_BRINGUP_RETRIES 3u
/* 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);
#endif
/*
* 函数功能:defaultTask 任务体(2026-09-25 自 Src/freertos.c StartDefaultTask 迁出,
* 逻辑零改动)
* 入口参数:argument - 未用
* 返回值:无(任务不返回)
* 限定条件:仅由 defaultTask 调用(CubeMX 接线 StartDefaultTask 转调);本任务
* 只在内核运行期被调度,启动段允许使用 CMSIS 对象
* 函数说明:启动段 = 存储 bring-up + 保存回调注册 + 控制台写者注册;
* 主循环 = 事件等待(配置保存/控制台待发,50ms 兜底)→ 控制台排空
* → 配置保存执行 → 2s 配置轮询(含 E-31 有界重试)→ 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()。
*
* E-31(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)
/* E-31 自愈:存储 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");
/* E-31 自愈:启动主循环中的有界重试(每 2s 一次) */
storage_retry = STORAGE_BRINGUP_RETRIES;
}
/* 注册"保存网络配置"请求通知:UI(uiTask)投递 → 置事件 → 本任务立即执行写盘
* (配置引擎属主 = 本任务;UI 保存不再直接触碰配置文件引擎,P1-3) */
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 (;;) {
/* round 8 取证:进段打点——每步前记录段号到 .noinit 槽(复位不丢)。
* 误复位后开机 `PREVRUN seg=N` 即"上一轮主循环死在哪个调用点"
* (round 7 初判"poller 自身停摆"经 round 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 再投递事件 = 天然重试 */
(void)prot_set_store_save();
/* 运行参数段落盘(卡 10):同款脏语义;save = 引擎整档外科写
* ([run_par] 9 键 + 被 adc_cal_set 联动改写的 [adc_cal] 两段一趟落) */
(void)run_par_store_save();
#endif
/* 配置文件变更轮询(2s 周期;沉降去抖在引擎内) */
if ((cfg_poll_cnt++ % 40u) == 0u) {
#if !defined(TEST_SUITE_STORAGE)
task_wd_seg(TASK_WD_SEG_CFGPOLL);
/* E-31 自愈:启动期挂载失败时,每 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");
}
}
/* P2-9:卷已就绪(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();
#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,便于 @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 次(E-31 自愈策略,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 */
/*
* 函数功能:"保存网络配置"请求通知(注册给 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