diff --git a/docs/子系统设计/网络Socket监听生命周期设计说明.md b/docs/子系统设计/网络Socket监听生命周期设计说明.md index 7f1fef92..804aa36d 100644 --- a/docs/子系统设计/网络Socket监听生命周期设计说明.md +++ b/docs/子系统设计/网络Socket监听生命周期设计说明.md @@ -83,7 +83,7 @@ B10 探针看到的"孤儿芯片态"不是芯片异常,而是本仓这个配 实测现场(Trap 25 三次定案的判别探针):`st=0x05/0x00` 且 `recv_len=1027`—— Socket 仍 OPEN、芯片 TCP 已 CLOSED、**RX 缓冲里 STOR 载荷一个不少**。这正是 bit3=1 按规格 -保留现场,不是芯片异常(`docs/问题记录/CH395F_Trap_Records.md` §877-878)。 +保留现场,不是芯片异常(`docs/问题记录/CH395F_Trap_Records.md` Trap 25「定案二」)。 **② bit3=0 的代价是「硬件保证的数据丢失」,且不可在软件层补救。** 本产品最典型的流量形态是「发完即 FIN」:ftplib `storbinary` 语义 = `sendall` 发完立即关 diff --git a/docs/问题记录/CH395F_Trap_Records.md b/docs/问题记录/CH395F_Trap_Records.md index 70fda55a..45ce90a5 100644 --- a/docs/问题记录/CH395F_Trap_Records.md +++ b/docs/问题记录/CH395F_Trap_Records.md @@ -2,957 +2,335 @@ 记录开发过程中遇到的所有坑、根因、解决方案,避免重复踩坑。 ---- +> **2026-10-13 重组审计**:合并同根因条目(14+17、13+21)、删除已被推翻的误判叙述(只留一行误判存档)、 +> 按主题分章并加索引;条目格式统一为 `## Trap NN · 短标题` + 固定小节(状态/现象/根因(定案)/修复/回归判据/教训, +> 视情况取舍)。**编号永不重排**——代码注释大量引用 `Trap NN`,合并条目标题保留双编号; +> Trap 19 从未存在(悬空引用登记在第六章待修清单)。 +> 状态图例:✅ 已修复定案 · ⏳ 待修 · ↩️ 已撤回(非缺陷) · — 从未存在。 -## Trap 01:Socket 4~7 自动分配不到 +## 索引 -### 现象 -多连接模式下最多接受 3 个客户端(Socket 1~3),Socket 4~7 的 CONNECT 中断从不触发,新客户端 SYN 被静默丢弃。 - -### 根因 -`net_listen()` 中数据 Socket 的配置顺序错误。CH395F 在 `TCP_LISTEN` 时刻**一次性扫描**当前所有可用 Socket 并建立内部查找表。如果数据 Socket 4~7 在 `TCP_LISTEN` **之后**才配置缓冲区/协议/端口,自动分配逻辑不认识它们。 - -### 正确顺序 -1. 先配置数据 Socket 1~7:`SET_SEND_BUF → SET_RECV_BUF → SET_SOUR_PORT → SET_PROTO_TYPE_TCP` -2. 最后配置监听 Socket 0:`SET_SEND_BUF → SET_RECV_BUF → SET_PROTO_TYPE_TCP → SET_SOUR_PORT → OPEN_SOCKET → TCP_LISTEN` - -### 注意事项 -- 数据 Socket 不调 `OPEN_SOCKET`,由 CH395F 在连接到达时自动打开 -- DISCONNECT 后 CH395F 自动关闭 Socket,但保留协议/端口/缓冲区配置,可被再次分配 -- `docs/芯片手册/CH395F.md` §9.2.6 要求数据 Socket **先设源端口再设协议类型**(与监听 Socket 顺序相反) - -### 对应文件 -`Drivers/BSP/NET/net_socket.c` → `net_listen()` - -### 发现时间 -Phase 8 压力测试,2025-07 - ---- - -## Trap 02:数据 Socket 缓冲区重叠 - -### 现象 -多连接模式下多个 Socket 数据互相覆盖,收到错误数据或客户端挂死。 - -### 根因 -CH395F 默认只为 Socket 0~3 各分配独立收发缓冲(共 48 块 × 512B 用满,见`docs/芯片手册/CH395F.md` §5.45),Socket 4~7 为零块。`net_listen()` 未对 Socket 4~7 显式分配独立缓冲区块,后续代码误用相同块号(如 28~31)导致数据重叠。 - -### 解决方案 -每个数据 Socket 独占 4 个缓冲区块(2 发 + 2 收),按 `ds * 4` 基址分配,确保不重叠: - -| Socket | 发送块 | 接收块 | -|--------|--------|--------| -| 1 | 4~5 | 6~7 | -| 2 | 8~9 | 10~11 | -| 3 | 12~13 | 14~15 | -| 4 | 16~17 | 18~19 | -| 5 | 20~21 | 22~23 | -| 6 | 24~25 | 26~27 | -| 7 | 28~29 | 30~31 | - -### 对应文件 -`Drivers/BSP/NET/net_socket.c` → `net_listen()` - -### 发现时间 -Phase 8 压力测试,2025-07 - ---- - -## Trap 03:DHCP 包 xid 偏移错误 - -### 现象 -Python DHCP Server 收到 DISCOVER 包后无法匹配 Offer/Ack 的 xid,握手失败。 - -### 根因 -错误使用了 `data[232:236]` 作为 xid(Wireshark 某旧版显示误导),实际 DHCP 标准偏移为 `data[4:8]`。 - -### 正确偏移 -- xid:`data[4:8]` -- msg_type:`data[242]` -- client_mac:`data[28:34]` -- magic cookie:`data[236:240]` - -### 对应文件 -`test/host/ch395f_socket_test.py` → DHCP server 模式 - -### 发现时间 -Phase 6, 2025-07 - ---- - -## Trap 04:UDP 发送后必须等待 SENDBUF_FREE - -### 现象 -UDP 发送后立即执行接收操作,接收长度始终为 0 或数据错误。 - -### 根因 -CH395F 手册要求每次 `WRITE_SEND_BUF` 后必须等待 `SINT_STAT_SENBUF_FREE` 中断,否则下次写入或接收操作可能失败。 - -### 解决方案 -每次 `ch395f_write_send_buf()` 后轮询 `ch395f_get_sock_int_status()` 检查 `SINT_STAT_SENBUF_FREE` 标志。 - -### 对应文件 -`Drivers/BSP/CH395F/ch395f.c` / test code in `ch395f_test.c` - -### 发现时间 -Phase 6 广播宣告模式,2025-07 - ---- - -## Trap 05:CH395F 与 RTL8305NBI 自动协商不兼容 - -### 现象 -`ch395f_get_phy_status()` 始终返回 `PHY_DISCONN`,链路无法建立。 - -### 根因 -CH395F PHY 与 RTL8305NBI-CG 直连时自动协商失败。 - -### 解决方案 -初始化协议栈后强制设为 100M 全双工: -```c -ch395f_set_phy(CH395F_PHY_100M_FULL); -``` - -### 对应文件 -`Drivers/BSP/CH395F/ch395f.c` - -### 发现时间 -项目初期硬件联调 - ---- - -## Trap 06:TCP KeepAlive 参数必须为 500ms 倍数 - -### 现象 -设置 KeepAlive 后 TCP 连接几秒内 TIMEOUT 断开。 - -### 根因 -CH395F 内部定时器以 500ms 为基准单位,传入非 500ms 倍数的值导致未定义行为。 -且 `IDLE` 必须 > `INTVL`。 - -### 正确值 -```c -ch395f_set_keepalive_idle(60000); // 60s(500 倍数) -ch395f_set_keepalive_intvl(5000); // 5s(500 倍数) -ch395f_set_keepalive_cnt(3); // 3 次 -``` - -### 对应文件 -`Drivers/BSP/NET/net_socket.c` → `net_init()` - -### 发现时间 -Phase 2 测试,2025-07 - ---- - -## Trap 07:TCP 关闭重连直接 close,不要 disconnect - -### 现象 -`disconnect → close` 后 Socket 卡在 `FIN_WAIT_2` 数分钟,无法重新打开。 - -### 根因 -`disconnect` 发送 FIN 将 Socket 推入 `FIN_WAIT_2`,此后 `close` 不再发 RST,必须等远端发 FIN 才能关闭。 - -### 解决方案 -直接调用 `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. **`close` 无 RST 语义**:§5.35 只说"关闭 Socket 前 CH395 会自动断开 TCP 连接"。手册中芯片主动发 RST 只出现在 §5.29(监听 Socket 已有连接时再来连接 →"发送 TCP RESET")与 §9.2.6(多连接找不到可用数据 Socket →"将连接复位")两处,均与 `close`/`disconnect` 无关;此外仅有错误码 `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()` 自首个提交起**只有定义、零调用点**(`git log --all -S` 在 `NET/App/test` 仅命中本仓 `bca6cc6` 一次),"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 说法。 - -### 补充(2026-10-13,E-02 台架):`close` 无 RST 的**边界**——对端内核存活时芯片停 `FIN_WAIT_2` 长尾 - -上条定案("`close` 无 RST 语义")在 E-02 收口实验里暴露出一个必须登记的边界,**不推翻定案、补全其适用条件**: - -- **对端配合**(收到 FIN 后其内核 close):芯片走 `TIME_WAIT` 过渡 ~150 ms → byte0 回 `00H`,reconciler 形态④ relisten → 新连接接入(`e02_probe` 更新后 queue=PASS / listener=PASS,对时"wall clock updated"落地)。 -- **对端内核存活但不 close**(挂起但连接仍活的客户端):本层发 FIN 后芯片停在 **`FIN_WAIT_2`(实测 byte0=0x05 / byte1=0x06)长尾**——没有 RST 就拆不掉,**监听恢复受限于对端 close**。这正是 E-02 场景(timeSync 30s 预算耗尽后 PC 侧 socket 仍活)的芯片侧形态。 -- **不配合场景的兜底** = 芯片自身 TIMEOUT / KeepAlive 拆僵尸会话(E13 定案:判死窗 30~90s,见 `docs/网络/N34-自愈审计/实验方案.md` E13)——**不是**应用可以加速的路径。 -- **应用侧出口 = `net_drop_conn(fd)`**(N34 阶段 3,E-02 收口):netTask 侧走驱动 `sock_reset` **QUARANTINE**(FIN + 排渣 + 排空事件 + 等 byte0=00H,400ms 底)+ 本层收口 CLOSED + `auto_relisten` 即时重监听;**保监听不毁**(单连接模型不得 `net_close` 监听 fd)。对端不 close 时复位机等在 `net_poll` 步进里(`ch395f_sock_reset_poll`),对端 FIN 到达即自动走完——**不搁浅**。 - -### 发现时间 -Phase 2 TCP Client 测试,2025-07(**日期有误**,实为 2026-07-19;定案更正与 TC-NET-204 实测 2026-10-11) +| 01 | Socket 配置顺序 | ✅ | `TCP_LISTEN` 时刻一次性扫描:先配数据 Socket 1~7,最后配监听 Socket 0 | +| 02 | Socket 缓冲显式分配 | ✅ | 48 块×512B 几何,Socket 4~7 默认零块,必须显式分配否则数据重叠/中断不触发 | +| 03 | DHCP xid 偏移 | ✅ | xid=`data[4:8]`(Wireshark 旧版显示误导) | +| 04 | UDP 发送后等 SENBUF_FREE | ✅ | 不等则下次写入被静默丢弃 | +| 05 | PHY 自动协商不兼容 | ✅ | 与 RTL8305NBI 直连协商失败,强制 100M 全双工 | +| 06 | KeepAlive 参数 500ms 倍数 | ✅ | 非 500ms 倍数 = 定时器未定义行为;IDLE>INTVL | +| 07 | TCP 关闭重连 | ✅ | 直接 `close`,不要先 `disconnect`;`close` 无 RST(E-02 边界:对端不配合则 `FIN_WAIT_2` 长尾,兜底靠查岗/`net_drop_conn`) | +| 08 | RECV 中断电平触发 | ✅ | 缓冲有数据就一直拉低,`net_poll` 每轮只处理一批 | +| 09 | 沉降窗(link up / close 后) | ✅ | open 失败重试 + `net_sock_reset_sync` 确定性排渣;LISTEN 态关闭是两段 CLOSE | +| 10 | 消息队列值拷贝 | ✅ | 结果必须走 `xTaskNotify` 通知值回传 | +| 11 | accept 拒绝 ESTABLISHED | ✅ | standalone_accept 后状态已 ESTABLISHED,入口检查须放行该组合 | +| 12 | FatFS 路径与 8.3 短名 | ✅ | 真约束=`FF_USE_LFN=0` 只认 8.3;本仓用 `"0:"` 卷前缀(原 #2/#4 根因已按 ff.c 更正) | +| 13/21 | 「recv_len 归零 / 接收冻结」家族 | ↩️ | 合并结案:非缺陷,全部为台架假象;机理与 §5.34 相左已撤回 | +| 14/17 | 残留 DISCONNECT 污染下一连接 | ✅ | 合并:滞后锁存 + SINT 粘滞 → 预清 + 反核实 + close 后排空 | +| 15 | DHCP 后 IP 寄存器提交滞后 | ✅ | 状态位≠寄存器生效:连续 3 次一致才采信 | +| 16 | IP/端口字节序 | ✅ | IP 四字节数组=点分顺序;16 位端口=低字节在前,两套口径 | +| 18 | FUN_PARA bit0 迷思 | ✅ | bit0 保留必须为 0(`0x08`);当年真正起作用的是缓冲显式分配 | +| 19 | — | — | 从未存在(`Test_Guide.md:878` 悬空引用待清) | +| 20 | 单次写超发送缓冲(写穿) | ✅ | 上限=本仓 2 块发送缓冲 1KB(`NET_SEND_CHUNK_MAX`),门控是 SENBUF_FREE | +| 22 | 快速复连丢中断 | ✅ | 芯片丢 CONNECT/DISCONNECT → 周期对账 `net_reconcile_tick`(reconciler 之源) | +| 23 | 上传中止后首笔复连被截断 | ✅ | 残渣投递给下一条连接 → 两级沉降/隔离 + 异常释放回 450 删截断文件 | +| 24 | SET_MAC 顺序 | ✅ | §9.2.1:SET_MAC 必须在 `INIT_CH395` **之前**(INIT 从 EEPROM 加载) | +| 25 | FTP 数据连接三连故障 | ✅ | 四重定案:丢中断窗口探针+幽灵甄别 / closed-with-data / 背靠背小段毒化 TX / 全局寄存器读即清(B3) | --- -## Trap 08:RECV 中断电平触发,避免无限循环 +# 第一章 初始化与配置 -### 现象 -`net_poll()` 中 RECV 中断无限触发,主循环(echo 等)永远得不到执行。 +## Trap 01 · Socket 配置顺序:先数据 Socket 后监听 Socket -### 根因 -CH395F 的 RECV 中断是电平触发的——只要接收缓冲区有数据就保持 `INT#` 低电平。 -`do { ... } while(int_status != 0)` 会无限循环。 +- **状态**:✅ 已定案(多连接模式虽已随 `319d00e3` 删除,**顺序规则仍适用于现行 `net_init`**——`net_socket.c` 注释「Trap 01:先配数据 Socket 1~7,再配监听 Socket 0」) +- **现象**:多连接模式下最多接受 3 个客户端,Socket 4~7 的 CONNECT 中断从不触发,SYN 被静默丢弃。 +- **根因(定案)**:CH395F 在 `TCP_LISTEN` 时刻**一次性扫描**当前所有可用 Socket 建立内部查找表;数据 Socket 若在 `TCP_LISTEN` 之后才配置缓冲区/协议/端口,自动分配逻辑不认识它们。 +- **修复**:正确顺序——① 数据 Socket 1~7:`SET_SEND_BUF → SET_RECV_BUF → SET_SOUR_PORT → SET_PROTO_TYPE_TCP`(不调 `OPEN_SOCKET`,连接到达时芯片自动打开);② 监听 Socket 0:`SET_SEND_BUF → SET_RECV_BUF → SET_PROTO_TYPE_TCP → SET_SOUR_PORT → OPEN_SOCKET → TCP_LISTEN`。 +- **教训**:`docs/芯片手册/CH395F.md` §9.2.6 要求数据 Socket **先设源端口再设协议类型**(与监听 Socket 顺序相反)。DISCONNECT 后芯片自动关闭 Socket 但保留配置,可再分配。 +- **发现时间**:Phase 8 压力测试(原记 2025-07,早于仓库首提交 `b1d05091` 2026-06-05,日期不可考)。 -### 解决方案 -`net_poll()` 使用 `do { ... } while(0)` 每次只处理一批中断,由主循环负责读取数据。 +## Trap 02 · Socket 缓冲区必须显式分配且不重叠 -### 对应文件 -`Drivers/BSP/NET/net_socket.c` → `net_poll()` +- **状态**:✅ 已定案(现行 `net_init` 固定布局 8×(4收+2发)=48 块即用满) +- **现象**:多 Socket 数据互相覆盖、收到错误数据或客户端挂死;未分配缓冲的 Socket 发送为固定垃圾内容(`0x0028` 填充)、接收存不进数据、CONNECT/RECV 中断不触发。 +- **根因(定案)**:芯片默认只为 Socket 0~3 分配独立收发缓冲(48 块×512B 用满,`docs/芯片手册/CH395F.md` §5.45),Socket 4~7 为零块;未显式分配或误用相同块号导致重叠。 +- **修复**:每个数据 Socket 独占块(历史多连接布局 2发+2收/Socket;现行 `net_init` 固定 6 块/Socket=4收+2发,与 MSS 解耦,见 Trap 21 解耦注)。**必须在 `open_socket` 之前**调 `ch395f_set_send_buf/set_recv_buf`。 +- **教训**:缓冲几何以 §5.45 的 48×512B 为准(此前文档"32×1KB"记录有误,Phase 7 TC704 40/40 实测确认块 0~47 全部有效)。 +- **发现时间**:Phase 8 压力测试(日期同 Trap 01,不可考)。 + +## Trap 05 · CH395F 与 RTL8305NBI 自动协商不兼容 + +- **状态**:✅ 已修复 +- **现象**:`ch395f_get_phy_status()` 恒返回 `PHY_DISCONN`,链路无法建立。 +- **根因(定案)**:CH395F PHY 与 RTL8305NBI-CG 直连时自动协商失败(板级组合)。 +- **修复**:初始化协议栈后强制 `ch395f_set_phy(CH395F_PHY_100M_FULL)`。 +- **发现时间**:项目初期硬件联调。 + +## Trap 06 · KeepAlive 参数必须为 500ms 倍数 + +- **状态**:✅ 已定案(产品参数 60s/5s/3 的转正过程见附录 A「KeepAlive 定案链」) +- **现象**:设置 KeepAlive 后 TCP 连接几秒内 TIMEOUT 断开。 +- **根因(定案)**:芯片内部定时器以 500ms 为基准,非 500ms 倍数 = 未定义行为;且 `IDLE` 必须 > `INTVL`。 +- **修复**:`idle=60000 / intvl=5000 / cnt=3`(均 500 倍数)。 +- **发现时间**:Phase 2 测试(原记 2025-07 不可考;KeepAlive 实验实际对应阶段 11)。 + +## Trap 18 · FUN_PARA bit0 迷思:bit0 保留必须为 0 + +- **状态**:✅ 已定案(`net_init` 用 `ch395f_set_fun_para(0x08)`) +- **误判存档**:本节曾断言"bit0=单/多 Socket 模式,必须 0x09 才能用 Socket 1~7"——**无任何出处,已撤回**;当年让 Socket 1~7 真正可用的是缓冲显式分配(Trap 02),不是 bit0。 +- **根因(定案)**: + 1. 官方手册 V1C/V1E/V2.2/V2.3 的 §5.52 位表中 bit0 一律「保留,必须为0」;官方 SDK `CH395INC.H` 无 bit0 宏; + 2. 官方例程直接反例:`EXAM5_TCP_Server` 传 `0x02`(bit0=0)却给 Socket 0~7 全配缓冲;官方 FTP 例程直接传 `0x08`;全 EVT 包无一处 0x09; + 3. 8 Socket 是芯片原生能力(`_SN` 索引寻址),真正的门槛是 §5.45 缓冲分配; + 4. 概念讹传源:bit1 `FUN_PARA_FLAG_TCP_SERVER` 的"服务器多连接"指**一个服务器能接几个客户端**,不是"能用几个 Socket"。 +- **为什么 0x09 当年"看起来有效"**:芯片忽略保留位;TC704 40/40 与 bit0 结论出自同一提交,属自证,无"0x08 下不通过"的对照。留着 bit0=1 的代价:违反保留位规定,后续固件版本一旦启用该位现网设备静默失效。 +- **实测闭环(2026-10-11,N-31 已销项)**:`FUN_PARA=0x08` 下 Phase 7 **34/34 全通过**(`PHASE7_MULTI_COUNT` 降到 5:E-23 未修,8 fd 减 3 常驻监听),并发回显实测落在 sock2/4/5/6/7 → Socket 4~7 在 bit0=0 下完全可用。 +- **台架前置(本轮踩到)**:① PC 网卡落在 Public 防火墙配置文件 → 入站 8081 被静默丢弃,TC701/702/704 全部 `CONNECT TIMEOUT`,极易误判成"0x08 毁了 client";② 上一轮遗留 `tcp_server` 进程占着 8081,新服务器绑不上端口却不报错。**判"芯片行为"前先确认对端真的收到了 SYN**(Trap 25 总纲铁律的又一次命中)。 +- **发现时间**:阶段 7 扩展 8 Socket 测试,2026-08-25;定案更正 2026-10-11。 + +## Trap 24 · SET_MAC 必须在 INIT_CH395 之前 + +- **状态**:✅ 已修复(纯顺序调整,无 RAM 变化) +- **现象**:UI 配置的 MAC 与设备实际在线 MAC 不一致("滞后一次上电")。静态审查发现,手册条文确证。 +- **根因(定案)**:`docs/芯片手册/CH395F.md` §9.2.1 初始化顺序 = `SET_MAC → SET_IP/GWIP/MASK → INIT_CH395`;§5.7 `SET_MAC_ADDR` 写芯片内部 EEPROM,§5.13 `INIT_CH395` 从 EEPROM **加载** MAC——SET_MAC 放 INIT 之后则本次上电不生效。原码顺序相反;`ch395f.c` 原注释把四个 SET 标为"芯片已初始化(之后)"加剧误排。 +- **修复**:`net_init()` 把 `ch395f_set_mac_addr()` 移到 `ch395f_init()` 之前;四个 SET 函数注释统一改"必须在 `ch395f_init()` 之前调用";`net_socket.h` 同步。 +- **发现时间**:2026-10-04(静态审查项 P1-7)。 --- -## 附:CH395F 驱动关键点 +# 第二章 中断与事件一致性 -### 初始化顺序 -- 必须按手册9.2.1节顺序:`SET_MAC → SET_IP/GWIP/MASK → INIT_CH395 → SET_PHY`(**违反此序曾导致 MAC 不生效,见 Trap 24**) -- **IP/网关/掩码必须在 `INIT_CH395` 之前设置**,INIT 会读取并锁定当前寄存器值到协议栈 -- **`SET_PHY` 必须在 `INIT_CH395` 之后**,复位 MAC/PHY 建立物理链路,不影响已锁定的协议栈参数 -- **`CMD_PING_ENABLE` 不需要显式调用**,INIT 后默认可用 +## Trap 08 · RECV 中断是电平触发 -### SPI 通信 -- 每次 SPI 事务需调用 `ch395f_spi_begin()` / `ch395f_spi_end()` 包裹 -- **大数据量收发使用 SPI2 DMA**:`ch395f_write_send_buf()` 和 `ch395f_read_recv_buf()` 已改造为 DMA 批量传输(DMA1_Stream3 RX, DMA1_Stream4 TX),命令和配置操作仍使用逐字节轮询 -- ⚠ **TSL 规格偏离(N34 E12,2026-10-13 登记)**:手册 §7.4 要求缓存区命令在 - "长度与数据之间"留 TSL(读 0.4us / 写 0.2us;其余字节间无需间隔;非缓存区命令 - 另有 TSC=0.6us / TSD=0.3us)。本仓两个缓存区函数把命令头与数据打包进**同一 - DMA 突发**(CS 全程拉低)→ 该间隔为 **0**,属规格偏离;其余命令逐字节传输, - TSC/TSD 有大余量。SPI2 = 5.25MHz(分频 8 @42MHz,单字节 1.52us)。 - **实测未发现危害**:460 轮 / ≈58MB 双向 SHA-256 全量比对仅 1 次未复现异常, - 且"两段+TSL"变体(+28B,吞吐无回退)与现状无差异 → 依 Trap 25 铁律暂不改产品 - 路径,偏离与证据登记于此、变体留作待用补丁。完整 A/B 与方法学见 - `docs/网络/N34-自愈审计/实验方案.md` E12。 -- DMA 缓冲区:`s_spi2_dma_tx_buf/rx_buf[CH395F_SPI_DMA_BUF_SIZE]` = 接收缓冲 2048 + 4 = **2052**(`ch395f.h:148`),4 字节对齐;单次有效载荷上限 2048(历史值 1500 / 4100 均已作废,见 Trap 13 复核注) +- **状态**:✅ 已定案 +- **现象**:`net_poll()` 中 RECV 中断无限触发,主循环永远得不到执行。 +- **根因(定案)**:接收缓冲区有数据期间 `INT#` 保持低电平,`do{...}while(int_status!=0)` 必然死循环。 +- **修复**:`net_poll()` 每轮只处理一批中断,由主循环负责读数据。 +- **推论**:RECV 是条件位(数据读空即落),所以 Trap 25 B3 的全量读清对 RECV 可下轮补、对其余位是永久销毁。 -### TCP 参数配置(必须在 INIT_CH395 之前) -- **重传参数**:`SET_RETRAN_COUNT`(默认12次,最大20)和 `SET_RETRAN_PERIOD`(默认500ms,最大1000ms),总重传时间 = 次数 × 周期 -- **KeepAlive 参数必须为 500ms 的倍数**:`IDLE`(默认20000ms)、`INTVL`(默认15000ms),且 IDLE > INTVL;传入非 500 倍数的值导致 CH395F 内部定时器异常 -- **KeepAlive 默认关闭**:需在 `SINT_STAT_CONNECT` 后调用 `ch395f_set_keepalive_enable(sock, 1)` 启用 -- ⚠ **E11 实测与转正(2026-10-13)**:此前 `ch395f_set_keepalive_enable` 全仓仅 - 客户端分支(`CONNECTING`)调用,而 `App/` 无任何 `net_connect()` → 产品上 - KeepAlive 实际全程关闭;服务器侧调用 2026-10-12 曾加后撤回(理由仅覆盖 - 物理链路失效)。E11 用"禁用 PC 网卡"注入"链路在、对端静默"(防火墙规则对 - 已建立连接无效,见实验方案 E11 方法学),实测服务器侧开启后:生产参数 - 60s/5s/3 → **判死 ≈30.0s**(早于手册公式 75s,以实测为准);短参数 6s/2s/2 - → ≈5.1s(证明参数生效);静默但存活的对端**不被误杀**(90s 空闲 KEEPLIVE - 回显正常)。消费链路现成:TIM_OUT → `handle_timeout_event` QUARANTINE → - pump relisten → 端口恢复。**2026-10-13 转正**:`handle_connect_event` - LISTENING 分支按 §5.56 开启,Phase 11 测试重新纳入回归(TC-NET-1101 步骤 - 已更新死对端注入法)。完整记录见 `docs/网络/N34-自愈审计/实验方案.md` E11。 +## Trap 14/17 · 上一条连接的残留 DISCONNECT 污染下一条连接(合并条目) -### Socket 4~7 缓冲区 -- CH395F 默认只给 Socket 0~3 分配独立收发缓冲(共 48 块 × 512B 用满),Socket 4~7 为零块 -- 多连接模式下使用 Socket 4~7 时,**必须在 `open_socket` 之前**显式分配: - - `ch395f_set_send_buf(sock, start_block, count)` - - `ch395f_set_recv_buf(sock, start_block, count)` -- 不分配会导致发送数据为固定垃圾内容(`0x0028` 填充)、接收缓冲区无法存储数据 +- **状态**:✅ 已定案(两级预清 + 排空为 Trap 23/25 排渣契约的前身) +- **合并说明**:Trap 14(2026-09,硬件层 TC-203 观察)与 Trap 17(2026-08-25,生产 `net_close_locked`)为**同一根因的两层表现**,2026-10-13 合并;代码注释常以「Trap 14/17」连用。 +- **现象**: + - 硬件层(14):`close_socket` → 已确认 CLOSED → 新会话第一次读 `GET_INT_STATUS_SN` 读到**上一会话遗留的 DISCONNECT**(0ms 即命中),负向判定/状态机误判(TC-203 三轮重连后 PC 握手实际成功、固件判"未连接")。 + - 生产层(17):单连接模式 close 后立即复用同一 Socket 重连,每次都 `openOK` 但紧接着 `DISCONNECTED, releasing` 秒断——旧连接的 DISCONNECT 在新连接 ESTABLISHED 之后才到达,`handle_disconnect_event` 把新连接误判断开。 +- **根因(定案)**:① `GET_SOCKET_STATUS` 是纯查询**不清中断**;② DISCONNECT 可能在 CLOSED 达成同时或之后才锁存;③ SINT **粘滞到被读**(close/open 都不清,Trap 25 B3 实证)→ 无人消费则跨会话存活。 +- **修复**: + 1. **预清**:新会话 OPEN 前读一次全局 + 本 Socket SINT 丢弃陈旧事件(`phase2_tc203()` 双保险;生产 `net_connect_locked` open 后清本 Socket 中断); + 2. **排空**:`net_close_locked` 在 `close_socket()` 后轮询 `GET_SOCKET_STATUS` 至 `SOCKET_CLOSED`(上限 500ms,实测 LAN 几毫秒)再读空本 Socket 中断; + 3. **反核实**:依据中断做结论前用 `GET_SOCKET_STATUS` 验证实际状态一致(负向事件与 ESTABLISHED 矛盾时以状态为准)。 +- **措辞更正(2026-10-13)**:原 Trap 17 写"芯片要等**对端** FIN/ACK 才置 CLOSED"——按 Trap 07 实测应为:自身 FIN 的 ACK + 本地 `TIME_WAIT` 过渡,0~150 ms。 +- **注意**:B3 修复(2026-10-12)后预清只准读**本 Socket** SINT,不得再全量读 8 个(见第六章 B3)。 +- **发现时间**:17=阶段 7 上板实测 2026-08-25;14=2026-09 TC-203 取证。 -### TCP 关闭重连 -- **直接调用 `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 时有意义**,别拿它当"卡住"的判据 +## Trap 22 · 快速复连丢 CONNECT/DISCONNECT 中断 → 监听静默死亡 -### SOCK_TIMEOUT 处理 -- 长时间无数据时可能触发 `SINT_STAT_SOCK_TIMEOUT`,**不应视为致命错误**——记录日志后继续操作即可,不要因此关闭 Socket +- **状态**:✅ 已修复定案(周期对账 `net_reconcile_tick` 是 N34 reconciler 的前身;阶段 3 已并入单一 100ms tick) +- **现象(生产固件实测)**:单连接监听 socket 快速复连后**永久失效**(后续 SYN 被拒或超时,10s 内不自恢复),芯片其余部分完全正常(其他监听端口照常工作)。两种表现: + - **拒连**:芯片完成握手但 CONNECT 中断未上报 → 控制块滞留 LISTENING;或本层因 DISCONNECT 中断丢失仍以为有客户端 → 两种情况芯片侧都占住单连接监听,后续 SYN 全被拒; + - **应用连锁**:`net_send` 被拒;或应用读不到 EOF(`g_client` 永不释放,**永不调用 accept**)。 +- **复现(可信判据,纯客户端;工具 `MDK-ARM/tcp_reconnect_probe.py`)**: + - 确定性:产品固件 + 待机 60s,对 `:7003` 以 300ms 快速复连 → 修复前**首 2 次成功、之后永久拒连**(2/10); + - 概率性:FTP `:21` 100ms×50 → 修复前**第 29 轮起永久拒连**;对照 `:21` 0ms/600ms 与 `:8888` 同期正常 → 触发条件 = 复连间隔 <~1s + 轻负载。 +- **机制(设备侧日志实证)**:① 芯片丢 CONNECT 或 DISCONNECT 中断;② 此后本层**收不到任何中断**,纯事件驱动无路可走(把 LISTENING 分支改"重建监听"仍无效——连 DISCONNECT 都没来);③ 故必须**周期回读芯片状态对账**。裸机芯片台架(剥离 OS/net/FTP)独立复现"握手完成但 CONNECT 中断被丢"——芯片天性,不是固件 bug。 +- **修复(`net_socket.c`)**: + 1. **周期对账 `net_reconcile_tick()`**(初版 200ms;N34 阶段 3 与 pump 合并为单一 100ms tick):修正失同步形态——①本层 ESTABLISHED 而芯片已无连接→按断开处理;②本层 LISTENING 而芯片已握手→补 accept;③本层 LISTENING 而芯片监听消失→重建;④孤儿监听(B10)→重建。**去抖+冷却**:同一失配连续 2 次才动手,动手后冷却 1s。 + 2. `handle_disconnect_event` LISTENING 分支:由"仅告警"改为"回读芯片状态——已不可监听即重建"。 +- **踩坑记录(务必先读)**:最初修法放在 **accept 路径**(每次 accept 回读自愈):单次状态读把"刚 open/listen 尚未生效"的瞬态误判为失配 → 监听反复重建直至 `relisten give up` → **三个监听口全部死亡(风暴)**。改"周期对账+去抖+冷却"后消除。**结论:凡基于单次状态读的重建动作,必须去抖。** +- **验证(修复前→修复后)**:`:7003` 300ms×10:2/10→**10/10**;`:21` 100ms×50:28/50→**50/50**;`:21` 0ms/600ms×50:**50/50/50/50**;`:7003` 200ms×20:**20/20**;FTP 10×64KB SHA-256:**10/10**;`relisten give up`:**0**。自愈实测计数:`chip closed -> disconnect` 12 次、`w/o CONNECT irq -> accept` 1 次、`listen lost -> relisten` 1 次——三类失同步均真实出现并被自愈。 +- **对应文件**:`Drivers/BSP/NET/net_socket.c`(`net_reconcile_tick`);`MDK-ARM/tcp_reconnect_probe.py`;`docs/任务规划/README.md` N-6(已销项)。 +- **发现时间**:2026-09-22(台架偶发)→ 2026-09-23(确定性复现+当日修复闭环)。 -### UDP 模式区分 -- **DesIP=0xFFFFFFFF → UDP Server 模式**:接受任意来源数据,接收数据前 8 字节为信息头(`reserved[2] src_port[2] src_ip[4]`),回发前需设置 `SET_DES_IP` 和 `SET_DES_PORT` -- **DesIP=具体 IP → UDP Client 模式**:只接收指定 IP:Port 的数据,接收数据无信息头 +## Trap 25 · FTP 数据连接三连故障——四重定案(N-30) -### UDP 发送缓冲 -- 每次 `ch395f_write_send_buf()` 后必须等待 `SINT_STAT_SENBUF_FREE` 中断,否则下次写入会被 CH395F 静默丢弃 +- **状态**:✅ 已定案并修复(回归 4 轮 11/11;B3 见第六章) +- **现象(生产 FTP 回归,多轮复现)**:同一会话内**第 2 次及以后**的数据连接(LIST/STOR/RETR)两类失败,概率随上一条数据连接的负载与间隔变化(NAND 目录越大越易触发),与提交历史无关(D20 基线同样复现,当年"11/11 通过"是慢速台架+干净目录的运气): + - **吞数据(SWALLOW)**:新连接建立成功、服务端 `net_send` 返回成功,客户端 20s 收不到任何字节; + - **425**:新连接被接受后 ~10-20ms 出现 DISCONNECT(`chip tcp=0x00`),accept 失败 → `425 Can't open data connection`。 +- **定案实验(裸机芯片台架,剥离 OS/net/FTP)**:实验分支 `n30-chip-bench`,`test/board/ch395f_reuse_bench.c` 内核启动前直接调 `ch395f_*` 受控矩阵 + 主机侧 `test/host/ch395f_reuse_host.py`。**剥离后 bug 依旧复现** → 纯芯片操作时序问题。关键现场:`post-listen st=0x05/0x04`(listen 成功但 TCP 态停在旧连接 ESTABLISHED=幽灵态)、`unexpected int=0x10`(只等来旧连接滞后 DISCONNECT)、`no CONNECT within 10s`(芯片状态已 0x04 但中断永远没来)。 +- **芯片级规律**:CH395F 拆链收尾窗口**变长且不可观测**(状态显示 CLOSED 持续 1s+ 也不代表拆完,实测可 >2s 且时长不定)。窗口内重新 `open+listen` 三种形态:① 握手完成但 **CONNECT 中断被丢**(Trap 22 机制的窗口期形态)→ 吞数据;② 状态直接假报 0x04 幽灵态(回读对端端口=旧连接端口);③ 旧连接滞后 DISCONNECT 打到新监听 → relisten 毁掉新连接 → 425。 +- **已证伪的修复**(受控实验全部无效,列出防重踩):close 后固定沉降 100~3000ms(2s 也挂过)/ Socket 轮换(跨 socket 毒化)/ listen 后 close+重试 / 缓冲重分配 / close 前 TX 排空延时 / 禁用对账 tick(对账不是元凶)。 +- **有效修复一:双信源原则——中断只是快路径,状态寄存器才是真相**: + 1. **accept 路径内联探针**(`net_lost_connect_probe`,`net_accept_locked` 每 100ms 挂探针):芯片已 ESTABLISHED 而 CONNECT 中断未到 → 连续 2 轮去抖后补 accept; + 2. **幽灵态甄别**:补 accept 前回读对端端口(`GET_REMOT_IPP`),**= 上一条连接端口(`last_peer_port`)→ 幽灵,不 accept**(生产 LIST#4"accept 成功但数据被吞"的直接成因就是对账②曾把幽灵补 accept); + 3. 对账②同步加幽灵甄别;`last_peer_port` 登记点:`handle_disconnect_event` / `handle_timeout_event` / `net_relisten_try`(清零 remote_port 前转登)/ `net_close_locked`(跨 memset 保留——幽灵属芯片 Socket 级,与 fd 复用无关)。 +- **定案二:所谓"kill 模式"是寄存器语义误读(2026-10-11,双向验证)**——误判存档:曾解读"芯片在 accept 后 10-20ms 杀连接",**有误**。真相: + 1. `FUN_PARA=0x08` 置 bit3 `SOCKET_CLOSE=1`(§5.52 ②):产生 DISCONNECT/TIMEOUT 后芯片**不关 Socket、不清缓冲**,就是留给 MCU 读出残留数据后再关; + 2. 探针现场 `kill-check recv_len=1027 re-read st=0x05/0x00`——Socket 仍 OPEN、TCP 已 CLOSED、**RX 缓冲 1027B = STOR 载荷一个不少全在**:芯片按规格**保留现场**,不是杀连接; + 3. 那个 DISCONNECT 是**客户端正常 FIN**(Python ftplib `storbinary` 发完即关数据连接,标准 FTP 行为,局域网小文件 ~5ms 完成)。 + - **425 真根因(竞态+自毁缓冲)**:客户端 5ms 内发完并关闭;accept RPC 赶到时 MCU 态已 CLOSED;旧 `net_accept_locked` 把 CLOSED 一律当"无连接" → `net_relisten_try`(close+open **清空芯片 RX 缓冲**)→ 1027B 被**我方亲手销毁** → 425。芯片全程无辜(此模式)。 + - **修复(closed-with-data accept)**:CLOSED + auto_relisten + **芯片 RX 有数据**(`GET_RECV_LEN_SN`>0)→ 接受这条"已关但可读"连接(日志 `closed-with-data accept (drain pending)`),应用快路径排空,排空后 EOF 自然触发 relisten——对齐 §5.52 ② 契约。**STOR #2 从 4/4 失败 → 4/4 通过**。 + - 连带修正:台架 mode0"accept 后 10-20ms DISCONNECT"大概率同为误读(主机收完数据自己关的);但台架**丢 CONNECT 中断模式**(主机 got=0 + 芯片 0x04 + 无中断,主机端独立证据)仍成立,双信源探针修复保留。 +- **定案三:LIST 吞包 = 背靠背小段写毒化芯片 TX(2026-10-11)**:决定性对照——同一目录同一载荷,LIST #1(FAT 冷、行间天然间隔)通过;LIST #4/#7(FAT 热、每行一次 `net_send` ~60B 背靠背小段)必挂。芯片对同一连接的**背靠背多段小写入**会毒化 TX 整条连接(芯片报 SEND_OK 但对端协议栈全部丢弃,无 RST 无 DISCONNECT);逐块协议(等 SENBUF_FREE 再写)本身合规(§5.45),但芯片对该模式的内部处理不可靠;RETR 1KB 满块连发不受影响。**修复**:`send_list`/`send_nlst` 整份目录**一次性批量发送**(static 1536B 缓冲,snprintf 累积 + 单次写;溢出截断判 -1→550;注意 `lftpd_inet_write` 成功返回**字节数**非 0)。**修复后 ftp_regression 4 轮 11/11** + `ftp_auth_bench.py` 13/13。 +- **与 Trap 22 的关系**:Trap 22 周期对账对常驻监听器有效;FTP 数据通道是"活 1 秒就死"的短命监听器,对账②无幽灵甄别时既救不全(冷却期内 accept 已超时)又帮凶(把幽灵补 accept)。Trap 25 把"看状态补 accept"内联进 accept 热路径(短去抖、无冷却)+ 端口甄别,对账②保留为兜底网。 +- **教训(Trap 25 总纲)**:本轮先后误读三次——把客户端正常 FIN 读成"芯片杀连接"、把 open 重试读成"拆链窗口"、把芯片假 SEND_OK 读成"数据已上线"。铁律:**CH395F 的"成功"(中断位/状态码)只代表芯片本地动作完成,不代表对端收到;判定必须回到对端可见证据(主机侧收发)+ 手册语义(FUN_PARA §5.52、状态码 §5.22)**。 +- **对应文件**:`net_socket.c`(`net_lost_connect_probe`/closed-with-data accept/`net_reconcile_tick`②/`last_peer_port`)、`net_types.h`、`lftpd.c`(批量发送);台架 `test/board/ch395f_reuse_bench.{c,h}` + `test/host/ch395f_reuse_host.py`(实验分支注册编译,主分支只存档)。 +- **发现时间**:2026-10-11(登记簿 N-30)。 --- -## Trap 09:PHY link up 后立即 open_socket 失败 +# 第三章 连接生命周期与自愈 -### 现象 -`net_listen()` 在 PHY_CHANGE 中断同一时刻调用 `ch395f_open_socket()` 返回错误码(非 BUSY 超时),导致首次 listen 失败。3 秒后重试成功。 +## Trap 07 · TCP 关闭重连:直接 close,不要 disconnect -``` -[NET] PHY_CHANGE: 0x08 ← PHY 刚 link up (100M FULL) -[ERR] error listening on socket ← 同一秒 open_socket 失败 -[FTP] waiting for connection... ← 3秒后重试成功 -``` +- **状态**:✅ 已定案(做法保留;原根因说法已推翻,见定案更正) +- **误判存档**:原断言"`disconnect → close` 让 Socket 卡 `FIN_WAIT_2` 数分钟、close 发 RST"——**TC-NET-204 实测推翻**(2026-10-11),机制说法作废,做法保留。 +- **定案(TC-NET-204,三模式各 5 轮 45 项判定全过)**: + | 模式 | 关闭序列 | 回 `SOCKET_CLOSED` 耗时 | 重开+重连+回显 | + |---|---|---|---| + | A | `close` | 0~150 ms(过渡态可见 `TIME_WAIT` 0x0A) | 5/5 OK | + | B | `disconnect → close` | **全部 +0 ms** | 5/5 OK | + | C | `disconnect`(不 close) | **10s 内从未回到**(byte0 恒 05H) | 直接 open 必返 **0x20 FAIL**(5/5);补 close 后 +0ms 回 CLOSED | + 1. `FIN_WAIT_2`(0x06) 全程未出现(15 轮×9 采样点); + 2. **`close` 无 RST 语义**:§5.35 只说"关闭前自动断链";手册中芯片主动发 RST 仅 §5.29(监听占用)与 §9.2.6(多连接无可用数据 Socket)两处,均与 close/disconnect 无关; + 3. **观测口径错误**:§5.22 第 1 字节只有 `00H/05H`,第 2 字节(TCP 态)**仅在 byte0=OPEN 时有意义**(官方 SDK 注释明文)——当年把"已 CLOSED 状态下的残留 TCP 态读数"读成了"卡在某个 TCP 态"。 + - **真正原因**(C 模式 5/5 复现):`FUN_PARA` bit3 `SOCKET_CLOSE=1`(§5.52 ②)下产生 DISCONNECT 后**芯片不关 Socket、不清缓冲**,长期停在 `05H/00H`;此时直接 `open_socket` 必返 `0x20` FAIL。**把 Socket 拉回 CLOSED 的动作是 `close`,与 RST 无关。** + - **做法为何仍对**:① §5.35 close 已自动断链,disconnect 纯属多余;② disconnect 会**制造** DISCONNECT 事件,bit3=1 下必须被消费/预清,否则污染下一条连接(Trap 14/17/23 实证);③ 它把芯片推进 closing 过渡态,重开需沉降(Trap 09)。15 轮重开未观察到残留 `DISCONNECT`(0x10)——close 顺带清掉锁存。 + - **与 Trap 17 的张力已消解**:实测支持 Trap 17 一侧(close 后 0~150ms 过渡),不支持"close 发 RST 瞬间回 CLOSED"。 +- **边界补充(2026-10-13,E-02 台架)**:`close` 无 RST 的适用条件—— + - 对端配合(收到 FIN 后其内核 close):芯片走 `TIME_WAIT` ~150ms → byte0 回 00H → relisten → 新连接接入; + - **对端内核存活但不 close**(挂起但连接仍活):本层发 FIN 后芯片停在 **`FIN_WAIT_2`(byte0=05H/byte1=06H)长尾**——没有 RST 就拆不掉,**监听恢复受限于对端 close**; + - 不配合场景的兜底 = 芯片自身 TIMEOUT/KeepAlive 拆僵尸会话(E13 定案:判死窗 30~90s,见 `docs/网络/N34-自愈审计/实验方案.md` E13)——**不是**应用可加速的路径; + - **应用侧出口 = `net_drop_conn(fd)`**(N34 阶段 3):netTask 侧走驱动 `sock_reset` QUARANTINE(FIN+排渣+排空事件+等 byte0=00H,400ms 底)+ 本层收口 CLOSED + `auto_relisten` 即时重监听;**保监听不毁**(单连接模型不得 `net_close` 监听 fd)。对端不 close 时复位机等在 `net_poll` 步进里(`ch395f_sock_reset_poll`),对端 FIN 到达即自动走完——**不搁浅**。 +- **判据**:关闭后轮询 `GET_SOCKET_STATUS` 的**第 1 字节**等待 `sock=0x00` 再 `open_socket`;别拿 byte1 当"卡住"判据。 +- **发现时间**:Phase 2 TCP Client 测试(原记 2025-07 **有误**,实为 2026-07-19;定案更正与 TC-NET-204 实测 2026-10-11;E-02 边界补充 2026-10-13)。 -### 根因 -CH395F 的 PHY link up 后,内部 TCP/IP 协议栈需要额外时间完成初始化(ARP 缓存、路由表等)。在 PHY_CHANGE 中断触发的同一 `net_poll()` 迭代内立即调用 `OPEN_SOCKET`,芯片可能返回 `ERR_BUSY` 或其他错误码。 +## Trap 09 · 沉降窗:PHY link up 后 / close 后不能立即 open -### 解决方案 -1. **应用层重试兜底**:FTP Server (`lftpd_start`) 的 `while(1)` 循环中,listen 失败后 `vTaskDelay(3000)` 重试,3 秒足够 PHY 稳定 -2. **启动延时消峰**:FTP 任务启动时 `osDelay(7000)`,等 netTask 完成 `net_init() → PHY 协商 → 首轮 net_poll()` 后再发起 listen,此时 PHY 已稳定 2 秒以上,首次就成功 +- **状态**:✅ 已定案(现行形态=open 重试环 + `net_sock_reset_sync`;原 `osDelay(7000)` 启动延时已被就绪屏障取代) +- **现象**:PHY_CHANGE 中断同一时刻 `ch395f_open_socket()` 返错(非 BUSY 超时),首次 listen 失败;3 秒后重试成功。 +- **根因(定案)**:PHY link up 后芯片内部协议栈需额外时间完成初始化;同一 `net_poll()` 迭代内立即 `OPEN_SOCKET` 可能返 `ERR_BUSY` 或其他错误码。与 FUN_PARA 无关。 +- **修复(现行形态)**: + 1. `net_listen_locked`/`net_connect_locked` open 失败重试 ×3(D10 `net_listener_retry_tick` 重监听重试环); + 2. open 失败先走 `net_sock_reset_sync`(确定性排渣,内置二段 CLOSE,见下补充)再重试——最短等 100ms ≪ 原 500ms 盲等; + 3. ~~FTP 任务 `osDelay(7000)` 错峰~~ → 已由**就绪屏障**(`SYS_EVT_NET_READY`/`FTP_READY`)取代(2026-10 起)。 +- **补充(2026-10-13,E2/E4 台架):LISTEN 态 Socket 的二段关闭**——本条"close 后需沉降才能重开"的前置形态: + - 对 LISTEN(byte0=05H/byte1=01H)发 `CLOSE_SOCKET`:命令返 SUCCESS 但**只拆监听**(byte1 01H→00H),byte0 恒 05H——按"等 byte0=00H"判据会永远等不到;**第二次 CLOSE** 才把 byte0 拉到 00H。即 LISTEN 态关闭是**两段**:拆监听 → 关 Socket。 + - 驱动侧已内置补发:`ch395f_sock_reset_*` 状态机 `WAIT_CLOSE` 段检测到"TCP 已 00H 而 byte0 仍 05H"自动补发第二次 CLOSE(契约见 `Drivers/BSP/CH395F/ch395f.h`)。 + - E4 定案 60 轮实证"复位后 open 必成"。 + - ⚠ 任何**新写的**"close 后等 CLOSED"逻辑必须走驱动复位原语,**不要**手拼"一次 CLOSE + 轮询 byte0"——会在 LISTEN 态上死等。 +- **发现时间**:FTP 改造期(原记 2025-07 不可考);二段 CLOSE 补充 2026-10-13。 -### 注意事项 -- 此问题在单连接模式 (`FUN_PARA=0x00`) 和多连接模式 (`0x02`) 下均存在,与 FUN_PARA 无关 -- 如果多个应用任务同时启动 listen,可能全部首次失败、全部重试成功——建议各任务错峰启动 +## Trap 11 · net_accept_locked 拒绝 standalone_accept 的 ESTABLISHED -### 补充(2026-10-13,E2 台架):LISTEN 态 Socket 的**二段关闭**——一次 CLOSE 只拆监听,二次 CLOSE 才回 CLOSED +- **状态**:✅ 已修复定案(现行 `net_accept_locked` 仍保留该放行逻辑) +- **现象**:FTP 客户端 TCP 连接成功(standalone accept),220 欢迎语不发送,`lftpd_inet_accept()` 死循环。 +- **根因(定案)**:入口检查 `state != LISTENING` 直接拒绝——standalone accept 后 state 已变 ESTABLISHED,永远到不了后续检查。 +- **修复**:放行 `standalone_accept && state == ESTABLISHED` 组合。 +- **注意**:仅影响 standalone_accept 模式(PASV 数据通道、单连接 FTP)。 +- **发现时间**:FTP 改造期(2026-08)。 -E2/E4 芯片实验(`docs/网络/N34-自愈审计/实验方案.md`)补充发现,属本条"close 后需沉降才能重开"的**前置形态**: +## Trap 23 · 上传中止后首笔复连上传被静默截断(≤2048B)并误报成功 -- 对处于 **LISTEN**(byte0=`05H` / byte1=`01H`)的 Socket 发 `CLOSE_SOCKET`:命令返 **SUCCESS**,但芯片**只拆监听**(byte1 `01H→00H`),**byte0 恒 `05H`**——单次 CLOSE 后按"等 byte0=00H"的判据会永远等不到。 -- **第二次 CLOSE** 才把 byte0 拉到 `00H`(Socket 关闭)。即 LISTEN 态的关闭是**两段**:拆监听 → 关 Socket。 -- **驱动侧已内置补发**:`ch395f_sock_reset_*` 状态机的 `WAIT_CLOSE` 段检测到"TCP 已 00H 而 byte0 仍 05H"自动补发第二次 CLOSE(契约见 `Drivers/BSP/CH395F/ch395f.h`)。 -- **生产路径由 Trap 09 的 open 重试环吸收**:`net_listen_locked` / `net_connect_locked` open 失败 → `net_sock_reset_sync`(确定性排渣,内置二段 CLOSE)→ 下一轮重试 open;E4 定案 60 轮实证"复位后 open 必成"。 -- ⚠ 任何**新写的**"close 后等 CLOSED"逻辑都必须走驱动复位原语,**不要**手拼"一次 CLOSE + 轮询 byte0"——那会在 LISTEN 态上死等(本条补充的直接动因)。 +- **状态**:✅ 已修复定案(三阶段全部闭环;回归判据常备:`test/host/ftp_abort_probe.py`) +- **现象**:客户端上传(STOR)中途断开(**RST 或优雅 FIN 均可**),**紧随其后的第一笔复连上传被截断到 ≤2048 字节(= 芯片单 socket RX 缓冲大小),且服务端回报 226 成功** → 客户端据此认为上传成功 = **静默数据损坏**;第二笔及以后恢复正常。 +- **证据(可信复现:生产固件、零固件探针;工具 `test/host/ftp_abort_probe.py`)**:判据 = FTP 协议结果 + 紧接一笔正常上传的 SHA-256 往返 + 控制通道 NOOP 存活探测。RST 中止 **10/10** 复现(请求 4096B → 设备日志 `UPLOAD '/ab_ok.bin' 2048 bytes OK`);优雅 FIN **5/5** 同样复现 → 与 RST/FIN 无关,凡"数据未发完即断开"均触发。对照(无中止)177 轮(8KB~1MB,含 100 轮零间隔浸泡)SHA-256 全匹配(`MDK-ARM/ftp_freeze_probe.py`)→ 中止路径是唯一触发条件。 +- **机制(设备侧日志实证)**:① 中止时数据 socket 释放(RST 时芯片报 `sock4 TIMEOUT`),net 层随即 relisten(监听端口复用);② 中止时在途数据(约一个缓冲=2048B 残渣)**随后才落进芯片 RX 缓冲**;③ 下一笔连接 accept 后 ~10-20ms 出现伪 `DISCONNECTED, releasing (chip tcp=0x00)`,lftpd 数据读循环据此读到 EOF → 把已到的 2048B 当整个文件 → 226;④ 再下一笔芯片已干净 → 正常(**一次性**而非持久故障)。**缺陷位于 fd 复用生命周期**(上一条连接的残留状态投递给下一条连接),不在 FTP 命令逻辑——lftpd 只是把它放大成误报 226。 +- **边界甄别(现场"1~2s"时间假设被推翻)**:gap 500/2000/5000ms 复测设备实存尺寸**全部仍 2048**(12/12)→ **与时间无关**;触发=设备侧**收过数据**的数据连接被对端关闭(纯 FIN 即触发);同 fd 非 FTP 服务(`:7003`)不受影响(不按传输复用)。 +- **修复(三阶段,`net_socket.c` + `lftpd.c`)**: + 1. **异常释放(TIMEOUT/RST)隔离**:`handle_timeout_event` 置 per-fd 隔离标记;`net_relisten_locked` 隔离期内持续排空芯片 RX(`net_drain_chip_rx()`)+ 主动 close + 排空该 socket 中断 + 等 `CLOSED/TCP_CLOSED`,满 `NET_RELISTEN_QUARANTINE_MS`(400ms) 才 open+listen;上限 `NET_RELISTEN_SETTLE_MS`(800ms) 强制放行——**必须小于** `NET_RELISTEN_DEADLINE_MS`(2000ms):实测取 2000ms 会耗尽重试预算 → give up → **监听器死亡**(已留痕); + 2. **正常释放(FIN)沉降**:`handle_disconnect_event` 登记(仅 `recv_bytes != 0` 时——纯连接即关与只发不收免沉降保复连时序),relisten 前最短等 `NET_RELISTEN_GRACEFUL_MS`(**100ms**,实测悬崖:0/30ms 无效、100ms 稳定通过,对 30ms 重试节拍留 3× 余量);日志区分 `relisten settle`/`relisten quarantine`; + 3. **应用侧硬化**:`net_sock_t.released_abnormal` 标记(TIMEOUT 置 1/DISCONNECT 置 0,新 fd 与新监听清零——新连接不得继承上一条的释放原因);`net_pending_tick` 断连了结按标记以 `NET_ERR_CONNRESET` 或 EOF(0) 回传;`lftpd receive_file` 错误路径 **unlink 截断文件** + STOR 回 **450**(原误报 226)。 +- **协议边界(不可判定,留痕)**:FTP STOR 无长度声明,"在途 FIN 关闭"与"正常完成"**逐字节不可区分** → 优雅中止仍回 226;仅 RST/掉链(芯片 TIMEOUT)可识别 → 450。FIN 边界由客户端长度校验(SIZE/RETR 往返)兜底。 +- **验证(终态基线)**:`--check-abort` RST **5/5**(450)+ FIN **5/5**(226 边界不变);followup 全过;正常往返 freeze SHA-256 全 MATCH(t_stor 0.68→0.18s);`:7003` sink 复用 RST×4/FIN×4 全过;`:8888` 真实收包写 RTC 成功。 +- **与 TC-NET-604 的关系**:本条是独立、已可信复现并已修复的真实缺陷;TC-NET-604 已结案为非缺陷(Trap 13/21)。现场"上传失败/文件不完整/像卡死"类报告优先用本条判据复核。 +- **发现时间**:2026-09-23(可信复现首轮);二阶段 09-24、硬化 09-25。 --- -## Trap 10:消息队列值拷贝导致结果无法回传 +# 第四章 数据通路与数值语义 -### 现象 -`net_send()` / `net_recv()` / `net_listen()` / `net_accept()` / `net_close()` 等线程安全 API(通过消息队列委托 netTask 执行)永远返回 `-1`,即使 netTask 内部操作成功。 +## Trap 04 · UDP 发送后必须等待 SENDBUF_FREE -### 根因 -CMSIS-RTOS v2 `osMessageQueue` 传递 `net_msg_t` 结构体是**值拷贝**。调用方 puts 后 `msg.result = -1`(栈变量),netTask 侧 `osMessageQueueGet` 拿到的是**队列中的副本**,修改副本的 `msg.result = 0` 后 `xTaskNotifyGive` 通知调用方。调用方的栈变量 `msg.result` 仍然是 `-1`,从未被更新。 +- **状态**:✅ 已定案 +- **现象**:UDP 发送后立即接收,接收长度恒 0 或数据错误。 +- **根因(定案)**:手册要求每次 `WRITE_SEND_BUF` 后必须等 `SINT_STAT_SENBUF_FREE`,否则下次写入被静默丢弃。 +- **修复**:每次 `ch395f_write_send_buf()` 后轮询 `ch395f_get_sock_int_status()` 检查该标志。 +- **发现时间**:广播宣告模式(原记 Phase 6 2025-07 有误,UDP 对应阶段 3)。 -```c -// 调用方(ftpTask) -msg.result = -1; -osMessageQueuePut(&msg); // 值拷贝到队列 -xTaskNotifyWait(¬ified); // 等待通知 -return msg.result; // 永远是 -1! +## Trap 13/21 · 「recv_len 归零 / 接收缓冲冻结」家族——非缺陷(合并结案) -// 处理方(netTask) -osMessageQueueGet(&msg); // 拿到的是队列副本 -msg.result = actual_result; // 修改的是副本 -xTaskNotify(...); // 通知回去了,但值丢了 -``` +- **状态**:↩️ 已撤回(2026-09-23 结案;2026-10-13 把 Trap 13 并入本条——同族误判,当年未同步) +- **合并说明**:Trap 13(2026-08-26,"分批读 recv_len 归零")与 Trap 21(TC-NET-604"恰好填满+排空窗口冻结")是同一轮误读的两半;Trap 13 的机理断言与手册直接相左,一并撤回。 +- **结案结论**:原报告(同连接前序突发后 4096B 突发"前 2KB 正常回显、其余静默无回显")**在当前固件上无法复现,判定为非缺陷**。可信判据(产品固件 + 真实 lftpd FTP 上传,成功 = FTP 协议结果 + **上传后 RETR 回读 SHA-256**)实测 **227 轮全过**(8KB/64KB/256KB/1MB;含零间隔快速重连与 100 轮浸泡),并发生产 UI 流 600 帧 CRC 全过。 +- **为什么原台架会"复现"(教训,务必先读)**: + 1. **测试程序自吞数据**:台架配方里的固件内读者(Phase 9/11 的 64B 读、固件探针读)把数据读走后不回显——PC 端少收被误当"设备停摆"。实测短收量**恰等于被吞字节数**(`4736−4608=128=2×64`);对照组(无额外读者)40/40 全过。 + 2. **判读工具误判**:`probe_classify.py` 把**通过**的运行报成 `STALL`(脚本层实测 `1460/1460` 且内容逐字节匹配)。 + 3. 由此派生的"部分排空致命 / 再武装区间 ≥160ms / 相位刀口"等结论**全部作废**。 +- **Trap 13 历史注**:当年确实观察到 `recv_len` 归零,但机理断言("读部分数据后 recv_len 被重置为 0")**与 §5.34 明文相左**("未读取的数据仍然保留在接收缓冲区中,可以继续读取"),本仓 TC-NET-201 注释也是相反口径;定案前须补一条"部分读 → 再查 `recv_len`"的定向 TC。⚠ 该已撤回机理**仍被当作现行约束**写在 `CH395F_Test_Guide.md` 多处与 `net_socket.c` 注释里——处置登记在第六章待修清单。 +- **台架纪律(保留)**:① 数据完整性以 **SHA-256 往返**为真值,并扣除固件读者吞噬的字节;② 冻结/卡死类走**真实生产路径 + 端到端内容校验**(`MDK-ARM/ftp_freeze_probe.py`),卡住时用控制通道 NOOP 区分"数据面卡死"与"整层卡死";③ **禁止把"固件内非回显读者"与固定期望值混用在同一配方里**——本轮最大教训。 +- **保留的代码变更(与撤回机理无关)**:缓冲块布局与 MSS 解耦(`ch395f.h`)、超限读 OVERSIZE 金丝雀(`ch395f.c`)不回退(防御性/可观测性改进)。 +- **发现/结案**:发现 2026-08-26(阶段 8);结案 2026-09-23;合并 2026-10-13。 -### 解决方案 -不通过消息队列回传结果,改用 **FreeRTOS 通知值**(notification value)携带返回值: +## Trap 16 · NET 层目的 IP 字节序写反 → CONNECT TIMEOUT -**Producer(netTask)**: -```c -xTaskNotify(msg.caller, (uint32_t)msg.result, eSetValueWithOverwrite); -``` +- **状态**:✅ 已修复定案(字节序总口径,多处引用) +- **现象**:NET 层 `net_connect()` 返回 0 但 `sock0 CONNECT TIMEOUT` 永远连不上;同一芯片同一目标的硬件层 client 完全正常。 +- **根因(定案)**:`net_connect_locked()` 把 `sin_addr.s_addr`(网络字节序)按**低字节在前**拆成 `remote_ip_arr[4]` 得到 `[2,1,168,192]` 交给 `ch395f_set_des_ip()`——但 **`SET_IP_ADDR_SN` 族按点分顺序(`[192,168,1,2]`)写入才生效**(硬件层 `test_parse_ip` 直出点分即成功;官方例程 `CH395IPAddr={192,168,1,101}` 同向)。芯片把 SYN 发往 `2.1.168.192` → 网关丢弃 → TIMEOUT。⚠ 手册 §5.8 总括"所有 IP 输入输出命令字节顺序均为低字节在前"与官方例程自相矛盾,**本仓以实测+例程为准**。 +- **误导性排查**:`GET_REMOT_IPP` 回读是**小端**——把写反的数组读回再按小端解释恰好又得 `192.168.1.2`,回读日志"看起来正确",掩盖真实写反。 +- **修复**:① `remote_ip_arr` 改**大端**提取(`arr[0]=(ip>>24)&0xFF …`);② UDP `net_send_locked()` 同样修正;③ 诊断回读按小端语义重组再打印。 +- **字节序总口径(务必分清两套)**: + - 写芯片 **IP 四字节数组**(set_des_ip/set_ip_addr/set_gwip_addr/set_mask_addr)= **点分顺序**; + - **16 位端口一律低字节在前**(§5.25/§5.26),两者口径不同,勿把"大端数组"套到端口上; + - 回读类命令(GET_REMOT_IPP/GET_IP_INF)若标注"低字节在前",打印前先按小端重组; + - 本层 `sin_addr.s_addr`/`remote_ip` = 网络字节序数字;`remote_ip_arr[4]` = 点分顺序(`net_types.h` 注释)。 +- **发现时间**:阶段 7 NET 层 TCP Client 上板实测,2026-08-25。 -**Consumer(调用方)**: -```c -uint32_t notified; -xTaskNotifyWait(0, 0, ¬ified, portMAX_DELAY); -msg.result = (int)notified; -``` +## Trap 20 · 单次写超过发送缓冲区(写穿) -### 注意事项 -- 指针成员(`msg.buf`)不受值拷贝影响——netTask 拿到的是同一个指针,可以读写调用方的缓冲区 -- `net_connect()` 不使用消息队列(在 netTask 内直接调用 `_locked` 版本),不受此 bug 影响 -- 内核级 API(`net_send_sock` / `net_recv_sock` / `net_listen_locked`)直接操作,同样不受影响 +- **状态**:✅ 已修复定案(措辞已按 2026-10-13 更正:不是"硬件 FIFO",是本仓分配的发送缓冲) +- **现象**:单次发送 >1KB 时数据中段错乱、连接被对端 RST、或 `WRITE_SEND_BUF` 后芯片不再产生 SENBUF_FREE——发送卡死或后续字节丢失;只在 >1KB 时稳定复现。 +- **根因(定案)**:分块上限误取 `NET_DMA_MAX_PAYLOAD`(2KB)/软件缓冲(4KB),而每 Socket 的发送缓冲 = 本仓固定 2 块×512B = **1KB**(§5.45 上电默认其实是 4 块=2KB,本仓布局自 2026-09-22 起固定 2 块、与 MSS 解耦);一次性写 2KB/4KB 直接**写穿发送缓冲**(超出部分覆盖/丢失或破坏指针)。 +- **修复**:`CH395F_SEND_BUF_SIZE_BYTES` 宏 + `NET_SEND_CHUNK_MAX = min(NET_DMA_MAX_PAYLOAD, CH395F_SEND_BUF_SIZE_BYTES)`(现 1KB);`net_send_locked`/`net_sendto` 分块上限改用它。门控是 **SENBUF_FREE**(不是"等 SEND_OK");接收方向不受影响(只读 `get_recv_len` ≤ 接收缓冲 2KB)。 +- **注意**:上限随 `CH395F_SEND_BLOCKS` 自动跟随;调用方可按任意长度调 `net_send`,分块由驱动保证。 +- **发现时间**:阶段 8 大文件传输实现,2026-08-26。 --- -## Trap 11:`net_accept_locked` 拒绝 standalone_accept 的 ESTABLISHED 状态 +# 第五章 上层集成与主机侧工具 -### 现象 -FTP 客户端 TCP 连接成功(standalone accept),220 欢迎语不发送,`lftpd_inet_accept()` 死循环。客户端连接成功但永远收不到任何 FTP 响应。 +## Trap 03 · DHCP 包 xid 偏移错误(主机工具) -### 根因 -`net_accept_locked()` 入口检查: +- **状态**:✅ 已修复 +- **现象**:Python DHCP Server 收到 DISCOVER 后无法匹配 Offer/Ack 的 xid。 +- **根因(定案)**:误用 `data[232:236]`(Wireshark 某旧版显示误导);标准偏移 xid=`data[4:8]`、msg_type=`data[242]`、client_mac=`data[28:34]`、magic cookie=`data[236:240]`。 +- **对应**:`test/host/ch395f_socket_test.py` DHCP server 模式。 +- **发现时间**:阶段 4(原记 Phase 6 2025-07 有误)。 -```c -if (p_listen_sock->state != NET_SOCK_STATE_LISTENING) { - return -1; // ← 直接拒绝! -} -``` +## Trap 10 · 消息队列值拷贝导致结果无法回传 -standalone accept 后 state 已变为 `ESTABLISHED`,永远到不了后续的 `standalone_accept` 检查——死锁。 +- **状态**:✅ 已定案(现行契约:`net_*` 错误码走通知值——AGENTS 模块约定) +- **现象**:`net_send/net_recv/net_listen/net_accept/net_close` 等委托 netTask 的 API 永远返回 -1,即使内部成功。 +- **根因(定案)**:`osMessageQueue` 传 `net_msg_t` 是**值拷贝**——netTask 修改的是队列副本,调用方栈变量的 `msg.result` 从未被更新。 +- **修复**:结果搭 `xTaskNotify(caller, (uint32_t)msg.result, eSetValueWithOverwrite)` 通知值回来,调用方 `xTaskNotifyWait` 取回。**勿给 `net_msg_t` 加"回传用"字段**(调用方只会读到栈垃圾)。 +- **注意**:指针成员(`msg.buf`)不受值拷贝影响;内核级 `_locked` API 直接操作不受影响。⚠ 原注"net_connect() 不使用消息队列"已失效——现行 `net_connect` 同样走消息队列、结果同样走通知值。 +- **发现时间**:FTP 改造期(2026-08)。 -### 解决方案 -```c -if (p_listen_sock->state != NET_SOCK_STATE_LISTENING && - !(p_listen_sock->standalone_accept && p_listen_sock->state == NET_SOCK_STATE_ESTABLISHED)) { - return -1; -} -``` +## Trap 12 · FatFS 路径与 8.3 短名 -允许 `standalone_accept + ESTABLISHED` 组合通过。 +- **状态**:✅ 已修复(修复有效;原根因 #2/#4 已按仓内 FatFs 源码更正) +- **现象**:FTP STOR `f_open("/upload_test.txt", ...)` 返 `FR_INVALID_NAME`(6);>8.3 的文件名同样 err=6。 +- **根因(定案,2026-10-13 更正)**:真约束是 `ffconf.h` `FF_USE_LFN=0` → **只支持严格 8.3 短名**(不得以点开头)。原记"FatFS 不接受 `/file` 前缀 / 空字符串失败"两条与仓内 `Lib/FatFs/ff.c` 矛盾(`ff.c:3101-3109` 会剥前导分隔符、`:3127-3129` 空名返回根目录)——**已删**;本仓现行路径口径是 `"0:"` 卷前缀(`FF_FS_RPATH=0` 下 `"."` 不是合法路径段)。 +- **修复**:`to_fatfs_path()` 剥离 `/` 前缀(保留,行为正确);长文件名需 `FF_USE_LFN=1` + `FF_LFN_UNICODE`(显著增 RAM,未启用)。 +- **发现时间**:FTP 改造期(2026-08)。 -### 注意事项 -- 不影响多连接模式(多连接监听 Socket 状态始终为 LISTENING,由数据 Socket 承载连接) -- 仅影响 standalone_accept 模式(PASV 数据通道、单连接 FTP) +## Trap 15 · DHCP 成功后 IP 寄存器提交滞后 + +- **状态**:✅ 已定案(测试侧稳定轮询+交叉比对) +- **现象**:`GET_DHCP_STATUS` 已置 0x00 后立即 `GET_IP_INF` 读到的仍是旧静态配置,而芯片实际已按新租约收发——三方数据矛盾构成假阳性(2026-08-24 上板实测)。 +- **根因(定案)**:DHCP 状态位仅代表协商完成;IP/MASK/GW 寄存器提交与状态置位**异步**,数百毫秒级窗口(实测 ~1s 内翻转)。 +- **修复**:① 连续 3 次读 `GET_IP_INF`(间隔 300ms,上限 8s)一致且非零才采信;② PC 端把收到的宣告 IP 与租约比对兜底。 +- **对应**:`test/board/ch395f_test_task.c: phase4_run()`;`docs/测试说明/CH395F_Test_Guide.md` 阶段 4 风险表 #4。 +- **发现时间**:2026-08-24。 --- -## Trap 12:FatFS `FF_USE_LFN=0` + 路径 `/` 前缀 → `FR_INVALID_NAME` - -### 现象 -FTP STOR 上传文件时 `f_open("/upload_test.txt", FA_WRITE | FA_CREATE_ALWAYS)` 返回 `err=6`(FR_INVALID_NAME)。`upload_test.txt` 文件名 11+3 字符 > 8.3 限制同样触发 err=6。 - -### 根因 -1. `ffconf.h` 中 `FF_USE_LFN = 0`(长文件名禁用),最大文件名 8+3 字符 -2. FatFS 不接受 `/file` 格式的路径(需要 `file` 或 `0:file`) -3. `lftpd_io_canonicalize_path("/", "file")` 输出必然带 `/` 前缀 -4. 根目录路径 `"/"` 剥离后为空字符串,`f_stat("")` 同样失败 - -### 解决方案 -```c -/* 剥离 "/" 前缀,根目录用 "." 表示 */ -static const char *to_fatfs_path(const char *path) { - if (path == NULL || *path == '\0') return "."; - if (*path == '/') { - path++; - if (*path == '\0') return "."; - } - return path; -} -``` - -所有 `lftpd_io_*` 函数调用 FatFS 前统一使用 `to_fatfs_path(path)`。 - -### 注意事项 -- 长文件名需设置 `FF_USE_LFN = 1` 并配置 `FF_LFN_UNICODE`,会显著增加 RAM 占用 -- `CWD .` 和 `CWD /` 同样受此问题影响,修复后正常工作 - ---- - -## 附:FTP 支持改造完整问题清单 - -| # | 问题 | 类型 | 修复位置 | -|---|------|------|----------| -| 1 | 消息队列值拷贝,6 个 API 永远返回 -1 | Trap 10 | `net_socket.c` | -| 2 | PHY link up 后 open_socket 失败 | Trap 09 | `freertos.c` + `lftpd.c` | -| 3 | `net_accept_locked` 拒绝 ESTABLISHED | Trap 11 | `net_socket.c` | -| 4 | CH395F 多连接模式不支持多监听 | 架构 | `net_socket.c: fun_para=0x00` | -| 5 | PASV 端口 0(net_bind 无自动分配) | 缺功能 | `net_socket.c: s_dynamic_port` | -| 6 | FatFS 路径 `/` 不兼容 + LFN 禁用 | Trap 12 | `lftpd_io.c: to_fatfs_path()` | -| 7 | `cmd_pasv` 连关连开 listen 失败 | Trap 09 子类 | `lftpd.c: 3次重试` | -| 8 | `ftpTask` 启动时 PHY 未稳定 | Trap 09 子类 | `freertos.c: osDelay(7000)` | -| 9 | 会话结束后立即重建监听失败 | Trap 09 子类 | `lftpd.c: vTaskDelay(200ms)` | -| 10 | `s_file_open` 单文件限制 | 已知限制 | `lftpd_io.c` (全局变量) | -| 11 | CH395F DMA buffer 太小,分批读导致 recv_len 归零 | Trap 13 | `ch395f.c + lftpd.c` | -| 12 | `malloc` 嵌入式环境失败 | Trap 13 子类 | `lftpd.c: static buffer` | -| 13 | `net_listen_locked` 缓冲区覆盖(s0/s1 争用 block 0-3) | Trap 02 翻版 | `net_socket.c: 单连接用默认 buf` | -| 14 | CH395F TIMEOUT 立即触发(数据连接无数据) | Trap 09 子类 | `net_socket.c: CLOSED 状态处理` | -| 15 | 发送单次写 >1KB 写穿 1KB 硬件发送 FIFO | Trap 20 | `net_socket.c: NET_SEND_CHUNK_MAX` | -| 15 | `NET_RECV_TIMEOUT_MS=5s` 控制通道超时断开 | 配置 | `net_config.h: 30000` | -| 16 | `receive_file` 0 bytes 当作成功 | 逻辑 | `lftpd.c: total==0 则失败` | -| 17 | MobaXterm 连数据端口不发数据(客户端特殊行为) | 外部 | Python 抓包确认,超时恢复 | - ---- - -## Trap 13:CH395F `recv_len` 分批读取时归零 - -### 现象 -MobaXterm 上传文件时 CH395F 收到数据(`s1 int=0x04` RECV_OK 持续触发),但 `ch395f_get_recv_len(1)` 第一次返回 2920,读 1024 字节后第二次永远返回 0。`GINT=0x0020` 反复触发但无数据可读——死循环。 - -### 根因 -`ch395f_read_recv_buf` 读部分数据后,CH395F 的 `recv_len` 寄存器**被重置为 0**(非递减)。缓冲区中剩余的 1896 字节仍然存在(RECV_OK 标志为真),但 `ch395f_get_recv_len` 报告 0。 - -> ⚠ **2026-10-11 复核:该机理未证实,且与手册直接相左——§5.34 明文"读取的数据长度可以小于缓冲区的实际有效数据长度,未读取的数据仍然保留在接收缓冲区中,单片机可以通过此命令继续读取";本仓自己的 TC-NET-201 注释也是相反口径(`test/board/ch395f_test_task.c:419` "只查询积压字节数…可反复调用"、`:476-477` "剩余字节下一轮 get_recv_len 会继续报告,不会丢")。同族结论"部分排空致命"已由 Trap 21 结案撤回,本条当时未同步。现象本身(当年确实观察到 `recv_len` 归零)保留为历史记录;定案前须补一条"部分读 → 再查 `recv_len`"的定向 TC。下方解法数字均为历史值:现 `CH395F_SPI_DMA_BUF_SIZE = 2048+4 = 2052`(`ch395f.h:148`)、`lftpd.c:228` 是 `static unsigned char buffer[1024]`,`s_recv_buf[4096]` 已不存在。** - -同时 `CH395F_DMA_BUF_SIZE=1500`(header 4 + max 1496 data),无法一次读取完整的 4096 字节 CH395F 接收缓冲区。 - -### 解决方案 -1. `CH395F_DMA_BUF_SIZE` 从 1500 增大到 4100(支持一次读 4096 字节 header+data) -2. `receive_file` 缓冲区从 1024 增大到 4096,一次性读空 CH395F 接收缓冲区 -3. 不能使用 `malloc`(嵌入式堆可能不可用),改用 `static unsigned char s_recv_buf[4096]` - -### 注意事项 -- CH395F 接收缓冲区默认 socket 0: 4096B, socket 1: 4096B。读空后客户端继续发数据,recv_len 重新递增——正常 -- 清理中断状态后 `handle_timeout_event` 中的 `ch395f_close_socket` 必须显式调用,确保下次 `open_socket` 成功 - ---- - -## Trap 14:close 后 DISCONNECT 滞后锁存,污染下次会话首次中断读取 - -### 现象 -`close_socket` → 轮询 `GET_SOCKET_STATUS` 已确认 CLOSED 后,紧接的下一会话(重新 open + connect)在第一次读 `GET_INT_STATUS_SN` 时读到**上一会话遗留的 DISCONNECT 事件**(0ms 即命中),导致负向判定/状态机误判。实测:TC-202 三轮重连后立即跑 TC-203,PC 端握手实际成功(连接被接受),固件却因陈旧 DISCONNECT 抢先判"未连接"。 - -### 根因 -- `wait-CLOSED` 轮询用的 `GET_SOCKET_STATUS` 是**纯查询,不清中断**; -- TCP 拆除过程中断(DISCONNECT)可能在与 CLOSED 状态达成几乎同时、甚至之后才锁存; -- 该事件无人消费,跨会话存活到下一次 `open_socket` 之后。 - -### 解决 -1. **预清**:新会话 OPEN 前读一次 `GET_GLOB_INT_STATUS_ALL` + `GET_INT_STATUS_SN` 丢弃陈旧事件; -2. **反核实**:依据中断做结论前,用 `GET_SOCKET_STATUS` 验证实际状态一致(如"负向事件 vs ESTABLISHED"矛盾时以状态为准)。 - -### 参考 -`test/board/ch395f_test_task.c: phase2_tc203()`(双保险实现);同类思路见 Phase 1 入口的 PHY_CHANGE 预清。 - ---- - -## Trap 15:DHCP 成功后 IP 寄存器提交滞后 - -### 现象 -`GET_DHCP_STATUS` 已置 0x00 后立即 `GET_IP_INF`,回读到的仍是**旧静态配置**(如 192.168.1.100),而芯片实际已按新租约(192.168.1.77)正常收发——串口打印与 Socket6 广播宣告均带出过期 IP。2026-08-24 上板实测:固件各检查全 PASS(6/6),但 PC 侧 HELLO 发往租约地址才得到应答,三方数据矛盾构成假阳性。 - -### 根因 -DHCP 状态位仅代表协商流程完成;IP/MASK/GW 寄存器的提交与状态置位是异步的,存在数百毫秒级窗口(实测 ~1s 内完成翻转)。 - -### 解决 -1. **稳定轮询**:连续 3 次读 `GET_IP_INF`(间隔 300ms,上限 8s)结果一致且非零才采信; -2. **交叉比对**:PC 端将收到的宣告 IP 与所分配租约比对,不一致即报错兜底。 - -### 参考 -`test/board/ch395f_test_task.c: phase4_run()`;`docs/测试说明/CH395F_Test_Guide.md` 阶段 4 风险表 #4。 - ---- - -## Trap 16:NET 层 TCP Client 目的 IP 字节序写反 → CONNECT TIMEOUT - -### 现象 -NET 层 TCP Client(阶段 7)`net_connect()` 返回 0,但 `net_poll()` 报 `sock0 CONNECT TIMEOUT`,连接永远建立不起来;而硬件层 TCP Client(阶段 2,同一 `net_init`/FUN_PARA=0x08,同一目标 192.168.1.2:8081)完全正常。PC 同网段、防火墙关闭、服务端正常监听。 - -### 根因 -`net_connect_locked()` 把 `sockaddr_in.sin_addr.s_addr`(网络字节序,`net_inet_addr` 返回)按 **低字节在前** 拆成 `remote_ip_arr[4]`,得到 `[2,1,168,192]`,再交给 `ch395f_set_des_ip()`。但 **`SET_IP_ADDR_SN` 族按点分顺序(`[192,168,1,2]`)写入才生效**(阶段 2 用 `test_parse_ip("192.168.1.2")` 直接得到 `[192,168,1,2]` 即成功,可证;官方例程 `EXAM5_TCP_Server/main.c:39 CH395IPAddr={192,168,1,101}` 同向)。⚠ 2026-10-11 复核:手册 §5.8 的总括"所有包含 IP 输入或输出的命令,字节顺序均为 IP 低字节在前"与官方例程自相矛盾,本仓以实测 + 例程为准;**端口不在此列**——§5.25/§5.26 明确低字节在前,驱动 `ch395f_set_des_port`/`set_sour_port` 即如此且工作正常,勿把"大端数组"套到端口上。于是芯片把 SYN 发往 `2.1.168.192`——一个跨网段、不可达的地址,SYN 被网关丢弃 → 无 SYN-ACK → `CONNECT TIMEOUT`。 - -**误导性排查**:`CMD_GET_REMOT_IPP_SN` 回读的是**小端**字节序(`ch395f.c` 注释 "IP和端口均为低字节在前")。把写反的 `[2,1,168,192]` 读回再按小端解释,恰好又得到 `192.168.1.2`,使回读日志看起来"目的地址正确",掩盖了真实写反。 - -### 解决 -1. `net_connect_locked()` 提取 `remote_ip_arr` 改为 **大端**:`arr[0]=(ip>>24)&0xFF … arr[3]=ip&0xFF`(ip 为网络字节序 s_addr)。 -2. 同样修正 UDP 发送路径 `net_send_locked()` 中 `set_des_ip` 的目的 IP 数组(同样低字节在前 bug)。 -3. 诊断回读 `GET_REMOT_IPP` 时按其小端语义还原:`dip=(rip[3]<<24)|…|rip[0]`,再按 `>>24…` 打印,方可显示芯片实际目的 IP。 - -### 注意事项 -- `net_inet_addr` 返回网络字节序;`sin_addr.s_addr` 一律按网络字节序理解。 -- 任何写 CH395F **IP 四字节数组**(set_des_ip / set_ip_addr / set_gwip_addr / set_mask_addr)都传**点分顺序数组**,与 `test_parse_ip` 产物一致;**16 位端口一律低字节在前**(§5.25/§5.26),两者口径不同。 -- 回读类命令(GET_REMOT_IPP / GET_IP_INF 等)若文档标注"低字节在前",打印时务必先按小端重组再格式化,否则会误判。 - -### 对应文件 -`Drivers/BSP/NET/net_socket.c`(`net_connect_locked` 提取 `remote_ip_arr` 大端;`net_send_locked` UDP 目的 IP 同样修正;`[CONN]` 回读按小端重组);`Drivers/BSP/NET/net_types.h`(`remote_ip_arr` 注释改为大端);`test/board/ch395f_test_task.c`(`test_parse_ip` 可作为大端构建范本)。 - -### 发现时间 -阶段 7 NET 层 TCP Client 上板实测,2026-08-25。 - ---- - -## Trap 17:单连接模式 client 重连被上一条连接的残留 DISCONNECT 秒断 - -### 现象 -阶段 7 TC701(首次 TCP Client 连接 + 回显)正常;TC702(close 后同 Socket 0 重连 ×3,每轮不同本地端口)每次都 `[CONN] openOK` 成功、但紧接着 `sock0 DISCONNECTED, releasing`,连接被秒断,PC 端看到"已建立连接但对端立即关闭、未收到数据"。阶段 6 的 `auto_relisten`/断线处理逻辑同源,但 `auto_relisten` 标志本身不是元凶(client 该标志为 0)。 - -### 根因 -单连接模式只有 Socket 0,新旧连接复用它。`net_close_locked()` 发 `close_socket()` 后芯片要等对端 FIN/ACK 才把 Socket 置 `SOCK_CLOSED` 并上报 `DISCONNECT` 中断(耗时数毫秒)。TC702 几乎在 TC701 关闭同时复用 Socket 0 去 `tcp_connect`:旧连接的 `DISCONNECT` 中断在 TC702 已 `ESTABLISHED` 之后才到达,`net_poll()` 的 `handle_disconnect_event()` 把状态置 `CLOSED` → 新连接被误判为"刚建立就断开"。`GET_REMOT_IPP` 回读仅确认目的地址正确,掩盖不了此事件串扰。 - -### 解决 -1. `net_close_locked()`:`close_socket()` 后轮询 `ch395f_get_socket_status()` 直到 `CH395F_SOCKET_CLOSED`,再读 `ch395f_get_sock_int_status()` / `ch395f_get_glob_int_status_all()` 清掉中断状态(上限 500ms,LAN 通常几毫秒),彻底排空上一条连接残留的 `DISCONNECT`。 -2. `net_connect_locked()`:`open_socket()` 成功后再次读空本 Socket 中断状态,作为双保险。 - -### 注意事项 -- 任何"关闭→复用同一 Socket 0 重新建连"的场景都必须先排空旧连接的中断,否则旧 `DISCONNECT` 会串扰新连接。 -- 若关闭后阻塞等待影响实时性(多连接模式另有 Socket),应改为非阻塞的"代次/epoch"机制;单连接模式直接排空即可。 -- `net_close_locked` 内阻塞 `osDelay` 仅在 netTask 上下文,单连接模式无其它并发 Socket,安全。 - -### 对应文件 -`Drivers/BSP/NET/net_socket.c`(`net_close_locked` 排空残留 DISCONNECT;`net_connect_locked` open 后清中断)。 - -### 发现时间 -阶段 7 NET 层 TCP Client 上板实测,2026-08-25。 - -## Trap 18:多 Socket 模式切换 + 8 个 Socket 缓冲分配 - -### 现象 -阶段 7 仅测了 Socket 0。要验证 Socket 1~7 也能作为 client,当时**推论**必须把 `FUN_PARA` 从"单 Socket 模式"切到"多 Socket 模式",否则 `open_socket(1~7)` 被芯片拒绝/无响应,1~7 完全不可用。 - -⚠ **2026-10-11 复核:这句"必须切多 Socket 模式"是当时的前置推论,不是实测记录**——本段无串口日志摘录、无 TC 编号、无主机侧独立证据;且当年 Socket 1~7 **根本没有显式分配缓冲**(`net_init` 注释"先不配置 buffer"、MSS=1460),按 Trap 01/02 未配缓冲的 Socket 中断不触发,混淆因子未被排除。bit0 语义见本节末「定案更正」。 - -### 根因 -`CMD_SET_FUN_PARA` 的 **bit0** 决定 Socket 数量模式:`0`=单 Socket 模式(仅 Socket 0,8KB 缓冲全归它);`1`=多 Socket 模式(Socket 0~7 全部可用,缓冲需自行分配)。之前 `net_init` 用 `0x08`(bit0=0)即单 Socket 模式,故 1~7 不存在。注意 bit1(TCP Server 单/多连接)与 bit0 是两回事:本方案只翻 bit0(多 Socket),bit1 保持 0(TCP Server 单连接),即"8 个 Socket 可用,但 Socket 0 作服务端时只接 1 客户端"。 - -⚠ **此"根因"已于 2026-10-11 证伪**(bit0 在所有版本官方手册里都是「保留,必须为0」;官方例程 bit0=0 下 8 个 Socket 照常可用)。当年真正起作用的是下面第 2 条的**缓冲显式分配**,不是 bit0。完整证据见本节末「定案更正」。 - -### 解决 -1. `net_init`:`ch395f_set_fun_para(0x09)`(bit0=1, bit1=0, bit3=1)。⚠ **2026-10-11 已改回 `0x08`**(bit0 保留必须为 0,见「定案更正」)。 -2. `ch395f_init()` 之后,循环 0~7 用 `ch395f_set_send_buf(sock, i*6+4, 2)` / `ch395f_set_recv_buf(sock, i*6, 4)` 给每个 Socket 分配独立收发缓冲。按`docs/芯片手册/CH395F.md` §5.45(48 块 × 512B = 24KB)+ §9.2.8 最优分配:每 Socket 6 块 = 接收 4 块(2KB) + 发送 2 块(1KB),`ch395f_set_tcp_mss(1024)`,恰好用尽 0~47 块、互不重叠。 -3. MSS 选取依据 §9.2.8:接收缓冲(2KB) ≥ 2×MSS(1024) 满足建议、≥ MSS 满足必须;发送缓冲(1KB) ≤ 8KB 满足必须。MSS 取 1024 为 8-Socket 共享 24KB 时的最大值。 -4. `net_init` 缓冲分配顺序遵循 Trap 01:先配数据 Socket,再配监听 Socket 0(本 Net 层 Socket 按需 open,故统一在 init 一次性配好)。UDP 路径 `net_bind()`(`net_socket.c:1000-1014`)中对 Socket 4~7 的兜底重设也同步为同一布局。 -5. PC 端 `tcp_server` 改为每连接一线程的并发回显(`--max-conn` 为并发计数),否则 Phase 7 全量连接会触发累计计数上限而拒掉 4~7。 - -### 注意事项 -- 4~7 的缓冲必须显式分配,否则其 CONNECT/RECV 中断不触发(Trap 01/02)。 -- 上述布局**以`docs/芯片手册/CH395F.md` §5.45 的 48×512B 缓冲几何为准**,并已由 Phase 7 TC704(40/40 PASS)实测确认块 0~47 全部有效(此前文档中"32×1KB"记录有误)。Socket 4~7 分配与 8 路并发回显均验证通过。 -- 多 Socket 模式下 TCP Server 单连接语义不变:Socket 0 监听且连接停留在本 Socket,Sockets 1~7 由应用当作独立 client 使用。 -- `net_poll` 用 `GET_GLOB_INT_STATUS_ALL`(2 字节,支持 0~7)并遍历 0~7,Socket n 中断位即 `bit(n+4)`(Socket 0~3→bit4~7,Socket 4~7→bit8~11),原代码正确无需改动。 - -### 对应文件 -`Drivers/BSP/NET/net_socket.c`(`net_init` FUN_PARA + 缓冲分配 + MSS);`test/board/net_test_task.c`(新增 TC704 八 Socket 并发回显);`test/host/ch395f_socket_test.py`(`tcp_server` 并发 + `--max-conn`)。 - -### 定案更正(2026-10-11):`FUN_PARA` 取 **0x08**,bit0 与 Socket 个数无关 - -**结论**:`net_init` 用 `ch395f_set_fun_para(0x08)`(bit3 SOCKET_CLOSE=1、bit0=0)。本节"bit0=单/多 Socket 模式"的说法**无任何出处,已撤回**;当年让 Socket 1~7 真正可用的是上面第 2 条的**缓冲显式分配**,不是 bit0。 - -**证据(一手,按强度排序)**: - -1. **官方手册位表**:本机 `D:\Code\DTU\使用的芯片手册\以太网控制器_CH395F(1).pdf`(WCH《CH395 手册 1》V2.2,页眉 `https://wch.cn`)§5.52 表跨 PDF 第 21→22 页,bit0 行原文 `0 - - 保留,必须为0`;bit2、`[5:6]`、`[31:8]` 同为保留;注释①~⑥ 无一处提 Socket 数量。官方现行版 V2.3 逐位一致。旧版(V1C、英文 V1E、2020 版 CH395L/Q)bit0 **同样是保留**(旧版多出的位是 bit2=LOW_PWR,V2.2 起收回为保留)→ **任何版本都不存在"bit0=多 Socket"这一行**。 -2. **官方 SDK 位宏**:`CH395EVT\EVT\EXAM\PUB\CH395INC.H:349-353` 只定义 bit1/3/4/7/8,**没有 bit0 宏**。 -3. **官方例程直接反例**:`EXAM5_TCP_Server\main.c:296` 传 `0x02`(bit0=0),同文件 `:337-359` 给 Socket 0~7 全部配缓冲 → **bit0=0 下 8 个 Socket 照常可用**;`EXAM11_FTPClient\FTP\CH395.C:319` 官方直接传 `0x08, 0x00, 0x00, 0x00`(与本仓取值逐字节相同);全 EVT 包 grep `SetFunPapr` 仅 3 处(0x02 / 0x02 / 0x08),**无一处 0x09**。 -4. **8 Socket 是芯片原生能力**:`docs/芯片手册/CH395F.md` 命令章前言「CH395 内部可以同时提供 8 个 Socket,其索引值分别为 0、1、2、3、4、5、6、7」;Socket 类命令一律 `_SN`(Socket 索引)寻址。 -5. **真正的门槛是缓冲**:`docs/芯片手册/CH395F.md` §5.45「如果单片机需要用到的 Socket 个数大于 4,需要重新对缓冲区进行分配」——正是本节第 2 条措施。 -6. **概念讹传源**:手册里唯一的"多"字样是 bit1 `FUN_PARA_FLAG_TCP_SERVER`(TCP 服务器**多连接**,全局配置),`docs/芯片手册/CH395F.md` §9.2.6「默认的设置下,服务器多连接功能被关闭……每个 Socket 只可以建立一个 TCP 连接」——单/多之分指**一个服务器能接几个客户端**,不是"能用几个 Socket"。 -7. **互联网侧**:多轮检索无任何来源称 bit0 为单/多 Socket 模式,无公开代码传 0x09;CH390/CH392/CH397 同族也无此位可串源。 - -**为什么 0x09 当年"看起来有效"**:芯片忽略了保留位(Trap 25 旁证:bit0=1 下 sock1/2/3 并行正常)。TC704 40/40 PASS 只证明"0x09 + 缓冲布局"通过,**不构成"0x08 下不通过"的对照**(与 bit0 结论出自同一提交,属自证)。留着 bit0=1 的代价是违反保留位规定——后续固件版本一旦启用该位,现网设备静默失效。 - -**实测闭环(2026-10-11 20:18,N-31 已销项)**:`FUN_PARA=0x08` 下跑 TC-NET-704(`PHASE7_MULTI_COUNT` 降到 5:E-23 未修,`NET_MAX_SOCKETS=8` 而 FTP/对时/远显三个监听常驻占 3 个 fd)→ **Phase 7 34/34 全通过(failed=0)**:5 路并发全部 ESTABLISHED、每路 64 B 回显字节级比对 OK;芯片 Socket 实测落在 **sock2 / sock4 / sock5 / sock6 / sock7**(0/1/3 被三个监听占用)→ **Socket 4~7 在 bit0=0 下完全可用**,与手册口径一致,定案成立。烧录后回读 `No difference found`。 - -⚠ **台架前置(本轮踩到,务必先排)**:① PC 的以太网适配器落在 **Public** 防火墙配置文件 → 入站 8081 被静默丢弃,症状是 TC701/702/704 **全部** `CONNECT TIMEOUT`(而 PC→设备 21 端口正常、ping 通、ARP 正常),**极易误判成"0x08 毁了 client"**;② 上一轮遗留的 `tcp_server` 进程仍占着 8081,新起的服务器绑不上端口却不报错。两条都排除(临时关 Public 配置文件 + 清残留 python 进程)后才拿到上面的结果——**判"芯片行为"前先确认对端真的收到了 SYN**(Trap 25 总纲铁律的又一次命中)。 - -### 发现时间 -阶段 7 扩展 8 Socket 测试,2026-08-25。 - -## Trap 20:发送缓冲区单次写入超过硬件 FIFO 上限(写穿) - -### 现象 -大文件/批量发送(如阶段 8 的 100KB,或 FTP 上传)时,数据在中间某处错乱、连接被对端 RST、或在 `WRITE_SEND_BUF` 后芯片不再产生 `SEND_OK`,表现为发送卡死或后续字节丢失。该问题只在单次发送长度 > 1KB 时稳定复现。 - -### 根因 -`net_send_locked`(TCP)与 `net_sendto`(UDP)在分块时以 **`NET_DMA_MAX_PAYLOAD`(2KB) / `NET_SEND_BUF_SIZE`(4KB)** 作为单次写长度上限,但 CH395F 每个 Socket 的**硬件发送 FIFO 只有 `CH395F_SEND_BLOCKS × CH395F_RAM_BLOCK_SIZE` = 2×512 = 1KB**(2026-09-22 起为固定布局 2×512=1KB,与 MSS 解耦,见 Trap 21;`ch395f.h`)。当某次调用 `net_send/sendto` 传入 >1KB 数据,驱动会在 `send_ready` 置位后一次性 `WRITE_SEND_BUF(2KB/4KB)`,直接**写穿 1KB 发送 FIFO**(超出部分覆盖/丢失或破坏 FIFO 指针),导致后续数据错位。原代码误把"SPI DMA 缓冲上限(2KB)"或"软件 API 缓冲(4KB)"当成硬件发送缓冲上限。 - -### 解决 -1. `ch395f.h` 新增硬件缓冲字节宏:`CH395F_SEND_BUF_SIZE_BYTES = CH395F_SEND_BLOCKS × CH395F_RAM_BLOCK_SIZE`(=1KB),并补充 `CH395F_RECV_BUF_SIZE_BYTES`。 -2. `net_socket.c` 新增 `NET_SEND_CHUNK_MAX = min(NET_DMA_MAX_PAYLOAD, CH395F_SEND_BUF_SIZE_BYTES)`,即**以硬件发送 FIFO 为硬上限**(当前 1KB < 2KB DMA 载荷,故生效 1KB)。 -3. `net_send_locked` 与 `net_sendto` 的单次写长度上限均由原来的 `NET_DMA_MAX_PAYLOAD`/`NET_SEND_BUF_SIZE` 改为 `NET_SEND_CHUNK_MAX`。`send_ready` 门控保证只有 FIFO 空闲时才写入,故写入 ≤1KB 安全(写满后等 `SEND_OK` 再写下一块)。 -4. 接收方向不受影响:`net_recv_locked` 只读 `ch395f_get_recv_len`(≤ 接收 FIFO 2KB)并受调用方 `len` 截断,不会写穿。 - -### 注意事项 -- 该上限随 `CH395F_SEND_BLOCKS`(2026-09-22 起固定 2 块=1KB,与 MSS 解耦,见 Trap 21);若将来发送缓冲改变,`NET_SEND_CHUNK_MAX` 自动跟随(仍取硬件缓冲与 DMA 载荷的较小者),无需改此处逻辑。 -- 调用方(阶段 8、FTP `lftpd`)仍可按任意长度调用 `net_send`,实际分块由驱动保证 ≤ 硬件 FIFO。 - -### 对应文件 -`Drivers/BSP/CH395F/ch395f.h`(`CH395F_SEND_BUF_SIZE_BYTES` 等);`Drivers/BSP/NET/net_socket.c`(`NET_SEND_CHUNK_MAX` + `net_send_locked`/`net_sendto` 分块上限)。 - -### 发现时间 -阶段 8 大文件传输实现,2026-08-26。 - -## Trap 21:接收缓冲"恰好填满 + 排空"窗口冻结(TC-NET-604)—— **结论:非缺陷(2026-09-23 结案)** - -### 结案结论 -原报告(阶段 8 大文件传输期间:同连接前序突发后 4096B 突发"前 2KB 正常回显、其余静默无回显")**在当前固件上无法复现,判定为非缺陷**。可信判据(产品固件 + 真实 lftpd FTP 上传,成功 = FTP 协议结果 + **上传后 RETR 回读比对 SHA-256**)实测:**227 轮全过**(8KB/64KB/256KB/1MB;含零间隔快速重连与 100 轮浸泡),并发生产 UI 流(`:7003`)600 帧 CRC 全过,设备侧日志无 FAIL/ERROR/WARN。 - -### 为什么原台架会"复现"——**教训,务必先读** -2026-09-22"精确定位轮"给出的**全部"冻结/停摆/WEDGE"结论均为台架假象,已撤回**: -1. **测试程序自吞数据**:台架配方里的固件内读者(Phase 9/11 的 64B 读、固件探针读)把对端数据读走后**不回显**——PC 端自然少收,被误当成"设备停摆"。实测短收量**恰等于被吞字节数**(例:`4736−4608 = 128 = 2×64` 探针读);所谓"V1 配方必冻结"就是 Phase 11 吞掉整包(设备日志 `avail=64 len=64`,PC 侧下一轮 `rx=0/64`)。对照组(仅 Phase 6、无额外读者)40/40 全过。 -2. **判读工具误判**:`probe_classify.py` A/C 模式把**通过**的运行报成 `STALL`(C0 的脚本层实测为 `1460/1460` + 尾段 `2100/2100`,且内容逐字节匹配)。 -3. 由此派生的"部分排空致命 / 再武装区间 ≥160ms / 相位刀口 / C 配方复现生产停摆"等结论**全部作废**;MSS=512 实验的"停摆"亦未经可信判据复核。 - -### 现在怎么做(台架纪律) -1. 判读一律用 `MDK-ARM/bench_verdict.py`:**以脚本层 STEP 行为真值**,并扣除固件读者吞噬的字节; -2. 冻结/卡死类问题走**真实生产路径 + 端到端内容校验**:`MDK-ARM/ftp_freeze_probe.py`(FTP 上传 + RETR 回读 SHA-256);卡住时用**控制通道 NOOP** 区分"数据面卡死"与"整层卡死"; -3. **禁止把"固件内非回显读者"与固定期望值混用在同一配方里**——这是本轮最大的教训。 - -### 保留的代码变更(与被撤回的机理无关) -缓冲块布局与 MSS 解耦(`ch395f.h`)、超限读 OVERSIZE 金丝雀(`ch395f.c`)保留:属防御性/可观测性改进,不回退。 - -### 相关 -- 真实缺陷见 **Trap 23**(上传中止后首笔复连上传被静默截断,已可信复现并修复); -- 发现:2026-08-26(阶段 8);结案:2026-09-23。 - -## Trap 22:快速复连丢 CONNECT/DISCONNECT 中断 → 监听静默死亡(2026-09-23 复现并修复) - -### 现象(生产固件实测) -单连接监听 socket 在快速复连后**永久失效**(后续 SYN 被拒或超时,10s 内不自恢复),而芯片/网栈其余部分完全正常(其他监听端口照常工作)。两种表现: -- **拒连(ConnectionRefused)**:芯片完成握手但 **CONNECT 中断未上报** → 控制块滞留 LISTENING;或本层因 **DISCONNECT 中断丢失**仍以为有客户端 → 两种情况都会让芯片侧那条连接**一直占住单连接监听**,后续 SYN 全被拒;且丢事件后本层收不到任何中断,无法靠事件自愈。 -- 应用侧连锁:`net_send` 被拒(send -1);或应用读不到 EOF(其 `g_client` 永不释放,因而**永不调用 accept**)。 - -### 复现(可信用例,纯客户端判据;工具 `MDK-ARM/tcp_reconnect_probe.py`) -- **确定性复现**:产品固件 + 待机 60s 后,对 UI 监听 `:7003` 以 300ms 间隔快速复连 → 修复前**首 2 次成功、之后永久拒连**(10 轮 2/10,10s 恢复窗口内无自恢复); -- **概率复现**:FTP 控制 `:21` 以 100ms 间隔连 50 次 → 修复前**第 29 轮起永久拒连**; -- 对照:FTP `:21` 在 0ms / 600ms 间隔下通过;`:8888`(对时)同期亦正常 → 触发与"复连间隔 <~1s + 轻负载(待机)"相关,与 2026-09-22 记录的边界一致。 - -### 机制(设备侧日志实证) -1. 芯片丢 **CONNECT** 中断 → 无 `standalone accept`,控制块滞留 LISTENING;或丢 **DISCONNECT** 中断 → 本层仍以为 ESTABLISHED; -2. 此后本层**收不到任何中断**(芯片静默持有/丢弃该连接),纯事件驱动无路可走——实测把 `handle_disconnect_event` 的 LISTENING 分支从"仅告警"改为"重建监听"后**仍然无效**(因为连 DISCONNECT 都没来); -3. 故必须**周期回读芯片状态对账**才能发现失同步。 - -### 修复(已实施,`Drivers/BSP/NET/net_socket.c`) -1. **周期对账 `net_reconcile_tick()`**(`net_process_messages` 每轮调用;周期 200ms):对在用 socket 回读芯片状态,修正三类失同步—— - ① 本层 ESTABLISHED 而芯片已无连接 → 按断开处理(应用读到 EOF 后自动重监听); - ② 本层 LISTENING 而芯片已完成握手 → 补做 standalone accept(复用 `handle_connect_event`); - ③ 本层 LISTENING 而芯片监听已消失 → 重建监听(`net_relisten_try`)。 - **去抖 + 冷却**:同一失配需连续 2 次(≈400ms)才动手;同一 socket 动手后冷却 1s。 -2. `handle_disconnect_event` 的 LISTENING 分支:由"仅告警"改为"回读芯片状态——已不可监听即重建"(仍在监听则判陈旧事件丢弃)。 - -### 踩坑记录(务必先读) -最初的修法放在 **accept 路径**(每次 accept 时回读状态并自愈):单次状态读会把"刚 open/listen 尚未生效"的瞬态误判为失配 → 监听被反复重建直至 `relisten give up` → **三个监听口全部死亡(风暴)**。改为"周期对账 + 去抖 + 冷却"后消除。 -**结论:凡基于单次状态读的重建动作,必须去抖。** - -### 验证(修复前 → 修复后) -| 用例 | 修复前 | 修复后 | -|---|---|---| -| 待机后 UI `:7003` 快速复连 ×10 | 2/10(之后永久拒连) | **10/10** | -| FTP `:21` 间隔 100ms ×50 | 28/50(第 29 轮起死亡) | **50/50** | -| FTP `:21` 间隔 0ms / 600ms ×50 | 通过 | **50/50 / 50/50** | -| UI `:7003` 间隔 200ms ×20 | — | **20/20** | -| FTP 正常上传 10×64KB(SHA-256 往返) | 通过 | **10/10** | -| 上传中止 RST / FIN 回归(Trap 23) | 通过 | **5/5 + 5/5** | -| 设备侧 `relisten give up` / 重建风暴 | — | **0** | - -自愈实测计数(整轮会话):`chip closed -> disconnect` **12** 次、`connection completed w/o CONNECT irq -> accept` **1** 次、`listen lost -> relisten` **1** 次——三类失同步均真实出现并被自愈;`relisten give up` 0 次。 - -### 对应文件 -`Drivers/BSP/NET/net_socket.c`(`net_reconcile_tick` / `handle_disconnect_event` LISTENING 分支);`MDK-ARM/tcp_reconnect_probe.py`(可信用例);`docs/任务规划/README.md` N-6(✅ 已销项,2026-09-26 清理)。 - -### 发现时间 -2026-09-22(台架偶发 1/1)→ 2026-09-23(生产固件确定性复现 + 当日修复闭环)。 - -## Trap 23:上传中止后首笔复连上传被静默截断(≤2048B)并误报成功 - -### 现象 -客户端上传(STOR)中途断开(用户取消 / 网络断;**RST 或优雅 FIN 均可**),**紧随其后的第一笔复连上传被截断到 ≤2048 字节(= 芯片单 socket RX 缓冲大小),且服务端回报 226 上传成功** → 客户端(ftplib 等)据此认为上传成功 = **静默数据损坏**;第二笔及以后恢复正常。 - -### 证据(可信复现:生产固件、零固件探针;工具 `MDK-ARM/ftp_abort_probe.py`) -- 判据 = FTP 协议结果 + **紧接一笔正常上传的 SHA-256 往返校验(STOR→RETR 比对)** + 控制通道 NOOP 存活探测; -- **RST 中止:10/10 复现**首笔复连上传截断(请求 4096B → 设备日志 `UPLOAD '/ab_ok.bin' 2048 bytes OK`); -- **优雅 FIN 中止:5/5 同样复现** → 与 RST/FIN 无关,凡"数据未发完即断开"均触发; -- 中止那一笔自身也被记为 `UPLOAD ... 2048 bytes OK` + 226(FTP 无长度声明,语义边界;但客户端会误判成功); -- **对照(无中止)**:177 轮(8KB/64KB/256KB/1MB,含 100 轮零间隔浸泡)全部 SHA-256 匹配(`MDK-ARM/ftp_freeze_probe.py`),并发 UI 流 600 帧 CRC 全过 → 中止路径是唯一触发条件。 - -### 机制(设备侧日志实证) -1. 中止时数据 socket 被释放(RST 时芯片上报 `sock4 TIMEOUT`),net 层随即 `auto-relisten ok` + `relisten retry ok`(**监听端口复用**); -2. 下一笔连接:`sock4 standalone accept` → `CMD STOR` → **紧接着一条陈旧 `sock4 DISCONNECTED, releasing (chip tcp=0x00)`**(旧连接收尾状态落在新连接上); -3. lftpd 数据读循环据此读到 EOF → 把已到的 2048B 当作整个文件 → `UPLOAD ... 2048 bytes OK` + 226; -4. 再下一笔连接时芯片状态已干净 → 正常(故为"一次性"而非持久故障)。 - -### 影响 -- **静默数据损坏**:上传被中断后"重试的第一笔"会生成截断文件且客户端收到成功回报(FTP 客户端通常不做长度校验)→ 现场可能表现为"上传的文件损坏/不完整"; -- 主风险面 = 生产路径 STOR(上传);RETR(下载)不涉及; -- 与 TC-NET-604 的关系:本条(Trap 23)是**独立、已可信复现并已修复**的真实缺陷;TC-NET-604 已结案为非缺陷(见 Trap 21)。现场"上传失败/文件不完整/像卡死"类报告,应优先用本条的判据复核:**FTP 协议结果 + 上传后 RETR 回读 SHA-256**(+ 卡住时控制通道 NOOP 探活)。 - -### 复现 -``` -& '.venv\Scripts\python.exe' MDK-ARM\ftp_abort_probe.py --rounds 10 --send 8192 --gap 1 -& '.venv\Scripts\python.exe' MDK-ARM\ftp_abort_probe.py --rounds 5 --send 8192 --gap 1 --graceful -``` - -### 修复方向(待办 N-8,全部闭环) -1. ✅(2026-09-23 一阶段)**relisten 前让芯片 socket 真正沉降**——隔离期:持续排空 RX + 主动 close + 等 `CLOSED/TCP_CLOSED`,隔离满 400ms 才 `open+listen`; -2. ✅(2026-09-23/24)**新连接的陈旧中断防御**——陈旧 DISCONNECT 以芯片状态回读甄别(D20:芯片 TCP 仍 ESTABLISHED 判陈旧丢弃)+ 释放残留经两级沉降/隔离(100ms/400ms)后不再投递给下一条连接; -3. ✅(2026-09-25)**应用侧兜底**——异常释放(TIMEOUT/RST)的挂起 recv 以 `NET_ERR_CONNRESET` 了结(正常 FIN 仍回 0——协议边界)→ STOR 异常结束回 450 并删除截断文件; -4. ✅ **回归判据**——`ftp_abort_probe.py` followup:RST 10/10 + FIN 5/5(一阶段)、8/8 + 5/5 + 5/5(二阶段);`--check-abort` 断言中止 STOR 应答码(RST→450 / FIN→226 边界);177 轮对照当时全匹配,其后各轮正常路径回归全过。 - -### 发现时间 -2026-09-23(可信复现首轮:真实生产路径 FTP 中止/复连场景,由 `MDK-ARM/ftp_abort_probe.py` 自动判定) - -### 修复实施与验证(2026-09-23 当日闭环) - -**真因(设备侧日志实证,含正常对照)**: -1. 客户端中止上传时对端**仍有在途数据**;芯片给出 `sock4 TIMEOUT`(此刻 MCU 侧 `recv=0`),net 层随即释放该 socket 并复监听; -2. 那些**在途段随后才落进芯片 RX 缓冲**(约一个缓冲 = 2048B 残渣),而正常传输时 DISCONNECT 到达刻芯片里也仍有数据(对照实测 `recv=64512/65536`,由应用读空 → 完整); -3. 中止后立即 `open+listen`,残渣被**下一条连接当作自己的数据读走**:新上传只“收到”2048B 就被判定结束并回报 226(首笔复连必中招,第二笔起芯片已干净 → 恢复正常)。 - -**修复(`Drivers/BSP/NET/net_socket.c`,仅异常释放路径,正常 FIN 路径零改动)**: -1. `handle_timeout_event`(ESTABLISHED 超时释放)置 per-fd **隔离标记** `s_relisten_quarantine[]`; -2. `net_relisten_locked` 对带隔离标记的 socket:**隔离期内持续排空芯片 RX**(新增 `net_drain_chip_rx()`,分块读空、零新增静态缓冲)+ **主动 `ch395f_close_socket` 并排空该 socket 中断** + 等芯片回到 `CLOSED/TCP_CLOSED`,隔离满 `NET_RELISTEN_QUARANTINE_MS`(400ms) 才 `open+listen`; -3. 上限 `NET_RELISTEN_SETTLE_MS`(800ms) 强制放行 —— **必须小于** `NET_RELISTEN_DEADLINE_MS`(2000ms):实测取 2000ms 会把重试预算耗尽 → 重试轮询"give up" → **监听器死亡(:21 拒连)**,已修正并在注释中留痕; -4. 隔离状态在 `net_relisten_clear` 内一并清除(关闭/新建 fd 时不残留)。 - -**验证(可信判据:FTP 协议结果 + 紧接一笔上传的 SHA-256 往返 + 控制通道存活)**: -- 修复前:RST 中止 **0/10**、优雅 FIN 中止 **0/5**(首笔复连上传全部被截断且回报成功); -- 修复后:RST 中止 **10/10**、优雅 FIN 中止 **5/5** 全部通过; -- 正常路径回归:30×64KB 零间隔 **30/30**、20×64KB **20/20**,SHA-256 全匹配; -- 启动门禁:`App init OK` / `net init: OK`、FORBID 0 行。 - -### 第二阶段:正常释放(FIN)路径——边界甄别与 100ms 沉降(2026-09-24) - -**边界再甄别(现场"1~2s"时间假设被推翻)**:中止后 gap 500/2000/5000ms 台架复测 → 设备实存尺寸**全部仍 2048**(各 4/4,合计 12/12)→ **与时间无关**;监听重开本征即时(`t_accept` 30~47ms)。真实边界: - -| 维度 | 结论 | 证据 | -|---|---|---| -| 触发 | 设备侧**收过数据**的数据连接被对端关闭(纯 FIN 即触发,不要求 RST) | 纯 `close()`(FIN)4/4 复现;RST 变体 5/5 | -| 受影响路径 | 仅**正常 DISCONNECT 释放路径**缺芯片状态沉降 | RST(TIMEOUT)路径已有一阶段 400ms 隔离保护 | -| 投递形式 | 芯片上一条连接的收尾残留,投递给**复用同一 fd 的下一条**连接(accept 后约 10~20ms 一条伪 DISCONNECT) | 设备日志:`standalone accept` → `DISCONNECTED, releasing (chip tcp=0x00)` | -| 影响面 | **一次性**(首条新连接中招,第二条起干净);同 fd 非 FTP 服务(`:7003` UI sink)不受影响(其 socket 不按传输复用) | 一次性 3/3 全对;`:7003` 复用 8/8 全过 | -| 时间 | 无关(gap 5s 仍失败)→ 现场"1~2s"只是人工重触的常见窗口,不是芯片机制 | gap 500/2000/5000ms 共 12/12 全部失败 | - -**修复(第二阶段,`Drivers/BSP/NET/net_socket.c` +36/−7,netTask 串行上下文,零新增缓冲)**: -1. relisten 前沉降分两级:异常释放(芯片 TIMEOUT)=清理 + 最短等 400ms(`NET_RELISTEN_QUARANTINE_MS`,一阶段行为不变);正常释放(芯片 DISCONNECT)=清理 + 最短等 **100ms**(`NET_RELISTEN_GRACEFUL_MS`,新增); -2. 正常释放沉降**仅在本连接收过数据时登记**(`recv_bytes != 0`):纯连接即关(Trap 22 的 0ms 复连用例)与只发不收的下载连接无残留 → 免沉降保住快速复连时序; -3. 日志区分 `relisten settle`(正常)/ `relisten quarantine`(异常),便于台架分辨触发路径。 - -**沉降值实测悬崖**(只清理不等待 = 0ms):0ms **无效**(4/4 仍截断)→ 30ms 仍失败(4/4)→ **100ms 通过**(多轮稳定)→ 取 100ms(对 30ms 重试节拍 `NET_RELISTEN_RETRY_MS` 留 3× 余量)。 - -**验证(第二阶段后;设备日志 `UPLOAD '' N bytes OK` 为权威真值)**: - -| 用例 | 修复前 | 修复后 | -|---|---|---| -| FIN 关闭后**首笔**上传 ×8 | **0/8**(实存 2048 + 回报 226) | **8/8**(实存 4096,sha OK) | -| 纯 `close()` 变体 ×4 | 中招 | **4/4** | -| `ftp_abort_probe --graceful` ×5 | **0/5** | **5/5** | -| `ftp_abort_probe`(RST)×5 | 5/5(一阶段) | **5/5** | -| 无中止对照 ×4 / 一次性连发 3 笔 ×1 | 4/4 / 第 1 笔坏 | **4/4 / 3/3** | -| 正常往返 freeze 4096×4 / 64KB×2 | 通过 | **4/4 / 2/2**(t_stor 0.68→**0.18s**,t_retr 1.13→**0.19s**) | -| 非 FTP `:7003` sink 复用 ×8(RST+FIN) | 8/8 | **8/8** | -| Trap 22:`:21` 0ms×50 / 100ms×50,`:7003` 300ms×10,`:8888` 0ms×20 | 50/50 / 50/50 / 10/10 / 20/20 | **50/50 / 50/50 / 10/10 / 20/20** | -| 启动门禁 / 控制台 / 堆 | — | `App init OK` / `net init: OK`,FORBID **0**,`CONSOLE dropped 0`,`heap free=8776 min_ever=4816` | - -**结论**:缺陷位于 **fd 复用生命周期**(芯片上一条连接的残留状态投递给下一条连接),不在 FTP 命令逻辑——lftpd 只是把它放大成误报 226。RST/TIMEOUT 半边由一阶段隔离处理,DISCONNECT 半边由本阶段沉降处理,合计覆盖全部"收过数据后关闭"的释放路径。已知残余边界:中止那一笔 STOR 自身仍回报 226(STOR 无长度声明、在途 FIN 与正常完成不可区分)——后续硬化见待办 #3。 - -**对应文件**:`Drivers/BSP/NET/net_socket.c`(`handle_disconnect_event` 沉降登记 + `net_relisten_locked` 两级沉降);`MDK-ARM/ftp_abort_probe.py` / `MDK-ARM/bench_diag/bench_diag_abort_gap.py`(边界甄别与回归判据)。 - -### 后续硬化(2026-09-25):异常结束回 5xx(待办 #3 闭环) - -**目标**:中止那一笔 STOR 自身不再误报 226——异常结束(连接重置/超时)时回 450 并删除截断文件,把"静默损坏"变成"显式失败"(纵深防御;客户端按协议重试)。 - -**实现(3 个工程文件 + 1 个台架脚本)**: -1. `net_types.h`:`net_sock_t` 增加释放来源标记 `released_abnormal`(RAM +8B:1B×8 socket,BSS,运行堆零影响); -2. `net_socket.c`:`handle_timeout_event`(异常释放/TIMEOUT)置 1;`handle_disconnect_event`(正常释放/DISCONNECT)置 0;`net_socket()`(新 fd)与 `net_relisten_locked` 成功尾(新监听)清零——新连接不得继承上一条连接的释放原因; -3. `net_socket.c` `net_pending_tick` 断连了结(③):排空缓冲后,标记为 1 以 `NET_ERR_CONNRESET` 了结(对端未发正常 FIN,不是正常 EOF),标记为 0 仍 EOF(0); -4. `lftpd.c` `receive_file`:`err < 0` 时 `unlink` 截断文件(原先残留于盘)并返回错误码 → STOR 处理器回 450; -5. `MDK-ARM/ftp_abort_probe.py` 增加 `--check-abort`:RST 中止要求中止 STOR 控制应答 4xx/5xx,优雅 FIN 要求 2xx(协议边界,见下)。 - -**协议边界(不可判定,留痕)**:FTP STOR 无长度声明,"在途 FIN 关闭"与"正常完成"**逐字节不可区分**(芯片均报 DISCONNECT、缓冲均排空)→ 优雅中止仍回 226(中止方客户端通常也不等应答);仅 RST/掉链(芯片 TIMEOUT)可识别 → 450。FIN 边界由客户端长度校验(SIZE/RETR 往返)兜底。 - -**各消费端负值处理复核**(均按"负值=结束"与 0 同路,行为不变): -- lftpd 控制通道 `read_line`:负值 → `line_len != 0` → 会话清理,与 0 同路(控制通道不 close,auto_relisten 恢复); -- time_sync(`:8888`):n<0 → break 弃连接,与 0 路径一致;8B 时间包已收齐时仍写 RTC; -- UI sink(`:7003`):DONTWAIT 非阻塞,不走 L3 pending,零影响; -- 测试套件:产品构建不启用;测试对端干净关闭,无影响。 - -**验证(硬化后,2026-09-25 台架)**: - -| 检查项 | 结果 | -|---|---| -| `ftp_abort_probe --check-abort --rounds 5`(RST) | **5/5**——中止 STOR → **450**(原 226),followup 5/5,控制存活 5/5 | -| 设备侧证据 | `sock4 TIMEOUT` → `[ERR][FTP] UPLOAD '/ab0001.bin' FAILED` → 450;followup `UPLOAD '/ab_ok.bin' 4096 bytes OK` | -| `--check-abort --graceful --rounds 5`(FIN) | **5/5**——中止 STOR → **226**(边界不变),followup 5/5 | -| 正常往返 freeze 4096×4 / 64KB×2 | **4/4 / 2/2**(SHA-256 全 MATCH) | -| Trap 22:`:21` 0ms×50 / 100ms×50,`:7003` 300ms×10,`:8888` 0ms×20 | **50/50 / 50/50 / 10/10 / 20/20** | -| 非 FTP `:7003` sink 复用(RST×4 / FIN×4,400 帧在途) | **4/4 / 4/4**(ack 2000/2000,LCD 3209/3209,CRC 全过) | -| 时间同步 `:8888` 真实收包 | `wall clock updated (epoch=…)` 写 RTC 成功,监听恢复 | -| 构建 / 回读 / 启动门禁 / 控制台 / 堆 | 0 错 0 警、`No difference found`、FORBID 0、`CONSOLE dropped 0`、`heap free=8776 min_ever=4816`(不变) | - -**对应文件**:`Drivers/BSP/NET/net_types.h`(`released_abnormal`)、`Drivers/BSP/NET/net_socket.c`(三处置位/清除 + `net_pending_tick` 了结分支)、`Drivers/BSP/NET/lftpd/lftpd.c`(`receive_file` 错误路径 unlink)、`MDK-ARM/ftp_abort_probe.py`(`--check-abort`)。 - ---- - -## Trap 24:MAC 写入顺序违反`docs/芯片手册/CH395F.md` §9.2.1(SET_MAC 必须在 INIT_CH395 之前) - -### 现象(静态审查发现;手册确证) -`net_init()` 原顺序为 `ch395f_init()`(CMD_INIT_CH395)→ `ch395f_set_mac_addr()`,与`docs/芯片手册/CH395F.md` §9.2.1 规定的初始化顺序**相反**。后果:INIT_CH395 已按 EEPROM 旧值加载 MAC,其后写入 EEPROM 的新 MAC 本次上电**不生效**(回落芯片内置/上次值,实际"滞后一次上电")→ UI 配置的 MAC 与设备实际在线 MAC 不一致,设备在线身份错误、网口排查困难。本轮未做"UI 改 MAC + 重启"台架复现,结论由手册条文确证。 - -### 根因 -`docs/芯片手册/CH395F.md` §9.2.1「初始化 CH395,必要操作」顺序为: -① `SET_MAC_ADDR` → ② `SET_IP_ADDR` → ③ `SET_GWIP_ADDR` → ④ `SET_MASK_ADDR` → ⑤ `INIT_CH395`。 -- §5.7:`SET_MAC_ADDR` 把 MAC **写入芯片内部 EEPROM**(约 30us); -- §5.13:`INIT_CH395` **初始化 CH395 的 MAC、PHY 以及 TCP/IP 协议栈**(约 5ms)。 - -即 INIT_CH395 会从 EEPROM **加载** MAC,故 SET_MAC 必须先于 INIT_CH395。原码把 MAC 放在 INIT 之后。 - -**加剧误排的注释**:`ch395f.c` 原把 `set_mac_addr`/`set_ip_addr`/`set_gwip_addr`/`set_mask_addr` 的限定条件标为"芯片已初始化"——与手册矛盾(这四个 SET 实际都在 INIT 之前下发),易被误读为"须在 init 之后调用"。 - -### 解决 -1. `Drivers/BSP/NET/net_socket.c` `net_init()`:把 `ch395f_set_mac_addr()` 从 `ch395f_init()` **之后**移到**之前**(置于 SET_IP/GWIP/MASK 之前,对齐手册步骤①); -2. `ch395f.c`:四个 SET 函数的限定条件注释统一改为"必须在 `ch395f_init()` 之前调用"; -3. `net_socket.h`:`net_init` 注释同步(MAC 于协议栈初始化**之前**下发)。 - -### 影响面 -- 仅 `net_init()` 一处调用(产品路径);改动为**纯顺序调整**,无新增/删除指令、无 RAM 变化; -- `net_stack_bring_up()` 在 `net_init` 后回读芯片 MAC 写 UI 影子(`net_cfg_set_active_mac`)——修复后回读值 = 实际在线 MAC,UI 显示与线上身份一致。 - -### 对应文件 -`Drivers/BSP/NET/net_socket.c`(`net_init` SET_MAC 顺序)、`Drivers/BSP/NET/net_socket.h`(`net_init` 注释)、`Drivers/BSP/CH395F/ch395f.c`(四个 SET 的限定条件注释);`docs/网络/BSD_Socket_API_函数调用关系.md`(§4.1 / §7 顺序)。 - -### 发现时间 -2026-10-04(静态审查项 P1-7;依据`docs/芯片手册/CH395F.md` §9.2.1 / §5.7 / §5.13 定案)。 - -## Trap 25:FTP 数据连接三连故障——丢 CONNECT 中断 / accept 竞态自毁缓冲 / 背靠背小段毒化 TX(N-30,2026-10-11 定案并修复,回归 4 轮 11/11) - -### 现象(生产 FTP 回归,多轮复现) -同一 FTP 会话内**第 2 次及以后**的数据连接(LIST/STOR/RETR 各一次)出现两类失败,两种面孔同一根因: -- **吞数据(SWALLOW)**:新数据连接建立成功(芯片 0x05/0x04 ESTABLISHED)、服务端 `net_send` 返回成功,客户端 20 s 收不到任何字节(无 RST、无 DISCONNECT,直到客户端放弃); -- **425**:新数据连接被接受后 ~10-20 ms 内出现 DISCONNECT(`chip tcp=0x00`),服务端 accept 失败 → `425 Can't open data connection`。(⚠ 本条当时的解读"芯片自杀连接"**有误**,见文末"三次定案"——实为客户端发完数据后的**正常 FIN** + 我方 accept 竞态自毁缓冲。) - -失败概率随**上一条数据连接的负载大小与间隔**变化(NAND 目录文件越多 LIST 越大越易触发),与提交历史无关——在 D20 基线 e5733b34 上以快速连发同样复现(D20 时代"11/11 通过"是慢速台架 + 干净目录的运气)。 - -### 定案实验(裸机芯片台架,剥离 OS/net/FTP) -实验分支 `n30-chip-bench`(worktree),`test/board/ch395f_reuse_bench.c` 在**内核启动前**直接调 `ch395f_*` 做受控矩阵:`{socket, close 后复开延迟, TX 长度, 关闭顺序}` 组合循环,每 50 ms 打印芯片两字节状态 + 中断寄存器;主机侧 `test/host/ch395f_reuse_host.py` 同步做纯 TCP 客户端。**OS/net/FTP 全部剥离后 bug 依旧复现** → 纯芯片操作时序问题,非上层逻辑、非芯片硬件缺陷。 - -关键现场(芯片日志): -``` -post-listen st=0x05/0x04 ← listen 返回成功,但 TCP 态停在旧连接的 ESTABLISHED(幽灵态) -unexpected int=0x10 while wait ← 只等来旧连接滞后的 DISCONNECT -no CONNECT within 10s ← 新连接的 CONNECT 中断永远没来(芯片状态却已是 0x04) -``` - -**芯片级规律**:CH395F 关闭连接后的拆链收尾窗口**变长且不可观测**——状态寄存器显示 CLOSED 持续 1 s 以上也不代表拆完(实测窗口可 >2 s 且时长不定)。窗口内重新 `open+listen`: -1. 新连接握手照常完成(状态寄存器 → ESTABLISHED)但 **CONNECT 中断被丢**(Trap 22 丢中断机制的窗口期形态)→ 事件驱动的 accept 永远等待 → 吞数据; -2. 或 listen 后状态直接假报 0x04(幽灵态),此时回读对端端口 = **旧连接端口**; -3. 或旧连接滞后的 DISCONNECT 打到新监听 → 触发 relisten 毁掉刚到的新连接 → 425。 - -### 已证伪的修复(全部经台架受控实验) -| 尝试 | 结果 | -|---|---| -| close 后固定沉降 100/300/800/2000/3000 ms | 仍挂(2 s 也挂过)——窗口变长,固定等待无保证 | -| Socket 轮换(sock3↔sock4 交替) | 仍挂——上一轮拆链尾巴跨 socket 毒化下一轮 | -| listen 后 close+重试 / 耐心等幽灵消退 | 持平对照组,无效 | -| 收发缓冲重分配(clear_recv_buf + set_*_buf) | 无效 | -| close 前 200 ms TX 排空延时 | 无效 | -| 禁用对账 tick(验证因果) | 仍挂——对账不是元凶 | - -### 有效修复(台架修复组 15/15,机制触发 5 次全部救回) -**双信源原则:中断只是快路径,状态寄存器才是真相。** 等连接时不能只等中断: -1. **accept 路径内联探针**(`net_lost_connect_probe`,`net_accept_locked` 每 100 ms 轮询挂探针):芯片状态已 ESTABLISHED 而 CONNECT 中断未到 → 连续 2 轮去抖后补做 accept(复用 `handle_connect_event` 簿记)。 -2. **幽灵态甄别**:补 accept 前回读对端端口(`GET_REMOTE_IPP`),**端口 = 上一条连接端口(`last_peer_port`)→ 幽灵,不 accept**——否则会把数据发给已死的旧连接(这正是生产 LIST#4"accept 成功但数据被吞"的直接成因:对账②曾把幽灵连接补 accept 了)。 -3. **对账②同步加幽灵甄别**(`net_reconcile_tick` 类型②分支同样比对 `last_peer_port`)。 -4. `last_peer_port` 登记点:`handle_disconnect_event` / `handle_timeout_event` / `net_relisten_try`(清零 remote_port 前转登)/ `net_close_locked`(跨 memset 保留——幽灵属芯片 Socket 级,与 fd 复用无关)/ NET_MSG_FIX 陈旧 ESTABLISHED 修复路径。 - -### 与 Trap 22 的关系 -Trap 22 的周期对账(200ms 节拍 + 400ms 去抖 + 1s 冷却)设计目标是"自愈兜底",对常驻监听器有效;但 FTP 数据通道是"活 1 秒就死"的短命监听器,且对账②**无幽灵甄别**——它既救不全(冷却期内 accept 已超时)又反过来帮凶(把幽灵补 accept)。Trap 25 把"看状态补 accept"内联进 accept 热路径(短去抖、无冷却)并补上端口甄别,对账②保留为兜底网。 - -### 对应文件 -`Drivers/BSP/NET/net_socket.c`(`net_lost_connect_probe` / `net_accept_locked`(含 closed-with-data accept)/ `net_reconcile_tick`② / `last_peer_port` 各登记点)、`Drivers/BSP/NET/net_types.h`(`last_peer_port` / `lost_conn_seen` 字段)、`Drivers/BSP/NET/lftpd/lftpd.c`(`send_list`/`send_nlst` 批量发送);台架工具 `test/board/ch395f_reuse_bench.{c,h}` + `test/host/ch395f_reuse_host.py`(实验分支 `n30-chip-bench` 注册编译,主分支只存档不注册)。 - -### 验证(台架层) -`ch395f_reuse_bench` 修复组(丢中断捕获自 accept)15/15,机制触发 5 次全部救回(对照同表 4/15 挂)。⚠ 生产回归当时未达 11/11——生产主导模式与台架不同,见下两节。 - -### 三次定案:所谓"kill 模式"是寄存器状态误读(2026-10-11 当日终案,双向验证) -第二轮验证(仅双信源探针修复,4 轮:1/11、5/11、2/11、2/11)暴露 STOR #2 仍 100% 失败,当时解读为"芯片在 accept 后 10-20 ms 杀连接"。后经**手册条文 + 网络资料双向核对**推翻: - -**寄存器读数本身全部正确**(`GET_SOCKET_STATUS_SN` §5.22:`05H=SOCKET_OPEN/00H=SOCKET_CLOSED`;第二字节 TCP 态 `00H=CLOSED/04H=ESTABLISHED`,与 WCH 官方数据手册表 5-8/5-9 一致;`GET_RECV_LEN_SN` §5.33 低字节在前,驱动实现一致)。**错在对 DISCONNECT 事件语义的解读**: - -1. `net_init` 的 `ch395f_set_fun_para(0x08)` 置 **FUN_PARA_FLAG_SOCKET_CLOSE=1**(§5.52 ②):产生 DISCONNECT/TIMEOUT 后芯片**不关 Socket、不清缓冲**,就是留给单片机把残留数据读出后再关 Socket。 -2. 判别探针现场:`kill-check recv_len=1027 re-read st=0x05/0x00`——Socket 仍 OPEN、TCP 已 CLOSED、**RX 缓冲 1027 字节 = STOR 载荷一个不少全在**。这是芯片按 §5.52 规格**保留现场**,不是杀连接。 -3. 那个 DISCONNECT 是**客户端的正常 FIN**:Python ftplib `storbinary` 语义 = `sendall` 发完立即关数据连接,然后回控制通道等 226——标准 FTP 行为。局域网小文件下"连接→发完→关闭"在 ~5 ms 内完成。 - -**425 真根因(竞态 + 自毁缓冲)**:客户端 5 ms 内完成发送并关闭;`cmd_stor` 的 accept RPC 赶到 net 层时 MCU 态已是 CLOSED;旧 `net_accept_locked` 把 CLOSED 一律当"无连接" → `net_relisten_try`(close+open **清空芯片 RX 缓冲**)→ 1027 字节被**我方亲手销毁** → WOULDBLOCK → lftpd 判"数据套接字已关" → 425。芯片全程无辜(此模式)。 - -**有效修复(已落地,`net_accept_locked` CLOSED 分支)**:CLOSED + auto_relisten + **芯片 RX 缓冲有数据**(`GET_RECV_LEN_SN`>0)→ 接受这条"已关但可读"连接(日志 `closed-with-data accept (drain pending)`),应用经 `net_recv_locked` 快路径(无状态门槛)排空,排空后 EOF 自然触发 relisten——完全对齐 §5.52 ②"读出残留数据后再关闭"的芯片契约。**验证:修复后 4 轮回归 STOR #2 从 4/4 失败 → 4/4 通过**(7/11、6/11、9/11、5/11,失败项中已无 STOR)。 - -**连带修正**:台架 mode0(主机先关)观察到的"accept 后 10-20 ms DISCONNECT"大概率同为误读——那是主机收完数据自己关的。"拆链窗口杀连接"理论对生产 425 不成立;台架**丢 CONNECT 中断模式**(主机侧 got=0 + 芯片 0x04 + 无中断,主机端独立证据)仍然成立,双信源探针修复保留。 - -**当时的遗留**:LIST #4/#7 超时(服务端 `net_send` 目录、客户端 20 s 收 0 字节、无 DISCONNECT)——发送方向的真问题,与上述 425 机制不同——已由下节"四次定案"解决。 - -### 四次定案:LIST 吞包 = 背靠背小段写毒化芯片 TX(2026-10-11 终案,修复后 8/8 通过) -探针定位过程(全部生产固件 + 临时探针,一轮复现): -1. PASV→accept 间隔 2 s(`net_listen_locked` open 重试在跑)→ 曾疑"listen 撞拆链窗口"; -2. accept 正常、CONNECT 正常、发送后中断位 `0x03`(SENBUF_FREE+SEND_OK)——**芯片自认发送成功**; -3. `GET_REMOTE_IPP` 对端 IP:port 完全正确(排除对端混淆); -4. **Socket 轮换**(服务端先关后 4 s 冷却、数据连接轮转 sock3~7):**证伪**——LIST 换到全新 sock5 仍 100% 挂(芯片行为与失败前完全一致); -5. **决定性对照**:同一目录同一载荷,LIST #1(会话首连,FAT 冷、readdir 慢、行间天然间隔大)通过;LIST #4/#7(FAT 热、每行一个 `net_send` 背靠背小段)必挂。 - -**定案**:`send_list`/`send_nlst` 每行一次 `net_send`(~60 B 小段、间隔仅 ms 级)——CH395F 对同一连接的**背靠背多段小写入**会把 TX 整条连接毒化(芯片报 SEND_OK 但对端协议栈全部丢弃,无 RST 无 DISCONNECT)。`net_send_locked` 的逐块协议(等 SENBUF_FREE 再写下一块)本身合规(§5.45),但芯片对该模式的内部处理不可靠;RETR 的 1 KB 满块连发不受影响(实测通过)。 - -**修复(`lftpd.c`)**:`send_list`/`send_nlst` 整份目录**一次性批量发送**(static 1536 B 缓冲,snprintf 累积 + 单次 `lftpd_inet_write`;溢出截断判 -1→550)。注意 `lftpd_inet_write` 成功返回**字节数**非 0(首版误按 0=成功判 → 全 550,已纠)。 - -**验证**:修复后 ftp_regression 连跑 **4 轮 11/11**(终版:批量发送 + closed-with-data accept + 丢中断探针 + 幽灵甄别,无轮换无探针)+ `ftp_auth_bench.py` **13/13** + `@test` 启动标记 PASS;此前同条件 LIST #4/#7 为 100% 失败、STOR #2 曾 100% 425。轮换冷却机制随证伪**已撤**(不留未证明的复杂度;其 5 轮 11/11 一并存档备查)。 - -**教训(Trap 25 总纲)**:本轮先后误读三次——把客户端正常 FIN 读成"芯片杀连接"、把 open 重试读成"拆链窗口"、把芯片假 SEND_OK 读成"数据已上线"。铁律:**CH395F 的"成功"(中断位/状态码)只代表芯片本地动作完成,不代表对端收到;判定必须回到对端可见证据(主机侧收发)+ 手册语义(FUN_PARA §5.52、状态码 §5.22)**。 - -**顺带发现的疑点(2026-10-11 已定案)**:`set_fun_para(0x09)` 的 bit0——§5.52 表明确写 **bit0 保留、必须为 0**,而 `net_init` 注释声称"bit0=1 多 Socket 模式"(手册与网络资料均无此说法,CH395 的 8 Socket 是芯片原生能力,靠 Socket 索引 `_SN` 寻址)。现固件 bit0=1 下 sock1/2/3 多路并行正常 → 该位被芯片忽略。**定案:改回 `0x08`**——官方手册 V1C/V1E/V2.2/V2.3 的 bit0 全是保留位,官方例程 bit0=0 下 8 个 Socket 照常可用(`EXAM5_TCP_Server` 传 0x02 却给 Socket 0~7 配缓冲),官方 FTP 例程本身就传 0x08。完整证据链见 Trap 18「定案更正」;**0x08 下的多 Socket 已台架实测通过**(2026-10-11 20:18,Phase 7 34/34,并发回显落在 sock2/4/5/6/7,N-31 已销项)。 - -### 发现时间 -2026-10-11(登记簿 N-30;裸机芯片台架定案实验见本节,实验分支 `n30-chip-bench`)。 - ---- - -## 2026-10-11 寄存器 / 数值口径全量审计(智能体团队,只读) +# 第六章 2026-10-11 寄存器/数值口径全量审计(A/B 系列) **方法**:四路并行——驱动层逐命令(含官方 SDK 逐字对照)、NET/测试层逐调用点、Trap 01–12 复核、Trap 13–25 复核。三源交叉:`docs/芯片手册/CH395F.md` + WCH 官方例程 `CH395EVT/EXAM/PUB/CH395CMD.c`、`CH395INC.H` + git 历史。 -### 干净清单(核对过、无异常) +## 干净清单(核对过、无异常) | 项 | 计数 | 结论 | |---|---|---| @@ -960,59 +338,83 @@ Trap 22 的周期对账(200ms 节拍 + 400ms 去抖 + 1s 冷却)设计目标 | 16/32 位量字节序 | 9 处 | 全部低字节在前,与手册明文 + SDK 逐字节一致 | | 读命令输出字节数 | 14 条 | 无少读/多读(`GET_SOCKET_STATUS`=2、`GET_IP_INF`=20 且顺序 IP→GW→MASK→DNS1→DNS2、`GET_UNREACH`=8、`GET_REMOT_IPP`=6) | | 常量表 | 60 个 | `ERR_*`18 / `TCP_*`11 / `SOCKET_*`2 / `PHY_*`6 / `PROTO_*`4 / `SINT_STAT_*`6 位号全对;唯一错值见 A3 | -| **`GET_GLOB_INT_STATUS_ALL` 拼装方向** | — | **无异常**:我们 `第1字节\|第2字节<<8` 与官方 `CH395CMDGetGlobIntStatus()` 逐字相同;`glob & (1<<(sock+4))` = §5.16 bit4..bit11 = SDK `GINT_STAT_SOCKn` | -| `get_socket_status` 缓冲区 | 17 处调用点 | 全部 `uint8_t st[2]`,无 1 字节残留(`db64e6ac` 引入 → `e5733b34` 修复,HEAD 无回归) | +| `GET_GLOB_INT_STATUS_ALL` 拼装方向 | — | 与官方 `CH395CMDGetGlobIntStatus()` 逐字相同;`glob & (1<<(sock+4))` = §5.16 bit4..bit11 | +| `get_socket_status` 缓冲区 | 17 处调用点 | 全部 `uint8_t st[2]`,无 1 字节残留 | | 中断判据 | 17 掩码 / 32 全局读 / 27 SINT 读 | 位号与 §5.16/§5.23 一致;只用 16 位 ALL 版 | -| 单位与取值范围 | 12 个参数点 | retrans 8/200 ms、KeepAlive 60000/5000(500 倍数、IDLE>INTVL)、ARP 100 ms 单位、MSS 1024 全部合规 | +| 单位与取值范围 | 12 个参数点 | retrans 8/200ms、KeepAlive 60000/5000、ARP 100ms 单位、MSS 1024 全部合规 | | 缓冲布局 | 3 处组合 | `net_init` 8×(4收+2发)=48 块恰好用满、无交叠(零裕量) | -| 显示假象 | 10 处 `%d.%d.%d.%d` | 只有已定案的 `net_socket.c:1631` 是 MSB 重排假象,无新增同类 | +| 显示假象 | 10 处 `%d.%d.%d.%d` | 只有已定案的 `net_socket.c` getsockname 一处是 MSB 重排假象(见 B5),无新增同类 | -### 新发现(去重后按严重度;已修 = 本次提交内) +## 新发现(去重后按严重度) | # | 严重度 | 位置 | 问题 | 状态 | |---|---|---|---|---| -| A1 | 中 | `test/board/ch395f_test_task.c:1005` | TC-203 的 Trap 14 反核实写成 `st_chk[0] == CH395F_TCP_ESTABLISHED`——byte0 只有 `00H/05H`,条件**恒假** → 该防线永久失效且日志无痕(与 Trap 07 同类:byte0/byte1 语义越界) | ✅ 已修(改 `byte0==OPEN && byte1==ESTABLISHED`) | -| A2 | 中 | 同上 `:1150` / `:1321` | Phase 3/4 把 `GET_RECV_LEN` 返回值直接当读取长度(本地 `rx_buf[300]`/`[64]`,芯片 RX 缓冲 2 KB)→ 栈溢出 | ✅ 已修(夹到 `sizeof` + 留痕) | -| A3 | 低 | `ch395f.h:253` | `CH395F_GINT_STAT_DHCPV6 = 0x8000`(bit15);SDK 是 `(1<<12)`、§5.16 明确 CH395F 的 `[12:15]` 保留 | ✅ 已修(改 `0x1000` + 注明本芯片恒 0;全仓零引用) | -| A4 | 低 | `ch395f.h:33-91`、`ch395f.c:484/506`、`ch395f.h:701` | 分组注释字节数错 4 处(`GET_GLOB_INT_STATUS_ALL`/`GET_UNREACH_IPPT` 归"1 字节输出"、`SET_FUN_PARA` 归"1 入 1 出"、`SET_RETRAN_PERIOD` 归"3 字节输入"、`GET_SOCKET_STATUS_SN` 归"1 字节输出");KeepAlive 参数注释写"秒"(§5.53/5.54 是 ms);`set_retrans_count` 写"0=默认值5"(§5.17 默认 12、无 0=默认语义)。**实现全部正确,注释误导**——Trap 25 的 1 字节缓冲栈越界正是这类注释发起的 | ✅ 已修 | -| A5 | 低 | `net_socket.c:238/1005/1080/1297/1666/708/3056` | 注释与手册/HEAD 相反:`FUN_PARA(SOCKET_CLOSE=0)`(实为 bit3=1,正是 closed-with-data accept 的契约依据)、"初始化成功后设置 MAC"(§9.2.1 ① 是之前)、"复用默认缓冲"(已 8 Socket 显式分配)、"布局由 MSS 推导"(固定布局、Trap 21 已解耦)、"等 SEND_OK"(门控是 SENBUF_FREE)、UDP 头 `[0-1] reserved`(§9.2.4 是数据长度) | ✅ 已修 | -| A6 | 低 | `ch395f.c:1176` | `write_send_buf` 超限**静默 return**(void、无日志),且上限取 DMA 2048 而非发送缓冲 1024;读路径有 OVERSIZE 金丝雀、写路径没有 | ✅ 已修(补 `[ERR]` 留痕 + >1 KB 告警,行为不变) | -| B1 | ~~中~~ **已推翻** | `net_socket.c:3553` | 原判"产品路径从未启用 Socket KeepAlive → 静默断开无人知晓"(§5.56 服务器侧应在 CONNECT 后开,产品三服务全是 Server,60s/5s/3 参数空转)。**2026-10-12 实测推翻前提**:连接静置时禁用 PC 网卡(真·静默死亡),芯片 **+2 s** 即报 `[ERR][NET] sock0 TIMEOUT` → 本层 `relisten quarantine` 释放重开;2 s 恰为 §5.17 重传 8×200 ms=1.6 s 量级,**与 KeepAlive 无关**(IDLE 最短 60 s)。链路层失效本就被快速覆盖。 | ✅ 已按 §5.56 补过使能并实测,**依 Trap 25「不留未证明的复杂度」撤回**,代码内留注释记录;残余场景见下「台架注入陷阱」 | -| B2 | 中低 | `net_socket.c:1638` | `ch395f_tcp_connect()` 返回值被丢弃(同文件 open/listen 都查)。§9.2.2 ⑧ 明文:`GET_CMD_STATUS` 返回 SUCCESS 仅表示**命令**执行成功、其他值表示**命令执行失败**,丢弃它 = 命令级被拒要等 30 s recv 超时才暴露 | ✅ 已修(2026-10-12):非 SUCCESS 且**非 BUSY** 才判失败——`poll_cmd_status` 超时返回的就是 `ERR_BUSY`(芯片仍在执行、命令未被拒),把它当失败会误杀慢芯片;明确失败 → `[ERR] rejected by chip: 0xNN` + 按 §9.2.2 ⑧"从 ⑤ 重开"关 Socket 回滚 + 返 `-1`。**Phase 7 实测回归**:TC701 回显 64 B 校验 PASS、TC702 重连 ×3 全 PASS(正常连接无误杀);不可达对端时芯片回的是 SUCCESS、`ret=0` 且不打 `rejected by chip`,失败仍由 `CONNECT TIMEOUT`(~1.6 s) 裁定 → 行为未变。⚠ 残余:`net_reconcile_tick` 仍只裁 ESTABLISHED/LISTENING,`CONNECTING` 丢中断无人收口(产品不走出向连接、无法实测,按"不加未证明复杂度"暂不动);TC703 断言 `rc==0` 把"返回值被丢弃"写进了判据,真发生命令级被拒时它会 FAIL(属正确告警) | -| B3 | 低 | **两处**:`net_socket.c:1120-1125`(`net_listen_locked`)+ `net_socket.c:2060-2065`(`net_relisten_locked`,审计原只登记前者,**后者才是每次断连的热路径**) | 两处对**全部 8 个 Socket** `GET_INT_STATUS_SN`(读即清)+ 无条件读清全局。机制:§5.15 明文「此命令执行完毕后…将全局中断状态清除」= 全局寄存器**整体读即清、无按位清除手段**,而 `net_poll` 只在**全局位置位时**才去读该 Socket 的 SINT(`:469` 门控)→ 丢弃式读全局把别的在用 Socket 的门一并焊死;SINT 又**粘滞到被读**(Trap 14 实证 close/open 都不清 SINT)→ 全量读走即**永久销毁**其 CONNECT/DISCONNECT/TIM_OUT/SENBUF_FREE(唯 RECV 是条件位可下轮补,Trap 08)。git 考古:`e862009bc`(2026-07-19 多连接时代,无注释引入,同提交还删掉了 net_poll 里"勿在此读 per-socket 中断"的警告) → `319d00e3` 原样搬进单连接 → `db64e6acf` 复制进 relisten;**不属于任何已登记 Trap 的修复**(Trap 14/17/23 指定的预清都在别处且是单 Socket)。官方 SDK 21 个例程一律"全局位图门控 + 单 Socket 读 + 立刻转存",open/listen 前后**不清任何中断**,无一例全量读 8 个 SINT | ✅ 已修(2026-10-12):两处收窄为**只清本 Socket 的 SINT** + **删除丢弃式全局读清**(全局交 `net_poll` 集中读清,与 AGENTS 模块约定「所有 CH395F 中断在 net_poll 内集中读取并清除」一致);本 Socket 残留的全局位被 `net_poll` 读到时 SINT 已为 0,无害了结。**两路子 Agent 曾分歧**:一路建议保留全局读清("只搁浅不销毁"),一路警告"保留=修一半"——采后者,芯片无按位清除,保留即热路径继续焊门。**回归**:`@build` 0/0 `Code=197008`(−24 B);`@test` PASS;Trap 25 `ftp_regression` **11/11 ×3**;Trap 23 `ftp_abort_probe --check-abort` RST **5/5**(450) + FIN **5/5**(226);Trap 22 `:21` 0ms×50 **50/50**、100ms×50 **50/50**、`:8888` 0ms×20 **20/20**(均与文档基线一致);`:7003` 腿见 B10(A/B 已证明与本改动无关;B10 修完后该腿回到 **10/10**)。⚠ 残余:`net_connect_locked:1601` / `net_close_locked:2571` / `net_relisten_locked:1986` 三处无条件全局读清仍会**搁浅**(延后、非销毁)其它 Socket 挂起事件——它们是 Trap 14/17/23 的指定两级预清,本次不动,宜与 B4 一并处置 | +| A1 | 中 | `test/board/ch395f_test_task.c:1005` | TC-203 的 Trap 14 反核实写成 `st_chk[0]==CH395F_TCP_ESTABLISHED`——byte0 只有 `00H/05H`,条件**恒假** → 防线永久失效且日志无痕(与 Trap 07 同类:byte0/byte1 语义越界) | ✅ 已修(改 `byte0==OPEN && byte1==ESTABLISHED`) | +| A2 | 中 | 同上 `:1150`/`:1321` | Phase 3/4 把 `GET_RECV_LEN` 返回值直接当读取长度(本地 `rx_buf[300]/[64]`,芯片 RX 2KB)→ 栈溢出 | ✅ 已修(夹到 `sizeof` + 留痕) | +| A3 | 低 | `ch395f.h:253` | `CH395F_GINT_STAT_DHCPV6=0x8000`(bit15);SDK 是 `(1<<12)`、§5.16 明确 `[12:15]` 保留 | ✅ 已修(改 `0x1000` + 注明本芯片恒 0;全仓零引用) | +| A4 | 低 | `ch395f.h/ch395f.c` 多处 | 分组注释字节数错 4 处;KeepAlive 参数注释写"秒"(§5.53/5.54 是 ms);`set_retrans_count` 写"0=默认值5"(§5.17 默认 12)。**实现全部正确,注释误导**——B3 的 1 字节缓冲栈越界正是这类注释发起的 | ✅ 已修 | +| A5 | 低 | `net_socket.c` 7 处注释 | 注释与手册/HEAD 相反:`FUN_PARA(SOCKET_CLOSE=0)`(实为 bit3=1,正是 closed-with-data accept 的契约依据)、"初始化成功后设置 MAC"(§9.2.1 ① 是之前)、"复用默认缓冲"、"布局由 MSS 推导"(Trap 13/21 已解耦)、"等 SEND_OK"(门控是 SENBUF_FREE)、UDP 头 `[0-1] reserved`(§9.2.4 是数据长度) | ✅ 已修 | +| A6 | 低 | `ch395f.c:1176` | `write_send_buf` 超限**静默 return**(void、无日志),且上限取 DMA 2048 而非发送缓冲 1024;读路径有 OVERSIZE 金丝雀、写路径没有 | ✅ 已修(补 `[ERR]` 留痕 + >1KB 告警,行为不变) | +| B1 | ~~中~~→定案 | `net_socket.c`(KeepAlive 使能点) | 原判"产品路径从未启用 Socket KeepAlive → 静默断开无人知晓"(§5.56 服务器侧应在 CONNECT 后开)。2026-10-12 曾推翻前提:禁用 PC 网卡(链路层死亡)芯片 **+2s** 即报 `sock0 TIMEOUT`(=§5.17 重传 8×200ms 量级,与 KeepAlive 无关)→ 依 Trap 25 铁律撤回使能。**2026-10-13 E11 再定案**:用"禁用网卡"注入"链路在、对端静默"(防火墙规则对已建立连接无效,方法学见 `docs/网络/N34-自愈审计/实验方案.md` E11)实测服务器侧开启后 60s/5s/3 → **判死 ≈30.0s**(早于手册公式 75s,以实测为准);短参数 6s/2s/2 → ≈5.1s(参数生效);静默但存活对端**不被误杀**(90s 空闲回显正常) | ✅ **已转正**(`handle_connect_event` LISTENING 分支按 `docs/芯片手册/CH395F.md` §5.56 开启;Phase 11 回归 TC-NET-1101 已更新死对端注入法) | +| B2 | 中低 | `net_socket.c`(`net_connect_locked`) | `ch395f_tcp_connect()` 返回值被丢弃。§9.2.2 ⑧:`GET_CMD_STATUS` SUCCESS 仅表示**命令**执行成功,丢弃它 = 命令级被拒要等 30s recv 超时才暴露 | ✅ 已修(2026-10-12):非 SUCCESS 且**非 BUSY** 才判失败(`poll_cmd_status` 超时返 `ERR_BUSY`=芯片仍在执行,当失败会误杀慢芯片);明确失败 → `[ERR] rejected by chip: 0xNN` + 按 §9.2.2 ⑧ 关 Socket 回滚 + 返 -1。TC701/702 回归无误杀。⚠ 残余:`net_reconcile_tick` 仍只裁 ESTABLISHED/LISTENING,`CONNECTING` 丢中断无人收口(产品不走出向连接、无法实测,按"不加未证明复杂度"暂不动) | +| B3 | 低→**结构性** | `net_listen_locked` + `net_relisten_locked`(后者是每次断连的热路径) | 两处对**全部 8 个 Socket** `GET_INT_STATUS_SN`(读即清)+ 无条件读清全局。机制:§5.15 全局中断寄存器**整体读即清、无按位清除手段**,而 `net_poll` 只在全局位置位时才读该 Socket 的 SINT(门控)→ 丢弃式读全局把别的在用 Socket 的门一并焊死;SINT 又**粘滞到被读**(Trap 14/17 实证)→ 全量读走即**永久销毁**其 CONNECT/DISCONNECT/TIM_OUT/SENBUF_FREE(唯 RECV 是条件位可下轮补,Trap 08)。git 考古:多连接时代引入 → 单连接搬迁 → relisten 复制,**不属于任何已登记 Trap 的修复**;官方 SDK 21 个例程一律"全局位图门控 + 单 Socket 读 + 立刻转存",无一例全量读 8 个 SINT | ✅ 已修(2026-10-12):两处收窄为**只清本 Socket SINT** + **删除丢弃式全局读清**(全局交 `net_poll` 集中读清=AGENTS 模块约定)。回归:Trap 25 11/11×3、Trap 23 RST 5/5+FIN 5/5、Trap 22 全腿与基线一致。⚠ 残余:`net_connect_locked`/`net_close_locked`/`net_relisten_locked` 三处指定预清仍含无条件全局读清(会**搁浅**而非销毁其它 Socket 挂起事件)——它们是 Trap 14/17/23 的两级预清,宜与 B4 一并处置 | | B4 | 低 | `net_socket.c:446/469` | 全局中断 bit0-3(`UNREACH`/`IP_CONFLI`/`PHY_CHANGE`/`DHCP`)被读清但从不消费;`ch395f_get_unreach_info()` 全仓零调用 → IP 冲突、ARP 不可达、链路抖动三类事件完全静默 | ⏳ 待修(可观测性缺口) | -| B5 | 低 | `net_socket.c:3878-3881`(`net_getsockname`,函数 :3851-3887;原登记 :3849-3854 已陈旧)→ `lftpd.c:803-809` | `net_getsockname` 的 `s_addr` 组装方向与本层约定(`net_inet_addr`/`net_inet_ntoa`)相反 → FTP 日志把设备自身 IP 打成 `100.1.168.192`。⚠ 同一 `getsockname` 的 PASV 227 消费点(`lftpd.c:497-503`)做了 `net_htonl` 再 MSB 拆,**双重反转恰好正确,勿误判**;风险前瞻:若把该 `s_addr` 喂给 `net_connect`/`net_sendto` 就会连到反序地址 | ⏳ 待修(改一处须同时核对 PASV) | +| B5 | 低 | `net_getsockname` → `lftpd.c` 日志 | `net_getsockname` 的 `s_addr` 组装方向与本层约定相反 + `net_inet_ntoa` 段序反转(NET_NTOHL 后主机数字的 MSB 才是首段,原实现低段在前)→ FTP 日志把 IP 打成 `100.1.168.192`;且 lftpd 打印的是 `getsockname`=**本机 IP:21** 而非对端。⚠ PASV 227 消费点做了 `net_htonl` 再 MSB 拆,**双重反转恰好正确,勿误判** | ✅ 已修(2026-10-13,N34 阶段 3 收尾;代码随本分支收尾批提交):`net_inet_ntoa` 段序改 MSB 首(MCU+模拟器同步);新增 `net_getpeername`(读 CONNECT 事件缓存,零 SPI);lftpd 改打印**对端** IP:port(台架判据:日志 `connection received from 192.168.1.x:<高位端口>`)。PASV 未动(原本正确) | | B6 | 低 | `net_socket.c:591/1225/3608` | 3 处状态判据只看 `byte1`、未按 §5.22 用 `byte0` 门控;reconcile 的 else-if 链里 `(00H, 陈旧 04H)` 会先命中"补 accept"而非"重建监听" | ⏳ 待修(Trap 07 实测 `byte0=00H` 时 `byte1` 读回 `0x00`,故为低概率) | -| B7 | 低 | `ch395f_test_task.c:1379/1504` | Phase 5 单次写 4093 B 超 1 KB 发送缓冲与 2048 DMA 载荷 → 整块被丢;注释仍写"4096 B = 发送缓冲上限"(2026-09-22 布局解耦前的旧事实)。A6 修后会打 `[ERR]` 而非静默 stall | ⏳ 待修(chunk 改 ≤1024 或分块) | +| B7 | 低 | `ch395f_test_task.c:1379/1504` | Phase 5 单次写 4093B 超 1KB 发送缓冲与 2048 DMA 载荷 → 整块被丢;注释仍写"4096B=发送缓冲上限"(旧事实)。A6 修后会打 `[ERR]` | ⏳ 待修(chunk 改 ≤1024 或分块) | | B8 | 低 | `ch395f_test_task.c:1290-1292` | Phase 4 把 sock6 缓冲重排到块 24-27,与 `net_init` 固定布局中 sock4 的 24-27 **交叠**(§5.45 禁止) | ⏳ 待修 | -| B9 | 低 | `ch395f_reuse_bench.c:93`、`net_socket.c:39`、`net_socket.c:262` | bench 用 `get_phy_status() != 0` 判链路(01H=断开也成立,`net_task.c:122` 的 `!= CH395F_PHY_DISCONN` 才对);`NET_PHY_CHECK_INTERVAL_MS` 死宏;无芯片版本门控(§5.16 <0x44 不支持 ALL 版、§5.52 bit3/4 需 ≥0x46,实测 0x4A 无影响) | ⏳ 待修(前两项死代码/卫生) | -| B10 | **中** | `net_reconcile_tick` 认领范围 + `handle_disconnect_event` 释放分支(`net_socket.c:3663-3686`) | **孤儿监听 → 端口永久拒连**。断开时释放分支把状态置 `CLOSED` 并注释「留给 `net_recv_locked` 把剩余数据读出来再关,**等待应用重新监听**」——即由应用负责重建;但 `ui_net_sink` 只在 `net_recv` 返 0(EOF)时才清 `g_client`,而 EOF 已被这个事件分支消费掉 → **net 层等应用重建 / 应用等 net 层给 EOF / 对账只裁 ESTABLISHED·LISTENING 直接跳过 / 芯片不会自己回 LISTEN**,四方互等。实测(临时探针):`TEMP orphan sock2 state=0 port=7003 chip=0x05/0x00` 每秒一条持续 12 s+,`:7003 300ms×10` **0/10**、+30 s 仍拒连。与 Trap 22 的关系:Trap 22 的①③两类都要求本层处于 ESTABLISHED/LISTENING,**孤儿态恰好落在两类之外**,所以那套对账全程沉默 | ✅ 已修(2026-10-12):`net_reconcile_tick` 新增**类型④**认领孤儿(`in_use` + `state==CLOSED` + `auto_relisten` + `type==STREAM` + `local_port!=0`,芯片侧不在 LISTEN)→ 复用 `net_relisten_try`(自带"已在重试队列则不重复登记"保护,与 D10 重试队列、Trap 23 沉降隔离兼容)。⚠ **`auto_relisten` 是决定性门**:少了它,"本层 CLOSED + 已绑端口"的**出向连接**(板级 Phase 7 TC701~703 用 `net_connect` 绑本地口)会被误判孤儿并被 relisten 成监听器。**回归**:`:7003 300ms×10` **0/10 → 10/10**(+20 s 存活);`:21` 0ms×50 **50/50**、100ms×50 **50/50**;`:8888` 0ms×20 **20/20**;`ftp_regression` **11/11 ×2**;Trap 23 RST **5/5**、FIN **5/5**;`@test` PASS(600 ms、回读无差异);`Code` 197008→197084。⚠ **残余(未根治,已立项根治)**:生产者仍在——释放分支依旧把状态停在 `CLOSED` 等应用重建,④是**兜底**(实测救援后端口 1 s 内恢复,日志 `reconcile: orphan listener … -> relisten` 可见)。⚠ **④ 有副作用**:`net_recv` 的 EOF 是**边沿**(只在 recv 那一刻 `state==CLOSED` 才给 0,`:1880-1888`;已被别处 relisten 回 LISTENING 则恒 `WOULDBLOCK`,`:1890`)→ ④ 抢先 relisten 会把慢消费者的 EOF 永久吞掉,`g_client` 不清、新客户端数据被旧会话静默吸收。**原理与分阶段根治方案 → `docs/子系统设计/网络Socket监听生命周期设计说明.md`**(含:芯片层为何永不自动回 LISTEN、本仓 `fun_para=0x08` 使 `0x05/0x00` 成为合法常驻态、RFC 1122 §4.2.2.18 与 POSIX accept 的规范冲突、12 个重建所有者与 U1~U15 无人负责组合、以及"芯片原生多连接模式(A′)为何对本产品不可用"的决定性证据) | +| B9 | 低 | `ch395f_reuse_bench.c:93`、`net_socket.c:39/262` | bench 用 `get_phy_status()!=0` 判链路(01H=断开也成立,应 `!= CH395F_PHY_DISCONN`);`NET_PHY_CHECK_INTERVAL_MS` 死宏;无芯片版本门控(§5.16 <0x44 不支持 ALL 版、§5.52 bit3/4 需 ≥0x46,实测 0x4A 无影响) | ⏳ 待修(前两项死代码/卫生) | +| B10 | **中** | `net_reconcile_tick` 认领范围 + `handle_disconnect_event` 释放分支 | **孤儿监听 → 端口永久拒连**:释放分支把状态置 CLOSED 等"应用重新监听",但 `ui_net_sink` 只在 `net_recv` 返 0(EOF)时才清 `g_client`,而 EOF 已被事件分支消费 → net 层等应用重建 / 应用等 net 层给 EOF / 对账只裁 ESTABLISHED·LISTENING 直接跳过 / 芯片不会自己回 LISTEN,**四方互等**。实测探针 `TEMP orphan sock2 state=0 port=7003 chip=0x05/0x00` 持续 12s+,`:7003 300ms×10` **0/10**。与 Trap 22 的关系:①③两类都要求本层处于 ESTABLISHED/LISTENING,**孤儿态恰好落在两类之外** | ✅ 已修(2026-10-12):对账新增**形态④**认领孤儿(`in_use`+`state==CLOSED`+`auto_relisten`+`type==STREAM`+`local_port!=0`,芯片侧不在 LISTEN)→ `net_relisten_try`。⚠ `auto_relisten` 是决定性门:少了它出向连接(Phase 7 TC701~703 绑本地口)会被误判孤儿 relisten 成监听器。回归:`:7003` 0/10→**10/10**;其余全腿与基线一致。⚠ ④ 的副作用:`net_recv` 的 EOF 是**边沿**——④抢先 relisten 会把慢消费者的 EOF 永久吞掉(`g_client` 不清、新客户端数据被旧会话静默吸收)。**根治出口 = `net_drop_conn` + 生命周期设计**(N34 阶段 3 已落,`6394853`):原理与分阶段方案 → `docs/子系统设计/网络Socket监听生命周期设计说明.md`(含:芯片为何永不自动回 LISTEN、`fun_para=0x08` 使 `0x05/0x00` 成为合法常驻态、RFC 1122 §4.2.2.18 与 POSIX accept 的规范冲突、12 个重建所有者与 U1~U15 无人负责组合、芯片原生多连接模式(A′)为何对本产品不可用) | -### 文档侧待修清单(本轮只改了最要紧的几处,其余登记在此) +## 台架注入陷阱(方法论,2026-10-12 起持续补充) -- **最高优先级**:Trap 13 的"部分读后 `recv_len` 归零"与 §5.34 直接冲突(已在本条加复核注),但**同一机理仍被当作现行约束**写在 `CH395F_Test_Guide.md:28/69/930/932/958`、`docs/网络/BSD_Socket_API_函数调用关系.md:196/362`、`net_socket.c` 两处注释里——须与 Trap 21 的撤回同批处置,并补一条"部分读 → 再查 `recv_len`"的定向 TC。 -- 陈旧数字:`CH395F_Test_Guide.md:14/24/645/660/1141/1200`(DMA 4100 / 载荷 4096,现 2052/2048)、`:72/1201`(`NET_DMA_MAX_PAYLOAD=4096`,现 2048;实际分块上限是 `NET_SEND_CHUNK_MAX`=1024)、`:1020-1040`(TC-NET-1002/1003 期望 4096/4097 单次 DMA)、`docs/网络/BSD_Socket_API_使用指南.md:1246`(`NET_SEND_BUF_SIZE` 宏已不存在)。 -- 悬空引用:`Trap 19` 从未存在(`Test_Guide.md:878` 仍引用);`Test_Guide.md:353/418` 的"见风险 #4 / 风险表 #1"指向已删除的表。 -- 口径冲突:`Test_Guide.md:1135` 的 TRAP-07 行仍写"卡 FIN_WAIT_2 / `close_socket`(RST)"(与 Trap 07 定案更正冲突);`:45` 的"TC704 40/40"与 `:880` 的"`PHASE7_MULTI_COUNT` ≤5"矛盾(HEAD 宏值仍是 8);`Drivers/BSP/NET/README.md:35` 与 `net_socket.c:1864` 的"正常 FIN 路径零改动"与 Trap 23 二阶段矛盾。 -- 机理措辞:Trap 17"等**对端** FIN/ACK"应按 Trap 07 实测改写(自身 FIN 的 ACK + 本地 TIME_WAIT,0~150 ms);Trap 20"硬件发送 FIFO 1 KB"应改"本仓 2 块分配的发送缓冲区"(§5.45 上电默认是 4 块=2 KB)、"等 `SEND_OK`"应为 `SENBUF_FREE`;Trap 25 的 `NET_MSG_FIX` 应为 `NET_MSG_RESYNC_LISTEN`、`GET_REMOTE_IPP` 应为 `GET_REMOT_IPP`、"§5.45"应为 §5.32+§5.23①。 -- 已失效条目未标注:Trap 01/02/11 的多连接模型随 2026-08-25 `319d00e3` 删除;Trap 09 的 `osDelay(7000)` 已被就绪屏障取代;Trap 10 的"`net_connect()` 不使用消息队列"已失效;Trap 12 的 #2/#4 与仓内 FatFs 源码矛盾(`ff.c:3101-3109` 剥前导分隔符、`:3127-3129` 空名返回根目录),且 `FF_FS_RPATH=0` 下 `"."` 不是合法路径段(HEAD 用 `"0:"`)。 -- 日期:Trap 01/02/03/04/06/07 的"2025-07"均早于仓库首提交 `b1d05091`(2026-06-05);Trap 03/04/06 的 Phase 编号也错(DHCP=阶段 4、UDP=阶段 3、KeepAlive=阶段 11)。 -- 证据通则:`*.log` 在 `.gitignore` 内 → 本文所有台架计数(40/40、34/34、15/15、4 轮 11/11、13/13…)**脚本在库、日志未入库 = 不可复核**;Trap 25 引用的实验分支 `n30-chip-bench`(tip `eddbede`)不含 bench 源文件与 uvprojx 注册,裸机芯片台架无法从任何已提交状态重建。 +- **Windows 出站防火墙规则不能模拟"对端静默死亡"**:WFP 对**已建立**连接有状态缓存,中途 `New-NetFirewallRule -Direction Outbound -Action Block` 不影响既有流——实测静置+阻断 250s 芯片毫无反应,看似"KeepAlive 不生效",实则 ACK 一直正常往返。 +- 更糟的副作用:该规则会**吃掉本机后来发出的 FIN** → 设备侧留下**幽灵 ESTABLISHED**,之后所有新连接被 §5.29"向试图连接的远端发送 TCP RESET"直接 RST,表现为"端口拒连"——易误判成设备故障(实为自伤)。撤规则后设备自行恢复。 +- **能真正掐断链路的手段**:`Disable-NetAdapter`(链路真 down)。⚠ 该 cmdlet **只有 `-Name`/`-InterfaceDescription`**,没有 `-InterfaceIndex`;网卡名是中文,用 `(Get-NetAdapter -InterfaceIndex N).Name` 取值再传入;禁用/恢复必须写在 `try/finally` 里。 +- **仍未覆盖的场景**:"链路仍在、对端静默"(NAT 会话过期、中间节点黑洞)没有安全注入手段(静态 ARP 黑洞被交换机泛洪兜底、删 IP 连带拆掉本机连接)——E11 用禁用网卡的近似注入完成定案。 +- **主机侧回显服务器收不到入站连接 → Phase 7 全量 `CONNECT TIMEOUT`**:本机对 `C:\programdata\anaconda3\python.exe` 有两条 Public 入站 Block 规则(`C:\python314\python.exe` 才是 Allow),而 `.venv\Scripts\python.exe` 的基座解释器正是 Anaconda python。Windows 防火墙**块规则优先于放行规则** → 中途加 Allow 规则**完全无效**,只有 `Set-NetFirewallProfile -Name Public -Enabled False` 才通。症状极具误导性:芯片 ~1.6s `CONNECT TIMEOUT`、`net_connect` 仍返 0、`[CONN] openOK` 照常打印,看着像芯片故障,实为主机防火墙。固定跑法:`try { 关 Public → 复跑 } finally { 开 Public }`。 +- **Phase 7 两条既有失败与改动无关,别追**:① TC704 `socket create #5` FAIL = E-23(`NET_MAX_SOCKETS=8` 减 3 常驻监听 → 并发上限 5,而 `PHASE7_MULTI_COUNT` 仍是 8);② TC703 要求 PC 服务器**离线**,与 701/702/704 环境互斥,同轮连跑必有一侧失真。 +- **对照组必须从干净态起测**:上一腿若**没死透**就重启下一腿,对照组失真。正确做法:每个样本前 `stlink_flash.py reset` + 等 12s 干净启动,并取多样本再比均值。 +- **`[PROF]`/`[PROF2]` 字段不是计数器,别拿来判"任务卡死"**:`netp` = `net_poll` 段最长耗时(µs)、`netn` = 采样瞬间在段内时长(`App/util/task_prof.c`)→ 数值恒定只说明"没刷新最大值"。真正的停摆信号是 `[ERR][WD] task stall`;其 `miss=` 打印的是**累计**数(连续数封顶在容忍值 8),容忍 = 8 轮×100ms = 连续 800ms 有任务缺席即停喂、IWDG 2.5s 溢出(`App/util/task_watchdog.c`)。缺席位图解码:`req=0x77` 为 DEFAULT/NET/FTP/UI/TIMESYNC/ADC,`mask=0x41` → 缺席 NET+FTP+UI+TIMESYNC = "netTask 卡住、所有做 `net_*` 的任务阻塞在队列+通知后面"的指纹。 +- **静默黑洞用临时探针抓状态,测完即删**:B10 的孤儿态在日志里毫无痕迹,靠对账跳过分支加一条 `TEMP orphan …` 才直接看到;同一轮还顺手用探针**否掉了自己的第一假设**。假设写盘前先测,比推理便宜。 +- **判读通则(与 Trap 25 同源)**:看到 `TIMEOUT` 先**算时序再归因**——2s 与 60s 一眼可分;否则会把"重传超时"误记成"KeepAlive 生效",把相关性当因果。 -### 主机侧测试工具过期(2026-10-12 实测) +## 主机侧测试工具过期(2026-10-12 实测) -- **`MDK-ARM/ftp_abort_probe.py` 已修**:`raw_login` 用 `USER anonymous`、`followup_ok` 用 `login("anonymous","x")`,N-19 后匿名被 530 拒 → Trap 23 两条腿**假 0/5**(round 1 报 `RuntimeError: PASV failed`,且这条半认证会话占死 FTP 唯一会话,导致后续轮全变 `ConnectionRefused`)。改 `admin/admin`(两处)后 RST **5/5**、FIN **5/5**,与 `:779`/`:781` 基线一致。**教训:跑既有回归腿前先确认凭据仍有效,否则会把工具过期读成固件回归。** -- **同族未修**:`test/host/ftp_up_dl_test.py`(:123-126)、`ftp_speed_test.py`(:116-121)、`ftp_test.py`(:93-99)仍用匿名登录 → 对当前固件必挂登录步;`docs/测试说明/FTP_Test_Guide.md` 推荐的这几个入口因此已失效(现行可用入口是 `test/host/ftp_regression.py`,默认 `admin/admin`)。 +- **`test/host/ftp_abort_probe.py` 已修**:`raw_login`/`followup_ok` 原用匿名登录,N-19 后匿名被 530 拒 → Trap 23 两条腿**假 0/5**(round 1 `PASV failed`,且半认证会话占死 FTP 唯一会话,后续轮全变 `ConnectionRefused`)。改 `admin/admin` 后 RST 5/5、FIN 5/5。**教训:跑既有回归腿前先确认凭据仍有效,否则会把工具过期读成固件回归。** +- **同族未修**:`test/host/ftp_up_dl_test.py`、`ftp_speed_test.py`、`ftp_test.py` 仍用匿名登录 → 对当前固件必挂登录步;`docs/测试说明/FTP_Test_Guide.md` 推荐的这几个入口因此已失效(现行可用入口是 `test/host/ftp_regression.py`,默认 `admin/admin`)。 -### 台架注入陷阱(2026-10-12,B1 复核过程中踩到) +## 文档侧待修清单(其余登记在此) -- **Windows 出站防火墙规则不能用来模拟"对端静默死亡"**:WFP 对**已建立**的连接有状态缓存,中途 `New-NetFirewallRule -Direction Outbound -Action Block -RemoteAddress <设备IP>` 不影响既有流——实测静置 + 阻断 250 s 芯片毫无反应,看似"KeepAlive 不生效",实则 ACK 一直在正常往返。 -- 更糟的副作用:该规则会**吃掉本机后来发出的 FIN** → 设备侧留下一条**幽灵 ESTABLISHED**,之后所有新连接被 §5.29"向试图连接的远端发送 TCP RESET"直接 RST,表现为"端口拒连",容易被误判成设备故障(实为自伤)。撤规则后设备自行恢复。 -- **能真正掐断链路的手段**:`Disable-NetAdapter`(链路真的 down)。⚠ 本机 PowerShell 的该 cmdlet **只有 `-Name` / `-InterfaceDescription`**,没有 `-InterfaceIndex`/`-ifIndex`;网卡名是中文,用 `(Get-NetAdapter -InterfaceIndex 19).Name` 取值再传入,避免命令行里写中文字面量。禁用/恢复必须写在 `try/finally` 里,否则网卡留在关闭状态。 -- **仍未覆盖的场景**:`Disable-NetAdapter` 只能造"链路层失效";"**链路仍在、对端静默**"(NAT 会话过期、中间节点黑洞)本轮没有安全注入手段(静态 ARP 黑洞会被交换机泛洪兜底、删 IP 会连带拆掉本机连接)。这才是服务器侧 KeepAlive 唯一可能的价值场景,**未证实**,故不保留该改动。 -- **主机侧回显服务器收不到入站连接 → Phase 7 全量 `CONNECT TIMEOUT`(2026-10-12 实测)**:本机对 `C:\programdata\anaconda3\python.exe` 有两条 **Public 入站 Block** 规则(`C:\python314\python.exe` 才是 Allow),而 `.venv\Scripts\python.exe` 的基座解释器正是 Anaconda python(构建报错自曝 `C:\ProgramData\anaconda3\python.exe: can't open file ...`)。Windows 防火墙**块规则优先于放行规则** → 中途加的 `New-NetFirewallRule -Direction Inbound -Action Allow -Protocol TCP -LocalPort 8081 -Profile Any` **完全无效**(实测两轮全灭),只有 `Set-NetFirewallProfile -Name Public -Enabled False` 才通。症状极具误导性:芯片 ~1.6 s 报 `CONNECT TIMEOUT`、`net_connect` 仍返 0、`[CONN] openOK` 照常打印,看着像芯片/驱动故障,实为主机防火墙。固定跑法:`try { 关 Public → 复跑 } finally { 开 Public }`(或改用 `C:\python314\python.exe` 起服务器)。 -- **Phase 7 的两条既有失败与改动无关,别追**:① TC704 `socket create #5` FAIL = E-23(`NET_MAX_SOCKETS=8` 减 3 个常驻监听 → 并发上限 5,而 `PHASE7_MULTI_COUNT` 仍是 8);② TC703 要求 PC 服务器**离线**,与 701/702/704 环境互斥,同轮连跑必有一侧失真(本轮服务器在线时它反而 PASS,只因芯片先 `CONNECT TIMEOUT`)。 -- **判读教训(与 Trap 25 同源)**:看到 `TIMEOUT` 先**算时序再归因**——2 s 与 60 s 一眼可分;否则会把"重传超时"误记成"KeepAlive 生效",把相关性当因果。 -- **A/B 归因前必须每腿硬复位(B10,2026-10-12)**:`:7003` 这类腿一旦把监听打死就**不自恢复**,下一腿是接着死状态跑的 → 对照组失真。本次第一腿(基线 1/10)就是这么废的;正确做法是**每个样本前 `stlink_flash.py reset` + 等 12 s 干净启动**,并取多样本(本轮基线 5/1/1/5 vs 改动 3/7/2/1,均值 3.0 vs 3.25 → 统计不可区分,才敢判"与改动无关")。 -- **`[PROF]`/`[PROF2]` 字段不是计数器,别拿来判"任务卡死"(B10)**:`netp` = `net_poll` **段最长耗时(µs)**、`netn` = 采样瞬间在段内的时长(`App/util/task_prof.c:172`)→ 数值恒定只说明"没刷新最大值"。真正的停摆信号是 `[ERR][WD] task stall`;且其 `miss=` 打印的是**累计** `s_miss_total`(不是连续缺席数,连续数封顶在容忍值 8),容忍 = 8 轮 ×100 ms = **连续 800 ms 有任务缺席即停喂**、IWDG 2.5 s 溢出(`App/util/task_watchdog.c:466-492`)。缺席位图解码:`req=0x77` 为 DEFAULT/NET/FTP/UI/TIMESYNC/ADC,`mask=0x41` → 缺席 NET+FTP+UI+TIMESYNC 而 DEFAULT/ADC 正常 = "netTask 卡住、所有做 `net_*` 的任务阻塞在 `osMessageQueuePut(osWaitForever)`+`xTaskNotifyWait(portMAX_DELAY)` 后面"的指纹。 -- **静默黑洞用临时探针抓状态,测完即删(B10 手法)**:B10 的孤儿态在日志里毫无痕迹,靠在对账跳过分支加一条 `TEMP orphan … state=%d port=%u chip=0x%02X/0x%02X` 才直接看到 `state=0 / chip=0x05/0x00`;同一轮还顺手用探针**否掉了自己的第一假设**(怀疑 `net_listen_locked` 的 `osDelay(500)` 冻结 netTask → 探针 0 次命中)。假设写盘前先测,比推理便宜。 +- **最高优先级**:Trap 13/21 已撤回的"部分读后 `recv_len` 归零"机理**仍被当作现行约束**写在 `CH395F_Test_Guide.md:28/69/930/932/958`、`docs/网络/BSD_Socket_API_函数调用关系.md:196/362`、`net_socket.c` 两处注释里——须同批处置,并补一条"部分读 → 再查 `recv_len`"的定向 TC。 +- 陈旧数字:`CH395F_Test_Guide.md:14/24/645/660/1141/1200`(DMA 4100/载荷 4096,现 2052/2048)、`:72/1201`(`NET_DMA_MAX_PAYLOAD=4096`,现 2048;实际分块上限是 `NET_SEND_CHUNK_MAX`=1024)、`:1020-1040`(TC-NET-1002/1003 期望 4096/4097 单次 DMA)、`docs/网络/BSD_Socket_API_使用指南.md:1246`(`NET_SEND_BUF_SIZE` 宏已不存在)。 +- 悬空引用:`Trap 19` 从未存在(`Test_Guide.md:878` 仍引用);`Test_Guide.md:353/418` 的"见风险 #4/风险表 #1"指向已删除的表。 +- 口径冲突:`Test_Guide.md:1135` 的 TRAP-07 行仍写"卡 FIN_WAIT_2/`close_socket`(RST)"(与 Trap 07 定案冲突);`:45` 的"TC704 40/40"与 `:880` 的"`PHASE7_MULTI_COUNT` ≤5"矛盾(HEAD 宏值仍是 8);`Drivers/BSP/NET/README.md:35` 与 `net_socket.c` 的"正常 FIN 路径零改动"与 Trap 23 二阶段矛盾。 +- ~~机理措辞~~(Trap 17/20 措辞、Trap 25 的 `NET_MSG_RESYNC_LISTEN`/`GET_REMOT_IPP`/§5.32+§5.23① 引用——**2026-10-13 重组已在本文件内更正**;`Test_Guide.md` 侧仍待同步)。 +- ~~已失效条目未标注~~(Trap 01/02/11 对现行初始化仍有效、Trap 09 `osDelay(7000)` 已被就绪屏障取代、Trap 10 注记失效、Trap 12 根因按 ff.c 更正——**2026-10-13 重组已全部就地标注**)。 +- ~~日期~~(Trap 01/02/03/04/06/07 的"2025-07"早于仓库首提交 `b1d05091` 2026-06-05;Trap 03/04/06 的 Phase 编号错——**2026-10-13 重组已标注为"不可考/已更正"**)。 +- 证据通则:`*.log` 在 `.gitignore` 内 → 本文所有台架计数(40/40、34/34、227 轮、4 轮 11/11、13/13…)**脚本在库、日志未入库 = 不可复核**;Trap 25 引用的实验分支 `n30-chip-bench`(tip `eddbede`)不含 bench 源文件与 uvprojx 注册,裸机芯片台架无法从任何已提交状态重建(主分支存档见 `test/board/ch395f_reuse_bench.c`)。 + +--- + +# 附录 A · 驱动关键点速查(细节以对应 Trap 为准) + +- **初始化顺序**:`SET_MAC → SET_IP/GWIP/MASK → INIT_CH395 → SET_PHY`(Trap 24;IP 族必须在 INIT 前——INIT 读取并锁定寄存器值;SET_PHY 在 INIT 后重建物理链路);`CMD_PING_ENABLE` 不需显式调用。 +- **SPI 通信**:每次事务 `ch395f_spi_begin()/end()` 包裹;分频 8(PCB 约束);大数据走 SPI2 DMA(`CH395F_SPI_DMA_BUF_SIZE`=2048+4=2052,`ch395f.h:148`;历史值 1500/4100 已作废,见 Trap 13/21)。⚠ **TSL 规格偏离(N34 E12,2026-10-13 登记)**:手册 §7.4 要求缓存区命令"长度与数据之间"留 TSL(读 0.4us/写 0.2us),本仓两个缓存区函数把命令头与数据打包进**同一 DMA 突发**(间隔=0)属规格偏离;**实测未发现危害**(460 轮/≈58MB 双向 SHA-256 仅 1 次未复现异常,"两段+TSL"变体与现状无差异)→ 依 Trap 25 铁律暂不改产品路径,偏离与证据登记于此、变体留作待用补丁(完整 A/B 见 `docs/网络/N34-自愈审计/实验方案.md` E12)。 +- **TCP 参数**(必须在 INIT_CH395 之前):重传 `SET_RETRAN_COUNT`(默认 12,最大 20)×`SET_RETRAN_PERIOD`(默认 500ms,最大 1000ms);KeepAlive 参数 500ms 倍数、IDLE>INTVL(Trap 06)。 +- **KeepAlive 定案链**:默认关闭 → 服务器侧应在 `SINT_STAT_CONNECT` 后开启(§5.56)→ 2026-10-12 曾因"链路层死亡已被重传 +2s 覆盖"撤回使能 → **2026-10-13 E11 转正**("链路在、对端静默"场景实测判死 ≈30s,参数生效、存活对端不误杀):`handle_connect_event` LISTENING 分支开启 60s/5s/3;消费链路 TIM_OUT → `handle_timeout_event` QUARANTINE → relisten → 端口恢复(详见第六章 B1 与 `docs/网络/N34-自愈审计/实验方案.md` E11/E13)。 +- **Socket 缓冲**:默认只给 Socket 0~3;Socket 4~7 必须 `open_socket` 前显式分配(Trap 02);现行固定布局 6 块/Socket。 +- **TCP 关闭重连**:直接 `ch395f_close_socket()`,不要先 `disconnect`;轮询 `GET_SOCKET_STATUS` **第 1 字节**等 `00H` 再 open;第 2 字节仅在 byte0=OPEN 时有意义(Trap 07)。 +- **SOCK_TIMEOUT**:`SINT_STAT_SOCK_TIMEOUT` 不应视为致命——记录日志后继续操作(消费侧 QUARANTINE 见 Trap 23/B1)。 +- **UDP 模式**:DesIP=0xFFFFFFFF → UDP Server(接收数据前 8 字节为信息头 `reserved[2] src_port[2] src_ip[4]`,§9.2.4 前 2 字节是数据长度);DesIP=具体 IP → UDP Client(无信息头)。发送后必须等 SENBUF_FREE(Trap 04)。 + +# 附录 B · FTP 支持改造一次性问题(历史存档,Trap 已单列者不重复) + +| # | 问题 | 处置 | +|---|------|------| +| 4 | CH395F 多连接模式不支持多监听 | 架构约束:`fun_para=0x00` 单连接(后定案 `0x08`,见 Trap 18) | +| 5 | PASV 端口 0(net_bind 无自动分配) | `net_socket.c: s_dynamic_port` | +| 10 | `s_file_open` 单文件限制 | `lftpd_io.c` 全局变量(已知限制) | +| 15 | `NET_RECV_TIMEOUT_MS=5s` 控制通道超时断开 | `net_config.h: 30000`(后随阶段 1.5 recv 语义再改) | +| 16 | `receive_file` 0 bytes 当作成功 | `lftpd.c: total==0 则失败` | +| 17 | MobaXterm 连数据端口不发数据 | 外部客户端行为(Python 抓包确认,超时恢复) |