/****************************************************************************** * 模块名称:CH395F Network Test Task * 模块功能:CH395F 全阶段测试入口,统一在独立 FreeRTOS 任务中运行。 * 测试分两组: * Phase 1~5(硬件层)— 阻塞式独占执行(各自有内部循环/等待),netTask 未运行 * Phase 4~11(NET 层)— 事件驱动,TCP 监听循环中运行(需 PC 客户端连接) * 所有测试结果通过 TEST_CHECK/TEST_REPORT 宏输出到串口。 * 作者:王建锋 * 创建日期:2026-07-22 ******************************************************************************/ #include #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] == 写入的 set_ip(回读与写入一致;值本身任意合法, * 不校验工厂默认——工厂默认断言在 net_cfg 主机单测里钉)。 * 失败:任意字节不匹配(SPI 写入/回读路径异常或寄存器未更新)。 */ { uint8_t set_ip[4] = {192, 168, 31, 100}; ch395f_set_ip_addr(set_ip); memset(ip_info, 0, sizeof(ip_info)); ch395f_get_ip_inf(ip_info); int32_t match = (memcmp(ip_info, set_ip, sizeof(set_ip)) == 0); DBG_INFO(" ch395f_set_ip_addr(%d.%d.%d.%d)", set_ip[0], set_ip[1], set_ip[2], set_ip[3]); DBG_INFO(" expect: IP[0..3] = 写入值(上一行)"); 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 %d.%d.%d.%d)", ip_info[0], ip_info[1], ip_info[2], ip_info[3], set_ip[0], set_ip[1], set_ip[2], set_ip[3]); } /* ---- 设置网关 → 回读验证 ---- * 原理:同「设置 IP → 回读」项,通过 ch395f_set_gwip_addr() 写入,ch395f_get_ip_inf() * 回读的第 5-8 字节为网关地址。 * 通过:回读 GW[0..3] == 写入的 set_gw(同「设置 IP → 回读」项,值任意合法)。 * 失败:任意字节不匹配。 */ { uint8_t set_gw[4] = {192, 168, 31, 1}; ch395f_set_gwip_addr(set_gw); memset(ip_info, 0, sizeof(ip_info)); ch395f_get_ip_inf(ip_info); int32_t match = (memcmp(ip_info + 4, set_gw, sizeof(set_gw)) == 0); DBG_INFO(" ch395f_set_gwip_addr(%d.%d.%d.%d)", set_gw[0], set_gw[1], set_gw[2], set_gw[3]); DBG_INFO(" expect: GW[0..3] = 写入值(上一行)"); 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 %d.%d.%d.%d)", ip_info[4], ip_info[5], ip_info[6], ip_info[7], set_gw[0], set_gw[1], set_gw[2], set_gw[3]); } /* ---- 设置掩码 → 回读验证 ---- * 原理:同「设置 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 TEST_PC_PEER_IP #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 未在 PHASE2_REMOTE_IP: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 发包到板 IP:60000(板 IP = config.ini [network] 设置值) * 会话结构:累计 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 TEST_PC_PEER_IP #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(); #elif defined(ENABLE_PHASE16_TESTS) /* 阶段 16(BSD UDP 数据面真机回归,PR #74):独立入口,不占 TCP 监听 socket; * 与阶段 7/11/14 互斥开法见 ch395f_test.h 里本宏上方的说明。 */ net_layer_udp_bench_test_main(); #else net_layer_test_main(); #endif #endif }