Files
STM32F4-Base/test/board/ch395f_test_task.c
T
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

1762 lines
76 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 Network Test Task
* 模块功能:CH395F 全阶段测试入口,统一在独立 FreeRTOS 任务中运行。
* 测试分两组:
* Phase 1~5(硬件层)— 阻塞式独占执行(各自有内部循环/等待),netTask 未运行
* Phase 4~11(NET 层)— 事件驱动,TCP 监听循环中运行(需 PC 客户端连接)
* 所有测试结果通过 TEST_CHECK/TEST_REPORT 宏输出到串口。
* 作者:王建锋
* 创建日期:2026-07-22
******************************************************************************/
#include <string.h>
#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"
#include "ch395f.h"
#include "test_config.h"
#include "storage_test.h"
#include "gd5f_test.h"
#include "sd2506_test.h"
#include "sys_events.h"
#define DBG_TAG "[TEST_TASK]"
#include "dbg_log.h"
/* 两种模式的分工:NET 层模式等 netTask 广播 SYS_EVT_NET_READY 后开始
* (见 ch395f_test_run);硬件层模式 netTask 自挂起(创建但不运行),
* 由本任务自行 net_stack_bring_up() 后独占 CH395F。 */
/*
* 全局测试统计数据,由 TEST_CHECK/TEST_REPORT 宏自动管理。
* 每次 TEST_CHECK 自增 total 并根据条件增 passed/failed;
* TEST_REPORT 汇总输出,不会自动清零 —— 每个 Phase 内自行 memset 重置。
*/
test_stats_t g_test_stats;
/* ===================================================================
* Phase 1 — 寄存器与 SPI 命令路径验证
*
* 测试目的:确认 CH395F 与 MCU 之间的 SPI 通信链路正常,所有底层命令
* 能够正确发送并得到预期回复。这是其他所有 Phase 的前提条件。
*
* 共 9 项测试,逐项执行,汇总报告。
* =================================================================== */
/* 解析点分十进制 IP 字符串为数组(避免引入 sscanf 库)。
* 仅硬件层 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;
int32_t i;
for (i = 0; i < 4; i++) {
val = 0;
while (*str >= '0' && *str <= '9') {
val = val * 10 + (*str - '0');
str++;
}
ip_arr[i] = (uint8_t)val;
if (*str == '.') {
str++;
}
}
}
#endif /* ENABLE_PHASE2_TESTS || ENABLE_PHASE5_TESTS */
#ifdef ENABLE_PHASE1_TESTS
static void phase1_run(void) {
uint8_t ip_info[20];
/* 全局中断寄存器读即清,唯一读者 = net_poll(唯一读者红线(docs/网络/网络自愈架构设计.md §7 I7);用户裁定 2026-10-13:
* 测试代码同样禁自行读取)。net_stack_bring_up() 的 set_phy 强制重协商会
* 锁存 GINT_STAT_PHY_CHANGE(0x0004)——由全局中断状态项内部双读自清,不在此预清。 */
DBG_INFO("=== CH395F Phase 1 Tests ===");
DBG_INFO("--- 9 test items ---");
/* ---- ch395f_check_exist() ----
* 原理:SPI 写入命令 0x06 (CMD_CHECK_EXIST) 后跟测试字节 0x57。
* CH395F 硬件将测试字节按位取反后返回,期望收到 ~0x57 = 0xA8。
* 通过:函数返回 CH395F_ERR_SUCCESS(0x00)= SPI 回显取反匹配。
* 失败:返回 CH395F_STATUS_NOT_DETECTED(0xFF)= SPI 通信异常或芯片未响应。
*
* 用例号:TC-NET-101(docs/测试说明/板级测试/CH395F_Test_Guide.md §2 阶段 1「SPI 命令路径
* 完整性」——该条目的各步就是本函数按序执行的九个测试项,项号见各项标注)。
*/
{
ch395f_status_t ret = ch395f_check_exist();
DBG_INFO(" ch395f_check_exist()");
DBG_INFO(" SPI write: CMD=0x06, DATA=0x57");
DBG_INFO(" expect: ~0x57 = 0xA8");
DBG_INFO(" actual: 0x%02X", (uint8_t)ret);
TEST_CHECK(ret == CH395F_ERR_SUCCESS,
"check_exist = 0x%02X (expect 0x00=~0x57=0xA8 match)", (uint8_t)ret);
}
/* ---- ch395f_get_version() ----
* 原理:SPI 写入命令 0x27 (CMD_GET_IC_VER),读取芯片版本字节。
* 通过:版本值 == 0x4A(CH395F 硬件版本)。
* 失败:版本值 ≠ 0x4A(芯片型号不匹配、未正确初始化或损坏)。
* 注意:不同批次的 CH395F 版本号可能不同,若更换芯片需更新此判断值。
*/
{
uint8_t ver = (uint8_t)ch395f_get_version();
DBG_INFO(" ch395f_get_version()");
DBG_INFO(" expect: 0x4A");
DBG_INFO(" actual: 0x%02X", ver);
TEST_CHECK(ver == 0x4A,
"version = 0x%02X (expect 0x4A)", ver);
}
/* ---- 设置 IP → 回读验证 ----
* 原理:ch395f_set_ip_addr() 将 IP 写入 CH395F 内部寄存器,
* 然后通过 ch395f_get_ip_inf() 回读完整网络信息(IP+GW+MASK)。
* 前 4 字节为 IP 地址。
* 通过:回读 IP[0..3] == 192.168.1.100(与写入值一致)。
* 失败:任意字节不匹配(SPI 写入/回读路径异常或寄存器未更新)。
*/
{
uint8_t set_ip[4] = {192, 168, 1, 100};
ch395f_set_ip_addr(set_ip);
memset(ip_info, 0, sizeof(ip_info));
ch395f_get_ip_inf(ip_info);
int32_t match = (ip_info[0] == 192 && ip_info[1] == 168 &&
ip_info[2] == 1 && ip_info[3] == 100);
DBG_INFO(" ch395f_set_ip_addr(192.168.1.100)");
DBG_INFO(" expect: IP[0..3] = 192.168.1.100");
DBG_INFO(" actual: %d.%d.%d.%d", ip_info[0], ip_info[1], ip_info[2], ip_info[3]);
TEST_CHECK(match, "IP = %d.%d.%d.%d (expect 192.168.1.100)",
ip_info[0], ip_info[1], ip_info[2], ip_info[3]);
}
/* ---- 设置网关 → 回读验证 ----
* 原理:同「设置 IP → 回读」项,通过 ch395f_set_gwip_addr() 写入,ch395f_get_ip_inf()
* 回读的第 5-8 字节为网关地址。
* 通过:回读 GW[0..3] == 192.168.1.1。
* 失败:任意字节不匹配。
*/
{
uint8_t set_gw[4] = {192, 168, 1, 1};
ch395f_set_gwip_addr(set_gw);
memset(ip_info, 0, sizeof(ip_info));
ch395f_get_ip_inf(ip_info);
int32_t match = (ip_info[4] == 192 && ip_info[5] == 168 &&
ip_info[6] == 1 && ip_info[7] == 1);
DBG_INFO(" ch395f_set_gwip_addr(192.168.1.1)");
DBG_INFO(" expect: GW[0..3] = 192.168.1.1");
DBG_INFO(" actual: %d.%d.%d.%d", ip_info[4], ip_info[5], ip_info[6], ip_info[7]);
TEST_CHECK(match, "GW = %d.%d.%d.%d (expect 192.168.1.1)",
ip_info[4], ip_info[5], ip_info[6], ip_info[7]);
}
/* ---- 设置掩码 → 回读验证 ----
* 原理:同「设置 IP → 回读」项,ch395f_set_mask_addr() 写入,ch395f_get_ip_inf()
* 回读的第 9-12 字节为子网掩码。
* 通过:回读 MASK[0..3] == 255.255.255.0。
* 失败:任意字节不匹配。
*/
{
uint8_t set_mask[4] = {255, 255, 255, 0};
ch395f_set_mask_addr(set_mask);
memset(ip_info, 0, sizeof(ip_info));
ch395f_get_ip_inf(ip_info);
int32_t match = (ip_info[8] == 255 && ip_info[9] == 255 &&
ip_info[10] == 255 && ip_info[11] == 0);
DBG_INFO(" ch395f_set_mask_addr(255.255.255.0)");
DBG_INFO(" expect: MASK[0..3] = 255.255.255.0");
DBG_INFO(" actual: %d.%d.%d.%d", ip_info[8], ip_info[9], ip_info[10], ip_info[11]);
TEST_CHECK(match, "MASK = %d.%d.%d.%d (expect 255.255.255.0)",
ip_info[8], ip_info[9], ip_info[10], ip_info[11]);
}
/* ---- ch395f_get_glob_int_status_all()(16 位版) ----
* 全库唯一允许 net_poll 之外读全局寄存器的位置——被测对象就是该 API 本身
* (硬件层模式独占上下文,无并发消费者)。读即清语义 → 先读一次自清
* (吸收 bring_up 锁存的 PHY_CHANGE),短暂延时后第二次读为断言值。
* 通过:第二次读返回 0x0000。
* 失败:第二次读非 0x0000(测试期间出现新事件)。
*
* 用例号:本项即原 TC-NET-102(全局中断状态一致性)。该条目在 2026-08-22
* 文档重构时被删、而 docs/测试说明/板级测试/CH395F_Test_Guide.md 附录 B 的
* 汇总行遗留至今,故在此把号挂回实现体。
*/
{
(void)ch395f_get_glob_int_status_all(); /* 自清读(被测 API 自身,见上) */
HAL_Delay(10);
uint16_t gint_all = ch395f_get_glob_int_status_all();
DBG_INFO(" ch395f_get_glob_int_status_all()");
DBG_INFO(" expect: 0x0000");
DBG_INFO(" actual: 0x%04X", gint_all);
TEST_CHECK(gint_all == 0x0000,
"glob_int_status_all = 0x%04X (expect 0x0000)", gint_all);
}
/* ---- 打开→关闭 Socket 0 → 验证 CMD_STATUS ----
* 原理:连续执行 open_socket(0) 和 close_socket(0),然后读取
* 命令状态寄存器,确认 Socket 状态机正常流转。
* 通过:CMD_STATUS == 0x00(CH395F_ERR_SUCCESS),表示两次命令均成功执行。
* 失败:CMD_STATUS 非 0x00(Socket 硬件状态异常或命令序列错误)。
*/
{
uint8_t open_ret = ch395f_open_socket(0);
uint8_t close_ret = ch395f_close_socket(0);
uint8_t cmd_st = ch395f_get_cmd_status();
DBG_INFO(" ch395f_open_socket(0) -> ch395f_close_socket(0)");
DBG_INFO(" expect: CMD_STATUS == 0x00 (CH395F_ERR_SUCCESS)");
DBG_INFO(" actual: open=%d, close=%d, CMD_STATUS=0x%02X",
open_ret, close_ret, cmd_st);
TEST_CHECK(cmd_st == CH395F_ERR_SUCCESS,
"open=%d close=%d cmd=0x%02X (expect 0x00)",
open_ret, close_ret, cmd_st);
}
/* ---- 设置 ARP 参数 ----
* 原理:ch395f_set_arp(period, retry) 配置 ARP 请求周期和重试次数。
* period = 10(×100ms = 1s)、retry = 5(最多 5 次重试)。
* 通过:CMD_STATUS == 0x00(参数写入成功)。
* 失败:CMD_STATUS 非 0x00(命令未被执行 —— 参数无效或 SPI 异常)。
*/
{
ch395f_set_arp(10, 5);
uint8_t cmd_st = ch395f_get_cmd_status();
DBG_INFO(" ch395f_set_arp(period=10, retry=5)");
DBG_INFO(" expect: CMD_STATUS == 0x00");
DBG_INFO(" actual: CMD_STATUS = 0x%02X", cmd_st);
TEST_CHECK(cmd_st == CH395F_ERR_SUCCESS,
"set_arp CMD_STATUS = 0x%02X (expect 0x00)", cmd_st);
}
/* ---- 设置 TTL ----
* 原理:ch395f_set_ttl(sock, ttl) 设置指定 Socket 的 IP 报 TTL 值。
* sock = 0(Socket 0)、ttl = 64(默认值)。
* 通过:CMD_STATUS == 0x00。
* 失败:CMD_STATUS 非 0x00。
*/
{
ch395f_set_ttl(0, 64);
uint8_t cmd_st = ch395f_get_cmd_status();
DBG_INFO(" ch395f_set_ttl(sock=0, ttl=64)");
DBG_INFO(" expect: CMD_STATUS == 0x00");
DBG_INFO(" actual: CMD_STATUS = 0x%02X", cmd_st);
TEST_CHECK(cmd_st == CH395F_ERR_SUCCESS,
"set_ttl CMD_STATUS = 0x%02X (expect 0x00)", cmd_st);
}
TEST_REPORT("Phase 1");
}
#endif
/* ===================================================================
* Phase 2 — TCP Client(CH395F 作为客户端主动连接 PC)
*
* 测试目的:验证 CH395F 作为 TCP Client 发起连接、发送数据、接收回显、
* 关闭连接、重连的完整生命周期。重复 3 轮以验证连接稳定性。
*
* 前置条件:PC 上运行 TCP 回显服务器(python test/ch395f_socket_test.py tcp_server --port 8081)
* 通过:3/3 轮回显成功。
* 失败:任意轮连接失败、回显超时或数据不一致。
* =================================================================== */
#ifdef ENABLE_PHASE2_TESTS
#ifndef PHASE2_REMOTE_IP
#define PHASE2_REMOTE_IP "192.168.1.2"
#endif
#ifndef PHASE2_REMOTE_PORT
#define PHASE2_REMOTE_PORT 8081
#endif
#ifndef PHASE2_LOCAL_PORT
#define PHASE2_LOCAL_PORT 50000
#endif
#ifndef PHASE2_LOOP_COUNT
#define PHASE2_LOOP_COUNT 3
#endif
#ifndef PHASE2_TIMEOUT_MS
#define PHASE2_TIMEOUT_MS 10000
#endif
#ifndef PHASE2_TC203_TIMEOUT_MS
#define PHASE2_TC203_TIMEOUT_MS 30000 /* TC-NET-203 连接超时窗(指南规定 30s) */
#endif
/* TCP Client 单次会话:对应 TC-NET-201 步 2~7 与 TC-NET-202 每轮 6 步。
* 调用前须已设置协议类型 / 目标 IP / 目标端口(TC-NET-201 步 1)。
* 每轮使用不同本地端口 local_port 规避 TIME_WAIT。
* 返回 1 表示全流程通过,0 表示中途失败(已记录 TEST_CHECK)。
*/
/* TC-NET-201(硬件层):单次 TCP Client 连接与数据交换,步骤与文档 7 步严格对应 */
static void phase2_tc201(void)
{
uint8_t sock = 0; /* 本用例操作的 Socket 索引,固定 0(单连接模式) */
uint8_t ip_arr[4]; /* 目标 IP 四字节数组,由 test_parse_ip() 解析 PHASE2_REMOTE_IP 填入 */
uint8_t tx[64]; /* 发送缓冲:64 字节递增模式数据 0x00..0x3F */
uint8_t rx[300]; /* 接收缓冲:存放 PC 回显数据,300 > 64 预留安全余量 */
uint8_t r; /* ch395f_* 命令返回值(0x00 = CH395F_ERR_SUCCESS) */
uint8_t ok; /* 当前步骤结果标志:1 = 成功跳出等待循环,0 = 超时失败 */
uint16_t p; /* 逐字节比对循环索引(rx[p] vs tx[p]) */
uint16_t n; /* 实际参与比对的字节数 = MIN(rl, 64) */
uint16_t rl; /* 本次实际读取长度 = MIN(收到长度, sizeof(rx)),防缓冲区溢出 */
uint32_t tick; /* HAL_GetTick() 快照;用无符号减法测超时窗口(回绕安全) */
int16_t k; /* 通用循环索引(如构造 tx 递增模式) */
int16_t match; /* 回显校验结果:1 = 全部字节一致,0 = 发现不符(提前 break) */
test_parse_ip(PHASE2_REMOTE_IP, ip_arr);
/* 步1:配置协议类型 / 目标 IP / 目标端口 / 本地端口 */
ch395f_set_proto_type(sock, CH395F_PROTO_TYPE_TCP);
ch395f_set_des_ip(sock, ip_arr);
ch395f_set_des_port(sock, PHASE2_REMOTE_PORT);
ch395f_set_sour_port(sock, (uint16_t)PHASE2_LOCAL_PORT);
TEST_CHECK(ch395f_get_cmd_status() == CH395F_ERR_SUCCESS,
"TC-NET-201 step1 cfg proto/ip/port");
if (ch395f_get_cmd_status() != CH395F_ERR_SUCCESS) return;
/* 构造 64 字节递增模式数据 0x00..0x3F */
for (k = 0; k < 64; k++) {
tx[k] = (uint8_t)k;
}
/* 步2:OPEN Socket 0 */
r = ch395f_open_socket(sock);
TEST_CHECK(r == CH395F_ERR_SUCCESS, "TC-NET-201 step2 open");
if (r != CH395F_ERR_SUCCESS) return;
/* 步3:CONNECT */
r = ch395f_tcp_connect(sock);
TEST_CHECK(r == CH395F_ERR_SUCCESS, "TC-NET-201 step3 connect");
if (r != CH395F_ERR_SUCCESS) { ch395f_close_socket(sock); return; }
/* 步4:等待 CONNECT 中断
*
* 直接读本 Socket 中断状态 CMD_GET_INT_STATUS_SN(8 位,读即清)——事件位:
* SENBUF_FREE / SEND_OK / RECV_OK / CONNECT / DISCONNECT / DISCARD / SOCK_TIMEOUT。
* 不用全局寄存器 GET_GLOB_INT_STATUS_ALL 做门控:它读即清,任何未配对 SINT
* 消费的读取都会吞掉事件通知;唯一读者 = net_poll(唯一读者红线(docs/网络/网络自愈架构设计.md §7 I7)),测试代码同样
* 禁自行读取(用户裁定 2026-10-13)。测试知道自己操作哪个 Socket,直读
* SINT 即可——每轮一次 SPI 事务,成本与旧两级方案相同,且观察无门控混淆。
*/
tick = HAL_GetTick();
ok = 0;
while (HAL_GetTick() - tick < PHASE2_TIMEOUT_MS) {
uint8_t si = ch395f_get_sock_int_status(sock);
if (si & CH395F_SINT_STAT_CONNECT) {
/* bit3 = 0x08:TCP 三次握手完成,进入 ESTABLISHED */
ok = 1;
break;
}
/* 是别的事件(理论上此阶段不应出现),已被读清、直接忽略,
* 继续下一轮轮询等 CONNECT */
HAL_Delay(10); /* 10ms 轮询周期,直至 CONNECT 或超时退出 */
}
TEST_CHECK(ok, "TC-NET-201 step4 connected");
if (!ok) {
ch395f_close_socket(sock);
return;
}
HAL_Delay(100);
/* 步5:发送 64 字节,并等待 SENBUF_FREE 中断确认发送缓冲释放 */
ch395f_write_send_buf(sock, tx, 64);
tick = HAL_GetTick();
ok = 0;
while (HAL_GetTick() - tick < 2000) {
if (ch395f_get_sock_int_status(sock) & CH395F_SINT_STAT_SENBUF_FREE) {
ok = 1;
break;
}
HAL_Delay(10);
}
TEST_CHECK(ok, "TC-NET-201 step5 sendbuf_free");
if (!ok) {
ch395f_close_socket(sock);
return;
}
/* 步6:等待回显并逐字节验证(递增模式 0x00..0x3F)
*
* 背景:步 5 已把 tx[64] 发给 PC,PC 端 tcp_server 收到后原样回显。
* 本步骤最多等 10 秒,每轮轮询做两件事:
* ① 查芯片接收缓冲有没有回显数据到达(优先处理);
* ② 查有没有 DISCONNECT / TIMEOUT 事件(连接断了就别傻等了)。
*
* 关键 API 语义区别:
* ch395f_get_recv_len() —— 只查询积压字节数,不取走数据(可反复调用)
* ch395f_read_recv_buf() —— 真正从芯片 RX FIFO 取出数据(取走即消耗)
*/
tick = HAL_GetTick();
ok = 0; /* 尚未收到任何回显数据 */
while (HAL_GetTick() - tick < 10000) {
/* ---- ① 数据到达检查 ----
* rlen:芯片 RX 缓冲区当前积压的字节数(纯查询、不消耗)。
* rlen > 0 说明 PC 回显已到达,进入读取 + 校验流程。 */
uint16_t rlen = ch395f_get_recv_len(sock);
if (rlen > 0) {
/* 截断保护:一次最多读 sizeof(rx)=300 字节,
* 防止 rlen 异常偏大时 read_recv_buf 写越界 rx[] */
rl = (rlen > (uint16_t)sizeof(rx)) ? (uint16_t)sizeof(rx) : rlen;
/* 清零将要填充的区域(防御性写法,避免残留脏字节干扰比对) */
memset(rx, 0, rl);
/* 从芯片 RX FIFO 取出回显 —— 此刻起这些数据已被消费 */
ch395f_read_recv_buf(sock, rx, rl);
/* 参与比对的长度:预期回显 64 字节;若 TCP 分片导致本次
* 只到了一部分(rl < 64),则只比对已到的部分 */
n = (rl < 64) ? rl : 64;
/* 判定初值:必须收满 64 字节才算通过。若 rl < 64(分片/短读),
* match 直接为 0 → "echo verify" 判 FAIL */
match = (rl >= 64);
/* 与发送的递增模式逐字节比对:tx[k]=k,任一不符立即置 FAIL 退出 */
for (p = 0; p < n; p++) {
if (rx[p] != tx[p]) {
match = 0;
break;
}
}
TEST_CHECK(match, "TC-NET-201 step6 echo verify");
ok = 1; /* 标记"收到过数据",循环外的 echo rcvd 检查不再重复报错 */
break; /* 校验结论已得出,无论 PASS/FAIL 都跳出 */
}
/* ---- ② 连接异常检查(还没等到数据才走到这里)----
* 直读本 Socket SINT(读即清)。先查数据再查断开:断开事件可能和
* 最后一批数据同时到达,数据优先保证"临终回显"不被漏掉。 */
uint8_t si = ch395f_get_sock_int_status(sock);
if (si & (CH395F_SINT_STAT_DISCONNECT | CH395F_SINT_STAT_SOCK_TIMEOUT)) {
break; /* 对端断开 / 超时:继续等也不会有回显了。
ok 保持 0,跳出后由下方统一判 FAIL */
}
/* 每 50ms 一轮。若本轮只读了部分数据(rlen > 300 的极端情况),
* 剩余字节下一轮 get_recv_len 会继续报告,不会丢 */
HAL_Delay(50);
}
/* 到这里只有两种情况:
* ok==1:收到过数据,结果已由循环内 "echo verify" 记录;
* ok==0:10s 内什么都没收到 / 中途断开 —— 在此补一条 FAIL。 */
if (!ok) {
TEST_CHECK(0, "TC-NET-201 step6 echo rcvd");
}
/* 步7:关闭 Socket 并等待状态回到 CLOSED(CH395F_SOCKET_CLOSED = 0x00) */
ch395f_close_socket(sock);
tick = HAL_GetTick();
ok = 0;
while (HAL_GetTick() - tick < 5000) {
uint8_t st[2];
ch395f_get_socket_status(sock, st);
if (st[0] == CH395F_SOCKET_CLOSED) {
ok = 1;
break;
}
HAL_Delay(50);
}
TEST_CHECK(ok, "TC-NET-201 step7 closed");
HAL_Delay(200);
}
/* TC-NET-202(硬件层):关闭重连 x PHASE2_LOOP_COUNT 轮,每轮 6 步 */
static void phase2_tc202_once(uint8_t sock, uint16_t local_port)
{
uint8_t tx[64];
uint8_t rx[300];
uint8_t r;
uint8_t ok;
uint16_t p;
uint16_t n;
uint16_t rl;
uint32_t tick;
int32_t k;
int32_t match;
for (k = 0; k < 64; k++) {
tx[k] = (uint8_t)k;
}
/* 步1:设置本地端口并 OPEN Socket 0 */
ch395f_set_sour_port(sock, local_port);
r = ch395f_open_socket(sock);
TEST_CHECK(r == CH395F_ERR_SUCCESS, "TC-NET-202 step1 open");
if (r != CH395F_ERR_SUCCESS) return;
/* 步2:CONNECT */
r = ch395f_tcp_connect(sock);
TEST_CHECK(r == CH395F_ERR_SUCCESS, "TC-NET-202 step2 connect");
if (r != CH395F_ERR_SUCCESS) { ch395f_close_socket(sock); return; }
/* 步3:等待 CONNECT 中断 */
tick = HAL_GetTick();
ok = 0;
while (HAL_GetTick() - tick < PHASE2_TIMEOUT_MS) {
if (ch395f_get_sock_int_status(sock) & CH395F_SINT_STAT_CONNECT) { ok = 1; break; }
HAL_Delay(10);
}
TEST_CHECK(ok, "TC-NET-202 step3 connected");
if (!ok) { ch395f_close_socket(sock); return; }
HAL_Delay(100);
/* 步4:发送 64 字节 + SENBUF_FREE */
ch395f_write_send_buf(sock, tx, 64);
tick = HAL_GetTick();
ok = 0;
while (HAL_GetTick() - tick < 2000) {
if (ch395f_get_sock_int_status(sock) & CH395F_SINT_STAT_SENBUF_FREE) { ok = 1; break; }
HAL_Delay(10);
}
TEST_CHECK(ok, "TC-NET-202 step4 sendbuf_free");
if (!ok) { ch395f_close_socket(sock); return; }
/* 步5:等待回显并验证 */
tick = HAL_GetTick();
ok = 0;
while (HAL_GetTick() - tick < 10000) {
uint16_t rlen = ch395f_get_recv_len(sock);
if (rlen > 0) {
rl = (rlen > (uint16_t)sizeof(rx)) ? (uint16_t)sizeof(rx) : rlen;
memset(rx, 0, rl);
ch395f_read_recv_buf(sock, rx, rl);
n = (rl < 64) ? rl : 64;
match = (rl >= 64);
for (p = 0; p < n; p++) {
if (rx[p] != tx[p]) { match = 0; break; }
}
TEST_CHECK(match, "TC-NET-202 step5 echo verify");
ok = 1;
break;
}
if (ch395f_get_sock_int_status(sock) &
(CH395F_SINT_STAT_DISCONNECT | CH395F_SINT_STAT_SOCK_TIMEOUT)) break;
HAL_Delay(50);
}
if (!ok) TEST_CHECK(0, "TC-NET-202 step5 echo rcvd");
/* 步6:关闭 + 等待 CLOSED */
ch395f_close_socket(sock);
tick = HAL_GetTick();
ok = 0;
while (HAL_GetTick() - tick < 5000) {
uint8_t st[2];
ch395f_get_socket_status(sock, st);
if (st[0] == CH395F_SOCKET_CLOSED) { ok = 1; break; }
HAL_Delay(50);
}
TEST_CHECK(ok, "TC-NET-202 step6 closed");
HAL_Delay(200);
}
static void phase2_tc202(void)
{
uint8_t sock = 0;
uint8_t ip_arr[4];
int32_t i;
test_parse_ip(PHASE2_REMOTE_IP, ip_arr);
/* 协议 / 目标端口在重连间保持不变,仅每轮更换本地端口 */
ch395f_set_proto_type(sock, CH395F_PROTO_TYPE_TCP);
ch395f_set_des_ip(sock, ip_arr);
ch395f_set_des_port(sock, PHASE2_REMOTE_PORT);
for (i = 0; i < PHASE2_LOOP_COUNT; i++) {
phase2_tc202_once(sock, (uint16_t)(PHASE2_LOCAL_PORT + i));
}
}
/* ===================================================================
* TC-NET-204(硬件层):TRAP-NET-07 定案对照——三种关闭次序 A/B/C
*
* 背景:TRAP-NET-07(2025-07)断言"`disconnect → close` 会让 Socket 卡 `FIN_WAIT_2`
* 数分钟、无法重开,因为 close 不再发 RST"。该断言有两处可疑:
* ① 手册无 RST 语义——§5.35 只说"关闭 Socket 前 CH395 会自动断开 TCP 连接",
* 全文 RST 只出现在错误码 CH395_ERR_RST 与 RST/RSTI 引脚;
* ② "FIN_WAIT_2"是把 GET_SOCKET_STATUS 第 2 字节当独立事实读出来的——
* §5.22 第 1 字节只有 00H(CLOSED)/05H(OPEN) 两个取值,第 2 字节才是 TCP 态,
* 而官方 SDK CH395CMDGetSocketStatus 注释明确:"Second byte — TCP state,
* **meaningful only if** TCP mode **and the first byte is open**"。
* 当年 Phase 2 收尾正是"byte0 已 CLOSED 才 break,然后照样打印 byte1"。
* 另:git 全历史里 ch395f_tcp_disconnect() 只有定义、零调用点,"disconnect→close"
* 序列从未进入过任何提交代码 → 现象来自一次未留痕的试验。
*
* 受控对照(每模式 x PHASE204_LOOP_COUNT 轮,同一 Socket、同一 PC 回显服务):
* 模式 A:close only(现行做法)
* 模式 B:tcp_disconnect → close(TRAP-NET-07 声称会卡死的序列)
* 模式 C:tcp_disconnect only,不 close(检验 FUN_PARA bit3=1 下芯片会不会自己关)
* 观测量:
* ① 回到 SOCKET_CLOSED(byte0=00H) 的耗时;② 该瞬间 byte1 的读数(验证"无意义"口径)
* ③ 重开 + 重连 + 回显是否成功(TRAP-NET-07 的"无法重新打开")
* ④ 重开会话首次读中断是否命中上一条连接残留的 DISCONNECT(TRAP-NET-12 轴)
* ⚠ 关闭与重开之间**不主动清中断**(与 2025-07 现场一致),残留事件由重开后的
* 首次中断读取现场暴露(读即清,无法窥视)。
* =================================================================== */
#ifndef PHASE204_LOOP_COUNT
#define PHASE204_LOOP_COUNT 5
#endif
#ifndef PHASE204_LOCAL_PORT
#define PHASE204_LOCAL_PORT 50100 /* 避开 201/202/203 用过的本地端口 */
#endif
#ifndef PHASE204_CLOSE_WAIT_MS
#define PHASE204_CLOSE_WAIT_MS 10000 /* 回到 CLOSED 的观察窗 */
#endif
/* 建立一次会话:open + connect + 等 CONNECT + 64B 回显比对
* 入口参数:sock - Socket 号;lport - 本地端口;mode/round - 仅用于日志定位
* 返回值:1=会话成功(含回显比对通过) 0=任一步失败 uint8_t
* 说明:等待 CONNECT 期间读到的其它中断(含上一条连接残留的 DISCONNECT)
* 现场打印,用于 TRAP-NET-12"滞后锁存"取证。 */
static uint8_t tc204_session(uint8_t sock, uint16_t lport, uint8_t mode, uint8_t round)
{
uint8_t tx[64];
uint8_t rx[300];
uint8_t ok = 0;
uint8_t r;
uint8_t match;
uint16_t rl;
uint16_t n;
uint32_t tick;
int32_t k;
for (k = 0; k < 64; k++) {
tx[k] = (uint8_t)k;
}
ch395f_set_sour_port(sock, lport);
r = ch395f_open_socket(sock);
if (r != CH395F_ERR_SUCCESS) {
DBG_INFO("TC-NET-204 m%u r%u open ret=0x%02X", (unsigned)mode, (unsigned)round, r);
return 0;
}
r = ch395f_tcp_connect(sock);
if (r != CH395F_ERR_SUCCESS) {
DBG_INFO("TC-NET-204 m%u r%u connect cmd ret=0x%02X", (unsigned)mode, (unsigned)round, r);
ch395f_close_socket(sock);
return 0;
}
/* 等 CONNECT 中断(期间其它中断现场打印 = 残留事件取证点;直读本 Socket SINT) */
tick = HAL_GetTick();
while (HAL_GetTick() - tick < PHASE2_TIMEOUT_MS) {
uint8_t si = ch395f_get_sock_int_status(sock);
if ((si & CH395F_SINT_STAT_CONNECT) != 0U) {
ok = 1;
break;
}
if (si != 0U) {
DBG_INFO("TC-NET-204 m%u r%u stale-int=0x%02X at +%ums",
(unsigned)mode, (unsigned)round, si,
(unsigned)(HAL_GetTick() - tick));
}
HAL_Delay(10);
}
if (!ok) {
DBG_INFO("TC-NET-204 m%u r%u no CONNECT within %ums", (unsigned)mode, (unsigned)round,
(unsigned)PHASE2_TIMEOUT_MS);
ch395f_close_socket(sock);
return 0;
}
HAL_Delay(100);
/* 64B 发送 + SENBUF_FREE + 回显比对(证明这是一条真实连接) */
ch395f_write_send_buf(sock, tx, 64);
tick = HAL_GetTick();
ok = 0;
while (HAL_GetTick() - tick < 2000) {
if (ch395f_get_sock_int_status(sock) & CH395F_SINT_STAT_SENBUF_FREE) {
ok = 1;
break;
}
HAL_Delay(10);
}
if (!ok) {
DBG_INFO("TC-NET-204 m%u r%u no SENBUF_FREE", (unsigned)mode, (unsigned)round);
ch395f_close_socket(sock);
return 0;
}
tick = HAL_GetTick();
while (HAL_GetTick() - tick < 10000) {
uint16_t rlen = ch395f_get_recv_len(sock);
if (rlen > 0U) {
rl = (rlen > (uint16_t)sizeof(rx)) ? (uint16_t)sizeof(rx) : rlen;
ch395f_read_recv_buf(sock, rx, rl);
n = (rl < 64U) ? rl : 64U;
match = (rl >= 64U) ? 1U : 0U;
for (k = 0; k < (int32_t)n; k++) {
if (rx[k] != tx[k]) {
match = 0;
break;
}
}
if (!match) {
DBG_INFO("TC-NET-204 m%u r%u echo MISMATCH (rlen=%u)", (unsigned)mode,
(unsigned)round, (unsigned)rlen);
}
return match;
}
HAL_Delay(20);
}
DBG_INFO("TC-NET-204 m%u r%u echo NOT rcvd", (unsigned)mode, (unsigned)round);
ch395f_close_socket(sock);
return 0;
}
/* 执行一种关闭次序,并测量"回到 SOCKET_CLOSED 的耗时"
* 入口参数:sock - Socket 号;mode - 0=A close / 1=B disconnect→close / 2=C disconnect only
* round - 日志定位
* 返回值:1=观察窗内 byte0 回到 CLOSED 0=未回到 uint8_t
* 说明:关闭与轮询之间不读中断(读即清会抹掉 TRAP-NET-12 的滞后事件现场)。 */
static uint8_t tc204_close_seq(uint8_t sock, uint8_t mode, uint8_t round)
{
/* 状态采样里程碑(ms):抓 byte0/byte1 的演变 */
static const uint16_t s_marks[] = { 0U, 50U, 100U, 250U, 500U, 1000U, 2000U, 5000U, 10000U };
uint8_t st[2] = { 0, 0 };
uint32_t tick;
uint32_t el = 0U;
uint8_t closed = 0U;
uint8_t mi = 0U;
uint8_t n_marks = (uint8_t)(sizeof(s_marks) / sizeof(s_marks[0]));
if (mode >= 1U) {
uint8_t r = ch395f_tcp_disconnect(sock);
DBG_INFO("TC-NET-204 m%u r%u disconnect ret=0x%02X", (unsigned)mode, (unsigned)round, r);
}
if (mode <= 1U) {
uint8_t r = ch395f_close_socket(sock);
DBG_INFO("TC-NET-204 m%u r%u close ret=0x%02X", (unsigned)mode, (unsigned)round, r);
}
tick = HAL_GetTick();
while (el <= (uint32_t)PHASE204_CLOSE_WAIT_MS) {
ch395f_get_socket_status(sock, st);
el = (uint32_t)(HAL_GetTick() - tick);
while ((mi < n_marks) && (el >= s_marks[mi])) {
DBG_INFO("TC-NET-204 m%u r%u poll +%ums st=0x%02X/0x%02X", (unsigned)mode,
(unsigned)round, (unsigned)s_marks[mi], st[0], st[1]);
mi++;
}
if (st[0] == CH395F_SOCKET_CLOSED) {
closed = 1U;
break;
}
HAL_Delay(20);
}
/* 判定瞬间再读一次:byte0 已 CLOSED 时 byte1 按 §5.22/官方注释属"无意义"区 */
ch395f_get_socket_status(sock, st);
if (closed != 0U) {
DBG_INFO("TC-NET-204 m%u r%u CLOSED at +%ums (byte0=0x%02X byte1=0x%02X)",
(unsigned)mode, (unsigned)round, (unsigned)el, st[0], st[1]);
} else {
DBG_INFO("TC-NET-204 m%u r%u NOT-CLOSED within %ums (byte0=0x%02X byte1=0x%02X)",
(unsigned)mode, (unsigned)round, (unsigned)PHASE204_CLOSE_WAIT_MS,
st[0], st[1]);
}
return closed;
}
/* 轮末收尾:关闭本轮第二条连接并等回 CLOSED
* 说明:芯片不允许对已 OPEN 的 Socket 再 open(返回 0x20 FAIL)——不收尾则下一轮
* 首轮 open 必失败并级联(2026-10-11 TC-NET-204 首跑实测踩过)。 */
static void tc204_round_teardown(uint8_t sock)
{
uint8_t st[2] = { 0, 0 };
uint32_t tick;
ch395f_close_socket(sock);
tick = HAL_GetTick();
while (HAL_GetTick() - tick < 3000) {
ch395f_get_socket_status(sock, st);
if (st[0] == CH395F_SOCKET_CLOSED) {
break;
}
HAL_Delay(20);
}
HAL_Delay(200);
}
/* 单轮:会话 → 关闭次序 → 重开会话 */
static void phase2_tc204_once(uint8_t mode, uint8_t round)
{
uint8_t sock = 0;
uint16_t lport = (uint16_t)(PHASE204_LOCAL_PORT + (uint16_t)(mode * 16U + round));
uint8_t s1;
uint8_t s2;
uint8_t closed;
DBG_INFO("TC-NET-204 === m%u r%u lport %u ===", (unsigned)mode, (unsigned)round,
(unsigned)lport);
/* 1. 建立真实连接(有连接才谈得上 FIN_WAIT_*) */
s1 = tc204_session(sock, lport, mode, round);
TEST_CHECK(s1, "TC-NET-204 m%u r%u session1 (connect+echo)", (unsigned)mode, (unsigned)round);
if (!s1) {
tc204_round_teardown(sock);
return;
}
/* 2. 关闭次序 + 回到 CLOSED 的耗时 */
closed = tc204_close_seq(sock, mode, round);
if (mode == 2U) {
/* 模式 C 的预期恰恰相反:bit3=1(§5.52 ②)下芯片不得自行关闭 Socket */
TEST_CHECK(!closed, "TC-NET-204 m2 r%u stays OPEN after disconnect (FUN_PARA bit3)",
(unsigned)round);
if (!closed) {
/* TRAP-NET-07 的"无法重新打开"现场:不 close 直接 open,看芯片返回什么 */
uint8_t r = ch395f_open_socket(sock);
DBG_INFO("TC-NET-204 m2 r%u open-without-close ret=0x%02X", (unsigned)round, r);
/* 收尾:显式 close 并测耗时(证明 close 才是把 Socket 拉回 CLOSED 的动作) */
(void)tc204_close_seq(sock, 0U, round);
}
} else {
TEST_CHECK(closed, "TC-NET-204 m%u r%u back to CLOSED within %ums", (unsigned)mode,
(unsigned)round, (unsigned)PHASE204_CLOSE_WAIT_MS);
}
/* 3. 重开 + 重连 + 回显(TRAP-NET-07 的"无法重新打开"判据;TRAP-NET-09 沉降 200ms) */
HAL_Delay(200);
s2 = tc204_session(sock, (uint16_t)(lport + 64U), mode, round);
TEST_CHECK(s2, "TC-NET-204 m%u r%u reopen+reconnect+echo OK", (unsigned)mode, (unsigned)round);
tc204_round_teardown(sock);
}
static void phase2_tc204(void)
{
uint8_t sock = 0;
uint8_t ip_arr[4];
uint8_t m;
uint8_t i;
test_parse_ip(PHASE2_REMOTE_IP, ip_arr);
/* 协议 / 目标在整组内保持不变,仅每轮换本地端口 */
ch395f_set_proto_type(sock, CH395F_PROTO_TYPE_TCP);
ch395f_set_des_ip(sock, ip_arr);
ch395f_set_des_port(sock, PHASE2_REMOTE_PORT);
for (m = 0; m < 3U; m++) {
for (i = 0; i < PHASE204_LOOP_COUNT; i++) {
phase2_tc204_once(m, i);
}
}
}
/* TC-NET-203(硬件层):TCP Client 连接超时(负向测试,P1)
*
* 前置环境:PC 未在 192.168.1.2:8081 提供服务(与 TC-NET-201/202 互斥,
* 需先关闭 PC 端 tcp_server)。固件自行辨别环境错误:
* 若等待期间误收到 CONNECT,说明 PC 服务未关,判 FAIL 并给出指引。
*
* 负向通过路径(本质都是"未建立连接",按事件+耗时细分原因):
* SOCK_TIMEOUT(快, <3s) —— 实测主路径:对端在线但端口关闭,RST 拒绝,
* 芯片毫秒级上报,并非等待重传耗尽
* SOCK_TIMEOUT(慢, ≥3s) —— 对端无响应(掉电/防火墙丢包),重传耗尽后报超时
* DISCONNECT —— 预留兼容路径(SYN_SENT 阶段实测基本不出现)
* UNREACH(全局位) —— ARP 失败、对端不在线(仅作备注,不影响判定)
*
* 已知陷阱(TRAP-NET-12):上一会话 close 的 DISCONNECT 会滞后锁存在
* SINT_SN(wait-CLOSED 的 STATUS 查询不清中断)。防御双保险:
* ① 步 2 前预清本 Socket SINT;
* ② 判负向后用 GET_SOCKET_STATUS 反核实,ESTABLISHED 即改判环境错误。
*/
static void phase2_tc203(void)
{
uint8_t sock = 0; /* 操作 Socket 0(单连接模式) */
uint8_t ip_arr[4]; /* 目标 IP 四字节数组 */
uint16_t sour_port = (uint16_t)(PHASE2_LOCAL_PORT + 16); /* 避开 201/202 用过的端口防 TIME_WAIT */
uint32_t tick;
uint32_t elapsed; /* 步3 从 CONNECT 到收到负向事件的耗时(ms),用于细分原因 */
uint8_t r;
uint8_t neg_si; /* 步3 命中的 socket 事件位(SOCK_TIMEOUT/DISCONNECT) */
uint8_t ev; /* 步3 结果:0=无事件 1=收到负向事件(过) 2=误收到CONNECT(环境错误) */
DBG_INFO("--- TC-NET-203: connect timeout (PC 8081 MUST be OFF) ---");
test_parse_ip(PHASE2_REMOTE_IP, ip_arr);
/* 步1:配置协议/目标 IP/目标端口/本地端口,校验 CMD_STATUS */
ch395f_set_proto_type(sock, CH395F_PROTO_TYPE_TCP);
ch395f_set_des_ip(sock, ip_arr);
ch395f_set_des_port(sock, (uint16_t)PHASE2_REMOTE_PORT);
ch395f_set_sour_port(sock, sour_port);
TEST_CHECK(ch395f_get_cmd_status() == CH395F_ERR_SUCCESS,
"TC-NET-203 step1 cfg proto/ip/port");
if (ch395f_get_cmd_status() != CH395F_ERR_SUCCESS) return;
/* 预清陈旧事件(TRAP-NET-12):上一会话 close 的 DISCONNECT 可能滞后锁存
* 在 SINT_SN 中(wait-CLOSED 轮询的 STATUS 是纯查询、不清中断),
* 若不清除,本用例首次读中断会误吃旧事件导致负向误判。
* 只清本 Socket SINT——全局寄存器读即清,唯一读者 = net_poll(唯一读者红线(docs/网络/网络自愈架构设计.md §7 I7))。 */
(void)ch395f_get_sock_int_status(sock);
/* 步2:OPEN + CONNECT。命令为异步下发,此时均应立即返回成功,
* 真正的失败结果由步 3 的中断体现 */
r = ch395f_open_socket(sock);
TEST_CHECK(r == CH395F_ERR_SUCCESS, "TC-NET-203 step2 open");
if (r != CH395F_ERR_SUCCESS) return;
r = ch395f_tcp_connect(sock);
TEST_CHECK(r == CH395F_ERR_SUCCESS, "TC-NET-203 step2 connect");
if (r != CH395F_ERR_SUCCESS) { ch395f_close_socket(sock); return; }
/* 步3:30s 直读本 Socket SINT 轮询(读即清),等"连不上"的证据。
* (旧版先扫全局 UNREACH 位作备注——已删:全局寄存器唯一读者 = net_poll,
* 该备注本就不影响判定。)
*
* 实测行为(2026-08 上板):PC 端口关闭时 SYN 被 RST 拒绝,
* 芯片以【毫秒级 SOCK_TIMEOUT】上报,并非等待 30s 静默重传耗尽;
* DISCONNECT 仅在已建立连接断开时触发,SYN_SENT 阶段基本不会出现。
* 因此同时记录耗时用于细分失败原因:
* <3s → 对端主动拒绝(RST,端口关闭)
* ≥3s → 真无应答(对端离线/防火墙丢包,重传耗尽) */
tick = HAL_GetTick();
ev = 0;
neg_si = 0;
while (HAL_GetTick() - tick < PHASE2_TC203_TIMEOUT_MS) {
uint8_t si = ch395f_get_sock_int_status(sock);
if (si & (CH395F_SINT_STAT_SOCK_TIMEOUT | CH395F_SINT_STAT_DISCONNECT)) {
neg_si = si; /* 记下事件位,跳出后再细分原因 */
ev = 1;
break;
}
if (si & CH395F_SINT_STAT_CONNECT) {
ev = 2; /* 连上了 = PC 服务没关,属环境错误而非芯片故障 */
break;
}
HAL_Delay(10);
}
elapsed = HAL_GetTick() - tick;
/* 反核实(TRAP-NET-12 第二道防线):负向事件与实际状态矛盾时以状态为准。
* 若收到"负向事件"但 socket 实际已 ESTABLISHED,说明刚才读到的是
* 上一会话遗留的陈旧 DISCONNECT,真实结果是连接成功 —— 环境错误。
* ⚠ §5.22:byte0 只有 00H(CLOSED)/05H(OPEN),TCP 态在 **byte1**;
* 拿 byte0 比 TCP_ESTABLISHED(0x04) 恒假(2026-10-11 审计修正)。 */
if (ev == 1) {
uint8_t st_chk[2];
ch395f_get_socket_status(sock, st_chk);
if (st_chk[0] == CH395F_SOCKET_OPEN &&
st_chk[1] == CH395F_TCP_ESTABLISHED) {
DBG_ERROR(" negative event but socket ESTABLISHED -> stale DISCONNECT!");
ev = 2;
}
}
if (ev == 1) {
const char *why;
const char *short_tag;
if (neg_si & CH395F_SINT_STAT_DISCONNECT) {
why = "DISCONNECT: peer sent RST (port closed)";
short_tag = "DISC/RST";
} else if (elapsed < 3000U) {
/* 毫秒级超时 = 收到了 RST 快速失败,而非静默无应答 */
why = "SOCK_TIMEOUT fast: peer actively refused (RST, port closed)";
short_tag = "T/O-fast(refused)";
} else {
why = "SOCK_TIMEOUT slow: no response (offline / firewall drop)";
short_tag = "T/O-slow(silence)";
}
DBG_INFO(" negative event @%lums: %s", elapsed, why);
TEST_CHECK(1, "TC-NET-203 step3 no-connect OK (%lums %s)", elapsed, short_tag);
} else if (ev == 2) {
DBG_ERROR(" CONNECT received -> PC 8081 server is still running!");
TEST_CHECK(0, "TC-NET-203 step3 connected?! stop PC server and retry");
} else {
TEST_CHECK(0, "TC-NET-203 step3 no interrupt in %dms (state machine hang?)",
PHASE2_TC203_TIMEOUT_MS);
}
/* 步4:无论步 3 结论如何都关闭 Socket,确认状态机可恢复到 CLOSED */
ch395f_close_socket(sock);
tick = HAL_GetTick();
ev = 0;
while (HAL_GetTick() - tick < 5000) {
uint8_t st[2];
ch395f_get_socket_status(sock, st);
if (st[0] == CH395F_SOCKET_CLOSED) { ev = 1; break; }
HAL_Delay(50);
}
TEST_CHECK(ev == 1, "TC-NET-203 step4 closed");
HAL_Delay(200);
}
static void phase2_run(void)
{
DBG_INFO("=== CH395F Phase 2 Tests (TCP Client) ===");
DBG_INFO("--- TC-NET-201: connect & 64B exchange ---");
phase2_tc201();
DBG_INFO("--- TC-NET-202: close/reconnect x%d ---", PHASE2_LOOP_COUNT);
phase2_tc202();
DBG_INFO("--- TC-NET-204: close A/B/C control (TRAP-NET-07) x3 modes x%d ---",
PHASE204_LOOP_COUNT);
phase2_tc204();
DBG_INFO("--- TC-NET-203: negative test, stop PC 8081 server first ---");
phase2_tc203();
TEST_REPORT("Phase 2");
}
#endif
/* ===================================================================
* Phase 3 — UDP Echo(CH395F 作为 UDP Server)
*
* 测试目的:验证 CH395F UDP 协议栈的收发能力,包括:
* 1. UDP Socket 打开和关闭
* 2. 从任意源接收 UDP 数据报
* 3. 解析 UDP 包头的源 IP/端口
* 4. 向对应源地址回显数据
* 5. SENDBUF_FREE 中断等待(确保发送缓冲可用)
*
* 前置条件:PC 运行 udp_client 发包到 192.168.1.100:60000
* 会话结构:累计 PHASE3_TOTAL_ROUNDS(60) 个有效数据包后关闭 Socket
* 第 1~30 轮 = TC-NET-301(PC 发 64B);第 31~60 轮 = TC-NET-302(1B/64B/200B)
* "HELLO"(5B) 为 PC 就绪探针:照常回显但不计入轮次
* 通过:60/60 有效轮回显成功。
* 失败:任意轮回显超时或解析错误。
* =================================================================== */
#ifdef ENABLE_PHASE3_TESTS
/* UDP 有效回显轮数:1~30 = TC-NET-301(64B),31~60 = TC-NET-302(变长)。
* "HELLO" 探针只回显不计数,与 test/ch395f_socket_test.py udp_client 约定 */
#ifndef PHASE3_TOTAL_ROUNDS
#define PHASE3_TOTAL_ROUNDS 60
#endif
static void phase3_run(void) {
uint8_t sock = 0;
uint8_t rx_buf[300] = {0};
int32_t counted;
DBG_INFO("=== CH395F Phase 3 Tests (UDP) ===");
/*
* 配置 Socket 0 为 UDP Server:
* - 协议类型 UDP
* - 本地端口 60000
* - 目标 IP/端口初始为广播(0xFF.0xFF.0xFF.0xFF:0),
* 收到数据后根据包头中的源地址动态设置。
*/
sock = 0;
ch395f_set_proto_type(sock, CH395F_PROTO_TYPE_UDP);
ch395f_set_sour_port(sock, 60000);
{
uint8_t bcast[4] = {0xFF, 0xFF, 0xFF, 0xFF};
ch395f_set_des_ip(sock, bcast);
}
ch395f_set_des_port(sock, 0);
TEST_CHECK(ch395f_open_socket(sock) == CH395F_ERR_SUCCESS,
"UDP socket %d opened (port=60000)", sock);
HAL_Delay(200);
/*
* 计数式回显循环:累计 PHASE3_TOTAL_ROUNDS 个有效数据包。
* CH395F UDP 接收数据格式(read_recv_buf 返回):
* [0-1] 保留
* [2-3] 源端口(大端)
* [4-7] 源 IP
* [8+] 载荷数据
* 回显前需根据源 IP/Port 设置目标地址,写回载荷数据。
* 每次 write_send_buf 后需等待 SENDBUF_FREE 中断,确保数据已
* 从 MCU 传输到 CH395F 内部发送缓冲,避免后续接收覆盖。
*
* "HELLO"(5B) 为 PC 就绪探针:照常回显但不计入轮次,
* 使脚本探针阶段不占用用例预算(与 udp_client 脚本约定)。
*/
counted = 0;
while (counted < PHASE3_TOTAL_ROUNDS) {
uint32_t tick = HAL_GetTick();
int32_t echo_ok = 0;
int32_t is_probe = 0;
while (HAL_GetTick() - tick < 30000) {
uint16_t rlen = ch395f_get_recv_len(sock);
/* ⚠ 芯片 RX 缓冲最大 2KB,本地 rx_buf 只有 sizeof(rx_buf):
* 必须夹到本地容量,否则 read_recv_buf 直接栈溢出(2026-10-11 审计修正) */
if (rlen > (uint16_t)sizeof(rx_buf)) {
DBG_INFO(" datagram %u B > local buf %u B, truncated",
rlen, (unsigned)sizeof(rx_buf));
rlen = (uint16_t)sizeof(rx_buf);
}
if (rlen > 8) {
memset(rx_buf, 0, sizeof(rx_buf));
ch395f_read_recv_buf(sock, rx_buf, rlen);
uint16_t payload_len = rlen - 8;
uint16_t src_port = (uint16_t)rx_buf[2] | ((uint16_t)rx_buf[3] << 8);
uint8_t *src_ip = &rx_buf[4];
uint8_t *payload = &rx_buf[8];
is_probe = (payload_len == 5 &&
memcmp(payload, "HELLO", 5) == 0);
ch395f_set_des_ip(sock, src_ip);
ch395f_set_des_port(sock, src_port);
ch395f_write_send_buf(sock, payload, payload_len);
uint32_t sw = HAL_GetTick();
while (HAL_GetTick() - sw < 2000) {
if (ch395f_get_sock_int_status(sock) & CH395F_SINT_STAT_SENBUF_FREE)
break;
HAL_Delay(10);
}
echo_ok = 1;
break;
}
HAL_Delay(50);
}
if (!echo_ok) {
TEST_CHECK(0, "UDP echo #%d timeout", counted + 1);
break;
}
if (!is_probe) {
counted++;
TEST_CHECK(1, "UDP echo #%d", counted);
}
}
ch395f_close_socket(sock);
TEST_CHECK(1, "UDP socket closed");
TEST_REPORT("Phase 3");
}
#endif
/* ===================================================================
* Phase 4 — DHCP 自动获取 IP
*
* 测试目的:验证 CH395F DHCP 客户端功能,包括:
* 1. DHCP DISCOVER → OFFER → REQUEST → ACK 完整握手
* 2. DHCP 成功后 IP 地址非零
* 3. 广播宣告(UDP 广播本机 IP)
* 4. 接收并回显 HELLO 消息
*
* 注意:DHCP 成功后会改变 CH395F 的 IP 配置,需与之前静态 IP 测试隔离。
* 前置条件:PC 上运行 DHCP Server(管理员权限)。
* 通过:DHCP SUCCESS + IP 非零 + HELLO 回显成功。
* 失败:DHCP 超时或 HELLO 无响应。
* =================================================================== */
#ifdef ENABLE_PHASE4_TESTS
#ifndef PHASE4_ANNOUNCE_PORT
#define PHASE4_ANNOUNCE_PORT 60001
#endif
#ifndef PHASE4_ANNOUNCE_SOCK
#define PHASE4_ANNOUNCE_SOCK 6
#endif
#ifndef PHASE4_HELLO_WAIT_MS
#define PHASE4_HELLO_WAIT_MS 30000 /* HELLO 回显窗:PC 在 ACK 后固定 sleep(3)s,
* 窗口须覆盖 PC 侧启动延迟(旧值 5s 重叠仅 ~2s) */
#endif
static void phase4_run(void) {
uint8_t ip_info[20] = {0};
int32_t same = 0;
DBG_INFO("=== CH395F Phase 4 Tests (DHCP) ===");
/*
* 启用 DHCP:ch395f_set_dhcp(1) 使 CH395F 进入 DHCP 自动协商。
* 然后轮询 DHCP 状态寄存器:0x00 = 成功,非 0x00 = 进行中/失败。
* 最多等待 30 秒。
*/
ch395f_set_dhcp(1);
TEST_CHECK(1, "DHCP enabled, waiting for IP...");
HAL_Delay(500);
uint32_t tick = HAL_GetTick();
uint8_t dhcp_ok = 0;
while (HAL_GetTick() - tick < 30000) {
/* DHCP 状态寄存器是真相(纯查询);不读全局寄存器——读即清,
* 唯一读者 = net_poll(唯一读者红线(docs/网络/网络自愈架构设计.md §7 I7)),测试代码同样禁自行读取 */
if (ch395f_get_dhcp_status() == 0x00) { dhcp_ok = 1; break; }
HAL_Delay(500);
}
TEST_CHECK(dhcp_ok, "DHCP completed within 30s");
if (!dhcp_ok) goto phase4_end;
/*
* IP 寄存器提交滞后(2026-08-24 上板实测):DHCP 状态已置成功、
* 芯片已按新租约通信,但此刻 get_ip_inf 仍回读旧静态配置——
* 串口打印与宣告广播会带出过期 IP。
* 对策:轮询直至连续 3 次(间隔 300ms)读到相同非零值才采信。
*/
{
uint8_t last[20] = {0};
tick = HAL_GetTick();
while (HAL_GetTick() - tick < 8000) {
memset(ip_info, 0, sizeof(ip_info));
ch395f_get_ip_inf(ip_info);
if ((ip_info[0] || ip_info[1] || ip_info[2] || ip_info[3]) &&
memcmp(last, ip_info, sizeof(ip_info)) == 0) {
if (++same >= 3) break;
} else {
same = 0;
}
if (memcmp(last, ip_info, sizeof(ip_info)) != 0) {
DBG_INFO(" DHCP IP report: %d.%d.%d.%d",
ip_info[0], ip_info[1], ip_info[2], ip_info[3]);
memcpy(last, ip_info, sizeof(ip_info));
}
HAL_Delay(300);
}
memcpy(ip_info, last, sizeof(ip_info));
}
TEST_CHECK(same >= 3, "IP registers settled (3 consecutive reads)");
DBG_INFO(" IP: %d.%d.%d.%d", ip_info[0], ip_info[1], ip_info[2], ip_info[3]);
TEST_CHECK(ip_info[0]!=0||ip_info[1]!=0||ip_info[2]!=0||ip_info[3]!=0,
"IP non-zero");
/*
* DHCP 成功后广播宣告本机 IP:通过 Socket 6(额外 UDP Socket)
* 向 255.255.255.255:60001 发送 4 字节 IP 地址。
* 然后等待 PC 回复 HELLO(5 字节),收到后原样回显。
*/
{
uint8_t anno_sock = PHASE4_ANNOUNCE_SOCK;
uint8_t bcast[4] = {0xFF,0xFF,0xFF,0xFF};
uint8_t rx_buf[64] = {0};
ch395f_set_proto_type(anno_sock, CH395F_PROTO_TYPE_UDP);
ch395f_set_send_buf(anno_sock, 24, 2);
ch395f_set_recv_buf(anno_sock, 26, 2);
ch395f_set_sour_port(anno_sock, 60000);
ch395f_set_des_ip(anno_sock, bcast);
ch395f_set_des_port(anno_sock, 0);
if (ch395f_open_socket(anno_sock) == CH395F_ERR_SUCCESS) {
ch395f_set_des_ip(anno_sock, bcast);
ch395f_set_des_port(anno_sock, PHASE4_ANNOUNCE_PORT);
ch395f_write_send_buf(anno_sock, ip_info, 4);
uint32_t sw = HAL_GetTick();
while (HAL_GetTick() - sw < 2000) {
if (ch395f_get_sock_int_status(anno_sock) & CH395F_SINT_STAT_SENBUF_FREE) break;
HAL_Delay(10);
}
TEST_CHECK(1, "DHCP announcement sent");
ch395f_set_des_ip(anno_sock, bcast);
ch395f_set_des_port(anno_sock, 0);
int32_t hello_ok = 0;
uint32_t wait = HAL_GetTick();
while (HAL_GetTick() - wait < PHASE4_HELLO_WAIT_MS) {
uint16_t rlen = ch395f_get_recv_len(anno_sock);
/* ⚠ 同 Phase 3:夹到本地缓冲容量,防栈溢出(2026-10-11 审计修正) */
if (rlen > (uint16_t)sizeof(rx_buf)) {
DBG_INFO(" announce %u B > local buf %u B, truncated",
rlen, (unsigned)sizeof(rx_buf));
rlen = (uint16_t)sizeof(rx_buf);
}
if (rlen > 8) {
memset(rx_buf,0,sizeof(rx_buf));
ch395f_read_recv_buf(anno_sock, rx_buf, rlen);
uint16_t plen = rlen - 8;
uint8_t *payload = &rx_buf[8];
if (plen == 5 && memcmp(payload, "HELLO", 5) == 0) {
uint8_t *src_ip = &rx_buf[4];
uint16_t src_port = (uint16_t)rx_buf[2] | ((uint16_t)rx_buf[3] << 8);
ch395f_set_des_ip(anno_sock, src_ip);
ch395f_set_des_port(anno_sock, src_port);
ch395f_write_send_buf(anno_sock, payload, plen);
TEST_CHECK(1, "HELLO echo OK");
hello_ok = 1;
break;
}
}
HAL_Delay(50);
}
TEST_CHECK(hello_ok, "HELLO echo within %dms", PHASE4_HELLO_WAIT_MS);
ch395f_close_socket(anno_sock);
}
}
phase4_end:
TEST_REPORT("Phase 4");
}
#endif
/* ===================================================================
* Phase 5 — 大文件传输(硬件层裸命令)
*
* 测试目的:不依赖 net_socket/netTask,直接用 ch395f_* 命令验证
* TCP 批量数据搬运能力:
* 1. 裸命令建立 TCP Client 连接(open/connect/等 CONNECT)
* 2. 顺序双测向:先连续发送 256KB(PC 只收不回声,SENBUF_FREE 流控),
* 再连续接收 256KB(PC 按相同 LCG 模式回放,滚动校验),内存 O(CHUNK)
* 3. 全量累加和 TX/RX 对总账 + 双向独立吞吐基线
*
* PC 配合:python test/ch395f_socket_test.py tcp_bulk --port 8081 --bulk 262144 --chunk 4096
* (先收满 256KB 不回声,再按 LCG 模式回放 256KB)
* 通过:全部块校验一致,且 SUM(TX)==SUM(RX)。
* 失败:连接超时 / SENBUF_FREE 超时 / RX 停滞 / 数据不一致。
* =================================================================== */
#ifdef ENABLE_PHASE5_TESTS
/* 回传参数(TC-NET-502 合规测试时按档位修改重编译) */
#ifndef PHASE5_REMOTE_IP
#define PHASE5_REMOTE_IP "192.168.1.2"
#endif
#ifndef PHASE5_REMOTE_PORT
#define PHASE5_REMOTE_PORT 8081 /* PC tcp_server 监听端口 */
#endif
#ifndef PHASE5_LOCAL_PORT
#define PHASE5_LOCAL_PORT 50010
#endif
#ifndef PHASE5_TOTAL_BYTES
#define PHASE5_TOTAL_BYTES (256*1024) /* 回传总量 */
#endif
#ifndef PHASE5_CHUNK_SIZE
#define PHASE5_CHUNK_SIZE 4093 /* 单块 = socket 默认发送缓冲,DMA 边界安全 */
#endif
#ifndef PHASE5_CONN_TIMEOUT_MS
#define PHASE5_CONN_TIMEOUT_MS 15000
#endif
#define PHASE5_TX_TIMEOUT_MS 2000 /* 单块 SENBUF_FREE 等待上限 */
#define PHASE5_RX_STALL_TIMEOUT_MS 5000 /* 发送完成后 RX 无进展上限 */
/* 32 位累加和:足以发现丢字节/错位,比 CRC32 便宜 */
static uint32_t phase5_sum32(const uint8_t *p, uint32_t n) {
uint32_t s = 0;
while (n--) s += *p++;
return s;
}
/* 按块索引确定性生成模式数据(LCG):发送与回读两侧可独立复现,逐字节比对 */
static void phase5_gen_block(uint8_t *buf, uint32_t idx) {
uint32_t seed = 0xC2D2E6F9u ^ (idx * 2654435761u);
uint32_t k;
for (k = 0; k < PHASE5_CHUNK_SIZE; k++) {
seed = seed * 1664525u + 1013904223u;
buf[k] = (uint8_t)(seed >> 24);
}
}
static void phase5_run(void) {
static uint8_t tx[PHASE5_CHUNK_SIZE];
static uint8_t rx[PHASE5_CHUNK_SIZE];
static uint8_t exp[PHASE5_CHUNK_SIZE];
uint8_t sock = 0;
uint8_t ip_arr[4];
int32_t chunks;
int32_t bad = 0;
int32_t conn_ok = 0;
int32_t blk_tx = 0;
int32_t blk_rx = 0;
int32_t can_write = 1;
int32_t progress_64k = -1;
uint32_t send_bytes;
uint32_t sum_tx = 0;
uint32_t sum_rx = 0;
uint32_t rx_total = 0;
uint32_t exp_off = 0;
uint32_t rx_stall = 0;
uint32_t tx_sw = 0;
uint32_t t0;
DBG_INFO("=== CH395F Phase 5 Tests (HW Bulk Transfer) ===");
DBG_INFO(" target %s:%d, %d bytes x %d B/chunk",
PHASE5_REMOTE_IP, PHASE5_REMOTE_PORT,
PHASE5_TOTAL_BYTES, PHASE5_CHUNK_SIZE);
/*
* 步1:裸命令建立 TCP 连接。
* 先 close 清理上轮残留(结果不关心),再配置并 connect。
*/
(void)ch395f_close_socket(sock);
HAL_Delay(50);
ch395f_set_proto_type(sock, CH395F_PROTO_TYPE_TCP);
test_parse_ip(PHASE5_REMOTE_IP, ip_arr);
ch395f_set_des_ip(sock, ip_arr);
ch395f_set_des_port(sock, PHASE5_REMOTE_PORT);
ch395f_set_sour_port(sock, PHASE5_LOCAL_PORT);
TEST_CHECK(ch395f_open_socket(sock) == CH395F_ERR_SUCCESS &&
ch395f_tcp_connect(sock) == CH395F_ERR_SUCCESS,
"step1 open+connect issued");
if (ch395f_get_cmd_status() != CH395F_ERR_SUCCESS) {
TEST_REPORT("Phase 5");
return;
}
t0 = HAL_GetTick();
while (HAL_GetTick() - t0 < PHASE5_CONN_TIMEOUT_MS) {
if (ch395f_get_sock_int_status(sock) & CH395F_SINT_STAT_CONNECT) {
conn_ok = 1;
break;
}
HAL_Delay(10);
}
TEST_CHECK(conn_ok, "connected within %dms", PHASE5_CONN_TIMEOUT_MS);
if (!conn_ok) {
(void)ch395f_close_socket(sock);
TEST_REPORT("Phase 5");
return;
}
/*
* 步2A:连续发送 256KB(单向,PC 只收不回声)。
* 节奏:写 4096B → 等 SENBUF_FREE(芯片搬走数据后触发)→ 写下一块,
* 全程不等回显,测得纯 TX 吞吐。
* 步2B:连续接收 256KB(PC 收到 256KB 后按相同 LCG 模式回放),
* 对字节流做滚动校验(期望流按块生成 exp[],字节对位 + sum_rx 累加),
* 天然处理 TCP 分包/粘包,测得纯 RX 吞吐。
*/
chunks = PHASE5_TOTAL_BYTES / PHASE5_CHUNK_SIZE;
send_bytes = (uint32_t)chunks * PHASE5_CHUNK_SIZE; /* 实际上送字节(4093 档 < TOTAL,余尾不送) */
/* ---- 步2A:连续发送 ----
* 目标:把 chunks×PHASE5_CHUNK_SIZE 字节按 LCG 模式数据一次性连续交出,
* 全程不等回显,测得纯 TX 吞吐。
*
* 发送状态机(can_write 标志):
* can_write == 1 : 发送缓冲空闲,可写入下一块
* can_write == 0 : 上一块已写入,等待芯片把数据搬出 socket 发送缓冲
* (首次进入 can_write 初始为 1,因为连接建立后发送缓冲为空)
*
* 芯片写节奏约束(CH395 手册 / Trap 记录):
* 每次 WRITE_SEND_BUF 后必须收到 SINT_STAT_SENBUF_FREE 才能再次写入,
* 否则下次写入会被芯片静默丢弃。SENBUF_FREE 表示芯片已把本次写入的数据
* 从 socket 发送缓冲搬到内部协议栈/MAC 缓冲(腾出空间),故每写一块
* 就转 can_write=0 等待该事件——这正是"连续发送"的天然流控节拍,
* 不会因超过发送缓冲(默认 4096B)而溢出。
*/
{
uint32_t tx_t0 = HAL_GetTick(); /* 发送阶段起点:用于计算 TX 吞吐 */
blk_tx = 0; /* 已交出的块计数 */
can_write = 1; /* 初始:连接后发送缓冲为空,可直接写 */
while (blk_tx < chunks && !bad) {
if (can_write) {
/* ---- 发送缓冲空闲:生成并写入一块 ---- */
phase5_gen_block(tx, (uint32_t)blk_tx); /* 确定性生成第 blk_tx 块模式(TX/RX 可复现) */
sum_tx += phase5_sum32(tx, PHASE5_CHUNK_SIZE); /* 先累加发送侧总账(与 RX 侧对账用) */
ch395f_write_send_buf(sock, tx, PHASE5_CHUNK_SIZE); /* 4096B 单次写入(= 发送缓冲上限,DMA 边界安全) */
blk_tx++; /* 该块已交出 */
can_write = 0; /* 进入等待 SENBUF_FREE 状态 */
tx_sw = HAL_GetTick(); /* 记录等待起点(供超时判定) */
} else {
/* ---- 等待上一次写入被芯片搬走(SENBUF_FREE) ----
* 直读本 Socket SINT(读即清);不读全局寄存器——读即清且
* 唯一读者 = net_poll(唯一读者红线(docs/网络/网络自愈架构设计.md §7 I7))。
*/
if (ch395f_get_sock_int_status(sock) & CH395F_SINT_STAT_SENBUF_FREE) {
can_write = 1; /* 缓冲已腾空,下一轮写入下一块 */
} else if (HAL_GetTick() - tx_sw >= PHASE5_TX_TIMEOUT_MS) {
/* 超时:芯片迟迟未腾出缓冲 —— 对端未收/链路异常/芯片堵死 */
bad++;
DBG_ERROR("TX SENBUF_FREE stall @chunk %d", blk_tx);
break;
}
}
/* ---- 进度打印(每交出 64KB 打一行) ---- */
if ((int)(((uint32_t)blk_tx * PHASE5_CHUNK_SIZE) / (64 * 1024)) != progress_64k) {
progress_64k = (int)(((uint32_t)blk_tx * PHASE5_CHUNK_SIZE) / (64 * 1024));
DBG_INFO(" TX %d/%d blocks (%lu bytes)", blk_tx, chunks,
(uint32_t)blk_tx * PHASE5_CHUNK_SIZE);
}
}
/* 全部块已写入发送缓冲即视为发送完成:最后一块已交给芯片,
* 无需再等其 SENBUF_FREE,数据会随 TCP 正常发往对端。 */
TEST_CHECK(bad == 0, "TX phase: %d/%d blocks (%lu bytes)",
blk_tx, chunks, (uint32_t)blk_tx * PHASE5_CHUNK_SIZE);
{
uint32_t ms = HAL_GetTick() - tx_t0;
DBG_INFO(" TX throughput: %lu bytes in %lums (%lu KB/s)",
(uint32_t)blk_tx * PHASE5_CHUNK_SIZE, ms,
ms > 0 ? (uint32_t)(((uint32_t)blk_tx * PHASE5_CHUNK_SIZE) / ms) : 0u);
}
}
/* ---- 步2B:连续接收 ----
* 目标:PC 收到 256KB 后按相同 LCG 模式回放,MCU 连续排空接收缓冲并对
* 字节流做滚动校验,测得纯 RX 吞吐。
*
* 滚动校验原理:
* 期望流按块确定性生成,同一时刻只保留当前块的 exp[](内存 O(CHUNK))。
* exp_off 指向当前期望块内的偏移;回显流可能分包/粘包,故按"字节流"消费:
* 每次读到 rlen 字节 → 切成若干段(take,每段不超过当前期望块剩余)逐字节
* 与 exp[exp_off+k] 比对,同时累加 sum_rx;当前块消费完(exp_off==CHUNK)
* 则回绕并生成下一块期望 —— 与 TCP 的分包/粘包边界完全解耦。
*
* 停滞检测(rx_stall):
* 记录"最后收到数据"的时刻,>PHASE5_RX_STALL_TIMEOUT_MS 无新数据即失败。
* 用最后进度而非阶段起点,是为了兼容 PC 端回放开始的延迟与块间间隙。
*/
if (!bad) {
uint32_t rx_t0 = HAL_GetTick(); /* 接收阶段起点:用于计算 RX 吞吐 */
exp_off = 0; /* 当前期望块内的消费偏移 */
blk_rx = 0; /* 已完整校验的块数 */
rx_total = 0; /* 已接收字节总数 */
rx_stall = rx_t0; /* 最后收到数据的时刻(停滞检测基准) */
progress_64k = -1; /* 重置进度打印基准(TX 阶段已推进过) */
phase5_gen_block(exp, 0); /* 预生成第 0 块期望数据 */
while (!bad && rx_total < send_bytes) {
/* ---- 排空接收缓冲:一次可能读出多包数据,全部消费完再进入下一步 ---- */
for (;;) {
uint16_t rlen = ch395f_get_recv_len(sock); /* 查询当前可读字节数 */
if (rlen == 0) {
break; /* 缓冲空,跳出排空循环 */
}
if (rlen > PHASE5_CHUNK_SIZE) {
rlen = PHASE5_CHUNK_SIZE; /* 单次读取上限 = 块大小(DMA 边界安全,4KB) */
}
ch395f_read_recv_buf(sock, rx, rlen); /* 一次 SPI 事务读走 rlen 字节 */
{
uint32_t off = 0; /* 本次读取数据内的消费偏移 */
while (off < rlen) {
uint32_t take = PHASE5_CHUNK_SIZE - exp_off; /* 当前期望块剩余 */
uint32_t k;
if (take > (uint32_t)(rlen - off)) {
take = rlen - off; /* 截断:本次读取只消费到块边界为止 */
}
for (k = 0; k < take; k++) {
uint8_t b = rx[off + k];
if (b != exp[exp_off + k]) {
bad++; /* 逐字节比对失败(定位精确偏移) */
DBG_ERROR("rx mismatch @%lu", rx_total + off + k);
}
sum_rx += b; /* 同时累加接收侧总账 */
}
off += take;
exp_off += take;
rx_total += take;
if (exp_off == PHASE5_CHUNK_SIZE) {
exp_off = 0; /* 当前块消费完:回绕并预生成下一块期望 */
blk_rx++;
if (blk_rx < chunks) {
phase5_gen_block(exp, blk_rx);
}
}
}
}
rx_stall = HAL_GetTick(); /* 有数据到达:推进停滞检测基准 */
}
if (bad) {
break;
}
/* ---- 停滞保护:发送方已回放完或链路异常,迟迟无新数据 ---- */
if (HAL_GetTick() - rx_stall >= PHASE5_RX_STALL_TIMEOUT_MS) {
bad++;
DBG_ERROR("RX stall @%lu/%lu", rx_total, send_bytes);
break;
}
/* ---- 进度打印(每收到 64KB 打一行) ---- */
if ((int)(rx_total / (64 * 1024)) != progress_64k) {
progress_64k = (int)(rx_total / (64 * 1024));
DBG_INFO(" RX %lu/%lu bytes (%d blocks)", rx_total, send_bytes, blk_rx);
}
HAL_Delay(2); /* 轮询间隔:让出总线/降低 SPI 事务频率 */
}
TEST_CHECK(bad == 0, "RX phase: %lu/%lu bytes (%d blocks)", rx_total, send_bytes, blk_rx);
{
uint32_t ms = HAL_GetTick() - rx_t0;
DBG_INFO(" RX throughput: %lu bytes in %lums (%lu KB/s)",
rx_total, ms,
ms > 0 ? (uint32_t)(rx_total / ms) : 0u);
}
}
TEST_CHECK(sum_tx == sum_rx, "checksum TX==RX (%08x/%08x)", sum_tx, sum_rx);
/* 步3:关闭,PC 侧应统计到收满 256KB(校验一致)并回放完成 */
ch395f_close_socket(sock);
TEST_CHECK(1, "socket closed");
TEST_REPORT("Phase 5");
}
#endif
/* ===================================================================
* Phase 15 — 自愈驱动原语(无需 PC)
*
* 测试目的:验证 ch395f 新增原语(设计 docs/网络/网络自愈架构设计.md §4.4)
* 语义正确:类型化状态回读一致性 / cmd_done(E3 定案=状态回读)/
* sock_reset 非阻塞状态机跑通(E4 定案序列)/ 链路态缓存 / 事件护栏。
* 污染会话下的复位载荷完整性已由 E4 bench 60 轮实证(《网络自愈架构设计》§5.2 E4),
* 本 Phase 只验"状态机跑通 + 判据语义",不依赖 PC。
* 上下文:硬件层模式(testTask 独占 CH395F),用 Socket 7(产品监听 0~4 不涉)。
* =================================================================== */
#ifdef ENABLE_PHASE15_TESTS
static void phase15_run(void) {
const uint8_t sn = 7U;
ch395f_sock_state_t st;
uint32_t t0;
int done;
int rc;
DBG_INFO("=== CH395F Phase 15 Tests (self-heal phase-2 primitives) ===");
/* ---- TC-NET-1501: 状态回读一致性 + cmd_done 语义(E2/E3 定案形态) ---- */
ch395f_set_proto_type(sn, CH395F_PROTO_TYPE_TCP);
ch395f_set_sour_port(sn, 5107U);
(void)ch395f_open_socket(sn);
t0 = HAL_GetTick();
done = 0;
while ((uint32_t)(HAL_GetTick() - t0) < 50U) {
if (ch395f_cmd_done(sn, CH395F_DONE_OPEN) == 1) { done = 1; break; }
HAL_Delay(1);
}
TEST_CHECK(done == 1, "TC-NET-1501 open done within 50ms");
(void)ch395f_tcp_listen(sn);
t0 = HAL_GetTick();
done = 0;
while ((uint32_t)(HAL_GetTick() - t0) < 20U) {
if (ch395f_cmd_done(sn, CH395F_DONE_LISTEN) == 1) { done = 1; break; }
HAL_Delay(1);
}
/* E2 矩阵:LISTEN 可见延迟 100% <=10ms(24 轮无长尾) */
TEST_CHECK(done == 1, "TC-NET-1501 listen done within 20ms (E2: 100 pct <=10ms)");
TEST_CHECK(ch395f_sock_is_listening(sn) == 1, "TC-NET-1501 cached is_listening");
st = ch395f_sock_state_cached(sn);
TEST_CHECK(st.open == CH395F_SOCKET_OPEN && st.tcp_state == CH395F_TCP_LISTEN,
"TC-NET-1501 cached state 05/01 (got %02X/%02X)", st.open, st.tcp_state);
/* 二段关闭实证(2026-10-13 本 TC 二跑定案):LISTEN 态 Socket 的
* CLOSE_SOCKET 返 SUCCESS 但只拆监听(TCP 01->00),byte0 恒 05;
* 再发一次 CLOSE 才落 00。生产 net 层靠 TRAP-NET-09 open 重试吸收此行为,
* sock_reset 状态机在 WAIT_CLOSE 阶段自行补发(见 ch395f.c)。 */
uint8_t close_st = ch395f_close_socket(sn);
TEST_CHECK(close_st == CH395F_ERR_SUCCESS,
"TC-NET-1501 close cmd st=0x%02X (expect 00)", close_st);
st = ch395f_get_sock_state(sn);
TEST_CHECK(st.open == CH395F_SOCKET_OPEN && st.tcp_state == CH395F_TCP_CLOSED,
"TC-NET-1501 listen-close stage1: 05/00 listen torn down (got %02X/%02X)",
st.open, st.tcp_state);
(void)ch395f_close_socket(sn);
t0 = HAL_GetTick();
done = -1;
while ((uint32_t)(HAL_GetTick() - t0) < 500U) {
if (ch395f_cmd_done(sn, CH395F_DONE_CLOSE) == 1) { done = (int)(HAL_GetTick() - t0); break; }
HAL_Delay(1);
}
TEST_CHECK(done >= 0, "TC-NET-1501 listen-close stage2: byte0=00 in %dms (win 500)", done);
st = ch395f_get_sock_state(sn);
TEST_CHECK(st.tcp_state == CH395F_SOCK_STATE_NONE,
"TC-NET-1501 tcp_state sentinel 0xFF when closed (got %02X)", st.tcp_state);
st = ch395f_get_sock_state(8U);
TEST_CHECK(st.open == CH395F_SOCKET_CLOSED && st.tcp_state == CH395F_SOCK_STATE_NONE,
"TC-NET-1501 sn=8 boundary -> CLOSED+sentinel");
/* ---- TC-NET-1502: sock_reset 非阻塞状态机跑通(E4 定案序列) ---- */
(void)ch395f_open_socket(sn);
rc = ch395f_sock_reset_begin(sn, CH395F_RESET_QUARANTINE);
TEST_CHECK(rc == 0, "TC-NET-1502 begin accepted (rc=%d)", rc);
TEST_CHECK(ch395f_sock_reset_begin(sn, CH395F_RESET_QUARANTINE) == -1,
"TC-NET-1502 busy reject on second begin");
t0 = HAL_GetTick();
rc = 1;
while (rc == 1 && (uint32_t)(HAL_GetTick() - t0) < 3000U) {
rc = ch395f_sock_reset_poll(sn);
HAL_Delay(1);
}
/* QUARANTINE 最短等待 400ms + 各阶段,3s 窗内必落定 */
TEST_CHECK(rc == 0, "TC-NET-1502 reset done within 3000ms (rc=%d)", rc);
TEST_CHECK(ch395f_cmd_done(sn, CH395F_DONE_CLOSE) == 1, "TC-NET-1502 chip CLOSED after reset");
TEST_CHECK(ch395f_sock_reset_poll(sn) == 0, "TC-NET-1502 poll idempotent after done");
/* 复位后可重开(复位语义 = 回到可用态) */
(void)ch395f_open_socket(sn);
TEST_CHECK(ch395f_cmd_done(sn, CH395F_DONE_OPEN) == 1, "TC-NET-1502 reopen after reset OK");
(void)ch395f_close_socket(sn);
/* TC-NET-1502b: LISTEN 态 Socket 复位(走 WAIT_CLOSE 二段补发路径,GRACEFUL) */
(void)ch395f_open_socket(sn);
(void)ch395f_tcp_listen(sn);
rc = ch395f_sock_reset_begin(sn, CH395F_RESET_GRACEFUL);
t0 = HAL_GetTick();
rc = 1;
while (rc == 1 && (uint32_t)(HAL_GetTick() - t0) < 3000U) {
rc = ch395f_sock_reset_poll(sn);
HAL_Delay(1);
}
TEST_CHECK(rc == 0, "TC-NET-1502b reset on LISTEN socket done (rc=%d, two-stage absorbed)", rc);
TEST_CHECK(ch395f_cmd_done(sn, CH395F_DONE_CLOSE) == 1, "TC-NET-1502b chip CLOSED after listen-reset");
/* ---- TC-NET-1503: 链路态缓存 + 事件护栏边界 ---- */
uint16_t c0 = ch395f_link_change_count();
ch395f_glob_event_feed(CH395F_GINT_STAT_PHY_CHANGE); /* 首次 feed 建档 */
ch395f_glob_event_feed(CH395F_GINT_STAT_PHY_CHANGE); /* 链路稳定:不应虚增计数 */
TEST_CHECK(ch395f_link_change_count() == c0, "TC-NET-1503 no spurious link change on stable link");
TEST_CHECK(ch395f_link_up_cached() == 1U, "TC-NET-1503 link_up cached (bench link present)");
TEST_CHECK(ch395f_sock_events(8U) == 0U, "TC-NET-1503 sock_events sn=8 boundary -> 0");
(void)ch395f_sock_events(sn); /* 读即清护栏:合法调用不崩 */
TEST_REPORT("Phase 15");
}
#endif
/*
* CH395F 测试套件入口(由 test_config.c 的 start_test_task 统一调度)。
* 此处只负责 CH395F 自身的硬件层 / NET 层逻辑,套件选择已上移到 test_config.c。
*/
void ch395f_test_run(void)
{
#if defined(ENABLE_HW_LAYER_TESTS)
/* 硬件层模式(阶段 1~5):netTask 未创建,由本任务完成一次性协议栈
* 初始化并独占 CH395F —— 无 net_poll() 并发读取中断,无 SPI 并发事务 */
if (net_stack_bring_up() != 0) {
DBG_ERROR("net stack bring-up FAIL");
} else {
/* ==================== 阻塞阶段 ====================
* Phase 1~5(硬件层)在 TCP 监听之前执行,不依赖外部连接。
* 各 Phase 有各自的测试逻辑,完成后继续下一 Phase。
*/
#ifdef ENABLE_PHASE1_TESTS
phase1_run();
#endif
#ifdef ENABLE_PHASE2_TESTS
phase2_run();
#endif
#ifdef ENABLE_PHASE3_TESTS
phase3_run();
#endif
#ifdef ENABLE_PHASE4_TESTS
phase4_run();
#endif
#ifdef ENABLE_PHASE5_TESTS
phase5_run();
#endif
#ifdef ENABLE_PHASE12_TESTS
/* Phase 12(链路时延基线 SPI_CMD/SPI_DMA,M1):独立于 Phase 1~5 的 SPI 时延
* 采样,放硬件层阶段末尾执行(说明文档:链路延迟测试说明.md) */
net_latency_phase12_run();
#endif
#ifdef ENABLE_PHASE15_TESTS
/* Phase 15(自愈驱动原语):独占态下直调驱动,放 HW 层末尾 */
phase15_run();
#endif
}
#else
/* NET 层模式(阶段 6~11):等待 netTask 完成 net_init() 及 CH395F 初始化。
* Phase 7(TCP Client)与阶段 6/8~11(TCP Server 监听循环)互斥,
* 二者共用单连接 Socket 0,故按宏选择入口。 */
/* 等待 netTask 广播网络就绪(取代 g_net_ready 忙等轮询) */
(void)osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY, osFlagsWaitAny | osFlagsNoClear, osWaitForever);
DBG_INFO("netTask ready");
#if defined(ENABLE_PHASE7_TESTS)
net_layer_client_test_main();
#else
net_layer_test_main();
#endif
#endif
}