7efa58d9a1
分两段(用户裁定 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 绝对路径)
782 lines
65 KiB
Markdown
782 lines
65 KiB
Markdown
# 链路延迟测试说明(分段基线 / 实测现状 / 故障对照)
|
||
|
||
> **模块名称**: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` |
|