/* * 模块名称:Task Watchdog(IWDG + 任务心跳位图) * 模块功能:独立看门狗与"全部任务存活"绑定的对外接口 * 适用平台:STM32F407ZGTx + FreeRTOS(CMSIS-RTOS V2) * 作者:王建锋 * 创建日期:2026-09-29 * 修改记录: * v1.0 2026-09-29 王建锋 创建 * * 设计依据:docs/子系统设计/看门狗设计说明.md */ #ifndef TASK_WATCHDOG_H #define TASK_WATCHDOG_H #include #ifdef __cplusplus extern "C" { #endif /* * 任务 ID(位序号 = 心跳位图上的位置,与必需掩码一一对应) * 顺序一旦确定不要插空/改动,掩码是编译期常量 */ typedef enum { TASK_WD_DEFAULT = 0, TASK_WD_NET = 1, TASK_WD_FTP = 2, TASK_WD_TEST = 3, TASK_WD_UI = 4, TASK_WD_TIMESYNC = 5, TASK_WD_ID_MAX } task_wd_id_t; /* * 函数功能:任务心跳置位(各任务在自己主循环里调用) * 入口参数:id - 任务 ID * 返回值:无 * 限定条件:任意上下文可调(单字原子写、无内核对象依赖) * 函数说明:仅 1~2 条指令,无锁、无阻塞,可放在中断外任意位置。 */ void task_wd_beat(task_wd_id_t id); /* * 函数功能:把当前任务句柄登记进 FTL 心跳路由表(任务入口调用一次) * 入口参数:id - 任务 ID * 返回值:无 * 限定条件:任务上下文调用(内核启动后) * 函数说明:nand_ftl_wd_beat() 强符号(本模块)按此表把 FTL 长耗时操作 * 的心跳路由到调用者任务的心跳位(见 gd5f2gq5ue.h 钩子说明)。 * testTask 刻意不登记:它不在必需掩码内(抽样为精确匹配), * 其上下文置位会引入多余位反而误杀。 */ void task_wd_bind(task_wd_id_t id); /* * 函数功能:启动 IWDG(**延后启动**) * 入口参数:无 * 返回值:无 * 限定条件:仅 defaultTask 在存储 bring-up 完成后调用一次 * 函数说明:MX_IWDG_Init() 只填参数不启动(原因见 Src/iwdg.c 的 * IWDG_DEFER_START 注释:HAL_IWDG_Init 内部会立即启动,而它被 * CubeMX 插在 app_main_init 之前,那段无喂狗点且耗时可达数秒)。 * 本函数才真正启动,使"启动段不喂狗、主循环段才喂"成立。 */ void task_wd_start(void); /* * 函数功能:心跳抽样与喂狗判定 * 入口参数:无 * 返回值:无 * 限定条件:仅 defaultTask 主循环调用(唯一喂狗者) * 函数说明:内部节流至 100ms 一次。全员到位才喂狗;连续 800ms 有任务 * 缺席则停止喂狗,由 IWDG 自然溢出复位(保留 IWDGRSTF 权威归因)。 */ void task_wd_poll(void); /* 观测接口(自检 / 串口报告用) */ uint32_t task_wd_last_mask(void); /* 上次抽样到的位图(0 位 = 缺席任务) */ uint32_t task_wd_miss_count(void); /* 累计未达标轮数 */ uint32_t task_wd_is_armed(void); /* 1 = IWDG 已启动 */ /* * ==================== round 8 取证:复位不丢的"进段打点" ==================== * 背景(2026-10-02 台架 round 7~8 取证,完整链条见 * docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录):误复位时判定分支(STALL)从未 * 执行,当时判为**喂狗点 task_wd_poll 自身停摆**(round 8b 已更正:真因是 IWDG 超时 * 被静默配成复位默认值),而非某个被监视任务饿死。RAM 态随复位消失、串口又可能来不及打, * 故需要一条**复位后仍可读**的"defaultTask 主循环最后走到哪一段"轨迹。 * * 实现:轨迹槽放在 .noinit(scatter 的 UNINIT 执行区,见 * MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct 的 UNINIT_CCM)——IWDG/引脚复位 * 不改变 SRAM 内容,故复位后仍能读出上一轮的最后进段。槽布局: * [0]=magic(校验是否为本模块写过)[1]=段号 [2]=进段时刻(ms) [3]=循环序号 * round 8b 追加"时间源存活性"槽(同样在 .noinit): * cur = [magic, uwTick, 本轮 TIM7 计数, 0](TIM7 ISR 每 1ms 更新) * prev = [magic, uwTick, 上一轮 TIM7 计数, 0](main() 在 HAL_Init 之前快照) * 注意:**仅 MCU 构建存在**(TASK_WATCHDOG_UNIT_TEST 下不编入,主机单测不受影响)。 * ========================================================================== */ /* defaultTask 主循环"进段"编号(task_wd_seg 的入参;复位后从 PREVRUN 行读出) */ typedef enum { TASK_WD_SEG_IDLE = 0, /* 未进入任何段(初值) */ TASK_WD_SEG_POLL = 1, /* task_wd_poll(喂狗判定) */ TASK_WD_SEG_EVTWAIT = 2, /* osEventFlagsWait(50ms 兜底等待) */ TASK_WD_SEG_DRAIN = 3, /* dbg_console_drain(控制台排空) */ TASK_WD_SEG_CFGSAVE = 4, /* net_cfg_save_run(配置保存) */ TASK_WD_SEG_CFGPOLL = 5, /* cfg_file_poll(2s 配置轮询) */ TASK_WD_SEG_LED = 6, /* LED 心跳 */ TASK_WD_SEG_SYSLOG = 7, /* syslog_drain(运行期日志落盘) */ TASK_WD_SEG_STACK = 8 /* stack_probe_poll(栈水位观测) */ } task_wd_seg_t; /* * 函数功能:记录"即将进入的主循环段"(round 8 取证;**仅 MCU 构建**) * 入口参数:seg - 段号(task_wd_seg_t) * 返回值:无 * 限定条件:defaultTask 主循环调用(任意上下文亦可,纯 SRAM 写) * 函数说明:4 字写(magic/段/时刻/序号),进 .noinit 槽;复位不丢。 */ void task_wd_seg(uint32_t seg); /* * 函数功能:开机上报上一轮最后进段 + 时间源存活性(round 8 取证;**仅 MCU 构建**) * 入口参数:无 * 返回值:无 * 限定条件:启动早期调用(内核启动前亦可,裸 UART 直发) * 函数说明:magic 命中才打一行 `PREVRUN seg=<段> seq=<序号> t=`(裸 UART, * 不依赖环形缓冲写者),随后清 magic(一次复位只报一次)。 * round 8b:紧接着打一行 `TICKRUN uw= n= duw=`——uw 为 TIM7 在**上一轮** * 最后一次记下的 HAL tick、n 为上一轮 TIM7 中断总数、duw = uw - t * (相对进段时刻的增量),据此判定 TIM7 是否在冻结同刻停摆。 */ void task_wd_seg_report(void); /* * 函数功能:开机快照上一轮的"时间源存活性"(round 8b 取证;**仅 MCU 构建**) * 入口参数:无 * 返回值:无 * 限定条件:**必须在 HAL_Init() 之前调用**(main() 的 USER CODE BEGIN 1) * 函数说明:TIM7 在 HAL_Init 里即开始 1ms 中断,而开机上报要到 app_main_init * (≈117ms)才跑;本函数把上一轮的终值先搬到 prev 槽再清空 cur, * 否则终值会被本轮开机瞬间的 tick 覆盖(2026-10-02 台架实测踩坑)。 */ void task_wd_tick_snapshot(void); /* * 函数功能:记录"时间源存活性"到复位不丢的 .noinit 槽(round 8b 取证;**仅 MCU 构建**) * 入口参数:无 * 返回值:无 * 限定条件:TIM7 中断上下文调用(`HAL_TIM_PeriodElapsedCallback` 内,每 1ms) * 函数说明:更新 cur 槽 = [magic, uwTick, 本轮 TIM7 中断计数, 0](纯 SRAM 写,无阻塞)。 * 与 task_wd_seg 的"进段打点"配合,复位后由 task_wd_seg_report() 上报。 * 背景(2026-10-02 台架 round 8b 取证,见 docs/子系统设计/看门狗设计说明.md * 2026-10-02 变更记录):HAL_UART_Transmit 的超时判据 * 依赖 uwTick,时间源停摆即死等——必须先把"TIM7 是否停摆"钉死再定向修复。 * ⚠ **本函数内严禁调用任何 FreeRTOS/CMSIS-RTOS API**:TIM7 在 HAL_Init * 阶段即开始中断(远早于 xPortStartScheduler),此时 ISR 版内核 API 会触发 * port.c 的优先级断言并把系统卡死在 TIM7 ISR 内(2026-10-02 台架实测根因)。 */ void task_wd_tick_probe(void); /* * ============================ 平台后端(内部) ============================ * 把判定状态机中的硬件访问收敛到三个薄钩子,使"取走清零 / 掩码判定 / 缺失 * 累计 / 节流"成为纯逻辑,可在主机侧用桩替换后离线穷举验证 * (见 test/host/watchdog/ 与 docs/测试说明/看门狗单元测试说明.md)。 * * 与 App/cfg/cfg_file_store.h 的"共享核心 + 平台后端"同款做法: * - 三者由 task_watchdog.c 以 __weak 提供 MCU 默认实现(读写 IWDG 寄存器 / * 调 HAL_GetTick),**行为与不抽钩子时逐字一致**; * - 主机侧单测提供同名**强符号**覆盖 → 不碰 MCU 代码即可观测/注入; * - MCU 构建零行为变化。 * * ⚠ 非模块对外接口,业务代码**不要**直接调用(由 task_wd_* 间接使用)。 * ======================================================================== */ /* * 函数功能:重装 IWDG 计数器(喂狗) * 返回值:无 * 函数说明:MCU 默认实现 = `IWDG->KR = 0x0000AAAAu;` * 主机侧单测覆盖此符号以记录"何时被喂"。 */ void wd_platform_feed(void); /* * 函数功能:按设计参数配置并启动 IWDG(不可逆,启动后只能靠复位停止) * 返回值:无 * 函数说明:MCU 默认实现 = 解锁 PR/RLR → 写 WD_IWDG_PRESCALER/WD_IWDG_RELOAD * (/64 + 1249 → 1701~2498ms,等 SR.PVU/RVU 同步)→ `IWDG->KR = 0xCCCC`。 * ⚠ 配置**必须**在此完成:`Src/iwdg.c` 为实现延后启动删掉了 * `HAL_IWDG_Init()`,而它是唯一把 `hiwdg.Init.*` 写进寄存器的路径; * 缺了这一步 PR/RLR 停在复位默认值(PR=0/RLR=4095 ≈ 400ms), * 超时比设计短约 5 倍 → 800ms 容忍分支成死代码、易 IWDG 误复位。 * 主机侧单测以强符号覆盖本钩子(不碰寄存器),故其行为不受影响。 */ void wd_platform_start(void); /* * 函数功能:读单调毫秒时基(用于 100ms 抽样节流) * 返回值:自启动以来的毫秒计数(32 位回绕安全,用无符号差比较) * 函数说明:MCU 默认实现 = `HAL_GetTick()` * 主机侧单测覆盖此符号以驱动时间推进。 */ uint32_t wd_platform_tick_ms(void); /* * 函数功能:lftpd 看门狗心跳钩子(**本模块提供强符号实现**) * 入口参数:无 * 返回值:无 * 限定条件:任意上下文(IWDG 启动前只置位图,不影响判定) * 函数说明:驱动层(lftpd.c / lftpd_inet.c 的节拍点)只调用弱钩子,本模块以 * 强符号覆盖并路由到 TASK_WD_FTP 心跳位——驱动层不向上依赖应用层 * (分层铁律)。声明于 lftpd.h 供驱动侧引用,此处为"本模块提供"侧的声明。 */ void lftpd_task_wd_beat(void); /* * 函数功能:NAND FTL 直接喂狗钩子(**本模块提供强符号实现**) * 入口参数:无 * 返回值:无 * 限定条件:任意上下文(IWDG 未启动时为空操作) * 函数说明:FTL 长耗时路径(GC/块擦除等待)在调用者任务上下文**直接写 IWDG->KR**, * 覆盖"喂狗点(defaultTask 主循环)自身被长 I/O 阻塞"的误复位形态。 * 声明于 gd5f2gq5ue.h 供驱动侧引用,此处为"本模块提供"侧的声明。 */ void nand_ftl_wd_feed(void); /* * 函数功能:复位判定状态机(**仅主机侧单元测试使用**) * 返回值:无 * 函数说明:仅当定义 TASK_WATCHDOG_UNIT_TEST 时编入(MCU 构建无此符号)。 * 用于在用例之间清空位图/计数/节流基线,保证用例互不污染。 */ #ifdef TASK_WATCHDOG_UNIT_TEST void task_wd_test_reset(void); #endif #ifdef __cplusplus } #endif #endif /* TASK_WATCHDOG_H */