703 lines
35 KiB
C
703 lines
35 KiB
C
/*
|
||
* 模块名称: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
|