Files
STM32F4-Base/App/task/default_task.c
T

435 lines
21 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 生成文件只留任务创建接线与崩溃钩子)
*/
#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 "sys_log.h"
#include "sys_events.h"
#include "dbg_log.h"
/* 任务句柄与属性块(CubeMX 生成全局,定义于 Src/freertos.c):
* 栈水位观测需 6 任务句柄 + 属性块 stack_size(配置栈大小的唯一定义处) */
extern osThreadId_t defaultTaskHandle;
extern const osThreadAttr_t defaultTask_attributes;
extern osThreadId_t netTaskHandle;
extern const osThreadAttr_t netTask_attributes;
extern osThreadId_t ftpTaskHandle;
extern const osThreadAttr_t ftpTask_attributes;
extern osThreadId_t testTaskHandle;
extern const osThreadAttr_t testTask_attributes;
extern osThreadId_t uiTaskHandle;
extern const osThreadAttr_t uiTask_attributes;
extern osThreadId_t timeSyncTaskHandle;
extern const osThreadAttr_t timeSyncTask_attributes;
/* Private defines -----------------------------------------------------------*/
/* 栈水位观测节奏:首次报告须晚于 bring-up 深链(mount/mkfs/探针/cfg/syslog),
* 之后每周期只判定"是否出现更深峰值",无新峰值不打印(保持串口静默窗口) */
#define STACK_PROBE_FIRST_MS 5000u
#define STACK_PROBE_PERIOD_MS 2000u
/* 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 },
};
#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;
/* 系统启动期挂载 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();
for (;;) {
/* 等待"保存网络配置"或"控制台待发":任一即醒;无事件 50ms 后返回
* (日志时延 = 事件唤醒,实测无丢失;50ms 兜底仅防事件位异常) */
if (sys_evtHandle != NULL) {
(void)osEventFlagsWait(sys_evtHandle,
SYS_EVT_CFG_SAVE | SYS_EVT_CONSOLE_TX,
osFlagsWaitAny, 50u);
}
/* 控制台唯一发送点:把环形缓冲中的日志按块发出(无待发时立即返回) */
dbg_console_drain();
/* 执行待处理保存(配置引擎属主:写盘 + 成功后立即应用);无请求 = 空操作 */
#if !defined(TEST_SUITE_STORAGE)
net_cfg_save_run();
#endif
/* 配置文件变更轮询(2s 周期;沉降去抖在引擎内) */
if ((cfg_poll_cnt++ % 40u) == 0u) {
#if !defined(TEST_SUITE_STORAGE)
/* 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");
}
}
cfg_file_poll();
#endif
}
/* LED 心跳:500ms 翻转一次(与旧 500ms 循环节奏等效;循环基准改 50ms 后
* 不能再每轮翻转,否则变成 10Hz 闪烁) */
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)
/* 排空运行期日志(上电/重启分类与自检故障落盘) */
syslog_drain();
#endif
/* 栈水位观测:首次 5s 后报告,此后每 2s 采样、出现更深峰值才打印 */
stack_probe_poll();
}
}
/*
* 函数功能:栈水位周期观测(可观测性,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;
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 = 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 { \
FIL __chk; \
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 副本(配置引擎属主 = 本任务;首启含旧档迁移;
* 文件缺失/损坏 = 工厂默认,不阻塞组网)。须在置位 STORAGE_READY 前,
* 使"存储就绪"屏障蕴含"配置已就绪",netTask 组网时配置恒有效 */
(void)net_cfg_load();
/* 广播"存储就绪"(= 卷挂载 + 配置加载完成),唤醒依赖文件系统的任务 */
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