73941da6b1
为 task_watchdog.c 的判定逻辑(位图取走清零 / 必需掩码 0x37 判定 / miss_streak 连续 8 次停喂 / 100ms 抽样节流 / tick 回绕安全)补一层 主机侧纯逻辑单元测试,弥补此前只能靠真机或模拟器才能发现回归的空白。 架构(沿用项目既有"共享核心 + 平台后端"做法,参照 cfg_file_store): - task_watchdog.h 末段新增"平台后端(内部)"声明 3 个钩子 (wd_platform_feed / _start / _tick_ms),MCU 端以 __weak 提供默认实现, 行为与重构前逐字一致(@build 0 错误 0 警告,Flash +20B)。 - 主机侧单测以同名强符号覆盖三钩子,即可离线注入/观测,无需碰 MCU 代码。 - 测试文件 #include 生产源文件本身,保证测的是真代码而非副本。 - 测试专用复位 task_wd_test_reset() 仅 TASK_WATCHDOG_UNIT_TEST 下编入, MCU 构建零代码零符号。 测试(test/host/watchdog/,MSVC 离线编译,纯标准库驱动): - 39 项断言全过(退出码 0);注入故障可复现退出码 1。 - run_unit_test.py 直接拼 INCLUDE/LIB 调 cl.exe,绕开本机被禁的 reg.exe。 - stub/ 三桩头覆盖 main.h / iwdg.h / dbg_log.h。 门禁:pre-commit 新增 watchdog-unit-test 本地钩子(L0.5 层),仅被测文件 或其测试变动时触发;测试开发标准流程.md 新增 L0.5 层说明。 顺带改善:task_watchdog.c 的节流基线 s_last_tick 由函数内 static 提为文件 static(可测性 + 模块状态归一)。
275 lines
11 KiB
C
275 lines
11 KiB
C
/*
|
||
* 模块名称:Task Watchdog(IWDG + 任务心跳位图)
|
||
* 模块功能:把独立看门狗(IWDG)与"全部任务存活"绑定,使单任务死锁/饿死/
|
||
* 断言挂死可自动复位恢复,而非人工断电。
|
||
* 核心语义:喂狗 = "所有被监视任务都在抽样窗口内动过"。
|
||
* 适用平台:STM32F407ZGTx + FreeRTOS(CMSIS-RTOS V2)
|
||
* 作者:王建锋
|
||
* 创建日期:2026-09-29
|
||
* 修改记录:
|
||
* v1.0 2026-09-29 王建锋 创建:心跳位图 + 抽样判定 + IWDG 延后启动
|
||
*
|
||
* 设计依据: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) */
|
||
|
||
#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 偏快时超时变短,按最坏校核) */
|
||
#define WD_IWDG_PRESCALER 4u
|
||
#define WD_IWDG_RELOAD 1249u
|
||
|
||
/* 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 节流基准) */
|
||
|
||
/* Private function prototypes -----------------------------------------------*/
|
||
|
||
static uint32_t wd_required_mask(void);
|
||
|
||
/* Private functions ---------------------------------------------------------*/
|
||
|
||
/*
|
||
* 函数功能:必需心跳位掩码(编译期按构建配置生成)
|
||
* 入口参数:无
|
||
* 返回值:本次构建要求"必须动过"的任务位集合
|
||
* 限定条件:无
|
||
* 函数说明:按 §4.1/§4.2 的设计,监视范围随构建配置变化:
|
||
* - testTask 仅在启用测试套件时创建
|
||
* - netTask 在 ENABLE_HW_LAYER_TESTS 下入口即自挂起(不干活),
|
||
* 不要求其心跳,否则该构建永不喂狗
|
||
* 注:ftpTask 当前统一纳入(其主循环为 lftpd 事件循环,需在
|
||
* lftpd 侧埋点;若某构建下 ftpTask 不创建,句柄检查兜底)。
|
||
*/
|
||
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);
|
||
|
||
/* netTask 自挂起(CH395F 由 testTask 独占):不要求心跳 */
|
||
#if defined(ENABLE_HW_LAYER_TESTS)
|
||
m &= ~(1u << (uint32_t)TASK_WD_NET);
|
||
#endif
|
||
|
||
return m;
|
||
}
|
||
|
||
/* 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 注释)。
|
||
* 此处才真正写 IWDG_Key 启动——使"启动段不喂狗"成立(§4.5)。
|
||
* IWDG 一旦启动不可停,只能靠复位终止。
|
||
*/
|
||
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;
|
||
|
||
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++;
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 函数功能:读上次抽样到的位图(观测/自检用)
|
||
*/
|
||
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 = 0xAAAA。100ms 级高频路径,故不经 HAL 封装。
|
||
*/
|
||
__weak void wd_platform_feed(void) {
|
||
IWDG->KR = 0x0000AAAAu; /* 重装计数器(喂狗) */
|
||
}
|
||
|
||
/*
|
||
* 函数功能:启动 IWDG——MCU 端默认实现
|
||
* 入口参数:无
|
||
* 返回值:无
|
||
* 函数说明:写 IWDG->KR = 0xCCCC(不可逆,之后只能靠复位停止)。
|
||
*/
|
||
__weak void wd_platform_start(void) {
|
||
IWDG->KR = 0x0000CCCCu; /* 启动看门狗(不可逆) */
|
||
}
|
||
|
||
/*
|
||
* 函数功能:读单调毫秒时基——MCU 端默认实现
|
||
* 入口参数:无
|
||
* 返回值:HAL 时基毫秒计数(32 位回绕安全,调用方用无符号差比较)
|
||
* 函数说明:HAL 时基 = TIM7(1ms),非 SysTick。
|
||
*/
|
||
__weak uint32_t wd_platform_tick_ms(void) {
|
||
return (uint32_t)HAL_GetTick();
|
||
}
|
||
|
||
#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
|