/* * 模块名称: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) * v1.4 2026-11-04 DTU 项目组 SNTP 客户端接入(#57 调研落地):accept 轮询 * 片内驱动 time_sntp_run()(48B UDP 请求/应答状态机 * 在 App/sync/time_sntp.c,节奏/退避/步进策略见该文件); * 本任务循环结构与 8888 语义不变 */ #include #include "time_sync.h" #include "time_sntp.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; } /* SNTP 客户端节拍:accept 轮询片内驱动(等待应答期单次 * 驻留 ≤50ms 收片,心跳仍由下面的分片延时喂,100ms 抽样窗 * 不破)。代次变化由 time_sntp_run 内部自理(弃旧 socket)。 */ time_sntp_run(gen); 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); } }