Files
STM32F4-Base/docs/测试说明/板级测试/CH395F_Test_Guide.md
T
wandering1 4a4ad6bc5f chore(全仓): @build/@test 旧文档别名清退为技能调用名
现状口径:/keil-build 与 /serial-boot-test 是 DSH 会话的技能调用名,
不是 shell 命令;「名 + 命令行参数」的命令形态改为可执行体
run_boot_test.py 的真实调用。

- AGENTS.md:别名说明重写为「技能调用名不是 shell 命令」,两条目改
  「编译(/keil-build)」「串口回归(/serial-boot-test)」
- keil-build / serial-boot-test 两 skill 调用名与示例同步;task-worktree
  方案文档去掉 @build/@test 惯例从句;@task 保留(不在点名范围)
- 123 文件现状陈述全改;台架捕获、变更记录/时间线、rename 映射输入
  等 41 行历史证据保留旧名
- gen_memory_svg.py 6 处措辞随改,3 张 SVG 产物同批重跑
- 顺手补并行方案同行漏网计数 51→52 钩子

验证:提交期快钩子对 123 文件实跑全 Passed;check_doc_links --all
rc=0(3038 处、无法判定 0);check_mermaid --all rc=0;YAML 解析 52
钩子不变;skill-size rc=0;py_compile 全过;字节卫生零漂移。
2026-10-30 13:03:12 +08:00

1233 lines
74 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 以太网驱动(STM32F407ZGTx,SPI2 接口)的测试规范。当前固件 `FUN_PARA=0x08`(bit3 SOCKET_CLOSE=1,bit0/bit1 均为 0):8 个 Socket 0~7 全部可用(**由 Socket 索引 `_SN` 命令族 + 缓冲显式分配实现,与 FUN_PARA 无关**,见 `问题记录/CH395F_Trap_Records.md` TRAP-NET-15),Socket 0 作 TCP Server 单连接,覆盖单连接与多并发客户端场景。测试按阶段组织,每个阶段包含独立测试用例(TC-NET-NNN)。
### 设计原则
| 原则 | 描述 | 验证用例 |
|------|------|----------|
| 单连接 socket | Socket 0 唯一,复用监听和数据通道;CONNECT 后切换数据通道;DISCONNECT/TIMEOUT 后重新 OPEN → LISTEN | TC-NET-201~203, 601~606, 701~703, 801, 802, 901, 902, 1001~1006 |
| 中断集中处理 | 所有 CH395F 中断在 `net_poll()` 中通过 `GET_GLOB_INT_STATUS` 统一读取并清除,ISR 及其他位置不处理 | TC-NET-101 |
| 线程安全 | 所有 CH395F 操作通过 `netMsgQueue` 在 netTask 串行处理 | TC-NET-301~302 |
| DMA 边界安全 | `CH395F_SPI_DMA_BUF_SIZE=4100`,单次事务最大载荷 `4096B`,超限自动分块 | TC-NET-1001~1005 |
### CH395F 缓冲架构(关键参数)
| 参数 | 值 | 说明 |
|------|-----|------|
| CH395F 内部总缓冲 | **24KB(48 块 × 512B)** | `docs/芯片手册/CH395F.md` §5.45;CH395F.md "内置 24K RAM"。**经 Phase 7 实测确认块 0~47 全部有效**(此前 32×1KB 记录有误) |
| 上电默认分配 | Socket 0~3 各 接收8块(4KB)+发送4块(2KB);Socket 4~7 未分配 | 见 `docs/芯片手册/CH395F.md` §5.45,用到 >4 个 Socket 必须重分配 |
| **本固件分配(net_init)** | 8 个 Socket 各 **接收 4 块(2KB) + 发送 2 块(1KB)** | 块数由 `CH395F_TCP_MSS` 推导(接收 `CH395F_RECV_BLOCKS`=2×MSS、发送 `CH395F_SEND_BLOCKS`=1×MSS,每块 512B);起始块 `recv=i*6`、`send=i*6+4`;用尽 0~47,互不重叠 |
| **TCP MSS** | **1024** | 由接收缓冲(2KB) ≥ 2×MSS 决定,8 Socket 共享 24KB 时的最大值 |
| **SPI DMA 缓冲** | **4100B** | `CH395F_SPI_DMA_BUF_SIZE` |
| **单次 SPI 最大载荷** | **4096B** | `CH395F_SPI_DMA_MAX_PAYLOAD` |
| recv_len 最大值 | 65535 | `uint16_t` |
> **关键约束**:本固件每个 Socket 接收缓冲为 **2KB**(4 块 × 512B),发送缓冲为 **1KB**(2 块)。旧"部分读后 `recv_len` 归零"机理**未证实且与 `docs/芯片手册/CH395F.md` §5.34 相左**(2026-09-23 复核降级),但**保守按"一次读空"执行**(≤2KB)。`CH395F_SPI_DMA_BUF_SIZE=2052` 恰好覆盖单 Socket 完整 2KB 接收缓冲。
### 多 Socket / 8 端口并发分配
`net_init` 中 `ch395f_set_fun_para(0x08)` 之后,循环 0~7 调用 `SET_RECV_BUF` / `SET_SEND_BUF` 重分配(**这一步才是 Socket 1~7 可用的前提**,见 `问题记录/CH395F_Trap_Records.md` TRAP-NET-15;必须在 `OPEN_SOCKET` 之前,顺序遵循 TRAP-NET-01:先数据 Socket 1~7 再监听 Socket 0):
| Socket | 接收起始块 | 接收块数 | 接收大小 | 发送起始块 | 发送块数 | 发送大小 |
|--------|-----------|---------|---------|-----------|---------|---------|
| 0 | 0 | 4 | 2KB | 4 | 2 | 1KB |
| 1 | 6 | 4 | 2KB | 10 | 2 | 1KB |
| 2 | 12 | 4 | 2KB | 16 | 2 | 1KB |
| 3 | 18 | 4 | 2KB | 22 | 2 | 1KB |
| 4 | 24 | 4 | 2KB | 28 | 2 | 1KB |
| 5 | 30 | 4 | 2KB | 34 | 2 | 1KB |
| 6 | 36 | 4 | 2KB | 40 | 2 | 1KB |
| 7 | 42 | 4 | 2KB | 46 | 2 | 1KB |
UDP 路径 `net_socket()` 中对 Socket 4~7 的兜底重设使用同一布局。该分配已通过 **Phase 7 TC704(5 Socket 并发回显)** 验证。
### 大文件传输(20KB+)策略
多 Socket 模式下,每个 Socket 拥有独立收发缓冲(接收 2KB / 发送 1KB,见上表)。超过单 Socket 接收缓冲(2KB)的数据通过分块传输:
```
发送端:net_send(buf, 20480)
→ net_send_locked 分块写入 CH395F 发送缓冲(每 Socket 发送缓冲 1KB)
→ 块 1: ch395f_write_send_buf(sock, buf[0..1023], 1024) // DMA 事务 1
→ 等待 SENBUF_FREE
→ 块 2: ch395f_write_send_buf(sock, buf[1024..2047], 1024) // DMA 事务 2
→ ...
→ 共 20 次 DMA 事务完成 20KB(MSS=1024,TCP 每段 ≤1024B)
接收端:net_recv(buf, 20480)
→ net_recv_locked 循环拉取
→ ch395f_get_recv_len() 返回硬件缓冲中当前可用数据(≤2KB)
→ ch395f_read_recv_buf() 一次性读空硬件缓冲(≤2KB/DMA 事务)
→ 循环直至全部 20480B 收完
```
**边界条件**:
- 发送 `≤1KB`:一次写发送缓冲完成(受发送缓冲 1KB 限制)
- 接收缓冲满(2KB):保守按一次性读空执行(旧"归零"机理未证实,见 `CH395F_Trap_Records.md` 台架纪律)
- 发送 `0B`:`net_send` 返回 `-1`(`NET_ERR_INVAL`)
> **注**:当前 `net_send` 的分块上限常量 `NET_DMA_MAX_PAYLOAD=4096` 大于单 Socket 发送缓冲(1KB),单次 `WRITE_SEND_BUF` 写入量受发送缓冲大小约束;大文件传输(Phase 6/8/10)需在硬件上实测确认无溢出。
### 测试环境
```
PC (192.168.1.2) ──── 交换机 ──── CH395F (192.168.1.100)
MCU: STM32F407ZGTx @ 168MHz, Keil MDK-ARM v5 (ARMCC)
```
### 构建模式与执行顺序
测试固件通过条件编译分为**两种互斥形态**(`test/board/ch395f_test.h` 中同时启用两层会触发 `#error`):
| 构建模式 | 启用宏 | 固件形态 |
|----------|--------|----------|
| **硬件层模式** | `ENABLE_PHASE1~5_TESTS`(另含 12/15) | netTask 自挂起(基础线程创建但不运行),CH395F 协议栈初始化由 testTask 完成后独占芯片执行阶段 1~5(裸调 `ch395f_*` 命令) |
| **NET 层模式** | `ENABLE_PHASE6~11_TESTS` | 完整网络栈:netTask 运行 `net_poll()` 轮询,testTask 经 `net_socket` API 执行阶段 6~11(`net_layer_test_main()` 回显、`net_layer_client_test_main()` Client 等) |
**执行顺序严格递增**:先烧录硬件层固件完成阶段 1→2→3→4→5(全部通过),再切换为 NET 层固件执行阶段 6→7→…→11。硬件层测试期间 netTask 轮询不存在,从根上排除中断偷取与 SPI 并发问题。
### 启用测试
编辑 `test/board/ch395f_test.h`,取消注释对应 `ENABLE_PHASEx_TESTS` 宏(同一模式内 Phase 6/7/8/9/10/11 可共存于 testTask 循环)。**注意:阶段 6 Server 回显为持续监听循环,故 Client 阶段 11 需单独构建(不与 `ENABLE_PHASE6_TESTS` 同时启用),否则 Client 用例不会被执行。**
---
## 2. 阶段 1:寄存器与 SPI 命令验证
| 阶段 | 阶段 1 |
|---------|-----------|
| **类型** | 独立运行 |
| **耗时** | ~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,后续阶段不执行
**覆盖原则**:中断集中处理
---
## 3. 阶段 2:TCP Client(CH395F 主动发起连接)
| 阶段 | 阶段 2 |
|---------|-----------|
| **类型** | 需 PC 配合 |
| **耗时** | ~60 秒 |
| **入口** | `ch395fTestTask` 启动后自动运行 |
| **出口** | 串口 "Phase 2 Tests Complete" 且 0 失败 |
**分层说明**:本阶段属于**硬件层模式**(netTask 自挂起,CH395F 由 testTask 独占),仅使用 `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 Server,64 字节收发 |
| **前置条件** | PC: `python test/host/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 回显,超时 10s(PC 端 tcp_server 收到数据后原样回显) | 收到 64 字节且与原数据逐字节一致 | 回显超时或任意字节不符 |
| 7 | `ch395f_close_socket(0)` 并轮询 `GET_SOCKET_STATUS` 等待 `CH395F_SOCKET_CLOSED` | Socket 回到 CLOSED(`0x00`)状态 | 5s 内未回到 CLOSED(状态机卡死) |
**通过标准**:7/7 通过,0 失败。任一失败 → 阶段标记 FAIL,后续阶段不执行
**覆盖原则**:单连接 socket(open→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 回显,超时 10s(PC 端 tcp_server 收到数据后原样回显) | 收到 64 字节且与原数据逐字节一致 | 回显超时或任意字节不符 |
| 6 | `ch395f_close_socket(0)` 并轮询 `GET_SOCKET_STATUS` 等待 `CH395F_SOCKET_CLOSED` | Socket 回到 CLOSED(`0x00`)状态 | 5s 内未回到 CLOSED(状态机卡死) |
**通过标准**:3/3 轮全部 6 步通过,0 失败。任一失败 → 阶段标记 FAIL,后续不执行
**覆盖原则**:单连接 socket(open→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
**覆盖原则**:单连接 socket(异常路径的 connect 失败处理,硬件层)
---
### TC-NET-204(硬件层): TCP 关闭次序 A/B/C 对照(TRAP-NET-07 定案用例)
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-204 |
| **优先级** | P1 |
| **类型** | 机理对照(硬件层,2026-10-11 新增) |
| **标题** | `close` / `disconnect→close` / `disconnect` 三种关闭次序的芯片行为 |
| **前置条件** | 1. PC: `python test/host/ch395f_socket_test.py tcp_server --port 8081 --timeout 900`(⚠ 默认 `--timeout 180` 且连接间隔 >60 s 会自行退出,长轮次须放大)<br>2. TC-NET-201 通过;`FUN_PARA=0x08`(`net_stack_bring_up` 与生产同构) |
| **规模** | 3 模式 × 5 轮,每轮 3 项判定(会话 / 关闭次序 / 重开会话),共 45 项 |
**测试步骤与判定标准**(每轮:`open`→`connect`→64 B 回显比对 → 按模式执行关闭次序 → 重开重连回显):
| 模式 | 关闭序列 | 成功标准 | 实测(2026-10-11,5/5) |
|----|------|----------|----------|
| A | `ch395f_close_socket()` | 观察窗 10 s 内 `GET_SOCKET_STATUS` **byte0** 回到 `0x00` | **0 / 0 / 150 / 0 / 0 ms**;过渡态可见 `TIME_WAIT`(`0x0A`),**无 RST 语义** |
| B | `ch395f_tcp_disconnect()` → `ch395f_close_socket()` | 同上 | **全部 +0 ms**,两字节均 `0x00` → TRAP-NET-07 的"卡 `FIN_WAIT_2` 数分钟"**不成立** |
| C | `ch395f_tcp_disconnect()`(**不** close) | **预期相反**:10 s 内 byte0 保持 `0x05`(`docs/芯片手册/CH395F.md` §5.52 ② bit3=1 芯片不自动关) | byte0 恒 `0x05`、byte1 `0x00`;直接 `open_socket` → **`0x20` FAIL**(5/5,即历史 `Phase 2 open_socket: 0x20` 签名);补 `close` 后 +0 ms 回 CLOSED |
**判据口径**:`GET_SOCKET_STATUS_SN` 输出 2 字节——byte0 只有 `00H/05H`,byte1 才是 TCP 态且**仅在 byte0=OPEN 时有意义**(`docs/芯片手册/CH395F.md` §5.22 + 官方 SDK 注释);关闭与重开之间**不主动清中断**,残留事件由重开后首次中断读取现场暴露(本轮 15 轮均未观察到残留 `DISCONNECT`)。
**通过标准**:45/45。任一模式偏离上表 → 说明芯片关闭语义或 `FUN_PARA` 配置变了,须回查 `问题记录/CH395F_Trap_Records.md` TRAP-NET-07。
**覆盖原则**:线程安全(状态机边界:关闭次序 × Socket 复用)
---
## 4. 阶段 3:UDP Server Echo(CH395F 作为 UDP Server)
| 阶段 | 阶段 3 |
|---------|-----------|
| **类型** | 需 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-NET-04)。
---
### TC-NET-301(硬件层): UDP 基础回显
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-301 |
| **优先级** | P0 |
| **类型** | 功能测试(硬件层) |
| **标题** | 30 轮 64B UDP 数据报回显 |
| **前置条件** | PC: `python test/host/ch395f_socket_test.py udp_client --ip 192.168.1.100 --port 60000` |
> **会话结构**(固件单会话,与 udp_client 脚本约定):累计 **60 个有效数据包**后关闭 Socket——第 1~30 轮对应本用例(PC 发 64B),第 31~60 轮对应 TC-NET-302(变长);脚本 `"HELLO"` 就绪探针只回显、**不计数**,不占用用例预算。
**测试步骤与判定标准**:
| 步 | 操作 | 成功标准 | 失败标准 |
|----|------|----------|----------|
| 1 | 配置 Socket 0:proto=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
**覆盖原则**:线程安全(UDP 收发路径,包头源地址解析)
---
### 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
**覆盖原则**:线程安全(变长载荷边界)
---
## 5. 阶段 4:DHCP 动态获取 IP
| 阶段 | 阶段 4 |
|---------|-----------|
| **类型** | PC 配合(脚本绑定 UDP/67,Windows 需**管理员权限**) |
| **耗时** | ~10s(上限 30s DISCOVER 轮询 + 30s HELLO 等待) |
| **入口** | `ENABLE_PHASE4_TESTS`,ch395fTestTask 启动后自动运行 |
| **出口** | 串口 "Phase 4: N/N PASSED (failed=0)" |
| **注意** | **改变 CH395F IP 配置**,必须独立运行(勿与其他阶段连跑) |
### 机制与会话结构
**DHCP 报文流**(PC = DHCP Server,CH395F = 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-NET-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`(滞后窗口实测 ~1 s,见 TRAP-NET-13) |
| 4 | Socket 6(UDP,本地 :60000,send/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-401(硬件层): DHCP 完整握手
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-401 |
| **优先级** | P0 |
| **类型** | 功能测试(硬件层) |
| **标题** | DISCOVER → OFFER → REQUEST → ACK 全流程获取 IP |
| **前置条件** | 管理员 PowerShell: `python test/host/ch395f_socket_test.py dhcp_server --ip 192.168.1.77 --dhcp-iface 192.168.1.2`(`--ip` 建议 ≠ 静态 IP,以证明确为动态获取) |
**测试步骤与判定标准**:
| 步 | 操作 | 成功标准 | 失败标准 |
|----|------|----------|----------|
| 1 | PC 先启动 dhcp_server;复位 MCU(ENABLE_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
**覆盖原则**:线程安全(DHCP 自动配置路径)
---
### TC-NET-402(硬件层): DHCP 后连通性验证(宣告 + HELLO 回显)
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-402 |
| **优先级** | P1 |
| **类型** | 功能测试(硬件层) |
| **标题** | 获取 IP 后广播宣告 + 按 UDP 包头源地址回显 HELLO |
| **前置条件** | TC-NET-401 通过;同一脚本会话内自动衔接 |
**测试步骤与判定标准**:
| 步 | 操作 | 成功标准 | 失败标准 |
|----|------|----------|----------|
| 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
**覆盖原则**:线程安全(动态 IP 下的端到端 UDP 通路)
---
## 6. 阶段 5:大文件传输(硬件层)
| 阶段 | 阶段 5 |
|---------|-----------|
| **类型** | 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 |
> 编号说明:阶段 5 用例 **501/502**(顺序双测向完整性 + 变总量合规);原 PHY 测试(旧编号 901/902)因板载拓扑不可测已删除(见 TRAP-NET-05)。
### 为什么 2KB 接收缓冲不会丢数据(TCP 流控原理)
PC 一次回放 256KB,而本固件为每个 Socket 分配的接收缓冲仅 2KB(4 块 × 512B)——不丢数据的保证**不靠缓冲够大,靠 TCP 流控(通告窗口)**。
数据不是全部驻留芯片,而是在一串缓冲区里"接力":
```
PC OS 发送缓冲(64~256KB) → 网络在途 → CH395F 接收缓冲(2KB) → MCU 读取
```
CH395F 是完整 TCP 栈 offload 芯片,内部实现 TCP 状态机,按**接收缓冲剩余空间**向 PC 通告接收窗口(advertised window):
1. PC 发数据,芯片收进自己的 2KB 接收环形缓冲;
2. 缓冲快满 → 通告窗口收缩直至 **窗口=0** → PC 的 `sendall` **阻塞**(TCP 背压),剩余数据停在 PC 自身 OS 发送缓冲,不再发出;
3. MCU `GET_RECV_LEN` → `READ_RECV_BUF` 读走 → 缓冲腾空 → 芯片重新通告窗口 → PC 恢复发送。
因此是**背压联动、自节流**:数据永不因芯片缓冲溢出被丢弃,只会"堵一会儿"。MCU 平均读取速率 ≥ PC 发送速率即可持续吞吐(本测试每 2ms 轮询、每次读 2KB,平均速率远高于发送速率,缓冲常年接近空)。
"先查长度再读"(`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 数据 = 触发 DMA 边界安全原则的单次读边界(注意:单 Socket 发送缓冲实际为 1KB,net_send 写入需受此约束)
- 双重完整性:逐字节比对 + 全量 32 位累加和 `SUM(TX)==SUM(RX)`(双向同源 LCG,相等即双向一致)
- 双向独立吞吐打印(TX/RX 各自 KB/s),作为 SPI 时钟/驱动调整的回归基线
**固件会话结构**(phase5_run):
| 序 | 操作 | 参数/超时 |
|----|------|----------|
| 1 | close 清残留 → proto/des_ip/des_port/sour_port 配置 → open+connect | 两级中断等 CONNECT,15s |
| 2A | 连续发送 64×4KB = 256KB(PC 只收) | SENBUF_FREE 单块 2s |
| 2B | 连续接收 256KB(PC 按 LCG 回放)滚动校验 | RX 停滞 5s 判 FAIL |
| 3 | close,打印双向吞吐与校验结论 | blk_tx/blk_rx==64 且 SUM(TX)==SUM(RX) |
---
### TC-NET-501(硬件层): 256KB 顺序双向(纯 TX + 纯 RX)完整性
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-501 |
| **优先级** | P0 |
| **类型** | 功能测试(硬件层) |
| **标题** | 先连续发送 256KB(PC 只收),再连续接收 256KB(PC 回放),逐字节校验 + 总账一致 |
| **前置条件** | PC: `python test/host/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
**覆盖原则**:中断集中处理(两级中断轮询)、DMA 边界安全(4KB DMA 读边界)
---
### TC-NET-502(硬件层): 变总量与跨包分块合规
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-502 |
| **优先级** | P1 |
| **类型** | 合规测试(硬件层) |
| **标题** | 三档总量(64KB/256KB/1MB)+ 非整除块边界(4093B)回归 |
| **前置条件** | 同 TC-NET-501;每档修改 `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-NET-501 |
| 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
**覆盖原则**:DMA 边界安全(跨包/非对齐边界)
> **记录(2026-08-24 实测)**:三档全部 **6/6 PASSED**,checksum 逐档相等 ——
> 64KB(4096B 块): TX 420 / RX 364 KB/s;1MB(4096B 块): TX 449 / RX 391 KB/s;
> 4093B 块(261952B): TX 448 / RX 368 KB/s。吞吐随总量/块大小基本恒定,
> 双方向均约 370~450 KB/s,疑似 SPI 总线(SPI2 DMA)为瓶颈。
---
## 7. 阶段 6:NET 层 TCP Server 回显
| 阶段 | 阶段 6 |
|---------|-----------|
| **类型** | PC 配合,netTask 运行 |
| **耗时** | 持续运行(Server 模式)/ 约 60s(Client 模式) |
| **入口** | `ENABLE_PHASE6_TESTS` → `net_layer_test_main()`(`net_test_task.c`,持续监听回显循环) |
| **出口** | 用户终止(Server)/ 用例结束自动回到 IDLE(Client) |
本阶段在 NET 层(单连接 socket)经 `net_socket` API 驱动,MCU 作 TCP Server 监听 8080,PC 主动连接,验证回显通路、单连接生命周期与字节级完整性 —— 已实现(net 层自动重监听,应用层零生命周期代码)
> Client 主动连接(原 TC-NET-607/608/609,现 701/702/703)已独立为 **阶段 7**(见 §8),使用独立宏 `ENABLE_PHASE7_TESTS` 与独立入口 `net_layer_client_test_main()`。
### 7.1 架构与会话结构
**Server 回显模式**(`net_layer_test_main()`,`net_test_task.c`,由 `ENABLE_PHASE6_TESTS` 启用):
**架构图(组件)**:
```mermaid
flowchart LR
PC["PC(Python)<br/>tcp_client / tcp_server"] -->|"TCP 8080 · 以太网"| CH["CH395F(192.168.1.100)<br/>Socket 0(单连接复用)"]
CH -->|"SPI2(分频=8)"| MCU["MCU(STM32F407)<br/>netTask: net_poll() 10ms<br/>testTask: net_layer_test_main()<br/>└─ phase6_run() 原样回显"]
```
**会话时序图(单连接生命周期)**:
```mermaid
sequenceDiagram
participant C as PC(Client)
participant S as CH395F Socket 0
participant T as MCU testTask
C->>S: SYN
S->>C: SYN+ACK
C->>S: ACK(ESTABLISHED)
S->>T: 中断 / GlobInt → net_poll()
Note over T: net_accept() → phase_active
C->>S: [64B 数据](recv_len = N)
S->>T: 中断 → net_poll()
Note over T: phase6_run():net_recv(N) → 字节校验 → TEST_CHECK(累加和)
S->>C: [64B 回显](net_send)
Note over C: 逐字节比对 + 累加和(重复 N 轮 / 多包)
C->>S: FIN/RST(DISCONNECT)
S->>T: 中断 → net_poll()
Note over T: net_recv EOF → auto_relisten
S->>S: 自动 OPEN + TCP_LISTEN
C->>S: SYN(新连接)
Note over C,S: LISTEN 自动恢复,重新接受
```
- `phase6_run()` 为**纯回显**:非阻塞 `net_recv(sockfd, s_rx_buf, TEST_BUF_SIZE, NET_MSG_DONTWAIT)` 收到 N 字节后,`net_send(sockfd, s_rx_buf, N, 0)` 原样回显;不做固件侧字节自检(字节完整性由 PC 侧 `data == test_data` 校验)。
- 单连接约束:Socket 0 复用监听/数据通道;连接断开(DISCONNECT / KeepAlive TIMEOUT / 静默丢失)后由 **net 层自动重监听**(`auto_relisten`:`net_recv` 收到 EOF 时内部重新 `OPEN` + `TCP_LISTEN`),应用层无需 `net_close`/`net_listen`,下一次 `net_accept` 即接到新连接,行为等同 Linux BSD Socket 监听常驻。
> Client 主动连接(原 TC-NET-607/608/609,现 701/702/703)已独立为 **阶段 7**,架构与用例见 §8。
### TC-NET-601: Socket 0 单连接生命周期(主路径由 TC-NET-602~605 与 TC-NET-701~704 覆盖;「accept 返回同一 fd」的语义与「仅 Socket 0 状态变化、无其它 Socket 干扰」两点无观测,2026-10-21 用例清点)
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-601 |
| **优先级** | P0 |
| **类型** | 功能测试 |
| **标题** | Socket 0:OPEN → TCP_LISTEN → ESTABLISHED(单连接) |
| **前置条件** | 1. `net_init("192.168.1.100")` 成功、SYS_EVT_NET_READY 已广播(netTask 周期 `net_poll()` 运行)<br>2. `net_layer_test_main()` 已 `net_socket`+`net_bind(8080)`+`net_listen`(进入 LISTEN)<br>3. PC: `python .\test\ch395f_socket_test.py tcp_client --ip 192.168.1.100 --port 8080` |
**测试步骤与判定标准**:
| 步 | 操作 | 成功标准 | 失败标准 |
|----|------|----------|----------|
| 1 | PC 连接 8080 | `net_accept()` 返回 fd(== 监听 fd,单连接),`net_get_sock(state)==ESTABLISHED` | 连接未被接受 / 状态未进入 ESTABLISHED |
| 2 | `phase6_run()` 非阻塞收 PC 数据 | `net_recv(DONTWAIT)` 返回 > 0 | 收超时(连接未真建立) |
| 3 | 固件收 PC 数据 → 字节级比对 + 32 位累加和 `TEST_CHECK` → `net_send()` 原样回显 | PC 收到与发送完全一致的数据且固件 `TEST_CHECK` 通过 | 回显丢失 / 错乱 / 固件校验失败 |
| 4 | 串口观测 `net_get_sock` 状态 | 仅 Socket 0 状态变化,无其它 Socket 干扰 | 出现非预期 Socket 状态跳变 |
**通过标准**:步 1~4 全部满足,单连接生命周期正常。任一失败 → 标记 FAIL
**覆盖原则**:单连接 socket(OPEN→LISTEN→ESTABLISHED 单连接主路径) |
### TC-NET-602: 64 字节小包回显
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-602 |
| **优先级** | P0 |
| **类型** | 功能测试 |
| **标题** | 10 轮 64B TCP 回显(原样回显,小包) |
| **前置条件** | 同 TC-NET-601(连接已建立,ESTABLISHED) |
**测试步骤与判定标准**:
| 步 | 操作 | 成功标准 | 失败标准 |
|----|------|----------|----------|
| 1 | PC 依次发送 10×64B(递增模式 `0x00..0x3F`) | 每轮 `net_recv` 收 64B | 收包不足 64B |
| 2 | `phase6_run()` 收 64B → 原样回显 | PC 逐轮收到 64B 且 `data == test_data` 一致 | 回显丢失 / 错乱 / PC 校验不符 |
| 3 | PC 汇总 10 轮并逐字节校验 | 10/10 匹配 | 任一轮失败 |
**通过标准**:10/10 匹配,0 失败。任一失败 → 标记 FAIL
**覆盖原则**:单连接 socket(单连接数据通路,小包) |
### TC-NET-603: 1460 字节回显(当前固件 MSS=1024,1460B 会按 MSS 分片)
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-603 |
| **优先级** | P0 |
| **类型** | 边界测试 |
| **标题** | 1460B 回显(注:当前固件 TCP MSS=1024,1460B 超过 MSS 将由 CH395F 按 MSS 分片) |
| **前置条件** | 同 TC-NET-601(连接已建立) |
**测试步骤与判定标准**:
| 步 | 操作 | 成功标准 | 失败标准 |
|----|------|----------|----------|
| 1 | PC 发单包 1460B(当前 TCP MSS=1024,1460B 会被分片为 1024+436;`net_send` 亦按 1KB 发送缓冲分块) | `net_recv` 一次收 1460B(≤2KB 接收缓冲,DMABUF=4100 一次读空) | 接收字节数 ≠ 1460(被截断 / 分片丢失) |
| 2 | 固件收 1460B → 原样回显 | PC 收到 1460B 且 `data == test_data` 一致 | 回显错乱 / PC 校验不符 |
| 3 | PC 校验 | 1460B 完全匹配 | 任意字节不符 |
**通过标准**:1460B 完全匹配,无截断。任一失败 → 标记 FAIL
**覆盖原则**:DMA 边界安全(1460 ≤ 2KB 接收缓冲,单次 DMA 读空) |
### TC-NET-604: 4096 字节缓冲满回显
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-604 |
| **优先级** | P0 |
| **类型** | 边界测试 |
| **标题** | 4096B 回显(接收缓冲仅 2KB,分两次取空) |
| **前置条件** | 同 TC-NET-601;`CH395F_SPI_DMA_BUF_SIZE=4100` 可一次性读空 2KB 硬件缓冲 |
**测试步骤与判定标准**:
| 步 | 操作 | 成功标准 | 失败标准 |
|----|------|----------|----------|
| 1 | PC 发单包 4096B(实际单 Socket 接收缓冲为 2KB,固件分两次 `net_recv` 取空) | `net_recv` 累计收满 4096B(分次) | 接收字节数 ≠ 4096(丢失,见台架纪律注) |
| 2 | 固件收 4096B → 原样回显(`net_send` 分块) | PC 收到 4096B 且 `data == test_data` 一致 | 回显错乱 / PC 校验不符 |
| 3 | PC 校验 | 4096B 完全匹配 | 任意字节不符 |
**通过标准**:4096B 完全匹配。任一失败 → 标记 FAIL
**覆盖原则**:DMA 边界安全(2KB 接收缓冲边界 + 单次 DMA 上限) |
> **2026-09-23 结案**:TC-NET-604 的"缓冲满回显"路径**判定为非缺陷**——真实生产路径(产品固件 + 真 lftpd FTP 上传,判据 = FTP 协议结果 + **上传后 RETR 回读 SHA-256**)实测 **227 轮全过**(8KB~1MB,含零间隔快速重连与 100 轮浸泡),并发生产 UI 流(`:7003`)600 帧 CRC 全过;原台架"停摆"系**测试程序自吞数据 + 判读工具误判**(详见 `CH395F_Trap_Records.md`「台架纪律」节)。
> - **本用例判据(不变)**:PC 发 4096B → 固件分次取空并回显 → **PC 侧内容完全匹配**即 PASS;上传/中止类真实缺陷见 **TRAP-NET-18**(回归工具 `test/host/ftp_abort_probe.py`)。
> - **台架纪律**:判读一律用 `MDK-ARM/bench_verdict.py`(脚本层 STEP 行为真值,并扣除固件读者吞噬的字节);`probe_classify.py` 的 A/C 判定**不可信**;**禁止**把"固件内非回显读者"(Phase 9/11、固件探针)与固定期望值混用在同一配方里。
> - **Phase 6-only 构建禁止无间隔连跑**:<~1s 重连间隔会触发 TRAP-NET-17 失同步(自 run2 起:无 standalone accept、`send -1`、其后全部 connection refused)——快速重连场景须留 ≥15s 间隔;带 Phase 9/11 的构建会被其读者吞数据,不能当对照。
### TC-NET-605: 断开重连
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-605 |
| **优先级** | P0 |
| **类型** | 恢复测试 |
| **标题** | PC 断开后 Socket 0 自动重新监听并接受新连接 |
| **前置条件** | 同 TC-NET-601(连接已建立,已完成至少一轮 64B 回显) |
**测试步骤与判定标准**:
| 步 | 操作 | 成功标准 | 失败标准 |
|----|------|----------|----------|
| 1 | PC 关闭连接(FIN/RST) | 固件 `net_recv` 收 EOF、`net` 层自动 `auto_relisten` 回到 LISTEN | 状态未变化(卡 ESTABLISHED) |
| 2 | net 层自动重监听,无需应用干预(`net_recv` EOF 内部重新 OPEN + TCP_LISTEN) | `net_accept` 在新连接到达时再次返回 fd | 未回到 LISTEN(状态机卡死) |
| 3 | PC 重新连接 8080 | `net_accept` 再次返回 fd,进入 ESTABLISHED | 重连失败 |
| 4 | PC 发 64B → 固件原样回显 | 回显正常且 PC `data == test_data` 通过 | 回显失败 / PC 校验不符 |
**通过标准**:重连成功且回显正常,0 失败。任一失败 → 标记 FAIL
**覆盖原则**:单连接 socket(DISCONNECT → CLOSED → re-OPEN → LISTEN 单连接恢复) |
### TC-NET-606: 连接异常恢复(静默断开 / KeepAlive 超时)(未实现:不发 FIN 的静默断开与 KeepAlive 超时后 `auto_relisten` 能否自动回 LISTEN,**无任何可重复观测**——PC 侧 TC-NET-605 走的是干净 close(DISCONNECT 事件),与本条要求的 TIMEOUT 路径机制不同,不算覆盖;断连恢复语义见 docs/网络/网络自愈架构设计.md,2026-10-21 用例清点)
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-606 |
| **优先级** | P1 |
| **类型** | 恢复测试 |
| **标题** | 无干净 FIN 或 KeepAlive 超时后 Socket 0 自动重新监听 |
| **前置条件** | 1. 同 TC-NET-601(连接已建立)<br>2. KeepAlive 触发场景需 `ENABLE_PHASE11_TESTS` 且 `ch395f_set_keepalive_*` 已配置(探测时长本身由 TC-NET-1101 覆盖) |
**测试步骤与判定标准**:
| 步 | 操作 | 成功标准 | 失败标准 |
|----|------|----------|----------|
| 1 | PC 静默断开(如直接断开网线 / 断电,不发 FIN)或 KeepAlive 探测超时触发 TIMEOUT | 固件经 `net_get_sock(state)` 轮询发现非 ESTABLISHED | 状态长期卡 ESTABLISHED(未恢复) |
| 2 | 固件 net 层 `auto_relisten` 自动回到 LISTEN(应用层无 `net_close`/`net_listen` 调用) | 回到 LISTEN,等待新连接 | 未恢复监听 |
| 3 | PC 重连 → 发 64B → 验证回显 | 回显正常 | 回显失败 |
**通过标准**:自动恢复监听,无需手动复位。任一异常 → 标记 FAIL
**覆盖原则**:单连接 socket(异常路径恢复;KeepAlive 探测机制见 TC-NET-1101) |
---
## 8. 阶段 7:NET 层 TCP Client 主动连接
| 阶段 | 阶段 7 |
|---------|-----------|
| **类型** | PC 配合,netTask 运行 |
| **耗时** | 约 60s |
| **入口** | `ENABLE_PHASE7_TESTS` → `net_layer_client_test_main()`(`net_test_task.c`) |
| **出口** | 用例结束自动回到 IDLE |
> 与硬件层阶段 2 的 TC-NET-201/202/203 一一对应(芯片行为 → API 封装),验证 `net_socket` / `net_connect` / `net_send` / `net_recv` / `net_close` 对 TCP Client 主路径及异常路径的正确封装。
> 由独立宏 `ENABLE_PHASE7_TESTS` 启用,独立于 Server 回显阶段 6(`ENABLE_PHASE6_TESTS`);因阶段 6 为持续监听循环,Client 阶段需**单独构建**(不与 `ENABLE_PHASE6_TESTS` 同时启用)。
> 原编号 204/205/206(原阶段 2 的 NET 层部分),2026-08 重排后为 701/702/703。
### 8.1 架构与会话结构
```
net_socket(NET_AF_INET, NET_SOCK_STREAM) // 取空闲 Socket(非监听 Socket 0)
→ net_connect(PC:8081) // 异步下发,net_poll 推进状态机
→ net_select(sockfd+1, NULL, &wfds, NULL, &tv) // 等可写 = ESTABLISHED
→ net_send(64B) → net_recv(64B) 校验 // 主路径(701)
→ net_close() // 释放
```
**架构图(组件)**:
```mermaid
flowchart LR
MCU["MCU(STM32F407)<br/>netTask: net_poll() 10ms<br/>testTask: net_layer_client_test_main()<br/>└─ net_connect → 收发 → close"] -->|"SPI2(分频=8)"| CH["CH395F(192.168.1.100)<br/>Socket N(非 0 空闲)"]
CH -->|"TCP 8081 · 以太网"| PC["PC(Python)<br/>tcp_server(回显服务器)"]
```
**会话时序图(Client 主动连接)**:
```mermaid
sequenceDiagram
participant C as PC(Server :8081)
participant S as CH395F Socket N
participant T as MCU testTask
Note over T: net_layer_client_test_main()
T->>S: net_socket() + net_connect()(异步下发 CONNECT)
S->>C: SYN
C->>S: SYN+ACK(ESTABLISHED)
S->>T: 中断 / GlobInt → net_poll()
Note over T: net_select() 可写 → 建链
T->>S: [64B](net_send)
S->>C: [64B]
Note over C: 回显服务器原样返回
C->>S: [64B 回显](recv_len = N)
S->>T: 中断 → net_poll()
Note over T: net_recv(N) → 字节校验 → TEST_CHECK(累加和)
T->>S: net_close()(释放 Socket N)
```
- `net_layer_client_test_main()` 为**独立入口**:使用非 0 空闲 Socket 连接 PC,不在 `net_layer_test_main()` 的监听循环内;由 `StartCh395fTestTask` 的 NET 分支按宏调用。
- **固件自检**:`net_recv` 后逐字节比对 + 32 位累加和 `TEST_CHECK`(固件侧独立校验);PC 同时校验。
- 关闭重连(702)、连接超时(703)见对应 TC。
### TC-NET-701: TCP Client 连接与数据交换
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-701 |
| **优先级** | P0 |
| **类型** | 功能测试(NET 层) |
| **标题** | net_socket TCP Client → PC TCP Server,64 字节收发 |
| **前置条件** | 1. PC: `python ./test/host/ch395f_socket_test.py tcp_server --port 8081`(TCP 回显服务器)<br>2. `net_init()` 已完成、SYS_EVT_NET_READY 已广播(netTask 周期 `net_poll()` 运行中)<br>3. `ENABLE_PHASE7_TESTS` 已定义,入口为 `net_layer_client_test_main()`(见「构建模式与执行顺序」)<br>4. 建议:硬件层固件中 TC-NET-201/202/203 已通过(跨构建前置,见「构建模式与执行顺序」) |
**测试步骤与判定标准**:
| 步 | 操作 | 成功标准 | 失败标准 |
|----|------|----------|----------|
| 1 | `net_socket(NET_AF_INET, NET_SOCK_STREAM)` 创建 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`、与原数据逐字节一致、且固件字节级比对 + 累加和 `TEST_CHECK` 通过 | 返回值 `< 0`(超时/断开)或 `≠ 64`(数据不完整)或任意字节不符 或 固件 `TEST_CHECK` 失败 |
| 6 | `net_close(sockfd)` 关闭 socket(TCP 先断开连接,释放控制块) | 返回 `0` | 返回 `-1`(关闭失败) |
**通过标准**:6/6 通过,0 失败。任一失败 → 标记 FAIL
**覆盖原则**:单连接 socket(net_socket→net_connect→net_send→net_recv→net_close 主路径,NET 层)
### TC-NET-702: TCP Client 关闭重连 ×3 轮
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-702 |
| **优先级** | P0 |
| **类型** | 功能测试 / 恢复测试(NET 层) |
| **标题** | close→重连循环 3 轮,每轮含数据交换 |
| **前置条件** | 同 TC-NET-701 |
**测试步骤与判定标准**(每轮 i = 0,1,2):
| 步 | 操作 | 成功标准 | 失败标准 |
|----|------|----------|----------|
| 1 | `net_socket(NET_AF_INET, NET_SOCK_STREAM)` 创建 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` 字节、固件字节级比对 + 累加和 `TEST_CHECK` 通过 | 收发失败 / 数据不符 / 固件 `TEST_CHECK` 失败 |
| 4 | `net_close(sockfd)` 关闭并释放控制块 | 返回 `0` | 返回 `-1` |
**通过标准**:3/3 轮全部通过,0 失败。任一失败 → 标记 FAIL
**覆盖原则**:单连接 socket(net 层 open→connect→close→re-open→reconnect)
### TC-NET-703: TCP Client 连接超时
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-703 |
| **优先级** | P1 |
| **类型** | 负向测试(NET 层) |
| **标题** | 目标端口无服务时 net_connect 超时 |
| **前置条件** | 1. PC **未**运行 TCP Server(`192.168.1.2:8081` 无监听)<br>2. TC-NET-701 通过 |
**测试步骤与判定标准**:
| 步 | 操作 | 成功标准 | 失败标准 |
|----|------|----------|----------|
| 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
**覆盖原则**:单连接 socket(NET 层异常路径 connect 失败处理)
> **实现说明**:TC-NET-703 与 TC-NET-701/702 的 PC 环境互斥(前者要求 PC 服务关闭、后两者要求开启),故固件中 TC-NET-703 由子开关 `PHASE7_TEST_TIMEOUT` 单独启用——默认 `ENABLE_PHASE7_TESTS` 仅跑 701/702;单独开启 `PHASE7_TEST_TIMEOUT` 并关闭 PC 服务后重编,方可执行 703。
### TC-NET-704: 5-Socket 并发 TCP Client(多 Socket 模式验证)
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-704 |
| **优先级** | P0 |
| **类型** | 功能测试 / 并发测试(NET 层) |
| **标题** | 同时打开 5 个 Socket 连接 PC Server,各自 64B 收发 |
| **前置条件** | 1. PC: `python ./test/host/ch395f_socket_test.py tcp_server --port 8081 --max-conn 5`(并发回显,`--max-conn` 为并发计数)<br>2. `net_init()` 已置 `FUN_PARA=0x08`,Socket 缓冲按「CH395F 缓冲架构」分配(每 Socket 接收 2KB + 发送 1KB,MSS=1024)——**缓冲显式分配是本用例的前提**(Socket 可用性不依赖 FUN_PARA)<br>3. `ENABLE_PHASE7_TESTS` 已定义<br>4. `PHASE7_MULTI_COUNT=5`(并发上限 = `NET_MAX_SOCKETS`(8) − 3 常驻监听;2026-10-15 由 8 降至此) |
**测试步骤与判定标准**:
| 步 | 操作 | 成功标准 | 失败标准 |
|----|------|----------|----------|
| 1 | 循环 i=0..4:`net_socket()` 创建 5 个 TCP socket(本地端口 51000+i),`net_connect(192.168.1.2:8081)` | 5 个 `net_connect` 均返回 `0` | 任一返回 `-1` |
| 2 | netTask `net_poll()` 推进下,`net_select` 等待全部 5 路可写 | 5 路均在 10s 内 ESTABLISHED | 任一超时未建立 |
| 3 | 每路 `net_send(sockfd, 64B)` → `net_recv(sockfd, 64B)` 校验 | 5 路收发各 64B、字节级比对 + 累加和 `TEST_CHECK` 通过 | 任一收发失败 / 数据不符 / `TEST_CHECK` 失败 |
**通过标准**:5/5 路全部通过,0 失败。任一失败 → 标记 FAIL
**覆盖原则**:单连接 socket(多 Socket 并发主路径)、缓冲架构(8×6 块分配,MSS=1024)
> **注**:PC 端 `--max-conn` 须为**并发**计数(连接关闭即减),否则 Phase 7 全量(701+702+704 共 12 连接)会按累计计数超限而拒掉 4~7。该用例已随 Phase 7 实测通过(并发回显落在 sock2/4/5/6/7,见 TRAP-NET-15 末)。
>
> **2026-10-11 复跑(`FUN_PARA=0x08`)**:**Phase 7 34/34 全通过**,5 路并发 ESTABLISHED + 64 B 回显字节级比对 OK,芯片 Socket 实测落在 **sock2/4/5/6/7**(0/1/3 被 FTP/对时/远显三个监听占用)→ 证明 Socket 可用性不依赖 FUN_PARA。两条**前置排雷**(否则会得到全量假 FAIL):① PC 适配器落在 **Public** 防火墙配置文件会静默丢入站 8081,症状是 TC701/702/704 全部 `CONNECT TIMEOUT`;② 残留的旧 `tcp_server` 进程占着 8081,新服务器绑不上端口不报错。另:`PHASE7_MULTI_COUNT=5`(2026-10-15 由 8 降至此;`NET_MAX_SOCKETS=8` 减去三个常驻监听)。证据链见 `问题记录/CH395F_Trap_Records.md` TRAP-NET-15。
---
## 9. 阶段 8:大文件 / 批量传输(100KB,文件收发)
| 阶段 | 阶段 8 |
|---------|-----------|
| **类型** | PC 配合(PC 作 TCP Client 连接 MCU:8080),netTask 运行 |
| **耗时** | 取决于文件大小与速率(100KB 约数秒) |
| **入口** | `ENABLE_PHASE8_TESTS` |
| **配置** | `PHASE8_FILE_SIZE`(默认 102400 = 100KB);`PHASE8_DIRECTION`(SEND / RECV / ECHO,默认 ECHO) |
| **PC 配合** | `python ./test/host/ch395f_socket_test.py tcp_file --ip 192.168.1.100 --port 8080 --op recv --file rx_100k.bin --size 102400`(文件发送)<br>`... --op send --file tx_100k.bin --size 102400`(文件接收) |
> 架构:MCU 为 TCP Server(`net_test_task` 监听 8080,`net_accept` 后 `phase8_run(acc)`);PC 用 `tcp_file` 子命令作 Client 连接 MCU:8080。一次 `phase8_run` 对应一次 PC 连接。
### 设计要点
- **流式分块,不缓存全文件**:`s_tx_buf`/`s_rx_buf` 仅 32KB,100KB 须按窗口逐块生成/校验(递增模式 `(offset+i)&0xFF`),避免占用大块 RAM。
- **方向覆盖(由 `PHASE8_DIRECTION` 选择,或分三次连接分别跑)**:
- **SEND(TC-NET-801)**:MCU 流式发出 100KB,PC 落盘 `rx_100k.bin` 并校验。
- **RECV(TC-NET-802)**:PC 读 `tx_100k.bin` 流式发出(尾部附 4 字节 CRC32),MCU 逐块接收并字节校验 + 整包 CRC32 比对。
- **ECHO(TC-NET-803)**:MCU 发 100KB 由 PC 原样回显,MCU 收齐校验(round-trip,覆盖收发双路径)。
- **完整性校验**:
- 字节级:递增模式 `(offset+i)&0xFF`,接收端按 offset 逐字节比对(MCU 侧自校验)。
- 整包级:发送端对 100KB 计算 CRC32,接收端比对;RECV 方向由 PC 在文件末尾附加 4 字节 CRC32(小端),MCU 收齐后比对自身运行的 CRC32。
### TC-NET-801: 100KB 文件发送(MCU→PC)
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-801 |
| **优先级** | P0 |
| **类型** | 功能测试(文件发送) |
| **标题** | MCU 流式发送 100KB 至 PC 并落盘校验 |
| **前置条件** | 1. PC: `python ./test/host/ch395f_socket_test.py tcp_file --ip 192.168.1.100 --port 8080 --op recv --file rx_100k.bin --size 102400`(先连接等待数据)<br>2. `ENABLE_PHASE8_TESTS`,`PHASE8_FILE_SIZE=102400`,`PHASE8_DIRECTION=SEND`<br>3. `net_init` 完成,MSS=1024 |
| **测试步骤** | 1. MCU `net_accept` 建链(PC 已连接)<br>2. 循环填充一个滑动窗口(≤ s_tx_buf)并调用 `net_send`;`net_send` 内部按硬件发送 FIFO(`NET_SEND_CHUNK_MAX`=1KB,见 TRAP-NET-16)自动拆分写入,直至 100KB 发完<br>3. PC 接收全部字节写入 `rx_100k.bin`,按递增模式或预置 CRC32 校验 |
| **通过标准** | PC 收满 102400B 且逐字节/CRC32 校验一致,MCU 无发送错误 |
| **覆盖原则** | DMA 边界安全(分块发送、DMA 边界)、缓冲架构(1KB 发送缓冲) |
| **实测** | ✅ PASS(2026-08-26):固件 `PHASE8_DIRECTION=SEND`,PC `tcp_file --op recv` 收满 102400B,递增模式逐字节校验通过,CRC32=0x9A0E0C8C,耗时 1209ms / 82.7 KB/s,落盘 `rx_100k.bin` 成功。 |
### TC-NET-802: 100KB 文件接收(PC→MCU)
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-802 |
| **优先级** | P0 |
| **类型** | 功能测试(文件接收) |
| **标题** | PC 流式发送 100KB 文件,MCU 接收并校验 |
| **前置条件** | 1. PC: `python ./test/host/ch395f_socket_test.py tcp_file --ip 192.168.1.100 --port 8080 --op send --file tx_100k.bin --size 102400`(先连接并发送;无文件则生成 100KB 递增模式)<br>2. 同 TC-NET-801(`PHASE8_DIRECTION=RECV`) |
| **测试步骤** | 1. MCU `net_accept` 建链(PC 已连接)<br>2. 循环 `net_recv`(≤2KB/次,保守一次读空)→ 按 offset 字节校验 + 累计 CRC32,直至收满 100KB<br>3. PC 在文件末尾附 4 字节 CRC32(小端);MCU 收齐后比对 CRC32 |
| **通过标准** | MCU 收满 102400B,字节校验全过且 CRC32 匹配,无溢出/截断 |
| **覆盖原则** | DMA 边界安全(接收缓冲 2KB 一次读空) |
| **实测** | 🟡 待验证(参考:默认递增模式 100KB 的 CRC32 经 SEND 实测为 `0x9A0E0C8C`,RECV 方向 MCU 累计 CRC32 应与之相等) |
### TC-NET-803: 100KB 回显 round-trip(可选)
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-803 |
| **优先级** | P1 |
| **类型** | 恢复/压力测试 |
| **标题** | MCU 发 100KB 经 PC 回显原样返回,MCU 收齐校验 |
| **前置条件** | MCU: `PHASE8_DIRECTION=ECHO`(net_accept 监听 8080);PC: `python ./test/host/ch395f_socket_test.py tcp_file --ip 192.168.1.100 --port 8080 --op echo`(PC 作 Client 连 MCU 并原样回显) |
| **测试步骤** | MCU 发 100KB → PC 回显 → MCU 收 100KB 字节校验 |
| **通过标准** | 100KB 全过,验证 100KB 经回显链路完整 |
| **覆盖原则** | 单连接 socket、DMA 边界安全 |
| **实测** | 🟢 已验证(MCU 收齐 102400B 逐字节校验通过;PC 回显客户端 1241ms @ 80.6 KB/s,对端关闭后正常退出;修复了 phase8_run 未关闭连接导致 MCU 在同连接上无限重复、PC 挂起的问题) |
### 实现说明
`phase8_run()` 状态机(netTestTask,`net_accept` 后执行,`sockfd` 为已建链 fd):
```
IDLE → ACCEPT → [SEND: 分块发 → DONE] / [RECV: 分块收+校验 → DONE] / [ECHO: 分块发 → 分块收(回显) → DONE] → 关闭 → 重新 ACCEPT
```
- **发送**:`phase8_fill(buf, chunk, offset)` 按窗口生成递增模式;`net_send` 经消息队列转发 netTask,由 `net_send_locked` 按发送缓冲(1KB)/`CH395F_SPI_DMA_MAX_PAYLOAD` 自动拆分。
- **接收**:`net_recv` 每次读 ≤2KB(=接收缓冲,保守一次读空),`phase8_verify(buf, n, offset)` 字节校验 + 累计 CRC32。
- **100KB 不整体驻留 RAM**:`s_tx_buf`/`s_rx_buf`=32KB 仅作滑动窗口;`PHASE8_FILE_SIZE` 控制总量。
- **PC `tcp_file` 子命令**(新增,作 Client 连 MCU:8080):`recv` 接收 `--size` 字节落盘并校验;`send` 读 `--file` 发送并附 4 字节 CRC32 尾。
---
## 10. 阶段 9:Select/Poll I/O 多路复用(单连接)
| 阶段 | 阶段 9 |
|---------|-----------|
| **类型** | PC 配合,netTask 运行 |
| **耗时** | 持续运行 |
| **入口** | `ENABLE_PHASE9_TESTS` |
### TC-NET-901: net_select() 单 socket 就绪检测
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-901 |
| **优先级** | 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()` 回显 |
| **通过标准** | 回显成功 |
| **覆盖原则** | 中断集中处理(select 内部调 net_poll) |
### TC-NET-902: net_select() 超时
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-902 |
| **优先级** | P1 |
| **类型** | 负向测试 |
| **标题** | 100ms 超时内无数据则返回 0 |
| **前置条件** | Socket 0 ESTABLISHED,PC 静置 |
| **测试步骤** | `net_select(1, &readfds, ..., 100ms)` |
| **通过标准** | 返回值 == 0,无崩溃 |
---
## 11. 阶段 10:边界条件测试
| 阶段 | 阶段 10 |
|---------|-----------|
| **类型** | PC 配合,netTask 运行 |
| **耗时** | 按需 |
| **入口** | `ENABLE_PHASE10_TESTS` |
### TC-NET-1001: DMA 边界 −1(4095 字节)
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-1001 |
| **优先级** | P0 |
| **类型** | 边界测试 |
| **标题** | 发送 4095 字节(SPI 最大载荷 −1,单次 DMA 可容纳) |
| **前置条件** | Socket 0 ESTABLISHED |
| **测试步骤** | MCU 生成 4095B 递增模式数据 → `net_send_sock(sk, data, 4095)` → 等待 PC 回显 → 验证 |
| **通过标准** | 4095B 完全匹配 |
| **覆盖原则** | DMA 边界安全 |
### TC-NET-1002: DMA 边界(4096 字节)
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-1002 |
| **优先级** | P0 |
| **类型** | 边界测试 |
| **标题** | 发送 4096 字节(等于 SPI 最大载荷,填满单次 DMA) |
| **测试步骤** | MCU 发 4096B → 等待回显 → 验证 |
| **通过标准** | 4096B 完全匹配 |
| **覆盖原则** | DMA 边界安全 |
### TC-NET-1003: DMA 边界 +1(4097 字节)
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-1003 |
| **优先级** | P0 |
| **类型** | 边界测试 |
| **标题** | 发送 4097 字节(超过 SPI 最大载荷,自动拆两次 DMA:4096+1) |
| **测试步骤** | MCU 发 4097B → `net_send_locked` 自动拆 4096+1 → 等待回显 → 验证 |
| **通过标准** | 4097B 完全匹配,两次 DMA 无缝衔接 |
| **覆盖原则** | DMA 边界安全 |
### TC-NET-1004: 零长度收发
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-1004 |
| **优先级** | P1 |
| **类型** | 负向测试 |
| **标题** | `net_send_sock(sk, buf, 0)` 返回 −1 |
| **测试步骤** | `net_send_sock(sk, s_phase9_tx, 0)` |
| **通过标准** | 返回 < 0,无崩溃 |
### TC-NET-1005: 连续快速发送 20 小包
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-1005 |
| **优先级** | P1 |
| **类型** | 压力测试 |
| **标题** | 20 × 100 字节连续快速发送(验证 SENDBUF_FREE 等待机制) |
| **测试步骤** | 循环 20 次:`net_send_sock(sk, buf, 100)`,间隔 1ms |
| **通过标准** | 20/20 成功,回显 2000B 完全匹配 |
### TC-NET-1006: 快速连接-断开 ×20 轮(未实现:20 轮「连上立即断开」压力与每轮回到 LISTEN 的断言都没有;单轮断开重连由 TC-NET-605 覆盖,2026-10-21 用例清点)
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-1006 |
| **优先级** | P1 |
| **类型** | 稳定性测试 |
| **标题** | 20 次快速连接-断开循环 |
| **测试步骤** | PC 连 8080 → 立即断开 → 等 s0 恢复 LISTEN → 重复 20 次 |
| **通过标准** | 20/20 不卡死,s0 始终回到 LISTEN |
---
## 12. 阶段 11:KeepAlive 超时断开
| 阶段 | 阶段 11 |
|---------|-----------|
| **类型** | PC 配合,netTask 运行 |
| **耗时** | ~1.5–2 分钟(连接+网卡禁用后实测判死 18~31s,加恢复重连;芯片定时语义以实测为准,见 E11/E13) |
| **入口** | `ENABLE_PHASE11_TESTS`(2026-10-13 起默认开启;服务器侧开关由 net 层在 CONNECT 后自动置位,`docs/芯片手册/CH395F.md` §5.56) |
| **配置** | KeepAlive 空闲 60s,探测间隔 5s,3 次(`net_config.h`) |
### TC-NET-1101: KeepAlive 触发断开
| 字段 | 值 |
|------|-----|
| **ID** | TC-NET-1101 |
| **优先级** | P1 |
| **类型** | 功能测试 |
| **标题** | TCP 连接空闲超过 KeepAlive 阈值后自动断开 |
| **前置条件** | 1. `NET_KEEPALIVE_IDLE_MS=60000` / `INTVL=5000` / `CNT=3`(`net_config.h`)<br>2. 对端须为**死对端**:PC 连接 8080 后**禁用网卡**制造"链路在、对端静默"(存活对端会 ACK 探测 → 永不 TIMEOUT;Windows 防火墙规则对已建立连接无效,E11 实证)<br>3. 勿与 Phase 6/9/10/13 混选(recv 互抢数据) |
| **测试步骤** | 1. PC 连接 8080,不发数据<br>2. 立即禁用 PC 网卡(中间有交换机 → 设备侧 PHY 链路保持)<br>3. 等待芯片报 TIM_OUT(实测判死窗:连接起 **18~31s**,芯片定时语义与手册公式 75s 不符,见 `docs/网络/网络自愈架构设计.md` §5.2 E11/E13)<br>4. 本层 QUARANTINE → reconciler 形态③④ 重建 → s0 回 LISTEN<br>5. 恢复网卡,重连 8080 验证端口已恢复 |
| **通过标准** | recv 返回 0 且耗时落在 **10s~120s 有界窗**(下限防"存活空闲会话被误杀"——空闲存活场景实测存活对端静默 90s 不触发;上限防"判死缺位");s0 回 LISTEN 且可接受新连接。报告后测试自动释放 socket 等新连接再测(一次性,防对已死连接 10ms 节奏刷假 FAIL) |
---
## 12.5 阶段 15:自愈驱动原语(硬件层,无需 PC)
验证 `ch395f` 自愈原语(`docs/网络/网络自愈架构设计.md` §4.4;形态定案锚点
同文档 §5.2 E2/E3/E4)。硬件层模式独占芯片,用 Socket 7
(产品监听 0~4 不涉),无需 PC 配合。污染会话下的复位载荷完整性不在本 Phase
(已由 E4 bench 60 轮实证),本 Phase 只验状态机跑通与判据语义。
| 用例 | 内容 | 通过标准 |
|------|------|----------|
| TC-NET-1501 | 状态回读一致性 + `ch395f_cmd_done` 语义 + 二段关闭 + 哨兵/边界 | open done ≤50ms;listen done ≤20ms(E2:100% ≤10ms);缓存 05/01;**LISTEN 态 close 二段**:一段后 05/00(监听已拆、byte0 未落)、二段后 byte0=00 ≤500ms;closed 时 tcp_state=0xFF 哨兵;sn=8 越界返 {00,FF} |
| TC-NET-1502 | `sock_reset_begin/poll` 非阻塞状态机(OPEN 态,QUARANTINE) | begin 受理;运行中二次 begin 返 -1(busy reject);≤3000ms 落定 rc=0;复位后芯片 CLOSED、poll 幂等返 0、可重开 |
| TC-NET-1502b | LISTEN 态 Socket 复位(GRACEFUL,走 WAIT_CLOSE 二段补发路径) | ≤3000ms 落定 rc=0(实测 ~180ms);复位后 CLOSED |
| TC-NET-1503 | 链路态缓存 + 事件护栏边界 | 稳定链路 feed 两次不虚增变化计数;link_up 缓存=1;`sock_events(8)` 越界返 0 |
**二段关闭语义**(本 Phase 首跑实证,详见 `docs/网络/网络自愈架构设计.md` §5.2 E2):LISTEN 态
`CLOSE_SOCKET` 返 SUCCESS 但只拆监听(TCP 01→00),byte0 恒 05;需再发一次
CLOSE 才落 00。与 TRAP-NET-07 的 `0x05/0x00` 驻留签名同源;`sock_reset_poll` 的
WAIT_CLOSE 阶段已内置二段补发(只补一次),生产 net 层则靠 TRAP-NET-09 open 重试吸收。
**运行方法**:`ch395f_test.h` 启用 `ENABLE_PHASE15_TESTS`(自动进入硬件层模式,
勿与 NET 层 Phase 混选)→ `/keil-build` → 烧录 → 串口看 `=== Phase 15: 20/20 PASSED ===`。
---
## 13. Python 测试脚本参考
| 命令 | 模式 | 说明 |
|------|------|------|
| `tcp_server --port 8081` | PC 作 TCP 回显服务器(回显循环) | 测试 TCP Client(阶段 2 硬件层;阶段 7 Client 701/702/703) |
| `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 数据 |
| `dhcp_server ...` | DHCP Server | 内置 DHCP(阶段 4,需管理员权限) |
### 通用参数
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `--ip` | 192.168.1.100 | 目标 IP |
| `--port` | 因模式而异 | 端口 |
| `--count` | 10 | 测试次数 |
| `--timeout` | 30 | 超时秒数 |
| `--size` | 1024 | 包大小 |
---
## 14. 已知陷阱
| ID | 陷阱 | 现象 | 修复 |
|----|------|------|------|
| TRAP-NET-03 | DHCP xid 偏移错误 | DHCP 握手失败 | xid 在 `data[4:8]` |
| TRAP-NET-04 | UDP 无 SENDBUF_FREE 等待 | 后续接收挂起 | 每次 write 后等 SENDBUF_FREE |
| TRAP-NET-05 | PHY 不兼容 RTL8305NBI | 链路抖动 | `ch395f_set_phy(0x08)` 强制 100M_FULL |
| TRAP-NET-06 | KeepAlive 非 500ms 倍数 | 参数不生效 | 必须 500ms 倍数 |
| TRAP-NET-07 | 只 `disconnect` 不 `close`(bit3=1) | 停在 `0x05/0x00`、重开返 `0x20` | 直接 `close_socket`(§5.35 自动断链,**非 RST**);见 TRAP-NET-07与 TC-NET-204 |
| TRAP-NET-08 | RECV 电平中断无限循环 | netTask 饿死 | `net_poll` 每次只一批,数据在应用中读 |
| TRAP-NET-09 | PHY 刚建立 link 立即 open | 首次连接失败 | 启动延时 + 重试 |
| TRAP-NET-10 | 消息队列值拷贝丢失 | API 返回旧值 | 改用 `xTaskNotify` |
| TRAP-NET-11 | `net_accept_locked` 拒 ESTABLISHED | FTP PASV 失败 | 允许 ESTABLISHED |
| 旧机理(未证实) | `recv_len` 分批读取归零 | 227 轮往返未复现 | 保守按一次读空执行;定向 TC 待补 |
| **TRAP-NET-12** | **`NET_SEND_BUF_SIZE` > DMA 限制** | **>1496B 发送静默失败** | **`net_send_locked` 按 `NET_DMA_MAX_PAYLOAD` 分块 ✅ 已修复** |
---
## 附录 B:通过/失败汇总
| 阶段 | TC ID | 优先级 | 类型 | 状态 |
|------|-------|--------|------|------|
| 阶段 1 | TC-NET-101 | P0 | 功能测试 | ✅ |
| 阶段 1 | TC-NET-102 | P1 | 功能测试 | ✅ 由阶段 1 第 6 项覆盖(`test/board/ch395f_test_task.c` 的 `ch395f_get_glob_int_status_all()` 双读断言);原条目在 2026-08-22 重构中删除、本汇总行未同步 → 2026-10-21 核对补注 |
| 阶段 2 | TC-NET-201 | P0 | 功能测试 | ✅ |
| 阶段 2 | TC-NET-202 | P0 | 恢复测试 | ✅ |
| 阶段 2 | TC-NET-203 | P1 | 负向测试 | ✅ |
| 阶段 3 | TC-NET-301 | P0 | 功能测试 | ✅ |
| 阶段 3 | TC-NET-302 | P1 | 合规测试 | ✅ |
| 阶段 4 | TC-NET-401 | P0 | 功能测试 | ✅ |
| 阶段 4 | TC-NET-402 | P1 | 功能测试 | ✅ |
| 阶段 5 | TC-NET-501 | P0 | 功能测试 | ✅ |
| 阶段 5 | TC-NET-502 | P1 | 合规测试 | ✅ |
| 阶段 6 | TC-NET-601 | P0 | 功能测试 | ✅ |
| 阶段 6 | TC-NET-602 | P0 | 功能测试 | ✅ |
| 阶段 6 | TC-NET-603 | P0 | 边界测试 | ✅ |
| 阶段 6 | TC-NET-604 | P0 | 边界测试 | ✅ |
| 阶段 6 | TC-NET-605 | P0 | 恢复测试 | ✅ |
| 阶段 6 | TC-NET-606 | P1 | 恢复测试 | ✅ |
| 阶段 7 | TC-NET-701 | P0 | 功能测试 | ✅ |
| 阶段 7 | TC-NET-702 | P0 | 恢复测试 | ✅ |
| 阶段 7 | TC-NET-703 | P1 | 负向测试 | ✅ |
| 阶段 7 | TC-NET-704 | P0 | 并发测试 | ✅ |
| 阶段 8 | TC-NET-801 | P0 | 文件发送 | ✅ |
| 阶段 8 | TC-NET-802 | P0 | 文件接收 | 🟡 |
| 阶段 8 | TC-NET-803 | P1 | 回显/压力 | 🟡 |
| 阶段 9 | TC-NET-901 | P0 | 功能测试 | 🟡 |
| 阶段 9 | TC-NET-902 | P1 | 负向测试 | 🟡 |
| 阶段 10 | TC-NET-1001 | P0 | 边界测试 | 🟡 |
| 阶段 10 | TC-NET-1002 | P0 | 边界测试 | 🟡 |
| 阶段 10 | TC-NET-1003 | P0 | 边界测试 | 🟡 |
| 阶段 10 | TC-NET-1004 | P1 | 负向测试 | 🟡 |
| 阶段 10 | TC-NET-1005 | P1 | 压力测试 | 🟡 |
| 阶段 10 | TC-NET-1006 | P1 | 稳定性测试 | 🟡 |
| 阶段 11 | TC-NET-1101 | P1 | 功能测试 | 🟡 |
| 阶段 15 | TC-NET-1501 | P0 | 状态回读/二段关闭 | ✅ |
| 阶段 15 | TC-NET-1502 | P0 | 复位状态机 | ✅ |
| 阶段 15 | TC-NET-1502b | P1 | LISTEN 态复位 | ✅ |
| 阶段 15 | TC-NET-1503 | P1 | 链路缓存/事件护栏 | ✅ |
✅ = 已实现并验证 | 🟡 = 已实现待验证 | 🟠 = 待实现
## 附录 C:设计原则覆盖
| 原则 | 覆盖用例 |
|------|----------|
| 单 socket 生命周期 | TC-NET-202, 203, 601, 605, 606, 701, 702, 703, 704, 801, 802, 901, 902, 1001~1006 |
| 中断即读即清 | TC-NET-101, 102, 501, 502, 901 |
| 消息队列线程安全 | TC-NET-301, 302, 401 |
| DMA 边界安全 | TC-NET-501, 502, 603, 604, 801, 802, 1001~1005 |
## 附录 D:CH395F 边界条件速查
| 操作 | 大小 | 行为 |
|------|------|------|
| `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` **自动分块**(受单 Socket 发送缓冲 1KB 约束;当前按 `NET_DMA_MAX_PAYLOAD=4096` 拆分,需实测) |
| `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` 之后**归零**,剩余数据**丢失** |