Merge pull request 'refactor(time_sync)!: 退役 TCP :8888 对时任务,timeSyncTask 转纯 SNTP 校时节拍宿主(#78)' (#80) from remove-8888-timesync-a6657322808f into master
CI / 静态门禁 (pre-commit) (push) Failing after 5m15s
CI / 主机侧单测 (MSVC) (push) Successful in 39s

Reviewed-on: #80
This commit was merged in pull request #80.
This commit is contained in:
2026-11-04 18:26:40 +08:00
50 changed files with 621 additions and 1567 deletions
+9 -9
View File
@@ -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 加)
+1 -1
View File
@@ -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`):
+1 -1
View File
@@ -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
+6 -7
View File
@@ -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` — 就绪屏障
+2 -2
View File
@@ -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;
+39 -226
View File
@@ -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 <string.h>
#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);
}
}
+6 -8
View File
@@ -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);
+2 -2
View File
@@ -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 上真实记录的翻页读取、
+2 -5
View File
@@ -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 → 板子) |
### 串口调试
+1 -1
View File
@@ -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 |
**板级测试/**(上板测试:模块测试指南 + 实机实施流程;动手写测试前先读)
+1 -1
View File
@@ -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 读到同一值。
+2 -2
View File
@@ -9,8 +9,8 @@
<text x="251" y="139" font-size="13" fill="#1e293b" text-anchor="middle">net_send() / net_recv() / net_close()(线程安全封装)</text>
<rect x="468" y="116" width="431" height="38" rx="8" fill="#ffffff" stroke="#2563eb" stroke-opacity="0.55"/>
<text x="684" y="139" font-size="13" fill="#1e293b" text-anchor="middle">net_select() / net_poll_events() / net_set_event_cb()</text>
<rect x="44" y="164" width="601" height="38" rx="8" fill="#ffffff" stroke="#2563eb" stroke-opacity="0.55"/>
<text x="344" y="187" font-size="13" fill="#1e293b" text-anchor="middle">消费方:lftpd(FTP :21)· time_sync(对时 :8888)· ui_net_sink(远程显示 :7003)</text>
<rect x="44" y="164" width="620" height="38" rx="8" fill="#ffffff" stroke="#2563eb" stroke-opacity="0.55"/>
<text x="354" y="187" font-size="13" fill="#1e293b" text-anchor="middle">消费方:lftpd(FTP :21)· time_sntp(SNTP 出向对时)· ui_net_sink(远程显示 :7003)</text>
<rect x="26" y="262" width="1148" height="140" rx="12" fill="#f5f3ff" stroke="#7c3aed" stroke-opacity="0.45"/>
<text x="44" y="288" font-size="17" font-weight="700" fill="#7c3aed">消息队列</text>
<text x="124" y="288" font-size="12" fill="#64748b">osMessageQueue · CubeMX 生成,在 MX_FREERTOS_Init() 中创建</text>

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

+75 -75
View File
@@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1560" height="1192" viewBox="0 0 1560 1192" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif">
<rect x="0" y="0" width="1560" height="1192" fill="#ffffff"/>
<svg xmlns="http://www.w3.org/2000/svg" width="1560" height="1240" viewBox="0 0 1560 1240" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif">
<rect x="0" y="0" width="1560" height="1240" fill="#ffffff"/>
<text x="26" y="40" font-size="20" font-weight="700" fill="#0f172a">STM32F4-Base 项目分层架构</text>
<text x="26" y="62" font-size="13" fill="#64748b">STM32F407ZGTx @168MHz · FreeRTOS V10.3.1 (CMSIS-RTOS2) · 上层只依赖相邻下层</text>
<rect x="26" y="78" width="1508" height="92" rx="12" fill="#eff6ff" stroke="#2563eb" stroke-opacity="0.45"/>
@@ -7,7 +7,7 @@
<text x="134" y="104" font-size="12" fill="#64748b">顶层共享配置 · 被 UI 与基础服务共用</text>
<rect x="44" y="116" width="489" height="38" rx="8" fill="#ffffff" stroke="#2563eb" stroke-opacity="0.55"/>
<text x="288" y="139" font-size="13" fill="#1e293b" text-anchor="middle">net_cfg(网口配置:IP/掩码/网关/MAC · /config.ini [network] 段)</text>
<rect x="26" y="214" width="741" height="284" rx="12" fill="#f5f3ff" stroke="#7c3aed" stroke-opacity="0.45"/>
<rect x="26" y="214" width="741" height="332" rx="12" fill="#f5f3ff" stroke="#7c3aed" stroke-opacity="0.45"/>
<text x="44" y="240" font-size="17" font-weight="700" fill="#7c3aed">② UI 层</text>
<text x="129" y="240" font-size="12" fill="#64748b">Drivers/BSP/UI · 一份显存三出(模拟器/屏板/远程)</text>
<rect x="44" y="252" width="162" height="38" rx="8" fill="#ffffff" stroke="#7c3aed" stroke-opacity="0.55"/>
@@ -32,7 +32,7 @@
<text x="330" y="419" font-size="13" fill="#1e293b" text-anchor="middle">ui_font_cn_12(中文字体)</text>
<rect x="444" y="396" width="172" height="38" rx="8" fill="#ffffff" stroke="#7c3aed" stroke-opacity="0.55"/>
<text x="530" y="419" font-size="13" fill="#1e293b" text-anchor="middle">ui_deco(浅 V 装饰)</text>
<rect x="793" y="214" width="741" height="284" rx="12" fill="#f0fdfa" stroke="#0d9488" stroke-opacity="0.45"/>
<rect x="793" y="214" width="741" height="332" rx="12" fill="#f0fdfa" stroke="#0d9488" stroke-opacity="0.45"/>
<text x="811" y="240" font-size="17" font-weight="700" fill="#0d9488">③ 基础服务层</text>
<text x="935" y="240" font-size="12" fill="#64748b">应用侧服务 / 编排</text>
<rect x="811" y="252" width="170" height="38" rx="8" fill="#ffffff" stroke="#0d9488" stroke-opacity="0.55"/>
@@ -55,77 +55,77 @@
<text x="960" y="467" font-size="13" fill="#1e293b" text-anchor="middle">cfg_file · prot_set_store(配置引擎)</text>
<rect x="1119" y="444" width="148" height="38" rx="8" fill="#ffffff" stroke="#0d9488" stroke-opacity="0.55"/>
<text x="1193" y="467" font-size="13" fill="#1e293b" text-anchor="middle">lftpd(FTP :21)</text>
<rect x="1277" y="444" width="227" height="38" rx="8" fill="#ffffff" stroke="#0d9488" stroke-opacity="0.55"/>
<text x="1391" y="467" font-size="13" fill="#1e293b" text-anchor="middle">time_sync(TCP 对时 :8888)</text>
<rect x="26" y="542" width="1508" height="140" rx="12" fill="#f0fdf4" stroke="#16a34a" stroke-opacity="0.45"/>
<text x="44" y="568" font-size="17" font-weight="700" fill="#16a34a">④ 中间件层</text>
<text x="151" y="568" font-size="12" fill="#64748b">通用库 / 协议栈</text>
<rect x="44" y="580" width="675" height="38" rx="8" fill="#ffffff" stroke="#16a34a" stroke-opacity="0.55"/>
<text x="381" y="603" font-size="13" fill="#1e293b" text-anchor="middle">net_socket / net_select / net_events / net_listener / net_pending(BSD API + 阻塞模型)</text>
<rect x="729" y="580" width="147" height="38" rx="8" fill="#ffffff" stroke="#16a34a" stroke-opacity="0.55"/>
<text x="803" y="603" font-size="13" fill="#1e293b" text-anchor="middle">FatFs(文件系统)</text>
<rect x="887" y="580" width="186" height="38" rx="8" fill="#ffffff" stroke="#16a34a" stroke-opacity="0.55"/>
<text x="980" y="603" font-size="13" fill="#1e293b" text-anchor="middle">nand_ftl(dhara FTL)</text>
<rect x="1083" y="580" width="164" height="38" rx="8" fill="#ffffff" stroke="#16a34a" stroke-opacity="0.55"/>
<text x="1165" y="603" font-size="13" fill="#1e293b" text-anchor="middle">LVGL v9.3(图形库)</text>
<rect x="1258" y="580" width="177" height="38" rx="8" fill="#ffffff" stroke="#16a34a" stroke-opacity="0.55"/>
<text x="1347" y="603" font-size="13" fill="#1e293b" text-anchor="middle">sys_clock(墙钟基准)</text>
<rect x="44" y="628" width="219" height="38" rx="8" fill="#ffffff" stroke="#16a34a" stroke-opacity="0.55"/>
<text x="153" y="651" font-size="13" fill="#1e293b" text-anchor="middle">task_watchdog(IWDG 判定)</text>
<rect x="26" y="726" width="1508" height="92" rx="12" fill="#fffbeb" stroke="#d97706" stroke-opacity="0.45"/>
<text x="44" y="752" font-size="17" font-weight="700" fill="#d97706">⑤ 驱动层</text>
<text x="134" y="752" font-size="12" fill="#64748b">Drivers/BSP · 芯片时序/中断</text>
<rect x="44" y="764" width="178" height="38" rx="8" fill="#ffffff" stroke="#d97706" stroke-opacity="0.55"/>
<text x="133" y="787" font-size="13" fill="#1e293b" text-anchor="middle">ch395f(SPI2 + DMA)</text>
<rect x="232" y="764" width="163" height="38" rx="8" fill="#ffffff" stroke="#d97706" stroke-opacity="0.55"/>
<text x="314" y="787" font-size="13" fill="#1e293b" text-anchor="middle">gd5f2gq5ue(SPI1)</text>
<rect x="406" y="764" width="195" height="38" rx="8" fill="#ffffff" stroke="#d97706" stroke-opacity="0.55"/>
<text x="503" y="787" font-size="13" fill="#1e293b" text-anchor="middle">tpafe5160(并行 16 位)</text>
<rect x="611" y="764" width="133" height="38" rx="8" fill="#ffffff" stroke="#d97706" stroke-opacity="0.55"/>
<text x="677" y="787" font-size="13" fill="#1e293b" text-anchor="middle">sd2506(I2C1)</text>
<rect x="754" y="764" width="171" height="38" rx="8" fill="#ffffff" stroke="#d97706" stroke-opacity="0.55"/>
<text x="840" y="787" font-size="13" fill="#1e293b" text-anchor="middle">rs485(UART4 + DE)</text>
<rect x="935" y="764" width="388" height="38" rx="8" fill="#ffffff" stroke="#d97706" stroke-opacity="0.55"/>
<text x="1130" y="787" font-size="13" fill="#1e293b" text-anchor="middle">sb_link · sb_proto(屏板链路 · USART1 700 kbaud)</text>
<rect x="26" y="862" width="1508" height="92" rx="12" fill="#f1f5f9" stroke="#475569" stroke-opacity="0.45"/>
<text x="44" y="888" font-size="17" font-weight="700" fill="#475569">⑥ 内核 / HAL</text>
<text x="176" y="888" font-size="12" fill="#64748b">调度 · 外设抽象</text>
<rect x="44" y="900" width="329" height="38" rx="8" fill="#ffffff" stroke="#475569" stroke-opacity="0.55"/>
<text x="208" y="923" font-size="13" fill="#1e293b" text-anchor="middle">FreeRTOS V10.3.1(CMSIS-RTOS2 · heap_4)</text>
<rect x="383" y="900" width="189" height="38" rx="8" fill="#ffffff" stroke="#475569" stroke-opacity="0.55"/>
<text x="478" y="923" font-size="13" fill="#1e293b" text-anchor="middle">STM32 HAL(时基 TIM7)</text>
<rect x="582" y="900" width="144" height="38" rx="8" fill="#ffffff" stroke="#475569" stroke-opacity="0.55"/>
<text x="655" y="923" font-size="13" fill="#1e293b" text-anchor="middle">CMSIS / 启动文件</text>
<rect x="26" y="998" width="1508" height="140" rx="12" fill="#fff1f2" stroke="#e11d48" stroke-opacity="0.45"/>
<text x="44" y="1024" font-size="17" font-weight="700" fill="#e11d48">⑦ 硬件</text>
<text x="117" y="1024" font-size="12" fill="#64748b">STM32F407ZGTx @168MHz</text>
<rect x="44" y="1036" width="179" height="38" rx="8" fill="#ffffff" stroke="#e11d48" stroke-opacity="0.55"/>
<text x="133" y="1059" font-size="13" fill="#1e293b" text-anchor="middle">CH395F 以太网(SPI2)</text>
<rect x="233" y="1036" width="201" height="38" rx="8" fill="#ffffff" stroke="#e11d48" stroke-opacity="0.55"/>
<text x="334" y="1059" font-size="13" fill="#1e293b" text-anchor="middle">GD5F2GQ5UE NAND(SPI1)</text>
<rect x="445" y="1036" width="182" height="38" rx="8" fill="#ffffff" stroke="#e11d48" stroke-opacity="0.55"/>
<text x="536" y="1059" font-size="13" fill="#1e293b" text-anchor="middle">TPAFE5160 ADC(并行)</text>
<rect x="637" y="1036" width="163" height="38" rx="8" fill="#ffffff" stroke="#e11d48" stroke-opacity="0.55"/>
<text x="718" y="1059" font-size="13" fill="#1e293b" text-anchor="middle">SD2506 RTC(I2C1)</text>
<rect x="810" y="1036" width="202" height="38" rx="8" fill="#ffffff" stroke="#e11d48" stroke-opacity="0.55"/>
<text x="912" y="1059" font-size="13" fill="#1e293b" text-anchor="middle">RS-485(UART4 · 未接线)</text>
<rect x="1023" y="1036" width="219" height="38" rx="8" fill="#ffffff" stroke="#e11d48" stroke-opacity="0.55"/>
<text x="1133" y="1059" font-size="13" fill="#1e293b" text-anchor="middle">屏板(USART1 · 700 kbaud)</text>
<rect x="1253" y="1036" width="249" height="38" rx="8" fill="#ffffff" stroke="#e11d48" stroke-opacity="0.55"/>
<text x="1377" y="1059" font-size="13" fill="#1e293b" text-anchor="middle">RS232#1 日志(UART5 · 115200)</text>
<rect x="44" y="1084" width="77" height="38" rx="8" fill="#ffffff" stroke="#e11d48" stroke-opacity="0.55"/>
<text x="82" y="1107" font-size="13" fill="#1e293b" text-anchor="middle">6x LED</text>
<rect x="811" y="492" width="355" height="38" rx="8" fill="#ffffff" stroke="#0d9488" stroke-opacity="0.55"/>
<text x="988" y="515" font-size="13" fill="#1e293b" text-anchor="middle">time_sntp(SNTP 对时,UDP 出向;:8888 已退役)</text>
<rect x="26" y="590" width="1508" height="140" rx="12" fill="#f0fdf4" stroke="#16a34a" stroke-opacity="0.45"/>
<text x="44" y="616" font-size="17" font-weight="700" fill="#16a34a">④ 中间件层</text>
<text x="151" y="616" font-size="12" fill="#64748b">通用库 / 协议栈</text>
<rect x="44" y="628" width="675" height="38" rx="8" fill="#ffffff" stroke="#16a34a" stroke-opacity="0.55"/>
<text x="381" y="651" font-size="13" fill="#1e293b" text-anchor="middle">net_socket / net_select / net_events / net_listener / net_pending(BSD API + 阻塞模型)</text>
<rect x="729" y="628" width="147" height="38" rx="8" fill="#ffffff" stroke="#16a34a" stroke-opacity="0.55"/>
<text x="803" y="651" font-size="13" fill="#1e293b" text-anchor="middle">FatFs(文件系统)</text>
<rect x="887" y="628" width="186" height="38" rx="8" fill="#ffffff" stroke="#16a34a" stroke-opacity="0.55"/>
<text x="980" y="651" font-size="13" fill="#1e293b" text-anchor="middle">nand_ftl(dhara FTL)</text>
<rect x="1083" y="628" width="164" height="38" rx="8" fill="#ffffff" stroke="#16a34a" stroke-opacity="0.55"/>
<text x="1165" y="651" font-size="13" fill="#1e293b" text-anchor="middle">LVGL v9.3(图形库)</text>
<rect x="1258" y="628" width="177" height="38" rx="8" fill="#ffffff" stroke="#16a34a" stroke-opacity="0.55"/>
<text x="1347" y="651" font-size="13" fill="#1e293b" text-anchor="middle">sys_clock(墙钟基准)</text>
<rect x="44" y="676" width="219" height="38" rx="8" fill="#ffffff" stroke="#16a34a" stroke-opacity="0.55"/>
<text x="153" y="699" font-size="13" fill="#1e293b" text-anchor="middle">task_watchdog(IWDG 判定)</text>
<rect x="26" y="774" width="1508" height="92" rx="12" fill="#fffbeb" stroke="#d97706" stroke-opacity="0.45"/>
<text x="44" y="800" font-size="17" font-weight="700" fill="#d97706">⑤ 驱动层</text>
<text x="134" y="800" font-size="12" fill="#64748b">Drivers/BSP · 芯片时序/中断</text>
<rect x="44" y="812" width="178" height="38" rx="8" fill="#ffffff" stroke="#d97706" stroke-opacity="0.55"/>
<text x="133" y="835" font-size="13" fill="#1e293b" text-anchor="middle">ch395f(SPI2 + DMA)</text>
<rect x="232" y="812" width="163" height="38" rx="8" fill="#ffffff" stroke="#d97706" stroke-opacity="0.55"/>
<text x="314" y="835" font-size="13" fill="#1e293b" text-anchor="middle">gd5f2gq5ue(SPI1)</text>
<rect x="406" y="812" width="195" height="38" rx="8" fill="#ffffff" stroke="#d97706" stroke-opacity="0.55"/>
<text x="503" y="835" font-size="13" fill="#1e293b" text-anchor="middle">tpafe5160(并行 16 位)</text>
<rect x="611" y="812" width="133" height="38" rx="8" fill="#ffffff" stroke="#d97706" stroke-opacity="0.55"/>
<text x="677" y="835" font-size="13" fill="#1e293b" text-anchor="middle">sd2506(I2C1)</text>
<rect x="754" y="812" width="171" height="38" rx="8" fill="#ffffff" stroke="#d97706" stroke-opacity="0.55"/>
<text x="840" y="835" font-size="13" fill="#1e293b" text-anchor="middle">rs485(UART4 + DE)</text>
<rect x="935" y="812" width="388" height="38" rx="8" fill="#ffffff" stroke="#d97706" stroke-opacity="0.55"/>
<text x="1130" y="835" font-size="13" fill="#1e293b" text-anchor="middle">sb_link · sb_proto(屏板链路 · USART1 700 kbaud)</text>
<rect x="26" y="910" width="1508" height="92" rx="12" fill="#f1f5f9" stroke="#475569" stroke-opacity="0.45"/>
<text x="44" y="936" font-size="17" font-weight="700" fill="#475569">⑥ 内核 / HAL</text>
<text x="176" y="936" font-size="12" fill="#64748b">调度 · 外设抽象</text>
<rect x="44" y="948" width="329" height="38" rx="8" fill="#ffffff" stroke="#475569" stroke-opacity="0.55"/>
<text x="208" y="971" font-size="13" fill="#1e293b" text-anchor="middle">FreeRTOS V10.3.1(CMSIS-RTOS2 · heap_4)</text>
<rect x="383" y="948" width="189" height="38" rx="8" fill="#ffffff" stroke="#475569" stroke-opacity="0.55"/>
<text x="478" y="971" font-size="13" fill="#1e293b" text-anchor="middle">STM32 HAL(时基 TIM7)</text>
<rect x="582" y="948" width="144" height="38" rx="8" fill="#ffffff" stroke="#475569" stroke-opacity="0.55"/>
<text x="655" y="971" font-size="13" fill="#1e293b" text-anchor="middle">CMSIS / 启动文件</text>
<rect x="26" y="1046" width="1508" height="140" rx="12" fill="#fff1f2" stroke="#e11d48" stroke-opacity="0.45"/>
<text x="44" y="1072" font-size="17" font-weight="700" fill="#e11d48">⑦ 硬件</text>
<text x="117" y="1072" font-size="12" fill="#64748b">STM32F407ZGTx @168MHz</text>
<rect x="44" y="1084" width="179" height="38" rx="8" fill="#ffffff" stroke="#e11d48" stroke-opacity="0.55"/>
<text x="133" y="1107" font-size="13" fill="#1e293b" text-anchor="middle">CH395F 以太网(SPI2)</text>
<rect x="233" y="1084" width="201" height="38" rx="8" fill="#ffffff" stroke="#e11d48" stroke-opacity="0.55"/>
<text x="334" y="1107" font-size="13" fill="#1e293b" text-anchor="middle">GD5F2GQ5UE NAND(SPI1)</text>
<rect x="445" y="1084" width="182" height="38" rx="8" fill="#ffffff" stroke="#e11d48" stroke-opacity="0.55"/>
<text x="536" y="1107" font-size="13" fill="#1e293b" text-anchor="middle">TPAFE5160 ADC(并行)</text>
<rect x="637" y="1084" width="163" height="38" rx="8" fill="#ffffff" stroke="#e11d48" stroke-opacity="0.55"/>
<text x="718" y="1107" font-size="13" fill="#1e293b" text-anchor="middle">SD2506 RTC(I2C1)</text>
<rect x="810" y="1084" width="202" height="38" rx="8" fill="#ffffff" stroke="#e11d48" stroke-opacity="0.55"/>
<text x="912" y="1107" font-size="13" fill="#1e293b" text-anchor="middle">RS-485(UART4 · 未接线)</text>
<rect x="1023" y="1084" width="219" height="38" rx="8" fill="#ffffff" stroke="#e11d48" stroke-opacity="0.55"/>
<text x="1133" y="1107" font-size="13" fill="#1e293b" text-anchor="middle">屏板(USART1 · 700 kbaud)</text>
<rect x="1253" y="1084" width="249" height="38" rx="8" fill="#ffffff" stroke="#e11d48" stroke-opacity="0.55"/>
<text x="1377" y="1107" font-size="13" fill="#1e293b" text-anchor="middle">RS232#1 日志(UART5 · 115200)</text>
<rect x="44" y="1132" width="77" height="38" rx="8" fill="#ffffff" stroke="#e11d48" stroke-opacity="0.55"/>
<text x="82" y="1155" font-size="13" fill="#1e293b" text-anchor="middle">6x LED</text>
<line x1="780.0" y1="176.0" x2="780.0" y2="199.0" stroke="#94a3b8" stroke-width="2.5"/>
<path d="M 780.0 206.0 l -6 -9 h 12 z" fill="#94a3b8"/>
<line x1="396.5" y1="504.0" x2="396.5" y2="527.0" stroke="#94a3b8" stroke-width="2.5"/>
<path d="M 396.5 534.0 l -6 -9 h 12 z" fill="#94a3b8"/>
<line x1="1163.5" y1="504.0" x2="1163.5" y2="527.0" stroke="#94a3b8" stroke-width="2.5"/>
<path d="M 1163.5 534.0 l -6 -9 h 12 z" fill="#94a3b8"/>
<line x1="780.0" y1="688.0" x2="780.0" y2="711.0" stroke="#94a3b8" stroke-width="2.5"/>
<path d="M 780.0 718.0 l -6 -9 h 12 z" fill="#94a3b8"/>
<line x1="780.0" y1="824.0" x2="780.0" y2="847.0" stroke="#94a3b8" stroke-width="2.5"/>
<path d="M 780.0 854.0 l -6 -9 h 12 z" fill="#94a3b8"/>
<line x1="780.0" y1="960.0" x2="780.0" y2="983.0" stroke="#94a3b8" stroke-width="2.5"/>
<path d="M 780.0 990.0 l -6 -9 h 12 z" fill="#94a3b8"/>
<text x="26" y="1174" font-size="12" fill="#94a3b8">上层只依赖相邻下层(单向);UI 层与基础服务层同级并列,均使用顶层 net_cfg。跨层调用细节见 docs/架构设计/项目架构总览.md §1.2</text>
<line x1="396.5" y1="552.0" x2="396.5" y2="575.0" stroke="#94a3b8" stroke-width="2.5"/>
<path d="M 396.5 582.0 l -6 -9 h 12 z" fill="#94a3b8"/>
<line x1="1163.5" y1="552.0" x2="1163.5" y2="575.0" stroke="#94a3b8" stroke-width="2.5"/>
<path d="M 1163.5 582.0 l -6 -9 h 12 z" fill="#94a3b8"/>
<line x1="780.0" y1="736.0" x2="780.0" y2="759.0" stroke="#94a3b8" stroke-width="2.5"/>
<path d="M 780.0 766.0 l -6 -9 h 12 z" fill="#94a3b8"/>
<line x1="780.0" y1="872.0" x2="780.0" y2="895.0" stroke="#94a3b8" stroke-width="2.5"/>
<path d="M 780.0 902.0 l -6 -9 h 12 z" fill="#94a3b8"/>
<line x1="780.0" y1="1008.0" x2="780.0" y2="1031.0" stroke="#94a3b8" stroke-width="2.5"/>
<path d="M 780.0 1038.0 l -6 -9 h 12 z" fill="#94a3b8"/>
<text x="26" y="1222" font-size="12" fill="#94a3b8">上层只依赖相邻下层(单向);UI 层与基础服务层同级并列,均使用顶层 net_cfg。跨层调用细节见 docs/架构设计/项目架构总览.md §1.2</text>
</svg>

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

+19 -15
View File
@@ -126,7 +126,8 @@ flowchart LR
end
DEV -- "USB:ST-Link 下载器" --> TGT
DEV -- "USB→RS-232 适配器:RS232#1 @115200 调试日志" --> TGT
TGT -- "以太网 CH395F<br/>FTP:21 / 对时:8888 / 远程显示:7003" --> RMT
TGT -- "以太网 CH395F<br/>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 上送 |
+6 -6
View File
@@ -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 关键依赖
+6 -3
View File
@@ -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」等于给扫描风暴开门,且该值现在没有任何既有消费点 | 有真实消费场景或多连接后端立项时再评 |
+1
View File
@@ -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 |
---
@@ -47,10 +47,10 @@ dhara 失败类 +84;RO 另 +208,全是该失败类名表)→ `ER_IROM1`
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
| 区域 | 用量 | 容量 | 占比 |
|---|---|---|---|
| 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%** |
<!-- MEM_SNAPSHOT:END -->
各区的容量与常量来源:
@@ -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%) |
+1 -1
View File
@@ -15,7 +15,7 @@
<!-- MEM_BUDGET:BEGIN generated by scripts/ram_baseline.py -->
| 区域 | 用量 | 容量 | 占比 | 门禁(≤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 |
@@ -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` |
@@ -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 双管(路由到调用者任务) |
@@ -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 <epoch>
[CLK] sys_clock_set: SD2506 RTC written
[TIME_SYNC] time_sync: wall clock updated (epoch=<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、退避与周期、阶跃域守卫、配置注册表)。
+4 -4
View File
@@ -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()` | 代次变化 → 关旧监听/客户端 → 重建 |
**关键设计点**:
@@ -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 上传的段不生效);
+6 -6
View File
@@ -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)<br/>→ 串口输出:waiting for connection...
U->>U: wait NET_READY(Any|NoClear)→ ui_port_init → ui_menu_init → ui_net_sink_start(7003)<br/>→ 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() 纯消费<br/>→ 标定→相量→测频 → wave_ring_push/tick → 每 200ms meas_tick() 发布快照
@@ -217,7 +217,7 @@ flowchart TB
NS --> Q["netMsgQueue<br/>(串行化到 netTask)"]
Q --> NT["netTask<br/>net_poll() + net_process_messages()"]
NS --> C1["lftpd<br/>FTP :21"]
NS --> C2["time_sync<br/>对时 :8888"]
NS --> C2["time_sync 任务<br/>time_sntp SNTP 客户端<br/>UDP 123 出向(:8888 已退役)"]
NS --> C3["ui_net_sink<br/>RemoDispBus :7003"]
NS --> C4["iec104_serv<br/>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)
@@ -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`)。
@@ -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 <stdint.h>`,无平台依赖 |
`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)。
@@ -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 复位**,
@@ -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:模块测试文档
@@ -35,7 +35,8 @@ flowchart LR
DEV -- "USB:下载器(ST-Link 等)" --> TGT
DEV -- "USB 串口 USART1<br/>COMx @115200 调试日志" --> TGT
TGT -- "以太网(CH395F)<br/>FTP:21 / 对时:8888 / 显示:7003" --> RMT
TGT -- "以太网(CH395F)<br/>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 图 |
@@ -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。
+1 -1
View File
@@ -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 - 状态轮询
@@ -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 |
+13 -7
View File
@@ -32,7 +32,8 @@
> - 带连字符的 `E-<nn>` = **历史审计发现**编号(属主文档已于 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-<nn>` 族,属主文档已于 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. 决策记录
| # | 决定 | 依据 |
+1 -1
View File
@@ -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
1 date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi
16 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
17 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
18 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
19 2026-11-04 254648 253428 30152 30084 2072 150088 284800 283512 27.2 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
+8 -8
View File
@@ -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
},
+16 -16
View File
@@ -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"
+8 -6
View File
@@ -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`
-112
View File
@@ -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 <run-tag>
退出码: 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)
+2 -1
View File
@@ -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
+11 -13
View File
@@ -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:",
-31
View File
@@ -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 <stdio.h>
/* 计数(在 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 */
+10 -45
View File
@@ -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
}
-29
View File
@@ -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 <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
int sys_clock_set_unix(uint32_t unix_secs);
#ifdef __cplusplus
}
#endif
#endif /* SYS_CLOCK_H */
+3 -2
View File
@@ -4,8 +4,9 @@
*
* ⚠ 仅供 test/host/time_sync/ 使用,不参与 MCU 构建。
*
* 两个接口由测试单元给强符号桩:vTaskDelay 记录每片睡眠时长(断言"≤50ms 分片"),
* xTaskGetTickCount 由测试单元提供可控时钟(驱动 30s 预算耗尽与 32 位回绕用例)。
* 接口由测试单元给强符号桩:vTaskDelay 记录每片睡眠时长(断言"≤50ms 分片");
* xTaskGetTickCount 保留可控时钟声明(:8888 退役后生产源已不读 tick,桩仍提供,
* 供未来需要预算/回绕类用例时直接取用)。
*
* 创建日期:2026-10-10
*/
+118 -605
View File
@@ -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 <setjmp.h>
@@ -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);
-83
View File
@@ -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] 已停止")
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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 保护判据 + 闭锁 + 定值校验和巡检"),
+1 -1
View File
@@ -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",