merge: task/wdacc-bits 合入 v2-on-ui

任务 wdacc(1 个提交)经 task-worktree 回收:
- fix(看门狗): 停喂判定按位分账——复合串窗不再误停喂(2026-10-25 大窗判读 F2)
This commit is contained in:
2026-10-25 17:59:47 +08:00
5 changed files with 189 additions and 52 deletions
+1 -1
View File
@@ -13,7 +13,7 @@
"stack-overflow-hook-impl": "a006edbd30d2c4d7",
"malloc-failed-hook-impl": "fcd7250e5cd6403b",
"stack-overflow-hook-helper": "cacd9fd028be9b5b",
"watchdog-module": "b3501b5ed1be1793",
"watchdog-module": "56bf2739f56a44e6",
"net-msgq-adopt": "a0f913e3f34c8e9e",
"reset-cause-latch": "983927d587cb9f48",
"iwdg-hal-driver": "4b902875a84554e9",
+75 -22
View File
@@ -21,6 +21,14 @@
* round 8a 定位到 console 通道后,需钉死是 TIM7(HAL tick)
* 还是 SysTick(FreeRTOS tick) 停摆(见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录)
*
* v1.5 2026-10-25 DTU 项目组 **停喂判定按位分账**(大窗判读 F2):stall/停喂的唯一
* 判据从"复合连续缺席窗数"改为"任一必需位的连续缺席窗数"
* (同一任务 800ms 没跑过才停喂申报复位);复合缺席窗仍照打
* "MISS start"(降级为诊断行,不再代表停喂)——根治"本窗缺 A、
* 下窗缺 B"式互不相干亚秒抖动被累计成复合 800ms 假停摆并停喂
* (2026-10-25 大窗 16:24/16:28 两起实证;真单任务停摆如
* 16:39 型 UI 单串 800ms+ 仍必报)。判据正文同步
* docs/子系统设计/看门狗设计说明.md §4;主机单测 TC-WATCHDOG-017/018
* v1.4 2026-10-02 王建锋 **P0 修复**:wd_platform_start() 补写 IWDG 的
* PR/RLR(= WD_IWDG_PRESCALER / WD_IWDG_RELOAD,即设计值
* /64 + 1249 → 1701~2498ms)。此前为"延后启动"删掉
@@ -111,7 +119,15 @@
* 单字读-改-写在 Cortex-M4 上天然原子,无需临界区(§3.3) */
static volatile uint32_t s_hb_bits;
static uint32_t s_miss_streak; /* 连续未达标轮数(达到容忍上限即停喂) */
static uint32_t s_miss_streak; /* 复合缺席连续窗数(饱和封顶)——自 v1.5 起仅作
* "MISS start" 诊断行的 episode 边沿检测,不再
* 参与停喂判定(停喂按 s_miss_bits 逐位分账) */
/* 按位分账(2026-10-25 大窗判读 F2):每个必需位"连续缺席抽样窗数"。位出现即清零,
* 缺席饱和到容忍值。停喂/stall 声明的唯一判据 = 任一必需位连击 >= WD_MISS_TOLERANCE,
* 即"同一个任务连续 8 个窗没跑过"。多任务各缺各的(本窗 A、下窗 B)各自清零,
* 不再串成复合 800ms 停喂——旧复合判据把互不相干的亚秒抖动累计成真停摆申报
* (2026-10-25 大窗 16:24/16:28 两起实证;判据正文 docs/子系统设计/看门狗设计说明.md §4)。 */
static uint8_t s_miss_bits[TASK_WD_ID_MAX];
static uint32_t s_miss_total; /* 累计未达标轮数(观测用) */
static uint32_t s_last_mask; /* 上次抽样到的位图(定位卡死任务用) */
static uint32_t s_started; /* 1 = IWDG 已启动 */
@@ -439,15 +455,22 @@ void task_wd_start(void) {
* 入口参数:无
* 返回值:无
* 限定条件:仅 defaultTask 主循环调用(唯一喂狗者);内部节流到 100ms 一次
* 函数说明:取走并清零位图 → 与必需掩码比较:
* 全中 → 清零缺失计数并喂狗;
* 有缺 → 累计;连续达到 WD_MISS_TOLERANCE 次后**停止喂狗**,
* 让 IWDG 自然溢出复位(这样 RCC_CSR 记录的是权威的
* IWDGRSTF,而不是 NVIC_SystemReset 留下的 SFTRSTF —— §5.3)。
* 函数说明:取走并清零位图 → 与必需掩码比较(v1.5 起按位分账,判据推导见设计说明 §4):
* 全中 → 清零复合边沿与证据门闩并喂狗;
* 有缺但每位连击 < 容忍 → **继续喂狗**(单窗/亚秒级缺席是负载抖动,不是
* 停摆;只按位记账并在复合边沿打一条诊断行);
* 任一位连击 >= WD_MISS_TOLERANCE(同一任务 800ms 没跑过)= 真停摆 →
* 申报 [WD] task stall 并**停止喂狗**,让 IWDG 自然溢出复位
* (RCC_CSR 记权威 IWDGRSTF 而非 SFTRSTF —— §5.3)。
* ⚠ 判定故意不用"复合连续缺席窗数"当停喂门槛:那会把"本窗缺 A、下窗缺
* B"这类各自远低于容忍度的抖动串成假停摆(2026-10-25 大窗实证)。
*/
void task_wd_poll(void) {
uint32_t now;
uint32_t snapshot;
uint32_t mask_now;
uint32_t worst;
uint32_t i;
if (s_started == 0u) {
return; /* 未启动:不做任何判定 */
@@ -469,37 +492,58 @@ void task_wd_poll(void) {
s_snap_hist_idx = (s_snap_hist_idx + 1u) % WD_SNAP_HIST_DEPTH;
#endif
if (snapshot == wd_required_mask()) {
mask_now = wd_required_mask();
/* 按位分账:位出现→该位清零;缺席→+1(饱和封顶)。掩码外的位不入账
* (监视范围随构建配置变化时,账本自动跟随,不会留旧账)。 */
worst = 0u;
for (i = 0u; i < (uint32_t)TASK_WD_ID_MAX; i++) {
if ((mask_now & (1u << i)) == 0u) {
s_miss_bits[i] = 0u;
continue;
}
if ((snapshot & (1u << i)) != 0u) {
s_miss_bits[i] = 0u;
} else if (s_miss_bits[i] < (uint8_t)WD_MISS_TOLERANCE) {
s_miss_bits[i]++;
}
if ((uint32_t)s_miss_bits[i] > worst) {
worst = (uint32_t)s_miss_bits[i];
}
}
if (snapshot == mask_now) {
s_miss_streak = 0u;
s_stall_evidenced = 0u; /* 恢复:允许下次停摆再落一次证据 */
wd_platform_feed(); /* 全员健康:喂狗 */
return;
}
/* 有任务缺席:累计(不喂狗) */
/* 本窗有缺席位(复合缺席):记总数;**复合边沿**(上一窗全中)只打一条
* 诊断标记行——v1.5 起该行不再意味着停喂,停喂只看按位连击 */
s_miss_total++;
if (s_miss_streak < WD_MISS_TOLERANCE) {
s_miss_streak++;
}
if (s_miss_streak == 0u) {
/* 复合缺席 episode 起点裸 UART 标记(每 episode 一条):证明 poller 仍在
* 抽样、并给出**首次**缺席位图——定位谁先停跑(round 7 出身)。⚠ v1.5 起
* 本行为**纯诊断**——复合边沿 ≠ 停喂,stall 判定权在下面的按位连击。 */
#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)snapshot, (unsigned)mask_now,
(unsigned long)now);
if (mn > 0) {
dbg_console_write_raw(m, (unsigned)strlen(m));
}
}
#endif
}
if (s_miss_streak < WD_MISS_TOLERANCE) {
s_miss_streak++; /* 饱和封顶;仅供复合 episode 边沿 */
}
if (s_miss_streak >= WD_MISS_TOLERANCE) {
/* 连续 800ms 有任务未动过 → 已停喂 → 约 2.5s 后 IWDG 复位。
* 最后遗言走裸 UART(不依赖内核、不遍历指针、有界长度)。 */
if (worst >= WD_MISS_TOLERANCE) {
/* 同一位连续缺席 8 窗(同一任务 800ms 没跑过)= 真停摆 → 停喂 → 约 2.5s
* 后 IWDG 复位。最后遗言走裸 UART(不依赖内核、不遍历指针、有界长度)。 */
DBG_ERROR("[WD] task stall: mask=0x%08X req=0x%08X miss=%lu",
(unsigned)snapshot,
(unsigned)wd_required_mask(),
@@ -507,8 +551,8 @@ void task_wd_poll(void) {
#ifndef TASK_WATCHDOG_UNIT_TEST
/* round 7 取证:把现场落 /log/wdev.log(掉电不丢)。
* 串口 stall 行经僵尸通道可能丢失,故此处独立留盘上证据。
* ⚠ 本 episode 只写**一次**(s_stall_evidenced 门闩;注意 s_miss_streak
* 达上限后会**饱和**在容忍值,不能用它判"首次"):证据落盘经
* ⚠ 本 episode 只写**一次**(s_stall_evidenced 门闩;注意 worst 连击达
* 上限后会**饱和**在容忍值,不能用它判"首次"):证据落盘经
* sys_log_evidence() 走 NAND 写 → FTL 路径调 nand_ftl_wd_feed()
* **直接喂狗**。若每轮 100ms 都写,等于在"停喂"窗口里持续喂狗 →
* IWDG 永不溢出 → 停喂语义被证据机制自身抵消(2026-10-03 台架 V2
@@ -520,6 +564,11 @@ void task_wd_poll(void) {
wd_stall_evidence(snapshot);
}
#endif
} else {
/* 有缺席位、但每一位连击都未到容忍值(含"本窗缺 A、下窗缺 B"的交替型):
* 照常喂狗——多任务各自亚秒抖动不再串成复合停喂(按位分账收口的正身,
* 2026-10-25 大窗 16:24/16:28 型事件的根治)。 */
wd_platform_feed();
}
}
@@ -724,8 +773,12 @@ void nand_ftl_wd_feed(void) {
*/
#ifdef TASK_WATCHDOG_UNIT_TEST
void task_wd_test_reset(void) {
uint32_t i;
s_hb_bits = 0u;
s_miss_streak = 0u;
for (i = 0u; i < (uint32_t)TASK_WD_ID_MAX; i++) {
s_miss_bits[i] = 0u; /* 按位账本一并清零(v1.5 判定输入) */
}
s_miss_total = 0u;
s_last_mask = 0u;
s_started = 0u;
+32 -11
View File
@@ -23,7 +23,7 @@
| 看门狗外设 | **IWDG**(独立 LSI ≈32 kHz),CubeMX 已勾选 | `.ioc`:`Mcu.IP4=IWDG`、`IWDG.Prescaler=IWDG_PRESCALER_64`、`IWDG.Reload=1249` |
| 超时 | 标称 **2498 ms**,最坏(LSI 47 kHz)**≈1701 ms** | `WD_IWDG_PRESCALER=4` / `WD_IWDG_RELOAD=1249`(`task_watchdog.c`) |
| 抽样周期 | **100 ms**(`WD_POLL_PERIOD_MS`) | 同上 |
| 缺失容忍 | **连续 8 轮 = 800 ms** 后停喂(`WD_MISS_TOLERANCE`) | 同上 |
| 缺失容忍 | **同一必需位**连续 8 窗缺席 = 800 ms 后停喂(`WD_MISS_TOLERANCE`;v1.5 起按位分账,多任务交替缺席各自清零、不再串成复合停喂) | 同上 |
| 必需掩码 | 产品构建 **`0x1F7`**(default/net/ftp/ui/timesync/adc/iec104/protect);`ENABLE_HW_LAYER_TESTS` 下 **`0x1F5`**(剔 net) | `wd_required_mask()` |
| 喂狗者 | **仅 defaultTask**(`task_wd_poll`,主循环每轮调用、内部节流) | `App/task/default_task.c` |
| 启动时刻 | **defaultTask 存储 bring-up 之后**(`task_wd_start()`);`MX_IWDG_Init()` 只填参数不启动 | `default_task.c:175`、`Src/iwdg.c` 的 `USER CODE BEGIN 0` |
@@ -93,10 +93,15 @@ T_IWDG = (4 × 2^PR) × RLR / F_LSI
**LSI 偏快时超时变短**,故按 47 kHz 校验。三个时长必须严格包含:
```
T_任务循环 < 监视窗口(100ms) < 停喂判定(800ms) < T_IWDG最坏(1701ms)
T_任务循环 < 监视窗口(100ms) < 停喂判定(同一任务连续缺席 800ms) < T_IWDG最坏(1701ms)
```
即:停喂后仍有约 **900 ms** 余量才到复位;实测 V2 从声明 stall 到复位 ≤2.5 s。
⚠ v1.5(2026-10-25)起本链**字面成立**:旧实现的停喂从第一个缺席窗就开始(全掩码
精确匹配是喂狗前置),800ms 只是申报时点;现在停喂时点 = 同一位第 8 连击窗,此前
1..7 窗照常喂狗。代价 = 真停摆从挂死到复位总时长 +约 0.7 s(最坏 ≈2.5 s / 标称
≈3.3 s,V2 判据「声明后 ≤2.5 s」仍成立);收益 = 「本窗缺 A、下窗缺 B」式互不相干
抖动不再累计成停喂(2026-10-25 大窗 16:24/16:28 两起误升级实证,判据改写动因)。
### 2.3 配置落点
@@ -155,9 +160,9 @@ static HAL_StatusTypeDef iwdg_deferred_start(const IWDG_HandleTypeDef *a_h) { (v
|---|---|
| ① 任务置位 | `task_wd_beat(id)`:`s_hb_bits \|= (1u << id)`(仅置位) |
| ② 校验者取走清零 | `snapshot = s_hb_bits; s_hb_bits = 0u;`(单字原子) |
| ③ 掩码判定 | `snapshot == wd_required_mask()`(**精确相等**,见 §3.5) |
| ④ 达标 → 喂狗 | `s_miss_streak = 0; s_stall_evidenced = 0; wd_platform_feed();` |
| ④ 缺席 → 累计 | `s_miss_total++`;首轮打 `MISS start`;连续 8 轮打 `[WD] task stall` 并**停喂**(每 episode 只落一次盘上证据) |
| ③ 按位分账(v1.5) | 对掩码内每一位:出现→该位连击清零;缺席→+1(饱和封顶)。掩码外的位不入账(监视范围随构建变化时账本自动跟随) |
| ④ 全中窗 | `snapshot == wd_required_mask()`(**精确相等**,§3.5)→ `s_miss_streak=0; s_stall_evidenced=0;` 喂狗 |
| ⑤ 缺席窗(v1.5) | `s_miss_total++`;复合边沿(上一窗全中)打一条 `MISS start`(**纯诊断**,不再代表停喂);**任一必需位连击 ≥8** → `[WD] task stall` + **停喂**(每 episode 只落一次盘上证据);所有位连击 <8 → **照常喂狗** |
### 3.3 为什么用"取走并清零"
@@ -337,12 +342,16 @@ void task_wd_tick_probe(void); /* TIM7 ISR 内,每 1ms */
未启动 → 直接返回
未到 100ms 边界 → 直接返回(节流)
取走并清零位图 → 记入近 8 次环形历史(取证)
按位分账:掩码内每位 出现→连击清零 / 缺席→+1(饱和);worst = 各位最大连击
snapshot == required → miss_streak=0、证据门闩复位、喂狗
否则 → miss_total++、miss_streak++(饱和在容忍值)
首轮 → 裸 UART `MISS start …`
miss_streak >= 8 → DBG_ERROR task stall + (本 episode 首次)落盘证据 + 不喂狗
否则 → miss_total++;复合边沿(miss_streak==0)→ 裸 UART `MISS start …`(诊断行)
worst >= 8 → DBG_ERROR task stall +(本 episode 首次)落盘证据 + 不喂狗
worst < 8 → **照常喂狗**(单任务亚容忍缺席不牵连全局停喂)
```
⚠ 2026-10-25 大窗判读前,后两行按复合 `miss_streak >= 8` 判定:互不相干的缺席
串成停喂链(变更记录同日行);按位分账后 `s_miss_streak` 退化为纯诊断边沿计数。
⚠ **证据门闩 `s_stall_evidenced`**:落盘走 NAND → FTL 路径会 `nand_ftl_wd_feed()` **直接喂狗**。若每 100 ms 都写,等于在"停喂"窗口里持续喂狗 → IWDG 永不溢出(2026-10-03 V2 首轮实测:stall 已声明、`miss` 累至 1157≈115 s 仍不复位)。每 episode 只落一次,恢复健康后复位门闩。注意 `s_miss_streak` 达上限后会**饱和**,不能用它判"首次"。
### 5.4 平台后端(可测性)
@@ -399,8 +408,8 @@ snapshot == required → miss_streak=0、证据门闩复位、喂狗
| `PREVRUN seg=… seq=… t=…` | 开机(裸 UART) | 上一轮 defaultTask 最后进段(magic 命中才打,一次复位只报一次) |
| `TICKRUN uw=… n=… duw=…` | 紧随其后 | 上一轮 HAL 时基终值;`duw≈0` → TIM7 与主循环同刻停摆;`duw≈2000+` → 另有阻塞点 |
| `[WD] armed: timeout ~2498ms (worst ~1701ms), miss_tol=800ms` | `task_wd_start()` | IWDG 已启动(真机自检必查标记) |
| `MISS start snap=… req=… t=…` | 缺席 episode 首轮(裸 UART) | poller 仍在抽样,给出首次缺席位图 |
| `[WD] task stall: mask=… req=… miss=…` | 连续 8 轮缺席(每轮一次) | 已停喂 → 最坏 1.7 s 后 IWDG 复位;`mask` 的 0 位 = 缺席任务 |
| `MISS start snap=… req=… t=…` | **复合**缺席 episode 首轮(裸 UART) | poller 仍在抽样,给出首次缺席位图。⚠ v1.5 起为**纯诊断行**:出现不代表停喂(停喂只看同一必需位第 8 连击窗) |
| `[WD] task stall: mask=… req=… miss=…` | **同一必需位**连续 8 窗缺席起,每窗一条(v1.5 按位分账;旧复合判据见变更记录) | 该位对应任务 800 ms 没跑过 → 停喂 → 最坏 1.7 s 后 IWDG 复位;`mask` 的 0 位 = 缺席任务(多个 0 位且**跨窗换位** = 抖动串联嫌疑,v1.5 后不再触发本行) |
| `STALL snap=… req=… h=… mg=… cf=… seg=…` | 停喂 episode 首次 | 盘上+串口双份证据;`mg` 最大喂狗间隔、`cf` 控制台发送故障计数 |
| `[ERR] ASSERT FAILED <file>:<line> (resetting)` | `configASSERT` | 断言失败,打印后软复位 |
| `[ERR] HARDFAULT / MEMFAULT / BUSFAULT / USAGEFAULT PC=… CFSR=… [BFAR=] [MMFAR=]` | 4 个 Fault | 崩溃现场 + 故障名归因 |
@@ -557,7 +566,7 @@ python scripts/check_cubemx_survival.py --write # 确认改动无误后重建
| 现象 | 正确读法 |
|---|---|
| 启动期出现 `MISS start snap=0x67 req=0x77` | **良性 episode**:`[WD] armed` 在 bring-up 完成后武装,而 `required_mask` 静态含 UI 位 → UI 首跳之前必现一次,随后自行恢复。**不是故障判据**,只作"UI 尚未起跳"的旁证 |
| `miss=11` / `miss=12` | `miss=` 是**累计**缺席次数(`s_miss_total`),**不是连续次数**;连续次数 `s_miss_streak` 饱和在 `WD_MISS_TOLERANCE=8`。缺口要用 `t=`(毫秒时基)差算,本例恰为 800 ms |
| `miss=11` / `miss=12` | `miss=` 是**累计**缺席次数(`s_miss_total`),**不是连续次数**;连续次数自 v1.5 起按位分账(`s_miss_bits[]`,各饱和在 `WD_MISS_TOLERANCE=8`;旧全局 `s_miss_streak` 退化为复合边沿计数)。缺口要用 `t=`(毫秒时基)差算,本例恰为 800 ms |
| `reset cause: software (NVIC_SystemReset)` | **不能**用来区分"IWDG 溢出"与"台架拉 NRST / 烧录工具软件复位"——本台架**健康**启动同样报这一串。停摆的权威证据是 `[ERR][WD] task stall` 行本身 |
### 11.2 诊断四步:先量化,再归因
@@ -629,6 +638,17 @@ enter/exit 段还能读出"此刻仍在段内已多久"——**永久阻塞的
| 把推流搬出 uiTask(独立 `uiSinkTask`) | 实测 `uis`=1 µs 而 `uip` 仍 60.9 ms = **LVGL 帧周期地板** → 搬走推流**不会让刷新变快**;收益只剩极端并发下的观感隔离,成本含**必改 `.ioc`** 的重生成与全套回归。若日后成问题,正确方向是先治 netTask 排队公平性(见 `../网络/Net层阻塞模型架构设计.md`),不是搬 UI |
| 把推流塞进 netTask | 零往返零新增 RAM,但共享源码要走 backend 端口(双端同改)、netTask 这个唯一执行者更胖、一个 UI 侧 bug 直接变成整机复位 |
> **2026-10-25 大窗补记(判读实锤,勿与上表混淆)**:条目「提高 uiTask 优先级」与
> 「有界等待」的否决理由**依旧成立**——大窗里 UI 的 ~1.2 s 单串停摆(9 条 stall 中唯一
> 的真单任务链,bit4 全程缺席到 mg≈900+)发生在**监听重建窗**内,正是上表所说
> 「UI 在等 netTask」的实况;修复方向仍按 v1.3 既注走**事件驱动 accept / 等待面搬家**,
> 不是提优先级、也不是给 net_* 裸加超时(迟到回执拾取风险原样在册)。同期暴露的
> **复合停喂判据**问题(本窗缺 UI、下窗缺 IEC104/FTP 串成 16:24/16:28 两起误申报,
> `mg=877/918` 均为复合链而非单任务链)已由 **v1.5 按位分账**根治:判据见 §2.2/§3.2/
> §5.3,回归钉 = 主机单测 TC-WATCHDOG-017(交替缺席零申报、持续喂狗)/ 018(同位 8
> 连击必报 + 恢复后账本重启)。原始日志判读材料:`docs/测试说明/boot_log_data/`
> (megawin 同日摘录入库,gz)。
---
## 附录 A:参考依据(当前有效)
@@ -688,3 +708,4 @@ enter/exit 段还能读出"此刻仍在段内已多久"——**永久阻塞的
| 2026-10-10 | **新增 §11:任务级饥饿的诊断方法与实测基线** | 承接 Task #32(`task stall: mask=0x67 req=0x77`,缺 `TASK_WD_UI`)结案后的沉淀——原分析文档 `docs/问题记录/UI任务看门狗停摆分析.md` 按"完成即销项"删除,可复用部分并入本文:**§11.1** 三条易误读的判读口径(启动期 `MISS start 0x67` 良性、`miss=` 是累计不是连续、`reset cause` 不能区分 IWDG 与台架);**§11.2** 诊断四步(先看掩码缺几位 → 量化占空比 → 用计数速率反证空转 → 用 `[PROF]` 指认占用者;本例否掉"CPU 饱和",实测 10%~13%);**§11.3** 三条结构性脆弱点(uiTask 唯一 BelowNormal/心跳依赖 netTask 往返/自定截止期不可恢复)与两个顺带修掉的**正确性**缺陷(无新帧仍压环 ≈3% 频率失配、失败拍静默沿用旧值);**§11.4** `[PROF]`/`[PROF2]` 全字段语义 + v2.8 基线(含 `netp=0` 是真实值、`adcm` 峰值来自心跳日志、`uip` 地板 = LVGL 帧周期)与三机理判读口诀;**§11.5** 三条教训(门控须留低频兜底、门控会消灭暴露路径、事件驱动要核对节拍源与消费者创建时刻);**§11.6** 明确不做的四条及理由。§9.1 的 R7 行改指本文 §11.3 |
| 2026-10-11 | **新增 §10.4:ARMCC `__asm` 的三条语法约束** | AGENTS.md 瘦身:「ARMCC 内联汇编语法」条目的完整细节(GCC 语法报 `#18`、块内联把寄存器名当 C 标识符报 `#20`、`__asm` 取不到 C 数据符号 `A1516E`、可用写法 = 整函数 `__asm void f(void)` + `B` 到 C 入口、`#define f f_Gen` 让位机理)迁入本文,AGENTS.md 只留一行禁令与指针 |
| 2026-10-12 | **iec104Task / protectTask 纳入监视(B4/B5):必需掩码 `0x77`→`0xF7`→`0x1F7`** | 两行合并记录(B4 落地时漏补本文,B5 一并纠偏):**B4** 新增 `TASK_WD_IEC104 = 7`(iec104Task 100 ms 服务循环喂位,NET_READY 屏障等待按 100 ms 分片喂位),产品 `0x77`→`0xF7`、HW 层 `0x75`→`0xF5`;**B5** 新增 `TASK_WD_PROTECT = 8`(protectTask 20 ms 判据节拍喂位,见 `docs/算法/继电保护功能复现规划.md` §7),产品 `0xF7`→`0x1F7`、HW 层 `0xF5`→`0x1F5`。两任务均**不依赖存储/网络之外的屏障**(protectTask 无任何屏障)→ 任何构建都置位;均不做 NAND I/O → 不登记 FTL 心跳路由。主机侧单测 `beat_all()` 与 `tc_003`/`tc_005`/`tc_011` 掩码断言同步至 `0x1F7`,39 断言全绿。⚠ 注意 `0x1F7` 不含 bit3(testTask 任何构建不入掩码,口径不变) |
| 2026-10-25 | **停喂判定按位分账(v1.5,大窗判读 F2)**:stall/停喂唯一判据从「复合连续缺席窗数」改为「**任一必需位的连续缺席窗数**」——同一任务连续 8 窗(800 ms)没跑过才申报停摆并停喂;多任务交替缺席(本窗缺 A、下窗缺 B)各自清零、且缺席窗在容忍线下**照常喂狗**(旧实现自第一个缺席窗起就不喂,800ms 只是申报点)。动因 = 2026-10-25 台架大窗:54 个探针重建窗里 9 条 stall 行中 16:24/16:28 两起为复合误升级(mg=877/918,无任何单位到 800ms 单串),16:39 为真 UI 单串(~1.2 s,受害者面修复另立 F3 票);`MISS start` 降级为复合边沿**诊断行**(不再代表停喂,判读口径 §6.1/§11.1 同步);真停摆到复位的总时长最坏 +约 0.7 s(§2.2 链注)。§0/§2.2/§3.2/§5.3/§6.1/§11.1/§11.6 同步;主机单测 `test/host/watchdog/watchdog_test.c` 断言按新判据改写 + 新增 **TC-WATCHDOG-017**(20 窗交替缺席:零 stall、20 窗持续喂)/ **TC-WATCHDOG-018**(同位 8/9/10 连击必报停 + 停喂;恢复窗喂、账本重启,第二段亚容忍串不误报),47 断言全绿 |
@@ -3,8 +3,8 @@
> **模块**:Task Watchdog(IWDG + 任务心跳位图,`App/util/task_watchdog.c`)
> **测试类型**:**主机侧纯逻辑单元测试**(L0.5 层,见 §1.5)——在 PC 上离线验证
> 判定状态机,零硬件、零烧录、秒级、不占 MCU 的 RAM。
> **状态**:✅ 已落地(39 用例,`@build` 0/0,已挂 pre-commit 钩子)
> **日期**:2026-10-01
> **状态**:✅ 已落地(47 断言,`@build` 0/0,已挂 pre-commit 钩子)
> **日期**:2026-10-01(2026-10-25 v1.5 按位分账改造,见变更记录)
> **关联文档**:
> [`../子系统设计/看门狗设计说明.md`](../子系统设计/看门狗设计说明.md)(被测行为的设计依据)、
> [`测试开发标准流程.md`](测试开发标准流程.md)(测试分层与门禁总览)、
@@ -20,7 +20,7 @@
| 半边 | 内容 | 可测性 |
|---|---|---|
| **判定状态机**(被测) | 位图取走清零、必需掩码比较、`miss_streak` 累计(容忍 8 次)、100 ms 抽样节流、tick 回绕安全 | 纯逻辑,与硬件无关 → **可在 PC 上位穷举** |
| **判定状态机**(被测) | 位图取走清零、必需掩码比较、`s_miss_bits[]` 按位分账(容忍 = 同一位 8 连击)、100 ms 抽样节流、tick 回绕安全 | 纯逻辑,与硬件无关 → **可在 PC 上位穷举** |
| **硬件副作用** | `IWDG->KR` 写(喂狗 / 启动)、`HAL_GetTick()` | 需真机或桩 |
板端套件(`test/board/`)只能"在真机上完整跑一遍",**无法穷举边界**——例如:
@@ -108,18 +108,20 @@ test/host/watchdog/watchdog_test.c
| **TC-WATCHDOG-014** | 重复 `task_wd_start()` 只启动 IWDG 一次 | 启动幂等(IWDG 不可逆,重复启动无意义) |
| **TC-WATCHDOG-015** | `start` 内部先喂一次(`feed_count==1`) | 首个抽样窗口从启动时刻起算,避免启动即超时 |
| **TC-WATCHDOG-002** | 50 ms 内 `poll` 不喂;累计 100 ms 喂 1 次 | 100 ms 抽样节流 |
| **TC-WATCHDOG-003** | 全员置位 → 喂狗 1 次;`last_mask==0x37` | 全员到位判定 + 掩码取值 |
| **TC-WATCHDOG-004** | 不重新置位则第二轮不喂 | **取走清零**语义(旧位不残留) |
| **TC-WATCHDOG-005** | 缺 UI(bit4)→ 不喂;`last_mask` 精确记录缺失位 | 必需掩码比较 + 缺失定位 |
| **TC-WATCHDOG-006** | 连续 1..7 次缺席不报;**第 8 次**打 `[WD] task stall`;缺席期间不喂 | `WD_MISS_TOLERANCE=8`(800 ms)停喂触发点 |
| **TC-WATCHDOG-003** | 全员置位 → 喂狗 1 次;`last_mask==0x1F7` | 全员到位判定 + 掩码取值(掩码史:0x37→0x77→0x1F7,见设计说明变更记录 2026-10-10/12) |
| **TC-WATCHDOG-004** | 不重新置位:第二窗(空位图,各位 streak=1)v1.5 **照常喂**;`last_mask==0` 证明位已取走 | **取走清零**语义(旧位不残留);停喂只看按位连击 |
| **TC-WATCHDOG-005** | 缺 UI(bit4)单窗(streak 1 < 容忍)→ **照常喂**;`last_mask` 精确记录缺失位 | 按位分账(v1.5)+ 缺失定位 |
| **TC-WATCHDOG-006** | 同一批缺席 1..7 窗不报且**持续喂**(feed_count==8);**第 8 窗**打 `[WD] task stall` 起停喂 | `WD_MISS_TOLERANCE=8`(同一任务 800 ms)停喂触发点 |
| **TC-WATCHDOG-007** | 停喂后**每 100 ms 复报**一条 stall(现状行为) | 见 §4 已知行为("最后遗言"持续到复位) |
| **TC-WATCHDOG-008** | 恢复全员到位 → 立即喂狗 + `streak` 归零 | 停顿可恢复(非锁死状态机) |
| **TC-WATCHDOG-008** | 恢复全员到位 → 立即喂狗 + 全部按位账本清零(v1.5:episode 内 1..7 窗已持续喂,恢复窗 feed_count==8) | 停顿可恢复(非锁死状态机) |
| **TC-WATCHDOG-009** | 99 ms 不判 / 100 ms 判 / 再 99 ms 不判 / 累计 100 ms 判 | 节流窗口**边界**(易错点) |
| **TC-WATCHDOG-010** | `tick` 跨 2^32 回绕后 100 ms 仍正确判 | 无符号差比较(回绕安全) |
| **TC-WATCHDOG-011** | `is_armed`/`last_mask`/`miss_count` 语义 | 观测接口正确性 |
| **TC-WATCHDOG-013** | 非法任务 ID(`>= TASK_WD_ID_MAX`)被忽略,不污染位图 | 防位图越界(`task_wd_beat` 的守卫) |
| **TC-WATCHDOG-013** | 非法任务 ID(`>= TASK_WD_ID_MAX`)被忽略,不污染位图;空位图单窗(streak 1)v1.5 照常喂 | 防位图越界(`task_wd_beat` 的守卫) |
| **TC-WATCHDOG-017** | UI/NET **交替缺席** 20 窗 → 零 stall、20 窗全部持续喂(v1.5 回归钉:旧复合判据会在第 8 窗误申报) | 复合串窗根治(2026-10-25 大窗 16:24/16:28 型误升级) |
| **TC-WATCHDOG-018** | 同一位(IEC104)缺席 7 窗:不报持续喂;8/9/10 窗连报 stall 且停喂;恢复窗喂并清零账本;第二段 7 窗亚容忍串再从零计、不误报 | 真单任务停摆保留杀伤力 + 账本重启(16:39 型真链仍可抓) |
**当前结果**:39/39 PASSED(`failed=0`)。
**当前结果**:47/47 PASSED(`failed=0`)。
---
@@ -180,5 +182,6 @@ if (s_miss_streak >= WD_MISS_TOLERANCE) { DBG_ERROR("[WD] task stall: ..."); }
| 日期 | 变更 |
|---|---|
| 2026-10-25 | **v1.5 按位分账同步**:停喂判据从复合连击改为**单个必需位**连击(设计说明 §2.2/§3.2/§5.3),TC-WATCHDOG-004/005/006/008/013 断言按新语义改写(缺席窗在容忍下照常喂)、新增 TC-WATCHDOG-017(交替缺席零误报)/ TC-WATCHDOG-018(同位连击必报 + 账本重启),39→47 断言;顺带修正 TC-WATCHDOG-003 行陈旧的 `0x37`(B4/B5 后应为 `0x1F7`) |
| 2026-10-01 | §1.3/§1.4/§1.5 更新:工具链逻辑抽取到通用 harness `test/host/common/unit_test_harness.py`(T-3),本模块 `run_unit_test.py` 改为**声明式** `TestBuild`(原 170 行 → 55 行);hooked 触发范围加 `test/host/common/unit_test_harness.py`。行为与退出码契约不变(39/39 PASSED) |
| 2026-10-01 | 创建:看门狗判定状态机主机侧单元测试(39 用例)+ 平台层抽取(`task_watchdog.h` 末段三个 `__weak` 钩子)+ pre-commit 钩子 `watchdog-unit-test`;记录两处已知行为(stall 复报、测试隔离) |
+68 -8
View File
@@ -163,12 +163,13 @@ static void tc_004_take_and_clear(void) {
beat_all();
poll_advance(100); /* 消费位图并喂狗 */
CHECK(s_feed_count == 1, "first round fed");
poll_advance(100); /* 不重新置位 */
CHECK(s_feed_count == 1, "no re-set -> second round does not feed (bitmap already taken and cleared)");
poll_advance(100); /* 不重新置位:空位图窗(各位 streak=1 < 容忍) */
CHECK(s_feed_count == 2, "v1.5: an empty-bitmap single window still feeds (streak 1 < tol)");
CHECK(task_wd_last_mask() == 0u, "bitmap was taken and cleared (no stale bits survive)");
}
static void tc_005_mask_required(void) {
printf("TC-WATCHDOG-005 mask check: missing any bit -> no feed\n");
printf("TC-WATCHDOG-005 mask check: one-window miss (streak 1) feeds; ledger records it\n");
start_and_clear();
/* 故意缺 UI(bit4) */
task_wd_beat(TASK_WD_DEFAULT);
@@ -179,13 +180,13 @@ static void tc_005_mask_required(void) {
task_wd_beat(TASK_WD_IEC104);
task_wd_beat(TASK_WD_PROTECT);
poll_advance(100);
CHECK(s_feed_count == 0, "UI bit missing -> no feed");
CHECK(s_feed_count == 1, "UI absent for ONE window (streak 1 < tol) -> still feeds (v1.5)");
CHECK(task_wd_last_mask() == (0x1F7u & ~(1u << TASK_WD_UI)),
"last_mask records the missing bit exactly");
}
static void tc_006_stall_8(void) {
printf("TC-WATCHDOG-006 8 consecutive misses -> print stall and stop feeding\n");
printf("TC-WATCHDOG-006 8 consecutive all-miss windows -> stall at the 8th, feeding stops\n");
start_and_clear();
beat_all(); poll_advance(100); /* streak 清零 */
int err0 = g_wdtest_err_count;
@@ -195,7 +196,7 @@ static void tc_006_stall_8(void) {
}
poll_advance(100); /* 第 8 次 */
CHECK(g_wdtest_err_count == err0 + 1, "8th miss prints [WD] task stall");
CHECK(s_feed_count == 1, "fed exactly once at the start only (nothing fed during the missing period)");
CHECK(s_feed_count == 8, "v1.5: windows 1..7 (streak < tol) kept feeding; the 8th stops");
}
static void tc_007_stall_repeats_until_reset(void) {
@@ -222,12 +223,69 @@ static void tc_008_recover(void) {
for (int i = 0; i < 8; i++) poll_advance(100); /* 触发 stall */
beat_all();
poll_advance(100);
CHECK(s_feed_count == 1, "feeds immediately after recovery");
CHECK(s_feed_count == 8, "feeds immediately after recovery (7 sub-tol windows fed + recovery)");
int err0 = g_wdtest_err_count;
poll_advance(100); /* 再缺一次 */
CHECK(g_wdtest_err_count == err0, "streak already reset: a single miss does not report stall");
}
/* 全员置位但跳过一位(按位连击用例的激励器;0x1F7 = 默认必需掩码,同 tc_005 用法) */
static void beat_all_except(task_wd_id_t skip) {
uint32_t bits = 0x1F7u;
uint32_t i;
for (i = 0u; i < (uint32_t)TASK_WD_ID_MAX; i++) {
if (((bits & (1u << i)) != 0u) && (i != (uint32_t)skip)) {
task_wd_beat((task_wd_id_t)i);
}
}
}
/* v1.5 按位分账·修复本体:交替缺席(本窗缺 UI、下窗缺 NET……)任何单位连击都
* 到不了容忍值——旧复合判据会在第 8 窗误报 stall 并停喂(2026-10-25 大窗
* 16:24/16:28 型),新判据必须全程零申报、持续喂狗 */
static void tc_017_mixed_alternating_no_false_stall(void) {
printf("TC-WATCHDOG-017 alternating per-bit misses never chain into a stall\n");
start_and_clear();
for (int i = 1; i <= 20; i++) {
if ((i % 2) != 0) {
beat_all_except(TASK_WD_UI); /* 奇数窗:UI 缺席 */
} else {
beat_all_except(TASK_WD_NET); /* 偶数窗:NET 缺席 */
}
poll_advance(100);
}
CHECK(g_wdtest_err_count == 0, "20 mixed-miss windows report ZERO stall (composite would have fired at 8th)");
CHECK(s_feed_count == 20, "feeding continues in every mixed window");
}
/* v1.5 按位分账·保留杀伤力:同一任务连续缺席到容忍值必须照报停摆、停喂;
* 恢复后账本清零,第二段亚容忍缺席重新从零计(16:39 型真单任务串仍会被抓) */
static void tc_018_single_bit_streak_stalls_and_restarts(void) {
printf("TC-WATCHDOG-018 same-bit streak reaches tol -> stall + feeding stops; ledger restarts after recovery\n");
start_and_clear();
for (int i = 1; i <= 7; i++) {
beat_all_except(TASK_WD_IEC104);
poll_advance(100);
}
CHECK(g_wdtest_err_count == 0 && s_feed_count == 7,
"7 windows missing the SAME bit stay below tol: no stall, feeding continues");
for (int i = 8; i <= 10; i++) {
beat_all_except(TASK_WD_IEC104);
poll_advance(100);
}
CHECK(g_wdtest_err_count == 3, "8th/9th/10th consecutive same-bit miss each report stall");
CHECK(s_feed_count == 7, "feeding stops from the 8th window onward");
beat_all();
poll_advance(100);
CHECK(s_feed_count == 8, "recovery window feeds and clears the per-bit ledger");
for (int i = 1; i <= 7; i++) {
beat_all_except(TASK_WD_IEC104);
poll_advance(100);
}
CHECK(g_wdtest_err_count == 3 && s_feed_count == 15,
"second sub-tol streak starts from zero again: no new stall");
}
static void tc_009_throttle_boundary(void) {
printf("TC-WATCHDOG-009 throttle window edges 99/100/199/200\n");
start_and_clear();
@@ -265,7 +323,7 @@ static void tc_013_bad_id(void) {
start_and_clear();
task_wd_beat((task_wd_id_t)(TASK_WD_ID_MAX + 3)); /* 非法:应被忽略 */
poll_advance(100);
CHECK(s_feed_count == 0, "invalid id does not pollute the bitmap (all required bits missing -> no feed)");
CHECK(s_feed_count == 1, "invalid id ignored; one all-miss window still feeds (streak 1 < tol, v1.5)");
CHECK(task_wd_last_mask() == 0u, "bitmap stays 0 (invalid bit never set)");
}
@@ -308,6 +366,8 @@ int main(void) {
fresh(); tc_006_stall_8();
fresh(); tc_007_stall_repeats_until_reset();
fresh(); tc_008_recover();
fresh(); tc_017_mixed_alternating_no_false_stall();
fresh(); tc_018_single_bit_streak_stalls_and_restarts();
fresh(); tc_009_throttle_boundary();
fresh(); tc_010_tick_wrap();
fresh(); tc_011_observers();