Files
STM32F4-Base/App/util/task_watchdog.c
T

703 lines
35 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.
/*
* 模块名称:Task Watchdog(IWDG + 任务心跳位图)
* 模块功能:把独立看门狗(IWDG)与"全部任务存活"绑定,使单任务死锁/饿死/
* 断言挂死可自动复位恢复,而非人工断电。
* 核心语义:喂狗 = "所有被监视任务都在抽样窗口内动过"。
* 适用平台:STM32F407ZGTx + FreeRTOS(CMSIS-RTOS V2)
* 作者:王建锋
* 创建日期:2026-09-29
* 修改记录:
* v1.0 2026-09-29 王建锋 创建:心跳位图 + 抽样判定 + IWDG 延后启动
* v1.1 2026-10-02 王建锋 round 7 纯取证埋点:近 8 次抽样位图环 + 最大喂狗
* 间隔统计 + 停喂现场落 /log/wdev.log(掉电不丢);
* 全部在 TASK_WATCHDOG_UNIT_TEST 守卫内,判定状态机
* 与主机侧单测逐字不变(见 实机测试规划.md §0.5.4)
* v1.2 2026-10-02 王建锋 round 8 取证:复位不丢的"进段打点"(.noinit 槽 +
* PREVRUN 开机上报 + STALL 行附 seg)——round 7 结论为
* "poller 自身停摆",需定位 defaultTask 主循环阻塞点
* v1.3 2026-10-02 王建锋 round 8b 取证:第二个 .noinit 槽"时间源存活性"
* (TIM7 ISR 每 1ms 写 uwTick/FreeRTOS tick/计数)+
* TICKRUN 开机上报(含相对进段时刻的增量 duw/dfr)——
* round 8a 定位到 console 通道后,需钉死是 TIM7(HAL tick)
* 还是 SysTick(FreeRTOS tick) 停摆(实机测试规划.md §0.5.4.3)
*
* v1.4 2026-10-02 王建锋 **P0 修复**:wd_platform_start() 补写 IWDG 的
* PR/RLR(= WD_IWDG_PRESCALER / WD_IWDG_RELOAD,即设计值
* /64 + 1249 → 1701~2498ms)。此前为"延后启动"删掉
* HAL_IWDG_Init() 时,一并丢掉了**唯一**写这两只寄存器的
* 路径 → 实机停在复位默认值 PR=0/RLR=4095(≈349~512ms),
* 比设计短约 5 倍 → 800ms 容忍分支成死代码、任何 >400ms
* 心跳缺口直接 IWDG 误复位(round 3~8 全部"空 STOR 必
* 复位"的共同根因)。SWD 现场改回设计值后同一用例 100% 通过
*
* 设计依据:docs/子系统设计/看门狗设计说明.md
* - §3.1 为何不用"每任务各自喂狗"(那只检测全系统同时挂死)
* - §3.3 为何用"单字位图 + 取走清零"(免临界区,天然原子)
* - §4.3 超时与窗口的取值推导(含 LSI 偏快 47kHz 的最坏情况校验)
* - §4.5 为何由 defaultTask 在 bring-up 完成后启动
* - §5.3 为何"停喂"而不直接 NVIC_SystemReset(保留 IWDGRSTF 权威归因)
*
* ⚠ 本模块不创建任何 FreeRTOS/CMSIS-RTOS 对象,可安全用于内核启动前后的上下文。
*
* === 可测性设计(2026-10-01)===
* 硬件访问(IWDG 寄存器读写 / HAL_GetTick)下沉到 task_watchdog.h 末段声明的
* 三个 __weak 钩子(wd_platform_feed / _start / _tick_ms);MCU 端默认实现见
* 文件末尾,行为与重构前逐字一致。判定状态机因此成为纯逻辑,主机侧单测
* (test/host/watchdog/)以强符号覆盖这三个钩子后即可离线穷举验证。
* 详见 docs/测试说明/看门狗单元测试说明.md。
*/
#include "task_watchdog.h"
#include "main.h" /* HAL_GetTick()(HAL 时基 = TIM7,1ms) */
#include "iwdg.h" /* hiwdg 参数来源(CubeMX 生成,见 Src/iwdg.c) */
#ifndef TASK_WATCHDOG_UNIT_TEST
#include <string.h> /* strlen(取证行裸 UART 直发长度) */
#include "FreeRTOS.h" /* xTaskGetCurrentTaskHandle / TaskHandle_t(FTL 心跳路由) */
#include "task.h"
#include "lftpd.h" /* lftpd_task_wd_beat() 弱钩子声明(本文件提供强符号,见下方) */
#include "sys_log.h" /* SYSLOG_RUN(flush 诱导落盘)+ sys_log_evidence()(round 7 取证落盘) */
#include "console_log.h" /* dbg_console_tx_fault()(僵尸通道计数) */
#endif
#define DBG_TAG "[WD]"
#include "dbg_log.h"
/* Private defines -----------------------------------------------------------*/
/* 抽样周期:defaultTask 主循环为 50ms 基准,100ms 是其整数倍(见 §4.3) */
#define WD_POLL_PERIOD_MS 100u
/* 连续未达标容忍次数:8 × 100ms = 800ms。
* 需容纳 NAND 擦写(最坏数百 ms)等长耗时操作,同时远小于 IWDG 最坏超时
* 1701ms,保证"停喂后仍能复上电"(§4.3 的包含关系) */
#define WD_MISS_TOLERANCE 8u
/* IWDG 超时参数:PR=4 → 64 分频;RLR=1249
* 标称 64 × 1249 / 32000 ≈ 2498 ms
* 最坏 64 × 1249 / 47000 ≈ 1701 ms(LSI 偏快时超时变短,按最坏校核)
* ⚠ 这两只值**必须写进寄存器**才生效——由 wd_platform_start() 在启动前写入
* (PR/RLR 的写访问需先写 KR=0x5555 解锁)。2026-10-02 之前无人写它们,
* 寄存器一直停在**复位默认值**(PR=0 → /4、RLR=4095 ≈ 349~512ms),
* 详见 wd_platform_start() 的函数说明。 */
#define WD_IWDG_PRESCALER 4u
#define WD_IWDG_RELOAD 1249u
/* IWDG 寄存器键值(RM0090 §21.4.4;KR 为只写寄存器,读回恒为 0) */
#define WD_IWDG_KEY_UNLOCK 0x00005555u /* 解锁 PR/RLR 写访问 */
#define WD_IWDG_KEY_FEED 0x0000AAAAu /* 重装计数器(喂狗) */
#define WD_IWDG_KEY_START 0x0000CCCCu /* 启动(此后不可停,只能靠复位) */
/* PVU/RVU 同步等待上界:PR/RLR 写入后需 ≤5 个 LSI 周期(≈0.15ms)同步到
* LSI 域,其间对应 PVU/RVU 位置 1 且**后续写会被丢弃**。此上界(168MHz 下
* 约 1ms)只为避免 LSI 异常时死等,正常路径远用不到。 */
#define WD_IWDG_SYNC_SPIN 0x10000u
/* Private types -------------------------------------------------------------*/
/* Private variables ---------------------------------------------------------*/
/* 心跳位图:各任务只置位,校验者"取走并清零"。
* 单字读-改-写在 Cortex-M4 上天然原子,无需临界区(§3.3) */
static volatile uint32_t s_hb_bits;
static uint32_t s_miss_streak; /* 连续未达标轮数(达到容忍上限即停喂) */
static uint32_t s_miss_total; /* 累计未达标轮数(观测用) */
static uint32_t s_last_mask; /* 上次抽样到的位图(定位卡死任务用) */
static uint32_t s_started; /* 1 = IWDG 已启动 */
static uint32_t s_last_tick; /* 上次抽样时刻(100ms 节流基准) */
/* ==== round 7 纯取证状态(仅 MCU 构建;判定状态机逐字不变)====
* 设计见 docs/测试说明/实机测试规划.md §0.5.4:误复位后 RAM 态随复位消失,
* 故把"停喂现场"快照落盘(/log/wdev.log)供复位后回读定性。 */
#ifndef TASK_WATCHDOG_UNIT_TEST
#define WD_SNAP_HIST_DEPTH 8u
static uint32_t s_snap_hist[WD_SNAP_HIST_DEPTH]; /* 近 8 次抽样位图(环形,定位缺席位) */
static uint32_t s_snap_hist_idx; /* 环形写索引(写入后自增) */
static uint32_t s_last_feed_tick; /* 上次喂狗时刻(ms) */
static uint32_t s_max_feed_gap; /* 观测到的最大喂狗间隔(ms) */
static uint32_t s_feed_seen; /* 1 = 已记录首个喂狗时刻 */
/* ==== round 8 取证:复位不丢的"进段打点"槽(.noinit → SRAM 保留)====
* IWDG/引脚复位不改变 SRAM 内容,故复位后仍能读出"上一轮 defaultTask 主循环
* 最后走到哪一段"。magic 用于区分"本模块写过"与上电时的随机值。
* 布局:[0]=magic [1]=段号 [2]=进段时刻(ms) [3]=循环序号
* ⚠ `zero_init` 必须与 `section` 同时给:只给 section 时 armcc 会把该段当
* "带初值 RW"(有 load 地址 → 启动 scatterload 会拷贝初值 = 抹掉保留内容);
* zero_init 使其成为 ZI,配合 scatter 的 UNINIT 执行区 → 启动**完全不碰**该段。 */
#define WD_SEG_MAGIC 0x57445347u /* 'WDSG' */
__attribute__((section(".noinit"), zero_init)) volatile uint32_t s_wd_seg_trail[4];
/* ==== round 8b 取证:复位不丢的"时间源存活性"槽(.noinit → SRAM 保留)====
* 两个槽(均在 UNINIT_CCM,复位不清):
* s_wd_tick_cur [0]=magic [1]=uwTick(最新) [2]=本轮 TIM7 中断计数 [3]=保留
* —— TIM7 ISR 每 1ms 更新("本轮活动值")
* s_wd_tick_prev[0]=magic [1]=uwTick(上一轮最后) [2]=上一轮 TIM7 中断计数
* —— 由 task_wd_tick_snapshot() 在 main() 开头(**HAL_Init 之前**)拷一次
* ⚠ 为何必须在 HAL_Init 之前快照:TIM7 在 HAL_Init 里就 `HAL_TIM_Base_Start_IT`
* 开始中断,1ms 后就会把 cur 覆盖成本轮的值;而开机上报 task_wd_seg_report()
* 要等到 app_main_init(≈117ms)才跑——不先搬走,上一轮的终值就丢了
* (2026-10-02 台架实测:报告里的 uw 恒为 ~117 = 本轮开机时刻,而非上一轮终值)。
* 判读(duw = prev[1] - t,t = seg 槽的进段时刻):
* duw ≈ 0 → TIM7 与 defaultTask 同刻停摆(HAL 时基停摆)
* duw ≈ 2000+ → TIM7 活到复位(冻结不是 HAL 时基停摆,另有阻塞点)
* prev[2] → 上一轮 TIM7 总共跳了多少次(≈ 该轮存活毫秒数,可交叉核对)
* ⚠ 严禁在本 ISR 里调用任何 FreeRTOS API(含 xTaskGetTickCountFromISR):
* TIM7 远早于 xPortStartScheduler(),此时 port.c 的 ulMaxPRIGROUPValue 仍为 0
* → vPortValidateInterruptPriority() 的 configASSERT 必失败 → 断言打印在
* "USART1 尚未初始化"(huart1.Instance == NULL)的上下文里 → 卡死在 TIM7 ISR 内、
* 开机零输出(2026-10-02 台架实测:IPSR=0x47=TIM7、uwTick 冻结于 1)。
* 本探针只写 SRAM + 读 uwTick,无任何内核对象/API 依赖。 */
#define WD_TICK_MAGIC 0x5744544Bu /* 'WDTK' */
__attribute__((section(".noinit"), zero_init)) volatile uint32_t s_wd_tick_cur[4];
__attribute__((section(".noinit"), zero_init)) volatile uint32_t s_wd_tick_prev[4];
#endif
/* Private function prototypes -----------------------------------------------*/
static uint32_t wd_required_mask(void);
#ifndef TASK_WATCHDOG_UNIT_TEST
static void wd_feed_track(void);
static void wd_stall_evidence(uint32_t snapshot);
#endif
/* Private functions ---------------------------------------------------------*/
/*
* 函数功能:必需心跳位掩码(编译期按构建配置生成)
* 入口参数:无
* 返回值:本次构建要求"必须动过"的任务位集合
* 限定条件:无
* 函数说明:按 §4.1/§4.2 的设计,监视范围随构建配置变化:
* - testTask 仅在启用测试套件时创建(其不置心跳位,任何构建均不
* 入掩码;注意抽样为精确匹配,"会置位者"的位集合须恰等于掩码)
* - ENABLE_HW_LAYER_TESTS 下 netTask 入口自挂起(CH395F 由 testTask
* 独占)→ NET_READY 位永不置位 → ftpTask / uiTask / timeSyncTask
* 永久阻塞于屏障等待,但自 2026-10-02 起等待按 100ms 分片喂心跳
* → 该构建"会置位者"= default + ftp + ui + timesync(netTask 的
* 1s 挂起心跳无法持续供位,仍剔除;抽样为精确匹配,掩码必须恰等于
* 会置位的任务集合)(2026-10-02 台架:旧版"仅剔 NET 位 + 等待者
* 不置位"两种旧状态都会导致测试构建 IWDG 复位循环)
* - ftpTask 统一纳入(其主循环为 lftpd 事件循环):驱动侧节拍点
* 调用弱钩子 lftpd_task_wd_beat()(见 lftpd.h),本文件提供强
* 符号将其路由至 TASK_WD_FTP(2026-10-02 落地,台架 IWDG 复位
* 循环的根因修复:此前 FTP 位无埋点,产品构建必需掩码永不满足)
*/
static uint32_t wd_required_mask(void) {
uint32_t m = (1u << (uint32_t)TASK_WD_DEFAULT)
| (1u << (uint32_t)TASK_WD_NET)
| (1u << (uint32_t)TASK_WD_FTP)
| (1u << (uint32_t)TASK_WD_UI)
| (1u << (uint32_t)TASK_WD_TIMESYNC);
#if defined(ENABLE_HW_LAYER_TESTS)
/* 见函数说明:CH395F 硬件层测试构建 = default + 屏障等待者(等待分片喂位) */
m = (1u << (uint32_t)TASK_WD_DEFAULT)
| (1u << (uint32_t)TASK_WD_FTP)
| (1u << (uint32_t)TASK_WD_UI)
| (1u << (uint32_t)TASK_WD_TIMESYNC);
#endif
return m;
}
#ifndef TASK_WATCHDOG_UNIT_TEST
/*
* 函数功能:记录喂狗时刻并累计最大喂狗间隔(round 7 纯取证)
* 入口参数:无
* 返回值:无
* 限定条件:由 wd_platform_feed() 内调用(任意喂狗路径均经过)
* 函数说明:直接量化"停喂前喂狗节奏是否已变慢"——若 s_max_feed_gap 在停喂
* 前已 > 100ms,则属节奏性饥饿(喂狗周期被拉长)而非硬停摆。
* 只更新观测变量,不参与判定(判定状态机逐字不变)。
*/
static void wd_feed_track(void) {
uint32_t now = wd_platform_tick_ms();
if (s_feed_seen != 0u) {
uint32_t gap = now - s_last_feed_tick; /* 无符号差:回绕安全 */
if (gap > s_max_feed_gap) {
s_max_feed_gap = gap;
}
}
s_last_feed_tick = now;
s_feed_seen = 1u;
}
/*
* 函数功能:把停喂现场落盘到 /log/wdev.log(round 7 纯取证)
* 入口参数:snapshot - 本次抽样到的位图 uint32_t
* 返回值:无
* 限定条件:task_wd_poll 停喂分支(defaultTask 上下文)
* 函数说明:一行内含 snap/req(本次与必需掩码)、h(近 8 次抽样历史,定位
* 缺席位)、mg(最大喂狗间隔)、cf(console tx_fault 僵尸通道计数)。
* 经 sys_log_evidence 落盘(内部含 flush 诱导);只取证,不改判定。
* 串口 stall 行经僵尸通道可能丢失,故此处独立于串口留盘上证据。
*/
static void wd_stall_evidence(uint32_t snapshot) {
char hbuf[(WD_SNAP_HIST_DEPTH * 9u) + 1u];
char line[192];
unsigned i;
unsigned o = 0u;
int n;
/* 按时间顺序拼接历史(环形起点 = 最旧一条) */
for (i = 0u; i < WD_SNAP_HIST_DEPTH; i++) {
uint32_t v = s_snap_hist[(s_snap_hist_idx + i) % WD_SNAP_HIST_DEPTH];
o += (unsigned)snprintf(hbuf + o, sizeof(hbuf) - o,
(i == 0u) ? "%08X" : ",%08X", (unsigned)v);
}
n = snprintf(line, sizeof(line),
"STALL snap=0x%08X req=0x%08X h=%s mg=%lu cf=%lu seg=%lu",
(unsigned)snapshot, (unsigned)wd_required_mask(), hbuf,
(unsigned long)s_max_feed_gap,
(unsigned long)dbg_console_tx_fault(),
(unsigned long)s_wd_seg_trail[1]);
if (n > 0) {
/* 裸 UART 直发(不经环形缓冲/不依赖 defaultTask 排空):停喂窗口里
* 环形缓冲的排空本身可能停摆,裸发是本行证据的可靠通道(与盘上
* 证据互补——卷锁被阻塞方持有时盘上通道会失败,见 §0.5.4 判据缺口) */
dbg_console_write_raw(line, (unsigned)strlen(line));
dbg_console_write_raw("\r\n", 2u);
(void)sys_log_evidence(line);
}
}
/* ==================== round 8 取证:复位不丢的"进段打点" ====================
* 见 task_watchdog.h 的说明与 实机测试规划.md §0.5.4 round 7 结论(假设 2:
* poller 自身停摆 → 需定位 defaultTask 主循环死在哪个调用点)。
* 槽放 .noinit(scatter UNINIT_CCM):IWDG/引脚复位不改变 SRAM,复位后仍可读。 */
/*
* 函数功能:记录"即将进入的主循环段"到复位不丢的 .noinit 槽(round 8 取证)
* 入口参数:seg - 段号(task_wd_seg_t) uint32_t
* 返回值:无
* 限定条件:defaultTask 主循环调用(任意上下文亦可,纯 SRAM 写)
* 函数说明:写 [magic, 段号, 当前 ms, 循环序号];复位后由 task_wd_seg_report()
* 读出"上一轮最后进段"。纯 4 字写,无阻塞、无内核对象依赖。
*/
void task_wd_seg(uint32_t seg) {
s_wd_seg_trail[0] = WD_SEG_MAGIC;
s_wd_seg_trail[1] = seg;
s_wd_seg_trail[2] = wd_platform_tick_ms();
s_wd_seg_trail[3] = s_wd_seg_trail[3] + 1u;
}
/*
* 函数功能:开机快照上一轮的"时间源存活性"(round 8b 取证)
* 入口参数:无
* 返回值:无
* 限定条件:**必须在 HAL_Init() 之前调用**(main() 的 USER CODE BEGIN 1)
* 函数说明:把 s_wd_tick_cur(上一轮的终值,此刻尚未被本轮的 TIM7 中断覆盖)
* 搬到 s_wd_tick_prev,再把 cur 清零开始本轮累积。
* ⚠ 时序是本函数存在的唯一理由:TIM7 在 HAL_Init 里即开始 1ms 中断,
* 而开机上报要到 app_main_init(≈117ms)才跑——不先搬走,上一轮终值
* 会被本轮开机瞬间的 tick 覆盖(2026-10-02 台架实测踩坑)。
*/
void task_wd_tick_snapshot(void) {
if (s_wd_tick_cur[0] == WD_TICK_MAGIC) {
s_wd_tick_prev[0] = WD_TICK_MAGIC;
s_wd_tick_prev[1] = s_wd_tick_cur[1]; /* 上一轮最后一次 uwTick */
s_wd_tick_prev[2] = s_wd_tick_cur[2]; /* 上一轮 TIM7 中断总数 */
s_wd_tick_prev[3] = s_wd_tick_cur[3];
} else {
s_wd_tick_prev[0] = 0u; /* 上电首启:无上一轮数据 */
}
s_wd_tick_cur[0] = 0u; /* 本轮重新累积 */
s_wd_tick_cur[1] = 0u;
s_wd_tick_cur[2] = 0u;
s_wd_tick_cur[3] = 0u;
}
/*
* 函数功能:记录"时间源存活性"到复位不丢的 .noinit 槽(round 8b 取证)
* 入口参数:无
* 返回值:无
* 限定条件:TIM7 中断上下文调用(HAL_TIM_PeriodElapsedCallback 内,每 1ms)
* 函数说明:更新 s_wd_tick_cur = [magic, uwTick, 本轮中断计数, 0]。纯 3 字 SRAM 写
* + 读 uwTick,**不调用任何 FreeRTOS/CMSIS-RTOS API**——TIM7 在 HAL_Init
* 阶段即开始中断,远早于 xPortStartScheduler(),此时任何 ISR 版内核 API
* 都会触发 port.c 的优先级断言(ulMaxPRIGROUPValue 尚为 0)并把系统卡死
* 在"USART1 未初始化"的断言打印路径上(2026-10-02 台架实测根因)。
*/
void task_wd_tick_probe(void) {
s_wd_tick_cur[0] = WD_TICK_MAGIC;
s_wd_tick_cur[1] = wd_platform_tick_ms();
s_wd_tick_cur[2] = s_wd_tick_cur[2] + 1u;
}
/*
* 函数功能:开机上报上一轮最后进段 + 时间源存活性(round 8 取证)
* 入口参数:无
* 返回值:无
* 限定条件:启动早期调用(内核启动前亦可,裸 UART 直发)
* 函数说明:magic 命中才打一行 `PREVRUN seg=<段> seq=<序号> t=<ms>`(裸 UART,
* 不依赖环形缓冲写者),随后清 magic(一次复位只报一次)。
* round 8b:紧接着打 `TICKRUN uw= n= duw=`——uw 为 TIM7 最后一次记下的
* HAL tick、n 为 TIM7 中断计数、duw = uw - t(时间源是否活到复位)。
*/
void task_wd_seg_report(void) {
char m[128];
int n;
uint32_t t_seg;
if (s_wd_seg_trail[0] != WD_SEG_MAGIC) {
return; /* 上电首启或已被清:无上一轮轨迹 */
}
t_seg = s_wd_seg_trail[2];
/* 裸 UART 直发:启动早期环形缓冲写者尚未注册,不能走 DBG_* */
n = snprintf(m, sizeof(m), "PREVRUN seg=%lu seq=%lu t=%lu\r\n",
(unsigned long)s_wd_seg_trail[1],
(unsigned long)s_wd_seg_trail[3],
(unsigned long)t_seg);
if (n > 0) {
dbg_console_write_raw(m, (unsigned)strlen(m));
}
s_wd_seg_trail[0] = 0u; /* 一次复位只报一次 */
/* round 8b:时间源存活性(取自开机快照 prev,增量相对上一行的 t = 最后进段时刻)。
* duw≈0 → TIM7 与 defaultTask 同刻停摆(HAL 时基停摆);duw≈2000+ → TIM7 活到
* 复位(冻结另有阻塞点);n = 上一轮 TIM7 中断总数(≈该轮存活毫秒数)。 */
if (s_wd_tick_prev[0] == WD_TICK_MAGIC) {
uint32_t uw = s_wd_tick_prev[1];
n = snprintf(m, sizeof(m), "TICKRUN uw=%lu n=%lu duw=%lu\r\n",
(unsigned long)uw,
(unsigned long)s_wd_tick_prev[2],
(unsigned long)(uint32_t)(uw - t_seg));
if (n > 0) {
dbg_console_write_raw(m, (unsigned)strlen(m));
}
}
}
#endif /* !TASK_WATCHDOG_UNIT_TEST */
/* Public functions ----------------------------------------------------------*/
/*
* 函数功能:任务心跳置位
* 入口参数:id - 任务 ID(见 task_watchdog.h)
* 返回值:无
* 限定条件:可在任意上下文调用(单字写,无内核对象依赖)
* 函数说明:各任务在自己的主循环里调用。仅 1~2 条指令。
*/
void task_wd_beat(task_wd_id_t id) {
if ((uint32_t)id < (uint32_t)TASK_WD_ID_MAX) {
s_hb_bits |= (1u << (uint32_t)id);
}
}
/*
* 函数功能:启动 IWDG(延后启动,由 defaultTask 在 bring-up 完成后调用)
* 入口参数:无
* 返回值:无
* 限定条件:仅 defaultTask 调用;须在 osKernelStart 之后(本函数只用 HAL/寄存器)
* 函数说明:MX_IWDG_Init() 只把参数填进全局 hiwdg(Prescaler/Reload 由 CubeMX
* 在 Src/iwdg.c 生成区维护),**既不写寄存器也不启动**(见该文件
* IWDG-DEFER-START-ANCHOR 注释)。此处才真正启动——使"启动段不喂狗"
* 成立(§4.5)。IWDG 一旦启动不可停,只能靠复位终止。
* ⚠ PR/RLR 的写入由 wd_platform_start() 一并完成(v1.4 修复):
* 删掉 HAL_IWDG_Init() 后它是唯一写这两只寄存器的路径,否则寄存器
* 停在复位默认值、超时只有 ~400ms(详见该函数说明)。
*/
void task_wd_start(void) {
if (s_started != 0u) {
return;
}
s_started = 1u;
/* 启动前先喂一次:让首个抽样窗口从"启动时刻"起算,
* 而非从 MX_IWDG_Init 的时刻起算(避免启动后立即超时) */
wd_platform_feed(); /* 重装计数器(喂狗) */
wd_platform_start(); /* 启动看门狗 */
DBG_INFO("[WD] armed: timeout ~2498ms (worst ~1701ms), miss_tol=800ms");
}
/*
* 函数功能:心跳抽样与喂狗判定(defaultTask 主循环调用)
* 入口参数:无
* 返回值:无
* 限定条件:仅 defaultTask 主循环调用(唯一喂狗者);内部节流到 100ms 一次
* 函数说明:取走并清零位图 → 与必需掩码比较:
* 全中 → 清零缺失计数并喂狗;
* 有缺 → 累计;连续达到 WD_MISS_TOLERANCE 次后**停止喂狗**,
* 让 IWDG 自然溢出复位(这样 RCC_CSR 记录的是权威的
* IWDGRSTF,而不是 NVIC_SystemReset 留下的 SFTRSTF —— §5.3)。
*/
void task_wd_poll(void) {
uint32_t now;
uint32_t snapshot;
if (s_started == 0u) {
return; /* 未启动:不做任何判定 */
}
now = wd_platform_tick_ms();
if ((now - s_last_tick) < WD_POLL_PERIOD_MS) {
return; /* 节流:非 100ms 边界直接返回 */
}
s_last_tick = now;
snapshot = s_hb_bits; /* 读 */
s_hb_bits = 0u; /* 清:语义 = "自上次抽样以来谁动过" */
s_last_mask = snapshot;
#ifndef TASK_WATCHDOG_UNIT_TEST
/* round 7 取证:留存近 8 次抽样位图(环形),停喂时随证据落盘 */
s_snap_hist[s_snap_hist_idx] = snapshot;
s_snap_hist_idx = (s_snap_hist_idx + 1u) % WD_SNAP_HIST_DEPTH;
#endif
if (snapshot == wd_required_mask()) {
s_miss_streak = 0u;
wd_platform_feed(); /* 全员健康:喂狗 */
return;
}
/* 有任务缺席:累计(不喂狗) */
s_miss_total++;
if (s_miss_streak < WD_MISS_TOLERANCE) {
s_miss_streak++;
}
#ifndef TASK_WATCHDOG_UNIT_TEST
if (s_miss_streak == 1u) {
/* 停喂起点裸 UART 标记(每次缺席 episode 仅一条):证明 poller 仍在
* 抽样、并给出**首次**缺席的位图——定位是哪一任务先停摆(round 7) */
char m[96];
int mn = snprintf(m, sizeof(m),
"MISS start snap=0x%08X req=0x%08X t=%lu\r\n",
(unsigned)snapshot, (unsigned)wd_required_mask(),
(unsigned long)now);
if (mn > 0) {
dbg_console_write_raw(m, (unsigned)strlen(m));
}
}
#endif
if (s_miss_streak >= WD_MISS_TOLERANCE) {
/* 连续 800ms 有任务未动过 → 已停喂 → 约 2.5s 后 IWDG 复位。
* 最后遗言走裸 UART(不依赖内核、不遍历指针、有界长度)。 */
DBG_ERROR("[WD] task stall: mask=0x%08X req=0x%08X miss=%lu",
(unsigned)snapshot,
(unsigned)wd_required_mask(),
(unsigned long)s_miss_total);
#ifndef TASK_WATCHDOG_UNIT_TEST
/* round 7 取证:把现场落 /log/wdev.log(掉电不丢)。
* 串口 stall 行经僵尸通道可能丢失,故此处独立留盘上证据。 */
wd_stall_evidence(snapshot);
#endif
}
}
/*
* 函数功能:读上次抽样到的位图(观测/自检用)
*/
uint32_t task_wd_last_mask(void) {
return s_last_mask;
}
/*
* 函数功能:读累计未达标轮数(观测/自检用)
*/
uint32_t task_wd_miss_count(void) {
return s_miss_total;
}
/*
* 函数功能:查询 IWDG 是否已启动(观测/自检用)
*/
uint32_t task_wd_is_armed(void) {
return (s_started != 0u) ? 1u : 0u;
}
/* Platform layer (MCU defaults, weak) ---------------------------------------*/
/*
* 以下三个 __weak 为 MCU 端默认实现,行为与重构前逐字一致。
* 主机侧单测提供同名强符号覆盖(链接器优先取强符号)以实现离线可观测。
* 声明见 task_watchdog.h 末段"平台后端(内部)";详见
* docs/测试说明/看门狗单元测试说明.md。
*
* 主机侧单测(同编译单元 #include 本文件)需回避这组默认定义与自身桩重名,
* 故由 TASK_WATCHDOG_UNIT_TEST 守卫:该宏下不编入默认实现,仅留桩。
*/
#ifndef TASK_WATCHDOG_UNIT_TEST
/*
* 函数功能:重装 IWDG 计数器(喂狗)——MCU 端默认实现
* 入口参数:无
* 返回值:无
* 函数说明:写 IWDG->KR = WD_IWDG_KEY_FEED(0xAAAA)。100ms 级高频路径,
* 故不经 HAL 封装。
*/
__weak void wd_platform_feed(void) {
IWDG->KR = WD_IWDG_KEY_FEED; /* 重装计数器(喂狗) */
wd_feed_track(); /* round 7 取证:记录喂狗时刻/最大间隔 */
}
/* ==== 为什么 IWDG 参数必须在这里写(2026-10-02 台架实测,P0 根因)====
* `Src/iwdg.c` 为实现"延后启动"删掉了 `HAL_IWDG_Init()`(见该文件
* IWDG-DEFER-START-ANCHOR),而 `HAL_IWDG_Init()` 是**唯一**会把
* `hiwdg.Init.Prescaler/Reload` 写进寄存器的路径——删掉后 `MX_IWDG_Init()`
* 只填结构体,PR/RLR 保持**复位默认值**(PR=0 → /4、RLR=4095)。
* 实测读数:`IWDG->PR = 0`、`IWDG->RLR = 4095` → 实际超时 4×4096/LSI
* ≈ 349~512ms,比设计的 1701~2498ms **短约 5 倍**。后果有二:
* ① `WD_MISS_TOLERANCE`(8×100ms=800ms)**永远等不到**就被 IWDG 复位
* → 停喂判定分支成为死代码(这正是 round 7 观察到"STALL 从未执行"
* 的原因,当时误判为"poller 自身停摆");
* ② 任何 >400ms 的心跳缺口都直接 IWDG 误复位。空 STOR(0 字节截断)
* → dhara/FTL flush 使 NET/FTP/UI 心跳缺 ~400ms → 复位(round 3~8
* 全部"空 STOR 必复位"的共同根因,P0-B 出口阻塞点)。
* 实测验证:SWD 现场把 PR/RLR 改成 4/1249 后,同一个空 STOR 由 5/5 复位
* 变为 100% 成功(`226 Closing data connection.`)。
* ==================================================================== */
/*
* 函数功能:按设计参数配置并启动 IWDG——MCU 端默认实现
* 入口参数:无
* 返回值:无
* 函数说明:解锁 PR/RLR → 写 WD_IWDG_PRESCALER / WD_IWDG_RELOAD(/64 + 1249
* → 1701~2498ms)→ 写 0xCCCC 启动(不可逆,只能靠复位停止)。
* 为何必须在此配置、否则超时短 5 倍,见上方长注释。
* ⚠ 顺序不可省:PR/RLR 有写保护,须先写 0x5555 解锁;每只寄存器写后要等
* `SR.PVU`/`SR.RVU` 清零(配置同步到 LSI 域,≤5 个 LSI 周期),否则**后写
* 的那只会被静默丢弃**(2026-10-02 SWD 实测:不等同步直接连写,RLR 未生效)。
*/
__weak void wd_platform_start(void) {
uint32_t spin;
IWDG->KR = WD_IWDG_KEY_UNLOCK; /* 解锁 PR/RLR 写访问 */
IWDG->PR = WD_IWDG_PRESCALER; /* 4 → /64 */
spin = WD_IWDG_SYNC_SPIN;
while (((IWDG->SR & IWDG_SR_PVU) != 0u) && (spin != 0u)) {
spin--; /* 等 PVU 清零:PR 已同步到 LSI 域 */
}
IWDG->RLR = WD_IWDG_RELOAD; /* 1249 */
spin = WD_IWDG_SYNC_SPIN;
while (((IWDG->SR & IWDG_SR_RVU) != 0u) && (spin != 0u)) {
spin--; /* 等 RVU 清零:RLR 已同步到 LSI 域 */
}
IWDG->KR = WD_IWDG_KEY_START; /* 启动看门狗(不可逆) */
}
/*
* 函数功能:读单调毫秒时基——MCU 端默认实现
* 入口参数:无
* 返回值:HAL 时基毫秒计数(32 位回绕安全,调用方用无符号差比较)
* 函数说明:HAL 时基 = TIM7(1ms),非 SysTick。
*/
__weak uint32_t wd_platform_tick_ms(void) {
return (uint32_t)HAL_GetTick();
}
/*
* 函数功能:lftpd 看门狗心跳钩子——MCU 端强符号实现
* 入口参数:无
* 返回值:无
* 限定条件:无(IWDG 启动前置位只写位图,不影响判定状态机)
* 函数说明:ftpTask 主循环 = lftpd 事件循环 / 传输等待点(埋点见 lftpd.c /
* lftpd_inet.c);驱动层只调用弱钩子 lftpd_task_wd_beat()
* (声明于 lftpd.h,弱空实现于 lftpd_inet.c),在此路由到任务
* 心跳位图的 FTP 位,使驱动层不向上依赖应用层(分层铁律)。
* 强符号覆盖同名弱实现(与 wd_platform_* 钩子同机制)。
* 背景:2026-10-02 台架实机——FTP 位无埋点 → 必需掩码永不满足
* → IWDG 启动后 800ms 停喂 → 约每秒一次 IWDG 复位、无限重启,
* 设备无法启动(P0-B 基线测试失败根因)。
*/
void lftpd_task_wd_beat(void) {
task_wd_beat(TASK_WD_FTP);
}
/* FTL 心跳路由表:任务句柄(0 = 未登记),下标 = 任务 ID */
static TaskHandle_t s_wd_bind[(uint32_t)TASK_WD_ID_MAX];
/*
* 函数功能:NAND FTL 看门狗心跳钩子 —— MCU 端强符号实现
* 入口参数:无
* 返回值:无
* 限定条件:任意上下文可调(内核启动前 / 未登记任务为空操作)
* 函数说明:gd5f_wait_busy 的长耗时轮询(块擦除等待最长 5 s、GC 页拷贝、
* 日志写)都运行在调用者任务上下文(default/ftp/ui/timesync 等),
* 此处按 task_wd_bind() 登记的句柄路由到调用者任务的心跳位,
* 防止长耗时窗口心跳缺席导致 IWDG 误复位。
* 背景:2026-10-02 台架——0 字节 STOR 截断 → dhara 日志块 GC
* (> 2 s,期间无埋点)→ FTP 位缺席 800 ms → 停喂 → IWDG 误复位。
* testTask 刻意不绑定:它不在必需掩码内(抽样为精确匹配),
* 其上下文置位会引入多余位反而误杀。
*/
void nand_ftl_wd_beat(void) {
TaskHandle_t h;
uint32_t i;
h = xTaskGetCurrentTaskHandle();
if (h == 0) {
return; /* 内核启动前:IWDG 未启动,无需心跳 */
}
for (i = 0u; i < (uint32_t)TASK_WD_ID_MAX; i++) {
if ((s_wd_bind[i] != 0) && (s_wd_bind[i] == h)) {
task_wd_beat((task_wd_id_t)i);
return;
}
}
/* 未登记任务(如 testTask):不置任何位(精确匹配抽样禁多余位) */
}
/*
* 函数功能:把当前任务句柄登记进 FTL 心跳路由表
* 入口参数:id - 任务 ID
* 返回值:无
* 限定条件:任务上下文调用(内核启动后)
* 函数说明:default/net/ftp/ui/timesync 五任务入口各调一次;
* 每任务 ID 一个句柄槽,重复登记覆盖(幂等)。
*/
void task_wd_bind(task_wd_id_t id) {
s_wd_bind[(uint32_t)id] = xTaskGetCurrentTaskHandle();
}
/*
* 函数功能:NAND FTL 看门狗直接喂狗钩子 —— MCU 端强符号实现
* 入口参数:无
* 返回值:无
* 限定条件:任意上下文可调(IWDG 未启动时为空操作)
* 函数说明:心跳钩子只维持心跳位,喂狗动作在 task_wd_poll(defaultTask 主
* 循环)。长耗时 FTL 操作(GC/整卷格式化)发生在 defaultTask 上下文
* 时,主循环阻塞在 FTL 调用内 → 喂狗点停摆 → 心跳位再新也无人喂 →
* IWDG 误复位(2026-10-02 台架:console tx fault 条目落盘触发 GC,
* poller 饥饿 ~2 s → IWDG 复位、无 stall 日志)。此处让 FTL 长路径
* 在操作进行中直接写 IWDG->KR,把"喂狗点自身被阻塞"的误复位形态
* 一并覆盖。语义边界(显式声明):NAND 侧真实卡死(wait_busy 自旋
* 超时后 dhara 恢复重试再卡死)时本路径持续喂狗 = 设备存活但功能
* 冻结(与 console ORE 自愈同类边界,V5 长跑观察)。
*/
void nand_ftl_wd_feed(void) {
if (s_started != 0u) {
wd_platform_feed();
}
}
#endif /* !TASK_WATCHDOG_UNIT_TEST */
/* Test-only hooks (compiled out unless TASK_WATCHDOG_UNIT_TEST) --------------*/
/*
* 函数功能:复位判定状态机到"刚上电"(仅主机侧单元测试使用)
* 入口参数:无
* 返回值:无
* 限定条件:仅当定义 TASK_WATCHDOG_UNIT_TEST 时存在
* 函数说明:把 s_started / 位图 / 计数 / 节流基线全部清零,使各用例互相独立。
* MCU 构建不定义该宏 → 零代码、零符号。
*/
#ifdef TASK_WATCHDOG_UNIT_TEST
void task_wd_test_reset(void) {
s_hb_bits = 0u;
s_miss_streak = 0u;
s_miss_total = 0u;
s_last_mask = 0u;
s_started = 0u;
s_last_tick = 0u;
}
#endif