diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f8fa2ca7..2f0a500a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -360,19 +360,19 @@ repos: files: ^(App/fs/vol_idle_sync\.(c|h)|App/task/default_task\.c|test/host/vol_idle_sync/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false - # 7g) 时间同步(time_sync)主机侧单元测试(2026-10-10 加) - # 背景:time_sync.c 是 TCP 时间同步协议状态机,也是探针窗口心跳缺口那项看门狗风险的**全部 - # 逻辑贡献者**——"listen 重建 -> 800ms~1s 心跳间隔"由本模块的分片延时/退避/ - # 结算等待组成,板端只能看到"是否复位"这一结果,看不到各段延时是否如设计。本钩子 - # 用 MSVC 离线编译真实源文件(#include 纳入)+ 可脚本化的网络/事件桩(含 longjmp - # 跳出 for(;;) 任务循环),把分片喂狗节奏、监听退避、报文重组(半包/粘连/EOF/真错/ - # 预算耗尽/tick 回绕)、重听门控逐条钉死。 - # files 含被测源与其头、被测的真实看门狗头,以及 harness 本身。 + # 7g) 时间同步(time_sync)主机侧单元测试(2026-10-10 加,2026-11-04 随 :8888 退役收缩) + # 背景::8888 退役后 time_sync.c 只剩 timeSyncTask 载体编排——就绪屏障 + 200ms 校时节拍 + # + SNTP 客户端调用。探针窗口心跳缺口那项看门狗风险的逻辑贡献者(分片延时节奏、屏障 + # 等待态喂心跳)仍在本模块,板端只能看到"是否复位"这一结果,看不到各段延时是否如设计; + # 本钩子用 MSVC 离线编译真实源文件(#include 纳入)+ 可脚本化的事件/代次/SNTP 桩 + # (含 longjmp 跳出 for(;;) 任务循环),把分片喂狗节奏、屏障分片、代次下传逐条钉死。 + # 历史(监听退避、TIME 报文重组、30s 预算、tick 回绕用例)随 :8888 退役删除,见 git 历史。 + # files 含被测源与其头、节拍契约头 time_sntp.h、被测的真实看门狗头,以及 harness 本身。 - id: time-sync-unit-test name: 时间同步单测(主机侧) entry: python test/host/time_sync/run_unit_test.py language: system - files: ^(App/sync/time_sync\.(c|h)|App/util/task_watchdog\.h|test/host/time_sync/.*|test/host/common/unit_test_harness\.py)$ + files: ^(App/sync/time_sync\.(c|h)|App/sync/time_sntp\.h|App/util/task_watchdog\.h|test/host/time_sync/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false # 7h) ADC 采样有效性校验主机侧单元测试(2026-10-11 加) diff --git a/AGENTS.md b/AGENTS.md index 838f6619..f0e5822e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,7 +16,7 @@ - **采集**:TPAFE5160(计量级 8 路,并行总线)+ 片内 ADC1(9 路,DMA)→ TIM3 1 kHz 节拍 + 原子快照 → adcTask → 语义取数与有效性校验 → 全周傅氏相量 → **测频与遥测快照(`meas` 200 ms 双区,104 与 UI 共用同一份上游)**+ 保护判据(protectTask 20 ms 判据节拍)→ 104 遥信/遥测上送 - **存储**:GD5F SPI NAND → dhara FTL → FatFs → `/log` 运行/自检等七本记录 + SOE、`/config.ini` 配置、FTP 根目录 -- **网络**:CH395F → netTask 串行 socket 栈 → FTP(21) / 对时(8888) / RemoDispBus 远程显示(7003) / IEC 60870-5-104 上行(工厂默认 2404) +- **网络**:CH395F → netTask 串行 socket 栈 → FTP(21) / RemoDispBus 远程显示(7003) / IEC 60870-5-104 上行(工厂默认 2404);对时 = timeSyncTask 的 SNTP 客户端(UDP 123 主动出网,:8888 推送监听已退役) - **人机**:LVGL 160×160 单色菜单(uiTask),**与 PC 模拟器同一份 UI 源码**,远端可看图 + 注键 **目录地图**(只列顶层——稳定边界;细结构自己 `ls`,别背路径;**仓库根不放一次性产物**——提交信息草稿 / 临时改写脚本 / 门禁输出与日志一律进 `ci-artifacts/`(已忽略,口径见 `.gitignore` 尾注),根目录只留工程配置与两份生成器输入 `ui_menu_tree.txt`、`ui_cn_chars.txt`): diff --git a/App/protocol/iec104/iec104_comm.h b/App/protocol/iec104/iec104_comm.h index f6cd8084..3bc24b73 100644 --- a/App/protocol/iec104/iec104_comm.h +++ b/App/protocol/iec104/iec104_comm.h @@ -48,7 +48,7 @@ extern "C" { * 同款先例): * ca 1..65535:0 为非法公共地址(104 半字地址域,点表无 0 号站) * port 1024..65535:拒绝把监听端口配到 1..1023 系统保留段,避免误配顶掉 - * 系统服务;与 8888/7003 带内相撞属配置者责任(启动锚点 + * 系统服务;与 7003 带内相撞属配置者责任(启动锚点 * "i104: listening on %u" 打印实际端口,可核) */ #define IEC104_COMM_CA_MIN 1u #define IEC104_COMM_CA_MAX 65535u diff --git a/App/sync/README.md b/App/sync/README.md index 7f533950..b8f18d57 100644 --- a/App/sync/README.md +++ b/App/sync/README.md @@ -7,18 +7,18 @@ | 文件 | 作用 | |---|---| -| `time_sync.c/h` | **时间同步任务**:等待网络就绪 → 与时间源交互 → 校准墙钟 | -| `time_sntp.c/h` | **SNTP 校时客户端**(2026-11-04 落地):挂在 time_sync 主循环里的 UDP/123 轮询,编解码用 `Lib/coreSNTP/` 序列化器,配置读 `[sntp]` 段(属主 `../../Drivers/BSP/NETCFG/sntp_cfg.c`);单测 `test/host/sntp/` | +| `time_sync.c/h` | **时间同步任务载体**:等待网络就绪 → 200 ms 节拍驱动 SNTP 客户端(:8888 私有推送已于 2026-11-04 退役,理由见墙钟维护说明 §15) | +| `time_sntp.c/h` | **SNTP 校时客户端**(2026-11-04 落地):由 time_sync 任务循环驱动的 UDP/123 轮询,编解码用 `Lib/coreSNTP/` 序列化器,配置读 `[sntp]` 段(属主 `../../Drivers/BSP/NETCFG/sntp_cfg.c`);单测 `test/host/sntp/` | ## 工作流 ``` timeSyncTask 启动 ↓ osEventFlagsWait(SYS_EVT_NET_READY, ...|osFlagsNoClear) // 状态位不可消费! -time_sync 主循环 → TCP :8888 被动接收("TIME"+u32 → sys_clock 校准) - ↘ 每轮顺带 time_sntp_run(gen):到点主动向 [sntp] 服务器发 48 B 查询 +time_sync 主循环(200 ms 节拍,长等待分片 ≤50ms 逐片喂心跳) + ↘ 每轮 time_sntp_run(gen):到点主动向 [sntp] 服务器发 48 B 查询 (单轮等待切片 ≤50 ms),校验通过按策略步进/只记日志, - 首个通过的同步置 SYS_EVT_TIMESYNC_READY + 首个通过的同步置 SYS_EVT_TIMESYNC_READY;网络代次变化自弃旧 socket ``` > ⚠️ 等待**就绪位**(`SYS_EVT_NET_READY`)必须 `osFlagsNoClear`——它是状态位, @@ -26,6 +26,5 @@ time_sync 主循环 → TCP :8888 被动接收("TIME"+u32 → sys_clock 校准 ## 相关文档 -- `docs/子系统设计/系统墙钟时间维护说明.md` — 墙钟基准 / tick 回绕防护 -- `test/host/time_sync_server.py` — PC 端对时推送服务(测试用) +- `docs/子系统设计/系统墙钟时间维护说明.md` — 墙钟基准 / tick 回绕防护 / §15 :8888 退役注记 / §16 SNTP - `docs/架构设计/项目启动顺序管理.md` — 就绪屏障 diff --git a/App/sync/time_sntp.c b/App/sync/time_sntp.c index 70e6f2e2..93593cb8 100644 --- a/App/sync/time_sntp.c +++ b/App/sync/time_sntp.c @@ -46,7 +46,7 @@ #define SNTP_STEP_DEADBAND_MS 2000 /* 内部钟面 = 本地时间(#56 裁定 §1.1:sys_clock 存"钟面秒",104 写 UTC 的 * 8 小时差已有文档口径)——SNTP 给的是 UTC,落钟面前加固定东八区偏移; - * 全仓无时区存储,本常量与 UI 手工对时 / 8888 推送同源 */ + * 全仓无时区存储,本常量与 UI 手工对时同口径(:8888 推送已退役) */ #define SNTP_TZ_LOCAL_OFFSET_SECS 28800U /* 合理性下限:2020-01-01 UTC;上限 = sys_clock 钟面域 0x7FFFFFFF 扣除时区 */ #define SNTP_UNIX_MIN_VALID 1577836800U @@ -313,7 +313,7 @@ static int32_t s_off_clamp(int64_t off) { * (含死区内未步进),复位退避并按 poll_min 重排下一轮; * 首个成功投 SYS_EVT_TIMESYNC_READY(状态位,不消费); * 每次实际步进落一条 quiet 操作记录("clock set by SNTP", - * 与 8888/104 同口径,失败不落痕) + * 与 104 同口径,失败不落痕) */ static void s_handle_success(const SntpResponseData_t *p_resp, uint32_t now) { int64_t utc; diff --git a/App/sync/time_sync.c b/App/sync/time_sync.c index 19931ed5..497a6edd 100644 --- a/App/sync/time_sync.c +++ b/App/sync/time_sync.c @@ -1,7 +1,7 @@ /* - * 模块名称:Time Sync — PC 本地时间推送接收(TCP) - * 模块功能:在独立任务 timeSyncTask 中以 TCP Server 方式监听端口,接收 PC 推送的本地时间包, - * 调用 sys_clock_set_unix() 更新系统墙钟并写回 SD2506 RTC + * 模块名称:Time Sync — timeSyncTask 载体(网络校时节拍宿主) + * 模块功能:独立任务 timeSyncTask:等网络就绪后按固定节拍驱动 SNTP 校时客户端 + * (App/sync/time_sntp.c),长等待一律分片并逐片喂任务心跳 * 适用平台:STM32F407ZGTx + CH395F * 作者:王建锋 * 创建日期:2026-08-27 @@ -10,58 +10,45 @@ * 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.3 2026-10-27 DTU 项目组 道G task-50:对时**成功应用**后投一条 quiet + * 操作记录("clock set by 8888")——task-49 盘点盘出 + * "104 对时留痕、本端口不留"的同类不对称,Lead 批准补齐 * v1.4 2026-11-04 DTU 项目组 SNTP 客户端接入(#57 调研落地):accept 轮询 * 片内驱动 time_sntp_run()(48B UDP 请求/应答状态机 - * 在 App/sync/time_sntp.c,节奏/退避/步进策略见该文件); - * 本任务循环结构与 8888 语义不变 + * 在 App/sync/time_sntp.c,节奏/退避/步进策略见该文件) + * v2.0 2026-11-04 DTU 项目组 私有 TCP :8888 对时退役(SNTP 上线后退出历史 + * 舞台):监听重建、"TIME" 包重组、OPER 留痕、头阻塞 + * 收口整条链随退——历史协议与退役理由见 + * docs/子系统设计/系统墙钟时间维护说明.md §15 退役注记; + * PC 推送工具 time_sync_server.py 与探针 e02_probe.py + * 同日删除。本任务收缩为纯节拍宿主:NET_READY 屏障 → + * 200ms 循环驱动 time_sntp_run()。心跳分片纪律 + * (v1.2,第 8b 轮消抖)与屏障喂心跳(2026-10-02 + * 台架修复)原样保留。 */ -#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 +/* 主循环节拍:每轮驱动一次 SNTP 状态机后的等待时长,与 :8888 时代的 + * accept 轮询片同值(time_sntp_run 对单次驻留另有 ≤50ms 收片上限, + * 与本延时片叠加不破 100ms 抽样窗,心跳纪律不变)。 */ +#define TIME_SYNC_TICK_MS 200U + /* * 函数功能:可喂心跳的延时(把长等待切成 ≤50ms 短片,每片喂一次心跳) * 入口参数:ms - 总延时(ms) uint32_t @@ -82,147 +69,16 @@ static void time_sync_delay_fed(uint32_t ms) { } } -/* - * 函数功能:创建并监听 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 串行执行, - * 与本任务/其它网络任务并发安全。 + * 函数说明:等待网络栈就绪后,按 TIME_SYNC_TICK_MS 节拍驱动 SNTP 校时客户端 + * (time_sntp_run 内部:UDP socket 懒建/代际变化自弃旧归还, + * 等待应答期单次驻留 ≤50ms)。心跳 = 每轮循环头一次 + 延时片每片 + * 一次;本任务在 IWDG required_mask 内,节拍上没有任何超过抽样窗 + * 的不喂窗口。全部 socket 操作经消息队列由 netTask 串行执行。 */ void time_sync_task(void *argument) { (void)argument; @@ -230,13 +86,12 @@ void time_sync_task(void *argument) { task_wd_bind(TASK_WD_TIMESYNC); /* FTL 心跳路由:登记本任务句柄(入口一次) */ /* 等网络栈真正就绪(取代拍脑袋的 8s 魔法延时),确保 netTask 已开始处理消息; - * osFlagsNoClear:就绪位是状态,不能被本任务消费掉(否则其它等待者永不唤醒) */ + * osFlagsNoClear:就绪位是状态,不能被本任务消费掉(否则其它等待者永不唤醒)。 + * 2026-10-02 台架修复:等待改 100ms 分片 + 每片喂心跳——IWDG 必需掩码含 + * TS 位,原 osWaitForever 无心跳 → 启动初期(存储已就绪、网络未就绪) + * 掩码 800ms 不可达 → 停喂 → 复位循环(2026-10-02 台架根因之一)。 + * 100ms 分片 = 抽样窗,纯任务睡眠零队列开销。 */ 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); @@ -247,57 +102,15 @@ void time_sync_task(void *argument) { } } - /* 监听 fd 跨会话常驻:仅首次/网络代次变化/被毁时重建 */ - int ls = -1; - for (;;) { - /* 心跳:本任务在 required_mask 内。以下每一处可能长于 100ms 抽样窗的 - * 阻塞点(监听重试 2s、accept 轮询 200ms/次、收尾 50ms)都就近补心跳。 */ + /* 心跳:本任务在 required_mask 内。time_sntp_run 的收片驻留 ≤50ms + * 落在两次心跳之间(循环头 + 下面延时片每片前置),抽样窗不破。 */ task_wd_beat(TASK_WD_TIMESYNC); - /* 记录本次监听对应的网络代次;代次变化(网络重建)则放弃旧监听重建 */ - uint32_t gen = net_get_generation(); + /* 网络代次随参数下传:断网/重建由 time_sntp_run 内部自理(关闭旧 + * socket 归还),代次变化后下一轮自然在新网上重新发起。 */ + time_sntp_run(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); + time_sync_delay_fed(TIME_SYNC_TICK_MS); } } diff --git a/App/sync/time_sync.h b/App/sync/time_sync.h index b025f225..20190eda 100644 --- a/App/sync/time_sync.h +++ b/App/sync/time_sync.h @@ -1,13 +1,14 @@ /* - * 模块名称:Time Sync — PC 本地时间推送接收(TCP) - * 模块功能:以 TCP Server 方式监听 TIME_SYNC_PORT,接收 PC 推送的本地时间包, - * 更新系统墙钟并写回 SD2506 RTC + * 模块名称:Time Sync — timeSyncTask 载体(网络校时节拍宿主) + * 模块功能:timeSyncTask 任务入口:等网络就绪后按固定节拍驱动 SNTP 校时客户端 * 适用平台:STM32F407ZGTx + CH395F * 作者:王建锋 * 创建日期:2026-08-27 * 修改记录: * v1.0 2026-08-27 王建锋 创建初始版本(UDP 推送) * v1.1 2026-08-28 王建锋 改为 TCP Server,迁入独立任务 timeSyncTask + * v2.0 2026-11-04 DTU 项目组 TCP :8888 对时退役,TIME_SYNC_PORT 宏随退, + * 任务收缩为 SNTP 节拍宿主(细节见 time_sync.c 文件头) */ #ifndef TIME_SYNC_H @@ -19,16 +20,13 @@ extern "C" { #endif -/* 监听端口:PC 推送端(TCP 客户端)需连到本端口 */ -#define TIME_SYNC_PORT 8888 - /* * 函数功能:时间同步任务主体(独立 FreeRTOS 任务) * 入口参数:argument - FreeRTOS 传入参数(未使用) * 返回值:无 * 限定条件:net_init() 已由 netTask 完成;网络消息队列由 netTask 处理 - * 函数说明:以 TCP Server 方式监听 TIME_SYNC_PORT,接收 PC 推送的本地时间包, - * 经消息队列(由 netTask 串行执行)完成 socket 操作,更新系统墙钟。 + * 函数说明:等网络就绪后按 200ms 节拍驱动 SNTP 校时客户端(App/sync/time_sntp.c), + * socket 操作经消息队列(由 netTask 串行执行)完成。 * 该任务与 FTP 等其它网络任务并发安全。 */ void time_sync_task(void *argument); diff --git a/Drivers/BSP/UI/ui_page_oper.c b/Drivers/BSP/UI/ui_page_oper.c index 45a938f3..2b02c809 100644 --- a/Drivers/BSP/UI/ui_page_oper.c +++ b/Drivers/BSP/UI/ui_page_oper.c @@ -5,8 +5,8 @@ * 注入 reader 取自 sys_log(/log/oper.log,容量上限 512 条,满时删除重建); * 空日志显示单条伪行"无记录";向框架 ui_content_page 提供页面描述符(零 LVGL 对象) * 记录口径(本文件只读显示、不实现写入方;权威表 App/log/README.md §五类记录): - * 触发判据 = 人工/远端操作留痕:定值保存、强制复归、对时(8888 与 104)、清除记录; - * 写入点 = App/task/default_task.c(定值保存)、App/protocol/iec104/iec104_pts.c(复归与整定写入)、App/task/iec104_serv.c 与 App/sync/time_sync.c(对时)、App/log/sys_log_mcu.c(清除记录留痕,OPER 自身除外); + * 触发判据 = 人工/远端操作留痕:定值保存、强制复归、对时(SNTP 与 104)、清除记录; + * 写入点 = App/task/default_task.c(定值保存)、App/protocol/iec104/iec104_pts.c(复归与整定写入)、App/task/iec104_serv.c 与 App/sync/time_sntp.c(对时)、App/log/sys_log_mcu.c(清除记录留痕,OPER 自身除外); * "记录类型 = 日志 ID 本身"(一类记录一个文件,记录体内没有类型字段位, * Lead 裁定 2026-10-26)——本页只读 oper.log 一个文件,不做跨文件混排。 * 台架待补(不在本批,已登记 docs/任务规划/登记簿.md):真 NAND 上真实记录的翻页读取、 diff --git a/README.md b/README.md index 701b277d..4d9c7e1f 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ STM32F4-Base/ │ ├── algo/ # 采集与算法层(语义取数与有效性 adc_input / 全周傅氏相量 phasor+freq / 保护判据 prot_set+prot_elm) │ ├── cfg/ # 配置引擎(cfg_file 解析/持久化/迁移 + 存储后端) │ ├── fs/ # 卷锁(vol_lock,syslog/cfg/lftpd 三条路径 try 语义) -│ ├── sync/ # 时间同步(time_sync 任务) +│ ├── sync/ # 时间同步(time_sync 任务载体 + time_sntp SNTP 客户端) │ ├── log/ # 日志(sys_log 运行/自检记录、console_log 控制台单写者) │ ├── task/ # FreeRTOS 任务实现 │ │ ├── default_task.c/h # 存储挂载 + 配置引擎属主 + syslog 写者 @@ -175,9 +175,7 @@ STM32F4-Base/ │ └── host/ # 主机端脚本(PC 侧;串口回归入口在 .dsh/skills/serial-boot-test/) │ ├── ch395f_socket_test.py # Socket 通信测试 │ ├── ftp_test.py / ftp_up_dl_test.py / ftp_regression.py / ftp_integrity_test.py / ftp_capture.py -│ ├── e02_probe.py # 队头阻塞专项探针 -│ ├── latency_peer.py # 链路时延 PC 对端 -│ └── time_sync_server.py # 时间同步推送服务 +│ └── latency_peer.py # 链路时延 PC 对端 ├── docs/ # 项目文档(全量索引见 docs/README.md) │ ├── README.md # 文档索引:目录结构 + 推荐阅读顺序 + 全量清单 │ ├── 任务规划/ # 统一待办登记簿(README 排序权威 + 拆分文件) @@ -325,7 +323,6 @@ UI 改动回归门禁(菜单像素回归 + 内容页黄金参考 + 池水位 | `ftp_regression.py` | FTP 功能回归(会话流程全序列) | | `ftp_integrity_test.py` | FTP 传输完整性压测(多轮 SHA-256 校验) | | `ftp_capture.py` | FTP 流量抓取 | -| `time_sync_server.py` | 时间同步推送服务(PC → 板子) | ### 串口调试 diff --git a/docs/README.md b/docs/README.md index ed118c51..cb9e0584 100644 --- a/docs/README.md +++ b/docs/README.md @@ -139,7 +139,7 @@ docs/ | [`ADC输入层单元测试说明.md`](测试说明/单元测试/ADC输入层单元测试说明.md) | adc_input 语义通道映射 / ADC1 顺序扫描(**Rank↔通道判据**)/ poll 契约 / HAL 故障路径,70 用例 | | [`相量层单元测试说明.md`](测试说明/单元测试/相量层单元测试说明.md) | phasor 全周傅氏系数表 / 幅值·相位 / 谐波抑制与提取 / 频偏 / 采样环 / 双区交换,57 用例 | | [`卷锁单元测试说明.md`](测试说明/单元测试/卷锁单元测试说明.md) | vol_lock 创建 / 获取 / 释放契约(**fail-open 不变量**:创建失败仍返回 1 且零阻塞)/ 幂等 / 超时透传,33 断言点 | -| [`时间同步单元测试说明.md`](测试说明/单元测试/时间同步单元测试说明.md) | time_sync 分片喂狗延时 / 监听退避 / 时间包重组 / 任务主循环(探针窗口心跳缺口的全部逻辑贡献者),80 断言点 | +| [`时间同步单元测试说明.md`](测试说明/单元测试/时间同步单元测试说明.md) | time_sync v2.0:分片喂狗延时 / 就绪屏障(100 ms 分片·NoClear)/ SNTP 节拍与网络代次交接 / 未出网守门(:8888 退役后的任务载体),33 断言点 | | [`主机单测覆盖率口径.md`](测试说明/单元测试/主机单测覆盖率口径.md) | **跨全部 16 套 主机侧纯逻辑单测的覆盖率**:`scripts/check_test_coverage.py` 用法 / 统计面与生成物例外 / 基线增量门禁(只拦回退)/ **为什么本机拿不到分支覆盖** / 九条限制与盲区 / 未挂 pre-commit 的三条理由与待粘 YAML | **板级测试/**(上板测试:模块测试指南 + 实机实施流程;动手写测试前先读) diff --git a/docs/UI设计/页面补齐规划.md b/docs/UI设计/页面补齐规划.md index edcfff3f..b7b1be1c 100644 --- a/docs/UI设计/页面补齐规划.md +++ b/docs/UI设计/页面补齐规划.md @@ -206,7 +206,7 @@ ui_page_ac / ui_page_yx / ui_page_setlist ... 9. **`ui_w_editor`**(§3.2)→ **定值整定 4 页**(8/9/10/11)。 10. **通讯设置**(29):需先建 `comm_cfg`(`[comm]` 段:104 公共地址/端口/主站地址),照 `net_cfg` 的"RAM 副本 + 保存请求交接"范式。 -11. **SNTP设置**(31):**建议只做页面不接线**,或直接推迟到 SNTP 立项——当前对时走 8888 端口私有协议,SNTP 是另一条链。 +11. **SNTP设置**(31)——已落地(2026-11-04):页面读写 `[sntp]` 段 RAM 副本(`ui_page_sntp.c`,PR 链 #73/#77);原「当前对时走 8888 端口私有协议」口径作废——8888 已退役(#78),SNTP 即对时唯一网络来源。 **退出判据**:就地改一条定值 → 保存 → 掉电重启 → 值保持且校验和自检通过;主站 C_RD 读到同一值。 diff --git a/docs/images/BSD网络栈分层.svg b/docs/images/BSD网络栈分层.svg index 83206708..e01903fb 100644 --- a/docs/images/BSD网络栈分层.svg +++ b/docs/images/BSD网络栈分层.svg @@ -9,8 +9,8 @@ net_send() / net_recv() / net_close()(线程安全封装) net_select() / net_poll_events() / net_set_event_cb() - -消费方:lftpd(FTP :21)· time_sync(对时 :8888)· ui_net_sink(远程显示 :7003) + +消费方:lftpd(FTP :21)· time_sntp(SNTP 出向对时)· ui_net_sink(远程显示 :7003) 消息队列 osMessageQueue · CubeMX 生成,在 MX_FREERTOS_Init() 中创建 diff --git a/docs/images/项目架构总览.svg b/docs/images/项目架构总览.svg index 415506d6..7c945826 100644 --- a/docs/images/项目架构总览.svg +++ b/docs/images/项目架构总览.svg @@ -1,5 +1,5 @@ - - + + STM32F4-Base 项目分层架构 STM32F407ZGTx @168MHz · FreeRTOS V10.3.1 (CMSIS-RTOS2) · 上层只依赖相邻下层 @@ -7,7 +7,7 @@ 顶层共享配置 · 被 UI 与基础服务共用 net_cfg(网口配置:IP/掩码/网关/MAC · /config.ini [network] 段) - + ② UI 层 Drivers/BSP/UI · 一份显存三出(模拟器/屏板/远程) @@ -32,7 +32,7 @@ ui_font_cn_12(中文字体) ui_deco(浅 V 装饰) - + ③ 基础服务层 应用侧服务 / 编排 @@ -55,77 +55,77 @@ cfg_file · prot_set_store(配置引擎) lftpd(FTP :21) - -time_sync(TCP 对时 :8888) - -④ 中间件层 -通用库 / 协议栈 - -net_socket / net_select / net_events / net_listener / net_pending(BSD API + 阻塞模型) - -FatFs(文件系统) - -nand_ftl(dhara FTL) - -LVGL v9.3(图形库) - -sys_clock(墙钟基准) - -task_watchdog(IWDG 判定) - -⑤ 驱动层 -Drivers/BSP · 芯片时序/中断 - -ch395f(SPI2 + DMA) - -gd5f2gq5ue(SPI1) - -tpafe5160(并行 16 位) - -sd2506(I2C1) - -rs485(UART4 + DE) - -sb_link · sb_proto(屏板链路 · USART1 700 kbaud) - -⑥ 内核 / HAL -调度 · 外设抽象 - -FreeRTOS V10.3.1(CMSIS-RTOS2 · heap_4) - -STM32 HAL(时基 TIM7) - -CMSIS / 启动文件 - -⑦ 硬件 -STM32F407ZGTx @168MHz - -CH395F 以太网(SPI2) - -GD5F2GQ5UE NAND(SPI1) - -TPAFE5160 ADC(并行) - -SD2506 RTC(I2C1) - -RS-485(UART4 · 未接线) - -屏板(USART1 · 700 kbaud) - -RS232#1 日志(UART5 · 115200) - -6x LED + +time_sntp(SNTP 对时,UDP 出向;:8888 已退役) + +④ 中间件层 +通用库 / 协议栈 + +net_socket / net_select / net_events / net_listener / net_pending(BSD API + 阻塞模型) + +FatFs(文件系统) + +nand_ftl(dhara FTL) + +LVGL v9.3(图形库) + +sys_clock(墙钟基准) + +task_watchdog(IWDG 判定) + +⑤ 驱动层 +Drivers/BSP · 芯片时序/中断 + +ch395f(SPI2 + DMA) + +gd5f2gq5ue(SPI1) + +tpafe5160(并行 16 位) + +sd2506(I2C1) + +rs485(UART4 + DE) + +sb_link · sb_proto(屏板链路 · USART1 700 kbaud) + +⑥ 内核 / HAL +调度 · 外设抽象 + +FreeRTOS V10.3.1(CMSIS-RTOS2 · heap_4) + +STM32 HAL(时基 TIM7) + +CMSIS / 启动文件 + +⑦ 硬件 +STM32F407ZGTx @168MHz + +CH395F 以太网(SPI2) + +GD5F2GQ5UE NAND(SPI1) + +TPAFE5160 ADC(并行) + +SD2506 RTC(I2C1) + +RS-485(UART4 · 未接线) + +屏板(USART1 · 700 kbaud) + +RS232#1 日志(UART5 · 115200) + +6x LED - - - - - - - - - - -上层只依赖相邻下层(单向);UI 层与基础服务层同级并列,均使用顶层 net_cfg。跨层调用细节见 docs/架构设计/项目架构总览.md §1.2 + + + + + + + + + + +上层只依赖相邻下层(单向);UI 层与基础服务层同级并列,均使用顶层 net_cfg。跨层调用细节见 docs/架构设计/项目架构总览.md §1.2 diff --git a/docs/产品需求/产品需求设计文档.md b/docs/产品需求/产品需求设计文档.md index bc78e0aa..2d618954 100644 --- a/docs/产品需求/产品需求设计文档.md +++ b/docs/产品需求/产品需求设计文档.md @@ -126,7 +126,8 @@ flowchart LR end DEV -- "USB:ST-Link 下载器" --> TGT DEV -- "USB→RS-232 适配器:RS232#1 @115200 调试日志" --> TGT - TGT -- "以太网 CH395F
FTP:21 / 对时:8888 / 远程显示:7003" --> RMT + TGT -- "以太网 CH395F
FTP:21 / 远程显示:7003" --> RMT + TGT -. "对时:SNTP 客户端 UDP 123 出向(:8888 已退役)" .-> NTP["现场/公网 NTP 源"] ``` | 环境 | 角色 | 必需性 | @@ -153,7 +154,7 @@ flowchart LR 1. **开关柜现场部署**:设备上电 → 按 `/config.ini` 出厂 IP 组网 → 主站连工厂默认 :2404 总召 → 现场用屏板看测量、按按键翻菜单;死机由看门狗自恢复(无需到场)。 2. **远程运维**:PC 经 :7003 看同一份 UI、注键操作;FTP 取 `/log`(运行/自检/SOE 记录) - 与 `/config.ini`;改 IP 后 2~5 s 生效不重启;PC 推 :8888 对时。 + 与 `/config.ini`;改 IP 后 2~5 s 生效不重启;SNTP 出向对时(原 PC 推 :8888 已退役)。 3. **脱离 PC 独立带屏运行**:串口屏板直连 USART1(700 kbaud),主机 275 ms 召唤、 ≤200 ms 回 4208 B 帧(3200 B 显存 + 状态灯掩码 + info)——无 PC 也能看与按(§5.16)。 4. **保护运行(等标定解锁)**:1 kHz 采样 → 全周傅氏相量 → 20 ms 判据节拍 → 动作只落 @@ -250,7 +251,7 @@ flowchart LR |---|---|---|---| | P0 | FR-NET 网络通信(§5.1) | ✅ | CH395F + BSD 抽象 + 单连接自愈 | | P0 | FR-STO 存储(§5.2) | ✅ | SPI NAND→dhara→FatFs,卷自愈 | -| P0 | FR-TIME 时间(§5.3) | ✅ | SD2506 墙钟 + :8888 对时(2038 已知限) | +| P0 | FR-TIME 时间(§5.3) | ✅ | SD2506 墙钟 + SNTP 对时(原 :8888 已退役;2038 已知限) | | P0 | FR-CFG 配置引擎(§5.4) | ✅ | INI 段键表 + 外科保存 + 免重启生效 | | P0 | FR-LOG 记录与日志(§5.5) | ✅ | 定长记录 + 控制台单写者 | | P0 | FR-WD 看门狗(§5.7) | ✅ | 心跳位图 + 复位原因 + Fault 现场 | @@ -319,14 +320,15 @@ flowchart LR - 实现:`sys_clock_init()` 读一次 RTC 建基准(无效时按默认值自动校时);base+delta 法;30 天惰性再基准化(防 49.7 天 tick 回绕);`sys_clock_get_str()` → `"YYYY-MM-DD HH:MM:SS"`。 - 判据:日志时间戳真实正确;主机侧单测 100 断言点通过。 -**FR-TIME-2 网络对时** +**FR-TIME-2 网络对时**(已退役 2026-11-04,由 FR-TIME-3 接替) - 需求:PC 主动通过 TCP :8888 推送时间到设备,设备写回 RTC。 -- 实现:`timeSyncTask` 等 `NET_READY` → TCP listen :8888 → 收 PC 推送 → 写 RTC;30 s 无包则掉线重建监听。 -- 判据:对时后设备墙钟更新、日志时间戳正确。 +- 退役:**已由 FR-TIME-3 SNTP 取代并退役**——`:8888` 监听、8 字节 "TIME" 协议、PC 推送器随 + 工单 #78 删除;网络校时唯一入口 = SNTP(FR-TIME-3)。历史实现见 git 历史与 + `docs/子系统设计/系统墙钟时间维护说明.md` §15。 **FR-TIME-3 SNTP 校时** ✅(2026-11-04) - 需求:设备作 SNTP(RFC 4330)客户端从配置的 IPv4 服务器经 UDP/123 取时写回 RTC;使能、主/备服务器、轮询周期可配(UI「SNTP 设置」页或 `/config.ini` `[sntp]` 段,保存后随下一轮生效)。 -- 实现:`timeSyncTask` 复用 8888 事件循环(≤50 ms 等待切片,不饿任务狗与 IWDG);48 字节报文由 vendored coreSNTP 序列化器(`Lib/coreSNTP/`)编解码,节奏与判据在本方;防伪 = OriginTimestamp 须逐位等于发出的 T1(含随机小数位),mode / stratum / 闰秒指示 / Kiss-o'-Death 按 RFC 拒绝;主备轮换 + 失败指数退避(5 s 起、60 s 封顶)+ 首轮 1~5 分钟随机;成功时 |offset| < 2 s 只记日志不阶跃,更大偏差阶跃写回 SD2506——钟面 = 本地时间(UTC+8,与 8888 同一时基口径,取舍见 docs/任务规划/UI补齐.md §4 时基口径注);首个校验通过的同步置 `SYS_EVT_TIMESYNC_READY`。 +- 实现:`timeSyncTask` 200 ms 校时节拍驱动(`time_sync.c` v2.0,:8888 退役后任务即纯宿主;≤50 ms 收片切片,不饿任务狗与 IWDG);48 字节报文由 vendored coreSNTP 序列化器(`Lib/coreSNTP/`)编解码,节奏与判据在本方;防伪 = OriginTimestamp 须逐位等于发出的 T1(含随机小数位),mode / stratum / 闰秒指示 / Kiss-o'-Death 按 RFC 拒绝;主备轮换 + 失败指数退避(5 s 起、60 s 封顶)+ 首轮 1~5 分钟随机;成功时 |offset| < 2 s 只记日志不阶跃,更大偏差阶跃写回 SD2506——钟面 = 本地时间(UTC+8,沿用本地钟面时基口径,取舍见 docs/任务规划/UI补齐.md §4 时基口径注);首个校验通过的同步置 `SYS_EVT_TIMESYNC_READY`。 - 判据:主机单测 `test/host/sntp`(报文格式 / 伪造拒收 / KoD 轮换 / 退避与周期 / 阶跃域守卫);真机 = 使能并配置服务器后时钟与时间源一致、`[SNTP] step offset=` 日志在场(台架窗口统一做)。 > **已知限制**:uint32 epoch → **2038 年**上限。 @@ -1198,7 +1200,7 @@ for (i = 1; i < LED_COUNT; i++) /* LE | 服务 | 协议 / 端口 | 用途 | 核对方法 | |---|---|---|---| | FTP | TCP **21** | 文件上传/下载(根目录 `/`,NAND 存储) | 登录后任意 FTP 客户端列目录/读写(鉴权见 §7.6) | -| 墙钟对时 | TCP **8888** | PC 主动推送时间到设备 | 对时后核对设备时间 / 日志时间戳 | +| 墙钟对时(SNTP 客户端) | UDP 出向 **123**(主/备服务器 `[sntp]` 段,enable=0 出厂默认) | 设备主动取 NTP 源时间写 RTC(:8888 推送监听**已删除**,2026-11-04,§6.3) | 使能后核对设备时间 / 日志时间戳与 `[SNTP] step offset=` 留痕 | | 远程显示(RemoDispBus) | TCP **7003** | 远端看图(160×160 1bpp)+ 远端导航按键 | DTU-RemoteLCD 或任意 TCP 客户端连 7003 收帧 | | IEC 60870-5-104 上行 | TCP **工厂默认 2404** | 被控站接入:遥测/遥信上送、总召、遥控/定值下发(点表见 §6.8) | 任意 104 主站连 2404,STARTDT+总召往返(→ `docs/子系统设计/IEC104上行设计说明.md`) | @@ -1254,11 +1256,13 @@ for (i = 1; i < LED_COUNT; i++) /* LE > 详见 `docs/UI设计/RemoDispBus协议与接收设计说明.md`。MCU 与模拟器**共用同一份** `ui_net_sink.c`。 -### 6.3 对时协议 +### 6.3 对时协议(SNTP 客户端) -- **载体**:TCP :8888,PC 主动推送,设备接收并写 RTC。 -- **行为**:`timeSyncTask` 监听 → 收包 → 写回 RTC;30 s 无包则掉线重建监听(约 4 s)。 -- **待补充**:推送报文的具体帧格式(当前实现细节见 `App/sync/time_sync.c` 与 `test/host/time_sync_server.py`)。 +- **载体**:设备作 **SNTP(RFC 4330)客户端**,向 `[sntp]` 段配置的主/备服务器 UDP/123 出向取时,写回 RTC。 +- **行为**:`timeSyncTask` 200 ms 节拍到点发 48 字节请求,应答经 OriginTimestamp 逐位防伪校验后按阈值步进;主备轮换 + 失败指数退避(5 s 起、60 s 封顶)。 +- **退役**:原 **TCP :8888 PC 推送**(8 字节 `"TIME"` 帧)已于 2026-11-04 退役删除(工单 #78), + 帧格式与实现细节见 git 历史;协议口径与教训沉淀于 `docs/子系统设计/系统墙钟时间维护说明.md` §15/§16。 +- **详规**:报文编解码用 vendored coreSNTP 序列化器(`Lib/coreSNTP/`),判据见 §5.3 FR-TIME-3。 ### 6.4 FTP 服务 @@ -1409,7 +1413,7 @@ mac=00:00:00:00:00:00 | 指标 | 基线值 | 来源 | |---|---|---| | M3 压力档帧周期 | med 60.1 / p95 66.4 / max 69.3 ms(零尖峰) | `docs/测试说明/专项验证/链路延迟测试说明.md` | -| 队列响应性(挂起客户端) | 92.5 ms(pending 一期) | `test/host/e02_probe.py` | +| 队列响应性(挂起客户端) | 92.5 ms(pending 一期) | 台架实测(探针 `e02_probe.py` 已删除——2026-11-04 随 :8888 退役,正文见 git 历史) | | FTP 传输速率 | 约 49 / 60 KB/s(上传/下载) | 128 KB 实测 | | 网络重建生效 | 2~5 s(含等链路 ≤3 s) | 配置设计 | | UI 刷新 | 10 ms 循环 | `ui_task_func` | @@ -1670,7 +1674,7 @@ docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5 | **多规约栈** | 17 | 12 种 | 1 种(104) | +11 种(101 / 102 / 103 / CDT / ModBus / Whm_GB / 61850 / CAN + 子站侧 3 类) | | **国密算法** | 18 | `Comm/Ecc/` 全套 | 0 | SM2 / SM3 / ECDSA | | **软压板铺开** | 11 | 9 面(表容量 32) | 1 面 | +8 面 | -| **SNTP 对时** | 17 | 参考对时走 SNTP | **SNTP 客户端已落地**(2026-11-04:coreSNTP 序列化器 + `[sntp]` 配置 + UI 设置页 + timeSyncTask 接入,判据 §5.3 FR-TIME-3);另有私有 TCP 8888 + 104 `C_CS` | 台架真机对时判据(现场时间源,docs/任务规划/登记簿.md) | +| **SNTP 对时** | 17 | 参考对时走 SNTP | **SNTP 客户端已落地**(2026-11-04:coreSNTP 序列化器 + `[sntp]` 配置 + UI 设置页 + timeSyncTask 接入,判据 §5.3 FR-TIME-3);另有 104 `C_CS`(私有 TCP 8888 已随 #78 退役删除) | 台架真机对时判据(现场时间源,docs/任务规划/登记簿.md) | | **固有参数** | 1 | `8001H~800AH` 字符串类参数 | 未实现(无法短浮点上送) | 随送检点表设计 | | **遥测增强** | 4、5 | k=1~9 谐波方和根 + 4 点平均 | 仅基波轨 | 谐波 / 等周波调频 / 零序自产外接切换 三条算法轨 | | **屏板链路对齐** | 20 | **串口屏板通道**(**700000 8N1 / USART1 TTL 直连、无 DE**,帧 A 6 B 召唤、帧 B **4208 B** 应答:显存 3200 B + **状态灯 4 B 位掩码** + 4 B info)+ **7003 四命令**(INIT **16 B**,单色口径)+ 续传 / 拒绝握手 / 链路监视 / F1·F2 | **串口通道已交付**(4208 B 应答 / 状态灯掩码 / 键上行 / 双出口并发,屏板链路设计说明 §9.1);7003 四命令(INIT **8 B**);无续传、无 `bNeedFlash` 拒绝、无 1 分钟监视、键值 6 个 | 7003 报文口径 6 项(`参考产品复刻明细.md` §11 第 2~9 条);串口侧验收尾巴见屏板链路设计说明 §9.2 | @@ -1754,7 +1758,7 @@ docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5 | 3 | 远程显示 | DTU-RemoteLCD 连 7003 | 出图(160×160 1bpp)+ 远端菜单导航可用 | | 4 | FTP | FTP 客户端连 :21 | 列目录 / 读写 `/` 下文件 | | 5 | UI 功能 | 远端操作 | 菜单 7 项导航、内容页、密码(查看 `000` / 修改 `123`)、网口设置保存后立即生效 | -| 6 | 对时 | PC 推送 8888 | 设备墙钟更新、日志时间戳正确 | +| 6 | 对时 | SNTP 配置服务器对时(原 PC 推送 8888 已退役) | 设备墙钟更新、日志时间戳正确 | | 7 | 掉电保持 | 写文件 / 改网口配置 / 改定值 → 重启 | 数据仍在(`/config.ini`、`/prot_set.bin` 按新值生效) | | 8 | LED 状态 | 目检 | 6 路 LED 随系统状态变化(低电平点亮) | | 9 | 104 总召与变位 | PC 主站连工厂默认 :2404 → STARTDT → 总召 | 四组数据回全、`actterm` 正常;制造变位后 COT=3 上送 | diff --git a/docs/任务规划/README.md b/docs/任务规划/README.md index e5b5823f..7fa21161 100644 --- a/docs/任务规划/README.md +++ b/docs/任务规划/README.md @@ -31,12 +31,12 @@ ## 1. UI 补齐(执行清单已于 2026-10-28 清空) -菜单树共 34 叶(节点行合计 43):按行内 #page= 实测**已绑 30 叶、未绑 4 叶**(2026-10-29 复扫, -判定规则与重跑命令见 docs/任务规划/UI补齐.md §1「叶数判定与复核」)。未绑的 4 叶因缺少开入硬件 -或 SNTP 产品定义而不做(原因与重开条件见 docs/任务规划/UI补齐.md §5)——**手动录波与录波回放两叶 +菜单树共 34 叶(节点行合计 43):按行内 #page= 实测**已绑 31 叶、未绑 3 叶**(2026-11-04 SNTP 设置 +叶随 #73 立项转绑 +1,判定规则与重跑命令见 docs/任务规划/UI补齐.md §1「叶数判定与复核」)。未绑的 3 叶 +因缺少开入硬件不做(原因与重开条件见 docs/任务规划/UI补齐.md §5)——**手动录波与录波回放两叶 已随录波道绑页**,「不做」清单里的录波那一项已销。 -**口径闭合**:未绑 4 = 待做 0(§1 清单已清空)+ 不做 4(遥信 3 / SNTP 1);**43 是节点行数、不是叶数**(= 9 个菜单节点 + 34 叶)。 -已绑叶数以生成物 UI_PAGE_MAP_COUNT 为准:不剔 ui_menu_tree.txt 文件头的注释示例行时,按 #page= 文本计数会得 31(= 30 + 1 行示例)。 +**口径闭合**:未绑 3 = 待做 0(§1 清单已清空)+ 不做 3(遥信 3);**43 是节点行数、不是叶数**(= 9 个菜单节点 + 34 叶)。 +已绑叶数以生成物 UI_PAGE_MAP_COUNT 为准:不剔 ui_menu_tree.txt 文件头的注释示例行时,按 #page= 文本计数会得 32(= 31 + 1 行示例)。 **UI 补齐执行清单已于 2026-10-28 清空**:记录 5 页(事故 / 操作 / 保护告警 / 保护启动 / 遥控)与 运行报告页同日落地并绑页,内容页黄金参考 53 → 60 张(runreport 分 2 页,第 2 页单独拍一张;判据 = simulator/tests/ref/ 入库 shot_*.bin 实数,张数 = 页面个数 + 分页多拍的那一张;**该集合 2026-10-29 已扩至 66 张**,多出的是录波回放页 6 态)。 @@ -64,7 +64,7 @@ docs/测试说明/专项验证/模拟器差距清单.md §2.3「开机账本 boo | **5** | 标准源送检与第三方 104 主站验收 | 等待标定完成 | 两点标定完成且保护 VALID 已置位;送检窗口前 2 日冻结特性 | | **6** | 保护元件与采集增强 | 等待标准源或硬件改版 | 保护先解除未标定闭锁;三相同时刻类功能等 V0.2 硬件 | | **7** | 验证窗口事项 | 逢条件顺带 | CI runner、PC peer、CH340、负向故障注入条件到位时执行 | -| **8** | 独立模块与暂缓项 | 当前不排 | 开入开出、SNTP、104 文件传输单独立项;RS-485 与内存优化按各自重启条件 | +| **8** | 独立模块与暂缓项 | 当前不排 | 开入开出、104 文件传输单独立项(SNTP 已单独立项并于 2026-11-04 落地 #73,:8888 随 #78 退役);RS-485 与内存优化按各自重启条件 | | **独立** | **故障录波(1 kHz · 8 路原始码值 · 单通道回放)** | **代码面已交付;2026-10-31 远程半轮过六项、2026-11-02 回放页枚举缺陷已修复闭环,余注量对表与人到场项** | 用户裁定 2026-10-29 **录波先行**:数据面最小闭环 → 手动与保护启动触发 → 回放页。三块的**代码侧判据已收口**(主机单测 113 / 170 / 110 全 rc=0、双端编译 0 错误 0 警告、板端编码器与 PC 侧解码器字节级互验、手动录波与录波回放两叶已绑页、内容页黄金参考扩至 66 张);**2026-10-31 远程半轮已过六项**(真 FAT 落盘、触发→落盘全部落在 2 s 封存超时窗内、冷却恢复、轮转、PC 侧坏段拒收、冻结窗内 104 并发);同轮发现的缺陷「回放页段表枚举恒空」**已于 2026-11-02 远程修复并板级闭环**(根因 = 读侧 `wave_store_seg_path()` 返回值判错,顺带修键处理缺失效;回放页真段/切段/切道已实测,登记簿条目已销项,证据见设计说明 §10「回放页枚举恒空缺陷修复记录」);**没跑的是注量对表那半轮**(注 1 V / 50 Hz、与 `meas:` 行和 104 浮点遥测对表、回放页放坏段与空态画面)加 P 触发 / bench T 源,全部结果与清单见 `docs/子系统设计/故障录波设计说明.md` §10「板级远程半轮记录」。**板级判据、留痕名清单与执行顺序图的属主 = docs/子系统设计/故障录波设计说明.md §9 与 §10**(原卡片登记簿 2026-10-30 随销项并入后删除);**与 UI 道互斥**(菜单树与中文字库是整文件生成物,同刻只允许一条道写)——本波按此串行执行:UI 道在数据面与接线冻结之后才动生成物 | ### 2.1 关键依赖 diff --git a/docs/任务规划/UI补齐.md b/docs/任务规划/UI补齐.md index d816a185..4fe66d8d 100644 --- a/docs/任务规划/UI补齐.md +++ b/docs/任务规划/UI补齐.md @@ -99,8 +99,11 @@ **单写者约束**:`ui_menu_tree.txt`、页面注册表、字库生成物不得由多个分支同时修改。 `MDK-ARM/*.uvprojx` 的新源文件注册按双端注册规则处理。 -**时基口径未定,勿顺手统一**:面板时钟页与 8888 对时把「本地钟面当 epoch」写入,而 104 `C_CS` 按 CP56Time2a 的 -UTC 写入,后写者赢 ⇒ 装置墙钟与主机本地时刻恒差 8 小时。用户裁定前**不要改动 RTC/epoch 语义**,也不要把 +**时基口径已裁定,勿顺手改语义**:口径 = 「先本地为准,内部真源是本地钟面」(用户裁定 2026-10-27, +`../子系统设计/系统墙钟时间维护说明.md` §1.1);当时冲突双方「面板时钟页 vs 8888 对时 vs 104 `C_CS` 写 UTC」中的 +8888 已于 2026-11-04 退役(写入口现为 SNTP / UI 手工 / 104 `C_CS`,一律先换算成本地钟面再落库)。唯一待决的是 +104 **上行** `CP56Time2a` 是否转 UTC(送检前单独定,前引 +`../子系统设计/系统墙钟时间维护说明.md` §1.1 末)。裁定前**不要改动 RTC/epoch 语义**,也不要把 时钟设置页的「回读 RTC」判据改成按某一时区补偿——那等于替产品做掉一个未定的决定。 **保护闭锁必须可见**:整定页可以修改定值,但当前产品在 UA/UN/IA 未标定时保护判据闭锁。 @@ -115,5 +118,5 @@ UTC 写入,后写者赢 ⇒ 装置墙钟与主机本地时刻恒差 8 小时 |---|---|---| | 遥信类型 / 防抖 / 双点 3 页 | V0.1 无开入硬件,配置没有消费者 | 开入固件立项后,与开入开出一起做 | | net 层与其他结构债 | 当前只做 UI,避免两条主线互相拖慢 | UI 当前清单完成后,按 README §2 切换 | -| RTC 时基统一 | 两套写入口径并存(面板与 8888 写本地钟面、104 写 UTC),恒差 8 小时;统一任一侧都要动 104 上行语义或 UI 判据 | 用户裁定时基方向后,作为跨模块变更单独立项 | +| 104 上行 UTC 换算(原「RTC 时基统一」) | 内部口径已由用户裁定(2026-10-27:本地钟面为真源,各写入口先换算再落库,见 §4 注),「恒差 8 小时」的后写者赢阶段已结束;剩下的只是 104 上行是否按标准转 UTC,动了要改 104 上行语义 | 送检/第三方主站验收方向定下来后,作为跨模块变更单独立项(墙钟说明 §1.1 末) | | 主站地址白名单 | CH395F 单连接 socket 上「拒绝即 close+重 listen」等于给扫描风暴开门,且该值现在没有任何既有消费点 | 有真实消费场景或多连接后端立项时再评 | diff --git a/docs/任务规划/登记簿.md b/docs/任务规划/登记簿.md index c7e57e41..cc4dd1d8 100644 --- a/docs/任务规划/登记簿.md +++ b/docs/任务规划/登记簿.md @@ -42,6 +42,7 @@ | rs485_task 白名单缺口 | **KNOWN_GAPS 白名单清理**——现存唯一缺口 = `App/task/rs485_task.c`(RS-485 接线门禁未满足、刻意不接线,随 RS-485 采集链路 暂缓,见 `暂缓-RS485.md`) | 白名单不清 = "写了不编译"盲区仍在 | 随 RS-485 采集链路 一并出白名单(`--strict` 供审计复跑) | 结构债 | 报告 N 族残余(报告已删) | | 缩堆迁 CCM 后半(暂缓) | **内存优化簇剩余**:原报告缩堆建议的后半(缩堆 56→32 KB → 迁 CCM)· 同类三项 | ⏸ **整簇暂缓**(2026-10-15 用户裁定):没有功能在等这块预算,而搬迁要动 scatter、要逐条过 DMA 可达性清单、缩堆还要上板复测。⚠ 代价落在"以后加任务 / 抬栈 / 加堆"那一刻——CCM 只剩 7,904 B、FreeRTOS 堆 `min_ever` 8,464 B,届时 `mem-budget` 会先报警 | 重开条件见 `../内存管理/RAM使用分析与优化方案.md` §4。⚠ **原报告另有一条已判定不可行**(CMSIS-RTOS2 `freertos_os2.h` 有 `#if (configMAX_PRIORITIES != 56) #error`,改小直接编译失败),不再列入 | **暂缓** | 报告 R 族剩余/§5.5(报告已删) | | 仓库体积膨胀观察 | **仓库膨胀**:`.git` **233 MB**(2026-10-25 实测)+ 手册图片 36.3 MB(受控文件合计仅 79.3 MB,`.git` 已是它的 3 倍) | clone/备份慢,且持续恶化 | 降采样 / 拆 submodule / LFS 三选一 **+ 一次 `git filter-repo` 历史重写**(三项都不解决历史膨胀)。**已决策(2026-10-11):暂不处理、登记观察**——推送/协作前重评 | **仓库决策** | 报告 D 族 | +| 静态链硬线双 FAIL | **timeSyncTask / iec104Task 静态最坏链跌破 ISR 硬线**(判据 = margin ≥ max(1024 B, cfg×30%),静态 margin 是唯一拦提交项但尚未挂 pre-commit 钩子,口径见 `../内存管理/RAM使用分析与优化方案.md` §4.0.7 第③条)——2026-11-04 复测:master 2b848c4a timeSyncTask **2056 B(余 504)**、iec104Task **1600 B(余 960)**,双 FAIL;#78 合入后 timeSyncTask **1904 B(余 656)**,只减不增(−152 B = 旧 8888 任务自身帧)。两链同尾 = `ch395f SPI 故障收尾 → dbg_console_drain 幻影环`(`../内存管理/RAM使用分析与优化方案.md` §4.0.7「休眠环」在 #74 UDP / #77 SNTP 出向链接入后被重新展开) | 抬栈属 Ask-first:先台架复采 `[STACK]` 水位(scripts/stack_gate.py 三档判据),拿真实余量再定抬栈或接受现状;幻影环是 armlink 记账非设备故障,**不得写成优化收益也不得当作已越线事故** | ⏳ 待台架复采 | #78 复测留痕 `docs/内存管理/data/stack_report_20261104.md`;`../内存管理/RAM使用分析与优化方案.md` §2.3 | --- diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index e59499d7..142f1e53 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -47,10 +47,10 @@ dhara 失败类 +84;RO 另 +208,全是该失败类名表)→ `ER_IROM1` | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 284,800 B (278.12 KB) | 1,048,576 B (1024 KB) | 27.2% | +| ER_IROM1 | 283,512 B (276.87 KB) | 1,048,576 B (1024 KB) | 27.0% | | RW_IRAM1 | 94,736 B (92.52 KB) | 131,072 B (128 KB) | 72.3% | | RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | -| **Flash (ROM 合计)** | **286,872 B (280.1 KB)** | 1,048,576 B (1024 KB) | **27.4%** | +| **Flash (ROM 合计)** | **285,584 B (278.9 KB)** | 1,048,576 B (1024 KB) | **27.2%** | 各区的容量与常量来源: @@ -125,7 +125,7 @@ dhara 失败类 +84;RO 另 +208,全是该失败类名表)→ `ER_IROM1` | ftpTask | `1024*4` | 4KB | lftpd FTP 服务 | 740 B(余 81%)/ 静态 1216 B | | testTask | `2048*4` | 8KB | 测试套件(产品构建不创建;存储套件深链需要) | 未创建 / 静态 496 B | | defaultTask | `1280*4` | 5KB | FatFS 挂载/探针 + 配置引擎 + syslog + 栈水位探针 | 2152 B(余 41%)/ 静态 **3016 B**(栈余量收口时抬栈 1024→1280,2026-10-12) | -| timeSyncTask | `640*4` | 2.5KB | TCP 时间同步 | 616 B(余 50%)/ 静态 **1264 B**(栈余量收口时抬栈 512→640,2026-10-12) | +| timeSyncTask | `640*4` | 2.5KB | 网络校时节拍宿主(SNTP 客户端;:8888 监听 2026-11-04 退役 #78,任务入口不变) | 616 B(余 50%,2026-10-12 实测)/ 静态 **1904 B**(2026-11-04 #78 交付态复测,`docs/内存管理/data/stack_report_20261104.md`;链尾 `time_sntp_run→net_sendto→ch395f_spi_fault_set→控制台 drain 幻影环`,§4.0.7 同族记账。**master 2b848c4a 同判据实测 2056 B = FAIL**,本 PR 只减不增(−152 B = 旧任务自身帧)——FAIL 属 #77/#74 后既有债,与 iec104Task 1600 B 同源,见登记簿「静态链硬线双 FAIL」;台架 `[STACK]` 水位待复采后再定抬栈与否,未据静态值擅自抬栈) | | adcTask | `640*4` | 2.5KB | TPAFE5160 采样消费 + 全周傅氏相量 + 有效性校验(TIM3 1 kHz 节拍,ADC 采集链路→全周傅氏相量→「采样有效性校验」 2026-10-11) | **952 / 2560**(余 1608 B / 62%,台架 `/serial-boot-test` 实测 2026-10-11);静态 **1320 B**(2026-10-12 复测,保护判据接线后) | | iec104Task | `640*4` | 2.5KB | IEC104 上行服务(默认 2404 监听 + link 状态机,IEC 104 上行 2026-10-12) | 未上板 / 静态 **1456 B**(拆链→重建监听→net_bind→SPI 故障→日志尾,日志主导;512 words 时余 592 B < ISR 底线 → 抬至 640,余 1104 B / 43%) | | protectTask | `512*4` | 2KB | 保护判据节拍(prot_elm_run 消费者 + 「自检失败闭锁」采样闭锁,保护任务 2026-10-12) | 未上板 / 静态 **872 B**(float 判据 + phasor 快照 + 日志尾,余 1176 B / 57%) | diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index ec1f9cd6..3ff7767e 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -15,7 +15,7 @@ | 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) | |---|---|---|---|---| -| ER_IROM1 (Flash) | 284,800 B | 1,048,576 B | 27.2% | 参考 | +| ER_IROM1 (Flash) | 283,512 B | 1,048,576 B | 27.0% | 参考 | | RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS | | UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 | | RW_IRAM1 | 94,736 B | 131,072 B | 72.3% | PASS | diff --git a/docs/内存管理/data/stack_report_20261104.md b/docs/内存管理/data/stack_report_20261104.md new file mode 100644 index 00000000..4c491192 --- /dev/null +++ b/docs/内存管理/data/stack_report_20261104.md @@ -0,0 +1,97 @@ +# Stack budget report + +- Source: `MDK-ARM\STM32F407-Demo\STM32F407-Demo.htm` (Keil static call graph) +- Linker run: Wed Nov 04 17:43:34 2026 +- Global maximum stack usage: **3040 B** (any call tree) + +Rule: `margin = cfg - max(static_max_depth, runtime_peak)` must be >= max(1024 B ISR, 30% of cfg). + +| Task | Entry | Cfg (B) | Static max (B) | Runtime peak (B) | Worst (B) | Margin (B) | Margin % | Verdict | +|---|---|---|---|---|---|---|---|---| +| defaultTask | `StartDefaultTask` | 5120 | 3040 | - | 3040 | 2080 | 40% | PASS | +| netTask | `StartNetTask` | 3840 | 1936 | - | 1936 | 1904 | 49% | PASS | +| ftpTask | `StartFtpTask` | 4096 | 1600 | - | 1600 | 2496 | 60% | PASS | +| uiTask | `StartUiTask` | 16384 | 1632 | - | 1632 | 14752 | 90% | PASS | +| testTask | `start_test_task` | 8192 | 744 | - | 744 | 7448 | 90% | PASS | +| timeSyncTask | `time_sync_task` | 2560 | 1904 | - | 1904 | 656 | 25% | FAIL | +| adcTask | `adc_task_func` | 2560 | 1424 | - | 1424 | 1136 | 44% | PASS | +| iec104Task | `iec104_serv_task` | 2560 | 1600 | - | 1600 | 960 | 37% | FAIL | +| protectTask | `protect_task` | 2048 | 880 | - | 880 | 1168 | 57% | PASS | + +`Static max` = armlink worst-case call-tree depth; `Runtime peak` = measured high-water mark (boot + whatever workload ran before the sample); `Worst` = max of the two, which is what the verdict is computed from. + +## Worst call chain per task + +### defaultTask: 3040 B (own frame 0 B) + +``` +StartDefaultTask -> default_task_func -> storage_bring_up -> net_cfg_load -> cfg_file_load -> s_write_template -> s_write_doc -> cfg_store_write_atomic -> f_rename -> sync_fs -> disk_ioctl -> ftl_flush_cache -> dhara_map_write -> prepare_write -> auto_gc -> dhara_map_gc -> try_recover -> raw_gc -> trace_path -> dhara_journal_read_meta -> dhara_nand_read -> gd5f_read_from_cache -> dbg_console_write_line -> con_append -> dbg_console_drain -> MX_UART5_Init -> HAL_UART_Init -> HAL_UART_MspInit -> Error_Handler -> dbg_console_write_line (Cycle) +``` + +### netTask: 1936 B (own frame 0 B) + +``` +StartNetTask -> net_task_func -> net_process_messages -> net_close_locked -> net_udp_settle_close -> net_udp_datagram_read -> ch395f_read_recv_buf -> ch395f_spi_dma_transfer -> ch395f_spi_fault_set -> dbg_console_write_line -> con_append -> dbg_console_drain -> MX_UART5_Init -> HAL_UART_Init -> HAL_UART_MspInit -> Error_Handler -> dbg_console_write_line (Cycle) +``` + +### ftpTask: 1600 B (own frame 24 B) + +``` +StartFtpTask -> lftpd_start -> lftpd_inet_listen -> net_bind -> net_udp_bind_exec -> ch395f_open_socket -> ch395f_poll_cmd_status -> ch395f_get_cmd_status -> ch395f_write_cmd -> ch395f_spi_fault_set -> dbg_console_write_line -> con_append -> dbg_console_drain -> MX_UART5_Init -> HAL_UART_Init -> HAL_UART_MspInit -> Error_Handler -> dbg_console_write_line (Cycle) +``` + +### uiTask: 1632 B (own frame 0 B) + +``` +StartUiTask -> ui_task_func -> ui_port_service_keys -> lv_refr_now -> lv_display_refr_timer -> refr_invalid_areas -> refr_area -> refr_configured_layer -> refr_obj_and_children -> refr_obj -> lv_obj_redraw -> refr_obj (Cycle) +``` + +### testTask: 744 B (own frame 224 B) + +``` +start_test_task -> dbg_console_write_line -> con_append -> dbg_console_drain -> MX_UART5_Init -> HAL_UART_Init -> HAL_UART_MspInit -> Error_Handler -> dbg_console_write_line (Cycle) +``` + +### timeSyncTask: 1904 B (own frame 0 B) + +``` +time_sync_task -> time_sntp_run -> s_send_round -> net_sendto -> net_udp_sendto_exec -> net_send_locked -> ch395f_write_send_buf -> ch395f_spi_dma_transfer -> ch395f_spi_fault_set -> dbg_console_write_line -> con_append -> dbg_console_drain -> MX_UART5_Init -> HAL_UART_Init -> HAL_UART_MspInit -> Error_Handler -> dbg_console_write_line (Cycle) +``` + +### adcTask: 1424 B (own frame 384 B) + +``` +adc_task_func -> adc_health_report -> syslog_post -> dbg_console_write_line -> con_append -> dbg_console_drain -> MX_UART5_Init -> HAL_UART_Init -> HAL_UART_MspInit -> Error_Handler -> dbg_console_write_line (Cycle) +``` + +### iec104Task: 1600 B (own frame 160 B) + +``` +iec104_serv_task -> s_serv_poll -> s_service_link -> s_drop_conn -> s_bind_listen -> net_bind -> net_udp_bind_exec -> ch395f_open_socket -> ch395f_poll_cmd_status -> ch395f_get_cmd_status -> ch395f_write_cmd -> ch395f_spi_fault_set -> dbg_console_write_line -> con_append -> dbg_console_drain -> MX_UART5_Init -> HAL_UART_Init -> HAL_UART_MspInit -> Error_Handler -> dbg_console_write_line (Cycle) +``` + +### protectTask: 880 B (own frame 160 B) + +``` +protect_task -> s_log_active_edge -> dbg_console_write_line -> con_append -> dbg_console_drain -> MX_UART5_Init -> HAL_UART_Init -> HAL_UART_MspInit -> Error_Handler -> dbg_console_write_line (Cycle) +``` + +## Frame-size ranking (own frame, top 15) + +| Bytes | Function | +|---|---| +| 624 | `lv_draw_sw_arc` | +| 400 | `ui_task_wave_seg_win` | +| 400 | `draw_line_skew` | +| 384 | `adc_task_func` | +| 384 | `sb_link_poll` | +| 352 | `lv_draw_sw_triangle` | +| 352 | `net_stack_bring_up` | +| 352 | `net_udp_datagram_read` | +| 344 | `ui_w_editor_draw` | +| 328 | `lv_obj_redraw` | +| 328 | `view_frame_draw_cb` | +| 324 | `s_transform` | +| 320 | `wd_stall_evidence` | +| 312 | `nc_draw_cb` | +| 312 | `sn_draw_cb` | diff --git a/docs/子系统设计/看门狗设计说明.md b/docs/子系统设计/看门狗设计说明.md index d55bd574..d877ca05 100644 --- a/docs/子系统设计/看门狗设计说明.md +++ b/docs/子系统设计/看门狗设计说明.md @@ -223,7 +223,7 @@ static HAL_StatusTypeDef iwdg_deferred_start(const IWDG_HandleTypeDef *a_h) { (v |---|---|---|---| | NAND 初始化 / `f_mkfs` / `dhara_map_resume` / `f_mount` | 启动段 | 数百 ms ~ 数秒 | **① 启动段不启动狗**(§4.1) | | PHY 链路等待(60×50 ms) | `net_stack_bring_up` | 最长 3 s | **② 分片心跳**:循环内每轮 `task_wd_beat` | -| `time_sync_handle_conn` 的 30 s 总预算 | `App/sync/time_sync.c` | 单次可睡满 30 s | **② 切片**:`TIME_SYNC_WAIT_SLICE_MS=200` 夹住每次 `net_recv_to` 的 `rem`,每片先喂位(总预算语义不变) | +| `time_sntp_run` 的单次响应等待(UDP `net_recvfrom_to`) | `App/sync/time_sntp.c` | 单次调用 ≤1 片 `SNTP_RECV_SLICE_MS=50` | **② 切片**:响应总预算(5 s 起指数放大)拆成 50 ms 片,任务 200 ms 节拍每轮喂位(旧 `time_sync_handle_conn` 30 s 预算行随 :8888 退役于 2026-11-04 删除,见 `../子系统设计/系统墙钟时间维护说明.md` §15) | | 就绪屏障等待(NET/STORAGE READY) | net / ui / ftp / timesync 入口 | 不定 | **② 分片**:等待按 100 ms 分片并喂位 | | 卷锁等待(FatFs 多任务) | `lftpd_io.c`、`App/fs/vol_lock.h` | 不定 | **② 分片**:100 ms 分片阻塞 + 每片 `lftpd_task_wd_beat()` | | 卷锁 try 失败(defaultTask / uiTask **从不等锁**,取不到就跳过本轮) | `App/fs/vol_lock.c` 观测计数 | 不定 | **④ 只计数、不在失败分支留日志**:锁层记 5 态(`acquired` / `try_timeout` / `blk_timeout` / `failopen`,16 B 静态),读口 `vol_lock_get_stats()` **不取锁**、任意上下文可调。为什么不许在 try 失败处补一条日志——唯一能把事写下来的通道(`/log/wdev.log`、`run.log`)自己也要取这把锁,而卷锁**非递归**,被卷忙挡住时既写不出还会自取死锁;一次性留痕交给 syslog 唯一写者按差值 latch 补投(道G task-17,接口口径见 `App/fs/vol_lock.h`) | @@ -376,7 +376,7 @@ snapshot == required → miss_streak=0、证据门闩复位、喂狗 | netTask | 主循环;`net_stack_bring_up` 的 PHY 等待循环内;就绪屏障等待分片;入口 `task_wd_bind()` | | ftpTask | 主循环 = lftpd 事件循环:`lftpd.c` 事件循环每轮、RETR/STOR 每分片;`lftpd_inet.c` 连接/数据等待每轮每片;`lftpd_io.c` 卷锁等待每片;入口 `task_wd_bind()` | | uiTask | 主循环;就绪屏障等待分片;入口 `task_wd_bind()` | -| timeSyncTask | 主循环;监听重试(500 ms/2 s)前;accept 轮询;`time_sync_handle_conn` 的 **200 ms 切片**;屏障等待分片;入口 `task_wd_bind()` | +| timeSyncTask | 主循环(200 ms 校时节拍,每轮首行喂位;`time_sntp_run` 单次 ≤50 ms 片 + `time_sync_delay_fed` 50 ms 分片);屏障等待 100 ms 分片;入口 `task_wd_bind()`(旧「监听重试 / accept 轮询 / handle_conn 200 ms 切片」三处埋点随 :8888 退役于 2026-11-04 删除,墙钟说明 §15) | | adcTask | 主循环每 1 ms 节拍 `task_wd_beat(TASK_WD_ADC)`;**不做 NAND I/O → 不登记 FTL 心跳路由**(无需 `task_wd_bind()`) | | testTask | **不埋点、不登记、不入掩码** | | NAND FTL | `gd5f2gq5ue.c`:长操作入口 `nand_ftl_wd_beat()` + `nand_ftl_wd_feed()`;`gd5f_wait_busy` 每 100 ms 双管(路由到调用者任务) | diff --git a/docs/子系统设计/系统墙钟时间维护说明.md b/docs/子系统设计/系统墙钟时间维护说明.md index bf4e5631..2c5b5f51 100644 --- a/docs/子系统设计/系统墙钟时间维护说明.md +++ b/docs/子系统设计/系统墙钟时间维护说明.md @@ -24,8 +24,9 @@ ### 1.1 时基口径:内部一律**本地钟面**(用户裁定 2026-10-27) -**裁定原文:「时基先本地为准」。** 定的是**冲突时谁赢**:面板时钟页与 8888 对时把「本地钟面当 epoch」写入, -而 104 的 `C_CS NA`(对时命令)携的是 UTC,两条写入口**后写者赢** ⇒ 装置墙钟与主机本地时刻恒差 8 小时。 +**裁定原文:「时基先本地为准」。** 定的是**冲突时谁赢**:(当时的)面板时钟页与 8888 对时把「本地钟面当 +epoch」写入,而 104 的 `C_CS NA`(对时命令)携的是 UTC,两条写入口**后写者赢** ⇒ 装置墙钟与主机本地时刻 +恒差 8 小时(8888 已于 2026-11-04 退役,写入口现为 SNTP / UI / 104,见 §15、§16)。 本裁定结束「后写者赢」:**内部真源 = 本地钟面**,任何来源写入前都要先换算成本地钟面再落库。 这条同时也纠正了**本文自己的矛盾**(这才是 8 小时的根):§2 曾写 epoch = 「自 1970-01-01 00:00:00 **UTC** @@ -298,8 +299,8 @@ uint32_t now = sys_clock_get(); /* Unix 秒,可用于 FatFS 文件时间、 | 项目 | 现状 | 可扩展方向 | |------|------|------------| | 时间范围 | 受 `uint32_t` 限制至 2038 | 改用 `uint64_t`/`time_t` 扩展 | -| 运行期校准 | 30 天惰性再基准化已实现(§7):纯自我参照、零 I2C;**有意不**周期性重读 RTC 重锚——RTC 漂移(±20ppm ≈ ±1.7 s/天)远大于 Tick 漂移,重锚反而降低精度;时间纠偏由时间同步(§15)负责 | -| 网络校时 | 已实现两条:PC 推送 / TCP 8888(§15);**SNTP 客户端 / UDP 123(§16,2026-11-04 落地)** | NTP 公网源需现场路由,口径不变;104 `C_CS` 写入口径见 §1.1 末待决项 | +| 运行期校准 | 30 天惰性再基准化已实现(§7):纯自我参照、零 I2C;**有意不**周期性重读 RTC 重锚——RTC 漂移(±20ppm ≈ ±1.7 s/天)远大于 Tick 漂移,重锚反而降低精度;时间纠偏由时间同步(§16)负责 | +| 网络校时 | **SNTP 客户端 / UDP 123(§16,2026-11-04 落地)为唯一网络校时来源**;PC 推送 / TCP 8888 同日退役(§15) | NTP 公网源需现场路由,口径不变;104 `C_CS` 写入口径见 §1.1 末待决项 | | 时区 | **口径已定:内部一律本地钟面,不存时区**(用户裁定 2026-10-27,见 §1.1) | 「API 层加偏移参数」这条旧建议**作废**;唯一待决的是 104 上行是否要转 UTC(§1.1 末,送检前定) | ### 13.1 用户 SRAM 的占用分配(共享资源,用之前先在这里登记) @@ -333,98 +334,45 @@ SD2506 有 **70 字节电池保持用户 SRAM**(寄存器 `2CH~71H`,`Drivers | `Drivers/BSP/SD2506/sd2506.c` / `.h` | SD2506 RTC I2C 驱动(`sd2506_get_time` / `sd2506_set_time`) | | `App/app/app_main.c` | 初始化调用顺序:`sd2506_init()` → `boot_stats_init()`(用户 SRAM 上电账本,§13.1)→ `sys_clock_init()` | | `App/util/boot_stats.c` / `.h` | 用户 SRAM 上电账本 + 本次开机复位原因快照(占用见 §13.1;单测 `test/host/boot_stats/`) | -| `App/sync/time_sync.c` / `time_sync.h` | 时间同步实现(TCP Server,`timeSyncTask`) | -| `App/sync/time_sntp.c` / `time_sntp.h` | SNTP 校时客户端(§16,同一任务内挂 8888 事件循环);单测 `test/host/sntp/` | +| `App/sync/time_sync.c` / `time_sync.h` | `timeSyncTask` 实现(网络校时节拍宿主;TCP :8888 推送监听 2026-11-04 退役,§15);单测 `test/host/time_sync/` | +| `App/sync/time_sntp.c` / `time_sntp.h` | SNTP 校时客户端(§16,同任务内由 200 ms 校时节拍驱动);单测 `test/host/sntp/` | | `Drivers/BSP/NETCFG/sntp_cfg.c` / `.h` | `[sntp]` 配置段属主(注册在 `net_cfg_load()`) | | `Lib/coreSNTP/` | vendored coreSNTP v2.0.0 序列化器(MIT,仅 serializer + LICENSE;48 字节报文编解码) | -| `test/host/time_sync_server.py` | PC 端时间推送客户端(TCP) | --- -## 15. 时间同步(PC 推送 / TCP) +## 15. 时间同步(PC 推送 / TCP :8888)——已退役(2026-11-04) -除 RTC 自身走时与开发期校时宏外,系统支持由 **PC 通过 TCP 主动推送本地墙钟** 来同步板子时间,使板子时间与开发电脑一致(无需公网 NTP)。 +> **状态:已退役。** SNTP 客户端上线(§16,PR #77)当日,`:8888` 推送监听、8 字节 "TIME" 协议、 +> PC 端推送器 `test/host/time_sync_server.py` 与探针 `test/host/e02_probe.py` 一并删除,网络校时 +> **唯一入口 = §16 SNTP**。历史实现(协议帧、监听/退避状态机、约 80 断言点的主机单测、真机验收日志) +> 以 git 历史为准(`App/sync/time_sync.c` v1.x 与本节旧版);旧验证锚点 +> `[TIME_SYNC] time_sync: TCP listen on :8888 (sock=1)` 仅供解读旧日志(`scripts/v5_log_gap_report.py` +> 的探针窗口分类器仍识别 `no TIME packet`,属历史取证,不改)。 -### 15.1 模块与角色 - -| 角色 | 提供方 | 说明 | -|------|--------|------| -| 时间同步服务端(板子) | `App/sync/time_sync.c` / `time_sync.h` | TCP Server,监听 `TIME_SYNC_PORT`(默认 8888) | -| 同步任务 | `timeSyncTask`(FreeRTOS 任务) | 独立任务,由 `Src/freertos.c` 创建(优先级 `osPriorityNormal`,栈 2KB) | -| 时间推送端(PC) | `test/host/time_sync_server.py` | Python TCP 客户端,周期推送本机本地时间 | -| 落盘接口 | `sys_clock_set_unix()` | 收到时间包后调用,内部转 `sd2506_time_t` 并写回 SD2506 | - -### 15.2 协议 - -PC 每轮发送固定 8 字节帧: - -``` -字节 0..3 : 魔术字 "TIME"(0x54 0x49 0x4D 0x45) -字节 4..7 : 本地 Unix 时间戳,uint32,大端(PC 本地时间,含时区,与电脑显示一致) -``` - -板子 `time_sync_handle_conn()` 读取后校验魔术字,取大端 uint32 调用 `sys_clock_set_unix(epoch)` 更新运行基准并写回 RTC。 - -### 15.3 任务流程 - -`timeSyncTask` 启动后先等待 `SYS_EVT_NET_READY` 事件标志(取代旧版 8s 魔法延时;网络不就绪则一直等待),进入循环: - -```c -/* osFlagsNoClear:就绪位是"状态"不是"事件",任何消费者都不得把它清掉 - * (否则其它等待者永不唤醒——2026-09-21 FTP 永不监听事故的根因) */ -osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY, osFlagsWaitAny | osFlagsNoClear, osWaitForever); - -for (;;) { - ls = time_sync_open_listener(); /* net_socket + net_bind + net_listen,失败重试 */ - conn = net_accept(ls, NULL, NULL); /* 阻塞等待 PC 连接(无连接立即返回 -1,循环重试) */ - time_sync_handle_conn(conn); /* net_recv 读到 8 字节 → sys_clock_set_unix → net_close */ - /* conn(单连接模式下即 ls)被 net_close 释放,下次循环重建监听 */ -} -``` - -> 单连接模式下监听 Socket 收到连接后即转为数据通道,连接关闭后被释放;因此每轮循环都**重新创建监听 Socket**(与 `lftpd` 同模式),与 FTP 不冲突。 - -### 15.4 为什么用 TCP 而非 UDP - -- 所有 socket 操作(`net_socket` / `net_bind` / `net_listen` / `net_accept` / `net_recv` / `net_close`)均走 `net_socket` 的**线程安全 API**,内部经消息队列由 `netTask` 串行执行; -- netTask 因此只需做 `net_poll()` + `net_process_messages()`,不掺杂任何应用层网络逻辑; -- **TCP 的全部 API 已经是基于消息队列的**(已在 `net_socket.c` 实现);UDP 的 `net_bind` / `net_sendto` / `net_recvfrom` **曾经**绕过队列,**2026-11-03 已并入队列**(改造事实见 `docs/网络/BSD_Socket_API_使用指南.md` §9.0)。也就是说:**"UDP 不安全"不再是本节选 TCP 的理由**——本节结论不变(现有 TCP:8888 时间同步通道继续有效),但若日后要换成 UDP(#57 SNTP 方向),不再需要先改 net 层。 - -### 15.5 Socket 分配与冲突 - -- `net_socket()` 从 0 开始分配第一个空闲 Socket 控制块(`alloc_socket()`),并把硬件 Socket 号设为该下标;调用时的第三个参数(0)仅为协议占位,不影响分配。 -- 当前启动顺序:FTP 任务(`NET_READY` + `STORAGE_READY` 屏障满足后,无魔法延时)先监听 → 占用一个 Socket(单连接模式下监听通道复用为数据通道);时间同步任务(`NET_READY` 后)后监听 → 占用另一个空闲 Socket。二者不冲突。 -- 单连接模式允许**任意** Socket 作为"监听 + 数据复用"通道,且总共 8 个 Socket,FTP 与时间同步各用 1 个,余量充足。 -- SNTP 客户端(§16)另占 1 个 UDP Socket:懒创建(首次到点才 `net_socket`+`net_bind` 本地口 0 = 芯片动态分配),断网代际变化、`enable=0` 或内核错误码时关闭归还;不监听、不参与端口台账。 - -### 15.6 客户端实现要点(避坑) - -PC 端 `test/host/time_sync_server.py` 发送 8 字节后**必须保持连接打开**,等待板子读取(或短延时)后再 `close()`: - -- 若发送后立刻关闭,CH395F 在收到 FIN 时会**丢弃接收缓冲里尚未被板子读出的 8 字节**; -- 板子 `net_recv` 经消息队列异步执行(有 ~10ms+ 调度延迟),等它去读时连接已 DISCONNECT、缓冲已清空,`net_recv` 走 CLOSED 分支返回 0,时间包丢失、同步失败; -- 客户端改为 `sendall` 后 `recv` 直到板子主动关闭(EOF)或 2s 超时,即可保证板子先读到数据。 - -### 15.7 验证 - -板子日志应出现: - -``` -[TIME_SYNC] time_sync: TCP listen on :8888 (sock=1) -[NET] sock1 standalone accept -[NET] recv sock1 len=8 -[CLK] sys_clock_set: wall clock updated to -[CLK] sys_clock_set: SD2506 RTC written -[TIME_SYNC] time_sync: wall clock updated (epoch=) -``` - -PC 端 `python test/host/time_sync_server.py` 每 `interval` 秒推送一次,epoch 随电脑本地时间递增即表示同步正常。 +- **当初定位**:开发期让板子时间对齐 PC(无需公网 NTP)——PC 周期推固定 8 字节帧 + `"TIME"(54 49 4D 45) + uint32 大端"本地 Unix 时间戳"(与电脑显示一致)`,板端校验魔术字后走 + `sys_clock_set_unix()` 同一收口写回 SD2506(§1.1 口径的第二个历史写入口)。 +- **退役理由**:① SNTP 落地后它只剩"开发期工具"定位,与 SNTP 双写并存时现场钟面**后写者赢**、 + 节奏不可控;② 单连接监听被 `time_sync_handle_conn()` 全程占用,一条连接不结算(30 s 预算/ + 半包等待)其他对时全排队——head-of-line 风险,为喂狗才做的 200 ms 分片只是缓解;③ 依赖 PC 上 + Python 推送器在场,现场不可持续;真机对时判据已由 UI 对时页 + `[SNTP]` 日志/OPER 痕覆盖。 +- **仍然有效的教训**:CH395F 单连接 TCP 服务在收到 **FIN 时会丢弃该 socket 内尚未读走的缓冲数据** + (旧推送器因此必须 sendall 后保持读至 EOF/2 s 超时)。`lftpd`、`ui_net_sink` 与今后任何常驻 + TCP 服务同样受此约束(先结算再断开);UDP 校时(§16)无此坑。 +- **Socket 预算变化**:`alloc_socket()` 从 0 分配、硬件 Socket 号=下标的机制不变;常驻监听由 + 21/8888/7003 三个减为 21/7003 两个,SNTP 的 1 个 UDP Socket 仍懒创建、按代次/开关归还(§16)。 +- **TCP 而非 UDP 的旧论证**(选 TCP 的队列/串行化背景)已被 2026-11-03 UDP 并入消息队列 + (`docs/网络/BSD_Socket_API_使用指南.md` §9.0)与本文第 16 节的落地取代,不再单独成立。 --- ## 16. SNTP 校时(客户端,2026-11-04 落地) -- 模块:`App/sync/time_sntp.c/.h`(挂在 timeSyncTask 的 8888 事件循环里,等待切片 ≤50 ms,不饿任务狗); +- 模块与任务骨架:`App/sync/time_sntp.c/.h` 由 `timeSyncTask`(`App/sync/time_sync.c` v2.0,:8888 + 退役后该任务即纯校时节拍宿主)驱动——屏障等 `SYS_EVT_NET_READY`(100 ms 分片喂心跳)→ 主循环 + 每 200 ms 一轮:`task_wd_beat` → `time_sntp_run(net_get_generation())`(应答收片 ≤50 ms,不饿任务狗; + 代次变化由 `time_sntp_run` 内部弃旧 Socket 自重建)→ `time_sync_delay_fed(200)` 分片续心跳; 48 字节报文编解码 = vendored coreSNTP 序列化器(`Lib/coreSNTP/`,MIT,零堆),节奏与准入判据在本方。 - 配置:`/config.ini` `[sntp]` 段(`enable` / `server1` / `server2` / `poll_min`,属主 `Drivers/BSP/NETCFG/sntp_cfg.c`, 注册在 `net_cfg_load()`);现场改法 = UI「通讯参数 → SNTP 设置」页(密码叶)保存后随下一轮生效,或 FTP 改文件(4 s 落定)。 @@ -436,5 +384,7 @@ PC 端 `python test/host/time_sync_server.py` 每 `interval` 秒推送一次,e 响应源地址必须等于请求目的地址。失败:超时指数退避(5 s 起、60 s 封顶)+ 主备轮换;成功:恢复 `poll_min` 周期。 - 步进策略:|offset| < 2 s 只记日志不步进;≥ 2 s 或冷启动直接步进并留痕 `[SNTP] step offset=`、`syslog` OPER 「clock set by SNTP」;首个校验通过的同步置 `SYS_EVT_TIMESYNC_READY`(就绪位,不覆盖后续步进语义)。 -- 与 8888 的关系:同一任务、同一 `sys_clock_set_unix` 收口,**后写者赢**维持现状(两路并存是部署选择,不是冲突缺陷)。 +- 与 8888 的关系(历史):2026-11-04 当日 SNTP 落地在前(PR #77)、:8888 清除在后,中间两路短暂 + 并存、同一 `sys_clock_set_unix` 收口**后写者赢**;退役后本模块是**唯一网络校时入口**, + `sys_clock_set_unix` 现存三个写入口 = SNTP / UI 手工对时 / 104 `C_CS`(口径见 §1.1)。 - 主机单测:`test/host/sntp/`(报文格式、防伪拒收、KoD、退避与周期、阶跃域守卫、配置注册表)。 diff --git a/docs/子系统设计/网络配置说明.md b/docs/子系统设计/网络配置说明.md index 622a1f61..54f92bf0 100644 --- a/docs/子系统设计/网络配置说明.md +++ b/docs/子系统设计/网络配置说明.md @@ -263,7 +263,7 @@ sequenceDiagram **机制**:`请求 → netTask 重建 → 代次号 → 服务自愈` -**约束与对策**:网络重建会复位 CH395F,所有监听 Socket 随之失效;而三个 TCP 服务(FTP :21 / 对时 :8888 / 远程显示 :7003)的监听由各自任务持有。为此引入"网络代次号"——各服务检测到代次变化后主动重建监听,在新 IP 上恢复服务。 +**约束与对策**:网络重建会复位 CH395F,所有监听 Socket 随之失效;而两个常驻 TCP 服务(FTP :21 / 远程显示 :7003;对时 :8888 已退役,2026-11-04)的监听由各自任务持有。为此引入"网络代次号"——各服务检测到代次变化后主动重建监听,在新 IP 上恢复服务。 ```mermaid sequenceDiagram @@ -271,7 +271,7 @@ sequenceDiagram participant CFG as net_cfg participant NT as netTask participant NS as net_socket - participant SVC as FTP/对时/远程显示 + participant SVC as FTP/远程显示/对时(SNTP 出向) UI->>CFG: 保存行 ENTER → net_cfg_save()(写盘) CFG->>NT: net_cfg_request_apply() → 回调置 SYS_EVT_NET_RECONFIG @@ -281,12 +281,12 @@ sequenceDiagram SVC->>SVC: 轮询发现代次变化 → 关闭旧监听 → 重建监听(新 IP) ``` -**三个服务的自愈点**: +**各服务的自愈点**: | 服务 | 端口 | 检测位置 | 自愈方式 | |---|---|---|---| | FTP | 21 | `lftpd_inet_accept()` | 代次变化 → 返回 -1 → `lftpd_start` 外层循环重建监听 | -| 对时 | 8888 | `time_sync_task()` 的 accept 循环 | 代次变化 → 关旧监听 → 回外层重建 | +| 对时(SNTP 客户端) | UDP 出向(无监听) | `time_sntp_run(net_gen)` 每轮比对代次 | 代次变化 → 弃旧 Socket 归还 → 下轮到点懒重建(2026-11-04 起原 :8888 监听行随退役删除) | | 远程显示 | 7003 | `ui_net_sink_poll()` | 代次变化 → 关旧监听/客户端 → 重建 | **关键设计点**: diff --git a/docs/架构设计/软件架构与运行时规格.md b/docs/架构设计/软件架构与运行时规格.md index 236f58f9..47bf4b55 100644 --- a/docs/架构设计/软件架构与运行时规格.md +++ b/docs/架构设计/软件架构与运行时规格.md @@ -42,7 +42,7 @@ | `ftpTask` | 1024 / 4096 | Normal | 等网络**且**存储(`WaitAll`)→ `lftpd_start("/", 21)` → 单客户端会话循环 | | `testTask` | 2048 / 8192 | BelowNormal | 测试套件入口(`test/board/test_config.h` 选择)。**产品构建不创建**(未选套件时立即 `vTaskDelete` 回收其 8 KB CCM 栈) | | `uiTask` | 4096 / 16384 | BelowNormal | 等网络 → `ui_port_init`/`ui_menu_init`/`ui_standby_init`(空闲 60 s 切待机屏)/`ui_net_sink` → 10 ms 刷新 + 数据注入 | -| `timeSyncTask` | 640 / 2560 | Normal | 等网络 → TCP :8888 收 PC 推送时间 → 写 RTC | +| `timeSyncTask` | 640 / 2560 | Normal | 等网络 → 200 ms 校时节拍 `time_sntp_run()`(SNTP 客户端 UDP 出向 → 写 RTC;:8888 推送监听已退役 2026-11-04) | | `adcTask` | 640 / 2560 | AboveNormal | **采集主线唯一消费者**:每拍 `adc_input_poll()`(TPAFE 快照 + ADC1 DMA 帧)→ `adc_health` 有效性 → `phasor_run()` 相量 → `freq_run()` 测频 → 每 200 ms `meas_tick()` 发布遥测快照 | | `iec104Task` | 640 / 2560 | Normal | 等网络 → TCP 工厂默认 :2404 监听 → 104 链路状态机(总召/变位/周期/遥控/定值/对时)+ 断链重听 + 网络代次自愈 | | `protectTask` | 512 / 2048 | Normal | **20 ms 判据节拍**:采样故障闭锁判定 → 定值校验和巡检 → `prot_elm_run()` 元件判据 → 动作边沿投递 SOE(**不做 NAND I/O**) | @@ -115,7 +115,7 @@ sequenceDiagram N-->>F: set SYS_EVT_NET_READY F->>F: wait NET_READY|STORAGE_READY(All|NoClear)→ lftpd_start("/", 21) U->>U: wait NET_READY(Any|NoClear)→ ui_port_init → ui_menu_init → ui_net_sink_start(7003) - I->>I: wait NET_READY(Any|NoClear)→ TCP listen :2404(默认)/ :8888 + I->>I: wait NET_READY(Any|NoClear)→ TCP listen :2404(默认)/ 200ms SNTP 校时节拍 ``` > **顺序铁律**:① 配置段注册必须**先于** `cfg_file_load`(否则 FTP 上传的段不生效); diff --git a/docs/架构设计/项目架构总览.md b/docs/架构设计/项目架构总览.md index 0af53031..0843e80e 100644 --- a/docs/架构设计/项目架构总览.md +++ b/docs/架构设计/项目架构总览.md @@ -43,9 +43,9 @@ | 录波链路(三任务交接) | adcTask `wave_ring_push()/tick()`(与相量主线同拍**并行**、互不阻塞)→ 触发冻结、后触发计满封段 → defaultTask 见 `wave_ring_sealed()` 游标分次搬进 `/wave` → uiTask 单通道回放页;落盘回执才解冻。取回 = FTP :21 | | SOE 链路 | protectTask 动作/复归边沿 `soe_store_post()`(纯 RAM:取时标 + 组 64B 记录 + 队列,**零文件系统**)→ defaultTask(`/log` 唯一写者,`syslog_drain` 尾部挂接)落 `/log/soe.log` | | `lftpd` / `net_cfg` / 测试 → `FatFs` → `nand_ftl` → `gd5f2gq5ue` | 文件、配置、录波段与 SOE 落 NAND | -| `ui_net_sink` / `time_sync` → `net_socket` | 远程显示 :7003 / 对时 :8888 共用同一 TCP 栈;屏板链路走 USART1 串口(不经 TCP),与 :7003 是同一份显存的两个出口(**串口为主**) | +| `ui_net_sink` / `time_sync` → `net_socket` | 远程显示 :7003 走 TCP;对时 = `time_sntp` 经同一消息队列走 **UDP 出向**(:8888 监听已退役,2026-11-04,墙钟说明 §15);屏板链路走 USART1 串口(不经 TCP),与 :7003 是同一份显存的两个出口(**串口为主**) | | `ui_menu` → `ui_content_page` → `ui_page_*` | 框架 + 页面插件(描述符解耦;节点→页面绑定由注册表 `ui_page_registry.c` 生成,见 §6) | -| `time_sync` / `sys_clock` → `sd2506` | 墙钟基准与写回 RTC | +| `time_sntp` / `sys_clock` → `sd2506` | 墙钟基准与写回 RTC(对时写钟入口,:8888 退役后唯一网络来源) | | `ui_port` / `ui_page_*` → `LVGL` | UI 全部绘制走 LVGL(1bpp 离屏渲染到内存帧缓冲) | ### 1.3 端到端链路鸟瞰(另一视角:一个电气量走到底) @@ -72,7 +72,7 @@ | `ftpTask` | 4096 | Normal | 等网络**且**存储(`WaitAll`)→ `lftpd_start("/", 21)` → 单客户端会话循环(断开后经 `net_listener` 重建监听);录波段文件的取回通道 | `StartFtpTask` | | `testTask` | 8192 | BelowNormal | 测试套件入口(`test/board/test_config.h` 选择)。**产品构建不创建**:CubeMX 生成区无条件建后,`MX_FREERTOS_Init()` 在未选套件时(`!TEST_SUITE_SELECTED`)立即 `vTaskDelete` 回收其 8KB CCM 栈(2026-10-04) | `start_test_task` | | `uiTask` | 16384 | BelowNormal | 等网络 → `ui_port_init`/`ui_menu_init`/`ui_standby_init`(空闲 60s 切待机屏)/`ui_net_sink` → 10ms 刷新:`ui_data_feed_tick()` 三帧装配 + 屏板链路驱动(组帧 B / 出键 / 离线判定 / 状态灯位)+ 录波回放页 | `ui_task_func` | -| `timeSyncTask` | 2560 | Normal | 等网络 → TCP :8888 收 PC 推送时间 → 写 RTC | `time_sync_task` | +| `timeSyncTask` | 2560 | Normal | 等网络 → 200 ms 校时节拍 `time_sntp_run()`(SNTP 客户端,UDP 出向;写 RTC 经 `sys_clock_set_unix`。:8888 推送监听已退役 2026-11-04) | `time_sync_task` | | `adcTask` | 2560 | AboveNormal | 纯消费者 + **唯一发布者**:`osThreadFlagsWait` 等 TIM3 1kHz 唤醒(ISR 内 `adc_tick_irq_notify` 置位)→ `adc_input_poll()`(TPAFE 原子快照 + STM32 ADC1 DMA 双缓冲)→ 标定折算 → 相量 → 测频;录波支路 `wave_ring_push()/tick()`;**每 200ms `meas_tick()` 发布遥测快照**(双区无锁,104 与 UI 共用同一份上游) | `adc_task_func` | | `iec104Task` | 2560 | Normal | 等网络 → IEC 60870-5-104 上行(工厂默认 :2404 监听 + link 状态机;遥测读 meas 快照、遥信读 `prot_elm` 元件态,COV 死区/变位上送;`[comm]`/`[run_par]` 配置;IEC 104 上行/第二轮)。任何构建都创建,纳入 IWDG `required_mask` | `iec104_serv_task` | | `protectTask` | 2048 | Normal | 保护判据 20ms 节拍:`prot_elm_run()` + 「自检失败闭锁」采样故障闭锁 + 定值校验和巡检 + 动作/复归边沿 SOE 投递(保护任务);⚠ **当前处于「未标定闭锁」态**——`adc_cal_flags()` 无 VALID 时判据停走,两点标定写入 `[adc_cal]` 后自动解卡(用户裁定 2026-10-25:保持闭锁,不用万用表走捷径);纳入 IWDG `required_mask`(产品 `0x1F7`) | `protect_task` | @@ -155,7 +155,7 @@ sequenceDiagram F->>F: wait NET_READY|STORAGE_READY(All|NoClear)→ lftpd_start("/", 21)
→ 串口输出:waiting for connection... U->>U: wait NET_READY(Any|NoClear)→ ui_port_init → ui_menu_init → ui_net_sink_start(7003)
→ 10ms 循环:ui_data_feed_tick + 屏板组帧/出键/灯位 - T->>T: wait NET_READY(Any|NoClear)→ TCP listen :8888 + T->>T: wait NET_READY(Any|NoClear)→ 200ms 校时节拍 time_sntp_run()(SNTP 客户端 UDP 出向) I->>I: wait NET_READY(Any|NoClear)→ IEC104 listen 默认 :2404(遥测读 meas 快照 / 遥信读 prot_elm) P->>P: 20ms 判据节拍 prot_elm_run()(不依赖网络;未标定即闭锁停走)+ SOE 边沿投递 C->>C: wait TIM3 1kHz 唤醒(ISR adc_tick_irq_notify)→ adc_input_poll() 纯消费
→ 标定→相量→测频 → wave_ring_push/tick → 每 200ms meas_tick() 发布快照 @@ -217,7 +217,7 @@ flowchart TB NS --> Q["netMsgQueue
(串行化到 netTask)"] Q --> NT["netTask
net_poll() + net_process_messages()"] NS --> C1["lftpd
FTP :21"] - NS --> C2["time_sync
对时 :8888"] + NS --> C2["time_sync 任务
time_sntp SNTP 客户端
UDP 123 出向(:8888 已退役)"] NS --> C3["ui_net_sink
RemoDispBus :7003"] NS --> C4["iec104_serv
104 上行默认 :2404"] C3 -.->|"回送 1bpp 帧 + 接收远端按键"| CLIENT["DTU-RemoteLCD 客户端"] @@ -321,7 +321,7 @@ STM32F4-Base/ │ ├── cfg/ 配置引擎:cfg_file(静态态单任务收敛于 defaultTask)、store(MCU=FreeRTOS 堆)、 │ │ prot_set_store(/prot_set.bin 原子写,保护定值持久化) │ ├── fs/ 卷锁 vol_lock(syslog / cfg / lftpd 三条路径 try 语义)、vol_idle_sync -│ ├── sync/ time_sync(TCP 对时服务) +│ ├── sync/ time_sync(timeSyncTask 载体)、time_sntp(SNTP 校时客户端,UDP) │ └── util/ sys_clock(墙钟)、task_watchdog(IWDG 判定状态机)、boot_stats、sha256、task_prof ├── Drivers/BSP/ 纯手工驱动/服务(不受 CubeMX 保护) │ ├── CH395F/ 以太网 MAC+PHY(SPI2 + DMA) diff --git a/docs/测试说明/专项验证/链路延迟测试说明.md b/docs/测试说明/专项验证/链路延迟测试说明.md index 98aa38e4..c1638bdf 100644 --- a/docs/测试说明/专项验证/链路延迟测试说明.md +++ b/docs/测试说明/专项验证/链路延迟测试说明.md @@ -39,7 +39,7 @@ MCU 侧真实处理只占 6.7ms,任务拾取/调度约 2.3ms(2026-09-22 重 |---|---|---| | 远端显示 RemoDispBus | 7003 | `ui_net_sink`(真实 uiTask 生产路径) | | FTP(lftpd) | 21 | 控制 + PASV 数据通道(数据通道复用监听 Socket,CH395F 单连接模型) | -| 对时 | 8888 | `time_sync` | +| 对时(本文实测时在场;2026-11-04 随 #78 退役,对时改 SNTP 出向) | 8888 | `time_sync`(历史) | | 时延测试链路(M2) | 8080 | Phase 13 测试协议(服务器驱动) | 客户端 = DTU-RemtoeLCD(`remo_disp_server.py`,与本工程同级的独立项目,目录名含历史拼写 `Remtoe`)。 diff --git a/docs/测试说明/单元测试/时间同步单元测试说明.md b/docs/测试说明/单元测试/时间同步单元测试说明.md index 9ed165c1..9045022b 100644 --- a/docs/测试说明/单元测试/时间同步单元测试说明.md +++ b/docs/测试说明/单元测试/时间同步单元测试说明.md @@ -1,93 +1,75 @@ # 时间同步单元测试说明(主机侧) -> **模块**:Time Sync(`App/sync/time_sync.c` + `App/sync/time_sync.h`) -> **测试类型**:**主机侧纯逻辑单元测试**——在 PC 上离线验证 TCP 时间同步协议 -> 状态机(分片喂狗延时 / 监听建立与退避 / 时间包重组 / 任务主循环),零硬件、零烧录、 -> 毫秒级、不占 MCU 的 RAM。 -> **状态**:✅ 已落地(80 断言点,`/W4` 0 告警,已挂 pre-commit 钩子) -> **日期**:2026-10-10 +> **模块**:Time Sync(`App/sync/time_sync.c` + `App/sync/time_sync.h`)——timeSyncTask 载体 +> **测试类型**:**主机侧纯逻辑单元测试**——在 PC 上离线验证任务编排逻辑(分片喂狗延时 / +> 就绪屏障等待 / SNTP 节拍驱动 / 网络代次交接),零硬件、零烧录、毫秒级、不占 MCU 的 RAM。 +> **状态**:✅ v2.0(:8888 退役后,33 断言点,已挂 pre-commit 钩子) +> **日期**:2026-11-04(v2.0;v1.0 2026-10-10 的 TCP :8888 协议状态机套随 #78 退役重写) > **关联文档**: > [`../../../AGENTS.md`](../../../AGENTS.md)(平台约束与看门狗铁律)、 > [`测试开发标准流程.md`](../流程与规范/测试开发标准流程.md)(测试分层与门禁总览)、 > [`看门狗单元测试说明.md`](看门狗单元测试说明.md)(喂狗位图判定状态机)、 -> [`卷锁单元测试说明.md`](卷锁单元测试说明.md)(同批落地的姊妹篇)、 -> [`../../子系统设计/看门狗设计说明.md`](../../子系统设计/看门狗设计说明.md)(`required_mask` / 抽样窗 / 长耗时白名单)、 -> [`../../任务规划/README.md`](../../任务规划/README.md)(登记条目「探针窗口心跳缺口」)、 -> 历史审计(netTask 头阻塞;属主文档已于 2026-10-15 删除,现状见 [`../../网络/Net层阻塞模型架构设计.md`](../../网络/Net层阻塞模型架构设计.md)) +> [`../../子系统设计/系统墙钟时间维护说明.md`](../../子系统设计/系统墙钟时间维护说明.md)(§15 退役历史 / §16 SNTP 现行设计)、 +> [`../../子系统设计/看门狗设计说明.md`](../../子系统设计/看门狗设计说明.md)(`required_mask` / 抽样窗 / 长耗时白名单) --- ## 1. 概述 -### 1.1 为什么单独做主机侧单测 +### 1.1 为什么退役后仍要单独做主机侧单测 -本模块是**两件已知风险的当事方**,而两件风险的成因**全部是本模块的纯逻辑**: +:8888 监听、`TIME` 包重组状态机、监听退避重试随 2026-11-04 退役删除(旧套 80 断言点的 +主体),但任务本身**不能删**——它是 timeSyncTask 的载体,SNTP 校时客户端 +(`App/sync/time_sntp.c`,设计见墙钟说明 §16)挂在它的 200 ms 节拍下运行。留存的纯逻辑 +仍然全是「误复位」当事方,且真机上只能"跑起来看复位不复位"间接观察: -1. **探针窗口心跳缺口(看门狗余量)**:登记簿条目记录"time_sync 监听重建期间心跳缺口 800ms~1s, - 距 IWDG 最坏超时 1701ms 余量仅约 0.7s"。该缺口的每一段都由本模块产生—— - `time_sync_delay_fed()` 的**分片喂狗节奏**、监听重试退避(500 / 2000ms)、 - `accept` 轮询退避(200ms)、收尾延时(50ms),以及"就绪屏障等待态也要喂心跳"。 - 真机上只能靠"跑起来看复位不复位"间接观察;主机侧可**逐片断言**心跳次数与片长。 -2. **头阻塞**:旧实现用单次 30s 阻塞 `net_recv` 等时间包,独占 `netTask`, - 使其余任务的网络操作整体停摆。现改为"分片有界读 + 总预算",本单测钉死其**分片语义** - (单次等待片长恒 ≤ 50ms、总预算 30s 不因分片而改变)。 +1. **心跳节奏**:`time_sync_delay_fed()` 的分片喂心跳(片长 ≤ 50 ms)与主循环每轮首行喂位; + 就绪屏障等待(100 ms 分片)未就绪时每轮也要补心跳——这是 2026-10-02 台架根因的回归防线。 +2. **SNTP 宿主的节拍契约**:每轮恰好调一次 `time_sntp_run(当前代次)`;网络代次变化必须 + **原样、当轮**传给客户端(代次交接是 `time_sntp_run` 内部弃旧 Socket 重建的唯一触发)。 +3. **网络未就绪绝不出网**:屏障通过之前不得读代次、不得驱动 SNTP。 -次要理由:**时间包重组状态机**(累积到 8 字节才解析、TCP 分段送达、EOF / 真错误 / -本片超时三种返回码分支、tick 回绕)是纯逻辑,板端套件无法精确构造 -"分段 + 预算耗尽 + tick 回绕"这类组合。 +旧套钉死的「头阻塞回归防线」(单次等待片长恒 ≤ 50 ms、总预算不因分片改变)语义由 +SNTP 模块自己的主机套 `test/host/sntp/` 与本文的延时分片用例共同接力。 ### 1.2 被测对象与桩 -测试单元**原样 `#include` 生产源文件**,平台依赖由 7 个桩头替身 + 1 个真实头组成: +测试单元**原样 `#include` 生产源文件**,平台依赖由 5 个桩头替身 + 3 个真实头组成: ``` test/host/time_sync/time_sync_test.c ├── #include "FreeRTOS.h" / "task.h" / "cmsis_os2.h" ← 桩 - ├── #include "net_socket.h" / "sys_clock.h" / - │ "sys_events.h" / "dbg_log.h" ← 桩 + ├── #include "net_socket.h" / "sys_events.h" ← 桩 ├── #include "task_watchdog.h" ← 真实头(App/util/) + ├── #include "time_sntp.h" ← 真实头(App/sync/,签名对表) └── #include "time_sync.c" ← 生产源文件(App/sync/),原样纳入 ``` | 头文件 | 来源 | 桩提供的能力 | |---|---|---| | `FreeRTOS.h` | 桩 | `TickType_t` = `uint32_t`;`configTICK_RATE_HZ` = 1000(故 `pdMS_TO_TICKS(ms) == ms`) | -| `task.h` | 桩 | `vTaskDelay`(记录每片时长)、`xTaskGetTickCount`(返回可控 tick) | -| `cmsis_os2.h` | 桩 | `osEventFlagsWait`(按脚本返回就绪 / 超时) | +| `task.h` | 桩 | `vTaskDelay`(记录每片时长);tick 时钟桩保留(生产侧现不再读 tick,留给未来用例) | +| `cmsis_os2.h` | 桩 | `osEventFlagsWait`(按脚本返回就绪 / 超时,可第 N 次 `longjmp` 跳出) | | `sys_events.h` | 桩 | `sys_evtHandle` + `SYS_EVT_NET_READY` 等位定义 | -| `sys_clock.h` | 桩 | `sys_clock_set_unix`(记录 epoch 与调用次数) | -| `dbg_log.h` | 桩 | `DBG_ERROR` / `DBG_INFO` 计数(`g_tstest_err_count` / `g_tstest_info_count`) | -| `net_socket.h` | 桩 | `socket` / `bind` / `listen` / `accept` / `close` / `recv_to` / `get_generation` / `htons` **全部脚本化** | +| `net_socket.h` | 桩 | **只剩 `net_get_generation`**(脚本化代次序列)——故意裁剪:生产代码若再引入监听类 API 会在本套编译/链接期即失败(绊线),旧 :8888 形态不会无声复活 | | `task_watchdog.h` | **真实**(`App/util/`) | 提供 `task_wd_id_t` / `TASK_WD_TIMESYNC`;`task_wd_beat` / `task_wd_bind` 由测试文件实现(记录心跳节奏与位号) | +| `time_sntp.h` | **真实**(`App/sync/`) | 契约头(仅 timeSyncTask 上下文、单次 ≤50 ms 收片、调用方负责片间喂心跳);`time_sntp_run` 由测试文件实现(记录每轮收到的代次 + 可第 N 次 `longjmp`) | | `time_sync.h` | **真实**(`App/sync/`) | 只 `#include `,无平台依赖 | `include_dirs` 顺序为 **`stub` → `App/util` → `App/sync` → 本目录**:桩目录必须排在 前面,否则会被真实头抢先命中。 -> ⚠ `dbg_log.h` 桩**不得**定义 `DBG_LINE_BUF_MAX` / `DBG_TAG`——生产源(`time_sync.c:28-29`) -> 已先定义它们,桩再定义会触发 C4005 宏重定义告警。 +### 1.3 关键契约(生产代码侧) -### 1.3 关键约定:分片喂心跳与报文重组契约 - -**(一)心跳必须落在同一个 100ms 抽样窗内。** -看门狗抽样窗为 `WD_POLL_PERIOD_MS = 100ms`(`App/util/task_watchdog.c`)。若一次长等待 -(如 2000ms 退避)期间不喂心跳,或心跳节奏恰好滑过两个连续抽样窗,该任务就会被判"卡死"。 -故 `time_sync_delay_fed()` 把任意长等待切成 **≤ `TIME_SYNC_WAIT_SLICE_MS` = 50ms** 的短片, -**每片前**喂一次心跳——语义等价于一次长延时,仅把心跳节奏钉在窗内(第 8b 轮台架消抖)。 - -**(二)`net_recv_to()` 的四态返回。** -`time_sync_handle_conn()` 的重组循环依赖该契约: - -| 返回 | 含义 | 本模块动作 | -|---|---|---| -| `> 0` | 收到 `n` 字节 | 累加;`>= 8` 即收齐并解析,否则继续等余下字节 | -| `0` | 对端关闭(EOF) | 退出循环,**不解析** | -| `-1` | 真错误 | 退出循环,**不解析** | -| `-2` | 本片预算等满、无数据 | 重算剩余预算,**下一片**继续 | - -**(三)总预算不因分片而改变。** -单次 `net_recv_to` 的等待片长 ≤ 50ms,但**跨片累积**仍以 `t0 = xTaskGetTickCount()` -为基准、总预算 `TIME_SYNC_TOTAL_BUDGET_MS` = 30000ms。tick 回绕由无符号减法天然处理。 +- **心跳必须落在同一个 100 ms 抽样窗内**:看门狗抽样窗 `WD_POLL_PERIOD_MS = 100 ms` + (`App/util/task_watchdog.c`)。`time_sync_delay_fed()` 把任意长等待切成 + ≤ `TIME_SYNC_WAIT_SLICE_MS` = **50 ms** 短片,每片前喂一次心跳。 +- **主循环节拍**:`task_wd_beat` → `time_sntp_run(net_get_generation())` → + `time_sync_delay_fed(TIME_SYNC_TICK_MS=200)`。SNTP 单次调用最多停留一片 50 ms + (`SNTP_RECV_SLICE_MS`,契约见 `time_sntp.h`),节拍间隙由载体任务喂位覆盖。 +- **就绪屏障**:`osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY, + osFlagsWaitAny | osFlagsNoClear, 100)`——就绪位**不得被消费**(`osFlagsNoClear`, + 见 AGENTS「RTOS 与内存」);`sys_evtHandle == NULL`(模拟器)跳过屏障。 ### 1.4 测试环境 @@ -114,67 +96,50 @@ test/host/time_sync/time_sync_test.c | 项 | 说明 | |---|---| -| **触发范围** | `App/sync/time_sync.c` / `time_sync.h`、`App/util/task_watchdog.h`,或 `test/host/time_sync/**`、`test/host/common/unit_test_harness.py` 变动 | +| **触发范围** | `App/sync/time_sync.c` / `time_sync.h` / `time_sntp.h`、`App/util/task_watchdog.h`,或 `test/host/time_sync/**`、`test/host/common/unit_test_harness.py` 变动 | | **门禁层级** | **主机侧纯逻辑单测**(介于静态门禁与板端测试套件之间):主机侧、秒级、纯逻辑 | | **pre-commit 钩子** | `time-sync-unit-test`(本地钩子,`.pre-commit-config.yaml` §7g) | -| **汇总行** | `=== Time Sync Tests: 80/80 PASSED (failed=0) ===` | -| **与板端测试的关系** | 互补:本单测保证**延时节奏与重组逻辑**符合设计;板端保证**真实 TCP 时序 + IWDG 不误复位**(该心跳缺口的实际余量只能由板端长跑判定) | +| **汇总行** | `=== Time Sync Tests: 33/33 PASSED (failed=0) ===` | +| **与板端测试的关系** | 互补:本单测保证**节拍编排与代次交接**符合设计;板端保证**真实 UDP/SNTP 时序 + IWDG 不误复位** | --- ## 2. 测试用例 -> 命名 `TC-TS-NNN`(TS = Time Sync)。断言数(80)多于用例数(27),因为每个用例通常 -> 包含 2~9 条子断言(后缀 `b` / `c` …)。桩状态在每用例前由 `fakes_reset()` 复位。 +> **编号停发口径**:本文档不再使用 `TC-TIMESYNC-NNN`(新条目一律语义短语, +> [`../../编号规范.md`](../../编号规范.md) §0);旧编号族已随退役整批作废,不复用。 +> 断言用**语义标签**命名。桩状态在每用例前由 `fakes_reset()` 复位。 -### 2.1 `time_sync_delay_fed` — 分片喂心跳延时(TC-TIMESYNC-001..006) +### 2.1 `time_sync_delay_fed` — 分片喂心跳延时(`tc_delay_fed`) -| 用例 | 断言 | +| 场景 | 断言 | |---|---| -| **TC-TIMESYNC-001** | `delay_fed(0)` 既不睡眠也不喂狗(零开销快路径) | -| **TC-TIMESYNC-002** | 恰好一片(50ms):1 次 `vTaskDelay(50)` + 1 次心跳 | -| **TC-TIMESYNC-003** | 非整片(120ms)切成 **50 / 50 / 20** 三片,每片喂一次心跳 | -| **TC-TIMESYNC-004** | 长延时(2000ms)= **40 片 × 50ms**,每片喂心跳(监听退避路径) | -| **TC-TIMESYNC-005** | 不足一片(49ms)单片处理,**不**被拆成 0 | -| **TC-TIMESYNC-006** | 总预算 30s = **600 片**,片长上限**恒** 50ms | +| 0 ms | 不睡眠不喂狗(零开销快路径) | +| 恰一片 50 ms | 1 次 `vTaskDelay(50)` + 1 次心跳 | +| 非整片 120 ms | 切成 **50 / 50 / 20** 三片,每片喂一次心跳 | +| 200 ms 节拍 | **4 片 × 50 ms**,每片喂心跳 | +| 不足一片 49 ms | 单片处理,**不**被拆成 0 | +| 30 s 长等待 | **600 片**,片长上限**恒** 50 ms | -### 2.2 `time_sync_open_listener` — 监听建立与退避(TC-TIMESYNC-010..015) +### 2.2 `time_sync_task` — 任务主循环(`tc_task`) -| 用例 | 断言 | +生产 `time_sync_task` 是 `for(;;)`,桩在脚本耗尽时 `longjmp` 跳出后断言副作用 +(§4.1)。四个场景: + +| 场景 | 断言 | |---|---| -| **TC-TIMESYNC-010** | 首次成功:`socket` / `bind` / `listen` 各 1 次;端口经 `net_htons(8888)`;`sin_family` = `NET_AF_INET`;成功路径**无退避延时** | -| **TC-TIMESYNC-011** | `bind` 失败 → **关闭该 socket** + 退避 2000ms(40 片)+ 留痕 `DBG_ERROR`,随后重试成功 | -| **TC-TIMESYNC-012** | `listen` 失败 → 关闭该 socket + 退避 2000ms,随后重试成功 | -| **TC-TIMESYNC-013** | `socket` 连续失败 → **5 次后返回 -1**;5 条 `DBG_ERROR`;5 × 500ms 退避(50 片) | -| **TC-TIMESYNC-014** | 第 3 次重试成功并返回**新**描述符;恰好尝试 3 次;两次失败退避 500ms × 2 | -| **TC-TIMESYNC-015** | 心跳总次数 = 每轮重试(5)+ 每片延时(50);**心跳位恒为 `TASK_WD_TIMESYNC`** | +| **A · NULL 屏障 + 单轮**(`sys_evtHandle == NULL`,第 2 次读代次时跳出) | 跳过屏障直接进循环;代次读 2 次、`time_sntp_run` 1 次且收到代次 = 1;延时 4 片 × 50 ms;心跳 6 次(轮首 + 4 片 + 跳出前轮首);全部打 `TASK_WD_TIMESYNC` 位 | +| **B · 代次 1→2 当轮交接**(第 3 次读代次时跳出) | `time_sntp_run` 收到代次**逐轮 = 1、2**(代次变化当轮原样送达,客户端内部弃旧 Socket 自重建);延时 8 片;心跳 11 次 =(1+4)×2+1 | +| **C · 就绪屏障 2 次超时 + 1 次就绪**(跳出屏障后立即读代次失败跳出) | `osEventFlagsWait` 3 次、超时参 **100 ms**、选项 `osFlagsWaitAny \| osFlagsNoClear`(**就绪位不被消费**);未就绪两轮各补 1 次心跳 + 循环首行 1 次 = 3;`task_wd_bind` 已先登记心跳路由;SNTP 未跑(网络未就绪绝不出网) | +| **D · 屏障永不就绪**(第 4 次等待时跳出) | 等待 4 次全超时、循环未挂死(2026-10-02 台架根因回归防线);每次超时后各喂 1 次心跳 + 首行 = 3;**代次 0 读、SNTP 0 跑** | -### 2.3 `time_sync_handle_conn` — 时间包重组状态机(TC-TIMESYNC-020..029) +### 2.3 SNTP 驻留不拖垮心跳(`tc_sntp_dwell_then_continue`) -| 用例 | 断言 | -|---|---| -| **TC-TIMESYNC-020** | 8 字节一次收齐 → **大端**解析 epoch + 更新墙钟 1 次 + 关连接;首次请求 16 字节;单次等待片长 = 50ms;留痕 `wall clock updated` | -| **TC-TIMESYNC-021** | 分段送达(4+4)→ 累积后解析正确;读了两次;第二次**只请求剩余 12 字节**;每片 50ms | -| **TC-TIMESYNC-022** | 一次到达 16 字节 → **只取前 8 字节**;一次读够即停 | -| **TC-TIMESYNC-023** | 前缀非 `"TIME"` → **不更新墙钟**,但仍关闭连接 | -| **TC-TIMESYNC-024** | 对端关闭(`n == 0`)→ 立即退出、不解析;关闭连接 | -| **TC-TIMESYNC-025** | 真错误(`n == -1`)→ 立即退出、不解析 | -| **TC-TIMESYNC-026** | 只收到 4 字节后 EOF → **不足 8 字节不解析**;期间继续等待后续字节 | -| **TC-TIMESYNC-027** | 30s 预算耗尽 → 不再读(只读 1 次)+ 留痕 `no TIME packet in 30s, drop conn` + 关连接 | -| **TC-TIMESYNC-028** | 剩余预算不足一片 → **末片按剩余(20ms)切分** | -| **TC-TIMESYNC-029** | **32 位 tick 回绕** → 预算计算仍正确(无符号减法) | +`time_sntp_run` 第 2 次调用时跳出:第 1 轮完整走完(首行心跳 + 4 延时片)后第 2 轮 +再次进入客户端——**每轮节拍都回到 SNTP**,驻留(应答收片 ≤ 50 ms)之后循环照常续转, +心跳位恒为 `TASK_WD_TIMESYNC`。 -### 2.4 `time_sync_task` — 任务主循环(TC-TIMESYNC-040..044) - -| 用例 | 断言 | -|---|---| -| **TC-TIMESYNC-040** | `sys_evtHandle == NULL` → **跳过就绪屏障**;`accept` 轮询 3 次(2 次无连接 + 1 次成功);收到时间包并更新墙钟;延时片数 = 4+4+1(200+200+50ms)且**全部按 50ms 分片**;心跳次数 = 主循环 1 + 退避 8 + 收连接 1 + 收尾 1 + 次轮 1 = **13**;所有心跳均打 `TASK_WD_TIMESYNC` 位 | -| **TC-TIMESYNC-041** | 就绪屏障轮询 3 次(2 次未就绪 + 1 次就绪);屏障等待按 **100ms 分片**;等待 `SYS_EVT_NET_READY` 位;`osFlagsWaitAny \| osFlagsNoClear`(**就绪位不被消费**);未就绪的两次各喂一次心跳;仍先 `task_wd_bind` 登记心跳路由 | -| **TC-TIMESYNC-042** | 屏障等待 4 次均未就绪 → **循环未挂死**(跳出并继续);每次未就绪后各喂一次心跳(2026-10-02 台架根因的**回归防线**) | -| **TC-TIMESYNC-043** | 网络代次变化 → **关闭旧监听**并重监听;留痕 `network re-init, relisten`(另含 2 条 listen 成功);每次循环只轮询一次 `accept` 即放弃 | -| **TC-TIMESYNC-044** | 监听建立失败 → 内部重试 5 次后放弃;6 条 `DBG_ERROR`(5 条 `net_socket FAIL` + 1 条 `retry later`);退避 5×500ms + 2000ms = **90 片**;心跳次数 = 2 + 5 + 50 + 40 = **97**;所有心跳均打 `TIMESYNC` 位 | - -**当前结果**:80/80 PASSED(`failed=0`)。 +**当前结果**:33/33 PASSED(`failed=0`)。 --- @@ -182,11 +147,11 @@ test/host/time_sync/time_sync_test.c 1. **纯延时类**:直接调 `time_sync_delay_fed(ms)`,断言 `s_delay_calls`(片数)与 `s_delays[]`(每片时长)以及 `s_beat_calls`(心跳数)。 -2. **脚本化网络**:给 `s_gen_seq[]` / `s_evt_seq[]` / `recv_step_t` 数组填序列, - 再驱动被测函数。 -3. **跳出任务循环**:生产 `time_sync_task` 是 `for(;;)`,无法自然返回。桩在脚本耗尽时 - 调 **`longjmp(s_jmp, N)`** 强行跳出(见 §4.1),跳出后正常断言。 -4. `CHECK(cond, "描述")` 断言;在 `main()` 里注册(并同步更新本文档 §2)。 +2. **脚本化节拍**:给 `s_gen_seq[]` / `s_evt_seq[]` 数组填序列,再驱动任务循环。 +3. **跳出任务循环**:桩在脚本化调用计数达到预设值时调 **`longjmp(s_jmp, N)`** 强行跳出 + (见 §4.1),跳出后正常断言;SNTP 侧用 `s_sntp_run_longjmp_at`,代次侧用 + `s_gen_longjmp_at`,屏障侧用 `s_evt_longjmp_at`。 +4. `CHECK(cond, "语义标签")` 断言;在 `main()` 里注册(并同步更新本文档 §2)。 --- @@ -196,36 +161,30 @@ test/host/time_sync/time_sync_test.c `time_sync_task` 是永不返回的任务循环。本单测在 `main()` 里 `setjmp` 设跳转点, 桩在"脚本化调用计数达到预设值"时 `longjmp` 回来,然后断言循环期间产生的副作用 -(心跳次数 / 延时片数 / `accept` 次数 / 墙钟更新)。 +(心跳次数 / 延时片数 / SNTP 轮次与代次)。 **取舍**:这比"把循环体抽成可测函数"更贴近真实——**测的就是任务主体本身**, 不需要为了可测性改动生产代码结构。代价是**必须精确设定跳出点**:跳出点设错 -(多一次或少一次桩调用)会导致断言数不符,`TC-TIMESYNC-040` 的跳出点曾因此修正。 +(多一次或少一次桩调用)会导致断言数不符。 ### 4.2 ⚠ C 的"暂定定义"会静默合并同名静态量 -本文件曾出现一个**无任何编译告警**的缺陷:文件作用域下两处 `static int s_bind_calls;` +历史教训(2026-10-10,v1.0 套):文件作用域下两处 `static int s_bind_calls;` (一处给 `task_wd_bind` 桩、一处给 `net_bind` 桩)被 C 的**暂定定义(tentative -definition)规则静默合并为同一个对象**——于是 `task_wd_bind` 与 `net_bind` 共用一个计数器, -8 条断言失败(72/80)。 +definition)规则静默合并为同一个对象**,8 条断言失败且**无任何编译告警**。 -**教训**:同一编译单元内**所有** `static` 变量名必须唯一,哪怕是不同桩的"各自的"计数器。 -修复手法是把看门狗侧改名为 `s_wd_bind_calls` / `s_wd_bind_id_bad`,并加注释说明。 -扩展用例时**务必**先 `grep` 目标名字是否已被占用。 +**教训**:同一编译单元内**所有** `static` 变量名必须唯一。扩展用例时**务必**先 +`grep` 目标名字是否已被占用。 -### 4.3 桩不模拟真实 TCP 时序 +### 4.3 桩不模拟真实 UDP/SNTP 时序 -`net_recv_to` / `net_accept` 桩按脚本返回,**不**模拟真实的分段到达时刻、重传、窗口、 -CH395F 缓冲区边界。因此本单测能保证的是"**给定返回码序列,状态机反应正确**", -**不能**保证真实链路上的时序行为——后者属板端与 PC 对端(`test/host/time_sync_server.py`)职责。 +`net_recvfrom_to` 等由 `test/host/sntp/` 套覆盖报文级行为;本套的 `time_sntp_run` 桩 +只记录"被调了几次、收到什么代次"。因此本套能保证的是"**给定节拍与脚本,任务编排正确**", +**不能**保证真实链路的 SNTP 时序——后者属 `test/host/sntp/` 与板端职责 +(真机台架对时判据已登记,见墙钟说明 §16 与 #73)。 -### 4.4 探针窗口心跳缺口的实际余量仍由板端判定 +### 4.4 心跳缺口的实际余量仍由板端判定 -本单测把"每片喂心跳、片长 ≤ 50ms、退避值正确"这些**逻辑前提**钉死,但该缺口关注的 -是**端到端心跳缺口 vs IWDG 超时**——这还取决于真实调度延迟、`netTask` 排队时长、 -Flash 写耗时等主机侧无法建模的因素。故: - -- 本单测 ✅ 保证"逻辑上不会出现超过 50ms 的**无喂狗**窗口"; -- 板端 ✅ 保证"实测缺口 + 调度抖动仍小于 1701ms"。 - -两者都过,这个心跳缺口才能闭环。 +本单测把"每片喂心跳、片长 ≤ 50 ms、屏障每轮补心跳"这些**逻辑前提**钉死,但端到端 +心跳缺口 vs IWDG 最坏超时(1701 ms)还取决于真实调度延迟与 `netTask` 排队时长, +主机侧无法建模——板端 V5 长跑继续兜底(`../板级测试/实机测试实施指南.md` §7)。 diff --git a/docs/测试说明/板级测试/实机测试实施指南.md b/docs/测试说明/板级测试/实机测试实施指南.md index 45576f2e..c6fe43eb 100644 --- a/docs/测试说明/板级测试/实机测试实施指南.md +++ b/docs/测试说明/板级测试/实机测试实施指南.md @@ -243,7 +243,9 @@ $cli = if ($env:DTU_STLINK_CLI) { $env:DTU_STLINK_CLI } else { (Get-Command ST-L - **30s 周期重试**,不自驻留死循环; - 恢复后各 TCP 服务(FTP 21 等)**据代次号重建监听**,功能正常。 -**验证手段**:`test/host/e02_probe.py`(网络探针)+ UI 网口设置页改 IP。 +**验证手段**:FTP 客户端连 :21 + UI 网口设置页改 IP(观察代次号重建监听)。原网络探针 +`e02_probe.py` **已删除**(2026-11-04 随 :8888 退役,正文见 git 历史)——它当时挂的是 :8888 +time_sync 触发形态,退役后该触发场景不再存在。 **2026-10-03 台架结论**: - **正常路径 ✅**:V5 长跑(30 min,FTP 512 KB 上/下传)**120/120 PASS、零 IWDG 复位**, diff --git a/docs/测试说明/流程与规范/测试开发标准流程.md b/docs/测试说明/流程与规范/测试开发标准流程.md index f5f1fbf3..3d9fd703 100644 --- a/docs/测试说明/流程与规范/测试开发标准流程.md +++ b/docs/测试说明/流程与规范/测试开发标准流程.md @@ -276,7 +276,7 @@ static void phaseN_run(void) { | **`test/host/nand_port/run_unit_test.py`** | `test/host/nand_port/` | **主机侧纯逻辑单测**:离线编译真实 `Drivers/BSP/GD5F2GQ5UE/nand_ftl.c` + 真实 dhara,以 **RAM 假芯片**驱动,149 断言钉死 is_free 整页扫描与六个入口的越界守卫 | | **`scripts/ci_local.py`** | `scripts/` | **本地跑与 CI 等价的门禁**(静态门禁 + 主机侧纯逻辑单测):自动选用装了 pre-commit 的解释器;`.map` 自适应跳过 `mem-numbers`;退出码 0/1/2 与 CI 对齐。见 §9.3 | | `.serialtest/config.json` | 仓库 | **串口端口单一配置源**(当前 COM6 / 115200);换端口只改这个文件 | -| `test/host/ftp_*.py`(4 个)/ `ch395f_socket_test.py` / `time_sync_server.py` | `test/host/` | PC 端交互测试工具(FTP 客户端 / socket 回显 / 对时推送) | +| `test/host/ftp_*.py`(4 个)/ `ch395f_socket_test.py` | `test/host/` | PC 端交互测试工具(FTP 客户端 / socket 回显;原对时推送 `time_sync_server.py` 已随 :8888 退役删除,2026-11-04) | | `simulator/tests/*.py`(4 个) | `simulator/tests/` | LVGL 模拟器回归脚本(见 §5) | ### 4.2 主机侧纯逻辑单测(纯逻辑,无硬件) @@ -609,7 +609,6 @@ CI 就静默停摆(2026-10-11 实测 run 16~21 共 12 个 job 零日志)。 | `run_boot_test.py` / `verify_log.py` + `serial_tool` | 自动化闭环三件套(纯 Python 标准库 / 仓库内置) | | `ftp_test.py` / `ftp_capture.py` / `ftp_up_dl_test.py` / `ftp_speed_test.py` | FTP PC 端测试工具 | | `ch395f_socket_test.py` | CH395F PC 端 socket 回显 / 大文件传输工具 | -| `time_sync_server.py` | 对时推送 PC 端服务器 | ## 附录 B:模块测试文档 diff --git a/docs/环境与部署/项目部署环境要求.md b/docs/环境与部署/项目部署环境要求.md index 8fa88fec..1f6a3682 100644 --- a/docs/环境与部署/项目部署环境要求.md +++ b/docs/环境与部署/项目部署环境要求.md @@ -35,7 +35,8 @@ flowchart LR DEV -- "USB:下载器(ST-Link 等)" --> TGT DEV -- "USB 串口 USART1
COMx @115200 调试日志" --> TGT - TGT -- "以太网(CH395F)
FTP:21 / 对时:8888 / 显示:7003" --> RMT + TGT -- "以太网(CH395F)
FTP:21 / 远程显示:7003" --> RMT + TGT -. "对时:SNTP 客户端 UDP 123 出向(原 :8888 推送已退役)" .-> NTP["现场 NTP / 时间源"] ``` | 环境 | 角色 | 必需性 | @@ -567,7 +568,7 @@ $env:DTU_KEIL_UV4 = 'D:\NotExist\UV4.exe' # 仅当前会话,不动用户级 | 服务 | 协议/端口 | 用途 | 核对方法 | |---|---|---|---| | 远程显示(RemoDispBus) | TCP **7003** | 远端看图(160×160 1bpp,3200 B/帧,客户端 LCDMEM 拉取)+ 远端导航按键 | DTU-RemoteLCD 或任意 TCP 客户端连 7003 收帧 | -| 墙钟对时 | TCP **8888** | PC 主动推送时间到设备 | 对时推送后核对设备时间 / 日志时间戳 | +| 墙钟对时(SNTP 客户端) | UDP **123**(出向;主/备服务器配 `[sntp]` 段,enable 默认关) | 设备作 SNTP 客户端从时间源取时写回 RTC(原 PC 推送 TCP :8888 已于 2026-11-04 退役删除) | 使能对时后核对设备时间 / 日志时间戳 | | FTP | TCP **21** | 文件上传/下载(根目录 `/`,NAND 存储) | 任意 FTP 客户端列目录/读写 | > 现场组网要求:设备与客户端**同网段**(默认 .100/24、网关 .1);跨网段部署先按 §4.1 改 IP 再上线。 @@ -575,7 +576,7 @@ $env:DTU_KEIL_UV4 = 'D:\NotExist\UV4.exe' # 仅当前会话,不动用户级 ### 4.3 存储与运行时 - 文件系统:FatFS(FAT32)挂 NAND;**上电无卷自动格式化**(`f_mkfs`);格式化会清除卷上全部文件(含网口配置 `/config.ini`) -- RTC:SD2506 芯片(驱动 `Drivers/BSP/SD2506/`)提供墙钟基准与日志时间戳;首次使用建议先经 8888 对时 +- RTC:SD2506 芯片(驱动 `Drivers/BSP/SD2506/`)提供墙钟基准与日志时间戳;首次使用建议配 `[sntp]` 段启用 SNTP 对时(或 UI 手工对时) - 启动时序:`app_main_init`(存储 → 时钟 → RS485 → NAND → ADC → 启动转换)→ 就绪屏障(STORAGE / NET / FTP / TIMESYNC 事件)→ 六任务运行;详见 `docs/架构设计/项目启动顺序管理.md`、`docs/架构设计/项目架构总览.md` ### 4.4 现场验收清单 @@ -587,7 +588,7 @@ $env:DTU_KEIL_UV4 = 'D:\NotExist\UV4.exe' # 仅当前会话,不动用户级 | 3 | 远程显示 | DTU-RemoteLCD 连 7003 | 出图(160×160 1bpp)+ 远端菜单导航可用 | | 4 | FTP | FTP 客户端连 :21 | 列目录 / 读写 `/` 下文件 | | 5 | UI 功能 | 屏幕 / 远端操作 | 菜单 7 项导航、内容页、密码(查看 `000` / 修改 `123`)、网口设置保存后立即生效 | -| 6 | 对时 | PC 推送 8888 | 设备墙钟更新、日志时间戳正确 | +| 6 | 对时 | 配 `[sntp]` 段启用 SNTP(或 UI 手工对时;原 PC 推送 8888 已退役) | 设备墙钟更新、日志时间戳正确 | | 7 | 掉电保持 | 写文件 / 改网口配置 → 重启 | 数据仍在(`/config.ini` 按新值生效) | | 8 | LED 状态 | 目检 | 6 路 LED 随系统状态变化(低电平点亮) | @@ -599,7 +600,7 @@ $env:DTU_KEIL_UV4 = 'D:\NotExist\UV4.exe' # 仅当前会话,不动用户级 |---|---|---| | 操作系统 | 任意(Windows / Linux / macOS,有浏览器或 Python 即可) | 与设备同网段 | | 远程显示客户端 | **DTU-RemoteLCD**(独立项目:Flask + WebSocket 参考实现)或任意符合 RemoDispBus 协议的 TCP 客户端 | 连设备 **TCP 7003**;协议见 `docs/UI设计/RemoDispBus协议与接收设计说明.md` | -| 对时工具 | 任意可 TCP 推送的工具/脚本 | 向设备 **8888** 推送 | +| 对时时间源 | 现场可达的 SNTP/NTP 服务器(配 `[sntp]` 段 server1/server2) | 设备作客户端 UDP/123 出向取时;原「向设备 8888 推送」的 TCP 工具随退役不再需要 | | FTP 客户端 | 任意(资源管理器 / FileZilla 等) | 设备端 lftpd,端口 21 | | 浏览器 | Chrome / Edge | 查看参考客户端 Web 页;开发期浏览文档 SVG 图 | diff --git a/docs/算法/继电保护功能复现规划.md b/docs/算法/继电保护功能复现规划.md index c3a03d53..cc575d03 100644 --- a/docs/算法/继电保护功能复现规划.md +++ b/docs/算法/继电保护功能复现规划.md @@ -87,7 +87,7 @@ | 遥信 | 双位置遥信上**双点**,其余**单点**;点号 `0+` | | 遥控 | **双点命令**,合闸选择 `82H`、执行 `02H`;点号 `6000H+` | | **RS-485** | **对下 ModBus RTU 采集**(下行采集口,非上行口) | -| 对时 | 点表要求 **SNTP `192.168.1.121:5105`**;本项目**保留自定义 TCP 8888**,SNTP 本期不实现(产品定义中的对时方式) | +| 对时 | 点表要求 **SNTP `192.168.1.121:5105`**;本项目 **SNTP 客户端已落地**(2026-11-04,#73:`[sntp]` 段配服务器、**固定 UDP/123**),原自定义 TCP 8888 **已退役删除**(#78)——点表的**非标端口 5105 尚不满足**(客户端端口不可配),要按点表对接需给 `[sntp]` 增「服务器端口可配」小改 | | 固有参数 | `8001H~800AH`:终端类型/操作系统/制造商/硬件版本/软件版本/校验码/规约类型/型号/ID/MAC | | 运行参数 | `8020H~8058H`:死区(电流/交流电压/直流电压/功率/频率/功率因数)、PT/CT 变比、负荷越限/重载/过载报警、录波投退与定值、功率置负 | | 定值 | `8820H~8882H`:过流 I/II/III 段、零序过流 I/II 段、重合闸、低压/低频、励磁涌流闭锁、小电流接地等 | @@ -127,7 +127,7 @@ | # | 事项 | 处置 | | --- | --- | --- | | 产品定义中的后备电源与直流遥测裁剪 | `UBA` 后备电源电压、`V1/V2` 直流量三路遥测的硬件来源 | **本期不做**——V0.1 无对应硬件通道,不加子板/通道;点表此三项本期空缺 | -| 产品定义中的对时方式 | 对时 | **保留自定义 TCP 8888**;点表要求的 **SNTP 5105 本期不实现** | +| 产品定义中的对时方式 | 对时 | **SNTP 客户端已落地**(2026-11-04,#73);原自定义 TCP 8888 已退役删除(#78);点表要求的 **SNTP 5105 非标端口仍未实现**(客户端固定 UDP/123,端口不可配) | | 产品定义中的本期保护范围 | 保护/控制范围 | **本期 = 骨架全做 + 有效通道子集闭环**(2026-10-10 用户确认),评估见 §1.6 | > **产品定义 至此全部闭环**:上行 IEC 60870-5-104 / 持续采集 / 1000 Hz 已定(§1.5 表 1~4);产品定义中的后备电源与直流遥测裁剪 / 产品定义中的对时方式 本期不做或维持现状;产品定义中的本期保护范围 的范围裁剪见 §1.6。 diff --git a/docs/网络/BSD_Socket_API_使用指南.md b/docs/网络/BSD_Socket_API_使用指南.md index ed8c4685..b6f1422f 100644 --- a/docs/网络/BSD_Socket_API_使用指南.md +++ b/docs/网络/BSD_Socket_API_使用指南.md @@ -890,7 +890,7 @@ void net_generation_inc(void); | `net_get_generation` | 获取当前网络代次号(首次 `net_init` 成功前为 0,每次网络重建 +1) | | `net_generation_inc` | 代次号加 1(netTask 重建成功、网络已可用后调用) | -**用途**:长驻 TCP 服务(FTP :21 / 对时 :8888 / 远程显示 :7003)在监听循环中检测代次号变化后主动重建监听 Socket,网口配置立即生效后可自动在新 IP 上恢复(重建会断开既有连接,属预期行为)。详见 `docs/子系统设计/网络配置说明.md` §4。 +**用途**:长驻 TCP 服务(FTP :21 / 远程显示 :7003;对时 :8888 已退役,2026-11-04)与 UDP 出向客户端(SNTP,代次变化弃旧 socket 懒重建)在循环中检测代次号变化后主动重建,网口配置立即生效后可自动在新 IP 上恢复(重建会断开既有连接,属预期行为)。详见 `docs/子系统设计/网络配置说明.md` §4。 #### net_poll - 状态轮询 diff --git a/docs/网络/Net层阻塞模型架构设计.md b/docs/网络/Net层阻塞模型架构设计.md index 016c1a50..12cf0f97 100644 --- a/docs/网络/Net层阻塞模型架构设计.md +++ b/docs/网络/Net层阻塞模型架构设计.md @@ -613,7 +613,7 @@ net_pending_capture_owner() ← 捕获执行者 | 指标 | 值 | 来源 / 条件 | |---|---|---| -| 队列响应性(挂起连接下) | **92.5 ms** | `test/host/e02_probe.py`:挂起对时客户端 + FTP banner 探测 | +| 队列响应性(挂起连接下) | **92.5 ms** | 台架实测(探针 `e02_probe.py` 已删除——2026-11-04 随 :8888 退役,正文见 git 历史):挂起对时客户端 + FTP banner 探测 | | M3 静默档帧周期 | med 58.9 / p95 69.9 / max 72.1 ms | `docs/测试说明/专项验证/链路延迟测试说明.md` §6.3(零尖峰) | | M3 压力档帧周期 | med 60.1 / p95 66.4 / max 69.3 ms | 同上(零尖峰) | | netTask 栈峰值 | **1116 B / 3840 B(余 70%)**,FTP 传输期峰 **1164 B**;静态最坏链 ≈1.9 KB | `docs/内存管理/RAM使用分析与优化方案.md` §2.3 | diff --git a/docs/网络/网络自愈架构设计.md b/docs/网络/网络自愈架构设计.md index d49af0df..67baff55 100644 --- a/docs/网络/网络自愈架构设计.md +++ b/docs/网络/网络自愈架构设计.md @@ -32,7 +32,8 @@ > - 带连字符的 `E-` = **历史审计发现**编号(属主文档已于 2026-10-15 删除, > 编号冻结于 `docs/编号规范.md` §4.3)—— > §5.2 末行引用它是为了记录该发现的**收口台架**,**不是**实验编号 -> (证据:`test/host/e02_probe.py` 的文档串自述它是该审计发现的 timeSync 触发形态探针); +> (证据:`test/host/e02_probe.py`(已随 :8888 退役于 2026-11-04 删除,正文见 git 历史) +> 的文档串自述它是该审计发现的 timeSync 触发形态探针); > - 该审计编号族已冻结、不再新增,引用一律改说人话。 ## 1. 为什么是这个架构 @@ -111,7 +112,8 @@ fd 别名让这个边沿**必须有人接**,而"谁重建监听"在类型上 1. **不支持多监听**——`docs/问题记录/CH395F_Trap_Records.md` 附 FTP 改造清单第 4 条 「CH395F 多连接模式不支持多监听(类型:架构)」,处置即 `fun_para=0x00`。 - 本产品有 21/8888/7003 **三个常驻监听** + 会话期动态 PASV 监听。 + 本产品有 21/7003/2404 **三个常驻监听** + 会话期动态 PASV 监听(当时第三个是 :8888 对时, + 2026-11-04 退役后由 104 :2404 在列——结论不依赖具体端口组合,见墙钟说明 §15)。 2. 池分配按**源端口匹配**(`docs/芯片手册/CH395F.md` §9.2.6 L1295),多监听器的池会互相串。 3. 实测:「多连接模式下最多接受 3 个客户端(Socket 1~3),Socket 4~7 的 CONNECT 中断从不触发」、 「多个 Socket 数据互相覆盖」——即 TRAP-NET-02/-11。 @@ -612,8 +614,9 @@ DISCONNECT 会被 `(void)` 丢掉。但 RECV_OK 是条件位(下轮补回) > 「假设 → 方法 → 判据 → 它门禁哪个决策」四段式;本表只给**方法与结论**,原始记录(假设、 > 原始日志、统计)已归档,取用命令见 §12。 > ⚠ **这些编号是冻结标识**:`net_socket.c` / `net_events.c` / `net_listener.c` 注释、 -> `test/host/e02_probe.py`、`test/host/e6_soak.py`、`docs/问题记录/CH395F_Trap_Records.md` -> 都直接引用它们,**重排或改名会断链**。 +> `test/host/e6_soak.py`、`docs/问题记录/CH395F_Trap_Records.md` +> 都直接引用它们(原 `test/host/e02_probe.py` 已删除——2026-11-04 随 :8888 退役,正文见 git 历史), +> **重排或改名会断链**。 > ⚠ 末行 **不是实验编号**——它记录的是**历史审计发现** > (带连字符的 `E-` 族,属主文档已于 2026-10-15 删除并冻结),列在此处只为记录该发现的**收口台架**。 > 区分规则:**带连字符 = 审计发现,无连字符 = 自愈实验**。 @@ -640,7 +643,7 @@ DISCONNECT 会被 `(void)` 丢掉。但 RECV_OK 是条件位(下轮补回) | **E17** | **单一所有者收口**:`net_listener_try`(当场动作、**无状态门**,各调用点自带门)改为 `net_listener_request`(只登记意图)+ `net_listener_tick`(唯一执行者,门集含"芯片已在 LISTEN 即了结"),同板全量回归 | 全绿;**延后一轮(≤1 ms)执行不伤即时性**(恢复窗 p50 与当场动作持平);relisten 留痕减半 | §4.2 登记/执行分离;§9.2 门集;§11.1 判据 | | **E18** | **负向注入打靶 · 三轮**(注入代码不入库):① 吞掉 DISCONNECT(在 i104 的 200 ms accept 轮询窗内 RST);③ 绕过本层簿记直接 close 芯片空闲监听;⑤ 开盲区吞掉芯片自发的中断 + 等芯片真 CLOSED 再 open+listen | **①③⑤ 全部命中并救回**;①⑤ 的前提会被更快的消费者路径消解 → 常态 0 命中;⑤ 还要先越过芯片 `FIN_WAIT_2`(E15 的 2MSL 长尾,实测 ≈1 s) | §4.1.4 / §4.1.5:**三条均不可降级为只留痕**;⑤ 的**取消挂起 recv/send** 不可省 | | **E19** | **`force open` 反事实 A/B**(本层已收口 CLOSED + 芯片停 `FIN_WAIT_2` `0x05/0x06` + **对端挂住不关** = 队头阻塞场景;A = 现状放行,B = 只留痕不放行) | **放行与不放行的端口恢复时刻同一分布** → 动作在全部可产现场景内无可测收益;真正兜住"监听器永久失效"的是 **D10 预算 + 形态④ 重新登记**。两条实测坑:① 本层直接改 `state` **不会**进沉降窗(窗只由 `net_listener_settle` 开启);② `relisten retry budget exhausted` 在此被稳定复现 = 对端不配合拆链的签名,**非回归** | §4.3:800 ms 上限降级为**只留痕**(`keep waiting` 只留首末、隔离标志不清 → 继续排渣) | -| **队头阻塞审计**(非实验编号) | `test/host/e02_probe.py` 台架定案 `net_drop_conn` 收口后的时序 | 对端配合(收到 FIN 后其内核 close):`TIME_WAIT` ~150 ms → 形态④ relisten,恢复全程 ~500 ms;对端内核存活但不 close:芯片停 `FIN_WAIT_2` 长尾,**监听恢复受限于对端 close** | §4.6 | +| **队头阻塞审计**(非实验编号) | `net_drop_conn` 收口后的时序由台架定案(探针 `e02_probe.py` 已删除,2026-11-04 随 :8888 退役,正文见 git 历史) | 对端配合(收到 FIN 后其内核 close):`TIME_WAIT` ~150 ms → 形态④ relisten,恢复全程 ~500 ms;对端内核存活但不 close:芯片停 `FIN_WAIT_2` 长尾,**监听恢复受限于对端 close** | §4.6 | ## 6. 红线(明确不做的事) @@ -723,15 +726,18 @@ DISCONNECT 会被 `(void)` 丢掉。但 RECV_OK 是条件位(下轮补回) | `net_listener_armed` | 唯一监听门控;**必须放行 ESTABLISHED**(§4.2) | | `net_listener_tick`(唯一执行者) | 门集**只在这一处**:状态 ∈ {LISTENING, CLOSED} + 监听型 + **芯片已在 LISTEN 即视为达成、绝不 open**(U12 结构性防护)+ 挂起保险。登记侧 `net_listener_request` 因此**不需要**状态门——任何"当场动作 + 调用点各自补门"的写法都会把 U12 风险复制 N 份(对已 LISTENING 的 fd 再 open+listen 会因 USE 错误走进关闭分支、**关掉正听着新 SYN 的 Socket**) | -### 9.3 三个服务的连接结束契约(现状) +### 9.3 长驻网络形态的连接结束契约(现状) | 服务 | 连接结束信号 | 监听重建者 | 备注 | |---|---|---|---| | `lftpd` 控制 :21 | `read_line != 0`;阻塞挂起 recv 保证 CLOSED 时必被结束(FIN→0 / 异常→CONNRESET) | net 层;应用**永不** close 控制 fd | 契约一致,**正面样板** | -| `time_sync` :8888 | EOF / 预算耗尽 → `net_drop_conn` 拆会话保监听 | net 层(形态④) | 同 fd 即监听 fd,应用侧**不得** `net_close`(红线 8)→ 走 §4.6 | | `ui_net_sink` :7003 | `net_recv` 返 0 或真错误;非阻塞收帧 | net 层 | 重建条件**不得**写成 `net_get_sock(g_listen)==NULL`(fd 不释放 → 恒假);靠 `session_dead` 电平 + 形态④ | | 模拟器后端 | `recv` 0 恒可得、fd 分离 | 无需 | Winsock 原生;对上述全部失效模式**零覆盖**(D6:双模型分裂暂不消除) | +> **历史行(:8888,2026-11-04 退役删除)**:`time_sync` :8888 | EOF / 预算耗尽 → `net_drop_conn` +> 拆会话保监听 | net 层(形态④)| 同 fd 即监听 fd,应用侧**不得** `net_close`(红线 8)→ 走 §4.6。 +> 退役后本表不再有应用侧 `net_drop_conn` 调用者(原全仓唯一调用者 `time_sync.c` 已随监听删除)。 + ## 10. 决策记录 | # | 决定 | 依据 | diff --git a/scripts/ram_baseline.tsv b/scripts/ram_baseline.tsv index adea5a7c..406a1379 100644 --- a/scripts/ram_baseline.tsv +++ b/scripts/ram_baseline.tsv @@ -16,4 +16,4 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi 2026-10-30 246720 29836 2052 149708 276556 26.4 94336 72.0 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6432;nand_ftl.o=6216;wave_ring.o=5296;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 2026-11-02 247548 29836 2052 149740 277384 26.5 94368 72.0 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6802;ui_port.o=6432;nand_ftl.o=6216;wave_ring.o=5296;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 2026-11-03 249112 29836 2052 149996 278948 26.6 94624 72.2 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6802;ui_port.o=6432;nand_ftl.o=6216;wave_ring.o=5296;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 -2026-11-04 254648 30152 2072 150088 284800 27.2 94736 72.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6802;ui_port.o=6432;nand_ftl.o=6216;wave_ring.o=5296;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 +2026-11-04 253428 30084 2072 150088 283512 27.0 94736 72.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6802;ui_port.o=6432;nand_ftl.o=6216;wave_ring.o=5296;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 diff --git a/scripts/size_attestation.json b/scripts/size_attestation.json index 19a5be08..52635ad7 100644 --- a/scripts/size_attestation.json +++ b/scripts/size_attestation.json @@ -1,17 +1,17 @@ { - "built_at": "2026-11-04T16:20:20+08:00", - "fingerprint": "6b401a3591de1df1ce9d532c275648650896fb04a175f90b3a0caa576c6c6359", + "built_at": "2026-11-04T17:59:02+08:00", + "fingerprint": "ab479959f5fd86c89166faf8436055a853db5040350937b93132d527e51f0b30", "fingerprint_files": 1479, "fingerprint_source": "git-index", "gated_regions": [ "RW_IRAM1", "RW_CCM" ], - "git_head": "10322208fd0908b2ef04292833d4bc2b417e3534", + "git_head": "ad7338365c3ac380c415fb821c464435f81da854", "map": "MDK-ARM/STM32F407-Demo/STM32F407-Demo.map", - "map_mtime": 1793780256, + "map_mtime": 1793785414, "pct": { - "ER_IROM1": 27.16, + "ER_IROM1": 27.04, "RW_CCM": 87.89, "RW_IRAM1": 72.28, "UNINIT_CCM": 18.75 @@ -19,7 +19,7 @@ "regions": { "ER_IROM1": { "max": 1048576, - "size": 284800 + "size": 283512 }, "RW_CCM": { "max": 65280, @@ -37,8 +37,8 @@ "schema": 2, "scope_note": "\u540c\u7801\u51ed\u8bc1\uff1a\u6307\u7eb9\u8986\u76d6\u56fa\u4ef6\u4f53\u79ef\u76f8\u5173\u6e90\u7801\uff0c\u53e3\u5f84\u89c1\u672c\u811a\u672c docstring", "sizes": { - "code": 254648, - "ro_data": 30152, + "code": 253428, + "ro_data": 30084, "rw_data": 2072, "zi_data": 150088 }, diff --git a/scripts/test_coverage_baseline.json b/scripts/test_coverage_baseline.json index cae6e139..2b3ed8a5 100644 --- a/scripts/test_coverage_baseline.json +++ b/scripts/test_coverage_baseline.json @@ -218,11 +218,11 @@ }, "App/sync/time_sync.c": { "func_rate": 1.0, - "funcs_covered": 4, - "funcs_valid": 4, + "funcs_covered": 2, + "funcs_valid": 2, "line_rate": 1.0, - "lines_covered": 101, - "lines_valid": 101 + "lines_covered": 20, + "lines_valid": 20 }, "App/task/iec104_serv.c": { "func_rate": 0.75, @@ -749,9 +749,9 @@ }, "time_sync": { "files": { - "App/sync/time_sync.c": 101 + "App/sync/time_sync.c": 20 }, - "lines_total": 101, + "lines_total": 20, "ok": true }, "ui_data": { @@ -876,12 +876,12 @@ "files_generated_excluded": 5, "files_tested": 54, "files_untested": 177, - "func_rate": 0.9292, - "funcs_covered": 643, - "funcs_valid": 692, - "line_rate": 0.9018, - "lines_covered": 8633, - "lines_valid": 9573, + "func_rate": 0.929, + "funcs_covered": 641, + "funcs_valid": 690, + "line_rate": 0.901, + "lines_covered": 8552, + "lines_valid": 9492, "modules_ok": 48, "modules_total": 48 }, @@ -1414,10 +1414,6 @@ "code_lines": 12, "path": "Drivers/BSP/UI/ui_net_sink_backend.h" }, - { - "code_lines": 11, - "path": "App/sync/time_sync.h" - }, { "code_lines": 11, "path": "App/task/net_task.h" @@ -1518,6 +1514,10 @@ "code_lines": 10, "path": "App/fs/vol_idle_sync.h" }, + { + "code_lines": 10, + "path": "App/sync/time_sync.h" + }, { "code_lines": 10, "path": "App/task/default_task.h" diff --git a/test/host/README.md b/test/host/README.md index 8a40cba5..0bc1ab00 100644 --- a/test/host/README.md +++ b/test/host/README.md @@ -10,7 +10,6 @@ |---|---| | `run_boot_test.py` | **`/serial-boot-test` 一键闭环**:开串口+清日志 → 烧录 → wait-idle → `verify_log` 校验(退出码 0=PASS) | | `verify_log.py` | **日志标记校验**:Required / Forbidden 标记 → PASS / FAIL;可选 `--require-suite-summary` 校验套件汇总行(`=== <名称>: N/M PASSED (failed=0) ===` 且 `N>0`),抓"没跑到汇总行/断言数为 0"的盲区 | -| `e02_probe.py` | 头阻塞专项探针 | ### 网络 / FTP @@ -38,7 +37,9 @@ | 脚本 | 作用 | |---|---| | `latency_peer.py` | 链路时延测试的 PC 对端(peer) | -| `time_sync_server.py` | 时间同步推送服务(对时源) | + +> 原 `time_sync_server.py`(:8888 推送对时源)已随 :8888 退役删除(2026-11-04,#78); +> 对时改为板端 SNTP 客户端出向(墙钟说明 §16),台架用真实/私有 NTP 源即可。 ### 主机侧单元测试(纯逻辑,无硬件) @@ -62,9 +63,9 @@ | `vol_lock/run_unit_test.py` | **卷锁单测**:离线编译真实 `App/fs/vol_lock.c`,33 断言点覆盖创建 / 获取 / 释放契约(**fail-open 不变量**:创建失败仍返回 1 且零阻塞)/ 幂等 / 超时透传 | | `vol_lock/vol_lock_test.c` | 测试单元本体(`#include` 生产源码 + CMSIS 互斥桩) | | `vol_lock/stub/` | 桩头文件(`cmsis_os.h`) | -| `time_sync/run_unit_test.py` | **时间同步单测**:离线编译真实 `App/sync/time_sync.c`,80 断言点覆盖分片喂狗延时 / 监听退避 / 时间包重组(半包·粘连·EOF·真错·预算耗尽·tick 回绕)/ 任务主循环(**探针窗口心跳缺口的全部逻辑贡献者**) | +| `time_sync/run_unit_test.py` | **时间同步任务载体单测(v2.0)**:离线编译真实 `App/sync/time_sync.c`,33 断言点覆盖分片喂狗延时 / 就绪屏障(100 ms 分片、`osFlagsNoClear`、永不就绪不挂死)/ SNTP 节拍驱动与**网络代次当轮交接** / 未出网守门(:8888 退役后任务即纯校时节拍宿主) | | `time_sync/time_sync_test.c` | 测试单元本体(`#include` 生产源码 + 网络 / 事件 / RTOS 可脚本化桩 + `longjmp` 跳出 `for(;;)` 任务循环) | -| `time_sync/stub/` | 桩头文件(`FreeRTOS.h` / `task.h` / `cmsis_os2.h` / `net_socket.h` / `sys_clock.h` / `sys_events.h` / `dbg_log.h`) | +| `time_sync/stub/` | 桩头文件(`FreeRTOS.h` / `task.h` / `cmsis_os2.h` / `net_socket.h`(裁剪至 `net_get_generation`,**编译期绊线**:生产码若再引监听类 API 立刻炸)/ `sys_events.h`) | | `iec104/run_unit_test.py` | **IEC 60870-5-104 协议栈单测(IEC 104 上行)**:离线编译真实 `App/protocol/iec104/` 三模块 + 数据源(prot_set/prot_elm/phasor/sample_ring),**82** 用例覆盖 CP56 往返 / APCI 半帧·粘帧·坏帧 / STARTDT 握手 / 总召分片+RBT / COV 死区 / k=12 窗满 / w=8 触发 S / t1·t2·t3 / N(R) 倒退判故障 / 遥控·定值两段式与 PN / C_CS / 未知类型·地址错误应答 | | `nand_port/run_unit_test.py` | **dhara NAND 移植层单测**:离线编译真实 `Drivers/BSP/GD5F2GQ5UE/nand_ftl.c` + 真实 `Lib/dhara/{journal,map}.c`,以 **RAM 假芯片**驱动,**149** 断言覆盖 is_free 整页扫描 / 六个 HAL 入口的越界守卫 / 故障注入 / dhara map"写入 → 重新 resume"场景 | | `nand_port/nand_port_test.c` | 测试单元本体(`#include` 生产源码 + RAM 假芯片;内联一份"只看页首 64 B"的**旧算法对照**,证明断言能咬人) | @@ -139,7 +140,8 @@ - 本目录**被 pre-commit 门禁的 cppcheck / 代码规范排除**(`test/` 豁免); 但各模块的 `run_unit_test.py` 自身**作为独立钩子**参与门禁(见上,共 22 个) - 台架探针族分工(2026-10-13 起):**自愈/Trap 常备回归腿在本目录**(`relisten_stress` / - `ftp_abort_probe` / `n30_repro` / `e02_probe` / `e6_soak` 等);`MDK-ARM/*.py` 只余被文档**当作 + `ftp_abort_probe` / `n30_repro` / `e6_soak` 等;`e02_probe` 已随 :8888 退役删除,2026-11-04); + `MDK-ARM/*.py` 只余被文档**当作 可信判据引用过**的三件(`ftp_freeze_probe` / `tcp_reconnect_probe` / `bench_verdict`)—— 2026-10-22 清掉了一批既无人引用、又因设备侧启用鉴权而再也登录不上的历史一次性探针 (`MDK-ARM/ftp_retr_abort_probe.py` 与整个 `MDK-ARM/bench_diag/`(4 个诊断脚本)),需要时从 @@ -157,5 +159,5 @@ - `docs/测试说明/单元测试/ADC输入层单元测试说明.md` — ADC 输入层单测(逐 Rank 钉死映射) - `docs/测试说明/单元测试/相量层单元测试说明.md` — 相量层单测(傅氏系数 / 双区交换) - `docs/测试说明/单元测试/卷锁单元测试说明.md` — 卷锁单测(fail-open 不变量) -- `docs/测试说明/单元测试/时间同步单元测试说明.md` — 时间同步单测(探针窗口心跳缺口逻辑贡献者) +- `docs/测试说明/单元测试/时间同步单元测试说明.md` — 时间同步任务载体单测 v2.0(SNTP 节拍 / 代次交接 / 未出网守门) - `docs/测试说明/板级测试/CH395F_Test_Guide.md`、`FTP_Test_Guide.md` diff --git a/test/host/e02_probe.py b/test/host/e02_probe.py deleted file mode 100644 index f5c611b8..00000000 --- a/test/host/e02_probe.py +++ /dev/null @@ -1,112 +0,0 @@ -#!/usr/bin/env python3 -"""头阻塞专项台架探针(timeSync 侧,A/B 对照用)。 - -场景(历史审计的头阻塞触发形态,属主文档已于 2026-10-15 删除): - 1. 一个"挂起"的对时客户端连上 :8888 后不发任何数据(空闲/故障客户端, - 常见工况)——旧固件在此触发 30s 阻塞 net_recv 独占 netTask, - 全体消费者的队列消息停摆最长 30s; - 2. 2s 后从 PC 发起 FTP(:21) 连接,测 banner 到达时延 = 队列响应性: - 修复前 ≈ 25~30s(队头冻结),修复后 ≈ 数百 ms 内; - 3. 34s 时 30s 总预算应已耗尽:设备拆掉该会话(net_drop_conn: - FIN 关闭 + 本层收口 + 重监听),随后**本探针模拟守规矩对端**: - close 自己的 socket; - 4. 芯片回 CLOSED(TC-NET-204 模式 A:TIME_WAIT 过渡 ~150ms)→ relisten, - :8888 应能再接受新连接(3s 重试窗内);新连接发完整 8 字节 TIME 包 → - 设备应落 "wall clock updated"。 - -⚠ 定案口径(TRAP-NET-07 定案更正 2026-10-11 + 《网络自愈架构设计》§5.2 E13):CH395F close -**无 RST 语义**。若对端内核存活(app 挂起但 socket 不关,内核自动 ACK FIN), -芯片停在 FIN_WAIT_2、监听恢复受限于对端 close——探针第 3 步的 close 正是 -模拟"对端配合"这一可恢复边界;不配合场景的兜底是芯片 TIMEOUT/keepalive -拆僵尸会话(E13 定案,30~90s)。 - -用法: python e02_probe.py -退出码: 0 = 队列响应性 PASS;1 = FAIL;2 = 探针自身异常 -""" -import socket -import sys -import time - -import bench_defaults # 台架连接默认值单一真源 - -DEV = bench_defaults.DEVICE_IP -tag = sys.argv[1] if len(sys.argv) > 1 else "run" - -t0 = time.time() - - -def log(msg): - print("[%s] %9.3f %s" % (tag, time.time() - t0, msg), flush=True) - - -def main(): - # 1. 挂起对时客户端:连接后不发任何数据 - ts = socket.create_connection((DEV, 8888), timeout=5) - log("time-sync conn open (hung client: sends nothing)") - - # 2. 2s 后测 FTP banner 时延(队列响应性) - time.sleep(2.0) - t1 = time.time() - banner = None - try: - ftp = socket.create_connection((DEV, 21), timeout=35) - banner = ftp.recv(256) - dt = time.time() - t1 - log("FTP banner after %8.1f ms: %r" % (dt * 1000, banner[:40])) - ftp.close() - except Exception as e: - dt = time.time() - t1 - log("FTP connect FAIL after %8.1f ms: %r" % (dt * 1000, e)) - dt = None - queue_ok = (banner is not None) and (dt is not None) and (dt < 3.0) - log("queue responsiveness: %s (banner %s ms)" - % ("PASS" if queue_ok else "FAIL", - ("%.0f" % (dt * 1000)) if dt is not None else "n/a")) - - # 3. 等 30s 总预算耗尽(设备应拆会话:FIN + 本层收口 + 重监听) - remain = 34.0 - (time.time() - t0) - if remain > 0: - time.sleep(remain) - - # 3b. 模拟守规矩对端:设备已发 FIN(drop_conn),本端 close 配合拆链。 - # CH395F close 无 RST 语义(TRAP-NET-07 定案更正):不配合的内核存活对端 - # 会把芯片停在 FIN_WAIT_2,监听恢复受限于对端 close——不在本探针判据内。 - try: - ts.close() - log("hung client socket closed (cooperate with device FIN)") - except OSError: - pass # 设备已先拆连,本地 close 报 ECONNRESET 属预期 - - # 4. 监听应已重建(芯片 TIME_WAIT 过渡 ~150ms + relisten):3s 重试窗 - t2 = time.time() - listener_ok = False - ts2 = None - while time.time() - t2 < 3.0: - try: - ts2 = socket.create_connection((DEV, 8888), timeout=2) - break - except Exception as e: - log("second time-sync conn retry: %r" % (e,)) - time.sleep(0.3) - if ts2 is not None: - dt2 = time.time() - t2 - epoch = int(time.time()) - log("new time-sync conn in %6.0f ms, sent full TIME packet (epoch=%d)" - % (dt2 * 1000, epoch)) - ts2.sendall(b"TIME" + epoch.to_bytes(4, "big")) - time.sleep(1.5) # 留时间给设备处理并打日志 - ts2.close() - listener_ok = True - else: - log("second time-sync conn FAIL (3s window)") - log("OVERALL: queue=%s listener=%s" - % ("PASS" if queue_ok else "FAIL", "PASS" if listener_ok else "FAIL")) - return 0 if (queue_ok and listener_ok) else 1 - - -if __name__ == "__main__": - try: - sys.exit(main()) - except Exception as e: - print("[%s] PROBE ERROR: %r" % (tag, e), flush=True) - sys.exit(2) diff --git a/test/host/ftp_auth_bench.py b/test/host/ftp_auth_bench.py index 715e81d6..7c0d1e2b 100644 --- a/test/host/ftp_auth_bench.py +++ b/test/host/ftp_auth_bench.py @@ -14,7 +14,8 @@ T7 会话结束重连、错口令一次 → 期望 530 且计数已清零(T5 成功清零) 注意:CH395F 单连接——每步用完必须 quit() 释放 socket,否则下一步连不上; - 跑之前确保 UI 远程显示(7003)/对时(8888)客户端未占用 socket。 + 跑之前确保 UI 远程显示(7003)客户端未占用 socket(对时 :8888 已退役, + SNTP 只占 UDP 通道,与 TCP 单连接语义无关)。 用法(设备默认见 bench_defaults.DEVICE_IP,出厂口令 admin/admin): & '.venv\\Scripts\\python.exe' test/host/ftp_auth_bench.py diff --git a/test/host/time_sync/run_unit_test.py b/test/host/time_sync/run_unit_test.py index 36da3377..f59f1524 100644 --- a/test/host/time_sync/run_unit_test.py +++ b/test/host/time_sync/run_unit_test.py @@ -5,19 +5,18 @@ 做什么: 用本机 MSVC 编译 test/host/time_sync/time_sync_test.c(它以 #include 纳入 **生产源文件** App/sync/time_sync.c),在 PC 上离线跑: - 分片延时(心跳节奏)/ 监听建立与退避 / 时间包重组状态机(分段 + 30s 预算 + - tick 回绕)/ 任务主循环(就绪屏障、accept 轮询、代次变化重监听)。 + 分片延时(心跳节奏)/ 任务主循环(就绪屏障、SNTP 节拍、代次下传)。 失败即非 0 退出。零硬件、零烧录、毫秒级。 工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码判定全部委托给通用 harness `test/host/common/unit_test_harness.py`(本文件只做声明)。 为什么要主机侧单测(而不是只用板端套件): - 本模块是探针窗口心跳缺口那件事的当事方——"监听重建期间心跳缺口 800ms~1s,距 IWDG 最坏 - 超时 1701ms 余量仅 ~0.7s"。缺口成因全是本模块的**纯逻辑**(分片延时的喂狗 - 节奏、500/2000ms 监听退避、200ms accept 退避、50ms 收尾、屏障等待态喂心跳)。 - 真机上只能"跑起来看复位不复位",主机侧可**逐片断言**。时间包重组状态机 - (分段送达 / 预算耗尽 / EOF / 真错误 / tick 回绕)同理。 + 本模块是探针窗口心跳缺口那件事的当事方——心跳节奏(delay_fed 的 ≤50ms + 分片 + 每片前置心跳、屏障等待态 100ms 分片喂心跳)全是**纯逻辑**,真机上 + 只能"跑起来看复位不复位",主机侧可**逐片断言**(2026-10-02 台架根因、 + 第 8b 轮消抖两条教训都钉在这里)。:8888 监听/协议用例已随 2026-11-04 的 + :8888 退役删除(墙钟维护说明 §15),历史覆盖见 git 历史。 用法(仓库根目录): python test/host/time_sync/run_unit_test.py @@ -25,8 +24,8 @@ 退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。 -设计依据:App/sync/time_sync.c 文件头注释 + docs/网络/网络自愈架构设计.md §4.6; -创建日期:2026-10-10 +设计依据:App/sync/time_sync.c 文件头注释 + docs/子系统设计/系统墙钟时间维护说明.md §15/§16; +创建日期:2026-10-10(2026-11-04 随 :8888 退役收缩) """ import os import sys @@ -39,9 +38,9 @@ sys.path.insert(0, os.path.join(ROOT, "test", "host", "common")) from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402 # 顺序敏感:stub 目录必须**先于**生产目录,才能覆盖 FreeRTOS.h / task.h / -# net_socket.h / sys_events.h / sys_clock.h / dbg_log.h(真实头会拖入整套 -# FreeRTOS / CMSIS-RTOS / HAL)。task_watchdog.h 用**真实头**(App/util,只依赖 -# stdint.h),time_sync.h 用真实头(接口契约原汁)。 +# net_socket.h / sys_events.h(真实头会拖入整套 FreeRTOS / CMSIS-RTOS / HAL)。 +# task_watchdog.h 与 time_sntp.h 用**真实头**(接口契约原汁:前者只依赖 stdint.h, +# 后者自包含——替身签名由编译器对表)。 BUILD = TestBuild( name="Time Sync Tests", src=os.path.join(HERE, "time_sync_test.c"), @@ -49,7 +48,6 @@ BUILD = TestBuild( os.path.join(HERE, "stub"), os.path.join(ROOT, "App", "util"), os.path.join(ROOT, "App", "sync"), - os.path.join(ROOT, "App", "log"), # sys_log.h 用真实头(纯 stdint,同 task_watchdog.h 先例) HERE, ], summary_marker="=== Time Sync Tests:", diff --git a/test/host/time_sync/stub/dbg_log.h b/test/host/time_sync/stub/dbg_log.h deleted file mode 100644 index e24d84bd..00000000 --- a/test/host/time_sync/stub/dbg_log.h +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 主机侧单测桩:dbg_log.h - * 模块功能:为 App/sync/time_sync.c 提供主机可编译的日志宏(映射到计数)。 - * - * ⚠ 仅供 test/host/time_sync/ 使用,不参与 MCU 构建。 - * - * 注意:time_sync.c 在 `#include "dbg_log.h"` 之前自行 `#define` 了 - * DBG_LINE_BUF_MAX 与 DBG_TAG(真实头会用到它们),故本桩**不得**再定义这两个宏, - * 否则触发 C4005 宏重定义告警(/W4)。 - * - * DBG_ERROR / DBG_INFO 计入计数器,供单测断言: - * - "net_socket/bind/listen FAIL"、"open listener FAIL, retry later" → ERROR - * - "no TIME packet in 30s, drop conn"、"wall clock updated"、"network re-init" → INFO - * - * 创建日期:2026-10-10 - */ - -#ifndef DBG_LOG_H -#define DBG_LOG_H - -#include - -/* 计数(在 time_sync_test.c 实现) */ -extern int g_tstest_err_count; -extern int g_tstest_info_count; - -#define DBG_INFO(fmt, ...) do { g_tstest_info_count++; } while (0) -#define DBG_DEBUG(fmt, ...) do { } while (0) -#define DBG_ERROR(fmt, ...) do { g_tstest_err_count++; } while (0) - -#endif /* DBG_LOG_H */ diff --git a/test/host/time_sync/stub/net_socket.h b/test/host/time_sync/stub/net_socket.h index 0cdd5ebb..83fe36dd 100644 --- a/test/host/time_sync/stub/net_socket.h +++ b/test/host/time_sync/stub/net_socket.h @@ -1,20 +1,18 @@ /* - * 主机侧单测桩:net_socket.h(含内联 net_types.h 子集) - * 模块功能:为 App/sync/time_sync.c 提供主机可编译的网络 Socket API 声明与类型。 + * 主机侧单测桩:net_socket.h(收缩版,:8888 退役后) + * 模块功能:为 App/sync/time_sync.c 提供主机可编译的 net_get_generation 声明。 * * ⚠ 仅供 test/host/time_sync/ 使用,不参与 MCU 构建。 * 真实头(Drivers/BSP/NET/net_socket.h)包含 net_types.h → net_config.h,并经 - * FreeRTOS.h / task.h 拖入整套 FreeRTOS;本桩改为包含同名桩头。 + * FreeRTOS.h / task.h 拖入整套 FreeRTOS;本桩改为包含同名桩头(含 FreeRTOS/task + * 两条**保真**包含——生产源正是靠真实头的这条链拿到 vTaskDelay / pdMS_TO_TICKS)。 * - * 保真点: - * - 地址族/类型/特殊地址常量与 Drivers/BSP/NET/net_types.h 逐字一致; - * - struct net_sockaddr / net_in_addr / net_sockaddr_in 字段与真实头一致 - * (被测代码写 sin_family / sin_port / sin_addr.s_addr,桩按同布局读取); - * - 7 个被测代码调用的接口签名与真实头一致(net_socket / net_bind / net_listen / - * net_accept / net_close / net_recv_to / net_get_generation / net_htons)。 - * 全部由测试单元给强符号桩(脚本化返回值 + 调用记录)。 + * 收缩说明(2026-11-04)::8888 退役后本任务只剩 net_get_generation 一处网络 + * 调用。socket / bind / listen / accept / close / recv 族声明**刻意不列**—— + * 若 time_sync 循环重新伸手碰监听族 API,测试 TU 会因"未声明 + 无强符号桩"在 + * 编译/链接期直接爆,这是退役语义的编译期绊线(替代退役前的运行时断言)。 * - * 创建日期:2026-10-10 + * 创建日期:2026-10-10(2026-11-04 随 :8888 退役收缩) */ #ifndef NET_SOCKET_H @@ -29,41 +27,8 @@ extern "C" { #endif -/* ===== 以下取自 Drivers/BSP/NET/net_types.h(逐字) ===== */ - -#define NET_AF_INET 2 /* IPv4 */ -#define NET_SOCK_STREAM 1 /* TCP */ -#define NET_SOCK_DGRAM 2 /* UDP */ -#define NET_INADDR_ANY 0 /* 任意地址 */ -#define NET_INADDR_NONE 0xFFFFFFFF /* 无效地址 */ - -struct net_sockaddr { - uint16_t sa_family; /* 地址族 */ - char sa_data[14]; /* 地址数据 */ -}; - -struct net_in_addr { - uint32_t s_addr; /* IP 地址(网络字节序) */ -}; - -struct net_sockaddr_in { - uint16_t sin_family; /* 地址族,必须为 AF_INET */ - uint16_t sin_port; /* 端口号(网络字节序) */ - struct net_in_addr sin_addr; /* IP 地址(网络字节序) */ - char sin_zero[8]; /* 填充字节 */ -}; - -/* ===== 被测代码用到的 Socket API(签名与真实头一致) ===== */ - -int net_socket(int domain, int type); -int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen); -int net_listen(int sockfd); -int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen); -int net_close(int sockfd); -int net_drop_conn(int sockfd); -int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms); +/* 与 Drivers/BSP/NET/net_socket.h 签名一致 */ uint32_t net_get_generation(void); -uint16_t net_htons(uint16_t hostshort); #ifdef __cplusplus } diff --git a/test/host/time_sync/stub/sys_clock.h b/test/host/time_sync/stub/sys_clock.h deleted file mode 100644 index 81c19706..00000000 --- a/test/host/time_sync/stub/sys_clock.h +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 主机侧单测桩:sys_clock.h - * 模块功能:为 App/sync/time_sync.c 提供主机可编译的墙钟设置接口声明。 - * - * ⚠ 仅供 test/host/time_sync/ 使用,不参与 MCU 构建。 - * 真实头(App/util/sys_clock.h)含日历/闰年等无关声明;本桩只保留被测代码 - * 真正调用的那一个(签名与真实头逐字一致:int sys_clock_set_unix(uint32_t))。 - * - * 由测试单元给强符号桩,记录"是否被调用"与"最后一次的 epoch"。 - * - * 创建日期:2026-10-10 - */ - -#ifndef SYS_CLOCK_H -#define SYS_CLOCK_H - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -int sys_clock_set_unix(uint32_t unix_secs); - -#ifdef __cplusplus -} -#endif - -#endif /* SYS_CLOCK_H */ diff --git a/test/host/time_sync/stub/task.h b/test/host/time_sync/stub/task.h index e6b6cbc7..d4b739ed 100644 --- a/test/host/time_sync/stub/task.h +++ b/test/host/time_sync/stub/task.h @@ -4,8 +4,9 @@ * * ⚠ 仅供 test/host/time_sync/ 使用,不参与 MCU 构建。 * - * 两个接口由测试单元给强符号桩:vTaskDelay 记录每片睡眠时长(断言"≤50ms 分片"), - * xTaskGetTickCount 由测试单元提供可控时钟(驱动 30s 预算耗尽与 32 位回绕用例)。 + * 接口由测试单元给强符号桩:vTaskDelay 记录每片睡眠时长(断言"≤50ms 分片"); + * xTaskGetTickCount 保留可控时钟声明(:8888 退役后生产源已不读 tick,桩仍提供, + * 供未来需要预算/回绕类用例时直接取用)。 * * 创建日期:2026-10-10 */ diff --git a/test/host/time_sync/time_sync_test.c b/test/host/time_sync/time_sync_test.c index 3a10d990..4c34376e 100644 --- a/test/host/time_sync/time_sync_test.c +++ b/test/host/time_sync/time_sync_test.c @@ -1,69 +1,31 @@ /* - * 模块名称:Time Sync(PC 本地时间推送接收)主机侧单元测试 - * 模块功能:在 PC 上对 App/sync/time_sync.c 做离线验证(4 个函数全覆盖: - * time_sync_delay_fed / time_sync_open_listener / time_sync_handle_conn / - * time_sync_task)。 + * 模块名称:Time Sync(timeSyncTask 节拍宿主)主机侧单元测试 + * 模块功能:在 PC 上对 App/sync/time_sync.c 做离线验证(2 个函数全覆盖: + * time_sync_delay_fed / time_sync_task)。 * * 被测对象:真实源文件 ../../../App/sync/time_sync.c(以 #include 原样纳入, * 保证测的是生产代码而非副本)。平台依赖由本文件提供的**可注入桩**实现: * - FreeRTOS/task.h -> vTaskDelay / xTaskGetTickCount(可控时钟) - * - net_socket.h -> socket/bind/listen/accept/close/recv_to/ - * get_generation/htons(全部脚本化) + * - net_socket.h -> net_get_generation(脚本化 + 可跳出主循环) * - sys_events.h -> osEventFlagsWait(可注入"未就绪") - * - sys_clock.h -> sys_clock_set_unix(记录 epoch) * - task_watchdog.h -> task_wd_beat / task_wd_bind(记录心跳节奏) - * - dbg_log.h -> DBG_ERROR/DBG_INFO 计数 + * - time_sntp.h -> time_sntp_run(替身:记录轮次与收到的代次) * - * 为什么要主机侧单测(而不是只用板端套件): - * 1. **本模块是探针窗口心跳缺口的当事方**:当时的记录写着"time_sync 监听重建期间心跳缺口 - * 800ms~1s,距 IWDG 最坏超时 1701ms 余量仅 ~0.7s"。缺口的成因全部是本模块的 - * **纯逻辑**——分片延时(delay_fed)的心跳节奏、监听重试退避(500/2000ms)、 - * accept 轮询退避(200ms)、收尾延时(50ms)、以及"屏障等待态也要喂心跳"。 - * 这些在真机上只能靠"跑起来看复位不复位"间接观察,主机侧可**逐片断言**。 - * 2. **时间包重组状态机**(handle_conn)是纯逻辑:累积到 8 字节才解析、30s 总预算 - * 随 tick 推进耗尽、TCP 分段送达、EOF/真错误/超时三种返回码分支——板端套件 - * 无法精确构造"分段 + 预算耗尽 + tick 回绕"的组合。 - * 3. 不占 MCU 的 RAM,毫秒级完成。 + * 版本史:v1 时代本套件还测 :8888 监听重试与 "TIME" 包重组状态机(80 断言点)。 + * 2026-11-04 :8888 对时退役(PR #77 后用户裁定,理由见墙钟维护说明 §15), + * 监听/协议/OPER 留痕三组用例随生产代码一并退场——历史覆盖见 git 历史 + * (test/host/time_sync/ 2026-10-10 版)。留下并扩强的部分是**这个任务 + * 现在的全部生产逻辑**:心跳分片节奏(探针窗口心跳缺口的当事方至今 + * 仍是本模块——第 8b 轮消抖、2026-10-02 屏障喂心跳两条台架教训都钉在 + * 这里)与"循环按代次驱动 SNTP"。 * - * 覆盖点(TC-TS-xxx): - * —— time_sync_delay_fed(可喂心跳的分片延时)—— - * TC-TIMESYNC-001 0ms 不睡眠不喂狗 - * TC-TIMESYNC-002 恰好一片(50ms) - * TC-TIMESYNC-003 非整片(120ms)切成 50/50/20,每片喂心跳 - * TC-TIMESYNC-004 长延时(2000ms)= 40 片 × 50ms,每片喂心跳 - * TC-TIMESYNC-005 不足一片(49ms)单片处理 - * TC-TIMESYNC-006 总预算 30s = 600 片(片长上限恒 50ms) - * —— time_sync_open_listener(监听建立与重试)—— - * TC-TIMESYNC-010 首次成功:socket/bind/listen 各一次,端口经 net_htons,无退避 - * TC-TIMESYNC-011 bind 失败 -> 关 socket + 退避 2000ms + 留痕,随后重试成功 - * TC-TIMESYNC-012 listen 失败 -> 关 socket + 退避 2000ms,随后重试成功 - * TC-TIMESYNC-013 socket 连续失败 -> 5 次后返回 -1(5 条 ERROR,5×500ms 退避) - * TC-TIMESYNC-014 第 3 次重试成功并返回新描述符 - * TC-TIMESYNC-015 每次重试前 + 每片延时各喂一次心跳,且心跳位恒为 TIMESYNC - * —— time_sync_handle_conn(时间包重组状态机)—— - * TC-TIMESYNC-020 8 字节一次收齐 -> 大端解析 epoch + 更新墙钟 + 关连接 - * TC-TIMESYNC-021 分段送达(4+4)-> 累积后解析;第二次只请求剩余 12 字节 - * TC-TIMESYNC-022 一次到达 16 字节 -> 只取前 8 字节 - * TC-TIMESYNC-023 前缀非 "TIME" -> 不更新墙钟,但仍关连接 - * TC-TIMESYNC-024 对端关闭(n==0)-> 立即退出、不解析 - * TC-TIMESYNC-025 真错误(n==-1)-> 立即退出、不解析 - * TC-TIMESYNC-026 只收到 4 字节后 EOF -> 不足 8 字节不解析 - * TC-TIMESYNC-027 30s 预算耗尽 -> 留痕 "no TIME packet in 30s, drop conn" - * TC-TIMESYNC-028 剩余预算不足一片 -> 末片按剩余(20ms)切分 - * TC-TIMESYNC-029 32 位 tick 回绕 -> 预算计算仍正确 - * —— time_sync_task(任务主循环)—— - * TC-TIMESYNC-040 sys_evtHandle==NULL:跳过就绪屏障;accept 轮询->成功->处理->收尾 - * TC-TIMESYNC-041 就绪屏障:100ms 分片等待 NET_READY(osFlagsNoClear 不消费就绪位) - * TC-TIMESYNC-042 屏障未就绪期间持续喂心跳(2026-10-02 台架根因的回归防线) - * TC-TIMESYNC-043 网络代次变化 -> 关闭旧监听并重监听 - * TC-TIMESYNC-044 监听建立失败 -> 5 次重试后放弃并退避 2000ms - * 对时操作留痕(道G task-50,语义行名不配号——编号规范已停发测试用例号): - * "sync trace on success" / "… rejected" / "… two in a row" / "… bad magic" 四组 + * 断言口径(用户裁定 2026-10-27,编号规范 §9):新用例一律**语义行名**, + * 不再配 TC 号;旧 TC-TIMESYNC-* 号随用例退役永久作废、不得复用。 * - * 通过:全部 TC PASS(main 返回 0)。 + * 通过:全部断言 PASS(main 返回 0)。 * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 * - * 创建日期:2026-10-10 + * 创建日期:2026-10-10(2026-11-04 随 :8888 退役收缩重写) */ #include @@ -75,14 +37,13 @@ #include "FreeRTOS.h" #include "task.h" #include "net_socket.h" -#include "sys_clock.h" #include "sys_events.h" #include "task_watchdog.h" /* 真实头(App/util):task_wd_id_t / TASK_WD_TIMESYNC */ -#include "sys_log.h" /* 真实头(App/log,纯 stdint 无 OS 依赖):syslog_id_t / SYSLOG_OPER 契约,假件签名与之逐字对齐 */ +#include "time_sntp.h" /* 真实头(App/sync):time_sntp_run 签名原汁,替身必须对表 */ /* ===================== FreeRTOS 桩 ===================== */ static TickType_t s_tick; -#define DELAY_LOG_MAX 800 +#define DELAY_LOG_MAX 2048 static uint32_t s_delays[DELAY_LOG_MAX]; static int s_delay_calls; @@ -102,7 +63,7 @@ osEventFlagsId_t sys_evtHandle = NULL; static uint32_t s_evt_seq[8]; static int s_evt_seq_n; -static uint32_t s_evt_default = 0xFFFFFFFFu; /* 未脚本化时的返回值(默认:就绪) */ +static uint32_t s_evt_default = 0xFFFFFFFFu; /* 未脚本化时的返回值 */ static int s_evt_calls; static int s_evt_longjmp_at = -1; /* >0 时第 N 次调用触发 longjmp */ static uint32_t s_evt_last_flags; @@ -129,12 +90,12 @@ uint32_t osEventFlagsWait(osEventFlagsId_t ef_id, uint32_t flags, uint32_t optio return s_evt_default; } -/* ===================== 看门狗桩 ===================== */ +/* ===================== 看门狗桩 ===================== + * ⚠ 计数变量命名带 s_wd_ 前缀:C 的"暂定定义"规则下,同一编译单元里 + * 两个同名 `static int x;` 会被静默合并(不报错),历史版本里 task_wd_bind + * 与 net_bind 的计数器撞过一次名导致断言全部失真(2026-10-10 踩过)。 */ static int s_beat_calls; static int s_beat_id_bad; -/* ⚠ 命名必须与 net_socket 桩的 net_bind 计数区分开:C 的"暂定定义"规则下, - * 同一编译单元里两个 `static int x;` 会被**静默合并为同一个对象**(不报错), - * 于是 task_wd_bind 与 net_bind 会共用计数器,断言全部失真(2026-10-10 踩过)。 */ static int s_wd_bind_calls; static int s_wd_bind_id_bad; @@ -152,126 +113,16 @@ void task_wd_bind(task_wd_id_t id) { } } -/* ===================== sys_clock 桩 ===================== */ -static int s_set_unix_calls; -static uint32_t s_set_unix_last; -static int s_set_unix_rc; /* 可脚本化返回值:非 0 = 对时被拒(task-50) */ - -int sys_clock_set_unix(uint32_t unix_secs) { - s_set_unix_calls++; - s_set_unix_last = unix_secs; - return s_set_unix_rc; -} - -/* ===================== sys_log 投递桩(task-50) ===================== - * 假件按投递顺序记录:条数 / 日志 ID / 末条描述 / 对时文案出现次数。 - * "出现次数"专为钉"连续两次不合并"——若生产侧哪天改成边沿锁存, - * 第三条用例会红,而那正是要拦的语义漂移。 */ -static int s_post_calls; -static int s_post_id; -static char s_post_last_desc[32]; -static int s_post_desc_count; - -void syslog_post_quiet(syslog_id_t id, const char *desc) { - s_post_calls++; - s_post_id = (int)id; - if (desc != NULL) { - (void)strncpy(s_post_last_desc, desc, sizeof(s_post_last_desc) - 1u); - s_post_last_desc[sizeof(s_post_last_desc) - 1u] = '\0'; - if (strcmp(desc, "clock set by 8888") == 0) { - s_post_desc_count++; - } - } -} - -/* ===================== net_socket 桩 ===================== */ -static uint16_t s_htons_last; - -static int s_socket_calls; -static int s_socket_seq[8]; -static int s_socket_seq_n; -static int s_socket_default = 5; - -static int s_bind_calls; -static int s_bind_seq[8]; -static int s_bind_seq_n; -static uint16_t s_bind_port; -static uint16_t s_bind_family; - -static int s_listen_calls; -static int s_listen_seq[8]; -static int s_listen_seq_n; - -static int s_accept_calls; -static int s_accept_seq[16]; -static int s_accept_seq_n; - -static int s_close_calls; -static int s_close_socks[32]; - +/* ===================== net_socket 桩 ===================== + * :8888 退役后本任务只剩 net_get_generation 一处网络调用。桩头刻意**不声明** + * socket/bind/listen/accept/close/recv——若循环哪天重新伸手碰监听族 API, + * 链接期直接爆(编译期绊线,替代退役前的运行时断言)。 */ static uint32_t s_gen_seq[8]; static int s_gen_seq_n; static uint32_t s_gen_default = 1u; static int s_gen_calls; static int s_gen_longjmp_at = -1; /* >0 时第 N 次调用触发 longjmp */ -uint16_t net_htons(uint16_t hostshort) { - s_htons_last = hostshort; - return (uint16_t)(((hostshort & 0xFFu) << 8) | (hostshort >> 8)); -} - -int net_socket(int domain, int type) { - int i = s_socket_calls++; - (void)domain; - (void)type; - return (i < s_socket_seq_n) ? s_socket_seq[i] : s_socket_default; -} - -int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen) { - const struct net_sockaddr_in *a = (const struct net_sockaddr_in *)addr; - int i = s_bind_calls++; - - (void)sockfd; - (void)addrlen; - s_bind_family = a->sin_family; - s_bind_port = a->sin_port; - return (i < s_bind_seq_n) ? s_bind_seq[i] : 0; -} - -int net_listen(int sockfd) { - int i = s_listen_calls++; - (void)sockfd; - return (i < s_listen_seq_n) ? s_listen_seq[i] : 0; -} - -int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen) { - int i = s_accept_calls++; - (void)sockfd; - (void)addr; - (void)addrlen; - return (i < s_accept_seq_n) ? s_accept_seq[i] : -1; -} - -int net_close(int sockfd) { - if (s_close_calls < 32) { - s_close_socks[s_close_calls] = sockfd; - } - s_close_calls++; - return 0; -} - -/* ---- net_drop_conn(头阻塞收口):计数 + 记录 fd ---- */ -static int s_drop_calls; -static int s_drop_socks[32]; - -int net_drop_conn(int sockfd) { - if (s_drop_calls < 32) { - s_drop_socks[s_drop_calls] = sockfd; - } - s_drop_calls++; - return 0; -} - uint32_t net_get_generation(void) { int i = s_gen_calls++; @@ -281,59 +132,29 @@ uint32_t net_get_generation(void) { return (i < s_gen_seq_n) ? s_gen_seq[i] : s_gen_default; } -/* ---- net_recv_to:脚本化返回码 + 可推进 tick ---- */ -typedef struct { - int n; /* 返回码:>0 字节数 / 0 EOF / -1 错误 / -2 超时无数据 */ - uint8_t data[16]; /* 要"送达"的字节 */ - int len; /* data 有效长度 */ - uint32_t tick_adv; /* 返回前把假时钟推进多少 ms */ -} recv_step_t; +/* ===================== SNTP 客户机替身 ===================== + * 行为断言在 test/host/sntp 独立套件里做;本套件只钉两件事: + * ① 每轮循环恰好调一次 time_sntp_run; + * ② 收到的代次 = 本轮 net_get_generation() 的返回值(断网重入语义的输入端)。 */ +static int s_sntp_run_calls; +static uint32_t s_sntp_gen_log[8]; +static int s_sntp_gen_log_n; +static int s_sntp_run_longjmp_at = -1; /* >0 时第 N 次调用触发 longjmp */ -static recv_step_t s_recv_seq[16]; -static int s_recv_seq_n; -static int s_recv_idx; -static int s_recv_calls; -static int s_recv_default = 0; /* 脚本用尽后的返回码(0=EOF,保证循环终止) */ -static uint32_t s_recv_to_log[64]; -static int s_recv_len_log[64]; +void time_sntp_run(uint32_t net_gen) { + int i = s_sntp_run_calls++; -int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms) { - int i = s_recv_calls; - - (void)sockfd; - if (i < 64) { - s_recv_to_log[i] = timeout_ms; - s_recv_len_log[i] = len; + if (i < (int)(sizeof(s_sntp_gen_log) / sizeof(s_sntp_gen_log[0]))) { + s_sntp_gen_log[i] = net_gen; + s_sntp_gen_log_n = i + 1; } - s_recv_calls++; - - if (s_recv_idx >= s_recv_seq_n) { - return s_recv_default; - } - - { - const recv_step_t *st = &s_recv_seq[s_recv_idx]; - int copy; - s_recv_idx++; - s_tick += st->tick_adv; - if (st->n <= 0) { - return st->n; - } - copy = (st->n <= len) ? st->n : len; - if (copy > st->len) { - copy = st->len; - } - if ((copy > 0) && (buf != NULL)) { - memcpy(buf, st->data, (size_t)copy); - } - return st->n; + if (s_sntp_run_longjmp_at > 0 && (i + 1) >= s_sntp_run_longjmp_at) { + longjmp(s_jmp, 3); } } /* ===================== 断言 ===================== */ static int s_total = 0, s_passed = 0, s_failed = 0; -int g_tstest_err_count = 0; -int g_tstest_info_count = 0; #define CHECK(cond, name) do { \ s_total++; \ @@ -341,24 +162,6 @@ int g_tstest_info_count = 0; else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \ } while (0) -/* ---- SNTP 客户机替身(time_sync.c v1.4 起循环调用 time_sntp_run; - * 真实头 App/sync/time_sntp.h 与 time_sync.c 同目录,引号包含直达声明, - * 这里只落三个空实现——行为断言在 test/host/sntp 独立套件里做) ---- */ -static int s_sntp_run_calls; - -void time_sntp_run(uint32_t net_gen) { - (void)net_gen; - s_sntp_run_calls++; -} - -uint32_t time_sntp_sync_count(void) { - return 0u; -} - -int32_t time_sntp_last_offset_ms(void) { - return 0; -} - /* ===================== 被测代码(原样纳入生产源) ===================== */ #include "time_sync.c" @@ -369,8 +172,8 @@ int32_t time_sntp_last_offset_ms(void) { * 入口参数:无 * 返 回 值:无 * 限定条件:无 - * 函数说明:默认 = 网络全成功、gen 恒为 1、recv 脚本为空(脚本用尽返回 EOF)、 - * 事件标志恒就绪。各用例按需覆盖。 + * 函数说明:默认 = 事件标志健康就绪(屏障一次通过)、gen 恒为 1、sntp 替身直通。 + * 各用例按需覆盖。 */ static void fakes_reset(void) { s_tick = 0; @@ -390,64 +193,14 @@ static void fakes_reset(void) { s_wd_bind_calls = 0; s_wd_bind_id_bad = 0; - s_set_unix_calls = 0; - s_set_unix_last = 0; - s_set_unix_rc = 0; - s_post_calls = 0; - s_post_id = -1; - s_post_last_desc[0] = '\0'; - s_post_desc_count = 0; - - s_htons_last = 0; - s_socket_calls = 0; - s_socket_seq_n = 0; - s_socket_default = 5; - s_bind_calls = 0; - s_bind_seq_n = 0; - s_bind_port = 0; - s_bind_family = 0; - s_listen_calls = 0; - s_listen_seq_n = 0; - s_accept_calls = 0; - s_accept_seq_n = 0; - s_close_calls = 0; - s_drop_calls = 0; s_gen_seq_n = 0; s_gen_default = 1u; s_gen_calls = 0; s_gen_longjmp_at = -1; - s_recv_seq_n = 0; - s_recv_idx = 0; - s_recv_calls = 0; - s_recv_default = 0; - - g_tstest_err_count = 0; - g_tstest_info_count = 0; -} - -/* 装配一个 recv 脚本步 */ -static void recv_set(int idx, int n, const uint8_t *data, int len, uint32_t tick_adv) { - memset(&s_recv_seq[idx], 0, sizeof(s_recv_seq[idx])); - s_recv_seq[idx].n = n; - s_recv_seq[idx].len = len; - s_recv_seq[idx].tick_adv = tick_adv; - if ((data != NULL) && (len > 0)) { - int copy = (len > 16) ? 16 : len; - memcpy(s_recv_seq[idx].data, data, (size_t)copy); - } -} - -/* 装配一个 "TIME" + uint32 大端 时间包 */ -static void make_time_packet(uint8_t *out, uint32_t epoch) { - out[0] = (uint8_t)'T'; - out[1] = (uint8_t)'I'; - out[2] = (uint8_t)'M'; - out[3] = (uint8_t)'E'; - out[4] = (uint8_t)(epoch >> 24); - out[5] = (uint8_t)(epoch >> 16); - out[6] = (uint8_t)(epoch >> 8); - out[7] = (uint8_t)(epoch); + s_sntp_run_calls = 0; + s_sntp_gen_log_n = 0; + s_sntp_run_longjmp_at = -1; } /* 检查 s_delays[0..n) 是否全部等于 v */ @@ -461,268 +214,100 @@ static int delays_all(uint32_t v, int n) { return 1; } -/* ===================== TC-TIMESYNC-001..006:delay_fed ===================== */ +/* ===================== delay_fed:心跳分片延时 ===================== + * 本模块的全部看门狗安全论证都压在这一个函数上:片长恒 ≤50ms、每片前置心跳, + * 抽样窗(100ms)内必有心跳——第 8b 轮消抖钉下的节奏,逐片断言。 */ static void tc_delay_fed(void) { fakes_reset(); time_sync_delay_fed(0U); - CHECK((s_beat_calls == 0) && (s_delay_calls == 0), "TC-TIMESYNC-001 delay_fed(0) neither sleeps nor beats"); + CHECK((s_beat_calls == 0) && (s_delay_calls == 0), "delay_fed(0) neither sleeps nor beats"); fakes_reset(); time_sync_delay_fed(50U); CHECK((s_beat_calls == 1) && (s_delay_calls == 1) && (s_delays[0] == 50U), - "TC-TIMESYNC-002 delay_fed(50) = one 50 ms slice + 1 beat"); + "delay_fed(50) = one 50 ms slice + 1 beat"); fakes_reset(); time_sync_delay_fed(120U); - CHECK((s_beat_calls == 3) && (s_delay_calls == 3), "TC-TIMESYNC-003 delay_fed(120) splits into 3 slices"); + CHECK((s_beat_calls == 3) && (s_delay_calls == 3), "delay_fed(120) splits into 3 slices"); CHECK((s_delays[0] == 50U) && (s_delays[1] == 50U) && (s_delays[2] == 20U), - "TC-TIMESYNC-003b slice lengths 50/50/20 (last slice is the remainder)"); + "delay_fed(120) slice lengths 50/50/20 (last slice is the remainder)"); fakes_reset(); time_sync_delay_fed(2000U); CHECK((s_delay_calls == 40) && (s_beat_calls == 40) && delays_all(50U, 40), - "TC-TIMESYNC-004 delay_fed(2000) = 40 slices x 50 ms, one beat per slice"); + "delay_fed(2000) = 40 slices x 50 ms, one beat per slice"); fakes_reset(); time_sync_delay_fed(49U); - CHECK((s_beat_calls == 1) && (s_delays[0] == 49U), "TC-TIMESYNC-005 less than one slice (49 ms) handled as a single slice"); + CHECK((s_beat_calls == 1) && (s_delays[0] == 49U), + "delay_fed(49) less than one slice handled as a single slice"); fakes_reset(); time_sync_delay_fed(30000U); - CHECK(s_delay_calls == 600, "TC-TIMESYNC-006 total budget 30 s = 600 slices (slice cap is always 50 ms)"); + CHECK(s_delay_calls == 600, "delay_fed(30000) = 600 slices (slice cap is always 50 ms)"); } -/* ===================== TC-TIMESYNC-010..015:open_listener ===================== */ - -static void tc_open_listener(void) { - fakes_reset(); - { - int s = time_sync_open_listener(); - CHECK(s == 5, "TC-TIMESYNC-010 open_listener success returns the socket descriptor"); - CHECK((s_socket_calls == 1) && (s_bind_calls == 1) && (s_listen_calls == 1), - "TC-TIMESYNC-010b socket / bind / listen called once each"); - CHECK(s_htons_last == (uint16_t)TIME_SYNC_PORT, "TC-TIMESYNC-010c port goes through net_htons(8888)"); - CHECK(s_bind_port == net_htons((uint16_t)TIME_SYNC_PORT), "TC-TIMESYNC-010d sin_port is in network byte order"); - CHECK(s_bind_family == (uint16_t)NET_AF_INET, "TC-TIMESYNC-010e sin_family = NET_AF_INET"); - CHECK(s_delay_calls == 0, "TC-TIMESYNC-010f no backoff delay on the success path"); - } - - fakes_reset(); - s_bind_seq[0] = -1; - s_bind_seq_n = 1; - { - int s = time_sync_open_listener(); - CHECK(s == 5, "TC-TIMESYNC-011 bind succeeds on retry after one failure"); - CHECK((s_close_calls == 1) && (s_close_socks[0] == 5), "TC-TIMESYNC-011b bind failure closes that socket"); - CHECK(s_delay_calls == 40, "TC-TIMESYNC-011c bind failure backs off 2000 ms (40 slices)"); - CHECK(g_tstest_err_count == 1, "TC-TIMESYNC-011d bind failure leaves a DBG_ERROR trace"); - } - - fakes_reset(); - s_listen_seq[0] = -1; - s_listen_seq_n = 1; - { - int s = time_sync_open_listener(); - CHECK(s == 5, "TC-TIMESYNC-012 listen succeeds on retry after one failure"); - CHECK((s_close_calls == 1) && (s_close_socks[0] == 5), "TC-TIMESYNC-012b listen failure closes that socket"); - CHECK(s_delay_calls == 40, "TC-TIMESYNC-012c listen failure backs off 2000 ms"); - } - - fakes_reset(); - s_socket_default = -1; - { - int s = time_sync_open_listener(); - CHECK(s == -1, "TC-TIMESYNC-013 socket keeps failing -> returns -1"); - CHECK(s_socket_calls == 5, "TC-TIMESYNC-013b retry cap is 5 attempts"); - CHECK(g_tstest_err_count == 5, "TC-TIMESYNC-013c every failure leaves its own trace (5 DBG_ERROR lines)"); - CHECK(s_delay_calls == 50, "TC-TIMESYNC-013d every failure backs off 500 ms (5 x 10 slices)"); - } - - fakes_reset(); - s_socket_seq[0] = -1; - s_socket_seq[1] = -1; - s_socket_seq[2] = 9; - s_socket_seq_n = 3; - { - int s = time_sync_open_listener(); - CHECK(s == 9, "TC-TIMESYNC-014 3rd retry succeeds and returns the new descriptor"); - CHECK(s_socket_calls == 3, "TC-TIMESYNC-014b exactly 3 attempts"); - CHECK(s_delay_calls == 20, "TC-TIMESYNC-014c two failures back off 500 ms x 2"); - } - - fakes_reset(); - s_socket_default = -1; - (void)time_sync_open_listener(); - CHECK(s_beat_calls == 55, "TC-TIMESYNC-015 every retry round (5) + every delay slice (50) beats once"); - CHECK(s_beat_id_bad == 0, "TC-TIMESYNC-015b the beat bit is always TASK_WD_TIMESYNC"); -} - -/* ===================== TC-TIMESYNC-020..029:handle_conn ===================== - * ⚠ 契约变更:handle_conn **不再** net_close - * (单连接模式下 conn==listen,close 会连监听一起毁);会话收尾只返回外层, - * 由 time_sync_task 在 `conn != ls`(模拟器多连接后端)时才关数据 fd。 - * 故本组断言一律为 s_close_calls == 0;任务级关闭见 TC-TIMESYNC-040e。 */ - -static void tc_handle_conn(void) { - uint8_t pkt[16]; - - /* 1) 8 字节一次收齐 */ - fakes_reset(); - make_time_packet(pkt, 0x12345678u); - recv_set(0, 8, pkt, 8, 0U); - s_recv_seq_n = 1; - s_tick = 100U; - time_sync_handle_conn(7); - CHECK(s_set_unix_calls == 1, "TC-TIMESYNC-020 TIME packet in one shot -> wall clock updated once"); - CHECK(s_set_unix_last == 0x12345678u, "TC-TIMESYNC-020b epoch parsed big-endian"); - CHECK((s_recv_calls == 1) && (s_recv_len_log[0] == 16), "TC-TIMESYNC-020c the first request asks for 16 bytes"); - CHECK(s_recv_to_log[0] == 50U, "TC-TIMESYNC-020d single wait slice length = 50 ms"); - CHECK(s_close_calls == 0, "TC-TIMESYNC-020e after handling, the connection is **not** closed (close is the outer conn!=ls branch job)"); - CHECK(s_drop_calls == 0, "TC-TIMESYNC-020g a successful session is not torn down (drop is only for give-up paths)"); - CHECK(g_tstest_info_count == 1, "TC-TIMESYNC-020f leaves the wall clock updated trace"); - - /* 2) 分段送达:4 + 4 */ - fakes_reset(); - make_time_packet(pkt, 10u); - recv_set(0, 4, pkt, 4, 10U); - recv_set(1, 4, pkt + 4, 4, 10U); - s_recv_seq_n = 2; - time_sync_handle_conn(3); - CHECK((s_set_unix_calls == 1) && (s_set_unix_last == 10u), "TC-TIMESYNC-021 pieces accumulate and then parse correctly"); - CHECK(s_recv_calls == 2, "TC-TIMESYNC-021b read twice"); - CHECK((s_recv_len_log[0] == 16) && (s_recv_len_log[1] == 12), - "TC-TIMESYNC-021c the second request asks only for the remaining 12 bytes"); - CHECK((s_recv_to_log[0] == 50U) && (s_recv_to_log[1] == 50U), "TC-TIMESYNC-021d 50 ms per slice (budget is ample)"); - - /* 3) 16 字节一次到达(只取前 8) */ - fakes_reset(); - make_time_packet(pkt, 5u); - recv_set(0, 16, pkt, 16, 0U); - s_recv_seq_n = 1; - time_sync_handle_conn(4); - CHECK((s_set_unix_calls == 1) && (s_set_unix_last == 5u), "TC-TIMESYNC-022 an over-long packet only takes the first 8 bytes"); - CHECK(s_recv_calls == 1, "TC-TIMESYNC-022b stops as soon as one read is enough"); - - /* 4) 前缀不是 TIME */ - fakes_reset(); - pkt[0] = (uint8_t)'X'; - pkt[1] = (uint8_t)'X'; - pkt[2] = (uint8_t)'X'; - pkt[3] = (uint8_t)'X'; - pkt[4] = 0U; - pkt[5] = 0U; - pkt[6] = 0U; - pkt[7] = 1U; - recv_set(0, 8, pkt, 8, 0U); - s_recv_seq_n = 1; - time_sync_handle_conn(4); - CHECK(s_set_unix_calls == 0, "TC-TIMESYNC-023 a non-TIME prefix does not update the wall clock"); - CHECK(s_close_calls == 0, "TC-TIMESYNC-023b a non-TIME prefix also does not close the connection"); - - /* 5) 对端关闭(EOF) */ - fakes_reset(); - recv_set(0, 0, NULL, 0, 0U); - s_recv_seq_n = 1; - time_sync_handle_conn(6); - CHECK((s_set_unix_calls == 0) && (s_recv_calls == 1), "TC-TIMESYNC-024 peer close (EOF) exits immediately without parsing"); - CHECK(s_close_calls == 0, "TC-TIMESYNC-024b EOF also does not close the connection (net-layer auto_relisten re-arms the listener)"); - - /* 6) 真错误 */ - fakes_reset(); - recv_set(0, -1, NULL, 0, 0U); - s_recv_seq_n = 1; - time_sync_handle_conn(6); - CHECK((s_recv_calls == 1) && (s_set_unix_calls == 0), "TC-TIMESYNC-025 a real error exits immediately without parsing"); - - /* 7) 4 字节后 EOF */ - fakes_reset(); - make_time_packet(pkt, 7u); - recv_set(0, 4, pkt, 4, 0U); - recv_set(1, 0, NULL, 0, 0U); - s_recv_seq_n = 2; - time_sync_handle_conn(6); - CHECK(s_set_unix_calls == 0, "TC-TIMESYNC-026 fewer than 8 bytes are not parsed"); - CHECK(s_recv_calls == 2, "TC-TIMESYNC-026b keeps waiting for the remaining bytes"); - - /* 8) 30s 预算耗尽 */ - fakes_reset(); - recv_set(0, -2, NULL, 0, 30000U); - s_recv_seq_n = 1; - time_sync_handle_conn(6); - CHECK(s_recv_calls == 1, "TC-TIMESYNC-027 no more reads once the budget is exhausted (only 1 read)"); - CHECK(g_tstest_info_count == 1, "TC-TIMESYNC-027b leaves the no TIME packet in 30s, drop conn trace"); - CHECK(s_close_calls == 0, "TC-TIMESYNC-027c budget exhausted still does not close the connection"); - CHECK(s_drop_calls == 1, "TC-TIMESYNC-027d budget exhausted drops the session on purpose (head-of-line wrap-up: net_drop_conn kills the connection, keeps the listener)"); - - /* 9) 剩余预算不足一片 */ - fakes_reset(); - recv_set(0, -2, NULL, 0, 29980U); - s_recv_seq_n = 1; - time_sync_handle_conn(6); - CHECK(s_recv_calls == 2, "TC-TIMESYNC-028 ends after two waits"); - CHECK((s_recv_to_log[0] == 50U) && (s_recv_to_log[1] == 20U), - "TC-TIMESYNC-028b second slice length = the remaining 20 ms"); - - /* 10) 32 位 tick 回绕 */ - fakes_reset(); - s_tick = 0xFFFFFF00U; - recv_set(0, -2, NULL, 0, 512U); /* 推进后回绕到 0x00000100 */ - s_recv_seq_n = 1; - time_sync_handle_conn(6); - CHECK((s_recv_calls == 2) && (s_recv_to_log[0] == 50U) && (s_recv_to_log[1] == 50U), - "TC-TIMESYNC-029 after a 32-bit tick wraparound the budget math is still correct"); -} - -/* ===================== TC-TIMESYNC-040..044:time_sync_task ===================== */ +/* ===================== time_sync_task:屏障 + SNTP 节拍 ===================== */ static void tc_task(void) { - uint8_t pkt[16]; - - /* A) 无就绪屏障(sys_evtHandle == NULL):accept 轮询 2 次失败后成功 */ + /* A) 无就绪屏障(sys_evtHandle == NULL):一轮完整节拍后于第 2 轮顶部脱出 */ fakes_reset(); - make_time_packet(pkt, 9u); - recv_set(0, 8, pkt, 8, 0U); - s_recv_seq_n = 1; - s_accept_seq[0] = -1; - s_accept_seq[1] = -1; - s_accept_seq[2] = 7; - s_accept_seq_n = 3; - /* gen 调用序:iter1 顶部(1) + accept 两次失败各一次(2,3) + iter2 顶部(4) → 第 4 次跳出 */ - s_gen_longjmp_at = 4; + s_gen_longjmp_at = 2; /* iter1 顶部(1) + iter2 顶部(2) -> 跳出 */ if (setjmp(s_jmp) == 0) { time_sync_task(NULL); } CHECK((s_wd_bind_calls == 1) && (s_wd_bind_id_bad == 0), - "TC-TIMESYNC-040 task entry registers the watchdog beat bit (task_wd_bind once)"); - CHECK(s_evt_calls == 0, "TC-TIMESYNC-040b skips the readiness barrier when sys_evtHandle==NULL"); - CHECK(s_accept_calls == 3, "TC-TIMESYNC-040c accept polled 3 times (2 with no connection + 1 success)"); - CHECK((s_set_unix_calls == 1) && (s_set_unix_last == 9u), "TC-TIMESYNC-040d the time packet arrived and the wall clock was updated"); - CHECK(s_close_calls == 1, "TC-TIMESYNC-040e the connection is closed once"); - CHECK(s_delay_calls == 9, "TC-TIMESYNC-040f delay slice count = 4+4+1 (200+200+50 ms)"); - CHECK(delays_all(50U, 9), "TC-TIMESYNC-040g every delay is sliced at 50 ms"); - CHECK(s_beat_calls == 13, "TC-TIMESYNC-040h beat count = main loop 1 + backoff 8 + accepted connection 1 + wrap-up 1 + next cycle 1"); - CHECK(s_beat_id_bad == 0, "TC-TIMESYNC-040i every beat sets the TIMESYNC bit"); + "task entry registers the watchdog beat bit (task_wd_bind once)"); + CHECK(s_evt_calls == 0, "skips the readiness barrier when sys_evtHandle==NULL"); + CHECK((s_gen_calls == 2) && (s_sntp_run_calls == 1), + "one full tick: generation read once per round, SNTP driven once per round"); + CHECK((s_sntp_gen_log_n == 1) && (s_sntp_gen_log[0] == 1u), + "time_sntp_run receives this round's generation value"); + CHECK(s_delay_calls == 4, "tick wait 200 ms = 4 slices (slice cap 50 ms unchanged)"); + CHECK(delays_all(50U, 4), "every tick slice sleeps exactly 50 ms"); + CHECK(s_beat_calls == 6, "beat count = loop heads 2 (2nd beat precedes the bail-out gen) + slices 4"); + CHECK(s_beat_id_bad == 0, "every beat sets the TIMESYNC bit"); - /* B) 就绪屏障:2 次未就绪 + 1 次就绪 */ + /* B) 代次变化原样下传:1 -> 2(断网重建后 SNTP 内部自弃旧 socket,循环本身 + * 不再有任何监听簿记——桩头不声明 socket 族 API,伸手即链接不过) */ + fakes_reset(); + s_gen_seq[0] = 1u; + s_gen_seq[1] = 2u; + s_gen_seq_n = 2; + s_gen_default = 2u; + s_gen_longjmp_at = 3; /* 第 3 轮顶部跳出:已跑满 2 个完整节拍 */ + if (setjmp(s_jmp) == 0) { + time_sync_task(NULL); + } + CHECK(s_sntp_run_calls == 2, "two full ticks before bail-out"); + CHECK((s_sntp_gen_log_n == 2) && (s_sntp_gen_log[0] == 1u) && (s_sntp_gen_log[1] == 2u), + "generation change is handed to the SNTP client verbatim, round by round"); + CHECK(s_delay_calls == 8, "each round waits its own 200 ms (4+4 slices)"); + CHECK(s_beat_calls == 11, "beat count = (loop head + 4 slices) x 2 rounds + loop head of round 3"); + + /* C) 就绪屏障:2 次未就绪 + 1 次就绪 */ fakes_reset(); sys_evtHandle = (osEventFlagsId_t)0x55; s_evt_seq[0] = osFlagsErrorTimeout; s_evt_seq[1] = osFlagsErrorTimeout; s_evt_seq[2] = SYS_EVT_NET_READY; s_evt_seq_n = 3; - s_gen_longjmp_at = 1; + s_gen_longjmp_at = 1; /* 屏障通过后第 1 轮顶部跳出 */ if (setjmp(s_jmp) == 0) { time_sync_task(NULL); } - CHECK(s_evt_calls == 3, "TC-TIMESYNC-041 readiness barrier polled 3 times (2 not ready + 1 ready)"); - CHECK(s_evt_last_timeout == 100U, "TC-TIMESYNC-041b barrier waits are sliced at 100 ms"); - CHECK(s_evt_last_flags == SYS_EVT_NET_READY, "TC-TIMESYNC-041c waits for the NET_READY bit"); - CHECK(s_evt_last_options == (osFlagsWaitAny | osFlagsNoClear), "TC-TIMESYNC-041d the ready bit is not consumed"); - CHECK(s_beat_calls == 3, "TC-TIMESYNC-041e the two not-ready rounds each beat once (+ 1 in the main loop)"); - CHECK(s_wd_bind_calls == 1, "TC-TIMESYNC-041f still registers the beat routing first"); + CHECK(s_evt_calls == 3, "readiness barrier polled 3 times (2 not ready + 1 ready)"); + CHECK(s_evt_last_timeout == 100U, "barrier waits are sliced at 100 ms"); + CHECK(s_evt_last_flags == SYS_EVT_NET_READY, "waits for the NET_READY bit"); + CHECK(s_evt_last_options == (osFlagsWaitAny | osFlagsNoClear), + "the ready bit is not consumed (ftpTask and friends may still see it)"); + CHECK(s_beat_calls == 3, "two not-ready rounds each beat once + 1 at the loop head"); + CHECK(s_sntp_run_calls == 0, "no SNTP round before the barrier passes"); + CHECK(s_wd_bind_calls == 1, "still registers the beat routing first"); - /* C) 屏障期间持续喂心跳(就绪位长期不置位) */ + /* D) 屏障期间持续喂心跳(就绪位长期不置位,2026-10-02 台架根因的回归防线) */ fakes_reset(); sys_evtHandle = (osEventFlagsId_t)0x55; s_evt_default = osFlagsErrorTimeout; @@ -730,93 +315,25 @@ static void tc_task(void) { if (setjmp(s_jmp) == 0) { time_sync_task(NULL); } - CHECK(s_evt_calls == 4, "TC-TIMESYNC-042 barrier waited 4 times, none ready (loop did not hang)"); - CHECK(s_beat_calls == 3, "TC-TIMESYNC-042b beats once after each not-ready round (before the 4th breaks out)"); - - /* D) 网络代次变化 -> 关闭旧监听并重监听 */ - fakes_reset(); - s_gen_seq[0] = 1u; - s_gen_seq[1] = 2u; - s_gen_seq_n = 2; - s_gen_default = 2u; - s_gen_longjmp_at = 4; - if (setjmp(s_jmp) == 0) { - time_sync_task(NULL); - } - CHECK((s_close_calls == 1) && (s_close_socks[0] == 5), "TC-TIMESYNC-043 closes the old listener when the network generation changes"); - /* INFO 三条:两次 open_listener 成功各一条 "TCP listen on" + 一条 "network re-init, relisten" */ - CHECK(g_tstest_info_count == 3, "TC-TIMESYNC-043b leaves the network re-init, relisten trace (plus 2 listen-success lines)"); - CHECK(s_accept_calls == 2, "TC-TIMESYNC-043c polls accept only once per loop then gives up"); - CHECK((s_socket_calls == 2) && (s_bind_calls == 2) && (s_listen_calls == 2), - "TC-TIMESYNC-043d the second loop rebuilt the listener"); - - /* E) 监听建立失败 -> 5 次重试后放弃并退避 2000ms */ - fakes_reset(); - s_socket_default = -1; - s_gen_longjmp_at = 2; - if (setjmp(s_jmp) == 0) { - time_sync_task(NULL); - } - CHECK(s_socket_calls == 5, "TC-TIMESYNC-044 open_listener gives up after 5 internal retries"); - CHECK(g_tstest_err_count == 6, "TC-TIMESYNC-044b 5 net_socket FAIL lines + 1 retry later line"); - CHECK(s_delay_calls == 90, "TC-TIMESYNC-044c backoff 5x500 ms + 2000 ms = 90 slices"); - CHECK(s_beat_calls == 97, "TC-TIMESYNC-044d beat count = 2 + 5 + 50 + 40"); - CHECK(s_beat_id_bad == 0, "TC-TIMESYNC-044e every beat sets the TIMESYNC bit"); + CHECK(s_evt_calls == 4, "barrier waited 4 times, none ready (loop did not hang)"); + CHECK(s_beat_calls == 3, "beats once after each not-ready round (before the 4th breaks out)"); + CHECK((s_gen_calls == 0) && (s_sntp_run_calls == 0), + "network untouched until NET_READY: no generation read, no SNTP round"); } -/* ===================== 对时操作留痕四组(道G task-50) ========================= - * 成功应用投一条 quiet OPER;被拒不投(失败不得记成"改过时钟"); - * 连续两次各一条不合并;魔术字不符不投。 - * ⚠ 断言标签用语义行名:编号规范 §9 批次三已对测试用例号停发(用户裁定 2026-10-27), - * 本组是新口径下写的——追溯靠函数名 tc_oper_trace + 下面的语义标签。 */ -static void tc_oper_trace(void) { - uint8_t pkt[16]; - - /* 1) 对时成功 → 恰一条 OPER */ +/* ===================== SNTP 驻留后的循环延续 ===================== + * time_sntp_run 的契约是"单次驻留 ≤50ms,调用方在片间喂心跳"——替身模拟一次 + * 驻留后返回,循环必须按原节拍继续(心跳不欠账)。 */ +static void tc_sntp_dwell_then_continue(void) { fakes_reset(); - make_time_packet(pkt, 0x20251027u); - recv_set(0, 8, pkt, 8, 0U); - s_recv_seq_n = 1; - s_set_unix_rc = 0; - time_sync_handle_conn(7); - CHECK(s_set_unix_calls == 1, "sync trace: clock layer applied once"); - CHECK(s_post_calls == 1, "sync trace on success: exactly one operation record"); - CHECK(s_post_id == (int)SYSLOG_OPER, "sync trace: record goes to the operation log"); - CHECK(strcmp(s_post_last_desc, "clock set by 8888") == 0, - "sync trace: wording of the same family as the 104 time-sync record"); - - /* 2) 对时被拒(时钟层返非 0)→ 零留痕 */ - fakes_reset(); - make_time_packet(pkt, 7u); - recv_set(0, 8, pkt, 8, 0U); - s_recv_seq_n = 1; - s_set_unix_rc = -1; - time_sync_handle_conn(7); - CHECK(s_set_unix_calls == 1, "sync trace rejected: call still reaches the clock layer once"); - CHECK(s_post_calls == 0, "sync trace rejected: NO operation record written"); - - /* 3) 连续两次成功会话 → 两条独立记录,不合并 */ - fakes_reset(); - s_set_unix_rc = 0; - make_time_packet(pkt, 100u); - recv_set(0, 8, pkt, 8, 0U); - recv_set(1, 8, pkt, 8, 0U); - s_recv_seq_n = 2; - time_sync_handle_conn(7); - time_sync_handle_conn(7); - CHECK(s_set_unix_calls == 2, "sync trace twice: two applied syncs"); - CHECK(s_post_calls == 2, "sync trace twice: two independent records, not merged"); - CHECK(s_post_desc_count == 2, "sync trace twice: both records are the time-sync wording (no coalescing latch)"); - - /* 4) 前缀不是 TIME → 时钟层都没到,更不该留痕 */ - fakes_reset(); - (void)memset(pkt, 'X', 8); - recv_set(0, 8, pkt, 8, 0U); - s_recv_seq_n = 1; - s_set_unix_rc = 0; - time_sync_handle_conn(7); - CHECK((s_set_unix_calls == 0) && (s_post_calls == 0), - "sync trace bad magic: neither moves the clock nor writes a record"); + s_sntp_run_longjmp_at = 2; /* 第 2 轮驻留中脱出(模拟慢收片场景收尾) */ + if (setjmp(s_jmp) == 0) { + time_sync_task(NULL); + } + CHECK(s_sntp_run_calls == 2, "loop re-enters the SNTP client every tick"); + CHECK(s_beat_calls == 6, "beats = (head + 4 slices) for round 1 + head of round 2 (dwell bail-out)"); + CHECK(s_delay_calls == 4, "round 1 waited its full 200 ms; round 2 bailed mid-tick"); + CHECK(s_beat_id_bad == 0, "the beat bit stays TIMESYNC across rounds"); } /* ===================== main ===================== */ @@ -826,14 +343,10 @@ int main(void) { printf("-- delay_fed (heartbeat-fed slicing) --\n"); tc_delay_fed(); - printf("-- open_listener (retry / backoff) --\n"); - tc_open_listener(); - printf("-- handle_conn (packet reassembly / budget) --\n"); - tc_handle_conn(); - printf("-- time_sync_task (barrier / accept / relisten) --\n"); + printf("-- time_sync_task (barrier / SNTP cadence) --\n"); tc_task(); - printf("-- operation trace on sync (task-50) --\n"); - tc_oper_trace(); + printf("-- sntp dwell then continue --\n"); + tc_sntp_dwell_then_continue(); printf("\n=== Time Sync Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed); diff --git a/test/host/time_sync_server.py b/test/host/time_sync_server.py deleted file mode 100644 index ff54a22c..00000000 --- a/test/host/time_sync_server.py +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -本地时间同步服务(PC -> MCU 板子) - -功能: - 周期性把本机(PC)的本地时间,通过 TCP 推送到 STM32 板子。 - 板子端在 timeSyncTask 中以 TCP Server 方式监听端口,收到后调用 - sys_clock_set_unix() 更新系统墙钟并写回 SD2506 RTC,使板子时间 == 你电脑的时间。 - -协议(板子端实现见 App/sync/time_sync.c 的 time_sync_handle_conn): - 字节 0..3 : 魔术字 "TIME" (0x54 0x49 0x4D 0x45) - 字节 4..7 : 本地 Unix 时间戳,uint32,大端(主机字节序转换后由板子分解) - -用法: - python time_sync_server.py # 默认推送到设备 IP(bench_defaults.DEVICE_IP):8888 - python time_sync_server.py <设备IP> # 指定板子 IP - python time_sync_server.py <设备IP> 9000 3 # 指定端口与间隔(秒) - -按 Ctrl+C 停止。 -""" - -import socket -import struct -import sys -import time -import calendar - -import bench_defaults # 台架连接默认值单一真源 - -DEFAULT_MCU_IP = bench_defaults.DEVICE_IP -DEFAULT_PORT = 8888 -DEFAULT_INTERVAL = 5.0 - - -def main(): - mcu_ip = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_MCU_IP - port = int(sys.argv[2]) if len(sys.argv) > 2 else DEFAULT_PORT - interval = float(sys.argv[3]) if len(sys.argv) > 3 else DEFAULT_INTERVAL - - print("[time_sync] 本地时间推送服务已启动(TCP 客户端)") - print(f" 目标板子 : {mcu_ip}:{port}") - print(f" 推送间隔 : {interval}s") - print(f" 协议帧 : 'TIME' + uint32(BE) 本地 Unix 时间戳") - print("[time_sync] 按 Ctrl+C 停止") - print("-" * 48) - - while True: - try: - # 本地 wall-clock 当作 UTC 算出的 epoch(与电脑显示一致)。 - # 注意:板子 RTC 存本地时间且 unix_to_sd2506 无时区处理, - # 故必须发"本地墙钟当 UTC"的伪 epoch(calendar.timegm), - # 不能用 time.mktime(返回真正 UTC epoch,会导致板子慢 8 小时)。 - local_epoch = int(calendar.timegm(time.localtime())) - pkt = b"TIME" + struct.pack(">I", local_epoch) - - with socket.create_connection((mcu_ip, port), timeout=5) as sock: - sock.sendall(pkt) - # 关键:发送后保持连接打开,给板子留出读完 8 字节的窗口。 - # 若立即 close,CH395F 收到 FIN 会丢弃接收缓冲里尚未被板子读出的 - # 8 字节时间包,导致板子 net_recv 看到 CLOSED 而无数据、时间同步失败。 - # 板子会话收尾**不再 close**(单连接监听跨会话常驻, - # EOF 时 net_close 会连监听一起毁掉)——这里 recv 等 2s 宽限后由 - # 本端关闭;板子 net 层 auto_relisten 随即恢复监听,下一轮照常连接。 - sock.settimeout(2.0) - try: - sock.recv(64) - except (socket.timeout, OSError): - pass - - now_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) - print(f"[{time.strftime('%H:%M:%S')}] -> {now_str} (epoch={local_epoch})") - except OSError as e: - print(f"[{time.strftime('%H:%M:%S')}] 连接/发送失败: {e}") - - time.sleep(interval) - - -if __name__ == "__main__": - try: - main() - except KeyboardInterrupt: - print("\n[time_sync] 已停止") diff --git a/tools/arch_diagram/gen_arch_svg.py b/tools/arch_diagram/gen_arch_svg.py index 5dac76ff..2559ec58 100644 --- a/tools/arch_diagram/gen_arch_svg.py +++ b/tools/arch_diagram/gen_arch_svg.py @@ -59,7 +59,7 @@ L_SVC = dict( "protocol/iec104(104 上行默认 :2404)", "ui_data_feed(App→UI 装配)", "sys_log · soe(记录引擎)", "console_log(UART5 控制台单写者)", "cfg_file · prot_set_store(配置引擎)", - "lftpd(FTP :21)", "time_sync(TCP 对时 :8888)"], + "lftpd(FTP :21)", "time_sntp(SNTP 对时,UDP 出向;:8888 已退役)"], ) L_MW = dict( title="④ 中间件层", sub="通用库 / 协议栈", color="#16a34a", bg="#f0fdf4", diff --git a/tools/arch_diagram/gen_memory_svg.py b/tools/arch_diagram/gen_memory_svg.py index cd4942db..29b20dc2 100644 --- a/tools/arch_diagram/gen_memory_svg.py +++ b/tools/arch_diagram/gen_memory_svg.py @@ -86,7 +86,7 @@ TASK_STACKS = [ ("ftpTask", 4096, "Normal", 1456, 1404, 2640, 64, "PASS", "lftpd FTP 服务(大缓冲已静态化)"), ("uiTask", 16384, "BelowNormal", 4052, 4052, 12332, 75, "PASS", "LVGL UI 主循环 + 远程显示推流"), ("testTask", 8192, "BelowNormal", 736, 736, 7456, 91, "PASS", "测试套件(产品构建不创建,取静态最坏链)"), - ("timeSyncTask", 2560, "Normal", 1264, 648, 1296, 50, "PASS", "TCP 时间同步(8888)"), + ("timeSyncTask", 2560, "Normal", 1264, 648, 1296, 50, "PASS", "时间同步(8888 退役,SNTP 宿主)"), ("adcTask", 2560, "AboveNormal", 1320, 952, 1240, 48, "PASS", "1 kHz 节拍消费者(语义取数与有效性采集,纯取数)"), ("iec104Task", 2560, "Normal", 1456, 540, 1104, 43, "PASS", "IEC 60870-5-104 上行(默认 2404)"), ("protectTask", 2048, "Normal", 872, 828, 1176, 57, "PASS", "20 ms 保护判据 + 闭锁 + 定值校验和巡检"), diff --git a/tools/arch_diagram/gen_net_stack_svg.py b/tools/arch_diagram/gen_net_stack_svg.py index 4de15e26..cb8e5431 100644 --- a/tools/arch_diagram/gen_net_stack_svg.py +++ b/tools/arch_diagram/gen_net_stack_svg.py @@ -45,7 +45,7 @@ L_APP = dict( title="应用层任务", sub="任意 FreeRTOS 任务可调用公共 API(线程安全)", color="#2563eb", bg="#eff6ff", chips=["net_send() / net_recv() / net_close()(线程安全封装)", "net_select() / net_poll_events() / net_set_event_cb()", - "消费方:lftpd(FTP :21)· time_sync(对时 :8888)· ui_net_sink(远程显示 :7003)"], + "消费方:lftpd(FTP :21)· time_sntp(SNTP 出向对时)· ui_net_sink(远程显示 :7003)"], ) L_QUEUE = dict( title="消息队列", sub="osMessageQueue · CubeMX 生成,在 MX_FREERTOS_Init() 中创建", color="#7c3aed", bg="#f5f3ff",