/****************************************************************************** * 模块名称: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 "net_test_task.h" #include "cmsis_os.h" #include "net_task.h" #include "net_socket.h" #include "ch395f_test.h" #include "ch395f.h" #define DBG_TAG "[TEST_TASK]" #include "dbg_log.h" /* netTask 初始化完成标志(由 net_task.c 在 net_stack_bring_up 成功后置 1)。 * 仅 NET 层模式使用 —— 硬件层模式下 netTask 不创建,由本任务自行初始化。 */ extern volatile uint8_t g_net_ready; /* * 全局测试统计数据,由 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 项测试(P1-01 ~ P1-09),逐项执行,汇总报告。 * =================================================================== */ /* 解析点分十进制 IP 字符串为数组(避免引入 sscanf 库)。 * 仅硬件层阶段(1~5)使用,NET 层无需此辅助。 */ #if defined(ENABLE_HW_LAYER_TESTS) static void test_parse_ip(const char *str, uint8_t *ip_arr) { uint32_t val; int 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_HW_LAYER_TESTS */ #ifdef ENABLE_PHASE1_TESTS static void phase1_run(void) { uint8_t ip_info[20]; /* ---- 前置清全局中断(读即清除) ---- * net_stack_bring_up() 在 set_phy(100M_FULL) 强制重协商 + 等待链路建立 * 期间,芯片会锁存 GINT_STAT_PHY_CHANGE(0x0004)标志;硬件层模式下无 * netTask 轮询消费它,若不清除将导致 P1-06 的"原始状态 0x0000"检查误报。 * 此处读取一次丢弃,保证 P1-06 反映的是测试期间的纯净状态。 */ (void)ch395f_get_glob_int_status_all(); DBG_INFO("=== CH395F Phase 1 Tests ==="); DBG_INFO("--- 9 test items (P1-01 ~ P1-09) ---"); /* ---- P1-01: ch395f_check_exist() ---- * 原理:SPI 写入命令 0x06 (CMD_CHECK_EXIST) 后跟测试字节 0x57。 * CH395F 硬件将测试字节按位取反后返回,期望收到 ~0x57 = 0xA8。 * 通过:函数返回 CH395F_ERR_SUCCESS(0x00)= SPI 回显取反匹配。 * 失败:返回 CH395F_STATUS_NOT_DETECTED(0xFF)= SPI 通信异常或芯片未响应。 */ { ch395f_status_t ret = ch395f_check_exist(); DBG_INFO(" [P1-01] 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, "P1-01 check_exist = 0x%02X (expect 0x00=~0x57=0xA8 match)", (uint8_t)ret); } /* ---- P1-02: ch395f_get_version() ---- * 原理:SPI 写入命令 0x27 (CMD_GET_IC_VER),读取芯片版本字节。 * 通过:版本值 == 0x4A(CH395F 硬件版本)。 * 失败:版本值 ≠ 0x4A(芯片型号不匹配、未正确初始化或损坏)。 * 注意:不同批次的 CH395F 版本号可能不同,若更换芯片需更新此判断值。 */ { uint8_t ver = (uint8_t)ch395f_get_version(); DBG_INFO(" [P1-02] ch395f_get_version()"); DBG_INFO(" expect: 0x4A"); DBG_INFO(" actual: 0x%02X", ver); TEST_CHECK(ver == 0x4A, "P1-02 version = 0x%02X (expect 0x4A)", ver); } /* ---- P1-03: 设置 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); int match = (ip_info[0] == 192 && ip_info[1] == 168 && ip_info[2] == 1 && ip_info[3] == 100); DBG_INFO(" [P1-03] 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, "P1-03 IP = %d.%d.%d.%d (expect 192.168.1.100)", ip_info[0], ip_info[1], ip_info[2], ip_info[3]); } /* ---- P1-04: 设置网关 → 回读验证 ---- * 原理:同 P1-03,通过 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); int match = (ip_info[4] == 192 && ip_info[5] == 168 && ip_info[6] == 1 && ip_info[7] == 1); DBG_INFO(" [P1-04] 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, "P1-04 GW = %d.%d.%d.%d (expect 192.168.1.1)", ip_info[4], ip_info[5], ip_info[6], ip_info[7]); } /* ---- P1-05: 设置掩码 → 回读验证 ---- * 原理:同 P1-03,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); int match = (ip_info[8] == 255 && ip_info[9] == 255 && ip_info[10] == 255 && ip_info[11] == 0); DBG_INFO(" [P1-05] 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, "P1-05 MASK = %d.%d.%d.%d (expect 255.255.255.0)", ip_info[8], ip_info[9], ip_info[10], ip_info[11]); } /* ---- P1-06: ch395f_get_glob_int_status_all()(16 位版) ---- * 原理:读取全局中断状态寄存器(2 字节版本,含所有 Socket 中断位)。 * 在无任何网络事件的空闲状态下,应为 0x0000。 * 通过:返回 0x0000。 * 失败:返回非 0x0000(有残留中断,如前序操作异常触发但未清除)。 * 注意:此寄存器为读后清零,故先读 16 位版再读 1 字节版。 */ { uint16_t gint_all = ch395f_get_glob_int_status_all(); DBG_INFO(" [P1-06] ch395f_get_glob_int_status_all()"); DBG_INFO(" expect: 0x0000"); DBG_INFO(" actual: 0x%04X", gint_all); TEST_CHECK(gint_all == 0x0000, "P1-06 glob_int_status_all = 0x%04X (expect 0x0000)", gint_all); } /* ---- P1-07: 打开→关闭 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(" [P1-07] 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, "P1-07 open=%d close=%d cmd=0x%02X (expect 0x00)", open_ret, close_ret, cmd_st); } /* ---- P1-08: 设置 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(" [P1-08] 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, "P1-08 set_arp CMD_STATUS = 0x%02X (expect 0x00)", cmd_st); } /* ---- P1-09: 设置 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(" [P1-09] 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, "P1-09 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-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, "TC201 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, "TC201 step2 open"); if (r != CH395F_ERR_SUCCESS) return; /* 步3:CONNECT */ r = ch395f_tcp_connect(sock); TEST_CHECK(r == CH395F_ERR_SUCCESS, "TC201 step3 connect"); if (r != CH395F_ERR_SUCCESS) { ch395f_close_socket(sock); return; } /* 步4:等待 CONNECT 中断 * * CH395F 的中断上报是【两级】结构: * * 第 1 级 —— 全局中断状态 GET_GLOB_INT_STATUS_ALL(16 位,读即清除) * 只回答"哪个 Socket 发生了事件",不回答具体是什么事件。 * 16 位布局(见 ch395f.h GINT_STAT 宏定义): * bit0 UNREACH (0x0001) 目标不可达 * bit1 IP_CONFLI (0x0002) IP 冲突 * bit2 PHY_CHANGE (0x0004) 链路状态变化 * bit3 DHCP (0x0008) DHCP 事件 * bit4~7 Socket0~3 (0x0010/0x20/0x40/0x80) * bit8~11 Socket4~7 (0x0100~0x0800) * bit15 DHCPV6 (0x8000) * * 第 2 级 —— Socket 中断状态 CMD_GET_INT_STATUS_SN(8 位,读即清除) * 才包含具体事件类型:SENBUF_FREE / SEND_OK / RECV_OK / * CONNECT / DISCONNECT / DISCARD / SOCK_TIMEOUT。 * 必须在第 1 级提示"本 Socket 有事件"后再读,否则读了也是空。 * * 【掩码 (1U << (sock + 4)) 的由来】 * 全局状态的 bit0~3 被 4 个全局事件占用,因此 Socket N 的事件标志 * 固定从 bit4 开始排列 —— Socket N 对应第 (N + 4) 位: * sock=0 → 1<<4 = 0x0010(CH395F_GINT_STAT_SOCK0) * sock=1 → 1<<5 = 0x0020(CH395F_GINT_STAT_SOCK1) * ... * sock=7 → 1<<11 = 0x0800(CH395F_GINT_STAT_SOCK7) * `int_st & (1U << (sock + 4))` 非 0 * ⇔ 芯片报告"Socket [sock] 有待处理事件", * 此时才值得再花一次 SPI 事务去读第 2 级确认具体事件。 */ tick = HAL_GetTick(); ok = 0; while (HAL_GetTick() - tick < PHASE2_TIMEOUT_MS) { /* 第 1 级:读全局状态。注意本次读取本身会清掉全部全局标志 * 并拉高 INT#(读即清除),所以每轮循环都在重新采样。 */ uint16_t int_st = ch395f_get_glob_int_status_all(); /* 本 Socket 在全局寄存器中有事件挂起? */ if (int_st & (1U << (sock + 4))) { /* 第 2 级:读本 Socket 的具体事件类型(同样读即清除) */ 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, "TC201 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) { uint16_t int_st = ch395f_get_glob_int_status_all(); if (int_st & (1U << (sock + 4))) { uint8_t si = ch395f_get_sock_int_status(sock); if (si & CH395F_SINT_STAT_SENBUF_FREE) { ok = 1; break; } } HAL_Delay(10); } TEST_CHECK(ok, "TC201 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, "TC201 step6 echo verify"); ok = 1; /* 标记"收到过数据",循环外的 echo rcvd 检查不再重复报错 */ break; /* 校验结论已得出,无论 PASS/FAIL 都跳出 */ } /* ---- ② 连接异常检查(还没等到数据才走到这里)---- * 两级中断:全局状态确认本 Socket 有事件 → 再读具体事件类型。 * 注意先查数据再查断开:断开事件可能和最后一批数据同时到达, * 数据优先保证"临终回显"不被漏掉。 */ uint16_t int_st = ch395f_get_glob_int_status_all(); if (int_st & (1U << (sock + 4))) { 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, "TC201 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, "TC201 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; int k; int 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, "TC202 step1 open"); if (r != CH395F_ERR_SUCCESS) return; /* 步2:CONNECT */ r = ch395f_tcp_connect(sock); TEST_CHECK(r == CH395F_ERR_SUCCESS, "TC202 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) { uint16_t int_st = ch395f_get_glob_int_status_all(); if (int_st & (1U << (sock + 4))) { uint8_t si = ch395f_get_sock_int_status(sock); if (si & CH395F_SINT_STAT_CONNECT) { ok = 1; break; } } HAL_Delay(10); } TEST_CHECK(ok, "TC202 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) { uint16_t int_st = ch395f_get_glob_int_status_all(); if (int_st & (1U << (sock + 4))) { uint8_t si = ch395f_get_sock_int_status(sock); if (si & CH395F_SINT_STAT_SENBUF_FREE) { ok = 1; break; } } HAL_Delay(10); } TEST_CHECK(ok, "TC202 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, "TC202 step5 echo verify"); ok = 1; break; } uint16_t int_st = ch395f_get_glob_int_status_all(); if (int_st & (1U << (sock + 4))) { uint8_t si = ch395f_get_sock_int_status(sock); if (si & (CH395F_SINT_STAT_DISCONNECT | CH395F_SINT_STAT_SOCK_TIMEOUT)) break; } HAL_Delay(50); } if (!ok) TEST_CHECK(0, "TC202 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, "TC202 step6 closed"); HAL_Delay(200); } static void phase2_tc202(void) { uint8_t sock = 0; uint8_t ip_arr[4]; int 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-203(硬件层):TCP Client 连接超时(负向测试,P1) * * 前置环境:PC 未在 192.168.1.2:8081 提供服务(与 TC-201/202 互斥, * 需先关闭 PC 端 tcp_server)。固件自行辨别环境错误: * 若等待期间误收到 CONNECT,说明 PC 服务未关,判 FAIL 并给出指引。 * * 负向通过路径(本质都是"未建立连接",按事件+耗时细分原因): * SOCK_TIMEOUT(快, <3s) —— 实测主路径:对端在线但端口关闭,RST 拒绝, * 芯片毫秒级上报,并非等待重传耗尽 * SOCK_TIMEOUT(慢, ≥3s) —— 对端无响应(掉电/防火墙丢包),重传耗尽后报超时 * DISCONNECT —— 预留兼容路径(SYN_SENT 阶段实测基本不出现) * UNREACH(全局位) —— ARP 失败、对端不在线(仅作备注,不影响判定) * * 已知陷阱(Trap 14):上一会话 close 的 DISCONNECT 会滞后锁存在 * SINT_SN(wait-CLOSED 的 STATUS 查询不清中断)。防御双保险: * ① 步 2 前预清两级中断; * ② 判负向后用 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, "TC203 step1 cfg proto/ip/port"); if (ch395f_get_cmd_status() != CH395F_ERR_SUCCESS) return; /* 预清陈旧事件(Trap 14):上一会话 close 的 DISCONNECT 可能滞后锁存 * 在 SINT_SN 中(wait-CLOSED 轮询的 STATUS 是纯查询、不清中断), * 若不清除,本用例首次读中断会误吃旧事件导致负向误判。 */ (void)ch395f_get_glob_int_status_all(); (void)ch395f_get_sock_int_status(sock); /* 步2:OPEN + CONNECT。命令为异步下发,此时均应立即返回成功, * 真正的失败结果由步 3 的中断体现 */ r = ch395f_open_socket(sock); TEST_CHECK(r == CH395F_ERR_SUCCESS, "TC203 step2 open"); if (r != CH395F_ERR_SUCCESS) return; r = ch395f_tcp_connect(sock); TEST_CHECK(r == CH395F_ERR_SUCCESS, "TC203 step2 connect"); if (r != CH395F_ERR_SUCCESS) { ch395f_close_socket(sock); return; } /* 步3:30s 两级中断轮询,等"连不上"的证据。 * 先扫全局 UNREACH 位作备注,再查本 socket 位取具体事件。 * * 实测行为(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) { uint16_t int_st = ch395f_get_glob_int_status_all(); if (int_st & CH395F_GINT_STAT_UNREACH) { DBG_INFO(" global UNREACH: ARP fail (peer offline)"); } if (int_st & (1U << (sock + 4))) { 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 14 第二道防线):负向事件与实际状态矛盾时以状态为准。 * 若收到"负向事件"但 socket 实际已 ESTABLISHED,说明刚才读到的是 * 上一会话遗留的陈旧 DISCONNECT,真实结果是连接成功 —— 环境错误。 */ if (ev == 1) { uint8_t st_chk[2]; ch395f_get_socket_status(sock, st_chk); if (st_chk[0] == 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, "TC203 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, "TC203 step3 connected?! stop PC server and retry"); } else { TEST_CHECK(0, "TC203 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, "TC203 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-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-301(PC 发 64B);第 31~60 轮 = TC-302(1B/64B/200B) * "HELLO"(5B) 为 PC 就绪探针:照常回显但不计入轮次 * 通过:60/60 有效轮回显成功。 * 失败:任意轮回显超时或解析错误。 * =================================================================== */ #ifdef ENABLE_PHASE3_TESTS /* UDP 有效回显轮数:1~30 = TC-301(64B),31~60 = TC-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}; int 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(); int echo_ok = 0; int is_probe = 0; while (HAL_GetTick() - tick < 30000) { uint16_t rlen = ch395f_get_recv_len(sock); 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) { uint16_t gs = ch395f_get_glob_int_status_all(); if (gs & (1U << (sock + 4))) { 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}; int 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) { ch395f_get_glob_int_status_all(); 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) { uint16_t gs = ch395f_get_glob_int_status_all(); if (gs & (1U << (anno_sock + 4))) { 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); int hello_ok = 0; uint32_t wait = HAL_GetTick(); while (HAL_GetTick() - wait < PHASE4_HELLO_WAIT_MS) { ch395f_get_glob_int_status_all(); uint16_t rlen = ch395f_get_recv_len(anno_sock); 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 默认发送缓冲,DP-04 边界 */ #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]; int chunks; int bad = 0; int conn_ok = 0; int blk_tx = 0; int blk_rx = 0; int can_write = 1; int 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) { uint16_t gs = ch395f_get_glob_int_status_all(); if ((gs & (1U << (sock + 4))) && (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 单次写入(= 发送缓冲上限,DP-04 边界) */ blk_tx++; /* 该块已交出 */ can_write = 0; /* 进入等待 SENBUF_FREE 状态 */ tx_sw = HAL_GetTick(); /* 记录等待起点(供超时判定) */ } else { /* ---- 等待上一次写入被芯片搬走(SENBUF_FREE) ---- * 两级中断轮询(DP-02):先读全局中断状态确认本 socket 有事件, * 再读该 socket 的中断状态确认具体为 SENBUF_FREE。 * 读全局中断状态即清除中断标志(读即清)。 */ uint16_t gs = ch395f_get_glob_int_status_all(); if ((gs & (1U << (sock + 4))) && /* socket 中断位(socket 0 → bit 4) */ (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; /* 单次读取上限 = 块大小(DP-04 4KB DMA 边界) */ } 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 void StartCh395fTestTask(void *argument) { (void)argument; DBG_INFO("Test task started"); #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 } #else /* NET 层模式(阶段 6~11):等待 netTask 完成 net_init() 及 CH395F 初始化。 * Phase 7(TCP Client)与阶段 6/8~11(TCP Server 监听循环)互斥, * 二者共用单连接 Socket 0,故按宏选择入口。 */ while (!g_net_ready) { osDelay(100); } DBG_INFO("netTask ready"); #if defined(ENABLE_PHASE7_TESTS) net_layer_client_test_main(); #else net_layer_test_main(); #endif #endif for (;;) { osDelay(1000); } }