From bca6cc694cd7296d0de7cc13dd6210037391a862 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Sun, 11 Oct 2026 21:40:45 +0800 Subject: [PATCH] =?UTF-8?q?test(net):=20TC-NET-204=20=E5=AE=9A=E6=A1=88=20?= =?UTF-8?q?Trap=2007=E2=80=94=E2=80=94disconnect=E2=86=92close=20=E4=B8=8D?= =?UTF-8?q?=E5=8D=A1=E6=AD=BB=EF=BC=8C=E7=9C=9F=E5=9B=A0=E6=98=AF=20bit3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trap 07(2026-07-19 写成"2025-07")断言"disconnect→close 让 Socket 卡 FIN_WAIT_2 数分钟、因为 close 不发 RST"。新增定案用例 TC-NET-204(Phase 2 硬件层,同一 Socket、同一 PC 回显、FUN_PARA=0x08,三模式 × 5 轮 = 45 项判定 全过)做受控对照,实测推翻其机制说法、保留其做法: A close → 回 CLOSED 0/0/150/0/0 ms,重开 5/5 OK B disconnect → close → 全部 +0 ms,重开 5/5 OK("卡死"未出现) C disconnect(不 close)→ 10 s 内从未回 CLOSED,byte0 恒 0x05、byte1 0x00; 直接 open → 0x20 FAIL(5/5),补 close 后 +0 ms 三条推翻:① FIN_WAIT_2(06H) 全程未出现(15 轮 × 9 采样点),实测过渡态只有 TIME_WAIT(0AH);② 手册无 RST 语义(§5.35 只说 close 前自动断链),且主动发 FIN 先进 FIN_WAIT_1 才进 FIN_WAIT_2,原说法跳拍;③ 观测越过了 byte1 的语义 边界——§5.22 byte0 只有 00H/05H,官方 SDK 注释明确 byte1 仅在 byte0=OPEN 时 有意义,而当年代码判据用的是正确的 byte0,break 后照样打印 byte1,于是"已 CLOSED 下的残留读数"被读成"卡在某个 TCP 态"。 真因:FUN_PARA bit3 SOCKET_CLOSE=1(§5.52 ②)下产生 DISCONNECT 后芯片不关 Socket、不清缓冲,Socket 停在 0x05(OPEN)/0x00(TCP_CLOSED),此时 open_socket 必返 0x20 FAIL——即历史那条 Phase 2 open_socket: 0x20 / socket 未完全关闭。 拉回 CLOSED 的动作是 close,与 RST 无关。 git 佐证:ch395f_tcp_disconnect() 自首个提交起只有定义、零调用点(含全部分支 与 11 个悬空提交),"disconnect→close" 从未进入提交代码;当时驱动已轮询 GET_CMD_STATUS(4 s 上限),也不是"命令没等完成"。 做法保留,理由换成三条有支撑的:§5.35 close 已自动断链故 disconnect 多余; disconnect 制造的 DISCONNECT 事件在 bit3=1 下须我方消费/预清(Trap 14/17/22); 它把芯片推进 closing 过渡态、重开需沉降(Trap 09)。本轮 15 轮重开均未观察到 残留 DISCONNECT(0x10)——close 顺带清掉锁存,这才是"直接 close"安全的原因。 另消解 Trap 17 与 Trap 07 的旧矛盾:实测支持 Trap 17(close 走 FIN teardown、 0~150 ms 可见 TIME_WAIT),不支持 RST 说法。 文档:Trap_Records.md 加 Trap 07「定案更正」整节 + 改写「驱动关键点 → TCP 关闭 重连」速查清单(删 RST 说法、补 byte0/byte1 口径与 0x20 陷阱);Test_Guide.md 新增 TC-NET-204 用例表(前置含 tcp_server --timeout 900 这条坑:首连后连接 间隔 >60 s 服务器会自行退出)。 验证:测试构建 0 错 0 警、Phase 2 74/75(唯一 FAIL 是 TC-203 环境守卫,PC 服务 未及时关闭);宏已还原,产品构建 Code=196788 与 HEAD 一致(用例全在测试宏内、 产品零足迹),@test 闭环 PASS(戳匹配 + 回读无差异 + 锚点 x1 + 590 ms)。 pre-commit 全绿(test-cppcheck / doc-links / no-mojibake 均 Passed)。 --- docs/测试说明/CH395F_Test_Guide.md | 27 +++ docs/问题记录/CH395F_Trap_Records.md | 39 +++- test/board/ch395f_test_task.c | 287 +++++++++++++++++++++++++++ 3 files changed, 346 insertions(+), 7 deletions(-) diff --git a/docs/测试说明/CH395F_Test_Guide.md b/docs/测试说明/CH395F_Test_Guide.md index 2991ebd5..9ce44f8a 100644 --- a/docs/测试说明/CH395F_Test_Guide.md +++ b/docs/测试说明/CH395F_Test_Guide.md @@ -227,6 +227,33 @@ MCU: STM32F407ZGTx @ 168MHz, Keil MDK-ARM v5 (ARMCC) --- +### TC-NET-204(硬件层): TCP 关闭次序 A/B/C 对照(Trap 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 会自行退出,长轮次须放大)
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 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 07「定案更正」。 + +**覆盖原则**:DP-03(状态机边界:关闭次序 × Socket 复用) + +--- + ## 4. 阶段 3:UDP Server Echo(CH395F 作为 UDP Server) | 阶段 ID | TM-PHY-03 | diff --git a/docs/问题记录/CH395F_Trap_Records.md b/docs/问题记录/CH395F_Trap_Records.md index 428a34f8..7a337e46 100644 --- a/docs/问题记录/CH395F_Trap_Records.md +++ b/docs/问题记录/CH395F_Trap_Records.md @@ -154,11 +154,35 @@ Phase 2 测试,2025-07 `disconnect` 发送 FIN 将 Socket 推入 `FIN_WAIT_2`,此后 `close` 不再发 RST,必须等远端发 FIN 才能关闭。 ### 解决方案 -直接调用 `ch395f_close_socket()`,在 ESTABLISHED 或 CLOSE_WAIT 下会发 RST 立即终止。 -关闭后轮询 `ch395f_get_socket_status()` 等待 `sock=0x00` 再 `open_socket`。 +直接调用 `ch395f_close_socket()`(**做法保留**,原"发 RST"的说法见下方定案更正)。 +关闭后轮询 `ch395f_get_socket_status()` 的**第 1 字节**等待 `sock=0x00` 再 `open_socket`。 + +### 定案更正(2026-10-11):机制说法作废,实测 `disconnect → close` 不卡死 + +新增定案用例 **TC-NET-204**(`test/board/ch395f_test_task.c`,用例表见 `docs/测试说明/CH395F_Test_Guide.md`):同一 Socket、同一 PC 回显服务、`FUN_PARA=0x08`、硬件层独占,三模式各 5 轮(45 项判定全过): + +| 模式 | 关闭序列 | 回到 `SOCKET_CLOSED` 耗时(5 轮) | 重开+重连+回显 | +|---|---|---|---| +| A | `close` | 0 / 0 / 150 / 0 / 0 ms | 5/5 OK | +| B | `disconnect → close` | **0 / 0 / 0 / 0 / 0 ms** | 5/5 OK | +| C | `disconnect`(不 close) | **10 s 内从未回到**,byte0 恒 `0x05`、byte1 `0x00` | 直接 `open` → **`0x20` FAIL**(5/5);补 `close` 后 +0 ms 回 CLOSED,重开 5/5 OK | + +**三条推翻原根因**: + +1. **`FIN_WAIT_2`(0x06) 全程未出现**(15 轮 × 9 个采样点)。实测过渡态只有 `TIME_WAIT`(0x0A)(A 模式一轮在 +0/+50/+100 ms 可见,+150 ms 回 CLOSED)与 `TCP_CLOSED`(0x00)。 +2. **手册无 RST 语义**:§5.35 只说"关闭 Socket 前 CH395 会自动断开 TCP 连接",全文 RST 仅见于错误码 `CH395_ERR_RST` 与 RST/RSTI 引脚;且主动发 FIN 先进 `FIN_WAIT_1`(05H)、收到 ACK 才进 `FIN_WAIT_2`(06H)——"disconnect 推入 FIN_WAIT_2"本身就跳了一拍。 +3. **观测口径错在把 byte1 当独立事实**:§5.22 第 1 字节只有 `00H/05H` 两个取值,第 2 字节才是 TCP 态;官方 SDK `CH395CMDGetSocketStatus()` 注释明确"Second byte — TCP state, **meaningful only if** TCP mode **and the first byte is open**"。当年 Phase 2 收尾代码的判据用的是**正确的 byte0**(`st[0]==CH395F_SOCKET_CLOSED`),break 之后照样打印 byte1 → 一个"已 CLOSED 状态下的残留 TCP 态读数"被读成了"卡在某个 TCP 态"。 + +**真正的原因**(C 模式 5/5 复现):`FUN_PARA` bit3 `SOCKET_CLOSE=1`(§5.52 ②,`0x08`/`0x09` 都置了)下,产生 DISCONNECT 后**芯片不关 Socket、不清缓冲**,Socket 长期停在 `byte0=0x05(OPEN) / byte1=0x00(TCP_CLOSED)`;此时直接 `open_socket` 必返 **`0x20` FAIL**——正是 2026-07-19 测试指南问题表里那条 `Phase 2 open_socket: 0x20 (FAIL) / socket 未完全关闭`。**把 Socket 拉回 CLOSED 的动作是 `close`,与 RST 无关。** + +**git 侧佐证**:`ch395f_tcp_disconnect()` 自首个提交起**只有定义、零调用点**(含全部分支与 11 个悬空提交),"disconnect→close" 序列从未进入任何提交代码;本条"发现时间 2025-07"也与仓库不符(仓库首个提交 `b1d05091` = 2026-06-05,Phase 2 对应 2026-07-19 的 `17109d9b`→`7a8a97f4`→`a08d26bc`)。当时驱动已在轮询 `GET_CMD_STATUS`(4 s 上限),故也不是"命令没等完成"。 + +**做法为何仍然对**(换成三条有支撑的理由):① §5.35 `close` 已自动断链,`disconnect` 纯属多余;② `disconnect` 会**制造**一个 DISCONNECT 事件,bit3=1 下芯片不自动收尾,该事件必须被消费/预清,否则污染下一条连接(Trap 14 / 17 / 22 实证);③ 它把芯片推进 closing 过渡态,重开需要沉降(Trap 09)。本轮 15 轮重开均未观察到残留 `DISCONNECT`(0x10)——`close` 顺带清掉了锁存,这正是"直接 close"安全的原因。 + +**与 Trap 17 的张力已消解**:Trap 17 写"close 后芯片要等对端 FIN/ACK 才把 Socket 置 CLOSED(耗时数毫秒)",与本条原"close 发 RST 瞬间回 CLOSED"矛盾;实测支持 Trap 17 一侧(A 模式可见 `TIME_WAIT` 过渡、0~150 ms),不支持 RST 说法。 ### 发现时间 -Phase 2 TCP Client 测试,2025-07 +Phase 2 TCP Client 测试,2025-07(**日期有误**,实为 2026-07-19;定案更正与 TC-NET-204 实测 2026-10-11) --- @@ -205,10 +229,11 @@ CH395F 的 RECV 中断是电平触发的——只要接收缓冲区有数据就 - 不分配会导致发送数据为固定垃圾内容(`0x0028` 填充)、接收缓冲区无法存储数据 ### TCP 关闭重连 -- **直接调用 `ch395f_close_socket()`,不要先调 `ch395f_tcp_disconnect()`** -- ❌ `disconnect → close`:disconnect 发 FIN 推入 FIN_WAIT_2,close 不再发 RST,Socket 卡住数分钟 -- ✅ `close` 直接:在 ESTABLISHED 或 CLOSE_WAIT 下 close 发 RST 立即终止,瞬间回到 CLOSED -- 关闭后需轮询 `ch395f_get_socket_status()` 等待 `sock=0x00` 再 `open_socket` +- **直接调用 `ch395f_close_socket()`,不要先调 `ch395f_tcp_disconnect()`**(做法不变;机理口径已于 2026-10-11 由 TC-NET-204 实测更正,见 Trap 07「定案更正」) +- ✅ `close` 直接:§5.35 关闭前芯片**自动断链**,无需先 disconnect;实测 0~150 ms 回到 `SOCKET_CLOSED`,过渡态可见 `TIME_WAIT`(0x0A)——**不是 RST**(手册无 RST 语义) +- ❌ 只 `disconnect` 不 `close`:`FUN_PARA` bit3=1(§5.52 ②)下芯片**不自动关 Socket**,Socket 长期停在 `0x05(OPEN)/0x00(TCP_CLOSED)`,再 `open_socket` 必返 **`0x20` FAIL** +- ❌ `disconnect → close` 本身**不卡死**(实测 5/5 全部 +0 ms 回 CLOSED),只是多余:它制造一个 DISCONNECT 事件,而 bit3=1 下该事件须由我方消费/预清(Trap 14 / 17 / 22) +- 关闭后需轮询 `ch395f_get_socket_status()` 的**第 1 字节**等待 `sock=0x00` 再 `open_socket`;**第 2 字节(TCP 态)仅在 byte0=OPEN 时有意义**,别拿它当"卡住"的判据 ### SOCK_TIMEOUT 处理 - 长时间无数据时可能触发 `SINT_STAT_SOCK_TIMEOUT`,**不应视为致命错误**——记录日志后继续操作即可,不要因此关闭 Socket diff --git a/test/board/ch395f_test_task.c b/test/board/ch395f_test_task.c index 32bcbe09..cb2dcd4a 100644 --- a/test/board/ch395f_test_task.c +++ b/test/board/ch395f_test_task.c @@ -619,6 +619,289 @@ static void phase2_tc202(void) } } +/* =================================================================== + * TC-NET-204(硬件层):Trap 07 定案对照——三种关闭次序 A/B/C + * + * 背景:Trap 07(2025-07)断言"`disconnect → close` 会让 Socket 卡 `FIN_WAIT_2` + * 数分钟、无法重开,因为 close 不再发 RST"。该断言有两处可疑: + * ① 手册无 RST 语义——§5.35 只说"关闭 Socket 前 CH395 会自动断开 TCP 连接", + * 全文 RST 只出现在错误码 CH395_ERR_RST 与 RST/RSTI 引脚; + * ② "FIN_WAIT_2"是把 GET_SOCKET_STATUS 第 2 字节当独立事实读出来的—— + * §5.22 第 1 字节只有 00H(CLOSED)/05H(OPEN) 两个取值,第 2 字节才是 TCP 态, + * 而官方 SDK CH395CMDGetSocketStatus 注释明确:"Second byte — TCP state, + * **meaningful only if** TCP mode **and the first byte is open**"。 + * 当年 Phase 2 收尾正是"byte0 已 CLOSED 才 break,然后照样打印 byte1"。 + * 另:git 全历史里 ch395f_tcp_disconnect() 只有定义、零调用点,"disconnect→close" + * 序列从未进入过任何提交代码 → 现象来自一次未留痕的试验。 + * + * 受控对照(每模式 x PHASE204_LOOP_COUNT 轮,同一 Socket、同一 PC 回显服务): + * 模式 A:close only(现行做法) + * 模式 B:tcp_disconnect → close(Trap 07 声称会卡死的序列) + * 模式 C:tcp_disconnect only,不 close(检验 FUN_PARA bit3=1 下芯片会不会自己关) + * 观测量: + * ① 回到 SOCKET_CLOSED(byte0=00H) 的耗时;② 该瞬间 byte1 的读数(验证"无意义"口径) + * ③ 重开 + 重连 + 回显是否成功(Trap 07 的"无法重新打开") + * ④ 重开会话首次读中断是否命中上一条连接残留的 DISCONNECT(Trap 14 轴) + * ⚠ 关闭与重开之间**不主动清中断**(与 2025-07 现场一致),残留事件由重开后的 + * 首次中断读取现场暴露(读即清,无法窥视)。 + * =================================================================== */ +#ifndef PHASE204_LOOP_COUNT +#define PHASE204_LOOP_COUNT 5 +#endif +#ifndef PHASE204_LOCAL_PORT +#define PHASE204_LOCAL_PORT 50100 /* 避开 201/202/203 用过的本地端口 */ +#endif +#ifndef PHASE204_CLOSE_WAIT_MS +#define PHASE204_CLOSE_WAIT_MS 10000 /* 回到 CLOSED 的观察窗 */ +#endif + +/* 建立一次会话:open + connect + 等 CONNECT + 64B 回显比对 + * 入口参数:sock - Socket 号;lport - 本地端口;mode/round - 仅用于日志定位 + * 返回值:1=会话成功(含回显比对通过) 0=任一步失败 uint8_t + * 说明:等待 CONNECT 期间读到的其它中断(含上一条连接残留的 DISCONNECT) + * 现场打印,用于 Trap 14"滞后锁存"取证。 */ +static uint8_t tc204_session(uint8_t sock, uint16_t lport, uint8_t mode, uint8_t round) +{ + uint8_t tx[64]; + uint8_t rx[300]; + uint8_t ok = 0; + uint8_t r; + uint8_t match; + uint16_t rl; + uint16_t n; + uint32_t tick; + int32_t k; + + for (k = 0; k < 64; k++) { + tx[k] = (uint8_t)k; + } + + ch395f_set_sour_port(sock, lport); + r = ch395f_open_socket(sock); + if (r != CH395F_ERR_SUCCESS) { + DBG_INFO("TC204 m%u r%u open ret=0x%02X", (unsigned)mode, (unsigned)round, r); + return 0; + } + r = ch395f_tcp_connect(sock); + if (r != CH395F_ERR_SUCCESS) { + DBG_INFO("TC204 m%u r%u connect cmd ret=0x%02X", (unsigned)mode, (unsigned)round, r); + ch395f_close_socket(sock); + return 0; + } + + /* 等 CONNECT 中断(期间其它中断现场打印 = 残留事件取证点) */ + tick = HAL_GetTick(); + while (HAL_GetTick() - tick < PHASE2_TIMEOUT_MS) { + uint16_t gl = ch395f_get_glob_int_status_all(); + if ((gl & (1U << (sock + 4))) != 0U) { + uint8_t si = ch395f_get_sock_int_status(sock); + if ((si & CH395F_SINT_STAT_CONNECT) != 0U) { + ok = 1; + break; + } + if (si != 0U) { + DBG_INFO("TC204 m%u r%u stale-int=0x%02X at +%ums", + (unsigned)mode, (unsigned)round, si, + (unsigned)(HAL_GetTick() - tick)); + } + } + HAL_Delay(10); + } + if (!ok) { + DBG_INFO("TC204 m%u r%u no CONNECT within %ums", (unsigned)mode, (unsigned)round, + (unsigned)PHASE2_TIMEOUT_MS); + ch395f_close_socket(sock); + return 0; + } + HAL_Delay(100); + + /* 64B 发送 + SENBUF_FREE + 回显比对(证明这是一条真实连接) */ + ch395f_write_send_buf(sock, tx, 64); + tick = HAL_GetTick(); + ok = 0; + while (HAL_GetTick() - tick < 2000) { + uint16_t gl = ch395f_get_glob_int_status_all(); + if ((gl & (1U << (sock + 4))) != 0U) { + uint8_t si = ch395f_get_sock_int_status(sock); + if ((si & CH395F_SINT_STAT_SENBUF_FREE) != 0U) { + ok = 1; + break; + } + } + HAL_Delay(10); + } + if (!ok) { + DBG_INFO("TC204 m%u r%u no SENBUF_FREE", (unsigned)mode, (unsigned)round); + ch395f_close_socket(sock); + return 0; + } + + tick = HAL_GetTick(); + while (HAL_GetTick() - tick < 10000) { + uint16_t rlen = ch395f_get_recv_len(sock); + if (rlen > 0U) { + rl = (rlen > (uint16_t)sizeof(rx)) ? (uint16_t)sizeof(rx) : rlen; + ch395f_read_recv_buf(sock, rx, rl); + n = (rl < 64U) ? rl : 64U; + match = (rl >= 64U) ? 1U : 0U; + for (k = 0; k < (int32_t)n; k++) { + if (rx[k] != tx[k]) { + match = 0; + break; + } + } + if (!match) { + DBG_INFO("TC204 m%u r%u echo MISMATCH (rlen=%u)", (unsigned)mode, + (unsigned)round, (unsigned)rlen); + } + return match; + } + HAL_Delay(20); + } + DBG_INFO("TC204 m%u r%u echo NOT rcvd", (unsigned)mode, (unsigned)round); + ch395f_close_socket(sock); + return 0; +} + +/* 执行一种关闭次序,并测量"回到 SOCKET_CLOSED 的耗时" + * 入口参数:sock - Socket 号;mode - 0=A close / 1=B disconnect→close / 2=C disconnect only + * round - 日志定位 + * 返回值:1=观察窗内 byte0 回到 CLOSED 0=未回到 uint8_t + * 说明:关闭与轮询之间不读中断(读即清会抹掉 Trap 14 的滞后事件现场)。 */ +static uint8_t tc204_close_seq(uint8_t sock, uint8_t mode, uint8_t round) +{ + /* 状态采样里程碑(ms):抓 byte0/byte1 的演变 */ + static const uint16_t s_marks[] = { 0U, 50U, 100U, 250U, 500U, 1000U, 2000U, 5000U, 10000U }; + uint8_t st[2] = { 0, 0 }; + uint32_t tick; + uint32_t el = 0U; + uint8_t closed = 0U; + uint8_t mi = 0U; + uint8_t n_marks = (uint8_t)(sizeof(s_marks) / sizeof(s_marks[0])); + + if (mode >= 1U) { + uint8_t r = ch395f_tcp_disconnect(sock); + DBG_INFO("TC204 m%u r%u disconnect ret=0x%02X", (unsigned)mode, (unsigned)round, r); + } + if (mode <= 1U) { + uint8_t r = ch395f_close_socket(sock); + DBG_INFO("TC204 m%u r%u close ret=0x%02X", (unsigned)mode, (unsigned)round, r); + } + + tick = HAL_GetTick(); + while (el <= (uint32_t)PHASE204_CLOSE_WAIT_MS) { + ch395f_get_socket_status(sock, st); + el = (uint32_t)(HAL_GetTick() - tick); + while ((mi < n_marks) && (el >= s_marks[mi])) { + DBG_INFO("TC204 m%u r%u poll +%ums st=0x%02X/0x%02X", (unsigned)mode, + (unsigned)round, (unsigned)s_marks[mi], st[0], st[1]); + mi++; + } + if (st[0] == CH395F_SOCKET_CLOSED) { + closed = 1U; + break; + } + HAL_Delay(20); + } + /* 判定瞬间再读一次:byte0 已 CLOSED 时 byte1 按 §5.22/官方注释属"无意义"区 */ + ch395f_get_socket_status(sock, st); + if (closed != 0U) { + DBG_INFO("TC204 m%u r%u CLOSED at +%ums (byte0=0x%02X byte1=0x%02X)", + (unsigned)mode, (unsigned)round, (unsigned)el, st[0], st[1]); + } else { + DBG_INFO("TC204 m%u r%u NOT-CLOSED within %ums (byte0=0x%02X byte1=0x%02X)", + (unsigned)mode, (unsigned)round, (unsigned)PHASE204_CLOSE_WAIT_MS, + st[0], st[1]); + } + return closed; +} + +/* 轮末收尾:关闭本轮第二条连接并等回 CLOSED + * 说明:芯片不允许对已 OPEN 的 Socket 再 open(返回 0x20 FAIL)——不收尾则下一轮 + * 首轮 open 必失败并级联(2026-10-11 TC204 首跑实测踩过)。 */ +static void tc204_round_teardown(uint8_t sock) +{ + uint8_t st[2] = { 0, 0 }; + uint32_t tick; + + ch395f_close_socket(sock); + tick = HAL_GetTick(); + while (HAL_GetTick() - tick < 3000) { + ch395f_get_socket_status(sock, st); + if (st[0] == CH395F_SOCKET_CLOSED) { + break; + } + HAL_Delay(20); + } + HAL_Delay(200); +} + +/* 单轮:会话 → 关闭次序 → 重开会话 */ +static void phase2_tc204_once(uint8_t mode, uint8_t round) +{ + uint8_t sock = 0; + uint16_t lport = (uint16_t)(PHASE204_LOCAL_PORT + (uint16_t)(mode * 16U + round)); + uint8_t s1; + uint8_t s2; + uint8_t closed; + + DBG_INFO("TC204 === m%u r%u lport %u ===", (unsigned)mode, (unsigned)round, + (unsigned)lport); + + /* 1. 建立真实连接(有连接才谈得上 FIN_WAIT_*) */ + s1 = tc204_session(sock, lport, mode, round); + TEST_CHECK(s1, "TC204 m%u r%u session1 (connect+echo)", (unsigned)mode, (unsigned)round); + if (!s1) { + tc204_round_teardown(sock); + return; + } + + /* 2. 关闭次序 + 回到 CLOSED 的耗时 */ + closed = tc204_close_seq(sock, mode, round); + if (mode == 2U) { + /* 模式 C 的预期恰恰相反:bit3=1(§5.52 ②)下芯片不得自行关闭 Socket */ + TEST_CHECK(!closed, "TC204 m2 r%u stays OPEN after disconnect (FUN_PARA bit3)", + (unsigned)round); + if (!closed) { + /* Trap 07 的"无法重新打开"现场:不 close 直接 open,看芯片返回什么 */ + uint8_t r = ch395f_open_socket(sock); + DBG_INFO("TC204 m2 r%u open-without-close ret=0x%02X", (unsigned)round, r); + /* 收尾:显式 close 并测耗时(证明 close 才是把 Socket 拉回 CLOSED 的动作) */ + (void)tc204_close_seq(sock, 0U, round); + } + } else { + TEST_CHECK(closed, "TC204 m%u r%u back to CLOSED within %ums", (unsigned)mode, + (unsigned)round, (unsigned)PHASE204_CLOSE_WAIT_MS); + } + + /* 3. 重开 + 重连 + 回显(Trap 07 的"无法重新打开"判据;Trap 09 沉降 200ms) */ + HAL_Delay(200); + s2 = tc204_session(sock, (uint16_t)(lport + 64U), mode, round); + TEST_CHECK(s2, "TC204 m%u r%u reopen+reconnect+echo OK", (unsigned)mode, (unsigned)round); + tc204_round_teardown(sock); +} + +static void phase2_tc204(void) +{ + uint8_t sock = 0; + uint8_t ip_arr[4]; + uint8_t m; + uint8_t i; + + test_parse_ip(PHASE2_REMOTE_IP, ip_arr); + + /* 协议 / 目标在整组内保持不变,仅每轮换本地端口 */ + ch395f_set_proto_type(sock, CH395F_PROTO_TYPE_TCP); + ch395f_set_des_ip(sock, ip_arr); + ch395f_set_des_port(sock, PHASE2_REMOTE_PORT); + + for (m = 0; m < 3U; m++) { + for (i = 0; i < PHASE204_LOOP_COUNT; i++) { + phase2_tc204_once(m, i); + } + } +} + /* TC-NET-203(硬件层):TCP Client 连接超时(负向测试,P1) * * 前置环境:PC 未在 192.168.1.2:8081 提供服务(与 TC-201/202 互斥, @@ -775,6 +1058,10 @@ static void phase2_run(void) DBG_INFO("--- TC-NET-202: close/reconnect x%d ---", PHASE2_LOOP_COUNT); phase2_tc202(); + DBG_INFO("--- TC-NET-204: close A/B/C control (Trap 07) x3 modes x%d ---", + PHASE204_LOOP_COUNT); + phase2_tc204(); + DBG_INFO("--- TC-NET-203: negative test, stop PC 8081 server first ---"); phase2_tc203();