链路时延M1-M3收尾:Phase12-14基线与生产路径Δ实测、D10/E-02/D17修复、D20定位与硬化、文档整理
This commit is contained in:
+14
-4
@@ -9,7 +9,7 @@
|
||||
* 修改记录:
|
||||
* 2026-07-22 全面重构为单连接模式,增加大文件传输、边界条件等测试
|
||||
*
|
||||
* === 测试阶段一览(硬件层 1~5 → NET 层 6~11,严格顺序执行) ===
|
||||
* === 测试阶段一览(硬件层 1~5/12 → NET 层 6~11/13/14,严格顺序执行) ===
|
||||
* Phase | 层 | 描述
|
||||
* ---------|----------|-----------------------------------------------
|
||||
* Phase 1 | 硬件层 | 独立运行:寄存器读写 + SPI 命令路径验证
|
||||
@@ -23,6 +23,10 @@
|
||||
* Phase 9 | NET 层 | testTask:Select/Poll I/O 多路复用
|
||||
* Phase 10 | NET 层 | testTask:边界条件测试(零长度、DMA 边界、缓冲溢出)
|
||||
* Phase 11 | NET 层 | testTask:KeepAlive 超时断开测试
|
||||
* Phase 12 | 硬件层 | 独立运行:SPI 字节命令 / DMA 时延基线(L1/L2,无需 PC)
|
||||
* Phase 13 | NET 层 | testTask:芯片 + 队列时延(L3~L7/L10,需 PC peer,独立运行)
|
||||
* Phase 14 | NET 层 | testTask:生产路径时延(L8~L11 + 模拟器 A/B,需 PC peer)〔M3 规划〕
|
||||
* 时延分段定义/判定基线/执行流程:docs/测试说明/链路延迟测试说明.md
|
||||
*/
|
||||
|
||||
#ifndef CH395F_TEST_H
|
||||
@@ -45,7 +49,7 @@ extern "C" {
|
||||
* Phase 4 (DHCP) 会改变 IP,必须独立运行;
|
||||
* 两层同时启用会在下方触发 #error。
|
||||
*/
|
||||
/* ---- 硬件层(阶段 1~5)---- */
|
||||
/* ---- 硬件层(阶段 1~5/12)---- */
|
||||
//#define ENABLE_PHASE1_TESTS /* [硬] 寄存器读写 + SPI 命令路径 */
|
||||
//#define ENABLE_PHASE2_TESTS /* [硬] TCP Client 收发 + 关闭重连 */
|
||||
//#define ENABLE_PHASE3_TESTS /* [硬] UDP Server Echo */
|
||||
@@ -58,16 +62,22 @@ extern "C" {
|
||||
//#define ENABLE_PHASE9_TESTS /* [网] Select/Poll I/O 多路复用 */
|
||||
//#define ENABLE_PHASE10_TESTS /* [网] 边界条件测试 */
|
||||
//#define ENABLE_PHASE11_TESTS /* [网] KeepAlive 超时断开测试 */
|
||||
/* ---- 链路时延基线(M1;分段/阈值见 docs/测试说明/链路延迟测试说明.md)---- */
|
||||
//#define ENABLE_PHASE12_TESTS /* [硬] SPI 字节命令 / DMA 时延基线(L1/L2,无需 PC)—— M1 已完成,基线见说明文档 §4.1 */
|
||||
/* ---- 链路时延(M2/M3;分段/阈值见 docs/测试说明/链路延迟测试说明.md)---- */
|
||||
//#define ENABLE_PHASE13_TESTS /* [网] 芯片 + 队列时延(L3~L7/L10,需 PC peer)—— M2 已完成,基线见说明文档 §4.2 */
|
||||
#define ENABLE_PHASE14_TESTS /* [网] 生产路径时延(L8~L11 + 模拟器 Δ)—— M3,需 peer --mode remo(:7003) */
|
||||
|
||||
#if defined(ENABLE_PHASE1_TESTS) || defined(ENABLE_PHASE2_TESTS) || \
|
||||
defined(ENABLE_PHASE3_TESTS) || defined(ENABLE_PHASE4_TESTS) || \
|
||||
defined(ENABLE_PHASE5_TESTS)
|
||||
defined(ENABLE_PHASE5_TESTS) || defined(ENABLE_PHASE12_TESTS)
|
||||
#define ENABLE_HW_LAYER_TESTS 1
|
||||
#endif
|
||||
|
||||
#if defined(ENABLE_PHASE6_TESTS) || defined(ENABLE_PHASE7_TESTS) || \
|
||||
defined(ENABLE_PHASE8_TESTS) || defined(ENABLE_PHASE9_TESTS) || \
|
||||
defined(ENABLE_PHASE10_TESTS) || defined(ENABLE_PHASE11_TESTS)
|
||||
defined(ENABLE_PHASE10_TESTS) || defined(ENABLE_PHASE11_TESTS) || \
|
||||
defined(ENABLE_PHASE13_TESTS) || defined(ENABLE_PHASE14_TESTS)
|
||||
#define ENABLE_NET_LAYER_TESTS 1
|
||||
#endif
|
||||
|
||||
|
||||
+11
-3
@@ -13,6 +13,7 @@
|
||||
#include "ch395f_test_task.h"
|
||||
#include "ch395f_test.h"
|
||||
#include "net_test_task.h"
|
||||
#include "net_latency_test.h"
|
||||
#include "cmsis_os.h"
|
||||
#include "net_task.h"
|
||||
#include "net_socket.h"
|
||||
@@ -49,8 +50,10 @@ test_stats_t g_test_stats;
|
||||
* =================================================================== */
|
||||
|
||||
/* 解析点分十进制 IP 字符串为数组(避免引入 sscanf 库)。
|
||||
* 仅硬件层阶段(1~5)使用,NET 层无需此辅助。 */
|
||||
#if defined(ENABLE_HW_LAYER_TESTS)
|
||||
* 仅硬件层 Phase 2(TCP Client 目标 IP)与 Phase 5(大文件目标 IP)使用;
|
||||
* 守卫精确到使用方,避免只启用其他 HW 阶段时产生 #177-D
|
||||
* (2026-09-22:仅启用 Phase 12 构建实证暴露此潜在警告)。 */
|
||||
#if defined(ENABLE_PHASE2_TESTS) || defined(ENABLE_PHASE5_TESTS)
|
||||
static void test_parse_ip(const char *str, uint8_t *ip_arr)
|
||||
{
|
||||
uint32_t val;
|
||||
@@ -68,7 +71,7 @@ static void test_parse_ip(const char *str, uint8_t *ip_arr)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif /* ENABLE_HW_LAYER_TESTS */
|
||||
#endif /* ENABLE_PHASE2_TESTS || ENABLE_PHASE5_TESTS */
|
||||
|
||||
#ifdef ENABLE_PHASE1_TESTS
|
||||
static void phase1_run(void) {
|
||||
@@ -1377,6 +1380,11 @@ void ch395f_test_run(void)
|
||||
#endif
|
||||
#ifdef ENABLE_PHASE5_TESTS
|
||||
phase5_run();
|
||||
#endif
|
||||
#ifdef ENABLE_PHASE12_TESTS
|
||||
/* Phase 12(链路时延基线 L1/L2,M1):独立于 Phase 1~5 的 SPI 时延
|
||||
* 采样,放硬件层阶段末尾执行(说明文档:链路延迟测试说明.md) */
|
||||
net_latency_phase12_run();
|
||||
#endif
|
||||
}
|
||||
#else
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""FTP 数据连接回归测试(D20 验证 + 日后回归)
|
||||
|
||||
背景(2026-09-22 台架实测 D20):同一 FTP 会话内**第二个数据连接必失败**
|
||||
(425 Can't open data connection)。根因是单连接 Socket 复用时的"陈旧 DISCONNECT"
|
||||
误判:上一条数据连接的 FIN/ACK 在 Socket 已复用为新连接之后才上报,新连接被
|
||||
net 层当作"刚建立就断开"释放。修复见 net_socket.c handle_disconnect_event 的
|
||||
陈旧事件甄别(回读芯片 TCP 状态,仍 ESTABLISHED 则丢弃该中断)。
|
||||
|
||||
本脚本在一个会话里连续做多次数据连接(LIST/NLST/STOR/RETR),并做大文件往返
|
||||
内容校验;任一环节失败即退出码 1。
|
||||
|
||||
用法:
|
||||
& '.venv\\Scripts\\python.exe' test/ftp_regression.py --host 192.168.1.100
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import sys
|
||||
import time
|
||||
from ftplib import FTP
|
||||
|
||||
SMALL = b"D20-small-" + b"A" * 1017 # 1024B
|
||||
BIG = bytes(((i * 7 + 3) & 0xFF) for i in range(32768)) # 32KB 模式数据
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
ap = argparse.ArgumentParser(description="FTP data-connection regression (D20)")
|
||||
ap.add_argument("--host", default="192.168.1.100", help="device IP")
|
||||
ap.add_argument("--port", type=int, default=21, help="FTP control port")
|
||||
ap.add_argument("--timeout", type=int, default=20, help="socket timeout (s)")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
results = []
|
||||
|
||||
def step(name, fn):
|
||||
"""执行一步并记录结果;不抛异常,便于一次跑完全部步骤。"""
|
||||
t0 = time.time()
|
||||
try:
|
||||
extra = fn()
|
||||
dt = time.time() - t0
|
||||
results.append((name, True, dt))
|
||||
print(" %-16s OK %6.2fs %s" % (name, dt, extra if extra is not None else ""))
|
||||
return True
|
||||
except Exception as e: # noqa: BLE001
|
||||
dt = time.time() - t0
|
||||
results.append((name, False, dt))
|
||||
print(" %-16s FAIL %6.2fs %s %s" % (name, dt, type(e).__name__, e))
|
||||
return False
|
||||
|
||||
f = FTP()
|
||||
print("[ftp] connecting %s:%d ..." % (args.host, args.port))
|
||||
f.connect(args.host, args.port, timeout=args.timeout)
|
||||
f.login("anonymous", "x@x")
|
||||
print("[ftp] login ok: %s" % f.getwelcome())
|
||||
|
||||
# 同一会话内连续 8 次数据连接(含两次大文件)—— D20 的失败点是第 2 次
|
||||
step("LIST #1", lambda: "%d lines" % len(f.nlst()))
|
||||
step("STOR #2 1KB", lambda: f.storbinary("STOR d20s.tmp", io.BytesIO(SMALL)) or "226")
|
||||
|
||||
got = bytearray()
|
||||
step("RETR #3 1KB", lambda: (f.retrbinary("RETR d20s.tmp", got.extend), "226")[1])
|
||||
if bytes(got) != SMALL:
|
||||
print(" !! RETR #3 content mismatch (%d bytes)" % len(got))
|
||||
results.append(("RETR#3-content", False, 0.0))
|
||||
else:
|
||||
results.append(("RETR#3-content", True, 0.0))
|
||||
|
||||
step("LIST #4", lambda: "%d lines" % len(f.nlst()))
|
||||
step("STOR #5 32KB", lambda: f.storbinary("STOR d20b.tmp", io.BytesIO(BIG)) or "226")
|
||||
|
||||
big = bytearray()
|
||||
step("RETR #6 32KB", lambda: (f.retrbinary("RETR d20b.tmp", big.extend), "226")[1])
|
||||
if bytes(big) != BIG:
|
||||
print(" !! RETR #6 content mismatch (%d/%d bytes)" % (len(big), len(BIG)))
|
||||
results.append(("RETR#6-content", False, 0.0))
|
||||
else:
|
||||
results.append(("RETR#6-content", True, 0.0))
|
||||
|
||||
step("LIST #7", lambda: "%d lines" % len(f.nlst()))
|
||||
step("cleanup", lambda: (f.delete("d20s.tmp"), f.delete("d20b.tmp"), "deleted")[2])
|
||||
step("QUIT", lambda: f.quit() or "221")
|
||||
|
||||
ok = sum(1 for _, good, _ in results if good)
|
||||
print("[ftp] %d/%d steps passed" % (ok, len(results)))
|
||||
return 0 if ok == len(results) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,836 @@
|
||||
#!/usr/bin/env python3
|
||||
"""latency_peer.py - Link latency test peer for Phase 13 (M2).
|
||||
|
||||
Server-driven protocol: the MCU (testTask, port 8080) sends one operation
|
||||
byte; the peer executes the corresponding traffic pattern. See
|
||||
docs/测试说明/链路延迟测试说明.md §2.5 for the full protocol spec.
|
||||
|
||||
Ops (MCU -> peer):
|
||||
0x01 L5 recv flood : peer sends 50 x 100B (5ms apart)
|
||||
0x02 L5 send drain : peer drains 50 x 100B
|
||||
0x03 L6 : peer sends 10B request, expects 10B echo (timed)
|
||||
0x04 L7 : peer sends 9B LCDMEM request, expects 3209B frame
|
||||
0x05 L3 send drain : peer drains 32 x 4096B
|
||||
0xFF done : session complete
|
||||
|
||||
The peer records host-side (L4-side) timing: first byte / frame total /
|
||||
per-batch arrival curve / >=500ms spikes (TCP retransmission fingerprint).
|
||||
Both the serial log and this JSON carry HOST timestamps -> direct correlation.
|
||||
|
||||
Usage:
|
||||
python latency_peer.py --host 192.168.1.100 --port 8080 --json m2_net.json
|
||||
python latency_peer.py --mode net --ftp-stress ... (E-02 head-of-line repro)
|
||||
|
||||
Pure stdlib (socket/json/time/argparse/threading). ASCII output only.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
# --- Protocol constants (mirror test/net_latency_test.c Phase 13 defines) ---
|
||||
HELLO = b"\x5a\xa5\x58\x01"
|
||||
OP_L5_RECV = 0x01
|
||||
OP_L5_SEND = 0x02
|
||||
OP_L6 = 0x03
|
||||
OP_L7 = 0x04
|
||||
OP_L3 = 0x05
|
||||
OP_DONE = 0xFF
|
||||
|
||||
L5_CHUNKS = 20 # 2026-09-22 run1: 50 chunks saturated the chip RX buffer
|
||||
# (window close -> retransmit re-delivery -> RST); 20 @ 50ms
|
||||
# = 1KB/s quiet baseline (see 链路延迟测试说明.md §8)
|
||||
L5_LEN = 100
|
||||
L6_SIZE = 10
|
||||
L7_REQ_LEN = 9
|
||||
L7_FRAME_LEN = 3209
|
||||
L3_SENDS = 16 # mirror firmware LAT_P13_L3_SENDS (run3: 32->16, 64 chunk samples)
|
||||
L3_LEN = 4096
|
||||
|
||||
SPIKE_MS = 500.0 # retransmission granularity fingerprint (D1)
|
||||
|
||||
# --- Production RemoDispBus constants (mirror drivers/BSP/UI/ui_net_sink.h) ---
|
||||
REMO_TAG_CLIENT = 0xAA
|
||||
REMO_TAG_DEVICE = 0xBB
|
||||
REMO_CMD_KEEP = 0
|
||||
REMO_CMD_INIT = 1
|
||||
REMO_CMD_KEY = 2
|
||||
REMO_CMD_LCDMEM = 3
|
||||
REMO_MEMSIZE = 3200 # 160x160 1bpp
|
||||
REMO_LCDMEM_FRAME = 4 + 4 + REMO_MEMSIZE + 1 # 3209 (header + offset + bitmap + CRC)
|
||||
REMO_CYCLE_DEADLINE_S = 45.0 # per-cycle recv deadline (E-02 stress: up to 30s)
|
||||
|
||||
|
||||
def now_ns():
|
||||
return time.perf_counter_ns()
|
||||
|
||||
|
||||
def iso_now():
|
||||
return time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def recv_exact(sock, n, deadline_ns):
|
||||
buf = bytearray()
|
||||
while len(buf) < n:
|
||||
left = deadline_ns - now_ns()
|
||||
if left <= 0:
|
||||
raise TimeoutError("recv_exact deadline (%d/%d bytes)" % (len(buf), n))
|
||||
try:
|
||||
sock.settimeout(max(left, 1) / 1e9 / 5.0)
|
||||
d = sock.recv(n - len(buf))
|
||||
except socket.timeout:
|
||||
continue
|
||||
if not d:
|
||||
raise ConnectionError("peer connection closed")
|
||||
buf.extend(d)
|
||||
return bytes(buf)
|
||||
|
||||
|
||||
def l7_request(seq=0):
|
||||
"""Production-shaped 9B LCDMEM request (ui_net_sink.c remo_handle):
|
||||
[TAG 0xAA][CMD 0x03][LEN=4 BE][offset u32 BE = 0][CRC = XOR of the 4 offset bytes].
|
||||
LEN is the DATA-segment length (4), NOT the requested bitmap size; the offset
|
||||
field must be 0 (device only serves full-frame requests)."""
|
||||
data = bytes([0, 0, 0, 0])
|
||||
crc = 0
|
||||
for b in data:
|
||||
crc ^= b
|
||||
return bytes([0xAA, 0x03, 0x00, 0x04]) + data + bytes([crc])
|
||||
|
||||
|
||||
def l7_frame_ok(data):
|
||||
"""Validate the 3209B production LCDMEM response (ui_net_sink.c remo_send):
|
||||
[TAG 0xBB][CMD 0x03][LEN=3204 BE][offset u32 = 0][1bpp bitmap 3200B]
|
||||
[CRC = XOR of the 3204 data bytes] -> 4 + 3204 + 1 = 3209B."""
|
||||
if len(data) != L7_FRAME_LEN:
|
||||
return False
|
||||
if data[0] != 0xBB or data[1] != 0x03 or data[2] != 0x0C or data[3] != 0x84:
|
||||
return False
|
||||
payload = data[4:4 + 3204]
|
||||
crc = 0
|
||||
for b in payload:
|
||||
crc ^= b
|
||||
if data[4 + 3204] != crc:
|
||||
return False
|
||||
if payload[:4] != bytes(4):
|
||||
return False
|
||||
for i in range(3200):
|
||||
if payload[4 + i] != (i & 0xFF):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def pattern(n, seed=0):
|
||||
return bytes((seed + i) & 0xFF for i in range(n))
|
||||
|
||||
|
||||
# ================= RemoDispBus production client (M3, :7003) =================
|
||||
|
||||
def xor_bytes(data):
|
||||
crc = 0
|
||||
for b in data:
|
||||
crc ^= b
|
||||
return crc
|
||||
|
||||
|
||||
def remo_request(cmd, data=b""):
|
||||
"""Client->device frame: [0xAA][CMD][LEN_H][LEN_L][DATA][CRC = XOR(DATA)]."""
|
||||
body = bytes(data)
|
||||
return (bytes([REMO_TAG_CLIENT, cmd, (len(body) >> 8) & 0xFF, len(body) & 0xFF])
|
||||
+ body + bytes([xor_bytes(body)]))
|
||||
|
||||
|
||||
def remo_lcdmem_request():
|
||||
"""9B full-frame LCDMEM request (offset 0) - what the production client sends."""
|
||||
return remo_request(REMO_CMD_LCDMEM, bytes(4))
|
||||
|
||||
|
||||
def recv_upto(sock, want, deadline_ns):
|
||||
"""Bounded read: never consumes more than `want` bytes (no over-read of a
|
||||
following frame/op byte - the run10 lesson)."""
|
||||
left = deadline_ns - now_ns()
|
||||
if left <= 0:
|
||||
raise TimeoutError("frame deadline")
|
||||
try:
|
||||
sock.settimeout(max(left, 1) / 1e9 / 5.0)
|
||||
return sock.recv(want)
|
||||
except socket.timeout:
|
||||
return b""
|
||||
|
||||
|
||||
def read_remo_frame(sock, deadline_ns, ack_probe=False):
|
||||
"""Read exactly one device frame; returns tag/cmd/data/CRC + arrival curve.
|
||||
|
||||
The first read is bounded to 8B so the segment-arrival curve stays
|
||||
fine-grained; subsequent reads are bounded by the remaining frame length
|
||||
(device never pipelines: one response per request).
|
||||
|
||||
ack_probe=True: as soon as the frame is known to be incomplete, send one
|
||||
byte the device parser discards (0x00, not TAG 0xAA). The resulting TCP
|
||||
segment carries a piggybacked ACK for the data already received, which
|
||||
discriminates "sender holds the last partial segment waiting for an ACK
|
||||
(Nagle-style)" from "chip-internal fixed timer" - without any packet capture."""
|
||||
buf = bytearray()
|
||||
batches = []
|
||||
first_ns = None
|
||||
total = None
|
||||
while True:
|
||||
want = (8 - len(buf)) if total is None else (total - len(buf))
|
||||
if want <= 0:
|
||||
break
|
||||
d = recv_upto(sock, min(want, 4096), deadline_ns)
|
||||
if not d:
|
||||
if total is not None:
|
||||
raise ConnectionError("frame truncated (%d/%d bytes)" % (len(buf), total))
|
||||
continue # header still incomplete: wait until deadline
|
||||
t = now_ns()
|
||||
if first_ns is None:
|
||||
first_ns = t
|
||||
buf.extend(d)
|
||||
batches.append([t, len(d)])
|
||||
if total is None and len(buf) >= 4:
|
||||
total = 4 + (((buf[2] << 8) | buf[3])) + 1
|
||||
if ack_probe and total is not None and len(buf) < total:
|
||||
try:
|
||||
sock.sendall(b"\x00") # parser-safe: discarded while waiting for TAG 0xAA
|
||||
except OSError:
|
||||
pass
|
||||
data = bytes(buf[4:total - 1])
|
||||
crc = buf[total - 1]
|
||||
return {"tag": buf[0], "cmd": buf[1], "data": data, "total": total,
|
||||
"crc_ok": crc == xor_bytes(data), "first_ns": first_ns, "batches": batches}
|
||||
|
||||
|
||||
def enable_immediate_ack(sock):
|
||||
"""Windows: disable delayed ACK on this socket (SIO_TCP_SET_ACK_FREQUENCY=1).
|
||||
|
||||
Root-cause fix for the ~50ms tail: the CH395F (MSS=1024) holds the final
|
||||
sub-MSS segment of a 3209B frame per Nagle until the peer ACKs; Windows'
|
||||
delayed ACK defers that ACK ~50ms. Setting ACK frequency=1 makes every
|
||||
segment ACKed immediately -> tail 52ms -> ~4ms (measured). Returns True on
|
||||
success. Other platforms: no-op (Linux uses TCP_QUICKACK per recv)."""
|
||||
if sys.platform != "win32":
|
||||
return False
|
||||
try:
|
||||
import ctypes
|
||||
ws2 = ctypes.WinDLL("ws2_32", use_last_error=True)
|
||||
WSAIoctl = ws2.WSAIoctl
|
||||
WSAIoctl.argtypes = [ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p, ctypes.c_uint32,
|
||||
ctypes.c_void_p, ctypes.c_uint32, ctypes.POINTER(ctypes.c_uint32),
|
||||
ctypes.c_void_p, ctypes.c_void_p]
|
||||
WSAIoctl.restype = ctypes.c_int
|
||||
val = ctypes.c_uint32(1) # 1 = ACK every segment
|
||||
ret = ctypes.c_uint32(0)
|
||||
rc = WSAIoctl(ctypes.c_void_p(sock.fileno()), 0x98000017,
|
||||
ctypes.byref(val), 4, None, 0, ctypes.byref(ret), None, None)
|
||||
return rc == 0
|
||||
except (OSError, AttributeError):
|
||||
return False
|
||||
|
||||
|
||||
def remo_bitmap_nontrivial(payload):
|
||||
"""Same 'non-trivial frame' criterion as simulator/tests/check_remo_sink.py:
|
||||
the 1bpp bitmap must not be a single repeated byte (blank/broken frame)."""
|
||||
bmp = payload[4:4 + REMO_MEMSIZE]
|
||||
return len(set(bmp)) > 2
|
||||
|
||||
|
||||
class RemoPeer:
|
||||
"""Production-path peer (M3): drives L8/L9 against :7003 (device) or the
|
||||
simulator, measuring the full client-observed frame cycle."""
|
||||
|
||||
def __init__(self, host, port, wait_s, json_path, samples, cadence_ms,
|
||||
stress_from, nodelay, reconnect_rounds=1, ack_probe=False, ack_fast=False):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.wait_s = wait_s
|
||||
self.json_path = json_path
|
||||
self.samples = samples
|
||||
self.cadence_s = cadence_ms / 1000.0
|
||||
self.stress_from = stress_from # cycle index where FTP stress starts
|
||||
self.nodelay = nodelay
|
||||
self.reconnect_rounds = max(1, reconnect_rounds)
|
||||
self.ack_probe = ack_probe # in-band ACK-release experiment (tail diagnosis)
|
||||
self.ack_fast = ack_fast # Windows immediate-ACK socket option
|
||||
self.ack_fast_ok = None
|
||||
self.sock = None
|
||||
self.t0_wall = None
|
||||
self.t0_ns = 0
|
||||
self.connect_ms = None
|
||||
self.reconnect_ms = None
|
||||
self.reconnect_list = []
|
||||
self.init_rtt_ms = None
|
||||
self.geom = {}
|
||||
self.frames = []
|
||||
|
||||
# --- connection ---
|
||||
def connect_once(self):
|
||||
t = now_ns()
|
||||
s = socket.create_connection((self.host, self.port), timeout=5.0)
|
||||
if self.nodelay:
|
||||
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
if self.ack_fast:
|
||||
self.ack_fast_ok = enable_immediate_ack(s)
|
||||
print("[peer] immediate-ACK (SIO_TCP_SET_ACK_FREQUENCY=1): %s"
|
||||
% ("applied" if self.ack_fast_ok else "not available"))
|
||||
self.sock = s
|
||||
return (now_ns() - t) / 1e6
|
||||
|
||||
def connect_with_retry(self):
|
||||
t_end = time.time() + self.wait_s
|
||||
last = None
|
||||
while time.time() < t_end:
|
||||
try:
|
||||
return self.connect_once()
|
||||
except OSError as e:
|
||||
last = e
|
||||
time.sleep(1.0)
|
||||
raise ConnectionError("cannot connect to %s:%d within %ds (%s)" %
|
||||
(self.host, self.port, self.wait_s, last))
|
||||
|
||||
def init_roundtrip(self, deadline_s=5.0):
|
||||
"""INIT request -> geometry reply; returns round-trip ms."""
|
||||
t = now_ns()
|
||||
self.sock.sendall(remo_request(REMO_CMD_INIT))
|
||||
f = read_remo_frame(self.sock, t + int(deadline_s * 1e9))
|
||||
ms = (now_ns() - t) / 1e6
|
||||
if f["cmd"] == REMO_CMD_INIT and len(f["data"]) >= 6:
|
||||
# 8B (fixed): [W u16][H u16][MEMSIZE u32 BE]; 6B = 旧固件(u32 被截半 → 0)
|
||||
self.geom = {"w": (f["data"][0] << 8) | f["data"][1],
|
||||
"h": (f["data"][2] << 8) | f["data"][3],
|
||||
"framebytes": int.from_bytes(f["data"][4:8], "big")
|
||||
if len(f["data"]) >= 8
|
||||
else ((f["data"][4] << 8) | f["data"][5]),
|
||||
"payload_len": len(f["data"])}
|
||||
return ms
|
||||
|
||||
# --- FTP head-of-line stress (E-02) ---
|
||||
def ftp_stress_worker(self, start_evt, stop_evt, hold_s):
|
||||
"""Wait for the stress window to open, then hold an idle FTP login:
|
||||
lftpd read_line -> blocking net_recv(0) inside netTask (NET_RECV_TIMEOUT_MS)
|
||||
-> every queued message (incl. the LCDMEM response) waits behind it (E-02)."""
|
||||
if not start_evt.wait(120.0):
|
||||
sys.stdout.write("[peer] ftp stress: window never opened\n")
|
||||
return
|
||||
try:
|
||||
s = socket.create_connection((self.host, 21), timeout=10.0)
|
||||
s.settimeout(10.0)
|
||||
s.sendall(b"USER latency\r\n")
|
||||
reply = b""
|
||||
while b"\r\n" not in reply:
|
||||
d = s.recv(64)
|
||||
if not d:
|
||||
break
|
||||
reply += d
|
||||
sys.stdout.write("[peer] ftp stress armed (331: %r)\n" % reply[:40])
|
||||
stop_evt.wait(hold_s)
|
||||
try:
|
||||
s.sendall(b"QUIT\r\n")
|
||||
except OSError:
|
||||
pass
|
||||
s.close()
|
||||
sys.stdout.write("[peer] ftp stress released\n")
|
||||
except OSError as e:
|
||||
sys.stdout.write("[peer] ftp stress FAILED: %s\n" % e)
|
||||
|
||||
# --- session ---
|
||||
def one_session(self):
|
||||
self.t0_wall = iso_now()
|
||||
self.t0_ns = now_ns()
|
||||
|
||||
self.connect_ms = self.connect_with_retry()
|
||||
print("[peer] connected %s:%d in %.1f ms" % (self.host, self.port, self.connect_ms))
|
||||
self.init_rtt_ms = self.init_roundtrip()
|
||||
print("[peer] INIT rtt=%.1f ms geom=%s" % (self.init_rtt_ms, self.geom))
|
||||
|
||||
stress_start = threading.Event()
|
||||
stress_stop = threading.Event()
|
||||
stress_thread = None
|
||||
if self.stress_from is not None:
|
||||
# hold long enough to cover the remaining cycles (+15s margin)
|
||||
hold_s = self.cadence_s * max(1, self.samples - self.stress_from) + 15.0
|
||||
stress_thread = threading.Thread(
|
||||
target=self.ftp_stress_worker,
|
||||
args=(stress_start, stress_stop, hold_s),
|
||||
daemon=True)
|
||||
stress_thread.start()
|
||||
|
||||
for i in range(self.samples):
|
||||
if self.stress_from is not None and i == self.stress_from:
|
||||
print("[peer] === FTP stress ON (E-02 head-of-line) at cycle %d ===" % i)
|
||||
stress_start.set()
|
||||
time.sleep(0.5) # let the FTP half-login block netTask
|
||||
|
||||
stressed = (self.stress_from is not None and i >= self.stress_from)
|
||||
t = now_ns()
|
||||
self.sock.sendall(remo_lcdmem_request())
|
||||
f = read_remo_frame(self.sock, t + int(REMO_CYCLE_DEADLINE_S * 1e9),
|
||||
ack_probe=self.ack_probe)
|
||||
t_end = now_ns()
|
||||
|
||||
first_ms = (f["first_ns"] - t) / 1e6
|
||||
total_ms = (t_end - t) / 1e6
|
||||
tail_ms = (f["batches"][-1][0] - f["batches"][0][0]) / 1e6 if len(f["batches"]) > 1 else 0.0
|
||||
rec = {
|
||||
"cycle": i,
|
||||
"stress": stressed,
|
||||
"first_ms": round(first_ms, 3),
|
||||
"total_ms": round(total_ms, 3),
|
||||
"tail_lag_ms": round(tail_ms, 3),
|
||||
"batches": [[round((b[0] - t) / 1e6, 3), b[1]] for b in f["batches"]],
|
||||
"bytes": f["total"],
|
||||
"tag": f["tag"],
|
||||
"cmd": f["cmd"],
|
||||
"crc_ok": bool(f["crc_ok"]),
|
||||
"nontrivial": bool(f["cmd"] == REMO_CMD_LCDMEM
|
||||
and len(f["data"]) == 4 + REMO_MEMSIZE
|
||||
and remo_bitmap_nontrivial(f["data"])),
|
||||
}
|
||||
self.frames.append(rec)
|
||||
if (i % 10) == 0 or stressed:
|
||||
print("[peer] cycle %3d %-7s first=%7.1f total=%8.1f ms batches=%d %s"
|
||||
% (i, "STRESS" if stressed else "quiet", first_ms, total_ms,
|
||||
len(f["batches"]),
|
||||
"OK" if (rec["crc_ok"] and rec["nontrivial"]) else "BAD"))
|
||||
if i + 1 < self.samples:
|
||||
time.sleep(self.cadence_s)
|
||||
|
||||
if stress_thread is not None:
|
||||
stress_stop.set()
|
||||
|
||||
self.dump_json()
|
||||
|
||||
# --- L9: close + reconnect rounds (each close drives the device auto-relisten
|
||||
# probe; each reconnect measures connect + INIT round trip) ---
|
||||
for r in range(self.reconnect_rounds):
|
||||
self.sock.close()
|
||||
print("[peer] round %d/%d: closed; reconnect in 1.5s (device relisten window) ..."
|
||||
% (r + 1, self.reconnect_rounds))
|
||||
time.sleep(1.5)
|
||||
t = now_ns()
|
||||
self.connect_once()
|
||||
self.init_rtt_ms = self.init_roundtrip()
|
||||
ms = (now_ns() - t) / 1e6
|
||||
self.reconnect_list.append(round(ms, 3))
|
||||
self.reconnect_ms = ms
|
||||
print("[peer] round %d: reconnect + INIT = %.1f ms" % (r + 1, ms))
|
||||
|
||||
rep = self.dump_json()
|
||||
self.print_summary(rep)
|
||||
self.sock.close()
|
||||
|
||||
# --- reporting ---
|
||||
def _split(self, key, stressed):
|
||||
return [f[key] for f in self.frames if f["stress"] is stressed]
|
||||
|
||||
def _spikes(self, vals):
|
||||
return sum(1 for x in vals if x >= SPIKE_MS)
|
||||
|
||||
def dump_json(self, error=None):
|
||||
all_total = [f["total_ms"] for f in self.frames]
|
||||
all_first = [f["first_ms"] for f in self.frames]
|
||||
all_tail = [f["tail_lag_ms"] for f in self.frames]
|
||||
quiet_total = self._split("total_ms", False)
|
||||
stress_total = self._split("total_ms", True)
|
||||
rep = {
|
||||
"mode": "remo",
|
||||
"host": self.host,
|
||||
"port": self.port,
|
||||
"ts_start": self.t0_wall,
|
||||
"ts_end": iso_now(),
|
||||
"duration_s": round((now_ns() - self.t0_ns) / 1e9, 2),
|
||||
"samples": self.samples,
|
||||
"cadence_ms": round(self.cadence_s * 1000.0, 1),
|
||||
"tcp_nodelay": bool(self.nodelay),
|
||||
"immediate_ack": (None if not self.ack_fast else bool(self.ack_fast_ok)),
|
||||
"in_band_ack_probe": bool(self.ack_probe),
|
||||
"stress_from": self.stress_from,
|
||||
"connect_ms": None if self.connect_ms is None else round(self.connect_ms, 3),
|
||||
"reconnect_ms": None if self.reconnect_ms is None else round(self.reconnect_ms, 3),
|
||||
"reconnect_list_ms": self.reconnect_list,
|
||||
"reconnect_stat": stat_block(self.reconnect_list),
|
||||
"init": {"rtt_ms": None if self.init_rtt_ms is None else round(self.init_rtt_ms, 3),
|
||||
"geom": self.geom},
|
||||
"cycle_all": stat_block(all_total, self._spikes(all_total)),
|
||||
"cycle_quiet": stat_block(quiet_total, self._spikes(quiet_total)),
|
||||
"cycle_stress": stat_block(stress_total, self._spikes(stress_total)),
|
||||
"first_byte_all": stat_block(all_first, self._spikes(all_first)),
|
||||
"tail_lag_all": stat_block(all_tail, self._spikes(all_tail)),
|
||||
"crc": {"ok": sum(1 for f in self.frames if f["crc_ok"]),
|
||||
"fail": sum(1 for f in self.frames if not f["crc_ok"])},
|
||||
"bitmap": {"nontrivial_ok": sum(1 for f in self.frames if f["nontrivial"]),
|
||||
"nontrivial_fail": sum(1 for f in self.frames if not f["nontrivial"])},
|
||||
"frames": self.frames,
|
||||
}
|
||||
if error:
|
||||
rep["error"] = error
|
||||
if self.json_path:
|
||||
with open(self.json_path, "w", encoding="utf-8") as fh:
|
||||
json.dump(rep, fh, ensure_ascii=False, indent=1)
|
||||
print("[peer] JSON report -> %s%s" % (self.json_path,
|
||||
" (PARTIAL - error)" if error else ""))
|
||||
return rep
|
||||
|
||||
def print_summary(self, rep):
|
||||
print("=== latency_peer summary (M3 / Phase 14, production RemoDispBus) ===")
|
||||
print("host=%s:%d samples=%d cadence=%.0fms nodelay=%d duration=%.1fs"
|
||||
% (self.host, self.port, rep["samples"], rep["cadence_ms"],
|
||||
int(rep["tcp_nodelay"]), rep["duration_s"]))
|
||||
print("connect=%s ms reconnect+INIT=%s ms geom=%s"
|
||||
% (rep["connect_ms"], rep["reconnect_ms"], rep["init"]["geom"]))
|
||||
for key in ("cycle_all", "cycle_quiet", "cycle_stress",
|
||||
"first_byte_all", "tail_lag_all"):
|
||||
b = rep[key]
|
||||
if b.get("n"):
|
||||
print("%-15s n=%-4d min=%9.1f med=%9.1f p95=%9.1f max=%9.1f ms spikes=%d"
|
||||
% (key, b["n"], b["min_ms"], b["med_ms"], b["p95_ms"], b["max_ms"],
|
||||
b.get("spikes500", 0)))
|
||||
else:
|
||||
print("%-15s (no samples)" % key)
|
||||
print("crc ok/fail = %d/%d bitmap nontrivial ok/fail = %d/%d"
|
||||
% (rep["crc"]["ok"], rep["crc"]["fail"],
|
||||
rep["bitmap"]["nontrivial_ok"], rep["bitmap"]["nontrivial_fail"]))
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.one_session()
|
||||
except (ConnectionError, TimeoutError, RuntimeError, OSError) as e:
|
||||
self.dump_json(error="%s: %s" % (type(e).__name__, e))
|
||||
raise
|
||||
|
||||
|
||||
def percentile(sorted_vals, pct):
|
||||
if not sorted_vals:
|
||||
return None
|
||||
idx = (len(sorted_vals) * pct) // 100
|
||||
if idx >= len(sorted_vals):
|
||||
idx = len(sorted_vals) - 1
|
||||
return sorted_vals[idx]
|
||||
|
||||
|
||||
def stat_block(ms_vals, spikes=0):
|
||||
if not ms_vals:
|
||||
return {"n": 0}
|
||||
s = sorted(ms_vals)
|
||||
return {
|
||||
"n": len(s),
|
||||
"min_ms": round(s[0], 3),
|
||||
"med_ms": round(s[len(s) // 2], 3),
|
||||
"p95_ms": round(percentile(s, 95), 3),
|
||||
"max_ms": round(s[-1], 3),
|
||||
"spikes500": spikes,
|
||||
}
|
||||
|
||||
|
||||
def fresh_reports():
|
||||
"""Per-session measurement containers (reset on each retry attempt)."""
|
||||
return {
|
||||
"l6": {"samples": [], "echo_ok": 0, "echo_fail": 0},
|
||||
"l7": {"frames": [], "first_ms": [], "total_ms": [], "spikes500": 0,
|
||||
"crc_ok": 0, "crc_fail": 0, "batches_per_frame": []},
|
||||
"l5_recv": {"bytes": 0, "window_ms": None},
|
||||
"l5_send": {"bytes": 0, "window_ms": None},
|
||||
"l3": {"bytes": 0, "window_ms": None},
|
||||
}
|
||||
|
||||
|
||||
class Peer:
|
||||
def __init__(self, host, port, wait_s, json_path, ftp_stress):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.wait_s = wait_s
|
||||
self.connect_wait_s = wait_s # per-attempt connect budget (retries shrink it)
|
||||
self.json_path = json_path
|
||||
self.ftp_stress = ftp_stress
|
||||
self.sock = None
|
||||
self.t0_wall = None
|
||||
self.t0_ns = 0
|
||||
self.n_ops = 0
|
||||
self.reports = fresh_reports()
|
||||
|
||||
def connect_with_retry(self):
|
||||
t_end = time.time() + self.connect_wait_s
|
||||
while time.time() < t_end:
|
||||
try:
|
||||
s = socket.create_connection((self.host, self.port), timeout=5.0)
|
||||
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
self.sock = s
|
||||
return
|
||||
except OSError:
|
||||
time.sleep(1.0)
|
||||
raise ConnectionError("cannot connect to %s:%d within %ds" %
|
||||
(self.host, self.port, self.wait_s))
|
||||
|
||||
def ftp_stress_worker(self, start_evt, stop_evt):
|
||||
"""Hold an idle FTP login: lftpd read_line -> blocking net_recv(0) in
|
||||
netTask (30s timeout) -> all queued messages wait behind it (E-02)."""
|
||||
try:
|
||||
s = socket.create_connection((self.host, 21), timeout=10.0)
|
||||
s.settimeout(10.0)
|
||||
s.sendall(b"USER latency\r\n")
|
||||
reply = b""
|
||||
while b"\r\n" not in reply:
|
||||
d = s.recv(64)
|
||||
if not d:
|
||||
break
|
||||
reply += d
|
||||
start_evt.set() # L5 window starts under stress
|
||||
sys.stdout.write("[peer] ftp stress armed (331: %r)\n" % reply[:40])
|
||||
stop_evt.wait(35.0) # > 30s: netTask read times out (full freeze)
|
||||
try:
|
||||
s.sendall(b"QUIT\r\n")
|
||||
except OSError:
|
||||
pass
|
||||
s.close()
|
||||
sys.stdout.write("[peer] ftp stress released\n")
|
||||
except OSError as e:
|
||||
sys.stdout.write("[peer] ftp stress FAILED: %s\n" % e)
|
||||
start_evt.set()
|
||||
|
||||
def dump_json(self, error=None):
|
||||
"""Build the report dict and write it (idempotent; error => partial)."""
|
||||
rep = {
|
||||
"mode": "net",
|
||||
"host": self.host,
|
||||
"port": self.port,
|
||||
"ts_start": self.t0_wall,
|
||||
"ts_end": iso_now(),
|
||||
"duration_s": round((now_ns() - self.t0_ns) / 1e9, 2),
|
||||
"ops_seen": self.n_ops,
|
||||
"ftp_stress": self.ftp_stress,
|
||||
"seg_l6": stat_block(self.reports["l6"]["samples"], 0)
|
||||
| {"echo_ok": self.reports["l6"]["echo_ok"], "echo_fail": self.reports["l6"]["echo_fail"]},
|
||||
"seg_l7_first_byte": stat_block(self.reports["l7"]["first_ms"]),
|
||||
"seg_l7_total": stat_block(self.reports["l7"]["total_ms"], self.reports["l7"]["spikes500"]),
|
||||
"l7_crc": {"ok": self.reports["l7"]["crc_ok"], "fail": self.reports["l7"]["crc_fail"],
|
||||
"batches_per_frame": sorted(set(self.reports["l7"]["batches_per_frame"]))},
|
||||
"l5_recv": self.reports["l5_recv"],
|
||||
"l5_send": self.reports["l5_send"],
|
||||
"l3_drain": self.reports["l3"],
|
||||
"frames_l7": self.reports["l7"]["frames"],
|
||||
}
|
||||
if error:
|
||||
rep["error"] = error
|
||||
if self.json_path:
|
||||
with open(self.json_path, "w", encoding="utf-8") as f:
|
||||
json.dump(rep, f, indent=1, ensure_ascii=True)
|
||||
print("[peer] JSON report -> %s%s" % (self.json_path,
|
||||
" (PARTIAL - error)" if error else ""))
|
||||
return rep
|
||||
|
||||
def run(self, max_attempts=3):
|
||||
"""整会话重试:芯片对空闲连接自行 RST(MCU 栈慢于芯片握手完成时,连接
|
||||
建立后 15~30s 无流量即被芯片超时断开,2026-09-22 台架实证)→ 重连 +
|
||||
MCU 端 re-HELLO 重启 = 闭环。重试时缩短 connect 预算(设备已在运行)。"""
|
||||
last_err = None
|
||||
for attempt in range(1, max_attempts + 1):
|
||||
self.t0_wall = iso_now()
|
||||
self.t0_ns = now_ns()
|
||||
self.n_ops = 0
|
||||
self.reports = fresh_reports()
|
||||
self.connect_wait_s = self.wait_s if attempt == 1 else min(self.wait_s, 60)
|
||||
try:
|
||||
self.one_session()
|
||||
return
|
||||
except (ConnectionError, TimeoutError, RuntimeError, OSError) as e:
|
||||
last_err = e
|
||||
self.close_quiet()
|
||||
if attempt < max_attempts:
|
||||
print("[peer] session attempt %d/%d failed: %s" % (attempt, max_attempts, e))
|
||||
print("[peer] reconnect in 2s (MCU auto-relistens; re-HELLO restarts) ...")
|
||||
time.sleep(2.0)
|
||||
raise RuntimeError("all %d session attempts failed (last: %s)" % (max_attempts, last_err))
|
||||
|
||||
def close_quiet(self):
|
||||
try:
|
||||
if self.sock is not None:
|
||||
self.sock.close()
|
||||
except OSError:
|
||||
pass
|
||||
self.sock = None
|
||||
|
||||
def one_session(self):
|
||||
t0_wall = self.t0_wall
|
||||
t0_ns = self.t0_ns
|
||||
print("[peer] connecting %s:%d (wait up to %ds) ..." % (self.host, self.port, self.connect_wait_s))
|
||||
self.connect_with_retry()
|
||||
print("[peer] connected at %s" % iso_now())
|
||||
|
||||
l5_start_evt = threading.Event()
|
||||
l5_stop_evt = threading.Event()
|
||||
if self.ftp_stress:
|
||||
th = threading.Thread(target=self.ftp_stress_worker,
|
||||
args=(l5_start_evt, l5_stop_evt), daemon=True)
|
||||
th.start()
|
||||
|
||||
self.sock.sendall(HELLO)
|
||||
print("[peer] HELLO sent, awaiting MCU ops ...")
|
||||
|
||||
l5_window_open = False
|
||||
l5_t0 = 0
|
||||
l3_t0 = 0
|
||||
while True:
|
||||
op = recv_exact(self.sock, 1, t0_ns + 300 * 1000 * 1000000)
|
||||
op = op[0]
|
||||
self.n_ops += 1
|
||||
if op == OP_DONE:
|
||||
print("[peer] op=done after %d ops" % self.n_ops)
|
||||
break
|
||||
|
||||
if op == OP_L5_RECV:
|
||||
if self.ftp_stress:
|
||||
l5_start_evt.wait(60.0) # start window only under full stress
|
||||
l5_window_open = True
|
||||
l5_t0 = now_ns()
|
||||
for i in range(L5_CHUNKS):
|
||||
self.sock.sendall(pattern(L5_LEN, seed=i))
|
||||
time.sleep(0.05) # 50ms: keep 1KB/s below MCU drain rate
|
||||
self.reports["l5_recv"]["window_ms"] = (now_ns() - l5_t0) / 1e6
|
||||
self.reports["l5_recv"]["bytes"] = L5_CHUNKS * L5_LEN
|
||||
l5_stop_evt.set()
|
||||
|
||||
elif op == OP_L5_SEND:
|
||||
t = now_ns()
|
||||
data = recv_exact(self.sock, L5_CHUNKS * L5_LEN, t + 120 * 1000 * 1000000)
|
||||
self.reports["l5_send"]["bytes"] = len(data)
|
||||
self.reports["l5_send"]["window_ms"] = (now_ns() - t) / 1e6
|
||||
|
||||
elif op == OP_L6:
|
||||
t0 = now_ns()
|
||||
req = pattern(L6_SIZE, seed=self.n_ops)
|
||||
self.sock.sendall(req)
|
||||
resp = recv_exact(self.sock, L6_SIZE, t0 + 40 * 1000 * 1000000)
|
||||
ms = (now_ns() - t0) / 1e6
|
||||
self.reports["l6"]["samples"].append(ms)
|
||||
if resp == req:
|
||||
self.reports["l6"]["echo_ok"] += 1
|
||||
else:
|
||||
self.reports["l6"]["echo_fail"] += 1
|
||||
|
||||
elif op == OP_L7:
|
||||
t_send = now_ns()
|
||||
self.sock.sendall(l7_request(self.n_ops))
|
||||
got = bytearray()
|
||||
batches = []
|
||||
first_ns = None
|
||||
while len(got) < L7_FRAME_LEN:
|
||||
# bounded read: never consume the next op byte (ASCII 0x04) that
|
||||
# follows the frame in the stream (run10: unbounded recv ate it ->
|
||||
# 3210-byte "frame" -> length check failed -> peer closed)
|
||||
d = self.sock.recv(min(4096, L7_FRAME_LEN - len(got)))
|
||||
if not d:
|
||||
raise ConnectionError("L7 frame truncated (%d/%d)" %
|
||||
(len(got), L7_FRAME_LEN))
|
||||
if first_ns is None:
|
||||
first_ns = now_ns()
|
||||
got.extend(d)
|
||||
batches.append([round((now_ns() - t_send) / 1e6, 3), len(d)])
|
||||
t_end = now_ns()
|
||||
first_ms = (first_ns - t_send) / 1e6 if first_ns else None
|
||||
total_ms = (t_end - t_send) / 1e6
|
||||
# inter-batch arrival gaps
|
||||
spikes = 0
|
||||
for k in range(1, len(batches)):
|
||||
if batches[k][0] - batches[k - 1][0] >= SPIKE_MS:
|
||||
spikes += 1
|
||||
self.reports["l7"]["first_ms"].append(first_ms if first_ms is not None else 0.0)
|
||||
self.reports["l7"]["total_ms"].append(total_ms)
|
||||
self.reports["l7"]["spikes500"] += spikes
|
||||
self.reports["l7"]["batches_per_frame"].append(len(batches))
|
||||
if l7_frame_ok(bytes(got)):
|
||||
self.reports["l7"]["crc_ok"] += 1
|
||||
else:
|
||||
self.reports["l7"]["crc_fail"] += 1
|
||||
self.reports["l7"]["frames"].append(
|
||||
{"first_ms": round(first_ms, 3) if first_ms is not None else None,
|
||||
"total_ms": round(total_ms, 3), "batches": batches})
|
||||
|
||||
elif op == OP_L3:
|
||||
l3_t0 = now_ns()
|
||||
recv_exact(self.sock, L3_SENDS * L3_LEN, l3_t0 + 120 * 1000 * 1000000)
|
||||
self.reports["l3"]["bytes"] = L3_SENDS * L3_LEN
|
||||
self.reports["l3"]["window_ms"] = (now_ns() - l3_t0) / 1e6
|
||||
|
||||
else:
|
||||
raise RuntimeError("unknown op byte 0x%02X (protocol desync)" % op)
|
||||
|
||||
# --- JSON report (also used on the error path: partial data kept) ---
|
||||
rep = self.dump_json()
|
||||
|
||||
print("=== latency_peer summary (M2 / Phase 13) ===")
|
||||
print("ops=%d duration=%.1fs ftp_stress=%d" % (self.n_ops, rep["duration_s"], int(self.ftp_stress)))
|
||||
for key in ("seg_l6", "seg_l7_first_byte", "seg_l7_total"):
|
||||
b = rep[key]
|
||||
if b.get("n"):
|
||||
print("%-18s n=%-4d min=%9.1f med=%9.1f p95=%9.1f max=%9.1f ms spikes=%d"
|
||||
% (key, b["n"], b["min_ms"], b["med_ms"], b["p95_ms"], b["max_ms"],
|
||||
b.get("spikes500", 0)))
|
||||
else:
|
||||
print("%-18s (no samples)" % key)
|
||||
print("l7 crc ok/fail = %d/%d batches/frame = %s"
|
||||
% (rep["l7_crc"]["ok"], rep["l7_crc"]["fail"], rep["l7_crc"]["batches_per_frame"]))
|
||||
self.sock.close()
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
ap = argparse.ArgumentParser(
|
||||
description="link latency test peer - M2 Phase 13 (net) / M3 Phase 14 (remo)")
|
||||
ap.add_argument("--mode", default="net", choices=["net", "remo"],
|
||||
help="net = Phase 13 test protocol :8080; "
|
||||
"remo = production RemoDispBus client :7003 (Phase 14 / simulator)")
|
||||
ap.add_argument("--host", default="192.168.1.100", help="device IP")
|
||||
ap.add_argument("--port", type=int, default=None,
|
||||
help="listen port (default: 8080 for net, 7003 for remo)")
|
||||
ap.add_argument("--wait", type=int, default=180,
|
||||
help="max seconds to retry connect (device boots after flash)")
|
||||
ap.add_argument("--json", default=None, help="write JSON report to this path")
|
||||
ap.add_argument("--ftp-stress", action="store_true",
|
||||
help="hold idle FTP login (E-02 head-of-line repro)")
|
||||
ap.add_argument("--samples", type=int, default=50,
|
||||
help="remo mode: LCDMEM frame cycles to measure (default 50)")
|
||||
ap.add_argument("--cadence-ms", type=int, default=100,
|
||||
help="remo mode: client poll cadence in ms (production client = 100)")
|
||||
ap.add_argument("--stress-from", type=int, default=None,
|
||||
help="remo mode: cycle index where FTP stress starts "
|
||||
"(default with --ftp-stress: half of --samples)")
|
||||
ap.add_argument("--nodelay", action="store_true",
|
||||
help="remo mode: set TCP_NODELAY on the client socket")
|
||||
ap.add_argument("--reconnect-rounds", type=int, default=1,
|
||||
help="remo mode: close+reconnect rounds at the end (each one drives "
|
||||
"the device auto-relisten probe; M3 uses 3)")
|
||||
ap.add_argument("--ack-probe", action="store_true",
|
||||
help="remo mode: when a frame is still incomplete, send one parser-safe "
|
||||
"byte so the TCP segment piggybacks an ACK (diagnoses whether the "
|
||||
"~50ms tail waits for an ACK; no packet capture needed)")
|
||||
ap.add_argument("--ack-fast", action="store_true",
|
||||
help="remo mode: Windows only - disable delayed ACK on this socket "
|
||||
"(SIO_TCP_SET_ACK_FREQUENCY=1); removes the ~50ms frame tail")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
if args.mode == "remo":
|
||||
port = args.port if args.port else 7003
|
||||
stress_from = args.stress_from
|
||||
if args.ftp_stress and stress_from is None:
|
||||
stress_from = max(1, args.samples // 2)
|
||||
peer = RemoPeer(args.host, port, args.wait, args.json, args.samples,
|
||||
args.cadence_ms, stress_from, args.nodelay,
|
||||
args.reconnect_rounds, args.ack_probe, args.ack_fast)
|
||||
else:
|
||||
peer = Peer(args.host, args.port or 8080, args.wait, args.json, args.ftp_stress)
|
||||
try:
|
||||
peer.run()
|
||||
return 0
|
||||
except (ConnectionError, TimeoutError, RuntimeError, OSError) as e:
|
||||
print("[peer] ERROR: %s" % e)
|
||||
try:
|
||||
peer.dump_json(error=str(e)) # partial report: keep measured data
|
||||
except OSError as e2:
|
||||
print("[peer] (JSON write failed: %s)" % e2)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,228 @@
|
||||
/******************************************************************************
|
||||
* 模块名称:CH395F 链路延迟测试
|
||||
* 模块功能:链路分段时延测量的微秒时基(DWT CYCCNT)、采样统计
|
||||
* (min/med/p95/max + 尖峰计数)、机器可读 [LAT] 报告输出与判定。
|
||||
* Phase 12(硬件层,M1):SPI 字节命令 / DMA 时延基线(L1/L2,无需 PC);
|
||||
* Phase 13/14(NET 层,M2/M3 规划):芯片发送握手 / 队列往返 /
|
||||
* 生产路径时延。分段定义、判定基线与执行流程见
|
||||
* docs/测试说明/链路延迟测试说明.md。
|
||||
* 适用平台:STM32F4 系列(CH395F)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-09-22
|
||||
* 修改记录:
|
||||
* 2026-09-22 王建锋 初版:时基 + 统计/报告 + Phase 12(L1/L2 SPI 时延基线)
|
||||
******************************************************************************/
|
||||
|
||||
#ifndef NET_LATENCY_TEST_H
|
||||
#define NET_LATENCY_TEST_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/*
|
||||
* === 链路时延测试阶段一览(见 链路延迟测试说明.md §2.2) ===
|
||||
* Phase 12 [硬件层] SPI 字节命令 / DMA 时延基线(L1/L2,无需 PC) —— M1(已完成)
|
||||
* Phase 13 [NET 层] 芯片 + 队列时延(L3~L7/L10,需 PC peer) —— M2(已实现)
|
||||
* Phase 14 [NET 层] 生产路径时延(L8~L11 + 模拟器 A/B,需 PC peer)—— M3(规划)
|
||||
*
|
||||
* 时基说明:DWT CYCCNT 为 32 位,168MHz 下回绕周期 25.17ms。
|
||||
* M1 各段(<5ms)与 M2 调用方侧测量(单次 <25ms)用单次读数差即可;
|
||||
* 跨 25ms 的段(L4/L8 整帧与重传尖峰)由 PC 端 peer 计时。
|
||||
*
|
||||
* Phase 13 协议(服务器驱动:MCU 发操作字节,peer 执行;详见说明文档 §2.5):
|
||||
* HELLO = 5A A5 58 01(peer → MCU,4 字节)
|
||||
* 操作字节(MCU → peer):0x01=L5 recv flood / 0x02=L5 send flood /
|
||||
* 0x03=L6 单请求 / 0x04=L7 LCDMEM 全帧 / 0x05=L3 send flood / 0xFF=完成
|
||||
* Phase 13 需**独立运行**(同 Phase 4 约定):与其他 Phase 共用 8080 端口
|
||||
* 的 socket,混跑会污染帧测量。
|
||||
*/
|
||||
|
||||
/* verdict 三态(lat_stat_report 输出,调用方据此汇总) */
|
||||
#define LAT_VERDICT_OK 0
|
||||
#define LAT_VERDICT_WARN 1
|
||||
#define LAT_VERDICT_FAIL 2
|
||||
|
||||
/*
|
||||
* 采样保存上限:静态数组容量(自研代码禁用 C 库堆,见 AGENTS.md)。
|
||||
* n 可超过该值(min/max/spikes 仍对全量累计;med/p95 按已保存样本计算)。
|
||||
*/
|
||||
#define LAT_MAX_SAMPLES 128
|
||||
|
||||
/*
|
||||
* 采样统计容器。
|
||||
* n - 总样本数(可 > LAT_MAX_SAMPLES)
|
||||
* mn/mx - 全量 min/max(us)
|
||||
* med - 中位数(us,lat_stat_finish 后有效)
|
||||
* p95 - 95 分位(us,lat_stat_finish 后有效)
|
||||
* spikes - 达到尖峰阈值的样本数(spike_th_us=0 表示本段不统计)
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t n;
|
||||
uint32_t mn;
|
||||
uint32_t med;
|
||||
uint32_t p95;
|
||||
uint32_t mx;
|
||||
uint16_t spikes;
|
||||
} lat_stats_t;
|
||||
|
||||
/*
|
||||
* 函数功能:使能 DWT CYCCNT 周期计数器时基(幂等)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:任何测量前调用一次即可;仅测试构建路径会执行(产品构建不调用)
|
||||
* 函数说明:CoreDebug->DEMCR(TRCENA) + DWT->CYCCNT 清零 + CYCCNTENA 置位
|
||||
*/
|
||||
void lat_time_init(void);
|
||||
|
||||
/*
|
||||
* 函数功能:重置统计容器(mn 置 0xFFFFFFFF,空集可辨识)
|
||||
* 入口参数:p - 统计容器
|
||||
*/
|
||||
void lat_stat_reset(lat_stats_t *p);
|
||||
|
||||
/*
|
||||
* 函数功能:追加一个时延样本(微秒)
|
||||
* 入口参数:p - 统计容器
|
||||
* us - 本样本测量时长(微秒)
|
||||
* spike_th_us - 尖峰阈值(us);0 表示本段不统计尖峰
|
||||
*/
|
||||
void lat_stat_add(lat_stats_t *p, uint32_t us, uint32_t spike_th_us);
|
||||
|
||||
/*
|
||||
* 函数功能:采样结束后排序计算 med/p95(内部静态工作区,零堆)
|
||||
* 入口参数:p - 统计容器(lat_stat_add 全部追加完成后调用)
|
||||
*/
|
||||
void lat_stat_finish(lat_stats_t *p);
|
||||
|
||||
/*
|
||||
* 函数功能:输出 [LAT] 报告行并给出本段 verdict
|
||||
* 入口参数:p_seg - 段名(纯 ASCII 标识,如 "spi_dma_1024")
|
||||
* p - 统计容器(须先 lat_stat_finish)
|
||||
* warn_med_us - WARN 阈值:med 超过 → WARN(重锁阈值提示,计 PASS)
|
||||
* fail_med_us - FAIL 阈值:med 超过 → FAIL
|
||||
* max_spikes - 尖峰数上限:spikes 超过 → FAIL
|
||||
* p_verdict - 输出:本段 verdict(LAT_VERDICT_*)
|
||||
* 返回值:无
|
||||
* 函数说明:报告行格式见 链路延迟测试说明.md §2.4;字符串字面量纯 ASCII
|
||||
*/
|
||||
void lat_stat_report(const char *p_seg, const lat_stats_t *p,
|
||||
uint32_t warn_med_us, uint32_t fail_med_us,
|
||||
uint16_t max_spikes, int *p_verdict);
|
||||
|
||||
/*
|
||||
* 函数功能:Phase 12 入口(硬件层:SPI 字节命令 / DMA 时延基线,L1/L2,无需 PC)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:由 ch395f_test_run() 硬件层分支调用(net_stack_bring_up 完成后
|
||||
* testTask 独占 CH395F,无 SPI 并发);仅台架执行
|
||||
* 函数说明:内部先做 check_exist 存活自检,失败即判 FAIL 并跳过;
|
||||
* 套件未选中本 Phase 时函数体为空(产品构建可编译、不执行)
|
||||
*/
|
||||
void net_latency_phase12_run(void);
|
||||
|
||||
/*
|
||||
* 函数功能:Phase 13 入口(NET 层:芯片 + 队列时延 L3~L7/L10,需 PC peer)
|
||||
* 入口参数:sockfd - 测试监听 socket(连接后的数据通道 fd,8080 端口)
|
||||
* 返回值:无
|
||||
* 限定条件:由 net_layer_test_main() 监听循环每 ~10ms 调用一次(事件驱动
|
||||
* 状态机);需 test/latency_peer.py --mode net 配合;须独立运行
|
||||
* 函数说明:服务器驱动协议(见文件头协议注释);会话完成后 s_done 置位,
|
||||
* 后续调用为空操作;套件未选中本 Phase 时函数体为空
|
||||
*/
|
||||
void net_latency_phase13_run(int32_t sockfd);
|
||||
|
||||
/*
|
||||
* 函数功能:Phase 13 会话是否活动(HELLO 等待/进行中/abort 重连等待)
|
||||
* 返回值:1 - 活动(监听循环应跳过 net_accept,由本模块轮询接管)
|
||||
* 0 - 非活动(正常完成/终态,恢复常规 accept 等待)
|
||||
* 限定条件:Phase 13 未启用时恒返回 0
|
||||
*/
|
||||
int net_latency_phase13_busy(void);
|
||||
|
||||
/*
|
||||
* 函数功能:Phase 14 入口(NET 层:生产路径时延 L8~L11,真实 uiTask + :7003)
|
||||
* 入口参数:无(本 Phase 不占用测试 socket,测量生产 socket 的服务端行为)
|
||||
* 返回值:无
|
||||
* 限定条件:由 net_layer_test_main() 循环每 ~10ms 调用一次;需
|
||||
* test/latency_peer.py --mode remo --port 7003 配合;须单独使能
|
||||
* (与 Phase 13 同启时两者都会跑,M3 台架建议只开 Phase 14)
|
||||
* 函数说明:状态机顺序 = ui_loop(本地节拍窗口)→ prod_cycle_mcu(等 peer
|
||||
* 发满 N 帧 LCDMEM)→ relisten(等 peer 断开重连)→ DONE;
|
||||
* 各窗口通过 TEST_LATENCY_PROBE 采集(uiTask/netTask 侧打点);
|
||||
* 未定义 TEST_LATENCY_PROBE 时只打印跳过说明;套件未选中时函数体为空
|
||||
*/
|
||||
void net_latency_phase14_run(void);
|
||||
|
||||
/*
|
||||
* 函数功能:Phase 14 是否活动(活动期监听循环应让位,保证 10ms 轮询节拍)
|
||||
* 返回值:1 - 活动;0 - 非活动/未启用
|
||||
*/
|
||||
int net_latency_phase14_busy(void);
|
||||
|
||||
/*
|
||||
* === TEST_LATENCY_PROBE 探针接口(M2/M3)===
|
||||
*
|
||||
* 用途:netTask 上下文(net_socket.c 发送分块循环)向 testTask 报告的
|
||||
* 段内打点。单写者(netTask)/单读者(testTask),32 位变量原子
|
||||
* 读写,无锁。仅在 TEST_LATENCY_PROBE 定义(测试构建)时编译。
|
||||
*/
|
||||
|
||||
/* 当前是否处于采样窗口(netTask 侧高频查询,须极廉价) */
|
||||
uint8_t lat_probe_armed(void);
|
||||
|
||||
/* testTask 侧开一个采样窗口:seg 段名、max_n 样本上限、spike_th_us 尖峰阈值 */
|
||||
void lat_probe_arm(const char *seg, uint32_t max_n, uint32_t spike_th_us);
|
||||
|
||||
/* netTask 侧追加一个样本(us);未武装或已满 → 无操作 */
|
||||
void lat_probe_add(uint32_t us);
|
||||
|
||||
/*
|
||||
* 函数功能:**按段名**追加样本(多打点站点共存时唯一安全的写法)
|
||||
* 入口参数:seg - 本打点站点所属段名(须与 lat_probe_arm 的 seg 完全一致)
|
||||
* us - 样本值(us)
|
||||
* 返回值:无
|
||||
* 函数说明:探针是**全局单窗口**:窗口武装成 A 段时,B 段站点的 lat_probe_add
|
||||
* 会把 B 的样本灌进 A 的窗口(2026-09-22 run17 实证:Phase 14 三段
|
||||
* 全被 uiTask 循环样本填满,prod/relisten 都读出 ~11ms 的 ui_loop 值)。
|
||||
* 只有段名匹配才落样本 → 各站点互不污染。多站点构建**一律**用本接口。
|
||||
*/
|
||||
void lat_probe_add_seg(const char *seg, uint32_t us);
|
||||
|
||||
/* 已采集样本数(32 位原子读) */
|
||||
uint32_t lat_probe_count(void);
|
||||
|
||||
/* 关闭采样窗口(此后的 lat_probe_add 无操作;在飞样本最多超 1 个) */
|
||||
void lat_probe_disarm(void);
|
||||
|
||||
/* testTask 侧取窗口统计(disarm 后调用;内部 lat_stat_finish 已含 med/p95) */
|
||||
const lat_stats_t *lat_probe_stats(void);
|
||||
|
||||
/*
|
||||
* 函数功能:DWT CYCCNT 读数 / 差值换算(供 net_socket.c 探针使用)
|
||||
* 入口参数:c_start - 起始读数(lat_cycles_now 返回值)
|
||||
* 返回值:当前读数(us 计数域)/ 自 c_start 起的微秒数(<25.17ms 有效)
|
||||
*/
|
||||
uint32_t lat_cycles_now(void);
|
||||
uint32_t lat_cycles_diff_us(uint32_t c_start);
|
||||
|
||||
/* ---- M3 探针计时标记(可跨 >25ms 区间的安全计时) ----
|
||||
* DWT CYCCNT 为 32 位 @168MHz → 单次差值超过 **25.17ms 即回绕**,会把 30ms 读成
|
||||
* 4.8ms 这类"静默错误小值"(M1 各段均 <25ms 时无此问题,M3 的压力档/重监听会超)。
|
||||
* lat_mark/lat_mark_us 同时记 DWT 与 HAL tick,跨度 ≥20ms 时自动改用 tick 兜底
|
||||
* (分辨率降为 1ms,量级正确);<20ms 用 DWT(亚微秒精度)。 */
|
||||
typedef struct {
|
||||
uint32_t cyc; /* DWT->CYCCNT 起始值 */
|
||||
uint32_t tick; /* HAL_GetTick() 起始值 */
|
||||
} lat_tmark_t;
|
||||
|
||||
void lat_mark(lat_tmark_t *p);
|
||||
uint32_t lat_mark_us(const lat_tmark_t *p);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NET_LATENCY_TEST_H */
|
||||
+21
-1
@@ -16,6 +16,7 @@
|
||||
#include "ch395f_test.h"
|
||||
#include "ch395f_test_task.h"
|
||||
#include "net_test_task.h"
|
||||
#include "net_latency_test.h"
|
||||
|
||||
#define DBG_TAG "[NET_TEST]"
|
||||
#include "dbg_log.h"
|
||||
@@ -867,7 +868,14 @@ void net_layer_test_main(void)
|
||||
* 断开后自动重新 listen。
|
||||
*/
|
||||
for (;;) {
|
||||
if (!phase_active) {
|
||||
if (!phase_active
|
||||
#ifdef ENABLE_PHASE13_TESTS
|
||||
&& net_latency_phase13_busy() == 0
|
||||
#endif
|
||||
#ifdef ENABLE_PHASE14_TESTS
|
||||
&& net_latency_phase14_busy() == 0
|
||||
#endif
|
||||
) {
|
||||
int32_t acc = net_accept(sockfd, (struct net_sockaddr *)&client_addr, &addrlen);
|
||||
if (acc < 0) {
|
||||
if (accept_retry % 100 == 0) {
|
||||
@@ -907,6 +915,18 @@ void net_layer_test_main(void)
|
||||
#ifdef ENABLE_PHASE11_TESTS
|
||||
phase11_run(sockfd);
|
||||
#endif
|
||||
#ifdef ENABLE_PHASE13_TESTS
|
||||
/* Phase 13(链路时延 L3~L7/L10,M2):独立运行,勿与其他 Phase 混选;
|
||||
* 注意此处**不**重置 phase_active —— 保持 10ms 节拍(时延测量对
|
||||
* 循环周期敏感,Phase 8 式 accept 重入的 100ms 退避会污染样本) */
|
||||
net_latency_phase13_run(sockfd);
|
||||
#endif
|
||||
#ifdef ENABLE_PHASE14_TESTS
|
||||
/* Phase 14(生产路径时延 L8~L11,M3):不占测试 socket —— 测量真实
|
||||
* uiTask + ui_net_sink(:7003) 生产路径,由 peer --mode remo 驱动;
|
||||
* 本调用只是探针窗口状态机(arm/disarm/report),同样保持 10ms 节拍 */
|
||||
net_latency_phase14_run();
|
||||
#endif
|
||||
|
||||
/*
|
||||
* 单连接模式:连接断开由 net 层自动重监听(auto_relisten),
|
||||
|
||||
+17
-2
@@ -22,8 +22,13 @@ extern "C" {
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* === 选择当前构建的测试套件(三选一,或都注释 = 正常产品构建)=== */
|
||||
//#define TEST_SUITE_CH395F
|
||||
/* === 选择当前构建的测试套件(三选一,或都注释 = 正常产品构建)===
|
||||
* 2026-09-22:M2(链路时延 Phase 13,NET 层,需 PC peer)交付 ——
|
||||
* 当前启用 CH395F 套件 + Phase 13(独立运行,勿与其他 Phase 混选)。
|
||||
* 执行:先起 test/latency_peer.py --mode net --port 8080,再 @test 烧录。
|
||||
* 恢复产品构建 = 注释掉下面两行(TEST_SUITE_CH395F 与 ch395f_test.h 的
|
||||
* ENABLE_PHASE13_TESTS)。详见 docs/测试说明/链路延迟测试说明.md §3.2。 */
|
||||
#define TEST_SUITE_CH395F
|
||||
//#define TEST_SUITE_STORAGE
|
||||
//#define TEST_SUITE_GD5F
|
||||
//#define TEST_SUITE_RTC
|
||||
@@ -91,6 +96,16 @@ extern "C" {
|
||||
#define TEST_SUITE_NAME "NONE"
|
||||
#endif
|
||||
|
||||
/*
|
||||
* 链路时延探针(M2/M3):在 net_socket.c 发送分块循环内定义 L3(chunk_wait)
|
||||
* 测量钩子(netTask 上下文);产品构建未定义 → 钩子代码编译消失(零开销)。
|
||||
* 依赖 ch395f_test.h 的 Phase 宏(上方 TEST_SUITE_CH395F 块内已包含)。
|
||||
*/
|
||||
#if defined(TEST_SUITE_CH395F) && \
|
||||
(defined(ENABLE_PHASE13_TESTS) || defined(ENABLE_PHASE14_TESTS))
|
||||
#define TEST_LATENCY_PROBE 1
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user