修复IWDG超时未配置与dhara resume回滚:看门狗误复位与复位后写入全部丢失,台架全量自检PASS
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
#include "gd5f2gq5ue.h"
|
||||
#include "tpafe5160.h"
|
||||
#include "sys_log.h"
|
||||
#include "task_watchdog.h" /* task_wd_seg_report():round 8 复位不丢"进段打点"上报 */
|
||||
|
||||
#define DBG_TAG "[APP]"
|
||||
#include "dbg_log.h"
|
||||
@@ -120,6 +121,13 @@ int app_main_init(void) {
|
||||
}
|
||||
}
|
||||
|
||||
/* 0b) round 8 取证:上报上一轮 defaultTask 主循环的**最后进段**。
|
||||
* 轨迹槽在 .noinit(scatter UNINIT_CCM),IWDG/引脚复位不清 SRAM →
|
||||
* 误复位后开机第一时间可见 `PREVRUN seg=N`,直接指向主循环阻塞点
|
||||
* (round 7 结论:判定分支未执行 = poller 自身停摆,见 实机测试规划.md §0.5.4)。
|
||||
* 裸 UART 直发,且一次复位只报一次(内部清 magic)。 */
|
||||
task_wd_seg_report();
|
||||
|
||||
/*
|
||||
* 1) 时间子系统初始化(尽早完成,后续所有日志带时间戳)
|
||||
* sd2506_init() 从 RTC 读取初始时间
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
* v1.2 2026-09-22 王建锋 原子写修复:rename 前先 unlink 已存在目标——本 FatFs
|
||||
* 移植 f_rename 不覆盖既有目标(FR_EXIST),旧实现一切
|
||||
* "目标存在的覆盖写"必然失败(E-01 台架验证暴露)
|
||||
* v1.3 2026-10-02 王建锋 四个后端函数纳入卷锁 try 语义(vol_lock_wait(0)):
|
||||
* 与 ftpTask 的 lftpd 文件操作互斥(并发 FTL 写 →
|
||||
* dhara journal 竞争实证);busy 返回 -1 由调用方
|
||||
* 按既有失败语义处理(poll 保留 RAM/保存待重试),
|
||||
* 本任务 = 看门狗喂狗点,绝不阻塞等锁
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
@@ -23,6 +28,7 @@
|
||||
#include "cfg_file_store.h"
|
||||
|
||||
#include "ff.h"
|
||||
#include "vol_lock.h"
|
||||
|
||||
/* 静态变量定义区 */
|
||||
|
||||
@@ -49,16 +55,22 @@ int cfg_store_read(const char *p_path, uint8_t *p_buf, size_t cap, size_t *p_nre
|
||||
if (p_buf == NULL || p_nread == NULL) {
|
||||
return -1;
|
||||
}
|
||||
if (vol_lock_wait(0u) == 0u) {
|
||||
return -1; /* 卷被占用:本轮跳过(调用方按读失败语义处理) */
|
||||
}
|
||||
fr = f_open(&f, p_path, FA_READ);
|
||||
if (fr != FR_OK) {
|
||||
vol_lock_release();
|
||||
return -1;
|
||||
}
|
||||
fr = f_read(&f, p_buf, (UINT)cap, &br);
|
||||
(void)f_close(&f);
|
||||
if (fr != FR_OK) {
|
||||
vol_lock_release();
|
||||
return -1;
|
||||
}
|
||||
*p_nread = (size_t)br;
|
||||
vol_lock_release();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -78,14 +90,19 @@ int cfg_store_write_atomic(const char *p_path, const uint8_t *p_buf, size_t len)
|
||||
if (p_buf == NULL) {
|
||||
return -1;
|
||||
}
|
||||
if (vol_lock_wait(0u) == 0u) {
|
||||
return -1; /* 卷被占用:本轮跳过(保存请求由 2s 轮询重试) */
|
||||
}
|
||||
fr = f_open(&f, s_path_tmp, FA_WRITE | FA_CREATE_ALWAYS);
|
||||
if (fr != FR_OK) {
|
||||
vol_lock_release();
|
||||
return -1;
|
||||
}
|
||||
fr = f_write(&f, (const void *)p_buf, (UINT)len, &bw);
|
||||
(void)f_close(&f);
|
||||
if (fr != FR_OK || bw != (UINT)len) {
|
||||
(void)f_unlink(s_path_tmp);
|
||||
vol_lock_release();
|
||||
return -1;
|
||||
}
|
||||
/* 本 FatFs 移植的 f_rename 对已存在目标返回 FR_EXIST、不覆盖(ff.c
|
||||
@@ -99,8 +116,10 @@ int cfg_store_write_atomic(const char *p_path, const uint8_t *p_buf, size_t len)
|
||||
fr = f_rename(s_path_tmp, p_path);
|
||||
if (fr != FR_OK) {
|
||||
(void)f_unlink(s_path_tmp);
|
||||
vol_lock_release();
|
||||
return -1;
|
||||
}
|
||||
vol_lock_release();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -120,12 +139,18 @@ int cfg_store_stat(const char *p_path, cfg_file_stat_t *p_st) {
|
||||
if (p_st == NULL) {
|
||||
return -1;
|
||||
}
|
||||
if (vol_lock_wait(0u) == 0u) {
|
||||
return -1; /* 卷被占用:调用方按"文件缺失/读失败"语义处理
|
||||
(poll = 保留 RAM 配置,安全) */
|
||||
}
|
||||
fr = f_stat(p_path, &fno);
|
||||
if (fr != FR_OK) {
|
||||
vol_lock_release();
|
||||
return -1;
|
||||
}
|
||||
p_st->size = (uint32_t)fno.fsize;
|
||||
p_st->mtime = (uint32_t)(((uint32_t)fno.fdate << 16) | (uint32_t)fno.ftime);
|
||||
vol_lock_release();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -136,9 +161,14 @@ int cfg_store_stat(const char *p_path, cfg_file_stat_t *p_st) {
|
||||
* 限定条件:无
|
||||
*/
|
||||
int cfg_store_remove(const char *p_path) {
|
||||
if (f_unlink(p_path) != FR_OK) {
|
||||
if (vol_lock_wait(0u) == 0u) {
|
||||
return -1;
|
||||
}
|
||||
if (f_unlink(p_path) != FR_OK) {
|
||||
vol_lock_release();
|
||||
return -1;
|
||||
}
|
||||
vol_lock_release();
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 模块名称:Volume Lock(卷锁)— FreeRTOS 实现
|
||||
* 模块功能:osMutex 串行化 NAND FatFs 卷访问(设计/使用约定见 vol_lock.h)
|
||||
* 适用平台:STM32F407ZGTx + FreeRTOS
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-02
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-02 王建锋 创建
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
#include "vol_lock.h"
|
||||
|
||||
#include "cmsis_os.h"
|
||||
|
||||
/* 静态变量定义区 */
|
||||
static osMutexId_t s_lock; /* 卷串行化互斥锁(vol_lock_init 创建) */
|
||||
|
||||
/* 函数定义区 */
|
||||
|
||||
/*
|
||||
* 函数功能:创建卷串行化互斥锁(幂等,重复调用安全)
|
||||
* 入口参数:无 void
|
||||
* 返回值:0 - 成功(已创建或已存在);-1 - osMutexNew 失败 int
|
||||
* 限定条件:可在内核启动后任意时刻调用;本实现不检查是否已启动调度器
|
||||
*/
|
||||
int vol_lock_init(void) {
|
||||
if (s_lock != NULL) {
|
||||
return 0; /* 幂等 */
|
||||
}
|
||||
s_lock = osMutexNew(NULL);
|
||||
return (s_lock != NULL) ? 0 : -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:获取卷锁(首次调用自动 vol_lock_init)
|
||||
* 入口参数:timeout_ms - 等待上限(毫秒);0 = try 语义(不阻塞) uint32_t
|
||||
* 返回值:1 - 已持锁;0 - 超时/未取得,或锁创建失败(fail-open) int
|
||||
* 限定条件:返回 0 时调用方**不得** vol_lock_release;fail-open 是为保证
|
||||
* 看门狗喂狗点(defaultTask)绝不能因等锁而阻塞
|
||||
*/
|
||||
int vol_lock_wait(uint32_t timeout_ms) {
|
||||
if (s_lock == NULL) {
|
||||
(void)vol_lock_init();
|
||||
if (s_lock == NULL) {
|
||||
return 1; /* 锁创建失败:fail-open(不阻塞,保持启动段可用) */
|
||||
}
|
||||
}
|
||||
return (osMutexWait(s_lock, timeout_ms) == osOK) ? 1 : 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:释放卷锁
|
||||
* 入口参数:无 void
|
||||
* 返回值:无 void
|
||||
* 限定条件:仅在 vol_lock_wait() 返回 1 后调用;锁未创建时静默返回
|
||||
*/
|
||||
void vol_lock_release(void) {
|
||||
if (s_lock != NULL) {
|
||||
(void)osMutexRelease(s_lock);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 模块名称:Volume Lock(卷锁)
|
||||
* 模块功能:NAND FatFs 卷的跨任务串行化锁——FatFs 卷状态(FS 窗口缓冲/FAT
|
||||
* 缓存)与 FTL 核心状态(单页 RAM 缓存 + dhara journal)均非线程安全;
|
||||
* defaultTask(syslog 落盘/配置引擎/存储 bring-up)与 ftpTask(lftpd
|
||||
* 文件操作)对卷的访问必须在本锁下串行执行
|
||||
* 适用平台:STM32F407ZGTx + FreeRTOS(CMSIS-RTOS V2)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-02
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-02 王建锋 创建(2026-10-02 台架实证:defaultTask 与
|
||||
* ftpTask 并发 FTL 写 → dhara journal 竞争 →
|
||||
* 长耗时恢复旋转饥饿 defaultTask 主循环(喂狗点)
|
||||
* → IWDG 误复位;单写者卷属主假设在多任务 FatFs
|
||||
* 调用下不成立,须显式串行化)
|
||||
*
|
||||
* 使用约定(铁律):
|
||||
* - 锁跨越"一次完整文件操作序列"(open...close / opendir...closedir /
|
||||
* 单发 stat/unlink/原子写),不得只锁单个 f_read/f_write;
|
||||
* - defaultTask 调用方一律用非阻塞 try(vol_lock_wait(0)):defaultTask 是
|
||||
* 看门狗喂狗点(task_wd_poll),阻塞等锁 = 主循环停摆 = 停喂 → IWDG 误复位;
|
||||
* try 失败 = 本轮跳过(syslog 条目滞留队列/配置保存待下轮重试);
|
||||
* - ftpTask 调用方用 100ms 分片阻塞等待 + 每片心跳(lftpd_task_wd_beat),
|
||||
* 防长等待导致 FTP 心跳位缺席误复位;
|
||||
* - 锁在内核启动后由 defaultTask 存储 bring-up 创建(vol_lock_init 幂等)。
|
||||
*/
|
||||
|
||||
#ifndef VOL_LOCK_H
|
||||
#define VOL_LOCK_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*
|
||||
* 函数功能:创建卷锁(幂等)
|
||||
* 入口参数:无
|
||||
* 返回值:0 - 成功/已创建;-1 - 创建失败 int
|
||||
* 限定条件:内核启动后调用(defaultTask 存储 bring-up 首行)
|
||||
*/
|
||||
int vol_lock_init(void);
|
||||
|
||||
/*
|
||||
* 函数功能:获取卷锁
|
||||
* 入口参数:timeout_ms - 等待超时(0 = 非阻塞 try) uint32_t
|
||||
* 返回值:1 - 已获取;0 - 超时未获取(卷被占用) int
|
||||
* 限定条件:见文件头使用约定(defaultTask 必须 timeout_ms=0)
|
||||
*/
|
||||
int vol_lock_wait(uint32_t timeout_ms);
|
||||
|
||||
/*
|
||||
* 函数功能:释放卷锁(获取方任务释放)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:必须与 vol_lock_wait 成对;未获取时调用无害(内部判空)
|
||||
*/
|
||||
void vol_lock_release(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* VOL_LOCK_H */
|
||||
+197
-9
@@ -8,6 +8,24 @@
|
||||
* 修改记录:
|
||||
* 2026-09-24 王建锋 创建(自 Src/usart.c USER CODE 区迁出,实现逻辑不变,
|
||||
* 仅变更文件位置:HAL 库生成文件不得包含应用代码)
|
||||
* 2026-10-02 王建锋 僵尸通道喂狗节奏保护(台架 gen-288 实证):round-3 的
|
||||
* "单次 drain 预算 250ms" 只限单次调用——ORE 锁存在外设软复位后
|
||||
* 毫秒级重锁(PA10 悬空 + RX 使能),背靠背 drain 每轮再耗 100ms
|
||||
* 块超时 → 主循环周期拉到 450~500ms → 100ms 节流的喂狗抽样间隙
|
||||
* 超过 IWDG 最坏容限 1.7s → 停喂 → 误复位(且心跳位全员健康、
|
||||
* 无 stall 日志,纯节奏性饥饿)。修复:发送故障后立即结束本轮
|
||||
* drain 并进入 300ms 发送冷却(冷却期内 drain 直接让位主循环),
|
||||
* 保证喂狗节奏 ≤ ~300ms。根治(USART1 TX-only 灭 ORE 机制)
|
||||
* 另行推进(.ioc 结构性修改 + CubeMX 重生成,见待处理内容①)。
|
||||
* 2026-10-02 王建锋 round 8b 加固:发送超时判据改用 DWT CYCCNT(时间源无关)。
|
||||
* 台架 round 8a 取证(实机测试规划.md §0.5.4.2)定位到:defaultTask
|
||||
* 冻结在 console 突发行内、无 STALL、复位为 IWDGRSTF。唯一自洽机理 =
|
||||
* HAL_UART_Transmit 的超时判据 `HAL_GetTick() - Tickstart` 依赖
|
||||
* TIM7 的 uwTick —— TIM7 一旦停摆,超时永不成立 → 写者死等 TXE →
|
||||
* 唯一喂狗者停摆 → IWDG 复位。本改动给"裸发送"与"缓冲满等待"两处
|
||||
* 都换成 DWT 周期计数硬上界(DWT 使能失败时退回旧 HAL 行为)。
|
||||
* 另加 USART1 未初始化(huart1.Instance == NULL)短路:防止在
|
||||
* HAL_Init 早期/断言-故障上报路径上解引用 NULL 读写地址 0。
|
||||
*/
|
||||
|
||||
#include "console_log.h"
|
||||
@@ -18,6 +36,7 @@
|
||||
#include "task.h"
|
||||
#include "cmsis_os.h"
|
||||
#include "sys_events.h"
|
||||
#include "sys_log.h"
|
||||
|
||||
/* ===== 控制台单写者日志(2026-09-24 加固,HardFault 定位结论驱动)=====
|
||||
* 背景:旧路径 DBG_* → printf → fputc **每字符一次** HAL_UART_Transmit。HAL 的锁只保证
|
||||
@@ -35,12 +54,29 @@
|
||||
#define DBG_CON_RING_SIZE 4096U /* 环形缓冲字节数(必须为 2 的幂) */
|
||||
#define DBG_CON_CHUNK_MAX 256U /* 写者单次 HAL_UART_Transmit 字节上限 */
|
||||
#define DBG_CON_WAIT_MS 200U /* 缓冲满时生产者最长等待(ms),仍满则丢弃并计数 */
|
||||
#define DBG_CON_TX_TIMEOUT_MS 100U /* 单次发送超时:256B 块 @115200 标称 22ms;僵尸通道限时 + 自愈 */
|
||||
#define DBG_CON_DRAIN_BUDGET_MS 250U /* 单次 drain 连续发送上限:让位喂狗轮询(缺失容忍 800ms) */
|
||||
#define DBG_CON_FAULT_COOL_MS 300U /* 故障后发送冷却:ORE 锁存毫秒级重锁,背靠连发无益
|
||||
且拖长主循环周期 → 喂狗节奏饥饿(gen-288 实证);
|
||||
冷却期内 drain 只让位、不发送,下轮重试 */
|
||||
|
||||
/* ==== round 8b:与时间源无关的硬上界(DWT CYCCNT)====
|
||||
* HAL_UART_Transmit 的超时判据是 `HAL_GetTick() - Tickstart > Timeout`,而 HAL 时基
|
||||
* = TIM7 的 uwTick。TIM7 停摆 → 超时永不成立 → 写者死等 TXE → 唯一喂狗者停摆 →
|
||||
* IWDG 误复位(round 8a 台架现场)。改用 DWT CYCCNT(内核硬件周期计数,不依赖任何
|
||||
* 中断/uwTick)做判据;DWT 使能失败时退回 HAL_UART_Transmit(旧行为,功能不退化)。 */
|
||||
#define CON_TX_TIMEOUT_CYC (SystemCoreClock / 10U) /* 发送硬上界 = 100ms 对应周期数 */
|
||||
#define CON_APPEND_WAIT_CYC (SystemCoreClock / 5U) /* 缓冲满等待硬上界 = 200ms 对应周期数 */
|
||||
|
||||
static uint8_t s_con_ring[DBG_CON_RING_SIZE];
|
||||
static volatile uint16_t s_con_head; /* 生产者写入位置 */
|
||||
static volatile uint16_t s_con_tail; /* 写者读取位置 */
|
||||
static volatile uint32_t s_con_drop; /* 因缓冲满丢弃的行数 */
|
||||
static volatile uint32_t s_con_tx_fault; /* TX 失败(僵尸通道)次数,均经外设软复位自愈 */
|
||||
static volatile uint32_t s_con_cool_until; /* 故障冷却截止 tick(0 = 无冷却) */
|
||||
static volatile uint8_t s_con_draining; /* 1 = 处于 drain 中(重入保护) */
|
||||
static volatile uint8_t s_con_writer_up; /* 1 = 写者已注册(此后走环形缓冲) */
|
||||
static volatile uint8_t s_con_dwt_up; /* 1 = DWT CYCCNT 已使能(硬上界基准可用) */
|
||||
static TaskHandle_t s_con_writer_task; /* 写者任务句柄(自等保护用) */
|
||||
|
||||
/*
|
||||
@@ -52,10 +88,75 @@ static uint16_t con_used(void) {
|
||||
return (uint16_t)((uint16_t)(s_con_head - s_con_tail) & (DBG_CON_RING_SIZE - 1U));
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:惰性使能 DWT 周期计数器(时间源无关的硬上界基准)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:任意上下文可调(幂等;纯寄存器写,不创建任何内核对象)
|
||||
* 函数说明:CoreDebug->DEMCR.TRCENA=1 → DWT->CYCCNT=0 → DWT->CTRL.CYCCNTENA=1;
|
||||
* 使能成功置 s_con_dwt_up=1(供超时判据选择),失败保持 0(退回 HAL 超时)。
|
||||
* 在 Cortex-M4 上 DWT 恒存在;失败仅可能出现在调试单元被硬件关闭时。
|
||||
*/
|
||||
static void con_dwt_ensure(void) {
|
||||
if (s_con_dwt_up != 0U) {
|
||||
return;
|
||||
}
|
||||
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
|
||||
DWT->CYCCNT = 0U;
|
||||
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
|
||||
if ((DWT->CTRL & DWT_CTRL_CYCCNTENA_Msk) != 0U) {
|
||||
s_con_dwt_up = 1U;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:有界裸发送(不依赖 uwTick / HAL 超时)
|
||||
* 入口参数:p_buf - 待发数据;len - 字节数(调用方已限 ≤ DBG_CON_CHUNK_MAX)
|
||||
* 返回值:HAL_OK - 已全部写入发送寄存器;HAL_TIMEOUT - 有界上界内未完成
|
||||
* 限定条件:USART1 已初始化
|
||||
* 函数说明:寄存器级轮询 TXE/TC,超时判据用 DWT CYCCNT(CON_TX_TIMEOUT_CYC);
|
||||
* DWT 未使能时退回 HAL_UART_Transmit(旧行为)。本函数只做 TX,
|
||||
* 不触碰 RX/错误标志——僵尸通道自愈由调用方按返回值处理。
|
||||
* ⚠ USART1 未初始化(huart1.Instance == NULL,如 HAL_Init 早期或
|
||||
* 断言/故障上报路径)时立即返回 HAL_ERROR:否则会解引用 NULL 去读写
|
||||
* 地址 0(flash 别名区)→ 在故障处理里再触发故障(2026-10-02 台架实测)。
|
||||
*/
|
||||
static HAL_StatusTypeDef con_tx_bounded(const uint8_t *p_buf, uint16_t len) {
|
||||
USART_TypeDef *u;
|
||||
uint32_t t0;
|
||||
uint16_t i;
|
||||
|
||||
con_dwt_ensure();
|
||||
if (huart1.Instance == NULL) {
|
||||
return HAL_ERROR; /* USART1 未初始化:无可用通道,安全让位 */
|
||||
}
|
||||
if (s_con_dwt_up == 0U) {
|
||||
return HAL_UART_Transmit(&huart1, (uint8_t *)p_buf, len, DBG_CON_TX_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
u = huart1.Instance;
|
||||
t0 = DWT->CYCCNT;
|
||||
for (i = 0U; i < len; i++) {
|
||||
while ((u->SR & USART_SR_TXE) == 0U) {
|
||||
if ((uint32_t)(DWT->CYCCNT - t0) >= CON_TX_TIMEOUT_CYC) {
|
||||
return HAL_TIMEOUT; /* 有界:本块放弃(调用方自愈) */
|
||||
}
|
||||
}
|
||||
u->DR = (uint16_t)p_buf[i];
|
||||
}
|
||||
while ((u->SR & USART_SR_TC) == 0U) {
|
||||
if ((uint32_t)(DWT->CYCCNT - t0) >= CON_TX_TIMEOUT_CYC) {
|
||||
return HAL_TIMEOUT;
|
||||
}
|
||||
}
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:控制台裸发送(异常/早期路径专用:有界、单次、不加锁)
|
||||
* 入口参数:buf - 文本缓冲;len - 字节数(超上限截断)
|
||||
* 限定条件:串口已初始化;仅用于写者未注册、中断/异常上下文、崩溃上报
|
||||
* 函数说明:走 con_tx_bounded(DWT 硬上界),tick 停摆时仍能在有界时间内返回。
|
||||
*/
|
||||
void dbg_console_write_raw(const char *buf, unsigned len) {
|
||||
if ((buf == NULL) || (len == 0U)) {
|
||||
@@ -64,7 +165,7 @@ void dbg_console_write_raw(const char *buf, unsigned len) {
|
||||
if (len > DBG_CON_CHUNK_MAX) {
|
||||
len = DBG_CON_CHUNK_MAX;
|
||||
}
|
||||
(void)HAL_UART_Transmit(&huart1, (uint8_t *)buf, (uint16_t)len, 0xFFFFu);
|
||||
(void)con_tx_bounded((const uint8_t *)buf, (uint16_t)len);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -76,31 +177,96 @@ void dbg_console_writer_attach(void) {
|
||||
s_con_writer_up = 1U;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:USART1 僵尸通道自愈(清错误锁存 + 整外设重初始化)
|
||||
* 函数说明:RX 悬空噪声(PA10 未接线)锁存的 ORE/NE/FE/PE 使 HAL 轮询 TX 恒快速失败
|
||||
* (控制台静默、系统不复位);发送器卡死状态则使 TXE 等待耗满超时(冻结唯一
|
||||
* 写者 → 看门狗误复位,2026-10-02 台架实证)。外设软复位清锁存与卡死状态后
|
||||
* 重新初始化,故障时间被单块超时限定
|
||||
*/
|
||||
static void con_uart_reinit(void) {
|
||||
__NVIC_ClearPendingIRQ(USART1_IRQn);
|
||||
__HAL_RCC_USART1_CLK_DISABLE();
|
||||
__HAL_RCC_USART1_CLK_ENABLE();
|
||||
huart1.gState = HAL_UART_STATE_RESET;
|
||||
huart1.RxState = HAL_UART_STATE_RESET;
|
||||
huart1.ErrorCode = HAL_UART_ERROR_NONE;
|
||||
(void)MX_USART1_UART_Init();
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:排空环形缓冲(仅写者任务调用)
|
||||
* 函数说明:按块一次 HAL_UART_Transmit 发出;每块完成后再推进读指针(临界区保护)
|
||||
* 函数说明:按块一次 con_tx_bounded 发出(DWT 硬上界,不依赖 uwTick);每块完成后
|
||||
* 再推进读指针(临界区保护);发送失败 → 外设软复位自愈(丢当前块);
|
||||
* 单次调用最多连续发 DBG_CON_DRAIN_BUDGET_MS 后让位主循环,保证看门狗轮询
|
||||
* 不被僵尸通道饿死
|
||||
*/
|
||||
void dbg_console_drain(void) {
|
||||
uint16_t used;
|
||||
uint32_t t0;
|
||||
|
||||
if (s_con_writer_up == 0U) {
|
||||
return;
|
||||
}
|
||||
if (s_con_draining != 0U) {
|
||||
return; /* 重入保护:写者自记录不触发嵌套 drain */
|
||||
}
|
||||
|
||||
s_con_draining = 1U;
|
||||
t0 = (uint32_t)HAL_GetTick();
|
||||
|
||||
/* 故障冷却:僵尸通道(ORE 重锁)秒内不自愈,冷却期内不发送——
|
||||
* 本轮 drain 直接让位主循环,保住喂狗节奏(gen-288 停喂误复位的
|
||||
* 直接诱因 = 背靠连发的块超时链)。无符号差值跨 49.7 天回绕安全 */
|
||||
if (s_con_cool_until != 0U) {
|
||||
if ((uint32_t)(t0 - s_con_cool_until) < DBG_CON_FAULT_COOL_MS) {
|
||||
s_con_draining = 0U;
|
||||
return;
|
||||
}
|
||||
s_con_cool_until = 0U;
|
||||
}
|
||||
|
||||
while ((used = con_used()) != 0U) {
|
||||
uint16_t tail = s_con_tail;
|
||||
uint16_t contig = (uint16_t)(DBG_CON_RING_SIZE - tail); /* 到环尾的连续段 */
|
||||
uint16_t n = (used < contig) ? used : contig;
|
||||
uint16_t tail;
|
||||
uint16_t contig;
|
||||
uint16_t n;
|
||||
HAL_StatusTypeDef st;
|
||||
|
||||
tail = s_con_tail;
|
||||
contig = (uint16_t)(DBG_CON_RING_SIZE - tail); /* 到环尾的连续段 */
|
||||
n = (used < contig) ? used : contig;
|
||||
|
||||
if (n > DBG_CON_CHUNK_MAX) {
|
||||
n = DBG_CON_CHUNK_MAX;
|
||||
}
|
||||
(void)HAL_UART_Transmit(&huart1, &s_con_ring[tail], n, 0xFFFFu);
|
||||
st = con_tx_bounded(&s_con_ring[tail], n);
|
||||
taskENTER_CRITICAL();
|
||||
s_con_tail = (uint16_t)((uint16_t)(s_con_tail + n) & (DBG_CON_RING_SIZE - 1U));
|
||||
taskEXIT_CRITICAL();
|
||||
|
||||
if (st != HAL_OK) {
|
||||
/* 僵尸通道:软复位自愈 + 进入发送冷却(ORE 毫秒级重锁,
|
||||
* 继续背靠连发只会把主循环耗在块超时链上);本块丢弃
|
||||
* (行已出环,不可重放) */
|
||||
con_uart_reinit();
|
||||
s_con_tx_fault++;
|
||||
s_con_cool_until = (uint32_t)HAL_GetTick();
|
||||
if (s_con_tx_fault == 1U) {
|
||||
static const char note[] = "\r\n[WARN] CONSOLE tx fault: UART reinit (self-healed)\r\n";
|
||||
|
||||
syslog_post(SYSLOG_RUN, "console tx fault, uart reinit"); /* 仅 run.log,不入自检判定 */
|
||||
(void)dbg_console_write_raw(note, (unsigned)sizeof(note) - 1U);
|
||||
}
|
||||
break; /* 故障即止:本轮 drain 到此为止,让位主循环 */
|
||||
}
|
||||
|
||||
if ((uint32_t)(HAL_GetTick() - t0) >= DBG_CON_DRAIN_BUDGET_MS) {
|
||||
break; /* 预算用尽:让位主循环(看门狗轮询),下轮续排 */
|
||||
}
|
||||
}
|
||||
|
||||
s_con_draining = 0U;
|
||||
|
||||
/* 丢弃可观测性:缓冲满丢行必须显式可见(否则会掩盖开机自检/测试标记的丢失) */
|
||||
if (s_con_drop != 0U) {
|
||||
char note[64];
|
||||
@@ -124,9 +290,13 @@ void dbg_console_drain(void) {
|
||||
* 返回值:0 - 已入缓冲或已直发;-1 - 缓冲满丢弃(计入 dbg_console_dropped)
|
||||
* 函数说明:生产者不持锁等待过久:最多 DBG_CON_WAIT_MS,仍满则丢弃该行;
|
||||
* 写者自身记录日志时先排空缓冲,避免自等。
|
||||
* 等待上界优先用 DWT 真实时间(CON_APPEND_WAIT_CYC,时间源无关)——
|
||||
* tick 停摆时 vTaskDelay 本身也会停,故不能用它作唯一判据(round 8b)。
|
||||
*/
|
||||
static int con_append(const char *buf, uint16_t len) {
|
||||
uint32_t waited = 0U;
|
||||
uint32_t d0 = 0U;
|
||||
uint8_t have_dwt = 0U;
|
||||
|
||||
if ((buf == NULL) || (len == 0U)) {
|
||||
return -1;
|
||||
@@ -140,8 +310,14 @@ static int con_append(const char *buf, uint16_t len) {
|
||||
len = (uint16_t)(DBG_CON_RING_SIZE / 2U); /* 超长保护(正常路径不会触发) */
|
||||
}
|
||||
|
||||
if (xTaskGetCurrentTaskHandle() == s_con_writer_task) {
|
||||
dbg_console_drain(); /* 写者自己记录:先腾空,避免自等 */
|
||||
if ((xTaskGetCurrentTaskHandle() == s_con_writer_task) && (s_con_draining == 0U)) {
|
||||
dbg_console_drain(); /* 写者自己记录:先腾空,避免自等(drain 进行中则跳过,防嵌套) */
|
||||
}
|
||||
|
||||
con_dwt_ensure();
|
||||
if (s_con_dwt_up != 0U) {
|
||||
d0 = DWT->CYCCNT;
|
||||
have_dwt = 1U;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
@@ -151,7 +327,12 @@ static int con_append(const char *buf, uint16_t len) {
|
||||
if (free_space >= len) {
|
||||
break;
|
||||
}
|
||||
if (waited >= DBG_CON_WAIT_MS) {
|
||||
if (have_dwt != 0U) {
|
||||
if ((uint32_t)(DWT->CYCCNT - d0) >= CON_APPEND_WAIT_CYC) {
|
||||
s_con_drop++;
|
||||
return -1;
|
||||
}
|
||||
} else if (waited >= DBG_CON_WAIT_MS) {
|
||||
s_con_drop++;
|
||||
return -1;
|
||||
}
|
||||
@@ -213,6 +394,13 @@ uint32_t dbg_console_dropped(void) {
|
||||
return s_con_drop;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:已发生的 TX 故障(僵尸通道自愈)次数(诊断用)
|
||||
*/
|
||||
uint32_t dbg_console_tx_fault(void) {
|
||||
return s_con_tx_fault;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:MicroLIB printf 底层重定向(库内部/遗留 printf 调用)
|
||||
* 函数说明:同样只把字节交给单写者路径;未注册写者时直发(与旧行为一致)
|
||||
|
||||
@@ -34,8 +34,9 @@ extern "C" {
|
||||
* DBG_PRINT 宏内用库 snprintf 完成)
|
||||
* dbg_console_write_raw - 异常/早期路径:有界单次直发(不加锁)
|
||||
* dbg_console_writer_attach- 写者任务(defaultTask)注册自己为唯一发送者
|
||||
* dbg_console_drain - 写者任务排空缓冲并发送
|
||||
* dbg_console_drain - 写者任务排空缓冲并发送(单次有界;TX 失败自动软复位自愈)
|
||||
* dbg_console_dropped - 因缓冲满丢弃的行数(诊断)
|
||||
* dbg_console_tx_fault - 因僵尸通道软复位自愈的次数(诊断)
|
||||
*/
|
||||
#define DBG_CON_LINE_MAX 192U /* 单行文本上限(含结尾 '\0',超出截断) */
|
||||
int dbg_console_write_line(const char *line, unsigned len);
|
||||
@@ -43,6 +44,7 @@ void dbg_console_write_raw(const char *buf, unsigned len);
|
||||
void dbg_console_writer_attach(void);
|
||||
void dbg_console_drain(void);
|
||||
uint32_t dbg_console_dropped(void);
|
||||
uint32_t dbg_console_tx_fault(void);
|
||||
|
||||
/*
|
||||
* 函数功能:MicroLIB printf 底层重定向(C 库 retarget 钩子,须为全局符号)
|
||||
|
||||
@@ -119,6 +119,21 @@ void syslog_mcu_mounted(void);
|
||||
*/
|
||||
void syslog_drain(void);
|
||||
|
||||
/*
|
||||
* 函数功能:把一条取证行追加到 /log/wdev.log(掉电不丢;变长行,不走 64B 定长格式)
|
||||
* 入口参数:p_desc - 取证描述行(调用方已格式化;不可为 NULL) const char *
|
||||
* 返回值:0 - 成功;-1 - 卷占用或写失败(证据可能回滚,**不阻断判定**) int
|
||||
* 限定条件:poller(defaultTask)上下文调用;卷锁一律 try(绝不阻塞,
|
||||
* 本上下文 = 看门狗喂狗点)
|
||||
* 函数说明:round 7 取证通道(设计见 docs/测试说明/实机测试规划.md §0.5.4:
|
||||
* 误复位后 RAM 态随复位消失、stall 行经僵尸串口丢失,故证据必须落盘)。
|
||||
* 内部在释放卷锁后立刻追加一条 s_append(SYSLOG_RUN, ...) 写另一个
|
||||
* 文件——FTL 单页缓存语义为"写到不同 LPN 才把前一脏页刷进 NAND",
|
||||
* 没有该 flush 诱导,本行证据会在约 1.7s 后的 IWDG 复位中回滚丢失。
|
||||
* 文件 > 8KB 时先 unlink 重建,防长跑无界增长。
|
||||
*/
|
||||
int sys_log_evidence(const char *p_desc);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
+99
-4
@@ -9,6 +9,13 @@
|
||||
* v1.0 2026-09-14 王建锋 创建初始版本(设计见 docs/子系统设计/运行与自检记录设计说明.md §4.4)
|
||||
* v1.1 2026-09-21 王建锋 消息队列改为内核启动后由 syslog_drain 惰性创建
|
||||
* (修复 osKernelStart 前创建 FreeRTOS 内核对象导致的启动失败)
|
||||
* v1.2 2026-10-02 王建锋 s_append 落盘序列纳入卷锁 try 语义(vol_lock_wait(0)):
|
||||
* 与 ftpTask 的 lftpd 文件操作互斥(并发 FTL 写 →
|
||||
* dhara journal 竞争实证);busy 返回 -2 静默顺延
|
||||
* (本任务 = 看门狗喂狗点,绝不阻塞等锁)
|
||||
* v1.3 2026-10-02 王建锋 新增 sys_log_evidence():把看门狗停喂现场
|
||||
* 证据落 /log/wdev.log(掉电不丢)+ flush 诱导
|
||||
* (round 7 纯取证,见 实机测试规划.md §0.5.4)
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
@@ -22,6 +29,7 @@
|
||||
#include "ff.h"
|
||||
#include "cmsis_os.h"
|
||||
#include "sys_clock.h"
|
||||
#include "vol_lock.h"
|
||||
|
||||
/* 静态变量定义区 */
|
||||
|
||||
@@ -48,7 +56,7 @@ static volatile uint8_t s_fs_ready; /* 卷挂载 + /log 目录
|
||||
* 函数功能:追加一条记录(格式化 64B + FA_OPEN_APPEND;容量满先删除重建)
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
* desc - 描述串(UTF-8,超长截断至 SYSLOG_DESC_MAX) const char *
|
||||
* 返回值:0 - 成功;-1 - 写失败 int
|
||||
* 返回值:0 - 成功;-1 - 写失败;-2 - 卷被占用(顺延,非故障) int
|
||||
* 限定条件:仅 defaultTask(唯一写者)调用;卷已挂载
|
||||
*/
|
||||
static int s_append(syslog_id_t id, const char *desc) {
|
||||
@@ -63,10 +71,17 @@ static int s_append(syslog_id_t id, const char *desc) {
|
||||
if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT || desc == NULL) {
|
||||
return -1;
|
||||
}
|
||||
/* 卷锁 try:本任务 = 看门狗喂狗点,绝不允许阻塞等锁(阻塞 = 喂狗停摆 =
|
||||
* 误复位);busy = FTP 文件操作持有卷,本轮顺延(条目滞留队列/待发
|
||||
* 数组),静默跳过、不计故障 */
|
||||
if (vol_lock_wait(0u) == 0u) {
|
||||
return -2;
|
||||
}
|
||||
if (syslog_count(id) >= s_caps[id]) {
|
||||
fr = f_unlink(s_paths[id]);
|
||||
DBG_INFO("cap full, recreate %s", s_paths[id]);
|
||||
if (fr != FR_OK && fr != FR_NO_FILE) {
|
||||
vol_lock_release();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -129,6 +144,7 @@ static int s_append(syslog_id_t id, const char *desc) {
|
||||
s_fault_reported[id] = 1u;
|
||||
DBG_ERROR("write %s fail (%d)", s_paths[id], (int)fr);
|
||||
}
|
||||
vol_lock_release();
|
||||
return rc;
|
||||
}
|
||||
|
||||
@@ -236,9 +252,16 @@ void syslog_drain(void) {
|
||||
if (osMessageQueueGet(s_queue, &e, 0u, 0u) != osOK) {
|
||||
break; /* 队列空 */
|
||||
}
|
||||
if (s_append((syslog_id_t)e.id, e.desc) != 0) {
|
||||
DBG_ERROR("drop entry id=%d", (int)e.id);
|
||||
break; /* 落盘失败:停止排空,避免连续丢弃 */
|
||||
{
|
||||
int arc = s_append((syslog_id_t)e.id, e.desc);
|
||||
if (arc == -2) {
|
||||
break; /* 卷被占用(FTP 文件操作进行中):本轮顺延,静默
|
||||
(条目已出队;run.log 尽力而为语义,不计故障) */
|
||||
}
|
||||
if (arc != 0) {
|
||||
DBG_ERROR("drop entry id=%d", (int)e.id);
|
||||
break; /* 落盘失败:停止排空,避免连续丢弃 */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -357,3 +380,75 @@ int syslog_last_epoch(syslog_id_t id, uint32_t *p_epoch) {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:把一条取证行追加到 /log/wdev.log(掉电不丢;round 7 纯取证)
|
||||
* 入口参数:p_desc - 取证描述行(调用方已格式化;不可为 NULL) const char *
|
||||
* 返回值:0 - 成功;-1 - 卷占用或写失败(证据可能回滚,不阻断判定) int
|
||||
* 限定条件:poller(defaultTask)上下文调用;卷锁一律 try(绝不阻塞)
|
||||
* 函数说明:
|
||||
* 为什么必须落盘:误复位后 RAM 态随复位消失、`[WD] task stall` 行经僵尸串口
|
||||
* 可能丢失(2026-10-02 台架 round 3~6 绕圈的判据缺口),故证据必须落到
|
||||
* **掉电不丢**的盘上,复位后经 FTP RETR 回读。
|
||||
* flush 诱导(关键):写完 wdev.log 后立刻 s_append(SYSLOG_RUN, ...) 写另一个
|
||||
* 文件——FTL 单页缓存语义为"写到**不同 LPN** 才把前一脏页刷进 NAND";没有
|
||||
* 这一步,本行证据会在约 1.7s 后的 IWDG 复位中回滚丢失(409 条里被回滚的
|
||||
* 07:56 记录即同类教训)。s_append 自行取卷锁(非递归锁),故必须在
|
||||
* vol_lock_release() **之后**调用,绝不可在持锁状态下调用。
|
||||
* 容量护栏:> 8KB 先 unlink 重建,防长跑无界增长(8.3 合法名 wdev.log)。
|
||||
*/
|
||||
int sys_log_evidence(const char *p_desc) {
|
||||
FIL f;
|
||||
FRESULT fr;
|
||||
UINT bw = 0u;
|
||||
UINT line_len;
|
||||
char line[192];
|
||||
char ts[SYSLOG_TS_LEN + 1u];
|
||||
int n;
|
||||
int rc = 0;
|
||||
|
||||
if (p_desc == NULL) {
|
||||
return -1;
|
||||
}
|
||||
/* 卷锁 try:本上下文 = 看门狗喂狗点,绝不阻塞等锁 */
|
||||
if (vol_lock_wait(0u) == 0u) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 容量护栏:超过 8KB 先删除重建(防长跑无界增长) */
|
||||
{
|
||||
FILINFO finfo;
|
||||
if (f_stat("/log/wdev.log", &finfo) == FR_OK && finfo.fsize > 8192u) {
|
||||
(void)f_unlink("/log/wdev.log");
|
||||
}
|
||||
}
|
||||
|
||||
if (sys_clock_get_str(ts, sizeof(ts)) == NULL) {
|
||||
memcpy(ts, "1970-01-01 08:00:00", SYSLOG_TS_LEN);
|
||||
}
|
||||
ts[SYSLOG_TS_LEN] = '\0';
|
||||
n = snprintf(line, sizeof(line), "%s %s\r\n", ts, p_desc);
|
||||
if (n <= 0) {
|
||||
vol_lock_release();
|
||||
return -1;
|
||||
}
|
||||
line_len = (UINT)strlen(line);
|
||||
|
||||
fr = f_open(&f, "/log/wdev.log", FA_WRITE | FA_OPEN_APPEND);
|
||||
if (fr == FR_OK) {
|
||||
fr = f_write(&f, line, line_len, &bw);
|
||||
(void)f_close(&f);
|
||||
if (fr != FR_OK || bw != line_len) {
|
||||
rc = -1;
|
||||
}
|
||||
} else {
|
||||
rc = -1;
|
||||
}
|
||||
vol_lock_release();
|
||||
|
||||
/* flush 诱导:写另一个文件(不同 LPN)把 wdev.log 的脏页刷进 NAND。
|
||||
* 必须在释放卷锁之后调用(s_append 自行取锁,非递归锁不可重入)。 */
|
||||
(void)s_append(SYSLOG_RUN, "wd stall declared");
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
+50
-16
@@ -10,6 +10,10 @@
|
||||
* v1.1 2026-08-28 王建锋 改为 TCP Server,迁入独立任务 timeSyncTask,
|
||||
* 所有 socket 操作经 net_socket 线程安全 API 走消息队列,
|
||||
* netTask 仅负责 net_poll/net_process_messages,多任务并发安全
|
||||
* v1.2 2026-10-02 王建锋 round 8b 消抖:等待片长 200ms→50ms,并把各处长
|
||||
* 等待(500/2000/200ms)统一走 time_sync_delay_fed()
|
||||
* 按 ≤50ms 分片、每片喂心跳——消除"心跳滑出两个连续
|
||||
* 100ms 抽样窗"的节拍抖动(round 7 台架 snap=0x17)
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
@@ -38,9 +42,31 @@
|
||||
#define TIME_SYNC_TOTAL_BUDGET_MS 30000U
|
||||
|
||||
/* 看门狗分片等待:单次 net_recv_to 的等待片长上限。须显著小于看门狗抽样窗
|
||||
* (task_watchdog.c WD_POLL_PERIOD_MS=100ms)的容错窗口(8 窗 = 800ms),
|
||||
* 200ms 留足余量;语义上仅把一次长等待拆成多次短等待,总预算不变。 */
|
||||
#define TIME_SYNC_WAIT_SLICE_MS 200U
|
||||
* (task_watchdog.c WD_POLL_PERIOD_MS=100ms)——片长 ≤50ms 时每片前喂的心跳
|
||||
* 必然落在同一个 100ms 抽样窗内,消除"心跳恰好滑过两个连续窗口"的边界抖动
|
||||
* (round 7 台架稳态 snap=0x17 即抽样窗 vs 心跳节拍的良性错位,round 8b 消抖)。
|
||||
* 语义上仅把一次长等待拆成多次短等待,总预算不变。 */
|
||||
#define TIME_SYNC_WAIT_SLICE_MS 50U
|
||||
|
||||
/*
|
||||
* 函数功能:可喂心跳的延时(把长等待切成 ≤50ms 短片,每片喂一次心跳)
|
||||
* 入口参数:ms - 总延时(ms) uint32_t
|
||||
* 返回值:无
|
||||
* 限定条件:任务上下文调用
|
||||
* 函数说明:看门狗抽样窗为 100ms(task_watchdog.c WD_POLL_PERIOD_MS)。把任意长
|
||||
* 等待切成 ≤TIME_SYNC_WAIT_SLICE_MS 的短片、每片前喂心跳,使心跳必然
|
||||
* 落在单个抽样窗内——语义等价于一次长延时,仅把"心跳节奏"钉在窗内
|
||||
* (round 8b 消抖;不改变总延时预算)。
|
||||
*/
|
||||
static void time_sync_delay_fed(uint32_t ms) {
|
||||
while (ms > 0U) {
|
||||
uint32_t slice = (ms > TIME_SYNC_WAIT_SLICE_MS) ? TIME_SYNC_WAIT_SLICE_MS : ms;
|
||||
|
||||
task_wd_beat(TASK_WD_TIMESYNC);
|
||||
vTaskDelay(pdMS_TO_TICKS(slice));
|
||||
ms -= slice;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:创建并监听 TCP 时间同步服务端口
|
||||
@@ -59,7 +85,7 @@ static int time_sync_open_listener(void) {
|
||||
int s = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
if (s < 0) {
|
||||
DBG_ERROR("time_sync: net_socket FAIL");
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
time_sync_delay_fed(500U);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -72,16 +98,14 @@ static int time_sync_open_listener(void) {
|
||||
if (net_bind(s, (struct net_sockaddr *)&addr, sizeof(addr)) != 0) {
|
||||
DBG_ERROR("time_sync: bind port %d FAIL", TIME_SYNC_PORT);
|
||||
net_close(s);
|
||||
task_wd_beat(TASK_WD_TIMESYNC);
|
||||
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
time_sync_delay_fed(2000U);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (net_listen(s) != 0) {
|
||||
DBG_ERROR("time_sync: listen FAIL");
|
||||
net_close(s);
|
||||
task_wd_beat(TASK_WD_TIMESYNC);
|
||||
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
time_sync_delay_fed(2000U);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -166,11 +190,24 @@ static void time_sync_handle_conn(int conn_sock) {
|
||||
void time_sync_task(void *argument) {
|
||||
(void)argument;
|
||||
|
||||
task_wd_bind(TASK_WD_TIMESYNC); /* FTL 心跳路由:登记本任务句柄(入口一次) */
|
||||
|
||||
/* 等网络栈真正就绪(取代拍脑袋的 8s 魔法延时),确保 netTask 已开始处理消息;
|
||||
* osFlagsNoClear:就绪位是状态,不能被本任务消费掉(否则其它等待者永不唤醒) */
|
||||
if (sys_evtHandle != NULL) {
|
||||
(void)osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY,
|
||||
osFlagsWaitAny | osFlagsNoClear, osWaitForever);
|
||||
/* osFlagsNoClear:就绪位是状态,不能被本任务消费掉(否则其它等待者永不唤醒)。
|
||||
* 2026-10-02 台架修复:等待改 100ms 分片 + 每片喂心跳——IWDG 必需掩码含
|
||||
* TS 位,原 osWaitForever 无心跳 → 启动初期(存储已就绪、网络未就绪)
|
||||
* 掩码 800ms 不可达 → 停喂 → 复位循环(2026-10-02 台架根因之一)。
|
||||
* 100ms 分片 = 抽样窗,纯任务睡眠零队列开销。 */
|
||||
for (;;) {
|
||||
uint32_t r = osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY,
|
||||
osFlagsWaitAny | osFlagsNoClear, 100u);
|
||||
if ((r & osFlagsError) == 0u) {
|
||||
break; /* NET_READY 已置位(状态位,未被消费) */
|
||||
}
|
||||
task_wd_beat(TASK_WD_TIMESYNC); /* 看门狗:分片喂心跳(屏障等待态) */
|
||||
}
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
@@ -184,8 +221,7 @@ void time_sync_task(void *argument) {
|
||||
int ls = time_sync_open_listener();
|
||||
if (ls < 0) {
|
||||
DBG_ERROR("time_sync: open listener FAIL, retry later");
|
||||
task_wd_beat(TASK_WD_TIMESYNC);
|
||||
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
time_sync_delay_fed(2000U);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -198,8 +234,7 @@ void time_sync_task(void *argument) {
|
||||
net_close(ls); /* 网络重建:旧监听失效,回外层重建 */
|
||||
break;
|
||||
}
|
||||
task_wd_beat(TASK_WD_TIMESYNC);
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
time_sync_delay_fed(200U);
|
||||
}
|
||||
} while (conn < 0);
|
||||
|
||||
@@ -211,7 +246,6 @@ void time_sync_task(void *argument) {
|
||||
time_sync_handle_conn(conn);
|
||||
/* conn(单连接模式下即 ls)已被 net_close 释放,下次循环重建监听 */
|
||||
|
||||
task_wd_beat(TASK_WD_TIMESYNC);
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
time_sync_delay_fed(50U);
|
||||
}
|
||||
}
|
||||
|
||||
+25
-1
@@ -15,6 +15,10 @@
|
||||
* v1.0 2026-09-25 王建锋 自 Src/freertos.c 迁出 StartDefaultTask 任务体及
|
||||
* stack_probe_poll / storage_bring_up / 保存通知回调
|
||||
* (CubeMX 生成文件只留任务创建接线与崩溃钩子)
|
||||
* v1.1 2026-10-02 王建锋 存储 bring-up 首行创建卷锁(vol_lock_init,幂等):
|
||||
* FTP 文件操作与 syslog/配置的卷访问自此串行化
|
||||
* (2026-10-02 台架:并发 FTL 写 → journal 竞争 →
|
||||
* 本任务主循环喂狗点饥饿 → IWDG 误复位)
|
||||
*/
|
||||
|
||||
#include "default_task.h"
|
||||
@@ -31,6 +35,7 @@
|
||||
#include "sys_events.h"
|
||||
#include "dbg_log.h"
|
||||
#include "task_watchdog.h"
|
||||
#include "vol_lock.h"
|
||||
|
||||
/* 任务句柄与属性块(CubeMX 生成全局,定义于 Src/freertos.c):
|
||||
* 栈水位观测需 6 任务句柄 + 属性块 stack_size(配置栈大小的唯一定义处) */
|
||||
@@ -96,6 +101,8 @@ static FRESULT storage_bring_up(FATFS *fs);
|
||||
void default_task_func(void *argument) {
|
||||
(void)argument;
|
||||
|
||||
task_wd_bind(TASK_WD_DEFAULT); /* FTL 心跳路由:登记本任务句柄(入口一次) */
|
||||
|
||||
/* 系统启动期挂载 NAND 上的 FatFS 卷,供 FTP 及其它模块访问。
|
||||
* f_mount(forced=1) 会触发 disk_initialize() → nand_ftl_init()
|
||||
* (dhara_map_resume)。此前默认构建从未挂载,导致 FTP 的
|
||||
@@ -159,26 +166,34 @@ void default_task_func(void *argument) {
|
||||
task_wd_start();
|
||||
|
||||
for (;;) {
|
||||
/* round 8 取证:进段打点——每步前记录段号到 .noinit 槽(复位不丢)。
|
||||
* 误复位后开机 `PREVRUN seg=N` 即"上一轮主循环死在哪个调用点"
|
||||
* (round 7 结论 = poller 自身停摆,见 实机测试规划.md §0.5.4)。 */
|
||||
task_wd_seg(TASK_WD_SEG_POLL);
|
||||
/* 心跳抽样与喂狗判定(内部节流至 100ms;全员到位才喂,否则累计停喂) */
|
||||
task_wd_poll();
|
||||
/* 本任务自己的心跳 */
|
||||
task_wd_beat(TASK_WD_DEFAULT);
|
||||
/* 等待"保存网络配置"或"控制台待发":任一即醒;无事件 50ms 后返回
|
||||
* (日志时延 = 事件唤醒,实测无丢失;50ms 兜底仅防事件位异常) */
|
||||
task_wd_seg(TASK_WD_SEG_EVTWAIT);
|
||||
if (sys_evtHandle != NULL) {
|
||||
(void)osEventFlagsWait(sys_evtHandle,
|
||||
SYS_EVT_CFG_SAVE | SYS_EVT_CONSOLE_TX,
|
||||
osFlagsWaitAny, 50u);
|
||||
}
|
||||
/* 控制台唯一发送点:把环形缓冲中的日志按块发出(无待发时立即返回) */
|
||||
task_wd_seg(TASK_WD_SEG_DRAIN);
|
||||
dbg_console_drain();
|
||||
/* 执行待处理保存(配置引擎属主:写盘 + 成功后立即应用);无请求 = 空操作 */
|
||||
#if !defined(TEST_SUITE_STORAGE)
|
||||
task_wd_seg(TASK_WD_SEG_CFGSAVE);
|
||||
net_cfg_save_run();
|
||||
#endif
|
||||
/* 配置文件变更轮询(2s 周期;沉降去抖在引擎内) */
|
||||
if ((cfg_poll_cnt++ % 40u) == 0u) {
|
||||
#if !defined(TEST_SUITE_STORAGE)
|
||||
task_wd_seg(TASK_WD_SEG_CFGPOLL);
|
||||
/* E-31 自愈:启动期挂载失败时,每 2s 重跑完整 bring-up——
|
||||
* 瞬态毛刺(如 dhara resume 扫描的瞬时 SPI 故障)通常第二次
|
||||
* 即恢复;成功后由 storage_bring_up 补跑启动时跳过的
|
||||
@@ -201,6 +216,7 @@ void default_task_func(void *argument) {
|
||||
}
|
||||
/* LED 心跳:500ms 翻转一次(与旧 500ms 循环节奏等效;循环基准改 50ms 后
|
||||
* 不能再每轮翻转,否则变成 10Hz 闪烁) */
|
||||
task_wd_seg(TASK_WD_SEG_LED);
|
||||
if ((HAL_GetTick() - led_tick) >= 500u) {
|
||||
led_tick = HAL_GetTick();
|
||||
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
|
||||
@@ -209,9 +225,11 @@ void default_task_func(void *argument) {
|
||||
}
|
||||
#if !defined(TEST_SUITE_STORAGE)
|
||||
/* 排空运行期日志(上电/重启分类与自检故障落盘) */
|
||||
task_wd_seg(TASK_WD_SEG_SYSLOG);
|
||||
syslog_drain();
|
||||
#endif
|
||||
/* 栈水位观测:首次 5s 后报告,此后每 2s 采样、出现更深峰值才打印 */
|
||||
task_wd_seg(TASK_WD_SEG_STACK);
|
||||
stack_probe_poll();
|
||||
}
|
||||
}
|
||||
@@ -322,7 +340,13 @@ static void stack_probe_poll(void) {
|
||||
*/
|
||||
#if !defined(TEST_SUITE_STORAGE)
|
||||
static FRESULT storage_bring_up(FATFS *fs) {
|
||||
FRESULT fr = f_mount(fs, "", 1);
|
||||
FRESULT fr;
|
||||
|
||||
/* 卷锁创建(幂等):必须早于本卷任何跨任务使用者——ftpTask 的 lftpd 文件
|
||||
* 操作受 STORAGE_READY 屏障门控,屏障在本函数末尾才置位,时序天然安全 */
|
||||
(void)vol_lock_init();
|
||||
|
||||
fr = f_mount(fs, "", 1);
|
||||
|
||||
if (fr == FR_NO_FILESYSTEM) {
|
||||
/* 卷为空(如恢复/坏块重建后清空):自动格式化建卷
|
||||
|
||||
+20
-3
@@ -165,6 +165,8 @@ int net_task_init(void) {
|
||||
void net_task_func(void const *arg) {
|
||||
(void)arg;
|
||||
|
||||
task_wd_bind(TASK_WD_NET); /* FTL 心跳路由:登记本任务句柄(入口一次) */
|
||||
|
||||
/* 硬件层测试模式:testTask 独占 CH395F,本任务自挂起(基础线程恒创建,
|
||||
* 该模式下"不运行"):不 bring_up、不 net_poll、不广播 NET_READY;
|
||||
* ftpTask/timeSyncTask 将永久阻塞于屏障 = 预期行为(见《项目启动顺序管理》) */
|
||||
@@ -181,9 +183,24 @@ void net_task_func(void const *arg) {
|
||||
/* 配置在 NAND 的 FatFS 卷上:等存储就绪再读(取代原 2s 魔法延时);
|
||||
* 超时(存储异常)继续以工厂默认组网,不卡网络启动 */
|
||||
if (sys_evtHandle != NULL) {
|
||||
/* osFlagsNoClear:就绪位是状态,不能被本任务消费(ftpTask 等仍要 WaitAll 两位) */
|
||||
(void)osEventFlagsWait(sys_evtHandle, SYS_EVT_STORAGE_READY,
|
||||
osFlagsWaitAny | osFlagsNoClear, 10000u);
|
||||
/* osFlagsNoClear:就绪位是状态,不能被本任务消费(ftpTask 等仍要 WaitAll 两位)。
|
||||
* 2026-10-02 台架修复:10s 超时等待改 100ms 分片 + 每片喂心跳——原实现
|
||||
* 在等待期无心跳:若存储 bring-up 失败,本等待跑满 10s 超时,而 IWDG
|
||||
* 此时已启动(defaultTask 在 bring-up 后即启动),NET 位长期缺失 →
|
||||
* 必需掩码不可达 → 复位循环,E-31 的 2s 自愈重试根本没有机会。
|
||||
* 分片喂位保证"存储未就绪"降级态下 NET 位持续新鲜。 */
|
||||
uint32_t rounds_left = 100u; /* 100 × 100ms = 原 10s 超时语义 */
|
||||
for (;;) {
|
||||
uint32_t r = osEventFlagsWait(sys_evtHandle, SYS_EVT_STORAGE_READY,
|
||||
osFlagsWaitAny | osFlagsNoClear, 100u);
|
||||
if ((r & osFlagsError) == 0u) {
|
||||
break; /* 存储就绪 */
|
||||
}
|
||||
task_wd_beat(TASK_WD_NET); /* 看门狗:分片喂心跳(屏障等待态) */
|
||||
if ((--rounds_left) == 0u) {
|
||||
break; /* 10s 超时:工厂默认组网,不阻塞网络启动 */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (net_stack_bring_up() != 0) {
|
||||
|
||||
+15
-3
@@ -47,9 +47,19 @@ static void ui_task_time_cb(char *buf, size_t len) {
|
||||
*/
|
||||
static void ui_task_wait_net_ready(void) {
|
||||
if (sys_evtHandle != NULL) {
|
||||
/* osFlagsNoClear:就绪位是状态,不能被本任务消费掉(否则其它等待者永不唤醒) */
|
||||
osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY,
|
||||
osFlagsWaitAny | osFlagsNoClear, osWaitForever);
|
||||
/* osFlagsNoClear:就绪位是状态,不能被本任务消费掉(否则其它等待者永不唤醒)。
|
||||
* 2026-10-02 台架修复:等待改 100ms 分片 + 每片喂心跳(与 timeSync/netTask/
|
||||
* ftpTask 入口同模式)——IWDG 必需掩码含 UI 位,原 osWaitForever 无心跳:
|
||||
* IWDG 启动(存储就绪)时网络未必就绪 → 掩码 800ms 不可达 → 停喂 → 复位循环。
|
||||
* 100ms 分片 = 抽样窗,纯任务睡眠零队列开销。 */
|
||||
for (;;) {
|
||||
uint32_t r = osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY,
|
||||
osFlagsWaitAny | osFlagsNoClear, 100u);
|
||||
if ((r & osFlagsError) == 0u) {
|
||||
break; /* NET_READY 已置位(状态位,未被消费) */
|
||||
}
|
||||
task_wd_beat(TASK_WD_UI); /* 看门狗:分片喂心跳(屏障等待态) */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +74,8 @@ static void ui_task_wait_net_ready(void) {
|
||||
void ui_task_func(void *argument) {
|
||||
(void)argument;
|
||||
|
||||
task_wd_bind(TASK_WD_UI); /* FTL 心跳路由:登记本任务句柄(入口一次) */
|
||||
|
||||
/* 等待网络就绪期间不可能喂狗:本任务此时尚未进入主循环,而就绪位由 netTask 置位。
|
||||
* 该段若长期不成立,看门狗会复位——这是期望行为(网络 bring-up 卡死须暴露)。
|
||||
* 注意本任务在 required_mask 内,主循环跑起来后须保证每 100ms 窗口内至少喂一次。 */
|
||||
|
||||
+445
-17
@@ -8,6 +8,27 @@
|
||||
* 创建日期: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 为何不用"每任务各自喂狗"(那只检测全系统同时挂死)
|
||||
@@ -30,6 +51,15 @@
|
||||
#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"
|
||||
|
||||
@@ -45,10 +75,24 @@
|
||||
|
||||
/* IWDG 超时参数:PR=4 → 64 分频;RLR=1249
|
||||
* 标称 64 × 1249 / 32000 ≈ 2498 ms
|
||||
* 最坏 64 × 1249 / 47000 ≈ 1701 ms(LSI 偏快时超时变短,按最坏校核) */
|
||||
* 最坏 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 ---------------------------------------------------------*/
|
||||
@@ -63,10 +107,61 @@ 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 ---------------------------------------------------------*/
|
||||
|
||||
/*
|
||||
@@ -75,11 +170,19 @@ static uint32_t wd_required_mask(void);
|
||||
* 返回值:本次构建要求"必须动过"的任务位集合
|
||||
* 限定条件:无
|
||||
* 函数说明:按 §4.1/§4.2 的设计,监视范围随构建配置变化:
|
||||
* - testTask 仅在启用测试套件时创建
|
||||
* - netTask 在 ENABLE_HW_LAYER_TESTS 下入口即自挂起(不干活),
|
||||
* 不要求其心跳,否则该构建永不喂狗
|
||||
* 注:ftpTask 当前统一纳入(其主循环为 lftpd 事件循环,需在
|
||||
* lftpd 侧埋点;若某构建下 ftpTask 不创建,句柄检查兜底)。
|
||||
* - 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)
|
||||
@@ -88,14 +191,188 @@ static uint32_t wd_required_mask(void) {
|
||||
| (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);
|
||||
/* 见函数说明: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 ----------------------------------------------------------*/
|
||||
|
||||
/*
|
||||
@@ -116,10 +393,13 @@ void task_wd_beat(task_wd_id_t id) {
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:仅 defaultTask 调用;须在 osKernelStart 之后(本函数只用 HAL/寄存器)
|
||||
* 函数说明:MX_IWDG_Init() 已把参数填入全局 hiwdg(Prescaler/Reload 由 CubeMX
|
||||
* 在 Src/iwdg.c 生成区维护),但不启动(见该文件 IWDG_DEFER_START 注释)。
|
||||
* 此处才真正写 IWDG_Key 启动——使"启动段不喂狗"成立(§4.5)。
|
||||
* IWDG 一旦启动不可停,只能靠复位终止。
|
||||
* 函数说明: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) {
|
||||
@@ -164,6 +444,12 @@ void task_wd_poll(void) {
|
||||
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(); /* 全员健康:喂狗 */
|
||||
@@ -176,6 +462,21 @@ void task_wd_poll(void) {
|
||||
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(不依赖内核、不遍历指针、有界长度)。 */
|
||||
@@ -183,6 +484,11 @@ void task_wd_poll(void) {
|
||||
(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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,20 +530,57 @@ uint32_t task_wd_is_armed(void) {
|
||||
* 函数功能:重装 IWDG 计数器(喂狗)——MCU 端默认实现
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 函数说明:写 IWDG->KR = 0xAAAA。100ms 级高频路径,故不经 HAL 封装。
|
||||
* 函数说明:写 IWDG->KR = WD_IWDG_KEY_FEED(0xAAAA)。100ms 级高频路径,
|
||||
* 故不经 HAL 封装。
|
||||
*/
|
||||
__weak void wd_platform_feed(void) {
|
||||
IWDG->KR = 0x0000AAAAu; /* 重装计数器(喂狗) */
|
||||
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 端默认实现
|
||||
* 函数功能:按设计参数配置并启动 IWDG——MCU 端默认实现
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 函数说明:写 IWDG->KR = 0xCCCC(不可逆,之后只能靠复位停止)。
|
||||
* 函数说明:解锁 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) {
|
||||
IWDG->KR = 0x0000CCCCu; /* 启动看门狗(不可逆) */
|
||||
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; /* 启动看门狗(不可逆) */
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -250,6 +593,91 @@ __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) --------------*/
|
||||
|
||||
+121
-2
@@ -42,6 +42,18 @@ typedef enum {
|
||||
*/
|
||||
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(**延后启动**)
|
||||
* 入口参数:无
|
||||
@@ -69,6 +81,85 @@ 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 取证:复位不丢的"进段打点" ====================
|
||||
* 背景(实机测试规划.md §0.5.4 round 7 结论):误复位时判定分支(STALL)从未
|
||||
* 执行 → **喂狗点 task_wd_poll 自身停摆**(假设 2:poller 被阻塞),而非某个被
|
||||
* 监视任务饿死。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=<ms>`(裸 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() 上报。
|
||||
* 背景(实机测试规划.md §0.5.4.3 第 2 条):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);
|
||||
|
||||
/*
|
||||
* ============================ 平台后端(内部) ============================
|
||||
* 把判定状态机中的硬件访问收敛到三个薄钩子,使"取走清零 / 掩码判定 / 缺失
|
||||
@@ -94,9 +185,15 @@ uint32_t task_wd_is_armed(void); /* 1 = IWDG 已启动 */
|
||||
void wd_platform_feed(void);
|
||||
|
||||
/*
|
||||
* 函数功能:启动 IWDG(不可逆,启动后只能靠复位停止)
|
||||
* 函数功能:按设计参数配置并启动 IWDG(不可逆,启动后只能靠复位停止)
|
||||
* 返回值:无
|
||||
* 函数说明:MCU 默认实现 = `IWDG->KR = 0x0000CCCCu;`
|
||||
* 函数说明: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);
|
||||
|
||||
@@ -108,6 +205,28 @@ void wd_platform_start(void);
|
||||
*/
|
||||
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);
|
||||
|
||||
/*
|
||||
* 函数功能:复位判定状态机(**仅主机侧单元测试使用**)
|
||||
* 返回值:无
|
||||
|
||||
Reference in New Issue
Block a user