Files
STM32F4-Base/test/ch395f_test_task.c
T
2026-08-22 19:53:24 +08:00

1155 lines
43 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/******************************************************************************
* 模块名称:CH395F Network Test Task
* 模块功能:CH395F 全阶段测试入口,统一在独立 FreeRTOS 任务中运行。
* 测试分两组:
* Phase 1/2/3/6/9 — 阻塞式,TCP 监听前依次执行(各自有内部循环/等待)
* Phase 4/5/7/8/10 — 事件驱动,TCP 监听循环中运行(需 PC 客户端连接)
* 所有测试结果通过 TEST_CHECK/TEST_REPORT 宏输出到串口。
* 作者:王建锋
* 创建日期:2026-07-22
******************************************************************************/
#include <string.h>
#include <stdio.h>
#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);
}
}