Files
STM32F4-Base/docs/测试说明/链路延迟测试说明.md
T

58 KiB
Raw Blame History

链路延迟测试说明(分段基线 / 实测现状 / 故障对照)

模块名称: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 历史为准。


1. 一句话结论(当前状态)

远端显示一帧(3209B 全帧)端到端实测中位 66.0ms,其中约 52.6ms(79%)是帧尾那 137B 小段的等待; MCU 侧真实处理只占 7.1ms,任务拾取/调度约 6.8ms。同一客户端、同一台 PC 打 PC 模拟器只要 6.8ms (Δ = 59.2ms 为 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)、E-02(一条空闲 FTP 控制连接冻结全网 17.7s)、 D17(INIT 应答帧字节数字段恒 0)等已量化并修复 + 台架复验(§8.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 一帧的旅程(按键 → 看到画面)

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 头阻塞窗口(已修复: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/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)
本文档 分段/架构/流程/基线/缺陷现状 —

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(:8080)
ENABLE_PHASE14_TESTS  [网] 生产路径时延(L8~L11 + Δ)—— 真实 uiTask/:7003,peer --mode remo
  • 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 启用)→ 仅测试构建定义;
  • 全部非破坏性 → 只读级,仅限台架。

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 的段(L4/L8 整帧与重传尖峰)由 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 识别后重置会话状态机
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 后进"重连等待态"(§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)上报;产品构建下该宏未定义,探针代码编译消失, 两种构建配置都必须过 @build(0 错 0 警)。

  • 多打点站点必须用 lat_probe_add_seg() 按段名过滤(探针窗口是串行全局窗口,直接打点会跨段污染,D18 教训);
  • 启用探针的构建必须在用例入口调 lat_time_init()(漏调 = 全 0 假绿,D19 教训;med==0 判无效兜底)。

5. 执行流程

5.1 M1:Phase 12(SPI 层基线,无需 PC)

# 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 均用下方定制集(见 §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"
# 3) 提取 [LAT] 报告行(直接读日志文件最可靠)
& '.venv\Scripts\python.exe' tools\serial_tool\serial_tool.py log --grep "\[LAT\]"

台架已跑通:7 段全部 OK、零尖峰、p95≈min(零抖动),§6.1 基线已按实测重锁。

5.2 M2:Phase 13(芯片+队列基线,需 PC peer)

# 0) 前置:test/test_config.h 启用 TEST_SUITE_CH395F,test/ch395f_test.h 仅启用
#    ENABLE_PHASE13_TESTS(Phase 13 必须独占 8080 socket,勿与其他 NET 层 Phase 同启)
& "MDK-ARM\build.bat"                      # 必须 0 错 0 警
# 1) 先 clear 再起 peer(顺序不可反:wait-idle 需要看到会话活跃期)
& '.venv\Scripts\python.exe' tools\serial_tool\serial_tool.py clear
& '.venv\Scripts\python.exe' test\latency_peer.py --host 192.168.1.100 --port 8080 `
    --json "docs\测试说明\m2_data\m2_quiet.json" --wait 170     # 后台作业
# 2) 烧录(Reset&Run 不可省)
& "MDK-ARM\download.bat"
# 3) 等空闲:会话本身约 2s,但 peer 首连常因"芯片空闲超时 RST"失败一次并 2s 后重试
& '.venv\Scripts\python.exe' tools\serial_tool\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 → 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() + 重启会话;期间周期自愈(10s 无 peer 且状态仍 LISTENING → 重发芯片重监听), 干净完成后停止自愈(否则每 10s 拆建监听器,日志永不静默、台架无法判空闲);
  • 安静档判定:ftp_stress=0(不加 --ftp-stress),即无 FTP/对时干扰的静默基线。

5.3 M3:Phase 14(生产路径 + 模拟器 Δ + 压力档)

# 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

5.4 D20 回归(test/ftp_regression.py)

单会话内 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
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,芯片 + 队列)

run12/run13/run15 三次独立完整 PASS(6 段全 OK、零尖峰、1/1 PASSED、peer exit 0); 下表 = run15(最终代码)基线。

MCU 侧([LAT] 报告,安静档 ftp_stress=0)

段(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 栈)

段 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,满块无尾批惩罚

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)。

6.3 M3 段(Phase 14,生产路径 + 模拟器 Δ)

MCU 侧([LAT] 报告,真实 uiTask + ui_net_sink :7003;修复后代码)

段(seg) 测量含义 实测 med(min/max, n) warn(med >) crit(med >)
ui_loop(L11) uiTask 生产循环实测周期(渲染+轮询+osDelay(10)) 10986µs(10970/21000, 120) 25ms 50ms
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 放宽到 100ms、crit 保持 500ms (仍能捕获"退回阻塞重试"的回归)。

PC 侧端到端(peer --mode remo,与模拟器 A/B)

指标 真机(65 帧) 模拟器(50 帧) Δ(真机−模拟器) 阈值
帧周期 total 66.0ms(53.3/75.3) 6.8ms(0.6/13.1) +59.2ms ≤80ms
首字节 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 —
帧 CRC / 位图非平凡 65/0、65/65 50/0、50/50 — 全通过
连接 / 断开重连(+INIT) 2.6ms / 23.5~34.7ms 10.5ms / 32.0ms — ≤150ms

压力档(--ftp-stress,E-02 复现;第 15 帧起 FTP 停留半登录态)

分桶 n med p95 max 尖峰(≥500ms)
quiet(0~14 帧) 15 65.9ms 75.8ms 75.8ms 0
stress(15~44 帧,E-02 修复后) 30 74.7ms 90.8ms 106.8ms 0

(E-02 修复前 stress 档 med 17718ms、尖峰 1 次 = 17.7s 全网冻结;修复后零尖峰,见 §8.1。)

6.4 全段基线总表

段 含义 实测(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 帧) 差值
帧周期 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 —

真机 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 + 首字节等待(918ms)+ 尾块 ~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 缺陷总表(编号 = 台架发现顺序;D10D20/E-02 为测试发现,与 §10.1 的静态分析编号 D1D10 不同体系)

编号 缺陷 状态 机理 修复 实测证据(前 → 后)
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 超 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 压力档爆表 E-02 头阻塞(已修) → 回归 定位触发源(阻塞 recv 的 FTP/对时);核对 net_recv_to 分片在位
L5/L6 静默档抬高 D5 调度(任务抢占/节拍) 查任务优先级与 osDelay;stack_report.py 核对栈水位
L6/L7 整体抬高且 L1~L5 正常 线路+PC 段(L4) Wireshark 段间间隙;Δ 对照定位 MCU 侧净贡献
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 逐段排查

10. 结构性成本项与待办方向

10.1 静态分析十项结构原因(D1~D10,独立于 §8.1 的台架缺陷编号)与当前状态

# 原因 实测验证(2026-09-22 台架) 当前状态
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)

10.2 待办优化项(按性价比排序,均未实施)

  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,探针 <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 内,产品构建编译消失,两种构建都须过 @build。

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