Files
wandering1 da36f4ba56 refactor(编号): 自愈层号与 timeSync 挂起、CubeMX 三层、采集链层名清号(代码面)
批次六余账之二(代码与脚本面)。四族都是「批次一 / 三c 改了文档、漏了代码」的半改
状态。

- 日志自愈:「L1 文件级自愈」→「文件级自愈」、「L2 错误去重」→「错误去重」(正文
  本来就这么叫,层号纯属多余):App/log/soe_store.c、sys_log_mcu.c 三处、
  test/host/sys_log_mcu 的测试与假件头共 10 处。
- timeSync:「L3 pending 化」→ 挂起请求 / pending 化——这条口径早写在记忆里,
  今天才算全仓一致。App/sync/time_sync.c 3 处。
- CubeMX 防丢失三层优先级:scripts/check_cubemx_survival.py 6 处「由 L3 升级为
  L2」「L2 受保护」「L3 兜底」→ 写进 .ioc / 搬进 USER CODE 区 / 审计兜底;变迁史
  句子按「换号留事实」改写、日期原样保留。
- 采集链旧层名:App/README.md「采集与算法层(L1/L2/L3)」→「采集与算法层」;
  protect_task.c/.h「(L3 板端接线,B5)」→「(板端接线,B5)」。
- phasor 断言文案 19 处:改为 ASCII 的 phasor-in ring(C 字面量必须纯 ASCII)。
  改前先证明这些串无耦合——全仓只有本文件含它们、harness 只看 returncode 与汇总
  标记;改后 phasor 57/57 与基线同数。
- 零行为:剥注释 + 套登记表后与 HEAD 逐字节 / AST 相同;sys_log_mcu 141/141、
  phasor 57/57、watchdog 47/47、freq 46/46 均与动手前基线同数。
- 同一四族的文档面(CubeMX 30、采集链 5、菜单层级 2、双写 14)由下一笔收。
2026-10-28 18:52:37 +08:00

295 lines
15 KiB
C
Raw Permalink 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,多任务并发安全
* v1.3 2026-10-27 DTU 项目组 道G task-50:对时**成功应用**后投一条 quiet
* 操作记录("clock set by 8888")——task-49 盘点盘出
* "104 对时留痕、本端口不留"的同类不对称,Lead 批准补齐;
* 被拒不落痕;epoch 时标语义未裁定,本改动不碰、不补
* 任何时区补偿。
* v1.2 2026-10-02 王建锋 第 8b 轮消抖:等待片长 200ms→50ms,并把各处长
* 等待(500/2000/200ms)统一走 time_sync_delay_fed()
* 按 ≤50ms 分片、每片喂心跳——消除"心跳滑出两个连续
* 100ms 抽样窗"的节拍抖动(第 7 轮台架 snap=0x17)
*/
#include <string.h>
#include "time_sync.h"
#include "net_socket.h"
#include "sys_clock.h"
#include "sys_events.h"
#include "sys_log.h"
#include "task_watchdog.h"
/* 深调用链模块(timeSyncTask 栈仅 2048 B,本任务链 = 建监听 → net_bind → CH395F):
* 日志行缓冲收到 128 B(默认 192),本模块最长行渲染后 < 128 → 不截断 */
#define DBG_LINE_BUF_MAX 128U
#define DBG_TAG "[TIME_SYNC]"
#include "dbg_log.h"
/* 监听端口:PC 推送端(TCP 客户端)需连到本端口 */
#define TIME_SYNC_PORT 8888
/*
* 头阻塞修复 + 挂起请求(pending)化(timeSync 侧;lftpd 侧见 lftpd_inet.c):
* 旧实现用单次 30s 阻塞 net_recv(默认 NET_RECV_TIMEOUT_MS)等时间包——等待期间
* netTask(全部 socket 操作的唯一执行者)被独占,其余任务的网络操作(远端显示 /
* FTP)整体停摆(历史审计遗留问题)。
* 2026-09-22 先落地分片有界读(5ms 片),同日 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)——片长 ≤50ms 时每片前喂的心跳
* 必然落在同一个 100ms 抽样窗内,消除"心跳恰好滑过两个连续窗口"的边界抖动
* (第 7 轮台架稳态 snap=0x17 即抽样窗 vs 心跳节拍的良性错位,第 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 的短片、每片前喂心跳,使心跳必然
* 落在单个抽样窗内——语义等价于一次长延时,仅把"心跳节奏"钉在窗内
* (第 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 时间同步服务端口
* 入口参数:无
* 返回值:监听 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");
time_sync_delay_fed(500U);
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);
time_sync_delay_fed(2000U);
continue;
}
if (net_listen(s) != 0) {
DBG_ERROR("time_sync: listen FAIL");
net_close(s);
time_sync_delay_fed(2000U);
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。**不在此 close**(单连接模式下 conn_sock 即
* 监听 fd,close 连监听一起毁、重建窗 ~0.3-1s;net 层 auto_relisten
* 已把同 fd 收回监听,外层直接续 accept——lftpd 样板)。每次等待 =
* 一次挂起请求(netTask 零占用),单次超时 = 剩余总预算
* (≤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)) {
/* 单次预算 = 剩余总预算(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]);
if (sys_clock_set_unix(epoch) == 0) {
/* 操作留痕(道G task-50):对时改的是装置时钟——与 104 的
* "clock set by 104"(App/task/iec104_serv.c 对时处)同类同口径,
* 补齐 task-49 盘出的不对称。**被拒不投**:失败不得记成"时钟被改过";
* 连续两次成功各投一条——每次对时是独立事件,无合并语义。
* quiet 口:本任务栈 2048 B(见本文件 DBG_LINE_BUF_MAX 段),
* syslog_post 的两个满态留痕分支各内联约 216 B 行缓冲,不选它。
* ⚠ 记录里的时标语义(本端口写钟面、104 写 UTC 的 8 小时差)
* 未裁定——留痕按现有通道原样落,不做任何时区补偿。 */
syslog_post_quiet(SYSLOG_OPER, "clock set by 8888");
}
DBG_INFO("time_sync: wall clock updated (epoch=%lu)", (unsigned long)epoch);
}
} else {
/* 头阻塞收口(docs/网络/网络自愈架构设计.md §5.2 E13):本层放弃会话 → 主动拆除。
* 挂起但 TCP 仍活的客户端(PC 内核自动 ACK keepalive,芯片僵尸判不死)
* 会永久占住单连接监听;EOF 路径 drop 幂等返 0(net 层已 relisten)。
* 模拟器多连接后端 conn≠ls,由 sim 实现按数据 fd 关闭。 */
(void)net_drop_conn(conn_sock);
}
/* 原 net_close(conn_sock) 已删——单连接模式 conn==监听 fd,
* close 毁监听(AGENTS 模块约定:EOF 时应用层不得 close);net 层
* auto_relisten 已把同 fd 收回 LISTENING,外层循环直接续 accept。
* 模拟器多连接后端 conn≠ls,由调用方(外层)负责关数据 fd。 */
}
/*
* 函数功能:时间同步任务主体(独立 FreeRTOS 任务)
* 入口参数:argument - 未使用
* 返回值:无
* 限定条件:net_init() 已由 netTask 完成;netTask 正在处理消息队列
* 函数说明:等待网络栈就绪后,循环:建监听 -> 等连接 -> 处理 -> **同 fd 续
* accept**(会话收尾不 close,监听跨会话常驻,仅网络
* 代次变化时重建)。全部 socket 操作经消息队列由 netTask 串行执行,
* 与本任务/其它网络任务并发安全。
*/
void time_sync_task(void *argument) {
(void)argument;
task_wd_bind(TASK_WD_TIMESYNC); /* FTL 心跳路由:登记本任务句柄(入口一次) */
/* 等网络栈真正就绪(取代拍脑袋的 8s 魔法延时),确保 netTask 已开始处理消息;
* osFlagsNoClear:就绪位是状态,不能被本任务消费掉(否则其它等待者永不唤醒) */
if (sys_evtHandle != NULL) {
/* 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); /* 看门狗:分片喂心跳(屏障等待态) */
}
}
/* 监听 fd 跨会话常驻:仅首次/网络代次变化/被毁时重建 */
int ls = -1;
for (;;) {
/* 心跳:本任务在 required_mask 内。以下每一处可能长于 100ms 抽样窗的
* 阻塞点(监听重试 2s、accept 轮询 200ms/次、收尾 50ms)都就近补心跳。 */
task_wd_beat(TASK_WD_TIMESYNC);
/* 记录本次监听对应的网络代次;代次变化(网络重建)则放弃旧监听重建 */
uint32_t gen = net_get_generation();
if (ls < 0) {
ls = time_sync_open_listener();
if (ls < 0) {
DBG_ERROR("time_sync: open listener FAIL, retry later");
time_sync_delay_fed(2000U);
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); /* 网络重建:旧监听失效,回外层重建 */
ls = -1;
break;
}
time_sync_delay_fed(200U);
}
} while (conn < 0);
if (conn < 0) {
DBG_INFO("time_sync: network re-init, relisten");
continue;
}
time_sync_handle_conn(conn);
/* 会话收尾不 close(单连接模式 conn==ls,close 连监听
* 一起毁);net 层 auto_relisten 已把同 fd 收回 LISTENING,下轮循环
* 直接续 accept。模拟器多连接后端(conn≠ls)才关数据 fd。 */
if (conn != ls) {
net_close(conn);
}
time_sync_delay_fed(50U);
}
}