Files
STM32F4-Base/App/sync/time_sync.c
T
wandering1 2e608118ce feat(对时): 8888 对时补一条操作记录留痕——与 104 对时对称,被拒不落痕
task-49 盘点盘出的唯一供给缺口,task-50 实现(道H 交付)。为什么这算缺陷而不是「维护工装不值得留痕」:
对时改变的是**其后每一条记录**的时标,而 104 侧的对时**已经留痕** —— 同一类行为两种结果,这个不对称
本身就是缺陷。FTP 上传不留痕则维持原状(那是传输动作,与「装置时钟被改了」不同类),本笔
刻意不扩面到它。

实现:`time_sync_handle_conn` 里 `sys_clock_set_unix` 的返回值原先是**裸丢弃**,现改为接住判
0,==0 才
投 `SYSLOG_OPER`(走 quiet 口,netTask 侧纪律,栈成本已在注释写明);**被拒零投**(不把失败记成改过时
钟);无合并锁存(连续两次对时留两条,不折叠);**epoch 语义零触碰、零时区补偿** —— 时基口径仍在用
户手里,本笔不替产品决定。

新用例四组十二断言:成功恰一条 / 被拒不落痕 / 连续两次两条 / 魔术字不符皆零。假件把 rc 做成可脚本,
并专门钉「文案出现次数」以证不合并;`sys_log.h` 用真实头(纯 stdint,走 task_watchdog 先例),零桩漂移面。

**停发口径的第一批**:新断言**不配 TC 号**,改用语义化用例名 + 函数名追溯,注释里标了口径出处 —— 裁定
能不能落地看的就是第一批,这条留在这里给后来者当样例。

证据:time_sync 92/92(存量 82 零回归);@build **0 错 0 警**,Code=235268 RO=26308 RW
=1572 ZI=136924
(与上一笔的差值是 728d6067 合账所致,不是本笔);栈**以表为证**:timeSyncTask 链 1272→1272 B 零增
量,own frame 152 不变,handle_conn 200 / post_quiet 40 / post 264 逐函数帧全等 ——
按今天刚定稿的栈判
据,「零增量」这句话必须是表,不能是论证。id-check --strict 0(零新号零裸形)、乱码/嵌套/BOM 净。

台账:`ram_baseline.py --update` 已记当日行(IRAM1 61.9% / CCM 87.9%)。覆盖率基线**留给 L
ead 重建** ——
上一版基线是在 time_sync 套件编译不过的破树下打的,少算了该套件的覆盖。

台账同步:本笔代码改了体积(Code=235268),RAM 预算台账受管区与文档快照区按 12:46 那次权威构建回填(ram_baselin
e --write 加 check_mem_numbers --write)。代码与数字不同笔,就是下一次 mem 钩子假红的来源——上一笔 d
ocs 提交被 mem-numbers 与 mem-budget 双双拦下,正是这个原因。
2026-10-27 13:49:38 +08:00

295 lines
14 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,多任务并发安全
* 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
/*
* 头阻塞修复 + L3 pending 化(timeSync 侧;lftpd 侧见 lftpd_inet.c):
* 旧实现用单次 30s 阻塞 net_recv(默认 NET_RECV_TIMEOUT_MS)等时间包——等待期间
* netTask(全部 socket 操作的唯一执行者)被独占,其余任务的网络操作(远端显示 /
* FTP)整体停摆(历史审计遗留问题)。
* 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)——片长 ≤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)) {
/* 单次预算 = 剩余总预算(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]);
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);
}
}