From 2cffd1d2cf3f768e659180e39d503c91c58eed8e Mon Sep 17 00:00:00 2001
From: Wanderingss <1624155937@qq.com>
Date: Tue, 22 Sep 2026 06:52:58 +0800
Subject: [PATCH] =?UTF-8?q?=E6=96=87=E6=A1=A3=E7=BB=9F=E4=B8=80=EF=BC=9A?=
=?UTF-8?q?=E4=B8=89=E4=BB=BD=E9=93=BE=E8=B7=AF=E6=97=B6=E5=BB=B6=E6=96=87?=
=?UTF-8?q?=E6=A1=A3=E5=90=88=E5=B9=B6=E4=B8=BA=E9=93=BE=E8=B7=AF=E5=BB=B6?=
=?UTF-8?q?=E8=BF=9F=E6=B5=8B=E8=AF=95=E8=AF=B4=E6=98=8E=EF=BC=8C=E5=88=A0?=
=?UTF-8?q?=E8=BF=87=E7=A8=8B=E6=80=A7=E5=8E=86=E5=8F=B2=EF=BC=8C=E6=9B=B4?=
=?UTF-8?q?=E6=96=B0=E5=85=A8=E9=83=A8=E4=BA=A4=E5=8F=89=E5=BC=95=E7=94=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Drivers/BSP/NET/lftpd/lftpd.c | 8 +-
Drivers/BSP/NET/net_socket.c | 2 +-
docs/测试说明/m2_data/README.md | 2 +-
docs/测试说明/链路延迟测试说明.md | 765 +++++++++++++++++++-------
docs/测试说明/链路时延实测数据分析.md | 359 ------------
docs/问题记录/远程显示延迟分析.md | 436 ---------------
test/latency_peer.py | 4 +-
test/net_latency_test.c | 6 +-
test/net_latency_test.h | 6 +-
test/test_config.h | 2 +-
10 files changed, 576 insertions(+), 1014 deletions(-)
delete mode 100644 docs/测试说明/链路时延实测数据分析.md
delete mode 100644 docs/问题记录/远程显示延迟分析.md
diff --git a/Drivers/BSP/NET/lftpd/lftpd.c b/Drivers/BSP/NET/lftpd/lftpd.c
index 5c8d3aa1..35658f96 100644
--- a/Drivers/BSP/NET/lftpd/lftpd.c
+++ b/Drivers/BSP/NET/lftpd/lftpd.c
@@ -320,7 +320,7 @@ static int cmd_list(lftpd_client_t *client, const char *arg)
int err = send_list(data_sock, client->directory);
/* 传输结束必须关闭数据连接:对端以 EOF 判定"数据传完"(LIST/RETR);
* 不关会让客户端一直读到超时(曾试"优雅断开+复用监听器"方案,因该语义限制
- * 与芯片行为未能稳定工作,见 链路时延实测数据分析.md D20 记录)。 */
+ * 与芯片行为未能稳定工作,见 链路延迟测试说明.md §8.2)。 */
lftpd_inet_close(data_sock);
client->data_socket = -1;
@@ -352,7 +352,7 @@ static int cmd_nlst(lftpd_client_t *client, const char *arg)
int err = send_nlst(data_sock, client->directory);
/* 传输结束必须关闭数据连接:对端以 EOF 判定"数据传完"(LIST/RETR);
* 不关会让客户端一直读到超时(曾试"优雅断开+复用监听器"方案,因该语义限制
- * 与芯片行为未能稳定工作,见 链路时延实测数据分析.md D20 记录)。 */
+ * 与芯片行为未能稳定工作,见 链路延迟测试说明.md §8.2)。 */
lftpd_inet_close(data_sock);
client->data_socket = -1;
@@ -459,7 +459,7 @@ static int cmd_retr(lftpd_client_t *client, const char *arg)
int err = send_file(data_sock, path);
/* 传输结束必须关闭数据连接:对端以 EOF 判定"数据传完"(LIST/RETR);
* 不关会让客户端一直读到超时(曾试"优雅断开+复用监听器"方案,因该语义限制
- * 与芯片行为未能稳定工作,见 链路时延实测数据分析.md D20 记录)。 */
+ * 与芯片行为未能稳定工作,见 链路延迟测试说明.md §8.2)。 */
lftpd_inet_close(data_sock);
client->data_socket = -1;
@@ -518,7 +518,7 @@ static int cmd_stor(lftpd_client_t *client, const char *arg)
int err = receive_file(data_sock, path);
/* 传输结束必须关闭数据连接:对端以 EOF 判定"数据传完"(LIST/RETR);
* 不关会让客户端一直读到超时(曾试"优雅断开+复用监听器"方案,因该语义限制
- * 与芯片行为未能稳定工作,见 链路时延实测数据分析.md D20 记录)。 */
+ * 与芯片行为未能稳定工作,见 链路延迟测试说明.md §8.2)。 */
lftpd_inet_close(data_sock);
client->data_socket = -1;
diff --git a/Drivers/BSP/NET/net_socket.c b/Drivers/BSP/NET/net_socket.c
index 9d32765b..14f15e0c 100644
--- a/Drivers/BSP/NET/net_socket.c
+++ b/Drivers/BSP/NET/net_socket.c
@@ -1051,7 +1051,7 @@ static int net_send_locked(net_sock_t *p_sock, const void *buf, int len) {
#if defined(TEST_LATENCY_PROBE)
/* L3 探针起点:发送缓冲写入刚完成
- * (样本 = 写缓冲 → SENBUF_FREE 等待,见 链路延迟测试说明 §1 L3) */
+ * (样本 = 写缓冲 → SENBUF_FREE 等待,见 链路延迟测试说明 §3 L3) */
uint32_t c_l3 = lat_cycles_now();
#endif
diff --git a/docs/测试说明/m2_data/README.md b/docs/测试说明/m2_data/README.md
index 8e4ca1d9..e16a0025 100644
--- a/docs/测试说明/m2_data/README.md
+++ b/docs/测试说明/m2_data/README.md
@@ -1,7 +1,7 @@
# M2 链路延迟实测数据(Phase 13,安静档)
本目录保存 M2(Phase 13:芯片 + 队列时延基线)的**原始实测产物**。判定阈值与解读见
-[`../链路延迟测试说明.md`](../链路延迟测试说明.md) §4.2;缺陷定位过程见该文 §6.1。
+[`../链路延迟测试说明.md`](../链路延迟测试说明.md) §6.2;缺陷总表见 §8.1。
## 1. 运行元数据
diff --git a/docs/测试说明/链路延迟测试说明.md b/docs/测试说明/链路延迟测试说明.md
index 1abf8724..457ddd4b 100644
--- a/docs/测试说明/链路延迟测试说明.md
+++ b/docs/测试说明/链路延迟测试说明.md
@@ -1,79 +1,164 @@
-# 链路延迟测试说明(CH395F 网络传输分段时延基线)
+# 链路延迟测试说明(分段基线 / 实测现状 / 故障对照)
-> **模块名称**:CH395F 网络链路分段时间延测试
-> **文档目的**:把"一次画面更新/一次收发"拆成独立可测的段,为每段建立**正常基线 + 阈值判定**,
-> 输出自动 verdict,使"延迟"从主观感觉变为"哪一段超了基线"的客观判断依据。
-> **日期**:2026-09-22
-> **状态**:M1(Phase 12,SPI 层基线)已实现;M2~M5 待排期
-> **配套文档**:`docs/问题记录/远程显示延迟分析.md`(延迟预算与 D1~D10 候选原因)、
-> `CH395F_Test_Guide.md`(套件构建模式与执行顺序)、`自动化测试流程.md`(@test 闭环)
+> **模块名称**:CH395F 网络链路(远端显示 RemoDispBus / FTP / 测试链路)
+> **文档目的**:链路时延的**唯一权威文档**——怎么测、当前实测值是多少、慢在哪一段、
+> 哪些缺陷已修/未修、新异常出现时查什么。
+> **数据日期**:2026-09-22 台架(COM6 @115200;STM32F407ZGTx @168MHz + CH395F,SPI2 分频 8)
+> **数据文件**:`m2_data/`、`m3_data/`(JSON + 串口证据片段);本文所有数字均可回溯到这些文件
+> **代码状态**:D10 / E-02 / D17 / D12 / D14 / D15 / D16 已修复并台架复验(§8);
+> D20(开机首个 FTP 会话第 2 个数据连接 425)已定位未修(§8.2)
+> **合并说明**:本文统一原《链路延迟测试说明》《链路时延实测数据分析》《远程显示延迟分析》
+> (2026-09-22);过程性历史记录(逐日变更、调试过程)不再保留,现状以本文与 git 历史为准。
---
-## 0. 目标与设计原则
+## 1. 一句话结论(当前状态)
-**目标**:对 CH395F 网络传输链路的每一段分别测量、分别判定,建立可重复回归的时延基线。
-任一段超标即能定位到对应原因(D1~D10,见《远程显示延迟分析》)并指向对应解决方案。
+**远端显示一帧(3209B 全帧)端到端实测中位 66.0ms,其中约 52.6ms(79%)是帧尾那 137B 小段的等待**;
+MCU 侧真实处理只占 7.1ms,任务拾取/调度约 6.8ms。同一客户端、同一台 PC 打 PC 模拟器只要 6.8ms
+(Δ = 59.2ms 为 CH395F 链路净贡献,且模拟器完全没有尾批延迟)。
-| 原则 | 说明 |
-|---|---|
-| **同一把尺子量两端** | PC 端 peer 脚本对**模拟器**和**MCU**使用完全相同的测量流程(同一协议、同一计时点),结果直接相减,差值 = CH395F 栈的净贡献(Δ,见 §1),消除"客户端成本"这个混淆项 |
-| **分段隔离 + 统计量** | 每段单独测 N 次,报 `min/med/p95/max` + **≥500ms 尖峰计数**(TCP 重传 500ms 粒度的指纹,D1 自动显形);Wireshark 仅用于需要逐段精确视图时的补充手段 |
-| **非侵入、非破坏** | 新增测量代码藏在 `#ifdef TEST_LATENCY_PROBE`(仅测试构建定义)→ 产品构建零代码零开销;不改任何设备持久状态(不写 IP/卷/BBT)。但 Phase 12/13/14 独占 CH395F 或占用 :7003 → **仅限台架**,与现有 HW 层约定一致 |
-| **基线锁板** | 初始阈值取自静态推算;**首块台架实测后以实测值重锁为该板基线**(更新 §4 表),后续每次回归与基线比对 |
+- **尾批根因(已确证,无需抓包)**:芯片按 Nagle 扣住帧尾的次 MSS 段(MSS=1024,3209 = 3×1024 + 137)
+ 等对端 ACK,Windows 延迟 ACK 把这一等推迟 ~50ms(§7.3 五组带内实验)。
+- **解法在客户端一侧(生产客户端已实装)**:Windows 连接时关延迟 ACK(帧耗时 **20.7ms**,3.3×)/
+ 非 Windows 用"入站轻推"兜底(**29.4ms**,2.3×);协议与固件都不需要改(§7.4)。
+- **设备侧无可行解**(帧长 3209 为质数、TX FIFO = MSS、CH395F 无 Nagle 开关)→ 不必再投入。
+- **缺陷**:D10(断线重监听 588ms 冻结 netTask)、E-02(一条空闲 FTP 控制连接冻结全网 17.7s)、
+ D17(INIT 应答帧字节数字段恒 0)等已量化并**修复 + 台架复验**(§8.1);D20 已定位未修(§8.2)。
-## 1. 链路分段与测量点
+---
-| 段 | 名称 | 测什么 | 测点 | 方法 | 预期正常值* | 超标指向 |
-|---|---|---|---|---|---|---|
-| **L1** | SPI 字节命令 | 一条小 SPI 事务(GET_IC_VER / CHECK_EXIST / GLOB_INT,4~7B)端到端 | MCU,DWT µs | **Phase 12(M1)**,N=100/命令 | ≤60µs | SPI 走线/PCB/分频/DMA 配置 |
-| **L2** | SPI DMA | `ch395f_read_recv_buf(N)` 完整 DMA 事务(含 CS 切换),N ∈ {4, 64, 1024, 2048} | MCU,DWT µs | **Phase 12(M1)**,N=100/档 | 1KB ≈ 1.56ms(5.25Mbit/s 理论)+0.1~0.3ms 固定开销 | SPI2/DMA/总线异常 |
-| **L3** | 发送分块握手 | `write_send_buf(1KB)` → `SENBUF_FREE` 的等待时长(= `net_send_locked` 每 chunk 的真实等待) | MCU(netTask 上下文) | Phase 13(M2),每帧 4 chunk × 50 帧 | 1~5ms | 芯片发送路径 / **重传(D1) / 链路(D7)** |
-| **L4** | 芯片→线上+PC | 数据交入芯片协议栈 → PC 首见字节 / 收齐全帧(peer 逐 `recv()` 批次打时间戳 → 字节到达曲线) | PC(peer) | Phase 13/14(M2/M3) | 首段 <10ms;**段间间隙 ≥500ms = 重传** | **D1/D4/D7**("网络延迟"直接证据) |
-| **L5** | 队列往返 | 调用方视角 `net_recv(DONTWAIT)` / `net_send` 全程 = 入队 + netTask 调度 + 处理 + 通知 | MCU(调用方计时) | Phase 13(M2),N=100,**两档**:静默基线 + 压力档(并发 syslog/FTP 流量) | 静默 ≤5ms;压力档 ≤100ms | **D3(E-02 头阻塞)** / D5 调度 |
-| **L6** | 请求→首字节(测试路径) | peer 发 10B 请求 → MCU 收齐并启动应答(服务端视角);与 PC 侧"发→首字节"对照,差值 = 线路 + PC 栈 | MCU + PC 双侧 | Phase 13(M2) | ≤50ms | MCU 收路径/调度 |
-| **L7** | 整帧 3209B 往返 | LCDMEM 全帧:PC 发 → 收齐(含 L3+L4) | PC 为主 | Phase 13(M2) | ≤150ms;**spike(≥500ms)=0** | D1+D2+D4 综合 |
-| **L8** | 生产路径帧周期 | **真实 uiTask + 真实 `ui_net_sink`**(:7003)的完整帧周期:请求拾取(10ms 拍)+ 队列 + 发送 + L4 + peer 侧编码 | MCU 探针 + PC | Phase 14(M3),N=50 帧 | 110~200ms(含 peer 100ms 节拍) | 全段(用户真实体感) |
-| **L9** | 连接/重监听 | connect→ESTABLISHED;close→relisten 成功 | MCU(net_poll 观测点) | Phase 14(M3) | ≤150ms / ≤10ms | 握手/队列;**≥500ms = D10 的 `osDelay(500)` 路径** |
-| **L10** | 日志税 | 一行 60 字符 `printf`(现 fputc 逐字节 @115200)耗时 | MCU | Phase 13 附带(M2) | ≈5ms(**预期值本身偏高**,作为 D6 的量化证据) | D6(信息项) |
-| **L11** | uiTask 节拍 | 生产循环实测周期(`osDelay(10)` 之外多花多少 = LVGL 渲染 + 渲染中排空) | MCU 探针 | Phase 14(M3) | 10~15ms | **D5/D9** |
-| **Δ** | CH395F 净贡献 | `L8(MCU) − L8(模拟器)`,同一 peer 交替跑 | PC | Phase 14 收尾(M3) | ≤80ms | 直接回答"网络延迟"占多少 |
+## 2. 链路与关键参数
-\* 初始阈值取自《远程显示延迟分析》§1.2/§1.3 的静态推算;首块台架实测后按 §4 重锁。
+### 2.1 拓扑与服务
-## 2. 测试架构
+| 服务 | 端口 | 后端 |
+|---|---|---|
+| 远端显示 RemoDispBus | 7003 | `ui_net_sink`(真实 uiTask 生产路径) |
+| FTP(lftpd) | 21 | 控制 + PASV 数据通道(数据通道复用监听 Socket,CH395F 单连接模型) |
+| 对时 | 8888 | `time_sync` |
+| 时延测试链路(M2) | 8080 | Phase 13 测试协议(服务器驱动) |
-### 2.1 文件与注册
+客户端 = DTU-RemtoeLCD(`remo_disp_server.py`,与本工程同级的独立项目,目录名含历史拼写 `Remtoe`)。
+MCU 端与 PC 模拟器共用 `ui_net_sink.c`:连模拟器(Winsock)流畅、连 MCU(CH395F)有明显延迟 →
+差异全部落在 `net_*` 的 MCU 后端,协议与客户端实现已排除。
+
+### 2.2 一帧的旅程(按键 → 看到画面)
+
+```mermaid
+sequenceDiagram
+ participant B as 浏览器
+ participant F as Flask/SocketIO
refresh_loop
+ participant C as CH395F
+ participant N as netTask
+ participant U as uiTask
+
+ B->>F: 按键 (socketio "key")
+ F->>C: KEY 帧 10B(on_key 不触发取帧)
+ Note over F: D8 最多等 100ms 才轮到下一次 fetch_screen
+ F->>C: LCDMEM 请求 10B
+ C-->>N: RECV 中断 → net_poll
+ Note over N: D3 netMsgQueue 头阻塞窗口(已修复:5ms 分片+让出)
+ U->>N: net_recv(64B)(队列 + TaskNotify)
+ Note over U: D5 最多 10ms 相位 + LVGL 渲染占时(渲染实测 ~1ms)
+ N-->>U: 按键注入 UI
+ Note over U: D9 下一拍才渲染(step 在 poll 之前)
+ U->>N: net_send 3209B
+ loop D2 4 次串行分块(chunk 握手实测 219µs/块,非瓶颈)
+ N->>C: WRITE_SEND_BUF ≤1KB(SPI DMA ≈1.6ms/KB)
+ C-->>N: SENBUF_FREE 中断
+ end
+ Note over C: D1 丢一段 → 重传 +500ms(现 8×500ms;D7 供包)
+ C-->>F: 3×1024B 满段 + 137B 尾段
+ Note over F: D4 Windows 延迟确认 40~200ms(生产客户端已关)→ 尾段扣住 ~50ms
+ F->>B: PNG → base64 → socketio → canvas
+```
+
+### 2.3 关键量化常量
+
+| 常量 | 值 | 来源 |
+|---|---|---|
+| 屏幕 / 帧大小 | 160×160 **1bpp** = 3200 B | `ui_net_sink.c`(`RM_FRAME_W/H`) |
+| LCDMEM 应答 wire 长度 | 4(头) + 4(偏移) + 3200(位图) + 1(CRC) = **3209 B** | `remo_send_init` / `remo_send_frame` |
+| TCP MSS | 1024 | `CH395F_TCP_MSS`(`ch395f.h`) |
+| CH395F 每 socket 发送缓冲 | 2 块 × 512B = **1KB** | `ch395f.h` |
+| `NET_SEND_CHUNK_MAX` | **1KB**(受发送缓冲与 DMA 载荷 2048 双重钳制) | `net_socket.c` |
+| LCDMEM 一帧分块数 / 分段数 | **4**(1024×3 + 137) | 3209 ÷ 1024 |
+| CH395F 内部 RAM | 48 块 × 512B = 24KB,`net_init` 按 8 socket 均分 6 块 | `net_init` |
+| TCP 重传 | **8 次 × 500ms = 4s 窗口**(固定周期,非 RTT 自适应) | `ch395f_set_retrans_count/period` |
+| SPI2 速率 | APB1 42MHz ÷ 8 ≈ **5.25 Mbit/s** → 1KB ≈ **1.6ms**(分频 8 为 PCB 约束,勿"优化") | `Src/spi.c` |
+| 发送等待超时 | 1s(`NET_SEND_TIMEOUT_MS`) | `net_config.h` |
+| 接收超时 | 30s(`NET_RECV_TIMEOUT_MS`) | `net_config.h` |
+| 调试串口 | USART1 115200,**逐字节阻塞发送**(`fputc` = `HAL_UART_Transmit` 单字节) | `Src/usart.c` |
+| LVGL 刷新周期 / 色深 | 20ms / 1bpp(`RENDER_MODE_FULL`) | `lv_conf.h`、`ui_port.c` |
+
+### 2.4 两种数据区制
+
+| 区制 | 实测 | 主导成本 | 适用 |
+|---|---|---|---|
+| **小帧交互**(≈1 chunk) | 3209B → 66ms(客户端关延迟 ACK 后 → 20.7ms) | 固定尾批惩罚 ~52ms(已客户端侧解决,§7.3/§7.4) | LCDMEM 整帧轮询 |
+| **批量流式**(多满块) | 4KB/7.8ms、64KB/125ms ≈ 2ms/KB | SPI DMA(L2 = 1.61ms/KB) | 大文件 / FTP 传数据 |
+
+两条独立的优化线:小帧交互的尾批已定性并解决;批量流式仍受 SPI DMA 约束。
+
+---
+
+## 3. 分段定义与测量点
+
+| 段 | 名称 | 测什么 | 测点 / 方法 | 基线(实测,详见 §6) | 超标指向 |
+|---|---|---|---|---|---|
+| **L1** | SPI 字节命令 | 一条小 SPI 事务(GET_IC_VER / CHECK_EXIST / GLOB_INT,4~7B)端到端 | MCU,DWT µs,Phase 12,N=100/命令 | 10 / 15 / 15µs | SPI 走线/PCB/分频/DMA 配置 |
+| **L2** | SPI DMA | `ch395f_read_recv_buf(N)` 完整 DMA 事务(含 CS 切换),N ∈ {4, 64, 1024, 2048} | MCU,DWT µs,Phase 12,N=100/档 | 20 / 114 / **1611** / 3208µs(斜率 1.56µs/B + 固定 14µs) | SPI2/DMA/总线异常 |
+| **L3** | 发送分块握手 | `write_send_buf(1KB)` → `SENBUF_FREE` 的等待时长(= `net_send_locked` 每 chunk 的真实等待) | MCU(netTask 上下文),Phase 13 | **219µs**/chunk | 芯片发送路径 / 重传(D1) / 链路(D7) |
+| **L4** | 芯片→线上+PC | 数据交入芯片协议栈 → PC 首见字节 / 收齐全帧(peer 逐 `recv()` 批次打时间戳) | PC(peer),Phase 13/14 | 首批 8.6ms;**段间间隙 ≥500ms = 重传** | D1/D4/D7("网络延迟"直接证据) |
+| **L5** | 队列往返 | 调用方视角 `net_recv(DONTWAIT)` / `net_send` 全程 = 入队 + netTask 调度 + 处理 + 通知 | MCU(调用方计时),Phase 13,**两档**:静默基线 + 压力档(并发 FTP 流量) | 静默:收 5997µs / 发 987µs | D3(E-02 头阻塞,已修) / D5 调度 |
+| **L6** | 请求→首字节 | peer 发 10B 请求 → MCU 收齐并回显(服务端视角);与 PC 侧对照,差值 = 线路 + PC 栈 | MCU + PC 双侧,Phase 13 | MCU 319µs / PC 1.4ms | MCU 收路径/调度 |
+| **L7** | 整帧 3209B 往返 | LCDMEM 全帧:PC 发 → 收齐(含 L3+L4) | PC 为主,Phase 13 | 61.0ms(M2);**spike(≥500ms)=0** | D1+D2+D4 综合 |
+| **L8** | 生产路径帧周期 | **真实 uiTask + 真实 `ui_net_sink`**(:7003)完整帧周期:请求拾取(10ms 拍)+ 队列 + 发送 + L4 + peer 侧编码 | MCU 探针 + PC,Phase 14 | 真机 66.0ms / 模拟器 6.8ms(含 peer 100ms 节拍) | 全段(用户真实体感) |
+| **L9** | 连接/重监听 | connect→ESTABLISHED;close→relisten 成功 | MCU(net_poll 观测点),Phase 14 | relisten **89ms**(D10 修复后) | 握手/队列;**≥500ms = D10 回归** |
+| **L10** | 日志税 | 一行 60 字符 `printf`(现 fputc 逐字节 @115200)耗时 | MCU,Phase 13 附带 | **4396µs/行** | D6(待办 P0-b/S8) |
+| **L11** | uiTask 节拍 | 生产循环实测周期(`osDelay(10)` 之外多花多少 = LVGL 渲染 + 渲染中排空) | MCU 探针,Phase 14 | **10986µs**(渲染仅 ~1ms/帧) | D5/D9(实测均非瓶颈) |
+| **Δ** | CH395F 净贡献 | `L8(真机) − L8(模拟器)`,同一 peer 交替跑 | PC,Phase 14 | **59.2ms**(阈值 ≤80ms) | 直接回答"网络延迟"占多少 |
+
+---
+
+## 4. 测试架构
+
+### 4.1 文件与注册
| 文件 | 职责 | 注册 |
|---|---|---|
| `test/net_latency_test.h` | 计时/统计/报告 API + Phase 入口声明 | 头文件随工程包含路径,无需注册 |
-| `test/net_latency_test.c` | DWT µs 时基 + 统计 + Phase 12 用例(M1);Phase 13/14 用例(M2/M3 追加) | **仅 `MDK-ARM/STM32F407-Demo.uvprojx`**(Test 组;模拟器 CMake 不编 `test/*.c`,与现有测试文件一致) |
+| `test/net_latency_test.c` | DWT µs 时基 + 统计 + Phase 12/13/14 用例 | **仅 `MDK-ARM/STM32F407-Demo.uvprojx`**(Test 组;模拟器 CMake 不编 `test/*.c`,与现有测试文件一致) |
| `test/latency_peer.py` | PC 端 peer(纯标准库):`--mode net`(M2)/`--mode remo`(M3),`--samples N`,`--json out.json` | 无(Python) |
-| 本文档 | 规划/流程/判定表/基线 | — |
+| `test/ftp_regression.py` | D20 复现/回归:单会话 7 次数据连接 + 1KB/32KB 往返内容校验,退出码 = 是否全通过 | 无(Python) |
+| 本文档 | 分段/架构/流程/基线/缺陷现状 | — |
-### 2.2 阶段编号(沿用 `ch395f_test.h` 互斥体系)
+### 4.2 阶段编号与构建配置(沿用 `ch395f_test.h` 互斥体系)
```
ENABLE_PHASE12_TESTS [硬] SPI 时延基线(L1/L2)—— testTask 独占 CH395F,**无需 PC**
-ENABLE_PHASE13_TESTS [网] 芯片+队列时延(L3~L7/L10)—— 需 latency_peer.py --mode net (M2)
-ENABLE_PHASE14_TESTS [网] 生产路径时延(L8~L11 + Δ)—— 真实 uiTask/:7003,peer --mode remo (M3)
+ENABLE_PHASE13_TESTS [网] 芯片+队列时延(L3~L7/L10)—— 需 latency_peer.py --mode net(:8080)
+ENABLE_PHASE14_TESTS [网] 生产路径时延(L8~L11 + Δ)—— 真实 uiTask/:7003,peer --mode remo
```
-- Phase 12 归 HW 组,13/14 归 NET 组 → 与现有"两层互斥 `#error`"自动兼容;
-- 13 与 14 同属 NET 组可同启(13 用 testTask 私有监听端口,14 用生产 :7003,socket 不冲突);
-- 全部非破坏性 → 只读级,**台架执行**。
+- Phase 12 归 HW 组,13/14 归 NET 组;13 与 14 可同启(13 用 testTask 私有监听 :8080,14 用生产 :7003,Socket 不冲突);
+- 测试套件总开关 `TEST_SUITE_CH395F`(`test/test_config.h`);**当前工作副本 = 测试构建**(Phase 14 已启用),
+ 产品构建 = `test_config.h` 与 `ch395f_test.h` 的测试宏全部注释;
+- 探针宏 `TEST_LATENCY_PROBE` = `TEST_SUITE_CH395F` 且(Phase 13 或 14 启用)→ 仅测试构建定义;
+- 全部非破坏性 → 只读级,**仅限台架**。
-### 2.3 时基(DWT CYCCNT)
+### 4.3 时基(DWT CYCCNT)
- 工程内首个 DWT 用户(`sys_clock` 是纯 HAL tick,无 µs 能力);`lat_time_init()` 幂等使能
`CoreDebug->DEMCR | TRCENA` + `DWT->CYCCNT=0` + `DWT->CTRL | CYCCNTENA`;
- 分辨率 1/168MHz ≈ 5.95ns;换算用 `SystemCoreClock`(不写死频率);
-- **32 位回绕 = 25.17ms**:M1 各段(<5ms)安全;M2/M3 的长段(整帧/重传尖峰)改用
- **累积采样**(等待循环中每 ~10ms 采样一次计数差并累加),届时在 Phase 13/14 代码内实现;
+- **32 位回绕 = 25.17ms**:M1 各段(<5ms)与 M2 调用方侧测量(单次 <25ms)用单次读数差;
+ 跨 25ms 的段(L4/L8 整帧与重传尖峰)由 PC 端 peer 计时,或 MCU 侧**累积采样**
+ (等待循环中每 ~10ms 采样计数差并累加,`lat_mark_us`);
- 每次运行先打一行校准线 `[LAT] cal core_clk=...`,台架可用逻辑分析仪核对时基正确性。
-### 2.4 统计与报告格式
+### 4.4 统计、报告行与校验标记
- 每段 N 次采样(静态数组,零堆):`min / med / p95 / max` + `spikes`(≥ 尖峰阈值计数);
- MCU 串口报告行(**纯 ASCII**,`verify_log.py` 可校验):
@@ -90,67 +175,69 @@ ENABLE_PHASE14_TESTS [网] 生产路径时延(L8~L11 + Δ)—— 真实 uiT
- verdict 语义:`OK`(全部段达标)/ `WARN`(某段 med 超 warn 阈值,**计 PASS**,提示重锁阈值)/
`FAIL`(某段 med 超 crit 阈值或 spikes 超限 → 套件判 FAIL);
-- **字段名刻意用 `crit` 而不用 `fail`**(2026-09-22 首跑实证):`verify_log.py` 默认
- Forbidden 集含大小写不敏感的 `FAIL`,报告行里出现 `fail<=` 字面量会把**自己的 OK 行**
- 判成 Forbidden 命中;`verdict=FAIL` 行才是真正的失败信号,须被捕获;
-- 校验标记(**按构建模式区分**,`Network Init` 仅 netTask 的 NET 分支打印,硬件层模式
- netTask 自挂起 → 无此标记):
+- **字段名刻意用 `crit` 而不用 `fail`**:`verify_log.py` 默认 Forbidden 集含大小写不敏感的 `FAIL`,
+ 报告行里出现 `fail<=` 字面量会把自己的 OK 行判成 Forbidden 命中;`verdict=FAIL` 行才是真正的失败信号;
+- 校验标记(**按构建模式区分**,`Network Init` 仅 netTask 的 NET 分支打印,硬件层模式 netTask 自挂起 → 无此标记):
- 硬件层(Phase 12):Required `App init OK;net init: OK;PHY: 100M FULL;[LAT] report complete`
- NET 层(Phase 13/14):Required `App init OK;Network Init;net init: OK;[LAT] report complete`
- - **Forbidden 必须覆盖默认集**(2026-09-22 首跑实证):套件 TEST_REPORT 行
- `(... failed=0)` 含 "fail" 子串会被默认 `FAIL` 模式自捕获 →
+ - **Forbidden 必须覆盖默认集**:套件 TEST_REPORT 行 `(... failed=0)` 含 "fail" 子串会被默认 `FAIL` 模式自捕获 →
`--forbidden "HardFault;assert;[ERR];[FAIL];verdict=FAIL"`
(真失败仍被捕获:TEST_CHECK 失败行含 `[FAIL]`+`[ERR]`,时延段失败含 `verdict=FAIL`)
- **M2 追加判定**(Phase 13):Required 再含 `=== Phase 13: 1/1 PASSED`;peer 侧以
**退出码 0**(收到 `0xFF` 并打印汇总)= PASS,JSON 里 `l7_crc.fail == 0` 为帧格式正确性证据。
-### 2.5 PC 端 peer 与关联(M2/M3)
+### 4.5 PC 端 peer 协议(`latency_peer.py`)
-- `latency_peer.py`:纯标准库(socket/json/time/argparse);`--mode net` 走测试协议
- (M2 已实现),`--mode remo` 模拟 DTU-RemoteLCD 客户端(M3);
- 输出每帧 `req2first / total / recv 批次到达时间` + 直方图 + spike 计数 + JSON 报告;
-- **M2 测试协议(`--mode net`,端口 8080,服务器驱动,MCU 逐个下发操作字节)**:
+纯标准库(socket/json/time/argparse);`--mode net` 走 M2 测试协议,`--mode remo` 模拟生产
+DTU-RemoteLCD 客户端;输出每帧 `req2first / total / recv 批次到达时间` + 直方图 + spike 计数 + JSON 报告。
- | 操作 | 值 | 语义 | peer 侧动作 |
- |---|---|---|---|
- | HELLO | `5A A5 58 01` | 会话起始 | 连接后立即发送;MCU 识别后重置会话状态机 |
- | L5_RECV | `0x01` | 对端灌入 | 20×100B,**50ms 间隔**(≈2KB/s,低于 MCU 抽干速率);测非阻塞收的队列往返 |
- | L5_SEND | `0x02` | MCU 连发 | 收 2000B(20×100B 阻塞发) |
- | L6 | `0x03` | 小请求/回显 | 发 10B 模式请求 → 校验 MCU 原样回显;逐样本重复 |
- | L7 | `0x04` | 全帧读取 | 发 9B LCDMEM 请求 → 收 3209B 全帧(4 批),逐样本重复 |
- | L3 | `0x05` | 4KB flood | 排空 16×4096B(=64KB);MCU 侧 chunk 等待由探针测 |
- | DONE | `0xFF` | 会话结束 | 打印汇总并退出(exit 0 = PASS) |
+**M2 测试协议(`--mode net`,端口 8080,服务器驱动:MCU 逐个下发操作字节)**
+
+| 操作 | 值 | 语义 | peer 侧动作 |
+|---|---|---|---|
+| HELLO | `5A A5 58 01` | 会话起始 | 连接后立即发送;MCU 识别后重置会话状态机 |
+| L5_RECV | `0x01` | 对端灌入 | 20×100B,**50ms 间隔**(≈1KB/s,低于 MCU 抽干速率);测非阻塞收的队列往返 |
+| L5_SEND | `0x02` | MCU 连发 | 收 2000B(20×100B 阻塞发) |
+| L6 | `0x03` | 小请求/回显 | 发 10B 模式请求 → 校验 MCU 原样回显;逐样本重复 |
+| L7 | `0x04` | 全帧读取 | 发 9B LCDMEM 请求 → 收 3209B 全帧(4 批);逐样本重复 |
+| L3 | `0x05` | 4KB flood | 排空 16×4096B(=64KB);MCU 侧 chunk 等待由探针测 |
+| DONE | `0xFF` | 会话结束 | 打印汇总并退出(exit 0 = PASS) |
- **帧格式严格照生产 RemoDispBus**(`ui_net_sink.c` `remo_handle`/`remo_send`):
- `[TAG][CMD][LEN_H][LEN_L][DATA][CRC]`,**CRC = 仅对 DATA 段逐字节异或**,
- 整帧 = 4 + len + 1;LCDMEM 请求 = `AA 03 00 04 00 00 00 00 00`(LEN=4 即"4B 偏移"的
- 数据段长度,偏移须为 0),应答 = `BB 03 0C 84` + `[偏移u32=0][位图3200B]` + CRC = **3209B**;
-- **peer 重试包装**:3 次会话尝试(首次预算 `--wait`,其后 ≤60s),遇连接错误/超时重建连接
- 并重发 HELLO;MCU 侧 abort 后进"重连等待态"(见 §3.2),两侧构成闭环;
+ `[TAG][CMD][LEN_H][LEN_L][DATA][CRC]`,**CRC = 仅对 DATA 段逐字节异或**,整帧 = 4 + len + 1;
+ LCDMEM 请求 = `AA 03 00 04 00 00 00 00 00`(LEN=4 即"4B 偏移"的数据段长度,偏移须为 0),
+ 应答 = `BB 03 0C 84` + `[偏移u32=0][位图3200B]` + CRC = **3209B**;
+- **peer 重试包装**:3 次会话尝试(首次预算 `--wait`,其后 ≤60s),遇连接错误/超时重建连接并重发 HELLO;
+ MCU 侧 abort 后进"重连等待态"(§5.2),两侧构成闭环;
- **尾批诊断/缓解选项(`--mode remo`,均无需抓包)**:`--ack-probe`(帧未收齐时发 1 个解析器忽略的字节,
- 让 TCP 段捎带 ACK)、`--ack-fast`(Windows 关闭本 socket 延迟 ACK:`SIO_TCP_SET_ACK_FREQUENCY=1`);
- 两者把帧尾 52ms 降到 3.9ms(结论见 `链路时延实测数据分析.md` §3.3);
+ 让 TCP 段捎带 ACK)、`--ack-fast`(Windows 关闭本 socket 延迟 ACK:`SIO_TCP_SET_ACK_FREQUENCY=1`)——
+ 两者把帧尾 52ms 降到 3.9ms(§7.3);
- **关联**:`serial.log` 行与 peer JSON 均带**主机时间戳** → 同一时间轴直接合并,无需 NTP;
需要逐段精确视图时再并行跑 Wireshark(`tcp.port==8080`)按时间对齐。
-### 2.6 MCU 侧探针(M3,`#ifdef TEST_LATENCY_PROBE`)
+### 4.6 MCU 侧探针(`#ifdef TEST_LATENCY_PROBE`)
产品代码(`net_socket.c`/`ui_net_sink.c`/`ui_task.c`/`ui_port.c`)内各数行打点,
经静态环形缓冲(总计 <1.5KB BSS)上报;**产品构建下该宏未定义,探针代码编译消失**,
两种构建配置都必须过 `@build`(0 错 0 警)。
-## 3. 执行流程
+- 多打点站点必须用 `lat_probe_add_seg()` 按段名过滤(探针窗口是串行全局窗口,直接打点会跨段污染,D18 教训);
+- 启用探针的构建必须在用例入口调 `lat_time_init()`(漏调 = 全 0 假绿,D19 教训;`med==0` 判无效兜底)。
-### 3.1 M1:Phase 12(SPI 层基线,**无需 PC**)
+---
+
+## 5. 执行流程
+
+### 5.1 M1:Phase 12(SPI 层基线,**无需 PC**)
```powershell
# 0) 前置:test/test_config.h 已启用 TEST_SUITE_CH395F + ENABLE_PHASE12_TESTS
-# (当前工作副本已按此配置;产品构建 = 全部注释掉)
+# (产品构建 = 全部注释掉)
# 1) 构建(0 错 0 警)
& "MDK-ARM\build.bat"
# 2) 一键闭环:开串口+清日志 → 烧录(reset.bat flash,Reset&Run)→ wait-idle → 标记校验
# 硬件层模式无 "Network Init" 标记(netTask 自挂起),且 TEST_REPORT 行含 "failed=0"
-# → Required/Forbidden 均用下方定制集(见 §2.4)
+# → Required/Forbidden 均用下方定制集(见 §4.4)
& '.venv\Scripts\python.exe' test\run_boot_test.py `
--required "App init OK;net init: OK;PHY: 100M FULL;[LAT] report complete" `
--forbidden "HardFault;assert;[ERR];[FAIL];verdict=FAIL"
@@ -158,9 +245,9 @@ ENABLE_PHASE14_TESTS [网] 生产路径时延(L8~L11 + Δ)—— 真实 uiT
& '.venv\Scripts\python.exe' tools\serial_tool\serial_tool.py log --grep "\[LAT\]"
```
-台架首跑已完成(2026-09-22):7 段全部 OK、零尖峰、p95≈min(零抖动);§4.1 基线已按实测重锁。
+台架已跑通:7 段全部 OK、零尖峰、p95≈min(零抖动),§6.1 基线已按实测重锁。
-### 3.2 M2:Phase 13(芯片+队列基线,**需 PC peer**)
+### 5.2 M2:Phase 13(芯片+队列基线,**需 PC peer**)
```powershell
# 0) 前置:test/test_config.h 启用 TEST_SUITE_CH395F,test/ch395f_test.h 仅启用
@@ -180,22 +267,57 @@ ENABLE_PHASE14_TESTS [网] 生产路径时延(L8~L11 + Δ)—— 真实 uiT
# Forbidden: HardFault / assert / [ERR] / [FAIL] / verdict=FAIL
```
-- **会话顺序(run3 重排后的最终顺序)**:HELLO → L5_RECV → L5_SEND → L3 → L10 → L6 → L7 → DONE。
+- **会话顺序**:HELLO → L5_RECV → L5_SEND → L3 → L10 → L6 → L7 → DONE。
易失步的 RX 协议段(L6/L7)置于末位,避免失步污染前置段;L6/L7 样本数 10,L3 = 16×4KB;
- **abort 后自动进"重连等待态"**(120s):DONTWAIT 轮询 scan `5A A5 58 01` → 命中即
- `p13_reinit()` + 重启会话(实测:attempt1 失败后 attempt2 在 +20s 自动接上并跑完);
- 期间周期自愈(10s 无 peer 且状态仍 LISTENING → 重发芯片重监听),**干净完成后停止自愈**
- (否则每 10s 拆建监听器,日志永不静默、台架无法判空闲);
+ `p13_reinit()` + 重启会话;期间周期自愈(10s 无 peer 且状态仍 LISTENING → 重发芯片重监听),
+ **干净完成后停止自愈**(否则每 10s 拆建监听器,日志永不静默、台架无法判空闲);
- **安静档判定**:`ftp_stress=0`(不加 `--ftp-stress`),即无 FTP/对时干扰的静默基线。
-M3 追加:`--mode remo --port 7003`(生产 RemoDispBus 客户端)+ `--ftp-stress`(E-02 复现)。
-M4(可选)将 3.1/3.2 编排为一键 `test/run_latency_test.py`(open/clear/flash/wait-idle/peer/collect/merge/verdict)。
+### 5.3 M3:Phase 14(生产路径 + 模拟器 Δ + 压力档)
-## 4. 判定基线表
+```powershell
+# M3(生产路径,需要设备已烧 Phase 14 固件)
+& "MDK-ARM\build.bat" # 0 错 0 警(Phase 14 构建)
+& '.venv\Scripts\python.exe' tools\serial_tool\serial_tool.py clear
+& "MDK-ARM\download.bat" # 先烧录,再起 peer(顺序不可反!)
+& '.venv\Scripts\python.exe' test\latency_peer.py --mode remo --host 192.168.1.100 --port 7003 `
+ --samples 45 --stress-from 15 --ftp-stress --json "docs\测试说明\m3_data\m3_dev_stress.json"
+# 模拟器基线(Δ 的对照端)
+& "simulator\build_sim.bat"; .\simulator\build\Debug\lvgl_sim.exe # 等 7003 就绪
+& '.venv\Scripts\python.exe' test\latency_peer.py --mode remo --host 127.0.0.1 --port 7003 `
+ --samples 50 --json "docs\测试说明\m3_data\m3_sim_quiet.json"
+# 尾批定性/解法(都不需要抓包工具)
+& '.venv\Scripts\python.exe' test\latency_peer.py --mode remo --host 192.168.1.100 --port 7003 `
+ --samples 30 --ack-probe --json "docs\测试说明\m3_data\m3_tail_ackprobe.json" # 带内补 ACK
+& '.venv\Scripts\python.exe' test\latency_peer.py --mode remo --host 192.168.1.100 --port 7003 `
+ --samples 30 --ack-fast --json "docs\测试说明\m3_data\m3_tail_ackfast.json" # 关延迟 ACK
+```
-### 4.1 M1 段(Phase 12)—— **已按台架首跑实测重锁**(2026-09-22,COM6 台架)
+### 5.4 D20 回归(`test/ftp_regression.py`)
-| 段(seg) | 实测 med(首跑,n=100) | 实测 min/max | warn(med >) | crit(med >) |
+单会话内 7 次数据连接 + 1KB/32KB 往返内容校验,退出码 = 是否全通过。
+**D20 的失败窗口在"每次开机后的第一个 FTP 会话"**(稳态会话恒过)→ 复现/验证必须以
+**复位开机**为边界(`MDK-ARM\reset.bat` 无参 = 只复位不烧录),首会话必跑、多轮统计。
+
+### 5.5 台架铁律与工作流陷阱
+
+1. **顺序**:`clear` → 烧录 → peer(`--wait`)→ `wait-idle` → Grep 日志文件判标记(采集必须先于烧录);
+2. **先烧录、后起 peer**——否则 peer 会连到**上一版固件**的 :7003(实测:首帧由旧固件应答后被复位 RST);
+3. peer 首连常因芯片空闲超时被 RST(10054,D13),`--wait` 重试是**正常路径**;
+4. **还原被回退的源文件后必须 touch**:`Copy-Item` 保留旧 mtime → Keil 判定"源比目标旧"→ 跳过重编译
+ → 固件里还是旧代码("改了却没效果")。排障路径:在改动处加临时日志 → 若无输出即"代码没进二进制",
+ 用特征字符串在 `.axf` 里验证(或核对编译日志里出现该文件);
+5. **net 热路径禁止逐调用打日志**:实测日志洪水把 `queue_rt_send` 抬高 7.8×、冲掉 `[LAT]` 证据行、
+ `wait-idle` 判不出空闲(D12-b);诊断改为在 `[LAT]` 段报告侧聚合。
+
+---
+
+## 6. 实测基线(2026-09-22 台架,已重锁)
+
+### 6.1 M1 段(Phase 12,SPI 层)
+
+| 段(seg) | 实测 med(n=100) | 实测 min/max | warn(med >) | crit(med >) |
|---|---|---|---|---|
| `spi_cmd_getver` | **10µs** | 10/12 | 40µs | 100µs |
| `spi_cmd_chkexist` | **15µs** | 15/16 | 40µs | 100µs |
@@ -205,56 +327,47 @@ M4(可选)将 3.1/3.2 编排为一键 `test/run_latency_test.py`(open/clea
| `spi_dma_1024` | **1611µs** | 1611/1612 | 3200µs | 6400µs |
| `spi_dma_2048` | **3208µs** | 3208/3210 | 6400µs | 12800µs |
-- 重锁规则:warn ≈ 2×med,crit ≈ 2×warn(兼顾板间/温度漂移与回归灵敏度:
- SPI 时钟减半 → 超 warn;再减半 → 超 crit;DMA 超时 1s → 必超 crit);
-- 首跑特征:p95 ≈ min(**零抖动**),时基校准行核对无误(168MHz,6µs/1024 周期);
-- DMA 斜率实测 1.56µs/B(≈ 5.25 Mbit/s,与 SPI2 分频 8 @42MHz APB1 设计值一致),
- 固定开销 ≈ 14µs(CS 切换 + DMA 配置)→ **每 chunk(1KB)SPI 成本基线 = 1.61ms**。
+DMA 斜率实测 1.56µs/B(≈5.25 Mbit/s,与 SPI2 分频 8 @42MHz APB1 设计值一致),固定开销 ≈14µs
+(CS 切换 + DMA 配置)→ **每 chunk(1KB)SPI 成本基线 = 1.61ms**。首跑 p95 ≈ min(零抖动),
+时基校准行核对无误(168MHz,6µs/1024 周期)。
-### 4.2 M2 段(Phase 13,芯片 + 队列)—— **已按台架实测重锁**(2026-09-22,COM6 台架;run12/run13/run15 三次独立 PASS)
+### 6.2 M2 段(Phase 13,芯片 + 队列)
+
+run12/run13/run15 三次独立完整 PASS(6 段全 OK、零尖峰、`1/1 PASSED`、peer exit 0);
+下表 = run15(最终代码)基线。
**MCU 侧(`[LAT]` 报告,安静档 `ftp_stress=0`)**
-| 段(seg) | 测量含义 | run12 med(min/max, n) | run13 med(min/max, n) | **run15(最终代码)med(min/max, n)** | warn(med >) | crit(med >) |
-|---|---|---|---|---|---|---|
-| `queue_rt_recv` | L5 对端灌 100B×20@50ms,MCU 非阻塞收的队列往返 | 6014µs(5365/6020, 19) | 6007µs(5392/6025, 20) | **5997µs**(5513/6033, 20) | 10ms | 100ms |
-| `queue_rt_send` | L5 MCU 阻塞发 100B×20 | 986µs(836/1204, 20) | 987µs(833/1209, 20) | **987µs**(838/1209, 20) | 10ms | 100ms |
-| `chunk_wait` | L3 16×4KB,芯片 chunk 内 SENBUF 等待(探针) | 235µs(215/238, 64) | 219µs(213/377, 64) | **219µs**(214/381, 64) | 10ms | 500ms(spike) |
-| `logtax_line` | 60 字符 printf @115200(本地) | 4396µs | 4396µs | **4396µs**(4394/4422) | 10ms | 25ms |
-| `req2first_mcu` | L6 10B 请求到齐 → 回显发出 | 310µs(296/338, 10) | 318µs(263/361, 10) | **319µs**(308/338, 10) | 20ms | 200ms |
-| `fullframe_mcu` | L7 9B 请求 → 3209B 全帧发给芯片(netTask 内 4 分块) | 6021µs(5986/6103, 10) | 6052µs(5966/6078, 10) | **6028µs**(5991/6066, 10) | 50ms | 500ms |
+| 段(seg) | 测量含义 | med(min/max, n) | warn(med >) | crit(med >) |
+|---|---|---|---|---|
+| `queue_rt_recv` | L5 对端灌 100B×20@50ms,MCU 非阻塞收的队列往返 | **5997µs**(5513/6033, 20) | 10ms | 100ms |
+| `queue_rt_send` | L5 MCU 阻塞发 100B×20 | **987µs**(838/1209, 20) | 10ms | 100ms |
+| `chunk_wait` | L3 16×4KB,芯片 chunk 内 SENBUF 等待(探针) | **219µs**(214/381, 64) | 10ms | 500ms(spike) |
+| `logtax_line` | 60 字符 printf @115200(本地) | **4396µs**(4394/4422, 10) | 10ms | 25ms |
+| `req2first_mcu` | L6 10B 请求到齐 → 回显发出 | **319µs**(308/338, 10) | 20ms | 200ms |
+| `fullframe_mcu` | L7 9B 请求 → 3209B 全帧发给芯片(netTask 内 4 分块) | **6028µs**(5991/6066, 10) | 50ms | 500ms |
**PC 侧端到端(peer JSON,含线路 + Windows 栈)**
-| 段 | run12 med(min/max) | run13 med(min/max) | run15 med(min/max) | 说明 |
-|---|---|---|---|---|
-| L6 端到端(op→收到回显) | 12.5ms(5.5/16.0) | 13.8ms(5.9/16.0) | 8.9ms(6.3/16.0) | 比 MCU 侧 `req2first_mcu` 多 ~10ms → PC/线路段 |
-| L7 首字节 | 10.9ms(7.9/16.2) | 11.8ms(7.5/17.7) | 11.3ms(7.8/16.5) | 同上量级 |
-| L7 全帧 3209B | 61.1ms(54.2/61.5) | 61.7ms(59.9/70.9) | 57.9ms(52.6/72.3) | 每帧固定 4 个 recv 批次 |
+| 段 | med(min/max) | 说明 |
+|---|---|---|
+| L6 端到端(op→收到回显) | 8.9ms(6.3/16.0) | 比 MCU 侧 `req2first_mcu` 多 ~7ms → PC/线路段 |
+| L7 首字节 | 11.3ms(7.8/16.5) | 同上量级 |
+| L7 全帧 3209B | 57.9ms(52.6/72.3) | 每帧固定 4 个 recv 批次;**尾批 137B 稳定迟到 ~50ms**(§7.3) |
+| L7 CRC 校验 | 10/0 fail | 帧格式/CRC 正确(生产格式对齐后) |
+| L3 flood 64KB(16×4KB) | 7.5~8.7ms/次 | ≈2ms/KB,满块无尾批惩罚 |
-> **⚠ 测量方法学(run14 实证)**:探针不得在 net 热路径逐调用打日志——曾加 `[NET] wait pick/block`
-> 两行后日志量 40KB → 1.04MB(9,644/9,779 行 = 98.6%),115200 下冲掉 `[LAT]` 证据行
-> (段行被截断/丢失)、`wait-idle` 永远判不出空闲,且把 `queue_rt_send` med 从 **987µs 抬到
-> 7685µs**(约 7.8×)、`queue_rt_recv` p95 抬到 21ms —— 即"观测行为本身改变被测对象"。
-> 该轮(run14)的 MCU 侧数据**不作为基线**,只保留 peer 侧 PASS 事实。
+**M2 三个结论**:
+1. **安静档不存在 500ms 台阶**——六段全部 `verdict=OK`、零尖峰、p95≈med;会话级路径的 MCU 净成本
+ 量级 ~6ms(`queue_rt_recv`),与 netTask 轮询节拍同量级;
+2. **芯片 chunk 级不是瓶颈**——`chunk_wait` ≈219µs ≈ 每 chunk SPI 成本(1.61ms)的 1/7,
+ `SENBUF_FREE` 等待远小于 SPI 搬运本身;
+3. **L7 全帧的最后 137B 尾巴稳定迟到 ~50ms**(10/10 帧)——尾批主导整帧时长,MCU 侧发完 3209B 仅 6.0ms
+ → 延迟不在 MCU 发送路径,在芯片 TCP 尾段发送/对端 ACK 交互(M3 定性见 §7.3)。
-| L7 CRC 校验 | 10/0 | 10/0 | 帧格式/CRC 正确(生产格式对齐后) |
+### 6.3 M3 段(Phase 14,生产路径 + 模拟器 Δ)
-- **结论一:安静档不存在 500ms 台阶**——三轮 6 段全部 `verdict=OK`、零尖峰、p95≈med;
- 会话级路径的 MCU 净成本量级为 **~6ms**(`queue_rt_recv`),与 netTask 轮询节拍同量级;
-- **结论二:芯片 chunk 级不是瓶颈**——`chunk_wait` med ≈ 219µs ≈ 每 chunk SPI 成本(1.61ms)的 1/7,
- 说明 `SENBUF_FREE` 等待远小于 SPI 搬运本身;
-- **结论三(M2 关键发现,待 M3 定性)**:L7 全帧的 **最后 137B 尾巴稳定迟到 ~50ms**——
- 10/10 帧的前 3 批 1024B 在 ~4ms 内到齐,第 4 批 137B 落在 +50ms(如 first=7.46ms、
- 尾批 61.20ms);该尾批主导整帧时长(total ≈ 61ms),MCU 侧发完 3209B 仅 6.0ms
- → 延迟不在 MCU 发送路径,而在**芯片 TCP 尾段发送/对端 ACK 交互**(候选:Nagle 式
- 小段等待 + 对端延迟 ACK)。M3 用 Wireshark 验证后再定对策(勿在未证实前改协议);
-- 重锁规则:warn ≈ 现状 ×1.7~10(保留回归灵敏度),crit 取"明显故障"档;
- `chunk_wait` 仍以 500ms spike 为 crit(D1 重传指纹)。
-
-### 4.3 M3 段(Phase 14,生产路径 + 模拟器 Δ)—— **已按台架实测重锁**(2026-09-22,COM6 台架)
-
-**MCU 侧(`[LAT]` 报告,真实 uiTask + `ui_net_sink` :7003)**
+**MCU 侧(`[LAT]` 报告,真实 uiTask + `ui_net_sink` :7003;修复后代码)**
| 段(seg) | 测量含义 | 实测 med(min/max, n) | warn(med >) | crit(med >) |
|---|---|---|---|---|
@@ -262,9 +375,9 @@ M4(可选)将 3.1/3.2 编排为一键 `test/run_latency_test.py`(open/clea
| `prod_cycle_mcu`(L8) | LCDMEM 取 1bpp + 组帧 + net_send 4 分块(不含 10ms 拾取拍) | **7142µs**(7111/7179, 50) | 20ms | 200ms |
| `relisten`(L9) | 断线后 auto-relisten 全程(**修复后**:单次尝试 + netTask 延迟重试) | **89000µs ×3**(三次同值) | 100ms | 500ms |
-> `relisten` 阈值重锁:修复前 588000µs(Trap 09 `osDelay(500)` 忙等)→ crit 500ms 即 D10 指纹;
-> **D10 修复后实测 89ms**(= 30ms 重试间隔 + 约 60ms 芯片沉降),故 warn 由 50ms 放宽到 **100ms**,crit 保持 500ms
-> (仍能捕获"退回阻塞重试"的回归)。修复前后对照见 `链路时延实测数据分析.md` §7.1。
+`relisten` 阈值说明:修复前 588000µs(Trap 09 `osDelay(500)` 忙等)→ crit 500ms 即 D10 指纹;
+D10 修复后实测 89ms(= 30ms 重试间隔 + 约 60ms 芯片沉降),故 warn 放宽到 **100ms**、crit 保持 500ms
+(仍能捕获"退回阻塞重试"的回归)。
**PC 侧端到端(peer `--mode remo`,与模拟器 A/B)**
@@ -276,102 +389,346 @@ M4(可选)将 3.1/3.2 编排为一键 `test/run_latency_test.py`(open/clea
| 帧 CRC / 位图非平凡 | 65/0、65/65 | 50/0、50/50 | — | 全通过 |
| 连接 / 断开重连(+INIT) | 2.6ms / 23.5~34.7ms | 10.5ms / 32.0ms | — | ≤150ms |
-- **一帧预算闭合**:66.0ms ≈ 拾取调度 6.8ms + MCU 净处理 7.1ms + **芯片尾批 52.6ms**;
- 尾批 = 不满块的 137B(=3209−3×1024),**与负载无关**(压力档 tail 中位 52.2ms 与安静档 52.6ms 一致);
-- **控制实验(都不需要抓包工具)**:① 同一客户端打模拟器 tail=0 → 非 PC 栈;② M2 `L3` 满块批量(16×4096B=64KB/125.4ms ≈2ms/KB)
- 无尾批惩罚 → 满 MSS 段不受影响;③ 压力档下尾批不变 → 属芯片 TX 行为,非调度拥塞;
-- **根因(M3 已证实)**:芯片 **MSS=1024**、发送 FIFO=1024B → MCU 每次 1024B 写 = 一个**满 MSS 段**(立即发),
- 而帧尾 137B 是**次 MSS 段** → 被 Nagle 扣住等 ACK;**Windows 的对端延迟 ACK** 使该 ACK 晚到 ~50ms;
-- **解法(客户端侧,实测)**:`--ack-probe`(帧未收齐即补 1 个解析器忽略的字节,捎带 ACK)或 `--ack-fast`
- (Windows 每 socket `SIO_TCP_SET_ACK_FREQUENCY=1`)→ tail **52→3.9ms**、帧周期 **65.5→13.7ms**;
- **设备侧无可行解**(帧长 3209 为质数、TX FIFO=MSS、CH395F 命令表无 Nagle/立即发送开关)。
-
-**压力档(`--ftp-stress`,E-02 复现)**
+**压力档(`--ftp-stress`,E-02 复现;第 15 帧起 FTP 停留半登录态)**
| 分桶 | n | med | p95 | max | 尖峰(≥500ms) |
|---|---|---|---|---|---|
-| quiet | 15 | 65.9ms | 75.8ms | 75.8ms | 0 |
-| stress | 30 | 69.1ms | 75.6ms | **17717.9ms** | **1** |
+| quiet(0~14 帧) | 15 | 65.9ms | 75.8ms | 75.8ms | 0 |
+| stress(15~44 帧,**E-02 修复后**) | 30 | **74.7ms** | 90.8ms | **106.8ms** | **0** |
-- 受压首帧等待 **17.7s**(= FTP 客户端空闲时长),`QUIT` 后立即恢复 55~75ms 正常服务;
-- 机制:`lftpd` 控制行用**阻塞** `net_recv(0)`,而 netTask 是全部 socket 操作的唯一执行者 → 单会话可冻结全网;
-- 上限:单次 recv 受 `NET_RECV_TIMEOUT_MS`(30s) 限制,但 `read_line` 会再次阻塞 → **空闲 FTP 连接可长期占用 netTask**。
+(E-02 修复前 stress 档 med 17718ms、尖峰 1 次 = 17.7s 全网冻结;修复后零尖峰,见 §8.1。)
-> 完整分析(时间预算拆解、三区制、缺陷量化、修复优先级)见 **`链路时延实测数据分析.md`**。
+### 6.4 全段基线总表
-## 5. 里程碑
+| 段 | 含义 | 实测(med) | min/max | n | 阈值 warn/crit | 判定 |
+|---|---|---|---|---|---|---|
+| **L1** `spi_cmd_*` | SPI 字节命令(3 条) | 10 / 15 / 15µs | 10~16µs | 100 | 40/100µs | OK |
+| **L2** `spi_dma_*` | SPI DMA 读(4/64/1024/2048B) | 20 / 114 / **1611** / 3208µs | ±2µs | 100 | 见 §6.1 | OK |
+| **L3** `chunk_wait` | 写发送缓冲 → `SENBUF_FREE`(每 chunk) | **219µs** | 213/381µs | 64 | 10ms/500ms | OK |
+| **L5** `queue_rt_recv` | 调用方 `net_recv(DONTWAIT)` 队列全程 | **5997µs** | 5513/6033µs | 20 | 10ms/100ms | OK |
+| **L5** `queue_rt_send` | 调用方 `net_send` 队列全程 | **987µs** | 838/1209µs | 20 | 10ms/100ms | OK |
+| **L6** `req2first_mcu` | 10B 请求到齐 → 回显发出(MCU 侧) | **319µs** | 308/338µs | 10 | 20ms/200ms | OK |
+| **L7** `fullframe_mcu` | 9B 请求 → 3209B 全帧交给芯片 | **6028µs** | 5991/6066µs | 10 | 50ms/500ms | OK |
+| **L8** `prod_cycle_mcu` | **生产路径**:LCDMEM 取帧+组帧+net_send(MCU 净成本) | **7142µs** | 7111/7179µs | 50 | 20ms/200ms | OK |
+| **L10** `logtax_line` | 一行 60 字符 `printf` @115200 | **4396µs** | 4394/4422µs | 10 | 10ms/25ms | OK(D6 量化) |
+| **L11** `ui_loop` | uiTask 生产循环实测周期 | **10986µs** | 10970/21000µs | 120 | 25ms/50ms | OK |
+| **L9** `relisten` | 断线后自动重监听耗时(MCU 侧,修复后) | **89000µs** | 89000µs ×3 | 3 | 100ms/500ms | OK |
+| **L4/L8** peer cycle | 生产客户端完整帧周期(发→收齐 3209B) | **66.0ms** | 53.3/77.4ms | 65 | — | 见 §7 |
+| **Δ** | `peer cycle(设备) − peer cycle(模拟器)` | **59.2ms** | — | 65/50 | ≤80ms | 见 §7.1 |
-| 里程碑 | 内容 | 验收 | 状态 |
+数据来源:L1/L2 = §6.1;其余 MCU 侧 = M2 run15 / M3 串口 `[LAT]` 行
+(`m2_data/m2_serial_excerpt_run15.txt`、`m3_data/` 各 JSON);peer 侧 = `m3_data/*.json`。
+
+### 6.5 重锁规则
+
+- warn ≈ 2×med,crit ≈ 2×warn(兼顾板间/温度漂移与回归灵敏度:SPI 时钟减半 → 超 warn;
+ 再减半 → 超 crit;DMA 超时 1s → 必超 crit);
+- M2 各段 warn ≈ 现状 ×1.7~10(保留回归灵敏度),crit 取"明显故障"档;
+ `chunk_wait` 以 500ms spike 为 crit(D1 重传指纹);
+- 换板/换固件重跑后,任何段超 warn → 重测确认后按实测重锁本表。
+
+---
+
+## 7. 一帧时间预算与尾批根因
+
+### 7.1 设备 vs 模拟器(Δ 的构成)
+
+同一客户端(`latency_peer.py --mode remo`,100ms 节拍)对**真机 :7003** 与**PC 模拟器 :7003** 各跑一轮:
+
+| 指标 | 真机(65 帧) | 模拟器(50 帧) | 差值 |
|---|---|---|---|
-| **M1** | 计时助手(DWT µs)+ Phase 12(L1/L2 SPI 基线)+ 本文档 + uvprojx 注册 | `@build` 0/0;`@test` 出 `[LAT]` 报告;产出 L1/L2 首份基线 | **完成**(2026-09-22 台架首跑 PASS,7 段全 OK,基线已重锁) |
-| **M2** | `latency_peer.py`(--mode net) + Phase 13(L3~L7/L10)+ `[LAT]` 标记进 @test | 台架首跑产出"芯片+队列"基线;可回答"500ms 台阶是否存在" | **完成**(2026-09-22 台架,run12/run13/run15 三次独立 PASS;6 段全 OK、零尖峰;顺带根治 net 层 D12 通知值缺陷;§4.2 已重锁;产出 `docs/测试说明/m2_data/`) |
-| **M3** | Phase 14:`TEST_LATENCY_PROBE` 探针 + `--mode remo` + 模拟器 Δ 对照 + 压力档(E-02 复现)+ §4.2 定稿 | 完整判断依据:任一段超标 → 故障对照表 | **完成**(2026-09-22 台架:L8/L9/L11 + Δ + E-02 全部实测;D10 量化为 588ms、E-02 量化为 17.7s;产出 `m3_data/` 与 `链路时延实测数据分析.md`) |
-| **M4(可选)** | `run_latency_test.py` 一键编排 | 一条命令出报告 | 待排期 |
-| **M5(可选,在役设备)** | 非独占采样器:事件触发,10s 旁路打点入 syslog | 现场不动台架也能测生产链路 | 设计预留 |
+| 帧周期 total(发请求 → 收齐 3209B) | **66.0ms**(53.3/75.3,max 77.4) | **6.8ms**(0.6/13.1,max 15.7) | **+59.2ms** |
+| 首字节 first byte | 13.9ms(8.5/19.1) | 6.7ms(0.5/13.0) | +7.2ms |
+| **尾批延迟 tail lag**(末批 → 首批) | **52.6ms**(43.3/61.5) | **0.0ms**(0/0.1) | **+52.6ms** |
+| recv 批次数/帧 | 5(8B 头 + 1016 + 1024 + 1024 + **137**) | 2 | — |
-## 6. 故障对照表(哪段超标 → 查什么)
+**真机 66.0ms 的构成**(三段相加 ≈ 66.5,闭合):
+
+```
+[T+0] 客户端发 9B LCDMEM 请求
+[T+6.8ms] 实测推算:uiTask 10ms 轮询拾取 + netTask 调度(≈ L11 的一半)
+[T+13.9ms] 首字节到达 = 6.8 + MCU 净处理 7.1ms(prod_cycle_mcu:取 1bpp + 组帧 + net_send 4 分块)
+[T+66.0ms] 全帧收齐 —— 最后的 137B 尾巴比前三批(1024B×3,前 4ms 内到齐)晚 52.6ms
+```
+
+- 前三批 1024B 在**首字节后约 4ms 内**全部到齐;
+- 第 4 批只有 **137B = 3209 − 3×1024**,比前三批晚 **52.6ms**(与负载无关:压力档 tail 中位 52.2ms);
+- **MCU 侧把 3209B 全部交给芯片只要 7.1ms** → 慢的不在 MCU,在芯片协议栈的尾段发送/ACK 交互。
+
+### 7.2 交叉对照(三条独立证据 → 归因)
+
+| 对照 | 观测 | 说明 |
+|---|---|---|
+| ① 同一客户端 → 模拟器 | tail = **0.0ms**,cycle 6.8ms | 客户端栈、Windows、请求格式全部相同 → 尾批**不是 PC 侧**造成 |
+| ② M2 `L3` 批量排空(16×4096B,全满块) | 65536B / **125.4ms**(≈2ms/KB) | 16 个满块若各带 50ms 尾巴应 ≥800ms → **满块无此惩罚** |
+| ③ 压力档(E-02 头阻塞)下的 L7/生产帧 | tail 仍 43~62ms | 尾批**不受 netTask 拥塞影响** → 属芯片 TX 行为 |
+
+### 7.3 尾批 ~52ms:机理与带内验证(全部不需要抓包)
+
+**机理(已确认)**:驱动把芯片 **MSS 设为 1024**(`CH395F_TCP_MSS`),发送 FIFO = 2×512B = 1024B
+→ MCU 每次 1024B 写恰好是**一个满 MSS 段**(Nagle 允许满段立即发送);帧长 3209B = 3×1024 + **137B**,
+最后这 137B 是**次 MSS 段** → 芯片按 Nagle 扣住,等前一数据被 ACK;Windows 的延迟 ACK 使这个 ACK 晚到 ~50ms。
+
+**判别实验**:
+
+| # | 实验 | 做法 | tail_lag(med) | 帧周期(med) | 结论 |
+|---|---|---|---|---|---|
+| 0 | 基线 | 串行"请求→等整帧→睡 100ms" | **52.0ms** | 65.5ms | 现象复现 |
+| 1 | **带内 ACK 探针** `--ack-probe` | 帧未收齐时立刻发 **1 个解析器会忽略的字节**(0x00,非 TAG 0xAA)→ 该 TCP 段**捎带**对已收数据的 ACK | **3.9ms** | **13.7ms** | 扣住**可被对端 ACK 解除** ⇒ 等 ACK(Nagle 式),非芯片固定定时器 |
+| 2 | **对端立即 ACK** `--ack-fast` | Windows 每 socket 关延迟 ACK:`SIO_TCP_SET_ACK_FREQUENCY=1`(ctypes 调 `WSAIoctl`,**不发任何探测字节**) | **3.9ms** | **20.7ms** | **根因 = 对端延迟 ACK**(去掉它,尾批自行消失) |
+| 3 | 对照:模拟器(同 peer/同 Windows) | — | 0.0ms | 6.8ms | 芯片侧才有该扣住行为 |
+| 4 | 对照:满块批量(M2 L3,16×4096B) | — | 无累积惩罚(64KB/125ms) | — | **满 MSS 段不受影响** |
+
+> 实验 1/2 已固化为 peer 选项:`--ack-probe`(可移植:任何 OS/客户端只要在帧未收齐时发一个字节即可)、
+> `--ack-fast`(仅 Windows,无需改协议)。原始数据:`m3_data/m3_tail_ackprobe.json`、`m3_data/m3_tail_ackfast.json`。
+
+### 7.4 解法与生产客户端实装
+
+| 方案 | 做法 | 实测效果 | 代价 / 约束 |
+|---|---|---|---|
+| **① 客户端"补一个段"** | 帧未收齐时发 1 字节(无效字节即可)或 `KEEPLIVE`,或干脆把**下一个请求提前发** | tail **52→3.9ms**,帧周期 **65→13.7ms**(≈4.8×) | 纯客户端改动,**协议不需变** |
+| **② Windows 关延迟 ACK** | 每 socket `SIO_TCP_SET_ACK_FREQUENCY=1`(或注册表 `TcpAckFrequency=1`) | tail **52→3.9ms** | 仅 Windows 客户端;注册表方式需管理员+重启 |
+| **③ 客户端固定节拍(不串行等待)** | 请求按固定 100ms 节拍发,响应按序收(下一个请求的段天然捎带 ACK) | 尾批同样被提前解除,显示帧率 ~6fps → ~10fps | 客户端需按序配对响应 |
+| **④ 设备侧** | 无可行解(见下) | — | — |
+
+**生产客户端已实装**:`DTU-RemtoeLCD\remo_disp_server.py` 把上述解法做成两条**自动择一**的路径,
+连接时打印生效模式(`[connect] 延迟确认抑制: ...`):
+
+| 路径 | 触发条件 | 实现 | 实测帧耗时(100ms 节拍) |
+|---|---|---|---|
+| **A. 关延迟确认** | Windows(默认) | `ctypes` 直调 `WS2_32!WSAIoctl(SIO_TCP_SET_ACK_FREQUENCY=1)`——Python `socket.ioctl()` 有控制码白名单,不接受该码 | **20.7ms**(改善前 68.8ms,**3.3×**) |
+| **B. 入站"轻推"兜底** | 非 Windows / A 失败 | 收帧途中帧已开始但停顿 ≥5ms → 补发 1 个 `0x00`(装置状态机只认帧头 `0xBB`,丢弃该字节)→ 该段捎带 ACK | **29.4ms**(2.3×) |
+
+> 剩余 ~20ms = 客户端 Python 逐字节解析 3209B + 首字节等待(9~18ms)+ 尾块 ~4ms;
+> 整体显示帧率由 ~6fps 提升到 ~9fps。两条路径**都不需要改协议、不需要改装置固件**。
+
+**为什么设备侧无解**(避免后续重复投入):
+- 帧长 **3209 是质数**,无论 MSS / 分块怎么取,最后一个 TCP 段必然 < MSS(除非补字节改线格式);
+- **发送 FIFO = MSS = 1024B**(`NET_SEND_CHUNK_MAX`),单次写不可能超过 MSS ⇒ 无法用"一次写整帧"绕过;
+- 手册命令表(`docs/芯片手册/CH395F.md` §5)**没有** Nagle / 立即发送 / TCP_NODELAY 类开关(只有
+ `CMD_SET_TCP_MSS`、收发缓冲、重传与 keepalive 参数)。
+
+**退路**(仅当客户端确实无法改动,如第三方 App):**协议补位**——把应答补齐为 1024 的整数倍使尾段变为
+满 MSS 段;需客户端同步改解析(跳过多出的补位字节),优先级低于客户端侧解法。
+
+### 7.5 队列与调度:静默档不存在台阶
+
+- **L5 队列往返**:收 5997µs / 发 987µs —— 与 netTask 10ms 轮询节拍同量级,属**调度周期**而非排队拥塞;
+ `queue_rt_send` 987µs ≈ 1 个 chunk(1611µs)量级,发送路径没有额外排队;
+- **L3 chunk 级**:219µs ≈ 每 chunk SPI 成本的 1/7 → **芯片 TX 缓冲不是瓶颈**,SPI 搬运才是;
+- **L11 uiTask 周期**:10986µs(= osDelay(10) + 渲染开销 ≈ 1ms)→ **LVGL 渲染只占约 1ms/帧**,
+ uiTask 不是延迟来源;
+- **结论:调优先级/栈不会改善体感**——预算应花在尾批解法(已做)与 D20(§8.2)。
+
+---
+
+## 8. 缺陷与修复状态
+
+### 8.1 缺陷总表(编号 = 台架发现顺序;D10~D20/E-02 为测试发现,与 §10.1 的静态分析编号 D1~D10 不同体系)
+
+| 编号 | 缺陷 | 状态 | 机理 | 修复 | 实测证据(前 → 后) |
+|---|---|---|---|---|---|
+| **D10** | 断线重监听 `osDelay(500)` 忙等冻结 netTask | **已修复** | Trap 09 沉降窗口内 `open` 必失败 → 旧代码在 netTask 里 `osDelay(500)` 阻塞重试,期间全网冻结 | 单次尝试 + netTask 每轮延迟重试(`net_relisten_try`/`net_relisten_retry_tick`,间隔 30ms、上限 2s,防饥饿 + 防 fd 残留) | relisten **588ms → 89ms** 且不阻塞;`[LAT]` FAIL → OK |
+| **D11** | 芯片 TCP RX 饱和下重复投递(+138%)并过度 ACK → 对端 RST | **芯片行为,应用侧规避** | RX 缓冲打满(窗口=0)后芯片把已收数据整段重投(发 5000B 实收 11884B) | 灌入限速 20×100B@50ms(1KB/s)+ MCU 冗余字节守卫(超阈值即 abort)+ L10 后 DONTWAIT 抽干 | L3 flood 7.5~8.7ms/chunk 全 OK |
+| **D12** | net 层 `xTaskNotifyWait` 清除掩码 0 = 不清值 → 陈旧结果被重读 | **已修复** | `xTaskNotifyWait(0,0,...)` 掩码 0 根本不清值 → 上次结果残留,零等待重读成"本次结果" → 结果错位/永久等待 | `net_wait_result()` 两段式 + **清除掩码全 1(读取即消费)** + **以返回值 `pdTRUE/pdFALSE` 判定落地** | 三次独立 PASS(run12/13/15) |
+| **D12-a** | 位域切分与 `v!=0` 判定两个同类陷阱(设计约束) | **已避开** | ① 序列号压通知值高 16 位 → 单次结果上限 32767(`TEST_BUF_SIZE=32768` 溢出成负);② `v!=0` 判落地 → 合法 0 结果(recv=对端关闭)误判 → 永久挂死 | 不切位域(32 位整体仍是结果);只用返回值判定。**改这条路径前必须复核** | code review |
+| **D12-b** | net 热路径逐调用日志 → 观测改变被测对象 | **已修复**(规则:热路径禁止日志) | 探针两行日志把串口流量抬 26×(40KB→1.04MB)→ 冲掉 `[LAT]` 证据行、`queue_rt_send` 抬高 7.8× | 删除热路径日志,诊断改在 `[LAT]` 段报告侧聚合 | run14 对照(不作基线) |
+| **D13** | 芯片空闲超时 RST(MCU 未就绪时握手已在芯片侧完成) | **芯片行为,客户端容忍** | 连接后 15~30s 无流量 → 芯片 RST(WinError 10054) | peer 3 次重试包装(`--wait` 重试是正常路径);生产端 E-02 修复消除了"冻结→RST→重连风暴"链条 | 每轮首连失败、+2s 重连成功 |
+| **D14** | 芯片静默回收 Socket(无 DISCONNECT 中断)→ MCU 状态滞留 | **已修复** | 芯片在 peer FIN 后静默回收回监听但不通知 MCU → `state` 滞留 ESTABLISHED,新连接字节被吞 | `net_sock_state()`(只读)+ `net_resync_listen()`(仅修 MCU 态,不发芯片命令)+ `net_relisten()`(芯片侧重监听,含 Trap 09 重试);abort 后按状态分派 + 10s 周期自愈(干净完成后停止) | run8/9 实证 + 三次 PASS |
+| **D15** | 测试 L7 帧与生产 RemoDispBus 格式不一致 + peer 超读 | **已修复** | 响应 LEN 应为 3204(非 3200)、缺 4B 偏移字段、CRC 只覆盖数据段;peer `recv(4096)` 超读吃掉下一操作字节 | MCU 建帧/校验与 peer 请求/校验全部对齐生产语义(`remo_send`/`remo_handle`);peer 改 `recv(min(4096, 剩余))` 有界读 | L7 10/0 CRC fail |
+| **D16** | Phase 13 状态机 L7 → 回跳 L3 无限循环 | **已修复** | 重排会话顺序前的旧迁移目标遗留 → 永不下发 `0xFF`,peer 空等 300s | L7 → `P13_ST_DONE`;该处加注释锁定顺序 L5→L3→L10→L6→L7→DONE | run11 → 三次 PASS |
+| **D17** | INIT 应答把 `帧字节数u32` 截成 6B → 字段恒 0 | **已修复** | `remo_send_init` 只发 8B 数据段的前 6B(u32 截半),客户端读到的帧字节数恒 0 | `len=6` 改 `len=8`(字段定义本就是 `[W u16][H u16][FB u32]`);协议文档同步勘误 | `framebytes` 0 → **3200** |
+| **D18** | 多打点站点共用全局探针窗口 → 跨段污染(测试工装) | **已修复** | 探针窗口串行全局,prod/relisten 段读出 ui_loop 的 ~11ms 值 | `lat_probe_add_seg()` 按段名过滤 | run17 对照 |
+| **D19** | Phase 14 漏调 `lat_time_init()` → DWT 恒 0 假绿(测试工装) | **已修复** | 时基未使能 → 三段全 0 却 `verdict=OK` | 补初始化 + `med==0` 判无效 | run16 对照 |
+| **D20** | FTP 开机首个会话的第 2 个数据连接失败(425) | **已定位未修**(暂缓) | 延迟 DISCONNECT 事件与首次 `net_accept` 的先后竞态(详见 §8.2) | 未实施;保留安全硬化(陈旧 DISCONNECT 甄别 + 2 字节状态缓冲越界修复);两次不稳定修法已回退 | 复现脚本 `test/ftp_regression.py` |
+| **E-02** | 空闲 FTP 控制连接头阻塞冻结 netTask(= §10.1 的 D3) | **已修复** | `lftpd` 控制行 `read_line` 走阻塞 `net_recv(0)`,netTask 是全部 socket 操作的唯一执行者 → 单会话可冻结全网最长 30s+ | 控制行/数据读改**分片有界读**(`net_recv_to` 5ms 片 + 每片 `vTaskDelay(1)` 让出,总预算仍 30s) | 受压帧 **17.7s → med 74.7ms / max 106.8ms,零尖峰** |
+
+### 8.2 D20 详情(已定位,未修复)
+
+**复现条件(精确)**:**每次开机后的第一个 FTP 会话**在其第 2 个数据连接失败
+(`LIST/NLST` 成功 → 随后 `STOR` 返回 425 Can't open data connection,控制连接随后被对端关闭);
+第 2 个及之后的会话(稳态)全部通过。不是"每会话第 2 个数据连接必失败"。
+
+**设备日志签名**(首会话失败时):
+
+```
+CMD PASV → sock4 standalone accept(数据连接建立)
+CMD STOR
+sock4 DISCONNECTED, releasing (chip tcp=0x00) ← 建立后 ~30ms 上报
+[ERR] data socket closed, accept aborted → 425
+sock4 auto-relisten ok / relisten retry ok ← 监听器随后自行恢复
+```
+
+**机理(2026-09-22 串口日志逐行复核后的修正版)**:
+
+1. DISCONNECT 事件在**成功**会话中同样出现:成功会话的 STOR 数据连接在 `standalone accept` 后
+ ~31ms 同样上报 `sock4 DISCONNECTED (chip tcp=0x00)`,但其后数据照常读出、传输完成 → 该事件不是
+ "芯片拆除新连接",而是**上一条数据连接(LIST 通道,服务端 `net_close`)的延迟断开事件**落在同一
+ 芯片 Socket 上(`net_close_locked` 排空窗口结束后芯片才补报);
+2. **成败唯一差异 = 竞态**:失败会话中 lftpd 首次 `net_accept`(`lftpd_inet_accept_timeout`)运行在
+ DISCONNECT 事件**之后**(state 已置 CLOSED → `accept aborted` → 425);成功会话中首次 accept 抢在
+ 事件之前,其后的 DISCONNECT 不影响数据路径(MCU 侧 CLOSED 状态不妨碍从芯片缓冲读数据,
+ fun_para bit3=1 保留缓冲);
+3. **现有甄别为何拦不住**:延迟 DISCONNECT 时刻 `ch395f_get_socket_status` 回读 `tcp=0x00`,不反映新连接
+ 的 ESTABLISHED 态 → "回读芯片状态判陈旧"(`st[1]==ESTABLISHED` 才丢弃)在此场景恒不成立;
+4. 与 D10(Trap 09 沉降)、D14(静默回收)同属"单连接 Socket 快速复用"家族。
+
+**已试并回退的修法(保留结论,勿重复投入)**:
+① 传输结束不关数据连接(保留监听器复用)→ **破坏 FTP EOF 语义**(客户端等不到 EOF 整会话挂死),必须关闭;
+② 新增 `net_shutdown()`(发 FIN 优雅断开)+ 复用监听器(等 auto_relisten,relisten 兜底)→ 部分会话通过但
+**不稳定**(仍有 425,且后续会话出现控制连接连不上)→ 回退。
+
+**后续修法方向**:
+1. **数据 Socket 轮换(优先)**:连续数据连接落在不同芯片 Socket(现 `alloc_socket` 固定复用最低空闲 fd=sock4),
+ 直接消除"上一条连接的断开事件打到下一条新连接"的路径;需 net 层支持分配偏好;
+2. 备选:已 accept 的连接对迟到 DISCONNECT 免疫(成功会话已证明数据路径不受影响)——但须同时处理
+ "事件先到、accept 后到"的顺序,单做不能覆盖失败场景;
+3. 寄存器级定位(如需):首会话复现窗口内打印 `CMD_GET_SOCKET_STATUS_SN`(2 字节)与 SINT 序列;
+4. **现场规避**:客户端重试(425 属 RFC 959 瞬态 4xx,重试一次即可)——最省事的临时手段。
+
+### 8.3 关键实现要点(D10/E-02 修复,供后续维护)
+
+1. **D10 重试必须防饥饿**:调用方(accept/recv 路径每 10ms 一次)只登记一次重试,实际重试由 netTask
+ 轮询到点执行;若让调用方每次立即重试并重置时刻,会永不重试。重试间隔 100ms→30ms 后恢复时间
+ 147ms→89ms(芯片沉降约 60ms);
+2. **E-02 分片长度 = 其他任务的最坏等待**:100ms 片下受压帧仍 ~750ms(多片排队),收到 **5ms** 片后才降到
+ ~75ms;
+3. **分片循环必须显式让出 CPU**:某片可能因 `net_poll` 自身耗时首轮即返回超时 → 本任务(Normal)立即
+ 重排队会形成忙循环并**饿死 BelowNormal 的 uiTask**(实测 2.4~3.4s 周期尖刺);`vTaskDelay(1)` 后尖刺归零。
+
+### 8.4 相关既有缺陷(会掩盖/放大延迟,见 `docs/问题记录/错误码传播审计.md`)
+
+| 编号 | 位置 | 与延迟的关系 |
+|---|---|---|
+| **E-04** | `ui_net_sink.c` `remo_send` | 丢弃 `net_send` 返回值 → 弱网短写静默截断,客户端 CRC 不过 → 断连 → 观感"卡完直接黑屏/卡住" |
+| **E-33/E-34** | `ch395f.c` SPI DMA/字节命令 | DMA 失败 void 化无错误通道;`HAL_MAX_DELAY` → 一次 SPI 故障可能让 netTask 永久挂死(无看门狗) |
+
+---
+
+## 9. 故障对照表(哪段超标 / 什么现象 → 查什么)
+
+### 9.1 症状分类(先分流)
+
+| 观测现象 | 首要指向 | 动作 |
+|---|---|---|
+| 每帧都**稳定**慢 ≈150~300ms | 结构性基线(§10.1 的 D2/D5/D6/D8) | 跑 §5 的 Phase 13/14 对照基线 |
+| **间歇**卡,且 ≈500ms 的整数倍 | **D1** 丢包重传 | Wireshark `tcp.analysis.retransmission`;查 PHY/网线/Wi-Fi |
+| **间歇**整屏冻结数秒~数十秒 | **E-02** 头阻塞(已修)/ D10 重连(已修)/ 回归 | 查 FTP/对时会话是否挂着;核对 §8.1 修复在位 |
+| 只在开 FTP / 对时后卡 | E-02 + 单连接 socket 争用 | 临时停 ftpTask/timeSyncTask 对照 |
+| 换网口/换线/换 PC 就好 | D7 物理链路 | §10.2-S3 拓扑纪律 |
+
+### 9.2 分段对照
| 现象(段超标) | 首要怀疑 | 动作 |
|---|---|---|
-| L1/L2 超 fail | SPI 走线/PCB/SPI2 分频或 DMA 配置 | 对照 `docs/问题记录/CH395F_Trap_Records.md`;逻辑分析仪抓 SCK/CS |
+| L1/L2 超 crit | SPI 走线/PCB/SPI2 分频或 DMA 配置 | 对照 `docs/问题记录/CH395F_Trap_Records.md`;逻辑分析仪抓 SCK/CS |
| L3 spike ≥500ms | **D1 重传 / D7 链路** | Wireshark `tcp.analysis.retransmission`;查 PHY 协商/网线/Wi-Fi |
| L3 基线(非 spike)抬高 | 芯片发送路径 / 中断响应 | 对照 CH395F 手册 §5.17/5.18;`GET_CMD_STATUS` 逐段核对 |
-| L5 压力档爆表 | **D3(E-02 头阻塞)** | 定位触发源(阻塞 recv 的 FTP/对时)→ 方案 S6 |
+| L5 压力档爆表 | **E-02 头阻塞(已修)** → 回归 | 定位触发源(阻塞 recv 的 FTP/对时);核对 `net_recv_to` 分片在位 |
| L5/L6 静默档抬高 | D5 调度(任务抢占/节拍) | 查任务优先级与 osDelay;`stack_report.py` 核对栈水位 |
| L6/L7 整体抬高且 L1~L5 正常 | 线路+PC 段(L4) | Wireshark 段间间隙;`Δ` 对照定位 MCU 侧净贡献 |
-| **L7 尾批固定迟到 ~50ms**(M2/M3 实测) | **已定性(无需抓包)**:芯片 MSS=1024 → 帧长 3209=3×1024+**137**,末段是次 MSS 段被 Nagle 扣住,等对端 ACK;**Windows 延迟 ACK** 把这一等推迟 ~50ms | **客户端侧解法已实测**(见 `链路时延实测数据分析.md` §3.3):帧未收齐时补发 1 字节捎带 ACK(`--ack-probe`)或关本 socket 延迟 ACK(`--ack-fast`)→ tail **52→3.9ms**、帧周期 **65.5→13.7ms**;**设备侧无解**(帧长为质数、TX FIFO=MSS、CH395F 无 Nagle 开关),不必再投入 |
-| **L5 出现重复投递**(收到 > 发送) | **D11**:芯片 TCP RX 饱和下重复递交 + 过度 ACK | 降低灌入速率/加抽干;见 §6 下方 D11 行 |
-| **会话在 L6/L7 无征兆失步**(结果错位/挂死) | **D12**:net 层任务通知值语义(清除掩码必须全 1 + 以返回值判定;32 位整体作结果) | 见 §6 下方 D12/D12-a 行;核对 `net_wait_result` 与 `net_process_messages` 的通知打包 |
-| **L7 一直超时 / peer 报 `l7_crc.fail`** | **D15**:测试帧与生产 RemoDispBus 格式不一致 / peer 超读吃掉操作字节 | 见 §6 下方 D15 行;对照 `ui_net_sink.c` 的 `remo_send`/`remo_handle` |
-| **会话跑完一段后无限循环、peer 空等 300s** | **D16**:Phase 13 状态机迁移目标错(L7 曾回跳 L3) | 见 §6 下方 D16 行;最终顺序 L5→L3→L10→L6→L7→DONE |
-| **`[LAT]` 段行缺失/截断、wait-idle 判不出空闲** | **D12-b**:探针在 net 热路径逐调用打日志(日志洪水) | 见 §6 下方 D12-b 行;删除热路径日志 |
-| **`relisten` 段 FAIL、每次断线固定 ~588ms** | **D10**:`net_relisten_locked` 的 Trap 09 重试含 `osDelay(500)`(netTask 忙等) | **已修复**:单次尝试 + netTask 延迟重试(间隔 30ms/上限 2s)→ 实测 **89ms 且不阻塞**;另:客户端 600ms 内重连被拒的问题同时消除 |
-| **远端显示整帧卡住十几秒后自恢复** | **E-02**:`lftpd` 阻塞 `net_recv(0)` 占用 netTask | **已修复**:控制行/数据读改分片有界读(`net_recv_to` 5ms 片 + 每片让出 1ms,总预算仍 30s)→ 受压帧 **17.7s → 74.7ms(max 106.8ms,零尖峰)** |
-| **INIT 应答帧字节数字段恒为 0** | **D17**:`remo_send_init` 只发出 8B 数据段的前 6B(u32 被截半) | **已修复**:改发 8B(字段定义本就是 `[W u16][H u16][FB u32]`)→ peer 读到 **framebytes=3200**;协议文档已同步勘误 |
-| **同一 FTP 会话内第二个数据连接失败(425)** | **D20**(已定位未修,见 `链路时延实测数据分析.md` §7.4) | 准确条件 = **每次开机后的第一个 FTP 会话**在其第 2 个数据连接失败(之后会话 4/4 通过);日志签名 = 数据连接建立后 ~10ms 芯片把新连接拆掉(`DISCONNECTED (chip tcp=0x00)`)→ `accept aborted` → 425。属芯片"同一 Socket 快速复用"竞态;已回退两次不稳定修法,保留安全硬化(陈旧 DISCONNECT 甄别 + 2 字节状态缓冲越界修复)。复现:`test/ftp_regression.py`;后续优先做数据 Socket 轮换,现场可用客户端重试规避 |
-| **`[LAT]` 多段数值雷同 / 全 0 却 verdict=OK** | **D18/D19**(测试工装):多站点共用全局探针窗口跨段污染;漏调 `lat_time_init()` → DWT 恒 0 假绿 | 均已修复:`lat_probe_add_seg()` 按段名过滤 + 补时基初始化 + `med==0` 判无效 |
-| **连接建立后 15~30s 无流量即被 RST** | **D13**:芯片空闲超时(MCU 尚未就绪) | 见 §6 下方 D13 行;客户端须容忍并重连 |
-| **peer 重连后 MCU 状态滞留、新连接字节被吞** | **D14**:芯片静默回收 Socket(无 DISCONNECT 中断) | 见 §6 下方 D14 行;用 `net_resync_listen`/`net_relisten` 自愈 |
-| L8 超而 L7 正常 | 生产路径附加成本(uiTask 节拍/LVGL/日志) | 对照 L10/L11;方案 P0-b/S7/S8 |
-| L9 relisten ≥500ms | **D10 `osDelay(500)` 重试路径** | 方案 S6(非阻塞重试) |
+| **L7/L8 尾批固定迟到 ~50ms** | 已定性(§7.3):次 MSS 尾段被 Nagle 扣住 + Windows 延迟 ACK | 客户端侧解法已实测(§7.4):`--ack-probe` / `--ack-fast` → tail 52→3.9ms;生产客户端已实装;**设备侧无解**,不必再投入 |
+| **L5 出现重复投递**(收到 > 发送) | **D11**:芯片 TCP RX 饱和重复递交 | 降低灌入速率/加抽干(§8.1 D11) |
+| **会话无征兆失步**(结果错位/挂死) | **D12**:net 层任务通知值语义 | 核对 `net_wait_result` 清除掩码全 1 + 按返回值判定(§8.1 D12/D12-a) |
+| **L7 一直超时 / peer 报 `l7_crc.fail`** | **D15**:帧格式不一致 / peer 超读 | 对照 `ui_net_sink.c` 的 `remo_send`/`remo_handle`(§8.1 D15) |
+| **会话跑完一段后无限循环、peer 空等 300s** | **D16**:Phase 13 状态机迁移目标错 | 核对顺序 L5→L3→L10→L6→L7→DONE(§8.1 D16) |
+| **`[LAT]` 段行缺失/截断、wait-idle 判不出空闲** | **D12-b**:热路径日志洪水 | 删除 net 热路径逐调用日志(§8.1 D12-b) |
+| **`relisten` 段 FAIL、断线固定 ~588ms** | **D10** 回归(退回 `osDelay(500)`) | 核对 `net_relisten_try`/`net_relisten_retry_tick` 非阻塞重试在位(§8.1 D10) |
+| **远端显示整帧卡住十几秒后自恢复** | **E-02** 回归 | 核对 `net_recv_to` 5ms 分片 + 让出在位(§8.1 E-02) |
+| **INIT 应答帧字节数字段恒为 0** | **D17** 回归 | 核对 `remo_send_init` 发满 8B(§8.1 D17) |
+| **开机首个 FTP 会话第 2 个数据连接 425** | **D20**(已定位未修,§8.2) | 复现:`test/ftp_regression.py`(复位开边界);现场规避:客户端重试 425(瞬态 4xx);根治方向:数据 Socket 轮换 |
+| **`[LAT]` 多段数值雷同 / 全 0 却 verdict=OK** | **D18/D19**(测试工装) | 核对 `lat_probe_add_seg` 段名过滤 + `lat_time_init` 调用 + `med==0` 判无效 |
+| **连接建立后 15~30s 无流量即被 RST** | **D13**:芯片空闲超时(芯片行为) | 客户端须容忍并重连(peer `--wait` 重试是正常路径) |
+| **peer 重连后 MCU 状态滞留、新连接字节被吞** | **D14**:芯片静默回收 Socket | `net_resync_listen`/`net_relisten` 自愈路径核对 |
+| L8 超而 L7 正常 | 生产路径附加成本(uiTask 节拍/LVGL/日志) | 对照 L10/L11 与 §10.1(渲染已排除;日志税待办 P0-b/S8) |
| Δ 显著 >80ms | CH395F 栈净开销异常 | 回到 L1~L9 逐段排查 |
-### 6.1 M2 台架实证缺陷与修复(D11~D14)
+---
-| 编号 | 现象(台架实证) | 根因 | 修复/状态 |
+## 10. 结构性成本项与待办方向
+
+### 10.1 静态分析十项结构原因(D1~D10,独立于 §8.1 的台架缺陷编号)与当前状态
+
+| # | 原因 | 实测验证(2026-09-22 台架) | 当前状态 |
|---|---|---|---|
-| **D11** | L5 灌入 5000B 时对端实收 11884B(+138%)并触发过度 ACK → 对端 RST | 芯片 TCP RX 缓冲饱和下重复递交(retransmit 再投递) | 灌入降到 20×100B@50ms + MCU 侧冗余字节守卫(`> 2000 + 2×256` 即 abort)+ L10 后 DONTWAIT 抽干;**未根治(芯片行为),靠应用侧限速规避** |
-| **D12** | L7 段 MCU 侧挂死 35s / 结果整段错位(run3~run11) | **net 层结果回传(任务通知值)语义**:`xTaskNotifyWait(0, 0, &v, ...)` 的**清除掩码为 0 = 根本不清值**(FreeRTOS 以位掩码 `ulNotifiedValue &= ~mask` 清值)→ 上一次调用的结果残留,被后续每次零等待重读成"本次结果" → 表现为"结果错位一格"或永久等待 | **已修复(最小改动)**:`net_wait_result()` 两段式 + **清除掩码全 1(读取即消费)** + **以 `xTaskNotifyWait` 返回值 `pdTRUE/pdFALSE` 判定是否已落地**。通知值 **32 位整体仍是结果**(不切分位域)。三次 PASS 验证 |
-| **D12-a** | ——(修复过程中自查发现的同类陷阱,务必记住) | ① 若把序列号压进通知值高 16 位,单次结果上限被压到 **32767**,而 `TEST_BUF_SIZE=32768`(Phase 6/8/9)会溢出成 **负数(假错误)**;② 若用 `v != 0` 判定"通知是否已落地",合法的 **0 结果**(`recv` 返回 0 = 对端关闭)会被误判成"无通知"→ 进入阻塞等待 → **永久挂死** | 二者均已避开:不做位域切分;只用返回值判定。**后续改这条路径前必须复核这两点** |
-| **D12-b** | 探针日志把测量对象改坏(run14) | 在 `net_wait_result` 内逐调用打 `pick/block` 日志 → 日志量 40KB→1.04MB(98.6% 是这两行)→ 115200 下丢行、`[LAT]` 证据被截断、`wait-idle` 判不出空闲,且 `queue_rt_send` med **987µs→7685µs(7.8×)**、`queue_rt_recv` p95→21ms | **已修复**:删除 net 热路径的逐调用日志(诊断改为在 `[LAT]` 段报告侧聚合)。**规则:热路径禁止逐调用打点** |
-| **D13** | 连接建立后 15~30s 无流量即被 RST(`WinError 10054`) | 芯片空闲超时:MCU 尚在启动/忙,握手已在芯片侧完成但无数据 → 芯片 RST | 测试侧:peer 3 次重试包装(run12/13/15 均首连失败、+2s 重连成功);**生产含义**:E-02 类 30s netTask 冻结会让已连客户端被芯片 RST → 触发重连风暴(与 D11/D3 叠加)→ 客户端须容忍重连 |
-| **D14** | peer 重连后 MCU 协议状态错乱、新连接 HELLO 被旧会话吞掉(run7 attempt3) | 芯片在 peer FIN 后**静默回收** Socket 回监听且**不发 DISCONNECT 中断** → MCU 侧 `state` 滞留 ESTABLISHED | **已修复(两层)**:① 新增只读 `net_sock_state()` + `net_resync_listen()`(仅修 MCU 侧状态,不发芯片命令——run8 实证发 close 会摧毁芯片侧健康监听器 → 连接永拒 170s);② 新增 `net_relisten()`(无条件芯片侧重监听,含 Trap 09 重试——run9 实证一次性 `net_listen` 落 Trap 09 窗口失败且被静默忽略 → 监听器永久丢失);abort 后按状态分派 + 10s 周期自愈(**干净完成后停止自愈**,否则每 10s 拆建监听器、台架永不判空闲) |
-| **D15** | L7 段从未完成:MCU 报 `L7 peer timeout` / peer 收 3210B 且 `l7_crc.fail=1`(run10) | 测试侧两处:① 测试 L7 帧与生产 RemoDispBus 格式不一致(响应 LEN 应为 3204 而非 3200、缺 4B 偏移字段、CRC 应只覆盖数据段、且多发 4B 垃圾;请求 LEN 应为 4);② peer `recv(4096)` **超读**,把下一个操作字节 `0x04` 当帧体吃掉 → 长度 3210 → 校验失败 → peer 关闭 | **已修复**:MCU 建帧/校验与 peer 请求/校验全部对齐生产(`remo_send`/`remo_handle` 语义);peer 改 `recv(min(4096, 剩余))` 有界读;离线自检(正确帧通过、错误 LEN/短帧/坏位图/坏 CRC 全拒) |
-| **D16** | 会话跑完 L7 后**永不结束**、L3→L10→L6→L7 无限循环、peer 空等 300s(run11) | Phase 13 状态机 L7 段的迁移目标是 `P13_ST_L3`(run3 重排前的旧顺序遗留)→ 永不下发 `0xFF`(DONE) | **已修复**:L7 → `P13_ST_DONE`;并在该处加注释说明最终顺序为 L5→L3→L10→L6→L7→DONE(防再次回跳) |
+| D1 | CH395F TCP 重传固定 500ms 周期(8 次 = 4s 窗口;非 RTT 自适应) | 台架链路(有线 100M 全双工)三轮零重传尖峰 → 台架无丢包;**弱网(Wi-Fi/双工不匹配)仍是秒级卡顿首要嫌疑**,0.5/1.0/1.5s 台阶 = 500ms 粒度指纹 | **未修**(仍 `ch395f_set_retrans_count(8)` + `period(500)`);待办 P0-a(周期 200ms,需台架验证) |
+| D2 | 3209B × 1KB 发送 FIFO 停止等待(4 次串行分块握手,期间 netTask 全程占用) | `chunk_wait` 219µs/块 ≈ SPI 搬运成本的 1/7 → 握手本身**不是瓶颈**;每帧固定成本小(MCU 净 7.1ms) | 基线成本,可接受;根治 = 差量/RLE + 帧版本号(待办 S2) |
+| D3 | netMsgQueue 头阻塞(= §8.1 E-02) | 已量化:一条空闲 FTP 控制连接冻结全网 17.7s | **已修复**(5ms 分片有界读 + 让出,§8.1) |
+| D4 | PC 端延迟 ACK 与"每段等 ACK"停止等待交互 | `--ack-fast` 带内实验证实:关掉延迟 ACK 尾批自行消失(52→3.9ms) | **已客户端侧解决**(生产客户端关延迟 ACK/轻推兜底,§7.4) |
+| D5 | uiTask 节拍:10ms 循环 + 每拍一次 64B recv + step 在 poll 前 | `ui_loop` 10986µs ≈ 设计值(渲染 ~1ms);拾取调度 ~6.8ms | 基线行为;可选优化 S7(poll 前置 + while 排空),低优先 |
+| D6 | 阻塞式日志在关键路径(fputc 逐字节 @115200) | `logtax_line` 4396µs/行;`net_recv_locked` 的 per-recv 日志仍在 | **部分待办**(P0-b:删/降级 per-recv 日志;S8:日志改非阻塞) |
+| D7 | 物理链路:强制 100M 全双工 vs 交换机自协商 → 双工不匹配静默丢包 | 台架 `PHY: 100M FULL` 成立、零尖峰 → 台架干净 | 现场排查项(S3):核对开机日志 PHY 行 + 交换机端口强制全双工 + 验证 PC 有线直连 |
+| D8 | 客户端固定成本:100ms 拉取节拍、按键后不立即取帧、1bpp→PNG→base64→WS 编码链 | 生产客户端优化后:帧耗时 20.7ms、显示帧率 ~9fps | 部分已优化(延迟确认抑制已上线);其余待办 S9(按键后立即取帧/不传 PNG/自适应节拍)——客户端仓库 |
+| D9 | LVGL 全量渲染(RENDER_MODE_FULL)+ uiTask BelowNormal 易被抢占 | `ui_loop` 显示渲染仅 ~1ms/帧 | **已排除**(非瓶颈;调优先级/栈无收益,§7.5) |
+| D10 | 断线重连路径 `osDelay(500)`(netTask 忙等) | 已量化:relisten 588ms×3 且冻结 netTask | **已修复**(非阻塞延迟重试 → 89ms,§8.1) |
-## 7. 约束核对(AGENTS.md)
+### 10.2 待办优化项(按性价比排序,均未实施)
-- 每里程碑结束 `@build`(0 错 0 警)+ `@test` 硬件闭环;
+1. **D20 数据 Socket 轮换**(§8.2)——唯一未修的功能性缺陷;现场可用客户端重试 425 规避;
+2. **P0-a** `ch395f_set_retrans_period(500) → 200`(一行级;治 D1 症状:单次丢包代价 500→200ms;
+ 总窗口 8×200=1.6s,链路真断时判失败更快;手册 §5.18 允许 ≤1000ms,最小值未标注 → **需台架验证 200 被接受**);
+3. **P0-b** 删除/降级 `net_socket.c` 的 per-recv `DBG_INFO`(每帧省 ~4.4ms netTask 阻塞,顺带减 syslog 噪声);
+4. **S1** 超时参数分档:`NET_SEND/RECV_TIMEOUT_MS` 全局一刀切 → 按服务分离(远显短超时+非阻塞,FTP 保留长超时);
+5. **S2** 差量/RLE + 帧版本号 + 由拉改推(**根治,收益最大**:典型变化 <100B → 单段一帧,延迟 ≈ 一个 RTT;
+ 稳态带宽 32KB/s → ≈0;D1 影响面 4 段 → 1 段;新命令走同一帧格式与 CRC,旧客户端兼容);
+6. **S4** 发送块配额与 MSS:48 块按实际使用的 3 个 socket 重新分配(各 16 块),MSS → 1460
+ (3209B 从 4 段降到 1~2 段);**约束**:单次写长度必须 ≤ 发送缓冲(Trap 20 铁律),
+ 改分配策略需同步 `net_bind` 重复分配路径,改前先读 `CH395F_Trap_Records.md`,改后跑 Phase 8 吞吐用例;
+7. **S5** 去掉一次 3200B 拷贝(`g_tx` 直接承接位图,省 ~3.2KB 常驻 RAM;改前跑 `scripts/ram_report.py`);
+8. **S7** uiTask:`ui_net_sink_poll()` 前置到 `ui_port_step` 前 + while 排空(省按键一整拍);
+9. **S8** 日志改非阻塞(`fputc` → 环形缓冲 + TX 中断/DMA;处理溢出丢弃与 HardFault 冲刷);
+10. **S9** 客户端侧(DTU-RemtoeLCD):按键后立即取帧、不传 PNG(直接发 1bpp/差量 + canvas 绘制)、
+ 自适应节拍(帧耗时补偿)、验证 PC 一律有线同交换机;
+11. **M4**(可选)`test/run_latency_test.py` 一键编排(open/clear/flash/wait-idle/peer/collect/merge/verdict)——待排期;
+12. **M5**(可选,在役设备)非独占采样器:事件触发、10s 旁路打点入 syslog——设计预留;
+13. 协议补位(尾批退路,仅当客户端无法改动时;见 §7.4)。
+
+---
+
+## 11. 约束核对(AGENTS.md)
+
+- 每个测试阶段/修复 `@build`(0 错 0 警)+ `@test` 硬件闭环;
- 新 `.c` 注册:`test/*.c` 仅 MCU 端编译 → **只注册 `MDK-ARM/STM32F407-Demo.uvprojx`**(Test 组;
模拟器 CMake 不含 test 文件,与 `ch395f_test_task.c` 等现状一致);
- 新增 C 字符串字面量**纯 ASCII**(`[LAT]` 报告行均为英文),中文只写注释;
-- 自研测试代码**零 C 库堆**:样本/缓冲全部静态(M1 新增 BSS ≈ 1.1KB),改前后跑 `scripts/ram_report.py` 留证;
+- 自研测试代码**零 C 库堆**:样本/缓冲全部静态(M1 新增 BSS ≈1.1KB,探针 <1.5KB),改前后跑 `scripts/ram_report.py` 留证;
- `testTask` 栈 2048 words:用例函数只用几十字节局部变量;
- 套件互斥宏体系不动:Phase 12 挂 HW 组,13/14 挂 NET 组,`#error` 自动防两层同启;
-- 非破坏性:不改 IP/卷/BBT;HW 层阶段独占 CH395F(网络中断)→ 文档标注"台架专用";
-- M3 触碰生产代码的探针全部在 `#ifdef TEST_LATENCY_PROBE` 内,产品构建编译消失,两种构建都须过 `@build`。
+- 非破坏性:不改 IP/卷/BBT;HW 层阶段独占 CH395F(netTask 自挂起)→ 标注"台架专用";
+- 生产代码探针全部在 `#ifdef TEST_LATENCY_PROBE` 内,产品构建编译消失,两种构建都须过 `@build`。
-## 8. 变更记录
+---
-| 2026-09-22 | **M2 实现**:`test/latency_peer.py`(`--mode net` 服务器驱动协议 + 重试包装 + JSON 报告);`test/net_latency_test.{h,c}` Phase 13 状态机(HELLO/L5_RECV/L5_SEND/L3/L10/L6/L7/DONE,含 abort 重连等待态与周期自愈);`test/ch395f_test.h` 增 `ENABLE_PHASE13_TESTS`;`test/net_test_task.c` 监听循环在 Phase 13 活动期跳过 `net_accept`(避免与协议消息争抢 netTask 队列)。**net 层(生产代码)修复**:`net_wait_result` 两段式 + 全 1 清除掩码 + 按返回值判定(D12)、`net_sock_state`/`net_resync_listen`/`net_relisten`(D14) |
-| 2026-09-22 | **M2 台架调试与判定**(COM6,run1~run15):run1 发现 **D11**(RX 饱和重复投递);run2 修 WOULDBLOCK 误判 EOF;run3~run11 定位 **D12**(`xTaskNotifyWait` 清除掩码 0 = 不清值 → 陈旧结果被重读,L7 挂死或结果错位)、**D13**(芯片空闲 RST)、**D14**(静默回收失步);run10 修 **D15**(L7 帧格式 + peer 超读);run11 修 **D16**(L7→DONE 跳转);run12/run13 两次独立完整 PASS;run14 暴露 **D12-b**(热路径探针日志洪水:9800 行/98.6%,并实测把 `queue_rt_send` 抬高 7.8×)→ 删日志;**run15(最终代码)第三次完整 PASS**(6 段全 OK、零尖峰、`1/1 PASSED`、peer exit 0、wait-idle 15.5s 判空闲),§4.2 按实测重锁 |
-| 2026-09-22 | **M2 交付物**:`docs/测试说明/m2_data/`(`m2_quiet.json`=run15 最终代码、`m2_quiet_run13.json`=run13、`m2_quiet_run14.json`=run14(仅 peer 侧有效,MCU 侧因日志洪水不作基线)、`m2_serial_excerpt_run13.txt`=串口 `[LAT]/[NET]` 证据片段、`README.md`=数据说明与逐帧明细);修复 `App/cfg_file.c` BOM 丢失(Edit 剥 BOM → ARMCC 按 CP936 解码中文字面量报 `#870-D`,`@build` 因此首轮 4 警告);生产构建与测试构建均 `@build` 0/0 |
-| 2026-09-22 | **M3 实现**:`test/net_latency_test.{h,c}` Phase 14 状态机(ui_loop → prod_cycle_mcu → relisten 三个探针窗口 + `lat_tmark_t`/`lat_mark` 跨 >25ms 安全计时 + `lat_probe_add_seg` 按段名过滤防跨段污染);生产代码探针(`ui_task.c` L11 循环周期、`ui_net_sink.c` LCDMEM 处理净成本、`net_socket.c` relisten 全程,全部在 `#ifdef TEST_LATENCY_PROBE` 内);`test/latency_peer.py` 增 `--mode remo`(生产 RemoDispBus 客户端:INIT/LCDMEM/重连多轮/有界读/JSON 报告/`--ftp-stress` 分桶);`test/ch395f_test.h` 切到 `ENABLE_PHASE14_TESTS` |
-| 2026-09-22 | **M3 台架实测**(COM6,run16~run19):修 **D18**(多站点探针跨段污染 → prod/relisten 读出 ui_loop 值)与 **D19**(Phase 14 漏调 `lat_time_init()` → 三段全 0 假绿,并加 `med==0` 判无效);修正运行顺序(**先烧录后起 peer**,否则首帧由旧固件应答);测得 **L11=10986µs、L8(MCU)=7142µs、L9 relisten=588000µs×3(D10 确认)**;peer 侧真机 65 帧 cycle med **66.0ms**、tail **52.6ms**,模拟器 50 帧 cycle med **6.8ms**、tail **0.0ms** → **Δ=59.2ms**;压力档 **E-02 量化 17.7s**(quiet/stress 分桶);新发现 **D17**(INIT 应答 u32 截半 → framebytes 恒 0) |
-| 2026-09-22 | **M3 交付物**:新增 **`docs/测试说明/链路时延实测数据分析.md`**(M1+M2+M3 汇总分析:全段基线总表、一帧时间预算拆解、三条对照实验归因、两种数据区制、缺陷量化清单 D10~D19、修复优先级建议);`docs/测试说明/m3_data/`(`m3_dev_quiet.json`/`m3_dev_stress.json`/`m3_sim_quiet.json`);本文档 §4.3 重锁 M3 段、§5 M3 完成、§6 新增 D10/E-02/D17/D18/D19 行 |
-| 2026-09-22 | **D20 调查(未完全修复,已定位)**:量化复现条件 = **每次开机后的第一个 FTP 会话**在其第 2 个数据连接返回 425(steady-state 会话 4/4 全通过);设备日志签名 = 数据连接 accept 后约 10~20ms 芯片把新连接拆掉(`sock4 DISCONNECTED, releasing (chip tcp=0x00)` → `data socket closed, accept aborted` → `cmd_stor accept failed` → 425),机制属芯片"同一 Socket 关闭→重开"快速复用竞态(与 D10/Trap 09 同族)。尝试并回退两种修法(① 传输后不关数据连接+复用监听器 → **破坏 FTP EOF 语义**,客户端等不到 EOF 整会话挂死;② 新增 `net_shutdown`(发 FIN 优雅断开)+ 等 auto_relisten 复用 + relisten 兜底 → 部分会话通过但不稳定)。**保留纯增益硬化**:`handle_disconnect_event` 在 ESTABLISHED 态回读芯片 TCP 状态、仍 ESTABLISHED 则丢弃陈旧 DISCONNECT;修复 `net_socket.c` 两处 `ch395f_get_socket_status` **只给 1 字节缓冲**(该 API 输出 2 字节)导致的栈越界 1 字节(`ch395f.h` 补显式告警)。新增复现/回归脚本 `test/ftp_regression.py`(单会话 7 次数据连接 + 1KB/32KB 往返校验)。后续:数据 Socket 轮换、或客户端遇 425 重试。`@build` 测试/产品两构建均 0/0 |
-| 2026-09-22 | **尾批解法实装到生产客户端**(`DTU-RemtoeLCD\remo_disp_server.py`):`connect()` 后自动关闭本 socket 延迟确认(Windows:`ctypes` 直调 `WSAIoctl(SIO_TCP_SET_ACK_FREQUENCY=1)`,因 Python `socket.ioctl()` 白名单不接受该码),失败则退化为"入站轻推"(帧中停顿 ≥5ms 补发 1 个 `0x00` 捎带 ACK;`NUDGE_POLL_S=5ms`/`NUDGE_INTERVAL=20ms`/上限 64 次)。用该客户端自身 `connect()+fetch_screen()` 实测:帧耗时 **68.8ms → 20.7ms(Windows 关延迟确认)/ 29.4ms(轻推兜底)**;Web 服务冒烟 HTTP 200 正常。README 增补"延迟确认抑制"章节;分析文档 §3.3 同步记录 |
-| 2026-09-22 | **L7/生产帧 ~52ms 尾批定性 + 客户端侧解法(不用抓包工具)**:机理 = 芯片 MSS=1024、发送 FIFO=1024B,MCU 每次 1024B 写为满 MSS 段(立即发送),帧尾 137B(3209=3×1024+137)为**次 MSS 段**被 Nagle 扣住等 ACK,而 Windows 的**延迟 ACK** 使该 ACK 晚到 ~50ms。三组带内实验定性:① 帧未收齐时补发 1 字节(`--ack-probe`,新增)→ tail 52→**3.9ms**;② Windows 每 socket `SIO_TCP_SET_ACK_FREQUENCY=1`(`--ack-fast`,新增,ctypes 调 `WSAIoctl`,不发任何字节)→ tail **3.9ms**(一锤定音:根因在对端延迟 ACK);③ 对照(模拟器 tail=0、满块批量无惩罚)不变。帧周期实测 **65.5→13.7ms**。**设备侧无解**(帧长 3209 为质数、TX FIFO=MSS、手册命令表无 Nagle/立即发送开关)→ 结论与数据写入 `链路时延实测数据分析.md` §3.3,数据文件 `m3_data/m3_tail_ack{probe,fast}.json` |
-| 2026-09-22 | **M3 缺陷修复与复验**(当日完成 D10/E-02/D17):**D10** `net_relisten_locked` 去掉 `osDelay(500)` 忙等 → 单次尝试 + `netTask` 每轮延迟重试(`net_relisten_try`/`net_relisten_retry_tick`,间隔 30ms/上限 2s,含防饥饿与防 fd 残留)→ relisten **588ms → 89ms** 且不再冻结 netTask,`[LAT]` 段由 FAIL 转 **OK**(阈值按实测重锁 warn 100ms);**E-02** `lftpd` 控制行/数据读改**分片有界读**(新增 `net_recv_to` + `net_msg_t.timeout_ms`;5ms 片 + 每片 `vTaskDelay(1)` 让出 CPU,总预算仍 30s)→ 受压帧 **17718ms → 74.7ms(max 106.8ms,零尖峰)**;**D17** `remo_send_init` 由 len=6 改 **8** → INIT 应答 `framebytes` **0 → 3200**,协议文档同步勘误。修复后安静档无回归(65.7ms vs 66.0ms)。另经 git 回退版 A/B 双跑确认 FTP 行为完全一致(并记录既有缺陷 **D20**:同一会话第二个数据连接恒 425);修复过程暴露"Copy-Item 还原保留旧 mtime → Keil 跳过重编译"的工作流陷阱,已写入 `链路时延实测数据分析.md` §7.2。`@build` 测试/产品两构建均 0/0 |
-| 2026-09-22 | 创建:完整规划(分段/架构/流程/基线/里程碑);**M1 实现**(`test/net_latency_test.h/.c`:DWT µs 时基 + 统计报告 + Phase 12 SPI 字节命令/DMA 时延基线;`ch395f_test.h` 增 `ENABLE_PHASE12_TESTS`;uvprojx Test 组注册) |
-| 2026-09-22 | **M1 台架首跑**(COM6):7 段全 OK、零尖峰、p95≈min;`spi_dma_1024` med=1611µs(每 chunk SPI 成本基线)。按实测重锁 §4.1 阈值(代码 `LAT_P12_CMD_*`/`a_p12_dma_warn/fail` 同步更新);修复报告行 `fail<=` → `crit<=`(防默认 Forbidden 自捕获);确定硬件层/NET 层两套 Required 标记集(硬件层无 `Network Init`)+ 测试构建 Forbidden 定制集(`TEST_REPORT` 行 `failed=0` 会自捕获默认 `FAIL` 模式);`test_parse_ip` 守卫精确到 Phase 2/5(仅 Phase 12 构建的 #177-D 实证)。`@test` 复跑 PASS |
+## 12. 数据文件与相关文档
+
+### 12.1 原始数据(`docs/测试说明/` 下)
+
+| 数据文件 | 内容 |
+|---|---|
+| `m2_data/m2_quiet.json` | M2 安静档 peer 侧(run15 最终代码):L3~L7 端到端 + 逐帧尾批明细 |
+| `m2_data/m2_quiet_run13.json` | M2 run13(独立 PASS 对照) |
+| `m2_data/m2_quiet_run14.json` | M2 run14(仅 peer 侧有效;MCU 侧因热路径日志洪水不作基线,D12-b) |
+| `m2_data/m2_serial_excerpt_run13/15.txt` | M2 串口 `[LAT]`/`[NET]` 证据片段 |
+| `m2_data/README.md` | M2 数据说明(逐帧尾批明细、方法学约束) |
+| `m3_data/m3_dev_quiet.json` | 真机 65 帧生产周期(D10/E-02/D17 修复前基线) |
+| `m3_data/m3_dev_quiet_fixed.json` | 真机 65 帧(修复后:帧周期 65.7ms、relisten OK、INIT framebytes=3200) |
+| `m3_data/m3_dev_stress.json` | E-02 压力档(修复前:17.7s 尖峰) |
+| `m3_data/m3_dev_stress_fixed.json` | E-02 压力档(修复后:med 74.7ms / max 106.8ms,零尖峰) |
+| `m3_data/m3_sim_quiet.json` | 模拟器基线 50 帧(Δ 对照,tail=0) |
+| `m3_data/m3_tail_ackprobe.json` | 尾批实验 ①:帧未收齐即回 1 字节(捎带 ACK)→ tail 3.9ms |
+| `m3_data/m3_tail_ackfast.json` | 尾批实验 ②:Windows 立即 ACK(不发探测字节)→ tail 3.9ms |
+
+### 12.2 相关文档
+
+| 主题 | 位置 |
+|---|---|
+| RemoDispBus 协议与接收设计(含 INIT 8B 勘误) | `docs/UI 设计/RemoDispBus协议与接收设计说明.md` |
+| UI 设计架构(离屏渲染 + 推流) | `docs/UI 设计/UI_设计架构.md` |
+| BSD Socket API 与双端后端 | `docs/BSD_Socket_API_使用指南.md` |
+| CH395F 陷阱史(Trap 9/20 等) | `docs/问题记录/CH395F_Trap_Records.md` |
+| 错误码传播审计(E-02 / E-04 / E-33 / E-34) | `docs/问题记录/错误码传播审计.md` |
+| CH395F 测试套件(吞吐 Phase 8 等) | `docs/测试说明/CH395F_Test_Guide.md` |
+| RAM 预算(`ui_net_sink` 6.4KB 常驻) | `docs/内存管理/RAM使用分析与优化方案.md` |
+| 串口助手(抓日志排查) | `docs/测试说明/串口助手使用指南.md` |
+| 远端显示客户端(**与本仓库同级的独立工程**,延迟确认抑制实装处) | `../DTU-RemtoeLCD/remo_disp_server.py`、`README.md` |
+| 芯片手册(§5.17/§5.18 重传、§5.42 ARP、SENBUF_FREE 语义) | `docs/芯片手册/CH395F.md` |
diff --git a/docs/测试说明/链路时延实测数据分析.md b/docs/测试说明/链路时延实测数据分析.md
deleted file mode 100644
index 157c47bd..00000000
--- a/docs/测试说明/链路时延实测数据分析.md
+++ /dev/null
@@ -1,359 +0,0 @@
-# 链路时延实测数据分析(M1 + M2 + M3 汇总)
-
-> 本文是**测量数据的汇总分析**:把 M1(SPI)、M2(芯片+队列)、M3(生产路径 + 模拟器 Δ + 压力档)
-> 三轮台架实测数据放在一起,回答一个工程问题——**远端显示"每帧要多久、慢在哪一段、能不能改"**。
->
-> - 测试规划/流程/判定阈值:`链路延迟测试说明.md`(本文 §6 的阈值与之一致)
-> - 原始数据:`m2_data/`、`m3_data/`(JSON + 串口证据片段),本文所有数字均可回溯到这些文件
-> - 台架:COM6 @115200,STM32F407ZGTx @168MHz + CH395F(SPI2 分频 8),2026-09-22,安静档无外部干扰
-> - 代码版本:M3 = Phase 14 构建(`TEST_SUITE_CH395F` + `ENABLE_PHASE14_TESTS`),`@build` 测试/产品两构建均 0 错误 0 警告
-
----
-
-## 1. 一句话结论
-
-**远端显示的一帧(3209B 全帧)端到端实测中位 66.0ms,其中约 52.6ms(79%)是帧尾那 137B 小段的等待**;
-MCU 侧真实处理只占 7.1ms,任务拾取/调度约 6.8ms。同一客户端、同一台 PC 打 PC 模拟器只要 6.8ms(Δ = 59.2ms,且模拟器**完全没有**这个尾批延迟)。
-→ **根因已确认(§3.3,无需抓包即可判定):芯片按 Nagle 扣住帧尾的次 MSS 段(MSS=1024,帧长 3209=3×1024+137),
-等对端 ACK;Windows 的延迟 ACK 把这一等推迟了 ~50ms。**
-**解法在客户端一侧**(帧未收齐时补发一个字节以捎带 ACK,或关闭本 socket 的延迟 ACK):实测 tail **52→3.9ms**、帧周期 **65.5→13.7ms(≈4.8×)**,**协议与设备代码都不需要改**。
-
-另有两个**生产级缺陷**被本轮量化,并已于当日**修复 + 台架复验**(详见 §7):
-- **D10**:断线自动重监听固定 **588ms**(Trap 09 的 `osDelay(500)` 忙等,期间 netTask 完全冻结)→ 修复后 **89ms 且不阻塞**;
-- **E-02**:一条**空闲的 FTP 控制连接**让 netTask 阻塞 **17.7s**(远端显示整帧无响应)→ 修复后受压帧 **med 74.7ms / max 106.8ms,零尖峰**;
-- **D17**:`INIT` 应答把 `帧字节数u32` 截成 6B 发出 → 客户端读到的帧字节数**恒为 0** → 修复后**正确返回 3200**。
-
-修复后安静档无性能回归(帧周期 65.7ms vs 修复前 66.0ms,尾批 50.6ms vs 52.6ms)。
-
----
-
-## 2. 全段基线总表(台架实测)
-
-| 段 | 含义 | 实测(med) | min/max | n | 阈值 warn/crit | 判定 |
-|---|---|---|---|---|---|---|
-| **L1** `spi_cmd_*` | SPI 字节命令(3 条) | 10 / 15 / 15µs | 10~16µs | 100 | 40/100µs | OK |
-| **L2** `spi_dma_*` | SPI DMA 读(4/64/1024/2048B) | 20 / 114 / **1611** / 3208µs | ±2µs | 100 | 见 §4.1 | OK |
-| **L3** `chunk_wait` | 写发送缓冲 → `SENBUF_FREE`(每 chunk) | **219µs** | 213/381µs | 64 | 10ms/500ms | OK |
-| **L5** `queue_rt_recv` | 调用方 `net_recv(DONTWAIT)` 队列全程 | **5997µs** | 5513/6033µs | 20 | 10ms/100ms | OK |
-| **L5** `queue_rt_send` | 调用方 `net_send` 队列全程 | **987µs** | 838/1209µs | 20 | 10ms/100ms | OK |
-| **L6** `req2first_mcu` | 10B 请求到齐 → 回显发出(MCU 侧) | **319µs** | 308/338µs | 10 | 20ms/200ms | OK |
-| **L7** `fullframe_mcu` | 9B 请求 → 3209B 全帧交给芯片 | **6028µs** | 5991/6066µs | 10 | 50ms/500ms | OK |
-| **L8** `prod_cycle_mcu` | **生产路径**:LCDMEM 取帧+组帧+net_send(MCU 净成本) | **7142µs** | 7111/7179µs | 50 | 20ms/200ms | OK |
-| **L10** `logtax_line` | 一行 60 字符 `printf` @115200 | **4396µs** | 4394/4422µs | 10 | 10ms/25ms | OK(D6 量化) |
-| **L11** `ui_loop` | uiTask 生产循环实测周期 | **10986µs** | 10970/21000µs | 120 | 25ms/50ms | OK |
-| **L9** `relisten` | 断线后自动重监听耗时(MCU 侧) | **588000µs** | 588000µs ×3 | 3 | 50ms/500ms | **FAIL(D10)** |
-| **L4/L8** peer cycle | 生产客户端完整帧周期(发→收齐 3209B) | **66.0ms** | 53.3/77.4ms | 65 | — | 见 §3 |
-| **Δ** | `peer cycle(设备) − peer cycle(模拟器)` | **59.2ms** | — | 65/50 | ≤80ms | 见 §4 |
-
-数据来源:L1/L2 = `链路延迟测试说明.md` §4.1;其余 MCU 侧 = M2 run15 / M3 run18 串口 `[LAT]` 行
-(`m2_data/m2_serial_excerpt_run15.txt`、`m3_data/` 各 JSON);peer 侧 = `m3_data/*.json`。
-
----
-
-## 3. 生产路径一帧的时间预算(M3 核心结果)
-
-同一客户端(`latency_peer.py --mode remo`,100ms 节拍)对**真机 :7003** 与**PC 模拟器 :7003** 各跑一轮:
-
-| 指标 | 真机(65 帧) | 模拟器(50 帧) | 差值 |
-|---|---|---|---|
-| 帧周期 total(发请求 → 收齐 3209B) | **66.0ms**(53.3/75.3,max 77.4) | **6.8ms**(0.6/13.1,max 15.7) | **+59.2ms** |
-| 首字节 first byte | 13.9ms(8.5/19.1) | 6.7ms(0.5/13.0) | +7.2ms |
-| **尾批延迟 tail lag**(末批 → 首批) | **52.6ms**(43.3/61.5) | **0.0ms**(0/0.1) | **+52.6ms** |
-| recv 批次数/帧 | 5(8B 头 + 1016 + 1024 + 1024 + **137**) | 2 | — |
-| CRC / 位图非平凡 | 65/0、65/65 | 50/0、50/50 | — |
-
-**真机 66.0ms 的构成**(三段相加 ≈ 66.5,闭合):
-
-```
-[T+0] 客户端发 9B LCDMEM 请求
-[T+6.8ms] 实测推算:uiTask 10ms 轮询拾取 + netTask 调度(≈ L11 的一半)
-[T+13.9ms] 首字节到达 = 6.8 + MCU 净处理 7.1ms(prod_cycle_mcu:取 1bpp + 组帧 + net_send 4 分块)
-[T+66.0ms] 全帧收齐 —— 最后的 137B 尾巴比前三批(1024B×3,前 4ms 内到齐)晚 52.6ms
-```
-
-- 前三批 1024B 在**首字节后约 4ms 内**全部到齐;
-- 第 4 批只有 **137B = 3209 − 3×1024**(不满块),固定迟到 43~62ms;
-- 该延迟**与负载无关**:压力档下 tail_lag 中位 52.2ms(安静档 52.6ms),说明不是调度/队列拥塞,而是**发送侧对不满块尾段的固定惩罚**。
-
-### 3.1 交叉对照实验(三条独立证据 → 归因)
-
-| 对照 | 观测 | 说明 |
-|---|---|---|
-| ① 同一客户端 → 模拟器 | tail = **0.0ms**,cycle 6.8ms | 客户端栈、Windows、请求格式全部相同 → 尾批**不是 PC 侧**造成 |
-| ② M2 `L3` 批量排空(16×4096B,全满块) | 65536B / **125.4ms**(≈2ms/KB) | 16 个满块若各带 50ms 尾巴应 ≥800ms → **满块无此惩罚** |
-| ③ 压力档(FTP 头阻塞)下的 L7/生产帧 | tail 仍 43~62ms | 尾批**不受 netTask 拥塞影响** → 属芯片 TX 行为 |
-
-**归因结论(已用带内实验证实,无需抓包)**:3209B 被分块发送后,芯片 TCP 侧对"最后一个不满块的小段"按
-**Nagle 规则扣住**(有未确认数据时不发次 MSS 小段),**对端(Windows)的延迟 ACK** 把"解除扣住"的时点推迟了约 50ms
-→ 见 §3.3 的三组判别实验(最终由"对端立即 ACK"实验一锤定音:不发任何数据也能把尾批从 52ms 降到 3.9ms)。
-
-### 3.3 尾批 ~52ms:不装 Wireshark 的定性与解法(M3 追加实验)
-
-**机理**(已确认):驱动把芯片 **MSS 设为 1024**(`CH395F_TCP_MSS`),发送 FIFO = 2×512B = 1024B
-→ MCU 每次 1024B 写恰好是**一个满 MSS 段**(Nagle 允许满段立即发送);帧长 3209B = 3×1024 + **137B**,
-最后这 137B 是**次 MSS 段** → 芯片按 Nagle 扣住,等前一数据被 ACK;Windows 的延迟 ACK 使这个 ACK 晚到 ~50ms。
-
-**判别实验(全部只用现有工具,不需要抓包)**:
-
-| # | 实验 | 做法 | tail_lag(med) | 帧周期(med) | 结论 |
-|---|---|---|---|---|---|
-| 0 | 基线 | 串行"请求→等整帧→睡 100ms" | **52.0ms** | 65.5ms | 现象复现 |
-| 1 | **带内 ACK 探针** `--ack-probe` | 帧未收齐时立刻发 **1 个解析器会忽略的字节**(0x00,非 TAG 0xAA)→ 该 TCP 段**捎带**对已收数据的 ACK | **3.9ms** | **13.7ms** | 扣住**可被对端 ACK 解除** ⇒ 等 ACK(Nagle 式),非芯片固定定时器 |
-| 2 | **对端立即 ACK** `--ack-fast` | Windows 每 socket 关延迟 ACK:`SIO_TCP_SET_ACK_FREQUENCY=1`(ctypes 调 `WSAIoctl`,**不发任何探测字节**) | **3.9ms** | **20.7ms** | **根因 = 对端延迟 ACK**(去掉它,尾批自行消失) |
-| 3 | 对照:模拟器(同 peer/同 Windows) | — | 0.0ms | 6.8ms | 芯片侧才有该扣住行为 |
-| 4 | 对照:满块批量(M2 L3,16×4096B) | — | 无累积惩罚(64KB/125ms) | — | **满 MSS 段不受影响** |
-
-> 实验 1/2 已固化为 peer 选项:`--ack-probe`(可移植:任何 OS/客户端只要在帧未收齐时发一个字节即可)、
-> `--ack-fast`(仅 Windows,无需改协议)。原始数据:`m3_data/m3_tail_ackprobe.json`、`m3_data/m3_tail_ackfast.json`。
-
-**可用解法(按推荐度)**:
-
-| 方案 | 做法 | 实测效果 | 代价 / 约束 |
-|---|---|---|---|
-| **① 客户端"补一个段"** | 帧未收齐时发 1 字节(无效字节即可)或 `KEEPLIVE`,或干脆把**下一个请求提前发** | tail **52→3.9ms**,帧周期 **65→13.7ms**(≈4.8×) | 纯客户端改动,**协议不需变**;无需任何抓包/系统设置 |
-| **② Windows 关延迟 ACK** | 每 socket `SIO_TCP_SET_ACK_FREQUENCY=1`(或注册表 `TcpAckFrequency=1`) | tail **52→3.9ms** | 仅 Windows 客户端;注册表方式需管理员+重启 |
-| **③ 客户端固定节拍(不串行等待)** | 请求按固定 100ms 节拍发,响应按序收(下一个请求的段天然捎带 ACK) | 尾批同样被提前解除,且显示帧率由 ~6fps 提到 ~10fps | 客户端需按序配对响应 |
-| **④ 设备侧** | 无可行解(见下) | — | — |
-
-**生产客户端已实装(2026-09-22)**:`DTU-RemtoeLCD\remo_disp_server.py`(远程显示工具,Web UI + exe 打包)
-把上述解法做成两条**自动择一**的路径,连接时打印生效模式:
-
-| 路径 | 触发条件 | 实现 | 实测帧耗时(客户端自身代码,100ms 节拍) |
-|---|---|---|---|
-| **A. 关延迟确认** | Windows(默认) | `ctypes` 直调 `WS2_32!WSAIoctl(SIO_TCP_SET_ACK_FREQUENCY=1)`——Python `socket.ioctl()` 有控制码白名单,不接受该码 | **20.7ms**(改善前 68.8ms,**3.3×**) |
-| **B. 入站"轻推"兜底** | 非 Windows / A 失败 | 收帧途中帧已开始但停顿 ≥5ms → 补发 1 个 `0x00`(装置状态机只认帧头 `0xBB`,丢弃该字节)→ 该段捎带 ACK | **29.4ms**(2.3×) |
-
-> 剩余 ~20ms = 客户端 Python 逐字节解析 3209B + 首字节等待(9~18ms)+ 尾块 ~4ms;
-> 该工具的刷新线程为"拉一帧 + 等 100ms",故整体显示帧率由 ~6fps 提升到 ~9fps。
-> 两条路径**都不需要改协议、不需要改装置固件**;`connect()` 会打印 `[connect] 延迟确认抑制: ...` 便于现场确认。
-
-**为什么设备侧无解**(避免后续重复投入):
-- 帧长 **3209 是质数**,无论 MSS / 分块怎么取,最后一个 TCP 段必然 < MSS(除非补字节改线格式);
-- **发送 FIFO = MSS = 1024B**(`NET_SEND_CHUNK_MAX`),单次写不可能超过 MSS ⇒ 无法用"一次写整帧"绕过;
-- 手册命令表(`docs/芯片手册/CH395F.md` §5)**没有** Nagle / 立即发送 / TCP_NODELAY 类开关(只有
- `CMD_SET_TCP_MSS`、收发缓冲、重传与 keepalive 参数;且手册 §9.2.8 明确"MSS 越小以太网小包越多")。
-
-### 3.4 两种数据区制(对容量规划的启示)
-
-| 区制 | 实测 | 主导成本 | 适用 |
-|---|---|---|---|
-| **小帧交互**(≤1 chunk 附近) | 3209B → 66ms | **固定尾批惩罚 ~52ms** | LCDMEM 整帧轮询 |
-| **批量流式**(多满块) | 4KB/7.8ms、64KB/125ms ≈ 2ms/KB | **SPI DMA(L2 = 1.61ms/KB)** | 大文件/FTP 传数据 |
-
-→ 小帧交互的 52ms 尾批**已定位并给出客户端侧解法**(§3.3:实测可降到 3.9ms);
-批量流式仍受 SPI DMA 约束(≈2ms/KB),两者是独立的两条优化线。
-
----
-
-## 4. 队列与调度:静默档不存在台阶
-
-- **L5 队列往返**:收 5997µs / 发 987µs —— 与 netTask 10ms 轮询节拍同量级,属**调度周期**而非排队拥塞;
- `queue_rt_send` 的 987µs ≈ L2 的 1 个 chunk(1611µs)量级,说明发送路径没有额外排队。
-- **L3 chunk 级**:219µs ≈ 每 chunk SPI 成本(1611µs)的 1/7 → **芯片 TX 缓冲不是瓶颈**,SPI 搬运才是。
-- **L11 uiTask 周期**:10986µs(= osDelay(10) + 渲染开销 ≈ 1ms);窗口内出现 1 次 21ms(开机菜单构建)。
- → **LVGL 渲染只占约 1ms/帧**,uiTask 不是延迟来源。
-- **M2 三次完整 PASS(run12/13/15)六段全 OK、零尖峰**:安静档下"500ms 台阶"现象**不存在**;
- 台阶只在特定条件下出现(见 §5)。
-
----
-
-## 5. 压力档:两个"秒级冻结"缺陷(E-02 与 D10)
-
-### 5.1 E-02:一条空闲 FTP 控制连接冻结全网 17.7s
-
-`--mode remo --ftp-stress`(第 15 帧起让 FTP 停留在"已 USER、未 PASS"的半登录态):
-
-| 分桶 | n | med | p95 | max | 尖峰(≥500ms) |
-|---|---|---|---|---|---|
-| quiet(0~14 帧) | 15 | 65.9ms | 75.8ms | 75.8ms | 0 |
-| stress(15~44 帧) | 30 | 69.1ms | 75.6ms | **17717.9ms** | **1** |
-
-- 症状:**第 15 帧(受压后第一帧)等待 17.7s 才拿到应答**,期间远端显示完全无响应;
-- 恢复:FTP 侧发出 `QUIT` 后**立即恢复** 55~75ms 正常服务;
-- 机制:`lftpd` 的 `read_line` 走**阻塞** `net_recv(0)`,而 netTask 是全部 socket 操作的唯一执行者
- → 该阻塞期间**所有**任务的 net 消息(含 uiTask 的 LCDMEM 应答)排队等待;
-- 边界:单次 `net_recv` 受 `NET_RECV_TIMEOUT_MS`(30s)限制,但 `read_line` 会再次进入阻塞
- → **只要客户端连接后不发完整行,netTask 就被长期占用**(实测 17.7s = 客户端空闲时长);
-- 生产含义:现场一个半开 FTP 连接 / 一次网络抖动中的 FTP 会话,即可让远端显示"整帧不动"十几秒,
- 与《远程显示延迟分析》里的 D3(E-02 头阻塞)指纹完全一致,**本轮首次给出定量证据**。
-
-### 5.2 D10:断线重监听固定 588ms(Trap 09 `osDelay(500)`)
-
-peer 侧三次"断开 → 1.5s → 重连"(`--reconnect-rounds 3`):
-
-| 通道 | 实测 |
-|---|---|
-| peer 侧:断开后重连 + INIT | 23.5 / 27.4 / 33.7ms(另两轮 23.2 / 34.7ms) |
-| **MCU 侧:auto-relisten 全程** | **588000µs × 3(三次全部 588ms)** → `verdict=FAIL` |
-
-- 588ms ≈ `net_relisten_locked()` 的 Trap 09 重试:**首次 `open` 必失败 → `close` → `osDelay(500)` → 重开成功**;
-- 这 500ms 是**真·忙等**(`osDelay` 在 netTask 里)→ 期间同样**全网冻结**;
-- 客户端可见影响:**断开后 600ms 内重连会被拒**(监听器尚未恢复);M2 调试期"170s 连接被拒"即该路径失败一次后的连锁表现;
-- 判定说明:`relisten` 段 FAIL 是**结论**(阈值 500ms 就是 D10 指纹),不是测试故障。
-
----
-
-## 6. 缺陷清单与量化(实测汇总)
-
-| 编号 | 缺陷 | 实测证据 | 状态 |
-|---|---|---|---|
-| **D10** | 重监听走 `osDelay(500)` 阻塞重试 | relisten **588ms ×3**(M3) | **已修复并验证**(588ms → **89ms**,非阻塞;§7.1) |
-| **D11** | 芯片 TCP RX 饱和下重复投递(+138%) | M2 run1:发 5000B 实收 11884B + RST | 应用侧限速规避 |
-| **D12** | net 层 `xTaskNotifyWait` **清除掩码 0 = 不清值** → 陈旧结果被重读 | M2 run3~11:L7 挂死 35s / 结果整段错位 | **已修复**(全 1 掩码 + 按返回值判定 + 32 位整体结果) |
-| **D12-a** | 位域切分与 `v!=0` 判定两个同类陷阱 | code review:32768 溢出为负、0 结果误判挂死 | 已避开并写入注释 |
-| **D12-b** | net 热路径逐调用日志 → 观测改变被测对象 | M2 run14:日志 40KB→1.04MB(98.6%),`queue_rt_send` 987µs→**7685µs** | **已修复**(删热路径日志) |
-| **D13** | 芯片空闲超时 RST(MCU 未就绪) | M2/M3 每轮首连均 10054,+2s 重连成功 | 客户端须容忍重连 |
-| **D14** | 芯片静默回收 socket → MCU 状态滞留 | M2 run7~9 | **已修复**(`net_sock_state`/`net_resync_listen`/`net_relisten`) |
-| **D15** | 测试 L7 帧与生产格式不一致 + peer 超读 | M2 run10:收 3210B、CRC fail | **已修复**(对齐 `remo_send`/`remo_handle` + 有界读) |
-| **D16** | Phase 13 L7 → 回跳 L3 造成无限循环 | M2 run11:永不下发 0xFF,peer 空等 300s | **已修复** |
-| **D17** | **INIT 应答把 u32 截成 6B → 帧字节数字段恒 0** | M3:真机与模拟器均 `framebytes=0`(应为 3200) | **已修复并验证**(发满 8B → `framebytes=3200`;§7.1) |
-| **D18** | **多打点站点共用全局探针窗口 → 跨段污染** | M3 run17:prod/relisten 都读出 ui_loop 的 ~11ms 值 | **已修复**(`lat_probe_add_seg` 按段名过滤) |
-| **D19** | **Phase 14 漏调 `lat_time_init()` → 全 0 假绿** | M3 run16:三段全 0 却 `verdict=OK` | **已修复**(补初始化 + med==0 判无效) |
-| **D20** | **FTP 同一会话的第 2 个数据连接失败(425)** | 复现条件已量化:**每次开机后的第一个 FTP 会话**在该会话第 2 个数据连接处失败(之后的会话 4/4 全通过);设备日志签名 = 数据连接 accept 后约 10~20ms 出现 `DISCONNECTED, releasing (chip tcp=0x00)` → `data socket closed, accept aborted` → 425 | **已定位未修**(详见 §7.4,2026-09-22 日志复核修正机理见 §7.4.1):属芯片"同一 Socket 快速复用(关闭→重开)"的时序竞态;两次修法(优雅断开+复用监听器 / relisten 兜底)都不稳定已回退;**保留安全硬化**(陈旧 DISCONNECT 甄别 + 两处 1 字节栈越界修复);复现脚本 `test/ftp_regression.py` |
-| **E-02** | 空闲 FTP 控制连接冻结 netTask(头阻塞) | M3:受压帧 **17718ms**(`QUIT` 即恢复) | **已修复并验证**(→ **74.7ms** med / 106.8ms max,零尖峰;§7.1) |
-
-> D17/D18/D19 为本轮 M3 自查发现(D18/D19 属测试工装缺陷,会导致"绿而无效"的数据);
-> D20 为修复过程中的 A/B 对照**顺带发现**的既有缺陷(已用 git 回退版固件双跑确认与本次改动无关)。
-
----
-
-## 7. 缺陷修复与验证(2026-09-22,修复当日台架复测)
-
-### 7.1 三项已修复缺陷的前后对照
-
-| 缺陷 | 修复内容 | 修复前实测 | **修复后实测** | 判定依据 |
-|---|---|---|---|---|
-| **D10** | `net_relisten_locked` 去掉 `osDelay(500)` 忙等 → 单次尝试 + `netTask` 每轮延迟重试(间隔 30ms,上限 2s) | relisten **588 000µs ×3**(且 netTask 冻结 500ms) | **89 000µs(89ms)**,`verdict=OK`,期间其他任务不受阻 | `[LAT] seg=relisten` + 日志 `relisten retry ok` |
-| **E-02** | `lftpd` 控制行/数据读改**分片有界读**(`net_recv_to` 5ms 片 + 每次让出 1ms,总预算仍 30s) | 受压帧 **17 718ms**,≥500ms 尖峰 1 次 | **med 74.7ms / p95 90.8ms / max 106.8ms,尖峰 0** | `m3_dev_stress_fixed.json`(30 受压帧) |
-| **D17** | `remo_send_init` 由 `len=6` 改 `len=8`(字段定义本就是 `[W u16][H u16][FB u32]`) | `framebytes=0`(u32 被截半) | **`framebytes=3200`,`payload_len=8`** | peer JSON `init.geom` |
-
-**修复后安静档(无回归)**:`ui_loop` 10986µs、`prod_cycle_mcu` 7140µs、帧周期 med **65.7ms**、尾批 **50.6ms**、
-CRC 65/0、`=== Phase 14: 1/1 PASSED ===` —— 与修复前(66.0ms / 52.6ms)在噪声内一致 ⇒ **修复未引入性能回归**。
-
-**关键实现要点(供后续维护)**:
-1. **D10 重试必须防饥饿**:调用方(accept/recv 路径每 10ms 一次)只登记一次重试,实际重试由 netTask 轮询到点执行;
- 若让调用方每次立即重试并重置时刻,会永不重试。重试间隔 100ms→30ms 后恢复时间 147ms→89ms(芯片沉降约 60ms)。
-2. **E-02 分片长度 = 其他任务的最坏等待**:100ms 片下受压帧仍 ~750ms(多片排队),收到 **5ms** 后才降到 ~75ms。
-3. **分片循环必须显式让出 CPU**:某片可能因 `net_poll` 自身耗时而在首轮即返回超时 → 本任务(Normal)立即重排队
- 会形成忙循环并**饿死 BelowNormal 的 uiTask**(实测 2.4~3.4s 周期尖刺);加 `vTaskDelay(1)` 后尖刺归零。
-
-### 7.2 关键教训(工程流程)
-
-修复过程中出现一次**"改了代码但没进固件"**:A/B 对照用 `git checkout` 回退后再用 `Copy-Item` 还原,
-`Copy-Item` 保留了**旧 mtime**,比回退版编出的 `.o` 更旧 → Keil 判定"源比目标旧"→ **未重新编译**,
-固件里仍是回退版(表现为"改了却没效果")。此后必须在还原后 **touch 源文件**(或核对编译日志里出现该文件)。
-排障路径(可复用):在改动处加临时日志 → 若无输出,即可判定"代码没进二进制",先用特征字符串在 `.axf` 里验证。
-
-### 7.4 D20 调查记录(FTP 数据连接 425,未完全修复)
-
-**现象与准确复现条件**(2026-09-22 台架,多轮统计):
-
-| 会话(同一固件、不重启) | 结果 |
-|---|---|
-| 开机后第 1 个 FTP 会话 | **失败在会话内第 2 个数据连接**:`LIST/NLST` 成功 → 随后的 `STOR` 返回 **425 Can't open data connection**;控制连接随后被对端关闭(客户端见 EOF) |
-| 第 2 个及之后的会话(稳态) | **全部 11/11 通过**(含 1KB 与 32KB 的 STOR/RETR 往返、连续 7 次数据连接) |
-
-即:**不是"每会话第 2 个数据连接必失败",而是"每次开机后的第一个会话"才会命中**(此前把两次 A/B 都在开机首会话做的,误判为确定性缺陷)。
-
-**设备日志签名**(首会话失败时):
-
-```
-CMD PASV → sock4 standalone accept(数据连接建立)
-CMD STOR
-sock4 DISCONNECTED, releasing (chip tcp=0x00) ← 建立后 ~10ms 芯片把新连接拆掉
-[ERR] data socket closed, accept aborted → cmd_stor accept failed → 425
-sock4 auto-relisten ok / relisten retry ok ← 监听器随后自行恢复
-```
-
-**机理(已定位)**:FTP 数据通道每次传输结束都要 `net_close`(对端以 EOF 判"传完"),随后下一次 PASV 又要在**同一芯片 Socket**上 `net_socket/bind/listen` 重开;在开机首个会话的时序下,上一条连接的 teardown 与本次重开交叠,芯片在新连接建立后立刻将其拆除(读到 chip TCP = CLOSED)。与 D10(Trap 09 沉降)、D14(静默回收)同属"单连接 Socket 复用"家族。
-〔2026-09-22 提交前日志复核修正:该 DISCONNECT 在**成功**会话中同样出现且其后数据照常读出,"芯片拆除新连接"不成立;失败的真实判据是**延迟 DISCONNECT 事件与首次 accept 的先后竞态**,详见 §7.4.1。〕
-
-**试过但未采用的修法**(均已回退,保留结论备查):
-
-| 尝试 | 结果 |
-|---|---|
-| 传输结束**不关**数据连接(保留监听器供复用) | **破坏 FTP 语义**:服务端不发 FIN 客户端就等不到 EOF,整会话挂死 → 必须关闭数据连接 |
-| 新增 `net_shutdown()`(发 FIN 优雅断开)+ 复用监听器(等 auto_relisten 回 LISTEN,最多 1s;兜底改走 `net_relisten()`) | 部分会话 11/11,但**不稳定**(3 会话中仍有 1 次 425,且后续会话出现控制连接连不上)→ 回退 |
-
-**保留的安全硬化**(与 D20 同源事件竞态,纯增益):
-
-1. `handle_disconnect_event`:ESTABLISHED 状态下收到 DISCONNECT 时**回读芯片 TCP 状态**,仍为 `CH395F_TCP_ESTABLISHED` 判为陈旧事件丢弃(防"上一条连接的迟到 teardown 误杀新连接");
-2. `net_socket.c` 两处 `ch395f_get_socket_status` 调用**只给了 1 字节缓冲**(该 API 输出 2 字节:`[0]`Socket 状态、`[1]`TCP 状态)→ 栈越界 1 字节;已改为 2 字节缓冲(`ch395f.h` 补显式告警)。
-
-**后续建议**(按性价比):
-
-1. **数据 Socket 轮换**:不要总是复用最低空闲 fd(现固定为 sock4),让连续两次数据连接落在不同芯片 Socket 上(需 net 层支持"分配偏好/最小索引",或 lftpd 保留一个 spare 监听器占位)→ 直接绕开"同一 Socket 快速复用"竞态;
-2. **客户端重试**:425 属 RFC 959 的瞬态 4xx,控制器/上位机在数据连接失败后重试一次即可规避(现场最省事的规避手段);
-3. **寄存器级定位**:在首会话复现窗口内打印 `CMD_GET_SOCKET_STATUS_SN`(2 字节)与 SINT 序列,确认芯片在 teardown/reopen 交叠时的内部状态迁移;
-4. 复现脚本:`test/ftp_regression.py --host `(单会话内 7 次数据连接 + 1KB/32KB 往返内容校验,退出码 = 是否全通过)。
-
-#### 7.4.1 补充:提交前串口日志复核(2026-09-22,纯日志分析,未改代码)
-
-对台架串口日志(00:26~00:27 窗口,含失败会话与紧随其后的多个成功会话)逐行复核,机理修正如下:
-
-1. **DISCONNECT 事件在成功会话中同样出现**:成功会话的 STOR 数据连接在 `standalone accept` 后约 31ms 同样上报 `sock4 DISCONNECTED, releasing (chip tcp=0x00)`,但其后数据照常读出(`recv sock4 len=1027`)、传输完成 → 该事件**不是**"芯片拆除新连接",而是**上一条数据连接(LIST 通道,服务端 `net_close`)的延迟断开事件**落在同一芯片 Socket 上(`net_close_locked` 的排空窗口结束后芯片才补报);
-2. **成败唯一差异 = 竞态**:失败会话中 lftpd 的首次 `net_accept`(`lftpd_inet_accept_timeout`)运行在 DISCONNECT 事件**之后**(state 已置 CLOSED → `data socket closed, accept aborted` → 425);成功会话中首次 accept 抢在事件之前(state=ESTABLISHED 立即返回),其后的 DISCONNECT 不影响数据路径——MCU 侧 CLOSED 状态不妨碍从芯片缓冲读数据(fun_para bit3=1 保留缓冲);
-3. **现有甄别为何拦不住**:延迟 DISCONNECT 时刻 `ch395f_get_socket_status` 回读 `tcp=0x00`,不反映新连接的 ESTABLISHED 态 → "回读芯片状态判陈旧"的甄别条件(`st[1]==ESTABLISHED` 才丢弃)在此场景恒不成立,只能防"芯片状态仍 ESTABLISHED"的另一类陈旧事件;
-4. **对修法方向的修正**:
- - **数据 Socket 轮换(优先,结论不变)**:连续数据连接落在不同芯片 Socket,直接消除"上一条连接的断开事件打到下一条新连接"的路径;
- - 备选:已 accept 的连接对迟到 DISCONNECT 免疫(成功会话已证明数据路径不受影响)——但须同时处理"事件先到、accept 后到"的顺序,单做此项不能覆盖失败场景;
- - 客户端重试(425 属 RFC 959 瞬态 4xx)作为现场规避手段不变。
-
-> 注:D20 修复按用户指示**暂缓**(2026-09-22);本节仅修正调查记录,未改任何代码。
-
-### 7.5 建议的后续项(按性价比)
-
-1. **尾批 ~52ms(占一帧 77%)—— 已定性并给出解法(§3.3)**:客户端在帧未收齐时补发一个字节
- (`--ack-probe`)或关闭本 socket 的延迟 ACK(`--ack-fast`),实测 **65.5ms → 13.7 / 20.7ms**,协议与设备代码均不改。
- 若客户端无法改动(第三方 App),退路是**协议补位**(把应答补齐为 1024 的整数倍,使尾段变为满 MSS 段),
- 但需客户端同步改解析(多出的补位字节须被跳过),优先级低于客户端侧解法。
-2. **D20(FTP 数据连接 425)**:见 §7.4 —— 已定位为"开机首会话 + 芯片同 Socket 快速复用"竞态;优先做**数据 Socket 轮换**,
- 现场可用**客户端重试**规避。
-3. **日志税**:一行 `printf` 4.4ms(L10);热路径同步日志会直接污染测量与时延(D12-b 实测 7.8×)→ 热路径禁止日志。
-4. **任务模型**:L11 显示 uiTask 渲染仅 ~1ms/帧、L5 队列往返 ~6ms 属轮询节拍
- ⇒ **调优先级/栈不会改善体感**;预算应花在上面第 1、2 项。
-
----
-
-## 8. 复现与数据文件清单
-
-```powershell
-# M3(生产路径,需要设备已烧 Phase 14 固件)
-& "MDK-ARM\build.bat" # 0 错 0 警(Phase 14 构建)
-& '.venv\Scripts\python.exe' tools\serial_tool\serial_tool.py clear
-& "MDK-ARM\download.bat" # 先烧录,再起 peer(顺序不可反!)
-& '.venv\Scripts\python.exe' test\latency_peer.py --mode remo --host 192.168.1.100 --port 7003 `
- --samples 65 --cadence-ms 100 --reconnect-rounds 3 --wait 120 `
- --json "docs\测试说明\m3_data\m3_dev_quiet.json"
-# 压力档(E-02)
-& '.venv\Scripts\python.exe' test\latency_peer.py --mode remo --host 192.168.1.100 --port 7003 `
- --samples 45 --stress-from 15 --ftp-stress --json "docs\测试说明\m3_data\m3_dev_stress.json"
-# 模拟器基线(Δ 的对照端)
-& "simulator\build_sim.bat"; .\simulator\build\Debug\lvgl_sim.exe # 等 7003 就绪
-& '.venv\Scripts\python.exe' test\latency_peer.py --mode remo --host 127.0.0.1 --port 7003 `
- --samples 50 --json "docs\测试说明\m3_data\m3_sim_quiet.json"
-# 尾批定性/解法(都不需要抓包工具)
-& '.venv\Scripts\python.exe' test\latency_peer.py --mode remo --host 192.168.1.100 --port 7003 `
- --samples 30 --ack-probe --json "docs\测试说明\m3_data\m3_tail_ackprobe.json" # 带内补 ACK
-& '.venv\Scripts\python.exe' test\latency_peer.py --mode remo --host 192.168.1.100 --port 7003 `
- --samples 30 --ack-fast --json "docs\测试说明\m3_data\m3_tail_ackfast.json" # 关延迟 ACK
-```
-
-| 数据文件 | 内容 |
-|---|---|
-| `m3_data/m3_dev_quiet.json` | 真机 65 帧生产周期(**修复前**基线) |
-| `m3_data/m3_dev_stress.json` | E-02 压力档(**修复前**:17.7s 尖峰) |
-| `m3_data/m3_dev_quiet_fixed.json` | 真机 65 帧(**修复后**:帧周期 65.7ms、relisten 已 OK、INIT framebytes=3200) |
-| `m3_data/m3_dev_stress_fixed.json` | E-02 压力档(**修复后**:med 74.7ms / max 106.8ms,零尖峰) |
-| `m3_data/m3_sim_quiet.json` | 模拟器基线 50 帧(Δ 对照,tail=0) |
-| `m3_data/m3_tail_ackprobe.json` | 尾批实验 ①:帧未收齐即回 1 字节(捎带 ACK)→ tail 3.9ms |
-| `m3_data/m3_tail_ackfast.json` | 尾批实验 ②:Windows 立即 ACK(不发探测字节)→ tail 3.9ms |
-| `m2_data/m2_quiet.json`(run15)等 | M2 芯片+队列基线与串口证据 |
-| `m2_data/README.md` | M2 数据说明(含逐帧尾批明细、方法学教训) |
-
-**复现注意**(M3 实测踩到的坑,务必遵守):
-1. **先烧录、后起 peer**——否则 peer 会连到**上一版固件**的 :7003(实测:首帧由旧固件应答后被复位 RST);
-2. peer 首连常因芯片空闲超时被 RST(10054),`--wait` 重试是**正常路径**;
-3. 探针三段窗口是**串行全局窗口**,多打点站点必须用 `lat_probe_add_seg()` 按段名过滤;
-4. `[LAT]`/peer JSON 都带主机时间戳,可直接合并;设备 RTC 比主机慢约 4s(显示差异,非测量误差);
-5. **还原被回退的源文件后必须 touch**(`Copy-Item` 保留旧 mtime,Keil 会因此跳过重编译 → "改了却没效果",见 §7.2)。
diff --git a/docs/问题记录/远程显示延迟分析.md b/docs/问题记录/远程显示延迟分析.md
deleted file mode 100644
index 38ca5c5d..00000000
--- a/docs/问题记录/远程显示延迟分析.md
+++ /dev/null
@@ -1,436 +0,0 @@
-# 远程显示(RemoDispBus)延迟分析报告
-
-> **模块名称**:远程显示链路(`ui_net_sink` → `net_socket` → CH395F → DTU-RemoteLCD)
-> **文档目的**:针对"远端 LCD 连 MCU 有明显延迟感、连 PC 模拟器没有"这一现象,做**静态全链路分析**:
-> 拆解延迟预算、列出候选原因(按可能性排序并给出代码级证据)、给出可执行的排查流程与逐因对策。
-> **日期**:2026-09-22
-> **状态**:**只读分析,未修改任何代码**;本文所有行号基于当前工作副本,改动后需复核。
-> **对照实验事实**:同一份共享服务端 `Drivers/BSP/UI/ui_net_sink.c` + 同一个 DTU-RemoteLCD 客户端,
-> 连模拟器(Winsock)流畅、连 MCU(CH395F)有明显延迟 → 差异全部落在 `net_*` 的 **MCU 后端**,
-> 协议与客户端实现可视为已排除。
-
----
-
-## 0. 摘要
-
-**这不是"网线慢",而是 MCU 侧 CH395F 通道上的结构性开销 + 芯片重传粒度放大。**
-
-"模拟器没有延迟"这个对照实验的证明力有限:它只证明了**协议与客户端没问题**,
-**没有**证明链路/芯片层没问题——因为模拟器根本不走 CH395F、不走 SPI、不走 netMsgQueue、
-不走 FreeRTOS 调度,Winsock 一次 `send()` 就把 3209 字节交给 PC 协议栈连续发出。
-
-### 原因排序
-
-| # | 原因 | 量级 | 可能性 | 关键证据 |
-|---|---|---|---|---|
-| **D1** | CH395F TCP 重传周期 **500ms** —— 一次丢包 = 整帧多等半秒 | **500ms ~ 4s** | 高 | `net_socket.c:143-144`(8 次 × 500ms,总窗口 4s);客户端注释自证"整帧分 4 块耗时 **0.5~3 秒**属正常"(`remo_disp_server.py:165-174`),0.5/1.0/1.5s 台阶正是 500ms 整数倍 |
-| **D2** | 整帧 3209B + CH395F **1KB 发送 FIFO 停止等待** —— 每帧固定 4 次串行分块握手 | 15~30ms 基线,弱网放大 | 高(基线) | `NET_SEND_CHUNK_MAX = CH395F_SEND_BUF_SIZE_BYTES = 1KB`(`net_socket.c:916-921`、`ch395f.h:119-129`);`net_send_locked` 每块写完必须等 `SENBUF_FREE`(`:955-985`) |
-| **D3** | **netMsgQueue 头阻塞**(既有审计 **E-02**):lftpd/对时的阻塞 recv 在 netTask 上下文空转 | **最长 30s 全网停摆** | 高(间歇性秒级卡顿的首要嫌疑) | `net_socket.c:357` 一次只取一条消息 + `:1019-1067` 阻塞 recv 在 netTask 内空转;触发源 `lftpd_inet.c:106-109`、`time_sync.c:84`(均 `flags=0`) |
-| **D4** | PC 端 TCP **延迟确认(40~200ms)** 与"每段等 ACK"的停止等待发送端交互 | 40~200ms/块 | 中高 | 客户端注释已记录;CH395F 发送缓冲仅 1×MSS,无法让 2 段同飞骗过延迟确认 |
-| **D5** | uiTask 节拍:10ms 循环 + **每拍只 recv 一次 64B** + step 在 poll **之前** | 10~30ms + 多等一拍 100ms | 中 | `ui_task.c:91-95`;`ui_net_sink.c:441`(`RM_RX_BUF = 64`,`:38`) |
-| **D6** | 阻塞式日志在关键路径:`fputc` 逐字节 `HAL_UART_Transmit` @115200 | **每行 ~5ms**(阻塞调用任务) | 中(确定的基线税) | `Src/usart.c:479-484`;`net_socket.c:1025` 每次收数据都 `DBG_INFO` 一行;`dbg_cfg.h:28/40` `DBG_ENABLE` + 时间戳均开 |
-| **D7** | 物理链路:双工不匹配 / 网线 / Wi-Fi / ARP | 不可预估,直接喂给 D1 | 中 | `net_task.c:100` **强制** 100M 全双工;若交换机自协商 → 双工不匹配是丢包经典成因 |
-| **D8** | 客户端固定成本:100ms 拉取节拍(**按键后不立即取帧**)+ 1bpp→PNG→base64→WebSocket | 100ms + 数 ms | 确定的基线 | `remo_disp_server.py:588-647`(`_refresh_stop.wait(0.1)`);`:736-741` `on_key` 只发键不触发取帧 |
-| **D9** | LVGL 全量渲染(`LV_DEF_REFR_PERIOD=20ms`、`RENDER_MODE_FULL`)+ 每次 flush 整帧 memcpy,uiTask 为 **BelowNormal** 易被抢占 | 单次渲染数 ms~数十 ms | 中低(待实测) | `ui_port.c:126-127/80-86`;`lv_conf.h:11/22`;任务优先级见 `AGENTS.md` 任务表 |
-| **D10** | 断线重连路径含 `osDelay(500)`(netTask 上下文,最多 3 次) | 0.5 ~ 1.5s | 中(仅断线/重连时) | `net_socket.c:1098-1105` |
-
-### 一句话行动建议
-
-先做 §3 的 **Step 1(客户端三点打点)+ Step 2(Wireshark 抓包)**,半天内不改固件即可定性;
-随后落 **P0 两项一行级改动**:`set_retrans_period(500) → 200`(治 D1 症状)+
-删 `net_socket.c:1025` 的 per-recv 日志(治 D6)。根治方向是 **差量/RLE + 帧版本号**(治 D2,连带把 D1 的影响面压到几乎为零)。
-
----
-
-## 1. 链路与延迟预算
-
-### 1.1 一次"按键 → 看到画面"的完整旅程
-
-```mermaid
-sequenceDiagram
- participant B as 浏览器
- participant F as Flask/SocketIO
refresh_loop
- participant C as CH395F
- participant N as netTask
- participant U as uiTask
-
- B->>F: 按键 (socketio "key")
- F->>C: KEY 帧 10B(on_key 不触发取帧)
- Note over F: D8 最多等 100ms 才轮到下一次 fetch_screen
- F->>C: LCDMEM 请求 10B
- C-->>N: RECV 中断 → net_poll
- Note over N: D3 netMsgQueue 头阻塞窗口
- U->>N: net_recv(64B)(队列 + TaskNotify)
- Note over U: D5 最多 10ms 相位 + LVGL 渲染占时
- N-->>U: 按键注入 UI
- Note over U: D9 下一拍才渲染(step 在 poll 之前)
- U->>N: net_send 3209B
- loop D2 4 次串行分块
- N->>C: WRITE_SEND_BUF ≤1KB(SPI DMA ≈1.6ms/KB)
- C-->>N: SENBUF_FREE 中断
- end
- Note over C: D1 丢一段 → 重传 +500ms(D7 供包)
- C-->>F: 4 × 1024B 段
- Note over F: D4 Windows 延迟确认 40~200ms
- F->>B: PNG → base64 → socketio → canvas
-```
-
-### 1.2 各段耗时(健康值 vs 异常值)
-
-| 段 | 位置 | 健康耗时 | 异常时的耗时 | 备注 |
-|---|---|---|---|---|
-| 客户端拉取节拍 | `remo_disp_server.py:647` | ≤100ms | ≤100ms(固定) | 按键后不提前取帧 |
-| 线路 RTT(请求) | 以太网 | ~0.1ms | 1~50ms(Wi-Fi) | — |
-| CH395F → netTask 事件 | `net_task.c:212-214` | ≤1ms | 数秒(D3) | 主循环 `osDelay(1)` |
-| uiTask 拾取请求 | `ui_task.c:91-95` | ≤10ms | 数十 ms(D5/D9) | 每拍只 recv 一次 |
-| 队列往返(每次 `net_*`) | `net_socket.c:1190-1290` | ~1-2ms | 数秒(D3) | 投递 + TaskNotify,2 次任务切换 |
-| 收数据日志 | `net_socket.c:1025` → `Src/usart.c:479` | ~5ms | ~5ms(固定税) | 阻塞在 netTask 内 |
-| 取帧 + 组帧 | `ui_net_sink.c:223-226` | ~0.1ms(2×3200B memcpy) | — | 见 D2-方案⑤ |
-| 4 次分块发送 | `net_socket.c:955-985` | 15~25ms | **每 chunk 上限 1s**(`NET_SEND_TIMEOUT_MS`)→ 4s | SPI 5.25Mbit/s ≈1.6ms/KB |
-| 分段传输 + ACK | 线路 + PC | ~0.5ms | **500ms/1s/1.5s 台阶**(D1)、40~200ms/块(D4) | 决定性判据 |
-| PNG + base64 + WS + canvas | `remo_disp_server.py:510-617` | 数 ms | 数 ms(固定) | 双端相同 |
-| **合计(典型)** | | **≈130~170ms** | **0.5s ~ 30s** | 模拟器合计 ≈105~115ms |
-
-### 1.3 关键量化常量
-
-| 常量 | 值 | 来源 |
-|---|---|---|
-| 屏幕 / 帧大小 | 160×160 **1bpp** = 3200 B | `ui_net_sink.c:33-35` |
-| LCDMEM 应答 wire 长度 | 4(头) + 4(偏移) + 3200(位图) + 1(CRC) = **3209 B** | `ui_net_sink.c:226` + `remo_send` |
-| TCP MSS | 1024 | `ch395f.h:119`、`net_socket.c:140` |
-| CH395F 每 socket 发送缓冲 | 2 块 × 512B = **1KB** | `ch395f.h:122-128` |
-| `NET_SEND_CHUNK_MAX` | **1KB**(受发送缓冲与 DMA 载荷 2048 双重钳制) | `net_socket.c:916-921` |
-| LCDMEM 一帧分块数 / 分段数 | **4**(1024×3 + 137) | 3209 ÷ 1024 |
-| CH395F 内部 RAM | 48 块 × 512B = 24KB,**8 socket 均分 6 块** | `net_socket.c:196-202` |
-| TCP 重传 | **8 次 × 500ms = 4s 窗口** | `net_socket.c:143-144`;手册 §5.17/§5.18 默认 12 次/500ms |
-| SPI2 速率 | APB1 42MHz ÷ 8 ≈ **5.25 Mbit/s** → 1KB ≈ **1.6ms** | `Src/spi.c:84`、`.ioc APB1CLKDivider=DIV4`;分频 8 为 PCB 约束(`AGENTS.md`) |
-| 发送等待超时 | 1s(`NET_SEND_TIMEOUT_MS`) | `net_config.h:32-34` |
-| 接收超时 | 30s(`NET_RECV_TIMEOUT_MS`) | `net_config.h:36-38` |
-| 调试串口 | USART1 115200,**逐字节阻塞发送** | `Src/usart.c:479-484` |
-| LVGL 刷新周期 / 色深 | 20ms / 1bpp(`RENDER_MODE_FULL`) | `lv_conf.h:11/22`、`ui_port.c:126-127` |
-
----
-
-## 2. 原因清单(机制 / 证据 / 判据 / 对策)
-
-### D1 CH395F TCP 重传周期 500ms —— 秒级延迟的放大器 ★首要嫌疑
-
-- **机制**:CH395F 的 TCP 重传是**固定周期**(非指数退避的 RTT 自适应)。工程显式设为
- `ch395f_set_retrans_count(8)` + `ch395f_set_retrans_period(500)`
- (`Drivers/BSP/NET/net_socket.c:142-144`)。任何一段 1024 字节数据在链路上丢失,
- 就要等满 500ms 才重发;两段的代价是 1s,依此类推,最坏 4s 后判发送失败。
- 一帧 LCDMEM 应答是 4 个段 → 丢包概率随段数线性上升。
-- **证据**:客户端源码已把这一现象写进注释——
- "整帧 LCDMEM(3209 字节) 分 4 块到达耗时 **0.5~3 秒以上属正常现象**"
- (`../DTU-RemtoeLCD/remo_disp_server.py:165-174`,客户端为**与本仓库同级**的独立工程)。
- 观测到的延迟若呈 0.5/1.0/1.5s 台阶,
- 就是 500ms 粒度的指纹。
-- **判据**:Wireshark `tcp.port==7003 && tcp.analysis.retransmission`,看是否有重传、
- 以及相邻重传的间隔是否 ≈500ms。
-- **对策**:见 §4-S1(调小重传周期)+ §4-S2(把一帧压到 1~2 段,从根本上减少需要重传的数据)。
-
-### D2 整帧 3209B × 1KB 发送 FIFO 停止等待
-
-- **机制**:CH395F 发送缓冲只有 2 块 = 1KB(`ch395f.h:122-128`),且手册明确
- "**单片机向 Socket 发送缓冲区写入一次数据后,必须等到产生 `SINT_STAT_SENBUF_FREE`
- 中断后才可以写入下一次数据**"(`docs/芯片手册/CH395F.md:568/623`)。
- `net_send_locked` 因此把 3209B 切成 4 块,每块"SPI 写 → 置 `send_ready=0` → 死循环
- `net_poll()` 等中断"(`net_socket.c:955-985`)。期间 **netTask 全程占用**,
- 既不能处理其它队列消息,也不能处理其它 socket 事件。
-- **叠加成本**:每次 `WRITE_SEND_BUF` 是 1KB SPI DMA(≈1.6ms @5.25Mbit/s,
- `ch395f.c:1117-1147`,内部忙等 `s_spi2_dma_done`,`ch395f.c:89-135`);
- 组帧路径还有两次 3200B `memcpy`(`ui_net_sink.c:223` 取帧 + `:114` 组帧)。
-- **基线代价**:约 15~25ms/帧;**常驻带宽 ≈32KB/s**(客户端 10Hz 拉取,画面没变也照发)。
-- **判据**:§3-Step4 实验 A(把应答临时改成 100B,延迟若立刻正常 → 确诊本项)。
-- **对策**:§4-S2(差量/RLE + 版本号 + 由拉改推)、§4-S4(提高发送块配额与 MSS)、
- §4-S5(少拷一次,顺带省 3.2KB RAM)。
-
-### D3 netMsgQueue 头阻塞(= 既有审计 **E-02**)
-
-- **机制**:所有跨任务 `net_*` 操作都排队到 netTask 串行执行
- (`net_socket.c:1190-1337` 投递 + `xTaskNotifyWait` 阻塞等回执),
- 而 `net_process_messages` **一次只取一条消息并处理到底**(`:357`)。
- 只要有任何一个消费者发起 `flags=0` 的阻塞 recv,netTask 就会在
- `net_recv_locked` 的等待循环里空转最长 **30s**(`:1019-1067`,
- `NET_RECV_TIMEOUT_MS=30000`),此时 **uiTask 每 10ms 的轮询、accept、其它服务的收发全部排在队里**。
-- **触发源**:`lftpd_inet.c:106-109`(FTP 控制连接 `read_line` 阻塞 recv——
- **FTP 客户端连上后空闲 >30s 即触发,属常见工况**)、`time_sync.c:84`。
- `ui_net_sink` 自己是正确的 `NET_MSG_DONTWAIT` 模式,所以它既是受害者也不是触发者。
-- **特征**:**间歇性整屏冻结数秒~数十秒**,与"是否在动 FTP/对时"相关。
-- **判据**:卡顿发生时检查是否有 FTP 会话挂着(或临时停掉 ftpTask/timeSyncTask 做对照,§3-Step4-C)。
-- **对策**:§4-S6(阻塞 recv 改 DONTWAIT 分片 + 调用方重投;超时按服务分档)。
-
-### D4 PC 端 TCP 延迟确认(Delayed ACK)
-
-- **机制**:Windows 对**满 MSS** 段默认延迟确认(最多 200ms,通常 40~200ms,等待捎带)。
- 它每收到 2 个满 MSS 段才立即 ACK。而 CH395F 发送缓冲只有 1×MSS,
- **无法把两个段同时放上线**,于是每一段都成了"新的第一段" → 每一段都吃满延迟确认。
-- **判据**:Wireshark `tcp.analysis.ack_rtt`;或看 4 个 1024 段之间的间隔是否 ≈40~200ms
- (正常应 <1ms)。
-- **对策**:§4-S2(一帧压到 1 段后自然免疫);必要时客户端机器关延迟确认
- (注册表 `TcpDelayedAcknowledge=0`,机器级副作用需评估),或参考客户端跑 Linux + `TCP_QUICKACK`。
-
-### D5 uiTask 节拍与"每拍只收一次 64B"
-
-- **机制**:主循环顺序是 `ui_port_step(10)` → `ui_net_sink_poll()` → `osDelay(10)`
- (`App/task/ui_task.c:91-95`)。两个后果:
- 1. **按键注入晚一拍**:KEY 是在 poll 阶段注入的,而 LVGL 的 `lv_timer_handler()`
- 已经在本轮开头跑完 → 按键要到下一轮(≤10ms 后)才被读到并渲染。
- 2. **每轮只做一次 `net_recv`,上限 64B**(`ui_net_sink.c:441`,`RM_RX_BUF=64`):
- 排空不彻底,突发多帧时跨多拍才吃完。
-- **判据**:uiTask 循环打点(实测循环周期);§3-Step4-D(把 poll 挪到 step 之前对照)。
-- **对策**:§4-S7(poll 前置 + while 排空)。
-
-### D6 阻塞式日志躺在关键路径上
-
-- **机制**:`fputc` = `HAL_UART_Transmit(&huart1, &ch, 1, HAL_MAX_DELAY)`
- (`Src/usart.c:479-484`),逐字节阻塞;`DBG_ENABLE` 与 `APP_TIMESTAMP_ENABLE` 均开
- (`dbg_cfg.h:28/40`),带时间戳一行 ≈55~70 字符 → **约 5ms 阻塞在调用任务内**。
- `net_recv_locked` **每次收到数据都打印一行**
- `DBG_INFO("recv sock%d len=%d", ...)`(`net_socket.c:1025`)→ 每帧固定给 netTask 加 ~5ms,
- 且这段时间 CH395F 完全没人轮询。
-- **判据**:临时注释该行后复测(这本身也是 P0 快改项之一)。
-- **对策**:§4-P0-b(删/降级该行)+ §4-S8(`fputc` 改环形缓冲 + TX 中断/DMA)。
-
-### D7 物理链路与协商("网络延迟"最字面的一种)
-
-- **机制**:`net_stack_bring_up` **强制** `ch395f_set_phy(CH395F_PHY_100M_FULL)`
- (`App/task/net_task.c:100`)。若交换机/网卡端口处于自协商,会出现**双工不匹配**
- (一端强制全双工、一端半双工)→ 冲突与静默丢包 → 直接喂给 D1,表现为"莫名秒级卡顿"。
- 另外 PC 在 Wi-Fi 上会引入抖动与丢包;ARP 表项失效也会造成首包停顿
- (CH395F 默认 ARP 重传 3 次 × 1s,手册 §5.42)。
-- **判据**:
- 1. 开机日志核对 `PHY: 100M FULL`(`net_task.c:117-122`)——若显示 `10M HALF`/`DISCONNECT`,本项即确诊;
- 2. 抓包看重传分布;
- 3. 用 `ch395f_get_unreach_ipp()`(`ch395f.c:1326-1331`)看是否有解析不到的目标 IP;
- 4. **拓扑对照**:PC 有线直连设备、不经交换机、远离 Wi-Fi,再测。
-- **对策**:§4-S3。
-
-### D8 客户端固定成本(双端相同,但决定"下限")
-
-- **机制**:刷新线程"取帧 → 编码 → 推送 → `wait(0.1)`"串行
- (`remo_disp_server.py:588-647`),因此**有效刷新率 = 1 / (fetch 耗时 + 100ms)**;
- 若 fetch = 0.5s,刷新率直接掉到 2fps,"延迟感"主要来自这里。
- `on_key` 只 `send_key`、**不触发取帧**(`:736-741`)→ 按键平均多等 50ms、最多 100ms。
- 1bpp → PIL PNG → base64 → SocketIO → 浏览器解码,是一串纯 CPU 开销(3200B 帧其实不需要 PNG)。
-- **判据**:§3-Step1 打点即可分离出"客户端等待"与"网络等待";§3-Step4-B 用裸 TCP 客户端对照。
-- **对策**:§4-S9。
-
-### D9 LVGL 全量渲染与任务优先级
-
-- **机制**:`RENDER_MODE_FULL`(`ui_port.c:126-127`)+ `LV_DEF_REFR_PERIOD=20ms`
- (`lv_conf.h:22`)→ 任何 invalidate 都重画整屏 160×160;flush 回调无条件整帧
- `memcpy` 3200B(`ui_port.c:80-86`)。uiTask 优先级 **BelowNormal**(`AGENTS.md` 任务表),
- 而 defaultTask(Normal,syslog 唯一写者,写 NAND)与 netTask/ftpTask/timeSyncTask(Normal)
- 都会抢占它 → `ui_net_sink_poll()` 的实际调用间隔可能远大于 10ms。
-- **判据**:uiTask 循环打点统计实际周期分布;待机页(时钟每秒刷新)与菜单页对比。
-- **对策**:§4-S7;若实测渲染是瓶颈,再考虑降低刷新率/按需 invalidate(改前先核 RAM)。
-
-### D10 断线重连路径含 netTask 内 `osDelay(500)`
-
-- **机制**:`net_relisten_locked` 里 `ch395f_open_socket` 失败时
- `ch395f_close_socket()` + `osDelay(500)`,最多重试 3 次(`net_socket.c:1098-1105`)。
- 这**在 netTask 上下文**执行 → 期间全网停摆 0.5~1.5s。
- 客户端侧连续 5 次拉取失败会主动断开重连(`remo_disp_server.py:597-644`),
- 于是"卡顿 → 判失败 → 重连 → 更卡"可能互相放大。
-- **判据**:日志中 `sock0 LISTEN TIMEOUT` / `open socket` 失败 / 客户端 `device_disconnected`。
-- **对策**:§4-S6(改非阻塞重试)。
-
-### 相关既有缺陷(会掩盖/放大延迟,建议一并处理)
-
-| 编号 | 位置 | 与延迟的关系 |
-|---|---|---|
-| **E-02** | `错误码传播审计.md` §3.2 | 即本文 D3,已立案 |
-| **E-04** | `ui_net_sink.c:117` | `remo_send` 丢弃 `net_send` 返回值 → 弱网**短写静默截断**(net 层只打 `short send`,`:990`),客户端 CRC 不过 → 断连 → 触发 D10 → 观感是"卡完直接黑屏/卡住" |
-| **E-33/E-34** | `ch395f.c:89-135/181-210` | DMA 失败 void 化无错误通道;字节级命令 `HAL_MAX_DELAY` → 一次 SPI 故障可能让 netTask 永久挂死(无看门狗) |
-
----
-
-## 3. 排查流程
-
-### Step 0 —— 先分类(决定后面查哪一支)
-
-| 观测现象 | 首要指向 | 跳到 |
-|---|---|---|
-| 每帧都**稳定**慢 ≈150~300ms | 结构性基线(D2/D5/D6/D8/D9) | Step 1 + Step 4-A |
-| **间歇**卡,且 ≈500ms 的整数倍 | **D1** 丢包重传 | Step 2 + Step 4-A |
-| **间歇**整屏冻结数秒~数十秒 | **D3** 队列头阻塞 / D10 重连 | Step 3 + Step 4-C |
-| 只在开 FTP / 对时后卡 | D3 + 单连接 socket 争用 | Step 4-C |
-| 换网口/换线/换 PC 就好 | D7 | Step 2 + 拓扑对照 |
-
-### Step 1 —— 客户端三点打点(成本最低,最先做)
-
-在 `../DTU-RemtoeLCD/remo_disp_server.py::fetch_screen` 记录 **t0 请求发出 / t1 收到首字节 /
-t2 整帧收齐**,每帧输出一行:`req2first=..ms total=..ms bytes=..`。
-
-判读规则(最有决定性的一步):
-
-| 结果 | 结论 | 下一步 |
-|---|---|---|
-| `req2first` 大(>50ms) | 设备迟迟不开口:uiTask 拾取慢 / 队列头阻塞 / CH395F 收包慢 | Step 3、Step 4-C/D |
-| `req2first` 小但 `total` 大 | 尾块传输慢:1KB 分块 + 重传 + 对端 ACK | Step 2 |
-| `total` 落在 500/1000/1500ms | **D1 确诊** | §4-P0-a |
-| `total` 稳定 ≈20~40ms 但观感仍慢 | 瓶颈在客户端 100ms 节拍/编码(D8) | §4-S9 |
-
-### Step 2 —— PC 抓包 `tcp.port == 7003`
-
-| 过滤器 / 观察项 | 判读 | 对应 |
-|---|---|---|
-| `tcp.analysis.retransmission` | 有重传 & 间隔 ≈500ms → **D1** | S1 / S3 |
-| 4 个 1024B 段的间隔 | 正常 <1ms;≈40~200ms → 被对端 ACK 门控(D4) | S2 / S9 |
-| 请求 → 响应首包间隔 | >50ms → 设备侧调度慢(D3/D5/D6) | Step 3 / Step 4 |
-| `tcp.analysis.zero_window` / `window_full` | CH395F 接收窗口仅 2KB 被打满 | 收路径核查 |
-| `tcp.analysis.out_of_order` / `dupack` | 链路丢包/乱序 | S3 |
-| 客户端 `sendall` 是否阻塞 | 请求发不出去 = 设备窗口小/未读 | 收路径核查 |
-
-### Step 3 —— MCU 侧打点(serial_tool 抓开机+运行日志)
-
-> ⚠️ **先关掉 `net_socket.c:1025` 的 per-recv 日志再打点**,否则 ~5ms/行的阻塞日志
-> 就在被测路径上,会自欺欺人(见 D6)。
-> ⚠️ 按 `AGENTS.md` 铁律:**新增字符串字面量必须纯 ASCII**。
-
-```powershell
-& '.venv\Scripts\python.exe' tools/serial_tool/serial_tool.py open
-& '.venv\Scripts\python.exe' tools/serial_tool/serial_tool.py clear
-# 烧录/复位(采集必须先于烧录)
-& '.venv\Scripts\python.exe' tools/serial_tool/serial_tool.py wait-idle --timeout 90 --settle-ms 8000 --max-ms 45000
-```
-
-打点位置与建议指标(用 `HAL_GetTick()` 或 DWT CYCCNT):
-
-| 位置 | 指标 |
-|---|---|
-| `net_send_locked`(`net_socket.c:938`) | 每 chunk 等待 ms + 整次总 ms + `sent/len`(已有 `short send` 日志可先看) |
-| `ui_net_sink_poll`(`ui_net_sink.c:406`) | `net_recv` 返回 → `net_send` 返回的 ms |
-| `ui_task` 循环(`ui_task.c:91`) | 实测循环周期(`osDelay(10)` 之外多花了多少)→ 反推 D5/D9 |
-| `net_process_messages`(`net_socket.c:357`) | 单条消息处理耗时 + 队列积压深度 → **直接量化 D3** |
-| 开机日志 | `PHY: 100M FULL` 是否成立(D7) |
-
-### Step 4 —— 四个隔离实验(二分定位)
-
-| # | 实验 | 做法 | 结论 |
-|---|---|---|---|
-| **A** | **缩小响应**(★最有决定性) | 临时让 `CMD_LCDMEM` 只回 ~100B(而非 3204B) | 延迟立刻正常 → 确诊 D2(及被 D1/D4 放大);仍慢 → 瓶颈在请求/调度路径(D3/D5/D6) |
-| **B** | **裸 TCP 客户端** | 30 行 Python:直连 7003,循环发 LCDMEM 并计时往返(不走 Flask/WebSocket/PNG) | 变快 → D8;不变 → 设备侧 |
-| **C** | **关掉 FTP/对时** | 临时只保留 :7003 服务 | 变快 → **D3**(E-02 头阻塞)/ 单连接 socket 争用 |
-| **D** | **poll 前置** | `ui_task.c` 循环改成先 `ui_net_sink_poll()` 再 `ui_port_step(10)` | 变快 → D5 |
-
-> 实验 A/B/D 是**临时验证手段**;正式修法以 §4 为准,勿把临时改动提交。
-
----
-
-## 4. 解决方案
-
-### P0 —— 一行级、低风险、先做(需 `@build` 0 错误 0 警告)
-
-| 编号 | 改动 | 治 | 预期 / 代价 |
-|---|---|---|---|
-| **P0-a** | `net_socket.c:144` `ch395f_set_retrans_period(500)` → **`200`** | D1 症状 | 单次丢包代价 500ms→200ms(体感卡顿降一档)。代价:总窗口 8×200=1.6s,链路真断时判失败更快(对远显是有利的)。手册 §5.18 允许 ≤1000ms,**最小值未标注,需台架确认 200 被接受**(可用 `GET_CMD_STATUS` 校验) |
-| **P0-b** | 删除/降级 `net_socket.c:1025` 的 per-recv `DBG_INFO` | D6 | 每帧省 ~5ms netTask 阻塞;顺带减少 syslog 噪声与 NAND 写入 |
-
-### S1 —— 重传与超时参数分档
-
-- `NET_SEND_TIMEOUT_MS=1000` / `NET_RECV_TIMEOUT_MS=30000`(`net_config.h:32-38`)
- 目前是**全局一刀切**:对远显,30s 阻塞是灾难;对 FTP,1s 发送超时可能偏紧。
- 建议按服务分离(`ui_net_sink` 用短超时 + 非阻塞,FTP 保留长超时)。
-
-### S2 —— 差量 / RLE + 帧版本号 + 由拉改推(**根治,收益最大**)
-
-| 手段 | 做法 | 效果 |
-|---|---|---|
-| **差量 + RLE** | `ui_flush_cb`(`ui_port.c:80-86`)中与上一帧比对,记录变化矩形/行程;协议新增 `CMD_LCDMEM_DIFF`(保留旧 `LCDMEM` 全帧以兼容) | 1bpp 菜单典型变化 **<100B** → 单段一帧 → 延迟 ≈ 一个 RTT;D1 丢包影响面从 4 段降到 1 段 |
-| **帧版本号** | 设备维护 `frame_seq`,客户端请求带上自己看到的 seq;未变则回 `NOCHANGE`(~10B) | 稳态带宽从 **32KB/s → ≈0**,把通道留给"真变化帧" |
-| **由拉改推** | flush 后设备主动推 UNSOL 帧,客户端不必等 100ms 节拍 | 按键响应从"≤100ms + 取帧"降到"一个 RTT" |
-| 兼容 | 新命令走同一帧格式与 CRC;旧客户端仍可用 `LCDMEM` 全帧 | 双端共用 `ui_net_sink.c` 不变 |
-
-### S3 —— 物理链路与协商
-
-1. 核对开机日志 `PHY: 100M FULL`(`net_task.c:117-122`)。
-2. 二选一:交换机端口也**强制 100M 全双工**(与 `net_task.c:100` 对齐),
- 或让 CH395F 走自协商(需评估既有约束后改动)。
-3. 换网线 / 换交换机端口 / **PC 有线直连**做对照;PC 不要在 Wi-Fi 上测。
-4. 用 `ch395f_get_unreach_ipp()` 观测不可达 IP(ARP 侧线索)。
-
-### S4 —— 提高发送块配额与 MSS
-
-- 现状:48 块 × 512B 均分给 8 个 socket(`net_socket.c:196-202`),**实际只用到 3 个**
- (:21 FTP / :8888 对时 / :7003 远显)。
-- 改法:按**实际使用数**分配,例如 3 个 socket 各 16 块(接收 3KB + 发送 3KB),
- MSS 提到 1460 → 3209B 从 4 chunk/4 段降到 **1~2 chunk/段**。
-- **约束**:
- - `NET_SEND_CHUNK_MAX` 仍受 SPI DMA 载荷 2048B 二次钳制(`net_socket.c:916-921`),
- 单块实际 ≤2048B;
- - **Trap 20 铁律**:单次写长度 **必须 ≤ 发送缓冲**,否则写穿 FIFO 静默丢数据
- (`docs/问题记录/CH395F_Trap_Records.md` Trap 20);
- - `net_init` 的分配是按 `i * blocks_per_socket` 硬算 8 个(`:196-202`),改分配策略需同步
- `net_bind` 里的重复分配路径(`:518-521`);
- - 改前先读 `docs/问题记录/CH395F_Trap_Records.md`,改后跑 CH395F 测试套件 Phase 8(100KB 吞吐)。
-
-### S5 —— 去掉一次 3200B 拷贝(顺带省 RAM)
-
-`g_tx[3209]`(`ui_net_sink.c:40`)与 `g_lcdmem_data[3204]`(`:41`)各占一份,
-LCDMEM 路径是"帧缓冲 → `g_lcdmem_data+4` → `g_tx+4`"两次 memcpy。
-让 `g_tx` 直接承接位图(取帧直接写到 `g_tx + 8`)可省一次 3200B 拷贝 + **约 3.2KB 常驻 RAM**
-(该缓冲已被 `docs/内存管理/RAM使用分析与优化方案.md` §1 列为 6.4KB 常驻大户)。
-**注意**:改 RAM 占用前先按 `AGENTS.md` 跑 `scripts/ram_report.py` 核算。
-
-### S6 —— 消除 netTask 头阻塞(= 修 E-02,顺带修 D10)
-
-1. `lftpd_inet.c:106-109`、`time_sync.c:84` 的 `flags=0` 阻塞 recv →
- **`NET_MSG_DONTWAIT` 分片 + 调用方重投 + 总超时**(`ui_net_sink` 已是正确范式)。
-2. `net_relisten_locked` 的 `osDelay(500)`(`net_socket.c:1104`)改为**非阻塞重试**
- (置状态位,交主循环下轮重试),别在 netTask 里睡。
-3. 顺带修 **E-04**:`ui_net_sink.c:117` 校验 `net_send` 返回值 `== len`,
- 短写则 `DBG_ERROR` + `net_close(g_client)` 触发客户端重连补帧(让截断可观测、可自愈)。
-
-### S7 —— uiTask 节拍与收包排空
-
-- `ui_task.c:91-95` 改为**先 `ui_net_sink_poll()` 再 `ui_port_step(10)`**(省掉按键的一整拍)。
-- `ui_net_sink_poll` 内改成 **while 排空**:一轮里连续 `net_recv` 直到 `WOULDBLOCK`,
- 而不是每轮只收一次 64B;必要时把 `RM_RX_BUF`(`ui_net_sink.c:38`)与请求帧长对齐。
-- 若 Step 3 实测渲染是瓶颈:降低 `LV_DEF_REFR_PERIOD` 的无效刷新、按需 `invalidate`,
- 或把 sink 轮询从渲染中解耦(渲染前后各排空一次收包)。
- **改栈/缓冲前先核 RAM 与 `scripts/stack_report.py`。**
-
-### S8 —— 日志改非阻塞
-
-`Src/usart.c:479-484` 的 `fputc` 从"逐字节阻塞 `HAL_UART_Transmit`"改为
-**环形缓冲 + TX 中断/DMA 发送**(关键路径上只写环形缓冲)。
-收益:所有任务的日志成本从 ~5ms/行降到 µs 级;风险:需处理缓冲溢出丢弃策略与
-`HardFault` 路径的同步冲刷。
-
-### S9 —— 客户端侧(DTU-RemoteLCD)
-
-1. **按键后立即取帧**:`on_key`(`:736-741`)里发完键直接触发一次 `fetch_screen`,
- 不等 100ms tick(省平均 50ms、最多 100ms)。
-2. **不传 PNG**:直接把 1bpp 位图(或差量)发给前端,用 canvas 绘制,
- 省掉 PIL 编码 + base64 + 浏览器 PNG 解码。
-3. **自适应节拍**:把固定 `wait(0.1)` 改为"上一帧耗时补偿"式调度,避免慢帧叠加。
-4. **关闭 Windows 延迟确认**(可选,机器级副作用需评估):
- `TcpDelayedAcknowledge=0`;或参考客户端跑 Linux + `TCP_QUICKACK`。
-5. **拓扑纪律**:验证用 PC 一律**有线**、与设备同交换机或直连。
-
----
-
-## 5. 相关文档
-
-| 主题 | 位置 |
-|---|---|
-| RemoDispBus 协议与接收设计 | `docs/UI 设计/RemoDispBus协议与接收设计说明.md` |
-| UI 设计架构(离屏渲染 + 推流) | `docs/UI 设计/UI_设计架构.md` |
-| BSD Socket API 与双端后端 | `docs/BSD_Socket_API_使用指南.md` |
-| CH395F 陷阱史(Trap 20 发送分块等) | `docs/问题记录/CH395F_Trap_Records.md` |
-| 错误码传播审计(**E-02 / E-04 / E-33 / E-34**) | `docs/问题记录/错误码传播审计.md` |
-| CH395F 测试套件(吞吐 Phase 8) | `docs/测试说明/CH395F_Test_Guide.md` |
-| RAM 预算(`ui_net_sink` 6.4KB 常驻) | `docs/内存管理/RAM使用分析与优化方案.md` |
-| 串口助手(抓日志排查) | `docs/测试说明/串口助手使用指南.md` |
-| 远端显示客户端(**与本仓库同级的独立工程**,目录名含历史拼写 `Remtoe`) | `../DTU-RemtoeLCD/remo_disp_server.py`、`remo_disp_ui.html` |
-| 芯片手册(§5.17/§5.18 重传、§5.42 ARP、SENBUF_FREE 语义) | `docs/芯片手册/CH395F.md` |
-
----
-
-## 6. 变更记录
-
-| 日期 | 变更 |
-|---|---|
-| 2026-09-22 | 创建:远程显示延迟静态全链路分析。拆解一帧旅程与延迟预算(典型 ≈130~170ms,异常 0.5s~30s);列 D1~D10 十项候选原因并给代码级证据与判据(首要嫌疑 = CH395F 重传周期 500ms `net_socket.c:143-144`,其次 = 3209B 整帧 × 1KB 发送 FIFO 停止等待);给出 Step 0~4 排查流程(分类 → 客户端三点打点 → Wireshark → MCU 打点 → 四个隔离实验)与逐因对策(P0 两项一行级改动 + S1~S9);关联既有审计 E-02/E-04/E-33/E-34。**未修改任何代码。** |
diff --git a/test/latency_peer.py b/test/latency_peer.py
index 46db6291..d00b1786 100644
--- a/test/latency_peer.py
+++ b/test/latency_peer.py
@@ -3,7 +3,7 @@
Server-driven protocol: the MCU (testTask, port 8080) sends one operation
byte; the peer executes the corresponding traffic pattern. See
-docs/测试说明/链路延迟测试说明.md §2.5 for the full protocol spec.
+docs/测试说明/链路延迟测试说明.md §4.5 for the full protocol spec.
Ops (MCU -> peer):
0x01 L5 recv flood : peer sends 50 x 100B (5ms apart)
@@ -42,7 +42,7 @@ OP_DONE = 0xFF
L5_CHUNKS = 20 # 2026-09-22 run1: 50 chunks saturated the chip RX buffer
# (window close -> retransmit re-delivery -> RST); 20 @ 50ms
- # = 1KB/s quiet baseline (see 链路延迟测试说明.md §8)
+ # = 1KB/s quiet baseline (see 链路延迟测试说明.md §6.2)
L5_LEN = 100
L6_SIZE = 10
L7_REQ_LEN = 9
diff --git a/test/net_latency_test.c b/test/net_latency_test.c
index fd58b080..fcad0ead 100644
--- a/test/net_latency_test.c
+++ b/test/net_latency_test.c
@@ -350,7 +350,7 @@ const lat_stats_t *lat_probe_stats(void)
/* ============================================================
* Phase 12 — SPI 时延基线(L1/L2,硬件层模式,无需 PC)
*
- * 测量对象(见 链路延迟测试说明.md §1):
+ * 测量对象(见 链路延迟测试说明.md §3):
* L1 SPI 字节命令 —— 3 个代表性命令的完整 SPI 事务时长
* (公开函数内部含 spi_begin/spi_end,即一次完整事务)
* L2 SPI DMA —— ch395f_read_recv_buf(N) 完整 DMA 事务
@@ -370,7 +370,7 @@ const lat_stats_t *lat_probe_stats(void)
#define LAT_P12_DMA_LEN_MAX 2048U /* L2 单发上限 = CH395F_SPI_DMA_MAX_PAYLOAD */
/* L1 阈值(us)—— 2026-09-22 台架首跑后重锁(实测 med 10~15us;
- * warn ≈ 2×med,crit ≈ 2×warn,见 链路延迟测试说明.md §4.1) */
+ * warn ≈ 2×med,crit ≈ 2×warn,见 链路延迟测试说明.md §6.1) */
#define LAT_P12_CMD_WARN_US 40U
#define LAT_P12_CMD_FAIL_US 100U
@@ -530,7 +530,7 @@ void net_latency_phase12_run(void)
#define LAT_P13_L5_CHUNKS 20U /* L5 flood 块数(2026-09-22 run1 实证:50×100B@5ms
* 达 20KB/s 饱和芯片接收缓冲 → 窗口关闭重传 →
* 重复递交 11884/5000B → 对端 RST;降至 1KB/s
- * 静默档,见 链路延迟测试说明.md §8 run1 记录) */
+ * 静默档,见 §6.2 M2 基线) */
#define LAT_P13_L5_CHUNK_LEN 100U
#define LAT_P13_L3_SENDS 16U /* 每次 4KB = 4 chunk → 16×4 = 64 探针样本
* (run3 后从 32 减半:控制持续 TX 总量) */
diff --git a/test/net_latency_test.h b/test/net_latency_test.h
index ff5471f1..c5fc2f45 100644
--- a/test/net_latency_test.h
+++ b/test/net_latency_test.h
@@ -23,7 +23,7 @@ extern "C" {
#include
/*
- * === 链路时延测试阶段一览(见 链路延迟测试说明.md §2.2) ===
+ * === 链路时延测试阶段一览(见 链路延迟测试说明.md §4.2) ===
* Phase 12 [硬件层] SPI 字节命令 / DMA 时延基线(L1/L2,无需 PC) —— M1(已完成)
* Phase 13 [NET 层] 芯片 + 队列时延(L3~L7/L10,需 PC peer) —— M2(已实现)
* Phase 14 [NET 层] 生产路径时延(L8~L11 + 模拟器 A/B,需 PC peer)—— M3(规划)
@@ -32,7 +32,7 @@ extern "C" {
* M1 各段(<5ms)与 M2 调用方侧测量(单次 <25ms)用单次读数差即可;
* 跨 25ms 的段(L4/L8 整帧与重传尖峰)由 PC 端 peer 计时。
*
- * Phase 13 协议(服务器驱动:MCU 发操作字节,peer 执行;详见说明文档 §2.5):
+ * Phase 13 协议(服务器驱动:MCU 发操作字节,peer 执行;详见说明文档 §4.5):
* HELLO = 5A A5 58 01(peer → MCU,4 字节)
* 操作字节(MCU → peer):0x01=L5 recv flood / 0x02=L5 send flood /
* 0x03=L6 单请求 / 0x04=L7 LCDMEM 全帧 / 0x05=L3 send flood / 0xFF=完成
@@ -106,7 +106,7 @@ void lat_stat_finish(lat_stats_t *p);
* max_spikes - 尖峰数上限:spikes 超过 → FAIL
* p_verdict - 输出:本段 verdict(LAT_VERDICT_*)
* 返回值:无
- * 函数说明:报告行格式见 链路延迟测试说明.md §2.4;字符串字面量纯 ASCII
+ * 函数说明:报告行格式见 链路延迟测试说明.md §4.4;字符串字面量纯 ASCII
*/
void lat_stat_report(const char *p_seg, const lat_stats_t *p,
uint32_t warn_med_us, uint32_t fail_med_us,
diff --git a/test/test_config.h b/test/test_config.h
index 560b5bc9..14513bce 100644
--- a/test/test_config.h
+++ b/test/test_config.h
@@ -27,7 +27,7 @@ extern "C" {
* 当前启用 CH395F 套件 + Phase 13(独立运行,勿与其他 Phase 混选)。
* 执行:先起 test/latency_peer.py --mode net --port 8080,再 @test 烧录。
* 恢复产品构建 = 注释掉下面两行(TEST_SUITE_CH395F 与 ch395f_test.h 的
- * ENABLE_PHASE13_TESTS)。详见 docs/测试说明/链路延迟测试说明.md §3.2。 */
+ * ENABLE_PHASE13_TESTS)。详见 docs/测试说明/链路延迟测试说明.md §5.2。 */
#define TEST_SUITE_CH395F
//#define TEST_SUITE_STORAGE
//#define TEST_SUITE_GD5F