Files
STM32F4-Base/test/board/net_test_task.c
T
super_agent_bot fc0d3e4b5c
CI / 静态门禁 (pre-commit) (push) Failing after 5m39s
CI / 主机侧单测 (MSVC) (push) Successful in 30s
CI / 静态门禁 (pre-commit) (pull_request) Failing after 4m15s
CI / 主机侧单测 (MSVC) (pull_request) Successful in 47s
test(net/udp): 阶段 16 台架加固 + 真机首跑证据回填(#73)
第一次上真机把台架的两处软肋照出来了,都不是被测代码的问题:

- peer 死在 Windows 的 ICMP 10054:板子还没 bind 时那一下端口不可达会被记在套接句上,
  下一次 recvfrom 才抛出来。本机 CPython 3.14.7 既没有 socket.SIO_UDP_CONNRESET 也不接受
  它的数值形式,所以主力防线改成把 10054 当「这一轮没收到」继续重试,并在回路自检里加
  冷端口探针——探针走脚本真正在用的 _recv_maybe,另拿裸套接句证明这台机器确实会回 ICMP,
  否则那条判定就没有牙。
- 单步预算错位:板端每步等 25 s,peer 每步只等 8 s,一步超时后整条梯子全灭。现在 peer
  每步都重发、单步预算 --step-budget 默认 35 s(小于 26 s 打警告);板端 udp_bench_expect
  按载荷第 0 字节的步号自愈,比本步小的旧包丢掉继续等(上限 UDP_BENCH_STALE_MAX = 8),
  比本步大的直接报出来。DBG_WARN 在本工程不存在、字符串字面量必须纯 ASCII,两处已改。

真机已拿到的证据:TC-NET-1601a/b/c(建 socket、bind :6001、net_udp_get_mode 回 SERVER)与
TC-NET-1602(服务端形态收到 20 B、addrlen/sin_family/源端口全对、逐字节回显、形态翻成
CLIENT)全绿——剥 8 字节信息头与源地址回填在真芯片上成立;②③④⑤ 因板子当晚两次失电
(19:19、21:0x,SWD No target connected + ping 不通)未测,指南 §12.6 与覆盖率口径第 4 轮
的状态条据此改写。产品态(宏回默认)重新构建 0 Error 0 Warning,Code 249,124 / RO 29,836 /
RW 2,052 / ZI 149,996 与本阶段之前逐字节一致,阶段 16 在产品里被预处理干净。
2026-11-03 21:28:36 +08:00

1372 lines
59 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*****************************************************************************
* 模块名称:CH395F NET 层测试(Phase 6~11)
* 模块功能:基于 BSD Socket API(单连接模式)的 NET 层测试,由 testTask
* 任务内的 net_layer_test_main() / net_layer_client_test_main() 事件
* 循环驱动,需 PC 配合。
* 包含 Phase 6(TCP Echo) / 7(TCP Client) / 8(大文件) / 9(Select) /
* 10(边界) / 11(KeepAlive) 及监听循环。
* 作者:王建锋
* 创建日期:2026-07-22
******************************************************************************/
#include <string.h>
#include "cmsis_os.h"
#include "net_socket.h"
#include "net_types.h"
#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"
/*
* 收发缓冲区 —— 全局静态,不在任务栈上。
* s_rx_buf 用于 Phase 6(Echo)/8(大文件)/9(Select)/10(边界)/11(KeepAlive)。
* s_tx_buf 仅在 Phase 8(大文件) 和 Phase 10(边界) 中使用。
* Phase 7(TCP Client) 使用函数内局部小缓冲(64B 递增 + 回显)。
*
* 大小依据:
* - CH395F 单 Socket 接收缓冲按 1KB block 配置(net_socket.c 中
* set_recv_buf(sock, 30, 2) = 2KB),单次 net_recv 至多返回 ~2KB,
* 故 s_rx_buf 实际只需 ≥ 2KB(此处 32KB 留足余量)。
* - Phase 8(大文件) 现改为流式分块:每次仅填一个窗口(≤1024B)即发出,
* 不再一次性填入整文件,故 s_tx_buf 只需 ≥ 单窗口大小(此处 32KB 留足余量)。
* 取 32KB(32768) 同时满足两者并留余量,且为 2 的幂;相比原 64KB
* 在 STM32F4(128KB RAM)上节省一半静态空间。
*/
#define TEST_BUF_SIZE 32768
/*
* 缓冲区声明前置于全部 Phase 代码:声明必须早于任何使用点(Phase 6
* 即首次使用 s_rx_buf)。历史修复:原声明位于文件后段(Phase 8 注释块
* 之后),phase6_run 使用先于声明 → ARMCC #20 identifier undefined,
* 全部 NET 层测试配置不可编译。声明条件按各 Phase 实际用缓冲精确门控:
* s_rx_buf —— Phase 6(Echo)/9(Select) 恒用,
* Phase 8(大文件) 非 SEND 方向用,Phase 10(边界) 用;
* (Phase 11 只用 64B 探针读,2026-10-13 改专用小缓冲——
* 产品 RAM 余量放不下 32KB,勿再把 11 加回本门控)
* s_tx_buf —— Phase 8(大文件) 非 RECV 方向(填充发送窗口)及
* Phase 10(边界) 用。
* PHASE8_DIRECTION 由下方方向配置块定义(仅 ENABLE_PHASE8_TESTS 时);
* #if 中未定义标识符按 0(ECHO) 处理,方向门控结果与配置一致。
*/
/* Phase 8 方向配置:须早于下方缓冲声明,以按方向精确控制 s_tx_buf 等是否参与编译 */
#ifdef ENABLE_PHASE8_TESTS
#define PHASE8_DIR_ECHO 0
#define PHASE8_DIR_SEND 1
#define PHASE8_DIR_RECV 2
#ifndef PHASE8_FILE_SIZE
#define PHASE8_FILE_SIZE 102400 /* 100KB */
#endif
#ifndef PHASE8_DIRECTION
#define PHASE8_DIRECTION PHASE8_DIR_ECHO
#endif
#endif
#if defined(ENABLE_PHASE6_TESTS) || \
(defined(ENABLE_PHASE8_TESTS) && (PHASE8_DIRECTION != PHASE8_DIR_SEND)) || \
defined(ENABLE_PHASE9_TESTS) || defined(ENABLE_PHASE10_TESTS)
static uint8_t s_rx_buf[TEST_BUF_SIZE];
#endif
/* s_tx_buf 仅 SEND/ECHO(及 Phase 10)需要;RECV 不发送,避免未使用变量警告 */
#if (defined(ENABLE_PHASE8_TESTS) && (PHASE8_DIRECTION != PHASE8_DIR_RECV)) || \
defined(ENABLE_PHASE10_TESTS)
static uint8_t s_tx_buf[TEST_BUF_SIZE];
#endif
/* ===================================================================
* Phase 6 — NET 层 TCP Echo(基础收发)
*
* 测试目的:验证 BSD Socket API(net_socket)的单连接 TCP Echo 通路。
* Socket 0 复用监听和数据通道。PC 发任意数据,MCU 原样返回。
* 这是 Phase 7/8/9/10 的基础。
*
* 入口条件:Socket 0 处于 ESTABLISHED(TCP 连接已建立)。
* 通过:数据收发一致。
* 失败:send/recv 数量不匹配。
* =================================================================== */
#ifdef ENABLE_PHASE6_TESTS
static void phase6_run(int32_t sockfd) {
/*
* 单连接 TCP Echo:NET_MSG_DONTWAIT 非阻塞收,原样回显。
* 连接断开由 net 层自动重监听(auto_relisten),应用层不维护生命周期;
* 字节完整性由 PC 侧校验(data == test_data),固件不做每连接自检。
*/
int32_t n = net_recv(sockfd, s_rx_buf, TEST_BUF_SIZE, NET_MSG_DONTWAIT);
if (n > 0) {
int32_t sent = net_send(sockfd, s_rx_buf, n, 0);
if (sent != n) {
DBG_ERROR("echo: send %d != recv %d", sent, n);
}
}
}
#endif
/* ===================================================================
* Phase 7 — NET 层 TCP Client(主动连接 PC 回显服务器)
*
* 测试目的:验证 BSD Socket API 的 TCP Client 路径(net_socket + net_bind
* + net_connect + net_send + net_recv):
* 1. MCU 主动连接 PC 回显服务器(默认 PHASE7_REMOTE_IP:8080)
* 2. 64B 递增数据回显后,固件侧逐字节比对 + 32 位累加和校验
* 3. 关闭并重新连接(规避 TIME_WAIT:每轮换本地端口)
* 4. 连接超时负向用例(PC 服务关闭时 connect 不应成功)
*
* 入口条件:PC 运行 `python test/ch395f_socket_test.py tcp_server --port 8081`
* (TC-NET-703 需先关闭该服务)。
* TC 映射:TC-NET-701(连接 + 回显)/ TC-NET-702(关闭重连)/ TC-NET-703(连接超时)。
* 通过:回显逐字节一致且 32 位 checksum 匹配;重连全部成功;超时用例未建连。
* 失败:连接失败 / 回显不一致 / 重连中断 / 超时用例误建连。
* =================================================================== */
#ifdef ENABLE_PHASE7_TESTS
#include "net_select.h"
#ifndef PHASE7_REMOTE_IP
#define PHASE7_REMOTE_IP TEST_PC_PEER_IP
#endif
#ifndef PHASE7_REMOTE_PORT
#define PHASE7_REMOTE_PORT 8081
#endif
#ifndef PHASE7_LOCAL_PORT
#define PHASE7_LOCAL_PORT 50000
#endif
#ifndef PHASE7_LOOP_COUNT
#define PHASE7_LOOP_COUNT 3
#endif
#ifndef PHASE7_EXCH_SIZE
#define PHASE7_EXCH_SIZE 64
#endif
#ifndef PHASE7_CONN_TIMEOUT_MS
#define PHASE7_CONN_TIMEOUT_MS 10000
#endif
#ifndef PHASE7_TIMEOUT_TEST_MS
#define PHASE7_TIMEOUT_TEST_MS 30000 /* TC-NET-703 连接超时窗(指南规定 30s) */
#endif
/* 32 位累加和:足以发现丢字节/错位,比 CRC32 便宜 */
static uint32_t phase7_cksum(const uint8_t *p, uint32_t n) {
uint32_t s = 0;
while (n--) s += *p++;
return s;
}
/* 单次 TCP Client 会话:对应 TC-NET-701 主体 / TC-NET-702 单轮。
* 每轮使用不同本地端口 local_port 规避 TIME_WAIT。
* 返回 1 表示全流程通过,0 表示中途失败(已记录 TEST_CHECK)。 */
static int32_t phase7_session(uint16_t local_port)
{
int32_t sockfd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
if (sockfd < 0) {
TEST_CHECK(0, "TC-NET-701 socket create");
return 0;
}
/* 绑定本地端口(不同轮用不同端口规避 TIME_WAIT) */
{
struct net_sockaddr_in laddr;
memset(&laddr, 0, sizeof(laddr));
laddr.sin_family = NET_AF_INET;
laddr.sin_port = net_htons(local_port);
if (net_bind(sockfd, (struct net_sockaddr *)&laddr, sizeof(laddr)) < 0) {
TEST_CHECK(0, "TC-NET-701 bind port %d", local_port);
net_close(sockfd);
return 0;
}
}
/* 目标地址 */
struct net_sockaddr_in raddr;
memset(&raddr, 0, sizeof(raddr));
raddr.sin_family = NET_AF_INET;
raddr.sin_port = net_htons((uint16_t)PHASE7_REMOTE_PORT);
raddr.sin_addr.s_addr = net_inet_addr(PHASE7_REMOTE_IP);
DBG_INFO("TC-NET-701 connecting %s:%d (local port %d)",
PHASE7_REMOTE_IP, PHASE7_REMOTE_PORT, local_port);
int32_t rc = net_connect(sockfd, (struct net_sockaddr *)&raddr, sizeof(raddr));
TEST_CHECK(rc == 0, "TC-NET-701 connect issued (ret=%d)", rc);
if (rc != 0) { net_close(sockfd); return 0; }
/* 等待连接建立:轮询 socket 状态(ESTABLISHED)。
* 用 net_select 触发 net_poll 并作为 100ms 节拍,状态以 net_get_sock 为准。 */
{
int32_t connected = 0;
uint32_t tick = HAL_GetTick();
while (HAL_GetTick() - tick < PHASE7_CONN_TIMEOUT_MS) {
net_sock_t *sk = net_get_sock(sockfd);
if (sk && sk->state == NET_SOCK_STATE_ESTABLISHED) {
connected = 1; break;
}
if (sk && sk->state == NET_SOCK_STATE_CLOSED) {
break; /* 连接失败 */
}
osDelay(100); /* 仅作等待/让出 CPU;连接状态由 netTask 的 net_poll 每 10ms 刷新 */
}
TEST_CHECK(connected, "TC-NET-701 connected within %dms", PHASE7_CONN_TIMEOUT_MS);
if (!connected) { net_close(sockfd); return 0; }
}
/* 数据交换:64B 递增模式 */
uint8_t tx[PHASE7_EXCH_SIZE];
uint8_t rx[PHASE7_EXCH_SIZE];
for (int32_t i = 0; i < PHASE7_EXCH_SIZE; i++) tx[i] = (uint8_t)i;
int32_t sent = net_send(sockfd, tx, PHASE7_EXCH_SIZE, 0);
TEST_CHECK(sent == PHASE7_EXCH_SIZE, "TC-NET-701 send %dB (sent=%d)", PHASE7_EXCH_SIZE, sent);
if (sent != PHASE7_EXCH_SIZE) { net_close(sockfd); return 0; }
int32_t recvd = 0;
uint32_t tick = HAL_GetTick();
while (recvd < PHASE7_EXCH_SIZE) {
int32_t n = net_recv(sockfd, rx + recvd, PHASE7_EXCH_SIZE - recvd, NET_MSG_DONTWAIT);
if (n > 0) {
recvd += n;
} else if (n == 0) {
break; /* 对端关闭 */
} else if (n < 0 && net_get_errno() == NET_ERR_WOULDBLOCK) {
if (HAL_GetTick() - tick > PHASE7_CONN_TIMEOUT_MS) break;
osDelay(10);
continue;
} else {
break; /* 其它错误 */
}
if (HAL_GetTick() - tick > PHASE7_CONN_TIMEOUT_MS) break;
}
int32_t ok = (recvd == PHASE7_EXCH_SIZE);
if (ok) {
int32_t bad = 0;
for (int32_t i = 0; i < PHASE7_EXCH_SIZE; i++) {
if (rx[i] != (uint8_t)i) { bad = 1; break; }
}
TEST_CHECK(!bad, "TC-NET-701 byte-compare %dB", PHASE7_EXCH_SIZE);
TEST_CHECK(1, "TC-NET-701 recv %dB, cksum=0x%08X", recvd, phase7_cksum(rx, PHASE7_EXCH_SIZE));
} else {
TEST_CHECK(0, "TC-NET-701 recv %dB (expect %d)", recvd, PHASE7_EXCH_SIZE);
}
net_close(sockfd);
return ok;
}
/* TC-NET-703(负向):连接超时。PC 未在 REMOTE_IP:REMOTE_PORT 提供服务时,
* connect 不应成功建立。本用例需先关闭 PC 端 tcp_server(与 TC-NET-701/702 互斥环境:
* 701/702 要求服务端在线,703 要求服务端离线,故 703 应在独立的一轮中运行)。 */
static void phase7_test_timeout(void)
{
int32_t sockfd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
if (sockfd < 0) { TEST_CHECK(0, "TC-NET-703 socket create"); return; }
struct net_sockaddr_in raddr;
memset(&raddr, 0, sizeof(raddr));
raddr.sin_family = NET_AF_INET;
raddr.sin_port = net_htons((uint16_t)PHASE7_REMOTE_PORT);
raddr.sin_addr.s_addr = net_inet_addr(PHASE7_REMOTE_IP);
DBG_INFO("TC-NET-703 connecting %s:%d (expect TIMEOUT, PC server must be OFF)",
PHASE7_REMOTE_IP, PHASE7_REMOTE_PORT);
int32_t rc = net_connect(sockfd, (struct net_sockaddr *)&raddr, sizeof(raddr));
TEST_CHECK(rc == 0, "TC-NET-703 connect issued (ret=%d)", rc);
if (rc != 0) { net_close(sockfd); return; }
int32_t established = 0;
uint32_t tick = HAL_GetTick();
while (HAL_GetTick() - tick < PHASE7_TIMEOUT_TEST_MS) {
net_sock_t *sk = net_get_sock(sockfd);
if (sk && sk->state == NET_SOCK_STATE_ESTABLISHED) { established = 1; break; }
if (sk && sk->state == NET_SOCK_STATE_CLOSED) break;
osDelay(100); /* 仅作等待/让出 CPU;连接状态由 netTask 的 net_poll 每 10ms 刷新 */
}
if (established) {
TEST_CHECK(0, "TC-NET-703 connected?! stop PC server and retry");
} else {
TEST_CHECK(1, "TC-NET-703 no-connect within %dms (timeout as expected)", PHASE7_TIMEOUT_TEST_MS);
}
net_close(sockfd);
}
/* TC-NET-704:多 Socket 并发 TCP Client。
* 并发上限 = NET_MAX_SOCKETS(8) − 3 个常驻监听(FTP / 对时 / 远显)= 5;
* 本用例同时打开 Socket 0~4,各自连 PC echo server,每路独立收发
* PHASE7_EXCH_SIZE 字节回显并比对,验证 5 个 Socket 均能独立工作。
* 依赖 PC 端 tcp_server 支持并发连接(--max-conn ≥ 5)。
* (2026-10-15:原值 8 超出并发上限,socket create #5 必失败。) */
#ifndef PHASE7_MULTI_COUNT
#define PHASE7_MULTI_COUNT 5
#endif
#ifndef PHASE7_MULTI_BASE_PORT
#define PHASE7_MULTI_BASE_PORT 51000
#endif
static int32_t phase7_session_multi(void)
{
int32_t sockfd[PHASE7_MULTI_COUNT];
uint8_t tx[PHASE7_EXCH_SIZE];
uint8_t rx[PHASE7_EXCH_SIZE];
for (int32_t i = 0; i < PHASE7_EXCH_SIZE; i++) tx[i] = (uint8_t)i;
/* 1) 打开并连接所有 Socket(各自绑定不同本地端口规避 TIME_WAIT) */
for (int32_t i = 0; i < PHASE7_MULTI_COUNT; i++) {
sockfd[i] = net_socket(NET_AF_INET, NET_SOCK_STREAM);
if (sockfd[i] < 0) {
TEST_CHECK(0, "TC-NET-704 socket create #%d", i);
for (int32_t j = 0; j < i; j++) net_close(sockfd[j]);
return 0;
}
struct net_sockaddr_in laddr;
memset(&laddr, 0, sizeof(laddr));
laddr.sin_family = NET_AF_INET;
laddr.sin_port = net_htons((uint16_t)(PHASE7_MULTI_BASE_PORT + i));
if (net_bind(sockfd[i], (struct net_sockaddr *)&laddr, sizeof(laddr)) < 0) {
TEST_CHECK(0, "TC-NET-704 bind #%d port %d", i, PHASE7_MULTI_BASE_PORT + i);
net_close(sockfd[i]);
for (int32_t j = 0; j < i; j++) net_close(sockfd[j]);
return 0;
}
struct net_sockaddr_in raddr;
memset(&raddr, 0, sizeof(raddr));
raddr.sin_family = NET_AF_INET;
raddr.sin_port = net_htons((uint16_t)PHASE7_REMOTE_PORT);
raddr.sin_addr.s_addr = net_inet_addr(PHASE7_REMOTE_IP);
int32_t rc = net_connect(sockfd[i], (struct net_sockaddr *)&raddr, sizeof(raddr));
TEST_CHECK(rc == 0, "TC-NET-704 connect #%d issued (ret=%d)", i, rc);
if (rc != 0) {
net_close(sockfd[i]);
for (int32_t j = 0; j < i; j++) net_close(sockfd[j]);
return 0;
}
}
/* 2) 等待所有连接建立(或超时) */
{
uint32_t tick = HAL_GetTick();
int32_t all_up = 0;
while (HAL_GetTick() - tick < PHASE7_CONN_TIMEOUT_MS) {
all_up = 1;
for (int32_t i = 0; i < PHASE7_MULTI_COUNT; i++) {
net_sock_t *sk = net_get_sock(sockfd[i]);
if (!sk || sk->state != NET_SOCK_STATE_ESTABLISHED) { all_up = 0; break; }
}
if (all_up) break;
osDelay(100);
}
TEST_CHECK(all_up, "TC-NET-704 all %d sockets established within %dms", PHASE7_MULTI_COUNT, PHASE7_CONN_TIMEOUT_MS);
if (!all_up) {
for (int32_t i = 0; i < PHASE7_MULTI_COUNT; i++) {
net_sock_t *sk = net_get_sock(sockfd[i]);
if (sk) DBG_ERROR("TC-NET-704 socket #%d state=%d", i, sk->state);
net_close(sockfd[i]);
}
return 0;
}
}
/* 3) 每路独立收发回显 */
int32_t ok = 1;
for (int32_t i = 0; i < PHASE7_MULTI_COUNT; i++) {
int32_t sent = net_send(sockfd[i], tx, PHASE7_EXCH_SIZE, 0);
if (sent != PHASE7_EXCH_SIZE) {
TEST_CHECK(0, "TC-NET-704 socket #%d send %dB (sent=%d)", i, PHASE7_EXCH_SIZE, sent);
ok = 0;
continue;
}
int32_t recvd = 0;
uint32_t tick = HAL_GetTick();
while (recvd < PHASE7_EXCH_SIZE) {
int32_t n = net_recv(sockfd[i], rx + recvd, PHASE7_EXCH_SIZE - recvd, NET_MSG_DONTWAIT);
if (n > 0) recvd += n;
else if (n == 0) break;
else if (n < 0 && net_get_errno() == NET_ERR_WOULDBLOCK) {
if (HAL_GetTick() - tick > PHASE7_CONN_TIMEOUT_MS) break;
osDelay(10);
continue;
} else break;
if (HAL_GetTick() - tick > PHASE7_CONN_TIMEOUT_MS) break;
}
int32_t sock_ok = (recvd == PHASE7_EXCH_SIZE);
if (sock_ok) {
int32_t bad = 0;
for (int32_t k = 0; k < PHASE7_EXCH_SIZE; k++) if (rx[k] != (uint8_t)k) { bad = 1; break; }
TEST_CHECK(!bad, "TC-NET-704 socket #%d echo %dB OK", i, PHASE7_EXCH_SIZE);
} else {
TEST_CHECK(0, "TC-NET-704 socket #%d recv %dB (expect %d)", i, recvd, PHASE7_EXCH_SIZE);
ok = 0;
}
}
/* 4) 关闭所有 */
for (int32_t i = 0; i < PHASE7_MULTI_COUNT; i++) net_close(sockfd[i]);
return ok;
}
void net_layer_client_test_main(void)
{
DBG_INFO("=== CH395F Phase 7 Tests (NET TCP Client) ===");
DBG_INFO("--- TC-NET-701: connect + %dB echo exchange ---", PHASE7_EXCH_SIZE);
phase7_session((uint16_t)PHASE7_LOCAL_PORT);
DBG_INFO("--- TC-NET-702: close/reconnect x%d ---", PHASE7_LOOP_COUNT);
{
int32_t all_ok = 1;
for (int32_t i = 0; i < PHASE7_LOOP_COUNT; i++) {
if (!phase7_session((uint16_t)(PHASE7_LOCAL_PORT + 1 + i))) all_ok = 0;
}
TEST_CHECK(all_ok, "TC-NET-702 reconnect x%d all passed", PHASE7_LOOP_COUNT);
}
DBG_INFO("--- TC-NET-704: %d-socket concurrent client (PC %s:%d MUST be ON) ---",
PHASE7_MULTI_COUNT, PHASE7_REMOTE_IP, PHASE7_REMOTE_PORT);
phase7_session_multi();
DBG_INFO("--- TC-NET-703: connect timeout (PC %s:%d MUST be OFF) ---",
PHASE7_REMOTE_IP, PHASE7_REMOTE_PORT);
phase7_test_timeout();
TEST_REPORT("Phase 7");
}
#endif
/* ===================================================================
* Phase 8 — 大文件 / 批量传输(默认 100KB,文件收发)
*
* 测试目的:验证超过 Socket 接收缓冲(2KB)的大块数据在 net_send /
* net_recv 层经分块后的正确处理;并支持「文件发送(MCU→PC) /
* 文件接收(PC→MCU) / 回显 round-trip」三种方向。
*
* 数据模式:递增字节,绝对位置 p 处字节 = (p & 0xFF);发送端按窗口
* (offset + i) 生成,接收端按 offset 逐字节比对(自校验)。
* 整包另用 CRC32(与 PC zlib.crc32 一致)做整体校验。
*
* 缓冲策略:s_tx_buf / s_rx_buf 仅 32KB,100KB 须按窗口逐块生成/校验,
* 不整体驻留 RAM(流式分块)。
*
* 方向选择(编译宏 PHASE8_DIRECTION):
* 0 = ECHO 发 100KB 经 PC 回显,MCU 收齐校验(默认)
* 1 = SEND MCU 发 100KB,PC 落盘 rx_100k.bin 校验
* 2 = RECV PC 发 100KB(+4B CRC32 尾),MCU 收齐字节+CRC 校验
* =================================================================== */
#ifdef ENABLE_PHASE8_TESTS
/* CRC32(标准,与 Python zlib.crc32 一致;初始 0xFFFFFFFF,末异或 0xFFFFFFFF)
* 仅 RECV 方向需要:MCU 收齐后比对 PC 附在文件末尾的 4 字节 CRC32。 */
#if PHASE8_DIRECTION == PHASE8_DIR_RECV
static uint32_t phase8_crc32_step(uint32_t crc, const uint8_t *data, uint32_t len) {
for (uint32_t i = 0; i < len; i++) {
crc ^= data[i];
for (int32_t k = 0; k < 8; k++) {
crc = (crc & 1u) ? (crc >> 1) ^ 0xEDB88320u : (crc >> 1);
}
}
return crc;
}
#endif
/* 仅 SEND/ECHO 方向需要:生成递增模式发送窗口 */
#if PHASE8_DIRECTION != PHASE8_DIR_RECV
static void phase8_fill(uint8_t *buf, uint32_t size, uint32_t offset) {
for (uint32_t i = 0; i < size; i++) {
buf[i] = (uint8_t)((offset + i) & 0xFF);
}
}
#endif
/* 仅 ECHO/RECV 方向需要:逐字节校验接收数据 */
#if PHASE8_DIRECTION != PHASE8_DIR_SEND
static int32_t phase8_verify(uint8_t *buf, uint32_t size, uint32_t offset) {
for (uint32_t i = 0; i < size; i++) {
if (buf[i] != (uint8_t)((offset + i) & 0xFF)) {
DBG_ERROR("Phase8: mismatch at %lu (expect 0x%02X, got 0x%02X)",
offset + i, (uint8_t)((offset + i) & 0xFF), buf[i]);
return 0;
}
}
return 1;
}
#endif
/* 用例号:TC-NET-801(SEND 方向)与 TC-NET-802(RECV 方向)——两向由
* PHASE8_DIRECTION 编译期切换,定义见 docs/测试说明/板级测试/CH395F_Test_Guide.md
* 阶段 8(本函数体从未改过名,只是一直没挂上号,2026-10-21 核对补注)。 */
static void phase8_run(int32_t sockfd) {
#if PHASE8_DIRECTION == PHASE8_DIR_RECV
/* PC 发送文件(尾附 4B CRC32),MCU 接收并逐字节 + CRC32 校验 */
DBG_INFO("Phase8[RECV]: receiving %d-byte file", PHASE8_FILE_SIZE);
uint32_t recvd = 0;
uint32_t crc = 0xFFFFFFFFu;
while (recvd < PHASE8_FILE_SIZE) {
int32_t to_read = (PHASE8_FILE_SIZE - recvd) > (TEST_BUF_SIZE - 4)
? (TEST_BUF_SIZE - 4) : (int)(PHASE8_FILE_SIZE - recvd);
int32_t n = net_recv(sockfd, s_rx_buf, to_read, 0);
if (n <= 0) {
DBG_ERROR("Phase8[RECV]: recv fail at %lu (ret=%d)", recvd, n);
return;
}
int32_t ok = phase8_verify(s_rx_buf, (uint32_t)n, recvd);
TEST_CHECK(ok, "Phase8[RECV]: verify at %lu (%d bytes)", recvd, n);
if (!ok) return;
crc = phase8_crc32_step(crc, s_rx_buf, (uint32_t)n);
recvd += (uint32_t)n;
}
/* 读取 PC 附在文件末尾的 4 字节 CRC32(小端) */
uint8_t foot[4];
uint32_t got = 0;
while (got < 4) {
int32_t n = net_recv(sockfd, foot + got, (int)(4 - got), 0);
if (n <= 0) {
DBG_ERROR("Phase8[RECV]: footer recv fail (ret=%d)", n);
return;
}
got += (uint32_t)n;
}
uint32_t crc_exp = crc ^ 0xFFFFFFFFu;
uint32_t crc_act = ((uint32_t)foot[0]) | ((uint32_t)foot[1] << 8)
| ((uint32_t)foot[2] << 16) | ((uint32_t)foot[3] << 24);
TEST_CHECK(crc_act == crc_exp,
"Phase8[RECV]: CRC32 exp=0x%08X act=0x%08X", crc_exp, crc_act);
DBG_INFO("Phase8[RECV]: done, CRC32=0x%08X", crc_exp);
#elif PHASE8_DIRECTION == PHASE8_DIR_SEND
/* MCU 发送文件,PC 落盘校验(PC 侧自行比对字节/CRC32) */
DBG_INFO("Phase8[SEND]: sending %d-byte file", PHASE8_FILE_SIZE);
uint32_t sent = 0;
while (sent < PHASE8_FILE_SIZE) {
/* 每次填充一个滑动窗口(≤ s_tx_buf 容量);net_send 内部按硬件发送 FIFO
* (NET_SEND_CHUNK_MAX=1KB,见 TRAP-NET-16) 自动拆分写入,无需此处逐 1KB 分块 */
uint32_t chunk = (PHASE8_FILE_SIZE - sent) > (uint32_t)TEST_BUF_SIZE
? (uint32_t)TEST_BUF_SIZE : (PHASE8_FILE_SIZE - sent);
phase8_fill(s_tx_buf, chunk, sent);
int32_t n = net_send(sockfd, s_tx_buf, (int)chunk, 0);
if (n <= 0) {
DBG_ERROR("Phase8[SEND]: send fail at %lu (ret=%d)", sent, n);
return;
}
sent += (uint32_t)n;
}
DBG_INFO("Phase8[SEND]: sent %d bytes", PHASE8_FILE_SIZE);
TEST_CHECK(1, "Phase8[SEND]: %d-byte file sent", PHASE8_FILE_SIZE);
#else /* PHASE8_DIR_ECHO */
/* 发 100KB 经 PC 回显(tcp_server --max-conn 1),MCU 收齐逐字节校验 */
DBG_INFO("Phase8[ECHO]: transferring %d-byte file", PHASE8_FILE_SIZE);
uint32_t sent = 0;
while (sent < PHASE8_FILE_SIZE) {
/* 同 SEND:填充滑动窗口(≤ s_tx_buf),由 net_send 内部按硬件 FIFO 自动拆分 */
uint32_t chunk = (PHASE8_FILE_SIZE - sent) > (uint32_t)TEST_BUF_SIZE
? (uint32_t)TEST_BUF_SIZE : (PHASE8_FILE_SIZE - sent);
phase8_fill(s_tx_buf, chunk, sent);
int32_t n = net_send(sockfd, s_tx_buf, (int)chunk, 0);
if (n <= 0) {
DBG_ERROR("Phase8[ECHO]: send fail at %lu (ret=%d)", sent, n);
return;
}
sent += (uint32_t)n;
}
uint32_t recvd = 0;
while (recvd < PHASE8_FILE_SIZE) {
int32_t n = net_recv(sockfd, s_rx_buf, TEST_BUF_SIZE, 0);
if (n <= 0) {
DBG_ERROR("Phase8[ECHO]: recv fail at %lu (ret=%d)", recvd, n);
return;
}
int32_t ok = phase8_verify(s_rx_buf, (uint32_t)n, recvd);
TEST_CHECK(ok, "Phase8[ECHO]: verify at %lu (%d bytes)", recvd, n);
if (!ok) return;
recvd += (uint32_t)n;
}
TEST_CHECK(1, "Phase8[ECHO]: %d-byte file transfer complete", PHASE8_FILE_SIZE);
#endif
/* 一次性传输完成:主动关闭数据连接,触发 net 层自动重监听。
* 否则 accept 循环会在“仍打开的同一条连接”上重复执行本 Phase——
* SEND/RECV 由 PC 端先关闭尚可终止,ECHO 方向 PC 回显客户端不会主动
* 关闭,会导致 MCU 无限重复、PC 端 tcp_file --op echo 一直挂起。 */
net_close(sockfd);
}
#endif
/* ===================================================================
* Phase 9 — Select/Poll I/O 多路复用
*
* 测试目的:验证 net_select() API 在单 Socket 模式下正确工作:
* 1. Socket 0 有数据到达时 select 返回正数
* 2. NET_FD_ISSET 正确指示就绪的 Socket
* 3. 超时(50ms 无数据)时返回 0
*
* 入口条件:Socket 0 处于 ESTABLISHED。
* 通过:select 检测到数据并成功回显。
* 失败:select 未检测到数据或回显失败。
* 用例号:TC-NET-901(单 Socket 就绪检测)与 TC-NET-902(超时返回 0),
* 定义见 docs/测试说明/板级测试/CH395F_Test_Guide.md 阶段 9——本测试一直在,
* 只是从未挂上这两个号(2026-10-21 核对补注)。
* =================================================================== */
#ifdef ENABLE_PHASE9_TESTS
#include "net_select.h"
static uint32_t s_phase9_count = 0;
static void phase9_run(int32_t sockfd) {
net_fd_set_t readfds;
net_timeval_t tv;
/*
* 配置 select:只监测 Socket 0 的可读状态,超时 50ms。
* NET_FD_ZERO 清空集合,NET_FD_SET 添加 Socket 0。
* 50ms 超时确保不阻塞任务循环太久。
*/
NET_FD_ZERO(&readfds);
NET_FD_SET(sockfd, &readfds);
tv.tv_sec = 0;
tv.tv_usec = 50000;
int32_t nfds = net_select(sockfd + 1, &readfds, NULL, NULL, &tv);
if (nfds <= 0) return;
/* Socket 0 可读:非阻塞接收并回显 */
if (NET_FD_ISSET(sockfd, &readfds)) {
int32_t n = net_recv(sockfd, s_rx_buf, TEST_BUF_SIZE, NET_MSG_DONTWAIT);
if (n > 0) {
int32_t sent = net_send(sockfd, s_rx_buf, n, 0);
if (sent == n) {
s_phase9_count++;
TEST_CHECK(1, "Phase9: select echo #%lu (%d bytes)", s_phase9_count, n);
}
}
}
}
#endif
/* ===================================================================
* Phase 10 — 边界条件测试
*
* 测试目的:验证以下 DMA 和协议栈边界情况:
* 1. TC-NET-1001: 4095B(SPI 最大载荷 -1,单次 DMA 容纳)
* 2. TC-NET-1002: 4096B(= SPI 最大载荷,填满单次 DMA)
* 3. TC-NET-1003: 4097B(超过 SPI 最大载荷,拆 2 次 DMA)
* 4. TC-NET-1004: 零长度发送(返回 -1)
* 5. TC-NET-1005: 20 × 100B 连续快速发送
*
* 各用例通过状态机(s_phase10_stage)串行执行,每次 phase10_run 推进一个
* 步骤(发送→等待→接收→验证→下一阶段)。发送数据为递增模式字节
* (buf[i] = seed + i),接收端按相同模式验证。
*
* 入口条件:Socket 0 处于 ESTABLISHED。
* 通过:各用例自身判定。
* =================================================================== */
#ifdef ENABLE_PHASE10_TESTS
static int32_t s_phase10_stage = 0;
static uint32_t s_phase10_recv = 0;
static void phase10_fill(uint8_t *buf, uint32_t size, uint8_t seed) {
for (uint32_t i = 0; i < size; i++) {
buf[i] = (uint8_t)(seed + i);
}
}
static int32_t phase10_verify(uint8_t *buf, uint32_t size, uint8_t seed) {
for (uint32_t i = 0; i < size; i++) {
if (buf[i] != (uint8_t)(seed + i)) return 0;
}
return 1;
}
static void phase10_run(int32_t sockfd) {
/* 阶段 0 → 1:发送 4095B */
if (s_phase10_stage == 0) {
DBG_INFO("Phase10: 4095B (DMA-1)");
phase10_fill(s_tx_buf, 4095, 0x10);
int32_t sent = net_send(sockfd, s_tx_buf, 4095, 0);
TEST_CHECK(sent == 4095, "Phase10: send 4095B (sent=%d)", sent);
s_phase10_recv = 0;
s_phase10_stage = 1;
return;
}
/* 阶段 1:接收并验证 4095B 回显 */
if (s_phase10_stage == 1) {
int32_t n = net_recv(sockfd, s_rx_buf + s_phase10_recv, (int)(4095 - s_phase10_recv), 0);
if (n > 0) {
s_phase10_recv += (uint32_t)n;
if (s_phase10_recv >= 4095) {
TEST_CHECK(phase10_verify(s_rx_buf, 4095, 0x10), "Phase10: verify 4095B");
s_phase10_stage = 2;
}
}
return;
}
/* 阶段 2 → 3:发送 4096B(DMA 边界精确值) */
if (s_phase10_stage == 2) {
DBG_INFO("Phase10: 4096B (DMA exact)");
phase10_fill(s_tx_buf, 4096, 0x20);
int32_t sent = net_send(sockfd, s_tx_buf, 4096, 0);
TEST_CHECK(sent == 4096, "Phase10: send 4096B (sent=%d)", sent);
s_phase10_recv = 0;
s_phase10_stage = 3;
return;
}
/* 阶段 3:接收并验证 4096B 回显 */
if (s_phase10_stage == 3) {
int32_t n = net_recv(sockfd, s_rx_buf + s_phase10_recv, (int)(4096 - s_phase10_recv), 0);
if (n > 0) {
s_phase10_recv += (uint32_t)n;
if (s_phase10_recv >= 4096) {
TEST_CHECK(phase10_verify(s_rx_buf, 4096, 0x20), "Phase10: verify 4096B");
s_phase10_stage = 4;
}
}
return;
}
/* 阶段 4 → 5:发送 4097B(超过 DMA 上限,自动拆 4096+1) */
if (s_phase10_stage == 4) {
DBG_INFO("Phase10: 4097B (DMA+1, 2 SPI xacts)");
phase10_fill(s_tx_buf, 4097, 0x30);
int32_t sent = net_send(sockfd, s_tx_buf, 4097, 0);
TEST_CHECK(sent == 4097, "Phase10: send 4097B (sent=%d)", sent);
s_phase10_recv = 0;
s_phase10_stage = 5;
return;
}
/* 阶段 5:接收并验证 4097B 回显 */
if (s_phase10_stage == 5) {
int32_t n = net_recv(sockfd, s_rx_buf + s_phase10_recv, (int)(4097 - s_phase10_recv), 0);
if (n > 0) {
s_phase10_recv += (uint32_t)n;
if (s_phase10_recv >= 4097) {
TEST_CHECK(phase10_verify(s_rx_buf, 4097, 0x30), "Phase10: verify 4097B");
s_phase10_stage = 6;
}
}
return;
}
/* 阶段 6:零长度发送 —— 期望 net_send 返回 -1(NET_ERR_INVAL) */
if (s_phase10_stage == 6) {
DBG_INFO("Phase10: zero-length send");
int32_t sent = net_send(sockfd, s_tx_buf, 0, 0);
TEST_CHECK(sent < 0, "Phase10: send 0B returns %d (expect <0)", sent);
s_phase10_stage = 7;
return;
}
/* 阶段 7:20 × 100B 快速发送 */
if (s_phase10_stage == 7) {
DBG_INFO("Phase10: 20x rapid sends (100B each)");
phase10_fill(s_tx_buf, 100, 0x40);
int32_t all_ok = 1;
for (int32_t i = 0; i < 20; i++) {
if (net_send(sockfd, s_tx_buf, 100, 0) != 100) {
TEST_CHECK(0, "Phase10: rapid #%d fail", i + 1);
all_ok = 0; break;
}
osDelay(1);
}
if (all_ok) {
TEST_CHECK(1, "Phase10: 20x rapid sends OK");
s_phase10_recv = 0;
s_phase10_stage = 8;
} else {
s_phase10_stage = 0;
}
return;
}
/* 阶段 8:接收 2000B 回显总和并验证 */
if (s_phase10_stage == 8) {
int32_t n = net_recv(sockfd, s_rx_buf + s_phase10_recv, (int)(2000 - s_phase10_recv), 0);
if (n > 0) {
s_phase10_recv += (uint32_t)n;
if (s_phase10_recv >= 2000) {
TEST_CHECK(1, "Phase10: all echo received");
TEST_REPORT("Phase 10");
s_phase10_stage = 0;
}
}
return;
}
}
#endif
/* ===================================================================
* Phase 11 — TCP KeepAlive 超时断开
*
* 测试目的:验证 CH395F 的 TCP KeepAlive 机制(服务器侧开关已由 net 层
* 在 CONNECT 事件后按 §5.56 转正开启,E11 2026-10-13):
* 1. 连接空闲超过 IDLE(60s)后芯片开始发送探测包
* 2. CNT × INTVL(3 × 5s)无 ACK → SINT_STAT_TIM_OUT
* 3. 本层 QUARANTINE → pump 重建 → 回 LISTEN
*
* 入口条件:Socket 处于 ESTABLISHED 且对端为**死对端**——PC 连 8080 后
* 禁用网卡(存活对端会 ACK 探测、TIMEOUT 永不触发,本测试将
* 一直等待;防火墙规则对已建立连接无效,E11 实证)。
* 通过:从连接建立到 recv 返回 0 的间隔落在 10s~120s 有界窗
* (芯片定时语义与手册公式 75s 不符,实测判死 18~31s——
* 见《网络自愈架构设计》§5.2 E11 与 phase11_run 内判据注释)。
* 用例号:TC-NET-1101(docs/测试说明/板级测试/CH395F_Test_Guide.md 阶段 11 KeepAlive 触发断开)。
* =================================================================== */
#ifdef ENABLE_PHASE11_TESTS
static int32_t s_phase11_armed = 0;
static uint32_t s_phase11_start = 0;
static uint8_t s_phase11_buf[64]; /* 专用小缓冲:不占 32KB s_rx_buf(RAM 余量) */
/* 返回值:0 = 测试进行中;1 = 已报告完成(主循环据此释放 phase_active,
* 等待**新连接**再测。勿删本机制——报告后连接已死,recv 恒返 0,
* 继续 arm 会以 10ms 节奏刷出数百条假 FAIL(2026-10-13 实跑实证) */
static int phase11_run(int32_t sockfd) {
/*
* 第一次调用:记录开始时间并 arm。
* 后续调用:非阻塞 recv,若返回 0 表示连接已断开(KeepAlive 触发)。
* 计算耗时与有界窗判据比较。
*/
if (!s_phase11_armed) {
s_phase11_armed = 1;
s_phase11_start = HAL_GetTick();
DBG_INFO("Phase11: idle wait for KeepAlive timeout (60s)");
return 0;
}
int32_t n = net_recv(sockfd, s_phase11_buf, 64, NET_MSG_DONTWAIT);
if (n == 0) {
uint32_t elapsed = HAL_GetTick() - s_phase11_start;
/* 判据(E11 实测修正版):芯片定时语义与手册公式(75s)不符——
* 生产参数实测判死窗口 18~31s(产品验证 连接+30.5s、E11 T5
* 对端失联+30s、T4 短参数 连接+~10s)。本 socket 无在途 TX、
* 对端静默且已失联 → 唯一开枪源是 keepalive,故判据为
* **有界窗**:>=10s(存活空闲会话 空闲存活场景实测 90s 不被杀,10s 下限
* 区分"误杀"与"keepalive 判死")且 <=120s(判死必须发生)。
* 勿改回 >=60000——那是对着手册公式写的,会在正确固件上假 FAIL */
TEST_CHECK(elapsed >= 10000U && elapsed <= 120000U,
"Phase11: KeepAlive triggered after %lums (expect 10000..120000ms)",
elapsed);
TEST_REPORT("Phase 11");
s_phase11_armed = 0;
return 1;
}
return 0;
}
#endif
void net_layer_test_main(void)
{
int32_t sockfd = -1;
int32_t phase_active = 0;
uint32_t accept_retry = 0;
uint32_t gen = 0; /* 建立监听时的网络代次(代次变化 = 网络已重建) */
struct net_sockaddr_in client_addr;
int32_t addrlen = sizeof(client_addr);
sockfd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
if (sockfd < 0) {
DBG_ERROR("Test task: net_socket failed");
return;
}
DBG_INFO("Test task: socket %d created", sockfd);
{
struct net_sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = NET_AF_INET;
addr.sin_port = net_htons(8080);
if (net_bind(sockfd, (struct net_sockaddr *)&addr, sizeof(addr)) < 0) {
DBG_ERROR("Test task: net_bind failed");
return;
}
DBG_INFO("Test task: bound to port 8080");
}
if (net_listen(sockfd) < 0) {
DBG_ERROR("Test task: net_listen failed");
return;
}
DBG_INFO("Test task: listening on 8080 (single-socket mode)");
gen = net_get_generation(); /* 记录本监听对应的网络代次 */
/* ==================== 事件驱动阶段 ====================
* Phase 6/8~11(NET 层)在 TCP 监听循环中运行,需要 PC 客户端连接。
* (Phase 7 TCP Client 为独立入口 net_layer_client_test_main,不在本循环。)
* 没有连接时循环等待;连接建立后执行对应 Phase。
* 断开后自动重新 listen。
*/
for (;;) {
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) {
/* 网络重建(代次变化)→ 旧监听随芯片复位失效,重建 8080 监听 */
if (net_get_generation() != gen) {
struct net_sockaddr_in addr;
DBG_INFO("Test task: network rebuilt, relisten 8080");
net_close(sockfd);
sockfd = net_socket(NET_AF_INET, NET_SOCK_STREAM);
memset(&addr, 0, sizeof(addr));
addr.sin_family = NET_AF_INET;
addr.sin_port = net_htons(8080);
if (sockfd < 0 ||
net_bind(sockfd, (struct net_sockaddr *)&addr, sizeof(addr)) < 0 ||
net_listen(sockfd) < 0) {
DBG_ERROR("Test task: relisten failed");
return;
}
gen = net_get_generation();
accept_retry = 0;
continue;
}
if (accept_retry % 100 == 0) {
DBG_INFO("Test task: waiting for connection...");
}
accept_retry++;
osDelay(100);
continue;
}
accept_retry = 0;
phase_active = 1;
DBG_INFO("Test task: client accepted (fd=%d)", acc);
/* 连接建立,重置各 Phase 内部状态 */
#ifdef ENABLE_PHASE10_TESTS
s_phase10_stage = 0;
#endif
#ifdef ENABLE_PHASE11_TESTS
s_phase11_armed = 0;
#endif
}
/* 每 10ms 依次执行各已启用 Phase */
#ifdef ENABLE_PHASE6_TESTS
phase6_run(sockfd);
#endif
#ifdef ENABLE_PHASE8_TESTS
phase8_run(sockfd);
phase_active = 0; /* Phase 8 一次性完成,发完即止 */
#endif
#ifdef ENABLE_PHASE9_TESTS
phase9_run(sockfd);
#endif
#ifdef ENABLE_PHASE10_TESTS
phase10_run(sockfd);
#endif
#ifdef ENABLE_PHASE11_TESTS
if (phase11_run(sockfd)) {
phase_active = 0; /* 已报告:释放主循环去 accept 新连接(net 层已 auto_relisten) */
}
#endif
#ifdef ENABLE_PHASE13_TESTS
/* Phase 13(链路时延 CHUNK_WAIT~FRAME_RT/LOGTAX,M2):独立运行,勿与其他 Phase 混选;
* 注意此处**不**重置 phase_active —— 保持 10ms 节拍(时延测量对
* 循环周期敏感,Phase 8 式 accept 重入的 100ms 退避会污染样本) */
net_latency_phase13_run(sockfd);
#endif
#ifdef ENABLE_PHASE14_TESTS
/* Phase 14(生产路径时延 PROD_CYCLE~UI_LOOP,M3):不占测试 socket —— 测量真实
* uiTask + ui_net_sink(:7003) 生产路径,由 peer --mode remo 驱动;
* 本调用只是探针窗口状态机(arm/disarm/report),同样保持 10ms 节拍 */
net_latency_phase14_run();
#endif
/*
* 单连接模式:连接断开由 net 层自动重监听(auto_relisten),
* 应用层不巡检/不重监听,回显循环持续服务新连接。
*/
osDelay(10);
}
}
/* ===================================================================
* Phase 16 — BSD UDP 数据面真机回归(2026-11-03 UDP 队列化改造,PR #74)
*
* 测试目的:把 net_socket.c/net_udp.c 这条「UDP 收发并入 netMsgQueue、在
* netTask 内串行执行」的新链路放上真芯片跑一遍,逐条对上
* docs/测试说明/单元测试/主机单测覆盖率口径.md 第 4 轮登记的
* 「台架必验」五项:
* 1602 → ① server 形态首包(bind 时 DesIP 已写在全F)+ ⑤ 顺序
* 1603 → TRAP-NET-22 缺陷本体:48 字节小应答在 client 形态下
* 必须一字节不少(旧实现恒剥 8 字节 → 这里会变成 40)
* 1604 → ② sendto 切形态后能否真恢复收其它源地址
* 1605 → ③ 截断包排空后下一个包是否完整
* 1607 → ④ 挂起 recv 等待期间的 IWDG 余量(无 [ERR][WD] task stall)
* 1608 → ⑤ 重新 open 后第一个包就要能收到
* 1609 → 分块边界(发送缓冲 1KB)与 1472 字节满 MTU 数据报
*
* 板端角色:UDP **服务端**(先 bind,由收到的包学出对端地址再回显)。
* 这样 PC 侧不需要固定 IP——台架机器是 DHCP,与 net_test_task.h 里
* TEST_PC_PEER_IP 的 192.168.31.2 并不一致,写死会让本阶段只能在那台机器上跑。
*
* PC 端:python test/host/udp_queue_bench.py --board <板 IP>
* 两侧的步骤表(tag/长度/端口/源套接字)必须一一对应,改一边就改另一边。
*
* 缓冲:本阶段自带 1600 B 静态缓冲(>1472 上限,≤2KB 约定),不复用 32KB 的
* s_rx_buf——阶段 11 那条「产品 RAM 余量放不下 32KB」的门控说明同样适用于此。
* =================================================================== */
#ifdef ENABLE_PHASE16_TESTS
#define UDP_BENCH_PORT 6001U /* 阶段前半段的服务端端口 */
#define UDP_BENCH_PORT2 6002U /* 关闭重开后换的端口(证明首包不丢) */
#define UDP_BENCH_BUF_SIZE 1600 /* 单包上限 1472 + 余量 */
#define UDP_BENCH_WAIT_MS 25000U /* 每步等 PC 的预算:PC 缺席时报 FAIL 而不是挂死 */
#define UDP_BENCH_QUICK_MS 1000U /* 超时判据用的短预算 */
#define UDP_BENCH_LATE_BUDGET 30000U /* 迟发包那一步的等待预算(比 PC 迟发时刻宽得多) */
#define UDP_BENCH_LATE_MIN_MS 7000U /* 迟发包实测等待的下界 */
#define UDP_BENCH_LATE_MAX_MS 25000U /* 迟发包实测等待的上界 */
#define UDP_BENCH_STALE_MAX 8U /* 单步最多丢掉几个旧步数据报(步序自愈的上限) */
static uint8_t s_udp_buf[UDP_BENCH_BUF_SIZE];
/*
* 函数功能:填充测试载荷(自校验模式,PC 端同式)
* 入口参数:p - 缓冲区 uint8_t * n - 长度 int tag - 步号(首字节)
* 出口参数:p - p[0]=tag,p[i]=(i*7+0x11)&0xFF (i>=1)
* 返回值:无
* 限定条件:n>=0
* 函数说明:模式与步号一起写,接收方只凭收到的字节就能判断
* 「是不是这一步的包」以及「有没有被剥头/错位」——
* 少 8 字节会让 p[1] 不再是 0x18,错位一眼可见。
*/
static void udp_bench_fill(uint8_t *p, int n, uint8_t tag)
{
int i;
if (n <= 0) {
return;
}
p[0] = tag;
for (i = 1; i < n; i++) {
p[i] = (uint8_t)(((uint32_t)i * 7U + 0x11U) & 0xFFU);
}
}
/*
* 函数功能:校验载荷模式
* 入口参数:p - 缓冲区 const uint8_t * n - 长度 int tag - 期望步号
* 出口参数:无
* 返回值:0 = 完全一致;否则返回第一个不符的字节下标 + 1(0 号位错返回 1)
* 限定条件:n>=0
* 函数说明:返回值直接进日志,指到字节号就能区分「整体少 8 字节」
* 与「只有尾部缺」这两类完全不同的故障。
*/
static int udp_bench_match(const uint8_t *p, int n, uint8_t tag)
{
int i;
if (n <= 0) {
return 0;
}
if (p[0] != tag) {
return 1;
}
for (i = 1; i < n; i++) {
if (p[i] != (uint8_t)(((uint32_t)i * 7U + 0x11U) & 0xFFU)) {
return i + 1;
}
}
return 0;
}
/*
* 函数功能:等一个数据报并校验(步号 + 长度 + 模式 + 源地址元数据)
* 入口参数:fd - Socket int tag - 期望步号 uint8_t
* exp - 期望字节数(截断步骤填截断后的长度)int
* cap - 本步接收上限 int to_ms - 等待预算 uint32_t
* src - 源地址输出(可为 NULL) struct net_sockaddr_in *
* who - 用例号(进日志) const char *
* 出口参数:src - 学到的对端地址(回显要用)
* 返回值:实际收到的字节数(>=0);-1 = 失败(已打日志)
* 限定条件:已由调用方 bind 好;buf 用 s_udp_buf
* 函数说明:源地址元数据(addrlen/family)也在本函数判,因为「客户端形态
* 下源地址退化成垃圾」正是 TRAP-NET-22 的后半段现象,
* 只在 PC 侧判会让板端日志失去定位能力。
*/
static int udp_bench_expect(int fd, uint8_t tag, int exp, int cap, uint32_t to_ms,
struct net_sockaddr_in *src, const char *who)
{
struct net_sockaddr_in from;
int alen = (int)sizeof(from);
uint32_t t_start;
uint32_t left_ms;
int stale = 0;
int n;
int bad;
/*
* 函数说明(步序自愈):步号写在载荷第 0 字节,所以先认步号再认内容。比本步小的
* = 上一步超时后对端重发留下的旧包,丢掉继续等;比本步大的 = 对端抢跑(不该发生,
* 报出来好定位)。没有这层判断时,任何一步只要超时一次,后面每一步都会错位 25 秒
* ——2026-11-03 第一次上台架就是这样:1602 通了,1603 起整条梯子在 PC 侧 8 秒预算
* 里全灭,分不清是板子的问题还是纯粹的错位。只允许有限次丢弃,防止对端脚本写错时
* 把包无限吞掉。
*/
t_start = HAL_GetTick();
for (;;) {
if ((HAL_GetTick() - t_start) >= to_ms) {
/* to_ms==0 在 net_recvfrom_to 里是「非法参数」(0 = 无限等由另一个入口表达),
* 预算见底时不能再拿 0 去调它,否则报出来的是 INVAL 而不是超时。 */
DBG_ERROR("%s: budget %ums exhausted before tag 0x%02X (stale drops=%d)",
who, (unsigned)to_ms, (unsigned)tag, stale);
return -1;
}
left_ms = to_ms - (HAL_GetTick() - t_start);
(void)memset(&from, 0, sizeof(from));
alen = (int)sizeof(from);
n = net_recvfrom_to(fd, s_udp_buf, cap, left_ms,
(src != NULL) ? (struct net_sockaddr *)&from : NULL,
(src != NULL) ? &alen : NULL);
if (n < 0) {
DBG_ERROR("%s: recv tag 0x%02X -> %d (errno=%d)", who, (unsigned)tag, n, net_get_errno());
return -1;
}
if (n < 1 || s_udp_buf[0] != tag) {
if ((n >= 1) && (s_udp_buf[0] < tag) && ((unsigned)stale < UDP_BENCH_STALE_MAX)) {
stale++;
DBG_INFO("%s: drop stale tag 0x%02X (drop #%d), still waiting for 0x%02X",
who, (unsigned)s_udp_buf[0], stale, (unsigned)tag);
continue;
}
DBG_ERROR("%s: tag 0x%02X got 0x%02X (n=%d, stale drops=%d)",
who, (unsigned)tag, (n >= 1) ? (unsigned)s_udp_buf[0] : 0U, n, stale);
return -1;
}
if (n != exp) {
DBG_ERROR("%s: tag 0x%02X len=%d expect=%d", who, (unsigned)tag, n, exp);
return -1;
}
bad = udp_bench_match(s_udp_buf, n, tag);
if (bad != 0) {
DBG_ERROR("%s: tag 0x%02X pattern breaks at byte %d (got 0x%02X, want 0x%02X)",
who, (unsigned)tag, bad - 1, s_udp_buf[bad - 1],
(bad == 1) ? (unsigned)tag : (uint8_t)(((uint32_t)(bad - 1) * 7U + 0x11U) & 0xFFU));
return -1;
}
break;
}
if (src != NULL) {
TEST_CHECK(alen == (int)sizeof(struct net_sockaddr_in),
"%s: src addrlen=%d (expect %d)", who, alen, (int)sizeof(struct net_sockaddr_in));
TEST_CHECK(from.sin_family == NET_AF_INET, "%s: src family=%d (expect %d)",
who, (int)from.sin_family, (int)NET_AF_INET);
*src = from;
DBG_INFO("%s: tag 0x%02X %dB from port %u OK", who, (unsigned)tag, n,
(unsigned)net_ntohs(from.sin_port));
} else {
DBG_INFO("%s: tag 0x%02X %dB OK", who, (unsigned)tag, n);
}
return n;
}
/*
* 函数功能:把刚收到的数据报原样回显给对端
* 入口参数:fd - Socket int n - 长度 int
* dst - 目的地址;NULL = 走芯片 DesIP 记账(客户端形态)
* who - 用例号
* 出口参数:无
* 返回值:net_sendto 的返回值(= n 才算成功)
* 限定条件:s_udp_buf 里就是那 n 个字节
* 函数说明:dst 是否为 NULL 决定走「显式目的地址(会切形态)」还是
* 「已记账对端(不碰寄存器)」两条不同的发送路径,1603 特意用
* NULL 把后者也压上台架。
*/
static int udp_bench_echo(int fd, int n, const struct net_sockaddr_in *dst, const char *who)
{
int rc;
rc = net_sendto(fd, s_udp_buf, n, 0,
(dst != NULL) ? (const struct net_sockaddr *)dst : NULL,
(dst != NULL) ? (int)sizeof(struct net_sockaddr_in) : 0);
if (rc != n) {
DBG_ERROR("%s: echo %dB -> %d (errno=%d)", who, n, rc, net_get_errno());
}
return rc;
}
void net_layer_udp_bench_test_main(void)
{
struct net_sockaddr_in me;
struct net_sockaddr_in peer;
uint32_t gen0;
uint32_t t0;
uint32_t dt;
int fd;
int rc;
int n;
int alen;
int i;
/* 分块边界尺寸表:1024 = 发送缓冲整块,1025 = 刚跨块,1472 = 满 MTU 数据报 */
static const int s_sizes[5] = { 1, 20, 1024, 1025, 1472 };
DBG_INFO("=== CH395F Phase 16 Tests (BSD UDP Queue) ===");
DBG_INFO("PC peer: python test/host/udp_queue_bench.py --board <ip> --port %u",
(unsigned)UDP_BENCH_PORT);
/* ---------- TC-NET-1600:模式函数自检(先信校验收件,再信它判的包) ---------- */
DBG_INFO("--- TC-NET-1600: pattern self-check ---");
udp_bench_fill(s_udp_buf, 64, 0xAA);
TEST_CHECK(udp_bench_match(s_udp_buf, 64, 0xAA) == 0, "TC-NET-1600a: fill/match agree");
s_udp_buf[9] = (uint8_t)(s_udp_buf[9] ^ 0xFFU);
TEST_CHECK(udp_bench_match(s_udp_buf, 64, 0xAA) == 10, "TC-NET-1600b: matcher points at byte 9");
udp_bench_fill(s_udp_buf, 64, 0xAA);
s_udp_buf[0] = 0xBB;
TEST_CHECK(udp_bench_match(s_udp_buf, 64, 0xAA) == 1, "TC-NET-1600c: wrong tag caught at slot 1");
(void)memset(s_udp_buf, 0, UDP_BENCH_BUF_SIZE);
/* ---------- TC-NET-1601:socket + bind(服务端形态) ---------- */
DBG_INFO("--- TC-NET-1601: socket/bind -> server mode ---");
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
TEST_CHECK(fd >= 0, "TC-NET-1601a: udp socket fd=%d", fd);
if (fd < 0) {
TEST_REPORT("Phase 16");
return;
}
(void)memset(&me, 0, sizeof(me));
me.sin_family = NET_AF_INET;
me.sin_port = net_htons((uint16_t)UDP_BENCH_PORT);
me.sin_addr.s_addr = 0U;
rc = net_bind(fd, (const struct net_sockaddr *)&me, (int)sizeof(me));
TEST_CHECK(rc == 0, "TC-NET-1601b: bind :%u rc=%d", (unsigned)UDP_BENCH_PORT, rc);
TEST_CHECK(net_udp_get_mode(fd) == NET_UDP_MODE_SERVER,
"TC-NET-1601c: mode=SERVER after bind (%d)", net_udp_get_mode(fd));
if (rc != 0) {
net_close(fd);
TEST_REPORT("Phase 16");
return;
}
/* ---------- TC-NET-1602:服务端首包(必验①/⑤) ---------- */
DBG_INFO("--- TC-NET-1602: first datagram after bind (server mode) ---");
n = udp_bench_expect(fd, 0x01, 20, UDP_BENCH_BUF_SIZE, UDP_BENCH_WAIT_MS, &peer, "TC-NET-1602");
if (n < 0) {
DBG_ERROR("TC-NET-1602: peer absent? aborting phase 16");
net_close(fd);
TEST_REPORT("Phase 16");
return;
}
rc = udp_bench_echo(fd, n, &peer, "TC-NET-1602");
TEST_CHECK(rc == n, "TC-NET-1602d: echo %dB rc=%d", n, rc);
TEST_CHECK(net_udp_get_mode(fd) == NET_UDP_MODE_CLIENT,
"TC-NET-1602e: sendto switched mode to CLIENT (%d)", net_udp_get_mode(fd));
/* ---------- TC-NET-1603:48 字节小应答(TRAP-NET-22 本体) ---------- */
DBG_INFO("--- TC-NET-1603: 48-byte reply, no header strip in client mode ---");
n = udp_bench_expect(fd, 0x02, 48, UDP_BENCH_BUF_SIZE, UDP_BENCH_WAIT_MS, NULL, "TC-NET-1603");
TEST_CHECK(n == 48, "TC-NET-1603a: 48B intact in client mode (n=%d)", n);
if (n == 48) {
rc = udp_bench_echo(fd, n, NULL, "TC-NET-1603");
TEST_CHECK(rc == 48, "TC-NET-1603b: echo via booked peer (dest NULL) rc=%d", rc);
}
/* ---------- TC-NET-1604:切回服务端形态收另一个源端口(必验②) ---------- */
DBG_INFO("--- TC-NET-1604: set_peer(NULL) restores other-source receive ---");
rc = net_udp_set_peer(fd, NULL);
TEST_CHECK(rc == 0, "TC-NET-1604a: set_peer(NULL) rc=%d", rc);
TEST_CHECK(net_udp_get_mode(fd) == NET_UDP_MODE_SERVER,
"TC-NET-1604b: mode back to SERVER (%d)", net_udp_get_mode(fd));
n = udp_bench_expect(fd, 0x03, 20, UDP_BENCH_BUF_SIZE, UDP_BENCH_WAIT_MS, &peer, "TC-NET-1604");
TEST_CHECK(n == 20, "TC-NET-1604c: datagram from a NEW source port received (n=%d)", n);
if (n == 20) {
rc = udp_bench_echo(fd, n, &peer, "TC-NET-1604");
TEST_CHECK(rc == 20, "TC-NET-1604d: echo to the new source rc=%d", rc);
}
/* ---------- TC-NET-1605:截断 + 排空(必验③) ---------- */
DBG_INFO("--- TC-NET-1605: truncated datagram then a clean one ---");
n = udp_bench_expect(fd, 0x04, 64, 64, UDP_BENCH_WAIT_MS, &peer, "TC-NET-1605");
TEST_CHECK(n == 64, "TC-NET-1605a: 200B read as 64 (n=%d)", n);
if (n == 64) {
rc = udp_bench_echo(fd, n, &peer, "TC-NET-1605");
TEST_CHECK(rc == 64, "TC-NET-1605b: echo truncated 64B rc=%d", rc);
}
n = udp_bench_expect(fd, 0x05, 30, UDP_BENCH_BUF_SIZE, UDP_BENCH_WAIT_MS, &peer, "TC-NET-1605");
TEST_CHECK(n == 30, "TC-NET-1605c: next datagram clean, no leftovers (n=%d)", n);
if (n == 30) {
rc = udp_bench_echo(fd, n, &peer, "TC-NET-1605");
TEST_CHECK(rc == 30, "TC-NET-1605d: echo 30B rc=%d", rc);
}
/* ---------- TC-NET-1606:超时语义 -2 且不留脏槽 ---------- */
DBG_INFO("--- TC-NET-1606: recvfrom_to timeout returns -2 ---");
t0 = HAL_GetTick();
n = net_recvfrom_to(fd, s_udp_buf, UDP_BENCH_BUF_SIZE, UDP_BENCH_QUICK_MS, NULL, NULL);
dt = HAL_GetTick() - t0;
TEST_CHECK(n == -2, "TC-NET-1606a: timeout returns -2 not errno (n=%d)", n);
TEST_CHECK(dt >= UDP_BENCH_QUICK_MS && dt < UDP_BENCH_QUICK_MS + 3000U,
"TC-NET-1606b: waited %ums with a %ums budget", (unsigned)dt, (unsigned)UDP_BENCH_QUICK_MS);
/* ---------- TC-NET-1607:挂起等待被迟发包唤醒(必验④) ---------- */
DBG_INFO("--- TC-NET-1607: parked recv woken by a late datagram ---");
gen0 = net_get_generation();
t0 = HAL_GetTick();
alen = (int)sizeof(peer);
(void)memset(&peer, 0, sizeof(peer));
/* 用有界预算而不是无限等:等包机制两者同一条(登记挂起 + tick 结算),
* 但无限等在 PC 中途走开会把手工台架钉死在这里,判据却一分不多。 */
n = net_recvfrom_to(fd, s_udp_buf, UDP_BENCH_BUF_SIZE, UDP_BENCH_LATE_BUDGET,
(struct net_sockaddr *)&peer, &alen);
dt = HAL_GetTick() - t0;
TEST_CHECK(n == 24, "TC-NET-1607a: late datagram delivered (n=%d)", n);
TEST_CHECK(dt >= UDP_BENCH_LATE_MIN_MS && dt <= UDP_BENCH_LATE_MAX_MS,
"TC-NET-1607b: waited %ums (expect %u..%u) - IWDG margin lives in the boot log",
(unsigned)dt, (unsigned)UDP_BENCH_LATE_MIN_MS, (unsigned)UDP_BENCH_LATE_MAX_MS);
TEST_CHECK(gen0 == net_get_generation(), "TC-NET-1607c: no net rebuild while waiting (gen %lu -> %lu)",
(unsigned long)gen0, (unsigned long)net_get_generation());
if (n == 24) {
TEST_CHECK(udp_bench_match(s_udp_buf, n, 0x06) == 0, "TC-NET-1607d: payload pattern intact");
rc = udp_bench_echo(fd, n, &peer, "TC-NET-1607");
TEST_CHECK(rc == n, "TC-NET-1607e: echo late datagram rc=%d", rc);
}
/* ---------- TC-NET-1608:关闭重开后首包必达(必验⑤) ---------- */
DBG_INFO("--- TC-NET-1608: close/rebind on a new port, first packet lands ---");
net_close(fd);
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
TEST_CHECK(fd >= 0, "TC-NET-1608a: fresh udp socket fd=%d", fd);
if (fd >= 0) {
(void)memset(&me, 0, sizeof(me));
me.sin_family = NET_AF_INET;
me.sin_port = net_htons((uint16_t)UDP_BENCH_PORT2);
me.sin_addr.s_addr = 0U;
rc = net_bind(fd, (const struct net_sockaddr *)&me, (int)sizeof(me));
TEST_CHECK(rc == 0, "TC-NET-1608b: bind :%u rc=%d", (unsigned)UDP_BENCH_PORT2, rc);
TEST_CHECK(net_udp_get_mode(fd) == NET_UDP_MODE_SERVER,
"TC-NET-1608c: fresh socket starts in SERVER mode (%d)", net_udp_get_mode(fd));
n = udp_bench_expect(fd, 0x07, 20, UDP_BENCH_BUF_SIZE, UDP_BENCH_WAIT_MS, &peer, "TC-NET-1608");
TEST_CHECK(n == 20, "TC-NET-1608d: first packet on a just-opened socket (n=%d)", n);
if (n == 20) {
rc = udp_bench_echo(fd, n, &peer, "TC-NET-1608");
TEST_CHECK(rc == 20, "TC-NET-1608e: echo rc=%d", rc);
}
}
/* ---------- TC-NET-1609:分块边界与满 MTU 数据报 ---------- */
DBG_INFO("--- TC-NET-1609: chunk boundary sizes 1/20/1024/1025/1472 ---");
for (i = 0; i < 5; i++) {
uint8_t tag = (uint8_t)(0x08 + i);
int sz = s_sizes[i];
n = udp_bench_expect(fd, tag, sz, UDP_BENCH_BUF_SIZE, UDP_BENCH_WAIT_MS, &peer, "TC-NET-1609");
if (n != sz) {
TEST_CHECK(0, "TC-NET-1609%c: size %d received (n=%d)", (char)('a' + i), sz, n);
continue;
}
TEST_CHECK(1, "TC-NET-1609%c: size %d round trip intact", (char)('a' + i), sz);
rc = udp_bench_echo(fd, n, &peer, "TC-NET-1609");
TEST_CHECK(rc == n, "TC-NET-1609%c: echo %dB rc=%d", (char)('a' + i), n, rc);
}
net_close(fd);
TEST_REPORT("Phase 16");
}
#endif /* ENABLE_PHASE16_TESTS */