fix(NET)+docs: lftpd 对端地址修复、E6 裁决收口、陷阱编号迁移重排 TRAP-NET-01~21

- lftpd:net_inet_ntoa 段序改 MSB 首 + 新增 net_getpeername,FTP 日志打印
  真实对端地址(真机实证 192.168.1.2:55524,登记 TRAP-NET-21)
- E6:NET_U15_PROBE 命中=0(含压测)→ 阶段 3 项 4 不立项,U15 登记观察项;
  插桩留仓(宏=0 零成本),soak 驱动 test/host/e6_soak.py 入库
- 编号:全库 Trap NN → TRAP-NET-<nn> 迁移并重排连续 01~21(单调映射,
  沿革见 CH395F_Trap_Records.md 头注);死号 12/13/19/21 引用全部清除;
  GD5F 域引用补正为 TRAP-GD5F-NN
- 陷阱文档全新化:索引 21 行行行真陷阱;删误判存档/发现时间/git 考古/
  FTP 附录;台架纪律教训沉淀第六章方法论
- RAM:Code 211348→211456(+108 lftpd 修复),RW+ZI 净增 +4B;
  优化方案/台账受管区/tsv 同笔同步
- ftp_abort_probe.py 挪 test/host/;引用关系图重生成
- 回归:@build 0/0;@test PASS;ftp_abort 10/10;relisten :7003 20/20;
  ci_local L0+L0.5 全绿;E6 窗口零复位,7 条 WD stall 登记压测假象备查
This commit is contained in:
2026-10-13 20:54:31 +08:00
parent 74ba7ea61b
commit 925058a5ae
58 changed files with 4391 additions and 3361 deletions
+170 -270
View File
@@ -1,273 +1,213 @@
# CH395F 驱动陷阱记录
# CH395F 驱动陷阱记录(TRAP-NET 系列)
记录开发过程中遇到的所有坑、根因、解决方案,避免重复踩坑。
CH395F 以太网驱动与 NET 层的实证陷阱:现象、定案根因、修复、回归判据。改驱动/NET 层前先读本文。
> **2026-10-13 重组审计**:合并同根因条目(14+17、13+21)、删除已被推翻的误判叙述(只留一行误判存档)、
> 按主题分章并加索引;条目格式统一为 `## Trap NN · 短标题` + 固定小节(状态/现象/根因(定案)/修复/回归判据/教训,
> 视情况取舍)。**编号永不重排**——代码注释大量引用 `Trap NN`,合并条目标题保留双编号;
> Trap 19 从未存在(悬空引用登记在第六章待修清单)。
> 状态图例:✅ 已修复定案 · ⏳ 待修 · ↩️ 已撤回(非缺陷) · ↦ 已移入他档 · — 从未存在。
> 编号口径:按 `docs/编号规范.md` 采用 `TRAP-NET-<nn>`,连续 01~21。编号沿革(2026-10-13 全库重排):
> 旧 01~11 不变;旧 14→12、15→13、16→14、17→12(并入)、18→15、20→16、22→17、23→18、24→19、
> 25→20、26→21;旧 12/13/19/21 已移出/并入/删除,无条目。旧形 `Trap NN` 为违规裸形已全库迁移;
> 旧号只存在于历史提交与旧日志。编号不复用、不重排。
> 状态图例:✅ 已修复定案 · ⏳ 待修。
## 索引
| 编号 | 标题 | 状态 | 一句话结论 |
|---|---|---|---|
| 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 短名 | ↦ | **非 CH395F 议题**(FTP 改造期归档错位)→ 已移入 `docs/架构设计/STM32F4-Base存储架构说明.md` §10.1 第 14 项 |
| 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-NET-01 | Socket 配置顺序 | ✅ | `TCP_LISTEN` 时刻一次性扫描:先配数据 Socket 1~7,最后配监听 Socket 0 |
| TRAP-NET-02 | Socket 缓冲显式分配 | ✅ | 48 块×512B 几何,Socket 4~7 默认零块,必须显式分配且不重叠 |
| TRAP-NET-03 | DHCP xid 偏移 | ✅ | xid=`data[4:8]`(Wireshark 旧版显示误导) |
| TRAP-NET-04 | UDP 发送后等 SENBUF_FREE | ✅ | 不等则下次写入被静默丢弃 |
| TRAP-NET-05 | PHY 自动协商不兼容 | ✅ | 与 RTL8305NBI 直连协商失败,强制 100M 全双工 |
| TRAP-NET-06 | KeepAlive 参数 500ms 倍数 | ✅ | 非 500ms 倍数 = 定时器未定义行为;IDLE>INTVL |
| TRAP-NET-07 | TCP 关闭重连 | ✅ | 直接 `close`,不要先 `disconnect`;`close` 无 RST(对端不配合则 `FIN_WAIT_2` 长尾,兜底靠查岗/`net_drop_conn`) |
| TRAP-NET-08 | RECV 中断电平触发 | ✅ | 缓冲有数据就一直拉低,`net_poll` 每轮只处理一批 |
| TRAP-NET-09 | 沉降窗(link up / close 后) | ✅ | open 失败重试 + `net_sock_reset_sync` 确定性排渣;LISTEN 态关闭是两段 CLOSE |
| TRAP-NET-10 | 消息队列值拷贝 | ✅ | 结果必须走 `xTaskNotify` 通知值回传 |
| TRAP-NET-11 | accept 拒绝 ESTABLISHED | ✅ | standalone_accept 后状态已 ESTABLISHED,入口检查须放行该组合 |
| TRAP-NET-12 | 残留 DISCONNECT 污染下一连接 | ✅ | 滞后锁存 + SINT 粘滞 → 预清 + 反核实 + close 后排空 |
| TRAP-NET-13 | DHCP 后 IP 寄存器提交滞后 | ✅ | 状态位≠寄存器生效:连续 3 次一致才采信 |
| TRAP-NET-14 | IP/端口字节序 | ✅ | IP 四字节数组=点分顺序;16 位端口=低字节在前,两套口径 |
| TRAP-NET-15 | FUN_PARA bit0 迷思 | ✅ | bit0 保留必须为 0(`0x08`);Socket 可用性取决于缓冲显式分配 |
| TRAP-NET-16 | 单次写超发送缓冲(写穿) | ✅ | 上限=本仓 2 块发送缓冲 1KB(`NET_SEND_CHUNK_MAX`),门控是 SENBUF_FREE |
| TRAP-NET-17 | 快速复连丢中断 | ✅ | 芯片丢 CONNECT/DISCONNECT → 周期对账 `net_reconcile_tick`(reconciler 之源) |
| TRAP-NET-18 | 上传中止后首笔复连被截断 | ✅ | 残渣投递给下一条连接 → 两级沉降/隔离 + 异常释放回 450 删截断文件 |
| TRAP-NET-19 | SET_MAC 顺序 | ✅ | §9.2.1:SET_MAC 必须在 `INIT_CH395` **之前**(INIT 从 EEPROM 加载) |
| TRAP-NET-20 | FTP 数据连接三连故障 | ✅ | 三重定案:丢中断窗口探针+幽灵甄别 / closed-with-data / 背靠背小段毒化 TX |
| TRAP-NET-21 | IP 显示段序与对端地址 | ✅ | `net_inet_ntoa` 段序 MSB 首;对端地址用 `net_getpeername`,勿用 `getsockname` |
---
# 第一章 初始化与配置
## Trap 01 · Socket 配置顺序:先数据 Socket 后监听 Socket
## TRAP-NET-01 · Socket 配置顺序:先数据 Socket 后监听 Socket
- **状态**:✅ 已定案(多连接模式虽已随 `319d00e3` 删除,**顺序规则仍适用于现行 `net_init`**——`net_socket.c` 注释「Trap 01:先配数据 Socket 1~7,再配监听 Socket 0」)
- **现象**:多连接模式下最多接受 3 个客户端,Socket 4~7 的 CONNECT 中断从不触发,SYN 被静默丢弃。
- **状态**:✅(顺序规则适用于现行 `net_init`——`net_socket.c` 注释「TRAP-NET-01:先配数据 Socket 1~7,再配监听 Socket 0」)
- **现象**: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,日期不可考)。
## Trap 02 · Socket 缓冲区必须显式分配且不重叠
## TRAP-NET-02 · Socket 缓冲区必须显式分配且不重叠
- **状态**:✅ 已定案(现行 `net_init` 固定布局 8×(4收+2发)=48 块即用满)
- **状态**:✅(现行 `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,不可考)。
- **修复**:每个 Socket 独占块(现行固定 6 块/Socket=4收+2发,与 MSS 解耦)。**必须在 `open_socket` 之前**调 `ch395f_set_send_buf/set_recv_buf`。
- **教训**:缓冲几何以 §5.45 的 48×512B 为准("32×1KB"说法有误,实测块 0~47 全部有效)。
## Trap 05 · CH395F 与 RTL8305NBI 自动协商不兼容
## TRAP-NET-05 · CH395F 与 RTL8305NBI 自动协商不兼容
- **状态**:✅ 已修复
- **状态**:✅
- **现象**:`ch395f_get_phy_status()` 恒返回 `PHY_DISCONN`,链路无法建立。
- **根因(定案)**:CH395F PHY 与 RTL8305NBI-CG 直连时自动协商失败(板级组合)。
- **修复**:初始化协议栈后强制 `ch395f_set_phy(CH395F_PHY_100M_FULL)`。
- **发现时间**:项目初期硬件联调。
## Trap 06 · KeepAlive 参数必须为 500ms 倍数
## TRAP-NET-06 · KeepAlive 参数必须为 500ms 倍数
- **状态**:✅ 已定案(产品参数 60s/5s/3 的转正过程见附录 A「KeepAlive 定案链」)
- **状态**:✅(产品参数 60s/5s/3 的定案链见附录 A)
- **现象**:设置 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
## TRAP-NET-15 · 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。
- **状态**:✅(`net_init` 用 `ch395f_set_fun_para(0x08)`)
- **现象**:曾以为必须 `0x09`(bit0=1"多 Socket 模式")才能使用 Socket 1~7。
- **根因(定案)**:bit0 在官方手册 V1C/V1E/V2.2/V2.3 的 §5.52 位表中一律「保留,必须为0」;官方 SDK 无 bit0 宏;官方例程传 `0x02` 却给 Socket 0~7 全配缓冲、官方 FTP 例程直接传 `0x08`。8 Socket 是芯片原生能力(`_SN` 索引寻址),真正的门槛是 §5.45 缓冲分配。概念讹传源:bit1「服务器多连接」指一个服务器能接几个客户端,不是能用几个 Socket。
- **修复/验证**:`0x08` 下 Phase 7 34/34 全通过,并发回显实测落在 sock2/4/5/6/7 → Socket 4~7 在 bit0=0 下完全可用。保留 bit0=1 的代价:违反保留位规定,后续固件版本一旦启用该位现网设备静默失效。
- **台架前置**:判"芯片行为"前先确认对端真的收到了 SYN——① PC 网卡落在 Public 防火墙配置文件 → 入站被静默丢弃,全量 `CONNECT TIMEOUT` 极易误判成芯片故障;② 残留 `tcp_server` 进程占端口不报错。
## Trap 24 · SET_MAC 必须在 INIT_CH395 之前
## TRAP-NET-19 · 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)。
- **状态**:✅(纯顺序调整,无 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 之后则本次上电不生效。
- **修复**:`net_init()` 把 `ch395f_set_mac_addr()` 移到 `ch395f_init()` 之前;四个 SET 函数注释统一"必须在 `ch395f_init()` 之前调用"。
---
# 第二章 中断与事件一致性
## Trap 08 · RECV 中断是电平触发
## TRAP-NET-08 · RECV 中断是电平触发
- **状态**:✅ 已定案
- **状态**:✅
- **现象**:`net_poll()` 中 RECV 中断无限触发,主循环永远得不到执行。
- **根因(定案)**:接收缓冲区有数据期间 `INT#` 保持低电平,`do{...}while(int_status!=0)` 必然死循环。
- **修复**:`net_poll()` 每轮只处理一批中断,由主循环负责读数据。
- **推论**:RECV 是条件位(数据读空即落),所以 Trap 25 B3 的全量读清对 RECV 可下轮补、对其余位是永久销毁。
- **推论**:RECV 是条件位(数据读空即落),所以 B3 的全量读清对 RECV 可下轮补、对其余位是永久销毁。
## Trap 14/17 · 上一条连接的残留 DISCONNECT 污染下一条连接(合并条目)
## TRAP-NET-12 · 上一条连接的残留 DISCONNECT 污染下一条连接
- **状态**:✅ 已定案(两级预清 + 排空为 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 实证)→ 无人消费则跨会话存活。
- **状态**:✅(两级预清 + 排空为 TRAP-NET-18/25 排渣契约的前身)
- **现象**:close 后立即复用同一 Socket 重连,新连接被上一条连接的 DISCONNECT 事件秒断(生产:`openOK` 后紧接 `DISCONNECTED, releasing`;硬件层:新会话第一次读 SINT 即命中上一会话遗留事件)。
- **根因(定案)**:① `GET_SOCKET_STATUS` 是纯查询**不清中断**;② DISCONNECT 可能在 CLOSED 达成同时或之后才锁存;③ SINT **粘滞到被读**(close/open 都不清,B3 实证)→ 无人消费则跨会话存活。关闭过渡实测:自身 FIN 的 ACK + 本地 `TIME_WAIT`,0~150 ms。
- **修复**:
1. **预清**:新会话 OPEN 前读一次全局 + 本 Socket SINT 丢弃陈旧事件(`phase2_tc203()` 双保险;生产 `net_connect_locked` open 后清本 Socket 中断);
1. **预清**:新会话 OPEN 前读一次全局 + 本 Socket SINT 丢弃陈旧事件(`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 取证。
- **注意**:B3 修复后预清只准读**本 Socket** SINT,不得再全量读 8 个(见第六章 B3)。
## Trap 22 · 快速复连丢 CONNECT/DISCONNECT 中断 → 监听静默死亡
## TRAP-NET-17 · 快速复连丢 CONNECT/DISCONNECT 中断 → 监听静默死亡
- **状态**:✅ 已修复定案(周期对账 `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_reconcile_tick` 是 N34 reconciler 的前身;阶段 3 已并入单一 100ms tick)
- **现象**:单连接监听 socket 快速复连后**永久失效**(后续 SYN 被拒或超时,10s 内不自恢复),芯片其余部分完全正常(其他监听端口照常工作)。两种表现:芯片完成握手但 CONNECT 中断未上报 → 控制块滞留 LISTENING;或 DISCONNECT 中断丢失 → 本层仍以为有客户端。两种情况芯片侧都占住单连接监听;应用侧连锁 `net_send` 被拒、读不到 EOF。
- **复现(可信判据,纯客户端;工具 `MDK-ARM/tcp_reconnect_probe.py`)**:待机 60s 后 `:7003` 300ms 快速复连 → 修复前首 2 次成功、之后永久拒连(2/10);FTP `:21` 100ms×50 → 第 29 轮起永久拒连。对照 `:21` 0ms/600ms 与 `:8888` 正常 → 触发条件 = 复连间隔 <~1s + 轻负载。
- **机制(设备侧日志实证)**:① 芯片丢 CONNECT 或 DISCONNECT 中断;② 此后本层**收不到任何中断**,纯事件驱动无路可走;③ 故必须**周期回读芯片状态对账**。裸机芯片台架(剥离 OS/net/FTP)独立复现——芯片天性,不是固件 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(确定性复现+当日修复闭环)。
1. **周期对账 `net_reconcile_tick()`**(与 pump 合并为单一 100ms tick):修正失同步形态——①本层 ESTABLISHED 而芯片已无连接→按断开处理;②本层 LISTENING 而芯片已握手→补 accept;③本层 LISTENING 而芯片监听消失→重建;④孤儿监听(B10)→重建。**去抖+冷却**:同一失配连续 2 次才动手,动手后冷却 1s。
2. `handle_disconnect_event` LISTENING 分支:回读芯片状态——已不可监听即重建。
- **踩坑记录(务必先读)**:基于单次状态读的重建动作放在 accept 热路径 → 把"刚 open/listen 尚未生效"的瞬态误判为失配 → 监听反复重建直至 `relisten give up` → **三个监听口全部死亡(风暴)**。**结论:凡基于单次状态读的重建动作,必须去抖。**
- **回归基线**:`:7003` 300ms×10:**10/10**;`:21` 100ms×50:**50/50**;`:21` 0ms/600ms×50:**50/50**;`:7003` 200ms×20:**20/20**;FTP 10×64KB SHA-256:**10/10**;`relisten give up`:**0**。三类失同步均有自愈实测计数留痕。
- **对应文件**:`net_socket.c`(`net_reconcile_tick`);`MDK-ARM/tcp_reconnect_probe.py`。
## Trap 25 · FTP 数据连接三连故障——四重定案(N-30)
## TRAP-NET-20 · FTP 数据连接三连故障——三重定案
- **状态**:✅ 已定案并修复(回归 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(对账不是元凶)。
- **有效修复一:双信源原则——中断只是快路径,状态寄存器才是真相**:
- **状态**:✅(回归 4 轮 11/11;全局寄存器读即清见第六章 B3)
- **现象**:同一会话内**第 2 次及以后**的数据连接(LIST/STOR/RETR)两类失败,概率随上一条数据连接的负载与间隔变化:① **吞数据**——新连接建立成功、服务端 `net_send` 返回成功,客户端 20s 收不到任何字节;② **425**——新连接被接受后 ~10-20ms 出现 DISCONNECT(`chip tcp=0x00`),accept 失败。
- **定案实验(裸机芯片台架,剥离 OS/net/FTP)**:`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-NET-17 机制的窗口期形态)→ 吞数据;② 状态直接假报 0x04 幽灵态(回读对端端口=旧连接端口);③ 旧连接滞后 DISCONNECT 打到新监听 → relisten 毁掉新连接 → 425。
- **已证伪的修复**(受控实验全部无效,列出防重踩):close 后固定沉降 100~3000ms / 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);
2. **幽灵态甄别**:补 accept 前回读对端端口(`GET_REMOT_IPP`),**= 上一条连接端口(`last_peer_port`)→ 幽灵,不 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)。
- **定案二:closed-with-data accept(425 真根因=accept 竞态自毁缓冲)**:`FUN_PARA` bit3 `SOCKET_CLOSE=1`(§5.52 ②)下产生 DISCONNECT 后芯片**不关 Socket、不清缓冲**,RX 缓冲里 STOR 载荷一个不少(探针实证 `recv_len=1027 re-read st=0x05/0x00`);那个 DISCONNECT 是**客户端正常 FIN**(ftplib `storbinary` 发完即关,局域网小文件 ~5ms)。旧 `net_accept_locked` 把 CLOSED 一律当"无连接" → `net_relisten_try`(close+open **清空芯片 RX 缓冲**)→ 数据被**我方亲手销毁** → 425。**修复**:CLOSED + auto_relisten + **芯片 RX 有数据**(`GET_RECV_LEN_SN`>0)→ 接受这条"已关但可读"连接(日志 `closed-with-data accept (drain pending)`),应用快路径排空,排空后 EOF 自然触发 relisten——对齐 §5.52 ② 契约。
- **定案三:LIST 吞包 = 背靠背小段写毒化芯片 TX**:决定性对照——同一目录,LIST #1(行间天然间隔)通过;LIST #4/#7(每行一次 `net_send` ~60B 背靠背小段)必挂。芯片对同一连接的**背靠背多段小写入**会毒化 TX(芯片报 SEND_OK 但对端全部丢弃,无 RST 无 DISCONNECT);RETR 1KB 满块连发不受影响。**修复**:`send_list`/`send_nlst` 整份目录**一次性批量发送**(static 1536B 缓冲;注意 `lftpd_inet_write` 成功返回**字节数**非 0)。
- **与 TRAP-NET-17 的关系**:周期对账对常驻监听器有效;FTP 数据通道是"活 1 秒就死"的短命监听器,对账②无幽灵甄别时既救不全又帮凶。本条把"看状态补 accept"内联进 accept 热路径(短去抖、无冷却)+ 端口甄别,对账②保留为兜底网。
- **教训(总纲)**:**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`(主分支存档)。
---
# 第三章 连接生命周期与自愈
## Trap 07 · TCP 关闭重连:直接 close,不要 disconnect
## TRAP-NET-07 · TCP 关闭重连:直接 close,不要 disconnect
- **状态**:✅ 已定案(做法保留;原根因说法已推翻,见定案更正)
- **误判存档**:原断言"`disconnect → close` 让 Socket 卡 `FIN_WAIT_2` 数分钟、close 发 RST"——**TC-NET-204 实测推翻**(2026-10-11),机制说法作废,做法保留。
- **定案(TC-NET-204,三模式各 5 轮 45 项判定全过)**:
- **状态**:✅ 定案(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)。
| C | `disconnect`(不 close) | **10s 内从未回到**(byte0 恒 05H) | 直接 open 必返 **0x20 FAIL**;补 close 后 +0ms 回 CLOSED |
1. `close` **无 RST 语义**:§5.35 只说"关闭前自动断链";手册中芯片主动发 RST 仅 §5.29(监听占用)与 §9.2.6(多连接无可用数据 Socket)两处,均与 close/disconnect 无关;
2. **C 模式真因**:`FUN_PARA` bit3 `SOCKET_CLOSE=1`(§5.52 ②)下产生 DISCONNECT 后**芯片不关 Socket、不清缓冲**,长期停在 `05H/00H`;此时直接 `open_socket` 必返 `0x20` FAIL——**把 Socket 拉回 CLOSED 的动作是 `close`**;
3. **做法**:直接 `ch395f_close_socket()`;`disconnect` 纯属多余且会**制造** DISCONNECT 事件(bit3=1 下必须被消费/预清,否则污染下一条连接,TRAP-NET-12/23 实证);
4. **观测口径**:§5.22 第 1 字节只有 `00H/05H`,第 2 字节(TCP 态)**仅在 byte0=OPEN 时有意义**——关闭后轮询**第 1 字节**等 `00H` 再 open,别拿 byte1 当"卡住"判据。
- **边界(E-02 台架)**:`close` 无 RST 的适用条件——对端配合(收到 FIN 后其内核 close):芯片走 `TIME_WAIT` ~150ms → relisten → 新连接接入;**对端内核存活但不 close**:芯片停在 **`FIN_WAIT_2`(byte0=05H/byte1=06H)长尾**——没有 RST 就拆不掉,**监听恢复受限于对端 close**。不配合场景的兜底 = 芯片自身 TIMEOUT/KeepAlive 拆僵尸会话(判死窗 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 到达即自动走完——**不搁浅**。
## Trap 09 · 沉降窗:PHY link up 后 / close 后不能立即 open
## TRAP-NET-09 · 沉降窗:PHY link up 后 / close 后不能立即 open
- **状态**:✅ 已定案(现行形态=open 重试环 + `net_sock_reset_sync`;原 `osDelay(7000)` 启动延时已被就绪屏障取代)
- **现象**:PHY_CHANGE 中断同一时刻 `ch395f_open_socket()` 返错(非 BUSY 超时),首次 listen 失败;3 秒后重试成功。
- **状态**:✅(现行形态=open 重试环 + `net_sock_reset_sync`;启动错峰已由就绪屏障承担)
- **现象**:PHY_CHANGE 中断同一时刻 `ch395f_open_socket()` 返错(非 BUSY 超时),首次 listen 失败。
- **根因(定案)**: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 后需沉降才能重开"的前置形态:
1. `net_listen_locked`/`net_connect_locked` open 失败重试 ×3(D10 `net_listener_retry_tick` 重试环);
2. open 失败先走 `net_sock_reset_sync`(确定性排渣,内置二段 CLOSE,见下)再重试——最短等 100ms;
3. 启动错峰走**就绪屏障**(`SYS_EVT_NET_READY`/`FTP_READY`),不用魔法延时。
- **补充:LISTEN 态 Socket 的二段关闭**(E2/E4 芯片实验,本条"沉降才能重开"的前置形态):
- 对 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 必成"。
- 驱动侧已内置补发:`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。
## Trap 11 · net_accept_locked 拒绝 standalone_accept 的 ESTABLISHED
## TRAP-NET-11 · net_accept_locked 拒绝 standalone_accept 的 ESTABLISHED
- **状态**:✅ 已修复定案(现行 `net_accept_locked` 仍保留该放行逻辑)
- **状态**:✅(现行 `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)。
- **修复**:放行 `standalone_accept && state == ESTABLISHED` 组合。仅影响 standalone_accept 模式(PASV 数据通道、单连接 FTP)。
## Trap 23 · 上传中止后首笔复连上传被静默截断(≤2048B)并误报成功
## TRAP-NET-18 · 上传中止后首笔复连上传被静默截断(≤2048B)并误报成功
- **状态**:✅ 已修复定案(三阶段全部闭环;回归判据常备:`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`)不受影响(不按传输复用)。
- **状态**:✅(三阶段全部闭环;回归判据常备:`test/host/ftp_abort_probe.py`)
- **现象**:客户端上传(STOR)中途断开(**RST 或优雅 FIN 均可**),**紧随其后的第一笔复连上传被截断到 ≤2048 字节(= 芯片单 socket RX 缓冲大小),且服务端回报 226 成功** = **静默数据损坏**;第二笔及以后恢复正常。
- **证据(可信复现:生产固件、零固件探针)**:判据 = FTP 协议结果 + 紧接一笔正常上传的 SHA-256 往返 + 控制通道 NOOP 存活探测。RST 中止 **10/10** 复现;优雅 FIN **5/5** 同样复现 → 凡"数据未发完即断开"均触发。对照(无中止)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 命令逻辑。
- **边界甄别**: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)。
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 稳定通过);日志区分 `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**。
- **协议边界(不可判定,留痕)**: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。
- **回归基线**:`--check-abort` RST **5/5**(450)+ FIN **5/5**(226 边界不变);followup 全过;正常往返 freeze SHA-256 全 MATCH;`:7003` sink 复用 RST×4/FIN×4 全过;`:8888` 真实收包写 RTC 成功。
- **与 TC-NET-604 的关系**:本条是独立、已可信复现并已修复的真实缺陷;TC-NET-604 已结案为非缺陷(判读教训见第六章台架纪律)。现场"上传失败/文件不完整/像卡死"类报告优先用本条判据复核。
---
# 第四章 数据通路与数值语义
## Trap 04 · UDP 发送后必须等待 SENDBUF_FREE
## TRAP-NET-04 · UDP 发送后必须等待 SENDBUF_FREE
- **状态**:✅ 已定案
- **状态**:✅
- **现象**:UDP 发送后立即接收,接收长度恒 0 或数据错误。
- **根因(定案)**:手册要求每次 `WRITE_SEND_BUF` 后必须等 `SINT_STAT_SENBUF_FREE`,否则下次写入被静默丢弃。
- **修复**:每次 `ch395f_write_send_buf()` 后轮询 `ch395f_get_sock_int_status()` 检查该标志。
- **发现时间**:广播宣告模式(原记 Phase 6 2025-07 有误,UDP 对应阶段 3)。
## Trap 13/21 · 「recv_len 归零 / 接收缓冲冻结」家族——非缺陷(合并结案)
## TRAP-NET-14 · NET 层目的 IP 字节序写反 → CONNECT TIMEOUT
- **状态**:↩️ 已撤回(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。
## Trap 16 · NET 层目的 IP 字节序写反 → CONNECT TIMEOUT
- **状态**:✅ 已修复定案(字节序总口径,多处引用)
- **状态**:✅(字节序总口径,多处引用)
- **现象**: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 输入输出命令字节顺序均为低字节在前"与官方例程自相矛盾,**本仓以实测+例程为准**。
- **根因(定案)**:`net_connect_locked()` 把 `sin_addr.s_addr`(网络字节序)按**低字节在前**拆成 `remote_ip_arr[4]` 交给 `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()` 同样修正;③ 诊断回读按小端语义重组再打印。
- **字节序总口径(务必分清两套)**:
@@ -275,143 +215,103 @@
- **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。
## Trap 20 · 单次写超过发送缓冲区(写穿)
## TRAP-NET-16 · 单次写超过发送缓冲区(写穿)
- **状态**:✅ 已修复定案(措辞已按 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 直接**写穿发送缓冲**(超出部分覆盖/丢失或破坏指针)。
- **根因(定案)**:分块上限误取 `NET_DMA_MAX_PAYLOAD`(2KB)/软件缓冲(4KB),而每 Socket 的发送缓冲 = 本仓固定 2 块×512B = **1KB**(§5.45 上电默认其实是 4 块=2KB,本仓布局固定 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-NET-21 · IP 显示段序与对端地址(getsockname ≠ 对端)
- **状态**:✅(N34 阶段 3 收尾修复)
- **现象**:FTP 日志把 IP 打成 `100.1.168.192`(字节反转),且打印的是**本机** IP:21 而非客户端地址——排查连接来源时被误导。
- **根因(定案)**:① `net_inet_ntoa` 段序反转——`NET_NTOHL` 后主机数字的 **MSB 才是点分首段**,原实现低段在前;② lftpd 打印 `net_getsockname` = 本机监听地址(端口恒 21),对端地址应读 `net_getpeername`。PASV 227 消费点做了 `net_htonl` 再 MSB 拆,**双重反转恰好正确,勿误判**。
- **修复**:`net_inet_ntoa` 段序改 MSB 首(MCU+模拟器同步);新增 `net_getpeername`(读 CONNECT 事件缓存,零 SPI);lftpd 改打印**对端** IP:port(台架判据:日志 `connection received from 192.168.1.x:<高位端口>`)。
- **教训**:显示层字节序 bug 不影响协议本身(FTP 功能全程正常),但会系统性误导排障——**日志里的地址也要当被测对象核对**。
---
# 第五章 上层集成与主机侧工具
## Trap 03 · DHCP 包 xid 偏移错误(主机工具)
## TRAP-NET-03 · DHCP 包 xid 偏移错误(主机工具)
- **状态**:✅ 已修复
- **状态**:✅
- **现象**: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 有误)。
## Trap 10 · 消息队列值拷贝导致结果无法回传
## TRAP-NET-10 · 消息队列值拷贝导致结果无法回传
- **状态**:✅ 已定案(现行契约:`net_*` 错误码走通知值——AGENTS 模块约定)
- **状态**:✅(现行契约:`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)。
- **注意**:指针成员(`msg.buf`)不受值拷贝影响;内核级 `_locked` API 直接操作不受影响。
## Trap 12 · FatFS 路径与 8.3 短名(↦ 已移入他档)
## TRAP-NET-13 · DHCP 成功后 IP 寄存器提交滞后
- **状态**:↦ 移档——**非 CH395F 议题**(FTP 改造期归档错位;`docs/网络/N34-自愈审计/CH395F驱动层审计.md` 早已点破"Trap 12 非 CH395F")。完整内容与根因更正(含按 `Lib/FatFs/ff.c` 删去的两条误判根因)已移入 `docs/架构设计/STM32F4-Base存储架构说明.md` §10.1 第 14 项(2026-10-13)。
- 编号在此保留仅为旧引用不断链;发现期:FTP 改造期(2026-08)。
## Trap 15 · DHCP 成功后 IP 寄存器提交滞后
- **状态**:✅ 已定案(测试侧稳定轮询+交叉比对)
- **现象**:`GET_DHCP_STATUS` 已置 0x00 后立即 `GET_IP_INF` 读到的仍是旧静态配置,而芯片实际已按新租约收发——三方数据矛盾构成假阳性(2026-08-24 上板实测)。
- **状态**:✅(测试侧稳定轮询+交叉比对)
- **现象**:`GET_DHCP_STATUS` 已置 0x00 后立即 `GET_IP_INF` 读到的仍是旧静态配置,而芯片实际已按新租约收发——三方数据矛盾构成假阳性。
- **根因(定案)**: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。
---
# 第六章 2026-10-11 寄存器/数值口径全量审计(A/B 系列)
# 第六章 审计遗留与待修(B 系列,冻结号)
**方法**:四路并行——驱动层逐命令(含官方 SDK 逐字对照)、NET/测试层逐调用点、Trap 01–12 复核、Trap 13–25 复核。三源交叉:`docs/芯片手册/CH395F.md` + WCH 官方例程 `CH395EVT/EXAM/PUB/CH395CMD.c`、`CH395INC.H` + git 历史。
## 干净清单(核对过、无异常)
| 项 | 计数 | 结论 |
|---|---|---|
| `CH395F_CMD_*` 命令码 | 55 | 与 §5 命令表 + SDK `CMDxx_` 逐个一致,无一抄错 |
| 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` 拼装方向 | — | 与官方 `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/200ms、KeepAlive 60000/5000、ARP 100ms 单位、MSS 1024 全部合规 |
| 缓冲布局 | 3 处组合 | `net_init` 8×(4收+2发)=48 块恰好用满、无交叠(零裕量) |
| 显示假象 | 10 处 `%d.%d.%d.%d` | 只有已定案的 `net_socket.c` getsockname 一处是 MSB 重排假象(见 B5),无新增同类 |
## 新发现(去重后按严重度)
2026-10-11 寄存器/数值口径全量审计(四路并行:驱动逐命令 + NET/测试逐调用点 + Trap 复核;三源交叉:`docs/芯片手册/CH395F.md` + WCH 官方例程 + git 历史)的未闭环项。A 系列与已修 B 项的结论已沉淀进上文条目,不再重复;B 编号冻结不重排。
| # | 严重度 | 位置 | 问题 | 状态 |
|---|---|---|---|---|
| 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_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 单次写 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/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′)为何对本产品不可用) |
| B2 | 中低 | `net_connect_locked` | `ch395f_tcp_connect()` 返回值曾被丢弃(§9.2.2 ⑧:SUCCESS 仅表示**命令**执行成功)。已修:非 SUCCESS 且**非 BUSY** 才判失败(`ERR_BUSY`=芯片仍在执行,当失败会误杀慢芯片);明确失败 → `[ERR] rejected by chip: 0xNN` + 按 §9.2.2 ⑧ 关 Socket 回滚 + 返 -1 | ⚠ 残余:`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 又**粘滞到被读** → 全量读走即**永久销毁**其 CONNECT/DISCONNECT/TIM_OUT/SENBUF_FREE(唯 RECV 是条件位可下轮补,TRAP-NET-08)。官方 SDK 21 个例程一律"全局位图门控 + 单 Socket 读 + 立刻转存",无一例全量读 8 个 SINT | ✅ 已修:两处收窄为**只清本 Socket SINT** + **删除丢弃式全局读清**(全局交 `net_poll` 集中读清=AGENTS 模块约定)。⚠ 残余:`net_connect_locked`/`net_close_locked`/`net_relisten_locked` 三处指定预清仍含无条件全局读清(会**搁浅**而非销毁其它 Socket 挂起事件)——它们是 TRAP-NET-12/23 的两级预清,宜与 B4 一并处置 |
| B4 | 低 | `net_socket.c` 全局中断分发 | 全局中断 bit0-3(`UNREACH`/`IP_CONFLI`/`PHY_CHANGE`/`DHCP`)被读清但从不消费;`ch395f_get_unreach_info()` 全仓零调用 → IP 冲突、ARP 不可达、链路抖动三类事件完全静默 | ⏳ 待修(可观测性缺口) |
| B6 | 低 | `net_socket.c` 3 处 | 状态判据只看 `byte1`、未按 §5.22 用 `byte0` 门控;reconcile 的 else-if 链里 `(00H, 陈旧 04H)` 会先命中"补 accept"而非"重建监听" | ⏳ 待修(TRAP-NET-07 实测 `byte0=00H` 时 `byte1` 读回 `0x00`,故为低概率) |
| B7 | 低 | `ch395f_test_task.c` Phase 5 | 单次写 4093B 超 1KB 发送缓冲与 2048 DMA 载荷 → 整块被丢;注释仍写旧上限。写路径金丝雀(A6)会打 `[ERR]` | ⏳ 待修(chunk 改 ≤1024 或分块) |
| B8 | 低 | `ch395f_test_task.c` Phase 4 | 把 sock6 缓冲重排到块 24-27,与 `net_init` 固定布局中 sock4 的 24-27 **交叠**(§5.45 禁止) | ⏳ 待修 |
| B9 | 低 | `ch395f_reuse_bench.c`、`net_socket.c` | 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,**四方互等**。对账①③都要求本层处于 ESTABLISHED/LISTENING,**孤儿态恰好落在两类之外** | ✅ 已修:对账新增**形态④**认领孤儿(`in_use`+`state==CLOSED`+`auto_relisten`+`type==STREAM`+`local_port!=0`,芯片侧不在 LISTEN)→ `net_relisten_try`。⚠ `auto_relisten` 是决定性门:少了它出向连接会被误判孤儿 relisten 成监听器。⚠ ④ 的副作用:`net_recv` 的 EOF 是**边沿**——④抢先 relisten 会把慢消费者的 EOF 永久吞掉。**根治出口 = `net_drop_conn` + 生命周期设计**(N34 阶段 3 已落):原理与分阶段方案 → `docs/子系统设计/网络Socket监听生命周期设计说明.md` |
## 台架注入陷阱(方法论,2026-10-12 起持续补充)
## 台架注入陷阱(方法论,持续补充)
- **Windows 出站防火墙规则不能模拟"对端静默死亡"**:WFP 对**已建立**连接有状态缓存,中途 `New-NetFirewallRule -Direction Outbound -Action Block` 不影响既有流——实测静置+阻断 250s 芯片毫无反应,看似"KeepAlive 不生效",实则 ACK 一直正常往返。
- **Windows 出站防火墙规则不能模拟"对端静默死亡"**:WFP 对**已建立**连接有状态缓存,中途 `New-NetFirewallRule -Direction Outbound -Action Block` 不影响既有流——看似"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 }`。
- **主机侧回显服务器收不到入站连接 → 全量 `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 干净启动,并取多样本再比均值。
- **对照组必须从干净态起测**:上一腿若**没死透**就重启下一腿,对照组失真。每个样本前 `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 生效",把相关性当因果。
- **判读通则(与 TRAP-NET-20 同源)**:看到 `TIMEOUT` 先**算时序再归因**——2s 与 60s 一眼可分;否则会把"重传超时"误记成"KeepAlive 生效",把相关性当因果。
- **台架纪律(数据完整性,沉淀自已撤回的「recv_len 归零 / 接收冻结」家族)**:TC-NET-604 及其同族(含旧号 13 的"部分读后 `recv_len` 归零"机理——与 §5.34 明文相左)结案为**非缺陷**,可信判据(产品固件 + 真实 lftpd 上传 + RETR 回读 SHA-256)**227 轮全过**(含零间隔与 100 轮浸泡)。原"复现"的根源全在测试配方自身:固件内非回显读者吞数据(短收量恰等于被吞字节数 `4736−4608=128=2×64`)、分类脚本把通过报成 `STALL`。三条规则:① 完整性以 **SHA-256 往返**为真值,并扣除固件读者吞噬的字节;② 冻结/卡死类走**真实生产路径 + 端到端校验**(`MDK-ARM/ftp_freeze_probe.py`),卡住时用控制通道 NOOP 区分"数据面卡死"与"整层卡死";③ **禁止把"固件内非回显读者"与固定期望值混用在同一配方里**。代码保留的 MSS 解耦(`ch395f.h`)与 OVERSIZE 金丝雀(`ch395f.c`)与撤回机理无关,**勿回退**。
## 主机侧测试工具过期(2026-10-12 实测)
## 主机侧测试工具过期
- **`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_abort_probe.py` 已修**:原用匿名登录,N-19 后匿名被 530 拒 → TRAP-NET-18 两条腿**假 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`)。
## 文档侧待修清单(其余登记在此)
## 文档侧待修清单
- **最高优先级**: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。
- ~~已撤回机理仍被当作现行约束~~(**2026-10-13 已处置**:`CH395F_Test_Guide.md` ×7、`BSD_Socket_API_函数调用关系.md` ×2、`net_socket.c` ×4、`ch395f.h` ×1 全部改写为"未证实、保守按一次读空执行")。残余:补一条"部分读 → 再查 `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 移入存储架构说明 §10.1——**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`)。
- 悬空引用:`Test_Guide.md:353/418` 的"见风险 #4/风险表 #1"指向已删除的表(旧 19 悬空引用已于 2026-10-13 重排时从 `BSD_Socket_API_函数调用关系.md` 清除)。
- 口径冲突:`Test_Guide.md:1135` 的 TRAP-NET-07 行仍写"卡 FIN_WAIT_2/`close_socket`(RST)"(与 TRAP-NET-07 定案冲突);`:45` 的"TC704 40/40"与 `:880` 的"`PHASE7_MULTI_COUNT` ≤5"矛盾(HEAD 宏值仍是 8);`Drivers/BSP/NET/README.md:35` 与 `net_socket.c` 的"正常 FIN 路径零改动"与 TRAP-NET-18 二阶段矛盾。
- 证据通则:`*.log` 在 `.gitignore` 内 → 本文所有台架计数(40/40、34/34、227 轮、4 轮 11/11、13/13…)**脚本在库、日志未入库 = 不可复核**;TRAP-NET-20 引用的实验分支 `n30-chip-bench`(tip `eddbede`)不含 bench 源文件与 uvprojx 注册,裸机芯片台架无法从任何已提交状态重建(主分支存档见 `test/board/ch395f_reuse_bench.c`)。
---
# 附录 A · 驱动关键点速查(细节以对应 Trap 为准)
# 附录 A · 驱动关键点速查(细节以对应条目为准)
- **初始化顺序**:`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 抓包确认,超时恢复) |
- **初始化顺序**:`SET_MAC → SET_IP/GWIP/MASK → INIT_CH395 → SET_PHY`(TRAP-NET-19;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 已作废)。⚠ **TSL 规格偏离(N34 E12 登记)**:手册 §7.4 要求缓存区命令"长度与数据之间"留 TSL(读 0.4us/写 0.2us),本仓两个缓存区函数把命令头与数据打包进**同一 DMA 突发**(间隔=0)属规格偏离;**实测未发现危害**(460 轮/≈58MB 双向 SHA-256 仅 1 次未复现异常,"两段+TSL"变体与现状无差异)→ 依 TRAP-NET-20 铁律暂不改产品路径,偏离与证据登记于此、变体留作待用补丁(完整 A/B 见 `docs/网络/N34-自愈审计/实验方案.md` E12)。
- **TCP 参数**(必须在 INIT_CH395 之前):重传 `SET_RETRAN_COUNT`(默认 12,最大 20)×`SET_RETRAN_PERIOD`(默认 500ms,最大 1000ms);KeepAlive 参数 500ms 倍数、IDLE>INTVL(TRAP-NET-06)。
- **KeepAlive 定案链**:默认关闭 → 服务器侧应在 `SINT_STAT_CONNECT` 后开启(§5.56)→ 链路层死亡实测 +2s 即被重传(§5.17 8×200ms)覆盖、与 KeepAlive 无关 → **"链路在、对端静默"场景实测判死 ≈30s**(60s/5s/3,早于手册公式 75s,以实测为准;短参数 6s/2s/2 → ≈5.1s 证明参数生效;静默但存活对端不误杀)→ **已转正**:`handle_connect_event` LISTENING 分支开启 60s/5s/3;消费链路 TIM_OUT → `handle_timeout_event` QUARANTINE → relisten → 端口恢复(方法学与数据见 `docs/网络/N34-自愈审计/实验方案.md` E11/E13)。
- **Socket 缓冲**:默认只给 Socket 0~3;Socket 4~7 必须 `open_socket` 前显式分配(TRAP-NET-02);现行固定布局 6 块/Socket。
- **TCP 关闭重连**:直接 `ch395f_close_socket()`,不要先 `disconnect`;轮询 `GET_SOCKET_STATUS` **第 1 字节**等 `00H` 再 open;第 2 字节仅在 byte0=OPEN 时有意义(TRAP-NET-07)。
- **SOCK_TIMEOUT**:`SINT_STAT_SOCK_TIMEOUT` 不应视为致命——记录日志后继续操作(消费侧 QUARANTINE 见 TRAP-NET-18/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-NET-04)。