1 Commits

Author SHA1 Message Date
super_agent_bot 10322208fd feat(sntp): 板端 SNTP 校时接入——coreSNTP 序列化器 + [sntp] 段 + 设置页(#73)
CI / 静态门禁 (pre-commit) (push) Failing after 5m1s
CI / 主机侧单测 (MSVC) (push) Successful in 42s
CI / 静态门禁 (pre-commit) (pull_request) Has been cancelled
CI / 主机侧单测 (MSVC) (pull_request) Has been cancelled
- vendored coreSNTP v2.0.0 序列化器(MIT,仅 serializer+LICENSE,零堆),
  48 字节报文编解码在库,节奏与准入判据在本方(RFC 4330 §10 调研口径)
- App/sync/time_sntp.c 挂 timeSyncTask 的 8888 事件循环,等待切片 ≤50 ms,
  不饿任务狗与 IWDG;懒建 UDP socket,代际变化/禁用/死错误码即关闭归还
- 防伪 = OriginTimestamp 逐位等于发出时 T1(含低 16 位随机小数位);
  mode/stratum/闰秒指示/KoD 按 RFC 拒收;DENY·RSTR 换服务器、RATE 只退避
- 主备轮换 + 失败指数退避(5 s 起、60 s 封顶)+ 首轮 1~5 分钟随机错峰;
  成功恢复 poll_min 周期(1~1440 分,出厂默认 60,默认不启用不发包)
- 钟面 = 本地时间:UTC+8 固定偏移落 sys_clock_set_unix(§1.1 时基裁定,
  第三个写入口,装置仍不存时区);|offset|<2 s 只记日志不步进,冷启动或
  更大偏差步进并回写 SD2506,留痕 [SNTP] step 与 syslog OPER
- 首个校验通过的同步置 SYS_EVT_TIMESYNC_READY(就绪位,消费方按需 NoClear)
- [sntp] 配置段(enable/server1/server2/poll_min)属主 sntp_cfg.c,
  注册在 net_cfg_load();UI 页 31「SNTP 设置」(密码叶):启用开关 +
  主/备服务器 octet 编辑 + 周期(分) + 保存,复用 net_cfg_save_request 链
- 测试与门禁:test/host/sntp 新套件(报文/防伪/KoD/退避周期/阶跃域/配置
  注册表)、net_cfg 与 time_sync 套件补 fake、黄金参考 66→74 张零漂移、
  字符集 228 字、Keil 0 错 0 警(RO +5,848 B 含字体)、覆盖率基线回填
- 文档:墙钟维护说明 §16、网络配置说明 [sntp] 节、配置文件设计说明变更记录、
  产品需求 FR-TIME-3 与 8.3/9.1.2 台账、UI 补齐规划/UI补齐 31 叶口径、
  登记簿销项并登记「SNTP 校时真机判据」等台架(PC 时间源 192.168.31.161/.13)
2026-11-04 16:13:24 +08:00
64 changed files with 2156 additions and 1615 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 加,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 本身。
# 7g) 时间同步(time_sync)主机侧单元测试(2026-10-10 加)
# 背景:time_sync.c 是 TCP 时间同步协议状态机,也是探针窗口心跳缺口那项看门狗风险的**全部
# 逻辑贡献者**——"listen 重建 -> 800ms~1s 心跳间隔"由本模块的分片延时/退避/
# 结算等待组成,板端只能看到"是否复位"这一结果,看不到各段延时是否如设计。本钩子
# 用 MSVC 离线编译真实源文件(#include 纳入)+ 可脚本化的网络/事件桩(含 longjmp
# 跳出 for(;;) 任务循环),把分片喂狗节奏、监听退避、报文重组(半包/粘连/EOF/真错/
# 预算耗尽/tick 回绕)、重听门控逐条钉死。
# files 含被测源与其头、被测的真实看门狗头,以及 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/sync/time_sntp\.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/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) / RemoDispBus 远程显示(7003) / IEC 60870-5-104 上行(工厂默认 2404);对时 = timeSyncTask 的 SNTP 客户端(UDP 123 主动出网,:8888 推送监听已退役)
- **网络**:CH395F → netTask 串行 socket 栈 → FTP(21) / 对时(8888) / RemoDispBus 远程显示(7003) / IEC 60870-5-104 上行(工厂默认 2404)
- **人机**: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 系统保留段,避免误配顶掉
* 系统服务;与 7003 带内相撞属配置者责任(启动锚点
* 系统服务;与 8888/7003 带内相撞属配置者责任(启动锚点
* "i104: listening on %u" 打印实际端口,可核) */
#define IEC104_COMM_CA_MIN 1u
#define IEC104_COMM_CA_MAX 65535u
+7 -6
View File
@@ -7,18 +7,18 @@
| 文件 | 作用 |
|---|---|
| `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/` |
| `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/` |
## 工作流
```
timeSyncTask 启动
↓ osEventFlagsWait(SYS_EVT_NET_READY, ...|osFlagsNoClear) // 状态位不可消费!
time_sync 主循环(200 ms 节拍,长等待分片 ≤50ms 逐片喂心跳)
↘ 每轮 time_sntp_run(gen):到点主动向 [sntp] 服务器发 48 B 查询
time_sync 主循环 → TCP :8888 被动接收("TIME"+u32 → sys_clock 校准)
↘ 每轮顺带 time_sntp_run(gen):到点主动向 [sntp] 服务器发 48 B 查询
(单轮等待切片 ≤50 ms),校验通过按策略步进/只记日志,
首个通过的同步置 SYS_EVT_TIMESYNC_READY;网络代次变化自弃旧 socket
首个通过的同步置 SYS_EVT_TIMESYNC_READY
```
> ⚠️ 等待**就绪位**(`SYS_EVT_NET_READY`)必须 `osFlagsNoClear`——它是状态位,
@@ -26,5 +26,6 @@ time_sync 主循环(200 ms 节拍,长等待分片 ≤50ms 逐片喂心跳)
## 相关文档
- `docs/子系统设计/系统墙钟时间维护说明.md` — 墙钟基准 / tick 回绕防护 / §15 :8888 退役注记 / §16 SNTP
- `docs/子系统设计/系统墙钟时间维护说明.md` — 墙钟基准 / tick 回绕防护
- `test/host/time_sync_server.py` — PC 端对时推送服务(测试用)
- `docs/架构设计/项目启动顺序管理.md` — 就绪屏障
+4 -3
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",
* 与 104 同口径,失败不落痕)
* 与 8888/104 同口径,失败不落痕)
*/
static void s_handle_success(const SntpResponseData_t *p_resp, uint32_t now) {
int64_t utc;
@@ -554,6 +554,7 @@ void time_sntp_run(uint32_t net_gen) {
case SNTP_ST_WAIT_RESP: {
uint8_t rxbuf[SNTP_RX_BUF_SIZE];
struct net_sockaddr_in src;
const uint8_t *srv_ip;
int src_len = (int)sizeof(src);
int n;
@@ -562,7 +563,7 @@ void time_sntp_run(uint32_t net_gen) {
SNTP_RECV_SLICE_MS,
(struct net_sockaddr *)&src, &src_len);
if (n > 0) {
const uint8_t *srv_ip = s_server_ip(cfg);
srv_ip = s_server_ip(cfg);
if (srv_ip == NULL) {
s_fail_backoff(now);
return;
+226 -39
View File
@@ -1,7 +1,7 @@
/*
* 模块名称:Time Sync — timeSyncTask 载体(网络校时节拍宿主)
* 模块功能:独立任务 timeSyncTask:等网络就绪后按固定节拍驱动 SNTP 校时客户端
* (App/sync/time_sntp.c),长等待一律分片并逐片喂任务心跳
* 模块名称:Time Sync — PC 本地时间推送接收(TCP)
* 模块功能:在独立任务 timeSyncTask 中以 TCP Server 方式监听端口,接收 PC 推送的本地时间包,
* 调用 sys_clock_set_unix() 更新系统墙钟并写回 SD2506 RTC
* 适用平台:STM32F407ZGTx + CH395F
* 作者:王建锋
* 创建日期:2026-08-27
@@ -10,45 +10,58 @@
* 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,节奏/退避/步进策略见该文件)
* 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
* 台架修复)原样保留。
* 在 App/sync/time_sntp.c,节奏/退避/步进策略见该文件);
* 本任务循环结构与 8888 语义不变
*/
#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
@@ -69,16 +82,147 @@ 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 正在处理消息队列
* 函数说明:等待网络栈就绪后,按 TIME_SYNC_TICK_MS 节拍驱动 SNTP 校时客户端
* (time_sntp_run 内部:UDP socket 懒建/代际变化自弃旧归还,
* 等待应答期单次驻留 ≤50ms)。心跳 = 每轮循环头一次 + 延时片每片
* 一次;本任务在 IWDG required_mask 内,节拍上没有任何超过抽样窗
* 的不喂窗口。全部 socket 操作经消息队列由 netTask 串行执行。
* 函数说明:等待网络栈就绪后,循环:建监听 -> 等连接 -> 处理 -> **同 fd 续
* accept**(会话收尾不 close,监听跨会话常驻,仅网络
* 代次变化时重建)。全部 socket 操作经消息队列由 netTask 串行执行,
* 与本任务/其它网络任务并发安全。
*/
void time_sync_task(void *argument) {
(void)argument;
@@ -86,12 +230,13 @@ void time_sync_task(void *argument) {
task_wd_bind(TASK_WD_TIMESYNC); /* FTL 心跳路由:登记本任务句柄(入口一次) */
/* 等网络栈真正就绪(取代拍脑袋的 8s 魔法延时),确保 netTask 已开始处理消息;
* osFlagsNoClear:就绪位是状态,不能被本任务消费掉(否则其它等待者永不唤醒)。
* 2026-10-02 台架修复:等待改 100ms 分片 + 每片喂心跳——IWDG 必需掩码含
* TS 位,原 osWaitForever 无心跳 → 启动初期(存储已就绪、网络未就绪)
* 掩码 800ms 不可达 → 停喂 → 复位循环(2026-10-02 台架根因之一)。
* 100ms 分片 = 抽样窗,纯任务睡眠零队列开销。 */
* osFlagsNoClear:就绪位是状态,不能被本任务消费掉(否则其它等待者永不唤醒) */
if (sys_evtHandle != NULL) {
/* osFlagsNoClear:就绪位是状态,不能被本任务消费掉(否则其它等待者永不唤醒)。
* 2026-10-02 台架修复:等待改 100ms 分片 + 每片喂心跳——IWDG 必需掩码含
* TS 位,原 osWaitForever 无心跳 → 启动初期(存储已就绪、网络未就绪)
* 掩码 800ms 不可达 → 停喂 → 复位循环(2026-10-02 台架根因之一)。
* 100ms 分片 = 抽样窗,纯任务睡眠零队列开销。 */
for (;;) {
uint32_t r = osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY,
osFlagsWaitAny | osFlagsNoClear, 100u);
@@ -102,15 +247,57 @@ void time_sync_task(void *argument) {
}
}
/* 监听 fd 跨会话常驻:仅首次/网络代次变化/被毁时重建 */
int ls = -1;
for (;;) {
/* 心跳:本任务在 required_mask 内。time_sntp_run 的收片驻留 ≤50ms
* 落在两次心跳之间(循环头 + 下面延时片每片前置),抽样窗不破。 */
/* 心跳:本任务在 required_mask 内。以下每一处可能长于 100ms 抽样窗的
* 阻塞点(监听重试 2s、accept 轮询 200ms/次、收尾 50ms)都就近补心跳。 */
task_wd_beat(TASK_WD_TIMESYNC);
/* 网络代次随参数下传:断网/重建由 time_sntp_run 内部自理(关闭旧
* socket 归还),代次变化后下一轮自然在新网上重新发起。 */
time_sntp_run(net_get_generation());
/* 记录本次监听对应的网络代次;代次变化(网络重建)则放弃旧监听重建 */
uint32_t gen = net_get_generation();
time_sync_delay_fed(TIME_SYNC_TICK_MS);
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);
}
}
+8 -6
View File
@@ -1,14 +1,13 @@
/*
* 模块名称:Time Sync — timeSyncTask 载体(网络校时节拍宿主)
* 模块功能:timeSyncTask 任务入口:等网络就绪后按固定节拍驱动 SNTP 校时客户端
* 模块名称:Time Sync — PC 本地时间推送接收(TCP)
* 模块功能:以 TCP Server 方式监听 TIME_SYNC_PORT,接收 PC 推送的本地时间包,
* 更新系统墙钟并写回 SD2506 RTC
* 适用平台: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
@@ -20,13 +19,16 @@
extern "C" {
#endif
/* 监听端口:PC 推送端(TCP 客户端)需连到本端口 */
#define TIME_SYNC_PORT 8888
/*
* 函数功能:时间同步任务主体(独立 FreeRTOS 任务)
* 入口参数:argument - FreeRTOS 传入参数(未使用)
* 返回值:无
* 限定条件:net_init() 已由 netTask 完成;网络消息队列由 netTask 处理
* 函数说明:等网络就绪后按 200ms 节拍驱动 SNTP 校时客户端(App/sync/time_sntp.c),
* socket 操作经消息队列(由 netTask 串行执行)完成。
* 函数说明:以 TCP Server 方式监听 TIME_SYNC_PORT,接收 PC 推送的本地时间包,
* 经消息队列(由 netTask 串行执行)完成 socket 操作,更新系统墙钟。
* 该任务与 FTP 等其它网络任务并发安全。
*/
void time_sync_task(void *argument);
+17 -44
View File
@@ -48,7 +48,6 @@
#include "main.h"
#include "ff.h"
#include "nand_ftl.h"
#include "bsp_err.h" /* BSP_ERR_NO_SPACE / BSP_ERR_CRC:卷健康探针按失败类分流(C4) */
#include "net_cfg.h"
#include "cfg_file.h"
#include "run_par.h" /* 运行参数段落盘(「数据可信」这一项,配置引擎属主 = 本任务) */
@@ -524,57 +523,31 @@ static FRESULT storage_bring_up(FATFS *fs) {
FS_VOL_PROBE(hfr); /* 两击确认 */
}
if (hfr != FR_OK) {
/* C4 消费点(2026-11-04,问题 #79):探针只拿到 FRESULT,失败类在
* disk_read/disk_write 返回时就被 FatFs 契约抹掉了。这里把 FTL 留下的
* 跨调用快照读出来,**先判"重格式化有没有意义"再决定动作**——
* 这正是 存储架构说明 §10.4 那条一直只写在文档里、没有消费者的判据。
*
* 为什么必须先判:BSP_ERR_NO_SPACE 是 journal/映射层块池见底(介质健康),
* 重格式化只会把问题推回去(文档原文);而 BSP_ERR_CRC 才是"页级太坏,
* 换页/reformat 可能救"。两类都走 reformat 的话,块池见底会被反复擦盘。 */
nand_ftl_fault_t fault;
nand_ftl_fault_get(&fault);
if (fault.active != 0u) {
DBG_ERROR("[FS] probe failed (fr=%d), FTL last fault: op=%u class=%ld lpn=%lu count=%lu",
(int)hfr, (unsigned)fault.op, (long)fault.cls,
(unsigned long)fault.lpn, (unsigned long)fault.count);
}
/* ⚠ FR_INVALID_NAME 绝不可入本集合:它是路径名语法错误(纯 RAM 判定、
* 不碰盘),与卷健康无关(曾误入 → "无限重格式化"事故) */
if (hfr == FR_DENIED || hfr == FR_INVALID_OBJECT ||
hfr == FR_INT_ERR || hfr == FR_DISK_ERR ||
hfr == FR_NO_FILE || hfr == FR_NO_PATH ||
hfr == FR_NO_FILESYSTEM) {
/* 按失败类分流:块池见底 ≠ 介质坏。前者擦卷救不回来,只上报
* (后续应走容量恢复,见 存储维护与容量恢复.md),后者才 reformat。 */
if ((fault.active != 0u) && (fault.cls == (int32_t)BSP_ERR_NO_SPACE)) {
DBG_ERROR("[FS] volume probe failed: FTL out of space, "
"reformat skipped (needs capacity recovery, not mkfs)");
syslog_post(SYSLOG_SELFTEST, "storage full");
fr = FR_INT_ERR;
} else {
DBG_INFO("[FS] volume FAT invalid (fr=%d), reformatting...",
(int)hfr);
f_mount(NULL, "", 0);
/* 格式化前清空 FTL map 树(同首启路径:新卷从空树写起) */
(void)nand_ftl_format();
{
static uint8_t fs_work2[FF_MAX_SS];
MKFS_PARM opts2 = { FM_FAT32, 0, 0, 0, 0 };
fr = f_mkfs("", &opts2, fs_work2, sizeof(fs_work2));
if (fr == FR_OK) {
fr = f_mount(fs, "", 1);
}
}
DBG_INFO("[FS] volume FAT invalid (fr=%d), reformatting...",
(int)hfr);
f_mount(NULL, "", 0);
/* 格式化前清空 FTL map 树(同首启路径:新卷从空树写起) */
(void)nand_ftl_format();
{
static uint8_t fs_work2[FF_MAX_SS];
MKFS_PARM opts2 = { FM_FAT32, 0, 0, 0, 0 };
fr = f_mkfs("", &opts2, fs_work2, sizeof(fs_work2));
if (fr == FR_OK) {
DBG_INFO("[FS] volume reformatted OK");
} else {
DBG_ERROR("[FS] volume repair failed (fr=%d)", (int)fr);
syslog_post(SYSLOG_SELFTEST, "storage fault");
fr = f_mount(fs, "", 1);
}
}
if (fr == FR_OK) {
DBG_INFO("[FS] volume reformatted OK");
} else {
DBG_ERROR("[FS] volume repair failed (fr=%d)", (int)fr);
syslog_post(SYSLOG_SELFTEST, "storage fault");
}
} else {
/* 其余错误(写保护/卷锁/参数类)不重格式化——只上报,
* 防对写锁卷整卷擦除(FTL 磨损最大操作) */
@@ -712,7 +685,7 @@ static void wave_store_drain(void) {
}
}
if ((why == NULL) && (s_wscan_done == 0u)) {
FRESULT rr = FR_OK;
FRESULT rr;
fr = f_opendir(&s_wdir, WAVE_ST_DIR);
if (fr == FR_OK) {
-52
View File
@@ -8,11 +8,6 @@
* 2026-07-16 王建锋 创建初始版本
* 2026-07-17 王建锋 切换硬件 SPI,修正擦除地址,参考 NuttX 驱动
* 2026-08-27 补充函数注释(代码规范 V1.0);修正工厂坏块扫描为首页;增加 BBT 持久化
* 2026-11-04 问题 #79 取证配套:① init 解锁块保护(A0h)后补回读校验——
* 与 2026-10-23 修过的 ECC(B0h)同族的"只写不回读"缺口
* (实测本板 A0h 已为 0,写保护非本次根因,校验留作哨兵);
* ② `gd5f_bbt_load()` 合并后若坏块数 > 出厂扫描数则打一条
* "BBT after merge",让现场一眼区分真坏与误标固化。
*/
/* 头文件包含区 */
@@ -844,39 +839,6 @@ int32_t gd5f2gq5ue_init(void) {
DBG_ERROR("Unlock block protection failed: %d", ret);
return ret;
}
/* 回读 A0h 校验保护位**真正清零**(2026-11-04,问题 #79 取证发现)。
*
* 为什么必须校验:本项与 2026-10-23 修过的 ECC(B0h) 是同一条缺陷的另一半——
* `gd5f_set_feature()` 只写不回读,"写了但没生效"对上层完全不可见。
* A0h 没清干净的后果比 ECC 没开更直接:**整片只读**。
*
* 台架现场(11-04 17:40 本次启动,`repair-nand-volume` 取证):
* BBT scan: 0 bad blocks found ← 介质出厂坏块为 0
* 读操作全部正常(Map resumed / FAT 探针能读到内容)
* 写操作**全部**失败:LPN=211(块 3)与 LPN=15(块 0)两个不同块
* 连 nand_ftl_format() 后的 map sync 都失败 → storage disabled
* "读正常 + 写全废 + 跨多个无关块 + 出厂坏块为 0" 这组特征排除了
* "某一页/某一块真坏"(那不会跨块、也不会让全新 map sync 失败),
* 指向芯片处于**写保护态**。
*
* 判据:A0h bit0-3 (BP0..BP3) 与非易失的 BP 位组合决定保护范围;
* 读到非 0 即保护未解除,必须**明确失败**而不是继续跑(继续跑会让
* 上层把"写保护"误判成"介质损坏",进而反复格式化——正是本单现场)。
*/
{
uint8_t prot = 0;
ret = gd5f_private_get_feature(GD5F_REG_PROTECT, &prot);
if (ret != GD5F_OK) {
DBG_ERROR("Read A0h failed: %d", ret);
return ret;
}
if (prot != 0u) {
DBG_ERROR("Block protection NOT cleared (A0h=0x%02X), NAND is read-only",
prot);
return GD5F_ERROR;
}
}
DBG_INFO("Locating BBT pool...");
ret = gd5f_private_bbt_locate_pool();
@@ -892,20 +854,6 @@ int32_t gd5f2gq5ue_init(void) {
DBG_ERROR("Load persisted BBT failed: %d (use scanned BBT)", ret);
}
/* 【#79】持久化 BBT 若把连续段判坏(出厂扫描 0 坏块、底层写全部成功),
* 那是运行期误标被固化后的结果,不是介质坏。此处只打一条汇总,
* 让下次现场一眼能区分"真坏"与"误标"。 */
{
uint32_t bad = 0;
for (uint32_t b = 0; b < GD5F_TOTAL_BLOCKS; b++) {
if ((s_bbt[b >> 3] >> (b & 7)) & 1) bad++;
}
if (bad > 0) {
DBG_ERROR("BBT after merge: %lu bad blocks (factory scan was 0)",
(unsigned long)bad);
}
}
DBG_INFO("NAND init OK");
return GD5F_OK;
}
+12 -162
View File
@@ -25,31 +25,10 @@
* 单一真源 + 两条负数组编译期守卫:越界上界依赖
* log2_ppb,换料时只改 GD5F_* 而忘改字面量会**静默**
* 把守卫算大 = 越界守卫失效,现改为直接编译错
* 2026-11-04 王建锋 问题 #79(台架 LPN=211 恒 TOO_BAD 致 f_mount 失败、
* 整卷禁用)两处修复:
* ① `nand_ftl_format()` 不再被脏缓存 flush 失败挡住——
* 旧实现先 flush、失败即 return,导致**恢复动作自己在
* 同一页上死锁**(map 从未清过,f_mkfs 在旧映射上必失败)。
* 格式化语义本就是销毁映射,故尽力刷一次后无条件丢缓存;
* ② 补 C4 跨调用失败类快照(`nand_ftl_fault_get/clear`),
* 让 disk_initialize/读/刷/同步的失败归类能穿过 FatFs 的
* RES_ERROR 边界,供 defaultTask 探针按类分流(见 存储架构
* 说明 §10.4);③ .c 自包含 include 本模块头(主机侧单测
* 直接 #include 本文件,缺头则类型不可见)。
* ④(真机取证后定案)`dhara_nand_erase/prog()` 不再把
* **传输层/等忙失败**当坏块证据:只有芯片明确回 E_FAIL /
* P_FAIL 才标坏。真凶即在此——非坏类失败被标坏后由
* mark_block_bad 立即持久化(不可撤销),台架 BBT 遂固化
* 129 个误标块(1016–1142 连续段),journal prepare_head()
* 连续 8 次 is_bad → TOO_BAD → 整卷禁用;已固化的误标由
* 恢复动作 A 重建 BBT 清除(见 存储维护与容量恢复 §2.1)。
*/
/* 头文件包含区 */
#include <string.h>
#include "nand_ftl.h" /* 本模块公开契约:失败快照类型(C4)与 nand_ftl_* 原型。
* 必须自包含——旧版靠调用方先 include 本头,主机侧单测
* 直接 #include "nand_ftl.c" 时类型不可见(实测编译错)。 */
#include "gd5f2gq5ue.h"
#include "ff.h"
#include "diskio.h"
@@ -146,19 +125,10 @@ static uint8_t s_copy_buf[FTL_PAGE_SIZE];
/* 初始化状态 */
static uint8_t s_initialized;
/* 失败快照(C4):把"最近一次存储失败"的归类带出函数边界。
* 为什么需要:disk_read/disk_write 对 FatFs 只能回 RES_ERROR(FatFs 契约),
* 失败类在 return 那一刻就被抹掉,上层因此无法区分「页级 TOO_BAD,重格式化可解」
* 与「块池见底 / 映射损坏,重格式化只会把问题推回去」。
* 多写者纪律:defaultTask 与 testTask 都会碰存储,故语义是"最近一次"而非账本;
* 并发下可能被另一路覆盖——诊断用途可接受,记在此处以免日后误当权威计数。 */
static nand_ftl_fault_t s_fault;
/* ======================== 私有函数声明 ======================== */
static bsp_err_t ftl_flush_cache(void);
static bsp_err_t ftl_read_page(dhara_sector_t lpn);
static void ftl_fault_note(uint32_t op, bsp_err_t cls, dhara_sector_t lpn);
/* SPI 原语从 gd5f2gq5ue.h 获取 */
@@ -214,52 +184,6 @@ static bsp_err_t ftl_class_from_dhara(dhara_error_t err) {
}
}
/* ======================== 失败快照(C4) ======================== */
/*
* 函数功能:记录一次存储失败到跨调用快照
* 入口参数:op - 失败的操作(NAND_FTL_OP_*)
* cls - 失败类(ftl_class_from_dhara 的结果)
* lpn - 涉及逻辑页号(未知传 NAND_FTL_LPN_NONE)
* 返回值:无 void
* 限定条件:可在任何上下文调用;本函数只写几个字,不加锁(见下方顺序说明)
* 函数说明:**不覆盖已有的非 NONE 归类除非新失败更具体**——不,刻意选择
* "后写覆盖":快照回答的是"最近一次失败是什么",最靠近处置点的现场才有用。
* count 单独累计,用来回答另一个问题:"是不是一直在撞同一页"(本单现场正是
* LPN=211 反复失败 15 次)。两者分开存,避免为了统计把最近现场挤掉。
*
* 写入顺序是有意的:先写内容字段,最后置 active=1(写侧无锁,读侧可能
* 在任意指令边界被抢占)。这样读到 active=1 时内容字段必然已经写好;
* 最坏情况是读到旧内容 —— 诊断用途可接受,且不会比"没有快照"更差。
*/
static void ftl_fault_note(uint32_t op, bsp_err_t cls, dhara_sector_t lpn) {
s_fault.op = op;
s_fault.cls = (int32_t)cls;
s_fault.lpn = (uint32_t)lpn;
if (s_fault.count < 0xFFFFFFFFu) {
s_fault.count++;
}
s_fault.active = 1u;
}
void nand_ftl_fault_get(nand_ftl_fault_t *p_out) {
if (p_out == NULL) {
return;
}
/* 整结构拷贝:调用方拿到的是一致快照,不会一半新一半旧 */
*p_out = s_fault;
}
void nand_ftl_fault_clear(void) {
/* 先清 active 再清内容:反序会让读侧看到 active=0 但 count 仍是旧值,
* 虽然无害,但"active=0 时其余字段无意义"这条契约更该由写法保证 */
s_fault.active = 0u;
s_fault.op = NAND_FTL_OP_NONE;
s_fault.cls = (int32_t)BSP_OK;
s_fault.lpn = NAND_FTL_LPN_NONE;
s_fault.count = 0u;
}
/* ======================== 页面缓存管理 ======================== */
static bsp_err_t ftl_flush_cache(void) {
@@ -272,7 +196,6 @@ static bsp_err_t ftl_flush_cache(void) {
bsp_err_t cls = ftl_class_from_dhara(err); /* 只在失败分支需要(cppcheck variableScope) */
DBG_ERROR("Cache flush FAIL: LPN=%lu dhara=%s class=%d",
s_cached_lpn, ftl_err_name(err), cls);
ftl_fault_note(NAND_FTL_OP_FLUSH, cls, s_cached_lpn);
return cls;
}
@@ -287,7 +210,6 @@ static bsp_err_t ftl_read_page(dhara_sector_t lpn) {
bsp_err_t cls = ftl_class_from_dhara(err); /* 只在失败分支需要(cppcheck variableScope) */
DBG_ERROR("Page read FAIL: LPN=%lu dhara=%s class=%d",
lpn, ftl_err_name(err), cls);
ftl_fault_note(NAND_FTL_OP_READ, cls, lpn);
return cls;
}
@@ -321,24 +243,9 @@ int dhara_nand_erase(const struct dhara_nand *n, dhara_block_t b, dhara_error_t
}
ret = gd5f_block_erase(ftl_phy_block(b));
if (ret != GD5F_OK) {
if (ret == GD5F_ERASE_FAIL) {
/* 芯片明确回 E_FAIL:该块确实擦不净,标坏是正确处置 */
DBG_ERROR("Erase failed with E_FAIL: block=%lu", (unsigned long)b);
dhara_nand_mark_bad(n, b);
dhara_set_error(err, DHARA_E_BAD_BLOCK);
} else {
/* 【#79 核心修复】传输层/等忙失败(GD5F_ERROR / GD5F_WAIT_EXPIRED)
* 不是坏块证据:芯片根本没回 E_FAIL,把块标坏等于把"线抖了一下"
* 固化成"这块永久报废",并立即写进持久化 BBT(不可撤销)。
* 这类失败只报 BAD_BLOCK 让 dhara 重试,**不标坏**。
* 真机取证(2026-11-04 17:53):出厂扫描 0 坏块、底层 prog/erase
* 全部成功,而持久化 BBT 把 1016–1142 连续段判坏,prepare_head()
* 连续 8 次撞 is_bad → DHARA_E_TOO_BAD → 整卷禁用。 */
DBG_ERROR("Erase transport/timeout failure (not a bad block): "
"block=%lu ret=%d", (unsigned long)b, ret);
dhara_set_error(err, DHARA_E_BAD_BLOCK);
}
if (ret == GD5F_ERASE_FAIL || ret != GD5F_OK) {
dhara_nand_mark_bad(n, b);
dhara_set_error(err, DHARA_E_BAD_BLOCK);
return -1;
}
return 0;
@@ -357,27 +264,15 @@ int dhara_nand_prog(const struct dhara_nand *n, dhara_page_t p, const uint8_t *d
gd5f_write_enable();
ret = gd5f_program_load(0, data, FTL_PAGE_SIZE);
if (ret != GD5F_OK) {
/* 【#79】装载失败是传输层问题(SPI/DMA/等忙),芯片未回 P_FAIL,
* 不得标坏块;只报 BAD_BLOCK 让 dhara 重试。 */
DBG_ERROR("Program load transport failure (not a bad block): "
"page=%lu ret=%d", (unsigned long)phy_pg, ret);
dhara_nand_mark_bad(n, p >> n->log2_ppb);
dhara_set_error(err, DHARA_E_BAD_BLOCK);
return -1;
}
ret = gd5f_program_exec(phy_pg);
if (ret != GD5F_OK) {
if (ret == GD5F_PROGRAM_FAIL) {
/* 芯片明确回 P_FAIL:页确实写不进去,标坏是正确处置 */
DBG_ERROR("Program failed with P_FAIL: page=%lu", (unsigned long)phy_pg);
dhara_nand_mark_bad(n, p >> n->log2_ppb);
dhara_set_error(err, DHARA_E_BAD_BLOCK);
} else {
/* 【#79】传输层/等忙失败不是坏块证据,不标坏,只重试 */
DBG_ERROR("Program transport/timeout failure (not a bad block): "
"page=%lu ret=%d", (unsigned long)phy_pg, ret);
dhara_set_error(err, DHARA_E_BAD_BLOCK);
}
if (ret == GD5F_PROGRAM_FAIL || ret != GD5F_OK) {
dhara_nand_mark_bad(n, p >> n->log2_ppb);
dhara_set_error(err, DHARA_E_BAD_BLOCK);
return -1;
}
@@ -536,14 +431,6 @@ DSTATUS disk_initialize(BYTE pdrv) {
s_cached_lpn = (dhara_sector_t)-1; /* -1 表示缓存未加载;LPN 0 合法,不能用 0 作哨兵 */
s_cache_dirty = 0;
/* 失败快照初值:一次上电从"无失败"起算。刻意放在 map_init 之后、
* resume 之前——resume 失败本身就是要被记进快照的第一件事。 */
s_fault.active = 0u;
s_fault.op = NAND_FTL_OP_NONE;
s_fault.cls = (int32_t)BSP_OK;
s_fault.lpn = NAND_FTL_LPN_NONE;
s_fault.count = 0u;
{
dhara_error_t err = DHARA_E_NONE;
if (dhara_map_resume(&s_map, &err) < 0) {
@@ -566,9 +453,6 @@ DSTATUS disk_initialize(BYTE pdrv) {
DBG_ERROR("Map resume failed (dhara=%s class=%d), volume kept, "
"storage unavailable", ftl_err_name(err),
ftl_class_from_dhara(err));
ftl_fault_note(NAND_FTL_OP_MAP_RESUME,
ftl_class_from_dhara(err),
(dhara_sector_t)NAND_FTL_LPN_NONE);
return STA_NOINIT;
}
} else {
@@ -714,8 +598,6 @@ DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) {
dhara_error_t err;
if (dhara_map_sync(&s_map, &err) < 0) {
DBG_ERROR("Map sync failed: %d", err);
ftl_fault_note(NAND_FTL_OP_SYNC, ftl_class_from_dhara(err),
(dhara_sector_t)NAND_FTL_LPN_NONE);
return RES_ERROR;
}
}
@@ -752,52 +634,20 @@ int nand_ftl_format(void) {
}
}
/* 处置开始:清掉历史失败快照,此后新失败重新记账
* (否则一次旧失败会永久污染"本次格式化到底成没成"的判定) */
nand_ftl_fault_clear();
/* 【本单核心修复 2026-11-04,问题 #79】
* 旧实现先 `if (ftl_flush_cache() != BSP_OK) return GD5F_ERROR;`——而
* ftl_flush_cache() 在失败分支**不清 s_cache_dirty**(那是刻意的:写不进去
* 就不能假装写成功了),于是只要脏页落在"写不动的那个 LPN"上,
* 本函数就在第一道闸门返回,**dhara_map_clear() 永远执行不到**。
*
* 台架现场(11-04 08:16 与 08:42 两次独立捕获,head/tail/epoch 一字不变):
* 探针写 /fschk.tmp → disk_write → flush LPN=211 失败(TOO_BAD)
* → 重试同页再次失败 → 判卷损坏 → 走本函数 → 又被同一页挡住
* → f_mkfs 在旧映射上跑 → 失败 → 3 轮重试耗尽 → storage disabled。
* 每次启动必现、断电重启不恢复,因为 s_cache_dirty 与 LPN=211 的脏数据
* 在**同一次启动内**恒存,而每轮启动都会重新踩上同一条路。
*
* 为什么"跳过 flush 直接丢缓存"在这里是正确的:
* 本函数的语义是**销毁整卷逻辑映射**(头文件:「破坏性:整卷逻辑地址→
* 物理映射作废」,只允许显式命令/恢复流程调用;现存两个调用点
* ——首启无卷与 FAT 探针判卷损坏——都紧接着 f_mkfs 重建卷)。既然这份
* 脏页映射注定被扔掉,为它挡住格式化既救不回数据,又让**恢复动作本身
* 永久失效**。故:尽力刷一次(成功则顺带落盘、日志更干净),失败就
* 丢缓存并继续——这正是"格式化"该有的优先级。
*
* ⚠ 不得把本改动挪到普通写路径:disk_write/disk_read 里的 flush 失败
* 必须继续向上报错(那是"这次写没落地"的真相),只有本函数有权丢弃缓存。
*/
if (ftl_flush_cache() != BSP_OK) {
DBG_ERROR("format: dirty cache at LPN=%lu undiscardable, "
"dropping it (map is being destroyed anyway)", s_cached_lpn);
return GD5F_ERROR;
}
/* 无条件失效页面缓存:格式化后旧映射作废,绝不能让它再被当成有效缓存 */
s_cache_dirty = 0;
s_cached_lpn = (dhara_sector_t)-1;
dhara_map_clear(&s_map);
if (dhara_map_sync(&s_map, &err) < 0) {
DBG_ERROR("Map sync after clear failed: %s class=%d",
ftl_err_name(err), ftl_class_from_dhara(err));
ftl_fault_note(NAND_FTL_OP_SYNC, ftl_class_from_dhara(err),
(dhara_sector_t)NAND_FTL_LPN_NONE);
DBG_ERROR("Map sync after clear failed: %d", err);
return GD5F_ERROR;
}
s_cache_dirty = 0;
s_cached_lpn = (dhara_sector_t)-1;
DBG_INFO("NAND FTL formatted, capacity: %lu pages", dhara_map_capacity(&s_map));
return GD5F_OK;
}
-61
View File
@@ -19,40 +19,6 @@ extern "C" {
#include <stdint.h>
/*
* 失败快照(C4):把"最近一次存储失败的归类"从失败点带出来。
*
* 为什么要单独成一个结构而不是复用 bsp_err_t:调用方需要的不只是失败类,
* 还要能回答"这次失败值不值得再试一次"(重试同一 LPN 会不会又撞同一页)。
* 故带上 LPN 与 op,便于 run.log/wdev.log 里一条日志说清现场。
*
* 字段:
* active - 1 = 快照可用(自上次 nand_ftl_fault_clear() 以来发生过失败)
* op - 失败发生的操作(NAND_FTL_OP_*),0 = 无
* cls - 失败类(bsp_err_t:BSP_ERR_CRC / BSP_ERR_NO_SPACE / BSP_ERR_IO …)
* lpn - 失败涉及的逻辑页号(未知时填 NAND_FTL_LPN_NONE)
* count - 自上次 clear 以来累计的失败次数(饱和不减,用于判"反复撞同一页")
*
* 尺寸:5 × uint32 = **20 B**,无隐式填充(字段全为 4 字节宽,任何编译器布局一致)。
* 不引入任何动态分配。
*/
#define NAND_FTL_LPN_NONE ((uint32_t)0xFFFFFFFFu)
#define NAND_FTL_OP_NONE ((uint32_t)0u)
#define NAND_FTL_OP_MAP_RESUME ((uint32_t)1u) /* disk_initialize 的 dhara_map_resume */
#define NAND_FTL_OP_READ ((uint32_t)2u) /* ftl_read_page(FatFs 读路径) */
#define NAND_FTL_OP_FLUSH ((uint32_t)3u) /* ftl_flush_cache(写回路径) */
#define NAND_FTL_OP_SYNC ((uint32_t)4u) /* disk_ioctl(CTRL_SYNC) 的 map sync */
typedef struct {
uint32_t active; /* 1 = 快照可用(自上次 nand_ftl_fault_clear() 以来发生过失败) */
uint32_t op; /* 失败发生的操作(NAND_FTL_OP_*),0 = 无 */
int32_t cls; /* 失败类(bsp_err_t:BSP_ERR_CRC / BSP_ERR_NO_SPACE / BSP_ERR_IO …)。
* 写死 int32_t 宽度:bsp_err_t 是 int,跨编译器布局才稳定 */
uint32_t lpn; /* 失败涉及的逻辑页号(未知时 = NAND_FTL_LPN_NONE) */
uint32_t count; /* 自上次 clear 以来累计失败次数(饱和不减,用于判"反复撞同一页") */
} nand_ftl_fault_t;
/*
* 函数功能:初始化 FTL(等价于 disk_initialize(0):SPI 起链 + dhara_map_resume)
* 入口参数:无
@@ -87,33 +53,6 @@ int nand_ftl_format(void);
*/
void nand_ftl_deinit(void);
/*
* 函数功能:取"最近一次存储失败"的跨调用快照(C4,2026-11-04 补还)
* 入口参数:p_out - 快照输出 nand_ftl_fault_t* 可为 NULL(=只查询是否有失败)
* 返回值:无 void(新 API 不把"有没有失败"塞进返回值:本函数是纯查询,
* 判定一律读 p_out->active)
* 限定条件:任何任务上下文可读;**仅诊断用途**,不得据此自动清卷/重格式化
* 函数说明:为什么需要它——disk_read/disk_write 对 FatFs 只能回 RES_ERROR
* (FatFs 契约),失败类在函数返回时就被抹掉,上层因此无法区分
* 「页级 TOO_BAD,重格式化可解」与「块池见底 / 映射损坏,重格式化只会把问题
* 推回去」。本快照把 nand_ftl.c 就地分类的结果留在 RAM 里,供
* defaultTask 的卷健康探针按类分支(消费点见 storage_bring_up())。
* ⚠ 多写者:defaultTask 与 testTask 都可能触发写入失败,故快照是"最近一次"
* 语义、非累计账本;并发下可能被另一路覆盖(诊断场景可接受)。
* 成功路径**不清**它(失败证据要留存到显式 clears),语义同 dbg latch。
*/
void nand_ftl_fault_get(nand_ftl_fault_t *p_out);
/*
* 函数功能:清除失败快照(把 active 置 0)
* 入口参数:无
* 返回值:无
* 限定条件:仅"已按快照处置完毕"的调用方清除(当前 = defaultTask 卷修复路径)
* 函数说明:不清就会让一次历史失败永久污染后续判定;清早了又会丢掉现场。
* 调用点因此只有一处,见 storage_bring_up() 的重格式化分支。
*/
void nand_ftl_fault_clear(void);
#ifdef __cplusplus
}
#endif
+2 -2
View File
@@ -5,8 +5,8 @@
* 注入 reader 取自 sys_log(/log/oper.log,容量上限 512 条,满时删除重建);
* 空日志显示单条伪行"无记录";向框架 ui_content_page 提供页面描述符(零 LVGL 对象)
* 记录口径(本文件只读显示、不实现写入方;权威表 App/log/README.md §五类记录):
* 触发判据 = 人工/远端操作留痕:定值保存、强制复归、对时(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 自身除外);
* 触发判据 = 人工/远端操作留痕:定值保存、强制复归、对时(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 自身除外);
* "记录类型 = 日志 ID 本身"(一类记录一个文件,记录体内没有类型字段位,
* Lead 裁定 2026-10-26)——本页只读 oper.log 一个文件,不做跨文件混排。
* 台架待补(不在本批,已登记 docs/任务规划/登记簿.md):真 NAND 上真实记录的翻页读取、
+5 -2
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 任务载体 + time_sntp SNTP 客户端)
│ ├── sync/ # 时间同步(time_sync 任务)
│ ├── log/ # 日志(sys_log 运行/自检记录、console_log 控制台单写者)
│ ├── task/ # FreeRTOS 任务实现
│ │ ├── default_task.c/h # 存储挂载 + 配置引擎属主 + syslog 写者
@@ -175,7 +175,9 @@ 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
│ └── latency_peer.py # 链路时延 PC 对端
│ ├── e02_probe.py # 队头阻塞专项探针
│ ├── latency_peer.py # 链路时延 PC 对端
│ └── time_sync_server.py # 时间同步推送服务
├── docs/ # 项目文档(全量索引见 docs/README.md)
│ ├── README.md # 文档索引:目录结构 + 推荐阅读顺序 + 全量清单
│ ├── 任务规划/ # 统一待办登记簿(README 排序权威 + 拆分文件)
@@ -323,6 +325,7 @@ 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 v2.0:分片喂狗延时 / 就绪屏障(100 ms 分片·NoClear)/ SNTP 节拍与网络代次交接 / 未出网守门(:8888 退役后的任务载体),33 断言点 |
| [`时间同步单元测试说明.md`](测试说明/单元测试/时间同步单元测试说明.md) | time_sync 分片喂狗延时 / 监听退避 / 时间包重组 / 任务主循环(探针窗口心跳缺口的全部逻辑贡献者),80 断言点 |
| [`主机单测覆盖率口径.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)——已落地(2026-11-04):页面读写 `[sntp]` 段 RAM 副本(`ui_page_sntp.c`,PR 链 #73/#77);原「当前对时走 8888 端口私有协议」口径作废——8888 已退役(#78),SNTP 即对时唯一网络来源。
11. **SNTP设置**(31):**建议只做页面不接线**,或直接推迟到 SNTP 立项——当前对时走 8888 端口私有协议,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="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="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="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

+33 -33
View File
@@ -1,23 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1060" height="680" viewBox="0 0 1060 680" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif">
<rect x="46.0" y="106.0" width="314.0" height="246.0" rx="10" fill="#eff6ff" stroke="#2563eb" stroke-width="2"/>
<text x="58.0" y="132.0" font-size="15" font-weight="700" fill="#0f172a">幅值轮</text>
<text x="348.0" y="132.0" font-size="10.5" text-anchor="end" font-weight="700" fill="#2563eb">先做 · 三档幅度</text>
<text x="348.0" y="132.0" font-size="10.5" text-anchor="end" font-weight="700" fill="#2563eb">先做 · 主用例</text>
<rect x="58.0" y="142.0" width="290.0" height="86.0" rx="6" fill="#ffffff" stroke="#cbd5e1" stroke-width="1"/>
<line x1="62.0" y1="185.0" x2="344.0" y2="185.0" stroke="#94a3b8" stroke-width="0.8" stroke-dasharray="3 3"/>
<line x1="68.0" y1="161.2" x2="338.0" y2="161.2" stroke="#2563eb" stroke-width="1" stroke-dasharray="5 4"/>
<line x1="68.0" y1="208.8" x2="338.0" y2="208.8" stroke="#2563eb" stroke-width="1" stroke-dasharray="5 4"/>
<polyline points="68.0,185.0 69.9,182.9 71.9,180.7 73.8,178.6 75.8,176.6 77.7,174.6 79.7,172.7 81.6,170.9 83.5,169.3 85.5,167.7 87.4,166.3 89.4,165.1 91.3,164.0 93.3,163.1 95.2,162.3 97.1,161.8 99.1,161.4 101.0,161.3 103.0,161.3 104.9,161.5 106.8,161.9 108.8,162.5 110.7,163.3 112.7,164.2 114.6,165.4 116.6,166.7 118.5,168.1 120.4,169.7 122.4,171.4 124.3,173.2 126.3,175.1 128.2,177.1 130.2,179.2 132.1,181.3 134.0,183.4 136.0,185.5 137.9,187.7 139.9,189.8 141.8,191.9 143.8,193.9 145.7,195.9 147.6,197.7 149.6,199.5 151.5,201.1 153.5,202.6 155.4,204.0 157.4,205.2 159.3,206.3 161.2,207.1 163.2,207.8 165.1,208.3 167.1,208.6 169.0,208.8 170.9,208.7 172.9,208.4 174.8,208.0 176.8,207.3 178.7,206.5 180.7,205.5 182.6,204.3 184.5,203.0 186.5,201.5 188.4,199.9 190.4,198.2 192.3,196.3 194.3,194.4 196.2,192.4 198.1,190.3 200.1,188.2 202.0,186.1 204.0,183.9 205.9,181.8 207.9,179.7 209.8,177.6 211.7,175.6 213.7,173.7 215.6,171.8 217.6,170.1 219.5,168.5 221.5,167.0 223.4,165.7 225.3,164.5 227.3,163.5 229.2,162.7 231.2,162.0 233.1,161.6 235.1,161.3 237.0,161.2 238.9,161.4 240.9,161.7 242.8,162.2 244.8,162.9 246.7,163.7 248.6,164.8 250.6,166.0 252.5,167.4 254.5,168.9 256.4,170.5 258.4,172.3 260.3,174.1 262.2,176.1 264.2,178.1 266.1,180.2 268.1,182.3 270.0,184.5 272.0,186.6 273.9,188.7 275.8,190.8 277.8,192.9 279.7,194.9 281.7,196.8 283.6,198.6 285.6,200.3 287.5,201.9 289.4,203.3 291.4,204.6 293.3,205.8 295.3,206.7 297.2,207.5 299.2,208.1 301.1,208.5 303.0,208.7 305.0,208.7 306.9,208.6 308.9,208.2 310.8,207.7 312.7,206.9 314.7,206.0 316.6,204.9 318.6,203.7 320.5,202.3 322.5,200.7 324.4,199.1 326.3,197.3 328.3,195.4 330.2,193.4 332.2,191.4 334.1,189.3 336.1,187.1 338.0,185.0" fill="none" stroke="#2563eb" stroke-width="2" stroke-linejoin="round"/>
<text x="68.0" y="151.0" font-size="10.5" fill="#64748b">峰 ±1.414 V · 基波有效值 1.000 V</text>
<text x="68.0" y="151.0" font-size="10.5" fill="#64748b">峰 ±1.414 V · 基波 RMS 1.000 V</text>
<text x="58.0" y="244.0" font-size="11" font-weight="700" fill="#2563eb">注入</text>
<text x="94.0" y="244.0" font-size="11.5" fill="#64748b">UA(R31)端子侧,正弦 50.000 Hz,三档 Vpp</text>
<text x="94.0" y="260.0" font-size="11.5" fill="#64748b"> 2.828 / 1.414 / 0.566</text>
<text x="94.0" y="244.0" font-size="11.5" fill="#64748b">UA(R31)端子侧,正弦 50.000 Hz、1.000 V</text>
<text x="94.0" y="260.0" font-size="11.5" fill="#64748b">rms</text>
<text x="58.0" y="280.0" font-size="11" font-weight="700" fill="#2563eb">判据</text>
<text x="94.0" y="280.0" font-size="11.5" fill="#0f172a">每档 ua_mv ≈ 万用表 × 1000,偏差 ≤2 %</text>
<text x="58.0" y="300.0" font-size="11" font-weight="700" fill="#2563eb">读数</text>
<text x="94.0" y="300.0" font-size="11.5" fill="#64748b">meas: ua_mv 为主(1 mV 分辨率)</text>
<text x="94.0" y="280.0" font-size="11.5" fill="#0f172a">ua_mv 中位数 ≈ 万用表 × 1000,偏差 ≤2 %</text>
<text x="58.0" y="300.0" font-size="11" font-weight="700" fill="#2563eb">出口</text>
<text x="94.0" y="300.0" font-size="11.5" fill="#64748b">meas: ua_mv 为主;104 0x4011 只作交叉读</text>
<text x="94.0" y="316.0" font-size="11.5" fill="#64748b">数</text>
<rect x="372.0" y="106.0" width="314.0" height="246.0" rx="10" fill="#f0fdfa" stroke="#0d9488" stroke-width="2"/>
<text x="384.0" y="132.0" font-size="15" font-weight="700" fill="#0f172a">波形轮</text>
<text x="674.0" y="132.0" font-size="10.5" text-anchor="end" font-weight="700" fill="#0d9488">真有效值 ≠ 基波</text>
<text x="674.0" y="132.0" font-size="10.5" text-anchor="end" font-weight="700" fill="#0d9488">真 RMS ≠ 基波</text>
<rect x="384.0" y="142.0" width="290.0" height="86.0" rx="6" fill="#ffffff" stroke="#cbd5e1" stroke-width="1"/>
<line x1="394.0" y1="161.2" x2="664.0" y2="161.2" stroke="#94a3b8" stroke-width="0.9" stroke-dasharray="5 4"/>
<polyline points="400.0,185.0 400.7,182.5 401.3,180.0 402.0,177.6 402.6,175.3 403.3,173.0 403.9,170.9 404.6,169.0 405.2,167.2 405.9,165.7 406.6,164.3 407.2,163.2 407.9,162.3 408.5,161.7 409.2,161.3 409.8,161.2 410.5,161.4 411.1,161.8 411.8,162.5 412.5,163.5 413.1,164.6 413.8,166.0 414.4,167.7 415.1,169.5 415.7,171.4 416.4,173.6 417.0,175.8 417.7,178.2 418.4,180.6 419.0,183.1 419.7,185.6 420.3,188.1 421.0,190.6 421.6,193.0 422.3,195.3 422.9,197.5 423.6,199.6 424.3,201.5 424.9,203.2 425.6,204.7 426.2,206.0 426.9,207.0 427.5,207.9 428.2,208.4 428.8,208.7 429.5,208.7 430.2,208.5 430.8,208.0 431.5,207.3 432.1,206.3 432.8,205.0 433.4,203.6 434.1,201.9 434.7,200.1 435.4,198.0 436.1,195.9 436.7,193.6 437.4,191.2 438.0,188.7 438.7,186.3 439.3,183.7 440.0,181.3 440.6,178.8 441.3,176.4 441.9,174.1 442.6,172.0 443.3,169.9 443.9,168.1 444.6,166.4 445.2,165.0 445.9,163.7 446.5,162.7 447.2,162.0 447.8,161.5 448.5,161.3 449.2,161.3 449.8,161.6 450.5,162.1 451.1,163.0 451.8,164.0 452.4,165.3 453.1,166.8 453.7,168.5 454.4,170.4 455.1,172.5 455.7,174.7 456.4,177.0 457.0,179.4 457.7,181.9 458.3,184.4 459.0,186.9 459.6,189.4 460.3,191.8 461.0,194.2 461.6,196.4 462.3,198.6 462.9,200.5 463.6,202.3 464.2,204.0 464.9,205.4 465.5,206.5 466.2,207.5 466.9,208.2 467.5,208.6 468.2,208.8 468.8,208.7 469.5,208.3 470.1,207.7 470.8,206.8 471.4,205.7 472.1,204.3 472.8,202.8 473.4,201.0 474.1,199.1 474.7,197.0 475.4,194.7 476.0,192.4 476.7,190.0 477.3,187.5 478.0,185.0" fill="none" stroke="#0d9488" stroke-width="1.8" stroke-linejoin="round"/>
@@ -26,17 +27,17 @@
<text x="529.0" y="226.0" font-size="10.5" text-anchor="middle" fill="#64748b">方波</text>
<polyline points="580.0,185.0 580.7,183.4 581.3,181.8 582.0,180.2 582.6,178.6 583.3,177.0 583.9,175.4 584.6,173.8 585.2,172.2 585.9,170.6 586.6,169.0 587.2,167.4 587.9,165.8 588.5,164.2 589.2,162.6 589.8,161.4 590.5,163.0 591.1,164.6 591.8,166.2 592.5,167.8 593.1,169.4 593.8,171.0 594.4,172.6 595.1,174.2 595.7,175.8 596.4,177.4 597.0,179.0 597.7,180.6 598.4,182.2 599.0,183.8 599.7,185.4 600.3,187.0 601.0,188.6 601.6,190.2 602.3,191.8 602.9,193.4 603.6,195.0 604.3,196.6 604.9,198.2 605.6,199.8 606.2,201.4 606.9,203.0 607.5,204.6 608.2,206.2 608.8,207.8 609.5,208.2 610.2,206.6 610.8,205.0 611.5,203.4 612.1,201.8 612.8,200.2 613.4,198.6 614.1,197.0 614.7,195.4 615.4,193.8 616.1,192.2 616.7,190.6 617.4,189.0 618.0,187.4 618.7,185.8 619.3,184.2 620.0,182.6 620.6,181.0 621.3,179.4 621.9,177.8 622.6,176.2 623.3,174.6 623.9,173.0 624.6,171.4 625.2,169.8 625.9,168.2 626.5,166.6 627.2,165.0 627.8,163.4 628.5,161.8 629.2,162.2 629.8,163.8 630.5,165.4 631.1,167.0 631.8,168.6 632.4,170.2 633.1,171.8 633.7,173.4 634.4,175.0 635.1,176.6 635.7,178.2 636.4,179.8 637.0,181.4 637.7,183.0 638.3,184.6 639.0,186.2 639.6,187.8 640.3,189.4 641.0,191.0 641.6,192.6 642.3,194.2 642.9,195.8 643.6,197.4 644.2,199.0 644.9,200.6 645.5,202.2 646.2,203.8 646.9,205.4 647.5,207.0 648.2,208.6 648.8,207.4 649.5,205.8 650.1,204.2 650.8,202.6 651.4,201.0 652.1,199.4 652.8,197.8 653.4,196.2 654.1,194.6 654.7,193.0 655.4,191.4 656.0,189.8 656.7,188.2 657.3,186.6 658.0,185.0" fill="none" stroke="#0d9488" stroke-width="1.8" stroke-linejoin="round"/>
<text x="619.0" y="226.0" font-size="10.5" text-anchor="middle" fill="#64748b">三角</text>
<text x="394.0" y="151.0" font-size="10.5" fill="#64748b">同设 Vpp 2.828 V(不是同有效值)</text>
<text x="394.0" y="151.0" font-size="10.5" fill="#64748b">同峰值对齐(不是同 Vrms)</text>
<text x="384.0" y="244.0" font-size="11" font-weight="700" fill="#0d9488">注入</text>
<text x="420.0" y="244.0" font-size="11.5" fill="#64748b">同设 Vpp 2.828 V 改方波 / 三角(面板有效</text>
<text x="420.0" y="260.0" font-size="11.5" fill="#64748b">值各异)</text>
<text x="420.0" y="244.0" font-size="11.5" fill="#64748b">同峰值 1.414 V 改方波 / 三角(面板有效值各</text>
<text x="420.0" y="260.0" font-size="11.5" fill="#64748b">异)</text>
<text x="384.0" y="280.0" font-size="11" font-weight="700" fill="#0d9488">判据</text>
<text x="420.0" y="280.0" font-size="11.5" fill="#0f172a">读数只跟基波:方波 ≈1278、三角 ≈817,±2 %</text>
<text x="384.0" y="300.0" font-size="11" font-weight="700" fill="#0d9488">读数</text>
<text x="384.0" y="300.0" font-size="11" font-weight="700" fill="#0d9488">出口</text>
<text x="420.0" y="300.0" font-size="11.5" fill="#64748b">meas: ua_mv;抬头记万用表响应类型</text>
<rect x="698.0" y="106.0" width="314.0" height="246.0" rx="10" fill="#f0fdf4" stroke="#16a34a" stroke-width="2"/>
<text x="710.0" y="132.0" font-size="15" font-weight="700" fill="#0f172a">相位轮</text>
<text x="1000.0" y="132.0" font-size="10.5" text-anchor="end" font-weight="700" fill="#16a34a">定量 · 录波</text>
<text x="1000.0" y="132.0" font-size="10.5" text-anchor="end" font-weight="700" fill="#16a34a">定量 · 取段</text>
<rect x="710.0" y="142.0" width="290.0" height="86.0" rx="6" fill="#ffffff" stroke="#cbd5e1" stroke-width="1"/>
<line x1="714.0" y1="185.0" x2="996.0" y2="185.0" stroke="#94a3b8" stroke-width="0.8" stroke-dasharray="3 3"/>
<polyline points="720.0,185.0 721.9,183.4 723.9,181.8 725.8,180.2 727.8,178.6 729.7,177.1 731.7,175.6 733.6,174.1 735.5,172.7 737.5,171.4 739.4,170.1 741.4,168.9 743.3,167.7 745.3,166.7 747.2,165.7 749.1,164.8 751.1,164.0 753.0,163.3 755.0,162.7 756.9,162.2 758.8,161.8 760.8,161.5 762.7,161.3 764.7,161.2 766.6,161.3 768.6,161.4 770.5,161.7 772.4,162.0 774.4,162.5 776.3,163.1 778.3,163.7 780.2,164.5 782.2,165.4 784.1,166.3 786.0,167.4 788.0,168.5 789.9,169.7 791.9,170.9 793.8,172.3 795.8,173.7 797.7,175.1 799.6,176.6 801.6,178.1 803.5,179.7 805.5,181.3 807.4,182.9 809.4,184.5 811.3,186.1 813.2,187.7 815.2,189.3 817.1,190.8 819.1,192.4 821.0,193.9 822.9,195.4 824.9,196.8 826.8,198.2 828.8,199.5 830.7,200.7 832.7,201.9 834.6,203.0 836.5,204.0 838.5,204.9 840.4,205.8 842.4,206.5 844.3,207.1 846.3,207.7 848.2,208.1 850.1,208.4 852.1,208.6 854.0,208.7 856.0,208.7 857.9,208.6 859.9,208.4 861.8,208.1 863.7,207.7 865.7,207.1 867.6,206.5 869.6,205.8 871.5,204.9 873.5,204.0 875.4,203.0 877.3,201.9 879.3,200.7 881.2,199.5 883.2,198.2 885.1,196.8 887.1,195.4 889.0,193.9 890.9,192.4 892.9,190.8 894.8,189.3 896.8,187.7 898.7,186.1 900.6,184.5 902.6,182.9 904.5,181.3 906.5,179.7 908.4,178.1 910.4,176.6 912.3,175.1 914.2,173.7 916.2,172.3 918.1,170.9 920.1,169.7 922.0,168.5 924.0,167.4 925.9,166.3 927.8,165.4 929.8,164.5 931.7,163.7 933.7,163.1 935.6,162.5 937.6,162.0 939.5,161.7 941.4,161.4 943.4,161.3 945.3,161.2 947.3,161.3 949.2,161.5 951.2,161.8 953.1,162.2 955.0,162.7 957.0,163.3 958.9,164.0 960.9,164.8 962.8,165.7 964.7,166.7 966.7,167.7 968.6,168.9 970.6,170.1 972.5,171.4 974.5,172.7 976.4,174.1 978.3,175.6 980.3,177.1 982.2,178.6 984.2,180.2 986.1,181.8 988.1,183.4 990.0,185.0" fill="none" stroke="#16a34a" stroke-width="2" stroke-linejoin="round"/>
@@ -44,14 +45,13 @@
<text x="720.0" y="226.0" font-size="10.5" font-weight="700" fill="#16a34a">UA 0° 实线</text>
<text x="798.0" y="226.0" font-size="10.5" font-weight="700" fill="#e11d48">UN +90° 红虚线</text>
<text x="710.0" y="244.0" font-size="11" font-weight="700" fill="#16a34a">注入</text>
<text x="746.0" y="244.0" font-size="11.5" fill="#64748b">UA(R31)与 UN(R28)同源同幅 Vpp 2.828 </text>
<text x="746.0" y="260.0" font-size="11.5" fill="#64748b">V</text>
<text x="710.0" y="280.0" font-size="11" font-weight="700" fill="#16a34a">判据</text>
<text x="746.0" y="280.0" font-size="11.5" fill="#0f172a">同段内相位差 ≈0°,B 通道改 90° 应跟到 ≈90</text>
<text x="746.0" y="296.0" font-size="11.5" fill="#0f172a">°,±2°</text>
<text x="710.0" y="316.0" font-size="11" font-weight="700" fill="#16a34a">读数</text>
<text x="746.0" y="316.0" font-size="11.5" fill="#64748b">wave_read.py fft 定量;phasor(hb) 只核</text>
<text x="746.0" y="332.0" font-size="11.5" fill="#64748b">象限</text>
<text x="746.0" y="244.0" font-size="11.5" fill="#64748b">UA(R31)与 UN(R28)同源同幅 1.000 Vrms</text>
<text x="710.0" y="264.0" font-size="11" font-weight="700" fill="#16a34a">判据</text>
<text x="746.0" y="264.0" font-size="11.5" fill="#0f172a">同段内相位差 ≈0°,B 通道改 90° 应跟到 ≈90</text>
<text x="746.0" y="280.0" font-size="11.5" fill="#0f172a">°,±2°</text>
<text x="710.0" y="300.0" font-size="11" font-weight="700" fill="#16a34a">出口</text>
<text x="746.0" y="300.0" font-size="11.5" fill="#64748b">wave_read.py fft 定量;phasor(hb) 只核</text>
<text x="746.0" y="316.0" font-size="11.5" fill="#64748b">象限</text>
<rect x="46.0" y="366.0" width="314.0" height="246.0" rx="10" fill="#fffbeb" stroke="#d97706" stroke-width="2"/>
<text x="58.0" y="392.0" font-size="15" font-weight="700" fill="#0f172a">测频轮</text>
<text x="348.0" y="392.0" font-size="10.5" text-anchor="end" font-weight="700" fill="#d97706">A 档无数 = 过</text>
@@ -68,8 +68,8 @@
<text x="58.0" y="524.0" font-size="11" font-weight="700" fill="#d97706">判据</text>
<text x="94.0" y="524.0" font-size="11.5" fill="#0f172a">A 档如实报不可测:valid=0、UI 显 --;报 5</text>
<text x="94.0" y="540.0" font-size="11.5" fill="#0f172a">0.00 反而是 bug</text>
<text x="58.0" y="560.0" font-size="11" font-weight="700" fill="#d97706">读数</text>
<text x="94.0" y="560.0" font-size="11.5" fill="#64748b">freq(hb) 0.01 Hz;UI Hz</text>
<text x="58.0" y="560.0" font-size="11" font-weight="700" fill="#d97706">出口</text>
<text x="94.0" y="560.0" font-size="11.5" fill="#64748b">freq(hb) 0.01 Hz;UI Hz、104 0x401D</text>
<rect x="372.0" y="366.0" width="314.0" height="246.0" rx="10" fill="#fff1f2" stroke="#e11d48" stroke-width="2"/>
<text x="384.0" y="392.0" font-size="15" font-weight="700" fill="#0f172a">削顶轮</text>
<text x="674.0" y="392.0" font-size="10.5" text-anchor="end" font-weight="700" fill="#e11d48">负向 · 单独日志</text>
@@ -81,24 +81,24 @@
<text x="420.0" y="504.0" font-size="11.5" fill="#64748b">UB(R52):DC −1.00 V 保持 ≥5 s,或 2.0 </text>
<text x="420.0" y="520.0" font-size="11.5" fill="#64748b">Hz 正弦</text>
<text x="384.0" y="540.0" font-size="11" font-weight="700" fill="#e11d48">判据</text>
<text x="420.0" y="540.0" font-size="11.5" fill="#0f172a">连续 20 拍贴底 → rail / edge 锁存;50 Hz</text>
<text x="420.0" y="556.0" font-size="11.5" fill="#0f172a"> 正弦不锁存 = 预期</text>
<text x="384.0" y="576.0" font-size="11" font-weight="700" fill="#e11d48">读数</text>
<text x="420.0" y="576.0" font-size="11.5" fill="#64748b">cal: rail / edge;录波再跑 shape 对表</text>
<text x="420.0" y="540.0" font-size="11.5" fill="#0f172a">连续 20 拍贴底 → rail / edge 锁存 + 104 </text>
<text x="420.0" y="556.0" font-size="11.5" fill="#0f172a">该路 IV 清零;50 Hz 正弦不锁存 = 预期</text>
<text x="384.0" y="576.0" font-size="11" font-weight="700" fill="#e11d48">出口</text>
<text x="420.0" y="576.0" font-size="11.5" fill="#64748b">cal: rail / edge;取段再跑 shape 对表</text>
<rect x="698.0" y="366.0" width="314.0" height="246.0" rx="10" fill="#f5f3ff" stroke="#7c3aed" stroke-width="2"/>
<text x="710.0" y="392.0" font-size="15" font-weight="700" fill="#0f172a">谐波轮</text>
<text x="1000.0" y="392.0" font-size="10.5" text-anchor="end" font-weight="700" fill="#7c3aed">定量 · 录波</text>
<text x="1000.0" y="392.0" font-size="10.5" text-anchor="end" font-weight="700" fill="#7c3aed">定量 · 取段</text>
<rect x="710.0" y="402.0" width="290.0" height="86.0" rx="6" fill="#ffffff" stroke="#cbd5e1" stroke-width="1"/>
<line x1="714.0" y1="445.0" x2="996.0" y2="445.0" stroke="#94a3b8" stroke-width="0.8" stroke-dasharray="3 3"/>
<polyline points="720.0,445.0 721.9,442.2 723.9,439.5 725.8,437.1 727.8,435.0 729.7,433.4 731.7,432.3 733.6,431.6 735.5,431.3 737.5,431.3 739.4,431.4 741.4,431.5 743.3,431.5 745.3,431.3 747.2,430.9 749.1,430.2 751.1,429.2 753.0,428.1 755.0,426.8 756.9,425.7 758.8,424.7 760.8,424.0 762.7,423.7 764.7,423.8 766.6,424.4 768.6,425.5 770.5,427.0 772.4,428.8 774.4,430.7 776.3,432.7 778.3,434.5 780.2,436.0 782.2,437.2 784.1,438.0 786.0,438.5 788.0,438.6 789.9,438.4 791.9,438.0 793.8,437.7 795.8,437.4 797.7,437.4 799.6,437.7 801.6,438.4 803.5,439.4 805.5,440.8 807.4,442.5 809.4,444.4 811.3,446.3 813.2,448.1 815.2,449.7 817.1,451.0 819.1,451.9 821.0,452.5 822.9,452.6 824.9,452.5 826.8,452.2 828.8,451.8 830.7,451.5 832.7,451.4 834.6,451.6 836.5,452.2 838.5,453.1 840.4,454.5 842.4,456.1 844.3,458.0 846.3,459.9 848.2,461.8 850.1,463.5 852.1,464.9 854.0,465.8 856.0,466.3 857.9,466.3 859.9,465.8 861.8,465.0 863.7,464.0 865.7,462.8 867.6,461.6 869.6,460.5 871.5,459.6 873.5,458.9 875.4,458.6 877.3,458.5 879.3,458.5 881.2,458.7 883.2,458.7 885.1,458.6 887.1,458.2 889.0,457.3 890.9,456.1 892.9,454.3 894.8,452.2 896.8,449.7 898.7,446.9 900.6,444.0 902.6,441.2 904.5,438.6 906.5,436.3 908.4,434.4 910.4,433.0 912.3,432.1 914.2,431.5 916.2,431.3 918.1,431.3 920.1,431.4 922.0,431.5 924.0,431.5 925.9,431.2 927.8,430.7 929.8,429.9 931.7,428.8 933.7,427.6 935.6,426.4 937.6,425.3 939.5,424.4 941.4,423.8 943.4,423.7 945.3,424.0 947.3,424.8 949.2,426.0 951.2,427.6 953.1,429.4 955.0,431.4 957.0,433.3 958.9,435.0 960.9,436.5 962.8,437.5 964.7,438.2 966.7,438.5 968.6,438.5 970.6,438.3 972.5,437.9 974.5,437.6 976.4,437.4 978.3,437.4 980.3,437.9 982.2,438.7 984.2,439.8 986.1,441.4 988.1,443.1 990.0,445.0" fill="none" stroke="#7c3aed" stroke-width="2" stroke-linejoin="round"/>
<text x="720.0" y="411.0" font-size="10.5" fill="#64748b">基波 + 2 次 + 5 次</text>
<text x="710.0" y="504.0" font-size="11" font-weight="700" fill="#7c3aed">注入</text>
<text x="746.0" y="504.0" font-size="11.5" fill="#64748b">IA(R19):分三注,基波 Vpp 2.263、二五次</text>
<text x="746.0" y="520.0" font-size="11.5" fill="#64748b">各 Vpp 0.453</text>
<text x="746.0" y="504.0" font-size="11.5" fill="#64748b">IA(R19):基波 0.800 + 二次 0.160 + 五</text>
<text x="746.0" y="520.0" font-size="11.5" fill="#64748b">次 0.160 Vrms</text>
<text x="710.0" y="540.0" font-size="11" font-weight="700" fill="#7c3aed">判据</text>
<text x="746.0" y="540.0" font-size="11.5" fill="#0f172a">Ia2 / Ia5 各读数 ≈0.160 V,±2 %(最坏峰</text>
<text x="746.0" y="556.0" font-size="11.5" fill="#0f172a">值 1.58 V 留余量)</text>
<text x="710.0" y="576.0" font-size="11" font-weight="700" fill="#7c3aed">读数</text>
<text x="746.0" y="540.0" font-size="11.5" fill="#0f172a">Ia2 / Ia5 各 ≈0.160 Vrms,±2 %(最坏峰值</text>
<text x="746.0" y="556.0" font-size="11.5" fill="#0f172a"> 1.58 V 留余量)</text>
<text x="710.0" y="576.0" font-size="11" font-weight="700" fill="#7c3aed">出口</text>
<text x="746.0" y="576.0" font-size="11.5" fill="#64748b">wave_read.py fft --ch 4;UI Ia2 / Ia5 </text>
<text x="746.0" y="592.0" font-size="11.5" fill="#64748b">目检旁证</text>
<line x1="46.0" y1="634.0" x2="1014.0" y2="634.0" stroke="#cbd5e1" stroke-width="1"/>

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

+75 -75
View File
@@ -1,5 +1,5 @@
<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"/>
<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"/>
<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="332" rx="12" fill="#f5f3ff" stroke="#7c3aed" stroke-opacity="0.45"/>
<rect x="26" y="214" width="741" height="284" 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="332" rx="12" fill="#f0fdfa" stroke="#0d9488" stroke-opacity="0.45"/>
<rect x="793" y="214" width="741" height="284" 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="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>
<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>
<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="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>
<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>
</svg>

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

+15 -19
View File
@@ -126,8 +126,7 @@ flowchart LR
end
DEV -- "USB:ST-Link 下载器" --> TGT
DEV -- "USB→RS-232 适配器:RS232#1 @115200 调试日志" --> TGT
TGT -- "以太网 CH395F<br/>FTP:21 / 远程显示:7003" --> RMT
TGT -. "对时:SNTP 客户端 UDP 123 出向(:8888 已退役)" .-> NTP["现场/公网 NTP 源"]
TGT -- "以太网 CH395F<br/>FTP:21 / 对时:8888 / 远程显示:7003" --> RMT
```
| 环境 | 角色 | 必需性 |
@@ -154,7 +153,7 @@ flowchart LR
1. **开关柜现场部署**:设备上电 → 按 `/config.ini` 出厂 IP 组网 → 主站连工厂默认 :2404 总召 →
现场用屏板看测量、按按键翻菜单;死机由看门狗自恢复(无需到场)。
2. **远程运维**:PC 经 :7003 看同一份 UI、注键操作;FTP 取 `/log`(运行/自检/SOE 记录)
与 `/config.ini`;改 IP 后 2~5 s 生效不重启;SNTP 出向对时(原 PC 推 :8888 已退役)。
与 `/config.ini`;改 IP 后 2~5 s 生效不重启;PC 推 :8888 对时。
3. **脱离 PC 独立带屏运行**:串口屏板直连 USART1(700 kbaud),主机 275 ms 召唤、
≤200 ms 回 4208 B 帧(3200 B 显存 + 状态灯掩码 + info)——无 PC 也能看与按(§5.16)。
4. **保护运行(等标定解锁)**:1 kHz 采样 → 全周傅氏相量 → 20 ms 判据节拍 → 动作只落
@@ -251,7 +250,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 墙钟 + SNTP 对时(原 :8888 已退役;2038 已知限) |
| P0 | FR-TIME 时间(§5.3) | ✅ | SD2506 墙钟 + :8888 对时(2038 已知限) |
| P0 | FR-CFG 配置引擎(§5.4) | ✅ | INI 段键表 + 外科保存 + 免重启生效 |
| P0 | FR-LOG 记录与日志(§5.5) | ✅ | 定长记录 + 控制台单写者 |
| P0 | FR-WD 看门狗(§5.7) | ✅ | 心跳位图 + 复位原因 + Fault 现场 |
@@ -320,15 +319,14 @@ 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 网络对时**(已退役 2026-11-04,由 FR-TIME-3 接替)
**FR-TIME-2 网络对时**
- 需求:PC 主动通过 TCP :8888 推送时间到设备,设备写回 RTC。
- 退役:**已由 FR-TIME-3 SNTP 取代并退役**——`:8888` 监听、8 字节 "TIME" 协议、PC 推送器随
工单 #78 删除;网络校时唯一入口 = SNTP(FR-TIME-3)。历史实现见 git 历史与
`docs/子系统设计/系统墙钟时间维护说明.md` §15。
- 实现:`timeSyncTask` 等 `NET_READY` → TCP listen :8888 → 收 PC 推送 → 写 RTC;30 s 无包则掉线重建监听。
- 判据:对时后设备墙钟更新、日志时间戳正确。
**FR-TIME-3 SNTP 校时** ✅(2026-11-04)
- 需求:设备作 SNTP(RFC 4330)客户端从配置的 IPv4 服务器经 UDP/123 取时写回 RTC;使能、主/备服务器、轮询周期可配(UI「SNTP 设置」页或 `/config.ini` `[sntp]` 段,保存后随下一轮生效)。
- 实现:`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`。
- 实现:`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`。
- 判据:主机单测 `test/host/sntp`(报文格式 / 伪造拒收 / KoD 轮换 / 退避与周期 / 阶跃域守卫);真机 = 使能并配置服务器后时钟与时间源一致、`[SNTP] step offset=` 日志在场(台架窗口统一做)。
> **已知限制**:uint32 epoch → **2038 年**上限。
@@ -1200,7 +1198,7 @@ for (i = 1; i < LED_COUNT; i++) /* LE
| 服务 | 协议 / 端口 | 用途 | 核对方法 |
|---|---|---|---|
| FTP | TCP **21** | 文件上传/下载(根目录 `/`,NAND 存储) | 登录后任意 FTP 客户端列目录/读写(鉴权见 §7.6) |
| 墙钟对时(SNTP 客户端) | UDP 出向 **123**(主/备服务器 `[sntp]` 段,enable=0 出厂默认) | 设备主动取 NTP 源时间写 RTC(:8888 推送监听**已删除**,2026-11-04,§6.3) | 使能后核对设备时间 / 日志时间戳与 `[SNTP] step offset=` 留痕 |
| 墙钟对时 | TCP **8888** | PC 主动推送时间到设备 | 对时后核对设备时间 / 日志时间戳 |
| 远程显示(RemoDispBus) | TCP **7003** | 远端看图(160×160 1bpp)+ 远端导航按键 | DTU-RemoteLCD 或任意 TCP 客户端连 7003 收帧 |
| IEC 60870-5-104 上行 | TCP **工厂默认 2404** | 被控站接入:遥测/遥信上送、总召、遥控/定值下发(点表见 §6.8) | 任意 104 主站连 2404,STARTDT+总召往返(→ `docs/子系统设计/IEC104上行设计说明.md`) |
@@ -1256,13 +1254,11 @@ for (i = 1; i < LED_COUNT; i++) /* LE
> 详见 `docs/UI设计/RemoDispBus协议与接收设计说明.md`。MCU 与模拟器**共用同一份** `ui_net_sink.c`。
### 6.3 对时协议(SNTP 客户端)
### 6.3 对时协议
- **载体**:设备作 **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。
- **载体**:TCP :8888,PC 主动推送,设备接收并写 RTC。
- **行为**:`timeSyncTask` 监听 → 收包 → 写回 RTC;30 s 无包则掉线重建监听(约 4 s)。
- **待补充**:推送报文的具体帧格式(当前实现细节见 `App/sync/time_sync.c` 与 `test/host/time_sync_server.py`)。
### 6.4 FTP 服务
@@ -1413,7 +1409,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 一期) | 台架实测(探针 `e02_probe.py` 已删除——2026-11-04 随 :8888 退役,正文见 git 历史) |
| 队列响应性(挂起客户端) | 92.5 ms(pending 一期) | `test/host/e02_probe.py` |
| FTP 传输速率 | 约 49 / 60 KB/s(上传/下载) | 128 KB 实测 |
| 网络重建生效 | 2~5 s(含等链路 ≤3 s) | 配置设计 |
| UI 刷新 | 10 ms 循环 | `ui_task_func` |
@@ -1674,7 +1670,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);另有 104 `C_CS`(私有 TCP 8888 已随 #78 退役删除) | 台架真机对时判据(现场时间源,docs/任务规划/登记簿.md) |
| **SNTP 对时** | 17 | 参考对时走 SNTP | **SNTP 客户端已落地**(2026-11-04:coreSNTP 序列化器 + `[sntp]` 配置 + UI 设置页 + timeSyncTask 接入,判据 §5.3 FR-TIME-3);另有私有 TCP 8888 + 104 `C_CS` | 台架真机对时判据(现场时间源,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 |
@@ -1758,7 +1754,7 @@ docs/任务规划/UI补齐.md §5);原「当前执行 6 项」(记录 5
| 3 | 远程显示 | DTU-RemoteLCD 连 7003 | 出图(160×160 1bpp)+ 远端菜单导航可用 |
| 4 | FTP | FTP 客户端连 :21 | 列目录 / 读写 `/` 下文件 |
| 5 | UI 功能 | 远端操作 | 菜单 7 项导航、内容页、密码(查看 `000` / 修改 `123`)、网口设置保存后立即生效 |
| 6 | 对时 | SNTP 配置服务器对时(原 PC 推送 8888 已退役) | 设备墙钟更新、日志时间戳正确 |
| 6 | 对时 | 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= 实测**已绑 31 叶、未绑 3 叶**(2026-11-04 SNTP 设置
叶随 #73 立项转绑 +1,判定规则与重跑命令见 docs/任务规划/UI补齐.md §1「叶数判定与复核」)。未绑的 3 叶
因缺少开入硬件不做(原因与重开条件见 docs/任务规划/UI补齐.md §5)——**手动录波与录波回放两叶
菜单树共 34 叶(节点行合计 43):按行内 #page= 实测**已绑 30 叶、未绑 4 叶**(2026-10-29 复扫,
判定规则与重跑命令见 docs/任务规划/UI补齐.md §1「叶数判定与复核」)。未绑的 4 叶因缺少开入硬件
或 SNTP 产品定义而不做(原因与重开条件见 docs/任务规划/UI补齐.md §5)——**手动录波与录波回放两叶
已随录波道绑页**,「不做」清单里的录波那一项已销。
**口径闭合**:未绑 3 = 待做 0(§1 清单已清空)+ 不做 3(遥信 3);**43 是节点行数、不是叶数**(= 9 个菜单节点 + 34 叶)。
已绑叶数以生成物 UI_PAGE_MAP_COUNT 为准:不剔 ui_menu_tree.txt 文件头的注释示例行时,按 #page= 文本计数会得 32(= 31 + 1 行示例)。
**口径闭合**:未绑 4 = 待做 0(§1 清单已清空)+ 不做 4(遥信 3 / SNTP 1);**43 是节点行数、不是叶数**(= 9 个菜单节点 + 34 叶)。
已绑叶数以生成物 UI_PAGE_MAP_COUNT 为准:不剔 ui_menu_tree.txt 文件头的注释示例行时,按 #page= 文本计数会得 31(= 30 + 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** | 独立模块与暂缓项 | 当前不排 | 开入开出、104 文件传输单独立项(SNTP 已单独立项并于 2026-11-04 落地 #73,:8888 随 #78 退役);RS-485 与内存优化按各自重启条件 |
| **8** | 独立模块与暂缓项 | 当前不排 | 开入开出、SNTP、104 文件传输单独立项;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 关键依赖
+3 -6
View File
@@ -99,11 +99,8 @@
**单写者约束**:`ui_menu_tree.txt`、页面注册表、字库生成物不得由多个分支同时修改。
`MDK-ARM/*.uvprojx` 的新源文件注册按双端注册规则处理。
**时基口径已裁定,勿顺手改语义**:口径 = 「先本地为准,内部真源是本地钟面」(用户裁定 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 语义**,也不要把
**时基口径未定,勿顺手统一**:面板时钟页与 8888 对时把「本地钟面当 epoch」写入,而 104 `C_CS` 按 CP56Time2a 的
UTC 写入,后写者赢 ⇒ 装置墙钟与主机本地时刻恒差 8 小时。用户裁定前**不要改动 RTC/epoch 语义**,也不要把
时钟设置页的「回读 RTC」判据改成按某一时区补偿——那等于替产品做掉一个未定的决定。
**保护闭锁必须可见**:整定页可以修改定值,但当前产品在 UA/UN/IA 未标定时保护判据闭锁。
@@ -118,5 +115,5 @@
|---|---|---|
| 遥信类型 / 防抖 / 双点 3 页 | V0.1 无开入硬件,配置没有消费者 | 开入固件立项后,与开入开出一起做 |
| net 层与其他结构债 | 当前只做 UI,避免两条主线互相拖慢 | UI 当前清单完成后,按 README §2 切换 |
| 104 上行 UTC 换算(原「RTC 时基统一」) | 内部口径已由用户裁定(2026-10-27:本地钟面为真源,各写入口先换算再落库,见 §4 注),「恒差 8 小时」的后写者赢阶段已结束;剩下的只是 104 上行是否按标准转 UTC,动了要改 104 上行语义 | 送检/第三方主站验收方向定下来后,作为跨模块变更单独立项(墙钟说明 §1.1 末) |
| RTC 时基统一 | 两套写入口径并存(面板与 8888 写本地钟面、104 写 UTC),恒差 8 小时;统一任一侧都要动 104 上行语义或 UI 判据 | 用户裁定时基方向后,作为跨模块变更单独立项 |
| 主站地址白名单 | CH395F 单连接 socket 上「拒绝即 close+重 listen」等于给扫描风暴开门,且该值现在没有任何既有消费点 | 有真实消费场景或多连接后端立项时再评 |
+25 -28
View File
@@ -46,7 +46,7 @@
|---|---|
| 判断现在这些数能信到什么程度 | §1 这张图 + §2.6(静态证到哪)+ §2.7(动态没证什么) |
| 改采集 / 算法 / 品质位相关代码 | §2.1 §2.2 §2.3(口径)+ §4.1~§4.5(设计依据) |
| 上台架做正弦验证 | §4.6(接法、六轮判据、数字从哪儿读、坑)+ §5 的「信号发生器动态波形验证」卡 |
| 上台架做正弦验证 | §4.6(接法、六轮判据、读数出口、坑)+ §5 的「信号发生器动态波形验证」卡 |
| 看还差谁、按什么顺序做 | §5 任务卡 + §6 顺序图 |
| 确认某件事是**故意不做**而不是漏了 | §9;有没有踩坑风险看 §8 |
@@ -157,11 +157,11 @@ V0.1 恒命中 UB/UC/IB/IC/IN,位图 `0xE6`),它表示"这路不能测交
| 下游 | 现在 | 为什么 |
|---|---|---|
| 基波有效值 | 🟡 算法在 PC 上证过(幅值 ≤1 %、49/51 Hz 下 ≤1.5 %),**经真 ADC 后一次没测** | 1 V 正弦进 ADC 后 `fAmSQ` 是否 = 1 V,无实测 |
| 基波有效值(RMS) | 🟡 算法在 PC 上证过(幅值 ≤1 %、49/51 Hz 下 ≤1.5 %),**经真 ADC 后一次没测** | 1 V 正弦进 ADC 后 `fAmSQ` 是否 = 1 V,无实测 |
| 相位 | ❌ 从未在真信号上比过;且代码里**没有跨通道相位符号约定** | 只许比同片内通道(§9 的硬件限制) |
| 频率 | ❌ **恒不启动** | 幅值门 50 V 是二次值口径,引脚域 ≤5 V 恒不过(§2.3 #1)→ UI `Hz` 行恒 `--`、104 `0x401D` 恒 IV |
| 削顶 / 品质 | 🟡 位图逻辑对,真削顶路径没跑过 | `rail:` 那条 `[ERR]` 至今只在直流饱和里起过 |
| 谐波 C04 / C05 | ❌ 实例建了,**无消费者、无处上送** | `prot_elm.c` 无引用,`meas` 只算 `harm=1` |
| 谐波 C04 / C05 | ❌ 实例建了,**无消费者、无出口** | `prot_elm.c` 无引用,`meas` 只算 `harm=1` |
| 保护判据 | 🔒 闭锁中 | §4.3 |
→ **下一跳就是 §4.6**:把一根真正的 50 Hz 正弦从**已经验证过的那个注入点**灌进去,
@@ -242,7 +242,7 @@ M4 的六条退出判据(逐条对齐现状,别整格打勾):
- 与"自检失败 / 采样故障闭锁"同构(`PROT_HF_LOCK_MASK` 那一套),**不新增机制**。
- 现有台架 `/serial-boot-test` 的期望会因此变化——**这是有意的**,任务卡里同步更新判据。
- 台架调试要旁路只能走编译期开关 `DTU_BENCH`,不进 cfg、不进 104。
⚠ **`DTU_BENCH` 只旁路这一处**:它**不**旁路 §2.3 #1 的测频幅值门,也不产生任何新的读数通道。
⚠ **`DTU_BENCH` 只旁路这一处**:它**不**旁路 §2.3 #1 的测频幅值门,也不产生任何读数出口。
### 4.4 落盘:拆两段,不碰引擎
@@ -300,7 +300,7 @@ typedef struct {
| 注入点已经验证过,且就在那儿 | `docs/测试说明/专项验证/ADC前端注入验证流程.md` §3 的注入点清单(串联电阻端子侧 = `U5` 排针);③ 段 1:1 已拿到台架证据(§2.6) |
| **不知道 ④ 变比也能验算法** | 注入点在 ④ **之前**,发生器给的电压就是引脚电压,算法链要证的正是"引脚域里算得对不对" |
| 幅度小两个数量级 | 只需 ±2 V 以内的正弦(TPAFE 侧判据上限);端子二次值验证要上 100 V / 220 V,风险与设备都在那一级 |
| 现有静态验证的读数的地方可直接复用 | 串口心跳,零固件改动(§4.6.3) |
| 现有静态验证的读数出口可直接复用 | 串口心跳 + 104 YC,零固件改动(§4.6.3) |
**这一跳证的是功能性**(算得对不对、品质报得实不实、节拍稳不稳),**不是计量**(误差几级)——
发生器不当"标准"的界线见 §4.6.5 与 §7 #9。
@@ -313,37 +313,34 @@ typedef struct {
> **轮次名一律用语义短语(用户裁定 2026-10-30:清退"字母+数字"判据号)**:D1 → 幅值轮、D2 → 波形轮、
> D3 → 相位轮、D4 → 测频轮、D5 → 削顶轮、D6 → 谐波轮;本族旧号作废不得再取走
> (与二极管位号、网络自愈决策记录、链路延迟缺陷编号只是同形,各不相干)。
>
> **注入幅度一律按峰峰值 Vpp 设定(用户裁定 2026-11-04)**:发生器面板的有效值显示不当设定基准
> (同一个 Vpp 换波形后有效值显示跟着变,切波形瞬间还残留上一波形的值);万用表 AC 档与板上读数
> 天然是有效值刻度,正弦下 有效值 = Vpp ÷ 2.828,换算着比。
| 轮次 | 验什么 | 接法 | 读哪 | 过判据 |
|---|---|---|---|---|
| **幅值轮** | 基波幅值计量 | UA(R31 / `AIN1`)注 Vpp 2.828 V(= 峰值 ±1.414 V)、50.000 Hz 正弦 | `meas:` 的 `ua_mv` | 读数 = 万用表 AC 档读数,偏差 ≤2 %(与静态同档)。⚠ 别用 `phasor(hb)` 行比幅值——那是 ×0.1 定点,注 1 V 只剩 10 格(§4.6.3) |
| **幅值轮** | 基波幅值计量 | UA(R31 / `AIN1`)注 1.000 Vrms、50.000 Hz 正弦 | `meas:` 的 `ua_mv`;或 104 `0x4011` | 读数 = 万用表 AC 档读数,偏差 ≤2 %(与静态同档)。⚠ 别用 `phasor(hb)` 行比幅值——那是 ×0.1 定点,注 1 V 只剩 10 格(§4.6.3) |
| **波形轮** | 读数是否只跟基波走 | 同幅值改方波、三角 | 同上 | 期望值 = **基波分量有效值**(同峰值下的基波 / 峰值比:方波 ≈ **0.90**,闭式 4/π ÷ √2 = 0.900、20 点整周期 DFT 实算 0.904;三角 ≈ **0.573**,闭式 8/π² ÷ √2 = 0.573、实算 0.578),**不是**真有效值(方波 = 1.000、三角 = 0.583 峰值);偏差 ≤2 %。算术与逐波形对照表见 `docs/测试说明/专项验证/ADC前端注入验证流程.md` §8.5 的波形卡。顺带把"这台仪表读的是基波"这条口径钉进文档——用户拿真有效值表去对会以为差了 10 % |
| **相位轮** | 相位 | UA 与 UN 两路**同片**(都走 TPAFE)注同幅、同相 | **定量档**:录波段文件 `test/host/wave_read.py fft`(浮点重算基波相位;段内表序 `UA` = 0、`UN` = 3);**定性档**:`phasor(hb)` 各行 `Re=` / `Im=` | **定量档**:同段内两路相位差 ≤2°,把发生器相位改 90° 后差值跟到 ≈90°。**定性档只判"跟随"**(差值落进 80°~100°)——`phasor` 串口行是 ×0.1 定点,Vpp 2.828 V(板上读数 ≈1000 mV)时每格 0.1 V,角度量化最坏 ≈ atan(0.1 / 0.707) ≈ 8°,判不动 2°(§4.6.3)。⚠ 段与发生器**无绝对相位锁定**(触发瞬间钉在段内点 159),只能比**同段内两路**的相对差,跨段比相位或拿发生器相位当绝对基准都不成立。**只许比同片内**——跨转换器无同刻性(§9),STM32 那五路还有 1 拍乒乓滞后 |
| **测频轮** | 测频 | 同幅值轮的 1 V/50 Hz | `freq(hb):` | **预期就是"不出数"**(幅值门 50 V 在引脚域恒不过,§2.3 #1)→ 这条的判据是"如实报不可测"(`freq_valid=0`、UI `Hz` 显示 `--`),**不是**报出一个错误的 50.00。想真验 45~55 Hz 精度(PC 侧判据 0.02 Hz)须临时把 `FREQ_AMP_MIN` 降到引脚域量级(如 0.5 V)单跑一轮 → §7 #8 + §8 风险行 |
| **削顶轮** | 削顶与品质路由(**负向用例**) | 给 UB(R52 / `AIN2`)注 **DC −1.00 V 并保持 ≥5 s**;或 Vpp 2.828 V 而频率 **2.0 Hz** 的正弦——两档都能造出"连续贴同一侧轨带"。⚠ **注 50 Hz 同幅正弦造不出锁存,且这是预期而非故障**:1 kHz 下一个周波只 20 点、正半周必落中带,而 `adc_health_rail_watch()` 任何一拍回中带就把计数清零(`App/algo/adc_health.c`),连续贴底最长 ≈ 209/360 × 20 ≈ 11 拍 < 20。TPAFE 侧在 ±2 V 判据上限内造不出这条(轨带 ±32112 码 ≈ ±4.9 V),只有 STM32 那五路能做 | `cal:` 行的 `rail=` / `edge=`;`rail: edge=…` 的 `[ERR]` 行;段文件档另加 `wave_read.py shape` 的三把尺子(`--strict` / 再加 `--clip-superset` / 不带),逐条配法见 `docs/测试说明/专项验证/ADC前端注入验证流程.md` §8.5 的削顶卡 | 负半周钉下轨 → **连续 20 拍同侧后该路贴轨位锁存**(`ADC_RAIL_N_RUN = 20`,`App/algo/adc_health.h:174`)、`edge` 起沿。锁存另有**释放沿前提**:该通道此前须出现过中带样本 → **先上电见基线摆动、再开发生器输出**,开机就注会测出假不通过。撤掉注入后 `rail` 仍非 0 是 sticky 语义(只有 `adc_health_rail_init()` / 复位清)——**复位后仍不清才是 bug**。⚠ 这条会**故意造出 `[ERR]`**,而 `/serial-boot-test` 的 forbidden 标记含 `[ERR]` → 人工判读、排在最后单独一轮(§8 第一行) |
| **谐波轮** | 谐波轨 | 基波 + 20 % 二次 + 20 % 五次(IA 上注)。⚠ 峰值预算:Vpp 2.828 V 基波 + 两个 20 % 分量的最坏对齐峰值 ≈ 1.98 V 已顶 ±2 V 红线 → 基波用 **Vpp 2.263 V**、二次与五次各 Vpp 0.453 V | **定量档**:段文件 `test/host/wave_read.py fft --ch 4 --all`(`IA` = 段内表序 4,直接给基波与 2 / 5 次谐波有效值,单位 mV);**旁证**:UI 交流量页 `Ia2` / `Ia5`(1 mV)与 `phasor(hb) C04-IA-F2` / `C05-IA-F5`(×0.1 定点,只证这根轨不是恒 0) | 谐波轨读数 = 注入分量(**±2 %,走定量档**;周期行档判不动:读数 0.160 V 在 ×0.1 串口行上只有 1~2 格)。⚠ 这两根轨**目前没有任何下游消费者**(`prot_elm.c` 无引用、`meas` 只算 `harm=1`),所以谐波轮只证"算得对",**不证"有人用"**——接到哪、给谁用,归 §9 的谐波轨立项 |
| **相位轮** | 相位 | UA 与 UN 两路**同片**(都走 TPAFE)注同幅、同相 | **定量档**:录波段文件 `test/host/wave_read.py fft`(浮点重算基波相位;段内表序 `UA` = 0、`UN` = 3);**定性档**:`phasor(hb)` 各行 `Re=` / `Im=` | **定量档**:同段内两路相位差 ≤2°,把发生器相位改 90° 后差值跟到 ≈90°。**定性档只判"跟随"**(差值落进 80°~100°)——`phasor` 出口是 ×0.1 定点,1.000 Vrms 时每格 0.1 V,角度量化最坏 ≈ atan(0.1 / 0.707) ≈ 8°,判不动 2°(§4.6.3)。⚠ 段与发生器**无绝对相位锁定**(触发瞬间钉在段内点 159),只能比**同段内两路**的相对差,跨段比相位或拿发生器相位当绝对基准都不成立。**只许比同片内**——跨转换器无同刻性(§9),STM32 那五路还有 1 拍乒乓滞后 |
| **测频轮** | 测频 | 同幅值轮的 1 V/50 Hz | `freq(hb):` 与 104 `0x401D` | **预期就是"不出数"**(幅值门 50 V 在引脚域恒不过,§2.3 #1)→ 这条的判据是"如实报不可测"(`freq_valid=0`、UI `Hz` 显示 `--`、104 上送 IV),**不是**报出一个错误的 50.00。想真验 45~55 Hz 精度(PC 侧判据 0.02 Hz)须临时把 `FREQ_AMP_MIN` 降到引脚域量级(如 0.5 V)单跑一轮 → §7 #8 + §8 风险行 |
| **削顶轮** | 削顶与品质路由(**负向用例**) | 给 UB(R52 / `AIN2`)注 **DC −1.00 V 并保持 ≥5 s**;或 1.000 Vrms 而频率 **2.0 Hz** 的正弦——两档都能造出"连续贴同一侧轨带"。⚠ **注 50 Hz 同幅正弦造不出锁存,且这是预期而非故障**:1 kHz 下一个周波只 20 点、正半周必落中带,而 `adc_health_rail_watch()` 任何一拍回中带就把计数清零(`App/algo/adc_health.c`),连续贴底最长 ≈ 209/360 × 20 ≈ 11 拍 < 20。TPAFE 侧在 ±2 V 判据上限内造不出这条(轨带 ±32112 码 ≈ ±4.9 V),只有 STM32 那五路能做 | `cal:` 行的 `rail=` / `edge=`;`rail: edge=…` 的 `[ERR]` 行;104 `0x4012`;段文件档另加 `wave_read.py shape` 的三把尺子(`--strict` / 再加 `--clip-superset` / 不带),逐条配法见 `docs/测试说明/专项验证/ADC前端注入验证流程.md` §8.5 的削顶卡 | 负半周钉下轨 → **连续 20 拍同侧后该路贴轨位锁存**(`ADC_RAIL_N_RUN = 20`,`App/algo/adc_health.h:174`)、`edge` 起沿、104 上该路 IV 且值清零。锁存另有**释放沿前提**:该通道此前须出现过中带样本 → **先上电见基线摆动、再开发生器输出**,开机就注会测出假不通过。撤掉注入后 `rail` 仍非 0 是 sticky 语义(只有 `adc_health_rail_init()` / 复位清)——**复位后仍不清才是 bug**。⚠ 这条会**故意造出 `[ERR]`**,而 `/serial-boot-test` 的 forbidden 标记含 `[ERR]` → 人工判读、排在最后单独一轮(§8 第一行) |
| **谐波轮** | 谐波轨 | 基波 + 20 % 二次 + 20 % 五次(IA 上注)。⚠ 峰值预算:1.000 Vrms 基波 + 两个 20 % 分量的最坏对齐 ≈ 1.98 V 已顶 ±2 V 红线 → 基波用 **0.800 Vrms**、二次与五次各 0.160 Vrms | **定量档**:段文件 `test/host/wave_read.py fft --ch 4 --all`(`IA` = 段内表序 4,直接给基波与 2 / 5 次谐波 RMS,单位 mV);**旁证**:UI 交流量页 `Ia2` / `Ia5`(1 mV)与 `phasor(hb) C04-IA-F2` / `C05-IA-F5`(×0.1 定点,只证这根轨不是恒 0) | 谐波轨读数 = 注入分量(**±2 %,走定量档**;周期行档判不动:0.160 Vrms 在 ×0.1 出口只有 1~2 格)。⚠ 这两根轨**目前没有任何下游消费者**(`prot_elm.c` 无引用、`meas` 只算 `harm=1`),所以谐波轮只证"算得对",**不证"有人用"**——出口归 §9 的谐波轨立项 |
**先做幅值 / 波形 / 削顶三轮**(纯读数、无跨通道约定),相位 / 谐波次之,测频轮视 §7 #8 的裁定。每条的两档跑法(周期行标量档 /
录波段文件定量档)、触发动作、耗时排布与失败分诊在 `docs/测试说明/专项验证/ADC前端注入验证流程.md` §8.6。
#### 4.6.3 数字能从哪儿读(零固件改动)
#### 4.6.3 读数出口(零固件改动)
| 读数的地方 | 有什么 | 坑 |
| 出口 | 有什么 | 坑 |
|---|---|---|
| 串口每 10 s 心跳(`App/task/adc_task.c` 心跳块) | `adc:`(9 路码值)、`cal:`(三路 mV + `vl`/`cl`/`rail`/`edge` 位图)、`phasor(hb)`(逐实例 `Re` / `Im` / `rms`)、`freq(hb)`、`meas:`(快照轨) | MicroLIB 无 `%f`,全是定点:`phasor` ×0.1(**1 V 注入只剩 10 格**)、`freq` ×0.01 Hz、`cal:` / `meas:` ×1000(1 mV)。10 s 一行的单点瞬时值对 50 Hz 等价于随机相位抽样 |
| 104 YC(现成 PC 主站 `test/host/iec104_master.py`) | `0x4011`~`0x4018` 八个电气量 + `0x401D` 频率,**浮点无量化**,还带品质 | UB/UC/IB/IC/IN 恒 IV、值清零 → 那五路只能回读串口码值域 |
| UI 交流量页 | `Ua` / `U0` / `Ia` / `Ia2` / `Ia5` / `Hz` / 标定徽标 共 7 行 | 160×160 单色;`--` **同时**表示"没数"和"有数但不可信" → 不能当判据,只能当旁证 |
| **段文件(原始样本,代码侧已落地,2026-10-29)** | 每次触发一段:`/wave/WAVE00.BIN` 起共 100 个号轮转(FTP 根下可见),8 路 × 320 点原始码值加封存瞬间的标定与品质快照;PC 侧 `test/host/wave_read.py` 提供 dump / fft / shape / plot 四个子命令(bench 工具,纯标准库,自带 `--selftest`) | 「比对派生标量」已可升级成「PC 独立重算 vs 板上读数」对表(判据与执行步骤见 `docs/测试说明/专项验证/ADC前端注入验证流程.md` §4,格式属主见 `docs/子系统设计/故障录波设计说明.md` §6.2 与 §6.3)。**但板级那一轮还没跑**(注 1 V / 50 Hz、与 `meas:` 对表、CRC 不符必须拒收的实测,见 `docs/子系统设计/故障录波设计说明.md` §10);示波器仍旧只作旁证、不当判据。段内点序:触发瞬间落在段内点 159(预触发 160 点的最后一点),段的绝对相位与发生器无锁定关系,可核对的是相对相位与极性 |
| **仍然没有的(2026-10-29 更新)** | 没有命令控制台、也没有「把窗里 20 / 40 个点导出来」的定点 dump 子命令——**但原始样本这条通道本身已成立**(上一行的段文件:一次 320 点 / 8 路,比原来规划的 20 / 40 点窗更宽) | 现场取回一律走现成 FTP(21 端口),形状就地看回放页单通道、或在 PC 侧用同一脚本出图;示波器只对照发生器、不当判据。**判据已升级为「PC 独立重算 vs 板上读数」对表**,对表的执行轮次待台架(见 `docs/子系统设计/故障录波设计说明.md` §11) |
| **段文件样本出口(代码侧已落地,2026-10-29)** | 每次触发一段:`/wave/WAVE00.BIN` 起共 100 个号轮转(FTP 根下可见),8 路 × 320 点原始码值加封存瞬间的标定与品质快照;PC 侧 `test/host/wave_read.py` 提供 dump / fft / shape / plot 四个子命令(bench 工具,纯标准库,自带 `--selftest`) | 「比对派生标量」已可升级成「PC 独立重算 vs 板上读数」对表(判据与执行步骤见 `docs/测试说明/专项验证/ADC前端注入验证流程.md` §4,格式属主见 `docs/子系统设计/故障录波设计说明.md` §6.2 与 §6.3)。**但板级那一轮还没跑**(注 1 V / 50 Hz、与 `meas:` 与 104 YC 对表、CRC 不符必须拒收的实测,见 `docs/子系统设计/故障录波设计说明.md` §10);示波器仍旧只作旁证、不当判据。段内点序:触发瞬间落在段内点 159(预触发 160 点的最后一点),段的绝对相位与发生器无锁定关系,可核对的是相对相位与极性 |
| **仍然没有的(2026-10-29 更新)** | 没有命令控制台、也没有「把窗里 20 / 40 个点导出来」的定点 dump 子命令——**但样本出口本身已成立**(上一行的段文件:一次 320 点 / 8 路,比原来规划的 20 / 40 点窗更宽) | 现场取回一律走现成 FTP(21 端口),形状就地看回放页单通道、或在 PC 侧用同一脚本出图;示波器只对照发生器、不当判据。**判据已升级为「PC 独立重算 vs 板上读数」对表**,对表的执行轮次待台架(见 `docs/子系统设计/故障录波设计说明.md` §11) |
#### 4.6.4 接法与红线(照抄既有红线,不新发明)
- 注入点:串联电阻的**端子侧**焊盘(`U5` 排针),逐路清单见 `docs/测试说明/专项验证/ADC前端注入验证流程.md` §3;
引脚侧没有串阻保护,必须外串 **10 k** 限流。
- 幅度按**峰峰值 / 峰值**核,别看有效值数字:Vpp 2.828 V 正弦峰值已 ±1.414 V;TPAFE 侧按判据只到 **±2 V 峰值(= Vpp 4 V)**(满量程 ±5 V),
- 幅度按**峰值**算,别看 Vrms 数字:1.000 Vrms 正弦峰值已 1.414 V;TPAFE 侧按判据只到 **±2 V**(满量程 ±5 V),
STM32 那五路是 **0~3.3 V 单极性**——给它们注交流只能是削顶轮那种"故意削顶"的用例,峰值同样不得超 3.3 V。
- 发生器与板**必须共地**(否则量到的是两地之间的差),且不形成地环:优先单点共地、必要时发生器浮地。
- **不许**因为"反正发生器便宜"就往端子侧上额定电压——那是 §5 第二张卡(要变比)与 §9(要标准源)的范围。
@@ -355,7 +352,7 @@ typedef struct {
| 端子二次值 | 注入点在 ④ 之前,算出来还是引脚域 | §5「端子/子板变比确认」 |
| 精度等级(0.5 级 / 1 级) | 发生器自己的幅值误差本身就是要被校的量,不能冒充标准源 | §5「两点标定与精度验收」+ §7 #1 |
| 跨转换器相位 / 功率 / 对称分量 | 两片转换器无共同采样触发(V0.1 硬件缺陷)+ STM32 侧 1 拍乒乓滞后;量纲链路口径见通道映射说明的链路分段一节 | §9(本波不做矢量类运算) |
| THD / 谐波方和根 | 固件里只有两根谐波轨,没有合成后的数送出去 | §9(挂谐波轨立项) |
| THD / 谐波方和根 | 固件里只有两根谐波轨,没有合成出口 | §9(挂谐波轨立项) |
| 温升、EMC、长时间漂移 | 单轮台架不覆盖 | 送检与型式试验 |
---
@@ -369,14 +366,14 @@ typedef struct {
| 标识(任务名)| DoD(可验证验收判据) | 硬依赖 | 资源 | 写范围 | 状态 |
|---|---|---|---|---|---|
| **信号发生器动态波形验证** | 按 §4.6.2 跑完六轮(测频轮允许以"如实报不可测"通过),逐条记实测 / 判据 / 结论;执行步骤与判据表**新增到** `docs/测试说明/专项验证/ADC前端注入验证流程.md`(直流节之后开一节"交流"),实测结论在 `docs/硬件平台/ADC通道映射说明.md` §8.1 之后新开一节;证据(串口日志 + 录波段文件)归档 `docs/测试说明/测试数据/boot_log_data/` | 静态注入闭环(✅ 2026-10-27)+ 一台能出 50 Hz 正弦、幅度可调到 Vpp 2.828 V 的交流信号发生器(**不需要标准源、不需要端子加压**) | 🔴 台架 + 手边发生器 | `docs/测试说明/`、`docs/硬件平台/` | ⬜ |
| **信号发生器动态波形验证** | 按 §4.6.2 跑完六轮(测频轮允许以"如实报不可测"通过),逐条记实测 / 判据 / 结论;执行步骤与判据表**新增到** `docs/测试说明/专项验证/ADC前端注入验证流程.md`(直流节之后开一节"交流"),实测结论在 `docs/硬件平台/ADC通道映射说明.md` §8.1 之后新开一节;证据(串口日志 + 104 落库)归档 `docs/测试说明/测试数据/boot_log_data/` | 静态注入闭环(✅ 2026-10-27)+ 一台能出 50 Hz 正弦、幅度可调到 1 Vrms 的交流信号发生器(**不需要标准源、不需要端子加压**) | 🔴 台架 + 手边发生器 | `docs/测试说明/`、`docs/硬件平台/` | ⬜ |
| **端子/子板变比确认** | 给出"端子电压 → ADC 引脚电压"的比值(万用表同时量端子与引脚,或查子板原理图后实测核对),写入 `docs/硬件平台/ADC通道映射说明.md`;据此判定 `eng` 能否置 1;数据缺失则本波只交付引脚电压并登记缺口 | 已知电压注入验证(✅);**与动态验证无依赖关系**,可并行 | 🟡 子板实物或原理图 | `docs/硬件平台/` | ⬜ |
| **两点标定与精度验收** | 电压 4 路(UA/UB/UC/UN)+ 电流 4 路(IA/IB/IC/IN)各做两点标定(零点 + 满度,温度不纳入),写入 `[adc_cal]` 并置 `VALID`;误差 ≤ 目标精度(待定:0.5 级 / 1 级 → §7 #1);证据归档 | 端子/子板变比确认;削顶五路的处置结论(削顶轮) | 🟡 标准源 / 继保测试仪 | `docs/算法/`、`docs/测试说明/测试数据/boot_log_data/` | ⬜ |
**为什么把动态验证排在第一**(不是因为它最重要,而是因为它把后面两张卡踩实):
1. 它**不依赖任何外部资源**,而另两张卡都在等东西;等待期正好做它。
2. 它复用刚闭环的注入点与读数的地方,**边际成本几乎为零**。
2. 它复用刚闭环的注入点与读数出口,**边际成本几乎为零**。
3. 它会先把削顶轮那类"预期内的 `[ERR]`"暴露在台架规程里并写清判读,避免第二、三张卡跑到一半
才发现"自动化测试把这轮读成 FAIL"。
4. 它顺手把"发生器能证 / 不能证"的界线钉住,两点标定开跑时判据不会串档。
@@ -437,9 +434,9 @@ flowchart TD
| 风险 | 触发条件 | 缓解 |
|---|---|---|
| **削顶负向用例被当成固件 bug** | 做削顶轮时 STM32 那五路必然钉下轨 → `rail: edge=…` 是 `[ERR]` 行,而 `/serial-boot-test` 的 forbidden 标记含 `[ERR]` → 整轮被判 FAIL | 判读规则写进流程文档:"做削顶轮的这一轮 `[ERR] rail:` 属预期,只人工判读、不计入 `/serial-boot-test` 结论",或把这五路排在最后单独一轮并事先声明。同时别忘 `cl=0xE6` 是**来源派生**(§2.6),不是本轮新削出来的 |
| **拿发生器的精度当标准用** | 用发生器自报的有效值数字当基准值去比 | 基准 = 万用表 AC 档实测(§7 #9);发生器只当"波形干净、频率可锁"的信号源,它自己的幅值误差正是要被校的量 |
| **拿发生器的精度当标准用** | 用发生器自报 Vrms 当基准值去比 | 基准 = 万用表 AC 档实测(§7 #9);发生器只当"波形干净、频率可锁"的信号源,它自己的幅值误差正是要被校的量 |
| **临时改了 `FREQ_AMP_MIN` 忘了还原** | 走 §7 #8 的 B 方案 | 改动落在产品头文件里、会编进固件。单列一条收尾判据:还原后 `/keil-build` 0 错误 0 警告 + `git diff` 里 `App/algo/freq.h` 必须为空;并在 §7 #8 那行留一句"本轮台架已还原" |
| **峰值当成有效值,顶到量程外** | 只看面板有效值数字:正弦有效值 1.000 V = Vpp 2.828 V,峰值已 ±1.414 V | 接线前按**峰值**核一遍:TPAFE 侧 ≤±2 V、STM32 侧峰值必须 <3.3 V(且负半周本就钉轨,只作削顶轮用) |
| **峰值当成有效值,顶到量程外** | 只看 Vrms 数字:1.000 Vrms 峰值已 1.414 V | 接线前按**峰值**核一遍:TPAFE 侧 ≤±2 V、STM32 侧峰值必须 <3.3 V(且负半周本就钉轨,只作削顶轮用) |
| **发生器与板不共地 / 地环** | 两台设备各自接电网地 | 单点共地,必要时发生器浮地;读数漂或出现与幅度无关的偏置先查这里 |
| **非整 50 Hz 注入被当成 `meas` 抖动 bug** | 注 49.5 / 50.5 Hz | `meas` 是**瞬时基波有效值透传、200 ms 是上送节拍不是平均窗**(§4.5)→ 快照轨会有拍频式周期间跳动,属预期。测频腿要扫频时读 `freq(hb)`,别读 `meas:` |
| **`cfg` 键数顶格** | 后续给 `[adc_cal]` 加键 | 已按 8+1 / 8 拆段;加键前先看 `CFG_FILE_MAX_KEYS`,必要时再拆 |
@@ -455,9 +452,9 @@ flowchart TD
| 不做 | 理由 / 归属 |
|---|---|
| THD / 谐波方和根真有效值、快照 4 点平均 | 挂 `docs/算法/继电保护功能复现规划.md` 的「遥测谐波轨(可选)」一起做;快照保持"基波有效值 + 直取相量"口径(§4.5) |
| **谐波轨被谁消费**(C04 / C05 目前无人消费) | 谐波轮只证"算得对";接线到判据或上送属谐波轨立项范围,本波不动 |
| **谐波轨的消费出口**(C04 / C05 目前无人消费) | 谐波轮只证"算得对";接线到判据或上送属谐波轨立项范围,本波不动 |
| 端子级额定电压、绝缘耐压、温升 | 要标准源 / 型式试验条件;本波所有注入都在 `U5` 排针侧 ±2 V 以内 |
| 录波(原始码值段文件) | **已交付代码面**(2026-10-29 立项并当日落地;判据属主 `docs/子系统设计/故障录波设计说明.md` §10,原卡片登记簿 2026-10-30 销项并入)本卡「动态波形验证」的原始样本**已**由录波段文件承担(§4.6.3 读数地点表已改),排序也随之调整(`docs/子系统设计/故障录波设计说明.md` §11);**没跑的是板级那一轮**(§10) |
| 录波(原始码值段文件) | **已交付代码面**(2026-10-29 立项并当日落地;判据属主 `docs/子系统设计/故障录波设计说明.md` §10,原卡片登记簿 2026-10-30 销项并入)本卡「动态波形验证」的样本出口**已**由录波段文件承担(§4.6.3 出口表已改),排序也随之调整(`docs/子系统设计/故障录波设计说明.md` §11);**没跑的是板级那一轮**(§10) |
| 104 文件传输(规约侧读文件) | 仍独立待排;现场与台架取回一律走现成 FTP(21 端口),规约侧文件服务等送检或第三方交接需求再议 |
| 等周波调频 | 与标定正交、无标准源时判据不可达,见 `docs/算法/继电保护功能复现规划.md` §3.1 |
| 功率 / 相位跨相比较 / 功率因数 / 对称分量 | **V0.1 硬件禁止**:跨转换器无同一时刻性,`docs/硬件平台/ADC通道映射说明.md` §7 通道归属行 |
@@ -475,9 +472,9 @@ flowchart TD
|---|---|
| **`docs/测试说明/专项验证/ADC前端注入验证流程.md`** | **执行步骤的属主**。本文只写"证到哪、判据是什么、为什么这么排";怎么接表笔、读哪一行、填哪张表一律在那篇里。动态验证的**步骤**同样落那篇(新开"交流"节),本文 §4.6 只留判据 |
| **通道映射说明** | `docs/硬件平台/ADC通道映射说明.md` §7.1(①②③④ 链路分段)与 §8.1(静态六项实测)是**量纲与实测记录的属主**;本文 §2.1 / §2.6 是它的**消费者**,不另立口径,动态实测结论也回写该文件 |
| **`docs/子系统设计/故障录波设计说明.md`** | 录波段文件是本卡「动态波形验证」的**原始样本来源**,数据面代码侧已落地(2026-10-29:环与段格式在 `App/algo/wave_ring.c` / `App/algo/wave_store.c`,PC 侧 `test/host/wave_read.py`);判据从「比对派生标量」升级为「PC 独立重算 vs 板上读数」对表的**执行轮次待台架**,动态验证的排序已因此让位给录波数据面(用户裁定 2026-10-29,理由与三处口径变更见 docs/子系统设计/故障录波设计说明.md §11) |
| **`docs/子系统设计/故障录波设计说明.md`** | 录波段文件是本卡「动态波形验证」的**样本出口**,数据面代码侧已落地(2026-10-29:环与段格式在 `App/algo/wave_ring.c` / `App/algo/wave_store.c`,PC 侧 `test/host/wave_read.py`);判据从「比对派生标量」升级为「PC 独立重算 vs 板上读数」对表的**执行轮次待台架**,动态验证的排序已因此让位给录波数据面(用户裁定 2026-10-29,理由与三处口径变更见 docs/子系统设计/故障录波设计说明.md §11) |
| **`docs/算法/采集与算法层设计说明.md`** | 算法与判据正文(全周傅氏、测频、保护元件);本文只规定"这些算法的信度到什么程度" |
| **`docs/子系统设计/IEC104上行设计说明.md`** | 104 上行协议本体(点表 `0x4011`~`0x4018` + `0x401D`、品质位绑定)——送检与验收的参照;**不是本验证轮的读数通道**;送检验收索引在 `docs/任务规划/登记簿.md` §7 |
| **`docs/子系统设计/IEC104上行设计说明.md`** | 动态验证的**读数出口**(`0x4011`~`0x4018` + `0x401D`);送检验收索引在 `docs/任务规划/登记簿.md` §7 |
| **送检验收** | 标准源送检依赖本文「两点标定与精度验收」;顺序不可倒(先变比 → 再标定 → 才解卡 → 才送检,§6) |
| **UI 补齐** | 已交付的 `meas_snap_t` + `adc_cal_flags()` 供 `ui_data_rt_t` 填 `eng`:现为 0 = 引脚电压,变比确认拿到变比后才升 1 = 二次值。**UI 不必等本文任何一张卡** |
| **双转换器无共同采样触发(V0.2 硬件改版)** | 本波的 `CLIPPED` 位就是这条硬件缺陷在软件里的可见形态;V0.2 改版后该位恒 0,页面与 104 绑定零改动 |
+1 -2
View File
@@ -21,7 +21,7 @@
| # | 事项 | 发现 | 证据与现状 | 出处 |
|---|---|---|---|---|
| dhara 重试自旋残留 | **dhara 重试自旋残留**——`try_recover()` 的 `DHARA_MAX_RETRIES`(8) 自旋在坏块密集时仍可能长时间占用 defaultTask(喂狗点) | 2026-10-02 | 低优先:现有 `vol_lock` + 喂狗节奏缓解;若台架再出"存储侧喂狗饥饿"再排期 | `Lib/dhara/map.c` |
| 可选全序列卷锁 | **可选全序列卷锁**——现卷锁仅覆盖 `syslog` / `cfg` / `lftpd` 三条路径(`vol_lock_wait(0)` try 语义),其它潜在访问点(如 UI 读日志)未纳入。⚠ 原本挂在本题下的「dhara 失败类快照(C4)」待还项**已于 2026-11-04 由问题 #79 还清**(`nand_ftl_fault_get/clear` + 探针按类分流,见 `../架构设计/STM32F4-Base存储架构说明.md` §10.4 已闭环段)——本题现只剩**卷锁覆盖面**本身,不再被 C4 阻塞 | 2026-10-02;C4 拆分 2026-11-04 | 待评估:先确认是否真有第二写者;无则关闭本项 | `App/fs/vol_lock.{c,h}` |
| 可选全序列卷锁 | **可选全序列卷锁**——现卷锁仅覆盖 `syslog` / `cfg` / `lftpd` 三条路径(`vol_lock_wait(0)` try 语义),其它潜在访问点(如 UI 读日志)未纳入 | 2026-10-02 | 待评估:先确认是否真有第二写者;无则关闭本项 | `App/fs/vol_lock.{c,h}` |
## 3. 硬件(V0.1 缺陷 → V0.2 改版项)
@@ -42,7 +42,6 @@
| 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 | 283,512 B (276.87 KB) | 1,048,576 B (1024 KB) | 27.0% |
| ER_IROM1 | 284,800 B (278.12 KB) | 1,048,576 B (1024 KB) | 27.2% |
| 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 合计)** | **285,584 B (278.9 KB)** | 1,048,576 B (1024 KB) | **27.2%** |
| **Flash (ROM 合计)** | **286,872 B (280.1 KB)** | 1,048,576 B (1024 KB) | **27.4%** |
<!-- 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 | 网络校时节拍宿主(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]` 水位待复采后再定抬栈与否,未据静态值擅自抬栈) |
| timeSyncTask | `640*4` | 2.5KB | TCP 时间同步 | 616 B(余 50%)/ 静态 **1264 B**(栈余量收口时抬栈 512→640,2026-10-12) |
| 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) | 283,512 B | 1,048,576 B | 27.0% | 参考 |
| ER_IROM1 (Flash) | 284,800 B | 1,048,576 B | 27.2% | 参考 |
| 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 |
@@ -1,97 +0,0 @@
# 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` |
@@ -19,7 +19,7 @@
关键事实:
1. **CCM 对 CPU 是全速内存**。Cortex-M4 无 L1 cache,"CCM 是慢内存"的直觉只适用于带 cache 的核(M7/L4/H7 的非 cacheable 旁路),**在本芯片上不成立**——任何"按速度分配区域"的论证对本文档无效。
1. **CCM 对 CPU 是全速内存**。Cortex-M4 无 语义取数与有效性 cache,"CCM 是慢内存"的直觉只适用于带 cache 的核(M7/出口与记录/H7 的非 cacheable 旁路),**在本芯片上不成立**——任何"按速度分配区域"的论证对本文档无效。
2. **DMA 对 CCM 地址的传输静默不落地**。2026-09-21 首轮 CCM 实验实证:以 CCM 上栈缓冲(dhara meta 132B / is_free 64B)为 DMA 源/目的时传输不生效,引发 FTL 重格式化循环与幻影坏块。此后 DMA 阈值提至 1024B 消除。**工程规则按"DMA 不可达 CCM"执行,不论数据手册如何表述。**
3. CCM 存在性已用 `ST-LINK_CLI -r32 0x10000000` 本机实测可读写;**克隆片可能缺 SRAM3,换料必须重测并回退布局**。
4. 工程不进入 Stop/Standby 低功耗模式,两区域掉电保留行为无差异,不构成分配判据。
@@ -2,11 +2,11 @@
> **模块名称**:故障录波——采样源沿用 TIM3 1 kHz 节拍,专用录波环(`App/algo/wave_ring.c`,已落地)、触发、落盘(defaultTask 是文件系统唯一写者)、取回(FTP 21 端口)、现场回放(UI 单通道页)。
> **文档定位**:本波的**设计属主**——口径、通道集、数据形态、触发模型、文件格式、预算与所有权边界、显示契约、二期不返工的结构约束在此定稿。**板级判据与本波执行顺序的属主即本文 §9 与 §10**——原任务规划下的《故障录波》卡片登记簿已于 2026-10-30 随销项并入本文后删除(沿革见 §14);排期顺位在 `docs/任务规划/README.md` §2.0「独立」行。
> **状态**:设计已定稿,**数据面 + 触发与落盘接线 + UI 两个页面(手动录波、录波回放)的代码面已交付(2026-10-29)**——主机单测 113 / 170 / 110 全 rc=0、双端编译 0 错误 0 警告、板端编码器与 PC 侧解码器两份独立实现字节级互验通过。**没做完的只有板级判据那一轮**(注量对表、FTP 拉回、与 `meas:` 对表、CRC 不符必须拒收的实测、回放页放一段真波、两个待台架核数值),逐条列在本文 §10 板级各行。三条已裁定口径见 §1.2。**2026-10-31 远程半轮**已把其中不需要到场与注入的六样跑过(落盘道、触发时序、冷却恢复、轮转、PC 侧坏段拒收、104 并发)——结果与回放页新缺陷「段表枚举恒空」全在 §10「板级远程半轮记录」小节;**该缺陷已于 2026-11-02 远程修复并板级闭环**(根因 = 读侧 `wave_store_seg_path()` 返回值判错 + 键处理缺失效,证据与前后对拍见 §10「回放页枚举恒空缺陷修复记录」)。
> **状态**:设计已定稿,**数据面 + 触发与落盘接线 + UI 两个页面(手动录波、录波回放)的代码面已交付(2026-10-29)**——主机单测 113 / 170 / 110 全 rc=0、双端编译 0 错误 0 警告、板端编码器与 PC 侧解码器两份独立实现字节级互验通过。**没做完的只有板级判据那一轮**(注量对表、FTP 拉回、与 `meas:` 和 104 浮点 YC 对表、CRC 不符必须拒收的实测、回放页放一段真波、两个待台架核数值),逐条列在本文 §10 板级各行。三条已裁定口径见 §1.2。**2026-10-31 远程半轮**已把其中不需要到场与注入的六样跑过(落盘道、触发时序、冷却恢复、轮转、PC 侧坏段拒收、104 并发)——结果与回放页新缺陷「段表枚举恒空」全在 §10「板级远程半轮记录」小节;**该缺陷已于 2026-11-02 远程修复并板级闭环**(根因 = 读侧 `wave_store_seg_path()` 返回值判错 + 键处理缺失效,证据与前后对拍见 §10「回放页枚举恒空缺陷修复记录」)。
> **相关文档**:
> `docs/子系统设计/ADC采样节拍设计说明.md`(节拍与时序账的属主;本文 §3 的数据源就是它)
> `docs/硬件平台/ADC通道映射说明.md`(网名 `AINn` 不等于板级 `ADCn`,也不等于转换器通道号;链路分段与削顶归属)
> `docs/任务规划/数据可信.md` §4.6(动态波形验证判据;录波落地后它的读数通路升级,见 §11)
> `docs/任务规划/数据可信.md` §4.6(动态波形验证判据;录波落地后它的读数出口升级,见 §11)
> `docs/架构设计/STM32F4-Base存储架构说明.md` §10.1(FatFs 两条实证陷阱与 8.3 短名限制)
> `docs/内存管理/内存内容分配策略.md` §6 与 R6 规则(新增静态缓冲大于 4 KB 的前置动作)
> `docs/UI设计/内容页架构.md`、`docs/UI设计/RemoDispBus协议与接收设计说明.md`(回放页与远端看图的能力边界)
@@ -311,7 +311,7 @@ flowchart TD
- **实际落地形态(逐字)**:打印点在 `App/task/adc_task.c`:782 行,格式串 `"wave: seg=%lu armed=%u src=%s pts=%u err=%lu ovf=%lu drop=%lu"`——**字段名与顺序就是上面的模板**(台架与 PC 脚本按行名 grep,动它就断脚本);`src` 取 M(手动)/ P(保护)/ T(定时自抓)/ `-`(本次上电还没触发过);`armed` 只在 FREEZE 与 SEALED 为 1(COOL 期只挡新触发不录新段,画进 armed 会被台架误读);`seg` 是累计**成功回执**的段数、不是轮转号;`err` = 落盘失败 + SEALED 滞留超时自弃(含 §5.2 第四条的 ring abandoned 判废),由环内计数收口,落盘道不数第二份账。`err` 与 `ovf` 的**已知容差**(lead 裁定 2026-10-29,不修只入档):这两个计数是跨上下文的读-改-自增(`ovf` 由任意上下文的拒绝路径加、`err` 由 adcTask 的滞留自弃与 defaultTask 的落盘失败分别加),本层双端编译、不许引 FreeRTOS / CMSIS 的临界区内在函数,所以并发拒绝或并发失败时**理论上可能少计一次**。定性为**观测计数的已知容差**:只用于趋势与判读,**不作为任何判据的唯一依据**——要坐实「冷却期内确实来了第二次触发」,同时看 `armed` 与那一刻 `/log/start.log` 的时刻。契约原文见 `App/algo/wave_ring.h` 文件头「观测计数的已知容差」段;
- **落盘道失败留痕名清单(2026-10-30 并入原卡片登记簿;台架判读按名索引,不新增第二种形态)**:单板上一律一行 `[WAVE] seg drop: <原因> (idx=… written=…)`,统一打印点 `App/task/default_task.c`:816——`crc selftest FAIL` / `mkdir /wave` / `opendir /wave` / `readdir /wave` / `seg_path` / `pack_start` / `f_open` / `pack_next` / `f_write short` / `pack stall` / `f_close` / `len mismatch` / **`ring abandoned`**(最后一条即 §5.2 第四条陷阱的处置留痕,代码与主机单测面已有、板级未测,观察项见 §10);另有一条非失败的慢段留痕 `[WAVE][WARN] seg held <毫秒> ms (idx=…)`(`App/task/default_task.c`:831)——搬得慢不是错,真失败才走 `seg drop`。
- 只在原先静默的黑洞处补锚点,别重复已有步骤——每条 DBG 日志约 90 B Flash 加 216 B 栈(`AGENTS.md` 的日志与工具链节);
- 该行已同步进 `docs/测试说明/专项验证/ADC前端注入验证流程.md` 的读数地点表(2026-10-29;同一张表还登记了录波段文件与 PC 侧读段工具)。
- 该行已同步进 `docs/测试说明/专项验证/ADC前端注入验证流程.md` 的读数出口表(2026-10-29;同一张表还登记了段文件出口与 PC 侧读段工具)。
---
@@ -322,7 +322,7 @@ flowchart TD
| 主机侧纯逻辑单测(新增,进 `test/host/`) | ① 环:预触发与后触发点数、冻结与解冻、冷却丢弃计数;② 段格式编解码往返,含 CRC 不符、零长度、字节数不符三条失败路径;③ 触发状态机三条来路(手动 / 保护 / bench)。**环与格式必须是纯逻辑**(不碰寄存器、不碰 RTOS)才能这样测——这是 `App/algo/` 既有分层纪律 |
| PC 侧工具的定性 | `test/host/wave_read.py` 是 **bench 工具**(`test/host/` 下的 loose .py,先例 `test/host/ftp_capture.py`),**不是**主机单测套件 → 不建 `run_unit_test.py`、不加 pre-commit 专属钩子(专属钩子默认不补,按 Lead 裁定 2026-10-27);它自带 `--selftest` 就是它的自带回归,跑法与四个子命令见 `docs/测试说明/专项验证/ADC前端注入验证流程.md` §4 |
| 跨工具链分叉(实证,写进来是为了下一个模块别重踩) | 两份录波 .c 只 include 自身头(链上只有 stdint.h)时,ARMCC 报 22 条 identifier "NULL" is undefined;而主机 MSVC / gcc 会传递性把 stddef.h 拉进来,**主机单测完全照不出这个分叉** → 新增模块需要用 NULL 就自己 include stddef.h(与 `App/algo/sample_ring.c` 同口径,别塞公共头) |
| 板级 | 信号发生器注 1 V / 50 Hz → 手动一段 → FTP 拉回 → PC 脚本量出周波数、幅值、极性与注入一致;再把 PC 重算的基波有效值与板上 `meas:` 对表。**判据只量四样:周波数、幅值、极性、钉轨路数——不比绝对相位**(理由见 §4.2「段内点序」:触发瞬间在工频周期内任意落点,段字节里没有绝对时间参考;真要比相位必须让发生器给同步触发,本波不做)。**钉轨对表的合格判据 = 头位图 ⊇ 实测钉轨路数,且差异必须能由注入条件解释**——因为 `clip_mask` 是按来源派生的链路可信性标记(§6.2 偏移 24 行),不是本段实测值:只加直流或小幅注入时那五路本就不该钉轨,硬取等式会假红。**同段另起一条强观察项**:V0.1 下这五路若在做**交流注入**时反而没钉轨,先怀疑接线或该路根本没被驱动——这是告警面,不是失败面(等式仍作为默认尺子留在 PC 侧 `shape --strict` 里,因为它有这个信号价值;要放行差异用那条判据的解释权,不靠改尺子)。**§5.2 第四条的板级观察项(代码与单测面已有、板级未测)**:bench 连续自抓期间人为把卷占住 2 s 以上(例如同一段跑 FTP 大文件读),看串口 `wave:` 行的 `err` 是否加一、是否只出现**一条** `[ERR] … seg drop: ring abandoned` 留痕(不是每拍刷屏)、且段号不推进(下一段仍写同号文件) |
| 板级 | 信号发生器注 1 V / 50 Hz → 手动一段 → FTP 拉回 → PC 脚本量出周波数、幅值、极性与注入一致;再把 PC 重算的基波有效值与板上 `meas:` 与 104 浮点 YC 对表。**判据只量四样:周波数、幅值、极性、钉轨路数——不比绝对相位**(理由见 §4.2「段内点序」:触发瞬间在工频周期内任意落点,段字节里没有绝对时间参考;真要比相位必须让发生器给同步触发,本波不做)。**钉轨对表的合格判据 = 头位图 ⊇ 实测钉轨路数,且差异必须能由注入条件解释**——因为 `clip_mask` 是按来源派生的链路可信性标记(§6.2 偏移 24 行),不是本段实测值:只加直流或小幅注入时那五路本就不该钉轨,硬取等式会假红。**同段另起一条强观察项**:V0.1 下这五路若在做**交流注入**时反而没钉轨,先怀疑接线或该路根本没被驱动——这是告警面,不是失败面(等式仍作为默认尺子留在 PC 侧 `shape --strict` 里,因为它有这个信号价值;要放行差异用那条判据的解释权,不靠改尺子)。**§5.2 第四条的板级观察项(代码与单测面已有、板级未测)**:bench 连续自抓期间人为把卷占住 2 s 以上(例如同一段跑 FTP 大文件读),看串口 `wave:` 行的 `err` 是否加一、是否只出现**一条** `[ERR] … seg drop: ring abandoned` 留痕(不是每拍刷屏)、且段号不推进(下一段仍写同号文件) |
| 板级·触发来路(2026-10-30 并入原卡「手动触发与保护启动接线」板级段) | 真机面板按一次「手动录波」出一段;造一次过流,段号与该时刻 `/log/start.log` 事件对齐;冷却期内二次触发只加 `ovf` 不重录(**同时看 `armed` 与那一刻 start.log 的时刻,不要只盯 `ovf` 数字**——容差见 §9);冻结窗口内 104 上送与保护判据不受影响(这条是「丢录波样本不丢采集」的验收) |
| 板级·回放页(2026-10-30 并入原卡「现场单通道回放页」板级段) | 真机上放一段真波(不是模拟器合成节拍);远端看图能拍到该页做留证(只作留证,不是波形通路,§8.4);**空态与 CRC 失败态绝不允许画成一条直线**——用坏段在真机复现一次。**2026-10-31 远程半轮进展见下「板级远程半轮记录」小节**:PC 侧坏段拒收已过;回放页坏段/空态画面的测试面**已于 2026-11-02 枚举缺陷修复后打开**(真机已验证真段显示 + 切段切道,见下「回放页枚举恒空缺陷修复记录」小节),坏段/空态两画面本身仍是到场轮要拍的项 |
| UI | `ui-verify` 五步;回放页入 `ui-golden` 黄金参考(**参考图一旦入库只能改代码对齐,禁止刷参考掩盖**);新叶走 `gen_menu.py` 重生成 + 字库链 |
@@ -380,8 +380,8 @@ try 锁纪律)照常读出并显示 ⇒ try 锁读径正常;`manual_req` 作
「0 段缓存」,回放页从此假显「无录波段」、直到下一次 `manual_req` 才重试;失败应不钉表、
下键重试,与 soe_store 读者同纪律。
**本半轮没跑的(人到场轮清单)**:注量对表(周波数 / 幅值 / 极性 / 钉轨路数)、`meas:`
浮点数值对表、两个待台架核数值(封存超时窗 §4.2 兜底行与冷却时长 `App/algo/wave_ring.h`)、P 触发与 start.log 对齐(还得先解标定闭锁)、
**本半轮没跑的(人到场轮清单)**:注量对表(周波数 / 幅值 / 极性 / 钉轨路数)、`meas:` 与 104
浮点 YC 数值对表、两个待台架核数值(封存超时窗 §4.2 兜底行与冷却时长 `App/algo/wave_ring.h`)、P 触发与 start.log 对齐(还得先解标定闭锁)、
bench T 源与卷占用 2 s 的 `ring abandoned` 观察(需 bench 构建)、~~回放页坏段画面与空态画面
(等枚举缺陷修复后才有测试面)~~ **(2026-11-02 枚举缺陷修复后测试面已开,见下小节)**、
`task_prof` 读路径耗时峰值。**执行动作侧**(到场的人做什么、
@@ -447,7 +447,7 @@ bench 锁全程持有;三次烧录(埋点 / 诊断加强 / 根因修复)
| 旧口径 | 新口径 | 原因 |
|---|---|---|
| 「录波通道必须先有工程量,排在本波之后」 | 录波录**原始码值**,不等工程量;解释靠头里的系数快照 | §3.2:现在根本没有工程量,等下去等于永远不动 |
| 动态波形验证读数的地方 = 派生标量(`meas:` / 104 / `phasor(hb)`) | 派生标量仍是**判据**,但样本改从录波段导出 → 判据升级为「PC 独立重算 vs 板上读数」对表 | 录波段就是现成的样本来源,不必再造一条串口通路 |
| 动态波形验证的读数出口 = 派生标量(`meas:` / 104 / `phasor(hb)`) | 派生标量仍是**判据**,但样本改从录波段导出 → 判据升级为「PC 独立重算 vs 板上读数」对表 | 录波段就是现成的样本出口,不必再造一条串口通路 |
| 动态波形验证「手边可做、排在最前」 | 排在录波 P1 之后(用户裁定 2026-10-29:录波先行) | 先有样本通路,验证才不留遗憾 |
(2026-10-30 更新:原卡片登记簿已销项并入本文,§10 尾有本波执行顺序图;排期顺位在 `docs/任务规划/README.md` §2.0「独立」行。)
@@ -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_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) |
| `time_sync_handle_conn` 的 30 s 总预算 | `App/sync/time_sync.c` | 单次可睡满 30 s | **② 切片**:`TIME_SYNC_WAIT_SLICE_MS=200` 夹住每次 `net_recv_to` 的 `rem`,每片先喂位(总预算语义不变) |
| 就绪屏障等待(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 | 主循环(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) |
| timeSyncTask | 主循环;监听重试(500 ms/2 s)前;accept 轮询;`time_sync_handle_conn` 的 **200 ms 切片**;屏障等待分片;入口 `task_wd_bind()` |
| 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,9 +24,8 @@
### 1.1 时基口径:内部一律**本地钟面**(用户裁定 2026-10-27)
**裁定原文:「时基先本地为准」。** 定的是**冲突时谁赢**:(当时的)面板时钟页与 8888 对时把「本地钟面当
epoch」写入,而 104 的 `C_CS NA`(对时命令)携的是 UTC,两条写入口**后写者赢** ⇒ 装置墙钟与主机本地时刻
恒差 8 小时(8888 已于 2026-11-04 退役,写入口现为 SNTP / UI / 104,见 §15、§16)。
**裁定原文:「时基先本地为准」。** 定的是**冲突时谁赢**:面板时钟页与 8888 对时把「本地钟面当 epoch」写入,
而 104 的 `C_CS NA`(对时命令)携的是 UTC,两条写入口**后写者赢** ⇒ 装置墙钟与主机本地时刻恒差 8 小时。
本裁定结束「后写者赢」:**内部真源 = 本地钟面**,任何来源写入前都要先换算成本地钟面再落库。
这条同时也纠正了**本文自己的矛盾**(这才是 8 小时的根):§2 曾写 epoch = 「自 1970-01-01 00:00:00 **UTC**
@@ -299,8 +298,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 漂移,重锚反而降低精度;时间纠偏由时间同步(§16)负责 |
| 网络校时 | **SNTP 客户端 / UDP 123(§16,2026-11-04 落地)为唯一网络校时来源**;PC 推送 / TCP 8888 同日退役(§15) | NTP 公网源需现场路由,口径不变;104 `C_CS` 写入口径见 §1.1 末待决项 |
| 运行期校准 | 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 末待决项 |
| 时区 | **口径已定:内部一律本地钟面,不存时区**(用户裁定 2026-10-27,见 §1.1) | 「API 层加偏移参数」这条旧建议**作废**;唯一待决的是 104 上行是否要转 UTC(§1.1 末,送检前定) |
### 13.1 用户 SRAM 的占用分配(共享资源,用之前先在这里登记)
@@ -334,45 +333,98 @@ 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` | `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/` |
| `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/` |
| `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 :8888)——已退役(2026-11-04)
## 15. 时间同步(PC 推送 / TCP)
> **状态:已退役。** 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`,属历史取证,不改)。
除 RTC 自身走时与开发期校时宏外,系统支持由 **PC 通过 TCP 主动推送本地墙钟** 来同步板子时间,使板子时间与开发电脑一致(无需公网 NTP)。
- **当初定位**:开发期让板子时间对齐 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 节的落地取代,不再单独成立。
### 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 随电脑本地时间递增即表示同步正常。
---
## 16. SNTP 校时(客户端,2026-11-04 落地)
- 模块与任务骨架:`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)` 分片续心跳;
- 模块:`App/sync/time_sntp.c/.h`(挂在 timeSyncTask 的 8888 事件循环里,等待切片 ≤50 ms,不饿任务狗);
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 落定)。
@@ -384,7 +436,5 @@ SD2506 有 **70 字节电池保持用户 SRAM**(寄存器 `2CH~71H`,`Drivers
响应源地址必须等于请求目的地址。失败:超时指数退避(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 的关系(历史):2026-11-04 当日 SNTP 落地在前(PR #77)、:8888 清除在后,中间两路短暂
并存、同一 `sys_clock_set_unix` 收口**后写者赢**;退役后本模块是**唯一网络校时入口**,
`sys_clock_set_unix` 现存三个写入口 = SNTP / UI 手工对时 / 104 `C_CS`(口径见 §1.1)。
- 与 8888 的关系:同一任务、同一 `sys_clock_set_unix` 收口,**后写者赢**维持现状(两路并存是部署选择,不是冲突缺陷)。
- 主机单测:`test/host/sntp/`(报文格式、防伪拒收、KoD、退避与周期、阶跃域守卫、配置注册表)。
+4 -4
View File
@@ -263,7 +263,7 @@ sequenceDiagram
**机制**:`请求 → netTask 重建 → 代次号 → 服务自愈`
**约束与对策**:网络重建会复位 CH395F,所有监听 Socket 随之失效;而两个常驻 TCP 服务(FTP :21 / 远程显示 :7003;对时 :8888 已退役,2026-11-04)的监听由各自任务持有。为此引入"网络代次号"——各服务检测到代次变化后主动重建监听,在新 IP 上恢复服务。
**约束与对策**:网络重建会复位 CH395F,所有监听 Socket 随之失效;而三个 TCP 服务(FTP :21 / 对时 :8888 / 远程显示 :7003)的监听由各自任务持有。为此引入"网络代次号"——各服务检测到代次变化后主动重建监听,在新 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/远程显示/对时(SNTP 出向)
participant SVC as FTP/对时/远程显示
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` 外层循环重建监听 |
| 对时(SNTP 客户端) | UDP 出向(无监听) | `time_sntp_run(net_gen)` 每轮比对代次 | 代次变化 → 弃旧 Socket 归还 → 下轮到点懒重建(2026-11-04 起原 :8888 监听行随退役删除) |
| 对时 | 8888 | `time_sync_task()` 的 accept 循环 | 代次变化 → 关旧监听 → 回外层重建 |
| 远程显示 | 7003 | `ui_net_sink_poll()` | 代次变化 → 关旧监听/客户端 → 重建 |
**关键设计点**:
@@ -643,7 +643,7 @@ LPN 与原始码的位置**就地分类并打印(`ftl_err_name()` 给可读名
| dhara 错误 | 归类 | 处置上为什么不同 |
|---|---|---|
| `ECC` / `TOO_BAD` | `BSP_ERR_CRC` | 位翻转不可纠或页太坏:**是页级问题**,读重试 / 由 dhara 换页,不代表卷坏(**但见下方 10.4.1:页级失败会因重试语义升级成整卷禁用**) |
| `ECC` / `TOO_BAD` | `BSP_ERR_CRC` | 位翻转不可纠或页太坏:**是页级问题**,读重试 / 由 dhara 换页,不代表卷坏 |
| `JOURNAL_FULL` / `MAP_FULL` | `BSP_ERR_NO_SPACE` | 介质还健康,是 **journal / 映射层块池见底**:该走容量恢复(`../测试说明/专项验证/存储维护与容量恢复.md`),此时重格式化只是把问题推回去 |
| `BAD_BLOCK` / `RECOVER` / `NOT_FOUND` / `CORRUPT_MAP` / 越界 | `BSP_ERR_IO` | 映射或介质**完整性**故障:保卷 + 上报,不许自愈重格式化 |
@@ -651,80 +651,7 @@ LPN 与原始码的位置**就地分类并打印(`ftl_err_name()` 给可读名
checkpoint(`DHARA_E_TOO_BAD`)才算真空白卷",本表则保证失败**可读**——两者合起来
才是"别把瞬态毛刺当卷损坏去 `f_mkfs`"(旧行为:单次 I2C/SPI 抖动 → 清 map → 整卷
重格式化 → 静默销毁全部用户数据)。
- **已闭环(2026-11-04,问题 #79)**:分类不再只进日志。`nand_ftl.c` 新增跨调用
失败快照 `nand_ftl_fault_get()` / `nand_ftl_fault_clear()`(`nand_ftl_fault_t`,
20 B:`active`/`op`/`cls`/`lpn`/`count`),在 `disk_initialize` 的 resume 失败、
`ftl_read_page`、`ftl_flush_cache`、`disk_ioctl(CTRL_SYNC)` 四个失败点记账;
defaultTask 的卷健康探针在 `storage_bring_up()` 里读它并按类分流:
`BSP_ERR_NO_SPACE` → **跳过重格式化**(只上报 `storage full`,留待容量恢复),
其余类 → 才 reformat。多写者纪律:语义是"最近一次"而非累计账本,并发下可能被
另一路覆盖(诊断用途可接受),故不加锁、不做权威计数。
### 10.4.1 「页级 TOO_BAD」为什么会升级成「整卷禁用」(2026-11-04 台架实证,问题 #79)
本表上一版只写了「`TOO_BAD` 是页级问题,不代表卷坏」。这句话在**单次读写**的尺度上
成立,但在**连续重试**的尺度上会误导——台架现场正是这条误解造成的:
```
[NAND_FTL] Map resumed: 14094/97504 sectors used ← 映射本身健康
[ERR][NAND_FTL] Cache flush FAIL: LPN=211 dhara=TOO_BAD class=-7
[ERR] [FS] volume FAT invalid (fr=1), reformatting...
[ERR] [FS] volume repair failed (fr=1)
[ERR] [FS] f_mount failed (fr=1) ← 3 轮重试
[ERR] [FS] storage retries exhausted (fr=1), storage disabled
```
两个独立的放大机制叠在一起:
1. **`ftl_flush_cache()` 失败不清 `s_cache_dirty`**(刻意的:写不进去不能假装写成功)
⇒ 缓存里那一页仍是脏的,**下一次任何存储访问都会重试同一个 LPN**。15 条
`Cache flush FAIL` 就是这么来的,11 秒内撞 15 次。
2. **`nand_ftl_format()` 旧实现先 flush、失败即 `return GD5F_ERROR`** ⇒ 本应"销毁
映射重建卷"的恢复动作,被**同一页**在第一步挡住,`dhara_map_clear()` 从未执行;
接着 `f_mkfs` 在**旧映射**上跑,必然失败。每轮启动都重新踩同一条路,故
**重启不恢复**。
- **判据修正**:`TOO_BAD` 仍归类 `BSP_ERR_CRC`(归类没错),但"不代表卷坏"必须加上
限定——**不代表介质坏,不代表卷内容好**。单个 LPN 反复写不进去,配合"脏缓存不清 +
格式化被挡"这两条,足以让整卷在本次启动内不可用。
- **处置修正**:`nand_ftl_format()` 现在尽力刷一次缓存,失败则**丢弃缓存并继续**
`dhara_map_clear()`——格式化语义本就是销毁整卷映射(见 `nand_ftl.h` 该函数注释
「破坏性」),为一份注定作废的脏页挡住恢复动作没有任何收益。⚠ 该"丢弃"权限
**只在 format 内**;`disk_read`/`disk_write` 的 flush 失败必须继续向上报 `RES_ERROR`。
- **与 10.4 表的关系**:格式化的**该不该做**由失败类决定(§10.4 快照分流),
格式化的**做得成做不成**不再被脏缓存劫持(本节)。两者一起才是完整判据。
### 10.4.2 上游真凶定案:误标坏块被持久化(2026-11-04 真机闭环,问题 #79)
10.4.1 的两条放大机制修掉后真机**并未恢复**——`format` 这次确实清了 map,但紧随的
`Map sync after clear failed: TOO_BAD` 说明**全新的 map 也落不了盘**。逐层加临时
诊断(prog/erase 原语失败打印 → is_bad 命中打印 → BBT 合并后打印)后定案:
| 观测 | 结论 |
|---|---|
| `[NAND] BBT scan: 0 bad blocks found` | 出厂无坏块 |
| prog/erase 原语 `[DIAG]` **零输出** | 底层写**全部成功**,TOO_BAD 不来自当次 I/O 失败 |
| `BBT: 129/2048 blocks bad`(合并持久化 BBT 后) | 坏块全部来自**持久化 BBT 的 OR 叠加** |
| 误标是 1016–1142 **连续段**;`Map head=65024`>>6 = **1016** 正中段首 | 不是随机位老化形态 |
| `is_bad hit` 连续命中 1016..1142 | `prepare_head()` 连续 8 次撞坏 → `DHARA_E_TOO_BAD`(journal.c:820-832) |
**机理链**:某次台架操作(断电 / ST-LINK reset 打在写窗口内)造成**传输层/等忙失败**
→ 旧 `dhara_nand_erase/prog()` 把一切失败都 `dhara_nand_mark_bad()` →
`gd5f2gq5ue_mark_block_bad()` **每次都立即持久化 BBT**(轮转池 + 版本号 + CRC,
不可撤销)→ 误标固化 → 此后每轮启动 journal 写头爬进误标段即 TOO_BAD。这解释了
「11-03 20:54 还好的、11-04 起每轮必现、重启不恢复」的全部特征。
- **修复**:只有芯片明确回 `E_FAIL` / `P_FAIL`(状态位证据)才标坏;传输层/超时
只报 `DHARA_E_BAD_BLOCK` 让 dhara 重试,**不标坏**。回归用例
TC-NANDPORT-207/208/505(主机侧,178 条全绿;把修复回退即见红)。
- **哨兵**:`gd5f2gq5ue_init()` 解锁块保护(A0h)后补回读校验(与 B0h ECC 同族
缺口;实测本板 A0h=0,写保护非本次根因,校验留作哨兵);`gd5f_bbt_load()` 合并后
坏块数 > 0 且出厂扫描为 0 时打 `BBT after merge: %lu bad blocks (factory scan
was 0)`——现场一眼区分真坏与误标。
- **存量误标的处置**:代码修复只防未来。已固化的 129 个误标用恢复动作 A
(`TEST_SUITE_STORAGE` + `ENABLE_STORAGE_RECOVERY_TESTS`,重建 BBT + format)
清除:`BBT rebuilt and persisted` → `Storage Recovery: 3/3 PASSED` →
持久化 BBT version=71。产品镜像复烧后 `[FS] FATFS mounted`、
`[CFG] load: file missing, factory template generated`,0 条 flush FAIL。
⚠ 动作 A 语义 = 卷内容全失(旧卷随 map clear 作废),本次台架卷为可弃数据。
- **仍未闭环**:分类目前**只进日志**;`disk_read`/`disk_write` 对 FatFs 仍只能回 `RES_ERROR`
(那是 FatFs 的契约)。要让 defaultTask 的卷健康探针**按类分支**,还需要一个跨调用
存活的失败类快照(`nand_ftl_last_error()`,8 B;代价是要先定多写者纪律——
defaultTask 与 testTask 都会碰存储)→ 即登记簿条目「可选全序列卷锁」的待还项。
@@ -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 | 等网络 → 200 ms 校时节拍 `time_sntp_run()`(SNTP 客户端 UDP 出向 → 写 RTC;:8888 推送监听已退役 2026-11-04) |
| `timeSyncTask` | 640 / 2560 | Normal | 等网络 → TCP :8888 收 PC 推送时间 → 写 RTC |
| `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(默认)/ 200ms SNTP 校时节拍
I->>I: wait NET_READY(Any|NoClear)→ TCP listen :2404(默认)/ :8888
```
> **顺序铁律**:① 配置段注册必须**先于** `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 走 TCP;对时 = `time_sntp` 经同一消息队列走 **UDP 出向**(:8888 监听已退役,2026-11-04,墙钟说明 §15);屏板链路走 USART1 串口(不经 TCP),与 :7003 是同一份显存的两个出口(**串口为主**) |
| `ui_net_sink` / `time_sync` → `net_socket` | 远程显示 :7003 / 对时 :8888 共用同一 TCP 栈;屏板链路走 USART1 串口(不经 TCP),与 :7003 是同一份显存的两个出口(**串口为主**) |
| `ui_menu` → `ui_content_page` → `ui_page_*` | 框架 + 页面插件(描述符解耦;节点→页面绑定由注册表 `ui_page_registry.c` 生成,见 §6) |
| `time_sntp` / `sys_clock` → `sd2506` | 墙钟基准与写回 RTC(对时写钟入口,:8888 退役后唯一网络来源) |
| `time_sync` / `sys_clock` → `sd2506` | 墙钟基准与写回 RTC |
| `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 | 等网络 → 200 ms 校时节拍 `time_sntp_run()`(SNTP 客户端,UDP 出向;写 RTC 经 `sys_clock_set_unix`。:8888 推送监听已退役 2026-11-04) | `time_sync_task` |
| `timeSyncTask` | 2560 | Normal | 等网络 → TCP :8888 收 PC 推送时间 → 写 RTC | `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)→ 200ms 校时节拍 time_sntp_run()(SNTP 客户端 UDP 出向)
T->>T: wait NET_READY(Any|NoClear)→ TCP listen :8888
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/>time_sntp SNTP 客户端<br/>UDP 123 出向(:8888 已退役)"]
NS --> C2["time_sync<br/>对时 :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(timeSyncTask 载体)、time_sntp(SNTP 校时客户端,UDP)
│ ├── sync/ time_sync(TCP 对时服务)
│ └── util/ sys_clock(墙钟)、task_watchdog(IWDG 判定状态机)、boot_stats、sha256、task_prof
├── Drivers/BSP/ 纯手工驱动/服务(不受 CubeMX 保护)
│ ├── CH395F/ 以太网 MAC+PHY(SPI2 + DMA)
@@ -1,22 +1,49 @@
# ADC 前端注入验证流程(台架)
> **这篇是什么**:已知电压注入验证的**执行侧**——怎么写表笔、读哪行日志、填哪张表、结果往哪回写。判据与实测记录在属主文档:直流轮 `docs/硬件平台/ADC通道映射说明.md` §8.1、交流轮 `docs/任务规划/数据可信.md` §4.6.2;**本文只写怎么跑**,分叉时判据从属主。证不到什么见 `docs/任务规划/数据可信.md` §4.6.1 与 §4.6.5,本文不重抄。
> **这篇是什么**:已知电压注入验证的**执行侧**——判据与实测记录在 `docs/硬件平台/ADC通道映射说明.md` §8.1,
> 这边写怎么接表笔、读哪一行日志、填哪张表、结果往哪回写。
>
> **两个轮次**:§1~§7 = **直流轮次**(2026-10-27 六项 PASS,只需万用表 + 电位器 + 串口日志);§8 = **交流轮次**(幅值 / 波形 / 相位 / 测频 / 削顶 / 谐波),一台信号发生器接 §3 同一批注入点就能跑,等标准源的只有**精度等级**。V0.1 五路单极性削顶(`UB/UC/IB/IC/IN`)在交流轮次只作"预期削顶"负向用例。
> **本文分两个轮次**:§1~§7 = **直流轮次**(零点 / 线性 / 通道映射 / 极性,2026-10-27 六项 PASS,
> 证的是"一条水平线的高度";**不需要**示波器、信号源、标准源,**只需要**一块万用表 + 一个电位器 + 串口日志)。
> §8 = **交流(正弦)动态验证轮次**(六轮:幅值 / 波形 / 相位 / 测频 / 削顶 / 谐波,证的是"给定电压算得对不对、品质报得实不实、节拍稳不稳")——
> 一台信号发生器接在本文 §3 的同一批注入点上就能跑,**同样不用等标准源**;
> 等标准源的只是**精度等级**那一步(`docs/任务规划/数据可信.md` §5 的「两点标定与精度验收」)。
> V0.1 有五路单极性削顶(`UB/UC/IB/IC/IN`),它们在交流轮次里只能作"预期削顶"的负向用例。
>
> **判据的属主是 `docs/任务规划/数据可信.md` §4.6.2**(六轮各验什么、过判据是多少),本文 §8 只写**怎么跑**。
> 两边于 2026-10-29 对齐;若将来再分叉,分两类裁定:**「验什么、算不算过」从
> `docs/任务规划/数据可信.md` §4.6.2**,**「期望值怎么算、怎么接线怎么跑」从本文 §8**。
> 这一跳为什么不用等标准源、又证不到什么,见其 §4.6.1 与 §4.6.5,本文不重抄。
---
## 1. 为什么非做不可
下游全部按"工程量"消费(保护判据、104 遥测、UI 交流量页读 `adc_cal_apply()` 输出)。系数错一倍,三处一起错,**没有任何一处会报错**。这一步是**给整条链路量纲盖章**,不是"补充测试"。
下游已经全部按"工程量"消费:保护判据、104 遥测、UI 交流量页读的都是 `adc_cal_apply()` 的输出。系数错
一倍,三处一起错,且**没有任何一处会报错**。"通路活"(9 通道转换成功、零超时、互异非饱和、跨次开机
读数不同)不证明读数对。所以这一步的性质是**给整条链路的量纲盖章**,不是"补充测试"。
## 2. 前置事实:这个前端不是分压器
2026-10-27 按 `docs/硬件平台/V0.1.tel` 逐通道追线(图由 `tools/arch_diagram/gen_adc_frontend_svg.py` 生成,改图改脚本重跑):
2026-10-27 按 `docs/硬件平台/V0.1.tel` 逐通道追线,以 UA 为例(图由
`tools/arch_diagram/gen_adc_frontend_svg.py` 生成,改图请改脚本重跑,勿手改 SVG):
![UA 通道前端:端子 → R31 → ADC7 → TPAFE AIN_7P;C44 / R32 在差分负端,不在信号通路上](../../images/ADC前端电路.svg)
`ADC7` 节点只有 R31、C44、U3.61,**无对地电阻**;"1k 到地"是 R32 接 TPAFE 负输入脚 AIN_7GND,属差分滤波非信号分压。STM32 侧同样无分流电阻。结论:① **不存在 2:1 分压**(`docs/任务规划/数据可信.md` §2.1 已改引脚侧系数 152.59 / 805.66),只需确认转换器 LSB 与逐通道映射;② **端子侧斜率 = 引脚侧斜率**,端子侧注入安全且能直接验 LSB。
`ADC7` 这个节点上**只有** R31、C44、U3.61 三样东西,**没有对地电阻**。所谓"1k 到地"是 R32,
它接在 TPAFE 的**负输入脚 AIN_7GND**(芯片手册 pin62)上,是把差分负端经 1k 拴到 AGND,
属于差分滤波,不是信号分压。STM32 侧更直接(图 B):`AIN2 —[R52 1k]— ADC17 —[C111]— AGND`,
同样没有分流电阻,连 R32 那个接地电阻都没有。
期望斜率(由转换器满量程推出):
**由此得到两条可直接用的结论**:
1. **不存在 2:1 分压**。旧口径"板级系数 305.18 / 1611.3 µV/码"(按 2:1 折算到 `AINn` 侧的条件值)
在本板上不成立;`docs/任务规划/数据可信.md` §2.1 已于 2026-10-27 改为引脚侧系数(152.59 / 805.66)。
所以已知电压注入验证只需**确认转换器 LSB 与逐通道映射**,不必再判别两种读法。
2. **端子侧斜率 = 引脚侧斜率**。所以在**端子侧**注入既安全(有 1k 串阻护着芯片)又能直接验 LSB,
不必去碰 LQFP 引脚。
期望斜率(由转换器满量程事实推出,与前端无关):
| 转换器 | 满量程 | LSB | **期望斜率** |
| ---------- | ----------------------- | ---------------- | -------------- |
@@ -25,7 +52,7 @@
## 3. 注入点清单
串阻两只焊盘 = 两个注入层级:**1 脚 = 端子侧(靠 U5)**,**2 脚 = 引脚侧(靠转换器)**。
串联电阻的两只焊盘就是两个注入层级:**1 脚 = 端子侧(靠 U5)**,**2 脚 = 引脚侧(靠转换器)**。
| 语义 | 串阻 | 端子侧节点 | 引脚侧节点 | 转换器脚 | 期望斜率 |
| -- | --- | --------------- | ------- | ----------- | ------------- |
@@ -39,41 +66,60 @@
| IN | R48 | `AIN11`(U5.C10) | `ADC11` | U17.36(PA2) | 1241.2 |
| 温度 | R47 | `V_ZL2`(U5.C11) | `ADC14` | U17.41(PA5) | 不验(非线性,不参与标定) |
> 引脚侧无串阻保护,注入前**必须外串 10 k 限流**;除非端子侧对不上需分离"转换器的错"和"前端的错",否则不用引脚侧。
> 引脚侧没有串阻保护,注入前**必须外串 10 k 限流**。除非端子侧结果对不上、需要分离
> "转换器的错"和"前端的错",否则不要用引脚侧。
## 4. 数字能从哪儿读(固件只有下面这些周期行,没有命令控制台)
## 4. 读数出口(固件只有下面这些周期行,没有命令控制台)
> **录波段文件能力已落地(2026-10-29)**:故障录波数据面进仓(格式属主 `docs/子系统设计/故障录波设计说明.md` §6.2/§6.3、板级判据 §10;口径变更 2026-10-30 并入该文 §11,原卡片登记簿销项后删除)——每次触发一段,段文件直接给 8 路 × 320 点原始码值,判据可从「比对派生标量」升级为「PC 独立重算 vs 板上读数」互相对表。下表是固件**周期性**打印的全部输出行(段文件只在触发时产生,不在其中),两类数字互为对照。
> **样本出口已落地(代码侧,2026-10-29)**:故障录波的数据面已进仓(格式与判据属主见 docs/子系统设计/故障录波设计说明.md §6.2 与 §6.3、板级判据见 §10、口径变更见该文 §11;原卡片登记簿 2026-10-30 销项并入后删除)——每次触发一段,段文件直接给出 8 路 × 320 点原始码值,动态验证的判据因此**可以**从「比对下表这些派生标量」升级为「PC 独立重算 vs 板上读数」对表。下表仍是固件唯一的**周期行**出口(段文件不是周期行,只在触发时产生),两种出口对表用。
| 日志行 | 频率 | 内容 | 用途 |
|---|---|---|---|
| `adc: UA=.. UB=.. UC=.. UN=.. IA=.. IB=.. IC=.. IN=.. T=..` | 每 10 s | **9 路原始码值** | **直流轮(§1~§7)主要读的数** |
| `cal: ua_mv=.. un_mv=.. ia_mv=.. vl=.. cl=..` | 每 10 s | UA/UN/IA 毫伏 + 标定/削顶位图 | 旁证:码值→电压自洽 |
| `first: UA=.. … T=..` | 每次开机一次 | 9 路原始码值 | 与 `adc:` 互校(同序同格式) |
| `frames=.. idle=.. adc1_err=.. tgap=..` | 每 10 s | 采集健康计数 | 判节奏用(判据在 §5.5,即步骤 4) |
| `phasor(hb) C0x-…: Re=.. Im=.. rms=..`、`freq(hb): f=.. valid=..` | 每 10 s | 基波相量与测频(×0.1 / ×0.01 定点) | **交流轮(§8)主要读的数**(直流轮只旁证) |
| `meas: seq=.. v=.. ua_mv=.. un_mv=.. ia_mv=.. f=.. fv=..` | 每 10 s | 200 ms 遥测快照轨(基波有效值 mV) | 交流幅值比对(1 mV 分辨率) |
| **段文件** `/wave/WAVE00.BIN` 起 100 号轮转(FTP 根下,与 /log 平级) | 每次触发一段 | 8 路 × 320 点 + 封存瞬间标定与品质快照(`docs/子系统设计/故障录波设计说明.md` §6.2) | **录波档要用的原始样本**:预触发 160 + 后触发 160 点,触发瞬间在段内点 159 |
| PC 侧 `test/host/wave_read.py`(纯标准库) | 手动跑;`--selftest` 自带回归 | 四子命令:dump(逐字段双 CRC)/ fft(全周傅氏重算基波有效值与相位)/ shape(削顶与形态,可 `--strict`)/ plot(SVG/PNG,PNG 用 `-o`) | 「PC 独立重算」执行体;CRC 不符或零长度必须**拒绝** |
| 日志行 | 频率 | 内容 | 用途 |
| ----------------------------------------------------------- | ------ | ------------------------ | ------------------- |
| `adc: UA=.. UB=.. UC=.. UN=.. IA=.. IB=.. IC=.. IN=.. T=..` | 每 10 s | **9 路原始码值** | 本次验证的主读数 |
| `cal: ua_mv=.. un_mv=.. ia_mv=.. vl=.. cl=..` | 每 10 s | UA/UN/IA 折算后毫伏 + 标定/削顶位图 | 旁证:码值 → 电压是否自洽 |
| `first: UA=.. … T=..` | 每次开机一次 | 9 路原始码值 | 与 `adc:` 行互校(同序同格式) |
| `frames=.. idle=.. adc1_err=.. tgap=..` | 每 10 s | 采集健康计数 | 步骤 4 的节奏判据 |
| `phasor(hb) C0x-…: Re=.. Im=.. rms=..`(逐实例)、`freq(hb): f=.. valid=..` | 每 10 s | 基波相量分量与测频读数(×0.1 / ×0.01 定点) | **交流轮次的主读数**(直流轮次只作旁证) |
| `meas: seq=.. v=.. ua_mv=.. un_mv=.. ia_mv=.. f=.. fv=..` | 每 10 s | 200 ms 遥测快照轨(基波有效值 mV) | 交流轮次的幅值比对(1 mV 分辨率,比 phasor 行细) |
| **段文件** `/wave/WAVE00.BIN` 起共 100 个号轮转(FTP 根下可见,与 /log 平级) | 每次触发一段(手动 / 保护启动 / bench 定时) | 8 路 × 320 点原始码值 + 封存瞬间的标定与品质快照(布局见 docs/子系统设计/故障录波设计说明.md §6.2) | **样本出口**:交流轮次的主对表源——预触发 160 点 + 后触发 160 点,触发瞬间落在段内点 159 |
| PC 侧脚本 `test/host/wave_read.py`(不是固件日志,bench 工具,纯标准库) | 手动跑;`--selftest` 是它的自带回归 | 四个子命令:dump(逐字段与双 CRC)/ fft(全周傅氏重算基波有效值与相位)/ shape(削顶与形态判定,可 `--strict`)/ plot(出 SVG 或 PNG,PNG 用 `-o` 落文件) | 把段文件还原成周波数、幅值、相位、削顶判定与图 = 「PC 独立重算」这条判据的执行体;CRC 不符或零长度它必须**拒绝**而不是画出东西 |
> ⚠ 交流轮次(判据 `docs/任务规划/数据可信.md` §4.6)必须用 `meas:` 比幅值,别用 `phasor(hb)`——那行 ×0.1 定点,注 1 V 只剩 10 格。
> ⚠ 直流轮次用不到后两行,但**它们一直都在**。交流轮次(判据见 `docs/任务规划/数据可信.md` §4.6)
> 必须用 `meas:` 或 104 YC 比幅值,别用 `phasor(hb)` 行——那行是 ×0.1 定点,注 1 V 只剩 10 格。
`adc:` 行 2026-10-27 新增(`App/task/adc_task.c` 心跳块),此前验 8 通道要复位二十余次,现在**全程不必复位**。
`adc:` 行是 2026-10-27 新增的(`App/task/adc_task.c` 心跳块),此前只有 `cal:` 的三路,
验 8 个通道要复位二十余次。现在**全程不必复位**。
**「码」是什么**:ADC 原始整数,**不是物理量**——电压 = 码 × LSB:
**「码」是什么**:ADC 输出的**原始整数**,中文常写「码值」/「AD 码」,英文对应 raw code / counts。
它表示"多少个最小刻度(LSB)",**本身不是物理量**——电压 = 码 × LSB,而两片转换器的 LSB 不同:
| 转换器 | 位数与极性 | 码范围 | 1 码 | 示例 |
| 转换器 | 位数与极性 | 码的范围 | 1 码 = | 换算示例 |
|---|---|---|---|---|
| TPAFE5160 | 16 位双极性 | −32768~+32767(65536 码 = 10 V) | 152.59 µV | 11430 码 ≈ **1.744 V** |
| STM32 ADC1 | 12 位单极性 | 0~4095(4096 码 = 3.3 V) | 805.66 µV | 1000 码 ≈ **0.806 V** |
| TPAFE5160 | 16 位双极性 | −32768 ~ +32767(共 65536 码 = 10 V 跨度) | 152.59 µV | 11430 码 × 152.59 µV ≈ **1.744 V** |
| STM32 ADC1 | 12 位单极性 | 0 ~ 4095(共 4096 码 = 3.3 V 跨度) | 805.66 µV | 1000 码 × 805.66 µV ≈ **0.806 V** |
![两把刻度尺:TPAFE 16 位双极性 1 码 152.59 µV;STM32 12 位单极性 1 码 805.66 µV](../../images/ADC码值刻度.svg)
同一个整数落在两把刻度尺上代表不同电压(图由 `tools/arch_diagram/gen_adc_code_scale_svg.py`
生成,改图请改脚本重跑,勿手改 SVG):
**两把尺子不能混用**(同样"1000"差 5.3 倍)。§5 各表每格必须写明哪一路(UA/UB/… 属 TPAFE,T 属 STM32),抄错一行全盘作废。本文一律用"码"不用"伏":固件 `adc:` 行打印的就是码;**标定还没做,把码换成伏正是本次要验证的事**。判据:**TPAFE ≤ 30 码、STM32 ≤ 5 码**(≈4.6 mV / ≈4.0 mV)。
![两把刻度尺:TPAFE5160 16 位双极性 −32768~+32767、1 码 152.59 µV、11430 码≈1.744 V;STM32 ADC1 12 位单极性 0~4095、1 码 805.66 µV、1000 码≈0.806 V](../../images/ADC码值刻度.svg)
⚠ **TPAFE 档 2026-10-27 由 ≤10 放宽到 ≤30**:首次实测残差 UA −14 / UN −15 / IA −13(≈2 mV)。原 ≤10 比实测噪声还紧,必然误判。残差若 >30 码 → 前端有真实失调,回前端查,不得再放宽。
**两把尺子不能混用**:同样是"1000",在 TPAFE 上是 0.153 V,在 STM32 上是 0.806 V,差 5.3 倍。
所以 §5 各步的记录表里每一格都必须写明是哪一路(UA/UB/… 属 TPAFE,T 属 STM32),
抄错一行全盘结论作废。
⚠ `adc:` 行某路取数失败时该槽打 0,"9 路全 0"读作"没数据",判据看同一心跳 `frames` 是否还在涨。
本文一律用"码"而不是"伏",有两个理由:① 固件 `adc:` 行打印的就是码,抄数不用心算;
② **标定还没做,把码换成伏正是本次要验证的事**——先换算再用,等于默认了尚未验证的系数。
判据按转换器分两档:**TPAFE 侧 ≤ 30 码、STM32 侧 ≤ 5 码**——即短接后读数离 0 不超过
30 / 5 个刻度,折算 **≈4.6 mV / ≈4.0 mV**。两档数值差 6 倍是因为两种刻度差 5.3 倍
(TPAFE 152.59 µV/码、STM32 805.66 µV/码),统一到物理量后都在 4~5 mV 量级。
⚠ **TPAFE 档于 2026-10-27 由 ≤10 放宽到 ≤30**:首次实测三路残差 UA −14 / UN −15 / IA −13
(≈2 mV),按 16 位 /10 V 跨度算只有 0.02% FS。原来的 ≤10(≈1.5 mV)定判据时**没有实测依据**,
比实测噪声还紧,必然误判失败。**这不是"把判据改成能过"**——若残差继续增大(>30 码 / ≈4.6 mV),
说明前端确有真实失调(R32 侧接地断、TPAFE 失调),那时必须回前端查,不得再放宽。
⚠ `adc:` 行某路取数失败时该槽打 0,所以"9 路全 0"要读作"没数据",判据是同一份心跳里
`frames` 是否还在涨。
## 5. 步骤
@@ -83,17 +129,25 @@
| 量哪里 | 期望 | 验的是什么 |
|---|---|---|
| **串阻两端**(1 脚 ↔ 2 脚) | **≈ 1 kΩ** | 串阻焊好、没虚焊 |
| **注入点 ↔ AGND**(TPAFE 侧 UA/UN/IA) | **≈ 1 MΩ 量级** | 无意外短路;**不是 1 kΩ** |
| **注入点 ↔ AGND**(STM32 侧 UB/UC/IB/IC/IN) | 高阻,不定量 | 无设计直流通路,只作短路排查 |
| **串阻两端**(1 脚 ↔ 2 脚,即端子侧焊盘 ↔ 引脚侧焊盘) | **≈ 1 kΩ** | 串阻焊好、没虚焊 |
| **注入点 ↔ AGND**(TPAFE 侧,UA/UN/IA) | **≈ 1 MΩ 量级** | 无意外短路;**不是 1 kΩ** |
| **注入点 ↔ AGND**(STM32 侧,UB/UC/IB/IC/IN) | 高阻,不定量 | 无设计直流通路,只作短路排查 |
### 5.2 步骤 1:零点(这一关先过:零点都不干净,后面每一步的数都不可信)
### 5.2 步骤 1:零点(这一步是整套测试的分水岭)
**做什么**(8 路逐个,每路约 1 min):断电把该路**注入点短接 AGND**(短接线先 0 Ω 档确认直通)→ 上电等 `adc:` 行 → 抄进下表 → 断电拆线换下一路。
**做什么**(8 路逐个重复,每路约 1 分钟):
**记录**(现场填;判据只看"短接后"列,"空载"列留证):
1. 断电,把该路**注入点短接到 AGND**(短接线之前先用 0 Ω 档确认该点与 AGND 直通);
2. 上电,等 `adc:` 行出现(每 10 s 一行,9 路码值都在里面);
3. 把该路码值抄进**下面的零点记录表**;
4. 断电拆短接线,换下一路。
> ⚠ **本表行序 = `adc:` 日志列序**(UA UB UC UN IA IB IC IN),照日志**从左往右**抄,别按"TPAFE 三路 / STM32 五路"分组。2026-10-27 就栽在这里:按语义分组抄,UN/IA 填成 STM32 ~1000、UB/UC 填成 TPAFE ~11450——量级物理上不可能。**"转换器"列就是给这一眼准备的**。
**记录**(现场填;判据只看"短接后"那一列,"空载"列是留证用):
> ⚠ **本表行序 = `adc:` 日志的列序**(UA UB UC UN IA IB IC IN),照着日志**从左往右**抄,
> 不要按"TPAFE 三路 / STM32 五路"重新分组。2026-10-27 现场实测就栽在这里:按语义分组抄,
> 结果 UN/IA 填成了 STM32 的 ~1000、UB/UC 填成了 TPAFE 的 ~11450——而 UN/IA 属 TPAFE、
> UB/UC 属 STM32,这两个量级在物理上不可能。**"转换器"列就是给这一眼准备的**。
| 通道 | 转换器 | 端子侧节点 | 空载码值(浮空) | 短接 AGND 后码值 | 判据 |
| --- | --- | --- | --- | --- | --- |
@@ -106,15 +160,26 @@
| IC | STM32 | `AIN10`(U5.C9) | 991~1092 | 0 | ≤ 5 |
| IN | STM32 | `AIN11`(U5.C10) | 979~1065 |1~2 | ≤ 5 |
**2026-10-27 实测:8 路全部 PASS。** 短接后 **0~5 码**(TPAFE ≤4.6 mV、STM32 ≤4.0 mV),远小于零点补偿上限 `ADC_CAL_B_MAX`(8192 码),**可正常标定、不触发回退**。
**为什么这一步最关键**:空载(输入浮空)读数比判据高两个数量级,最可能的解释是输入悬空后
漏电流在滤波电容上建立的平衡点——短接 AGND 后应塌回 0。但如果**短接后仍读 11430**,那就是
真实偏置,且超过零点补偿上限 `ADC_CAL_B_MAX`(8192 码)——`adc_cal_set()` 会直接拒掉、
回退 ROM 系数、永远置不了 `VALID`,保护也就永远解不了锁。**那种情况必须回前端查**
(R32 那侧接地是否断、TPAFE 是否失调),不能靠软件补偿。
⚠ **IB 最贴边**:2~5 码正好触上限(1.6~4.0 mV,物理没问题),**八路里余量最小**;日后 IB 零点变大第一个查 R49 那侧(`AIN9` / U5.A8)。
**2026-10-27 实测结论:8 路全部 PASS。** 短接后落在 **0~5 码**(TPAFE ≤4.6 mV、STM32 ≤4.0 mV),
全部远小于零点补偿上限 `ADC_CAL_B_MAX`(8192 码),**可以正常标定、不会触发回退**。
空载 → 短接的塌陷幅度:TPAFE 约 11430 → 0~4(三个数量级),STM32 约 1000 → 0~5(两个数量级),
两者都证明短接真实到位、输入前端没有意外直流通路。
### 5.3 步骤 2:三点线性(用 UA,免复位)
⚠ **IB 最贴边**:2~5 码,上限 5 码正好触到。折算 1.6~4.0 mV,物理上没问题,但**它是八路里
余量最小的一路**;若日后 IB 零点继续变大,第一个该查的是 R49 那侧(`AIN9` / U5.A8)。
3.3 V 轨经 10 k 电位器分压作源,逐点注入,**每点同时记两个数**:万用表读注入点电压、`adc:` 行读码值。
### 5.3 步骤 2:三点线性(用 UA 做,免复位)
**记录**(UA 三点;"目标"列是靶子,"实测"列填万用表):
3.3 V 轨经 10 k 电位器分压作源,逐点注入。**每点同时记两个数**:万用表读注入点实际电压、
`adc:` 行读对应码值。
**记录**(UA 一路,三点;"目标"列是调电位器的靶子,"实测"列填万用表读数):
| 目标注入 | 实测电压(万用表) | 码值 | 反算 µV/码 | TPAFE 期望码 | STM32 期望码 |
| -------- | --------- | -- | ------- | --------- | --------- |
@@ -124,17 +189,26 @@
拟合 R² = 0.9999997 斜率 = 6544.2 码/V 偏差 = −0.14 % 截距 = −7.2 码(≈ −1.1 mV)
反算 LSB = 152.81 µV/码,与 TPAFE 期望 152.59 差 0.14 %——**与斜率偏差同源**。期望码两列并列:同样三点也可复验 STM32 侧某路,两列差 5.3 倍,一眼确认表笔接的是哪片转换器。
反算 LSB = 152.81 µV/码,与 TPAFE 期望 152.59 差 0.14 %——**与斜率偏差同源,是同一个数**。
判据:三点拟合 **R² ≥ 0.999**、斜率偏差 **≤ 2 %**。偏差 > 2 % **先查前端**(焊点、串阻实际值、电位器负载效应),不要用系数凑。
期望码两列并列,是因为同样的三个注入点也可能拿去复验 STM32 侧某一路(UA 对不上时换路交叉验证);
两列数值差 5.3 倍,一眼能确认手上这支表笔当前接的是哪一片转换器。
**2026-10-27 实测:PASS。** R² = 0.9999997、斜率偏差 −0.14 %。
判据:三点线性拟合 **R² ≥ 0.999**,斜率与期望值偏差 **≤ 2 %**。
偏差 > 2 % 时**先查前端**(焊点、串阻实际值、电位器负载效应),不要用系数去凑——
把硬件的错固化成"标定值"是最坏的结果。
**2026-10-27 实测结论:PASS。** R² = 0.9999997(判据 ≥0.999)、斜率偏差 −0.14 %(判据 ≤2 %)。
三条残差 −2.0 / +3.5 / −1.4 码,落在读数噪声内,**无系统性弯曲**——即前端在这三个点上确实是
线性的,不存在未识别的分压元件。这是"③ 段变比 = 1:1"在台架上的第一次直接证据。
### 5.4 步骤 3:极性(只 TPAFE 三路)
TPAFE 双极性补码可注负压;STM32 单极性负半周测不了。UA/UN/IA 各加 **−1.0 V** 点。
TPAFE 是双极性、二进制补码,可以注负压;STM32 是单极性,负半周测不了。给 UA/UN/IA 各加一个
**−1.0 V** 点。
**记录**(三路各两点):
**记录**(三路各两个点):
| 通道 | +1.0 V 码值 | −1.0 V 码值 | 对称性偏差 |
| -- | --------- | --------- | ----- |
@@ -142,26 +216,40 @@ TPAFE 双极性补码可注负压;STM32 单极性负半周测不了。UA/UN/IA
| UN | 6549 | −6568 | 0.29 % |
| IA | 6548 | −6566 | 0.27 % |
判据:−1.0 V 点码值**为负**且落在 **−6554** 附近,与 +1.0 V 对称(差 ≤ 2 %)→ 符号位与补码解释正确。
判据:−1.0 V 点码值为**负**且落在 **−6554** 附近,幅值与 +1.0 V 点对称(差值 ≤ 2 %)
→ 符号位与补码解释正确。这一步专门补"连极性都没证过"这个缺口。
**2026-10-27 实测:PASS。** 三路 −1.0 V 全负,落在 −6566~−6568;对称性 0.27 %~0.34 %,远小于 2 %。
**2026-10-27 实测结论:PASS。** 三路 −1.0 V 点全部为负,落在 −6566~−6568(相对 −6554 偏
−0.18 %~−0.21 %);对称性偏差 0.27 %~0.34 %,远小于 2 % 判据。**补码符号位解释正确**。
### 5.5 步骤 4:节奏(注入期间顺带看)
注入期间 `frames=` 应仍**严格 1000 Hz 递增**(10 s +10000 左右),`idle=0`、`adc1_err=0`。
注入期间 `frames=` 应仍**严格 1000 Hz 递增**(10 s 间隔 +10000 左右),`idle=0`,
`adc1_err=0`。这一条替代了原方案里"用示波器看 U3.14 BUSY 有脉冲"的判据——不需要示波器。
**2026-10-27 实测:PASS。** 注入**前**(20:58)与**后**(21:29)各取两行相隔 10 s 心跳,两边都是 **Δ9998 码 / ≈10.0 s ≈ 1000.1 Hz**,`idle=0`、`adc1_rc=0`、`adc1_err=0`:
**2026-10-27 实测结论:PASS。** 注入**前**(20:58 那次开机)与注入**后**(21:29)各取两行相隔
10 s 的心跳,两边都是 **Δ9998 码 / ≈10.0 s ≈ 1000.1 Hz**,`idle=0`、`adc1_rc=0`、`adc1_err=0`:
| 时点 | `frames=` 两行 | Δ | 折算 |
|---|---|---|---|
| 注入前(20:58) | 29997 → 39995 | 9998 | 1000.5 Hz |
| 注入后(21:29) | 1819639 → 1829637 | 9998 | 1000.1 Hz |
⚠ **口径说明**:这两次是"注入**前后**",不是"注入**进行中**"——已知电压注入验证判据 ⑤ 要的正是"注入前后
`tpafe5160_frame_seq()` 仍严格 1000 Hz 递增",故按此口径已达标。若日后要连"进行中"也钉住,
在任一注入过程里再抓两行即可(注入不改变 TIM3 硬件触发,理论上无影响,但没测过就不算证过)。
`adc1_gap` / `tgap` / `skip` 是**本次开机以来的累计值**(548 / 363 / 183),10 s 内只增 3 / 2 / 1,
占比约 0.03 %,属正常漏检量级,不是停摆。
### 5.6 步骤 5:回写
1. 实测斜率、零点、映射写进 `docs/硬件平台/ADC通道映射说明.md` §8("尚未执行"改实测值 + 日期);
2. 确认无分压后,把 `docs/硬件平台/ADC通道映射说明.md` §7.1 量纲链路的第 3 段(`AINn` 焊盘 ↔ 转换器引脚)改成实测结论;
3. `docs/任务规划/数据可信.md` 已知电压注入行按"完成即删"销项;判据与证据属主 `docs/硬件平台/ADC通道映射说明.md` §8.1。
1. 把实测斜率、零点、映射结果写进 `docs/硬件平台/ADC通道映射说明.md` §8,把"尚未执行"改成
实测值 + 日期;
2. 若实测确认无分压,同步在 `docs/硬件平台/ADC通道映射说明.md` §7.1 的链路分段表把第 ③ 段
(`AINn` ↔ 转换器引脚)从"待台架实测"改为实测结论;
3. `docs/任务规划/数据可信.md` 的已知电压注入验证行按"完成即删"销项;判据与实测证据的属主是
`docs/硬件平台/ADC通道映射说明.md` §8.1。
## 6. 判据汇总
@@ -174,321 +262,438 @@ TPAFE 双极性补码可注负压;STM32 单极性负半周测不了。UA/UN/IA
| 极性 | −1.0 V → −6554 码 | −6566 / −6568 / −6566,对称性 ≤0.34 % | PASS | 补码符号处理错 |
| 节奏 | 注入期间仍 1000 Hz | 注入前/后各 **Δ9998 / 10 s ≈ 1000 Hz**,`idle=0`、`adc1_err=0` | PASS | 注入改变了输入共模,触发转换异常 |
**总体结论(2026-10-27)**:**六项全部 PASS**——结论一:从焊盘到转换器引脚那一段变比 1:1、无隐藏分压;结论二:8 路语义 ↔ 物理端子一一对应;结论三:TPAFE 补码符号位正确。全部闭环(→ `docs/硬件平台/ADC通道映射说明.md` §8.1)。⚠ **两项残留**(不影响 PASS):**串扰**未单独证;**节奏**只测注入前后。
**总体结论(2026-10-27)**:**六项全部 PASS**。合起来证明了三件事:
① ③ 段(`AINn` ↔ 转换器引脚)**变比确为 1:1**,无隐藏分压(斜率偏差 −0.14 %);
② 8 路语义 ↔ 物理端子**一一对应**(零点步骤逐路代偿);
③ TPAFE 补码符号位解释正确(对称性 ≤0.34 %)。
已知电压注入验证的判据 ①②③④⑤⑥ 至此全部闭环(判据与实测记录 → `docs/硬件平台/ADC通道映射说明.md` §8.1)。
⚠ **两项残留口径**(不影响本轮 PASS,但别当成已验证):**串扰**未单独证(见上表"映射"行);
**节奏**只测了注入前后、没测注入进行中。两项若日后怀疑,各自的补做方法已写在对应小节。
## 7. 安全红线
1. **STM32 侧严禁超过 3.3 V**(VDDA + 0.3 V 即击穿),电位器上限接 3.3 V 轨。
2. **TPAFE 侧按判据只到 ±2 V**(芯片容 ±5 V 但判据不需要更大)。
3. **端子侧不许直接上额定电压**(220 V / 100 V 二次值);"加压试保护"另排(`docs/硬件平台/ADC通道映射说明.md` §7.1 ④ 行)。
4. **引脚侧注入必须外串 10 k**,先接线后上电、先断电后拆线。
5. 注入源与板子**共地**;电位器一端 3.3 V、一端 AGND、抽头串 10 k 到注入点。
1. **STM32 侧严禁超过 3.3 V**(VDDA + 0.3 V 即击穿),电位器上限就接 3.3 V 轨,不要接 5 V。
2. **TPAFE 侧按判据只到 ±2 V**,虽芯片容 ±5 V,但判据不需要更大。
3. **端子侧不许直接上额定电压**(220 V / 100 V 二次值)。本次是直流低压注入,
与"加压试保护"是两件事,后者要等端子/子板变比确认(`docs/硬件平台/ADC通道映射说明.md` §7.1 链路分段表 ④ 行)之后单独排。
4. **引脚侧注入必须外串 10 k**,且先接线后上电、先断电后拆线。
5. 注入源与板子**共地**;电位器一端接 3.3 V、一端接 AGND、中间抽头串 10 k 到注入点。
---
## 8. 交流(正弦)动态验证轮次(六轮:幅值 / 波形 / 相位 / 测频 / 削顶 / 谐波)
> **引用写法**:引用判据文档小节一律「路径 + 小节号」紧邻;只写 §8.x 的是本文自己的节。本节只写**怎么跑**(接哪个点、发生器怎么设、读哪行哪个字段、期望值怎么算、结果往哪回写)。
> **判据的属主是 `docs/任务规划/数据可信.md` §4.6.2**(六轮各验什么、过判据是多少),本节只写**怎么跑**:
> 接哪个点、发生器怎么设、读哪一行的哪个字段、期望值怎么算、结果往哪回写。两边于 2026-10-29 对齐;将来
> 若再分叉,按两类裁定——**「验什么、算不算过」从 `docs/任务规划/数据可信.md` §4.6.2,「期望值怎么算、
> 怎么接线怎么跑」从本节**。为什么这一跳不用等标准源、以及它证不到什么,看其 §4.6.1 与 §4.6.5,本节不重抄。
>
> **引用写法**:本节凡引用判据文档的小节,一律写成「文件名 + 小节号」的紧邻形式;只写 §8.x 的是本文自己的节。
![六轮一图速查:注入信号形状 → 接法与设定 → 判据一句话 → 数字从哪儿读;过判据属主在 数据可信.md §4.6.2](../../images/交流六轮速查.svg)
先下图建立全局轮廓——每一轮注什么波形、验什么、读哪个出口,再进 §8.5 逐轮执行卡:
### 8.1 动手前先弄清楚:数字从哪儿读、能读到多细(决定今天判得动哪几轮)
![六轮一图速查:注入信号形状 → 接法与设定 → 判据一句话 → 主读数出口;过判据属主在 数据可信.md §4.6.2](../../images/交流六轮速查.svg)
这轮能读到数字的地方共 5 处——3 处是**串口周期打印的日志行**、1 处是**板子屏幕**、1 处是**触发录波段文件后在 PC 上重算**。分辨率天差地别,各自判得动的轮次也不同,下表逐处列清:
### 8.1 动手前先把出口分辨率算清楚(决定今天能判到哪一档)
| 在哪儿读 | 看到的形态 | 数字精度 | 判得动什么 | 判不动什么(为什么) |
这一轮和直流轮次**不一样:样本出口已经有了**。2026-10-29 故障录波的数据面并入主干,一次触发就是一段
`/wave/WAVE<NN>.BIN`(`NN` = 00~99 轮转号;8 路 × 320 点原始码值 + 封存瞬间的标定与品质快照),PC 侧 `test/host/wave_read.py` 把它还原成
周波数、幅值、**相位**、谐波与削顶判定。所以本轮每条用例都有两个档:**周期行标量档**(只读串口 / 104 / UI,
快,不用 FTP)与**段文件对表档**(多一次触发 + 一次取段 + 跑脚本,判得到位)。各出口的实际分辨率如下——
这张表决定每条用例走哪个档:
| 出口 | 打印处 | 分辨率 | 能判 | 判不动 |
|---|---|---|---|---|
| **串口心跳行** `meas:`(打印处 `App/task/adc_task.c:605`,200 ms 一行) | `meas: seq=.. v=.. ua_mv=.. un_mv=.. ia_mv=.. f=.. fv=..` | **1 mV**(整数毫伏;电气量只发 UA/UN/IA 三路) | 幅值轮、波形轮的幅值 ≤2 % | 相位、谐波(这行没这两个数);其余五路(这行不发) |
| **串口心跳行** `phasor(hb)`(打印处 `App/task/adc_task.c:209`) | `phasor(hb) C0x-…-F1: Re=.. Im=.. rms=.. (x0.1)` | **0.1 V、向下截断**(×0.1 定点) | 有数没数、量级、象限 | ⚠ 相位 ≤2°、谐波 ±2 % 想都别想:注 Vpp 2.828 V(板上 ≈1000 mV)时 `rms` 满打满算 10 格,20 % 谐波只占 2 格 |
| **串口心跳行** `freq(hb)`(打印处 `App/task/adc_task.c:224`) | `freq(hb): f=.. valid=.. (x0.01)` | 0.01 Hz | 测频轮的读数和新鲜度(`valid`) | — |
| **板子屏幕** 主界面→实时数据→交流量(代码 `Drivers/BSP/UI/ui_page_ac.c:47`) | 页 7 行 `Ua/U0/Ia/Ia2/Ia5/Hz/量纲`;量值行 3 位小数、Hz 行 2 位 | **1 mV**(靠人眼看屏 + 拍照留证) | 谐波轮 `Ia2`/`Ia5` 幅值;幅值轮交叉旁证;测频轮 `Hz` 显 `--` | 相位(页面上没有角度) |
| **录波段文件 + PC 重算**:一次触发写一段 `/wave/WAVE<NN>.BIN` 到板 FTP 根目录(`NN` = 00~99 轮转),取回本机用 `test/host/wave_read.py` 算(`dump`/`fft`/`shape`/`plot`,纯标准库;格式属主 `docs/子系统设计/故障录波设计说明.md` §6.2/§6.3,2026-10-29 并入主干) | 段内 8 路 × 320 点原始码 + 封存瞬间的标定与品质快照 | `fft` 浮点重算:基波有效值、相位、2 次与 5 次谐波有效值(mV) | 相位 ≤2°、谐波 ±2 %——**只有这条路判得动**;顺带 PC 独立重算与板上数字互相对表 | 段与发生器**没有绝对相位锁**(触发点固定在段内点 159)→ 只能比同一段内的相对相位;一段 320 ms,不代表长时间稳定 |
| `meas: seq=.. v=.. ua_mv=.. un_mv=.. ia_mv=.. f=.. fv=..` | `App/task/adc_task.c:605` | **1 mV**(整数毫伏,只发 UA/UN/IA) | 幅值轮、波形轮的幅值 ≤2 % | 相位、谐波、其余五路 |
| 104 YC `0x4011`~`0x4018` + `0x401D`(短浮点 + 品质) | `App/protocol/iec104/iec104_pts.c:70` | 3 位有效数字 ≈ 0.01 V | 单个静态工作点的幅值交叉读数、品质 IV | **小步进**(每点 0.5 V / 0.005 A 变化死区)、相位、谐波 |
| `phasor(hb) C0x-…-F1: Re=.. Im=.. rms=.. (x0.1)` | `App/task/adc_task.c:209` | **0.1 V,且向下截断** | 有数没数、量级、象限 | ⚠ **相位轮的 ≤2° 与谐波轮的 ±2 %**:注 1.000 Vrms 时 `rms` 只有 10 格,20 % 的谐波只有 2 格 |
| `freq(hb): f=.. valid=.. (x0.01)` | `App/task/adc_task.c:224` | 0.01 Hz | 测频轮的读数与新鲜度 | — |
| UI 交流量页 `Ua/U0/Ia/Ia2/Ia5`(3 位小数)、`Hz`(2 位) | `Drivers/BSP/UI/ui_page_ac.c:47` | **1 mV**(人工目检 + 拍照留证) | 谐波轮谐波幅值、幅值轮交叉旁证、测频轮的 `Hz` 显 `--` | 相位(页面上没有角度) |
| 录波段文件 `/wave/WAVE<NN>.BIN`(`NN` = 00~99,100 个号轮转;FTP 根下,与 `/log` 平级)+ `test/host/wave_read.py`(`dump` / `fft` / `shape` / `plot`,纯标准库) | **已落地**(2026-10-29 并入 `v2-on-ui`;格式属主 `docs/子系统设计/故障录波设计说明.md` §6.2 与 §6.3) | 一次触发 8 路 × 320 点原始码值;`fft` 给基波 RMS、相位、2 次与 5 次谐波 RMS(mV) | **相位轮的 ≤2° 与谐波轮的 ±2 % 现在判得动**:PC 独立重算 vs 板上读数对表 | 段与发生器**无绝对相位锁定**(触发点在段内点 159)→ 只能比同段内相对相位;320 ms 快照不等于长时间稳定性 |
**六轮用例先定「什么证据才算过」——这张快查表逐轮指定证据档**(背景:2026-10-29 录波段文件能力落地后,判据档位跟着重划过一次,经过见 `docs/子系统设计/故障录波设计说明.md` §11):
**所以每条用例按两个档摆**(出口从"缺样本"变成"有段文件"的经过与口径变更,见 `docs/子系统设计/故障录波设计说明.md` §11;原卡片登记簿 2026-10-30 销项并入该文 §10):
| 证据档 | 哪些轮用 | 过关要求 | 定这个档的原因 |
|---|---|---|---|
| **心跳档**——串口周期行看够,**不用录波** | 幅值、波形、测频、削顶(削顶的接法以 §8.5 削顶卡为准,别按 50 Hz 老接法想当然) | 串口周期行(`meas:` / `freq(hb)` / `rail:` 位图)达判据即算过 | 判据阈值 ≥ 周期行分辨率(±2 % 对 1 mV、位图对 1 bit),录波无增益 |
| **录波档**——**必须录一段波**才算定量 | 相位、谐波 | 面板触发手动录波 → FTP 把段文件取回 → `wave_read.py fft` PC 浮点重算,与板上读数对表达判据 | 板上周期行是 ×0.1 截断定点(上限表「判不动」列),≤2°、±2 % 只有重算才判得动;谐波轮就算拿 UI 的 1 mV 也不行——**屏上那个数本身就是板子 DFT 算出来的答案**,系数表用错行、索引错位这类算术错会原样透传到屏上,必须靠 PC 独立重算当第二把尺子(段文件还顺带核对频位:100 Hz 确落 2 次 bin);**没录段文件只能退回「心跳档」记,定量判据不算过** |
| **改码退路**(默认不走) | 相位抬打印倍数、测频放宽 `FREQ_AMP_MIN` | 仅当录波档不可用(段取不回、PC 跑不了脚本)才考虑;做则重编 + 烧录 + 按 §8.9 全量还原 | 动了固件就不是「零改动验证」了——**动手前必须问用户** |
| 档 | 条目 | 说明 |
|---|---|---|
| **周期行就判得动** | 幅值、波形、测频、削顶四轮(削顶轮的接法要按 §8.5 改,原因写在那张卡里) | 判据在 `meas:` / 104 / `freq(hb)` / `rail:` 上分辨率都够,不取段也能收工 |
| **段文件档才判到位** | 相位轮、谐波轮 | 段文件已落地 → 浮点重算,≤2° 与 ±2 % 都是**定量**判据;周期行退为旁证,只在没取到段时走定性档。触发动作与命令写在 §8.5 的相位卡 / 谐波卡 |
| **要改代码的退路** | 相位轮抬倍数、测频轮 B 档 | 段文件落地后相位轮这条基本不必走;测频轮 B 档(放宽 `FREQ_AMP_MIN`)仍是唯一路子。两者都要重编 + 烧录 + 按 §8.9 还原,**动手前先问用户** |
### 8.2 设备清单与开机前置门
| 项 | 要求 | 为什么 / 怎么核 |
|---|---|---|
| 信号发生器 | 正弦 / 方波 / 三角,**并有 DC 档**;频率可锁 50.000 Hz、也能设 2 Hz;幅度 0~3 Vpp | 只要求"波形干净、幅度稳、频率锁得住",刻度不当基准(`docs/任务规划/数据可信.md` §7 #9)。DC 档给削顶轮的直流削顶档 |
| ⚠ 发生器端接 | 接板子时设**高阻负载**,不按 50 Ω 端接 | 50 Ω 端接而实际高阻 → 面板值与真实电压差 2 倍;台架"差一倍"最常见来源 |
| 万用表 AC 档 | 能读 mV;**记明型号与响应类型**(真有效值 / 均值)——判别法:表身或说明书没印 "True RMS" 就当均值档;拿发生器注一个 Vpp 2.828 V 方波到 AC 档,读数 ≈1.41 V = 真有效值档、≈1.57 V = 均值档(差 11 %,一眼分辨) | 基准就是它;波形轮两种表读数都与基波期望差很多,不记响应类型说不清谁错 |
| 板子 | 产品构建(`test/board/test_config.h` 四 `TEST_SUITE_*` 全注释)+ `/keil-build` 0 错 0 警 + 烧录 `-Rst -Run` + 回读校验 | → `stlink-flash` skill 与 AGENTS.md「构建与验证」 |
| `[adc_cal]` 状态 | **保持未标定**(`eng=0`),别为"读数好看"先写标定值 | 现在 `meas:` / `phasor` 量纲是 **ADC 引脚电压 V**——未标定态的设计本来就不折算(`App/algo/meas.h`);标定一生效换二次值,本节全部期望值作废 |
| 串口 | 全程抓 RX 落盘:`serial-assistant` skill(`open` → `clear` → 跑 → 读日志 / `log --tail`) | 日志文件就是证据本体,§8.10 归档它;**长跑别用 `/serial-boot-test`**——它绑一轮启动语义(静默 8 s、总超时 30 s、`max_boot_count`=1,forbidden 含 `[ERR]`,削顶轮故意造 `[ERR]`),约 135 min 必超时或误判;daemon 写 `state/serial.log`(20 MB 截头保尾 10 MB)→ `docs/测试说明/流程与规范/自动化测试流程.md` 长时间采集 |
| FTP 客户端 | 走 21 端口取段文件(出厂 `[ftp]` 账号,连续 5 次失败锁 60 s) | 相位 / 谐波的定量判据只能拿段文件来算;`/wave/WAVE<NN>.BIN`(00~99 轮转),**取回本机后立刻改名归档**,否则下次触发盖掉 |
| 录波触发路径 | 产品构建唯一**免改代码**路径 = UI 菜单第 26 叶「手动录波」确认页(`Drivers/BSP/UI/ui_page_wave_manual.c` → `App/task/ui_task.c:605`) | 保护启动上升沿也算触发,但保护判据现处**未标定闭锁**(AGENTS.md「现状」),别指望自动来段;bench 自抓在 `DTU_BENCH` 且 `WAVE_BENCH_AUTOCAP` 非 0(默认 0,`App/task/adc_task.c:153`),开它走 §8.9 |
| 信号发生器 | 正弦 / 方波 / 三角,**并有 DC 档**;频率可锁 50.000 Hz、也能设到 2 Hz;幅度 0~3 Vpp | **只要求"波形干净、幅度稳、频率锁得住"**,它的刻度不当基准(`docs/任务规划/数据可信.md` §7 #9)。DC 档是给削顶轮改法 A 用的 |
| ⚠ 发生器输出端接设置 | 接板子时设成**高阻负载**,不要按 50 Ω 端接 | 按 50 Ω 端接而实际接的是高阻板输入 → 面板值与真实电压差 2 倍;这是台架"差一倍"最常见来源 |
| 万用表 AC 档 | 能读到 mV;**记明型号与响应类型**(真 RMS / 均值响应) | 基准就是它;波形轮那一步两种表的读数都与基波期望差很多,不记响应类型就说不清是谁错 |
| 板子 | 产品构建(`test/board/test_config.h` 四个 `TEST_SUITE_*` 全注释)+ `/keil-build` 0 错误 0 警告 + 烧录 `-Rst -Run` 与回读校验 | → `stlink-flash` skill 与 AGENTS.md「构建与验证」 |
| `[adc_cal]` 状态 | **保持未标定**(`eng=0`),别为"读数好看"先写标定值 | 现在 `meas:` / `phasor` 的量纲是 **ADC 引脚电压 V**(`App/algo/meas.h` 文件头的 A-lite 口径);标定一生效就换成二次值,本节全部期望值作废 |
| 串口 | 全程抓 RX 落盘:用 `serial-assistant` skill(`open` → `clear` → 跑 → 读日志文件或 `log --tail`) | 日志文件就是证据本体,§8.10 归档归档的就是它;**长跑别用 `/serial-boot-test`**——它绑定一轮启动语义(静默 8 s、总超时 30 s、`max_boot_count` = 1,且 forbidden 含 `[ERR]`,而削顶轮故意造 `[ERR]`),约 135 min 的 session 必然超时或误判;daemon 全量写 `state/serial.log`(20 MB 上限自动截头保尾 10 MB)→ `docs/测试说明/流程与规范/自动化测试流程.md` 长时间采集一节 |
| 网口(可选) | 要读 104 那一路就插网线、PC 与板同网段(出厂 192.168.31.100),跑现成主站 `test/host/iec104_master.py`(`--ip` / `--port` / `--addr`) | 104 是浮点无量化 + 带品质,是 `meas:` 之外的第二条腿 |
| FTP 客户端 | 走现成 21 端口取段文件(出厂 `[ftp]` 账号,连续 5 次失败锁 60 s) | 段文件是相位 / 谐波轮定量档的样本出口;段文件名 `/wave/WAVE<NN>.BIN`(00~99 轮转),**取完立刻改名归档**,否则下一次触发把它盖掉 |
| 录波触发路径 | 产品构建里唯一**免改代码**的路径 = UI 菜单第 26 叶「手动录波」确认页(`Drivers/BSP/UI/ui_page_wave_manual.c` → `App/task/ui_task.c:605`) | 保护启动上升沿也算一次触发,但保护判据现处**未标定闭锁**态(AGENTS.md「现状」),别指望它自动来段;bench 定时自抓在双层编译门 `DTU_BENCH` 且 `WAVE_BENCH_AUTOCAP` 非 0 之内(默认 0,`App/task/adc_task.c:153`),开它就走 §8.9 的还原判据 |
| 共地 | 发生器 LO/GND 与板 AGND **单点**相连 | 不共地量到的是两地之差;现象见 §8.8 |
**开机前置门**——插上网线、开了串口采集、**还没接发生器**之前,串口日志就该通过下面 8 条;任何一条不过,先回 §5.5 处理,别带着脏链往下跑:
**开机前置门**——接发生器**之前**日志就该长这样,不满足先回 §5 步骤 5.5 查节奏,别往下做:
| # | 看什么 | 正常该是这样 | 为什么容易看错 / 不满足怎么办 |
|---|---|---|---|
| 1 | `adc:` 行 | 10 s 一行;`frames` 在涨;`idle=0`、`adc1_err=0` | **九路数值全 0 不等于"没数据"**(没输入当然读 0,判据是 `frames` 涨不涨,§4 尾注);`frames` 不涨或 `adc1_err≠0` → §5.5 |
| 2 | `meas:` 行 | `v=1`;`seq` 每 10 s 约 +500(200 ms 一条) | `seq` 不涨 = 发布闸门停 → §5.5 |
| 3 | `cal:` 行的 `cl=0xE6` | **开机就该是这个数,不是故障** | 这是来源派生静态掩码:V0.1 硬件上 `UB/UC/IB/IC/IN` 五路本就触底(`docs/任务规划/数据可信.md` §2.6)。非战果非故障,别去"修"它。位序(`App/algo/adc_input.h:38`):bit0~8 = `UA UB UC UN IA IB IC IN TEMP`,`0xE6` = bit1/2/5/6/7 恰好这五路 |
| 4 | **开机与注入的先后** | 先上电 → 在串口看到 `adc: UB` 数值有正常摆动 → **再**开发生器输出 | 顺序反了会埋雷:贴轨锁存要求该通道"此前出现过中带样本"(`App/algo/adc_health.c` `adc_health_rail_watch()`),开机就注大信号,削顶轮的锁存**装不上**,测出假"不锁存" |
| 5 | 开机单发行 | `first:`、`phasor(first)`、`freq(first)` 各只一条(`App/task/adc_task.c:478`/`:201`) | 数心跳周期时把它们剔掉,别算进"每 10 s 一行"里 |
| 6 | `phasor(hb)` 行 | 有数就打出来 | **有数 ≠ 数有效**:这行只查 `phasor_get()==0` 不看 valid 位(`App/task/adc_task.c:209`),判读必须配着 `vl`/`cl`/`rail` 一起看 |
| 7 | `health:` / `sampling fault:` | 不该出现 | 这些位只吃序号差/错误计数/tick 差(`App/algo/adc_health.c:91-189`),注入多大正弦都不该点亮;亮了 = 节拍/丢帧问题 → §8.8 |
| 8 | `wave:` 行(`App/task/adc_task.c:782`) | 基线 `armed=0`、`err=0`、`ovf=0`、`drop=0`;`seg=` 成段数(**0 也正常**,从没触发过就该是 0) | 后面每触发一段核一次:`seg` +1 且那三个计数**不涨**;计数涨了先按 §8.8 分诊再谈读数 |
- `adc:` 行 10 s 一行,同一心跳里 `frames` 在涨("9 路全 0"要读成"没数据",§4 尾注);`idle=0`、`adc1_err=0`;
- `meas:` 的 `v=1`、`seq` 每 10 s 约 +500(200 ms 一帧;不涨 = 发布闸门停);
- `cal:` 的 `cl=0xE6` —— **这是来源派生**(V0.1 那五路本就触底,见 `docs/任务规划/数据可信.md` §2.6),既不是本轮战果也不是本轮故障;
- 四个位图 `vl` / `cl` / `rail` / `edge` 同一个位序(语义通道枚举 `App/algo/adc_input.h:38`):bit0 `UA`、bit1 `UB`、
bit2 `UC`、bit3 `UN`、bit4 `IA`、bit5 `IB`、bit6 `IC`、bit7 `IN`、bit8 `TEMP` → `0xE6` = bit1/2/5/6/7 = `UB/UC/IB/IC/IN`;
- **先上电、在基线里看到 `adc: UB` 有正常摆动,再开发生器输出**:贴轨锁存要求该通道"此前出现过中带样本"(释放沿前提,
`App/algo/adc_health.c` 的 `adc_health_rail_watch()`),开机就注大信号会让削顶轮的锁存**装不上**,测出假不通过。
- 开机各有一条 `first:` 与 `phasor(first)` / `freq(first)` 单发行(`App/task/adc_task.c:478` 与 `App/task/adc_task.c:201`
的 `tag` 参数),与 10 s 一轮的 `hb` 行是两拨——数心跳时别把 `first` 计进去;
- `phasor` 行**只要有数就打**:打印条件只有 `phasor_get() == 0`,不带任何 valid 位(`App/task/adc_task.c:209`)→
「phasor 有数」既不说明标定可用、也不说明这一路没在削顶,判读必须配 `vl` / `cl` / `rail` 三个位图一起看;
- 注入正弦**不会**点亮 `health:` 或 `sampling fault:` 那几行:`adc_health` 的位只吃序号差、错误计数增量与 tick 差
(`App/algo/adc_health.c:91-189`),与波形幅值无关;它们若亮是节拍 / 丢帧问题,别归因给发生器(§8.8 同一条)。
- `wave:` 行(`App/task/adc_task.c:782`)基线:`armed=0`、`err=0`、`ovf=0`、`drop=0`,`seg=` 是本开机以来的成段数(**可以是 0**)。
本轮每触发一段就核 `seg` +1 且那三个计数不涨;涨了先按 §8.8 的落盘行分诊,别接着往上下注入。
### 8.3 接线的四条(与直流轮次比,差别只在第 2、3 条)
1. **夹子接哪儿**:跟直流轮同一个位置——§3 清单的**端子侧**焊盘(`U5` 排针,串阻 1 脚侧)。接这里的好处是**读数与设定值不用换算**:从焊盘到 ADC 引脚这一段是 1:1(直流轮已实测盖章,斜率偏差 −0.14 %,见 §6),端子侧电压就是引脚电压。⚠ 换个位置注(直接进引脚)才需要外串 10 k(§7 第 4 条),照本条接就不用。
2. **幅度一律按峰峰值 Vpp 设定**,别拿面板有效值显示当设定基准——同一个 Vpp 换波形后有效值显示跟着变,切波形瞬间那格还残留上一波形时代的值(2026-11-02 实测就被坑过一轮,见波形轮注)。对表时注意换算:发生器设定 = Vpp;万用表 AC 档与板上读数天然是有效值刻度,正弦下 有效值 = Vpp ÷ 2.828。
1. **注入点与直流轮次完全相同**:§3 清单的**端子侧**焊盘(`U5` 排针,串阻的 1 脚侧)。③ 段 1:1 已由直流轮次实测
(斜率偏差 −0.14 %,§6),所以端子侧电压就是引脚电压,不用再算变比。引脚侧仍须外串 10 k(§7 第 4 条)。
2. **幅度按峰值核,别看面板上的 Vrms**:1.000 Vrms 正弦峰值已 1.414 V。
| 侧 | 判据上限(§7 红线 2) | 允许的最大正弦 Vpp | 本节用量 |
| 侧 | 判据上限(§7 红线 2) | 允许的最大正弦 Vrms | 本节用量 |
|---|---|---|---|
| TPAFE(`UA`/`UN`/`IA`) | 峰值 ±2 V | 4 V | **2.828 V**(= 峰值 ±1.414 V) |
| STM32(`UB`/`UC`/`IB`/`IC`/`IN`) | 0~3.3 V 单极性 | 无正向意义 | 只在削顶轮用:注交流 = **故意触底**,负半周本就钉 0 |
| TPAFE(`UA` / `UN` / `IA`) | ±2 V 峰值 | 1.414 Vrms | **1.000 Vrms**(峰值 1.414 V) |
| STM32(`UB` / `UC` / `IB` / `IC` / `IN`) | 0~3.3 V 单极性 | 无正向意义 | 只在削顶轮用:注交流 = **故意触底**,负半周本就钉 0 |
3. **一次只注一路**(相位轮除外,它要 `UA` 与 `UN` 两路同源),换表笔前先关输出。端子侧串阻 **1 k**(R31/R28/R19/R52…)。STM32 五路负半周经**片内**钳位灌向 VDDA(网表 `docs/硬件平台/V0.1.tel` :228-241 实证:无外置钳位二极管、无隔直电容);Vpp 2.828 V 时峰值电流 ≈ **1.1 mA**(量级估算),**别往上加幅度**——这种注入只允许出现在削顶轮。
4. **电流通道也是注电压**:`IA` 注入点同样是电压进 R19 端子侧。这一轮证"给定引脚电压算得对不对",不是"一次侧 1 A 怎么办"——后者要变比,属 `docs/任务规划/数据可信.md` §5 的端子 / 子板变比确认。
3. **一次只注一路**(相位轮除外,它要 `UA` 与 `UN` 两路同源),换表笔前先关输出。端子侧串阻是 **1 k**(R31 / R28 / R19 / R52 …),
STM32 那五路负半周经**片内**钳位 / ESD 路径灌向 VDDA——网表 `docs/硬件平台/V0.1.tel` :228-241 逐条实证这些节点只有
「电容 .2 + 串阻 .2 + MCU 脚」三件,**无外置钳位二极管、无隔直电容**,所以这条电流走的是芯片内部路径;
1.000 Vrms 时峰值电流约 (1.414 − 0.3) / 1 k ≈ **1.1 mA**(按片内钳位压降 0.3 V 估、仓内无实测,只用来论证量级),
但**别往上加幅度**——这种注入只允许出现在削顶轮这类"故意削顶"的用例里。
4. **电流通道也是注电压**:`IA` 的注入点同样是电压进 R19 端子侧。这一轮证的是"给定引脚电压算得对不对",
不是"一次侧 1 A 怎么办"——后者要变比,属 `docs/任务规划/数据可信.md` §5 的端子 / 子板变比确认。
四条接线画在一张图上(发生器 → 端子侧焊盘是一条主干,万用表并接、共地是两条必须存在的旁路):
```mermaid
flowchart LR
G["信号发生器<br/>输出端接 = 高阻<br/>(按 50 Ω 端接读数差 2 倍)"] -->|"注 UA / UN / IA:端子侧焊盘<br/>(1:1 已由直流轮次实测)"| R31["串阻 1 k 的 1 脚侧<br/>R31 / R28 / R19 …"]
R31 --> FE["前端分压 / 滤波"] --> U5["U5 排针 → 转换器引脚<br/>(TPAFE AIN_x 或 STM32 脚)"]
M["万用表 AC 档<br/>记响应类型"] -.->|"并接同一注入点<br/>量真实加到板上的电压"| R31
G -.->|"LO/GND 与板 AGND<br/>单点共地"| AGND["板 AGND"]
AGND -->|"不共地量到的是两地之差"| U5
G -.->|"注 UB 等 STM32 那五路(仅削顶轮)<br/>负半周经片内钳位 ≈1.1 mA"| R52["R52 端子侧"] --> STM["STM32 ADC 脚<br/>(无外置钳位 / 隔直电容)"]
```
### 8.4 记录表抬头(每条用例一份,与日志一起归档)
这份抬头回答一个问题:**这组数字是在什么条件下量出来的**。缺任何一项,三个月后这页记录就没法复核,§8.10 归档台账的「固件版本」栏也就没有出处。做法只有一件事——照着下表,从右边写的地方**抄**过来,不用你判断任何数:
| 要抄的信息 | 从哪儿抄、为什么它要紧 |
|---|---|
| 日期、操作人 | 王建锋 |
| 板子序列号 + 固件版本 | 串口开机日志里 `fw build:` 开头那一行,**整行照抄**(例:`fw build: Nov 2 2026 14:21:24 suite=NONE dstr=off`)。不抄版本,下次构建一变,这批数就对不上号。 |
| 发生器型号 + 端接 | VICTOR 2026H(端接必须设"高阻",设成 50 Ω 输出会被砍一半,所有期望值作废) |
| 万用表型号 + 响应类型 | FNIRST-DMT-99 + 真有效值档(判别法见 §8.3 设备表;波形轮不写响应类型,说不清 1414/1570 谁对谁错) |
| 标定状态 | 串口 `cal:` 行行尾 `eng=` 后面的数,**必须是 0**(未标定)。是 1 就停下——全部期望表都是按引脚电压写的,标定态下整表不适用。 |
| 基线读数 | **开信号发生器之前**,从串口各抄一行:`meas:` 行的 ua_mv / un_mv / ia_mv,`cal:` 行的 rail / cl。没有这行,分不清"本来就是零"还是"注了没进来"。 |
```text
日期 ____ / 操作人 ____ / 板子序列号 ____
固件 commit ________(/keil-build 0 错误 0 警告:是 / 否)
发生器:型号 ______ 输出端接 = 高阻 / 50 Ω 波形 ____ 频率设定 ____
万用表:型号 ______ AC 响应类型 = 真 RMS / 均值 共地方式 = 单点 / 发生器浮地
注入点:路名 ____(端子侧 U5 / 引脚侧外串 ____ Ω)
[adc_cal] = 未标定(eng=0)—— 必须是这个状态
基线(未注时):meas: ua_mv=____ un_mv=____ ia_mv=____ rail=0x____ cl=0x____
段文件(走定量档的用例):文件名 ________ 触发方式 = 手动 / 保护 / bench 取回时刻 ________
脚本:test/host/wave_read.py(先跑 --selftest:通过 / 否) fft / shape / dump 输出与 plot 图归档在 ________
```
### 8.5 逐条执行卡
**通用动作(六张卡都按这个走)**:
**每条卡的通用动作**:接好 → 开输出 → 等 ≥30 s(相量窗 + `meas` 200 ms 轨稳定)→ 取**连续 ≥10 行**心跳 →
先确认 `frames` 还在涨、`adc1_err=0`,再核数 → 关输出 → 填表。"连续 ≥10 行"不是保守:`meas:` 是
**瞬时基波有效值透传**、200 ms 是上送节拍不是平均窗(`docs/任务规划/数据可信.md` §4.5),
单行读数带拍频式跳动,必须看一片取**中间水平(中位数)**才能定数。
1. 接好线、开发生器输出,等 ≥30 s 读数稳定;
2. 从串口取**连续 ≥10 行**心跳,先确认 `frames` 在涨、`adc1_err=0`(采不到算不得数),再核对卡片要求的读数——数字要取这 ≥10 行的**中位数**,别拿单行(`meas:` 是瞬时基波有效值透传、200 ms 是上送节拍非平均窗,`docs/任务规划/数据可信.md` §4.5,单行带拍频式跳动);
3. 这张卡的判据要录波取证的:抄完数后,从面板第 26 叶「手动录波」触发一段,把录波页屏上显示的文件名(`WAVE<nn>`)抄进该轮记录表;分析前在本机跑一次 `test/host/wave_read.py --selftest`,**不通过,分析结果不算证据**(不赶时间,第 7 步统一取回再做,§8.6);
4. 关掉发生器输出,把数填进该卡记录表。
通用动作主干与两处分岔(采集链没清 → 回头修;偏差没进 → 先复核查再判):
**两种情况先修再测,别往下走**:采集链没清(`frames` 不涨 / `adc1_err≠0`)→ 查 §8.8;偏差没进判据 → 先复查三件事(端接 / 万用表响应类型 / 共地),改完重测,复查仍超才进 §8.8。
```mermaid
flowchart TD
W["接好本路,开机前置门已过(§8.2)"] --> ON["开发生器输出"]
ON --> ST["等 ≥30 s:相量窗与 meas 轨稳定"]
ST --> HB["取连续 ≥10 行心跳"]
HB --> Q1{"frames 在涨、adc1_err = 0 ?"}
Q1 -->|"否:是节拍 / 丢帧问题"| TRI["先查 §8.8 分诊<br/>别归因给波形"]
TRI -.->|"修复后重来"| ST
Q1 -->|是| MED["10 行读数排序<br/>取第 5、6 个的平均 = 中位数"]
MED --> Q2{"与期望的偏差在判据内?"}
Q2 -->|是| OK["记通过"]
Q2 -->|"否"| CHK["先复核查:发生器端接、<br/>万用表响应类型、共地"]
CHK -->|"复查仍超"| BAD["记不通过,进 §8.8 分诊"]
CHK -->|"查出问题改完重测"| ST
OK --> OFF["关输出,填记录表"]
BAD --> OFF
OFF --> NEXT["下一条 / 下一轮"]
```
#### 幅值轮(先做:三档幅值的偏差都在 ±2 % 内)
为什么这一步是"中位数"而不是单行或平均——机理与操作:
一句话:发生器注 50.000 Hz 正弦,**Vpp 2.828 / 1.414 / 0.566 V 三档挨个来**,每档从串口 `meas:` 行读 `ua_mv`,和并接在同一注入点上的万用表读数比——**每档偏差 ≤2 % 就算通过**。
![为什么连取 10 行取中位数:窗滑动让读数拍频式上下跳(左);中位数贴着密集簇,平均被毛刺拖走(右)](../../images/交流定数取中位数.svg)
#### 幅值轮:基波幅值(主用例,先做)
| 项 | 内容 |
|---|---|
| 接法 | 发生器 → `UA`(R31 / `AIN1` / U5.A3)端子侧;万用表 AC 档**并同一注入点**——量真实加到板上的电压,不是面板值 |
| 设定 | 正弦、50.000 Hz,三档挨个来:**Vpp 2.828 V → 1.414 V → 0.566 V**(档位不用拧精确,记万用表实际读数就行) |
| 读哪个数 | `meas: ua_mv=`(1 mV) |
| 通过判据 | 每档 `ua_mv` ≈ 万用表读数 × 1000,偏差 **≤2 %**(与静态轮同档) |
| 设定 | 正弦、50.000 Hz、**1.000 Vrms**(峰值 1.414 V) |
| 主读数 | `meas: ua_mv=`(1 mV) |
| 期望与判据 | `ua_mv` ≈ 万用表读数 × 1000,偏差 **≤2 %**(与静态同档) |
| 交叉读数 | 104 `0x4011`(短浮点)应 ≈ 万用表读数,单位是**引脚电压域**(`eng=0`,不是二次值) |
| 坑 ① | **别用 `phasor(hb)` 比幅值**——×0.1 定点,1 V 只剩 10 格(§4 尾注同一条) |
| 坑 ② | 104 每点 0.5 V(电流 0.005 A)变化死区,生效值在 `App/algo/run_par.c` 的 `[run_par]` 副本:1.000 → 1.100 V **不会**上报变化,那是死区不是坏了;连续扫幅度只看 `meas:` |
| 坑 ③ | **`cal: ua_mv` 与 `meas: ua_mv` 同名不同物**:前者是当拍瞬时码折算(`App/task/adc_task.c:587`),后者是 200 ms 快照里那一周波的基波有效值(`App/task/adc_task.c:605`),50 Hz 下二者可以差很多——`App/task/adc_task.c:595-599` 的原注就是**勿互为判据**,幅值轮只认 `meas:` |
记录表(每行 = 连续 10 行心跳 `ua_mv` 的**中位数**:排序取第 5、6 个平均;不取单行、不取峰值。"序号"是一次重测):
记录表(记录表每一行 = 连续 10 行心跳 `ua_mv` 的**中位数**:排序后取第 5、6 个的平均;
不取单行、不取峰值——单行带拍频跳动,毛刺行会拖偏算术平均。表里"序号"是一次重测):
| 序号 | 万用表 AC (V) | `meas: ua_mv` | 期望 = 万用表 × 1000 | 偏差 % | 结论 |
|---|---|---|---|---|---|
| 1 | 0.998 | 996 | 998 | -0.20 | PASS |
| 2 | 0.499 | 498 | 499 | -0.20 | PASS |
| 3 | 0.248 | 249 | 248 | +0.40 | PASS |
| 1 | | | | | |
| 2 | | | | | |
| 3 | | | | | |
**加做(3 min,非判据)**:0.500 / 0.200 Vrms 两点看是否**等比**——不是验精度,是看有没有隐藏的触底或死区。
0.2 Vrms 那点 104 必然被死区吞掉,只读 `meas:`。
#### 波形轮:读数是否只跟基波走(同峰值改波形)
三个波形都按**峰峰值对齐**:发生器统一设 **Vpp 2.828 V**(峰值 ±1.414 V)。期望的**基波有效值**(全周傅氏基波口径:`App/algo/phasor.c:3` 系数已预乘 √2/N、`fAmSQ` 直接就是有效值;下表 20 点整周期 DFT 值按固件同一个窗算):
按**峰值对齐**(不是按 Vrms 对齐):三者峰值都取 1.414 V,发生器面板 Vrms 分别约 正弦 1.000 V、方波 1.414 V、
三角 0.816 V。期望的**基波有效值**(全周傅氏基波口径:`App/algo/phasor.c:3` 的系数已预乘 √2/N、`fAmSQ` 直接就是
有效值;下表的 20 点整周期 DFT 值就是按固件同一个窗算的):
| 波形 | 基波有效值 / 峰值 | Vpp 2.828 V 时的期望 `ua_mv` | 真有效值 / 峰值(对照) |
| 波形 | 基波有效值 / 峰值 | 峰值 1.414 V 时的期望 `ua_mv` | 真有效值 / 峰值(对照) |
|---|---|---|---|
| 正弦 | 0.707 | 1000 | 0.707 |
| 方波 | 0.904(闭式 4/π/√2 = 0.900) | ≈1278 | 1.000 |
| 三角 | 0.578(闭式 8/π²/√2 = 0.573) | ≈817 | 0.583 |
- 判据:读数 = **基波**期望 ±2 %,**不是**真有效值。方波栏:真有效值表 ≈1414 mV、均值响应表 ≈1570 mV、板上 ≈1278 mV——**正是"读的是基波"的证据,不是 bug**;抬头必须写清万用表响应类型(判据文档同一条)。
- 系数:方波基波 = 4/π × 峰值、三角 = 8/π² × 峰值,除 √2 得基波有效值 → 闭式 **0.900 / 0.573**,20 点 DFT 实算 **0.904 / 0.578**(`docs/任务规划/数据可信.md` §4.6.2 同值,算术以本节为准)。
- 判据:读数 = **基波**期望 ±2 %,**不是**真有效值。方波那栏最直观——真 RMS 表会读 ≈1414 mV、
均值响应表读 ≈1570 mV,而板上读 ≈1278 mV,**这正是"这台仪表读的是基波"的证据,不是 bug**;
所以抬头里必须写清万用表响应类型(判据文档同一条:拿真有效值表去对会以为差了 10 %)。
- 系数的来路:方波基波幅值 = 4/π × 峰值、三角 = 8/π² × 峰值,再除 √2 得基波有效值 → 闭式 **0.900 / 0.573**,
20 点整周期 DFT 实算 **0.904 / 0.578**;`docs/任务规划/数据可信.md` §4.6.2 的"波形轮"行与本节同值,算术以本节为准。
记录表:
| 波形 | 设定 Vpp (V) | 万用表 AC 实测 (V,有效值档) | `ua_mv` | 期望 | 偏差 % | 结论 |
|---|---|---|---|---|---|---|
| 方波 | 2.828 | 1.407 | 1269 | ≈1278 | -0.70 | PASS |
| 三角 | 2.828 | 0.814 | 807 | ≈817 | -1.22 | PASS |
| 波形 | 峰值设定 (V) | 面板 Vrms | 万用表 AC 实测 | `ua_mv` | 期望 | 偏差 % | 结论 |
|---|---|---|---|---|---|---|---|
| 方波 | 1.414 | | | | ≈1278 | | |
| 三角 | 1.414 | | | | ≈817 | | |
#### 相位轮:相位(只许比同片内)
| 项 | 内容 |
|---|---|
| 接法 | `UA`(R31)与 `UN`(R28)**都走 TPAFE**,同幅 Vpp 2.828 V(峰值 ±1.414 V)、同相(发生器单路分两路即天然同相;双通道机型两通道都设 0°) |
| 读数 | `phasor(hb) C02-UA-F1` 与 `C03-UN-F1`,各取 `atan2(Im, Re)` 再比差值 |
| 判据(`docs/任务规划/数据可信.md` §4.6.2) | **录波档**:同段内两路相位差 ≤2°,改 90° 后差值 ≈90°;**心跳档只判跟随**(差值落 80°~100°,容差写记录表) |
| ⚠ 分辨率 | 串口行的 ×0.1 向下截断:Vpp 2.828 V(板上 ≈1000 mV)时 Re/Im 各 ±10 格,**一格 0.1 V** → 角度量化误差最坏 ≈ 8°,**≤2° 串口判不出来** |
| 怎么取证 | ① **录波档(首选,零改代码)**:触发一段,`wave_read.py fft` 重算基波相位,比同段内 `UA`(表序 0)与 `UN`(表序 3)相位差——浮点重算不受 ×0.1 量化限制,≤2° 判得动(动作见下段);② **心跳档**:没录到段文件时退用,同相 ≈0°、改 90° 后差值落 80°~100° 即算跟随;③ **抬倍数退路**:临时 `log_phasors()` 倍数改 ×1000 并改行尾 `(x0.1)`,跑完按 §8.9 还原——录波档可用时基本不必走,要走**先问用户** |
| 红线 | **只许比同片内**:跨转换器无同刻性(V0.1 硬件缺陷)+ STM32 五路另有 1 拍乒乓滞后 → `docs/硬件平台/ADC通道映射说明.md` §7 |
| 接法 | `UA`(R31)与 `UN`(R28)**都走 TPAFE**,同幅 1.000 Vrms、同相——发生器单路输出分两路即天然同相;双通道机型则两通道都设 0° |
| 读数 | `phasor(hb) C02-UA-F1: Re=.. Im=..` 与 `C03-UN-F1: Re=.. Im=..`,各取 `atan2(Im, Re)` 再比差值 |
| 判据(§4.6.2) | **定量档**:同相时同段内两路相位差 ≤2°,把发生器(或 B 通道)改 90° 后差值跟到 ≈90°;**定性档只判跟随**(差值落进 80°~100°,容差写进记录表) |
| ⚠ 分辨率 | 出口只有 ×0.1 且向下截断:1.000 Vrms 时 Re/Im 各只有 ±10 格,**一格就是 0.1 V** → 角度量化误差最坏 ≈ atan(0.1 / 0.707) ≈ 8°,**≤2° 今天用串口判不出来** |
| 三个档 | ① **段文件定量档(首选,零改代码)**:同一次注入触发一段,`test/host/wave_read.py fft` 重算基波相位,比**同段内** `UA`(表序 0)与 `UN`(表序 3)的相位差——浮点重算,不受 ×0.1 量化限制,≤2° 判得动,动作见下面第 2 段;② **定性档**:没取到段时退而用,同相 ≈0°、改 90° 后差值落进 80°~100° 即算跟随,容差写进记录表;③ **抬倍数退路**:临时把 `log_phasors()` 的倍数改成 ×1000 并改掉行尾 `(x0.1)`,跑完按 §8.9 还原——段文件档已落地,这条基本不必走,要走仍**先问用户** |
| 红线 | **只许比同片内**:跨转换器无同刻性(V0.1 硬件缺陷)+ STM32 那五路另有 1 拍乒乓滞后 → `docs/硬件平台/ADC通道映射说明.md` §7 |
记录表(按心跳档抄屏;录了波的轮次直接记脚本输出,归档口径见 §8.10 第 1 条):
记录表(定性档;定量档记脚本输出,归档口径见 §8.10 第 1 条):
| 发生器相位设定 | C02-UA `Re`/`Im` | C03-UN `Re`/`Im` | 两路 `atan2` 之差 | 结论 |
| 发生器相位设定 | C02-UA `Re` / `Im` | C03-UN `Re` / `Im` | 两路 `atan2` 之差 | 结论 |
|---|---|---|---|---|
| 0° / 0° | -8 / -5 | -8 / -5 | 心跳档两路象限全同 → Δ=0.0°(×0.1 判不动 ≤2°);**录波档 WAVE06 段实测 Δφ = 0.07°**(fft:UA 995.79 mV ∠+18.23° / UN 995.70 mV ∠+18.16°) | **PASS**(录波档 ≤2°) |
| 0° / 90° | -2 / -9 | 9 / -2 | atan2:UA -102.53°、UN -12.53° → 差 **+90.0°**,跟随成立(该点未录段文件,心跳档判据 80°~100°) | **PASS**(心跳档) |
| 0° / 0° | | | | |
| 0° / 90° | | | | |
**录波档的具体动作(相位轮首选,零改代码)**
**段文件定量档的具体动作(相位轮首选,零改代码)**
1. `UA`(R31)与 `UN`(R28)两路接同一发生器输出(同相),注 **Vpp 2.828 V / 50.000 Hz**;稳定 ≥30 s 后在 UI「手动录波」确认页触发一次;成段有**秒级**延迟(落盘在 defaultTask,卷空闲后搬段)→ 等 `wave:` 行 `seg=` +1、`armed` 回 0 再取文件;
2. FTP 取 `/wave/WAVE<NN>.BIN`,**取完立刻改名**(100 号轮转会被覆盖);
3. 跑 `& '.venv\Scripts\python.exe' test/host/wave_read.py fft WAVE<NN>.BIN --all`——每个整周波给一行:基波有效值、相位、2 次与 5 次谐波有效值(mV,系数镜像 `App/algo/phasor.c`);
4. 判据:`UA`(`--ch 0`)与 `UN`(`--ch 3`)在**同一周波**内相位差——同相 ≈0°,改 90° 后 ≈90°,容差 ±2°(`docs/任务规划/数据可信.md` §4.6.2);
5. ⚠ 段**绝对**相位与发生器无锁定(触发瞬间落在段内点 159)→ **只能比同段内相对差**,跨段比相位不成立(段内点序 `docs/子系统设计/故障录波设计说明.md` §6.2);
6. 交叉旁证:同一段先 `dump`(逐字段 + 双 CRC16),再 `plot --ch 0`(CRC 不符或零长度**必须拒收**);板上 `phasor(hb)` 象限应与脚本一致(×0.1 只够核象限)。
1. `UA`(R31)与 `UN`(R28)两路接同一发生器输出(同相),注 **1.000 Vrms / 50.000 Hz**;
2. 稳定 ≥30 s 后,在 UI「手动录波」确认页触发一次;成段有**秒级**延迟(落盘在 defaultTask,卷空闲后一次心跳搬完整段)
→ 等串口 `wave:` 行的 `seg=` +1、`armed` 回 0 再去取文件;
3. FTP 取 `/wave/WAVE<NN>.BIN`(本轮那一段的确切号),**取完立刻改名**——100 个号轮转会被覆盖;
4. 主机侧跑:`& '.venv\Scripts\python.exe' test/host/wave_read.py fft WAVE<NN>.BIN --all`
—— 每个整周波给一行:基波 RMS、相位、2 次与 5 次谐波 RMS(单位 mV,系数镜像 `App/algo/phasor.c`);
5. 判据:`UA`(`--ch 0`)与 `UN`(`--ch 3`)在**同一周波**内的相位差——同相注入应 ≈0°,把发生器(或 B 通道)改 90° 后应 ≈90°,
容差 ±2°(`docs/任务规划/数据可信.md` §4.6.2"相位轮"行);
6. ⚠ 段的**绝对**相位与发生器没有锁定关系(触发瞬间固定落在段内点 159),所以**只能比同段内两路的相对差**;
跨段比相位、或拿发生器相位当绝对基准都不成立(段内点序见 `docs/子系统设计/故障录波设计说明.md` §6.2);
7. 交叉旁证:同一段先 `dump`(逐字段 + 双 CRC16,看封存瞬间的标定与品质快照),再 `plot --ch 0`(CRC 不符或零长度它
**必须拒收**而不是画出半张图);板上 `phasor(hb)` 的 `Re` / `Im` 象限应与脚本给的象限一致(×0.1 只够核象限)。
#### 测频轮(现状:测不出频率才是对的)
一句话:给 `UA` 注与幅值轮同样的 Vpp 2.828 V / 50.000 Hz 正弦,看串口 `freq(hb)` 行——**固件现状就不该出数**,出数反而是算错了。
#### 测频轮(默认 A 档 = 判"如实报不可测")
| 项 | 内容 |
|---|---|
| 接法与设定 | 同幅值轮(`UA`,Vpp 2.828 V / 50.000 Hz) |
| 读数 | `freq(hb): f=.. valid=..`;UI 交流量页 `Hz` |
| 期望(现状固件) | **就是不出数**:幅值门 `FREQ_AMP_MIN 50.0f`(`App/algo/freq.h:62`,`App/algo/freq.c:231` 拿它与基波有效值比)在引脚电压域恒不过 → `valid=0`、`f=0`、UI `Hz` 显 `--` |
| 接法与设定 | 同幅值轮(`UA`,1.000 Vrms / 50.000 Hz) |
| 读数 | `freq(hb): f=.. valid=..`;UI 交流量页 `Hz`;104 `0x401D` |
| 期望(A 档 = 现状) | **就是不出数**:幅值门 `FREQ_AMP_MIN 50.0f`(`App/algo/freq.h:62`,`App/algo/freq.c:231` 拿它与基波有效值比)在引脚电压域恒不过 → `valid=0`、`f=0`、UI `Hz` 显 `--`、104 `0x401D` 上送 IV |
| 判据 | **如实报不可测**,而不是报一个看起来对的 50.00。若门限没改却报出 50.00 → 反而是 bug,停下查 `App/algo/freq.c` |
| 另有一档(需先取得同意) | 临时把 `FREQ_AMP_MIN` 由 `50.0f` 降到 `0.5f` → 重编(`/keil-build`,要 build 锁)+ 重烧 → 扫 45.000 / 50.000 / 55.000 Hz 三点,PC 侧判据 0.02 Hz → 跑完**必须还原**,收尾判据见 §8.9。这一档要改固件重烧,动它之前先问过你;不做也不影响上面那条判据成立 |
| 坑 | 非整 50 Hz 时 `meas:` 拍频式跳动属预期 → **扫频读 `freq(hb)` 别读 `meas:`**(`docs/任务规划/数据可信.md` §8 同一条);取源先 `UA`(C02)再退 `IA`(C01)(`App/algo/freq.c:47`)——注 `UA` 走 UA 源;若只注 `IA`,测频跟 IA 走 |
| B 档(要用户先点头) | 临时把 `FREQ_AMP_MIN` 由 `50.0f` 降到 `0.5f` → 重编(`/keil-build`,要 build 锁)+ 重烧 → 扫 45.000 / 50.000 / 55.000 Hz 三点,PC 侧判据 0.02 Hz → 跑完**必须还原**,收尾判据见 §8.9 |
| 坑 | 非整 50 Hz 时 `meas:` 会拍频式跳动,属预期 → **扫频读 `freq(hb)`,别读 `meas:`**(`docs/任务规划/数据可信.md` §8 风险表同一条);测频的取源顺序是先 `UA`(相量实例 C02)再退 `IA`(C01)(`App/algo/freq.c:47`)——本卡注 `UA` 走的就是 UA 源;若哪天只注 `IA`,测频会跟着 IA 走,别以为它锁在 UA |
记录表:
记录表(A 档):
| 频率设定 (Hz) | `freq(hb)` f | valid | UI `Hz` | 结论 |
|---|---|---|---|---|
| 50.000 | 0 | 0 | 未记(期望 `--`,与 fv 同源,无补测价值) | **PASS**:冻结窗口 229 行 `freq(hb)` **全部** `f=0.00 valid=0 src=0`(含 UA 满幅 Vpp 2.828 V 注入时段)——幅值门 `FREQ_AMP_MIN=50.0f`(`App/algo/freq.h:62`、`App/algo/freq.c:231`)拿**电压域有效值**比(`phasor(hb)` rms=9 = ×0.1 V 刻度),引脚域 ≈1 V 恒不过门 → **如实报不可测**,与现状判据逐字一致 |
| 频率设定 (Hz) | `freq(hb)` f | valid | UI `Hz` | 104 `0x401D` 品质 | 结论 |
|---|---|---|---|---|---|
| 50.000 | | | | | |
#### 削顶轮:触底与品质路由(负向用例)
- **判据**(`docs/任务规划/数据可信.md` §4.6.2"削顶轮"行):给 `UB`(R52 / `AIN2` / U5.B7)注 **DC −1.00 V 保持 ≥5 s** 或 **Vpp 2.828 V / 2.0 Hz** 正弦 → 负半周钉下轨 → 连续 20 拍同侧后该路贴轨位锁存、`edge` 起沿。
- ⚠ **50 Hz 同幅正弦造不出锁存,是实现约束不是固件缺功能**:锁存条件"连续 `ADC_RAIL_N_RUN = 20` 拍贴同一侧轨带"(`App/algo/adc_health.h:174`),`adc_health_rail_watch()` 里**任何一拍回中带就清零**;1 kHz 下一个 50 Hz 周波只有 20 点、正半周必落中带 → 连续贴底最长 ≈ 209°/360° × 20 ≈ **11 拍 < 20**。
- **轨带数值**(`App/algo/adc_cal.c:452` `adc_cal_rail_ths()`,TEMP 不监视):STM32 贴底 = 码 ≤ **81**(2 % FS ≈ 65 mV)、贴顶 = 码 ≥ 4013;TPAFE = ±32112 码(≈ ±4.9 V)→ **TPAFE 侧在 ±2 V 判据上限内造不出来**,只有 STM32 五路能做。
- **直流削顶档(首选,零固件改动)**:发生器切 **DC −1.00 V** 注 `UB` 端子侧,保持 ≥5 s。期望:`rail=0x002`、`edge=0x002`、一条 `[ERR] rail: edge=0x2 sticky=0x2 (code-domain, 20-run+release)`(`App/task/adc_task.c:428`)、`adc: UB` 贴 0。撤掉注入后 **sticky 位不随输入恢复而清**(只有 `adc_health_rail_init()` / 复位才清)→ 复位一次记录"是否随复位清",本轮战果之一。
- **工频对照档**:原接法注 50 Hz → 连续贴底最长 ≈ **11 拍 < 20**,**不锁存是预期**,非失败非战果。
- **2 Hz 低频削顶档**:发生器 Vpp 2.828 V 但**频率设 2.0 Hz** → 负半周 250 ms = 250 拍贴底 ≥ 20 → 锁存;顺带证"贴轨检测与工频无关"。
- **反向假阳性**:注 Vpp <130 mV(峰值 <65 mV)的 50 Hz 小信号会让 `UB` 贴底**锁存**——"信号太小进不了中带",不是削顶。读 `rail` 时一并排除。
- **与 `cl` 区分**:`cl=0xE6` 是**来源派生**静态贴底掩码(`docs/任务规划/数据可信.md` §2.6),削顶轮要动态 `rail=`/`edge=`;别把 `cl` 当本轮战果。
- **判据**(`docs/任务规划/数据可信.md` §4.6.2"削顶轮"行):给 `UB`(R52 / `AIN2` / U5.B7)注 **DC −1.00 V 保持 ≥5 s**
或 **1.000 Vrms / 2.0 Hz** 的正弦 → 负半周钉下轨 → 连续 20 拍同侧后该路贴轨位锁存、`edge` 起沿、104 该路 IV 且值清零。
- ⚠ **注 50 Hz 同幅正弦造不出这个锁存,这是实现约束不是固件缺功能**:锁存条件是"连续 `ADC_RAIL_N_RUN = 20` 拍
贴同一侧轨带"(`App/algo/adc_health.h:174`),而 `adc_health_rail_watch()` 里**任何一拍回到中带就把两侧计数清零**;
1 kHz 下一个 50 Hz 周波只有 20 个点、正半周必然落在中带 → 连续贴底最长 ≈ 209°/360° × 20 ≈ **11 拍 < 20**。
- **轨带数值**(`App/algo/adc_cal.c:452` 的 `adc_cal_rail_ths()`,TEMP 不监视):STM32 侧贴底 = 码 ≤ **81**(2 % FS ≈ 65 mV)、
贴顶 = 码 ≥ 4013;TPAFE 侧 = ±32112 码(≈ ±4.9 V)→ **TPAFE 侧这一条在 ±2 V 判据上限内根本造不出来**,
只有 STM32 那五路能做。
- **改法 A(首选,零固件改动)**:发生器切 **DC 输出 −1.00 V** 注 `UB` 端子侧,保持 ≥5 s。期望:`cal:` 的 `rail=0x002`、
`edge=0x002`、一条 `[ERR] rail: edge=0x2 sticky=0x2 (code-domain, 20-run+release)`(`App/task/adc_task.c:428`)、
104 `0x4012` 该路 IV 且值清零、`adc: UB` 码值贴 0。撤掉注入后 **sticky 位不随输入恢复而清**(锁存语义,
只有 `adc_health_rail_init()` / 复位清)→ 复位一次并把"是否随复位清"记进表:这条实测行为是本轮的产出之一。
- **改法 B**:发生器 1.000 Vrms 但**频率设 2.0 Hz** → 负半周 250 ms = 250 拍贴底 ≥ 20 → 锁存;
顺带证"贴轨检测与工频无关"这条口径。
- **50 Hz 档照跑,判据就是"不锁存"**:记下 `adc: UB` 负半周钉 0 而 `rail` 仍 `0x000` —— 这一档证的是
"锁存要连续 20 拍同侧"这条实现约束本身,不是失败,也不是战果。
- **反向假阳性**:反过来注一个峰值 <65 mV(<46 mVrms)的 50 Hz 小信号,会让 `UB` 一路贴底而**锁存**——
那是"信号小到进不了中带",不是削顶。读 `rail` 时要把这条一起排除。
- **与 `cl` 区分**:`cl=0xE6` 是**来源派生**的静态触底掩码(`docs/任务规划/数据可信.md` §2.6),削顶轮要的是动态
`rail=` / `edge=`;别拿 `cl` 当本轮战果。
记录表:
| 步 | 设定 | `rail` | `edge` | `[ERR] rail:` 行数 | 复位后 `rail` | 结论 |
|---|---|---|---|---|---|---|
| 1 | 50.000 Hz / Vpp 2.828 V 正弦 | 0x000 | 0x000 | 0 | 未测 | ✓ 不锁存(正弦连续贴底 ≈11 拍 < 20,本步证"连续 20 拍"约束本身,非战果非失败) |
| 2 | DC −1.00 V ≥5 s(直流削顶档) | — | — | — | — | **本轮未做**:冻结窗 16:25–17:03 段日志 `rail` 恒 0x000、零 `[ERR]`,与"未做"一致;16:25 前日志已被 daemon 截头,无法反证。补做与否待排 |
| 3 | 2.0 Hz / Vpp 2.828 V(低频削顶档) | 0x000 | 0x000 | 0 | 未记 | ⚠ **待确认注入哪一路**:注 `UB` → 期望锁存 `rail=0x002`,填 0x000 即 FAIL 需重测;注 `UA`(对照点,`docs/任务规划/数据可信.md` §4.6.2)→ 0x000 = 期望(TPAFE 侧轨带在 ±2 V 判据上限内造不出,不会锁存) |
| 步 | 设定 | `rail` | `edge` | `[ERR] rail:` 行数 | 104 `0x4012` | 复位后 `rail` | 结论 |
|---|---|---|---|---|---|---|---|
| 1 | 50.000 Hz / 1.000 Vrms 正弦 | | | | | 不适用 | |
| 2 | DC −1.00 V(改法 A) | | | | | | |
| 3 | 2.0 Hz / 1.000 Vrms(改法 B) | | | | | | |
**段文件档的两个量具("来源派生 vs 实测"一次分清)**
**段文件档的两把尺子(现在能把"来源派生 vs 实测"一次核清)**
`shape` 拿**实测**贴底与段头 `clip_mask` 对表;段头 bit **来源派生、从不实测**(`test/host/wave_read.py` 文件头与 `cl=0xE6` 同源互证)→ 三种注入各配一个量具:
`shape` 拿**实测**贴轨判定与段头 `clip_mask` 对表;而段头那一位**来自通道来源、从不实测**(`test/host/wave_read.py`
文件头的实证与本卡上面的 `cl=0xE6` 同源,两处互证)→ 三种注入分别配三把尺子:
| 注入 | 命令 | 期望 |
|---|---|---|
| 直流削顶档(`UB` DC −1.00 V ≥5 s) | `shape WAVE<NN>.BIN --strict` | 实测贴底落 bit1;段头已含 bit1 → 等式成立。**实测冒出段头没有的 bit** = 失败,先查接线 |
| 极小注入 / 非驱动通道 | 同上 + `--clip-superset` | "段头覆盖实测":容"封存了但没贴底";实测有、段头没有仍判失败 |
| 原接法(50 Hz 注 `UB`) | `shape WAVE<NN>.BIN` | 只读贴底 run:正弦过零必回中带,`--min-run` 默认 4 判不出——**别把"判不出"读成 bug** |
| 改法 A(`UB` 注 DC −1.00 V 保持 ≥5 s) | `shape WAVE<NN>.BIN --strict` | 实测贴底落进 bit1;段头静态已含 bit1 → 等式成立。**实测冒出段头没有的位**才算失败 = 有别的路被推到轨上,先查接线 |
| 极小注入 / 未驱动通道 | 同上再加 `--clip-superset` | 换成"段头覆盖实测"这把尺子:容忍"封存了但本轮没贴轨";实测有、段头没有仍然失败 |
| 原接法(50 Hz 正弦注 `UB`) | `shape WAVE<NN>.BIN`(不带 `--strict`) | 只读逐通道贴轨游程:正弦过零必回中带,`--min-run` 默认 4 也可能判不出贴轨——**别把"没判"读成 bug** |
`--min-run` 与固件"连续 20 拍"是**两个量具**,别互换;固件侧结论以 `rail:`/`edge=` 为准(本卡上半)。
`--min-run`(连续几点算贴轨)与固件的"连续 20 拍"是**两把不同的尺子**,别互相换算;固件侧结论仍以 `rail:` / `edge=`
位图为准(本卡上半段)。
#### 谐波轮:谐波卡(录波档 fft + UI 目检旁证)
#### 谐波轮:谐波轨(**段文件 `fft` 定量档 + UI 目检旁证**)
| 项 | 内容 |
|---|---|
| 接法 | 发生器 → `IA`(R19 / `AIN8` / U5.B5)端子侧 |
| 设定 | 基波 + 20 % 二次 + 20 % 五次(发生器有谐波叠加 / ARB 模式就用它) |
| ⚠ 峰值预算 | 基波 Vpp 2.828 V + 2 个 20 % 分量,最坏对齐峰值 ≈ 1.414 + 0.283 + 0.283 = **1.98 V,已顶到 ±2 V 判据上限**。所以基波改 **Vpp 2.263 V**、二次与五次各 Vpp 0.453 V,最坏峰值 ≈ 1.58 V 留余量;期望值随之换算 |
| 读哪个数 | UI 交流量页 `Ia2`/`Ia5`(3 位小数)——**1 mV**(人工目检 + 拍照留证,`Drivers/BSP/UI/ui_page_ac.c:47`);谐波幅值、交叉旁证 |
| 期望与判据 | 板上读数(有效值刻度)≈ 各分量 0.160 V(对应设定 Vpp 0.453 V)±2 % |
| 旁证 | `phasor(hb) C04-IA-F2`/`C05-IA-F5` 的 `rms=`:×0.1 只 1~2 格,**只能证"该轨非恒 0"**,判不了 ±2 %(§8.1) |
| 无谐波叠加功能时 | 三次单注:只注 Vpp 0.453 V / 100 Hz → `Ia2` 有数(读数 ≈0.160 V)、`Ia5` 应 ≈0;只注 Vpp 0.453 V / 250 Hz → `Ia5` 有数、`Ia2` 应 ≈0;再注纯基波 Vpp 2.263 V → 两轨都 ≈0。这一档证"两轨各认各的次",**不证叠加工况**,结论里写清 |
| 口径 | 这两轨**当前无下游消费者**(`App/algo/prot_elm.c` 无引用、`meas` 只算基波)→ 谐波轮只证"算得对",不证"有人用";这两轨归谁用,见 `docs/任务规划/数据可信.md` §9 谐波轨立项 |
| ⚠ 峰值预算 | 1.000 Vrms 基波 + 两个 20 % 分量的最坏对齐峰值 ≈ 1.414 + 0.283 + 0.283 = **1.98 V,已顶到 ±2 V 判据上限**。所以基波改 **0.800 Vrms**、二次与五次各 0.160 Vrms,最坏峰值 ≈ 1.58 V 留余量;期望值随之换算 |
| 主读数 | UI 交流量页 `Ia2` / `Ia5` 两行——**3 位小数 = 1 mV**(`Drivers/BSP/UI/ui_page_ac.c:47`),目检 + 拍照;远端可走 7003 看图截屏 |
| 期望与判据 | 期望 ≈ 各分量注入值(0.160 Vrms)±2 % |
| 旁证 | `phasor(hb) C04-IA-F2` / `C05-IA-F5` 的 `rms=`:×0.1 只有 1~2 格,**只能证明这根轨不是恒 0**,判不了 ±2 %(§8.1) |
| 没有谐波叠加功能时 | 分三次单注:只注 0.160 Vrms / 100 Hz → `Ia2` 有数、`Ia5` 应 ≈0;只注 0.160 Vrms / 250 Hz → `Ia5` 有数、`Ia2` 应 ≈0;再注纯基波 → 两行都 ≈0。这一档证的是"两根轨各认各的次",**不证叠加工况**,结论里要写明 |
| 口径 | 这两根轨**目前没有任何下游消费者**(`App/algo/prot_elm.c` 无引用、`meas` 只算基波)→ 谐波轮只证"算得对",不证"有人用";出口归属 `docs/任务规划/数据可信.md` §9 的谐波轨立项 |
记录表:
| 注入分量 | 发生器设定 | UI 读数 (V) | 期望 (V) | 偏差 % | `phasor` 旁证 | 结论 |
|---|---|---|---|---|---|---|
| A 只注二次 | 100.00 Hz / Vpp 0.453 V | `Ia=0` / `Ia2=0.158` / `Ia5=0` | 0.160 / ≈0 | **−1.25 %** | —(A/B 步落在 18:22–18:28 日志盲区,心跳旁证未留) | **过**(±2 % 内;选择性成立——100 Hz 没漏进基波 / 五次桶) |
| B 只注五次 | 250.00 Hz / Vpp 0.453 V | `Ia=0` / `Ia2=0` / `Ia5=0.152` | ≈0 / 0.160 | **−5.0 %**(超 ±2 %) | —(同上盲区) | **待定分诊**:选择性成立(基波 / 二次桶 = 0),但 250 Hz 幅值超差——按注"五次超差分诊"复核查再改本行 |
| C 纯基波 | 50.00 Hz / Vpp 0.453 V(实际按 0.453 注,Vpp 2.263 那档未做) | `Ia=0.159` / `Ia2` `Ia5` 未抄屏 | ≈0 / ≈0 | 基波 **−0.6 %**(对 0.160 读数) | **有**:18:33:43 起串口可见窗 `C01 rms=1 / C04 rms=0 / C05 rms=0`,与 C 步工况吻合 | **过**(两谐波轨零值经串口旁证补认;基波 −0.6 % 与幅值轮同档一致) |
| 叠加(原行,**本轮不做**) | 基波 + 二次 + 五次同段 | — | — | — | — | 发生器无谐波叠加 / ARB 功能 → 本轮不做·设备缺口·TC-PHASOR-006 单测覆盖(`test/host/phasor/phasor_test.c`,3 条谱线各 ±1 %,实跑 57/57 PASSED) |
| 基波 0.800 Vrms + 二次 0.160 + 五次 0.160 | | `Ia2=` / `Ia5=` | 0.160 / 0.160 | | | |
> **2026-11-02 本轮执行档注**:发生器无叠加功能,按本卡"无谐波叠加功能时"行走**三注替代档**(用户裁定)。每注稳定后读 UI `Ia2`/`Ia5`(3 位小数 = 1 mV,判据 ±2 % 只作用于 0.160 那一行),并各触发一段录波取回跑 `fft WAVE<NN>.BIN --ch 4 --all`:A 的二次行应 ≈160 mV、五次行 ≈0;B 相反;C 两谐波行 ≈0。单注峰值 ≈0.226 V(0.800 基波 ≈1.13 V),远离 ±2 V 判据上限,无贴轨风险。
**段文件定量档(谐波现在判得到位)**
> **五次超差分诊注**(B 行 −5.0 %,2026-11-02):最可能位置在**发生器本身**——多数函数发生器在 250 Hz 段带载(高阻)输出幅值会 sag,5 % 量级完全在其规格影内;板端 50 Hz 幅值轮三点 −0.2 % ~ +0.4 % 只锚定 50 Hz 频响,**证不到 250 Hz**。一刀分两边:**B 步工况用万用表(真有效值档)直接量发生器输出端**——表读 ≈0.152 → 发生器 sag、板端忠实,B 行改判"过(按实际注入基准)"并在设定列补记实测;表读 ≈0.160 → 板端频响嫌疑,登记 IA 前端 250 Hz 频响专项(R19 通道滤波网表核算),不再归因给仪表。同段顺带触发一段录波,`fft --ch 4 --all` 的五次行与 UI `Ia5` 互核(同链复核查,非独立证据,只排除显示档笔误)。
**录波档动作(谐波现在判得到位)**
同一次注入(叠加档设想值:基波 Vpp 2.263 V + 二次与五次各 Vpp 0.453 V,峰值预算见上表;本轮发生器无叠加功能、未跑此档)触发一段,FTP 取回跑:
同一次注入(基波 0.800 Vrms + 二次 0.160 + 五次 0.160,峰值预算见上表)触发一段、FTP 取回后跑:
```text
& '.venv\Scripts\python.exe' test/host/wave_read.py fft WAVE<NN>.BIN --ch 4 --all
```
`fft` 每整周波给一行:**基波有效值 + 2 次与 5 次谐波有效值**(mV,浮点重算),±2 % 是定量判据;`--ch` = 段内通道表序,`IA` = 4(`UA UB UC UN IA IB IC IN`,TEMP 不录,`App/algo/wave_ring.h:216`)。UI 目检退**旁证**(证"板上与脚本算的是同一个东西"),不是替代判据。THD 与根均方不在本轮(`docs/任务规划/数据可信.md` §4.6.5)。
`fft` 逐整周波给**基波 RMS + 2 次与 5 次谐波 RMS**(mV,浮点重算),±2 % 在这条档是定量判据;`--ch` 是段内通道表序,
`IA` = 4(表序 `UA UB UC UN IA IB IC IN`,TEMP 不录,`App/algo/wave_ring.h:216`)。UI 目检与 7003 截图降为**旁证**——
它证的是"板上 `Ia2` / `Ia5` 与脚本算的是同一件事"(差值应在显示位数之内),不是替代判据。THD 与方和根仍不在本轮,
口径见 `docs/任务规划/数据可信.md` §4.6.5。
### 8.6 顺序与耗时(一个台架轮次跑完)
### 8.6 顺序与耗时(一次台架 session 跑完)
| 序 | 做什么 | 估算 | 注 |
| 序 | 做什么 | 预计 | 备注 |
|---|---|---|---|
| 0 | 前置门 + 基线 30 s | 10 min | §8.2,不满足先停 |
| 1 | 幅值轮(三档幅值) | 15 min | 先做 |
| 2 | 波形轮(方波 / 三角,峰值对齐) | 15 min | 记万用表响应类型 |
| 3 | 相位轮:段文件相对相位(`UA`/`UN` 同源,再改 90°) | 20 min | 没录到段文件时退心跳档 |
| 4 | 测频轮(判"如实报不可测") | 5 min | 现状就该不出数 |
| 5 | 削顶轮:50 Hz 对照 + 直流档 + 2 Hz 档 + 复位复核 | 30 min | **单独日志**,§8.7 |
| 6 | 谐波轮:段文件 `fft` + UI 旁证 | 25 min | §8.5 谐波卡 |
| 7 | 段文件取回 + 脚本归档(FTP + `dump`/`fft`/`shape`/`plot`) | 15 min | 取完立刻改名,100 号轮转 |
| — | 改固件的档:测频门限降到 `0.5f` / 相位串口行抬十倍读数分辨率 | +40 min | **这两档要改代码、重烧固件,动手前一定先问过用户**;各含重编 + 烧录 + 还原 |
| 0 | 前置门 + 基线 30 s | 10 min | §8.2,不满足就停 |
| 1 | 幅值轮(含 0.5 / 0.2 Vrms 加做) | 15 min | 主用例 |
| 2 | 波形轮(方波 / 三角,峰值对齐) | 15 min | 抬头里记万用表响应类型 |
| 3 | 谐波轮:段文件 `fft` 定量档 + UI 目检旁证(同一次注入触发一段) | 25 min | 命令见 §8.5 的谐波卡 |
| 4 | 相位轮:段文件相对相位档(`UA` / `UN` 同源,再移 90°) | 20 min | 定性档只在没取到段时退用 |
| 5 | 测频轮 A 档 | 5 min | 判"如实报不可测" |
| 6 | 削顶轮:50 Hz 档 + 改法 A + 改法 B + 复位复观(每档各触发一段) | 30 min | **单独一份日志**,判读规则见 §8.7 |
| 7 | 同一份接线下把 104 交叉读数复跑一遍 | 15 min | `test/host/iec104_master.py` |
| 8 | 取段与脚本产物归档(FTP `get` + `dump` / `fft` / `shape` / `plot`) | 15 min | 每段取完立刻改名,100 号轮转会覆盖 |
| — | (需用户点头)测频轮 B 档 / 相位轮抬倍数档 | +40 min | 各含一次重编、烧录与还原;段文件落地后相位轮这条基本不必走 |
合计约 **135 min**(= 上表各行相加;不含"需用户点头"两档)。各步含换线/改设定/抄表的间隙,前后一步可留点重叠,实际往往用不满。段文件取回与脚本跑集中在第 7 步统一做,中间各步只"触发 + `seg=` +1",避免反复插拔网线。
合计约 **135 min**(不含"需用户点头"那两档)。取段与跑脚本集中在末尾第 8 行统一做,中间各条只负责"触发 + 让
`seg=` +1",免得一次 session 里反复插拔网线。
### 8.7 `[ERR]` 判读与 `/serial-boot-test` forbidden 冲突说明(削顶轮单独一份日志)
### 8.7 `[ERR]` 的判读规则(削顶轮单独跑、单独归档)
- 削顶轮**故意造** `[ERR] rail: edge=… sticky=… (code-domain, 20-run+release)`,而 `/serial-boot-test` forbidden 含 `[ERR]`(AGENTS.md「构建与验证」)→ **别拿 `/serial-boot-test` 标记校验当结论**:改人工判读,归档写清"预期 [ERR]:贴轨锁存负向用例"(`docs/任务规划/数据可信.md` §8 风险表同一条)。
- 除这一条 `[ERR] rail:`,**任何 `[ERR]` 都是失败**;四个 Fault 崩溃日志也以 `[ERR]` 开头 → 出现 Fault 即本次不通过(`docs/子系统设计/看门狗设计说明.md` §10),别接着注入。
- 每次开机 `rail:` 沿只打一条、`SYSLOG` 只记一次(设计不是漏报);想看第二条沿就复位一次。
- 削顶轮会**故意造出** `[ERR] rail: edge=… sticky=… (code-domain, 20-run+release)`,而 `/serial-boot-test` 的 forbidden 标记含
`[ERR]`(AGENTS.md「构建与验证」)→ **这一轮不要把 `/serial-boot-test` 的标记校验当结论**:改人工判读,并在归档文件名与
抬头里写明「预期 [ERR]:贴轨锁存负向用例」;§8.6 已把它排在最后单独一轮(`docs/任务规划/数据可信.md` §8 风险表「削顶负向用例被当成固件 bug」行同一条)。
- 除这一条 `[ERR] rail:` 之外,**任何 `[ERR]` 都是不通过**;四个 Fault 的崩溃日志也以 `[ERR]` 开头 →
出现 Fault 立即停线取现场(`docs/子系统设计/看门狗设计说明.md` §10),别再继续注。
- 每次开机 `rail:` 边沿只打一条、`SYSLOG` 也只留痕一次("adc code rail fault" 进 `selftest.log`)——这是设计不是漏报;
要看第二次边沿就复位一次。
### 8.8 失败分诊(先查这一列,别急着改固件)
| 现象 | 先查 | 依据 |
|---|---|---|
| 与万用表差约 2 倍 | 发生器输出端接设成 50 Ω | §8.2 表第 2 行 |
| 差约 5.3 倍,或数带 6553.6 / 1241.2 痕迹 | 拿错刻度尺,或把 `adc:` 码值当 mV 读 | §2 两把尺子、§4「码是什么」 |
| `phasor` 行 `rms` 只 10 格量级 | 正常(×0.1),不是 bug | §4 尾注、§8.1 |
| 非整 50 Hz 时 `meas:` 拍频式跳动 | 预期:瞬时透传、200 ms 不是平均窗 → 改读 `freq(hb)` | `docs/任务规划/数据可信.md` §8 风险表 |
| 50 Hz 正弦注 `UB` 但 `rail` 不锁存 | **预期**(连续贴底不足 20 拍)→ 改用直流档或 2 Hz 档重注 | §8.5 削顶卡 |
| 改 DC 也不锁存 | 释放沿前提没满足:开机后该路从未出现中带样本 → 先建基线再注 | `App/algo/adc_health.c`、§8.2 前置门末条 |
| 撤掉注入后 `rail` 仍非 0 | 锁存语义(sticky),只 `adc_health_rail_init()` / 复位才清 → 复位后仍不清**才是** bug | §8.5 削顶卡直流削顶档 |
| 读数带与幅值无关的偏移或漂移 | 共地 / 地环 | §8.2 共地行 |
| `meas: seq` 不涨 | 发布闸门停 → 先查采集链,别判波形 | §8.2 前置门 |
| `health:` 或 `sampling fault:` 亮 | 与注入波形无关:`adc_health` 的位只吃序号差、错误计数增量与 tick 差(`App/algo/adc_health.c:91-189`)→ 查节拍 / 丢帧,别查发生器 | §8.2 前置门 |
| 9 路全 0 | 读作"没数据",不是"输入零" | §4 尾注 |
| `wave_read.py` 报错退出 / 拒画 | **这是正确行为**:CRC 不符或零长度必须拒收而不是画出半张图 → 取回一段、查是不是传输半品;仍拒 `dump` 看双 CRC16 哪个不符 | `test/host/wave_read.py` 文件头 |
| `shape --strict` 说实测与段头不等 | 先分量具:段头 `CLIPPED` bit 是**来源派生、从不实测**;DC 偏移 / 极小注入 / 非驱动通道该换 `--clip-superset` | §8.5 削顶卡段文件档 |
| 触发后 `seg=` 不涨或 `drop`/`ovf` 涨 | 落盘路径在 defaultTask、卷空闲后才搬段,先等几秒;仍不涨查 `wave:` 行三个计数与 `/wave` 目录可写(`[ftp]` 写权限) | `App/task/adc_task.c:782` |
| 与万用表差约 2 倍 | 发生器输出端接设成了 50 Ω | §8.2 表第 2 行 |
| 差约 5.3 倍,或数字带 6553.6 / 1241.2 的痕迹 | 拿错刻度,或把 `adc:` 的码值当 mV 读 | §2 两把尺子、§4「码是什么」 |
| `phasor` 行 `rms` 只有 10 格量级 | 正常(×0.1),不是 bug | §4 尾注、§8.1 |
| 非整 50 Hz 时 `meas:` 来回跳 | 预期:瞬时透传、200 ms 不是平均窗 → 改读 `freq(hb)` | `docs/任务规划/数据可信.md` §8 风险表 |
| 104 读数不动 | 每点 0.5 V / 0.005 A 变化死区(生效值 `App/algo/run_par.c`) | §8.5 的幅值卡坑 ② |
| 50 Hz 正弦注 `UB` 但 `rail` 不锁存 | **预期**(连续贴底不足 20 拍)→ 改法 A / B | §8.5 的削顶卡 |
| 改 DC 也不锁存 | 释放沿前提没满足:开机后该通道没出现过中带样本 → 先建基线再注入 | `App/algo/adc_health.c`、§8.2 前置门末条 |
| 撤掉注入后 `rail` 仍非 0 | 锁存语义(sticky),只 `adc_health_rail_init()` / 复位清 → 复位后仍不清**才是** bug | §8.5 的削顶卡改法 A |
| 读数带与幅度无关的偏置或漂移 | 共地 / 地环 | §8.2 末行 |
| `meas: seq` 不涨 | 发布闸门停 → 先查采集链,别判交流 | §8.2 前置门 |
| `health:` 或 `sampling fault:` 亮 | 与注入的波形无关:`adc_health` 的位只吃序号差、错误计数增量与 tick 差(`App/algo/adc_health.c:91-189`)→ 查节拍 / 丢帧,别查发生器 | §8.2 前置门 |
| 9 路全 0 | 读作"没数据",不是"输入为零" | §4 尾注 |
| `wave_read.py` 报错退出 / 拒绝出图 | **这是正确行为**:CRC 不符或零长度它必须拒收而不是画出东西 → 重取一份、核对是不是搬到一半的半成品;仍拒收就 `dump` 看两处 CRC16 哪个不符 | `test/host/wave_read.py` 文件头 |
| `shape --strict` 说实测与段头不等 | 先分清尺子:段头 `CLIPPED` 位来自**通道来源、从不实测**;DC 偏置 / 极小注入 / 未驱动通道应改用 `--clip-superset` | §8.5 的削顶卡段文件档 |
| 触发后 `seg=` 不涨或 `drop` / `ovf` 涨 | 落盘道在 defaultTask 卷空闲后才搬段,先等几秒;仍不涨查 `wave:` 行三计数与 `/wave` 目录可写(`[ftp]` 写权限) | `App/task/adc_task.c:782` |
### 8.9 还原与收尾清单(临时改动一律还原)
### 8.9 收尾判据(走过临时改动的一条都不许留)
| 临时改动 | 还原判据 |
| 临时改动 | 收尾判据 |
|---|---|
| 测频轮改码档:`FREQ_AMP_MIN` 50.0f → 0.5f(`App/algo/freq.h:62`) | 还原 + `/keil-build` 0 错 0 警 + `git diff` 的 `App/algo/freq.h` 为空 + `docs/任务规划/数据可信.md` §7 #8 留"本轮台架已还原" |
| 相位轮抬倍数:`log_phasors()` `* 10.0f` → `* 1000.0f`(`App/task/adc_task.c:209`) | 同上,`git diff` 的 `App/task/adc_task.c` 为空 |
| bench 定时自抓门(`DTU_BENCH` + `WAVE_BENCH_AUTOCAP` 非 0,`App/task/adc_task.c:153`) | 还原默认 0 + `/keil-build` 0 错 0 警 + `git diff` 的 `App/task/adc_task.c` 为空 |
| 只动接线与发生器 | `git status` 除归档目录外干净 |
| 全程 | `test/board/test_config.h` 四套件宏全注释(产品构建);烧录 `-Rst -Run` + 回读校验 |
| 测频轮 B 档:`FREQ_AMP_MIN` 50.0f → 0.5f(`App/algo/freq.h:62`) | 还原 + `/keil-build` 0 错误 0 警告 + **`git diff` 里 `App/algo/freq.h` 必须为空** + 在 `docs/任务规划/数据可信.md` §7 #8 那行留一句"本轮台架已还原" |
| 相位轮改倍数:`log_phasors()` 的 `* 10.0f` → `* 1000.0f`(`App/task/adc_task.c:209`) | 同上,`git diff` 里 `App/task/adc_task.c` 必须为空 |
| 为 bench 定时自抓开过编译门(`DTU_BENCH` + `WAVE_BENCH_AUTOCAP` 非 0,`App/task/adc_task.c:153`) | 还原成默认 0 + `/keil-build` 0 错误 0 警告 + `git diff` 里 `App/task/adc_task.c` 必须为空(产品镜像里这段必须零存在) |
| 只动接线与发生器、没动代码 | `git status` 除归档目录外干净 |
| 全程 | `test/board/test_config.h` 四个套件宏保持全注释(产品构建);烧录仍须 `-Rst -Run` + 回读校验 |
### 8.10 归档与回写(跑完必做,缺一条不算收尾)
### 8.10 归档与回写(跑完必做,缺一算没收尾)
1. **证据归档** → `docs/测试说明/测试数据/boot_log_data/`(日期_主题_类型.txt):`20261104_ac_amp_meas_log.txt`、`20261104_ac_clip_rail_log.txt`;UI 照片同目录。**录波档多用例多带两份**:段原件(`20261104_ac_phase_wave<nn>.BIN`)+ 脚本输出(`fft`/`shape`/`dump` 文本 + `plot` 图)——录波档证据本体是**那段原始码值**,日志只是旁证;段原件被覆盖就取不回来,先改名再触发下一段。
2. **实测结论回写** → `docs/硬件平台/ADC通道映射说明.md` §8.1 之后**新开一节**(`docs/任务规划/数据可信.md` §5 DoD 这么分)。本文只留"怎么跑",**不抄实测数**。
3. **本文回写** → 只在 §8.1 表下加一句指针("YYYY-MM-DD 已跑通,实测见 …");**不建第二份结论表**(`docs/文档规范.md` 同一事实只详述一处)。
4. **判据一致性** → 本文 §8 期望值算术、接法与证据分档已写进 `docs/任务规划/数据可信.md` §4.6.2(2026-10-29)。再分叉按开头指引块裁定(语义从那节、算术与步骤从本文),**不在本文建第二份判据表**。
5. **销项** → `docs/任务规划/数据可信.md` §2.7 与 §6 卡片表按"完成即删",属 Ask first,须用户指示。
1. **证据归档** → `docs/测试说明/测试数据/boot_log_data/`,沿用现有命名惯例(日期_主题_类型.txt):
`20261104_ac_amp_meas_log.txt`、`20261104_ac_clip_rail_log.txt`、`20261104_ac_iec104_log.txt`;UI 目检照片同目录
(该目录若不收图,就在抬头里写清照片存放位置)。**走段文件定量档的用例另加两样**:取回的段原件(原始字节,
`20261104_ac_phase_wave<nn>.BIN`)与脚本输出(`fft` / `shape` / `dump` 三份文本 + `plot` 的图)——定量档的证据本体是
**那一段原始码值**,日志只是旁证;段原件一旦覆盖就再也拿不回来,先改名再触发下一段。
2. **实测结论回写** → `docs/硬件平台/ADC通道映射说明.md`,在 §8.1(直流轮次)之后**新开一节**——
任务卡 `docs/任务规划/数据可信.md` §5 的 DoD 就是这么分的。本文只留"怎么跑",**不抄实测数**。
3. **本文回写** → 只在 §8.1 那张表下加一行指针级别的句子("YYYY-MM-DD 已按本节跑通,实测见 …");
不在本文建第二份结论表,否则两份必然漂移(`docs/文档规范.md` 同一事实只在一处详述)。
4. **判据文档的一致性维护** → 本文 §8 的期望值算术、接法与出口分档已写进
`docs/任务规划/数据可信.md` §4.6.2(2026-10-29:波形轮的逐波形系数、削顶轮的 DC / 2 Hz 接法与 50 Hz 档的
"不锁存属预期"、相位轮与谐波轮的"定量档走段文件、定性档走 `phasor(hb)`")。今后两边若再分叉,按开头引导块的
两类裁定处理(语义从那节、算术与步骤从本文),**别在本文另立第二份判据表**。
5. **销项** → `docs/任务规划/数据可信.md` §2.7 与 §6 卡片表里这一条按登记簿「完成即销项」处理,属 Ask first,
须用户明确指示后再做。
### 8.11 这一轮证不到什么(别把读数当证据)
### 8.11 这一轮证不到(别让读数长得像证据)
判据属主 `docs/任务规划/数据可信.md` §4.6.5(端子二次值 / 精度等级 / 跨转换器相位与功率与对称分量 / THD 与谐波根均方 / 温升 EMC 长时间漂移)。本节按录波能力落地改过三条(2026-10-29 并入录波数据面后):**录波段文件已可用** → 相位 / 谐波走录波档,结论**形状级**非标量级;**板端对表还没跑过**(`docs/子系统设计/故障录波设计说明.md` §10),本轮是第一份实测;**段与发生器无绝对相位锁定**(触发点在段内点 159)→ 只能比同段相对相位;**320 点 = 320 ms**,不等于长时间稳定性。示波器只作旁证。
`docs/任务规划/数据可信.md` §4.6.5 那张五行表整条适用(端子二次值 / 精度等级 / 跨转换器相位与功率与对称分量 /
THD 与谐波方和根 / 温升 EMC 长时间漂移)只适用,本节按出口已落地改三条现状(2026-10-29 录波数据面并入主干之后):
**样本出口已成立**(`/wave/WAVE<NN>.BIN` + `test/host/wave_read.py`)→ 相位轮 / 谐波轮走定量档时结论是**形状级**,不再只是标量级;
但**板级那一轮对表本身还没跑过**(判据见 `docs/子系统设计/故障录波设计说明.md` §10),本轮跑出来的就是第一批实测;
段与发生器**没有绝对相位锁定**(触发瞬间只钉在段内点 159)→ 能核的是同段内两路的**相对**相位与极性,
全局相角基准仍证不到;一次触发只有 320 点 = 320 ms,**不等于长时间稳定性**。示波器仍旧只作旁证、不进判据。
边界:**算法精度已在纯数字域钉住**——`test/host/phasor/phasor_test.c` TC-PHASOR-003/004/006、`test/host/freq/freq_test.c` J1,全把已知正弦直接灌 `sample_ring`、绕开 `adc_cal` 折算(幅值是工程量口径非引脚 µV)。§8 只证"信号到 `sample_ring` 之前":发生器刻度与端接、前端串阻与滤波电容幅频、两转换器同刻性、码到 mV 折算、各处打印的量化损失。过**不等于**"算法精度已实测验证"。
再补一条「别把台架做成功能性重复」的边界:**算法本体的精度已经在纯数字域钉住了**——`test/host/phasor/phasor_test.c`
的 TC-PHASOR-003 与 TC-PHASOR-004(基波 = 峰值 / √2 容差 1 %、注入相角容差 0.5°)和 TC-PHASOR-006(2 / 3 / 5 次
谐波 1 %),以及 `test/host/freq/freq_test.c` 的扫频判据 J1(45~55 Hz 内 ≤0.02 Hz),都是把已知正弦**直接喂进
`sample_ring`**,绕过了 `adc_cal` 的折算(那里的幅值是工程量口径,不是引脚 µV)。所以 §8 这一轮真正要证的只有
「信号走到 `sample_ring` 之前」那一段:发生器刻度与输出端接、前端串阻与滤波电容的幅频、两片转换器的同刻性、
码到 mV 的折算、出口的量化。这一轮过了**不等于**「算法精度已实测验证」,结论里别那么写。
@@ -17,25 +17,9 @@
## 2. 为什么"容量"需要单独恢复(根因)
- 健康满容量 = **96064 pages(187 MB)**(`No valid map found, creating fresh` 分支下的满值)。
> ⚠ 2026-11-04 台架待核(问题 #79):三次捕获都打 `capacity: 97504 pages = 190 MB`
> (比本基线大 1440 页),且**故障前后数值一致**(不是被扣损的结果)。两种可能未定:
> ① 台架那块板的 `bb_last/bb_current` 与"全新初始化"不同口径;② 本文基线陈旧。
> 判定方法:台架跑一次动作 B(全片擦除 + 断电重启,走 `dhara_map_clear` 全新分支)
> 后读同一行的 `capacity:`——若仍是 97504,则本文基线应改。(另:本行是**故障本身
> 无关**的证据,见 架构说明 §10.4.1:容量正常 ⇒ 不是块池耗尽。)
> **2026-11-04 更新(问题 #79)**:当日跑的是动作 A(非 B),BBT 重建 0 坏块 +
> format 后容量仍报 97504=190 MB;且 129 个误标坏块存在期间读数也是 97504
> (误标只毁 journal 写头,不改本行读数)。基线改判**仍需动作 B**,未跑,本行保留。
- dhara 的坏块计数 `bb_current` / `bb_last` **持久化写在 journal checkpoint 头里**
(`Lib/dhara/journal.c` 的 `hdr_set_bb_current/hdr_set_bb_last`,随 checkpoint 一起落盘)。
容量按 `good_blocks = num_blocks − max(bb_last, bb_current) − 1` 递减。
- **⚠ 还有一处独立于 dhara 计数的坏块持久化**:板级 BBT(`gd5f2gq5ue.c` 的
`s_bbt[]` 轮转池,`mark_block_bad` 每次**立即落盘、不可撤销**)。2026-11-04 的
台架故障根因即在此:传输层失败被旧 `dhara_nand_erase/prog()` 误当坏块证据标坏,
129 个好块固化进 BBT,journal 写头爬进误标段 → `prepare_head()` 连续 8 次
`is_bad` → `TOO_BAD` → 整卷禁用(机理见 架构说明 §10.4.2)。这种"误标卷"的**处置
就是动作 A**(重扫出厂坏块会覆盖误标并持久化);A 后若 `BBT after merge` 再报
非零增量,看的是新增标坏日志 `Erase/Program ... (not a bad block)` 有没有出现。
- `dhara_journal_clear()`(即 `nand_ftl_format()` 走的路)只做 `bb_last = bb_current`,**不归零**——
所以动作 A 清不掉被扣掉的容量,断电重扫也一样(计数从 checkpoint 读回)。
- 只有让下次启动 `dhara_map_resume` **失败**、走 `dhara_map_clear` 的**全新初始化**分支
@@ -43,23 +27,6 @@
- 记账的触发:一次 `program verify` 失败(日志 `[ERR][NAND_FTL] program verify: data mismatch, blk N`)
就让 dhara 多记一个坏块;破坏性套件(写完擦、坏块注入、格式化)会累积这个数字。
### 2.1 动作 A 在 2026-11-04 之前是**静默失效**的(问题 #79,已修)
`nand_ftl_format()` 旧实现的第一步是 `if (ftl_flush_cache() != BSP_OK) return GD5F_ERROR;`,
而 `ftl_flush_cache()` 失败时刻意**不清** `s_cache_dirty`。于是只要脏页落在"写不动的那个
LPN"上,格式化就在第一道闸门返回,`dhara_map_clear()` **从未执行**——动作 A 名义上跑了,
实际上一个映射都没清,紧随其后的 `f_mkfs` 仍在旧映射上工作、必然失败。
- **现场特征**:`Cache flush FAIL: LPN=<固定值> dhara=TOO_BAD` 连打十余条 →
`reformatting...` → `volume repair failed (fr=1)` → `storage disabled`,且**每次启动必现**。
看到这条链就别再连续重试动作 A,先确认脏缓存是否正卡在坏页上。
- **修复后判据**:跑动作 A/B 时若出现新增日志
`format: dirty cache at LPN=<n> undiscardable, dropping it (map is being destroyed anyway)`
→ 说明确实撞上了"脏页卡坏页",但**格式化已经继续执行**(这是预期行为,不是新故障)。
其后应能看到 `NAND FTL formatted, capacity: ...`。
- 语义边界:丢弃脏缓存的权限**只在 format 内**。`disk_read`/`disk_write` 的 flush 失败
必须继续向上报 `RES_ERROR`——那是"这次写没落地"的真相。
## 3. 操作步骤(以容量恢复为例)
1. `test/board/test_config.h`:取消注释 `TEST_SUITE_STORAGE`;
@@ -39,7 +39,7 @@ MCU 侧真实处理只占 6.7ms,任务拾取/调度约 2.3ms(2026-09-22 重
|---|---|---|
| 远端显示 RemoDispBus | 7003 | `ui_net_sink`(真实 uiTask 生产路径) |
| FTP(lftpd) | 21 | 控制 + PASV 数据通道(数据通道复用监听 Socket,CH395F 单连接模型) |
| 对时(本文实测时在场;2026-11-04 随 #78 退役,对时改 SNTP 出向) | 8888 | `time_sync`(历史) |
| 对时 | 8888 | `time_sync` |
| 时延测试链路(M2) | 8080 | Phase 13 测试协议(服务器驱动) |
客户端 = DTU-RemtoeLCD(`remo_disp_server.py`,与本工程同级的独立项目,目录名含历史拼写 `Remtoe`)。
@@ -1,75 +1,93 @@
# 时间同步单元测试说明(主机侧)
> **模块**: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 退役重写)
> **模块**:Time Sync(`App/sync/time_sync.c` + `App/sync/time_sync.h`)
> **测试类型**:**主机侧纯逻辑单元测试**——在 PC 上离线验证 TCP 时间同步协议
> 状态机(分片喂狗延时 / 监听建立与退避 / 时间包重组 / 任务主循环),零硬件、零烧录、
> 毫秒级、不占 MCU 的 RAM。
> **状态**:✅ 已落地(80 断言点,`/W4` 0 告警,已挂 pre-commit 钩子)
> **日期**:2026-10-10
> **关联文档**:
> [`../../../AGENTS.md`](../../../AGENTS.md)(平台约束与看门狗铁律)、
> [`测试开发标准流程.md`](../流程与规范/测试开发标准流程.md)(测试分层与门禁总览)、
> [`看门狗单元测试说明.md`](看门狗单元测试说明.md)(喂狗位图判定状态机)、
> [`../../子系统设计/系统墙钟时间维护说明.md`](../../子系统设计/系统墙钟时间维护说明.md)(§15 退役历史 / §16 SNTP 现行设计)、
> [`../../子系统设计/看门狗设计说明.md`](../../子系统设计/看门狗设计说明.md)(`required_mask` / 抽样窗 / 长耗时白名单)
> [`卷锁单元测试说明.md`](卷锁单元测试说明.md)(同批落地的姊妹篇)、
> [`../../子系统设计/看门狗设计说明.md`](../../子系统设计/看门狗设计说明.md)(`required_mask` / 抽样窗 / 长耗时白名单)、
> [`../../任务规划/README.md`](../../任务规划/README.md)(登记条目「探针窗口心跳缺口」)、
> 历史审计(netTask 头阻塞;属主文档已于 2026-10-15 删除,现状见 [`../../网络/Net层阻塞模型架构设计.md`](../../网络/Net层阻塞模型架构设计.md))
---
## 1. 概述
### 1.1 为什么退役后仍要单独做主机侧单测
### 1.1 为什么单独做主机侧单测
:8888 监听、`TIME` 包重组状态机、监听退避重试随 2026-11-04 退役删除(旧套 80 断言点的
主体),但任务本身**不能删**——它是 timeSyncTask 的载体,SNTP 校时客户端
(`App/sync/time_sntp.c`,设计见墙钟说明 §16)挂在它的 200 ms 节拍下运行。留存的纯逻辑
仍然全是「误复位」当事方,且真机上只能"跑起来看复位不复位"间接观察:
本模块是**两件已知风险的当事方**,而两件风险的成因**全部是本模块的纯逻辑**:
1. **心跳节奏**:`time_sync_delay_fed()` 的分片喂心跳(片长 ≤ 50 ms)与主循环每轮首行喂位;
就绪屏障等待(100 ms 分片)未就绪时每轮也要补心跳——这是 2026-10-02 台架根因的回归防线。
2. **SNTP 宿主的节拍契约**:每轮恰好调一次 `time_sntp_run(当前代次)`;网络代次变化必须
**原样、当轮**传给客户端(代次交接是 `time_sntp_run` 内部弃旧 Socket 重建的唯一触发)。
3. **网络未就绪绝不出网**:屏障通过之前不得读代次、不得驱动 SNTP。
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 不因分片而改变)。
旧套钉死的「头阻塞回归防线」(单次等待片长恒 ≤ 50 ms、总预算不因分片改变)语义由
SNTP 模块自己的主机套 `test/host/sntp/` 与本文的延时分片用例共同接力。
次要理由:**时间包重组状态机**(累积到 8 字节才解析、TCP 分段送达、EOF / 真错误 /
本片超时三种返回码分支、tick 回绕)是纯逻辑,板端套件无法精确构造
"分段 + 预算耗尽 + tick 回绕"这类组合。
### 1.2 被测对象与桩
测试单元**原样 `#include` 生产源文件**,平台依赖由 5 个桩头替身 + 3 个真实头组成:
测试单元**原样 `#include` 生产源文件**,平台依赖由 7 个桩头替身 + 1 个真实头组成:
```
test/host/time_sync/time_sync_test.c
├── #include "FreeRTOS.h" / "task.h" / "cmsis_os2.h" ← 桩
├── #include "net_socket.h" / "sys_events.h" ← 桩
├── #include "net_socket.h" / "sys_clock.h" /
│ "sys_events.h" / "dbg_log.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`(记录每片时长);tick 时钟桩保留(生产侧现不再读 tick,留给未来用例) |
| `cmsis_os2.h` | 桩 | `osEventFlagsWait`(按脚本返回就绪 / 超时,可第 N 次 `longjmp` 跳出) |
| `task.h` | 桩 | `vTaskDelay`(记录每片时长)、`xTaskGetTickCount`(返回可控 tick) |
| `cmsis_os2.h` | 桩 | `osEventFlagsWait`(按脚本返回就绪 / 超时) |
| `sys_events.h` | 桩 | `sys_evtHandle` + `SYS_EVT_NET_READY` 等位定义 |
| `net_socket.h` | 桩 | **只剩 `net_get_generation`**(脚本化代次序列)——故意裁剪:生产代码若再引入监听类 API 会在本套编译/链接期即失败(绊线),旧 :8888 形态不会无声复活 |
| `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` **全部脚本化** |
| `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` → 本目录**:桩目录必须排在
前面,否则会被真实头抢先命中。
### 1.3 关键契约(生产代码侧)
> ⚠ `dbg_log.h` 桩**不得**定义 `DBG_LINE_BUF_MAX` / `DBG_TAG`——生产源(`time_sync.c:28-29`)
> 已先定义它们,桩再定义会触发 C4005 宏重定义告警。
- **心跳必须落在同一个 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.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 回绕由无符号减法天然处理。
### 1.4 测试环境
@@ -96,50 +114,67 @@ test/host/time_sync/time_sync_test.c
| 项 | 说明 |
|---|---|
| **触发范围** | `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` 变动 |
| **触发范围** | `App/sync/time_sync.c` / `time_sync.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: 33/33 PASSED (failed=0) ===` |
| **与板端测试的关系** | 互补:本单测保证**节拍编排与代次交接**符合设计;板端保证**真实 UDP/SNTP 时序 + IWDG 不误复位** |
| **汇总行** | `=== Time Sync Tests: 80/80 PASSED (failed=0) ===` |
| **与板端测试的关系** | 互补:本单测保证**延时节奏与重组逻辑**符合设计;板端保证**真实 TCP 时序 + IWDG 不误复位**(该心跳缺口的实际余量只能由板端长跑判定) |
---
## 2. 测试用例
> **编号停发口径**:本文档不再使用 `TC-TIMESYNC-NNN`(新条目一律语义短语,
> [`../../编号规范.md`](../../编号规范.md) §0);旧编号族已随退役整批作废,不复用。
> 断言用**语义标签**命名。桩状态在每用例前由 `fakes_reset()` 复位。
> 命名 `TC-TS-NNN`(TS = Time Sync)。断言数(80)多于用例数(27),因为每个用例通常
> 包含 2~9 条子断言(后缀 `b` / `c` …)。桩状态在每用例前由 `fakes_reset()` 复位。
### 2.1 `time_sync_delay_fed` — 分片喂心跳延时(`tc_delay_fed`)
### 2.1 `time_sync_delay_fed` — 分片喂心跳延时(TC-TIMESYNC-001..006)
| 场景 | 断言 |
| 用例 | 断言 |
|---|---|
| 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 |
| **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 |
### 2.2 `time_sync_task` — 任务主循环(`tc_task`)
### 2.2 `time_sync_open_listener` — 监听建立与退避(TC-TIMESYNC-010..015)
生产 `time_sync_task` 是 `for(;;)`,桩在脚本耗尽时 `longjmp` 跳出后断言副作用
(§4.1)。四个场景:
| 场景 | 断言 |
| 用例 | 断言 |
|---|---|
| **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 跑** |
| **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`** |
### 2.3 SNTP 驻留不拖垮心跳(`tc_sntp_dwell_then_continue`)
### 2.3 `time_sync_handle_conn` — 时间包重组状态机(TC-TIMESYNC-020..029)
`time_sntp_run` 第 2 次调用时跳出:第 1 轮完整走完(首行心跳 + 4 延时片)后第 2 轮
再次进入客户端——**每轮节拍都回到 SNTP**,驻留(应答收片 ≤ 50 ms)之后循环照常续转,
心跳位恒为 `TASK_WD_TIMESYNC`。
| 用例 | 断言 |
|---|---|
| **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 回绕** → 预算计算仍正确(无符号减法) |
**当前结果**:33/33 PASSED(`failed=0`)。
### 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`)。
---
@@ -147,11 +182,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[]` 数组填序列,再驱动任务循环。
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)。
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)。
---
@@ -161,30 +196,36 @@ test/host/time_sync/time_sync_test.c
`time_sync_task` 是永不返回的任务循环。本单测在 `main()` 里 `setjmp` 设跳转点,
桩在"脚本化调用计数达到预设值"时 `longjmp` 回来,然后断言循环期间产生的副作用
(心跳次数 / 延时片数 / SNTP 轮次与代次)。
(心跳次数 / 延时片数 / `accept` 次数 / 墙钟更新)。
**取舍**:这比"把循环体抽成可测函数"更贴近真实——**测的就是任务主体本身**,
不需要为了可测性改动生产代码结构。代价是**必须精确设定跳出点**:跳出点设错
(多一次或少一次桩调用)会导致断言数不符。
(多一次或少一次桩调用)会导致断言数不符,`TC-TIMESYNC-040` 的跳出点曾因此修正。
### 4.2 ⚠ C 的"暂定定义"会静默合并同名静态量
历史教训(2026-10-10,v1.0 套):文件作用域下两处 `static int s_bind_calls;`
本文件曾出现一个**无任何编译告警**的缺陷:文件作用域下两处 `static int s_bind_calls;`
(一处给 `task_wd_bind` 桩、一处给 `net_bind` 桩)被 C 的**暂定定义(tentative
definition)规则静默合并为同一个对象**,8 条断言失败且**无任何编译告警**。
definition)规则静默合并为同一个对象**——于是 `task_wd_bind` 与 `net_bind` 共用一个计数器,
8 条断言失败(72/80)。
**教训**:同一编译单元内**所有** `static` 变量名必须唯一。扩展用例时**务必**先
`grep` 目标名字是否已被占用。
**教训**:同一编译单元内**所有** `static` 变量名必须唯一,哪怕是不同桩的"各自的"计数器。
修复手法是把看门狗侧改名为 `s_wd_bind_calls` / `s_wd_bind_id_bad`,并加注释说明。
扩展用例时**务必**先 `grep` 目标名字是否已被占用。
### 4.3 桩不模拟真实 UDP/SNTP 时序
### 4.3 桩不模拟真实 TCP 时序
`net_recvfrom_to` 等由 `test/host/sntp/` 套覆盖报文级行为;本套的 `time_sntp_run` 桩
只记录"被调了几次、收到什么代次"。因此本套能保证的是"**给定节拍与脚本,任务编排正确**",
**不能**保证真实链路的 SNTP 时序——后者属 `test/host/sntp/` 与板端职责
(真机台架对时判据已登记,见墙钟说明 §16 与 #73)。
`net_recv_to` / `net_accept` 桩按脚本返回,**不**模拟真实的分段到达时刻、重传、窗口、
CH395F 缓冲区边界。因此本单测能保证的是"**给定返回码序列,状态机反应正确**",
**不能**保证真实链路上的时序行为——后者属板端与 PC 对端(`test/host/time_sync_server.py`)职责。
### 4.4 心跳缺口的实际余量仍由板端判定
### 4.4 探针窗口心跳缺口的实际余量仍由板端判定
本单测把"每片喂心跳、片长 ≤ 50 ms、屏障每轮补心跳"这些**逻辑前提**钉死,但端到端
心跳缺口 vs IWDG 最坏超时(1701 ms)还取决于真实调度延迟与 `netTask` 排队时长,
主机侧无法建模——板端 V5 长跑继续兜底(`../板级测试/实机测试实施指南.md` §7)。
本单测把"每片喂心跳、片长 ≤ 50ms、退避值正确"这些**逻辑前提**钉死,但该缺口关注的
是**端到端心跳缺口 vs IWDG 超时**——这还取决于真实调度延迟、`netTask` 排队时长、
Flash 写耗时等主机侧无法建模的因素。故:
- 本单测 ✅ 保证"逻辑上不会出现超过 50ms 的**无喂狗**窗口";
- 板端 ✅ 保证"实测缺口 + 调度抖动仍小于 1701ms"。
两者都过,这个心跳缺口才能闭环。
@@ -243,9 +243,7 @@ $cli = if ($env:DTU_STLINK_CLI) { $env:DTU_STLINK_CLI } else { (Get-Command ST-L
- **30s 周期重试**,不自驻留死循环;
- 恢复后各 TCP 服务(FTP 21 等)**据代次号重建监听**,功能正常。
**验证手段**:FTP 客户端连 :21 + UI 网口设置页改 IP(观察代次号重建监听)。原网络探针
`e02_probe.py` **已删除**(2026-11-04 随 :8888 退役,正文见 git 历史)——它当时挂的是 :8888
time_sync 触发形态,退役后该触发场景不再存在。
**验证手段**:`test/host/e02_probe.py`(网络探针)+ UI 网口设置页改 IP。
**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` | `test/host/` | PC 端交互测试工具(FTP 客户端 / socket 回显;原对时推送 `time_sync_server.py` 已随 :8888 退役删除,2026-11-04) |
| `test/host/ftp_*.py`(4 个)/ `ch395f_socket_test.py` / `time_sync_server.py` | `test/host/` | PC 端交互测试工具(FTP 客户端 / socket 回显 / 对时推送) |
| `simulator/tests/*.py`(4 个) | `simulator/tests/` | LVGL 模拟器回归脚本(见 §5) |
### 4.2 主机侧纯逻辑单测(纯逻辑,无硬件)
@@ -609,6 +609,7 @@ 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,8 +35,7 @@ flowchart LR
DEV -- "USB:下载器(ST-Link 等)" --> TGT
DEV -- "USB 串口 USART1<br/>COMx @115200 调试日志" --> TGT
TGT -- "以太网(CH395F)<br/>FTP:21 / 远程显示:7003" --> RMT
TGT -. "对时:SNTP 客户端 UDP 123 出向(原 :8888 推送已退役)" .-> NTP["现场 NTP / 时间源"]
TGT -- "以太网(CH395F)<br/>FTP:21 / 对时:8888 / 显示:7003" --> RMT
```
| 环境 | 角色 | 必需性 |
@@ -568,7 +567,7 @@ $env:DTU_KEIL_UV4 = 'D:\NotExist\UV4.exe' # 仅当前会话,不动用户级
| 服务 | 协议/端口 | 用途 | 核对方法 |
|---|---|---|---|
| 远程显示(RemoDispBus) | TCP **7003** | 远端看图(160×160 1bpp,3200 B/帧,客户端 LCDMEM 拉取)+ 远端导航按键 | DTU-RemoteLCD 或任意 TCP 客户端连 7003 收帧 |
| 墙钟对时(SNTP 客户端) | UDP **123**(出向;主/备服务器配 `[sntp]` 段,enable 默认关) | 设备作 SNTP 客户端从时间源取时写回 RTC(原 PC 推送 TCP :8888 已于 2026-11-04 退役删除) | 使能对时后核对设备时间 / 日志时间戳 |
| 墙钟对时 | TCP **8888** | PC 主动推送时间到设备 | 对时推送后核对设备时间 / 日志时间戳 |
| FTP | TCP **21** | 文件上传/下载(根目录 `/`,NAND 存储) | 任意 FTP 客户端列目录/读写 |
> 现场组网要求:设备与客户端**同网段**(默认 .100/24、网关 .1);跨网段部署先按 §4.1 改 IP 再上线。
@@ -576,7 +575,7 @@ $env:DTU_KEIL_UV4 = 'D:\NotExist\UV4.exe' # 仅当前会话,不动用户级
### 4.3 存储与运行时
- 文件系统:FatFS(FAT32)挂 NAND;**上电无卷自动格式化**(`f_mkfs`);格式化会清除卷上全部文件(含网口配置 `/config.ini`)
- RTC:SD2506 芯片(驱动 `Drivers/BSP/SD2506/`)提供墙钟基准与日志时间戳;首次使用建议配 `[sntp]` 段启用 SNTP 对时(或 UI 手工对时)
- RTC:SD2506 芯片(驱动 `Drivers/BSP/SD2506/`)提供墙钟基准与日志时间戳;首次使用建议先经 8888 对时
- 启动时序:`app_main_init`(存储 → 时钟 → RS485 → NAND → ADC → 启动转换)→ 就绪屏障(STORAGE / NET / FTP / TIMESYNC 事件)→ 六任务运行;详见 `docs/架构设计/项目启动顺序管理.md`、`docs/架构设计/项目架构总览.md`
### 4.4 现场验收清单
@@ -588,7 +587,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 | 对时 | 配 `[sntp]` 段启用 SNTP(或 UI 手工对时;原 PC 推送 8888 已退役) | 设备墙钟更新、日志时间戳正确 |
| 6 | 对时 | PC 推送 8888 | 设备墙钟更新、日志时间戳正确 |
| 7 | 掉电保持 | 写文件 / 改网口配置 → 重启 | 数据仍在(`/config.ini` 按新值生效) |
| 8 | LED 状态 | 目检 | 6 路 LED 随系统状态变化(低电平点亮) |
@@ -600,7 +599,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` |
| 对时时间源 | 现场可达的 SNTP/NTP 服务器(配 `[sntp]` 段 server1/server2) | 设备作客户端 UDP/123 出向取时;原「向设备 8888 推送」的 TCP 工具随退役不再需要 |
| 对时工具 | 任意可 TCP 推送的工具/脚本 | 向设备 **8888** 推送 |
| 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`**;本项目 **SNTP 客户端已落地**(2026-11-04,#73:`[sntp]` 段配服务器、**固定 UDP/123**),原自定义 TCP 8888 **已退役删除**(#78)——点表的**非标端口 5105 尚不满足**(客户端端口不可配),要按点表对接需给 `[sntp]` 增「服务器端口可配」小改 |
| 对时 | 点表要求 **SNTP `192.168.1.121:5105`**;本项目**保留自定义 TCP 8888**,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 无对应硬件通道,不加子板/通道;点表此三项本期空缺 |
| 产品定义中的对时方式 | 对时 | **SNTP 客户端已落地**(2026-11-04,#73);原自定义 TCP 8888 已退役删除(#78);点表要求的 **SNTP 5105 非标端口仍未实现**(客户端固定 UDP/123,端口不可配) |
| 产品定义中的对时方式 | 对时 | **保留自定义 TCP 8888**;点表要求的 **SNTP 5105 本期不实现** |
| 产品定义中的本期保护范围 | 保护/控制范围 | **本期 = 骨架全做 + 有效通道子集闭环**(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 / 远程显示 :7003;对时 :8888 已退役,2026-11-04)与 UDP 出向客户端(SNTP,代次变化弃旧 socket 懒重建)在循环中检测代次号变化后主动重建,网口配置立即生效后可自动在新 IP 上恢复(重建会断开既有连接,属预期行为)。详见 `docs/子系统设计/网络配置说明.md` §4。
**用途**:长驻 TCP 服务(FTP :21 / 对时 :8888 / 远程显示 :7003)在监听循环中检测代次号变化后主动重建监听 Socket,网口配置立即生效后可自动在新 IP 上恢复(重建会断开既有连接,属预期行为)。详见 `docs/子系统设计/网络配置说明.md` §4。
#### net_poll - 状态轮询
@@ -613,7 +613,7 @@ net_pending_capture_owner() ← 捕获执行者
| 指标 | 值 | 来源 / 条件 |
|---|---|---|
| 队列响应性(挂起连接下) | **92.5 ms** | 台架实测(探针 `e02_probe.py` 已删除——2026-11-04 随 :8888 退役,正文见 git 历史):挂起对时客户端 + FTP banner 探测 |
| 队列响应性(挂起连接下) | **92.5 ms** | `test/host/e02_probe.py`:挂起对时客户端 + 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 |
+7 -13
View File
@@ -32,8 +32,7 @@
> - 带连字符的 `E-<nn>` = **历史审计发现**编号(属主文档已于 2026-10-15 删除,
> 编号冻结于 `docs/编号规范.md` §4.3)——
> §5.2 末行引用它是为了记录该发现的**收口台架**,**不是**实验编号
> (证据:`test/host/e02_probe.py`(已随 :8888 退役于 2026-11-04 删除,正文见 git 历史)
> 的文档串自述它是该审计发现的 timeSync 触发形态探针);
> (证据:`test/host/e02_probe.py` 的文档串自述它是该审计发现的 timeSync 触发形态探针);
> - 该审计编号族已冻结、不再新增,引用一律改说人话。
## 1. 为什么是这个架构
@@ -112,8 +111,7 @@ fd 别名让这个边沿**必须有人接**,而"谁重建监听"在类型上
1. **不支持多监听**——`docs/问题记录/CH395F_Trap_Records.md` 附 FTP 改造清单第 4 条
「CH395F 多连接模式不支持多监听(类型:架构)」,处置即 `fun_para=0x00`。
本产品有 21/7003/2404 **三个常驻监听** + 会话期动态 PASV 监听(当时第三个是 :8888 对时,
2026-11-04 退役后由 104 :2404 在列——结论不依赖具体端口组合,见墙钟说明 §15)。
本产品有 21/8888/7003 **三个常驻监听** + 会话期动态 PASV 监听。
2. 池分配按**源端口匹配**(`docs/芯片手册/CH395F.md` §9.2.6 L1295),多监听器的池会互相串。
3. 实测:「多连接模式下最多接受 3 个客户端(Socket 1~3),Socket 4~7 的 CONNECT 中断从不触发」、
「多个 Socket 数据互相覆盖」——即 TRAP-NET-02/-11。
@@ -614,9 +612,8 @@ DISCONNECT 会被 `(void)` 丢掉。但 RECV_OK 是条件位(下轮补回)
> 「假设 → 方法 → 判据 → 它门禁哪个决策」四段式;本表只给**方法与结论**,原始记录(假设、
> 原始日志、统计)已归档,取用命令见 §12。
> ⚠ **这些编号是冻结标识**:`net_socket.c` / `net_events.c` / `net_listener.c` 注释、
> `test/host/e6_soak.py`、`docs/问题记录/CH395F_Trap_Records.md`
> 都直接引用它们(原 `test/host/e02_probe.py` 已删除——2026-11-04 随 :8888 退役,正文见 git 历史),
> **重排或改名会断链**。
> `test/host/e02_probe.py`、`test/host/e6_soak.py`、`docs/问题记录/CH395F_Trap_Records.md`
> 都直接引用它们,**重排或改名会断链**。
> ⚠ 末行 **不是实验编号**——它记录的是**历史审计发现**
> (带连字符的 `E-<nn>` 族,属主文档已于 2026-10-15 删除并冻结),列在此处只为记录该发现的**收口台架**。
> 区分规则:**带连字符 = 审计发现,无连字符 = 自愈实验**。
@@ -643,7 +640,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` 只留首末、隔离标志不清 → 继续排渣) |
| **队头阻塞审计**(非实验编号) | `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 |
| **队头阻塞审计**(非实验编号) | `test/host/e02_probe.py` 台架定案 `net_drop_conn` 收口后的时序 | 对端配合(收到 FIN 后其内核 close):`TIME_WAIT` ~150 ms → 形态④ relisten,恢复全程 ~500 ms;对端内核存活但不 close:芯片停 `FIN_WAIT_2` 长尾,**监听恢复受限于对端 close** | §4.6 |
## 6. 红线(明确不做的事)
@@ -726,18 +723,15 @@ 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 254332 30824 2072 150104 285156 27.2 94752 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=6236;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
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 254332 254648 30824 30152 2072 150104 150088 285156 284800 27.2 94752 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=6236;wave_ring.o=5296;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 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
+11 -11
View File
@@ -1,25 +1,25 @@
{
"built_at": "2026-11-04T18:58:01+08:00",
"fingerprint": "e144163b2fe1843cedb07864b926cab203f608524605a75fec38dc660b661d29",
"built_at": "2026-11-04T16:13:16+08:00",
"fingerprint": "b17ef8ac89e3f05d556ab890b1e0cc29c45f77c5739449187de0e5d3e4314b3a",
"fingerprint_files": 1479,
"fingerprint_source": "git-index",
"gated_regions": [
"RW_IRAM1",
"RW_CCM"
],
"git_head": "1f94015434ba809e7e3645316ad3fb4f331824c9",
"git_head": "912c16b75744cd0679b4f489696c02867d4b6f7a",
"map": "MDK-ARM/STM32F407-Demo/STM32F407-Demo.map",
"map_mtime": 1793789873,
"map_mtime": 1793779933,
"pct": {
"ER_IROM1": 27.19,
"ER_IROM1": 27.16,
"RW_CCM": 87.89,
"RW_IRAM1": 72.29,
"RW_IRAM1": 72.28,
"UNINIT_CCM": 18.75
},
"regions": {
"ER_IROM1": {
"max": 1048576,
"size": 285156
"size": 284800
},
"RW_CCM": {
"max": 65280,
@@ -27,7 +27,7 @@
},
"RW_IRAM1": {
"max": 131072,
"size": 94752
"size": 94736
},
"UNINIT_CCM": {
"max": 256,
@@ -37,10 +37,10 @@
"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": 254332,
"ro_data": 30824,
"code": 254648,
"ro_data": 30152,
"rw_data": 2072,
"zi_data": 150104
"zi_data": 150088
},
"threshold_pct": 92.0,
"worktree_dirt_at_stamp": 0,
+16 -16
View File
@@ -218,11 +218,11 @@
},
"App/sync/time_sync.c": {
"func_rate": 1.0,
"funcs_covered": 2,
"funcs_valid": 2,
"funcs_covered": 4,
"funcs_valid": 4,
"line_rate": 1.0,
"lines_covered": 20,
"lines_valid": 20
"lines_covered": 101,
"lines_valid": 101
},
"App/task/iec104_serv.c": {
"func_rate": 0.75,
@@ -749,9 +749,9 @@
},
"time_sync": {
"files": {
"App/sync/time_sync.c": 20
"App/sync/time_sync.c": 101
},
"lines_total": 20,
"lines_total": 101,
"ok": true
},
"ui_data": {
@@ -876,12 +876,12 @@
"files_generated_excluded": 5,
"files_tested": 54,
"files_untested": 177,
"func_rate": 0.929,
"funcs_covered": 641,
"funcs_valid": 690,
"line_rate": 0.901,
"lines_covered": 8552,
"lines_valid": 9492,
"func_rate": 0.9292,
"funcs_covered": 643,
"funcs_valid": 692,
"line_rate": 0.9018,
"lines_covered": 8633,
"lines_valid": 9573,
"modules_ok": 48,
"modules_total": 48
},
@@ -1414,6 +1414,10 @@
"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"
@@ -1514,10 +1518,6 @@
"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"
+1 -1
View File
@@ -33,7 +33,7 @@ void storage_test_run(void);
/* 存储套件恢复测试(默认关闭):取消注释并编译 STORAGE 套件运行一次,
* 即可重建 BBT(清除 TC-STORAGE-03 等注入的坏块)并重新格式化,恢复设备干净态。
* 恢复完成后建议重新注释并编译正常构建。 */
//#define ENABLE_STORAGE_RECOVERY_TESTS /* #79:2026-11-04 已用本开关清除 129 个误标坏块并 format 成功,保持注释 */
//#define ENABLE_STORAGE_RECOVERY_TESTS /* 台架已跑过一次恢复(2026-09-20 BBT 回出厂态),保持注释 */
/* 恢复模式的扩展(默认关闭,需与上面 ENABLE_STORAGE_RECOVERY_TESTS 同时打开):
* 把 dhara 可见区**整片擦净**(连 journal checkpoint 一起),用于**把被扣掉的容量恢复回来**。
+6 -8
View File
@@ -10,6 +10,7 @@
|---|---|
| `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
@@ -37,9 +38,7 @@
| 脚本 | 作用 |
|---|---|
| `latency_peer.py` | 链路时延测试的 PC 对端(peer) |
> 原 `time_sync_server.py`(:8888 推送对时源)已随 :8888 退役删除(2026-11-04,#78);
> 对时改为板端 SNTP 客户端出向(墙钟说明 §16),台架用真实/私有 NTP 源即可。
| `time_sync_server.py` | 时间同步推送服务(对时源) |
### 主机侧单元测试(纯逻辑,无硬件)
@@ -63,9 +62,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` | **时间同步任务载体单测(v2.0)**:离线编译真实 `App/sync/time_sync.c`,33 断言点覆盖分片喂狗延时 / 就绪屏障(100 ms 分片、`osFlagsNoClear`、永不就绪不挂死)/ SNTP 节拍驱动与**网络代次当轮交接** / 未出网守门(:8888 退役后任务即纯校时节拍宿主) |
| `time_sync/run_unit_test.py` | **时间同步单测**:离线编译真实 `App/sync/time_sync.c`,80 断言点覆盖分片喂狗延时 / 监听退避 / 时间包重组(半包·粘连·EOF·真错·预算耗尽·tick 回绕)/ 任务主循环(**探针窗口心跳缺口的全部逻辑贡献者**) |
| `time_sync/time_sync_test.c` | 测试单元本体(`#include` 生产源码 + 网络 / 事件 / RTOS 可脚本化桩 + `longjmp` 跳出 `for(;;)` 任务循环) |
| `time_sync/stub/` | 桩头文件(`FreeRTOS.h` / `task.h` / `cmsis_os2.h` / `net_socket.h`(裁剪至 `net_get_generation`,**编译期绊线**:生产码若再引监听类 API 立刻炸)/ `sys_events.h`) |
| `time_sync/stub/` | 桩头文件(`FreeRTOS.h` / `task.h` / `cmsis_os2.h` / `net_socket.h` / `sys_clock.h` / `sys_events.h` / `dbg_log.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"的**旧算法对照**,证明断言能咬人) |
@@ -140,8 +139,7 @@
- 本目录**被 pre-commit 门禁的 cppcheck / 代码规范排除**(`test/` 豁免);
但各模块的 `run_unit_test.py` 自身**作为独立钩子**参与门禁(见上,共 22 个)
- 台架探针族分工(2026-10-13 起):**自愈/Trap 常备回归腿在本目录**(`relisten_stress` /
`ftp_abort_probe` / `n30_repro` / `e6_soak` 等;`e02_probe` 已随 :8888 退役删除,2026-11-04);
`MDK-ARM/*.py` 只余被文档**当作
`ftp_abort_probe` / `n30_repro` / `e02_probe` / `e6_soak` 等);`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 个诊断脚本)),需要时从
@@ -159,5 +157,5 @@
- `docs/测试说明/单元测试/ADC输入层单元测试说明.md` — ADC 输入层单测(逐 Rank 钉死映射)
- `docs/测试说明/单元测试/相量层单元测试说明.md` — 相量层单测(傅氏系数 / 双区交换)
- `docs/测试说明/单元测试/卷锁单元测试说明.md` — 卷锁单测(fail-open 不变量)
- `docs/测试说明/单元测试/时间同步单元测试说明.md` — 时间同步任务载体单测 v2.0(SNTP 节拍 / 代次交接 / 未出网守门)
- `docs/测试说明/单元测试/时间同步单元测试说明.md` — 时间同步单测(探针窗口心跳缺口逻辑贡献者)
- `docs/测试说明/板级测试/CH395F_Test_Guide.md`、`FTP_Test_Guide.md`
+112
View File
@@ -0,0 +1,112 @@
#!/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)
+1 -2
View File
@@ -14,8 +14,7 @@
T7 会话结束重连、错口令一次 → 期望 530 且计数已清零(T5 成功清零)
注意:CH395F 单连接——每步用完必须 quit() 释放 socket,否则下一步连不上;
跑之前确保 UI 远程显示(7003)客户端未占用 socket(对时 :8888 已退役,
SNTP 只占 UDP 通道,与 TCP 单连接语义无关)。
跑之前确保 UI 远程显示(7003)/对时(8888)客户端未占用 socket。
用法(设备默认见 bench_defaults.DEVICE_IP,出厂口令 admin/admin):
& '.venv\\Scripts\\python.exe' test/host/ftp_auth_bench.py
+8 -220
View File
@@ -109,12 +109,6 @@ static int s_fail_check_ecc;
static int s_fail_program_load;
static int s_fail_program_exec;
static int s_fail_block_erase;
/* 定向坏页注入(问题 #79):非 0 时,**只有**该物理页的 program_exec 失败 */
static uint32_t s_fail_program_page = 0u;
/* 传输层失败注入(问题 #79):非 0 时 erase / program_exec 返回传输/超时错误
* (GD5F_ERROR,芯片**没有**回 E_FAIL/P_FAIL),用于验证"不得标坏块"。 */
static int s_fail_erase_transport;
static int s_fail_exec_transport;
/* ---- SPI 原语(stub/gd5f2gq5ue.h 声明,签名与真实驱动逐字一致) ---- */
@@ -196,15 +190,6 @@ int32_t gd5f_program_exec(uint32_t page_addr) {
if (s_fail_program_exec != 0) {
return GD5F_PROGRAM_FAIL;
}
if (s_fail_exec_transport != 0) {
return GD5F_ERROR; /* 传输/等忙失败:芯片没回 P_FAIL */
}
/* 定向坏页注入(问题 #79):现场是**某一个 PPA 恒写不进去**,其余页正常。
* 全局 s_fail_program_exec 无法复现"只有那一页坏"的形态——它连格式化
* 之后的新写入也一起打死,反而测不出"换个页就能写"这个关键结论。 */
if ((s_fail_program_page != 0u) && (page_addr == s_fail_program_page)) {
return GD5F_PROGRAM_FAIL;
}
/* 严格语义探针:目标页已有非 0xFF 字节 = 双编程(is_free 误判的现场证据) */
for (i = 0; i < FAKE_PAGE_SIZE; i++) {
@@ -232,9 +217,6 @@ int32_t gd5f_block_erase(uint32_t block_addr) {
if (s_fail_block_erase != 0) {
return GD5F_ERASE_FAIL;
}
if (s_fail_erase_transport != 0) {
return GD5F_ERROR; /* 传输/等忙失败:芯片没回 E_FAIL */
}
(void)memset(&s_chip[block_addr * FAKE_PAGES_PER_BLOCK][0], 0xFF,
(size_t)FAKE_PAGES_PER_BLOCK * FAKE_PAGE_SIZE);
return GD5F_OK;
@@ -300,9 +282,6 @@ static void fake_chip_reset(void) {
s_fail_program_load = 0;
s_fail_program_exec = 0;
s_fail_block_erase = 0;
s_fail_program_page = 0u; /* 定向坏页注入复位(0 = 不注入,0 号页本身也不允许当靶) */
s_fail_erase_transport = 0;
s_fail_exec_transport = 0;
g_nandtest_err_count = 0;
g_nandtest_info_count = 0;
g_nandtest_debug_count = 0;
@@ -680,10 +659,9 @@ static void tc_prog(void) {
CHECK(g_fake_mark_bad_cnt == 1u, "program failure marks exactly one block bad");
CHECK(g_fake_mark_bad_log[0] == 2u, "the block marked bad is the page's own block");
/* TC-NANDPORT-207:program_load 失败(装载阶段)-> BAD_BLOCK,但**不标坏块**。
* 【#79 修复】装载失败是传输层问题(02h 都没送达芯片),芯片从未回 P_FAIL,
* 把块标坏等于把"线抖了一下"固化成永久坏块。旧实现在此标坏,本用例把守。 */
printf("[TC-NANDPORT-207] program_load failure -> BAD_BLOCK, NO mark bad\n");
/* TC-NANDPORT-207:program_load 失败(装载阶段)-> 同样 BAD_BLOCK + 标坏该块,
* 且在 exec 之前就短路(真实芯片:02h 都没成,10h 自然不发) */
printf("[TC-NANDPORT-207] program_load failure -> BAD_BLOCK + mark bad\n");
fake_chip_reset();
s_fail_program_load = 1;
err = DHARA_E_NONE;
@@ -691,21 +669,8 @@ static void tc_prog(void) {
"program_load failure -> -1");
CHECK(err == DHARA_E_BAD_BLOCK, "program_load failure -> DHARA_E_BAD_BLOCK");
CHECK(g_fake_program_exec_cnt == 0u, "load failure short-circuits before program_exec");
CHECK(g_fake_mark_bad_cnt == 0u, "load failure must NOT mark bad (transport, not media)");
CHECK(fake_bad_unchanged(), "load failure leaves the BBT untouched");
/* TC-NANDPORT-208:program_exec 传输/超时失败(芯片未回 P_FAIL)-> 不标坏块。
* 【#79 核心】真机现场:出厂 0 坏块、底层写全部成功,而持久化 BBT 把
* 1016–1142 连续段判坏——正是这类失败被旧实现标坏并固化所致。 */
printf("[TC-NANDPORT-208] program_exec transport failure -> BAD_BLOCK, NO mark bad\n");
fake_chip_reset();
s_fail_exec_transport = 1;
err = DHARA_E_NONE;
CHECK(dhara_nand_prog(&s_ut_nand, 4u * 64u, page_buf, &err) == -1,
"program_exec transport failure -> -1");
CHECK(err == DHARA_E_BAD_BLOCK, "program_exec transport failure -> DHARA_E_BAD_BLOCK");
CHECK(g_fake_mark_bad_cnt == 0u, "exec transport failure must NOT mark bad");
CHECK(fake_bad_unchanged(), "exec transport failure leaves the BBT untouched");
CHECK(g_fake_mark_bad_cnt == 1u, "load failure marks exactly one block bad");
CHECK(g_fake_mark_bad_log[0] == 3u, "the block marked bad is the page's own block");
}
/* ===================== TC-NANDPORT-300..307:dhara_nand_read ===================== */
@@ -902,27 +867,15 @@ static void tc_erase(void) {
CHECK(dhara_nand_erase(&s_ut_nand, UT_OOB_BLOCK - 1u, &err) == 0,
"erase block == num_blocks - 1 -> 0");
/* TC-NANDPORT-504:芯片回 E_FAIL 的擦除失败 -> BAD_BLOCK + 标坏该块(正确处置) */
printf("[TC-NANDPORT-504] erase E_FAIL -> BAD_BLOCK + mark bad\n");
/* TC-NANDPORT-504:擦除失败 -> BAD_BLOCK + 标坏该块 */
printf("[TC-NANDPORT-504] erase failure -> BAD_BLOCK + mark bad\n");
fake_chip_reset();
s_fail_block_erase = 1;
err = DHARA_E_NONE;
CHECK(dhara_nand_erase(&s_ut_nand, 3u, &err) == -1, "block_erase failure -> -1");
CHECK(err == DHARA_E_BAD_BLOCK, "block_erase failure -> DHARA_E_BAD_BLOCK");
CHECK(g_fake_mark_bad_cnt == 1u, "erase E_FAIL marks exactly one block bad");
CHECK(g_fake_mark_bad_cnt == 1u, "erase failure marks exactly one block bad");
CHECK(g_fake_mark_bad_log[0] == 3u, "the block marked bad is the one that failed");
/* TC-NANDPORT-505:擦除传输/超时失败(芯片未回 E_FAIL)-> 不标坏块。
* 【#79 核心】这是把 129 个好块误标并持久化的那条路径;标坏是**不可撤销**
* 的持久化动作,只有芯片明确回 E_FAIL 才允许。 */
printf("[TC-NANDPORT-505] erase transport failure -> BAD_BLOCK, NO mark bad\n");
fake_chip_reset();
s_fail_erase_transport = 1;
err = DHARA_E_NONE;
CHECK(dhara_nand_erase(&s_ut_nand, 5u, &err) == -1, "erase transport failure -> -1");
CHECK(err == DHARA_E_BAD_BLOCK, "erase transport failure -> DHARA_E_BAD_BLOCK");
CHECK(g_fake_mark_bad_cnt == 0u, "erase transport failure must NOT mark bad");
CHECK(fake_bad_unchanged(), "erase transport failure leaves the BBT untouched");
}
/* ===================== TC-NANDPORT-600..605:is_bad / mark_bad ===================== */
@@ -1028,168 +981,6 @@ static void tc_dhara_scenario(void) {
"write + resume scenario: zero double-program violations (scenario guard)");
}
/* ===================== TC-NANDPORT-800..813:问题 #79 回归 =====================
*
* 现场(2026-11-04 台架,两次独立捕获):
* 探针写 /fschk.tmp → disk_write → ftl_flush_cache 对 LPN=211 反复 TOO_BAD
* → 判卷损坏 → storage_bring_up 调 nand_ftl_format() 想重建
* → **旧版 format 先 flush 同一脏页、失败即 return** → map 从没被清过
* → f_mkfs 在旧映射上跑 → 3 轮重试耗尽 → storage disabled(每次启动必现)。
*
* 本组测三件事:
* ① 脏缓存写不进去时,format **仍然成功**并清掉映射(修复点);
* ② format 后脏缓存被丢弃,不会带着旧 LPN 幽灵继续写(配套不变量);
* ③ 失败快照(C4)真的把"操作 / 失败类 / LPN / 次数"带出函数边界(新能力)。
*
* 假芯片的失败注入是**按页**的(s_fail_program_exec 是全局开关),而现场是
* "某个 LPN 的写回恒定失败"。这里用"让 program_exec 恒失败"逼近同一形态:
* 脏页写不进去 → flush 必失败,正是旧版 return 的那个分支。
*/
/* 把脏缓存喂进去:直接经 FatFs 叶子的 disk_write 不可行(那会先触发 init),
* 故手工构一个与 disk_write 相同的前置状态:缓存里有一页待写数据。 */
static void prep_dirty_cache(uint32_t lpn, uint8_t seed) {
fake_chip_reset();
s_cached_lpn = (dhara_sector_t)lpn;
fake_fill_pattern(s_cache_buf, FTL_PAGE_SIZE, seed);
s_cache_dirty = 1u;
}
static void tc_format_survives_flush_failure(void) {
uint8_t before[FTL_PAGE_SIZE];
/* ---- 前置:一个已建好、里面真有数据的卷(模拟台架"卷原本可用") ---- */
fake_chip_reset();
s_ut_nand.log2_page_size = 11u;
s_ut_nand.log2_ppb = 6u;
s_ut_nand.num_blocks = (unsigned int)FAKE_USABLE_BLOCKS;
dhara_map_init(&s_map, &s_ut_nand, s_page_buf, 4);
memset(s_cache_buf, 0, FTL_PAGE_SIZE);
s_cached_lpn = (dhara_sector_t)-1;
s_cache_dirty = 0;
s_initialized = 1;
(void)dhara_map_clear(&s_map);
{
dhara_error_t e;
(void)dhara_map_sync(&s_map, &e);
}
fake_fill_pattern(s_cache_buf, FTL_PAGE_SIZE, 0xA5u);
{
dhara_error_t e;
CHECK(dhara_map_write(&s_map, 0u, s_cache_buf, &e) == 0,
"TC-NANDPORT-800 precondition: sector 0 written");
}
fake_fill_pattern(s_cache_buf, FTL_PAGE_SIZE, 0x5Au);
{
dhara_error_t e;
CHECK(dhara_map_write(&s_map, 1u, s_cache_buf, &e) == 0,
"TC-NANDPORT-801 precondition: sector 1 written");
}
CHECK(dhara_map_size(&s_map) == 2u, "TC-NANDPORT-802 precondition: 2 sectors used");
/* ---- 关键形态:缓存脏着(脏页写不出去),此时调 format() ----
* 阶段 A 用"介质整体写不动"逼近最坏情况:program_load 全失败 ⇒ dhara 反复
* 标坏块 + 重试 8 次后 TOO_BAD,正是 `Cache flush FAIL: LPN=211 … TOO_BAD`
* 那条日志的来源。这一阶段**只断言死锁被打破**,不断言 format 返回 OK——
* 介质真的全写不动时它本来就落不了盘,报错是诚实的。 */
memset(s_cache_buf, 0xC3, FTL_PAGE_SIZE);
s_cached_lpn = (dhara_sector_t)211u; /* 与现场 LPN=211 同形 */
s_cache_dirty = 1u;
memcpy(before, s_cache_buf, FTL_PAGE_SIZE);
s_fail_program_load = 1;
(void)nand_ftl_format();
/* ★ 本单的核心断言:**旧实现到这里 map 一个字节都没清**
* (它在 ftl_flush_cache() 失败那行就 return 了,dhara_map_clear 从未执行)。
* 新实现必须已经把 map 清空、缓存作废——这正是"恢复动作本身被永久挡住"
* 这条死锁的破点。 */
CHECK(dhara_map_size(&s_map) == 0u,
"TC-NANDPORT-804 format clears the map even when dirty-cache flush fails (fix for #79)");
CHECK(s_cache_dirty == 0u, "TC-NANDPORT-805 format discards the dirty cache flag");
CHECK(s_cached_lpn == (dhara_sector_t)-1,
"TC-NANDPORT-806 format invalidates cached LPN (no ghost page reuse)");
CHECK(memcmp(s_cache_buf, before, FTL_PAGE_SIZE) == 0,
"TC-NANDPORT-807 format does not fabricate page content");
/* ---- 阶段 B:介质可用后(新片上电 = 无坏块积累、map 重新 resume),
* format 必须产出真正可写的空卷。现场对应"下次可写的格式化"。 ---- */
fake_chip_reset(); /* 同时复位全部失败注入 */
dhara_map_init(&s_map, &s_ut_nand, s_page_buf, 4);
memset(s_cache_buf, 0, FTL_PAGE_SIZE);
s_cached_lpn = (dhara_sector_t)-1;
s_cache_dirty = 0;
s_initialized = 1;
nand_ftl_fault_clear();
{
int rc = nand_ftl_format();
CHECK(rc == GD5F_OK, "TC-NANDPORT-808 on recovered media: format returns OK");
CHECK(dhara_map_size(&s_map) == 0u,
"TC-NANDPORT-808b on recovered media: map is empty after format");
}
{
dhara_error_t e;
int wrc;
fake_fill_pattern(s_cache_buf, FTL_PAGE_SIZE, 0x11u);
wrc = dhara_map_write(&s_map, 0u, s_cache_buf, &e);
CHECK(wrc == 0, "TC-NANDPORT-809 after format: new volume accepts writes");
CHECK(dhara_map_size(&s_map) == 1u, "TC-NANDPORT-810 after format: map counts 1 sector");
}
/* ---- 失败快照(C4):新能力,把失败类带出函数边界 ---- */
nand_ftl_fault_clear();
{
nand_ftl_fault_t f;
nand_ftl_fault_get(&f);
CHECK(f.active == 0u, "TC-NANDPORT-811 fault snapshot clear -> inactive");
CHECK(f.count == 0u, "TC-NANDPORT-811b fault snapshot clear -> count reset");
}
/* 造一次可归类的失败:脏缓存 + 写不动 */
fake_chip_reset();
memset(s_cache_buf, 0x77, FTL_PAGE_SIZE);
s_cached_lpn = (dhara_sector_t)211u;
s_cache_dirty = 1u;
s_fail_program_load = 1;
{
bsp_err_t cls = ftl_flush_cache();
nand_ftl_fault_t f;
CHECK(cls != BSP_OK, "TC-NANDPORT-812a injected flush failure returns an error class");
nand_ftl_fault_get(&f);
CHECK(f.active == 1u, "TC-NANDPORT-812 fault snapshot becomes active after a failure");
CHECK(f.op == NAND_FTL_OP_FLUSH, "TC-NANDPORT-812b snapshot records the failing operation");
CHECK(f.cls == (int32_t)cls, "TC-NANDPORT-812c snapshot records the failure class");
CHECK(f.lpn == 211u, "TC-NANDPORT-812d snapshot records the failing LPN");
CHECK(f.count == 1u, "TC-NANDPORT-812e snapshot counts the failure");
}
/* 再失败一次:count 累计(现场是同一页撞 15 次) */
{
nand_ftl_fault_t f;
(void)ftl_flush_cache();
nand_ftl_fault_get(&f);
CHECK(f.count == 2u, "TC-NANDPORT-813 repeated failures accumulate (same-page retry)");
CHECK(f.lpn == 211u, "TC-NANDPORT-813b repeated failures keep the same LPN");
}
s_fail_program_load = 0;
s_fail_program_page = 0u;
nand_ftl_fault_clear();
s_initialized = 0;
s_cached_lpn = (dhara_sector_t)-1;
s_cache_dirty = 0;
}
/* ===================== main ===================== */
int main(void) {
@@ -1232,9 +1023,6 @@ int main(void) {
printf("-- dhara map scenario (real dhara, fake chip) --\n");
tc_dhara_scenario();
printf("-- format survives dirty-cache flush failure + fault snapshot (issue #79) --\n");
tc_format_survives_flush_failure();
printf("\n=== NAND Port Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
+13 -11
View File
@@ -5,18 +5,19 @@
做什么:
用本机 MSVC 编译 test/host/time_sync/time_sync_test.c(它以 #include 纳入
**生产源文件** App/sync/time_sync.c),在 PC 上离线跑:
分片延时(心跳节奏)/ 任务主循环(就绪屏障、SNTP 节拍、代次下传)。
分片延时(心跳节奏)/ 监听建立与退避 / 时间包重组状态机(分段 + 30s 预算 +
tick 回绕)/ 任务主循环(就绪屏障、accept 轮询、代次变化重监听)。
失败即非 0 退出。零硬件、零烧录、毫秒级。
工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码判定全部委托给通用 harness
`test/host/common/unit_test_harness.py`(本文件只做声明)。
为什么要主机侧单测(而不是只用板端套件):
本模块是探针窗口心跳缺口那件事的当事方——心跳节奏(delay_fed 的 ≤50ms
分片 + 每片前置心跳、屏障等待态 100ms 分片喂心跳)全是**纯逻辑**,真机上
只能"跑起来看复位不复位",主机侧可**逐片断言**(2026-10-02 台架根因、
第 8b 轮消抖两条教训都钉在这里)。:8888 监听/协议用例已随 2026-11-04 的
:8888 退役删除(墙钟维护说明 §15),历史覆盖见 git 历史。
本模块是探针窗口心跳缺口那件事的当事方——"监听重建期间心跳缺口 800ms~1s,距 IWDG 最坏
超时 1701ms 余量仅 ~0.7s"。缺口成因全是本模块的**纯逻辑**(分片延时的喂狗
节奏、500/2000ms 监听退避、200ms accept 退避、50ms 收尾、屏障等待态喂心跳)。
真机上只能"跑起来看复位不复位",主机侧可**逐片断言**。时间包重组状态机
(分段送达 / 预算耗尽 / EOF / 真错误 / tick 回绕)同理。
用法(仓库根目录):
python test/host/time_sync/run_unit_test.py
@@ -24,8 +25,8 @@
退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。
设计依据:App/sync/time_sync.c 文件头注释 + docs/子系统设计/系统墙钟时间维护说明.md §15/§16;
创建日期:2026-10-10(2026-11-04 随 :8888 退役收缩)
设计依据:App/sync/time_sync.c 文件头注释 + docs/网络/网络自愈架构设计.md §4.6;
创建日期:2026-10-10
"""
import os
import sys
@@ -38,9 +39,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(真实头会拖入整套 FreeRTOS / CMSIS-RTOS / HAL)。
# task_watchdog.h 与 time_sntp.h 用**真实头**(接口契约原汁:前者只依赖 stdint.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 用真实头(接口契约原汁)。
BUILD = TestBuild(
name="Time Sync Tests",
src=os.path.join(HERE, "time_sync_test.c"),
@@ -48,6 +49,7 @@ 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
@@ -0,0 +1,31 @@
/*
* 主机侧单测桩: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 */
+45 -10
View File
@@ -1,18 +1,20 @@
/*
* 主机侧单测桩:net_socket.h(收缩版,:8888 退役后)
* 模块功能:为 App/sync/time_sync.c 提供主机可编译的 net_get_generation 声明。
* 主机侧单测桩:net_socket.h(含内联 net_types.h 子集)
* 模块功能:为 App/sync/time_sync.c 提供主机可编译的网络 Socket API 声明与类型。
*
* ⚠ 仅供 test/host/time_sync/ 使用,不参与 MCU 构建。
* 真实头(Drivers/BSP/NET/net_socket.h)包含 net_types.h → net_config.h,并经
* FreeRTOS.h / task.h 拖入整套 FreeRTOS;本桩改为包含同名桩头(含 FreeRTOS/task
* 两条**保真**包含——生产源正是靠真实头的这条链拿到 vTaskDelay / pdMS_TO_TICKS)。
* FreeRTOS.h / task.h 拖入整套 FreeRTOS;本桩改为包含同名桩头。
*
* 收缩说明(2026-11-04)::8888 退役后本任务只剩 net_get_generation 一处网络
* 调用。socket / bind / listen / accept / close / recv 族声明**刻意不列**——
* 若 time_sync 循环重新伸手碰监听族 API,测试 TU 会因"未声明 + 无强符号桩"在
* 编译/链接期直接爆,这是退役语义的编译期绊线(替代退役前的运行时断言)。
* 保真点:
* - 地址族/类型/特殊地址常量与 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-10-10(2026-11-04 随 :8888 退役收缩)
* 创建日期:2026-10-10
*/
#ifndef NET_SOCKET_H
@@ -27,8 +29,41 @@
extern "C" {
#endif
/* 与 Drivers/BSP/NET/net_socket.h 签名一致 */
/* ===== 以下取自 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);
uint32_t net_get_generation(void);
uint16_t net_htons(uint16_t hostshort);
#ifdef __cplusplus
}
+29
View File
@@ -0,0 +1,29 @@
/*
* 主机侧单测桩: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 */
+2 -3
View File
@@ -4,9 +4,8 @@
*
* ⚠ 仅供 test/host/time_sync/ 使用,不参与 MCU 构建。
*
* 接口由测试单元给强符号桩:vTaskDelay 记录每片睡眠时长(断言"≤50ms 分片");
* xTaskGetTickCount 保留可控时钟声明(:8888 退役后生产源已不读 tick,桩仍提供,
* 供未来需要预算/回绕类用例时直接取用)。
* 两个接口由测试单元给强符号桩:vTaskDelay 记录每片睡眠时长(断言"≤50ms 分片"),
* xTaskGetTickCount 由测试单元提供可控时钟(驱动 30s 预算耗尽与 32 位回绕用例)。
*
* 创建日期:2026-10-10
*/
+603 -116
View File
@@ -1,31 +1,69 @@
/*
* 模块名称:Time Sync(timeSyncTask 节拍宿主)主机侧单元测试
* 模块功能:在 PC 上对 App/sync/time_sync.c 做离线验证(2 个函数全覆盖:
* time_sync_delay_fed / time_sync_task)。
* 模块名称: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)。
*
* 被测对象:真实源文件 ../../../App/sync/time_sync.c(以 #include 原样纳入,
* 保证测的是生产代码而非副本)。平台依赖由本文件提供的**可注入桩**实现:
* - FreeRTOS/task.h -> vTaskDelay / xTaskGetTickCount(可控时钟)
* - net_socket.h -> net_get_generation(脚本化 + 可跳出主循环)
* - net_socket.h -> socket/bind/listen/accept/close/recv_to/
* get_generation/htons(全部脚本化)
* - sys_events.h -> osEventFlagsWait(可注入"未就绪")
* - sys_clock.h -> sys_clock_set_unix(记录 epoch)
* - task_watchdog.h -> task_wd_beat / task_wd_bind(记录心跳节奏)
* - time_sntp.h -> time_sntp_run(替身:记录轮次与收到的代次)
* - dbg_log.h -> DBG_ERROR/DBG_INFO 计数
*
* 版本史: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"。
* 为什么要主机侧单测(而不是只用板端套件):
* 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,毫秒级完成。
*
* 断言口径(用户裁定 2026-10-27,编号规范 §9):新用例一律**语义行名**,
* 不再配 TC 号;旧 TC-TIMESYNC-* 号随用例退役永久作废、不得复用。
* 覆盖点(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" 四组
*
* 通过:全部断言 PASS(main 返回 0)。
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-10(2026-11-04 随 :8888 退役收缩重写)
* 创建日期:2026-10-10
*/
#include <setjmp.h>
@@ -37,13 +75,14 @@
#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 "time_sntp.h" /* 真实头(App/sync):time_sntp_run 签名原汁,替身必须对表 */
#include "sys_log.h" /* 真实头(App/log,纯 stdint 无 OS 依赖):syslog_id_t / SYSLOG_OPER 契约,假件签名与之逐字对齐 */
/* ===================== FreeRTOS 桩 ===================== */
static TickType_t s_tick;
#define DELAY_LOG_MAX 2048
#define DELAY_LOG_MAX 800
static uint32_t s_delays[DELAY_LOG_MAX];
static int s_delay_calls;
@@ -63,7 +102,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;
@@ -90,12 +129,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;
@@ -113,16 +152,126 @@ void task_wd_bind(task_wd_id_t id) {
}
}
/* ===================== net_socket 桩 =====================
* :8888 退役后本任务只剩 net_get_generation 一处网络调用。桩头刻意**不声明**
* socket/bind/listen/accept/close/recv——若循环哪天重新伸手碰监听族 API,
* 链接期直接爆(编译期绊线,替代退役前的运行时断言)。 */
/* ===================== 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];
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++;
@@ -132,29 +281,59 @@ uint32_t net_get_generation(void) {
return (i < s_gen_seq_n) ? s_gen_seq[i] : s_gen_default;
}
/* ===================== 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 */
/* ---- 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;
void time_sntp_run(uint32_t net_gen) {
int i = s_sntp_run_calls++;
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];
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;
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 (s_sntp_run_longjmp_at > 0 && (i + 1) >= s_sntp_run_longjmp_at) {
longjmp(s_jmp, 3);
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;
}
}
/* ===================== 断言 ===================== */
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++; \
@@ -162,6 +341,24 @@ static int s_total = 0, s_passed = 0, s_failed = 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"
@@ -172,8 +369,8 @@ static int s_total = 0, s_passed = 0, s_failed = 0;
* 入口参数:无
* 返 回 值:无
* 限定条件:无
* 函数说明:默认 = 事件标志健康就绪(屏障一次通过)、gen 恒为 1、sntp 替身直通。
* 各用例按需覆盖。
* 函数说明:默认 = 网络全成功、gen 恒为 1、recv 脚本为空(脚本用尽返回 EOF)、
* 事件标志恒就绪。各用例按需覆盖。
*/
static void fakes_reset(void) {
s_tick = 0;
@@ -193,14 +390,64 @@ 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_sntp_run_calls = 0;
s_sntp_gen_log_n = 0;
s_sntp_run_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_delays[0..n) 是否全部等于 v */
@@ -214,100 +461,268 @@ static int delays_all(uint32_t v, int n) {
return 1;
}
/* ===================== delay_fed:心跳分片延时 =====================
* 本模块的全部看门狗安全论证都压在这一个函数上:片长恒 ≤50ms、每片前置心跳,
* 抽样窗(100ms)内必有心跳——第 8b 轮消抖钉下的节奏,逐片断言。 */
/* ===================== TC-TIMESYNC-001..006:delay_fed ===================== */
static void tc_delay_fed(void) {
fakes_reset();
time_sync_delay_fed(0U);
CHECK((s_beat_calls == 0) && (s_delay_calls == 0), "delay_fed(0) neither sleeps nor beats");
CHECK((s_beat_calls == 0) && (s_delay_calls == 0), "TC-TIMESYNC-001 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),
"delay_fed(50) = one 50 ms slice + 1 beat");
"TC-TIMESYNC-002 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), "delay_fed(120) splits into 3 slices");
CHECK((s_beat_calls == 3) && (s_delay_calls == 3), "TC-TIMESYNC-003 delay_fed(120) splits into 3 slices");
CHECK((s_delays[0] == 50U) && (s_delays[1] == 50U) && (s_delays[2] == 20U),
"delay_fed(120) slice lengths 50/50/20 (last slice is the remainder)");
"TC-TIMESYNC-003b 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),
"delay_fed(2000) = 40 slices x 50 ms, one beat per slice");
"TC-TIMESYNC-004 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),
"delay_fed(49) less than one slice handled as a single slice");
CHECK((s_beat_calls == 1) && (s_delays[0] == 49U), "TC-TIMESYNC-005 less than one slice (49 ms) handled as a single slice");
fakes_reset();
time_sync_delay_fed(30000U);
CHECK(s_delay_calls == 600, "delay_fed(30000) = 600 slices (slice cap is always 50 ms)");
CHECK(s_delay_calls == 600, "TC-TIMESYNC-006 total budget 30 s = 600 slices (slice cap is always 50 ms)");
}
/* ===================== time_sync_task:屏障 + SNTP 节拍 ===================== */
/* ===================== 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 ===================== */
static void tc_task(void) {
/* A) 无就绪屏障(sys_evtHandle == NULL):一轮完整节拍后于第 2 轮顶部脱出 */
uint8_t pkt[16];
/* A) 无就绪屏障(sys_evtHandle == NULL):accept 轮询 2 次失败后成功 */
fakes_reset();
s_gen_longjmp_at = 2; /* iter1 顶部(1) + iter2 顶部(2) -> 跳出 */
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;
if (setjmp(s_jmp) == 0) {
time_sync_task(NULL);
}
CHECK((s_wd_bind_calls == 1) && (s_wd_bind_id_bad == 0),
"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");
"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");
/* 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 次就绪 */
/* B) 就绪屏障: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; /* 屏障通过后第 1 轮顶部跳出 */
s_gen_longjmp_at = 1;
if (setjmp(s_jmp) == 0) {
time_sync_task(NULL);
}
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");
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");
/* D) 屏障期间持续喂心跳(就绪位长期不置位,2026-10-02 台架根因的回归防线) */
/* C) 屏障期间持续喂心跳(就绪位长期不置位) */
fakes_reset();
sys_evtHandle = (osEventFlagsId_t)0x55;
s_evt_default = osFlagsErrorTimeout;
@@ -315,25 +730,93 @@ static void tc_task(void) {
if (setjmp(s_jmp) == 0) {
time_sync_task(NULL);
}
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");
}
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)");
/* ===================== SNTP 驻留后的循环延续 =====================
* time_sntp_run 的契约是"单次驻留 ≤50ms,调用方在片间喂心跳"——替身模拟一次
* 驻留后返回,循环必须按原节拍继续(心跳不欠账)。 */
static void tc_sntp_dwell_then_continue(void) {
/* D) 网络代次变化 -> 关闭旧监听并重监听 */
fakes_reset();
s_sntp_run_longjmp_at = 2; /* 第 2 轮驻留中脱出(模拟慢收片场景收尾) */
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_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");
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");
}
/* ===================== 对时操作留痕四组(道G task-50) =========================
* 成功应用投一条 quiet OPER;被拒不投(失败不得记成"改过时钟");
* 连续两次各一条不合并;魔术字不符不投。
* ⚠ 断言标签用语义行名:编号规范 §9 批次三已对测试用例号停发(用户裁定 2026-10-27),
* 本组是新口径下写的——追溯靠函数名 tc_oper_trace + 下面的语义标签。 */
static void tc_oper_trace(void) {
uint8_t pkt[16];
/* 1) 对时成功 → 恰一条 OPER */
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");
}
/* ===================== main ===================== */
@@ -343,10 +826,14 @@ int main(void) {
printf("-- delay_fed (heartbeat-fed slicing) --\n");
tc_delay_fed();
printf("-- time_sync_task (barrier / SNTP cadence) --\n");
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");
tc_task();
printf("-- sntp dwell then continue --\n");
tc_sntp_dwell_then_continue();
printf("-- operation trace on sync (task-50) --\n");
tc_oper_trace();
printf("\n=== Time Sync Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
+83
View File
@@ -0,0 +1,83 @@
#!/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] 已停止")
+23 -23
View File
@@ -1,10 +1,10 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""生成 docs/images/交流六轮速查.svg —— 交流(正弦)动态验证六轮:每一轮注什么信号、
验什么、数字从哪儿读,一张图先给全局轮廓,再进 §8.5 逐轮执行卡。
验什么、主读数走哪个出口,一张图先给全局轮廓,再进 §8.5 逐轮执行卡。
为什么不用 Mermaid:本图的语义重心是「每轮注入信号的波形形状」与「三块小面板里
信号缩略图 + 判据 + 读数的固定排版对位」——Mermaid 画不出正弦/方波/三角/削顶/谐波
信号缩略图 + 判据 + 出口的固定排版对位」——Mermaid 画不出正弦/方波/三角/削顶/谐波
叠加这些波形,也无法把六格对齐成 3x2 卡片。它只适合关系拓扑(那部分另在
docs/测试说明/专项验证/ADC前端注入验证流程.md §8.3 / §8.5 用 Mermaid 表达)。
@@ -12,7 +12,7 @@ docs/测试说明/专项验证/ADC前端注入验证流程.md §8.3 / §8.5 用
python tools/arch_diagram/gen_ac_rounds_svg.py
输出:
docs/images/交流六轮速查.svg
改内容:轮次事实(接法 / 判据 / 读数)变了才重画;调坐标 / 文案编辑下方常量后重跑。
改内容:轮次事实(接法 / 判据 / 出口)变了才重画;调坐标 / 文案编辑下方常量后重跑。
"""
import html
import math
@@ -74,40 +74,40 @@ GAPY = 14
X0 = PAD
Y0 = 106
THUMB_H = 86
LABEL_W = 36 # 「注入 / 判据 / 读数」标签列宽
LABEL_W = 36 # 「注入 / 判据 / 出口」标签列宽
# ----------------------------- 数据(改这里) -----------------------------
TITLE = "交流动态验证六轮 —— 每一轮注什么信号、读哪个口"
SUB1 = ("一卡一轮:注入信号缩略图 → 接法与设定 → 判据一句话 → 数字从哪儿读。"
SUB1 = ("一卡一轮:注入信号缩略图 → 接法与设定 → 判据一句话 → 主读数出口。"
"先看图建立全局轮廓,再按 §8.5 的对应执行卡逐轮跑")
SUB2 = ("六轮共用同一套接线与通用动作;过判据的属主是 docs/任务规划/数据可信.md §4.6.2,"
"本图只是导航,不含新判据")
# 每卡:(轮名, 角标, 主色, 底色, 波形类型, 注入, 判据, 读数的地方)
# 每卡:(轮名, 角标, 主色, 底色, 波形类型, 注入, 判据, 出口)
CARDS = [
("幅值轮", "先做 · 三档幅度", C_BLUE, BG_BLUE, "amp",
"UA(R31)端子侧,正弦 50.000 Hz,三档 Vpp 2.828 / 1.414 / 0.566",
"每档 ua_mv ≈ 万用表 × 1000,偏差 ≤2 %",
"meas: ua_mv 为主(1 mV 分辨率)"),
("波形轮", "真有效值 ≠ 基波", C_TEAL, BG_TEAL, "three",
"同设 Vpp 2.828 V 改方波 / 三角(面板有效值各异)",
("幅值轮", "先做 · 主用例", C_BLUE, BG_BLUE, "amp",
"UA(R31)端子侧,正弦 50.000 Hz、1.000 Vrms",
"ua_mv 中位数 ≈ 万用表 × 1000,偏差 ≤2 %",
"meas: ua_mv 为主;104 0x4011 只作交叉读数"),
("波形轮", "真 RMS ≠ 基波", C_TEAL, BG_TEAL, "three",
"同峰值 1.414 V 改方波 / 三角(面板有效值各异)",
"读数只跟基波:方波 ≈1278、三角 ≈817,±2 %",
"meas: ua_mv;抬头记万用表响应类型"),
("相位轮", "定量 · 录波", C_GREEN, BG_GREEN, "phase",
"UA(R31)与 UN(R28)同源同幅 Vpp 2.828 V",
("相位轮", "定量 · 取段", C_GREEN, BG_GREEN, "phase",
"UA(R31)与 UN(R28)同源同幅 1.000 Vrms",
"同段内相位差 ≈0°,B 通道改 90° 应跟到 ≈90°,±2°",
"wave_read.py fft 定量;phasor(hb) 只核象限"),
("测频轮", "A 档无数 = 过", C_AMBER, BG_AMBER, "sweep",
"UA 同幅值轮接法,扫 45 / 50 / 55 Hz",
"A 档如实报不可测:valid=0、UI 显 --;报 50.00 反而是 bug",
"freq(hb) 0.01 Hz;UI Hz"),
"freq(hb) 0.01 Hz;UI Hz、104 0x401D"),
("削顶轮", "负向 · 单独日志", C_ROSE, BG_ROSE, "clip",
"UB(R52):DC −1.00 V 保持 ≥5 s,或 2.0 Hz 正弦",
"连续 20 拍贴底 → rail / edge 锁存;50 Hz 正弦不锁存 = 预期",
"cal: rail / edge;录波再跑 shape 对表"),
("谐波轮", "定量 · 录波", C_PURPLE, BG_PURPLE, "harm",
"IA(R19):分三注,基波 Vpp 2.263、二五次各 Vpp 0.453",
"Ia2 / Ia5 各读数 ≈0.160 V,±2 %(最坏峰值 1.58 V 留余量)",
"连续 20 拍贴底 → rail / edge 锁存 + 104 该路 IV 清零;50 Hz 正弦不锁存 = 预期",
"cal: rail / edge;取段再跑 shape 对表"),
("谐波轮", "定量 · 取段", C_PURPLE, BG_PURPLE, "harm",
"IA(R19):基波 0.800 + 二次 0.160 + 五次 0.160 Vrms",
"Ia2 / Ia5 各 ≈0.160 Vrms,±2 %(最坏峰值 1.58 V 留余量)",
"wave_read.py fft --ch 4;UI Ia2 / Ia5 目检旁证"),
]
@@ -203,7 +203,7 @@ def thumb(x, y, w, h, kind, c):
out.append(line(x + pad, yc - amp, x + w - pad, yc - amp, c, 1.0, dash="5 4"))
out.append(line(x + pad, yc + amp, x + w - pad, yc + amp, c, 1.0, dash="5 4"))
out.append(poly(_sample(lambda t: math.sin(2 * math.pi * 2 * t), ix, y + pad, iw, ih), c))
out.append(txt(x + pad, y + pad - 1, "峰 ±1.414 V · 基波有效值 1.000 V", TINY_FONT, MUTED))
out.append(txt(x + pad, y + pad - 1, "峰 ±1.414 V · 基波 RMS 1.000 V", TINY_FONT, MUTED))
elif kind == "three":
yc = y + h / 2.0
amp = ih * 0.36
@@ -217,7 +217,7 @@ def thumb(x, y, w, h, kind, c):
sx = ix + i * slot
out.append(poly(_sample(fv, sx + 6, y + pad, slot - 12, ih, n=120), c, 1.8))
out.append(txt(sx + slot / 2, y + h - 2, names[i], TINY_FONT, MUTED, anchor="middle"))
out.append(txt(x + pad, y + pad - 1, "同设 Vpp 2.828 V(不是同有效值)", TINY_FONT, MUTED))
out.append(txt(x + pad, y + pad - 1, "同峰值对齐(不是同 Vrms)", TINY_FONT, MUTED))
elif kind == "phase":
out.append(line(x + 4, y + h / 2, x + w - 4, y + h / 2, FAINT, 0.8, dash="3 3"))
out.append(poly(_sample(lambda t: math.sin(2 * math.pi * 1.5 * t), ix, y + pad, iw, ih),
@@ -290,7 +290,7 @@ def build():
add(el)
ty = cy + 36 + THUMB_H + 16
for label, s, mx in (("注入", inj, 2), ("判据", rule, 2), ("读数", ex, 2)):
for label, s, mx in (("注入", inj, 2), ("判据", rule, 2), ("出口", ex, 2)):
add(txt(cx + 12, ty, label, LABEL_FONT, c, weight=700))
for ln in wrap(s, BODY_FONT, body_avail, mx, "card %s %s" % (name, label)):
add(txt(body_x, ty, ln, BODY_FONT, INK if label == "判据" else MUTED))
+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_sntp(SNTP 对时,UDP 出向;:8888 已退役)"],
"lftpd(FTP :21)", "time_sync(TCP 对时 :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", "时间同步(8888 退役,SNTP 宿主)"),
("timeSyncTask", 2560, "Normal", 1264, 648, 1296, 50, "PASS", "TCP 时间同步(8888)"),
("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_sntp(SNTP 出向对时)· ui_net_sink(远程显示 :7003)"],
"消费方:lftpd(FTP :21)· time_sync(对时 :8888)· ui_net_sink(远程显示 :7003)"],
)
L_QUEUE = dict(
title="消息队列", sub="osMessageQueue · CubeMX 生成,在 MX_FREERTOS_Init() 中创建", color="#7c3aed", bg="#f5f3ff",