Files
STM32F4-Base/docs/CH395F_Test_Guide.md
2026-08-24 16:16:18 +08:00

970 lines
49 KiB
Markdown
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 网络测试规范
## 1. 概述
本文档定义 CH395F 以太网驱动STM32F407ZGTxSPI2 接口)在**单连接 socket 模式**下的测试规范。测试按阶段TM-PHY-NN组织每个阶段包含独立测试用例TC-NET-NNN
### 设计原则
| 原则 | 描述 | 验证用例 |
|------|------|----------|
| DP-01 | 单连接 socketSocket 0 唯一复用监听和数据通道CONNECT 后切换数据通道DISCONNECT/TIMEOUT 后重新 OPEN → LISTEN | TC-NET-201~406 |
| DP-02 | 中断集中处理:所有 CH395F 中断在 `net_poll()` 中通过 `GET_GLOB_INT_STATUS` 统一读取并清除ISR 及其他位置不处理 | TC-NET-101 |
| DP-03 | 线程安全:所有 CH395F 操作通过 `netMsgQueue` 在 netTask 串行处理 | TC-NET-301~302 |
| DP-04 | DMA 边界安全:`CH395F_SPI_DMA_BUF_SIZE=4100`,单次事务最大载荷 `4096B`,超限自动分块 | TC-NET-801~805 |
### CH395F 缓冲架构(关键参数)
| 参数 | 值 | 说明 |
|------|-----|------|
| CH395F 内部总缓冲 | 32KB32 块 × 1KB | Socket 0~3 各 4KB 默认 |
| Socket 0 发送缓冲 | 4096B硬件默认 | 未显式配置时使用 |
| Socket 0 接收缓冲 | 4096B硬件默认 | 未显式配置时使用 |
| **SPI DMA 缓冲** | **4100B** | `CH395F_SPI_DMA_BUF_SIZE` |
| **单次 SPI 最大载荷** | **4096B** | `CH395F_SPI_DMA_MAX_PAYLOAD` |
| 单次 net_send 最大分块 | 4096B | 超限自动分多次 DMA |
| recv_len 最大值 | 65535 | `uint16_t` |
> **关键约束**CH395F 硬件接收缓冲 4096B。`recv_len` 在调 `read_recv_buf` 读取**部分**数据后归零Trap 13。因此每次接收调用需确保 `read_len >= recv_len`,或增大 DMA 缓冲以一次性读空。当前 `CH395F_SPI_DMA_BUF_SIZE=4100` 可覆盖完整 4096B 接收缓冲。
### 大文件传输20KB+)策略
单连接模式下,超过 Socket 0 硬件缓冲4KB的数据通过以下机制传输
```
发送端net_send(buf, 20480)
→ net_send_locked 分块,每块 ≤ 4096B
→ 块 1: ch395f_write_send_buf(sock, buf[0..4095], 4096) // DMA 事务 1
→ 等待 SENDBUF_FREE
→ 块 2: ch395f_write_send_buf(sock, buf[4096..8191], 4096) // DMA 事务 2
→ ...
→ 共 5 次 DMA 事务完成 20KB
接收端net_recv(buf, 20480)
→ net_recv_locked 循环拉取
→ ch395f_get_recv_len() 返回硬件缓冲中当前可用数据≤4096B
→ ch395f_read_recv_buf() 一次性读空硬件缓冲≤4096B/DMA 事务)
→ 循环直至全部 20480B 收完
```
**边界条件**
- 发送 `≤4096B`:一次 DMA 事务完成
- 发送 `4097B`:两次 DMA 事务4096 + 1
- 接收缓冲满4096B必须一次性读空否则 `recv_len` 归零导致剩余数据丢失
- 发送 `0B``net_send` 返回 `-1``NET_ERR_INVAL`
### 测试环境
```
PC (192.168.1.2) ──── 交换机 ──── CH395F (192.168.1.100)
MCU: STM32F407ZGTx @ 168MHz, Keil MDK-ARM v5 (ARMCC)
```
### 构建模式与执行顺序
测试固件通过条件编译分为**两种互斥形态**`test/ch395f_test.h` 中同时启用两层会触发 `#error`
| 构建模式 | 启用宏 | 固件形态 |
|----------|--------|----------|
| **硬件层模式** | `ENABLE_PHASE1~5_TESTS` | **netTask 不创建**CH395F 协议栈初始化由 ch395fTestTask 完成后独占芯片执行阶段 1~5裸调 `ch395f_*` 命令) |
| **NET 层模式** | `ENABLE_PHASE6~10_TESTS` | 完整网络栈netTask 运行 `net_poll()` 轮询testTask 经 `net_socket` API 在 TCP 监听循环中执行阶段 6~10 |
**执行顺序严格递增**:先烧录硬件层固件完成阶段 1→2→3→4→5全部通过再切换为 NET 层固件执行阶段 6→…→10。硬件层测试期间 netTask 轮询不存在,从根上排除中断偷取与 SPI 并发问题。
### 阶段编号对照2026-08 重排)
| 新阶段 | 内容 | 旧阶段 | 层 |
|--------|------|--------|-----|
| 阶段 1 | 寄存器/SPI 命令验证 | 阶段 1 | 硬件层 |
| 阶段 2 | TCP Client 收发/重连 | 阶段 2 | 硬件层 |
| 阶段 3 | UDP Server 回显 | 阶段 3 | 硬件层 |
| 阶段 4 | DHCP | 阶段 6 | 硬件层 |
| 阶段 5 | 大文件传输(硬件层) | 新增(替代原 PHY 测试) | 硬件层 |
| 阶段 6 | NET 层 TCPEcho + Client 扩展) | 阶段 4 | NET 层 |
| 阶段 7 | 大文件传输 | 阶段 5 | NET 层 |
| 阶段 8 | Select/Poll | 阶段 7 | NET 层 |
| 阶段 9 | 边界条件 | 阶段 8 | NET 层 |
| 阶段 10 | KeepAlive | 阶段 10 | NET 层 |
> TC 用例 IDTC-NET-xxx保持不变仅阶段号/TM-PHY 重排;历史记录按上表追溯。
> 原阶段 2 内嵌的 NET 层部分TC-NET-204/205/206已并入**阶段 6**「Client 模式扩展」——硬件层阶段1~5不再包含任何 BSD Socket API 用例。
### 启用测试
编辑 `test/ch395f_test.h`,取消注释对应 `ENABLE_PHASEx_TESTS` 宏(同一模式内 Phase 6/7/8/9 可共存于 testTask 循环)。
---
## 2. 阶段 1寄存器与 SPI 命令验证
| 阶段 ID | TM-PHY-01 |
|---------|-----------|
| **类型** | 独立运行 |
| **耗时** | ~1 秒 |
| **入口** | `ch395fTestTask` 启动后自动运行 |
| **出口** | 串口 "Phase 1 Tests Complete" 且 0 失败 |
### TC-NET-101: SPI 命令路径完整性
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-101 |
| **优先级** | P0 |
| **类型** | 功能测试 |
| **标题** | 验证所有 SPI 命令路径返回预期值 |
| **前置条件** | 1. CH395F 已上电SPI2 已初始化<br>2. 未启用其他阶段 |
**测试步骤与判定标准**
| 步 | 操作 | 成功标准 | 失败标准 |
|----|------|----------|----------|
| 1 | `ch395f_check_exist()`<br>SPI写: CMD=0x06, DATA=0x57<br>回读: ~0x57 = 0xA8 | 返回 `0x00``CH395F_ERR_SUCCESS`<br>= 回读字节 `0xA8` 匹配 `~0x57` | 返回 `0xFF``CH395F_STATUS_NOT_DETECTED`<br>= 回读字节 ≠ `0xA8`,回显取反不匹配 |
| 2 | `ch395f_get_version()` | 版本值 == `0x4A`CH395F 芯片版本) | 版本值 ≠ `0x4A`(芯片不匹配或未响应) |
| 3 | `ch395f_set_ip_addr(192.168.1.100)``ch395f_get_ip_inf()` 回读 IP | `ip_info[0..3] == 192.168.1.100` | 任意字节不匹配(写入/回读不一致) |
| 4 | `ch395f_set_gwip_addr(192.168.1.1)``ch395f_get_ip_inf()` 回读 GW | `ip_info[4..7] == 192.168.1.1` | 任意字节不匹配 |
| 5 | `ch395f_set_mask_addr(255.255.255.0)``ch395f_get_ip_inf()` 回读 MASK | `ip_info[8..11] == 255.255.255.0` | 任意字节不匹配 |
| 6 | `ch395f_get_glob_int_status_all()`16 位)<br>注:测试入口处已预读一次清除 `PHY_CHANGE` 残留(`net_stack_bring_up()` 强制 PHY 重协商 + 等待链路期间锁存,硬件层模式无 netTask 消费) | 返回值 == `0x0000`(无待处理中断) | 返回值 ≠ `0x0000`(有残留中断未清除) |
| 7 | `ch395f_open_socket(0)``ch395f_close_socket(0)``ch395f_get_cmd_status()` | `CMD_STATUS == 0x00``CH395F_ERR_SUCCESS` | `CMD_STATUS``0x00`Socket 操作失败) |
| 8 | `ch395f_set_arp(10, 5)``ch395f_get_cmd_status()` | `CMD_STATUS == 0x00` | `CMD_STATUS``0x00` |
| 9 | `ch395f_set_ttl(0, 64)``ch395f_get_cmd_status()` | `CMD_STATUS == 0x00` | `CMD_STATUS``0x00` |
**通过标准**9/9 通过0 失败。任一失败 → 阶段标记 FAIL后续阶段不执行
**覆盖原则**DP-02
---
## 3. 阶段 2TCP ClientCH395F 主动发起连接)
| 阶段 ID | TM-PHY-02 |
|---------|-----------|
| **类型** | 需 PC 配合 |
| **耗时** | ~60 秒 |
| **入口** | `ch395fTestTask` 启动后自动运行 |
| **出口** | 串口 "Phase 2 Tests Complete" 且 0 失败 |
**分层说明**:本阶段属于**硬件层模式**netTask 不创建CH395F 由 ch395fTestTask 独占),仅使用 `ch395f_*` 裸命令验证芯片 TCP Client 行为TCP 状态机、`CH395F_SINT_STAT_CONNECT` / `CH395F_SINT_STAT_SOCK_TIMEOUT` 中断、发送/接收缓冲与 `CH395F_SOCKET_CLOSED` 状态流转。
---
### TC-NET-201硬件层: TCP Client 连接与数据交换
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-201 |
| **优先级** | P0 |
| **类型** | 功能测试(硬件层) |
| **标题** | ch395f TCP Client → PC TCP Server64 字节收发 |
| **前置条件** | PC: `python test/ch395f_socket_test.py tcp_server --port 8081`PC 作 TCP 回显服务器,收到数据原样回显) |
**测试步骤与判定标准**
| 步 | 操作 | 成功标准 | 失败标准 |
|----|------|----------|----------|
| 1 | `ch395f_set_proto_type(0, CH395F_PROTO_TYPE_TCP)` + 设置目标 IP `192.168.1.2`、端口 `8081`、本地端口 `50000` | 配置写入成功CMD_STATUS == `0x00` | 配置失败SPI 异常或寄存器未更新) |
| 2 | `ch395f_open_socket(0)` 打开 Socket 0 | 返回 `CH395F_ERR_SUCCESS``0x00` | 返回值非 `0x00`Socket 硬件异常或 SPI 通信失败) |
| 3 | `ch395f_tcp_connect(0)` 发起 TCP 连接 | 返回 `CH395F_ERR_SUCCESS``0x00` | 返回值非 `0x00`(连接命令下发失败) |
| 4 | 轮询 `GET_GLOB_INT_STATUS``(sock+4)` 位 + `SINT_STAT` 等待 `CH395F_SINT_STAT_CONNECT`,超时 10s | 检测到 CONNECT 中断(连接已建立,进入 ESTABLISHED | 超时未建立连接(目标不可达 / 链路异常) |
| 5 | `ch395f_write_send_buf(0, buf, 64)` 发送 64 字节递增模式数据(`0x00..0x3F` | 发送完成且产生 `SENDBUF_FREE` 中断 | 发送缓冲未释放 / 发送失败 |
| 6 | 轮询 `GET_RECV_LEN` > 0 读取 PC 回显,超时 10sPC 端 tcp_server 收到数据后原样回显) | 收到 64 字节且与原数据逐字节一致 | 回显超时或任意字节不符 |
| 7 | `ch395f_close_socket(0)` 并轮询 `GET_SOCKET_STATUS` 等待 `CH395F_SOCKET_CLOSED` | Socket 回到 CLOSED`0x00`)状态 | 5s 内未回到 CLOSED状态机卡死 |
**通过标准**7/7 通过0 失败。任一失败 → 阶段标记 FAIL后续阶段不执行
**覆盖原则**DP-01open→connect→send→recv→close 主路径,硬件层)
---
### TC-NET-202硬件层: TCP Client 关闭重连 ×3 轮
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-202 |
| **优先级** | P0 |
| **类型** | 功能测试 / 恢复测试(硬件层) |
| **标题** | 关闭→重连循环 3 轮,每轮含数据交换 |
| **前置条件** | 1. PC 运行 `python ch395f_socket_test.py tcp_server --port 8081`TCP 回显服务器)<br>2. TC-NET-201 通过Socket 0 硬件正常) |
**测试步骤与判定标准**(每轮 i = 0,1,2本地端口取 `50000 + i` 规避 TIME_WAIT
| 步 | 操作 | 成功标准 | 失败标准 |
|----|------|----------|----------|
| 1 | `ch395f_open_socket(0)` 打开 Socket 0 | 返回 `CH395F_ERR_SUCCESS``0x00` | 返回值非 `0x00`Socket 硬件异常或 SPI 通信失败) |
| 2 | `ch395f_tcp_connect(0)` 发起 TCP 连接 | 返回 `CH395F_ERR_SUCCESS``0x00` | 返回值非 `0x00`(连接命令下发失败) |
| 3 | 轮询 `GET_GLOB_INT_STATUS``(sock+4)` 位,并读取 `SINT_STAT` 等待 `CH395F_SINT_STAT_CONNECT`,超时 10s | 检测到 CONNECT 中断(连接已建立) | 超时未建立连接(对端/链路异常) |
| 4 | `ch395f_write_send_buf(0, buf, 64)` 发送 64 字节递增模式数据(`0x00..0x3F` | 发送完成且产生 `SENDBUF_FREE` 中断 | 发送缓冲未释放 / 发送失败 |
| 5 | 轮询 `GET_RECV_LEN` > 0 读取 PC 回显,超时 10sPC 端 tcp_server 收到数据后原样回显) | 收到 64 字节且与原数据逐字节一致 | 回显超时或任意字节不符 |
| 6 | `ch395f_close_socket(0)` 并轮询 `GET_SOCKET_STATUS` 等待 `CH395F_SOCKET_CLOSED` | Socket 回到 CLOSED`0x00`)状态 | 5s 内未回到 CLOSED状态机卡死 |
**通过标准**3/3 轮全部 6 步通过0 失败。任一失败 → 阶段标记 FAIL后续不执行
**覆盖原则**DP-01open→connect→close→re-open→reconnect硬件层
---
### TC-NET-203硬件层: TCP Client 连接超时
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-203 |
| **优先级** | P1 |
| **类型** | 负向测试(硬件层) |
| **标题** | 目标端口无服务时连接超时 |
| **前置条件** | 1. PC **未**运行 TCP Server`192.168.1.2:8081` 无监听)<br>2. TC-NET-201 通过 |
**测试步骤与判定标准**
| 步 | 操作 | 成功标准 | 失败标准 |
|----|------|----------|----------|
| 1 | `ch395f_set_proto_type(0, CH395F_PROTO_TYPE_TCP)` + 设置目标 IP `192.168.1.2`、端口 `8081` | 配置写入成功CMD_STATUS == `0x00` | 配置失败SPI 异常或寄存器未更新) |
| 2 | `ch395f_open_socket(0)``ch395f_tcp_connect(0)` | OPEN / CONNECT 均返回 `CH395F_ERR_SUCCESS``0x00`),命令异步下发不立即报错 | 命令返回值非 `0x00`Socket 硬件异常) |
| 3 | 轮询 `GET_GLOB_INT_STATUS``(sock+4)` 位,等待负向结果,超时窗 30s同时扫全局 `UNREACH` 位作备注 | 收到 `SOCK_TIMEOUT``DISCONNECT`,且期间未收到 `CH395F_SINT_STAT_CONNECT`。按耗时细分:<br>**<3s** = RST 快速拒绝(端口关闭,实测主路径——芯片毫秒级上报 SOCK_TIMEOUT<br>**≥3s** = 真无应答(离线/防火墙丢包)。全局 `UNREACH`ARP 失败)仅备注不影响判定 | 误收到 `CONNECT`PC 服务未关闭,环境错误——串口给出明确指引)或 30s 内无任何中断(状态机挂死) |
| 4 | `ch395f_close_socket(0)` 并轮询 `GET_SOCKET_STATUS` 等待 `CH395F_SOCKET_CLOSED` | Socket 正常关闭、系统无崩溃 / 死锁 | 关闭异常或任务挂死 |
**通过标准**:检测到 `SOCK_TIMEOUT` / `DISCONNECT` 且系统稳定0 崩溃。任一异常 → 阶段标记 FAIL
**覆盖原则**DP-01异常路径的 connect 失败处理,硬件层)
---
## 4. 阶段 3UDP Server EchoCH395F 作为 UDP Server
| 阶段 ID | TM-PHY-03 |
|---------|-----------|
| **类型** | 需 PC 配合 |
| **耗时** | ~60 秒 |
| **入口** | `ENABLE_PHASE3_TESTS`ch395fTestTask 启动后自动运行 |
| **出口** | 串口 "Phase 3 Tests Complete" 且 0 失败 |
**分层说明**:本阶段属于**硬件层模式**netTask 不创建),仅用 `ch395f_*` 裸命令验证芯片 UDP 能力UDP Socket 打开、数据报接收、**包头源地址解析**与按源地址动态回显。
> CH395F UDP 接收帧格式(`ch395f_read_recv_buf` 返回):[0-1] 保留|[2-3] 源端口(大端)|[4-7] 源 IP[8+] 载荷。回显前须以此源地址重设目标,且每次发送后等 `SENDBUF_FREE` 确认Trap 04
---
### TC-NET-301硬件层: UDP 基础回显
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-301 |
| **优先级** | P0 |
| **类型** | 功能测试(硬件层) |
| **标题** | 30 轮 64B UDP 数据报回显 |
| **前置条件** | PC: `python test/ch395f_socket_test.py udp_client --ip 192.168.1.100 --port 60000` |
> **会话结构**(固件单会话,与 udp_client 脚本约定):累计 **60 个有效数据包**后关闭 Socket——第 1~30 轮对应本用例PC 发 64B第 31~60 轮对应 TC-302变长脚本 `"HELLO"` 就绪探针只回显、**不计数**,不占用用例预算。
**测试步骤与判定标准**
| 步 | 操作 | 成功标准 | 失败标准 |
|----|------|----------|----------|
| 1 | 配置 Socket 0proto=UDP、`sour_port=60000`、des=广播 `255.255.255.255:0`,后 `open_socket(0)` | OPEN 返回 `0x00` | 返回非 `0x00`Socket/SPI 异常) |
| 2 | 轮询 `GET_RECV_LEN``rlen > 8` 时读收并解析包头源端口/源 IP单轮超时 30s | 收到 PC 数据报且解析合法 | 30s 无数据(链路/环境异常) |
| 3 | 以源地址重设目标 → `write_send_buf` 回显载荷 → 等 `SENBUF_FREE`(超时 2s | 发送缓冲释放,进入下一轮 | 发送缓冲超时 |
| 4 | 重复至第 30 个有效包后 `close_socket(0)`(串口按 `UDP echo #N` 连续递增) | 前 30 轮全部 PASS | 任意轮回显超时/解析错误(提前终止) |
**通过标准**30/30 轮回显成功0 失败。任一失败 → 阶段标记 FAIL
**覆盖原则**DP-03UDP 收发路径,包头源地址解析)
---
### TC-NET-302硬件层: UDP 合规测试(变长载荷)
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-302 |
| **优先级** | P1 |
| **类型** | 合规测试(硬件层) |
| **标题** | 变长载荷1B / 64B / 200B各 10 轮回显,验证 `rlen-8` 解析与载荷完整性 |
| **前置条件** | 同 TC-NET-301同一固件会话的第 31~60 轮);载荷上限受 `rx_buf[300]` 约束≤292B |
**测试步骤**:同 TC-NET-301 流程PC 端 udp_client Phase C 按 1B→64B→200B 各 `--count` 轮(默认 10循环发送MCU 回显后由 PC 逐字节比对一致性。
**通过标准**30/30 轮回显成功且 PC 端比对无误0 失败。任一失败 → 阶段标记 FAIL
**覆盖原则**DP-03变长载荷边界
---
## 5. 阶段 4DHCP 动态获取 IP
| 阶段 ID | TM-PHY-04 |
|---------|-----------|
| **类型** | PC 配合(脚本绑定 UDP/67Windows 需**管理员权限** |
| **耗时** | ~10s上限 30s DISCOVER 轮询 + 30s HELLO 等待) |
| **入口** | `ENABLE_PHASE4_TESTS`ch395fTestTask 启动后自动运行 |
| **出口** | 串口 "Phase 4: N/N PASSED (failed=0)" |
| **注意** | **改变 CH395F IP 配置**,必须独立运行(勿与其他阶段连跑) |
### 机制与会话结构
**DHCP 报文流**PC = DHCP ServerCH395F = Client
```
CH395F PC (dhcp_server)
| DISCOVER (xid, chaddr) ---------> | data[242]==1
| <-------- OFFER (yiaddr=分配IP) | xid/chaddr 原样回填,双广播发送
| REQUEST (server_id 校验) ------> | data[242]==3
| <-------- ACK (掩码/网关/租约) | data[242]==5
应用 IP -> Socket6 广播宣告 -> HELLO 回显
```
**关键实现约束**
- **xid/chaddr 回填**OFFER/ACK 必须原样回传 DISCOVER 的 `xid([4:8])``chaddr([28:34])`Trap 03早期 xid 偏移错误导致芯片丢弃响应)
- **option53 位置**:报文类型在 `data[242]`236B 固定头 + magic `63825363` + option 偏移),`len<=250` 视为无效包
- **双广播发送**OFFER/ACK 同时发 `255.255.255.255``192.168.1.255`(规避 Windows 定向广播路由限制)
- **REQUEST 重试**PC 最多等 8×5s期间收到重复 DISCOVER 则重发 OFFER
**固件会话结构**phase4_run
| 序 | 操作 | 参数 |
|----|------|------|
| 1 | `ch395f_set_dhcp(1)` 启动协商 | - |
| 2 | 轮询 `GET_DHCP_STATUS == 0x00` | 超时 30s间隔 500ms循环中同步读清 GINT_ALL |
| 3 | `GET_IP_INF` 轮询至连续 3 次一致(间隔 300ms上限 8s后打印 | 连续一致且非 `0.0.0.0`(见风险 #4 |
| 4 | Socket 6UDP本地 :60000send/recv buf 各 512B`255.255.255.255:60001` 广播 4B 本机 IP 宣告 | SENBUF_FREE 等待 2s |
| 5 | 同 Socket 收 PC `"HELLO"`(5B),按包头源地址回显一次后关闭 | 窗口 30s`PHASE4_HELLO_WAIT_MS` |
---
### TC-NET-601硬件层: DHCP 完整握手
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-601 |
| **优先级** | P0 |
| **类型** | 功能测试(硬件层) |
| **标题** | DISCOVER → OFFER → REQUEST → ACK 全流程获取 IP |
| **前置条件** | 管理员 PowerShell: `python test/ch395f_socket_test.py dhcp_server --ip 192.168.1.77 --dhcp-iface 192.168.1.2``--ip` 建议 ≠ 静态 IP以证明确为动态获取 |
**测试步骤与判定标准**
| 步 | 操作 | 成功标准 | 失败标准 |
|----|------|----------|----------|
| 1 | PC 先启动 dhcp_server复位 MCUENABLE_PHASE4_TESTS | PC 打印 "等待 CH395F DISCOVER" | 60s 无 DISCOVER链路/SPI 异常) |
| 2 | CH395F 发 DISCOVER → PC 回 OFFER | PC: "收到 DISCOVER (xid=..)" + "发送 OFFER" | msg_type ≠ 1 或 len ≤ 250 |
| 3 | CH395F 发 REQUEST → PC 回 ACK | PC: "收到 REQUEST" + "发送 ACK → {offer_ip}" | 8×5s 内无 REQUEST仅重发 OFFER |
| 4 | 芯片应用配置,串口回读 IP | "DHCP completed within 30s" PASS 且 IP == {offer_ip} | 状态轮询超时或 IP = 0.0.0.0 |
**通过标准**:步 2~4 全部满足,串口 IP 与 PC 分配一致。任一失败 → 阶段标记 FAIL
**覆盖原则**DP-03DHCP 自动配置路径)
---
### TC-NET-602硬件层: DHCP 后连通性验证(宣告 + HELLO 回显)
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-602 |
| **优先级** | P1 |
| **类型** | 功能测试(硬件层) |
| **标题** | 获取 IP 后广播宣告 + 按 UDP 包头源地址回显 HELLO |
| **前置条件** | TC-NET-601 通过;同一脚本会话内自动衔接 |
**测试步骤与判定标准**
| 步 | 操作 | 成功标准 | 失败标准 |
|----|------|----------|----------|
| 1 | Socket 6 广播 4B IP 宣告 → :60001 | 串口 "DHCP announcement sent"PC 打印 "收到 IP 宣告: {offer_ip}" | OPEN 失败 / SENBUF_FREE 2s 超时 |
| 2 | PC 发 `"HELLO"` → {offer_ip}:60000最多 5 次,每次等 2s | 串口 "HELLO echo OK" | 30s 窗口内无有效 HELLO |
| 3 | 芯片按包头源地址回显 HELLO关闭 Socket 6 | PC: "连通性验证通过" | PC 5 轮 retry 全超时 |
**通过标准**:步 1~3 全部满足HELLO 至少 1 次完整往返;固件设计为单次回显后关闭)。任一失败 → 阶段标记 FAIL
**覆盖原则**DP-03动态 IP 下的端到端 UDP 通路)
---
## 6. 阶段 5大文件传输硬件层
| 阶段 ID | TM-PHY-05 |
|---------|-----------|
| **类型** | PC 配合(`tcp_bulk`:先收 256KB 不回声 → 再按 LCG 回放 256KB |
| **耗时** | ~20 秒256KB 顺序双向TX 256KB + RX 256KB视链路/SPI 吞吐) |
| **入口** | `ENABLE_PHASE5_TESTS`ch395fTestTask 启动后自动运行 |
| **出口** | 串口 "Phase 5: N/N PASSED (failed=0)" + 吞吐统计 |
| **注意** | 与 NET 层阶段 7 同主题不同层:本阶段用 `ch395f_*` 裸命令直驱芯片,不经 net_socket/netTask |
> 编号说明TC-NET-**511/512** 取 5xx"文件传输族"顺延501/502 已属 NET 层阶段 7原阶段 5 的 TC-NET-901/902 因板载拓扑不可测已整体删除(见风险表 #1
### 为什么 4KB 接收缓冲不会丢数据TCP 流控原理)
PC 一次回放 256KB而 CH395F socket 接收缓冲默认仅 4KB——不丢数据的保证**不靠缓冲够大,靠 TCP 流控(通告窗口)**。
数据不是全部驻留芯片,而是在一串缓冲区里"接力"
```
PC OS 发送缓冲(64~256KB) → 网络在途 → CH395F 接收缓冲(4KB) → MCU 读取
```
CH395F 是完整 TCP 栈 offload 芯片,内部实现 TCP 状态机,按**接收缓冲剩余空间**向 PC 通告接收窗口advertised window
1. PC 发数据,芯片收进自己的 4KB 接收环形缓冲;
2. 缓冲快满 → 通告窗口收缩直至 **窗口=0** → PC 的 `sendall` **阻塞**TCP 背压),剩余数据停在 PC 自身 OS 发送缓冲,不再发出;
3. MCU `GET_RECV_LEN``READ_RECV_BUF` 读走 → 缓冲腾空 → 芯片重新通告窗口 → PC 恢复发送。
因此是**背压联动、自节流**:数据永不因芯片缓冲溢出被丢弃,只会"堵一会儿"。MCU 平均读取速率 ≥ PC 发送速率即可持续吞吐(本测试每 2ms 轮询、每次读 4KB平均速率远高于发送速率缓冲常年接近空
"先查长度再读"`GET_RECV_LEN``READ_RECV_BUF`)安全的原因:查询返回"当前未读字节数",流控保证其必然 ≤ 缓冲容量;查询与读取之间新到的数据进入环形缓冲尾部,由芯片读写指针管理,**不会覆盖未读数据**,留待下一次迭代读取。
会真正丢数据的只有两种情况:**MCU 复位 / TCP 连接断开**(连接级丢失,属预期失败)、**软件 bug**(读错长度/越界导致错位)。正常流控下接收缓冲本身不会溢出。
---
### 机制与会话结构
**顺序双测向协议**TX 与 RX 单向隔离,独立测吞吐/完整性):
```
MCU (TCP Client, ch395f_* 裸命令) PC (tcp_bulk --port 8081)
| open/connect -> CONNECT accept
|== 步2A: 连续发送 256KB (单向, 不等回显) ===|
| 写 4KB -> 等 SENBUF_FREE(2s) -> 下一块 |
|------------------------------------------>| recv 收满 256KB (不回声, LCG 校验)
|== 步2B: 连续接收 256KB ===================|
|<------------------------------------------| sendall 按 LCG 回放 256KB
| get_recv_len -> 滚动校验 (exp[] 按块生成) |
|===========================================|
close -> PC 侧 "阶段A 收满...校验一致" + "阶段B 回放完成"
```
**要点**
- **为什么顺序双测向**:单向隔离,分别测得**纯 TX / 纯 RX** 吞吐与完整性——不被"回声耦合"掩盖各自链路的真实能力
- **TX 节奏**:写 4096B → 等 `SENBUF_FREE`(芯片把 socket 缓冲数据搬入内部协议栈/MAC 缓冲后触发)→ 写下一块,全程不等回显;单向发送阶段 PC 只收不回声,故无回读死锁问题
- **RX 校验**PC 按相同 LCG 模式回放MCU 期望流按块生成 `exp[]` 字节对位 + `sum_rx` 累加,天然处理 TCP 分包/粘包
- 单块 4096B = socket 默认发送缓冲上限,同时命中 DP-04 的 DMA 单次读边界
- 双重完整性:逐字节比对 + 全量 32 位累加和 `SUM(TX)==SUM(RX)`(双向同源 LCG相等即双向一致
- 双向独立吞吐打印TX/RX 各自 KB/s作为 SPI 时钟/驱动调整的回归基线
**固件会话结构**phase5_run
| 序 | 操作 | 参数/超时 |
|----|------|----------|
| 1 | close 清残留 → proto/des_ip/des_port/sour_port 配置 → open+connect | 两级中断等 CONNECT15s |
| 2A | 连续发送 64×4KB = 256KBPC 只收) | SENBUF_FREE 单块 2s |
| 2B | 连续接收 256KBPC 按 LCG 回放)滚动校验 | RX 停滞 5s 判 FAIL |
| 3 | close打印双向吞吐与校验结论 | blk_tx/blk_rx==64 且 SUM(TX)==SUM(RX) |
---
### TC-NET-511硬件层: 256KB 顺序双向(纯 TX + 纯 RX完整性
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-511 |
| **优先级** | P0 |
| **类型** | 功能测试(硬件层) |
| **标题** | 先连续发送 256KBPC 只收),再连续接收 256KBPC 回放),逐字节校验 + 总账一致 |
| **前置条件** | PC: `python test/ch395f_socket_test.py tcp_bulk --port 8081 --bulk 262144 --chunk 4096`(先启动等待连接) |
**测试步骤与判定标准**
| 步 | 操作 | 成功标准 | 失败标准 |
|----|------|----------|----------|
| 1 | 复位 MCU → 裸命令 connect 到 :8081 | "connected within 15000ms" | 连接超时 / cmd_status 非 0x00 |
| 2A | 连续发送 64×4KB每 64KB 打印 TX 进度 | "TX 64/64 blocks (262144 bytes)"PC "阶段A 收满 262144 字节LCG 校验一致" | SENBUF_FREE 2s 超时 / PC 端校验不一致 |
| 2B | 连续接收 256KB 并滚动校验,每 64KB 打印 RX 进度 | "RX 262144/262144 bytes (64 blocks)"PC "阶段B 回放 262144 字节完成" | RX 停滞 5s / 字节不匹配 |
| 3 | close 并输出结论 | "TX phase: 64/64 blocks" + "RX phase: ..." + "checksum TX==RX" | bad_chunks>0 / 累加和不一致 |
**通过标准**:步 1~3 全部满足。任一失败 → 阶段标记 FAIL
**覆盖原则**DP-02两级中断轮询、DP-044KB DMA 读边界)
---
### TC-NET-512硬件层: 变总量与跨包分块合规
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-512 |
| **优先级** | P1 |
| **类型** | 合规测试(硬件层) |
| **标题** | 三档总量64KB/256KB/1MB+ 非整除块边界4093B回归 |
| **前置条件** | 同 TC-NET-511每档修改 `PHASE5_TOTAL_BYTES` / `PHASE5_CHUNK_SIZE` 宏后重编译,且 PC `--bulk``chunks×PHASE5_CHUNK_SIZE``--chunk``PHASE5_CHUNK_SIZE` 一致 |
**测试步骤与判定标准**
| 步 | 操作 | 成功标准 | 失败标准 |
|----|------|----------|----------|
| 1 | `PHASE5_TOTAL_BYTES=64*1024` 重编译PC `--bulk 65536` 跑一遍 | 全绿 | 同 TC-511 |
| 2 | `PHASE5_TOTAL_BYTES=1024*1024` 重编译PC `--bulk 1048576` 跑一遍 | 全绿且吞吐无劣化趋势 | 同上 |
| 3 | `PHASE5_CHUNK_SIZE=4093`(非 4 对齐制造跨包边界PC `--chunk 4093 --bulk 261952` 跑一遍 | 全绿4093 档实际回传 `64×4093=261952B`,不足一块的尾数不上送,完成判定按实际上送字节) | 尾块校验失败 |
**通过标准**:三组配置均全绿。任一失败 → 阶段标记 FAIL
**覆盖原则**DP-04跨包/非对齐边界)
> **记录2026-08-24 实测)**:三档全部 **6/6 PASSED**checksum 逐档相等 ——
> 64KB4096B 块): TX 420 / RX 364 KB/s1MB4096B 块): TX 449 / RX 391 KB/s
> 4093B 块261952B: TX 448 / RX 368 KB/s。吞吐随总量/块大小基本恒定,
> 双方向均约 370~450 KB/s疑似 SPI 总线SPI2 DMA为瓶颈。
---
### 已知风险与待实施项
| # | 事项 | 影响 | 状态 |
|---|------|------|------|
| 1 | 原 PHY 测试TC-NET-901/902删除CH395F 直连板载 RTL8305NBI 交换机PHY 恒 link up拔插外线上联无法在芯片 PHY 产生变化事件2026-08-24 实测 0/1 FAIL 后决议) | 编号 901/902 弃用 | ✅ 已移除 |
| 2 | DP-05PHY 链路恢复)失去覆盖对象 | 设计原则表缩减 | ✅ 已同步删除 |
| 3 | 吞吐基线2026-08-24 实测TX 420~449 KB/s、RX 364~391 KB/s随总量/块大小基本恒定,疑似 SPI 总线为瓶颈 | 后续 SPI 时钟/驱动优化以该基线为参照 | ✅ 已记录 |
---
## 7. 阶段 6NET 层 TCPServer 回显 + Client 主动连接)
| 阶段 ID | TM-PHY-06 |
|---------|-----------|
| **类型** | PC 配合netTask 运行 |
| **耗时** | 持续运行 |
| **入口** | `ENABLE_PHASE6_TESTS``ch395fTestTask` TCP 回显循环中运行 |
| **出口** | 用户终止 |
两部分组成:
- **Server 模式TC-NET-401~406**MCU 监听 8080PC 主动连接,验证回显通路 —— 已实现 🟡
- **Client 模式扩展TC-NET-204/205/206**MCU 经 BSD Socket API 主动连接 PC Server补全 `net_connect()` 路径 —— 待实现 🟠ID 沿用历史编号,原属阶段 2 的 NET 层部分)
### TC-NET-401: Socket 0 单连接生命周期
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-401 |
| **优先级** | P0 |
| **类型** | 功能测试 |
| **标题** | Socket 0: OPEN → TCP_LISTEN → ESTABLISHED单连接 |
| **前置条件** | 1. `net_init("192.168.1.100")` 成功<br>2. `net_listen(sock0)` 已调用<br>3. PC: `python ch395f_socket_test.py tcp_client --ip 192.168.1.100 --port 8080` |
| **测试步骤** | 1. PC 连接 8080<br>2. Socket 0 收到 CONNECT → ESTABLISHED<br>3. 发数据 → 回显 |
| **通过标准** | 仅 s0 状态变化;串口 `s0:int=0x08` → ESTABLISHED |
| **覆盖原则** | DP-01 |
### TC-NET-402: 64 字节小包回显
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-402 |
| **优先级** | P0 |
| **类型** | 功能测试 |
| **标题** | 10 轮 64B TCP 回显 |
| **测试步骤** | PC 发 10×64BMCU 回显 |
| **通过标准** | 10/10 匹配 |
### TC-NET-403: 1460 字节 MSS 回显
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-403 |
| **优先级** | P0 |
| **类型** | 边界测试 |
| **标题** | MSS 大小1460BTCP 回显 |
| **测试步骤** | PC 发 1460BMCU 回显 |
| **通过标准** | 1460B 完全匹配,无截断 |
### TC-NET-404: 4096 字节缓冲满回显
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-404 |
| **优先级** | P0 |
| **类型** | 边界测试 |
| **标题** | 填满 Socket 0 接收缓冲4096B回显 |
| **前置条件** | DMABUF=4100 可一次性读空 |
| **测试步骤** | PC 发 4096BMCU 经 DMA 一次性读取并回显 |
| **通过标准** | 4096B 完全匹配 |
| **覆盖原则** | DP-04 |
### TC-NET-405: 断开重连
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-405 |
| **优先级** | P0 |
| **类型** | 恢复测试 |
| **标题** | PC 断开后 Socket 0 自动重新监听并接受新连接 |
| **测试步骤** | 1. PC 断连 → s0 收 DISCONNECT → CLOSED → re-OPEN → LISTEN<br>2. PC 重连 → s0 → ESTABLISHED<br>3. 发 64B → 验证回显 |
| **通过标准** | 重连成功,回显正常 |
| **覆盖原则** | DP-01 |
### TC-NET-406: 超时恢复
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-406 |
| **优先级** | P1 |
| **类型** | 恢复测试 |
| **标题** | KeepAlive 超时后 Socket 0 自动重新监听 |
| **测试步骤** | 1. PC 断连后停止响应<br>2. 等待 KeepAlive 触发 TIMEOUT<br>3. s0 → CLOSED → re-OPEN → LISTEN<br>4. PC 重连 → 验证回显 |
| **通过标准** | 自动恢复,无需手动复位 |
| **覆盖原则** | DP-01 |
---
### Client 模式扩展:主动连接 PCTC-NET-204 / 205 / 206🟠 待实现)
> 与硬件层阶段 2 的 TC-NET-201/202/203 一一对应(芯片行为 → API 封装),验证 `net_socket` / `net_connect` / `net_send` / `net_recv` / `net_close` 对 TCP Client 主路径及异常路径的正确封装。
> 用例 ID 沿用历史编号(原阶段 2 的 NET 层部分),保证测试记录可追溯;实现于 `ENABLE_PHASE6_TESTS` 固件中。
#### TC-NET-204NET 层): TCP Client 连接与数据交换
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-204 |
| **优先级** | P0 |
| **类型** | 功能测试NET 层) |
| **标题** | net_socket TCP Client → PC TCP Server64 字节收发 |
| **前置条件** | 1. PC: `python ch395f_socket_test.py tcp_server --port 8081`TCP 回显服务器)<br>2. `net_init()` 已完成(`g_net_ready == 1`netTask 周期 `net_poll()` 运行中)<br>3. 建议:硬件层固件中 TC-NET-201/202/203 已通过(跨构建前置,见「构建模式与执行顺序」) |
**测试步骤与判定标准**
| 步 | 操作 | 成功标准 | 失败标准 |
|----|------|----------|----------|
| 1 | `net_socket(NET_AF_INET, NET_SOCK_STREAM, 0)` 创建 TCP socket | 返回 sockfd`0~7`),非 `-1` | 返回 `-1`(无空闲 Socket 或 net 层未初始化) |
| 2 | 构造 `struct net_sockaddr_in`sin_family=`NET_AF_INET`sin_port=`net_htons(8081)`sin_addr=`net_inet_addr("192.168.1.2")`)后 `net_connect(sockfd, &addr, len)` | 返回 `0`(连接命令已异步下发) | 返回 `-1`(地址非法或协议栈异常) |
| 3 | 在 netTask 周期 `net_poll()` 推进下,调用 `net_select(sockfd+1, NULL, &wfds, NULL, &tv)` 等待可写事件,超时 10s | 返回正数且 `NET_FD_ISSET(sockfd)` 指示连接建立ESTABLISHED | 超时未连接,`net_errno == NET_ERR_WOULDBLOCK` 持续 |
| 4 | `net_send(sockfd, buf, 64, 0)` 发送 64 字节递增模式数据(`0x00..0x3F` | 返回值 == `64`(全部发出) | 返回值 `< 0`(发送失败)或 `≠ 64`(未完全发出) |
| 5 | `net_recv(sockfd, buf, 64, 0)` 阻塞接收回显,超时 10s | 返回值 == `64` 且与原数据逐字节一致 | 返回值 `< 0`(超时/断开)或 `≠ 64`(数据不完整)或任意字节不符 |
| 6 | `net_close(sockfd)` 关闭 socketTCP 先断开连接,释放控制块) | 返回 `0` | 返回 `-1`(关闭失败) |
**通过标准**6/6 通过0 失败。任一失败 → 标记 FAIL
**覆盖原则**DP-01net_socket→net_connect→net_send→net_recv→net_close 主路径NET 层)
#### TC-NET-205NET 层): TCP Client 关闭重连 ×3 轮
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-205 |
| **优先级** | P0 |
| **类型** | 功能测试 / 恢复测试NET 层) |
| **标题** | close→重连循环 3 轮,每轮含数据交换 |
| **前置条件** | 同 TC-NET-204 |
**测试步骤与判定标准**(每轮 i = 0,1,2
| 步 | 操作 | 成功标准 | 失败标准 |
|----|------|----------|----------|
| 1 | `net_socket(NET_AF_INET, NET_SOCK_STREAM, 0)` 创建 socket | 返回 sockfd 非 `-1` | 返回 `-1`(无空闲 Socket |
| 2 | 构造地址 + `net_connect(sockfd, &addr, len)`,再 `net_select` 等待可写确认 ESTABLISHED | 连接建立成功 | 连接失败或超时未建立 |
| 3 | `net_send(sockfd, buf, 64, 0)``net_recv(sockfd, buf, 64, 0)` | 收发各 `64` 字节且逐字节一致 | 收发失败或数据不符 |
| 4 | `net_close(sockfd)` 关闭并释放控制块 | 返回 `0` | 返回 `-1` |
**通过标准**3/3 轮全部通过0 失败。任一失败 → 标记 FAIL
**覆盖原则**DP-01net 层 open→connect→close→re-open→reconnect
#### TC-NET-206NET 层): TCP Client 连接超时
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-206 |
| **优先级** | P1 |
| **类型** | 负向测试NET 层) |
| **标题** | 目标端口无服务时 net_connect 超时 |
| **前置条件** | 1. PC **未**运行 TCP Server`192.168.1.2:8081` 无监听)<br>2. TC-NET-204 通过 |
**测试步骤与判定标准**
| 步 | 操作 | 成功标准 | 失败标准 |
|----|------|----------|----------|
| 1 | `net_socket(...)` + `net_connect(sockfd, &addr, len)`(目标不可达) | `net_connect` 返回 `0`(异步下发) | 返回 `-1`(地址非法) |
| 2 | `net_select(sockfd+1, NULL, &wfds, NULL, &tv)` 等待可写,超时窗 30s | 返回 `0`(超时,连接未建立),`net_errno` 指示超时/不可达 | 误报告可写(异常响应)或 30s 内无结果(挂死) |
| 3 | `net_close(sockfd)` 释放 socket | 返回 `0` | 返回 `-1` |
**通过标准**`net_select` 超时且资源正确释放0 崩溃。任一异常 → 标记 FAIL
**覆盖原则**DP-01NET 层异常路径 connect 失败处理)
---
## 8. 阶段 7大文件传输20KB+
| 阶段 ID | TM-PHY-07 |
|---------|-----------|
| **类型** | PC 配合netTask 运行 |
| **耗时** | 取决于文件大小和速率 |
| **入口** | `ENABLE_PHASE7_TESTS` |
| **配置** | `PHASE7_FILE_SIZE`(默认 20480|
### TC-NET-501: 20KB 分块传输
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-501 |
| **优先级** | P0 |
| **类型** | 功能测试 |
| **标题** | 20480 字节 TCP 分块收发(>Socket 0 硬件缓冲 4KB |
| **前置条件** | `net_send_locked` 支持自动分块(每块 ≤4096B |
| **测试步骤** | 1. MCU 填 20KB 递增模式数据<br>2. MCU 分块发送,每块 4096B<br>3. PC 回显<br>4. MCU 接收并逐字节验证 |
| **通过标准** | 20KB 完全匹配,每字节验证通过 |
| **覆盖原则** | DP-04 |
### TC-NET-502: 64KB 传输recv_len 上限)
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-502 |
| **优先级** | P1 |
| **类型** | 边界测试 |
| **标题** | 65535 字节传输(`recv_len` uint16 上限) |
| **前置条件** | TC-NET-501 通过 |
| **测试步骤** | 同 501文件大小 65535 |
| **通过标准** | 65535B 完全匹配 |
| **覆盖原则** | DP-04 |
### 实现说明
`phase7_run()` 状态机(`ch395fTestTask` 内):
```
IDLE → (连接建立) → SENDING(分块发) → RECVING(分块收+验证) → DONE → IDLE
```
- 每次 `net_send_sock` 自动分块(`net_send_locked` 内按 `NET_DMA_MAX_PAYLOAD=4096` 拆分)
- 每次 `net_recv_sock` 通过 `ch395f_get_recv_len` 获取硬件缓冲可用数据量≤4096B一次性读空
- 验证模式递增字节offset & 0xFF接收端逐字节比较
---
## 9. 阶段 8Select/Poll I/O 多路复用(单连接)
| 阶段 ID | TM-PHY-08 |
|---------|-----------|
| **类型** | PC 配合netTask 运行 |
| **耗时** | 持续运行 |
| **入口** | `ENABLE_PHASE8_TESTS` |
### TC-NET-701: net_select() 单 socket 就绪检测
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-701 |
| **优先级** | P0 |
| **类型** | 功能测试 |
| **标题** | `net_select()` 在 Socket 0 数据到达时正确返回 |
| **前置条件** | Socket 0 ESTABLISHED |
| **测试步骤** | 1. PC 发送数据<br>2. `net_select(1, &readfds, ...)` → 返回 > 0<br>3. `NET_FD_ISSET(0, &readfds)` == true<br>4. `net_recv()``net_send()` 回显 |
| **通过标准** | 回显成功 |
| **覆盖原则** | DP-02select 内部调 net_poll |
### TC-NET-702: net_select() 超时
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-702 |
| **优先级** | P1 |
| **类型** | 负向测试 |
| **标题** | 100ms 超时内无数据则返回 0 |
| **前置条件** | Socket 0 ESTABLISHEDPC 静置 |
| **测试步骤** | `net_select(1, &readfds, ..., 100ms)` |
| **通过标准** | 返回值 == 0无崩溃 |
---
## 10. 阶段 9边界条件测试
| 阶段 ID | TM-PHY-09 |
|---------|-----------|
| **类型** | PC 配合netTask 运行 |
| **耗时** | 按需 |
| **入口** | `ENABLE_PHASE9_TESTS` |
### TC-NET-801: DMA 边界 14095 字节)
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-801 |
| **优先级** | P0 |
| **类型** | 边界测试 |
| **标题** | 发送 4095 字节SPI 最大载荷 1单次 DMA 可容纳) |
| **前置条件** | Socket 0 ESTABLISHED |
| **测试步骤** | MCU 生成 4095B 递增模式数据 → `net_send_sock(sk, data, 4095)` → 等待 PC 回显 → 验证 |
| **通过标准** | 4095B 完全匹配 |
| **覆盖原则** | DP-04 |
### TC-NET-802: DMA 边界4096 字节)
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-802 |
| **优先级** | P0 |
| **类型** | 边界测试 |
| **标题** | 发送 4096 字节(等于 SPI 最大载荷,填满单次 DMA |
| **测试步骤** | MCU 发 4096B → 等待回显 → 验证 |
| **通过标准** | 4096B 完全匹配 |
| **覆盖原则** | DP-04 |
### TC-NET-803: DMA 边界 +14097 字节)
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-803 |
| **优先级** | P0 |
| **类型** | 边界测试 |
| **标题** | 发送 4097 字节(超过 SPI 最大载荷,自动拆两次 DMA4096+1 |
| **测试步骤** | MCU 发 4097B → `net_send_locked` 自动拆 4096+1 → 等待回显 → 验证 |
| **通过标准** | 4097B 完全匹配,两次 DMA 无缝衔接 |
| **覆盖原则** | DP-04 |
### TC-NET-804: 零长度收发
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-804 |
| **优先级** | P1 |
| **类型** | 负向测试 |
| **标题** | `net_send_sock(sk, buf, 0)` 返回 1 |
| **测试步骤** | `net_send_sock(sk, s_phase9_tx, 0)` |
| **通过标准** | 返回 < 0无崩溃 |
### TC-NET-805: 连续快速发送 20 小包
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-805 |
| **优先级** | P1 |
| **类型** | 压力测试 |
| **标题** | 20 × 100 字节连续快速发送(验证 SENDBUF_FREE 等待机制) |
| **测试步骤** | 循环 20 次:`net_send_sock(sk, buf, 100)`,间隔 1ms |
| **通过标准** | 20/20 成功,回显 2000B 完全匹配 |
### TC-NET-806: 快速连接-断开 ×20 轮
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-806 |
| **优先级** | P1 |
| **类型** | 稳定性测试 |
| **标题** | 20 次快速连接-断开循环 |
| **测试步骤** | PC 连 8080 → 立即断开 → 等 s0 恢复 LISTEN → 重复 20 次 |
| **通过标准** | 20/20 不卡死s0 始终回到 LISTEN |
---
## 11. 阶段 10KeepAlive 超时断开
| 阶段 ID | TM-PHY-10 |
|---------|-----------|
| **类型** | PC 配合netTask 运行 |
| **耗时** | ~65 秒60s idle + 15s probes |
| **入口** | `ENABLE_PHASE10_TESTS` |
| **配置** | KeepAlive 空闲 60s探测间隔 5s3 次 |
### TC-NET-1001: KeepAlive 触发断开
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-1001 |
| **优先级** | P1 |
| **类型** | 功能测试 |
| **标题** | TCP 连接空闲超过 KeepAlive 阈值后自动断开 |
| **前置条件** | 1. `ch395f_set_keepalive_idle(60000)`<br>2. `ch395f_set_keepalive_intvl(5000)`<br>3. `ch395f_set_keepalive_cnt(3)`<br>4. PC 连接后静置(不发数据) |
| **测试步骤** | 1. PC 连接 8080不发数据<br>2. 等待 KeepAlive 探测60s idle + 3×5s = 75s 最大)<br>3. CH395F 接收 TIMEOUT → s0 → CLOSED → LISTEN |
| **通过标准** | 超时时间 ≥ 60ss0 回到 LISTEN |
---
## 12. Python 测试脚本参考
| 命令 | 模式 | 说明 |
|------|------|------|
| `tcp_server --port 8081` | PC 作 TCP 回显服务器(多连接循环) | 测试 TCP Client阶段 2 硬件层;阶段 6 Client 扩展 204/205/206 |
| `tcp_client --ip ... --port 8080` | PC 作 TCP Client | 测试 TCP Echo阶段 6/8/9 |
| `tcp_file --ip ... --port 8080 --size 20480` | PC 作文件接收 | 测试大文件传输(阶段 7 |
| `udp_client --ip ... --port 60000` | PC 作 UDP Client | 测试 UDP Echo阶段 3 |
| `udp_server --port 8082` | PC 作 UDP Server | 接收 CH395F UDP 数据 |
| `stress --ip ... --port 8080 --count 10` | 压力测试 | 7 并发 × 10 轮 |
| `dhcp_server ...` | DHCP Server | 内置 DHCP阶段 4需管理员权限 |
### 通用参数
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `--ip` | 192.168.1.100 | 目标 IP |
| `--port` | 因模式而异 | 端口 |
| `--count` | 10 | 测试次数 |
| `--timeout` | 30 | 超时秒数 |
| `--size` | 1024 | 包大小 |
---
## 13. 已知陷阱
| ID | 陷阱 | 现象 | 修复 |
|----|------|------|------|
| TRAP-01 | ~~多连接初始化顺序~~ | 已废弃(单连接模式) | — |
| TRAP-02 | ~~数据 socket 缓冲重叠~~ | 已废弃(单连接模式) | — |
| TRAP-03 | DHCP xid 偏移错误 | DHCP 握手失败 | xid 在 `data[4:8]` |
| TRAP-04 | UDP 无 SENDBUF_FREE 等待 | 后续接收挂起 | 每次 write 后等 SENDBUF_FREE |
| TRAP-05 | PHY 不兼容 RTL8305NBI | 链路抖动 | `ch395f_set_phy(0x08)` 强制 100M_FULL |
| TRAP-06 | KeepAlive 非 500ms 倍数 | 参数不生效 | 必须 500ms 倍数 |
| TRAP-07 | TCP 误用 disconnect | 卡 FIN_WAIT_2 | 用 `close_socket`(RST) 非 `disconnect` |
| TRAP-08 | RECV 电平中断无限循环 | netTask 饿死 | `net_poll` 每次只一批,数据在应用中读 |
| TRAP-09 | PHY 刚建立 link 立即 open | 首次连接失败 | 启动延时 + 重试 |
| TRAP-10 | 消息队列值拷贝丢失 | API 返回旧值 | 改用 `xTaskNotify` |
| TRAP-11 | `net_accept_locked` 拒 ESTABLISHED | FTP PASV 失败 | 允许 ESTABLISHED |
| TRAP-12 | FatFS 不接受 `/` 前缀 | FTP 路径解析失败 | `to_fatfs_path()` 剥离 |
| **TRAP-13** | **`recv_len` 分批读取归零** | **大包截断** | **DMA 缓冲增至 4100一次读空 ✅ 已修复** |
| **TRAP-14** | **`NET_SEND_BUF_SIZE` > DMA 限制** | **>1496B 发送静默失败** | **`net_send_locked``NET_DMA_MAX_PAYLOAD` 分块 ✅ 已修复** |
---
## 附录 A通过/失败汇总
| 阶段 | TC ID | 优先级 | 类型 | 状态 |
|------|-------|--------|------|------|
| TM-PHY-01 | TC-NET-101 | P0 | 功能测试 | ✅ |
| TM-PHY-01 | TC-NET-102 | P1 | 功能测试 | ✅ |
| TM-PHY-02 | TC-NET-201 | P0 | 功能测试 | ✅ |
| TM-PHY-02 | TC-NET-202 | P0 | 恢复测试 | ✅ |
| TM-PHY-02 | TC-NET-203 | P1 | 负向测试 | ✅ |
| TM-PHY-03 | TC-NET-301 | P0 | 功能测试 | ✅ |
| TM-PHY-03 | TC-NET-302 | P1 | 合规测试 | ✅ |
| TM-PHY-04 | TC-NET-601 | P0 | 功能测试 | ✅ |
| TM-PHY-04 | TC-NET-602 | P1 | 功能测试 | ✅ |
| TM-PHY-05 | TC-NET-511 | P0 | 功能测试 | 🟡 |
| TM-PHY-05 | TC-NET-512 | P1 | 合规测试 | ✅ |
| TM-PHY-06 | TC-NET-204 | P0 | 功能测试 | 🟠 |
| TM-PHY-06 | TC-NET-205 | P0 | 恢复测试 | 🟠 |
| TM-PHY-06 | TC-NET-206 | P1 | 负向测试 | 🟠 |
| TM-PHY-06 | TC-NET-401 | P0 | 功能测试 | 🟡 |
| TM-PHY-06 | TC-NET-402 | P0 | 功能测试 | 🟡 |
| TM-PHY-06 | TC-NET-403 | P0 | 边界测试 | 🟡 |
| TM-PHY-06 | TC-NET-404 | P0 | 边界测试 | 🟡 |
| TM-PHY-06 | TC-NET-405 | P0 | 恢复测试 | 🟡 |
| TM-PHY-06 | TC-NET-406 | P1 | 恢复测试 | 🟡 |
| TM-PHY-07 | TC-NET-501 | P0 | 功能测试 | 🟡 |
| TM-PHY-07 | TC-NET-502 | P1 | 边界测试 | 🟡 |
| TM-PHY-08 | TC-NET-701 | P0 | 功能测试 | 🟡 |
| TM-PHY-08 | TC-NET-702 | P1 | 负向测试 | 🟡 |
| TM-PHY-09 | TC-NET-801 | P0 | 边界测试 | 🟡 |
| TM-PHY-09 | TC-NET-802 | P0 | 边界测试 | 🟡 |
| TM-PHY-09 | TC-NET-803 | P0 | 边界测试 | 🟡 |
| TM-PHY-09 | TC-NET-804 | P1 | 负向测试 | 🟡 |
| TM-PHY-09 | TC-NET-805 | P1 | 压力测试 | 🟡 |
| TM-PHY-09 | TC-NET-806 | P1 | 稳定性测试 | 🟡 |
| TM-PHY-10 | TC-NET-1001 | P1 | 功能测试 | 🟡 |
✅ = 已实现并验证 | 🟡 = 已实现待验证 | 🟠 = 待实现
## 附录 B设计原则覆盖
| 原则 | 覆盖用例 |
|------|----------|
| DP-01单 socket 生命周期 | TC-NET-202, 203, 401, 405, 406, 806, 902 |
| DP-02中断即读即清 | TC-NET-101, 102, 511, 512, 701 |
| DP-03消息队列线程安全 | TC-NET-301, 302, 601 |
| DP-04DMA 边界安全 | TC-NET-403, 404, 501, 502, 511, 512, 801, 802, 803, 804, 805 |
## 附录 CCH395F 边界条件速查
| 操作 | 大小 | 行为 |
|------|------|------|
| `net_send(buf, ≤1496)` | 1~1496B | 单次 DMA 事务(旧 DMABUF=1500 时完全容纳) |
| `net_send(buf, 1497~4096)` | 1497~4096B | 旧代码**静默失败**;现 DMABUF=4100 **一次 DMA 容纳** |
| `net_send(buf, 4097~65535)` | >4096B | `net_send_locked` **自动分块**,每块 4096B |
| `net_send(buf, 0)` | 0 | 返回 `-1``NET_ERR_INVAL` |
| `ch395f_get_recv_len()` | 0~65535 | 返回 CH395F 硬件缓冲当前可用数据量 |
| `ch395f_read_recv_buf(buf, 4096)` | 4096B | 一次性读空 Socket 0 整个接收缓冲 |
| `ch395f_read_recv_buf(buf, 100)` | 100B | ⚠️ `recv_len` 之后**归零**,剩余数据**丢失** |