Files
STM32F4-Base/docs/测试说明/专项验证/链路延迟测试说明.md
T
wandering1 7efa58d9a1
CI / 静态门禁 (pre-commit) (push) Failing after 25s
CI / 主机侧单测 (MSVC) (push) Successful in 48s
network: 台架 IP 迁到 192.168.31.100,并把重复写死的值收进单一真源
分两段(用户裁定 2026-11-02):先做地址迁移,再把同类字面量收敛成
单一真源,以后再改地址只动一处。

一、IP 迁移 192.168.1.100 → 192.168.31.100(gw 同步 → .31.1)
- 工厂默认:Drivers/BSP/NETCFG/net_cfg.c 的 NET_CFG_DEF_IP/_GW
- 注释/文档示例、主机与板级测试、台架 py 脚本、两份 sim_config.ini
- 字节序教学点同步换算 hex:0x6401A8C0 → 0x641FA8C0(内存
  C0 A8 1F 64),旧版示例引文与历史 Trap 记录按冻结纪律不动
- UI 黄金基准:网口设置 4 态 + shot_info 共 5 张 .bin 由
  check_content_pages.py --update 重刷(渲染值 IP 变了,属预期变更)
- 保留判定:客户点表引用(IEC104/继电保护规划)、芯片手册摘录、
  日期冻结的变更记录行、已退役台架档案 MDK-ARM/retracted_bench_*

二、单一真源收口(A+B+C+D,用户批准全做)
- 新建 test/host/bench_defaults.py:台架 py 默认值唯一定义点
  (DEVICE_IP/DEVICE_GW/PC_IP/NET_BCAST),22 个脚本改为引用;
  MDK-ARM 两探针与 scripts/board_ui_nav.py 经 sys.path shim 引入
- test/board/net_test_task.h 新增 TEST_PC_PEER_IP,板测三处
  PHASE*_REMOTE_IP 改引该宏(保留 -D 覆盖);Phase1 断言改
  memcmp 数组比较,数组各只出现一次
- simulator/tests/check_content_pages.py 从 net_cfg.c 正则解析
  NET_CFG_DEF_* 推导期望值(解析失败即报错、不静默兜底),
  脚本内台架 IP 字面量清零
- 装饰性注释去具体值(net_cfg.h/net_socket.h/net_socket.c/
  net_socket_sim.c);字节序教学 hex 对刻意保留为唯一写值点
- 刻意不动:主机单测夹具 IP(逐文件钉住的任意值是正确测试
  实践)、文档正文 IP(那是文档内容本身)

顺带:scripts/gen_task_table.py 规范串清掉「,B5」——编号清退提交
72b4b094 改了受管区没重跑生成器,task-table 门禁因此一直红

验证:产品 @build 0/0;CH395F 套件开关构建 0/0 后还原产品态;
6 套主机单测 rc=0;content_pages(含宏推导路径)/菜单 8 态/
推流位比对全 PASS;bom-drift 99 无漂移;doc-links/coding-standard/
cppcheck 绿;py_compile 23 文件 + --help 冒烟(含 shim 绝对路径)
2026-11-02 14:46:42 +08:00

782 lines
65 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 链路延迟测试说明(分段基线 / 实测现状 / 故障对照)
> **模块名称**:CH395F 网络链路(远端显示 RemoDispBus / FTP / 测试链路)
> **文档目的**:链路时延的**唯一权威文档**——怎么测、当前实测值是多少、慢在哪一段、
> 哪些缺陷已修/未修、新异常出现时查什么。
> **数据日期**:2026-09-22 台架(COM6 @115200;STM32F407ZGTx @168MHz + CH395F,SPI2 分频 8)
> **数据文件**:`../测试数据/m2_data/`、`../测试数据/m3_data/`(JSON + 串口证据片段);本文所有数字均可回溯到这些文件
> **代码状态**:D10 / D17 / D12 / D14 / D15 / D16 已修复并台架复验(§8);
> D1(重传周期 200ms)/ D6(per-recv 日志)2026-09-22 已实施并台架复验(P0-a/P0-b,§10.1);
> D20(开机首个 FTP 会话第 2 个数据连接 425)已定位未修(§8.2)
> **合并说明**:本文统一原《链路延迟测试说明》《链路时延实测数据分析》《远程显示延迟分析》
> (2026-09-22);过程性历史记录(逐日变更、调试过程)不再保留,现状以本文与 git 历史为准。
---
## 1. 一句话结论(当前状态)
**远端显示一帧(3209B 全帧)端到端实测中位 61.6ms,其中约 52.6ms(85%)是帧尾那 137B 小段的等待**;
MCU 侧真实处理只占 6.7ms,任务拾取/调度约 2.3ms(2026-09-22 重锁:P0-b 删 per-recv 日志后
帧周期 66.0→61.6ms,拾取/调度 6.8→2.3ms,差的 ~4.4ms 即原 per-recv 日志税)。同一客户端、同一台 PC 打 PC 模拟器只要 6.8ms
(Δ = 54.8ms 为 CH395F 链路净贡献,且模拟器完全没有尾批延迟)。
- **尾批根因(已确证,无需抓包)**:芯片按 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)、头阻塞(一条空闲 FTP 控制连接冻结全网 17.7s)、
D17(INIT 应答帧字节数字段恒 0)等已量化并**修复 + 台架复验**(§8);
D1(重传周期 500→200ms)/ D6(per-recv 日志)2026-09-22 已实施(§10.1);D20 已定位未修(§8.2)。
---
## 2. 链路与关键参数
### 2.1 拓扑与服务
| 服务 | 端口 | 后端 |
|---|---|---|
| 远端显示 RemoDispBus | 7003 | `ui_net_sink`(真实 uiTask 生产路径) |
| FTP(lftpd) | 21 | 控制 + PASV 数据通道(数据通道复用监听 Socket,CH395F 单连接模型) |
| 对时 | 8888 | `time_sync` |
| 时延测试链路(M2) | 8080 | Phase 13 测试协议(服务器驱动) |
客户端 = 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<br/>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 头阻塞窗口(已两阶段修复:分片 → 挂起 recv 零占用)
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 丢一段 → 重传 +200ms(现 8×200ms,P0-a 已实装;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 次 × 200ms = 1.6s 窗口**(固定周期,非 RTT 自适应;原 8×500ms=4s) | `NET_RETRANS_COUNT/PERIOD_MS`(`net_config.h`) |
| SPI2 速率 | APB1 42MHz ÷ 8 ≈ **5.25 Mbit/s** → 1KB ≈ **1.6ms**(分频 8 为 PCB 约束,勿"优化") | `Src/spi.c` |
| 发送等待超时 | 1s(`NET_SEND_TIMEOUT_MS`) | `net_config.h` |
| 接收超时 | **无默认预算**(`net_recv` 无限阻塞,超时归调用方——`net_select` / `net_recv_to`;无 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(SPI DMA = 1.61ms/KB) | 大文件 / FTP 传数据 |
两条独立的优化线:小帧交互的尾批已定性并解决;批量流式仍受 SPI DMA 约束。
---
## 3. 分段定义与测量点
| 名称 | 测什么 | 测点 / 方法 | 基线(实测,详见 §6) | 超标指向 |
|---|---|---|---|---|
| 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 配置 |
| 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/总线异常 |
| 发送分块握手 | `write_send_buf(1KB)` → `SENBUF_FREE` 的等待时长(= `net_send_locked` 每 chunk 的真实等待) | MCU(netTask 上下文),Phase 13 | **219µs**/chunk | 芯片发送路径 / 重传(D1) / 链路(D7) |
| 芯片→线上+PC | 数据交入芯片协议栈 → PC 首见字节 / 收齐全帧(peer 逐 `recv()` 批次打时间戳) | PC(peer),Phase 13/14 | 首批 8.6ms;**段间间隙 ≥200ms = 重传**(P0-a 后粒度;旧固件 500ms) | D1/D4/D7("网络延迟"直接证据) |
| 队列往返 | 调用方视角 `net_recv(DONTWAIT)` / `net_send` 全程 = 入队 + netTask 调度 + 处理 + 通知 | MCU(调用方计时),Phase 13,**两档**:静默基线 + 压力档(并发 FTP 流量) | 静默:收 1223µs / 发 988µs(2026-09-22 重锁,P0-b 后;发送分块握手对 DONTWAIT 路径逐位不变,重验留待 Phase 13 下次窗口) | D3(头阻塞,已两阶段修复) / D5 调度 |
| 请求→首字节 | peer 发 10B 请求 → MCU 收齐并回显(服务端视角);与 PC 侧对照,差值 = 线路 + PC 栈 | MCU + PC 双侧,Phase 13 | MCU 986µs / PC 3.9ms | MCU 收路径/调度 |
| 整帧 3209B 往返 | LCDMEM 全帧:PC 发 → 收齐(含发送分块握手+芯片→线上+PC) | PC 为主,Phase 13 | 61.0ms(M2);**spike(≥500ms)=0** | D1+D2+D4 综合 |
| 生产路径帧周期 | **真实 uiTask + 真实 `ui_net_sink`**(:7003)完整帧周期:请求拾取(10ms 拍)+ 队列 + 发送 + 芯片→线上+PC + peer 侧编码 | MCU 探针 + PC,Phase 14 | 真机 61.6ms / 模拟器 6.8ms(含 peer 100ms 节拍) | 全段(用户真实体感) |
| 连接/重监听 | connect→ESTABLISHED;close→relisten 成功 | MCU(net_poll 观测点),Phase 14 | relisten **89ms**(D10 修复后) | 握手/队列;**≥500ms = D10 回归** |
| 日志税 | 一行 60 字符 `printf`(现 fputc 逐字节 @115200)耗时 | MCU,Phase 13 附带 | **4402µs/行** | D6(per-recv 日志已删 2026-09-22;非阻塞日志仍待办) |
| uiTask 节拍 | 生产循环实测周期(`osDelay(10)` 之外多花多少 = LVGL 渲染 + 渲染中排空) | MCU 探针,Phase 14 | **10986µs**(渲染仅 ~1ms/帧) | D5/D9(实测均非瓶颈) |
| CH395F 净贡献 | `生产路径帧周期(真机) − 生产路径帧周期(模拟器)`,同一 peer 交替跑 | PC,Phase 14 | **54.8ms**(阈值 ≤80ms) | 直接回答"网络延迟"占多少 |
---
## 4. 测试架构
### 4.1 文件与注册
| 文件 | 职责 | 注册 |
|---|---|---|
| `test/board/net_latency_test.h` | 计时/统计/报告 API + Phase 入口声明 | 头文件随工程包含路径,无需注册 |
| `test/board/net_latency_test.c` | DWT µs 时基 + 统计 + Phase 12/13/14 用例 | **仅 `MDK-ARM/STM32F407-Demo.uvprojx`**(Test 组;模拟器 CMake 不编 `test/*.c`,与现有测试文件一致) |
| `test/host/latency_peer.py` | PC 端 peer(纯标准库):`--mode net`(M2)/`--mode remo`(M3),`--samples N`,`--json out.json` | 无(Python) |
| `test/host/ftp_regression.py` | D20 复现/回归:单会话 7 次数据连接 + 1KB/32KB 往返内容校验,退出码 = 是否全通过 | 无(Python) |
| 本文档 | 分段/架构/流程/基线/缺陷现状 | — |
### 4.2 阶段编号与构建配置(沿用 `ch395f_test.h` 互斥体系)
```
ENABLE_PHASE12_TESTS [硬] SPI 时延基线(SPI 字节命令/SPI DMA)—— testTask 独占 CH395F,**无需 PC**
ENABLE_PHASE13_TESTS [网] 芯片+队列时延(发送分块握手~整帧往返/日志税)—— 需 latency_peer.py --mode net(:8080)
ENABLE_PHASE14_TESTS [网] 生产路径时延(生产路径帧周期~uiTask 节拍 + Δ)—— 真实 uiTask/:7003,peer --mode remo
```
- Phase 12 归 HW 组,13/14 归 NET 组;13 与 14 可同启(13 用 testTask 私有监听 :8080,14 用生产 :7003,Socket 不冲突);
- 测试套件总开关 `TEST_SUITE_CH395F`(`test/board/test_config.h`);**当前工作副本 = 测试构建**(Phase 14 已启用),
产品构建 = `test_config.h` 与 `ch395f_test.h` 的测试宏全部注释;
- 探针宏 `TEST_LATENCY_PROBE` = `TEST_SUITE_CH395F` 且(Phase 13 或 14 启用)→ 仅测试构建定义;
- 全部非破坏性 → 只读级,**仅限台架**。
### 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 调用方侧测量(单次 <25ms)用单次读数差;
跨 25ms 的段(芯片→线上+PC/生产路径帧周期整帧与重传尖峰)由 PC 端 peer 计时,或 MCU 侧**累积采样**
(等待循环中每 ~10ms 采样计数差并累加,`lat_mark_us`);
- 每次运行先打一行校准线 `[LAT] cal core_clk=...`,台架可用逻辑分析仪核对时基正确性。
### 4.4 统计、报告行与校验标记
- 每段 N 次采样(静态数组,零堆):`min / med / p95 / max` + `spikes`(≥ 尖峰阈值计数);
- MCU 串口报告行(**纯 ASCII**,`verify_log.py` 可校验):
```
[LAT] run_id=<tick> phase=12
[LAT] cal core_clk=168000000 us_per_1024cyc=6
[LAT] seg=spi_cmd_getver n=100 min=10 med=10 p95=10 max=12 us spikes=0
[LAT] seg=spi_dma_1024 n=100 min=1611 med=1611 p95=1612 max=1612 us spikes=0
[LAT] verdict=OK seg=spi_dma_1024 (med=1611us warn<=3200 crit<=6400 spikes=0/0)
[LAT] summary verdict=OK
[LAT] report complete
```
- verdict 语义:`OK`(全部段达标)/ `WARN`(某段 med 超 warn 阈值,**计 PASS**,提示重锁阈值)/
`FAIL`(某段 med 超 crit 阈值或 spikes 超限 → 套件判 FAIL);
- **字段名刻意用 `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 必须覆盖默认集**:套件 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` 为帧格式正确性证据。
### 4.5 PC 端 peer 协议(`latency_peer.py`)
纯标准库(socket/json/time/argparse);`--mode net` 走 M2 测试协议,`--mode remo` 模拟生产
DTU-RemoteLCD 客户端;输出每帧 `req2first / total / recv 批次到达时间` + 直方图 + spike 计数 + JSON 报告。
**M2 测试协议(`--mode net`,端口 8080,服务器驱动:MCU 逐个下发操作字节)**
| 操作 | 值 | 语义 | peer 侧动作 |
|---|---|---|---|
| HELLO | `5A A5 58 01` | 会话起始 | 连接后立即发送;MCU 识别后重置会话状态机 |
| 队列往返_RECV | `0x01` | 对端灌入 | 20×100B,**50ms 间隔**(≈1KB/s,低于 MCU 抽干速率);测非阻塞收的队列往返 |
| 队列往返_SEND | `0x02` | MCU 连发 | 收 2000B(20×100B 阻塞发) |
| 请求→首字节 | `0x03` | 小请求/回显 | 发 10B 模式请求 → 校验 MCU 原样回显;逐样本重复 |
| 整帧 3209B 往返 | `0x04` | 全帧读取 | 发 9B LCDMEM 请求 → 收 3209B 全帧(4 批);逐样本重复 |
| 发送分块握手 | `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 后进"重连等待态"(§5.2),两侧构成闭环;
- **尾批诊断/缓解选项(`--mode remo`,均无需抓包)**:`--ack-probe`(帧未收齐时发 1 个解析器忽略的字节,
让 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`)按时间对齐。
### 4.6 MCU 侧探针(`#ifdef TEST_LATENCY_PROBE`)
产品代码(`net_socket.c`/`ui_net_sink.c`/`ui_task.c`/`ui_port.c`)内各数行打点,
经静态环形缓冲(总计 <1.5KB BSS)上报;**产品构建下该宏未定义,探针代码编译消失**,
两种构建配置都必须过 `/keil-build`(0 错 0 警)。
- 多打点站点必须用 `lat_probe_add_seg()` 按段名过滤(探针窗口是串行全局窗口,直接打点会跨段污染,D18 教训);
- 启用探针的构建必须在用例入口调 `lat_time_init()`(漏调 = 全 0 假绿,D19 教训;`med==0` 判无效兜底)。
---
## 5. 执行流程
### 5.1 M1:Phase 12(SPI 层基线,**无需 PC**)
```powershell
# 0) 前置:test/board/test_config.h 已启用 TEST_SUITE_CH395F + ENABLE_PHASE12_TESTS
# (产品构建 = 全部注释掉)
# 1) 构建(0 错 0 警)
& '.venv\Scripts\python.exe' .dsh\skills\keil-build\scripts\keil_build.py
# 2) 一键闭环:开串口+清日志 → 烧录(stlink_flash.py flash,Reset&Run)→ wait-idle → 标记校验
# 硬件层模式无 "Network Init" 标记(netTask 自挂起),且 TEST_REPORT 行含 "failed=0"
# → Required/Forbidden 均用下方定制集(见 §4.4)
& '.venv\Scripts\python.exe' .dsh\skills\serial-boot-test\scripts\run_boot_test.py `
--required "App init OK;net init: OK;PHY: 100M FULL;[LAT] report complete" `
--forbidden "HardFault;assert;[ERR];[FAIL];verdict=FAIL"
# 3) 提取 [LAT] 报告行(直接读日志文件最可靠)
& '.venv\Scripts\python.exe' .dsh\skills\serial-assistant\scripts\serial_tool.py log --grep "\[LAT\]"
```
台架已跑通:7 段全部 OK、零尖峰、p95≈min(零抖动),§6.1 基线已按实测重锁。
### 5.2 M2:Phase 13(芯片+队列基线,**需 PC peer**)
```powershell
# 0) 前置:test/board/test_config.h 启用 TEST_SUITE_CH395F,test/board/ch395f_test.h 仅启用
# ENABLE_PHASE13_TESTS(Phase 13 必须独占 8080 socket,勿与其他 NET 层 Phase 同启)
& '.venv\Scripts\python.exe' .dsh\skills\keil-build\scripts\keil_build.py # 必须 0 错 0 警
# 1) 先 clear 再起 peer(顺序不可反:wait-idle 需要看到会话活跃期)
& '.venv\Scripts\python.exe' .dsh\skills\serial-assistant\scripts\serial_tool.py clear
& '.venv\Scripts\python.exe' test\host\latency_peer.py --host 192.168.31.100 --port 8080 `
--json "docs\测试说明\测试数据\m2_data\m2_quiet.json" --wait 170 # 后台作业
# 2) 烧录(Reset&Run 不可省)
& '.venv\Scripts\python.exe' .dsh\skills\stlink-flash\scripts\stlink_flash.py flash
# 3) 等空闲:会话本身约 2s,但 peer 首连常因"芯片空闲超时 RST"失败一次并 2s 后重试
& '.venv\Scripts\python.exe' .dsh\skills\serial-assistant\scripts\serial_tool.py wait-idle `
--timeout 120 --settle-ms 10000 --max-ms 110000
# 4) 判定:**直接 Grep serial.log 文件**(勿用 log --grep,勿只看 wait-idle)
# Required: "App init OK" / "net init: OK" / "[LAT] report complete" / "=== Phase 13: 1/1 PASSED"
# Forbidden: HardFault / assert / [ERR] / [FAIL] / verdict=FAIL
```
- **会话顺序**:HELLO → 队列往返_RECV → 队列往返_SEND → 发送分块握手 → 日志税 → 请求→首字节 → 整帧 3209B 往返 → DONE。
易失步的 RX 协议段(请求→首字节/整帧 3209B 往返)置于末位,避免失步污染前置段;请求→首字节/整帧 3209B 往返样本数 10,发送分块握手 = 16×4KB;
- **abort 后自动进"重连等待态"**(120s):DONTWAIT 轮询 scan `5A A5 58 01` → 命中即
`p13_reinit()` + 重启会话;期间周期自愈(10s 无 peer 且状态仍 LISTENING → 重发芯片重监听),
**干净完成后停止自愈**(否则每 10s 拆建监听器,日志永不静默、台架无法判空闲);
- **安静档判定**:`ftp_stress=0`(不加 `--ftp-stress`),即无 FTP/对时干扰的静默基线。
### 5.3 M3:Phase 14(生产路径 + 模拟器 Δ + 压力档)
```powershell
# M3(生产路径,需要设备已烧 Phase 14 固件)
& '.venv\Scripts\python.exe' .dsh\skills\keil-build\scripts\keil_build.py # 0 错 0 警(Phase 14 构建)
& '.venv\Scripts\python.exe' .dsh\skills\serial-assistant\scripts\serial_tool.py clear
& '.venv\Scripts\python.exe' .dsh\skills\stlink-flash\scripts\stlink_flash.py flash # 先烧录,再起 peer(顺序不可反!)
& '.venv\Scripts\python.exe' test\host\latency_peer.py --mode remo --host 192.168.31.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\host\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\host\latency_peer.py --mode remo --host 192.168.31.100 --port 7003 `
--samples 30 --ack-probe --json "docs\测试说明\测试数据\m3_data\m3_tail_ackprobe.json" # 带内补 ACK
& '.venv\Scripts\python.exe' test\host\latency_peer.py --mode remo --host 192.168.31.100 --port 7003 `
--samples 30 --ack-fast --json "docs\测试说明\测试数据\m3_data\m3_tail_ackfast.json" # 关延迟 ACK
```
### 5.4 D20 回归(`test/host/ftp_regression.py`)
单会话内 7 次数据连接 + 1KB/32KB 往返内容校验,退出码 = 是否全通过。
**D20 的失败窗口在"每次开机后的第一个 FTP 会话"**(稳态会话恒过)→ 复现/验证必须以
**复位开机**为边界(`stlink_flash.py reset` = 只复位不烧录),首会话必跑、多轮统计。
### 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 台架,已重锁)
> 2026-09-22 注记:该轮代码变更(缓冲块布局与 MSS 解耦 + 超限读 OVERSIZE 金丝雀)
> 不改变任何芯片行为参数(MSS=1024、块布局 4/2、DMA 缓冲 2052B 均不变)→ 本节全部基线继续有效,无需重锁。
> (2026-09-23 补:TC-NET-604 的"RX 冻结"结论已结案为非缺陷,该轮代码变更按防御性/可观测性改进保留。)
### 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 |
| `spi_cmd_gint16` | **15µs** | 15/16 | 40µs | 100µs |
| `spi_dma_4` | **20µs** | 20/20 | 40µs | 100µs |
| `spi_dma_64` | **114µs** | 114/115 | 250µs | 600µs |
| `spi_dma_1024` | **1611µs** | 1611/1612 | 3200µs | 6400µs |
| `spi_dma_2048` | **3208µs** | 3208/3210 | 6400µs | 12800µs |
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 周期)。
### 6.2 M2 段(Phase 13,芯片 + 队列)
2026-09-22 重锁(P0-a/P0-b):同日 A/B 两档对照——**ADC 采集链路**(周期 500ms + per-recv 日志,= run15 条件,
复跑数值与 run15 基线重合 ±0.2%)→ **RS-485 采集链路**(周期 200ms + per-recv 日志已删,= 当前代码)。
两档全部 `1/1 PASSED`、零尖峰、peer exit 0;下表 = **RS-485 采集链路 基线**(A/B 差值即 P0-b 效果,P0-a 在干净链路上惰性)。
**MCU 侧(`[LAT]` 报告,安静档 `ftp_stress=0`)**
| 段(seg) | 测量含义 | med(min/max, n) | warn(med >) | crit(med >) |
|---|---|---|---|---|
| `queue_rt_recv` | 队列往返对端灌 100B×20@50ms,MCU 非阻塞收的队列往返 | **1223µs**(1194/1223, 20) | 10ms | 100ms |
| `queue_rt_send` | 队列往返 MCU 阻塞发 100B×20 | **988µs**(840/1216, 20) | 10ms | 100ms |
| `chunk_wait` | 发送分块握手 16×4KB,芯片 chunk 内 SENBUF 等待(探针) | **219µs**(213/378, 64) | 10ms | 500ms(spike) |
| `logtax_line` | 60 字符 printf @115200(本地) | **4402µs**(4396/8779, 10) | 10ms | 25ms |
| `req2first_mcu` | 请求→首字节 10B 请求到齐 → 回显发出 | **986µs**(979/1015, 10) | 20ms | 200ms |
| `fullframe_mcu` | 整帧 3209B 往返 9B 请求 → 3209B 全帧发给芯片(netTask 内 4 分块) | **6614µs**(6546/6649, 10) | 50ms | 500ms |
(旧 run15 值对照:`queue_rt_recv` 5997 / `req2first_mcu` 319 / `fullframe_mcu` 6028µs——前两项差异均为
per-recv 日志移除的调度效应,非芯片行为变化:`queue_rt_recv` 差值 4.8ms ≈ 一行日志税;`req2first_mcu`
旧值 319µs 是日志阻塞期 netTask 恰在循环内(同轮命中)的幸运快值,新值 ≈ 1 个 netTask 循环周期(~1ms),
是隔离小帧发送的真实成本。`queue_rt_send`/`chunk_wait` 两档同值 = 与日志无关的段。)
**PC 侧端到端(peer JSON,含线路 + Windows 栈)**
| 段 | med(min/max) | 说明 |
|---|---|---|
| 请求→首字节端到端(op→收到回显) | 3.9ms(1.7/12.0) | 比 MCU 侧 `req2first_mcu` 多 ~2.9ms → PC/线路段 |
| 整帧 3209B 往返首字节 | 10.0ms(4.0/13.8) | 同上量级 |
| 整帧 3209B 往返全帧 3209B | 57.9ms(53.6/69.9) | 每帧固定 4 个 recv 批次;**尾批 137B 稳定迟到 ~50ms**(§7.3) |
| 整帧 3209B 往返 CRC 校验 | 10/0 fail | 帧格式/CRC 正确(生产格式对齐后) |
| 发送分块握手 flood 64KB(16×4KB) | 7.5~8.7ms/次 | ≈2ms/KB,满块无尾批惩罚(MCU 发送路径,与 per-recv 日志无关) |
**M2 三个结论**:
1. **安静档不存在重传台阶**——六段全部 `verdict=OK`、零尖峰、p95≈med;会话级路径的 MCU 净成本
~1.2ms(`queue_rt_recv`,P0-b 删日志后;旧值 5.997ms 含 per-recv 日志税 4.4ms/行);
2. **芯片 chunk 级不是瓶颈**——`chunk_wait` ≈219µs ≈ 每 chunk SPI 成本(1.61ms)的 1/7,
`SENBUF_FREE` 等待远小于 SPI 搬运本身;
3. **整帧 3209B 往返全帧的最后 137B 尾巴稳定迟到 ~50ms**(10/10 帧)——尾批主导整帧时长,MCU 侧发完 3209B 仅 6.6ms
→ 延迟不在 MCU 发送路径,在芯片 TCP 尾段发送/对端 ACK 交互(M3 定性见 §7.3)。
### 6.3 M3 段(Phase 14,生产路径 + 模拟器 Δ)
**MCU 侧(`[LAT]` 报告,真实 uiTask + `ui_net_sink` :7003;P0-a/P0-b 代码,2026-09-22 重锁)**
| 段(seg) | 测量含义 | 实测 med(min/max, n) | warn(med >) | crit(med >) |
|---|---|---|---|---|
| `ui_loop`(uiTask 节拍) | uiTask 生产循环实测周期(渲染+轮询+osDelay(10)) | **10986µs**(10951/11022, 120) | 25ms | 50ms |
| `prod_cycle_mcu`(生产路径帧周期) | LCDMEM 取 1bpp + 组帧 + net_send 4 分块(不含 10ms 拾取拍) | **6707µs**(6701/6757, 50) | 20ms | 200ms |
| `relisten`(连接/重监听) | 断线后 auto-relisten 全程(**修复后**:单次尝试 + netTask 延迟重试) | **89000µs ×3**(三次同值) | 100ms | 500ms |
(旧值对照:`prod_cycle_mcu` 7142µs → 6707µs = P0-b 移除请求路径 per-recv 日志的 ~435µs;
`ui_loop`/`relisten` 两档同值 = 与日志无关的段。)
`relisten` 阈值说明:修复前 588000µs(TRAP-NET-09 `osDelay(500)` 忙等)→ crit 500ms 即 D10 指纹;
D10 修复后实测 89ms(= 30ms 重试间隔 + 约 60ms 芯片沉降),故 warn 放宽到 **100ms**、crit 保持 500ms
(仍能捕获"退回阻塞重试"的回归)。
**PC 侧端到端(peer `--mode remo`,与模拟器 A/B)**
| 指标 | 真机(50 帧静默档) | 模拟器(50 帧) | Δ(真机−模拟器) | 阈值 |
|---|---|---|---|---|
| 帧周期 total | **58.9ms**(47.7/72.1,p95 69.9) | **6.8ms**(0.6/13.1) | **+52.1ms** | ≤80ms |
| 首字节 | 9.2ms(3.7/16.7) | 6.7ms(0.5/13.0) | +2.5ms | — |
| **尾批延迟 tail lag** | **50.4ms**(43.4/58.6) | **0.0ms**(0/0.1) | **+50.4ms** | — |
| 帧 CRC / 位图非平凡 | 50/0、50/50 | 50/0、50/50 | — | 全通过 |
| 连接 / 断开重连(+INIT) | 20.9ms / 22.6ms(3/32.6) | 10.5ms / 32.0ms | — | ≤150ms |
(模拟器列为 2026-09-21 基线,模拟器工程不受发送分块握手影响(公开 API 不变);真机列 =
**2026-09-22 16:42 挂起 recv 构建重锁**(`../测试数据/m3_data/m3_pending_quiet.json`,50/50 帧全 OK、零尖峰)。
上一档真机值(P0-a/P0-b 分片代码,同日 11:35):帧周期 61.6ms / 首字节 9.6ms / 尾批 52.6ms /
重连 17.1/22.4ms → pending 构建:帧周期 med **58.9ms**(−2.7ms)、尾批 **50.4ms**(−2.2ms)——
执行器零占用使生产路径净缩短(更早的空闲窗口)。)
**压力档(`--ftp-stress`,头阻塞复现;第 25 帧起 FTP 空闲登录保持)**
| 分桶 | n | med | p95 | max | 尖峰(≥500ms) |
|---|---|---|---|---|---|
| quiet(0~24 帧) | 25 | 58.4ms | 69.0ms | 69.8ms | 0 |
| stress(25~49 帧,**挂起 recv 构建**) | 25 | **60.1ms** | 66.4ms | **69.3ms** | **0** |
(头阻塞修复前 stress 档 med 17718ms、尖峰 1 次 = 17.7s 全网冻结(旧固件);阶段一 5ms 分片修复后
med 65.0ms/max 133.5ms(133.5 = 压力启动瞬态单帧);**挂起 recv 化后 med 60.1ms/max 69.3ms**
——p99 尾部较分片档压缩近半(133.5→69.3),MCU 侧 `prod_cycle_mcu` 6706µs 在压力下纹丝不动
(min 6698/max 6720):头阻塞工况不再向 UI 帧周期传导 = 结构性收益。本档重锁 =
`../测试数据/m3_data/m3_pending_stress.json`,2026-09-22 16:43。)
### 6.4 全段基线总表
| 段 | 含义 | 实测(med) | min/max | n | 阈值 warn/crit | 判定 |
|---|---|---|---|---|---|---|
| **SPI 字节命令** `spi_cmd_*` | SPI 字节命令(3 条) | 10 / 15 / 15µs | 10~16µs | 100 | 40/100µs | OK |
| **SPI DMA** `spi_dma_*` | SPI DMA 读(4/64/1024/2048B) | 20 / 114 / **1611** / 3208µs | ±2µs | 100 | 见 §6.1 | OK |
| **发送分块握手** `chunk_wait` | 写发送缓冲 → `SENBUF_FREE`(每 chunk) | **219µs** | 213/378µs | 64 | 10ms/500ms | OK |
| **队列往返** `queue_rt_recv` | 调用方 `net_recv(DONTWAIT)` 队列全程 | **1223µs**¹ | 1194/1223µs | 20 | 10ms/100ms | OK |
| **队列往返** `queue_rt_send` | 调用方 `net_send` 队列全程 | **988µs**¹ | 840/1216µs | 20 | 10ms/100ms | OK |
| **请求→首字节** `req2first_mcu` | 10B 请求到齐 → 回显发出(MCU 侧) | **986µs** | 979/1015µs | 10 | 20ms/200ms | OK |
| **整帧 3209B 往返** `fullframe_mcu` | 9B 请求 → 3209B 全帧交给芯片 | **6614µs** | 6546/6649µs | 10 | 50ms/500ms | OK |
| **生产路径帧周期** `prod_cycle_mcu` | **生产路径**:LCDMEM 取帧+组帧+net_send(MCU 净成本) | **6706µs**(发送分块握手重锁) | 6698/6720µs | 50 | 20ms/200ms | OK |
| **日志税** `logtax_line` | 一行 60 字符 `printf` @115200 | **4402µs** | 4396/8779µs | 10 | 10ms/25ms | OK(D6 量化) |
| **uiTask 节拍** `ui_loop` | uiTask 生产循环实测周期 | **10986µs**(发送分块握手重锁,值不变) | 10951/11022µs | 120 | 25ms/50ms | OK |
| **连接/重监听** `relisten` | 断线后自动重监听耗时(MCU 侧,修复后) | **90000µs**(发送分块握手重锁) | 90000µs ×3 | 3 | 100ms/500ms | OK |
| **芯片→线上+PC/生产路径帧周期** peer cycle | 生产客户端完整帧周期(发→收齐 3209B) | **58.9ms**(发送分块握手重锁) | 47.7/72.1ms | 50 | — | 见 §7 |
| **Δ** | `peer cycle(设备) − peer cycle(模拟器)` | **52.1ms**(发送分块握手重锁) | — | 50/50 | ≤80ms | 见 §7.1 |
¹ 队列往返两行为 2026-09-22 上午分片时代(M2 RS-485 采集链路 档)数值:发送分块握手对 DONTWAIT 快速路径**逐位不变**(dispatch
→ `net_poll` → `get_recv_len` 读,无 readable 门控),pending tick 无 pending 时为纯指针读零 SPI
(µs 级)——机制上不影响队列往返;M2 重验未在本轮执行(低风险,留待 Phase 13 下次窗口,见 `docs/任务规划/README.md` 条目「队列往返基线待重验」)。
数据来源:SPI 字节命令/SPI DMA = §6.1;发送分块握手/队列往返/请求→首字节/整帧 3209B 往返 = M2 RS-485 采集链路 档(200ms、per-recv 日志已删)串口 `[LAT]` 行
(`../测试数据/m2_data/m2_quiet_r200.json`);生产路径帧周期/连接/重监听/uiTask 节拍 = **挂起 recv 构建 2026-09-22 16:42/16:43 重锁**
(`../测试数据/m3_data/m3_pending_quiet.json` / `m3_pending_stress.json` 之 MCU 侧 `[LAT]` 行);日志税 = M3 静默档
(`../测试数据/m3_data/m3_dev_quiet_r200.json`,与发送分块握手无关的串口税段);A/B 对照档 = `m2_quiet_r500.json`(旧条件);
peer 侧 = `../测试数据/m3_data/m3_pending_*.json`(发送分块握手重锁)与 `../测试数据/m3_data/m3_dev_*_r200.json`(上一档,历史对照)。
旧 run15 摘录(`m2_serial_excerpt_run15.txt`)保留作 A/B 历史对照。
### 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(旧 500ms 周期的重传指纹;P0-a 后单次重传台阶 = 200ms,
低于 crit,弱网需看**连续台阶**而非单点);
- 换板/换固件重跑后,任何段超 warn → 重测确认后按实测重锁本表。
---
## 7. 一帧时间预算与尾批根因
### 7.1 设备 vs 模拟器(Δ 的构成)
同一客户端(`latency_peer.py --mode remo`,100ms 节拍)对**真机 :7003** 与**PC 模拟器 :7003** 各跑一轮:
| 指标 | 真机(50 帧静默档) | 模拟器(50 帧) | 差值 |
|---|---|---|---|
| 帧周期 total(发请求 → 收齐 3209B) | **61.6ms**(50.4/69.8,max 69.8) | **6.8ms**(0.6/13.1,max 15.7) | **+54.8ms** |
| 首字节 first byte | 9.6ms(3.2/14.2) | 6.7ms(0.5/13.0) | +2.9ms |
| **尾批延迟 tail lag**(末批 → 首批) | **52.6ms**(44.1/59.1) | **0.0ms**(0/0.1) | **+52.6ms** |
| recv 批次数/帧 | 5(8B 头 + 1016 + 1024 + 1024 + **137**) | 2 | — |
**真机 61.6ms 的构成**(三段相加 ≈ 61.6,闭合):
```
[T+0] 客户端发 9B LCDMEM 请求
[T+2.3ms] 实测推算:uiTask 10ms 轮询拾取 + netTask 调度(P0-b 删日志后 6.8→2.3ms)
[T+9.6ms] 首字节到达 = 2.3 + MCU 净处理 6.7ms + 首批线路段
(prod_cycle_mcu:取 1bpp + 组帧 + net_send 4 分块)
[T+61.6ms] 全帧收齐 —— 最后的 137B 尾巴比前三批(1024B×3,前 4ms 内到齐)晚 52.6ms
```
- 前三批 1024B 在**首字节后约 4ms 内**全部到齐;
- 第 4 批只有 **137B = 3209 − 3×1024**,比前三批晚 **52.6ms**(与负载无关:压力档 tail 中位 50.2ms);
- **MCU 侧把 3209B 全部交给芯片只要 6.7ms** → 慢的不在 MCU,在芯片协议栈的尾段发送/ACK 交互。
### 7.2 交叉对照(三条独立证据 → 归因)
| 对照 | 观测 | 说明 |
|---|---|---|
| ① 同一客户端 → 模拟器 | tail = **0.0ms**,cycle 6.8ms | 客户端栈、Windows、请求格式全部相同 → 尾批**不是 PC 侧**造成 |
| ② M2 `发送分块握手` 批量排空(16×4096B,全满块) | 65536B / **125.4ms**(≈2ms/KB) | 16 个满块若各带 50ms 尾巴应 ≥800ms → **满块无此惩罚** |
| ③ 压力档(头阻塞)下的整帧 3209B 往返/生产帧 | 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 发送分块握手,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 队列与调度:静默档不存在台阶
- **队列往返**:收 1223µs / 发 988µs(P0-b 后;旧值 5997/987µs)——收段 ≈ 1 个 netTask 循环周期量级,
属**调度周期**而非排队拥塞(旧收值 5997µs 含 per-recv 日志税 4.4ms/行,已移除);
`queue_rt_send` 988µs ≈ 1 个循环周期,发送路径没有额外排队;
- **发送分块握手 chunk 级**:219µs ≈ 每 chunk SPI 成本的 1/7 → **芯片 TX 缓冲不是瓶颈**,SPI 搬运才是;
- **uiTask 节拍 uiTask 周期**:10986µs(= osDelay(10) + 渲染开销 ≈ 1ms)→ **LVGL 渲染只占约 1ms/帧**,
uiTask 不是延迟来源;
- **结论:调优先级/栈不会改善体感**——预算应花在尾批解法(已做)与 D20(§8.2)。
---
## 8. 缺陷与修复状态
### 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-NET-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/头阻塞修复,供后续维护)
1. **D10 重试必须防饥饿**:调用方(accept/recv 路径每 10ms 一次)只登记一次重试,实际重试由 netTask
轮询到点执行;若让调用方每次立即重试并重置时刻,会永不重试。重试间隔 100ms→30ms 后恢复时间
147ms→89ms(芯片沉降约 60ms);
2. **(历史,阶段一)头阻塞修复的分片长度 = 其他任务的最坏等待**:100ms 片下受压帧仍 ~750ms(多片排队),收到
**5ms** 片后才降到 ~75ms——该"分片即上限"模型已于 2026-09-22 由 **挂起 recv 化**取代
(`a45c0cb`):执行器零占用后,其他任务的最坏等待 = 一个主循环节拍(~1ms),与任何片长无关
(M3 压力档实测:med 60.1ms/max 69.3ms,p99 尾部较分片档 133.5ms 压缩近半);
3. **(历史,阶段一)分片循环必须显式让出 CPU**:某片可能因 `net_poll` 自身耗时首轮即返回超时 → 本任务
(Normal)立即重排队会形成忙循环并**饿死 BelowNormal 的 uiTask**(实测 2.4~3.4s 周期尖刺);
`vTaskDelay(1)` 后尖刺归零——挂起 recv 化后该让出协议随分片循环一并退役(调用方改睡任务通知,
天然让出;netTask 侧无循环可忙);
4. **(现行,挂起 recv 化)阻塞等待不在执行器内发生**:`net_recv_locked` 阻塞分支在数据不可得时登记
pending(每 fd 至多一个、**不可覆盖**;`caller==NULL`/netTask 自身拒绝)即返回哨兵
`NET_PENDING_REGISTERED`;完成判定在 `net_pending_tick`(主循环节拍,与 `net_process_messages`
同位)按 数据→截止→CLOSED 排空/EOF+relisten→1s 看门狗 顺序执行,**cancel/settle 必通知**
(调用方永不悬空);pending 表独立于 `net_sock_t`(close 的 memset 不得抹掉跨时间状态)。
维护注意:队列中长 SEND/CLOSE 消息处理期间,pending 完成顺延至下轮 tick(~1ms 级;长 SEND
stall 期间最多顺延相应时长)——已知代价,勿"优化"回执行器内等待。
### 8.4 相关既有缺陷(会掩盖/放大延迟)
| 缺陷 | 位置 | 与延迟的关系 |
|---|---|---|
| **`remo_send` 短写** | `ui_net_sink.c` `remo_send` | 丢弃 `net_send` 返回值 → 弱网短写静默截断,客户端 CRC 不过 → 断连 → 观感"卡完直接黑屏/卡住" |
| **SPI 错误通道** | `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 对照基线 |
| **间歇**卡,且 ≈200ms 的整数倍(旧固件 500ms) | **D1** 丢包重传 | Wireshark `tcp.analysis.retransmission`;查 PHY/网线/Wi-Fi |
| **间歇**整屏冻结数秒~数十秒 | **头阻塞**(已修)/ D10 重连(已修)/ 回归 | 查 FTP/对时会话是否挂着;核对 §8 修复在位 |
| 只在开 FTP / 对时后卡 | 头阻塞 + 单连接 socket 争用 | 临时停 ftpTask/timeSyncTask 对照 |
| 换网口/换线/换 PC 就好 | D7 物理链路 | 拓扑纪律:核对开机日志 PHY 行 + 交换机端口强制全双工 + PC 有线直连 |
### 9.2 分段对照
| 现象(段超标) | 首要怀疑 | 动作 |
|---|---|---|
| SPI 字节命令/SPI DMA 超 crit | SPI 走线/PCB/SPI2 分频或 DMA 配置 | 对照 `docs/问题记录/CH395F_Trap_Records.md`;逻辑分析仪抓 SCK/CS |
| 发送分块握手 spike ≥200ms(连续台阶;旧固件 500ms) | **D1 重传 / D7 链路** | Wireshark `tcp.analysis.retransmission`;查 PHY 协商/网线/Wi-Fi |
| 发送分块握手基线(非 spike)抬高 | 芯片发送路径 / 中断响应 | 对照 `docs/芯片手册/CH395F.md` §5.17/5.18(注意:状态寄存器**不被 SET 类命令更新**,`GET_CMD_STATUS` 恒读到 RESET 的 0x10 陈旧残留,2026-09-22 台架实证——不可用作 SET 值验收) |
| 队列往返压力档爆表 | **头阻塞(已修)→ 回归** | 定位触发源(阻塞 recv 的 FTP/对时);核对挂起 recv 在位(`net_recv_locked` 阻塞分支 = 登记 pending 而非等待;`net_pending_tick` 在主循环) |
| 队列往返/请求→首字节静默档抬高 | D5 调度(任务抢占/节拍) | 查任务优先级与 osDelay;`stack_report.py` 核对栈水位 |
| 请求→首字节/整帧 3209B 往返整体抬高且 SPI 字节命令~队列往返正常 | 线路+PC 段(芯片→线上+PC) | Wireshark 段间间隙;`Δ` 对照定位 MCU 侧净贡献 |
| **整帧 3209B 往返/生产路径帧周期尾批固定迟到 ~50ms** | 已定性(§7.3):次 MSS 尾段被 Nagle 扣住 + Windows 延迟 ACK | 客户端侧解法已实测(§7.4):`--ack-probe` / `--ack-fast` → tail 52→3.9ms;生产客户端已实装;**设备侧无解**,不必再投入 |
| **队列往返出现重复投递**(收到 > 发送) | **D11**:芯片 TCP RX 饱和重复递交 | 降低灌入速率/加抽干(§8 D11) |
| **会话无征兆失步**(结果错位/挂死) | **D12**:net 层任务通知值语义 | 核对 `net_wait_result` 清除掩码全 1 + 按返回值判定(§8 D12/D12-a) |
| **整帧 3209B 往返一直超时 / peer 报 `l7_crc.fail`** | **D15**:帧格式不一致 / peer 超读 | 对照 `ui_net_sink.c` 的 `remo_send`/`remo_handle`(§8 D15) |
| **会话跑完一段后无限循环、peer 空等 300s** | **D16**:Phase 13 状态机迁移目标错 | 核对顺序队列往返→发送分块握手→日志税→请求→首字节→整帧 3209B 往返→DONE(§8 D16) |
| **`[LAT]` 段行缺失/截断、wait-idle 判不出空闲** | **D12-b**:热路径日志洪水 | 删除 net 热路径逐调用日志(§8 D12-b) |
| **开机日志连续丢行**(含必检标记 `net init: OK` 等)但系统健康 | **日志并发竞态**:多任务并发 `printf` → 非重入 `fputc`/`HAL_UART_Transmit` 丢整行、UART 状态短暂卡住(2026-09-22 台架实证:boot 时 testTask 启动行与 netTask 日志并发 → 连续 4 行丢失、~2s 后自恢复) | 必现证据行采用"静默采集 + 延迟到安静窗口打印"模式(范例:P0-a 验收行曾按此模式实装,验收完成后探针已移除);根治 = 日志非阻塞/串行化(§10.2) |
| **`relisten` 段 FAIL、断线固定 ~588ms** | **D10** 回归(退回 `osDelay(500)`) | 核对 `net_listener_request`/`net_listener_tick` 非阻塞重试在位(§8 D10) |
| **远端显示整帧卡住十几秒后自恢复** | **头阻塞回归** | 核对 挂起 recv 在位:`net_recv_locked` 阻塞分支登记 pending(哨兵 `NET_PENDING_REGISTERED`)+ `net_pending_tick` 在主循环 + 调用方剩余预算循环(lftpd/timeSync)(§8.3 要点 4) |
| **INIT 应答帧字节数字段恒为 0** | **D17** 回归 | 核对 `remo_send_init` 发满 8B(§8 D17) |
| **开机首个 FTP 会话第 2 个数据连接 425** | **D20**(已定位未修,§8.2) | 复现:`test/host/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` 已退役,Phase 13 abort 期自愈亦交 net 层) |
| 生产路径帧周期超而整帧 3209B 往返正常 | 生产路径附加成本(uiTask 节拍/LVGL/日志) | 对照日志税/uiTask 节拍与 §10.1(渲染已排除;per-recv 日志已删,非阻塞日志仍待办) |
| Δ 显著 >80ms | CH395F 栈净开销异常 | 回到 SPI 字节命令~连接/重监听逐段排查 |
---
## 10. 结构性成本项与待办方向
### 10.1 静态分析十项结构原因(D1~D10,独立于 §8 的台架缺陷编号)与当前状态
| # | 原因 | 实测验证(2026-09-22 台架) | 当前状态 |
|---|---|---|---|
| D1 | CH395F TCP 重传固定周期(非 RTT 自适应) | 台架链路(有线 100M 全双工)多轮零重传尖峰 → 台架无丢包;**弱网(Wi-Fi/双工不匹配)仍是秒级卡顿首要嫌疑**,200ms 粒度台阶 = 重传指纹(旧固件 500ms) | **已修(P0-a,2026-09-22)**:`NET_RETRANS_PERIOD_MS` 500→**200**(窗口 8×200=1.6s,原 4s)。台架验收说明:`GET_CMD_STATUS` 对 SET 类命令无判别力(500/2000ms 超限对照均读 0x10 陈旧残留,§9.2)→ 接受性以手册范围(≤1000ms)+ 全套件零回归(§6.2/§6.3)+ 与 500ms 行为同构为准。验收探针(3×20ms 状态轮询 + 证据行)验收完成后已从代码移除,结论保留于本文档 |
| D2 | 3209B × 1KB 发送 FIFO 停止等待(4 次串行分块握手,期间 netTask 全程占用) | `chunk_wait` 219µs/块 ≈ SPI 搬运成本的 1/7 → 握手本身**不是瓶颈**;每帧固定成本小(MCU 净 7.1ms) | 基线成本,可接受;根治 = 差量/RLE + 帧版本号(待办) |
| D3 | netMsgQueue 头阻塞 | 已量化:一条空闲 FTP 控制连接冻结全网 17.7s | **两阶段闭环**:① 5ms 分片有界读 + 让出(f8d6e90/4a1605b,缓解)② **挂起 recv 化**(`a45c0cb`,结构性——执行器零占用,缺陷类消灭,§8.3 要点 4) |
| 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);拾取调度 ~2.3ms(P0-b 删 per-recv 日志后,原 ~6.8ms) | 基线行为;可选优化(poll 前置 + while 排空),低优先 |
| D6 | 阻塞式日志在关键路径(fputc 逐字节 @115200) | `logtax_line` 4402µs/行;per-recv 日志已删(2026-09-22):`queue_rt_recv` 5997→1223µs、帧周期 66.0→61.6ms、首字节 13.9→9.6ms | **部分已修(P0-b,2026-09-22)**;非阻塞日志仍待办(同时是开机日志并发丢行 §9.2 的根治项) |
| D7 | 物理链路:强制 100M 全双工 vs 交换机自协商 → 双工不匹配静默丢包 | 台架 `PHY: 100M FULL` 成立、零尖峰 → 台架干净 | 现场排查项:核对开机日志 PHY 行 + 交换机端口强制全双工 + 验证 PC 有线直连 |
| D8 | 客户端固定成本:100ms 拉取节拍、按键后不立即取帧、1bpp→PNG→base64→WS 编码链 | 生产客户端优化后:帧耗时 20.7ms、显示帧率 ~9fps | 部分已优化(延迟确认抑制已上线);其余待办(按键后立即取帧/不传 PNG/自适应节拍)——客户端仓库 |
### 10.2 待办优化项(按性价比排序,均未实施)
1. **D20 数据 Socket 轮换**(§8.2)——唯一未修的功能性缺陷;现场可用客户端重试 425 规避;
2. **~~P0-a~~ 已实施(2026-09-22)**:`NET_RETRANS_PERIOD_MS` 500→200 + `GET_CMD_STATUS` 一次性验收检查
(实测该状态对 SET 类命令无判别力,见 §10.1 D1);全套件复验零回归(§6.2/§6.3);验收完成后探针已移除;
3. **~~P0-b~~ 已实施(2026-09-22)**:删除 `net_socket.c` per-recv `DBG_INFO`;实测 `queue_rt_recv` −80%
(5997→1223µs)、生产帧周期 −4.4ms(66.0→61.6ms)、首字节 −4.3ms(13.9→9.6ms);
4. 超时参数分档:`NET_SEND/RECV_TIMEOUT_MS` 全局一刀切 → 按服务分离(远显短超时+非阻塞,FTP 保留长超时);
5. **差量/RLE + 帧版本号 + 由拉改推**(**根治,收益最大**:典型变化 <100B → 单段一帧,延迟 ≈ 一个 RTT;
稳态带宽 32KB/s → ≈0;D1 影响面 4 段 → 1 段;新命令走同一帧格式与 CRC,旧客户端兼容);
6. 发送块配额与 MSS:48 块按实际使用的 3 个 socket 重新分配(各 16 块),MSS → 1460
(3209B 从 4 段降到 1~2 段);**约束**:单次写长度必须 ≤ 发送缓冲(TRAP-NET-16 铁律),
改分配策略需同步 `net_bind` 重复分配路径,改前先读 `CH395F_Trap_Records.md`,改后跑 Phase 8 吞吐用例;
**另**:MSS/块配额变更**必须**跑可信用例(`test/host/ch395f_socket_test.py tcp_client` 全序列 + 新连接独立 4096B 对照),判据 = **客户端侧内容完全匹配**(勿用带固件内读者的台架配方,其短收是读者吞数据不是故障);
7. 去掉一次 3200B 拷贝(`g_tx` 直接承接位图,省 ~3.2KB 常驻 RAM;改前跑 `scripts/ram_report.py`);
8. uiTask:`ui_net_sink_poll()` 前置到 `ui_port_step` 前 + while 排空(省按键一整拍);
9. **日志非阻塞化:已实施·形态变更(生产者侧 2026-09-24 单写者;2026-10-25 与日志静默观察项合并评估收口)**:
`fputc` → 环形缓冲**已落地**(4KB 环 + 单写者 + 事件位;4402µs/行 → µs 级入环,
溢出 = 有界 200ms + dropped 账 + `[ERR]` 留痕;冲刷 = 有界裸发,见
`../../子系统设计/控制台单写者日志设计说明.md` §4);**TX 中断/DMA 维持否决**
(该文档 D4),原绑定的复议条件(根治项落地后实测 `tx_timeout`)已随 2026-10-26 裁定不做而失效——日志静默观察项 × 日志单写者化合并评估全文
结论见同文档文末附录;
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)。
14. **日志并发保护(2026-09-22 新增,与日志非阻塞化同源、优先级上调)**:开机多任务并发 `printf` 丢整行
(实测 §9.2:必检标记 `net init: OK` 曾被竞态吞掉 → 影响开机验收可靠性)。过渡:必现证据行按
"静默采集 + 延迟到安静窗口打印"模式(范例:P0-a 验收行,验收完成后已移除);根治 = 日志非阻塞化(串口 TX 环形缓冲
+ 中断/DMA,溢出丢弃策略需定义)。
**现状(2026-10-25)**:根治已随单写者落地——行级原子 = 临界区整行入环、单写者发出,
交错/丢行结构性消除;「写者注册前」窗口为裸发例外,纪律 = `dbg_console_writer_ready()`
门控(`../../子系统设计/控制台单写者日志设计说明.md` §3.4)。
---
## 11. 约束核对(AGENTS.md)
- 每个测试阶段/修复 `/keil-build`(0 错 0 警)+ `/serial-boot-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,探针 <1.5KB),改前后跑 `scripts/ram_report.py` 留证;
- `testTask` 栈 2048 words:用例函数只用几十字节局部变量;
- 套件互斥宏体系不动:Phase 12 挂 HW 组,13/14 挂 NET 组,`#error` 自动防两层同启;
- 非破坏性:不改 IP/卷/BBT;HW 层阶段独占 CH395F(netTask 自挂起)→ 标注"台架专用";
- 生产代码探针全部在 `#ifdef TEST_LATENCY_PROBE` 内,产品构建编译消失,两种构建都须过 `/keil-build`。
---
## 12. 数据文件与相关文档
### 12.1 原始数据(`docs/测试说明/` 下)
| 数据文件 | 内容 |
|---|---|
| `../测试数据/m2_data/m2_quiet.json` | M2 安静档 peer 侧(run15 最终代码):发送分块握手~整帧 3209B 往返端到端 + 逐帧尾批明细 |
| `../测试数据/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]` 证据片段(旧基线,A/B 历史对照) |
| `../测试数据/m2_data/m2_quiet_r500.json` | 2026-09-22 A/B 对照档 ADC 采集链路(500ms + per-recv 日志 = 旧基线条件;复跑与 run15 重合 ±0.2%) |
| `../测试数据/m2_data/m2_quiet_r200.json` | **2026-09-22 M2 重锁基线** RS-485 采集链路(200ms、per-recv 日志已删 = 当前代码) |
| `../测试数据/m2_data/README.md` | M2 数据说明(逐帧尾批明细、方法学约束) |
| `../测试数据/m3_data/m3_dev_quiet.json` | 真机 65 帧生产周期(D10/头阻塞/D17 修复前基线) |
| `../测试数据/m3_data/m3_dev_quiet_fixed.json` | 真机 65 帧(修复后:帧周期 65.7ms、relisten OK、INIT framebytes=3200) |
| `../测试数据/m3_data/m3_dev_stress.json` | 头阻塞压力档(修复前:17.7s 尖峰) |
| `../测试数据/m3_data/m3_dev_stress_fixed.json` | 头阻塞压力档(修复后:med 74.7ms / max 106.8ms,零尖峰) |
| `../测试数据/m3_data/m3_dev_quiet_r200.json` | **2026-09-22 M3 静默档重锁基线**(50 帧,P0-a/P0-b 代码:帧周期 61.6ms) |
| `../测试数据/m3_data/m3_dev_stress_r200.json` | **2026-09-22 M3 压力档重锁基线**(45 帧,第 15 帧起 FTP stress:med 65.0ms / max 133.5ms,零尖峰)——分片时代数值,**已被发送分块握手档取代**(历史对照) |
| `../测试数据/m3_data/m3_pending_quiet.json` | **2026-09-22 M3 静默档发送分块握手重锁基线(当前)**(50 帧,pending 构建 16:42:帧周期 58.9ms / 首字节 9.2ms / 尾批 50.4ms,零尖峰,CRC 50/0) |
| `../测试数据/m3_data/m3_pending_stress.json` | **2026-09-22 M3 压力档发送分块握手重锁基线(当前)**(50 帧,第 25 帧起 FTP 空闲登录保持:stress 段 med 60.1ms / p95 66.4ms / max 69.3ms,零尖峰,CRC 50/0;MCU 侧 prod_cycle_mcu 6706µs 压力下平坦) |
| `../测试数据/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-NET-09/-16 等) | `docs/问题记录/CH395F_Trap_Records.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` |