Files
STM32F4-Base/App/sync/time_sync.c
T
wandering1 7feeea6c56 实现独立看门狗:CubeMX 配置 IWDG + 任务心跳位图,恢复三处被覆盖定制
经 CubeMX 图形界面启用 IWDG(Prescaler=64 / Reload=1249,标称 2498ms /
最坏 1701ms),并落地心跳位图判定、各任务埋点、RCC_CSR 复位原因上报与
configASSERT 改造。重生成覆盖的既有定制已全部恢复。

P0(开机死循环):HAL_IWDG_Init() 内部立即启动看门狗,而 CubeMX 把
MX_IWDG_Init() 插在 app_main_init() 之前——该段 NAND/RTC/ADC 初始化耗时
数秒无喂狗点,按 2.5s 超时必然复位。改 Src/iwdg.c 为只填参数不启动,
启动移交 task_wd_start(),由 defaultTask 在存储 bring-up 完成后调用。

恢复的三处定制(重生成覆盖):
- Src/stm32f4xx_it.c:HardFault_Handler 崩溃现场采集被打回空 while(1)
- Inc/FreeRTOSConfig.h:栈溢出检测与堆失败钩子两个宏被整块删除
- Src/freertos.c:三处任务栈实测记录中文注释丢失(数值未丢)

新增:
- App/util/task_watchdog.{c,h}:心跳位图 + 抽样判定 + IWDG 延后启动
- time_sync.c 长等待切片:net_recv_to 单次可睡满 30s 预算,切成 200ms 片
- Src/main.c 复位原因锁存(读后即清 RMVF,防残留误判)

埋点:defaultTask(唯一喂狗者)/ netTask(含 PHY 等待循环)/ uiTask /
timeSyncTask。ftpTask 首期剔除掩码,待二期改 lftpd。

门禁:build 0 错误 0 警告;pre-commit 10 钩子全通过;Flash +2956B / RAM +40B。
2026-09-29 07:22:04 +08:00

218 lines
9.5 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:Time Sync — PC 本地时间推送接收(TCP)
* 模块功能:在独立任务 timeSyncTask 中以 TCP Server 方式监听端口,接收 PC 推送的本地时间包,
* 调用 sys_clock_set_unix() 更新系统墙钟并写回 SD2506 RTC
* 适用平台:STM32F407ZGTx + CH395F
* 作者:王建锋
* 创建日期:2026-08-27
* 修改记录:
* v1.0 2026-08-27 王建锋 创建初始版本(UDP 推送,netTask 内轮询)
* v1.1 2026-08-28 王建锋 改为 TCP Server,迁入独立任务 timeSyncTask,
* 所有 socket 操作经 net_socket 线程安全 API 走消息队列,
* netTask 仅负责 net_poll/net_process_messages,多任务并发安全
*/
#include <string.h>
#include "time_sync.h"
#include "net_socket.h"
#include "sys_clock.h"
#include "sys_events.h"
#include "task_watchdog.h"
#define DBG_TAG "[TIME_SYNC]"
#include "dbg_log.h"
/* 监听端口:PC 推送端(TCP 客户端)需连到本端口 */
#define TIME_SYNC_PORT 8888
/*
* E-02 头阻塞修复 + L3 pending 化(timeSync 侧;lftpd 侧见 lftpd_inet.c):
* 旧实现用单次 30s 阻塞 net_recv(默认 NET_RECV_TIMEOUT_MS)等时间包——等待期间
* netTask(全部 socket 操作的唯一执行者)被独占,其余任务的网络操作(远端显示 /
* FTP)整体停摆(docs/问题记录/错误码传播审计.md E-02)。
* 2026-09-22 先落地分片有界读(5ms 片),同日 L3 pending 化后改为"挂起请求":
* 每次等待 = 一次挂起登记(netTask 零占用,完成按主循环节拍检查),调用方
* 单次超时 = 剩余总预算(总等待 ≤ TIME_SYNC_TOTAL_BUDGET_MS,语义与旧 30s
* 一致);等待期间调用方在自己上下文睡任务通知,旧分片协议(片长/片间让出)退役。
*/
#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
/*
* 函数功能:创建并监听 TCP 时间同步服务端口
* 入口参数:无
* 返回值:监听 Socket 描述符(>=0),失败返回 -1
* 限定条件:net_init() 已成功;可在非 netTask 上下文调用
* 函数说明:net_socket/net_bind/net_listen 均为线程安全 API,内部经消息队列由 netTask 串行处理。
* 单连接模式下监听 Socket 在收到连接后会转为数据通道,连接关闭后即被释放,
* 因此每次调用都会新建监听 Socket(与 lftpd 同模式),失败自动重试。
*/
static int time_sync_open_listener(void) {
int retry;
for (retry = 0; retry < 5; retry++) {
task_wd_beat(TASK_WD_TIMESYNC); /* 每次重试前喂心跳(见下方 500ms/2000ms 延时) */
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));
continue;
}
struct net_sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = NET_AF_INET;
addr.sin_port = net_htons(TIME_SYNC_PORT);
addr.sin_addr.s_addr = NET_INADDR_ANY;
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));
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));
continue;
}
DBG_INFO("time_sync: TCP listen on :%d (sock=%d)", TIME_SYNC_PORT, s);
return s;
}
return -1;
}
/*
* 函数功能:处理一个已建立的时间同步连接
* 入口参数:conn_sock - 已建立的连接 Socket 描述符(单连接模式下即监听 Socket 自身)
* 返回值:无
* 函数说明:pending 读取 PC 发来的时间包("TIME"+uint32 大端 本地时间),解析后
* 写回 RTC,然后关闭连接。每次等待 = 一次挂起请求(netTask 零占用,
* L3 pending 化),单次超时 = 剩余总预算(≤30s,语义与旧 30s 阻塞一致);
* 跨调用累积,容忍时间包被 TCP 分段送达
*/
static void time_sync_handle_conn(int conn_sock) {
uint8_t buf[16];
int got = 0;
TickType_t t0 = xTaskGetTickCount();
while (got < (int)sizeof(buf)) {
/* 单次预算 = 剩余总预算(L3 pending:等待期 netTask 零占用,调用方
* 在自己上下文睡任务通知,旧片间 vTaskDelay(1) 让出退役)
* ⚠ 看门狗约束:本任务在 required_mask 内,而单次 net_recv_to 可睡满
* 30s 预算 —— 等待期必须周期性喂心跳,否则"对端连上但不发时间包"这种
* 完全正常的场景会被判为任务卡死(见设计说明 §4.4 长耗时白名单)。
* 做法:把长等待切成 WD 窗口内的短片(片长 ≤200ms),每片先喂心跳。 */
TickType_t elapsed = xTaskGetTickCount() - t0;
uint32_t rem = (elapsed >= (TickType_t)pdMS_TO_TICKS(TIME_SYNC_TOTAL_BUDGET_MS)) ? 0U
: (uint32_t)((TickType_t)TIME_SYNC_TOTAL_BUDGET_MS - elapsed);
if (rem == 0U) {
DBG_INFO("time_sync: no TIME packet in 30s, drop conn");
break; /* 总预算耗尽(语义同旧 30s 超时) */
}
task_wd_beat(TASK_WD_TIMESYNC);
if (rem > TIME_SYNC_WAIT_SLICE_MS) {
rem = TIME_SYNC_WAIT_SLICE_MS; /* 分片:等待期保持心跳节奏 */
}
int n = net_recv_to(conn_sock, buf + got, (int)(sizeof(buf) - got), rem);
if (n > 0) {
got += n;
if (got >= 8) {
break; /* 时间包收齐,解析 */
}
continue; /* 包未收齐(分段送达),继续等余下字节 */
}
if (n == 0) {
break; /* 对端关闭(EOF) */
}
if (n == -2) {
continue; /* 本片预算等满无数据 → 下一片重算剩余预算 */
}
break; /* 真错误 */
}
if (got >= 8) {
if (buf[0] == 'T' && buf[1] == 'I' && buf[2] == 'M' && buf[3] == 'E') {
uint32_t epoch = ((uint32_t)buf[4] << 24) |
((uint32_t)buf[5] << 16) |
((uint32_t)buf[6] << 8) |
((uint32_t)buf[7]);
sys_clock_set_unix(epoch);
DBG_INFO("time_sync: wall clock updated (epoch=%lu)", (unsigned long)epoch);
}
}
net_close(conn_sock);
}
/*
* 函数功能:时间同步任务主体(独立 FreeRTOS 任务)
* 入口参数:argument - 未使用
* 返回值:无
* 限定条件:net_init() 已由 netTask 完成;netTask 正在处理消息队列
* 函数说明:等待网络栈就绪后,循环:建监听 -> 等连接 -> 处理 -> 关闭 -> 重建监听。
* 全部 socket 操作经消息队列由 netTask 串行执行,与本任务/其它网络任务并发安全。
*/
void time_sync_task(void *argument) {
(void)argument;
/* 等网络栈真正就绪(取代拍脑袋的 8s 魔法延时),确保 netTask 已开始处理消息;
* osFlagsNoClear:就绪位是状态,不能被本任务消费掉(否则其它等待者永不唤醒) */
if (sys_evtHandle != NULL) {
(void)osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY,
osFlagsWaitAny | osFlagsNoClear, osWaitForever);
}
for (;;) {
/* 心跳:本任务在 required_mask 内。以下每一处可能长于 100ms 抽样窗的
* 阻塞点(监听重试 2s、accept 轮询 200ms/次、收尾 50ms)都就近补心跳。 */
task_wd_beat(TASK_WD_TIMESYNC);
/* 记录本次监听对应的网络代次;代次变化(网络重建)则放弃旧监听重建 */
uint32_t gen = net_get_generation();
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));
continue;
}
/* 等待 PC 连接:net_accept 在无连接时立即返回 -1,循环重试同一监听 Socket */
int conn = -1;
do {
conn = net_accept(ls, NULL, NULL);
if (conn < 0) {
if (net_get_generation() != gen) {
net_close(ls); /* 网络重建:旧监听失效,回外层重建 */
break;
}
task_wd_beat(TASK_WD_TIMESYNC);
vTaskDelay(pdMS_TO_TICKS(200));
}
} while (conn < 0);
if (conn < 0) {
DBG_INFO("time_sync: network re-init, relisten");
continue;
}
time_sync_handle_conn(conn);
/* conn(单连接模式下即 ls)已被 net_close 释放,下次循环重建监听 */
task_wd_beat(TASK_WD_TIMESYNC);
vTaskDelay(pdMS_TO_TICKS(50));
}
}