test(net): TC-NET-204 定案 Trap 07——disconnect→close 不卡死,真因是 bit3

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)。
This commit is contained in:
2026-10-11 21:40:45 +08:00
parent c5227291bd
commit bca6cc694c
3 changed files with 346 additions and 7 deletions
+287
View File
@@ -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();