/****************************************************************************** * 模块名称:CH395F Network Test Task * 模块功能:CH395F 全阶段测试入口,统一在独立 FreeRTOS 任务中运行。 * 测试分两组: * Phase 1/2/3/6/9 — 阻塞式,TCP 监听前依次执行(各自有内部循环/等待) * Phase 4/5/7/8/10 — 事件驱动,TCP 监听循环中运行(需 PC 客户端连接) * 所有测试结果通过 TEST_CHECK/TEST_REPORT 宏输出到串口。 * 作者:王建锋 * 创建日期:2026-07-22 ******************************************************************************/ #include #include #include "ch395f_test_task.h" #include "cmsis_os.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_init 成功后置 1) */ 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; /* * 收发缓冲区 —— 全局静态,不在任务栈上。 * s_tx_buf 仅在 Phase 5(大文件传输)和 Phase 8(边界条件)中使用, * 其他 Phase 不需要发送固定模式数据,故条件编译节省 RAM。 */ #define TEST_BUF_SIZE 65536 #if defined(ENABLE_PHASE4_TESTS) || defined(ENABLE_PHASE5_TESTS) || \ defined(ENABLE_PHASE7_TESTS) || defined(ENABLE_PHASE8_TESTS) || \ defined(ENABLE_PHASE10_TESTS) static uint8_t s_rx_buf[TEST_BUF_SIZE]; #endif #if defined(ENABLE_PHASE5_TESTS) || defined(ENABLE_PHASE8_TESTS) static uint8_t s_tx_buf[TEST_BUF_SIZE]; #endif /* =================================================================== * Phase 1 — 寄存器与 SPI 命令路径验证 * * 测试目的:确认 CH395F 与 MCU 之间的 SPI 通信链路正常,所有底层命令 * 能够正确发送并得到预期回复。这是其他所有 Phase 的前提条件。 * * 共 9 项测试(P1-01 ~ P1-09),逐项执行,汇总报告。 * =================================================================== */ #ifdef ENABLE_PHASE1_TESTS static void phase1_run(void) { uint8_t ip_info[20]; 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 Server(python 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 static void phase2_run(void) { uint8_t sock = 0; uint8_t rx_buf[300] = {0}; int i; DBG_INFO("=== CH395F Phase 2 Tests (TCP Client) ==="); /* 解析远程 IP 字符串为字节数组 */ uint8_t ip_arr[4]; { uint32_t a[4]; sscanf(PHASE2_REMOTE_IP, "%lu.%lu.%lu.%lu", &a[0], &a[1], &a[2], &a[3]); ip_arr[0] = (uint8_t)a[0]; ip_arr[1] = (uint8_t)a[1]; ip_arr[2] = (uint8_t)a[2]; ip_arr[3] = (uint8_t)a[3]; } DBG_INFO("Target: %s:%d", PHASE2_REMOTE_IP, PHASE2_REMOTE_PORT); /* * 配置 Socket 0 为 TCP Client 模式: * - 协议类型 TCP * - 目标 IP/端口:PC Server * - 本地端口:循环内每次+1,避免 TIME_WAIT 冲突 */ sock = 0; 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++) { DBG_INFO("--- Iteration %d/%d ---", i + 1, PHASE2_LOOP_COUNT); /* * 每轮使用不同本地端口(防止前一轮 TIME_WAIT 未完全释放导致绑定失败)。 * 先 OPEN() 创建 Socket,再 CONNECT() 发 SYN。 * OPEN() 和 CONNECT() 都是异步命令,需轮询中断状态确认连接完成。 */ ch395f_set_sour_port(sock, PHASE2_LOCAL_PORT + i); if (ch395f_open_socket(sock) != CH395F_ERR_SUCCESS) { TEST_CHECK(0, "open socket"); continue; } if (ch395f_tcp_connect(sock) != CH395F_ERR_SUCCESS) { TEST_CHECK(0, "tcp connect"); ch395f_close_socket(sock); continue; } /* 等待 CONNECT 中断(最多 PHASE2_TIMEOUT_MS) */ uint32_t tick = HAL_GetTick(); uint8_t connected = 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) { connected = 1; break; } } HAL_Delay(10); } if (!connected) { TEST_CHECK(0, "connect timeout"); ch395f_close_socket(sock); continue; } HAL_Delay(100); /* * 发送 64 字节测试数据(前部为可读字符串,后部填充数字字符)。 * 然后等待 PC Server 回显(将收到的数据原样发回), * 收到后立即把回显数据再次发送回去(实现双向验证)。 */ { uint8_t test_buf[64] = {0}; const char *msg = "HelloFromCH395F_TCP_Client! This is a 64-byte test message."; int p, mlen = (int)strlen(msg); memcpy(test_buf, msg, mlen); for (p = mlen; p < 64; p++) test_buf[p] = (uint8_t)('0' + (p - mlen) % 10); ch395f_write_send_buf(sock, test_buf, 64); } /* 等待回显(最多 10 秒),收到后原样发回作为二次验证 */ int got_echo = 0; uint32_t wait = HAL_GetTick(); while (HAL_GetTick() - wait < 10000) { uint16_t rlen = ch395f_get_recv_len(sock); if (rlen > 0) { uint16_t read_len = (rlen > sizeof(rx_buf)) ? (uint16_t)sizeof(rx_buf) : rlen; memset(rx_buf, 0, sizeof(rx_buf)); ch395f_read_recv_buf(sock, rx_buf, read_len); ch395f_write_send_buf(sock, rx_buf, read_len); got_echo = 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) break; if (si & CH395F_SINT_STAT_SOCK_TIMEOUT) break; } HAL_Delay(50); } TEST_CHECK(got_echo, "Echo #%d", i + 1); /* * 关闭 Socket(使用 close_socket 而非 disconnect,避免进入 FIN_WAIT_2)。 * 等待状态机回到 CLOSED(CH395F_SOCKET_CLOSED = 0x05)。 */ ch395f_close_socket(sock); { uint8_t st[2]; wait = HAL_GetTick(); while (HAL_GetTick() - wait < 5000) { ch395f_get_socket_status(sock, st); if (st[0] == CH395F_SOCKET_CLOSED) break; HAL_Delay(50); } DBG_INFO("closed: sock=0x%02X tcp=0x%02X", st[0], st[1]); } HAL_Delay(200); } 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 包到 192.168.1.100:60000 * 通过:30/30 轮回显成功。 * 失败:任意轮回显超时或解析错误。 * =================================================================== */ #ifdef ENABLE_PHASE3_TESTS static void phase3_run(void) { uint8_t sock = 0; uint8_t rx_buf[300] = {0}; int i; 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); /* * 循环接收并回显,最多 30 轮。 * CH395F UDP 接收数据格式(read_recv_buf 返回): * [0-1] 保留 * [2-3] 源端口(大端) * [4-7] 源 IP * [8+] 载荷数据 * 回显前需根据源 IP/Port 设置目标地址,写回载荷数据。 * 每次 write_send_buf 后需等待 SENDBUF_FREE 中断,确保数据已 * 从 MCU 传输到 CH395F 内部发送缓冲,避免后续接收覆盖。 */ for (i = 0; i < 30; i++) { uint32_t tick = HAL_GetTick(); int echo_ok = 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]; 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); } TEST_CHECK(echo_ok, "UDP echo #%d", i + 1); if (!echo_ok) break; } ch395f_close_socket(sock); TEST_CHECK(1, "UDP socket closed"); TEST_REPORT("Phase 3"); } #endif /* =================================================================== * Phase 4 — NET 层 TCP Echo(基础收发) * * 测试目的:验证 BSD Socket API(net_socket)的单连接 TCP Echo 通路。 * Socket 0 复用监听和数据通道。PC 发任意数据,MCU 原样返回。 * 这是 Phase 5/7/8/10 的基础。 * * 入口条件:Socket 0 处于 ESTABLISHED(TCP 连接已建立)。 * 通过:数据收发一致。 * 失败:send/recv 数量不匹配。 * =================================================================== */ #ifdef ENABLE_PHASE4_TESTS static uint32_t s_phase4_count = 0; static void phase4_run(int sockfd) { /* * 使用 NET_MSG_DONTWAIT 非阻塞接收 —— 没有数据时立即返回, * 不阻塞任务循环(Phase 5/7/8/10 也共享同一个循环)。 * 收到数据后立即原样发送回显。 */ int n = net_recv(sockfd, s_rx_buf, TEST_BUF_SIZE, NET_MSG_DONTWAIT); if (n > 0) { int sent = net_send(sockfd, s_rx_buf, n, 0); if (sent != n) { DBG_ERROR("echo: send %d != recv %d", sent, n); return; } s_phase4_count++; if (s_phase4_count <= 10 || (s_phase4_count % 100 == 0)) { DBG_INFO("Phase4: echo #%lu (%d bytes)", s_phase4_count, n); } } } #endif /* =================================================================== * Phase 5 — 大文件传输(超过 CH395F 硬件缓冲 4KB) * * 测试目的:验证超过 Socket 0 接收缓冲(4KB)的数据在 net_send/ * net_recv 层的正确处理。net_send_locked 自动将大数据 * 拆分为 ≤4096B 的块,每块通过一次 DMA 事务写入 CH395F。 * 接收端同样分多次调用 ch395f_read_recv_buf 读空硬件缓冲。 * * 数据模式:递增字节(s_tx_buf[i] = i & 0xFF),接收端按 offset 逐字节比对。 * 通过:全部 20480 字节逐字节匹配。 * 失败:任意字节不一致或收发过程中断。 * =================================================================== */ #ifdef ENABLE_PHASE5_TESTS #define PHASE5_FILE_SIZE 20480 static void phase5_fill(uint8_t *buf, uint32_t size) { for (uint32_t i = 0; i < size; i++) { buf[i] = (uint8_t)(i & 0xFF); } } static int phase5_verify(uint8_t *buf, uint32_t size, uint32_t offset) { for (uint32_t i = 0; i < size; i++) { if (buf[i] != (uint8_t)((offset + i) & 0xFF)) { DBG_ERROR("Phase5: mismatch at %lu (expect 0x%02X, got 0x%02X)", offset + i, (uint8_t)((offset + i) & 0xFF), buf[i]); return 0; } } return 1; } static void phase5_run(int sockfd) { DBG_INFO("Phase5: starting %d-byte file transfer", PHASE5_FILE_SIZE); phase5_fill(s_tx_buf, PHASE5_FILE_SIZE); /* * 发送阶段:分块发送,每块最多 4096B。 * net_send 内部通过消息队列转发到 netTask,由 net_send_locked * 按 CH395F_SPI_DMA_MAX_PAYLOAD(4096)自动拆分 DMA 事务。 * 等待 PC 端回显(echod)全部数据。 */ uint32_t sent = 0; while (sent < PHASE5_FILE_SIZE) { int chunk = (PHASE5_FILE_SIZE - sent) > 4096 ? 4096 : (PHASE5_FILE_SIZE - sent); int n = net_send(sockfd, s_tx_buf + sent, chunk, 0); if (n <= 0) { DBG_ERROR("Phase5: send fail at %lu (ret=%d)", sent, n); return; } sent += (uint32_t)n; DBG_INFO("Phase5: sent %lu/%d", sent, PHASE5_FILE_SIZE); } /* * 接收阶段:分块接收,逐块验证。 * PC 回显数据需与发送数据逐字节一致(递增模式)。 * 验证失败立即返回,不继续接收剩余数据。 */ uint32_t recvd = 0; while (recvd < PHASE5_FILE_SIZE) { int n = net_recv(sockfd, s_rx_buf, TEST_BUF_SIZE, 0); if (n <= 0) { DBG_ERROR("Phase5: recv fail at %lu (ret=%d)", recvd, n); return; } int ok = phase5_verify(s_rx_buf, (uint32_t)n, recvd); TEST_CHECK(ok, "Phase5: verify at %lu (%d bytes)", recvd, n); if (!ok) return; recvd += (uint32_t)n; } TEST_CHECK(1, "Phase5: %d-byte file transfer complete", PHASE5_FILE_SIZE); } #endif /* =================================================================== * Phase 6 — 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_PHASE6_TESTS #ifndef PHASE6_ANNOUNCE_PORT #define PHASE6_ANNOUNCE_PORT 60001 #endif #ifndef PHASE6_ANNOUNCE_SOCK #define PHASE6_ANNOUNCE_SOCK 6 #endif static void phase6_run(void) { uint8_t ip_info[20] = {0}; DBG_INFO("=== CH395F Phase 6 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 phase6_end; ch395f_get_ip_inf(ip_info); 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 = PHASE6_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, PHASE6_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 < 5000) { 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 5s"); ch395f_close_socket(anno_sock); } } phase6_end: TEST_REPORT("Phase 6"); } #endif /* =================================================================== * Phase 7 — Select/Poll I/O 多路复用 * * 测试目的:验证 net_select() API 在单 Socket 模式下正确工作: * 1. Socket 0 有数据到达时 select 返回正数 * 2. NET_FD_ISSET 正确指示就绪的 Socket * 3. 超时(50ms 无数据)时返回 0 * * 入口条件:Socket 0 处于 ESTABLISHED。 * 通过:select 检测到数据并成功回显。 * 失败:select 未检测到数据或回显失败。 * =================================================================== */ #ifdef ENABLE_PHASE7_TESTS #include "net_select.h" static uint32_t s_phase7_count = 0; static void phase7_run(int sockfd) { net_fd_set readfds; net_timeval tv; /* * 配置 select:只监测 Socket 0 的可读状态,超时 50ms。 * NET_FD_ZERO 清空集合,NET_FD_SET 添加 Socket 0。 * 50ms 超时确保不阻塞任务循环太久。 */ NET_FD_ZERO(&readfds); NET_FD_SET(sockfd, &readfds); tv.tv_sec = 0; tv.tv_usec = 50000; int nfds = net_select(sockfd + 1, &readfds, NULL, NULL, &tv); if (nfds <= 0) return; /* Socket 0 可读:非阻塞接收并回显 */ if (NET_FD_ISSET(sockfd, &readfds)) { int n = net_recv(sockfd, s_rx_buf, TEST_BUF_SIZE, NET_MSG_DONTWAIT); if (n > 0) { int sent = net_send(sockfd, s_rx_buf, n, 0); if (sent == n) { s_phase7_count++; TEST_CHECK(1, "Phase7: select echo #%lu (%d bytes)", s_phase7_count, n); } } } } #endif /* =================================================================== * Phase 8 — 边界条件测试 * * 测试目的:验证以下 DMA 和协议栈边界情况: * 1. TC-NET-801: 4095B(SPI 最大载荷 -1,单次 DMA 容纳) * 2. TC-NET-802: 4096B(= SPI 最大载荷,填满单次 DMA) * 3. TC-NET-803: 4097B(超过 SPI 最大载荷,拆 2 次 DMA) * 4. TC-NET-804: 零长度发送(返回 -1) * 5. TC-NET-805: 20 × 100B 连续快速发送 * * 各用例通过状态机(s_phase8_stage)串行执行,每次 phase8_run 推进一个 * 步骤(发送→等待→接收→验证→下一阶段)。发送数据为递增模式字节 * (buf[i] = seed + i),接收端按相同模式验证。 * * 入口条件:Socket 0 处于 ESTABLISHED。 * 通过:各用例自身判定。 * =================================================================== */ #ifdef ENABLE_PHASE8_TESTS static int s_phase8_stage = 0; static uint32_t s_phase8_recv = 0; static void phase8_fill(uint8_t *buf, uint32_t size, uint8_t seed) { for (uint32_t i = 0; i < size; i++) { buf[i] = (uint8_t)(seed + i); } } static int phase8_verify(uint8_t *buf, uint32_t size, uint8_t seed) { for (uint32_t i = 0; i < size; i++) { if (buf[i] != (uint8_t)(seed + i)) return 0; } return 1; } static void phase8_run(int sockfd) { /* 阶段 0 → 1:发送 4095B */ if (s_phase8_stage == 0) { DBG_INFO("Phase8: 4095B (DMA-1)"); phase8_fill(s_tx_buf, 4095, 0x10); int sent = net_send(sockfd, s_tx_buf, 4095, 0); TEST_CHECK(sent == 4095, "Phase8: send 4095B (sent=%d)", sent); s_phase8_recv = 0; s_phase8_stage = 1; return; } /* 阶段 1:接收并验证 4095B 回显 */ if (s_phase8_stage == 1) { int n = net_recv(sockfd, s_rx_buf + s_phase8_recv, (int)(4095 - s_phase8_recv), 0); if (n > 0) { s_phase8_recv += (uint32_t)n; if (s_phase8_recv >= 4095) { TEST_CHECK(phase8_verify(s_rx_buf, 4095, 0x10), "Phase8: verify 4095B"); s_phase8_stage = 2; } } return; } /* 阶段 2 → 3:发送 4096B(DMA 边界精确值) */ if (s_phase8_stage == 2) { DBG_INFO("Phase8: 4096B (DMA exact)"); phase8_fill(s_tx_buf, 4096, 0x20); int sent = net_send(sockfd, s_tx_buf, 4096, 0); TEST_CHECK(sent == 4096, "Phase8: send 4096B (sent=%d)", sent); s_phase8_recv = 0; s_phase8_stage = 3; return; } /* 阶段 3:接收并验证 4096B 回显 */ if (s_phase8_stage == 3) { int n = net_recv(sockfd, s_rx_buf + s_phase8_recv, (int)(4096 - s_phase8_recv), 0); if (n > 0) { s_phase8_recv += (uint32_t)n; if (s_phase8_recv >= 4096) { TEST_CHECK(phase8_verify(s_rx_buf, 4096, 0x20), "Phase8: verify 4096B"); s_phase8_stage = 4; } } return; } /* 阶段 4 → 5:发送 4097B(超过 DMA 上限,自动拆 4096+1) */ if (s_phase8_stage == 4) { DBG_INFO("Phase8: 4097B (DMA+1, 2 SPI xacts)"); phase8_fill(s_tx_buf, 4097, 0x30); int sent = net_send(sockfd, s_tx_buf, 4097, 0); TEST_CHECK(sent == 4097, "Phase8: send 4097B (sent=%d)", sent); s_phase8_recv = 0; s_phase8_stage = 5; return; } /* 阶段 5:接收并验证 4097B 回显 */ if (s_phase8_stage == 5) { int n = net_recv(sockfd, s_rx_buf + s_phase8_recv, (int)(4097 - s_phase8_recv), 0); if (n > 0) { s_phase8_recv += (uint32_t)n; if (s_phase8_recv >= 4097) { TEST_CHECK(phase8_verify(s_rx_buf, 4097, 0x30), "Phase8: verify 4097B"); s_phase8_stage = 6; } } return; } /* 阶段 6:零长度发送 —— 期望 net_send 返回 -1(NET_ERR_INVAL) */ if (s_phase8_stage == 6) { DBG_INFO("Phase8: zero-length send"); int sent = net_send(sockfd, s_tx_buf, 0, 0); TEST_CHECK(sent < 0, "Phase8: send 0B returns %d (expect <0)", sent); s_phase8_stage = 7; return; } /* 阶段 7:20 × 100B 快速发送 */ if (s_phase8_stage == 7) { DBG_INFO("Phase8: 20x rapid sends (100B each)"); phase8_fill(s_tx_buf, 100, 0x40); int all_ok = 1; for (int i = 0; i < 20; i++) { if (net_send(sockfd, s_tx_buf, 100, 0) != 100) { TEST_CHECK(0, "Phase8: rapid #%d fail", i + 1); all_ok = 0; break; } osDelay(1); } if (all_ok) { TEST_CHECK(1, "Phase8: 20x rapid sends OK"); s_phase8_recv = 0; s_phase8_stage = 8; } else { s_phase8_stage = 0; } return; } /* 阶段 8:接收 2000B 回显总和并验证 */ if (s_phase8_stage == 8) { int n = net_recv(sockfd, s_rx_buf + s_phase8_recv, (int)(2000 - s_phase8_recv), 0); if (n > 0) { s_phase8_recv += (uint32_t)n; if (s_phase8_recv >= 2000) { TEST_CHECK(1, "Phase8: all echo received"); TEST_REPORT("Phase 8"); s_phase8_stage = 0; } } return; } } #endif /* =================================================================== * Phase 9 — PHY 链路恢复 * * 测试目的:验证 CH395F 的 PHY 状态监测功能: * 1. 拔网线后 net_poll 检测到 PHY_CHANGE 中断并识别 DISCONN * 2. 插回网线后 PHY 自动恢复连接 * * 需物理操作(拔插网线),测试会等待最多 15s 检测断开、30s 检测重连。 * 通过:断开和重连均被检测到。 * 失败:任意步骤超时。 * =================================================================== */ #ifdef ENABLE_PHASE9_TESTS static void phase9_run(void) { DBG_INFO("=== CH395F Phase 9 Tests (PHY Recovery) ==="); /* * 第一步:等待 PHY 断开(需拔网线)。 * 轮询 GINT 状态寄存器中的 PHY_CHANGE 位, * 然后读取 PHY 状态确认是否为 DISCONN。 * 超时 15 秒 —— 若超时未检测到,提示拔网线。 */ uint8_t phy_disconnected = 0; uint32_t wait = HAL_GetTick(); while (HAL_GetTick() - wait < 15000) { if (ch395f_get_glob_int_status_all() & CH395F_GINT_STAT_PHY_CHANGE) { if (ch395f_get_phy_status() == CH395F_PHY_DISCONN) { TEST_CHECK(1, "PHY disconnect detected"); phy_disconnected = 1; break; } } HAL_Delay(100); } if (!phy_disconnected) { TEST_CHECK(0, "PHY disconnect NOT detected (unplug cable?)"); goto phase9_end; } /* * 第二步:等待 PHY 重连(需插回网线)。 * 同样检测 PHY_CHANGE 中断,然后读取 PHY 状态确认不是 DISCONN。 * 超时 30 秒。 */ uint8_t phy_reconnected = 0; wait = HAL_GetTick(); while (HAL_GetTick() - wait < 30000) { if (ch395f_get_glob_int_status_all() & CH395F_GINT_STAT_PHY_CHANGE) { uint8_t phy = ch395f_get_phy_status(); if (phy != CH395F_PHY_DISCONN) { TEST_CHECK(1, "PHY reconnected (0x%02X)", phy); phy_reconnected = 1; break; } } HAL_Delay(100); } if (!phy_reconnected) { TEST_CHECK(0, "PHY reconnect NOT detected (re-plug cable?)"); } phase9_end: TEST_REPORT("Phase 9"); } #endif /* =================================================================== * Phase 10 — TCP KeepAlive 超时断开 * * 测试目的:验证 CH395F 的 TCP KeepAlive 机制: * 1. 连接空闲超过 idle 阈值(60s)后开始发送探测包 * 2. 探测无响应(默认 3 次 × 5s = 15s)后触发 TIMEOUT * 3. Socket 自动关闭并回到 LISTEN * * 入口条件:Socket 0 处于 ESTABLISHED,PC 连接后静置(不发数据)。 * 通过:KeepAlive 断开时间 ≥ 60s(从连接建立到 recv 返回 0 的间隔)。 * 失败:不到 60s 就断开(可能是其他原因导致)或 75s 后仍未断开。 * =================================================================== */ #ifdef ENABLE_PHASE10_TESTS static int s_phase10_armed = 0; static uint32_t s_phase10_start = 0; static void phase10_run(int sockfd) { /* * 第一次调用:记录开始时间并 arm。 * 后续调用:非阻塞 recv,若返回 0 表示连接已断开(KeepAlive 触发)。 * 计算耗时与 60s idle 阈值比较。 */ if (!s_phase10_armed) { s_phase10_armed = 1; s_phase10_start = HAL_GetTick(); DBG_INFO("Phase10: idle wait for KeepAlive timeout (60s)"); return; } int n = net_recv(sockfd, s_rx_buf, 64, NET_MSG_DONTWAIT); if (n == 0) { uint32_t elapsed = HAL_GetTick() - s_phase10_start; TEST_CHECK(elapsed >= 60000, "Phase10: KeepAlive triggered after %lums (expect >=60000ms)", elapsed); TEST_REPORT("Phase 10"); s_phase10_armed = 0; } } #endif /* =================================================================== * 任务入口:StartCh395fTestTask * * 执行顺序: * 1. 等待 g_net_ready(netTask 初始化完成标志) * 2. 阻塞执行 Phase 1/2/3/6/9(各 Phase 内部有独立循环和等待) * 3. 创建 TCP 监听 Socket(单连接模式,端口 8080) * 4. 进入事件驱动循环,等待 PC 连接并执行 Phase 4/5/7/8/10 * * 单连接模式说明: * Socket 0 复用监听和数据通道。accept 返回的 fd 与监听 fd 相同。 * 连接断开后需重新 listen。连接恢复检测在循环末尾通过 * net_get_sock() 检查状态并自动处理。 * =================================================================== */ void StartCh395fTestTask(void *argument) { (void)argument; int sockfd = -1; int phase_active = 0; uint32_t accept_retry = 0; struct net_sockaddr_in client_addr; int addrlen = sizeof(client_addr); DBG_INFO("Test task started, waiting for netTask..."); /* 等待 netTask 完成 net_init() 及 CH395F 初始化 */ while (!g_net_ready) { osDelay(100); } DBG_INFO("netTask ready"); /* ==================== 阻塞阶段 ==================== * Phase 1/2/3/6/9 在 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_PHASE6_TESTS phase6_run(); #endif #ifdef ENABLE_PHASE9_TESTS phase9_run(); #endif /* ==================== 创建 TCP 监听 ==================== */ DBG_INFO("Creating test socket..."); sockfd = net_socket(NET_AF_INET, NET_SOCK_STREAM, 0); if (sockfd < 0) { DBG_ERROR("Test task: net_socket failed"); goto task_end; } DBG_INFO("Test task: socket %d created", sockfd); { struct net_sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = NET_AF_INET; addr.sin_port = net_htons(8080); if (net_bind(sockfd, (struct net_sockaddr *)&addr, sizeof(addr)) < 0) { DBG_ERROR("Test task: net_bind failed"); goto task_end; } DBG_INFO("Test task: bound to port 8080"); } if (net_listen(sockfd, 0) < 0) { DBG_ERROR("Test task: net_listen failed"); goto task_end; } DBG_INFO("Test task: listening on 8080 (single-socket mode)"); /* ==================== 事件驱动阶段 ==================== * Phase 4/5/7/8/10 在 TCP 监听循环中运行,需要 PC 客户端连接。 * 没有连接时循环等待;连接建立后执行对应 Phase。 * 断开后自动重新 listen。 */ for (;;) { if (!phase_active) { int acc = net_accept(sockfd, (struct net_sockaddr *)&client_addr, &addrlen); if (acc < 0) { if (accept_retry % 100 == 0) { DBG_INFO("Test task: waiting for connection..."); } accept_retry++; osDelay(100); continue; } accept_retry = 0; phase_active = 1; DBG_INFO("Test task: client accepted (fd=%d)", acc); /* 连接建立,重置各 Phase 内部状态 */ #ifdef ENABLE_PHASE8_TESTS s_phase8_stage = 0; #endif #ifdef ENABLE_PHASE10_TESTS s_phase10_armed = 0; #endif } /* 每 10ms 依次执行各已启用 Phase */ #ifdef ENABLE_PHASE4_TESTS phase4_run(sockfd); #endif #ifdef ENABLE_PHASE5_TESTS phase5_run(sockfd); phase_active = 0; /* Phase 5 一次性完成,发完即止 */ #endif #ifdef ENABLE_PHASE7_TESTS phase7_run(sockfd); #endif #ifdef ENABLE_PHASE8_TESTS phase8_run(sockfd); #endif #ifdef ENABLE_PHASE10_TESTS phase10_run(sockfd); #endif /* * 连接状态检查:通过 net_get_sock 获取 Socket 控制块, * 如果连接已断开(state != ESTABLISHED),关闭并重新 listen。 * 这是单连接模式的关键 —— Socket 0 必须始终处于 LISTEN 或 ESTABLISHED。 */ { net_sock_t *sk = net_get_sock(sockfd); if (sk == NULL || !sk->in_use || sk->state != NET_SOCK_STATE_ESTABLISHED) { DBG_INFO("Test task: connection closed, re-listen..."); net_close(sockfd); osDelay(500); net_listen(sockfd, 0); phase_active = 0; } } osDelay(10); } task_end: for (;;) { osDelay(1000); } }