b8c6a3dab0
BREAKING CHANGE: 删除公开 API net_relisten / net_resync_listen / net_can_accept 与消息类型 NET_MSG_RESYNC_LISTEN / NET_MSG_RELISTEN。 生产调用者为零(仅看门狗文档与 iec104 门控),应用侧无迁移成本; 如有外部消费者须改用对账/pump 自动机制(见下)。 1. 对账①扩展为①②⑤(F-NET-2 缺口,先补缺口再删 API) net_reconcile_tick 新增 bad=5:本层 ESTABLISHED 且芯片 0x05/0x01 (Socket OPEN + TCP LISTEN,静默回收回监听、不发 DISCONNECT) → RESYNC 语义只修本层(state→LISTENING + last_peer_port 转登 + lost_conn_seen 清零 + 取消挂起 recv),**不发任何芯片命令** (纪律同 pump③ U11,run8 实证重下发会 170s 永拒)。 原判据①要求 TCP==CLOSED、②要求本层 LISTENING,均不命中此形态 ——此前生产无人认领(仅手动 API 能修)。 2. 删 net_relisten(net_socket.c 函数 + 消息分派 + 头文件声明) footgun:对健康监听器无条件 open→USE→close 自毁 ~1s(原注释自认); 与 pump(100ms)/D10(30ms) 完全重叠。 3. 删 net_resync_listen(同上;语义并入第 1 条) 4. 删 net_can_accept(net_socket.c + net_socket.h + sim 三处) 只认 ESTABLISHED 的旧门控把"监听器在服役、待 CONNECT"窗口全挡掉, :2404 丢 CONNECT 探针因此从不运行;iec104 门控迁 net_listener_armed。 5. timeSync :8888 会话收尾不再 net_close(同 fd 续 accept,监听跨会话 常驻;原 close→重建每会话一个 Trap 09 沉降窗)。server 脚本注释同步 新契约(PC 侧 2s 宽限后自行关闭)。 6. lftpd 应用层阻塞重试退役:lftpd_inet_listen 5 轮(500ms/2000ms,最坏 10s 阻塞 ftpTask) → 单次 + 失败即 425;cmd_pasv 3×200ms 同删。 就绪屏障已消除 Trap 09 启动形态,客户端自带传输重试语义。 台架实证修正(本提交内二次修复,务必留痕): - net_listener_armed 补齐 **ESTABLISHED 放行**。首版迁移只放行 LISTENING/CLOSED+残留,漏了 ESTABLISHED=单连接模型的"待 accept"电平 → iec104 200ms 任务周期抢不过对账② 的 ~400ms 去抖,对账②先 standalone accept 转 ESTABLISHED 后 armed=0 → 应用整场不 accept,18/18 检查全 FAIL (设备日志证据:sock1 standalone accept 后无 i104 accepted,直至对端 FIN 才由 closed-with-data 救回)。补放行后 18/18 PASS,且耗时 127s→8.3s。 该态 accept 必返 fd,无 RPC 风暴风险。 - 期间顺带验证 M3 无辜:sock1 suspect DISCONNECT on live conn 命中后 defer→对账①→closed-with-data accept 把未 accept 的 RX 数据救回, 正是 M3 设计意图(资产门保护)。 验证(全绿):@build 0/0(Code=209580;含 CH395F 套件+Phase13 全量重编 0/0 的编译验证);@test PASS(回读无差异);iec104 18/18(8.3s); n30 0/50;:21 @0ms 50/50 p50=512;:7003 50/50 p50=517;ftp_regression 11/11;Trap23 RST 5/5 + FIN 5/5;time_sync :8888 连续推送正常(RTC 更新、 无 [ERR])。文档同步:BSD 指南(§3.3/3.6/10.1/10.2/12 + 变更记录)、 函数调用关系、监听生命周期设计说明、IEC104 上行设计说明、模拟器差距 清单、链路延迟测试说明。 门禁说明:doc-links 工作树 junction 伪影按既定口径 SKIP;mem-budget 分支 .map 漂移同前;cppcheck/coding-standard 手跑 Passed。
273 lines
13 KiB
C
273 lines
13 KiB
C
/*
|
||
* 模块名称: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.2 2026-10-02 王建锋 round 8b 消抖:等待片长 200ms→50ms,并把各处长
|
||
* 等待(500/2000/200ms)统一走 time_sync_delay_fed()
|
||
* 按 ≤50ms 分片、每片喂心跳——消除"心跳滑出两个连续
|
||
* 100ms 抽样窗"的节拍抖动(round 7 台架 snap=0x17)
|
||
*/
|
||
|
||
#include <string.h>
|
||
#include "time_sync.h"
|
||
#include "net_socket.h"
|
||
#include "sys_clock.h"
|
||
#include "sys_events.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
|
||
|
||
/*
|
||
* 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)——片长 ≤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 时间同步服务端口
|
||
* 入口参数:无
|
||
* 返回值:监听 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**(N34 阶段1-5:单连接模式下 conn_sock 即
|
||
* 监听 fd,close 连监听一起毁、重建窗 ~0.3-1s;net 层 auto_relisten
|
||
* 已把同 fd 收回监听,外层直接续 accept——lftpd 样板)。每次等待 =
|
||
* 一次挂起请求(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);
|
||
}
|
||
}
|
||
|
||
/* N34 阶段1-5:原 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**(N34 阶段1-5:会话收尾不 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 跨会话常驻(N34 阶段1-5):仅首次/网络代次变化/被毁时重建 */
|
||
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);
|
||
/* N34 阶段1-5:会话收尾不 close(单连接模式 conn==ls,close 连监听
|
||
* 一起毁);net 层 auto_relisten 已把同 fd 收回 LISTENING,下轮循环
|
||
* 直接续 accept。模拟器多连接后端(conn≠ls)才关数据 fd。 */
|
||
if (conn != ls) {
|
||
net_close(conn);
|
||
}
|
||
|
||
time_sync_delay_fed(50U);
|
||
}
|
||
}
|