Files
STM32F4-Base/test/board/net_latency_test.c
wandering1 f754d59218 docs(测试说明): 按用途分六个子目录并同步全库引用
分层:流程与规范/ 单元测试/ 板级测试/ 专项验证/ 测试工具/ 测试数据/,
顶层只留新增的目录导航 README.md(归层判据 + 硬约束 + 本次处置记录)。

- 搬迁 28 个路径:测试数据三个目录 58 个文件为纯改名(R100,内容逐字节不变)
- 全库引用同步 124 个文件:AGENTS / docs 索引 / skills / 门禁注释 / scripts
- 门禁面:scripts/check_test_traceability.py 的 glob 改 rglob
  (分层后不递归扫描=追溯门禁直接瞎掉)
- scripts/rename_ref.py:收尾的 ℹ 提示在 GBK 代码页抛 UnicodeEncodeError,
  让进程以退出码 1 收场而改写其实已落盘(RC 与真实结果相反);改为把
  stdout/stderr reconfigure 成 UTF-8,与 check_doc_links.py 等同法
- docs/文档规范.md §1.1 补「子目录文件过多时再分一层」;§3.6 补 rename_ref
  搬整目录的两个实测坑:① 会改写目录内部文件的内容 ② 内部文件的 ../ 出链
  不补层级(本次 23 个被误改的证据文件已按 HEAD 原始字节逐字节还原)
- docs/README.md §6 改为与六个子目录一一对应,并补登原先漏登记的
  「启动日志原子性复验流程」一行
- .cubemx_survival_baseline.json:App/util/task_watchdog.c 的注释路径也随分层更新,
  该文件是存活审计的 watchdog-module 漂移锚点,按脚本自己的提示跑 --write 重建基线
  (实测重建后仅 watchdog-module 一条哈希变化)
- 铁律 4 例外(本次在任务树里改了单写者文件):AGENTS.md 与
  .pre-commit-config.yaml 只被 rename_ref 改写路径字面量(后者全在注释行),
  任务表块与钩子声明一字未动

验证:check_doc_links.py --sections-strict 覆盖全部 88 个改动 .md 全绿;
check_test_traceability.py 输出与主树逐行一致;196 个文本文件无 BOM、无新增
CR、无 U+FFFD、无行尾空白;陈旧引用面由主树 25 处降到 6 处(无新增断链)。
2026-10-30 07:16:07 +08:00

1313 lines
51 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/******************************************************************************
* 模块名称:CH395F 链路延迟测试(实现)
* 模块功能:DWT CYCCNT 微秒时基 + 采样统计(min/med/p95/max/尖峰)+ [LAT]
* 机器可读报告与 verdict;Phase 12(M1):SPI 字节命令 / DMA 时延
* 基线(SPI_CMD/SPI_DMA,硬件层模式,无需 PC)。分段定义与判定基线见
* docs/测试说明/专项验证/链路延迟测试说明.md。
* 适用平台:STM32F4 系列(CH395F)
* 作者:王建锋
* 创建日期:2026-09-22
* 修改记录:
* 2026-09-22 王建锋 初版:时基 + 统计/报告 + Phase 12(SPI_CMD/SPI_DMA)
*
* 构建形态:
* - 产品构建:三个 Phase 宏均未定义 → 本文件除 include 外全部不参与编译;
* - 测试构建(TEST_SUITE_CH395F):按所选 Phase 编译对应代码。
* 所有字符串字面量纯 ASCII([LAT] 报告行),中文仅注释。
******************************************************************************/
#include <string.h>
#include "net_latency_test.h"
#include "ch395f_test.h" /* TEST_CHECK / TEST_REPORT / g_test_stats / Phase 宏 */
#include "ch395f.h" /* 驱动公开 API(SPI_CMD 字节命令 / SPI_DMA DMA) */
#include "net_socket.h" /* net_send / net_recv / NET_MSG_DONTWAIT(Phase 13) */
#include "test_config.h" /* TEST_LATENCY_PROBE(探针编译开关) */
#include "main.h" /* CoreDebug / DWT / SystemCoreClock / HAL_GetTick */
#define DBG_TAG "[LAT_TEST]"
#include "dbg_log.h"
#if defined(ENABLE_PHASE12_TESTS) || \
defined(ENABLE_PHASE13_TESTS) || \
defined(ENABLE_PHASE14_TESTS)
/* ============================================================
* DWT CYCCNT 微秒时基(测试专用)
*
* 工程内首个 DWT 用户(sys_clock 为纯 HAL tick,无 us 能力)。
* 32 位计数 168MHz 下回绕周期 25.17ms:M1 各段(<5ms)与 M2
* 调用方侧测量(单次 <25ms)用单次读数差即可;跨 25ms 的段
* (TO_PC/PROD_CYCLE 整帧与重传尖峰)由 PC 端 peer 计时(说明文档 §2.3)。
* 读取 DWT->CYCCNT 无共享状态,任意任务上下文安全。
* ============================================================ */
static int s_dwt_ready = 0;
void lat_time_init(void)
{
if (s_dwt_ready != 0) {
return;
}
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; /* 使能 DWT 的前置条件 */
DWT->CYCCNT = 0U;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
s_dwt_ready = 1;
}
#ifdef ENABLE_PHASE12_TESTS
/* Phase 12 专用静态助手(仅该阶段引用;守卫防止单阶段构建的 #177-D) */
static uint32_t lat_cycles(void)
{
return DWT->CYCCNT;
}
#endif /* ENABLE_PHASE12_TESTS */
static uint32_t lat_us_from_cycles(uint32_t cycles)
{
/* 换算用 SystemCoreClock(不写死频率),64 位中间量防溢出 */
return (uint32_t)(((uint64_t)cycles * 1000000UL) / (uint64_t)SystemCoreClock);
}
#ifdef ENABLE_PHASE12_TESTS
static uint32_t lat_elapsed_us(uint32_t c_start)
{
/* 有效区间:单次测量 < 25.17ms(32 位回绕周期),M1 各段均满足 */
return lat_us_from_cycles(DWT->CYCCNT - c_start);
}
#endif /* ENABLE_PHASE12_TESTS */
/* 公开读数接口(net_socket.c 的 CHUNK_WAIT 探针经此访问 DWT,避免直接碰寄存器) */
uint32_t lat_cycles_now(void)
{
return DWT->CYCCNT;
}
uint32_t lat_cycles_diff_us(uint32_t c_start)
{
return lat_us_from_cycles(DWT->CYCCNT - c_start);
}
/* M3 探针计时标记:DWT(亚微秒) + tick(>25ms 回绕兜底),见头文件说明 */
void lat_mark(lat_tmark_t *p)
{
p->cyc = DWT->CYCCNT;
p->tick = HAL_GetTick();
}
uint32_t lat_mark_us(const lat_tmark_t *p)
{
uint32_t dt_ms = HAL_GetTick() - p->tick;
/* 跨度 ≥20ms:DWT 可能已回绕(25.17ms)→ 用 tick 兜底(1ms 分辨率) */
if (dt_ms >= 20U) {
return dt_ms * 1000U;
}
return lat_us_from_cycles(DWT->CYCCNT - p->cyc);
}
/* ============================================================
* 采样统计(静态数组,零 C 库堆)
* ============================================================ */
static uint32_t s_lat_samples[LAT_MAX_SAMPLES]; /* 原始样本(按段复用) */
static uint32_t s_lat_sort[LAT_MAX_SAMPLES]; /* 排序工作区(lat_stat_finish) */
void lat_stat_reset(lat_stats_t *p)
{
p->n = 0U;
p->spikes = 0U;
p->mn = 0xFFFFFFFFU; /* 空集哨兵 */
p->med = 0U;
p->p95 = 0U;
p->mx = 0U;
}
void lat_stat_add(lat_stats_t *p, uint32_t us, uint32_t spike_th_us)
{
if (p->n < (uint16_t)LAT_MAX_SAMPLES) {
s_lat_samples[p->n] = us;
}
p->n++;
if (us < p->mn) {
p->mn = us;
}
if (us > p->mx) {
p->mx = us;
}
if ((spike_th_us != 0U) && (us >= spike_th_us)) {
p->spikes++;
}
}
void lat_stat_finish(lat_stats_t *p)
{
uint32_t n = (p->n < (uint32_t)LAT_MAX_SAMPLES) ? (uint32_t)p->n
: (uint32_t)LAT_MAX_SAMPLES;
uint32_t i, j;
uint32_t idx;
for (i = 0U; i < n; i++) {
s_lat_sort[i] = s_lat_samples[i];
}
/* 插入排序(n<=128,O(n^2) 可接受;零堆) */
for (i = 1U; i < n; i++) {
uint32_t key = s_lat_sort[i];
j = i;
while ((j > 0U) && (s_lat_sort[j - 1U] > key)) {
s_lat_sort[j] = s_lat_sort[j - 1U];
j--;
}
s_lat_sort[j] = key;
}
p->med = (n > 0U) ? s_lat_sort[n / 2U] : 0U;
idx = (n * 95U) / 100U;
if (idx >= n) {
idx = (n > 0U) ? (n - 1U) : 0U;
}
p->p95 = (n > 0U) ? s_lat_sort[idx] : 0U;
}
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)
{
int v = LAT_VERDICT_OK;
DBG_INFO("[LAT] seg=%s n=%u min=%lu med=%lu p95=%lu max=%lu us spikes=%u",
p_seg, (unsigned)p->n,
(unsigned long)p->mn, (unsigned long)p->med,
(unsigned long)p->p95, (unsigned long)p->mx,
(unsigned)p->spikes);
if (p->n == 0U) {
v = LAT_VERDICT_FAIL; /* 空集 = 测量未发生,判 FAIL */
} else if (p->med > fail_med_us) {
v = LAT_VERDICT_FAIL;
} else if (p->spikes > max_spikes) {
v = LAT_VERDICT_FAIL;
} else if (p->med > warn_med_us) {
v = LAT_VERDICT_WARN; /* 超 warn 未超 fail:计 PASS,提示重锁阈值 */
}
DBG_INFO("[LAT] verdict=%s seg=%s (med=%lums warn<=%lu crit<=%lu spikes=%u/%u)",
(v == LAT_VERDICT_OK) ? "OK" : ((v == LAT_VERDICT_WARN) ? "WARN" : "FAIL"),
p_seg, (unsigned long)p->med,
(unsigned long)warn_med_us, (unsigned long)fail_med_us,
(unsigned)p->spikes, (unsigned)max_spikes);
if (p_verdict != NULL) {
*p_verdict = v;
}
}
/* ============================================================
* TEST_LATENCY_PROBE 探针(M2/M3)
*
* netTask 上下文(net_socket.c 发送分块循环)向 testTask 报告段内
* 打点(CHUNK_WAIT chunk_wait 等调用方侧无法测量的段)。
* 并发模型:单写者(netTask)/单读者(testTask);
* - s_probe_n 为 32 位(Cortex-M4 单字读写原子,无锁)
* - 样本写入 → n++ 的顺序保证读者看到一致的 (n, samples[0..n))
* - disarm 后 netTask 最多再补 1 个在飞样本(统计已定稿,忽略)
* 探针用独立样本/排序数组,不与 lat_stat_* 的工作区共享。
* ============================================================ */
#if defined(TEST_LATENCY_PROBE)
static char s_probe_seg[24];
static uint32_t s_probe_max;
static volatile uint32_t s_probe_n; /* 32 位原子:netTask 写 / testTask 读 */
static uint32_t s_probe_spike_th;
static uint32_t s_probe_spikes;
static uint32_t s_probe_mn;
static uint32_t s_probe_mx;
static uint8_t s_probe_armed;
static uint32_t s_probe_samples[LAT_MAX_SAMPLES];
static uint32_t s_probe_sort[LAT_MAX_SAMPLES];
static lat_stats_t s_probe_stats;
uint8_t lat_probe_armed(void)
{
return s_probe_armed;
}
void lat_probe_arm(const char *seg, uint32_t max_n, uint32_t spike_th_us)
{
if (max_n > (uint32_t)LAT_MAX_SAMPLES) {
max_n = (uint32_t)LAT_MAX_SAMPLES;
}
s_probe_max = max_n;
s_probe_n = 0U;
s_probe_spikes = 0U;
s_probe_mn = 0xFFFFFFFFU;
s_probe_mx = 0U;
s_probe_spike_th = spike_th_us;
s_probe_armed = 0U; /* 最后置位:arm 完成前 netTask 不写入 */
/* 有界拷贝:先取源串实际长度(全部调用点均为字符串字面量),再夹到目标容量
* 以内。原实现"按目标容量逐字节读源 + 遇 '\0' break"虽对字面量安全,但
* cppcheck 无法证明源串提前终止 → 报 arrayIndexOutOfBounds(S5,2026-10-03);
* 改为 strlen+memcpy 后既有界又可证。 */
{
size_t n = strlen(seg);
if (n > sizeof(s_probe_seg) - 1U) {
n = sizeof(s_probe_seg) - 1U;
}
memcpy(s_probe_seg, seg, n);
s_probe_seg[n] = '\0';
}
s_probe_armed = 1U;
}
void lat_probe_add(uint32_t us)
{
if (s_probe_armed == 0U) {
return;
}
if (s_probe_n >= s_probe_max) {
return;
}
s_probe_samples[s_probe_n] = us;
if (us < s_probe_mn) {
s_probe_mn = us;
}
if (us > s_probe_mx) {
s_probe_mx = us;
}
if ((s_probe_spike_th != 0U) && (us >= s_probe_spike_th)) {
s_probe_spikes++;
}
s_probe_n++;
}
uint32_t lat_probe_count(void)
{
return s_probe_n;
}
/* 段名比较(避免为一处探针引入 <string.h> 依赖) */
static int lat_probe_seg_is(const char *seg)
{
uint32_t i;
for (i = 0U; i < sizeof(s_probe_seg); i++) {
if (s_probe_seg[i] != seg[i]) {
return 0;
}
if (seg[i] == '\0') {
return 1;
}
}
return 0;
}
void lat_probe_add_seg(const char *seg, uint32_t us)
{
if (s_probe_armed == 0U) {
return;
}
if (lat_probe_seg_is(seg) == 0) {
return; /* 当前窗口不是本段 → 丢弃(防跨段污染) */
}
lat_probe_add(us);
}
void lat_probe_disarm(void)
{
uint32_t n = s_probe_n;
uint32_t i, j;
uint32_t idx;
s_probe_armed = 0U;
for (i = 0U; i < n; i++) {
s_probe_sort[i] = s_probe_samples[i];
}
for (i = 1U; i < n; i++) {
uint32_t key = s_probe_sort[i];
j = i;
while ((j > 0U) && (s_probe_sort[j - 1U] > key)) {
s_probe_sort[j] = s_probe_sort[j - 1U];
j--;
}
s_probe_sort[j] = key;
}
s_probe_stats.n = (uint16_t)n;
s_probe_stats.mn = (n > 0U) ? s_probe_mn : 0U;
s_probe_stats.mx = s_probe_mx;
s_probe_stats.med = (n > 0U) ? s_probe_sort[n / 2U] : 0U;
idx = (n * 95U) / 100U;
if (idx >= n) {
idx = (n > 0U) ? (n - 1U) : 0U;
}
s_probe_stats.p95 = (n > 0U) ? s_probe_sort[idx] : 0U;
s_probe_stats.spikes = (uint16_t)s_probe_spikes;
}
const lat_stats_t *lat_probe_stats(void)
{
return &s_probe_stats;
}
#endif /* TEST_LATENCY_PROBE */
/* ============================================================
* Phase 12 — SPI 时延基线(SPI_CMD/SPI_DMA,硬件层模式,无需 PC)
*
* 测量对象(见 链路延迟测试说明.md §3):
* SPI_CMD SPI 字节命令 —— 3 个代表性命令的完整 SPI 事务时长
* (公开函数内部含 spi_begin/spi_end,即一次完整事务)
* SPI_DMA SPI DMA —— ch395f_read_recv_buf(N) 完整 DMA 事务
* (读方向:MCU 主发时钟,芯片侧输出内容不影响传输时长;
* 硬件层模式 socket 未打开,读方向不改芯片任何状态,
* 保持非侵入。写方向 DMA 机制相同(s_spi2_dma_tx_buf),
* 由 Phase 13(M2)发送分块握手覆盖)
*
* 前置:ch395f_test_run() 硬件层分支已完成 net_stack_bring_up()
* 并独占 CH395F(无 net_poll 并发 SPI 事务)。
* 初始阈值:静态推算值,台架首跑后重锁(说明文档 §4.1/§8)。
* ============================================================ */
#ifdef ENABLE_PHASE12_TESTS
#define LAT_P12_CMD_N 100U /* SPI_CMD 每命令样本数 */
#define LAT_P12_DMA_N 100U /* SPI_DMA 每长度档样本数 */
#define LAT_P12_DMA_LEN_MAX 2048U /* SPI_DMA 单发上限 = CH395F_SPI_DMA_MAX_PAYLOAD */
/* SPI_CMD 阈值(us)—— 2026-09-22 台架首跑后重锁(实测 med 10~15us;
* warn ≈ 2×med,crit ≈ 2×warn,见 链路延迟测试说明.md §6.1) */
#define LAT_P12_CMD_WARN_US 40U
#define LAT_P12_CMD_FAIL_US 100U
static const char *const a_p12_cmd_seg[3] = {
"spi_cmd_getver", /* GET_IC_VER:1 写 + 1 读 */
"spi_cmd_chkexist", /* CHECK_EXIST:1 写 + 1 读(回显取反自检) */
"spi_cmd_gint16", /* GET_GLOB_INT_STATUS:1 写 + 2 读(读即清) */
};
static const uint16_t a_p12_dma_len[4] = { 4U, 64U, 1024U, 2048U };
static const char *const a_p12_dma_seg[4] = {
"spi_dma_4", "spi_dma_64", "spi_dma_1024", "spi_dma_2048"
};
/* 2026-09-22 台架首跑后重锁(实测 med 20/114/1611/3208us;
* warn ≈ 2×med,crit ≈ 2×warn) */
static const uint32_t a_p12_dma_warn[4] = { 40U, 250U, 3200U, 6400U };
static const uint32_t a_p12_dma_fail[4] = { 100U, 600U, 6400U, 12800U };
#endif /* ENABLE_PHASE12_TESTS */
#ifdef ENABLE_PHASE12_TESTS
void net_latency_phase12_run(void)
{
lat_stats_t st;
static uint8_t s_dma_buf[LAT_P12_DMA_LEN_MAX]; /* DMA 读输出缓冲(静态,零堆) */
int v;
int v_all = LAT_VERDICT_OK;
uint32_t i;
uint32_t k;
/* Phase 12 为硬件层运行的最后阶段:重置全局测试统计不影响
* 前序阶段已打印的 TEST_REPORT,使 Phase 12 计数独立清晰 */
memset(&g_test_stats, 0U, sizeof(g_test_stats));
DBG_INFO("=== CH395F Phase 12 Tests (link latency baseline, SPI_CMD/SPI_DMA) ===");
lat_time_init();
DBG_INFO("[LAT] run_id=%lu phase=12", (unsigned long)HAL_GetTick());
/* 时基校准行:台架可用逻辑分析仪核对 us/cycle 换算 */
DBG_INFO("[LAT] cal core_clk=%lu us_per_1024cyc=%lu",
(unsigned long)SystemCoreClock,
(unsigned long)(((uint64_t)1024U * 1000000UL) / (uint64_t)SystemCoreClock));
/* ---- 前置:芯片存活(无芯片则无法测量,判 FAIL 跳过) ---- */
{
ch395f_status_t ret = ch395f_check_exist();
if (ret != CH395F_ERR_SUCCESS) {
DBG_ERROR("[LAT] chip not detected (0x%02X), Phase 12 skipped",
(uint8_t)ret);
TEST_CHECK(0, "P12 pre: ch395f_check_exist");
DBG_INFO("[LAT] summary verdict=FAIL");
DBG_INFO("[LAT] report complete");
TEST_REPORT("Phase 12");
return;
}
}
memset(s_dma_buf, 0U, sizeof(s_dma_buf));
/* ---- SPI_CMD:SPI 字节命令时延(3 个代表性命令 × 100) ---- */
for (i = 0U; i < 3U; i++) {
lat_stat_reset(&st);
for (k = 0U; k < LAT_P12_CMD_N; k++) {
uint32_t c0 = lat_cycles();
switch (i) {
case 0U:
(void)ch395f_get_version();
break;
case 1U:
(void)ch395f_check_exist();
break;
default:
/* 例外(同 Phase 1 全局中断状态用例):被测对象就是本 API 的 SPI 时延——硬件层
* 独占上下文、无并发消费者;除此之外测试代码禁读全局寄存器
* (读即清,唯一读者 = net_poll,唯一读者红线) */
(void)ch395f_get_glob_int_status_all();
break;
}
lat_stat_add(&st, lat_elapsed_us(c0), 0U);
}
lat_stat_finish(&st);
lat_stat_report(a_p12_cmd_seg[i], &st,
LAT_P12_CMD_WARN_US, LAT_P12_CMD_FAIL_US, 0U, &v);
if (v > v_all) {
v_all = v;
}
}
/* ---- SPI_DMA:SPI DMA 时延(4 档长度 × 100) ---- */
for (i = 0U; i < 4U; i++) {
lat_stat_reset(&st);
for (k = 0U; k < LAT_P12_DMA_N; k++) {
uint32_t c0 = lat_cycles();
(void)ch395f_read_recv_buf(0U, s_dma_buf, a_p12_dma_len[i]);
lat_stat_add(&st, lat_elapsed_us(c0), 0U);
}
lat_stat_finish(&st);
lat_stat_report(a_p12_dma_seg[i], &st,
a_p12_dma_warn[i], a_p12_dma_fail[i], 0U, &v);
if (v > v_all) {
v_all = v;
}
}
/* ---- 汇总 ---- */
{
const char *p_name = (v_all == LAT_VERDICT_OK) ? "OK"
: ((v_all == LAT_VERDICT_WARN) ? "WARN" : "FAIL");
DBG_INFO("[LAT] summary verdict=%s", p_name);
}
/* WARN 计 PASS(提示重锁阈值);FAIL 才使套件失败 */
TEST_CHECK(v_all != LAT_VERDICT_FAIL,
"P12 latency baseline (verdict=%s)",
(v_all == LAT_VERDICT_OK) ? "OK"
: ((v_all == LAT_VERDICT_WARN) ? "WARN" : "FAIL"));
DBG_INFO("[LAT] report complete");
TEST_REPORT("Phase 12");
}
#else /* ENABLE_PHASE12_TESTS */
void net_latency_phase12_run(void)
{
/* 本构建未选中 Phase 12:空实现(符号保留,行为不参与) */
}
#endif /* ENABLE_PHASE12_TESTS */
#ifdef ENABLE_PHASE13_TESTS
/* ====================================================================
* Phase 13 — 芯片 + 队列时延(CHUNK_WAIT~FRAME_RT/LOGTAX,需 PC peer,M2)
*
* 由 net_layer_test_main() 监听循环(8080 端口,~10ms 节拍)事件驱动。
* 服务器驱动协议:MCU 发操作字节,peer(test/host/latency_peer.py --mode net)
* 执行对应流量;段完成后 MCU 打 [LAT] 报告行并推进下一操作。
*
* 段定义(说明文档 §1):
* QUEUE_RT queue_rt_recv / queue_rt_send —— 调用方视角 net_recv/net_send
* 全程(入队 + netTask 调度 + 处理 + 通知),静默基线档;
* 压力档 = peer --ftp-stress(lftpd 阻塞读占住 netTask → 头阻塞复现)
* ECHO_RT req2first_mcu —— 请求收齐 → 应答发送完成(MCU 侧,含队列+1 chunk)
* FRAME_RT fullframe_mcu —— 9B LCDMEM 请求 → 3209B 全帧发送完成
* (netTask 内 4 次分块停止等待,= 生产路径发送段)
* CHUNK_WAIT chunk_wait —— netTask 内"写发送缓冲 → SENBUF_FREE"等待
* (调用方不可见,经 TEST_LATENCY_PROBE 探针,见 net_socket.c)
* LOGTAX logtax_line —— 一行 60 字符 printf(fputc 逐字节 @115200)
*
* 执行顺序(2026-09-22 run3 后重排):QUEUE_RT → CHUNK_WAIT → LOGTAX → ECHO_RT → FRAME_RT ——
* 失步敏感的小帧 RX 协议段(ECHO_RT/FRAME_RT)放最后,先锁住抗失步段的基线。
* 对端侧(TO_PC 线上传输/整帧到达/尖峰)由 peer JSON 报告,按主机时间戳关联。
*
* 阈值:初始值,台架首跑后重锁(说明文档 §4.2)。
* 独立运行:与其他 Phase 共用 8080 socket,混跑会污染帧测量。
* ==================================================================== */
#define LAT_P13_SAMPLES 10U /* ECHO_RT/FRAME_RT 每段样本数(2026-09-22 run3 实证:ECHO_RT 50 轮
* 小交换后芯片 TCP 失步 → TX 停滞 + 重复递交 9B 碎片
* → 降至 10,先取干净基线再量化失步边界) */
#define LAT_P13_QUEUE_RT_CHUNKS 20U /* QUEUE_RT flood 块数(2026-09-22 run1 实证:50×100B@5ms
* 达 20KB/s 饱和芯片接收缓冲 → 窗口关闭重传 →
* 重复递交 11884/5000B → 对端 RST;降至 1KB/s
* 静默档,见 §6.2 M2 基线) */
#define LAT_P13_QUEUE_RT_CHUNK_LEN 100U
#define LAT_P13_CHUNK_WAIT_SENDS 16U /* 每次 4KB = 4 chunk → 16×4 = 64 探针样本
* (run3 后从 32 减半:控制持续 TX 总量) */
#define LAT_P13_CHUNK_WAIT_SEND_LEN 4096U
#define LAT_P13_CHUNK_WAIT_SAMPLES (LAT_P13_CHUNK_WAIT_SENDS * 4U)
#define LAT_P13_ECHO_RT_SIZE 10U
#define LAT_P13_FRAME_RT_REQ_LEN 9U
#define LAT_P13_FRAME_LEN 3209U /* 4B 头 + 3204B 数据段(偏移u32+位图3200B) + 1B CRC */
#define LAT_P13_RECV_LEN 256U
#define LAT_P13_TIMEOUT_MS 240000U /* 整体会话超时(压力档含 30s 冻结) */
#define LAT_P13_SAMPLE_T_MS 35000U /* 单样本等待超时(覆盖一个 30s 冻结) */
/* 初始阈值(us):warn ≈ 2~3×预期,crit 覆盖硬失败(DMA/发送超时 1s 必超) */
#define LAT_P13_QUEUE_RT_WARN_US 10000U
#define LAT_P13_QUEUE_RT_CRIT_US 100000U
#define LAT_P13_REQ2FIRST_WARN_US 20000U
#define LAT_P13_REQ2FIRST_CRIT_US 200000U
#define LAT_P13_FULLFRAME_WARN_US 50000U
#define LAT_P13_FULLFRAME_CRIT_US 500000U
#define LAT_P13_CHUNK_WAIT_WARN_US 10000U
#define LAT_P13_CHUNK_WAIT_CRIT_US 500000U
#define LAT_P13_CHUNK_WAIT_SPIKE_US 500000U /* ≥500ms = 重传粒度指纹(D1) */
#define LAT_P13_LOGTAX_WARN_US 10000U
#define LAT_P13_LOGTAX_CRIT_US 25000U
/* 操作字节(MCU → peer) */
#define LAT_P13_OP_QUEUE_RT_RECV 0x01U
#define LAT_P13_OP_QUEUE_RT_SEND 0x02U
#define LAT_P13_OP_ECHO_RT 0x03U
#define LAT_P13_OP_FRAME_RT 0x04U
#define LAT_P13_OP_CHUNK_WAIT 0x05U
#define LAT_P13_OP_DONE 0xFFU
/* HELLO 魔术字(peer → MCU,4 字节) */
static const uint8_t a_p13_hello[4] = { 0x5AU, 0xA5U, 0x58U, 0x01U };
enum {
P13_ST_HELLO = 0,
P13_ST_QUEUE_RT_RECV,
P13_ST_QUEUE_RT_SEND,
P13_ST_ECHO_RT,
P13_ST_FRAME_RT,
P13_ST_CHUNK_WAIT,
P13_ST_LOGTAX,
P13_ST_DONE,
P13_ST_ABORT
};
static uint8_t s_p13_state = P13_ST_HELLO;
static uint8_t s_p13_done = 0U; /* 会话完成/中止:后续调用空操作 */
static uint8_t s_p13_hello_n = 0U;
static int s_p13_verdict_all = LAT_VERDICT_OK;
static int s_p13_seg_force_fail = 0; /* 当前段出现短写/错误 → 强制 FAIL */
static uint32_t s_p13_start_tick;
static uint32_t s_p13_step_tick; /* ECHO_RT/FRAME_RT 单样本等待计时 */
static uint32_t s_p13_stage;
static uint32_t s_p13_rx_bytes;
static uint16_t s_p13_l6rx_n;
static uint16_t s_p13_l7rx_n;
static lat_stats_t s_p13_st;
static uint8_t s_p13_rx[LAT_P13_RECV_LEN];
static uint8_t s_p13_tx[LAT_P13_CHUNK_WAIT_SEND_LEN]; /* QUEUE_RT/CHUNK_WAIT flood 载荷(模式预填) */
static uint8_t s_p13_frame[LAT_P13_FRAME_LEN]; /* FRAME_RT 应答帧(建一次) */
static uint8_t s_p13_l6rx[LAT_P13_ECHO_RT_SIZE];
static uint8_t s_p13_l7rx[LAT_P13_FRAME_RT_REQ_LEN];
/* 构造 FRAME_RT 应答帧(严格照生产 RemoDispBus 帧格式,ui_net_sink.c remo_send):
* [TAG=0xBB][CMD=0x03][LEN=3204 大端][DATA = 偏移u32(0) + 1bpp 位图 3200B][CRC]
* CRC = 仅对 DATA 段(3204B)逐字节异或;整帧 = 4 + 3204 + 1 = 3209B */
static void p13_build_frame(void)
{
uint32_t i;
uint8_t crc = 0U;
s_p13_frame[0] = 0xBBU;
s_p13_frame[1] = 0x03U;
s_p13_frame[2] = 0x0CU; /* 3204 = 0x0C84,大端高字节 */
s_p13_frame[3] = 0x84U;
s_p13_frame[4] = 0U; /* 数据段首 4B = 起始偏移,大端 0 */
s_p13_frame[5] = 0U;
s_p13_frame[6] = 0U;
s_p13_frame[7] = 0U;
for (i = 0U; i < 3200U; i++) {
s_p13_frame[8U + i] = (uint8_t)(i & 0xFFU);
}
for (i = 0U; i < 4U + 3200U; i++) {
crc ^= s_p13_frame[4U + i]; /* CRC 覆盖数据段(索引 4..3207) */
}
s_p13_frame[8U + 3200U] = crc; /* 索引 3208 = 帧尾 CRC */
}
static uint8_t p13_send_op(int32_t sockfd, uint8_t op)
{
int n = net_send(sockfd, &op, 1, 0);
if (n != 1) {
/* 协议操作字节发送失败 = 与 peer 的状态机同步永久丢失(run9 实证:
* op 静默丢失 → peer 空等 300s 超时 → 整段会话作废)→ 必须可见 */
DBG_INFO("[LAT] note: op 0x%02X send failed n=%d errno=%d",
op, n, net_get_errno());
}
return (n == 1) ? 1U : 0U;
}
/* 当前段收尾:报告 + verdict 累计 + 统计容器复位 */
static void p13_report_seg(const char *p_seg, uint32_t warn_us, uint32_t crit_us,
uint16_t max_spikes)
{
int v = LAT_VERDICT_OK;
if (s_p13_seg_force_fail != 0) {
v = LAT_VERDICT_FAIL;
}
lat_stat_finish(&s_p13_st);
lat_stat_report(p_seg, &s_p13_st, warn_us, crit_us, max_spikes, &v);
if (s_p13_seg_force_fail != 0) {
DBG_INFO("[LAT] note: %s has send error/short write -> FAIL", p_seg);
}
if (v > s_p13_verdict_all) {
s_p13_verdict_all = v;
}
s_p13_seg_force_fail = 0;
lat_stat_reset(&s_p13_st);
}
/* 会话中止:尽力报告进行中段的 partial 统计 + FAIL 汇总(标记链不断) */
static void p13_abort(const char *p_why)
{
const char *p_name = "FAIL";
if (s_p13_stage > 0U) {
lat_stat_finish(&s_p13_st);
DBG_INFO("[LAT] seg=partial n=%u min=%lu med=%lu p95=%lu max=%lu us (interrupted)",
(unsigned)s_p13_st.n, (unsigned long)s_p13_st.mn,
(unsigned long)s_p13_st.med, (unsigned long)s_p13_st.p95,
(unsigned long)s_p13_st.mx);
s_p13_verdict_all = LAT_VERDICT_FAIL;
}
#if defined(TEST_LATENCY_PROBE)
if (lat_probe_armed() != 0U) {
const lat_stats_t *p_ps;
lat_probe_disarm();
p_ps = lat_probe_stats();
DBG_INFO("[LAT] seg=chunk_wait n=%u min=%lu med=%lu p95=%lu max=%lu us spikes=%u (partial, interrupted)",
(unsigned)p_ps->n, (unsigned long)p_ps->mn,
(unsigned long)p_ps->med, (unsigned long)p_ps->p95,
(unsigned long)p_ps->mx, (unsigned)p_ps->spikes);
s_p13_verdict_all = LAT_VERDICT_FAIL;
}
#endif
DBG_ERROR("[LAT] phase13 abort: %s", p_why);
TEST_CHECK(0, "P13 abort: %s", p_why);
DBG_INFO("[LAT] summary verdict=%s", p_name);
DBG_INFO("[LAT] report complete");
TEST_REPORT("Phase 13");
s_p13_done = 1U;
s_p13_state = P13_ST_ABORT;
}
/* HELLO 确认后的公共入口(首连与重启共用):复位会话进度 → 发 op 0x01 进 QUEUE_RT */
static void p13_on_hello(int32_t sockfd)
{
s_p13_hello_n = 0U;
DBG_INFO("[LAT] peer session start");
lat_stat_reset(&s_p13_st);
s_p13_stage = 0U;
s_p13_rx_bytes = 0U;
s_p13_state = P13_ST_QUEUE_RT_RECV;
(void)p13_send_op(sockfd, LAT_P13_OP_QUEUE_RT_RECV);
}
/* 会话重启初始化(abort 后 peer 重连再 HELLO 时调用):
* 2026-09-22 run6 实证:芯片对空闲连接自行超时发 RST(MCU 网络栈慢于
* 芯片握手完成时,连接建立后 15~30s 内无流量即被 RST)→ peer 重连 +
* 本重启 = 闭环,避免一次慢启动毁掉整轮测试 */
static void p13_reinit(void)
{
#if defined(TEST_LATENCY_PROBE)
if (lat_probe_armed() != 0U) {
lat_probe_disarm();
}
#endif
s_p13_state = P13_ST_HELLO; /* 由 p13_on_hello 覆盖为 QUEUE_RT_RECV */
s_p13_stage = 0U;
s_p13_rx_bytes = 0U;
s_p13_l6rx_n = 0U;
s_p13_l7rx_n = 0U;
s_p13_step_tick = HAL_GetTick();
s_p13_start_tick = HAL_GetTick(); /* 整体 240s 预算按重启重新计时 */
s_p13_seg_force_fail = 0;
s_p13_verdict_all = LAT_VERDICT_OK;
s_p13_done = 0U;
s_p13_hello_n = 0U;
lat_stat_reset(&s_p13_st);
}
/* 会话是否活动(供 net_test_task 的监听循环判断:活动期跳过 net_accept,
* 连接由芯片 CONNECT 事件建立 + 本模块 HELLO/重连轮询接管,run8 实证:
* 10ms 一次 accept 与协议消息争抢 netTask 队列并产生大量陈旧通知噪声) */
int net_latency_phase13_busy(void)
{
#ifdef ENABLE_PHASE13_TESTS
return (s_p13_done == 0U || s_p13_state == (uint8_t)P13_ST_ABORT) ? 1 : 0;
#else
return 0;
#endif
}
void net_latency_phase13_run(int32_t sockfd)
{
#ifdef ENABLE_PHASE13_TESTS
int32_t n;
uint32_t i;
uint32_t c0;
uint32_t us;
if (s_p13_done != 0U) {
if (s_p13_state != P13_ST_ABORT) {
return; /* 正常完成/DONE:会话终态,不再处理 */
}
/* 冗余 API 面清理(docs/网络/网络自愈架构设计.md §7):原"进 abort 分状态手动重同步 + 10s 周期 relisten"
* 已删——三种去向全部由 net 层自动机制认领(静默回收滞留 ESTABLISHED
* → 对账⑤ RESYNC;CLOSED 失位 → 对账③④ 认领重建;LISTENING 静默丢
* 监听 → 形态③"芯片不在听"判据),手动 API net_resync_listen/net_relisten
* 已退役。此处只保留收包与 peer re-HELLO 探测。 */
n = net_recv(sockfd, s_p13_rx, (int)sizeof(s_p13_rx), NET_MSG_DONTWAIT);
if (n > 0) {
for (i = 0U; i + 4U <= (uint32_t)n; i++) {
if (s_p13_rx[i] == 0x5AU && s_p13_rx[i + 1U] == 0xA5U &&
s_p13_rx[i + 2U] == 0x58U && s_p13_rx[i + 3U] == 0x01U) {
p13_reinit();
DBG_INFO("[LAT] peer re-HELLO -> session restart");
p13_on_hello(sockfd); /* HELLO 已消耗 → 直接进 QUEUE_RT 并回 op */
break;
}
}
}
/* 重启等待总超时(abort 后再给 120s):无 peer 则转终态,停止轮询 */
if (s_p13_done != 0U &&
(HAL_GetTick() - s_p13_start_tick) >
(uint32_t)((uint32_t)LAT_P13_TIMEOUT_MS + 120000U)) {
s_p13_state = P13_ST_DONE;
}
return;
}
/* ---- 会话起点:时基/帧/载荷一次性准备 ---- */
if (s_p13_state == P13_ST_HELLO && s_p13_start_tick == 0U) {
s_p13_start_tick = HAL_GetTick();
lat_time_init();
p13_build_frame();
for (i = 0U; i < LAT_P13_CHUNK_WAIT_SEND_LEN; i++) {
s_p13_tx[i] = (uint8_t)(i & 0xFFU);
}
DBG_INFO("=== CH395F Phase 13 Tests (chip+queue latency, CHUNK_WAIT~FRAME_RT/LOGTAX) ===");
DBG_INFO("[LAT] run_id=%lu phase=13 cal core_clk=%lu us_per_1024cyc=%lu",
(unsigned long)HAL_GetTick(),
(unsigned long)SystemCoreClock,
(unsigned long)(((uint64_t)1024U * 1000000UL) / (uint64_t)SystemCoreClock));
DBG_INFO("[LAT] waiting for peer HELLO (port 8080, latency_peer.py --mode net)");
}
if ((HAL_GetTick() - s_p13_start_tick) > LAT_P13_TIMEOUT_MS) {
p13_abort("overall timeout");
return;
}
switch (s_p13_state) {
/* ---- HELLO:4 字节魔术字(容忍杂散字节:失配即从头对齐) ---- */
case P13_ST_HELLO: {
n = net_recv(sockfd, s_p13_rx, (int)sizeof(s_p13_rx), NET_MSG_DONTWAIT);
if (n == 0) {
p13_abort("peer connection closed (EOF)");
return;
}
if (n > 0) {
for (i = 0U; (int32_t)i < n; i++) {
uint8_t b = s_p13_rx[i];
if (s_p13_hello_n == 0U && b != a_p13_hello[0U]) {
continue;
}
if (b != a_p13_hello[s_p13_hello_n]) {
s_p13_hello_n = 0U;
continue;
}
s_p13_hello_n++;
if (s_p13_hello_n == 4U) {
p13_on_hello(sockfd);
}
}
}
/* n < 0: DONTWAIT 空轮询(WOULDBLOCK = 暂无数据)= 正常态,继续等待 */
break;
}
/* ---- QUEUE_RT 静默基线:peer flood 50×100B,MCU 逐次 DONTWAIT 收取 ----
* 仅对"收到数据"的调用计时(队列往返 + netTask SPI 读);空轮询不采样 */
case P13_ST_QUEUE_RT_RECV: {
c0 = lat_cycles_now();
n = net_recv(sockfd, s_p13_rx, (int)sizeof(s_p13_rx), NET_MSG_DONTWAIT);
us = lat_cycles_diff_us(c0);
if (n == 0) {
p13_abort("peer connection closed (QUEUE_RT recv)");
return;
}
if (n > 0) {
lat_stat_add(&s_p13_st, us, 0U);
s_p13_rx_bytes += (uint32_t)n;
}
/* 重复递交守卫(run1 实证缺陷 D11):芯片接收缓冲饱和后窗口
* 关闭/重开会把重传段重复递交(run1:5000B 送达 11884B,+138%)→
* 超容差即判 FAIL 并落 partial 报告(段内数据仍记录) */
if (s_p13_rx_bytes >
(LAT_P13_QUEUE_RT_CHUNKS * LAT_P13_QUEUE_RT_CHUNK_LEN) + (2U * LAT_P13_RECV_LEN)) {
p13_abort("QUEUE_RT duplicate delivery (CH395F TCP retransmit re-delivery)");
return;
}
if (s_p13_rx_bytes >= (LAT_P13_QUEUE_RT_CHUNKS * LAT_P13_QUEUE_RT_CHUNK_LEN)) {
p13_report_seg("queue_rt_recv", LAT_P13_QUEUE_RT_WARN_US, LAT_P13_QUEUE_RT_CRIT_US, 0U);
s_p13_state = P13_ST_QUEUE_RT_SEND;
s_p13_stage = 0U;
lat_stat_reset(&s_p13_st);
if (p13_send_op(sockfd, LAT_P13_OP_QUEUE_RT_SEND) == 0U) {
p13_abort("QUEUE_RT->QUEUE_RT_SEND op send failed (peer desync)");
return;
}
}
break;
}
/* ---- QUEUE_RT 静默基线:MCU 阻塞 net_send 50×100B(调用即含队列往返全程) ----
* 阻塞调用经 xTaskNotifyWait 让出 → netTask(Normal) 自然抢占,无额外延迟 */
case P13_ST_QUEUE_RT_SEND: {
while (s_p13_stage < LAT_P13_QUEUE_RT_CHUNKS) {
c0 = lat_cycles_now();
n = net_send(sockfd, s_p13_tx, (int32_t)LAT_P13_QUEUE_RT_CHUNK_LEN, 0);
us = lat_cycles_diff_us(c0);
if (n != (int32_t)LAT_P13_QUEUE_RT_CHUNK_LEN) {
s_p13_seg_force_fail = 1;
#if defined(TEST_LATENCY_PROBE)
DBG_INFO("[LAT] QUEUE_RT send unexpected n=%d stage=%u",
(int)n, (unsigned)s_p13_stage);
#endif
}
lat_stat_add(&s_p13_st, us, 0U);
s_p13_stage++;
}
p13_report_seg("queue_rt_send", LAT_P13_QUEUE_RT_WARN_US, LAT_P13_QUEUE_RT_CRIT_US, 0U);
/* run3 重排:先 CHUNK_WAIT(纯 TX flood,1s 超时保护下不挂死)+ LOGTAX(本地),
* 后 ECHO_RT/FRAME_RT(RX 协议段;run3 实证芯片 TCP 对小帧交换易失步 → 靠后并降样本数) */
s_p13_state = P13_ST_CHUNK_WAIT;
s_p13_stage = 0U;
break;
}
/* ---- ECHO_RT:每样本一轮 op→10B 请求→10B 回显(MCU 侧:收齐→发出) ---- */
case P13_ST_ECHO_RT: {
n = net_recv(sockfd, s_p13_l6rx + s_p13_l6rx_n,
(int)(LAT_P13_ECHO_RT_SIZE - s_p13_l6rx_n), NET_MSG_DONTWAIT);
if (n == 0) {
p13_abort("peer connection closed (ECHO_RT)");
return;
}
if (n > 0) {
s_p13_l6rx_n += (uint16_t)n;
}
if (s_p13_l6rx_n >= LAT_P13_ECHO_RT_SIZE) {
c0 = lat_cycles_now();
n = net_send(sockfd, s_p13_l6rx, (int32_t)LAT_P13_ECHO_RT_SIZE, 0); /* 回显 */
us = lat_cycles_diff_us(c0);
if (n != (int32_t)LAT_P13_ECHO_RT_SIZE) {
s_p13_seg_force_fail = 1;
#if defined(TEST_LATENCY_PROBE)
DBG_INFO("[LAT] ECHO_RT echo unexpected n=%d stage=%u",
(int)n, (unsigned)s_p13_stage);
#endif
}
lat_stat_add(&s_p13_st, us, 0U);
s_p13_stage++;
if (s_p13_stage >= LAT_P13_SAMPLES) {
p13_report_seg("req2first_mcu", LAT_P13_REQ2FIRST_WARN_US, LAT_P13_REQ2FIRST_CRIT_US, 0U);
s_p13_state = P13_ST_FRAME_RT;
s_p13_stage = 0U;
s_p13_l7rx_n = 0U;
s_p13_step_tick = HAL_GetTick();
(void)p13_send_op(sockfd, LAT_P13_OP_FRAME_RT);
} else {
s_p13_l6rx_n = 0U;
s_p13_step_tick = HAL_GetTick();
(void)p13_send_op(sockfd, LAT_P13_OP_ECHO_RT);
}
} else if ((HAL_GetTick() - s_p13_step_tick) > LAT_P13_SAMPLE_T_MS) {
p13_abort("ECHO_RT peer timeout");
return;
}
break;
}
/* ---- FRAME_RT:9B LCDMEM 请求 → 3209B 全帧(netTask 内 4 分块停止等待) ---- */
case P13_ST_FRAME_RT: {
n = net_recv(sockfd, s_p13_l7rx + s_p13_l7rx_n,
(int)(LAT_P13_FRAME_RT_REQ_LEN - s_p13_l7rx_n), NET_MSG_DONTWAIT);
if (n == 0) {
p13_abort("peer connection closed (FRAME_RT)");
return;
}
if (n > 0) {
s_p13_l7rx_n += (uint16_t)n;
}
if (s_p13_l7rx_n >= LAT_P13_FRAME_RT_REQ_LEN) {
/* 校验请求帧(严格照生产解析器 ui_net_sink.c remo_handle 的语义):
* [TAG=0xAA][CMD=0x03][LEN=4 大端][偏移u32 大端=0][CRC=XOR(数据段 4B)]
* 注意 LEN 是"数据段长度"(4),非请求的位图字节数;CRC 只覆盖数据段 */
{
uint8_t crc = 0U;
uint32_t len16;
for (i = 4U; i < 8U; i++) {
crc ^= s_p13_l7rx[i];
}
len16 = ((uint32_t)s_p13_l7rx[2U] << 8) | (uint32_t)s_p13_l7rx[3U];
if (s_p13_l7rx[0U] != 0xAAU || s_p13_l7rx[1U] != 0x03U ||
len16 != 4U || crc != s_p13_l7rx[8U] ||
s_p13_l7rx[4U] != 0U || s_p13_l7rx[5U] != 0U ||
s_p13_l7rx[6U] != 0U || s_p13_l7rx[7U] != 0U) {
p13_abort("FRAME_RT bad request frame");
return;
}
}
c0 = lat_cycles_now();
n = net_send(sockfd, s_p13_frame, (int32_t)LAT_P13_FRAME_LEN, 0);
us = lat_cycles_diff_us(c0);
if (n != (int32_t)LAT_P13_FRAME_LEN) {
s_p13_seg_force_fail = 1;
}
lat_stat_add(&s_p13_st, us, 0U);
s_p13_stage++;
if (s_p13_stage >= LAT_P13_SAMPLES) {
p13_report_seg("fullframe_mcu", LAT_P13_FULLFRAME_WARN_US, LAT_P13_FULLFRAME_CRIT_US, 0U);
/* FRAME_RT 为末段(run3 重排后的最终顺序:QUEUE_RT→CHUNK_WAIT→LOGTAX→ECHO_RT→FRAME_RT→DONE);
* 此处若回跳 CHUNK_WAIT 会形成 CHUNK_WAIT→LOGTAX→ECHO_RT→FRAME_RT 无限循环、永不下发 0xFF,
* peer 只能等 300s 超时(run11 实证) */
s_p13_state = P13_ST_DONE;
s_p13_stage = 0U;
} else {
s_p13_l7rx_n = 0U;
s_p13_step_tick = HAL_GetTick();
(void)p13_send_op(sockfd, LAT_P13_OP_FRAME_RT);
}
} else if ((HAL_GetTick() - s_p13_step_tick) > LAT_P13_SAMPLE_T_MS) {
p13_abort("FRAME_RT peer timeout");
return;
}
break;
}
#if defined(TEST_LATENCY_PROBE)
/* ---- CHUNK_WAIT:探针武装 + 32×4KB 发送 flood(= 128 chunk 样本) ----
* chunk_wait 在 netTask 内测量(写发送缓冲 → SENBUF_FREE),
* 调用方侧不可见 → 必须经 net_socket.c 探针(TEST_LATENCY_PROBE) */
case P13_ST_CHUNK_WAIT: {
if (s_p13_stage == 0U) {
(void)p13_send_op(sockfd, LAT_P13_OP_CHUNK_WAIT);
lat_probe_arm("chunk_wait", (uint32_t)LAT_P13_CHUNK_WAIT_SAMPLES, LAT_P13_CHUNK_WAIT_SPIKE_US);
}
while (s_p13_stage < LAT_P13_CHUNK_WAIT_SENDS) {
n = net_send(sockfd, s_p13_tx, (int32_t)LAT_P13_CHUNK_WAIT_SEND_LEN, 0);
if (n != (int32_t)LAT_P13_CHUNK_WAIT_SEND_LEN) {
s_p13_seg_force_fail = 1;
#if defined(TEST_LATENCY_PROBE)
DBG_INFO("[LAT] CHUNK_WAIT send unexpected n=%d stage=%u",
(int)n, (unsigned)s_p13_stage);
#endif
}
s_p13_stage++;
}
/* 阻塞发送均"芯片确认本块全部 chunk 完成(SEND_OK)"才返回 → 此刻探针
* 样本已定稿;芯片 TX 失步时以短写记录(1s 超时保护,不会挂死调用方) */
{
int v = LAT_VERDICT_OK;
const lat_stats_t *p_ps;
lat_probe_disarm();
p_ps = lat_probe_stats();
if (s_p13_seg_force_fail != 0) {
v = LAT_VERDICT_FAIL;
}
lat_stat_report("chunk_wait", p_ps, LAT_P13_CHUNK_WAIT_WARN_US,
LAT_P13_CHUNK_WAIT_CRIT_US, 0U, &v);
if (s_p13_seg_force_fail != 0) {
DBG_INFO("[LAT] note: chunk_wait has send error/short write -> FAIL");
}
if (v > s_p13_verdict_all) {
s_p13_verdict_all = v;
}
if (s_p13_seg_force_fail != 0) {
DBG_INFO("[LAT] note: chunk_wait partial n=%u/%u (chip TX stall suspected)",
(unsigned)p_ps->n, (unsigned)LAT_P13_CHUNK_WAIT_SAMPLES);
}
s_p13_seg_force_fail = 0;
s_p13_state = P13_ST_LOGTAX;
s_p13_stage = 0U;
}
break;
}
#else /* TEST_LATENCY_PROBE */
case P13_ST_CHUNK_WAIT: {
/* 探针未编译(未选 Phase 13/14 之外的组合):跳过本段 */
DBG_INFO("[LAT] skip chunk_wait (probe not compiled)");
s_p13_state = P13_ST_LOGTAX;
s_p13_stage = 0U;
break;
}
#endif /* TEST_LATENCY_PROBE */
/* ---- LOGTAX:一行 60 字符 printf 的耗时(fputc 逐字节 @115200,D6 量化) ---- */
case P13_ST_LOGTAX: {
static char s_line[64];
if (s_p13_stage == 0U) {
snprintf(s_line, sizeof(s_line), "LAT logtax line: 0123456789 0123456789 012345");
lat_stat_reset(&s_p13_st);
}
while (s_p13_stage < LAT_P13_SAMPLES) {
c0 = lat_cycles_now();
printf("%s\n", s_line);
lat_stat_add(&s_p13_st, lat_cycles_diff_us(c0), 0U);
s_p13_stage++;
}
p13_report_seg("logtax_line", LAT_P13_LOGTAX_WARN_US, LAT_P13_LOGTAX_CRIT_US, 0U);
/* ECHO_RT 前清空 socket 残留(重传/重复递交字节,run1 实证 6884B、run3 实证 9B 碎片):
* 残留会与 ECHO_RT/FRAME_RT 的请求帧错位混读 → 协议失步。此窗口对端静默
* (对端只在收到操作字节后才发包),DONTWAIT 抽干即安全。 */
{
int32_t d;
uint32_t drained = 0U;
uint32_t t0 = HAL_GetTick();
while (drained < 65536U && (HAL_GetTick() - t0) < 5000U) {
d = net_recv(sockfd, s_p13_rx, (int)sizeof(s_p13_rx), NET_MSG_DONTWAIT);
if (d <= 0) {
break; /* d==0 = 已排空/EOF;d<0 = 空轮询(WOULDBLOCK) */
}
drained += (uint32_t)d;
}
if (drained > 0U) {
DBG_INFO("[LAT] note: drained %u residual bytes before ECHO_RT (retransmit/duplicate)",
(unsigned)drained);
}
}
s_p13_state = P13_ST_ECHO_RT;
s_p13_stage = 0U;
s_p13_l6rx_n = 0U;
s_p13_step_tick = HAL_GetTick();
(void)p13_send_op(sockfd, LAT_P13_OP_ECHO_RT);
break;
}
case P13_ST_DONE: {
const char *p_name;
(void)p13_send_op(sockfd, LAT_P13_OP_DONE);
p_name = (s_p13_verdict_all == LAT_VERDICT_OK) ? "OK"
: ((s_p13_verdict_all == LAT_VERDICT_WARN) ? "WARN" : "FAIL");
DBG_INFO("[LAT] summary verdict=%s", p_name);
TEST_CHECK(s_p13_verdict_all != LAT_VERDICT_FAIL,
"P13 latency baseline (verdict=%s)", p_name);
DBG_INFO("[LAT] report complete");
TEST_REPORT("Phase 13");
s_p13_done = 1U;
s_p13_state = P13_ST_ABORT;
break;
}
default:
break;
}
#else /* ENABLE_PHASE13_TESTS */
(void)sockfd;
#endif /* ENABLE_PHASE13_TESTS */
}
#endif /* ENABLE_PHASE13_TESTS */
#ifdef ENABLE_PHASE14_TESTS
/* ====================================================================
* Phase 14 — 生产路径时延(PROD_CYCLE~UI_LOOP,M3)
*
* 与 Phase 13 的关键区别:**不占用测试 socket**,测的是真实生产路径
* (uiTask + ui_net_sink :7003 + 真实 LVGL 取帧),由 PC 端
* `latency_peer.py --mode remo --port 7003` 驱动。
*
* 探针是全局单窗口 → 三个窗口必须串行:
* ui_loop uiTask 主循环实测周期(本地,不需要 peer,120 样本)
* prod_cycle_mcu ui_net_sink 处理一帧 LCDMEM 的 MCU 净成本
* (取 1bpp 位图 + 组帧 + net_send 队列/芯片;不含 10ms 拾取拍)
* relisten peer 断开后自动重监听耗时(量化 D10:TRAP-NET-09 重试含 osDelay(500))
*
* 端口分工:peer 走 :7003(生产 socket,uiTask 持有),本 Phase 不碰 :8080。
* 段阈值(首轮 M3 实测后按 §4.2 重锁):
* ui_loop warn 25ms / crit 50ms
* prod_cycle warn 20ms / crit 200ms
* relisten warn 50ms / crit 500ms(spike ≥500ms = D10 指纹)
* ==================================================================== */
#define LAT_P14_UI_SAMPLES 120U
#define LAT_P14_PROD_SAMPLES 50U
#define LAT_P14_REL_SAMPLES 3U /* = peer --reconnect-rounds(每次 close 触发一次 auto-relisten) */
#define LAT_P14_UI_WIN_MS 20000U
#define LAT_P14_PROD_WIN_MS 90000U
#define LAT_P14_REL_WIN_MS 60000U
#define LAT_P14_TIMEOUT_MS 240000U
#define LAT_P14_UI_WARN_US 25000U
#define LAT_P14_UI_CRIT_US 50000U
#define LAT_P14_PROD_WARN_US 20000U
#define LAT_P14_PROD_CRIT_US 200000U
#define LAT_P14_REL_WARN_US 100000U /* D10 修复后实测 89ms(重试间隔 30ms + 芯片沉降);
* 原 50ms 阈值对应"旧实现 588ms"已不适用 */
#define LAT_P14_REL_CRIT_US 500000U
enum {
P14_ST_UI_LOOP = 0,
P14_ST_PROD,
P14_ST_RELISTEN,
P14_ST_DONE
};
static uint8_t s_p14_done = 0U;
static uint8_t s_p14_banner = 0U;
#if defined(TEST_LATENCY_PROBE)
static uint8_t s_p14_state = (uint8_t)P14_ST_UI_LOOP;
static uint8_t s_p14_win_open = 0U;
static int s_p14_verdict_all = LAT_VERDICT_OK;
static uint32_t s_p14_start_tick;
static uint32_t s_p14_win_tick;
#endif
int net_latency_phase14_busy(void)
{
return (s_p14_done == 0U) ? 1 : 0;
}
#if defined(TEST_LATENCY_PROBE)
/* 收尾一个窗口:disarm → 取统计 → 报告 → verdict 累计 */
static void p14_close_seg(const char *p_seg, uint32_t warn_us, uint32_t crit_us,
uint16_t max_spikes)
{
const lat_stats_t *p_st;
int v = LAT_VERDICT_OK;
lat_probe_disarm();
p_st = lat_probe_stats();
if (p_st->n == 0U) {
DBG_INFO("[LAT] note: %s window got 0 samples (peer missing/timeout)", p_seg);
v = LAT_VERDICT_FAIL;
} else if (p_st->med == 0U) {
/* 中位数为 0 = 计时器未使能/探针未生效(真实处理不可能 0µs)。
* 2026-09-22 run16 实证:本 Phase 曾漏调 lat_time_init() → DWT CYCCNT
* 恒 0 → 三段全 0 却输出"verdict=OK"的假绿 → 必须判无效。 */
DBG_ERROR("[LAT] note: %s med=0us -> probe timer not armed (result invalid)", p_seg);
v = LAT_VERDICT_FAIL;
} else {
lat_stat_report(p_seg, p_st, warn_us, crit_us, max_spikes, &v);
}
if (v > s_p14_verdict_all) {
s_p14_verdict_all = v;
}
}
#endif /* TEST_LATENCY_PROBE */
void net_latency_phase14_run(void)
{
#if defined(TEST_LATENCY_PROBE)
if (s_p14_done != 0U) {
return;
}
/* 一次性横幅 + 操作提示 */
if (s_p14_banner == 0U) {
s_p14_banner = 1U;
s_p14_start_tick = HAL_GetTick();
lat_time_init(); /* DWT CYCCNT 使能(漏调则所有样本恒 0) */
DBG_INFO("=== CH395F Phase 14 Tests (prod-path latency, PROD_CYCLE~UI_LOOP) ===");
DBG_INFO("[LAT] run_id=%lu phase=14 cal core_clk=%lu us_per_1024cyc=%lu",
(unsigned long)s_p14_start_tick,
(unsigned long)SystemCoreClock,
(unsigned long)(((uint64_t)1024U * 1000000UL) / (uint64_t)SystemCoreClock));
DBG_INFO("[LAT] prod path: uiTask + ui_net_sink :7003 "
"(run latency_peer.py --mode remo --port 7003)");
}
/* 整体超时保护:避免 peer 缺席时无限等窗口 */
if ((HAL_GetTick() - s_p14_start_tick) > LAT_P14_TIMEOUT_MS) {
DBG_ERROR("[LAT] phase14 abort: overall timeout");
s_p14_done = 1U;
return;
}
switch (s_p14_state) {
case P14_ST_UI_LOOP:
if (s_p14_win_open == 0U) {
lat_probe_arm("ui_loop", LAT_P14_UI_SAMPLES, 0U);
s_p14_win_tick = HAL_GetTick();
s_p14_win_open = 1U;
break;
}
if ((lat_probe_count() >= LAT_P14_UI_SAMPLES) ||
((HAL_GetTick() - s_p14_win_tick) > LAT_P14_UI_WIN_MS)) {
p14_close_seg("ui_loop", LAT_P14_UI_WARN_US, LAT_P14_UI_CRIT_US, 0U);
s_p14_state = (uint8_t)P14_ST_PROD;
s_p14_win_open = 0U;
}
break;
case P14_ST_PROD:
if (s_p14_win_open == 0U) {
lat_probe_arm("prod_cycle_mcu", LAT_P14_PROD_SAMPLES, 500000U);
s_p14_win_tick = HAL_GetTick();
s_p14_win_open = 1U;
break;
}
if ((lat_probe_count() >= LAT_P14_PROD_SAMPLES) ||
((HAL_GetTick() - s_p14_win_tick) > LAT_P14_PROD_WIN_MS)) {
p14_close_seg("prod_cycle_mcu", LAT_P14_PROD_WARN_US, LAT_P14_PROD_CRIT_US, 0U);
s_p14_state = (uint8_t)P14_ST_RELISTEN;
s_p14_win_open = 0U;
}
break;
case P14_ST_RELISTEN:
if (s_p14_win_open == 0U) {
lat_probe_arm("relisten", LAT_P14_REL_SAMPLES, LAT_P14_REL_CRIT_US);
s_p14_win_tick = HAL_GetTick();
s_p14_win_open = 1U;
break;
}
if ((lat_probe_count() >= LAT_P14_REL_SAMPLES) ||
((HAL_GetTick() - s_p14_win_tick) > LAT_P14_REL_WIN_MS)) {
p14_close_seg("relisten", LAT_P14_REL_WARN_US, LAT_P14_REL_CRIT_US, 1U);
s_p14_state = (uint8_t)P14_ST_DONE;
s_p14_win_open = 0U;
}
break;
case P14_ST_DONE:
default: {
const char *p_name = (s_p14_verdict_all == LAT_VERDICT_OK) ? "OK"
: ((s_p14_verdict_all == LAT_VERDICT_WARN) ? "WARN" : "FAIL");
DBG_INFO("[LAT] summary verdict=%s", p_name);
TEST_CHECK(s_p14_verdict_all != LAT_VERDICT_FAIL,
"P14 prod-path latency baseline (verdict=%s)", p_name);
DBG_INFO("[LAT] report complete");
TEST_REPORT("Phase 14");
s_p14_done = 1U;
break;
}
}
#else /* TEST_LATENCY_PROBE */
if (s_p14_banner == 0U) {
s_p14_banner = 1U;
DBG_INFO("[LAT] phase14 skipped: TEST_LATENCY_PROBE not compiled");
s_p14_done = 1U;
}
#endif /* TEST_LATENCY_PROBE */
}
#endif /* ENABLE_PHASE14_TESTS */
#endif /* ENABLE_PHASE12/13/14 任一使能 */