From 1a35b3684ce2ff918da1365394294d65032c6493 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 13 Oct 2026 23:23:55 +0800 Subject: [PATCH] =?UTF-8?q?docs(N34):=20=E8=87=AA=E6=84=88=E5=AE=A1?= =?UTF-8?q?=E8=AE=A1=E7=9B=AE=E5=BD=95=207=20=E4=BB=BD=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E4=B8=BA=E5=8D=95=E6=96=87=E6=A1=A3=EF=BC=8C?= =?UTF-8?q?=E5=85=A8=E5=BA=93=E5=BC=95=E7=94=A8=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `docs/网络/N34-自愈审计/` 目录 7 份文档(根因总览 / net 层盘点 / 驱动层审计 / 主流对照 / 汇总裁决 / 实施方案 / 实验方案)合并为 `docs/网络/N34-自愈审计.md`(§0~§6),原目录删除;docs/README.md 索引与变更日志登记归档 - 全库引用同步:4 篇文档 + 驱动/NET/test 代码注释 + 配图生成器与 SVG - 内容零丢失抽查:E0~E13 实验记录、E6 裁决(命中=0 不立项)、 提交表(含 `925058a` 收口行)均在位 - 门禁:@build 0/0(注释级路径更新);check_doc_links --all 143 文档 通过(净 −6 = 删 7 增 1 吻合);ci_local L0+L0.5 全绿 --- Drivers/BSP/CH395F/ch395f.c | 4 +- Drivers/BSP/CH395F/ch395f.h | 4 +- Drivers/BSP/NET/net_socket.c | 4 +- Drivers/BSP/NET/net_types.h | 2 +- docs/README.md | 2 + docs/images/网络不稳根因总览.svg | 2 +- .../网络Socket监听生命周期设计说明.md | 6 +- docs/测试说明/CH395F_Test_Guide.md | 4 +- docs/网络/BSD_Socket_API_使用指南.md | 8 +- docs/网络/N34-自愈审计.md | 2062 +++++++++++++++++ docs/网络/N34-自愈审计/CH395F驱动层审计.md | 293 --- docs/网络/N34-自愈审计/README.md | 135 -- docs/网络/N34-自愈审计/net层自愈机制盘点.md | 346 --- docs/网络/N34-自愈审计/主流方案对照.md | 162 -- docs/网络/N34-自愈审计/实施方案.md | 223 -- docs/网络/N34-自愈审计/实验方案.md | 735 ------ docs/网络/N34-自愈审计/网络不稳根因总览.md | 119 - docs/网络/Net层阻塞模型架构设计.md | 4 +- docs/问题记录/CH395F_Trap_Records.md | 6 +- test/board/ch395f_reuse_bench.c | 2 +- test/board/ch395f_test_task.c | 2 +- test/host/e6_soak.py | 2 +- tools/arch_diagram/gen_net_instability_svg.py | 2 +- 23 files changed, 2090 insertions(+), 2039 deletions(-) create mode 100644 docs/网络/N34-自愈审计.md delete mode 100644 docs/网络/N34-自愈审计/CH395F驱动层审计.md delete mode 100644 docs/网络/N34-自愈审计/README.md delete mode 100644 docs/网络/N34-自愈审计/net层自愈机制盘点.md delete mode 100644 docs/网络/N34-自愈审计/主流方案对照.md delete mode 100644 docs/网络/N34-自愈审计/实施方案.md delete mode 100644 docs/网络/N34-自愈审计/实验方案.md delete mode 100644 docs/网络/N34-自愈审计/网络不稳根因总览.md diff --git a/Drivers/BSP/CH395F/ch395f.c b/Drivers/BSP/CH395F/ch395f.c index 912738da..a04d0212 100644 --- a/Drivers/BSP/CH395F/ch395f.c +++ b/Drivers/BSP/CH395F/ch395f.c @@ -1410,8 +1410,8 @@ void ch395f_get_unreach_info(uint8_t *p_buf) { } /* ============================================================================ - * N34 阶段 2 驱动原语(实施方案 docs/网络/N34-自愈审计/实施方案.md §4; - * 形态定案 docs/网络/N34-自愈审计/实验方案.md E2/E3/E4,2026-10-13 台架实证) + * N34 阶段 2 驱动原语(实施方案 docs/网络/N34-自愈审计.md §5.4; + * 形态定案 docs/网络/N34-自愈审计.md §6 E2/E3/E4,2026-10-13 台架实证) * 只加不接线:net_poll 全量刷新 / 两级预清改调用 / 链路 feed 接线 = 阶段 3。 * ========================================================================== */ diff --git a/Drivers/BSP/CH395F/ch395f.h b/Drivers/BSP/CH395F/ch395f.h index c2966c8e..ba9da2d1 100644 --- a/Drivers/BSP/CH395F/ch395f.h +++ b/Drivers/BSP/CH395F/ch395f.h @@ -748,8 +748,8 @@ void ch395f_set_ttl(uint8_t sock, uint8_t ttl); void ch395f_get_unreach_info(uint8_t *p_buf); /* - * 函数声明区 - N34 阶段 2 驱动原语(实施方案 docs/网络/N34-自愈审计/实施方案.md §4; - * 形态定案 docs/网络/N34-自愈审计/实验方案.md E2/E3/E4) + * 函数声明区 - N34 阶段 2 驱动原语(实施方案 docs/网络/N34-自愈审计.md §5.4; + * 形态定案 docs/网络/N34-自愈审计.md §6 E2/E3/E4) * ⚠ 阶段 2 = 只加不接线:net_poll 全量刷新、三处两级预清改调用、链路 feed 接线 * 均在阶段 3 收敛时进行。E2 实测:单次状态回读 18.1us;LISTEN 可见延迟 100% <=10ms。 */ diff --git a/Drivers/BSP/NET/net_socket.c b/Drivers/BSP/NET/net_socket.c index 02ba6996..da878edb 100644 --- a/Drivers/BSP/NET/net_socket.c +++ b/Drivers/BSP/NET/net_socket.c @@ -39,7 +39,7 @@ */ #define NET_PHY_CHECK_INTERVAL_MS 5000U /* PHY 状态定期检查间隔(ms) */ -/* N34 E6 实验插桩(U15 失配频率,docs/网络/N34-自愈审计/实验方案.md §E6): +/* N34 E6 实验插桩(U15 失配频率,docs/网络/N34-自愈审计.md §6 E6): * 假设=「RX 非空但 readable=0」(RECV_OK 中断丢失的数据面形态)在生产真实 * 发生。置 1 烧实验固件:net_poll 每 32 轮对在用 fd 发一条 GET_RECV_LEN, * 命中计数 + 低频日志(每小时 ≤1 条防刷屏);跑 24h 常态 + E1 压测。 @@ -632,7 +632,7 @@ int net_poll(void) { /* * 函数功能:芯片/本层 socket 状态周期对账(TRAP-NET-17 丢中断自愈)+ 监听器 pump - * (N-32 P2)—— N34 阶段 3 合并为单一 100ms tick(docs/网络/N34-自愈审计/实施方案.md §5 项 1) + * (N-32 P2)—— N34 阶段 3 合并为单一 100ms tick(docs/网络/N34-自愈审计.md §5.5 项 1) * 入口参数:无 * 出口参数:无 * 返回值:无 diff --git a/Drivers/BSP/NET/net_types.h b/Drivers/BSP/NET/net_types.h index 20156a8b..5d252e53 100644 --- a/Drivers/BSP/NET/net_types.h +++ b/Drivers/BSP/NET/net_types.h @@ -112,7 +112,7 @@ struct net_sockaddr_in { /* * Socket 状态枚举(**本层会话投影**,非芯片状态事实源) - * N34 阶段 3(镜像降级,实施方案 docs/网络/N34-自愈审计/实施方案.md §5 项 5): + * N34 阶段 3(镜像降级,实施方案 docs/网络/N34-自愈审计.md §5.5 项 5): * 芯片侧状态唯一事实源=驱动状态缓存(ch395f_sock_state_cached,刷新点=net_poll); * 本 8 态只表达**本层会话语义**(fd 生命周期/待 accept 电平/门控展示), * 事件与对账把它与芯片事实对齐。新增"按芯片状态决策"的代码一律读驱动缓存, diff --git a/docs/README.md b/docs/README.md index d6bcb60b..32a0f952 100644 --- a/docs/README.md +++ b/docs/README.md @@ -95,6 +95,7 @@ docs/ | [`网络/BSD_Socket_API_使用指南.md`](网络/BSD_Socket_API_使用指南.md) | **上层视图**:BSD Socket API 公开语义、示例、常见问题 | | [`网络/BSD_Socket_API_函数调用关系.md`](网络/BSD_Socket_API_函数调用关系.md) | **内部视图**:netTask / 消息队列 / L3 挂起 / 重试对账的调用关系与设计意图 | | [`网络/Net层阻塞模型架构设计.md`](网络/Net层阻塞模型架构设计.md) | **等待模型**:pending 请求架构、五路完成语义、设计边界与性能特征 | +| [`网络/N34-自愈审计.md`](网络/N34-自愈审计.md) | **N34 自愈审计(合并文档)**:net 层/驱动层/主流对照三路审计(F-NET/F-DRV/F-MS 编号发现)+ 汇总裁决与目标架构 + 分阶段实施方案 + E0~E13 实验记录;阶段 0~3 与 E6 已完成并转正 | --- @@ -271,6 +272,7 @@ docs/ | 日期 | 变更 | 说明 | |---|---|---| +| 2026-10-13 | N34 审计归档 | 原 `docs/网络/N34-自愈审计/` 目录 7 份文档(根因总览 / net 层盘点 / 驱动层审计 / 主流对照 / 汇总裁决 / 实施方案 / 实验方案)**合并为单文档 [`网络/N34-自愈审计.md`](网络/N34-自愈审计.md)**,原目录删除;全库引用同步(`docs/` 4 篇 + 驱动/网络代码注释 + 配图生成器与 SVG)。本索引 §5.1 补登记 | | 2026-10-12 | 新增环境与部署 | 新建 `docs/环境与部署/并行任务工作树方案.md`(**多任务并行开发机制规格**:`git worktree` 隔离、目录与共享/独占分配、`task-worktree` / `@task` skill 设计、单写者文档与合并残留审计、台架锁;**已落地并端到端实测**);本索引 §9.2 补登记 | | 2026-10-12 | 远程 CI 文档独立成目录 | 新建 `docs/CI/`:原散在 4 处的远程 CI 内容(`环境与部署/CI配置指南.md` 477 行 + `环境与部署/Gitea_Runner部署指南.md` 917 行 + `测试开发标准流程.md` §9 + `AGENTS.md` 提交门禁节 4 条,共约 2000 行)**合并为 `CI/远程CI完整指南.md` 一篇**,`CI/README.md` 作唯一入口;`AGENTS.md` 压成一行指针、测试流程 §9 只留覆盖边界与本地等价。动因:远程 CI **只跑在本机一台 runner**,与日常开发规范混在一起每次会话都被读,造成困惑 | | 2026-10-11 | 功能 | N-19 FTP 鉴权:`/config.ini` 新增 `[ftp]` 段(`user` / `pwd_hash` = SHA-256(user‖password) / `write`)+ 恒定时间比较 + 连续 5 次失败锁 60 s + 未登录命令门;出厂默认 admin/admin,默认口令在用有启动告警。新增 `App/util/sha256.*`、`Drivers/BSP/NETCFG/ftp_cfg.*`、`Drivers/BSP/NET/lftpd/lftpd_auth.*`;主机侧单测 `test/host/sha256/`(13 例)+ `test/host/ftp_auth/`(36 例),pre-commit 钩子 26→28;台架脚本 `test/host/ftp_auth_bench.py`。文档同步:`docs/子系统设计/网络配置说明.md`、`docs/架构设计/STM32F4-Base存储架构说明.md`、`docs/代码规范/pre-commit使用说明.md`、`docs/产品需求/产品需求设计文档.md` | diff --git a/docs/images/网络不稳根因总览.svg b/docs/images/网络不稳根因总览.svg index 90a3b380..b2d36251 100644 --- a/docs/images/网络不稳根因总览.svg +++ b/docs/images/网络不稳根因总览.svg @@ -151,5 +151,5 @@ 逐环拆掉 = 稳(N34 阶段 0~3 已实施) 不依赖门铃 · 唯一主人 · 永不冻结 · 100ms 对账 · 开张前排渣——台架回归全绿 -证据链:docs/问题记录/CH395F_Trap_Records.md(TRAP-NET-07/-09/-12/-17/-18/-20) · docs/网络/N34-自愈审计/实验方案.md(E1~E13) · 修复落地见 docs/网络/N34-自愈审计/实施方案.md +证据链:docs/问题记录/CH395F_Trap_Records.md(TRAP-NET-07/-09/-12/-17/-18/-20) · docs/网络/N34-自愈审计.md §6 实验记录(E1~E13) · 修复落地见同文档 §5 实施方案 diff --git a/docs/子系统设计/网络Socket监听生命周期设计说明.md b/docs/子系统设计/网络Socket监听生命周期设计说明.md index 8837e42f..5cdb69f0 100644 --- a/docs/子系统设计/网络Socket监听生命周期设计说明.md +++ b/docs/子系统设计/网络Socket监听生命周期设计说明.md @@ -121,7 +121,7 @@ B10 行)→ 中断丢了,bit3=0 同样停在已关/半关态,孤儿 Socket > 入口已主流化:具名 API `net_recv_nb()`(单次非阻塞)+ `net_setblocking(fd, on)` > (模式挂控制块 `blocking` 属性);`net_recv()` 默认改为**无限阻塞**(30s 默认预算 > 删除),受 IWDG 监控任务的有界等待契约见 `docs/网络/BSD_Socket_API_使用指南.md` -> §10.1 与 `docs/网络/N34-自愈审计/实施方案.md` §3.5。本文所有「DONTWAIT 判据 / +> §10.1 与 `docs/网络/N34-自愈审计.md` §5.3.5。本文所有「DONTWAIT 判据 / > 返 WOULDBLOCK」表述按新命名逐字仍成立,不逐处改写。 **⑥ 本次(P0)已落地的口径修正**:`net_socket.c:3627` / `:3641` 原注释写「DISCONNECT 后 @@ -269,7 +269,7 @@ DONTWAIT 在 LISTENING 下返 `WOULDBLOCK`(`:1890`)→ `ui_net_sink.c:560-56 **P0(零风险)** - [x] 修 `Drivers/BSP/NET/net_socket.c:3627`、`:3641` 的 FUN_PARA 口径错误注释(实际 `0x08`,芯片**不**自动关)—— 2026-10-12 已改并留痕(§2.4⑥),`@build` 0 错 0 警 -- [x] 把 U11 / U12 / U14 / U15 登记进 `docs/问题记录/CH395F_Trap_Records.md`——**实际落点改 N34 审计文档**:U11/U12/U14 机制归属见 `docs/网络/N34-自愈审计/net层自愈机制盘点.md`;U15 经 E6 裁决(命中=0)登记观察项(`docs/网络/N34-自愈审计/实验方案.md` E6 结果) +- [x] 把 U11 / U12 / U14 / U15 登记进 `docs/问题记录/CH395F_Trap_Records.md`——**实际落点改 N34 审计文档**:U11/U12/U14 机制归属见 `docs/网络/N34-自愈审计.md` §1;U15 经 E6 裁决(命中=0)登记观察项(`docs/网络/N34-自愈审计.md` §6 E6 结果) - [x] 在 B10 行确认已记「④ 扩大 EOF 吞没窗口」——2026-10-13 复核:新 TRAP-NET 文档 B10 行在位(✅ 已修 + 副作用注记 + `net_drop_conn` 根治出口) **P1(边沿→电平)** @@ -562,7 +562,7 @@ void net_listener_tick(void); ### 10.10 N34 阶段 3:pump 并入 reconciler(**已执行 2026-10-13**) -审计(`docs/网络/N34-自愈审计/实施方案.md` §5 项 1)指出:`net_reconcile_tick`(200ms,①②⑤)与 +审计(`docs/网络/N34-自愈审计.md` §5.5 项 1)指出:`net_reconcile_tick`(200ms,①②⑤)与 `net_listener_pump`(100ms,③④/U11)是**同一件事的两个半片**——都在"读芯片状态 → 比对镜像 → 分级处置", 拆两个 tick 只是历史演化(P2a 从对账里拆出去),既多一份去抖/冷却状态机,又让"对账① 置 CLOSED 后 要等下一个 pump 轮才重建"多一跳延迟。阶段 3 合并为**单一 100ms `net_reconcile_tick`**: diff --git a/docs/测试说明/CH395F_Test_Guide.md b/docs/测试说明/CH395F_Test_Guide.md index 8ca59c29..f96290d6 100644 --- a/docs/测试说明/CH395F_Test_Guide.md +++ b/docs/测试说明/CH395F_Test_Guide.md @@ -1103,8 +1103,8 @@ IDLE → ACCEPT → [SEND: 分块发 → DONE] / [RECV: 分块收+校验 → DON ## 12.5 阶段 15:N34 阶段 2 驱动原语(硬件层,无需 PC) -验证 `ch395f` 新增自愈原语(`docs/网络/N34-自愈审计/实施方案.md` §4;形态定案 -`docs/网络/N34-自愈审计/实验方案.md` E2/E3/E4)。硬件层模式独占芯片,用 Socket 7 +验证 `ch395f` 新增自愈原语(`docs/网络/N34-自愈审计.md` §5.4;形态定案 +`docs/网络/N34-自愈审计.md` §6 E2/E3/E4)。硬件层模式独占芯片,用 Socket 7 (产品监听 0~4 不涉),无需 PC 配合。污染会话下的复位载荷完整性不在本 Phase (已由 E4 bench 60 轮实证),本 Phase 只验状态机跑通与判据语义。 diff --git a/docs/网络/BSD_Socket_API_使用指南.md b/docs/网络/BSD_Socket_API_使用指南.md index bd114cfe..ab7b2221 100644 --- a/docs/网络/BSD_Socket_API_使用指南.md +++ b/docs/网络/BSD_Socket_API_使用指南.md @@ -1010,7 +1010,7 @@ int net_close(int sockfd); int net_drop_conn(int sockfd); ``` -应用**放弃会话**(对时 30s 预算耗尽、协议超时)时主动拆掉当前 TCP 会话并**保留监听**——单连接模型下挂起但 TCP 仍活的客户端会永久占住监听(PC 内核自动 ACK keepalive,芯片僵尸判不死,定案见 `docs/网络/N34-自愈审计/实验方案.md` E13),而应用又不得 `net_close` 监听 fd(毁监听);本 API 补上"拆连接不拆监听"的应用侧形态。 +应用**放弃会话**(对时 30s 预算耗尽、协议超时)时主动拆掉当前 TCP 会话并**保留监听**——单连接模型下挂起但 TCP 仍活的客户端会永久占住监听(PC 内核自动 ACK keepalive,芯片僵尸判不死,定案见 `docs/网络/N34-自愈审计.md` §6 E13),而应用又不得 `net_close` 监听 fd(毁监听);本 API 补上"拆连接不拆监听"的应用侧形态。 - **netTask 侧**:先了结本 fd 挂起 recv(排空/取消 + 必通知),再 `sock_reset` QUARANTINE(FIN + 排渣 + 排空事件 + 等 byte0=00H,TRAP-NET-18 异常释放语义,400ms 底);本层收口 CLOSED;`auto_relisten` fd 随即 `net_listener_try` 即时重监听(失败交 D10 重试 + reconciler 形态④) - **幂等**:无活动会话(非 ESTABLISHED/CONNECTING)直接返 0,不动监听 @@ -1458,9 +1458,9 @@ CH395F 底层驱动返回的状态码(`ch395f_get_cmd_status()` 或 `ch395f_po | 日期 | 变更 | |------|------| -| 2026-10-13 | **N34 阶段 3 自愈合并 + E-02 收口**(实施规格 `docs/网络/N34-自愈审计/实施方案.md` §5):`net_listener_pump` **删除**,与 `net_reconcile_tick` 合并为**单一 100ms tick**(覆盖原对账①②⑤ + 原 pump③④/U11;判据读**驱动状态缓存** `ch395f_sock_state_cached`,唯一刷新点 = `net_poll` 每轮全量刷新,E2 实测单次回读 18.1µs;**分级去抖** ②=1 轮 / ③④⑤=2 轮 / ①=4 轮(M3 让位窗 400ms)+ 1s 冷却;§2.2 / §3.3 / §3.6(五层 → **四层**)/ §10.2 / §12.1 / §12.10 同步,主循环自愈 tick 由 4 个回到 **3 个**;常量 `NET_LISTENER_PUMP_PERIOD_MS` / `NET_LISTENER_STABLE_N` / `NET_LISTENER_COOLDOWN_MS` 删除);**忙等统一**——`net_listen_locked` / `net_connect_locked` open 失败重试"close+盲等 500ms"与 `net_close_locked` 500ms 排空循环改走驱动 `sock_reset` 原语(`net_sock_reset_sync`,GRACEFUL 100ms 底),`net_close_locked` 不再读清全局中断寄存器(F-NET-7:全局寄存器唯一读者 = `net_poll`);新增 **`net_drop_conn(sockfd)`**(§10.1,E-02 收口:拆会话**保监听**、幂等、EOF 路径不得调;新消息类型 `NET_MSG_DROP_CONN`;timeSync 30s 预算耗尽路径接入;模拟器实现为关数据连接);`net_poll` 每轮对在用 Socket 步进 `ch395f_sock_reset_poll`(同步包装 600ms 放弃后复位机不搁浅);`net_sock_state_t` 注释声明为"本层会话投影"(芯片事实源 = 驱动缓存,`net_types.h`);台架回归全绿(最终构建 Code=211348,E-02 双 PASS,见 `docs/网络/N34-自愈审计/实验方案.md` E-02 收口台架) | -| 2026-10-13 | **N34 阶段 1.5 recv 语义主流化**(规格 `docs/网络/N34-自愈审计/实施方案.md` §3.5):`net_recv()` 默认**无限阻塞**(删 `NET_RECV_TIMEOUT_MS` 30s 默认预算,唤醒源=数据/EOF/close/网络重建 CONNRESET);新增 `net_recv_nb()`(`NET_MSG_DONTWAIT` 路径转正)与 `net_setblocking(fd, on)`(模式挂 socket 控制块属性,`net_sock_t.blocking`,默认阻塞);`net_recv_to()` 保留为**显式预算**便捷入口(`timeout_ms=0` 语义从"默认 30s"改为"无限"故仍拒 INVAL);内部 recv 看门狗降级为 **tripwire**(挂起超 60s 只 `[ERR]` 留痕一次,不再"到点 +1s 强制了结"伪造 TIMEDOUT);⚠ 新增**文档契约**:IWDG `required_mask` 内任务禁止无限阻塞 recv(必须 `net_select` 超时或 `net_recv_to` 显式预算);`ui_net_sink` / `iec104_serv` 迁移 `net_recv_nb`,lftpd / timeSync 维持显式预算;模拟器 `net_socket_sim.c` 镜像三 API(blocking 属性包装层模拟) | -| 2026-10-13 | **N34 阶段1 API 面收敛**(审计见 `docs/网络/N34-自愈审计/README.md`):删除手动自愈 API `net_relisten()` / `net_resync_listen()`(生产零调用;后者语义并入对账**⑤**——本层 ESTABLISHED 而芯片 0x05/0x01 静默回收 → RESYNC 只修本层,§3.6 对账由两类扩为三类①②⑤;前者与 pump/D10 重叠且对健康监听器是 footgun)与 `net_can_accept()`(只认 ESTABLISHED 的旧门控挡死丢 CONNECT 探针,监听门控统一 `net_listener_armed()`,§10.1 门控节改写);`NET_MSG_RESYNC_LISTEN` / `NET_MSG_RELISTEN` 消息类型删除;timeSync 会话收尾不再 `net_close`(同 fd 续 accept,监听跨会话常驻);lftpd 应用层阻塞重试退役(就绪屏障已消除 TRAP-NET-09 启动形态) | +| 2026-10-13 | **N34 阶段 3 自愈合并 + E-02 收口**(实施规格 `docs/网络/N34-自愈审计.md` §5.5):`net_listener_pump` **删除**,与 `net_reconcile_tick` 合并为**单一 100ms tick**(覆盖原对账①②⑤ + 原 pump③④/U11;判据读**驱动状态缓存** `ch395f_sock_state_cached`,唯一刷新点 = `net_poll` 每轮全量刷新,E2 实测单次回读 18.1µs;**分级去抖** ②=1 轮 / ③④⑤=2 轮 / ①=4 轮(M3 让位窗 400ms)+ 1s 冷却;§2.2 / §3.3 / §3.6(五层 → **四层**)/ §10.2 / §12.1 / §12.10 同步,主循环自愈 tick 由 4 个回到 **3 个**;常量 `NET_LISTENER_PUMP_PERIOD_MS` / `NET_LISTENER_STABLE_N` / `NET_LISTENER_COOLDOWN_MS` 删除);**忙等统一**——`net_listen_locked` / `net_connect_locked` open 失败重试"close+盲等 500ms"与 `net_close_locked` 500ms 排空循环改走驱动 `sock_reset` 原语(`net_sock_reset_sync`,GRACEFUL 100ms 底),`net_close_locked` 不再读清全局中断寄存器(F-NET-7:全局寄存器唯一读者 = `net_poll`);新增 **`net_drop_conn(sockfd)`**(§10.1,E-02 收口:拆会话**保监听**、幂等、EOF 路径不得调;新消息类型 `NET_MSG_DROP_CONN`;timeSync 30s 预算耗尽路径接入;模拟器实现为关数据连接);`net_poll` 每轮对在用 Socket 步进 `ch395f_sock_reset_poll`(同步包装 600ms 放弃后复位机不搁浅);`net_sock_state_t` 注释声明为"本层会话投影"(芯片事实源 = 驱动缓存,`net_types.h`);台架回归全绿(最终构建 Code=211348,E-02 双 PASS,见 `docs/网络/N34-自愈审计.md` §6 E-02 收口台架) | +| 2026-10-13 | **N34 阶段 1.5 recv 语义主流化**(规格 `docs/网络/N34-自愈审计.md` §5.3.5):`net_recv()` 默认**无限阻塞**(删 `NET_RECV_TIMEOUT_MS` 30s 默认预算,唤醒源=数据/EOF/close/网络重建 CONNRESET);新增 `net_recv_nb()`(`NET_MSG_DONTWAIT` 路径转正)与 `net_setblocking(fd, on)`(模式挂 socket 控制块属性,`net_sock_t.blocking`,默认阻塞);`net_recv_to()` 保留为**显式预算**便捷入口(`timeout_ms=0` 语义从"默认 30s"改为"无限"故仍拒 INVAL);内部 recv 看门狗降级为 **tripwire**(挂起超 60s 只 `[ERR]` 留痕一次,不再"到点 +1s 强制了结"伪造 TIMEDOUT);⚠ 新增**文档契约**:IWDG `required_mask` 内任务禁止无限阻塞 recv(必须 `net_select` 超时或 `net_recv_to` 显式预算);`ui_net_sink` / `iec104_serv` 迁移 `net_recv_nb`,lftpd / timeSync 维持显式预算;模拟器 `net_socket_sim.c` 镜像三 API(blocking 属性包装层模拟) | +| 2026-10-13 | **N34 阶段1 API 面收敛**(审计见 `docs/网络/N34-自愈审计.md` §4):删除手动自愈 API `net_relisten()` / `net_resync_listen()`(生产零调用;后者语义并入对账**⑤**——本层 ESTABLISHED 而芯片 0x05/0x01 静默回收 → RESYNC 只修本层,§3.6 对账由两类扩为三类①②⑤;前者与 pump/D10 重叠且对健康监听器是 footgun)与 `net_can_accept()`(只认 ESTABLISHED 的旧门控挡死丢 CONNECT 探针,监听门控统一 `net_listener_armed()`,§10.1 门控节改写);`NET_MSG_RESYNC_LISTEN` / `NET_MSG_RELISTEN` 消息类型删除;timeSync 会话收尾不再 `net_close`(同 fd 续 accept,监听跨会话常驻);lftpd 应用层阻塞重试退役(就绪屏障已消除 TRAP-NET-09 启动形态) | | 2026-10-12 | **N-32 监听生命周期模块化 + 单一所有者**:监听生命周期抽为 `net_listener.c`(§2.2 文件表新增该行,真机专属);主循环自愈 tick 由 3 个增至 **4 个**(新增 `net_listener_pump`,100ms,**唯一重建入口**);`net_reconcile_tick` 对账收窄为 ①②,③④ 移交 pump;`handle_disconnect_event` / `handle_timeout_event` 改为 `net_listener_settle(GRACEFUL 100ms / QUARANTINE 400ms)` **只置事实**、不再自行重建;§3.3 / §3.6(四层自愈 → **五层**)/ §12.10 同步。权威设计见 `docs/子系统设计/网络Socket监听生命周期设计说明.md` §10 | | 2026-10-10 | 新增 **`net_can_accept()` / `net_can_read()` 就绪门控 API**(§10.1):纯 RAM 读控制块、**不走消息队列、不等 netTask 回执**,供实时性敏感任务在发 RPC 前判断"有没有活"(动因 = `ui_net_sink_poll()` 每 10 ms 一次 `net_accept` 把 uiTask 拖成最坏 ≈50 ms;原拟登记 N-19 / C7,当日结案不入登记簿;设计与实测见 `docs/子系统设计/看门狗设计说明.md` §11(原分析文档已并入该节));§3.5 补"netTask 运行要求"例外条;⚠ 记录两条使用约束——**不可作为唯一判据**(`readable` 只由 RECV_OK 中断置位,须保留低频兜底 recv)与**模拟器后端恒返 1**(`net_socket_sim.c`,Winsock 无 RPC 成本) | | 2026-09-23 | 同步 L3 pending 化、D10 非阻塞重监听、TRAP-NET-17 对账、TRAP-NET-18 排渣隔离后的现状:新增 `net_recv_to` / `net_sock_state` / `net_resync_listen` / `net_relisten`(§10.1)及 `net_getsockname`;§3 新增 3.6(对账与自愈四机制)、3.3/3.5 改写(recv 挂起模型、队列语义、netTask 运行要求);§4/§5 示例更新(监听由就绪屏障后服务任务建立、`net_sock_state` 轮询);§10.2 改述 `net_process_messages()` 每轮节奏(重监听重试 → 挂起 recv 完成 → 200ms 对账 → 队列),补 `timeout_ms`/新消息类型说明;新增 §11.2 分片阻塞读(`net_recv_to`);§12.1/12.3/12.4/12.9 更新,新增 §12.10(自愈行为数据与排障判据,含 TC-NET-604 结案) | diff --git a/docs/网络/N34-自愈审计.md b/docs/网络/N34-自愈审计.md new file mode 100644 index 00000000..e17f1904 --- /dev/null +++ b/docs/网络/N34-自愈审计.md @@ -0,0 +1,2062 @@ +# N34 自愈审计(合并文档) + +> **本文件由原 `docs/网络/N34-自愈审计/` 目录的 7 份文档合并而成(2026-10-13),原目录已删除。** +> 合并方式:**忠实合并**——保留全部审计发现(F-NET-1..15 / F-DRV-1..16 / F-MS-1..9)、 +> 逐机制裁决、实施方案与 E0~E13 实验记录;仅统一章节编号、修正交叉引用与配图路径。 +> +> 审计对象:BSD Socket API 的「芯片/本层状态对账与重监听自愈」五层机制 +> (auto_relisten / D10 重试 / TRAP-NET-17 对账 / N-32 pump / TRAP-NET-18 排渣隔离) +> 及其显式 API(`net_relisten` / `net_resync_listen`)。 +> 方法:三路独立审计汇总(net 层 / 驱动层 / 主流对照)+ 汇总裁决;全部证据带 文件:行号, +> HEAD `09d6195` 实测。性质:审计 + 实施方案 + 实验记录(阶段 0~3 与 E6 均已完成并转正)。 +> +> | 原文件 | 本文章节 | +> |---|---| +> | `网络不稳根因总览.md` | §0 网络不稳根因总览(人话版复盘) | +> | `net层自愈机制盘点.md` | §1 net 层自愈机制盘点(T1) | +> | `CH395F驱动层审计.md` | §2 CH395F 驱动层审计(T2) | +> | `主流方案对照.md` | §3 主流嵌入式 socket 栈对照研究(T3) | +> | `README.md` | §4 三方汇总裁决与目标架构(T4) | +> | `实施方案.md` | §5 实施方案 | +> | `实验方案.md` | §6 实验方案与记录 | + + +--- + +## 0. 网络不稳根因总览(人话版复盘) + +> 定位:本文是 N34 自愈审计的**人类可读复盘总览**——把散在 Trap 记录、实验台账、 +> §5 里的根因,用一个贯穿比喻串成一条因果链,并逐环给出已落地的拆法与证据出处。 +> 技术细节与行号证据一律指向本文其余章节与 `docs/问题记录/CH395F_Trap_Records.md`, +> 此处不重复。适合新成员第一遍读,也适合对外解释"我们的网络为什么曾经时好时坏"。 + +**一句话回答**:不稳从来不是"网断了",而是**一个会丢门铃、只会礼貌送客的芯片**, +配上一套**靠门铃驱动、人人插手重建、唯一传达员还会被按在椅子上半分钟**的软件—— +每个环节单看都"正常工作",叠起来就是随机、无日志、时好时坏的"玄学"。 + +### 0.1 角色表 + +把 CH395F 芯片想成一个**外国籍传达室大爷**:他会收发信件(TCP 栈),但**门铃会坏** +(中断会丢)、**只会礼貌送客不会轰人**(close 无 RST)。netTask 是**全楼唯一能进传达室 +的传达员**(所有 CH395F 事件只在 `net_poll` 集中读清)。FTP、对时、UI 推流、IEC104 +是**楼里等信的住户**(各自监听一个端口,但共用这一个传达员)。 + +### 0.2 七个误会(叠加才成为"玄学") + +#### ① 门铃会丢(TRAP-NET-17:丢中断) + +快速复连时芯片本应"按门铃"(INT# 拉低)通知有新来访/客人离开,但门铃**会哑**—— +人已经站在屋里,铃没响。传达员只靠门铃干活,于是本层状态**静默滞留**:端口其实 +死了,日志里**一个错都不报**。当年"待机后 UI 复连 2/10 永久拒连、FTP 第 29 轮自己 +死掉"就是它——不是断网,是**没人报信**。裸机芯片台架(剥离 OS/net/FTP)独立复现, +证明这是芯片天性,不是固件 bug(见 `docs/网络/N34-自愈审计.md` §4 结论 1)。 + +#### ② 门铃总闸是"看一眼就清零"(TRAP-NET-20 B3) + +全局中断寄存器是**一整排灯,读一次全部熄灭**。历史上 open/relisten 里有两处 +"丢弃式读一遍"——等于**查自己门铃时把全楼别的住户的门铃线也拔了**,别人的 +CONNECT/DISCONNECT 从此永久失踪。这是"自家制造的丢中断"放大器。 + +#### ③ 柜台和货梯是同一台(单连接模型) + +监听 fd 就是数据 fd——**商店柜台平时兼货运电梯**。住户看到"客人走了"(EOF)习惯性 +`net_close` → **把柜台连电梯一起拆了** → 端口"连接被拒"。而芯片侧其实已自动恢复 +(auto_relisten),应用这一刀纯属帮倒忙。注意:单连接复用本身**不是**错误(主流硬 +TCP 栈全部同形),错的是住户不懂模型就动手。 + +#### ④ 三个临时工都在修电梯(多所有者 relisten) + +accept、recv、pending 三处都会"顺手重建监听",谁先看到断连谁动手 → 互相踩脚: +重建风暴、慢消费者还没读完的 EOF 被吞、新客户端数据被旧会话静默吸收。修得越多越乱。 + +#### ⑤ 传达员被按在椅子上(队头阻塞) + +一个挂起的对时客户端不发数据 → 旧代码 **30 秒阻塞收**;而 netTask 是**全楼唯一 +传达员**——这 30 秒里 FTP 的开门请求、UI 的连接请求全部排队冻结(E-02 实测 FTP +banner 迟到 25~30 s)。各处还有 500 ms 盲等忙等,传达员频繁"冻结半秒"。 + +#### ⑥ 僵尸客人赖着不走(E13:网卡闪断) + +PC 网卡闪断一下 → PC 内核**悄悄把自己的连接记录撕了**(不发 FIN),芯片那头还认为 +"客人正在屋里聊天"(ESTABLISHED)。单连接模型下**唯一的柜台被这个不存在的客人永久 +占用**,真客人来了只得到"连接被拒"。当时看起来像"设备网络莫名死了",其实是 +**和幽灵做生意**。 + +#### ⑦ 大爷只会礼貌送客,不会轰人(close 无 RST,手册定案) + +芯片的 close 只发 FIN("您请回吧")。对面**内核活着但程序挂死**时——内核礼貌回 +ACK,但客人**就是不起身**——芯片永远站在门口等(FIN_WAIT_2 长尾,E-02 定案:监听 +恢复受限于对端 close)。它**没有"轰出去"(RST)这个动作**——手册全文 RST 只出现在 +listen 占用与多连接两处。这是芯片天性。 + +#### ⑧ 垃圾不清,越扫越脏(残渣风暴态) + +上面每一条的残骸(迟到的 DISCONNECT 锁存、半死 Socket、粘滞中断位)都会**带进下 +一条连接**。所以故障率不是常数:残渣少时 2%,堆起来能摆到 54%(E8 实测)——同一份 +固件时好时坏,看起来像"玄学"。 + +### 0.3 全景图 + +![七个误会与逐个拆掉](../images/网络不稳根因总览.svg) + +### 0.4 僵尸会话时间线(误会⑥ 的完整一生) + +```mermaid +sequenceDiagram + participant PC as PC(网卡+内核) + participant CH as CH395F(大爷) + participant NT as netTask(传达员) + Note over PC,CH: 会话正常通信(ESTABLISHED) + Note over PC: 网卡闪断(驱动重启/电源管理) + PC--)CH: 内核撕掉 socket,不发 FIN(静默) + Note over CH: 仍认为 ESTABLISHED,唯一柜台被幽灵占用 + Note over NT: 新连接到达 → 连接被拒(端口"死了") + CH->>PC: keepalive 探测(空闲 60s 起,每 5s,共 3 次) + PC--xCH: 无人认领 → 探测无响应 + CH->>NT: TIM_OUT 中断(≈30~90 s) + NT->>CH: QUARANTINE:sock_reset 排渣(读空→CLOSE→排空→确认) + NT->>CH: relisten → LISTEN + Note over NT: 端口恢复,新客人正常进门 +``` + +### 0.5 环 → 拆法 → 落点(对照表) + +| 环 | 拆法(已落地) | 证据/落点 | +|---|---|---| +| ① 丢中断 | 单一 reconciler:100 ms 巡逻对账五种形态,判据读驱动状态缓存 | TRAP-NET-17;§6 E2(回读 18.1 µs 背书);§5 阶段 3 项 1 | +| ② 总闸读清 | 全局寄存器唯一读者=net_poll(F-NET-7);open/relisten 只清本 Socket SINT | TRAP-NET-20 B3;§5 阶段 0/2 | +| ③ 拆柜台 | 铁律"EOF 不得 close 监听 fd" + 新 API `net_drop_conn`(拆会话保监听) | 生命周期设计说明 §10.10;§6 E-02 | +| ④ 多所有者 | 重建收敛唯一所有者(net_listener + reconciler),其余只置事实不动手 | §4.0 结论 4(不变量 II);§5 阶段 3 项 1 | +| ⑤ 队头阻塞 | 忙等统一 `net_sock_reset_sync` 有界排渣(600 ms 硬顶);recv 非阻塞消息化 | §6 E-02(banner 39 ms);阶段 1.5 | +| ⑥ 僵尸会话 | keepalive 60/5/3 + 芯片 TIMEOUT → QUARANTINE → 重建(30~90 s 自愈) | §6 E11/E13(用户判断"不该依赖冷启动"被证实) | +| ⑦ 无 RST | 定案入 TRAP-NET-07:兜底靠查岗不靠 RST;`net_drop_conn` 本地拆除 + 对账③④ 认领 | TRAP-NET-07 定案更正;§6 E-02 收口 | +| ⑧ 残渣风暴 | `sock_reset` 标准排渣(读空→CLOSE→排空事件→byte0=00H 确认)+ M3 矛盾态门 | TRAP-NET-18/-20;§5 M3/M5(E8/E9 风暴态免疫) | + +### 0.6 一句话总结 + +原来的"不稳" = 芯片天性(丢门铃、无 RST)× 软件形态(靠门铃驱动、人人插手、 +唯一传达员会冻结)× 残渣复利(脏态带进下条连接)。N34 做的事不是修某一个 bug, +而是把每一环改成**不依赖门铃(100 ms 对账)、只有一个主人(单所有者)、永远不冻结 +(有界排渣+消息化)、开张前扫垃圾(sock_reset)**——所以现在的"稳"是结构性的。 + +> 遗留观察项:E6 正在挂机量化"RX 非空但 readable=0"(RECV_OK 中断丢失的数据面 +> 形态)在生产是否真实发生(`NET_U15_PROBE`,§6 E6 节)——命中>0 则上层 +> 32 轮盲收内化进 net 层(阶段 3 项 4);=0 则保留现状登记观察。 + + +--- + +## 1. net 层自愈机制盘点(T1) + +> 审计对象:`Drivers/BSP/NET/net_socket.c`(3671 行)与 `net_listener.c`(554 行)/ `net_listener.h`。 +> 方法:全文通读 + 调用点 grep + git 考古(`git log -S`)+ 问题记录与设计文档对照。纯只读,未改任何代码。 +> 行号均为本工作树 HEAD(`09d6195`)实测行号。 +> 参照文档:CH395F_Trap_Records.md(TRAP-NET-09/-17/-18/-20 与 2026-10-11 寄存器审计章节 B1~B10 条目)、 +> 网络Socket监听生命周期设计说明.md(B10 根治设计,含 P0/P1/P2 落地记录与模块化边界)、 +> BSD_Socket_API_使用指南.md(五层自愈口径)。 + +### 1.1 机制 × 属性总表 + +核心七机制(用户指定盘点范围): + +| # | 机制 | 代码位置 | 触发条件 | 改本层哪些状态 | 发哪些芯片命令 | 定时器/去抖/冷却 | 重叠/互为兜底的对象 | 根因分类 | 初步裁决 | +|---|---|---|---|---|---|---|---|---|---| +| 1 | **auto_relisten**(监听常驻标志) | 置位 net_socket.c:1119(`net_listen_locked`);字段 net_types.h:172;消费点 net_socket.c:1316/1924-1938/2136-2162/1483、net_listener.c:501 | `net_listen` 成功即置 1;此后任何"会话已结束"路径(DISCONNECT/TIMEOUT/对账①)把 fd 置 CLOSED 后,由消费点或 pump 依据该标志重建 | 本身不改状态,是**门控标志**;`net_close`(memset net_socket.c:2351)与 `net_init`(:244)清零 | 无(纯标志) | 无 | 是所有重建路径(机制 2/4/6 与 accept/recv/pending 三处即时重建)的共同前提门 | (a) 芯片永不自动回 LISTEN(手册无此条款,设计文档认定;单连接复用模型锁死) | **保留**(模型基石,不可删) | +| 2 | **D10 非阻塞重监听重试** `net_listener_retry_tick` | net_listener.c:394-432;登记 net_listener.c:374-378;常量 :77-79 | 任何 `net_listener_try` 单次重建失败(open 失败 / LISTEN 失败 / 隔离未满)即登记 `s_relisten_wait`,到点重试 | 成功经 `net_relisten_locked` 置 `state=LISTENING`(net_listener.c:233)、清 `released_abnormal`、转登 `last_peer_port`、清 `lost_conn_seen`、清对端簿记(:239-255);**刻意不清 `session_dead`**(:234-238 契约注释) | 隔离段见机制 5;正常段:SET_PROTO_TYPE、SET_SOUR_PORT、OPEN_SOCKET、TCP_LISTEN(:207-230);open 失败补 CLOSE_SOCKET(:218);成功尾读本 Socket SINT 读即清(:262) | 重试间隔 30ms(:77)、放弃时限 2s(:79,自首次尝试);无去抖(单次尝试);**L3 保险**:同 fd 有在途挂起 recv 则跳过(:405-408,经 net_socket.c:2202 `net_pending_active`) | 是机制 4(pump)与三处消费者即时重建的**执行器**(它们调 `net_listener_try`,失败落到这里);"2s 放弃"并非终态——pump 冷却后会再次触发(见 F-NET-4) | (a) 芯片 close 后重开需沉降(TRAP-NET-09 PHY 形态 + TRAP-NET-20 裸机台架实证拆链窗口 >2s 不定长);旧实现 osDelay(500) 忙等属 (b) 已修(e5733b3) | **保留**;"give up" 语义需简化(F-NET-4) | +| 3 | **TRAP-NET-17 周期对账** `net_reconcile_tick` | net_socket.c:548-641;常量 :544-546 | 每 200ms 对**认领范围内**(仅本层 ESTABLISHED / LISTENING,:579-581)的 fd 回读芯片两字节状态:① 本层 ESTABLISHED 而芯片 SOCKET_CLOSED 或 TCP_CLOSED(:588-590);② 本层 LISTENING 而芯片 TCP_ESTABLISHED(:591-592) | ① 复用 `handle_disconnect_event`(:633)→ 置 `session_dead=1`、`last_peer_port`、`state=CLOSED`、可能登记 GRACEFUL 沉降;② 复用 `handle_connect_event`(:638)→ 置 ESTABLISHED、清 `session_dead`、记对端 | GET_SOCKET_STATUS(每认领 fd 一条,:586);② 前 GET_REMOT_IPP 幽灵甄别(:597-602);后续按事件路径的命令 | 周期 200ms(:544)、连续 2 次失配去抖(:545,≈400ms)、动手后 1s 冷却(:546) | ② 与 accept 路径内联探针 `net_lost_connect_probe`(net_socket.c:1228-1267)职责重叠(同一失同步双兜底,TRAP-NET-20 定案"探针快路径 + 对账兜底网");① 与 `net_resync_listen` 语义相邻但判据不同(见 F-NET-2);原③④ 已移交 pump(:536-537/606-607) | (a) 芯片丢 CONNECT/DISCONNECT 中断(TRAP-NET-17 生产实测 + TRAP-NET-20 裸机台架独立实证,**与 B3 固件 bug 无关**);幽灵态 (a) | **保留**;建议扩展①(F-NET-2) | +| 4 | **N-32 监听器 pump** `net_listener_pump` | net_listener.c:465-553;常量 :437-441 | 每 100ms 对**监听型 fd**(in_use + auto_relisten + STREAM + local_port≠0,:501-507)且本层处于 LISTENING/CLOSED(:509-513)者回读芯片:① 本层 LISTENING 而芯片不在 LISTEN → 重建;② 本层 CLOSED 而芯片不在 LISTEN → 重建;③ 本层 CLOSED 而芯片**正在** LISTEN(U11)→ 只重同步 | ③:仅 `state: CLOSED→LISTENING`(:528)+ 转登 `last_peer_port`、清 `lost_conn_seen`(:532-533),**不发任何芯片命令**;①②:经 `net_listener_try` 走机制 2 的状态改动;`session_dead` 不清(:462-463 契约) | 每监听型 fd 一条 GET_SOCKET_STATUS(:516);③ 零命令;①② 同机制 2 | 周期 100ms(:437)、连续 2 次去抖(:440)、1s 冷却(:441) | 本层**唯一周期性重建决策者**(不变量 II,设计文档第 4 节);与三处消费者即时重建(accept net_socket.c:1327 / recv :1938 / pending :2162)构成"周期兜底 + 消费者驱动"双层——该分工是 P2a 台架实测推翻原计划后的定案(删消费者驱动会致 `[WD] task stall`) | (c) 设计缺口(多所有者竞争)的**架构解**;其底层需求(芯片不回 LISTEN + 事件会丢)为 (a) | **保留**(这就是"内聚"的落点,不是补丁) | +| 5 | **TRAP-NET-18 排渣隔离** `net_drain_chip_rx` + 隔离段 | 排空 net_listener.c:102-121;隔离段 net_listener.c:149-204;登记 net_listener.c:282-289(`net_listener_settle`),登记点 net_socket.c:3430(GRACEFUL,仅 `recv_bytes≠0`)/:3523(QUARANTINE) | 异常释放(芯片 TIMEOUT,net_socket.c:3512-3524)登记 QUARANTINE;正常释放(芯片 DISCONNECT 且本连接收过数据,:3424-3432)登记 GRACEFUL;下一次 `net_relisten_locked` 进入隔离段 | 隔离期满清 `s_relisten_quarantine/_settle_start/_settle_dead`(:199-203);不改 socket 状态(状态改动仍由机制 2 成功尾做) | 隔离期内每轮:GET_RECV_LEN + READ_RECV_BUF(排空,:172→:107-120)、CLOSE_SOCKET(主动关,:174)、读本 Socket SINT 读即清(:176)、**读全局中断寄存器**(:178,见 F-NET-7)、GET_SOCKET_STATUS(:180) | QUARANTINE 最短 400ms / GRACEFUL 最短 100ms / 上限 800ms 强制放行(net_listener.c:53-55;上限必须 < 2s 重试时限,实测 2000ms 会把监听耗死) | 与 `net_close_locked` 的"close→等 CLOSED→排空中断"循环(net_socket.c:2326-2345)是同一芯片契约的**两个变体实现**(下沉候选,见 F-NET-9);GRACEFUL 的 `recv_bytes≠0` 门是为保护 TRAP-NET-17 快速复连时序而设(两机制互相制衡) | (a) FUN_PARA bit3=1(0x08,D4 定案必须保留)下芯片保留现场 + 在途数据在 TIMEOUT 之后才落缓冲 + 残留 DISCONNECT 投递给下一条连接(TRAP-NET-18 两阶段台架实证;0ms/30ms 沉降无效、100ms 通过) | **保留**;"close+排空+等 CLOSED"原语可**下沉**驱动层合并(F-NET-9) | +| 6 | **net_resync_listen / net_relisten** 显式自愈 API | API net_socket.c:3026-3066 / :3080-3120;分派 :774-799 / :800-814 | 应用层判定"连接已死/监听器可能失位"后主动调用;**生产代码零调用者**,唯一调用者 test/board/net_latency_test.c:783-784/787/800(Phase 13 台架,产品构建不启用) | RESYNC:仅本层 `state: ESTABLISHED→LISTENING` + 转登 `last_peer_port` + 清 `lost_conn_seen` + 取消挂起 recv(:789-797),**不发芯片命令**;RELISTEN:经 `net_listener_try` 无条件芯片侧重建(机制 2 全套) | RESYNC:无;RELISTEN:机制 2 全套命令(含隔离段) | 无自身定时;RELISTEN 失败落 D10 重试 | RESYNC 与 pump③(U11)语义相邻但覆盖的本地状态不同(ESTABLISHED vs CLOSED);RELISTEN 与 pump①② 完全重叠且**更危险**——对健康监听器无条件 open→USE→close 会造成 ~1s 监听中断(net_socket.c:3078 注释自认) | RESYNC 针对的"芯片静默回收"为 (a)(run8 台架实证,当时 FUN_PARA 已是 0x09);但 API 形态属 (c) 时代遗留(上层镜像芯片状态、手动修);RELISTEN 属 (c) 冗余 | **可删**(生产路径):RESYNC 语义并入对账①扩展后删 API;RELISTEN 直接删(F-NET-2/F-NET-3) | +| 7 | **L3 挂起 recv**(pending)及其交互保险 | 结构 net_socket.c:184-193;登记 :2024-2063;完成 tick :2074-2190;了结 :1985-2007;取消 :2237-2256;close 前了结 :2267-2298;查询 :2202-2208 | 阻塞 recv 无数据即登记;`net_pending_tick` 每轮按 ①数据 ②到点 ③断连(`CLOSED ∥ session_dead`,:2136)顺序了结 | ③ 断连分支:排空后按 `released_abnormal` 回 CONNRESET 或 EOF(0)(:2164-2174);`state==CLOSED` 且 auto_relisten 时**消费者驱动即时重建**(:2159-2162);close 路径先 settle(排空或取消,必通知,:2316-2318);`net_init` 重建前 `cancel_all`(:241) | GET_RECV_LEN + READ_RECV_BUF(①③ 读数据);③ 即时重建走机制 2 命令 | 每 fd 至多一个挂起(:2038-2041 防覆盖挂死);deadline=登记+预算(默认 30s,net_config.h:39);**看门狗**:超 deadline 后 1s 强制了结 + DBG_ERROR(:182/:2181-2188);与机制 2 的交互保险 = retry_tick 的 `net_pending_active` 跳过(net_listener.c:405-408) | 与机制 2 互为保险(挂起在途则 D10 不动该 fd);③ 的即时重建与 recv_locked DONTWAIT 的即时重建同型(:1936-1938);`cancel_all`(net_init)与 tick 的"fd 已关闭兜底"(:2092-2096)双保险 | 阻塞模型演进(E-02 头阻塞修复),非严格自愈;③ 的排空+EOF 语义对应 (a)(旧观察注:FIN 先于数据、bit3=1 保留现场);看门狗属 (c) 完成逻辑缺位的 tripwire | **保留**(含看门狗,留痕成本极低);它是"消费者驱动重建"三处之一,P2a 已定案不可删 | + +配套机制(不在七项内但参与同一失同步网,盘点完整性所需): + +| 机制 | 位置 | 一句话属性 | 根因 | 裁决 | +|---|---|---|---|---| +| `net_lost_connect_probe`(TRAP-NET-20 双信源探针) | net_socket.c:1228-1267,调用 :1298-1300 | accept 轮询内联补 accept;去抖 2 轮(:1214)无冷却;幽灵甄别 `last_peer_port` | (a) 拆链窗口丢 CONNECT(裸机台架 15/15) | 保留 | +| closed-with-data accept | net_socket.c:1305-1335 | CLOSED + RX 有数据 → 接受"已关但可读"连接排空(bit3=1 排空契约) | (a)(TRAP-NET-20 三次定案:425 真根因是我方 relisten 自毁缓冲) | 保留 | +| `session_dead` 电平(N-32 P1 不变量 I) | net_types.h:192-206;置位 net_socket.c:1723/3439/3535;清零 :3310/3347;不清 net_listener.c:234-238 | "会话已结束"从边沿改粘滞电平,消费者不再被抢先 relisten 饿死 | (c) 设计缺口的**架构解** | 保留(这是根治本身) | +| `released_abnormal` | net_types.h:174-180;置位 net_socket.c:3529/3434 | 异常/正常释放区分 → 挂起 recv 回 CONNRESET 还是 EOF | (a) 协议边界(在途 FIN 与正常完成不可区分,TRAP-NET-18 硬化节留痕) | 保留 | +| `net_listen_locked` 3×osDelay(500) 阻塞重试 | net_socket.c:1092-1108 | 首次监听路径仍忙等(P2b 暂缓项),与 D10 非阻塞模式并存 | (b) 时代遗留(D10 已修 relisten 侧,listen 侧未统一) | 简化(统一走 `net_listener_try`) | +| `net_close_locked` 500ms 等 CLOSED 忙等 | net_socket.c:2326-2345 | 关闭热路径同步忙等(P2b 暂缓项) | (c) 同步等待设计债;台架实测危害理论性(设计文档 P2b 决策) | 简化(暂缓有据,留观察) | +| 陈旧 DISCONNECT 甄别(D20) | net_socket.c:3400-3415 | ESTABLISHED 收 DISCONNECT 时回读芯片,TCP 仍 ESTABLISHED 判陈旧丢弃 | (a) TRAP-NET-12 滞后锁存 | 保留 | +| 代次号 `s_net_generation` | net_socket.c:49/358-374;递增 net_task.c:145 | 网络重建后服务层重建监听的依据(应用层重建所有者通道) | (c) 但边界清晰(芯片复位后 fd 全灭是事实) | 保留 | + +### 1.2 根因分类汇总 + +**(a) 芯片真实行为(手册条文或裸机台架实证,无法从根上消除)——对应机制必须保留:** + +| 芯片行为 | 证据 | 支撑的机制 | +|---|---|---| +| 连接结束后芯片永不自动回 LISTEN(单连接复用模型下监听槽位被消耗) | 设计文档第 2.1 节引手册;官方 EXAM10 例程在事件现场当场重建 | auto_relisten、pump、D10、resync 语义 | +| 快速复连/拆链窗口内丢 CONNECT/DISCONNECT 中断 | TRAP-NET-17 生产实测;**TRAP-NET-20 裸机台架剥离 OS/net 层仍复现**(关键:证明与固件层 bug 无关) | 对账①②、探针、pump | +| 拆链收尾窗口变长且不可观测(>2s 不定长),窗口内假报 ESTABLISHED(幽灵态,对端端口=旧连接) | TRAP-NET-20 定案实验 | 幽灵甄别 `last_peer_port`、去抖+冷却纪律 | +| FUN_PARA bit3=1(0x08,D4 定案保留):DISCONNECT/TIMEOUT 后芯片不关 Socket 不清缓冲(停 0x05/0x00),MCU 必须排空后显式 close | 手册 §5.52②/§5.23;TRAP-NET-20 三次定案判别探针(recv_len=1027 完整载荷) | closed-with-data accept、TRAP-NET-18 排渣、close_locked 排空循环 | +| 中止时段的在途数据在 TIMEOUT **之后**才落进 RX 缓冲(~2KB 残渣被下条连接读走) | TRAP-NET-18 一阶段(RST 10/10 复现→修复 10/10) | QUARANTINE 400ms + 持续排空 | +| 上一条连接残留 DISCONNECT 会在复用同 fd 的下一条连接 accept 后 10~20ms 投递 | TRAP-NET-18 二阶段(0ms/30ms 沉降无效、100ms 通过) | GRACEFUL 100ms(仅收过数据时登记) | +| close 后重开 Socket 需沉降(PHY 形态 + 拆链形态) | TRAP-NET-09;TRAP-NET-20 台架(固定沉降 100~3000ms 均证伪"等就好") | D10 到点重试(而非固定延时) | +| 芯片可静默回收连接回监听且不发 DISCONNECT | run8 台架实证(当时 FUN_PARA=0x09,bit3 已=1,git 核实) | **阶段 1 起由对账⑤自动认领**(原仅 `net_resync_listen` 覆盖 = F-NET-2 缺口,已闭环) | +| 全局中断寄存器整体读即清、无按位清除;SINT 粘滞到被读 | 手册 §5.15;TRAP-NET-12 | net_poll 集中读清纪律;也解释了为何"丢弃式读全局"有害(F-NET-7) | + +**(b) 早期固件 bug(寄存器数值/引用/时序误用)——根因已修,评估补丁是否仍必要:** + +| 早期 bug | 修复 | 曾支撑的补丁 | 根因修复后补丁是否仍必要? | +|---|---|---|---| +| B3:listen/relisten 对全部 8 个 Socket 读 SINT + 丢弃式读清全局(多连接时代调试遗留,git 考古 e862009bc→319d00e3→db64e6acf) | 4f2cc8b(2026-10-12)收窄为只清本 Socket | TRAP-NET-17 对账在生产中的部分命中 | **仍必要**:TRAP-NET-20 裸机台架(无 net 层代码)独立实证芯片自身丢中断;但 B3 修复后对账在生产日志的命中数应显著下降——可台架统计验证(F-NET-8) | +| `get_socket_status` 曾给 1 字节缓冲(栈越界、状态误读) | e5733b3 | 曾可能污染对账/甄别读数 | 已修,无遗留补丁 | +| TRAP-NET-15:FUN_PARA bit0=1(保留位,无出处) | d758d5c 定案 0x08 | 多 Socket 模式误判相关 | 已修;对自愈机制无直接影响 | +| B2:`ch395f_tcp_connect` 返回值被丢弃 → CONNECTING 滞留 30s | 794a688 + a844241(命令级失败置 session_dead) | 无独立补丁,靠 recv 超时暴露 | 已修,无遗留补丁 | +| relisten 旧实现 osDelay(500) 忙等(全网冻结 500ms) | e5733b3(D10 非阻塞化) | — | 忙等已修;**同族的 `net_listen_locked` 3×500ms 与 `net_close_locked` 500ms 忙等仍在**(P2b 暂缓,F-NET-5) | +| B1:服务器侧 KeepAlive 使能(按手册补的"未证明复杂度") | 9a45be1 撤回(实测芯片 2s 重传超时已覆盖静默死亡) | — | 正面案例:根因复核后主动删除,应作为本次审计的裁决范式 | +| **U12(未修)**:relisten 的 TCP_LISTEN 失败不 close 芯片 Socket(与 open 失败路径不对称) | 未修(设计文档 P0 登记项未勾选) | 靠下一轮重试 open→USE→close 兜住 | 见 F-NET-6 | + +**(c) 设计缺口(本层镜像芯片状态导致必须对账/多处所有者)——可由架构消解(多数已消解):** + +| 缺口 | 现状 | +|---|---| +| 重建多所有者(曾 12 个) | P2a pump 收敛周期性路径;消费者驱动 3 处经台架定案保留;显式 API 2 处(RELISTEN/RESYNC)与 net_can_accept 旧门控**已于阶段 1 删除**(F-NET-2/3 ✅) | +| EOF 边沿化(抢先 relisten 吞 EOF) | P1 `session_dead` 电平化已消解 | +| 上层直接读 net 层状态镜像 | `lftpd_inet_accept_timeout` 直读 `p_sock->state`(lftpd_inet.c:117)、`net_listener_armed` 用 `readable` 代理"RX 可能有残留"(net_socket.c:1480-1485)、`net_can_read` 自认"对账不含 RX 非空但 readable=0"(:1500-1501)迫使上层保留每 32 轮兜底 recv(ui_net_sink.c:552-558、iec104_serv.c:246-253)——**第三层兜底散在应用层**(F-NET-12) | +| 应用层仍是重建所有者 | ~~time_sync 每会话 close 监听 fd 全量重建~~(**阶段 1 已修** `b8c6a3d`:同 fd 跨会话常驻);lftpd 应用层阻塞重试已删(阶段 1);iec104 `s_drop_conn` close 监听重建(协议违例场景,有意例外)(F-NET-13) | +| fd 别名(监听=连接) | 硬件上限(多连接模式不支持多监听,D1 出局),不可消解;只能靠不变量 I/II 约束 | + +### 1.3 编号发现 + +**F-NET-1 · 重建入口实测 5 处,与设计文档定案一致;"唯一重建入口"仅指周期性** +证据:`net_listener_try` 调用点共 4(阶段 1 删 `NET_MSG_RELISTEN` 后)——net_listener.c:552(pump,唯一周期性)、net_socket.c:1327(accept CLOSED 分支)、:1938(recv DONTWAIT EOF 分支)、:2162(pending_tick ③)。后三处为"消费者动作触发",P2a 台架实测删除会把 CLOSED 窗口拉长到 pump 周期+隔离期并触发 `[WD] task stall`(设计文档第 9 节 P2a 修正记录)。 +影响:这不是缺陷,是**已实测裁决的重叠**;但"唯一重建入口"的表述在 BSD 指南与注释中易被读成"仅 pump 一处",误导后续维护者删消费者驱动路径。 +建议:保留现状;文档口径统一为"唯一**周期性**入口 + 三处消费者驱动"。 + +**F-NET-2 · "本层 ESTABLISHED 而芯片已回 LISTEN"失同步无任何自动机制覆盖;唯一解药 API 生产零调用** +证据:对账①判据要求芯片 SOCKET_CLOSED 或 TCP_CLOSED(net_socket.c:588-589),芯片回 LISTEN(0x05/0x01)时不命中;对账②要求本层 LISTENING(:591);pump 只裁本层 LISTENING/CLOSED(net_listener.c:509-513);`handle_connect_event` 对本层 ESTABLISHED 的新 CONNECT 中断两个分支都不命中、静默丢弃(net_socket.c:3306 起)。该场景为芯片真实行为(run8 实证"芯片静默回收、监听器健康",git 核实当时 FUN_PARA=0x09 即 bit3=1)。现仅 `net_resync_listen`(net_socket.c:774-799)能修,而其生产调用者为零(仅 net_latency_test.c:783)。 +影响:一旦生产出现"静默回收 + 无 DISCONNECT",端口将滞留假 ESTABLISHED,新连接被芯片接住但本层无感知,直到下一次应用 close 或网络重建。 +建议:把该形态并入对账①——本层 ESTABLISHED 且芯片 TCP=LISTEN → 按 RESYNC 语义只改本层状态(不发芯片命令,纪律同 pump③ U11 与 :785-788 注释);并入后 `net_resync_listen` API 可删(见 F-NET-3)。 +**✅ 已实施(阶段 1,`b8c6a3d`)**:对账判据新增 **bad=5**(本层 ESTABLISHED 且 `st[0]!=SOCKET_CLOSED && st[1]==TCP_LISTEN`)→ RESYNC 语义(state→LISTENING + `last_peer_port` 转登 + `lost_conn_seen` 清零 + `net_pending_cancel(CONNRESET)`),不发任何芯片命令;手动 API 同批删除。台架验证见 §6 E10。 + +**F-NET-3 · `net_relisten` / `net_resync_listen` / `net_can_accept` 三个 API 是冗余或过期面** +证据:`net_relisten`(net_socket.c:3080)与 `net_resync_listen`(:3026)生产零调用(grep 全仓仅 test/board/net_latency_test.c:776-801);`net_relisten` 对健康监听器无条件 open→USE→close,自认造成 ~1s 监听中断(:3078 注释),与 pump③"芯片在听绝不重下发"的 U12 教训直接冲突。`net_can_accept`(:1427)唯一生产调用者 iec104_serv.c:227。 +影响:公开 API 面越大,上层"手动自愈"路径越多,正是用户怀疑的"补丁式代码难维护"的来源;`net_relisten` 是 footgun。 +建议:`net_relisten` 删除(台架 Phase 13 改用 pump 语义或测试专用内部入口);`net_resync_listen` 在 F-NET-2 并入对账后删除;`net_can_accept` 在 F-NET-4 迁移后删除。 +**✅ 已实施(阶段 1,`b8c6a3d`)**:三 API 与 `NET_MSG_RELISTEN` / `NET_MSG_RESYNC_LISTEN` 消息类型全部删除(MCU + sim 双端),生产/测试调用者归零;Phase 13 的自愈段交回 net 层自动机制。**⚠ 遗留铁律**:门控迁移必须给 `net_listener_armed` 补 ESTABLISHED 放行,见 F-NET-4 实施注。 + +**F-NET-4 · iec104(:2404)仍用过期门控 `net_can_accept` → 丢 CONNECT 探针对该端口从不运行** +证据:iec104_serv.c:227 以 `net_can_accept`(只认 ESTABLISHED,net_socket.c:1434)门控 accept;`net_lost_connect_probe` 只在 `net_accept_locked` 内 LISTENING 态运行(:1298-1300),门控挡在门外则永不触发——与 N-32 P1 为 :7003 修复的"探针失效"完全同型(ui_net_sink.c:521-526 注释记载原委)。:2404 的丢 CONNECT 只剩对账②兜底,而②带 400ms 去抖 + 1s 冷却(TRAP-NET-20 已实证该节奏对短命监听器"救不全")。 +影响:104 上行端口的丢中断恢复被拖慢到秒级;三个常驻监听里 :21/:8888 无条件轮询 accept、:7003 已换 `net_listener_armed`,唯独 :2404 漏换。 +建议:iec104 门控换 `net_listener_armed`(双端 sim 已有实现);随后 `net_can_accept` 可退役,消除"两个相似门控语义不同"的长期困惑。 +**✅ 已实施(阶段 1,`b8c6a3d`)**:门控已迁 `net_listener_armed`,`net_can_accept` 三处(MCU / 头文件 / sim)删除。 +**⚠⚠ 实施中发现的坑(必读,§6 E10 有完整证据链)**:`net_listener_armed` 首版只放行 LISTENING / CLOSED+残留,**漏放行 ESTABLISHED**——而单连接模型下"连接已到、应用未 accept"的电平正是 ESTABLISHED(CONNECT 处理把监听 fd 自身转数据通道),也就是旧 `net_can_accept` 唯一放行的状态。后果:i104 200ms 任务周期抢不过对账② 的 ~400ms 去抖,对账②先 standalone accept 转 ESTABLISHED → armed=0 → 应用整场不 accept,**iec104 18/18 全 FAIL**(127s 跑满超时;设备日志:`sock1 standalone accept` 后整场无 `i104: accepted`,直至对端 FIN 才由 closed-with-data 救回)。ui_net_sink 之所以不受影响,是 10ms 周期 + `net_accept_locked` 自带 `net_poll` 通常在状态翻转前就拿到 fd——**属竞态幸存,不是设计正确**。修复:armed 放行 ESTABLISHED(此态 accept 必返 fd,一次调用即离开,无 RPC 风暴风险);修复后 18/18 PASS 且耗时 127s→8.3s。 + +**F-NET-5 · 两套重监听重试策略并存:D10 非阻塞 vs listen 路径 3×osDelay(500) 阻塞** +证据:net_listener.c:71-79(D10,注释明言旧 osDelay(500) 忙等致全网冻结 500ms、实测恢复 588ms);但 net_socket.c:1093-1104 `net_listen_locked` 仍是 open 失败→close→osDelay(500)→重试 ×3(最坏 netTask 冻结 1.5s)。设计文档 P2b 已登记该忙等并**台架依据暂缓**。 +影响:同一芯片沉降问题两套答案;首次监听/PASV 监听落在沉降窗时 netTask 停摆(回执全链排队)。 +建议:裁决"简化"——`net_listen_locked` 的失败路径改走 `net_listener_try` 登记 + D10 到点重试(P2b 范围,非紧急,但两套策略不应长期并存)。 + +**F-NET-6 · U12 不对称仍在:TCP_LISTEN 命令失败不 close 芯片 Socket** +证据:net_listener.c:214-222 open 失败→close→交重试;:227-230 LISTEN 失败→直接返 BUSY,**不 close**;net_socket.c:1112-1114 `net_listen_locked` 同样不 close。设计文档第 5 节 P2 曾列"补 U12 的 close 对称",第 9 节 P0 登记 U12 进 Trap 记录的复选框未勾选,代码亦未修。 +影响:LISTEN 失败后芯片处于 OPEN 但未监听;下一轮重试 open 返 USE 才走 close 分支兜回(多一轮 30ms + 一次 USE 竞态面);与 pump③ 防的"把正听着的 Socket 关掉"是同源机理的反向形态。 +建议:小修——两处 LISTEN 失败补 `ch395f_close_socket`,与 open 失败路径对称;同时把 U12 正式登记进 Trap 记录(P0 欠账)。 + +**F-NET-7 · 三处无条件"读清全局中断寄存器"仍在,会搁浅其它 Socket 的挂起事件——对账/pump 因此仍部分背锅** +证据:net_connect_locked net_socket.c:1668-1670、net_close_locked :2332-2334、relisten 隔离段 net_listener.c:176-178。B3 修复(4f2cc8b)只收窄了 listen/relisten 成功尾两处;Trap 记录 B3 行残余注记明确点名这三处"仍会搁浅(延后、非销毁)其它 Socket 挂起事件——它们是 TRAP-NET-12/-18 指定的两级预清,本次不动"。 +影响:全局寄存器整体读即清(手册 §5.15),这三处每次执行都可能吞掉别的在用 Socket 的 CONNECT/DISCONNECT 边沿位——即"丢中断"有一部分仍是**自家制造**,对账①②与 pump 的必要性里混入了这部分本可消除的噪声。 +建议:与驱动层审计(T2)合并处置:评估两级预清能否收窄为"只读本 Socket SINT"(B3 已证该做法成立且官方例程一律如此);若可行,对账①②的生产命中应同步下降(见 F-NET-8 的验证腿)。 + +**F-NET-8 · TRAP-NET-17 对账不是 B3 固件 bug 的补丁——裸机台架已独立实证芯片丢中断,故 B3 修复后对账不可删;但命中数应可测得下降** +证据:TRAP-NET-20 定案实验在**剥离 OS/net/FTP 的裸机芯片台架**上复现"握手完成但 CONNECT 中断被丢"(修复组 15/15,机制触发 5 次全救回);B3 修复在其后(4f2cc8b)。 +影响:直接回答本次审计的核心疑问——"根因已修的补丁是否仍必要":对 TRAP-NET-17 而言,其根因是芯片行为而非固件 bug,**必要**。 +建议:保留对账①②;补一条低成本台架验证腿:B3 修复前后各跑 `:21 100ms×50`,serial-assistant 统计 `reconcile:` 日志命中数,量化"自家制造的丢中断"占比,为 F-NET-7 收窄决策提供数据。 + +**F-NET-9 · "close + 排空 RX + 排空中断 + 等 CLOSED"在 net 层有两套变体实现——下沉驱动层的最佳候选** +证据:net_close_locked net_socket.c:2326-2345(close→循环 GET_SOCKET_STATUS + 读 SINT + 读全局→等 CLOSED,500ms 上限)与 net_relisten_locked 隔离段 net_listener.c:171-184(排空 RX→close→读 SINT→读全局→读状态→等 CLOSED/TCP_CLOSED + 最短等待)。两者语义同构(bit3=1 排空契约),参数不同(500ms vs 400/100/800ms),后者多排空 RX。 +影响:同一芯片契约两处实现,参数漂移风险(例如只有一处排 RX——close_locked 路径不排空 RX,依赖"close 前可读"契约由上层先排)。 +建议:裁决"下沉":在 CH395F 驱动层收口一个 `ch395f_close_flushed(sock, drain_rx, wait_ms)` 原语(具体签名归 T2 评估),net 层两处调用它并各自保留策略参数;BSD 层不再手拼命令序列。这是"让 BSD API 更主流"最实的一步。 + +**F-NET-10 · retry_tick 的 L3 保险只挡"在途挂起 recv",不挡"ESTABLISHED 无挂起"——与 pump 的状态门不对称** +证据:net_listener.c:400-412 重试循环无本层状态门(不看 state),仅 :405-408 跳过有挂起 recv 的 fd;对比 pump 明确只裁 LISTENING/CLOSED(net_listener.c:509-513),且 recv/accept/pending 三处即时重建都带内层 `state==CLOSED` 门(:1936/:2159 注释明言防 U12 同源事故)。 +影响:fd 在重试队列中且已因新 CONNECT 转 ESTABLISHED(无挂起 recv)时,到点重试会 close+open 摧毁新连接。可达性存疑(芯片不在听时新 SYN 难成,需 open 成/LISTEN 败的 U12 窗口配合),但不对称本身是隐患。 +建议:给 retry_tick 补与 pump 同型的状态门(仅 LISTENING/CLOSED 才重试),一行条件消掉整类竞态;补后 `net_pending_active` 保险可保留作第二道。 + +**F-NET-11 · 注释漂移:handle_disconnect_event 写"pump 每 50 ms 回读",实际 100ms** +证据:net_socket.c:3384("每 50 ms")vs net_listener.c:437(`NET_LISTENER_PUMP_PERIOD_MS 100U`,注释记载 50ms 曾把 uiTask 最坏单轮推到 1034ms 的实测)。 +影响:排障时按 50ms 预期判读日志时序会错判。 +建议:改注释;顺带该 LISTENING 分支(:3380-3399)现已纯留痕不动作,其 GET_SOCKET_STATUS 回读可裁决"简化"(pump 的 `chip not listening` 日志已含同等信息),删分支则少一条 SPI 与一处重复判读口径——低优先级。 + +**F-NET-12 · "RX 非空但 readable=0"无人对账(U15),兜底被推给上层的低频盲收——三层兜底中最隐蔽的补丁** +证据:net_can_read 注释自认"readable 仅由 RECV_OK 置位,TRAP-NET-17 对账不含 RX 非空但 readable=0 → 中断丢失时恒返 0,调用方须保留低频兜底 recv"(net_socket.c:1500-1501);ui_net_sink.c:552-558 与 iec104_serv.c:246-253 各自实现"每 32 轮无条件收一次";`net_listener_armed` 又用 `readable` 代理"芯片 RX 可能残留"(:1482-1485)。Trap 记录 B6/B10 一带将 U15 登记为未修残余。 +影响:数据面丢中断的兜底散落在两个应用任务里,net 层无对账——与"自愈内聚进 net/驱动层"的目标方向相反。 +建议:中期把"GET_RECV_LEN>0 但 readable=0"并入 pump 或独立低频对账(每 fd 一条命令,成本与 pump 同量级),随后上层 32 轮兜底可退役;短期保留现状但在两处上层注释里指向同一编号(U15),避免第三处复制。 + +**F-NET-13 · time_sync 每会话 close 监听 fd 全量重建,违反自家铁律,是应用层重建所有者的最大残留** +证据:time_sync.c:182 `net_close(conn_sock)`(单连接下 conn==listen)后外层循环 socket+bind+listen 重建(:224);设计文档第 7.4 节契约表已点名"违反『EOF 不得 close 监听 fd』→ close→重建沉降窗(TRAP-NET-09)+ 最坏 ~10s 端口下线"。对照正面样板:lftpd 控制通道明确不 close(lftpd.c:746-750/826-827)、iec104 EOF 分支带 `s_client != s_listen` 护栏(iec104_serv.c:259-262)。 +影响::8888 每次会话结束都主动制造一次 TRAP-NET-09 沉降窗;也是"应用层还能毁监听器"的活例。 +建议:裁决"简化(上层)":time_sync 会话收尾不 close,直接回外层在同一 fd 上 accept(lftpd 模式);iec104 `s_drop_conn` 的 close 属协议违例强制断连,语义正当,保留但注释标注为"有意例外"。 +**✅ 已实施(阶段 1,`b8c6a3d`)**:`net_close(conn_sock)` 删除,`ls` 提到外层循环跨会话常驻(仅网络代次变化/被毁时重建),`conn != ls`(模拟器多连接后端)才关数据 fd。台架实测连续 6 轮推送正常(周期 ~4s),每轮走 `standalone accept → DISCONNECTED, releasing → settle → auto-relisten ok`,零 `[ERR]`。契约变化已同步 `test/host/time_sync_server.py`(板子不再先关,PC 侧 2s 宽限后自关);⚠ 强杀 PC 进程会走芯片 `[ERR] TIMEOUT` + QUARANTINE 沉降(异常收尾,非正常路径)。 + +**F-NET-14 · D10 的"2s 放弃"不是终态,`relisten give up` 日志误导排障** +证据:net_listener.c:414-419 到期清队列 + DBG_ERROR;但 pump 在冷却后看到"芯片不在听"会再次 `net_listener_try`(net_listener.c:538-552),监听器实际会无限期重试(每 ~1.2s 一轮)。 +影响:日志出现 give up 时端口并非永久死亡,排障者可能误判严重度;同时"无限重试"缺少上限留痕(每轮一条 pump 日志)。 +建议:give up 文案改为"重试预算耗尽,交 pump 周期接管";或把 pump 的重触发也纳入同一预算计数(二选一,倾向改文案——保留最终兜底)。 +**✅ 已实施(阶段 0,`4c0a0af`)**:文案改为 `relisten retry budget exhausted, pump takes over`;同批补 0-4 retry_tick 状态门(仅 LISTENING/CLOSED/BOUND 之外的状态清陈旧登记,防 open→close 摧毁活连接)。 + +### 1.4 重叠/兜底关系速览(谁兜谁) + +- 监听器重建:pump(周期唯一)→ 失败落 D10(30ms×2s)→ D10 放弃后 pump 冷却再触发(无限);消费者三处即时重建把 CLOSED 窗口压到 ~0(防上层 RPC 空转)。 +- 丢 CONNECT:探针(accept 内联,快)↔ 对账②(周期,兜底网);两者共享幽灵甄别;:2404 因门控过期只剩②(F-NET-4)。 +- 丢 DISCONNECT/静默回收:对账①(芯片 CLOSED 形态)↔ 对账⑤(芯片回 LISTEN 形态,阶段 1 起自动认领 = F-NET-2 已闭环)。 +- 会话结束感知:session_dead 电平(P1)兜住"抢先 relisten 吞 EOF";pending_tick ③ 与 recv DONTWAIT 双路径给 EOF;看门狗兜"完成逻辑缺位"。 +- 芯片收尾残留:TRAP-NET-18 沉降/隔离(重建前)↔ close_locked 排空循环(显式关闭)↔ listen/relisten 成功尾读本 Socket SINT——三处清理纪律,两套命令变体(F-NET-9 下沉候选)。 +- 数据面丢中断:RECV_OK 电平重放(TRAP-NET-08)+ readable 门控 + 上层 32 轮盲收(F-NET-12,唯一未内聚的一层)。 + +### 1.5 裁决汇总 + +| 机制 | 裁决 | 一句话依据 | +|---|---|---| +| auto_relisten / session_dead / released_abnormal / 幽灵甄别 / closed-with-data / 探针 / 对账①② / pump / D10 / TRAP-NET-18 沉降隔离 / L3 pending + 看门狗 | **保留** | 根因是芯片行为或已实测裁决的架构解;TRAP-NET-20 裸机台架是"不可从根上消除"的判据 | +| net_relisten | **可删** | 生产零调用 + 对健康监听器是 footgun | +| net_resync_listen | **可删**(先并入对账①) | 语义正确但载体错位:应是自动对账,不是手动 API | +| net_can_accept | **可删**(先迁 iec104 到 armed 门控) | 过期门控致 :2404 探针失效 | +| handle_disconnect_event LISTENING 分支回读 | **简化**(可选) | 已纯留痕,pump 日志可替代 | +| net_listen_locked 3×500ms / net_close_locked 500ms 忙等 | **简化**(P2b,暂缓有据) | 与 D10 策略统一;等 task stall 因果证据 | +| U12 LISTEN 失败不 close | **修复**(小) | 一行对称补齐 | +| retry_tick 状态门 | **修复**(小) | 与 pump 对称,一行条件 | +| 三处全局读清残留 | **下沉/收窄**(与 T2 合并) | B3 已证单 Socket 收窄可行 | +| close+排空+等 CLOSED 命令序列 | **下沉驱动层**(与 T2 合并) | 两套变体一套契约 | +| 上层 32 轮盲收兜底 | **下沉 net 层**(中期) | U15 并入对账后上层退役 | +| time_sync 会话收尾 close | **简化(上层)** | 改复用同 fd accept(lftpd 样板) | +| GRACEFUL 沉降门 `recv_bytes≠0` | **修正(一行级,见 F-NET-15)** | E1 背靠背失效循环的放大器;门的原保护目标已被 N-32 pump 延迟化架空 | + +### F-NET-15 背靠背数据连接失效循环(E1 发现 B 代码级根因,T5) + +失败签名(完整时间线见 §6 的 E1 结果一节):`n30_repro` 背靠背 STOR 4/50、复跑 1/10 成功,其余 425;sock4(FTP 数据监听)上数据连接 accept 后 ~20ms 被上一条连接的陈旧 DISCONNECT 判死(`DISCONNECTED, releasing (chip st=0x05/0x00)`),被杀会话 `recv_bytes=0` → 不登记 GRACEFUL → 立即 relisten → 下一条连接继续被杀(自持循环);closed-with-data accept 偶发救回 2 次但被 lftpd 镜像门控错过。 + +#### Q1 · D20 陈旧甄别为何在芯片 TCP=0x00 时放行 + +判据本体:net_socket.c:3404-3415(回读 :3408,判别 :3410)—— 单一规则:芯片 TCP==0x04(ESTABLISHED)判陈旧丢弃,**否则(含 CLOSE_WAIT/CLOSED)一律按真断开**;CLOSED 是刻意放进"真"集合的(注释 :3403「真断开时芯片处于 CLOSE_WAIT(0x07)/CLOSED(0x00) 等状态照常处理」)。 + +放行的根因是**判据的证据通道被事件自身污染**: + +1. 滞后 DISCONNECT 在**拆链收尾完成时刻**锁存(TRAP-NET-12:DISCONNECT 在 FIN/ACK 完成后才上报),而拆链收尾完成的标志正是把 TCP_CLOSED(0x00) 写进该 Socket 的状态寄存器。事件投递与寄存器写 0x00 是同一时刻的两个面——甄别读到的 0x00 无法区分「真断开」与「陈旧事件 + 新连接仍活着」。 +2. 每 Socket 只有一个状态槽,存的是**最后一次状态迁移**(TRAP-NET-20 已证该寄存器在拆链窗口内双向不可信:既会假报 ESTABLISHED 幽灵,也会假报 CLOSED——新连接的 0x04 迁移被旧连接的 0x00 迁移覆盖)。E1 签名 `st=0x05/0x00` 即:Socket 仍 OPEN(0x05,被杀连接在芯片侧还活着),TCP 槽显示旧拆链的 0x00。 +3. **事件无连接身份**:芯片事件位不带连接 ID;幽灵甄别那套对端端口比对(:597-604、:1249)在此不可用——新握手已把芯片对端寄存器覆写为新端口,两种假设下都读到 `peer == remote_port(新) ≠ last_peer_port(旧)`,无判别力。 +4. 投递时序吻合:net_poll 同一次 SINT 读(:469)内 CONNECT 先于 DISCONNECT 派发(:480-489);`handle_connect_event` 在 accept 尾部读清 SINT(:3314-3316),所以致死位必须在 accept **之后**锁存——正是拆链收尾完成时刻(10~20ms,TRAP-NET-18 二阶段与 TRAP-NET-20 现象 3 均有记载)。新连接自己的握手 ACK 流量往往正是完成旧拆链最后一次 FIN/ACK 交换的那笔流量 → 背靠背强相关。 + +结论:D20 在该时刻**结构性失明**——它的规则是在 TRAP-NET-16 时代标定的(当时事件先于寄存器写投递,芯片显示 0x04 可辨)。剩余可用判别轴只有两条:时间上的电平确认(对账去抖语义)或"年轻连接+零数据"启发式。 + +#### Q2 · GRACEFUL 沉降门 `recv_bytes≠0` 对失效循环为何失效 + +门本体:net_socket.c:3424 `recv_bytes != 0U`(外层已是 ESTABLISHED 状态门 :3400)才登记 GRACEFUL(:3430);不登记则下一次 relisten 跳过整个隔离段(net_listener.c:149 以 `quarantine != 0` 为门)。 + +1. **受害者类别由事件时序定义,与数据无关**:连接在 accept 后 ~20ms 被杀,MCU 尚未读任何字节 → `recv_bytes==0` 是构造性必然(即便客户端已把 STOR 数据灌进芯片 RX——门统计的是 MCU 已读字节,不是芯片 RX 存量)。门按"收过数据"划危险类别,恰好把每一任受害者排除在外。 +2. **循环自持的关键:每一轮都重新制造残渣**。被杀连接在芯片侧仍 OPEN 活着(st[0]=0x05);下一次 relisten 的 open 返 USE → 失败分支 close(net_listener.c:214-222)→ 在芯片侧摧毁该连接 → 新拆链 → TRAP-NET-12 锁存新的滞后 DISCONNECT → 下一条连接再被杀。最初的残渣只是种子,循环靠"杀→close→拆链→滞后事件"自我繁殖。 +3. **为什么登记沉降就能断环**:登记后 relisten 走隔离段——提前 close 芯片 Socket(:174)、每 30ms 重试轮读清 SINT(:176)、≥100ms 保持期满才 open+listen(:182-197)。滞后 DISCONNECT 在"芯片 Socket 已关、本层无会话"的窗口内被消费,无害(对照:落在 LISTENING 上也只是被动留痕,net_socket.c:3380-3399)。 +4. **门的原保护目标已被架空**:注释 :3420-3422 写明门是为保住 TRAP-NET-17 的 `:21 @0ms×50` 快速复连时序而设(零数据连接免 100ms 沉降)。但 E1 发现 A 实测:N-32 P2a 之后**每次**会话收尾都要等 pump(115–900ms)+ D10,恢复窗 0.3–1s,`:7003` 复连 p50=1020ms(SYN 静默丢 → 客户端 RTO)。门如今省下的 100ms 落在 0.3–1s 的窗里不可见,换来的却是 P0 失效循环。 +5. 对照佐证:TIMEOUT 路径的 QUARANTINE **无条件**登记(net_socket.c:3517-3524)——所以 TRAP-NET-18 的 RST 中止修复从未循环过;循环是 DISCONNECT 有门路径独有的。 + +#### Q3 · closed-with-data 救回后 lftpd 镜像门控为何错过 + +门本体:lftpd_inet.c:116-120 —— 每次 accept 失败后 ftpTask **直读** `p_sock->state`(跨任务裸镜像,F-NET-12 家族),非 LISTENING 立即 `return -1` → 425(预算 5s,lftpd.c:369/401/532/599)。 + +1. 每轮 `net_accept` 内部先走 CLOSED 分支(net_socket.c:1316-1328):RX 有数据 → closed-with-data 救回(返 fd);否则即时 relisten。所以镜像检查看到的是 accept 之后的状态:relisten 成功 → LISTENING(继续轮询);**relisten 失败 → 仍 CLOSED → 立即中止**。 +2. 而 relisten 失败恰恰集中发生在芯片拆链忙期(open 返 USE/BUSY)——镜像门在**恢复正在进行、剩余预算最值钱**的时刻中止,把最多 ~4.9s 预算直接扔掉。N-32 不变量 II 已把 `CLOSED + auto_relisten` 定义为"监听器仍在服役"(`net_listener_armed`,net_socket.c:1459-1489),lftpd 数据门控早于该语义、未迁移。 +3. 救回窗口本身很窄:被杀 → pump/D10/accept 轮在 ~30–130ms 内 relisten,其 close 顺带清掉芯片 RX;lftpd 100ms 轮询通常落在 RX 已清之后 → 救回输给 relisten(E1:46 败仅 2 救)。 + +#### 因果链(一轮迭代,全部落行号) + +1. conn N accept:CONNECT → ESTABLISHED,SINT 预清(net_socket.c:3314-3316)。 +2. 旧拆链于 10~20ms 后完成(常由 conn N 自身 ACK 流量促成):状态寄存器写 0x00 + 锁存 DISCONNECT。 +3. net_poll 派发 → D20 读 0x05/0x00 → 判真断开 → CLOSED/session_dead/last_peer_port(:3434-3444);`recv_bytes==0` → 不登记 GRACEFUL(:3424)。 +4. relisten:open 返 USE(Socket 仍 OPEN 且 conn N 活着)→ close(net_listener.c:218)→ conn N 被芯片侧摧毁 → 拆链 N 滞后锁存;30ms 后 D10 重试 open+listen 成功(:262 成功尾 SINT 只清"已锁存"位)。 +5. 客户端下一条命令重连 → CONNECT → ESTABLISHED;拆链 N 完成 → DISCONNECT N → 回到第 2 步。 +6. 期间 lftpd accept_timeout 轮询撞上"relisten 失败的 CLOSED" → 镜像门中止 → 425。 + +#### 最小修复候选(按侵入度递增) + +- **M1(一行级,net 层,首选先手)**:net_socket.c:3424 删去 `recv_bytes != 0U` 门(使 :3430 的 GRACEFUL 登记无条件化,外层 ESTABLISHED 门 :3400 保留)。断环位置=因果链第 3 步:登记后第 4 步自动走隔离段(提前 close + 逐轮 SINT 排空 + ≥100ms 保持),下一条连接出生干净。代价:每次会话收尾恢复窗 +100ms——相对实测 0.3–1s 的 pump 主导窗为边际量。必跑回归:`:21 @0ms×50`(门当初的守护用例)、`ftp_regression` 11/11、`ftp_abort_probe` RST/FIN、`n30_repro --n 50`。 +- **M2(一行级,应用层)**:lftpd_inet.c:117 改判 `net_listener_armed(socket) == 0`(模拟器双端已有实现)。让 5s 预算穿过"恢复中的 CLOSED"窗口,救回/pump 重建得以在窗内落地;同时消灭一处裸镜像。注意 M2 单独不杀循环(连接仍被杀),只把"立即 425"换成"预算内恢复";与 M1 合用后循环死亡,M2 退化为时延/健壮性修正。 +- **M3(结构性,阶段 1)**:D20 年轻连接电平确认——对"刚转 ESTABLISHED 且零数据"的连接上的 DISCONNECT 不吃边沿,交给对账①电平确认(200ms×2 去抖):连接真活则下轮读到 0x04 存活;真断则 ~400ms 内由对账①释放。需一个"年轻"标记(复用 `lost_conn_seen` 或加 accept 时刻字段)。直击"杀"本身,改动面大于 M1,建议 M1 台架数据出来后再做。 +- **M4(供给面,即 F-NET-7)**:三处全局读清(net_socket.c:1668-1670、:2332-2334,net_listener.c:176-178)收窄为单 Socket SINT 纪律。全局位被丢弃式读清 + SINT 粘滞 = **搁浅**:滞后 DISCONNECT 可被推迟到任意未来连接上投递,把有界的 10~20ms 危险窗变成无界。§6 E1 的 G2 归因即此。M1 不依赖 M4 生效,但 M4 缩小危险供给面。 +- **M5(结构性,即 F-NET-9)**:`sock_reset` 复合原语(close+排空 RX+排空中断+沉降+relisten 收进一个驱动层事务)让沉降**无条件且内联**,同时消掉发现 A 的 pump 延迟窗(`:7003` p50 1020ms 的根源)——M1 的 +100ms 代价在该形态下归零。 + +#### 与此前发现的关系 + +- **F-NET-7**:全局读清是本循环的**供给放大器**(把滞后事件从"有界 10~20ms"放大为"无界搁浅");M1 是需求侧止血,M4 是供给侧收口,两者正交。 +- **F-NET-9**:M1 是在现有"两套沉降变体"之上打的一行补丁;F-NET-9 的复合原语是同一方向的深水区——"沉降无条件化"在事务化后不再需要门来平衡时延。一行级修正与下沉原语不矛盾,是同一裁决的先后两步。 +- **F-NET-12**:lftpd 镜像门控是"上层直读 net 状态镜像"的第一例**实测 P0 代价**,为 F-NET-4 的 armed 门控迁移提供了数据支撑。 +- **F-NET-11** 结论仍立:落在 LISTENING 上的滞后 DISCONNECT 无害(被动留痕);循环需要它落在 ESTABLISHED 上,即投递必须晚于 accept——这正是拆链完成时刻的投递形态。 + +#### 回归集建议 + +把 `n30_repro`(背靠背 STOR)纳入常备回归集——TRAP-NET-20 终版回归只含有间隔的 `ftp_regression`,背靠背形态不在集内,这是本次 9/10 能潜伏的测试面原因(§6 对照台账已登记)。验收线建议:`n30_repro --n 50` 通过数与 `:21 @0ms×50`、`ftp_regression`、`ftp_abort_probe` 同时全绿,由台架定具体门槛。 + +### F-NET-17 残余偶发的真身:风暴态(E8 深查,2026-10-13) + +E7b 的"残余 ~3%"不是稳定值:同固件同负载,故障率随设备状态在 **2%(全新启动)~ 54%(残渣累积)~ 12%(仅复位)** 间摆动(§6 E8 矩阵)。"偶发"的真身是**可自持的风暴态**,三个缺陷叠加: + +1. **沉降自再生锁存**(M1 的硬币):沉降段 `net_listener.c:174` close 之后,拆链收尾在 ~10-20ms 后(TRAP-NET-12 延迟锁存)**重新置位** DISCONNECT;`:176` 的 SINT 读清与 close 同轮发生,读不清这枚迟到的锁存 → 沉降最后一轮"清干净"的假象下,relisten 完成后 11ms 首条连接被杀。30ms 重试粒度 vs 10-20ms 锁存延迟 = 每次重建一次硬币——E7b 的 2% 即此。 +2. **泵与救回互抢**(风暴燃料):沉降段 `:172` `net_drain_chip_rx` 排空的正是 closed-with-data 救回(net_socket.c:1317)依赖的 RX 数据;救回 accept 成功返回 fd(:1352)但会话随即被杀/数据被排空 → lftpd 短读 → 425。E8 窗口 31 次 accept timeout 与 38 次救回并存为直接证据。 +3. **芯片 OPEN 卡死放大**(自持结构):被杀会话芯片侧仍 OPEN(0x05),新 SYN 被接进卡死 socket(TCP 槽 0x00)→ accept → 下一枚锁存再杀 → 直到 800ms 强制放行 + close 真正落拆链才出簇。风暴期时间线(0.75s 内 3 轮"救回→杀→救回")见 §6 E8 节。 + +**设计修正(覆盖 T5 版 M3/M5 表述)**: +- **M3 判据**:`chip=0x05/0x00`(Socket OPEN + TCP 槽 0x00)+ 连接年轻(<1s)= 逻辑矛盾态(活连接应 0x04)→ 不吃边沿,交对账①电平去抖二次确认。风暴窗口 100% 命中、健康路径(0x00/0x00 或 0x07)零误伤。 +- **M5 语义**:`sock_reset` 必须 **drain-until-quiescent**(close 后轮询 SINT 直到 DISCONNECT 锁存出现并读清、连续两轮无新锁存才 open+listen);固定时间窗在锁存延迟抖动下必输硬币。 +- **新红线**:RX 有数据 = 消费者资产——沉降/pump 重建前必须让位 accept 救回路径,排空后才准 reset;救回与重建必须单一所有者(`sock_reset` 内联裁决)。 + +### F-NET-17 补充:风暴态三机制代码级核实与 M3/M5 设计(T6,2026-10-13) + +> 行号 = 提交 9ac9879(M1+M2+M4-lite)后的实测行号。只读核实,未改产品代码。 + +#### T6-① 「close 已 CLOSED 的芯片 Socket 产生 DISCONNECT 锁存」——基石**修正**而非照单确认 + +**手册口径**:§5.35「关闭 Socket 前 CH395 会自动断开 TCP 连接」(Trap 记录定案更正引用)——close 的动作序是"先断链再关 Socket";若 TCP 已 CLOSED,断链环节无事可做。§5.52② bit3=1 只豁免 DISCONNECT/TIMEOUT 场景的自动关闭/清缓冲,**不豁免 CLOSE_SOCKET 命令本身**(close 仍清缓冲——net_socket.c:1311 注释"close+open 清缓冲"同此)。SINT bit4=DISCONNECT(ch395f.h:268)。 + +**证伪强形式(干净情形)**:TC-NET-204(TRAP-NET-07 定案更正,Trap 记录实测表)模式 C 正是本问题的受控实验——DISCONNECT 已发生、芯片停在 `0x05/0x00`(与风暴签名逐位相同),补 `close` → **+0 ms 回 CLOSED**、重开重连 5/5 OK;且"15 轮重开均未观察到残留 DISCONNECT(0x10)——close 顺带清掉了锁存"。即:**close 一个已 CLOSED 的 Socket 不产生新锁存,反而顺带清掉已锁存的位**。模式 A(close 一条活连接,主动拆链)5 轮同样无残留锁存。 + +**与 TRAP-NET-12 的张力消解**:TRAP-NET-12 的残留锁存不是 close 命令制造的,而是**拆链完成事件**——"DISCONNECT 可能在与 CLOSED 状态达成几乎同时、甚至之后才锁存"(Trap 记录 TRAP-NET-12 根因节),粘滞跨会话(B3 条目"SINT 粘滞到被读")。锁存的生成者是拆链过程(多为对端 FIN 的被动拆链收尾),不是 close 命令。 + +**风暴窗口的直接反证**:E8 时间线里 kill 发生在 `auto-relisten ok` **之前**(窗口内 relisten 尚未完成,沉降段 close 还没跑或没再跑)——那几枚 11ms 杀连接锁存**不可能由沉降段 close 产生**。真实来源两条:(a) 上一条 STOR 的客户端 FIN(FTP 正常行为,发完即关,TRAP-NET-20 定案)的被动拆链收尾迟到完成(TRAP-NET-12/-18 的 10~20ms,TRAP-NET-20 的 >2s 不可观测窗);(b) 卡死 socket(0x05/0x00)把新 SYN 接进来又自行拆掉(`docs/芯片手册/CH395F.md` §5.29 语义域),每轮自产一对 CONNECT/DISCONNECT。 + +**但"沉降末轮 SINT 读清不能保证静默"的实操结论仍成立**,经两条路径: +1. 拆链完成滞后于末轮读清(lead 说的硬币——生成者是拆链,不是 close); +2. **搁浅**:剩余两处全局读清(net_socket.c:1670 `net_connect_locked`、:2334 `net_close_locked` 等待环)焊死 net_poll 全局门(:441→:464)→ 锁存被搁到下一条连接的 CONNECT 全局位被读到那一刻,net_poll 同轮读 SINT 见 CONNECT|DISCONNECT 双置 → 先 CONNECT(:480-483)后 DISCONNECT(:486-489)→ accept 后毫秒级被杀。这是"锁存跨越整个沉降窗落到下条连接"的投递机制(F-NET-7 残余的实测代价)。 + +**基石判定**:现象成立、机制表述须修正——"close 已 CLOSED Socket 会再锁存"应改写为"**TCP 槽 0x00 不证明拆链已完成;拆链完成事件滞后且粘滞;close 命令本身对死 Socket 不产生锁存(TC-NET-204 模式 C),对活连接的主动拆链在该实验窗口内亦无残留(模式 A)**"。此修正直接改写 M5:静默判据必须是"**等到预期锁存出现并读清 + 连续静默轮**",而不是"close 后假设会生成锁存然后清掉";且静默可被新连接自身流量打破(无界窗)→ **M3 是结构性必需,不是可选加固**。 + +#### T6-② 泵与救回抢同一 RX 缓冲——**证实**,排空后救回判据必然失败(对已存在数据) + +- 同一缓冲:`net_drain_chip_rx`(net_listener.c:102-121)经 GET_RECV_LEN+READ_RECV_BUF 消费(读即推进);closed-with-data 救回判据 net_socket.c:1317 `ch395f_get_recv_len(sockfd) > 0` 读同一寄存器源。两者都在 netTask 上下文(accept RPC 与 pump/relisten 串行)→ **不是数据竞态,是先后竞态**:谁先跑谁定生死。 +- 排空后 `recv_len==0` → :1317 判据对"排空时已存在的数据"**必然失败** → accept CLOSED 分支落到 :1327 即时 relisten(同样进沉降段)→ 资产二次销毁(close 清缓冲)。唯一例外:引擎仍活、客户端续发,可在两次排空间隙回填 RX——但沉降段每 30ms 排一次(:172 每重试轮)、lftpd 100ms 轮询一次 → 泵压倒性赢。 +- E8 佐证:风暴窗 31 次 accept timeout 与 38 次救回并存 = 竞态双峰的直接证据(赢一次的救回拿到的也常是被排空半份的数据 → 短读 425)。 +- 结论:lead 的新红线成立。裁决点收进 sock_reset 第一步(见 T6-④ QUIESCE 第 1 步):RX>0 且芯片 st[0]==OPEN 且本层 CLOSED+auto_relisten → **让位 accept**(返 BUSY 不排空),RX==0 或强制放行后才准排空+reset。 + +#### T6-③ M3 最小补丁设计(一行级判据 + 一行级前置清零) + +**字段选型:复用 `lost_conn_seen`(net_types.h:190)作 ESTABLISHED 期"可疑电平"(0/1),零 RAM**。 +- 生命周期兼容:探针只在 LISTENING 期跑(accept 门 :1298),与 ESTABLISHED 期不重叠;relisten(net_listener.c:249)/pump③(:538)/listen(:1121)/RESYNC(:792)已有清零点。 +- **前置一行(必须)**:`handle_connect_event` 现**不清** lost_conn_seen(grep 实测:清零点无此函数)——LISTENING 期探针残留的计数会带进新连接,使可疑判据首轮即被旁路、M3 失效。须在两个转 ESTABLISHED 分支(:3306/:3344 附近)补 `p_sock->lost_conn_seen = 0U;`。 +- 备选(新增 `uint32_t estab_ms` 支持"<1s 年轻窗",32B/8 sock)不推荐:矛盾判据本身即判别器,flag 比时钟更准——滞后锁存同样能杀老年连接,age 窗会漏;代价仅为真 RST 释放也延 ~400ms(可接受,见误伤分析)。 + +**判据插入点**:`handle_disconnect_event` ESTABLISHED 分支,D20 甄别(:3410-3415)之后、释放留痕(:3418)之前——`st[]` 已在 :3408 读过,**零额外 SPI**: + +```c +if ((st[0] == CH395F_SOCKET_OPEN) && (st[1] == CH395F_TCP_CLOSED) && + (p_sock->lost_conn_seen == 0U)) { + p_sock->lost_conn_seen = 1U; + DBG_WARN("sock%d suspect DISCONNECT on live conn (chip 0x05/0x00), defer to reconcile", sockfd); + return; +} +``` + +**交对账①的衔接语义——现有代码可直接认领,对账零改动**:对账①判据(:588-589)`ESTABLISHED && (st[0]==CLOSED || st[1]==CLOSED)`,0x05/0x00 经 `st[1]==CLOSED` 命中;去抖 `s_bad` 连续 2 轮(200ms×2≈400ms)后复用 `handle_disconnect_event`(:633)再入——此时 `lost_conn_seen==1` 旁路可疑判据 → 正常释放(GRACEFUL 登记、session_dead、CLOSED、回调全套不变)。双锁存场景(第二枚边沿到来)同样因 flag 已置而直接释放:双锁存=强证据,不再三确认。 + +**去抖期间的状态摆位——保持 ESTABLISHED、session_dead 保持 0,这正是收益所在**: +1. lftpd accept 轮询见 ESTABLISHED → :1305 门通过 → **accept 正常返 fd**(不再依赖 closed-with-data 救回); +2. recv 快路径无状态门槛 → 消费者在 400ms 租约内排空 RX(引擎活则数据续到); +3. 对 n30 的小段 STOR:400ms 租约足以让传输完成 + 客户端正常 FIN(0x07 健康释放)→ **风暴链被"会话正常完成"掐断**; +4. 槽恢复 0x04 的形态(TRAP-NET-16 时代可恢复类):对账① bad=0 → 连接存活,flag 由 relisten/connect 清; +5. 引擎真死:消费者排空后 WOULDBLOCK,对账①释放后得 EOF——EOF 晚 ~400ms,语义不变。 + +**:21 @0ms 快复连为何零误伤**: +1. 健康 FIN 释放:芯片处 CLOSE_WAIT(0x07)(bit3=1 芯片不关 Socket,CLOSE_WAIT 等 MCU 关——D20 注释 :3403),判据要求恰 `0x05/0x00`,0x07 不命中; +2. 芯片静默回收形态 `0x00/0x00`:判据要求 st[0]==0x05,不命中; +3. 真 RST(恰 0x05/0x00)会命中可疑 → 代价仅释放延 ~400ms(对账①两轮),不是错状态;且 @0ms 复连时序已被 pump 窗主导(E7b `:7003` p50=1011ms),400ms 落在窗内不可见; +4. E5 健康 FIN 20/20 样本走 ①②的形态,均在可疑集合之外。 + +**成本**:判据 4 行 + connect 清零 1 行 + 1 条 DBG(~90B Flash),零 RAM。 + +#### T6-④ M5 `sock_reset` drain-until-quiescent 状态机草案(阶段 2) + +替换现三数组 `s_relisten_quarantine/_settle_start/_settle_dead`(net_listener.c:150-208)为 per-socket 相位机;五个重建入口(pump/D10/accept CLOSED 分支/recv EOF/pending③)已全部收敛到 `net_listener_try`,状态机天然单所有者。 + +``` +IDLE ──(settle 登记或 try 进入)──▶ CLOSE + CLOSE: 发一次 CLOSE_SOCKET(TC-NET-204:顺带清已锁存位,保留此动作);t0=now;latch_seen=0;quiet=0 +QUIESCE(每 30ms 重试轮,非阻塞): + 1. RX 裁决(红线): GET_RECV_LEN>0 且芯片 st[0]==OPEN 且本层 CLOSED+auto_relisten + → 让位 accept:返 BUSY,本轮不排空不 close + 否则 → net_drain_chip_rx 排空 + 2. GET_INT_STATUS_SN 读即清: 见 DISCONNECT 位 → latch_seen=1, quiet=0; + 否则若 latch_seen → quiet++ + 3. GET_SOCKET_STATUS: 记 st + 4. 静默成立 := st[0]==CLOSED + && (latch_seen || now-t0 >= LATCH_GRACE_MS) // 等过锁存延迟窗 + && quiet >= QUIET_ROUNDS // 连续两轮无新锁存 + && RX==0 + && now-t0 >= hold_min // GRACEFUL 100 / QUARANTINE 400(沿用) + → OPEN + 5. now-t0 >= RESET_MAX_MS → 强制放行 OPEN(留痕,沿用现兜底) +OPEN: SET_PROTO_TYPE/SET_SOUR_PORT/OPEN/TCP_LISTEN + 成功尾(现 :207-267 不动); + 失败 → D10 30ms 重试,每轮先回 QUIESCE 第 2/3 步轻量复核(1 条 SINT + 1 条状态) +``` + +- 参数建议:`LATCH_GRACE_MS=50`(实测锁存延迟 10~20ms,×2.5 余量)、`QUIET_ROUNDS=2`(2×30ms)、`RESET_MAX_MS=800`(保持"必须 < 2000 放弃时限"规则)、`hold_min` 沿用两级。 +- **诚实边界(来自 T6-① 修正)**:静默不是有界保证——拆链完成可被**新连接自身流量**触发(TRAP-NET-20 窗口 >2s 不可观测),状态机只能静默"已在途的拆链";"未来流量触发的拆链"只有 M3 的电平确认能接住。**M5 与 M3 互补成对,不是替代关系**:M5 消灭"沉降窗内可静默"的绝大多数锁存(E7b 的 2% 硬币),M3 兜住无界残余(风暴态)。 +- RAM:三数组(uint8 + 2×uint32 ≈ 9B/sock)→ 相位 uint8 + t0 uint32 + 打包 flags ≈ 6B/sock,微负,预算友好。 +- 与 T6-② 的闭环:QUIESCE 第 1 步即红线裁决点——"泵与救回互抢"在状态机内单点裁决,五入口不再各自竞速。 + + +--- + +## 2. CH395F 驱动层审计(T2) + +> 审计对象:`Drivers/BSP/CH395F/ch395f.c`(1411 行)/ `ch395f.h`(750 行)、`Src/` 中断与引脚配置、 +> `Drivers/BSP/NET/net_socket.c`(3671 行)与 `net_listener.c`(554 行)中**由驱动层契约决定的部分**。 +> 方法:驱动 API 全量清点 + 中断路径实证 + 全部 Trap 台账通读归类 + git 考古定位根因修复落点。 +> 行号均为本工作树 HEAD(`09d6195`,分支 `task/N34-bsd-selfheal-audit`)实测行号。纯只读,未改任何代码。 +> 参照文档:CH395F_Trap_Records.md(含 2026-10-11 寄存器审计 B1~B10)、 +> 网络Socket监听生命周期设计说明.md、BSD_Socket_API_使用指南.md、§3(主流对照,T3)、 +> §1(net 层盘点,T1)。 + +### 2.1 驱动层 API 面盘点 + +#### 2.1.1 驱动"知道"什么(可回读的芯片事实) + +| 芯片事实 | 驱动 API | 位置 | 语义要点 | +|---|---|---|---| +| Socket 是否打开 + TCP 态 | `ch395f_get_socket_status` | ch395f.c:1356 | **2 字节**输出:`[0]`=`00H`CLOSED/`05H`OPEN,`[1]`=TCP 态;`[1]` 仅在 `[0]`=OPEN 时有意义 | +| 单 Socket 事件 | `ch395f_get_sock_int_status` | ch395f.c:1324 | **读即清**(GET_INT_STATUS_SN) | +| 全局事件汇聚(16 位) | `ch395f_get_glob_int_status_all` | ch395f.c:1305 | **整体读即清**,芯片无按位清除手段 | +| RX 积压字节 | `ch395f_get_recv_len` | ch395f.c:1274 | 低字节在前;可反复查询(手册 5.34) | +| 对端 IP:Port | `ch395f_get_remot_ipp` | ch395f.c:336 | 6 字节,**小端**(与 SET_DES_IP 的点分序相反) | +| PHY/链路 | `ch395f_get_phy_status` | ch395f.c:875 | 返回 `01H`DISCONN…`08H`100M FULL | +| 命令完成 | `ch395f_get_cmd_status` / `ch395f_poll_cmd_status` | ch395f.c:629 / :654 | 后者 `HAL_Delay(20)` **阻塞忙等** | +| SPI 通道健康 | `ch395f_spi_byte_fault` | ch395f.c:265 | 锁存式(复位前不清),仅首条故障记日志 | +| 芯片版本 | `ch395f_get_version` | ch395f.c:310 | 仅 net_init 打印一次(net_socket.c:259),**不参与门控** | +| MAC/IP/掩码/网关 | `ch395f_get_mac_addr` / `ch395f_get_ip_inf` | ch395f.c:819 / :847 | 回读用于自检留痕 | +| ARP 不可达现场 | `ch395f_get_unreach_info` | ch395f.c:1402 | **全仓零调用**(B4) | + +#### 2.1.2 驱动暴露的原语(51 个公共函数,按职责分组) + +| 组 | 函数 | 位置 | +|---|---|---| +| SPI 事务 | `write_cmd` / `write_data` / `read_data`(**公开**)、`spi_byte_fault`;`spi_begin`/`spi_end`/`spi_dma_transfer`/`spi_fault_set` 为 static | ch395f.c:212/229/246/265;:76/:87/:120/:100 | +| 芯片检测与初始化 | `check_exist` / `get_version` / `reset` / `get_cmd_status` / `poll_cmd_status` / `init` | :278/:310/:613/:629/:654/:680 | +| 网络参数 | `set_ip_addr` / `set_gwip_addr` / `set_mask_addr` / `set_mac_addr` / `get_mac_addr` / `get_ip_inf` / `set_phy` / `get_phy_status` / `ping_enable` / `set_dhcp` / `get_dhcp_status` / `set_arp` / `set_ttl` / `get_unreach_info` | :702/:731/:760/:791/:819/:847/:898/:875/:918/:937/:956/:1375/:1389/:1402 | +| 缓冲几何 | `set_recv_buf` / `set_send_buf` / `clear_recv_buf` | :416/:443/:468 | +| TCP 全局参数 | `set_fun_para` / `set_tcp_mss` / `set_retrans_count` / `set_retrans_period` / `set_keepalive_idle` / `_intvl` / `_cnt` / `_enable` | :372/:394/:573/:592/:487/:509/:531/:551 | +| Socket 生命周期 | `set_proto_type` / `set_des_ip` / `set_des_port` / `set_sour_port` / `open_socket` / `close_socket` / `tcp_listen` / `tcp_connect` / `tcp_disconnect` | :980/:1003/:1034/:1059/:1085/:1100/:1115/:1130/:1145 | +| 数据面 | `write_send_buf` / `read_recv_buf` / `get_recv_len` | :1166/:1222/:1274 | +| 状态回读 | `get_glob_int_status_all` / `get_sock_int_status` / `get_socket_status` / `get_remot_ipp` | :1305/:1324/:1356/:336 | + +**驱动持有的状态 = 零。** 全文件仅 4 个 static(ch395f.c:50-56):`s_spi2_dma_done`、`s_spi_fault`、 +两个 DMA 缓冲。没有任何 socket 状态缓存、没有链路态缓存、没有事件缓存——驱动是纯命令转发层。 + +#### 2.1.3 SPI 事务包裹与集中中断读取约定 + +- 每次事务由 static 的 `ch395f_spi_begin()`(CS PB12 SET→RESET)/ `ch395f_spi_end()` 包裹(ch395f.c:76/:87), + 但 `write_cmd/write_data/read_data` 是**公开**的(ch395f.h:301/:308)——任何调用方都能拼一条未包裹的事务。 +- SPI2 分频 **8**(Src/spi.c:77-88,CPOL LOW / CPHA 1EDGE / 软 NSS),PCB 约束,勿改。 +- 集中中断读取约定落在 **net 层**:`net_poll()` 先读 INT# 引脚(net_socket.c:435),低电平才读 + `GET_GLOB_INT_STATUS_ALL`(:441),再按 `1U<<(i+4)` 门控去读该 Socket 的 SINT(:464/:469)。 + 驱动只提供两个裸的读即清原语,**没有任何护栏**。 + +#### 2.1.4 中断路径结论:纯轮询,无 ISR + +- `CH395_INT_Pin`(PE5)在 `Src/gpio.c:76-80` 配为 `GPIO_MODE_INPUT` + `GPIO_NOPULL` → **无 EXTI、无 ISR**。 +- `EXTI9_5_IRQn` 属于 `TP_BUSY_Pin`(PC7,Src/main.h:124-125、gpio.c:170-171、stm32f4xx_it.c:391),与 CH395F 无关。 +- 事件消费节奏 = netTask 主循环 `net_poll()` + `net_process_messages()`(App/task/net_task.c:241-279,约 1ms 一轮)。 + +**判定:现状正确,不应改 EXTI。** 芯片的全局事件寄存器是"整体读即清",ISR 内读寄存器会把事件从 +netTask 手里抢走(正是 B3 事故的形状,见 F-DRV-2)。缺的不是中断,而是**驱动侧的事件分发与缓存**。 + +### 2.2 Trap 根因归类总表 + +台账共 **24 条**(旧编号 01–25,**旧 19 从未存在**,属悬空引用)。根因类别: +**(a)** 芯片真实行为(不可在固件侧根治);**(b)** 固件寄存器值/常量错误;**(c)** 引用错误 +(字节序/偏移/索引/单位);**(d)** 时序误用(顺序/忙等/沉降);**(e)** 设计缺口;**(f)** 判读或台架假象。 + +| Trap | 症状(一句话) | 类别 | 根因修复落点 | 催生自愈补丁 | 根因修好后补丁是否仍必要 | +|---|---|---|---|---|---| +| 01 | Socket 4~7 的 CONNECT 中断不触发 | (d)+(a) | net_socket.c:333-340(net_init 内 7→0 逆序给全部 8 个 Socket 显式配缓冲) | 否 | — | +| 02 | Socket 4~7 发送固定垃圾/接收不落数据(缓冲重叠) | (c) | net_socket.c:334-340 + ch395f.h:136-141(固定 6 块/Socket,用满 48 块) | 否 | — | +| 03 | DHCP 包 xid 偏移错 | (c) | 主机侧测试工具(历史) | 否 | — | +| 04 | UDP 写完不等 SENBUF_FREE → 下次写被静默丢弃 | (a) | net_socket.c:2721-2753(`send_ready` 门控) | 否(属正常流控) | 是 | +| 05 | 自动协商与 RTL8305NBI 不兼容 | (a) | net_task.c:110(强制 100M 全双工) | 否 | 是 | +| 06 | KeepAlive 非 500ms 倍数 → 内部定时器异常 | (a) | net_config.h:61-71 + net_socket.c:288-290 | 否 | 是 | +| 07 | 只 disconnect 不 close → 重开必返 `0x20` | (f)→(a) | 原判"FIN_WAIT_2/RST"已证伪;真因 `FUN_PARA` bit3=1 芯片不自动关 Socket。轮询 **byte0**:net_socket.c:2326-2345 | 间接(催生"close→等 CLOSED→排空"序列) | 是(bit3=1 契约) | +| 08 | RECV 中断无限触发 | (a) | net_socket.c:429(`do{}while(0)` 单批处理) | 否 | 是 | +| 09 | PHY link up 同一时刻 `open_socket` 失败 | (a)+(d) | 根因侧已修:就绪屏障 net_task.c:232 + 启动期等 PHY link net_task.c:118-130 | **是**(D10 层) | **部分**:启动形态已被根因消除;close→reopen 沉降仍是芯片行为(TRAP-NET-20 实证窗口不定长)→ 重试保留,**应用层重复重试可删** | +| 10 | 6 个 API 永远返回 -1(消息队列按值拷贝) | (e) | 错误码改经 `xTaskNotify` 通知值回传 | 否 | — | +| 11 | accept 拒绝 standalone_accept 的 ESTABLISHED | (e) | net_accept_locked(net_socket.c:1281-1342) | 否 | — | +| 12 | FatFs `FR_INVALID_NAME` | 非 CH395F | lftpd_io.c | 否 | — | +| 13 | 分批读后 `recv_len` 归零 | (f) 存疑 | 无(机理未证实,与手册 5.34 及本仓 TC-NET-201 相反) | 否 | **约束已事实作废**(见 F-DRV-11) | +| 14 | close 后 DISCONNECT 滞后锁存,污染下次首读 | (a) | 预清 + 反核实:net_socket.c:2326-2345 / :1137、net_listener.c:262、net_socket.c:1670 | **是** | 是(TRAP-NET-18 实测伪 DISCONNECT 仍投递给下一条连接) | +| 15 | DHCP 成功后 IP 寄存器提交滞后 | (a) | 主机侧判据修正(历史) | 否 | — | +| 16 | TCP Client 目的 IP 字节序写反 → CONNECT TIMEOUT | (c) | net_socket.c:1636-1641 + :2691-2701(改大端),commit `db64e6ac` | 否 | 根因已修;**驱动 API 未封装字节序 → 复发面仍在**(F-DRV-5) | +| 17 | client 重连被上一条连接的残留 DISCONNECT 秒断 | (a) | 预清 + 反核实:net_socket.c:1660-1726 | **是** | 是 | +| 18 | "bit0=1 才是多 Socket 模式" | (f) 伪根因 | net_socket.c:270 回到 `0x08`(commit `d758d5c`);真正起作用的是 TRAP-NET-02 的缓冲分配 | 否 | 驱动侧残留:语义宏未被使用(F-DRV-6) | +| 19 | — | — | **不存在**(悬空引用) | — | — | +| 20 | 单次写 >1KB 写穿发送 FIFO → 数据错乱/卡死 | (c) | ch395f.h:138-141 + net_socket.c:1745/1788/2717(`NET_SEND_CHUNK_MAX`) | 否 | 根因已修;驱动只告警不拦截(F-DRV-7) | +| 21 | 接收缓冲"恰好填满+排空"窗口冻结 | (f) 非缺陷 | 台架自吞数据 + 判读工具误判;保留金丝雀 ch395f.c:1176-1189 与布局解耦 ch395f.h:130-153 | 否 | — | +| 22 | 快速复连丢 CONNECT/DISCONNECT → 监听静默死亡 | (a)(B3 曾自造同类症状,commit `4f2cc8b` 修) | net_reconcile_tick net_socket.c:548-641 | **是**(对账层) | **是**(TRAP-NET-20 裸机台架独立实证芯片丢中断);但命中数应重测 | +| 23 | 上传中止后首笔复连被静默截断(≤2KB) | (a)+(e) | net_listener.c:102-121(排空)+ :149-204(隔离段);登记点 net_socket.c:3430/:3523 | **是**(隔离排渣层) | 是 | +| 24 | MAC 写入顺序违反手册(SET_MAC 须在 INIT 前) | (d) | net_socket.c:292-299(commit `de1b50e`) | 否 | 已根治 | +| 25 | FTP 数据连接三连:丢 CONNECT / accept 竞态自毁缓冲 / 背靠背小段毒化 TX | ①(a) ②(e) ③(a) | ①探针 net_lost_connect_probe net_socket.c:1228-1267 + 幽灵甄别 `last_peer_port`;②closed-with-data accept net_socket.c:1306-1334(**我方 bug,已根治**);③批量发送 lftpd.c:107-155 | **是** | ①③是;②属契约实现,保留 | + +**归类小结**(按定案后的主因计,跨类条目只计一次):(a) 芯片行为 12 条、(c) 引用/数值错误 4 条、 +(f) 判读假象 3 条、(d) 时序误用 2 条、(e) 设计缺口 2 条;另旧 12 非 CH395F、旧 19 不存在。 + +**催生五层自愈补丁的 6 条(09/14/17/22/23/25)中,只有 TRAP-NET-09 的 +"启动形态"和 TRAP-NET-20 的"②accept 竞态"被根因修复消掉;其余全部落在芯片行为上,不可从根上消除。** + +### 2.3 五层自愈 × 驱动层契约 + +| 自愈层 | 参数与位置 | 驱动层是否已有对应能力 | 结论 | +|---|---|---|---| +| `auto_relisten`(监听常驻) | net_types.h:172;置位 net_socket.c:1119 | 无——芯片永不自动回 LISTEN(单连接复用模型),必须软件承诺 | **保留**(模型基石) | +| D10 非阻塞重监听重试 | net_listener.c:77-79(30ms/2s)、:394-432 | 驱动只有**阻塞**的 `poll_cmd_status`(ch395f.c:654)→ 非阻塞重试是在 net 层绕开驱动阻塞原语 | **保留**,但真正的解法是驱动提供非阻塞命令完成查询(F-DRV-8) | +| TRAP-NET-17 周期对账 | net_socket.c:544-546(200ms/去抖 2/冷却 1s)、:548-641 | 驱动无状态缓存 → 对账只能每周期重新发 SPI | **保留**(根因是芯片丢中断);参数与 pump 重复,应收敛(F-DRV-1) | +| N-32 监听器 pump | net_listener.c:437-441(100ms/去抖 2/冷却 1s)、:465-553 | 同上 | **保留**(不变量 II 的落点,不是补丁) | +| TRAP-NET-18 排渣隔离 | net_listener.c:53-56(400/100/800ms)、:102-121、:149-204 | 排空 RX 的**机械**(`get_recv_len`+`read_recv_buf` 循环)住在 net 层;驱动只有裸 `clear_recv_buf`(且产品零调用) | **下沉驱动**(F-DRV-4) | + +### 2.4 驱动层缺口清单(缺什么原语) + +1. **无 socket 状态缓存 / 无权威回读单一来源**:7 处各自手拼 `GET_SOCKET_STATUS` + (net_socket.c:586/1235/1694/2330/3391/3408、net_listener.c:516),三套去抖/冷却参数并存。 +2. **无类型化状态谓词**:`byte0 门控 byte1` 只写在 ch395f.h:592-601 注释里,仍有 3 处只判 `byte1` + (net_socket.c:592/1237/3410)→ B6 类误读未收口。 +3. **无"是否在听"谓词**:`chip_listening` 判据手拼在 net_listener.c:517-518。 +4. **无复合复位原语**(排空 RX → close → 排空事件 → 有界等 CLOSED):net 层两套同构变体。 +5. **无 RX 排空原语**:`net_drain_chip_rx`(net_listener.c:102-121)是纯芯片机械。 +6. **读即清原语无护栏**:驱动裸暴露"整体读即清"的全局寄存器,B3 事故即此 API 形状的产物。 +7. **无非阻塞命令完成查询**:`poll_cmd_status` 用 `HAL_Delay(20)` 忙等,最长 4s。 +8. **无链路态缓存与边沿事件**:PHY_CHANGE/DHCP/UNREACH/IP_CONFLI 四个全局位读清后无人消费(B4)。 +9. **无版本门控**:`get_version` 只打印不判(net_socket.c:259);bit3 需 ≥0x46、bit1 需 ≥0x44(B9)。 +10. **无发送缓冲空闲查询**:SENBUF_FREE 只存在于读即清的 SINT 里 → `send_ready` 镜像无处下沉。 +11. **无 socket 索引边界校验**:51 个函数中 `sock` 一律不校验 0~7。 +12. **无 SPI 总线互斥**:串行化仅靠"只有 netTask 碰驱动"的约定(AGENTS 模块约定)。 +13. **IP 字节序契约未封装**:4 处调用点手拼大端数组,驱动注释还与实测相反。 +14. **无事件分发中枢**:`net_poll` 里的 glob→SINT→handler 分发是 net 层手拼,驱动无 dispatch 表。 + +### 2.5 编号发现 + +**F-DRV-1 · 同一芯片事实被 7 处独立回读、三套去抖参数并存——"单一事实源"缺失是五层膨胀的地基** +证据:`ch395f_get_socket_status` 调用点 net_socket.c:586(对账 200ms/去抖 2/冷却 1s,:544-546)、 +:1235(探针 100ms/去抖 2,`NET_LOST_CONN_STABLE_N`)、:2330(close 等待 500ms)、:3391/:3408(事件留痕)、 +net_listener.c:516(pump 100ms/去抖 2/冷却 1s,:437-441)。驱动 static 变量只有 4 个(ch395f.c:50-56), +无任何缓存。 +影响:同一 socket 的状态在 100ms 窗口内可被读 3~4 次,判读口径与防抖参数各不相同;任何一处口径漂移 +就产生新的"镜像说 A、芯片是 B"窗口(与 T3 的 F-MS-8 同判)。 +裁决:**下沉驱动**——驱动持每 socket 的最近一次回读 + 一个权威刷新入口(每 net_poll 周期一次), +对账/pump/探针/close 共用同一份事实与同一套去抖。 + +**F-DRV-2 · 读即清原语无护栏:B3 事故是驱动 API 形状的直接产物,修复落在 net 层,API 形状未变** +证据:ch395f.c:1305(全局 16 位整体读即清)与 :1324(单 Socket 读即清)裸暴露;B3 修复只在 +net_socket.c:1122-1137 用注释收口("原为遍历 8 个 Socket 全清 + 再读清全局"→ 仅清本 Socket), +git 考古链 `e862009bc`→`319d00e3`→`db64e6acf`(commit `4f2cc8b` 修)。 +影响:同类事故(吞掉其它在用 Socket 的 CONNECT/DISCONNECT 边沿事件)可在新调用点复发; +这正是"看起来像 TRAP-NET-17 的自家 bug"的来源。 +裁决:**下沉驱动**——提供 `ch395f_sock_events(sn)`(只读该 Socket,永不碰全局), +并把"全局寄存器唯一读者 = net_poll"写成驱动侧断言/文档契约。 + +**F-DRV-3 · 无类型化状态回读:`byte0 门控 byte1` 只活在注释里,3 处只判 byte1** +证据:ch395f.h:592-601 注释规定 `[1]` 仅在 `[0]`=OPEN 时有意义;违反口径的判读点 +net_socket.c:592(`st[1]==ESTABLISHED` 未先看 `st[0]`)、:1237(探针同型)、:3410(陈旧 DISCONNECT 甄别同型)。 +TRAP-NET-07 定案更正第 3 条正是把这类读数读成"卡在某个 TCP 态"。 +影响:幽灵态/孤儿态判定的正确性依赖每个调用点都记得这条规则——已三次失守。 +裁决:**下沉驱动**——返回 `{open, tcp_state}` 结构 + `ch395f_sock_is_listening()/is_established()` 谓词, +`open=0` 时 TCP 态返回"无意义"哨兵值。 + +**F-DRV-4 · "close + 排空 RX + 排空事件 + 等 CLOSED"在 net 层有两套变体,是下沉驱动的最佳落点** +证据:net_socket.c:2326-2345(close→循环读状态 + 读 SINT + 读全局→等 CLOSED,上限 500ms)与 +net_listener.c:149-204(排空 RX→close→读 SINT→读全局→读状态→等 `CLOSED/TCP_CLOSED` + 最短等待 +400/100ms,上限 800ms)。语义同构(bit3=1 排空契约),参数漂移,**且只有后者排空 RX**。 +影响:同一芯片契约两处实现;close 路径不排空 RX,依赖"上层先排"的隐含前提。 +裁决:**下沉驱动**——`ch395f_sock_reset(sn, mode, wait_ms)`(mode=异常/正常), +net 层只选 mode。与 T1 的 F-NET-9 同判,签名归 T4 定。 + +**F-DRV-5 · IP 字节序契约未封装,且驱动注释与实测相反(TRAP-NET-14 的复发面仍在)** +证据:TRAP-NET-14 根因是照抄手册"所有 IP 命令低字节在前"的总括,实测 SET_IP/SET_DES_IP 收**点分序**; +修复落在 net 层(net_socket.c:1636-1641、:2691-2701,commit `db64e6ac`)。但驱动侧: +`ch395f_set_des_ip` 头注释完全不写字节序(ch395f.h:479-484),`set_ip_addr/set_gwip/set_mask` +仍写"低字节在前"(ch395f.h:380/:387/:394),而 `get_remot_ipp` 是**小端**(ch395f.h:612)。 +影响:同一驱动内两种字节序、注释还指错方向;4 个调用点各自手拼数组,新调用点极易再犯。 +裁决:**下沉驱动**——驱动内做字节序转换(对外收 `uint32_t` 网络序或明确点分序), +并改正 ch395f.h 三处注释;这是"让 BSD API 更主流"里成本最低、收益最直接的一步。 + +**F-DRV-6 · FUN_PARA 语义宏是死代码,net_init 传裸 `0x08`** +证据:ch395f.h:204-206 定义 `CH395F_FUN_PARA_TCP_SERVER/SOCKET_CLOSE/DISABLE_SEND_OK`, +全仓零引用(grep 仅命中定义处);net_socket.c:270 写 `ch395f_set_fun_para(0x08)`。 +影响:TRAP-NET-15 的"bit0=1 多 Socket 模式"伪根因能存活一年多,正是因为语义没有类型化到宏/枚举层, +只能靠注释与手册对质。 +裁决:**简化**——net_init 改用宏组合(`CH395F_FUN_PARA_SOCKET_CLOSE`),删掉未使用的 +`CH395F_GINT_STAT_SOCK0..3`(net_poll 用 `1U<<(i+4)`,net_socket.c:464)。 + +**F-DRV-7 · 驱动对超限写只告警不拦截——现状正确,但契约必须显式化** +证据:ch395f.c:1183-1189 明确"超出不会被命令拒绝,而是写穿发送缓冲(TRAP-NET-16)→ 只告警不拦截, +分块责任在调用方";分块上限 `NET_SEND_CHUNK_MAX` 在 net 层(net_socket.c:1745,应用于 :1788/:2717)。 +影响:若驱动内自动分块,需在分块间等 SENBUF_FREE,而该事件由 net_poll 集中读清 → 驱动内等待会自锁。 +裁决:**保留**(现状合理);把"单次写 ≤ `CH395F_SEND_BUF_SIZE_BYTES`"从注释升格为 +驱动 README 的显式契约,并把 OVERSIZE 金丝雀计数暴露给上层(当前只打日志)。 + +**F-DRV-8 · 驱动的命令完成等待是阻塞原语:每条 socket 命令至少 20ms、最坏 4s 的 netTask 忙等** +证据:ch395f.c:654-670 `poll_cmd_status` 用 `HAL_Delay(20U)` 忙等(不让出 CPU); +`open_socket`/`close_socket`/`tcp_listen`/`tcp_connect`/`tcp_disconnect` 全部以 4000ms 上限调用它 +(:1091/:1106/:1121/:1136/:1151);DMA 路径另有 1s 忙等(:138-160)。 +影响:D10 的"非阻塞"只覆盖 net 层策略层,驱动层仍是阻塞原语——netTask 停摆指纹 +(`req=0x77`/`mask=0x41`)与 P2b 暂缓的"忙等"问题,真正的根在驱动层。 +裁决:**下沉驱动**——新增非阻塞 `ch395f_cmd_done()`(单次查询,返回 BUSY/终态), +把 open/listen/connect 拆成"发命令"+"查完成"两步;net 层的 D10 重试即可直接复用。 + +**F-DRV-9 · 阻塞重试三处残留,其中两处属应用层——与 D10 非阻塞设计矛盾,构成第 6/7 层自愈** +证据:net_socket.c:1092-1104(`net_listen_locked` 3×`osDelay(500)`)、:1647-1659 +(`net_connect_locked` 同型);应用层 lftpd_inet.c:39-70(5 轮,`vTaskDelay(2000)`)、 +lftpd.c:473-477(3 轮 ×200ms)。 +影响:TRAP-NET-09 的"启动形态"已被就绪屏障 + 启动期等 PHY link 消除(net_task.c:118-130/:232), +应用层重试的原始理由已不存在;保留它只会把恢复时间拉长并制造额外的重建所有者。 +裁决:**简化**——net 层两处并入 D10(P2b);应用层两处**可删**(保留 lftpd 的重试前先确认 +`net_relisten` 语义已覆盖,见 T1 的裁决)。 + +**F-DRV-10 · 无版本门控:`get_version` 只打印不判** +证据:net_socket.c:259 `DBG_INFO("ch395f ver=0x%02X fun_para=0x08", ch395f_get_version())`; +驱动无版本判断(ch395f.c:310)。FUN_PARA bit3 需芯片 ≥0x46、bit1 需 ≥0x44(B9)。 +影响:换料/降版本时 `0x08` 静默失效,而整套排空契约(bit3=1)正是建立在 bit3 生效之上。 +裁决:**简化**——`net_init` 内门控(版本不足则拒绝置 bit3 并留自检记录),一行判定消掉整类静默。 + +**F-DRV-11 · 旧机理的"必须一次读空整个接收缓冲"约束已事实作废,但注释仍在四处约束后人** +证据:ch395f.h:146(旧注释"接收方向需一次性读空整个 Socket 接收缓冲…避免部分读导致剩余数据不可见")、 +net_socket.c:1908/:1969/:3466/:3470。而 `net_recv_locked` 常态按 `min(len, recv_len)` 部分读 +(net_socket.c:1882-1952),TC-NET-604 结案已撤回"部分排空致命",TRAP-NET-18 回归 10/10+5/5 全在部分读下通过。 +影响:注释与代码事实相反;`CH395F_SPI_DMA_BUF_SIZE = 2048+4` 的 sizing 理由(ch395f.h:143-153) +仍挂在一条未证实的机理上。 +裁决:**可删**(注释级)——把旧机理降级为"未证实的历史观察",补一条"部分读→再查 recv_len" +定向 TC 结案;DMA 缓冲 sizing 改以"接收缓冲 2KB"为唯一理由。 + +**F-DRV-12 · 死声明与零调用 API 清单(驱动 API 面虚胖)** +证据:`CH395F_CMD_GET_GLOB_INT_STATUS` 0x29(ch395f.h:36,只有 2 字节版 0x19 有实现)、 +`CH395F_SPI_BEGIN_DELAY_MS`(ch395f.c:42,从未使用)、EEPROM/GPIO/SLEEP 命令 +(ch395f.h:163-167 等)、`CH395F_GINT_STAT_SOCK0..3`(:244-247)、 +`ch395f_tcp_disconnect`(ch395f.c:1145,产品零调用,仅 TC-NET-204 用例 ch395f_test_task.c:783)、 +`ch395f_get_unreach_info`(:1402,全仓零调用)、`ch395f_clear_recv_buf`(:468,仅未注册的 +ch395f_reuse_bench.c:169)。 +影响:API 面越大,"哪个是产品路径"越难判;TRAP-NET-07 的"disconnect→close"错误序列正是从 +一个零调用的 API 出发的。 +裁决:**可删/登记**——删死宏与未实现命令声明;`tcp_disconnect` 保留但注释标注"产品路径禁用"; +`clear_recv_buf` 保留(TRAP-NET-20 已证"缓冲重分配无效",排渣用读空而非 CLEAR_RECV_BUF)。 + +**F-DRV-13 · 链路事件静默:四个全局中断位读清后无人消费,驱动无链路缓存** +证据:net_socket.c:441 读全局后只按 `bit4..bit11` 分派 socket 事件(:464), +bit0~bit3(UNREACH/IP_CONFLI/PHY_CHANGE/DHCP)被丢弃;`ch395f_get_unreach_info` 零调用; +链路只在启动期等一次(net_task.c:118-130)。 +影响:运行期拔网线/链路抖动/IP 冲突/ARP 不可达完全静默,只能靠芯片 TIMEOUT(≈1.6s)间接暴露。 +裁决:**下沉驱动**——驱动持链路态缓存 + 变化计数 + `ch395f_link_up()` 谓词,net 层订阅; +这是 T3 的"PHY/链路变化收在驱动层"结论的落点。 + +**F-DRV-14 · 中断路径实证:纯轮询无 ISR,且不应改为 EXTI** +证据:`Src/gpio.c:76-80` PE5 = `GPIO_MODE_INPUT`/`GPIO_NOPULL`;`EXTI9_5_IRQn` 归 TP_BUSY(PC7, +gpio.c:170-171、stm32f4xx_it.c:391);消费节奏 = net_task.c:241-279 主循环。 +影响:与 AGENTS 模块约定一致;改 EXTI 会让 ISR 与 netTask 争抢"整体读即清"的全局寄存器(F-DRV-2 形状)。 +裁决:**保留**——但把"为什么不用 EXTI"从口头约定升格为驱动 README 的显式理由(当前只有一句约定)。 + +**F-DRV-15 · HAL 全局回调跨模块耦合:CH395F 驱动代管 SPI1 完成标志** +证据:ch395f.c:180-199 的 `HAL_SPI_TxRxCpltCallback`/`HAL_SPI_ErrorCallback` 同时处理 SPI2 与 SPI1, +并 `extern volatile uint8_t g_spi1_dma_done`(:178,定义在 GD5F 驱动里)。 +影响:HAL 全局回调只能有一份,谁定义谁就代管别人;改 CH395F 驱动可能静默破坏 NAND 的 DMA 完成路径。 +裁决:**简化**——收进一个 SPI 适配层(或按实例注册),至少把该耦合写进两个模块的 README。 + +**F-DRV-16 · 单连接复用不是偏离主流;差距在驱动不持事实、不推事件、无复合事务原语** +证据:T3 的 F-MS-2/F-MS-5(ioLibrary 无 accept、CH395 每 Socket 一连接、FreeRTOS+TCP +`REUSE_LISTEN_SOCKET` 同形);本仓驱动 static 变量仅 4 个(ch395f.c:50-56)、无事件分发、 +无复合原语(§2.4 缺口 1/6/7/14)。 +影响:把"要不要改成 per-socket 独立状态机"当成问题是问错了方向——芯片侧本来就是 +每 Socket 一连接、监听与数据通道复用;缺的是驱动把芯片事实**收口**成一份可查询的真相。 +裁决:**保留模型**;按 F-DRV-1/2/3/4/8/13 补驱动原语后,五层里的对账/pump/探针失去存在理由, +隔离排渣下沉为驱动原语(与 T3 的 4.3/4.4 一致)。 + +### 2.6 内聚评估:驱动持缓存 + 权威回读后,reconcile/pump/drain 能否下沉或删除 + +| 机制 | 能否因"驱动持事实"而删 | 理由 | +|---|---|---| +| TRAP-NET-17 对账①② | **不能删,但可大幅收窄** | 根因是芯片丢中断(TRAP-NET-20 裸机台架独立实证)。驱动持缓存后,对账的"回读"部分变成读缓存;"发现失配"仍需存在,只是从"周期 SPI 轮询"降为"每 poll 周期一次刷新 + 比对" | +| N-32 pump | **不能删** | pump 是"单一重建所有者"(不变量 II),是架构解不是补丁;驱动持事实后它的判据读缓存,动作仍在 net 层(策略) | +| D10 重试 | **可降级** | 若驱动提供非阻塞 `cmd_done()` + `sock_reset()` 内联有界等待(F-DRV-4/8),独立 30ms/2s 定时器可退化为事务内重试(T3 同判) | +| TRAP-NET-18 排渣隔离 | **可下沉** | 排空/等待是纯芯片机械;net 层只保留"异常/正常"两档 mode | +| 探针 `net_lost_connect_probe` | **不能删,可去抖简化** | 丢 CONNECT 是芯片行为;驱动持事实后去抖可去掉(读缓存无瞬态噪声),保留"状态已 ESTABLISHED 而本层 LISTENING → 补 accept" | +| `auto_relisten` | **不能删** | 芯片永不自动回 LISTEN,这是模型承诺 | + +**一句话结论**:五层里有四层(对账、pump、探针、resync)存在的直接理由是"驱动不持事实、net 层只能自建镜像"; +把它们删掉的前提不是"根因已修",而是"驱动成为唯一事实源"。TRAP-NET-18 的排渣隔离则相反—— +它的根因(在途数据落 RX)不可修,但它的**实现位置**应当从 net 层下沉到驱动。 + +### 2.7 对 T4 汇总的输入 + +1. 驱动层最该补的三个原语(按收益/成本排序): + ① 类型化状态回读 + 每 socket 缓存(F-DRV-1/3);② `ch395f_sock_reset()` 复合复位(F-DRV-4); + ③ 非阻塞 `ch395f_cmd_done()`(F-DRV-8)。三者都不改芯片行为,只把事实与机械收进驱动。 +2. 立即可做、零风险的清理:F-DRV-6(用宏)、F-DRV-10(版本门控)、F-DRV-11(撤旧"部分读归零"注释约束)、 + F-DRV-12(删死声明)、F-DRV-5 的注释更正。 +3. 需要重测的量化项:B3 修复(`4f2cc8b`)前后各跑 `:21 100ms×50`,统计 `reconcile:` 命中数, + 量化"自家制造的丢中断"占比(与 T1 的 F-NET-8 同一实验,勿重复做)。 +4. 与 T3 的分歧点:T3 的 4.2 第 5 条称"FUN_PARA_TCP_SERVER(多连接模式)已在 net_init 启用"—— + **与代码不符**。net_socket.c:261-270 明确 `bit1=0`(多连接模式不用),实传 `0x08`; + 设计文档的 A′ 出局论证(不支持多监听)成立。T4 汇总时须以代码为准。 + + +--- + +## 3. 主流嵌入式 socket 栈对照研究(T3) + +> 审计组发现文件(mainstream-benchmark)。性质:web 检索 + 一手源码阅读 + 本项目代码对照。 +> 结论先行:**五层自愈补丁不是"芯片怪癖的必然代价",而是"软件镜像 + 纯事件驱动 + 决策不看芯片"这一组合的补偿税**。 +> 主流方案(含同为硬 TCP 栈的 WIZnet 官方库)没有一处维护第二份 socket 状态、也没有一处做周期对账; +> 芯片怪癖(丢中断、close 沉降、RX 残渣)在任何架构下都存在,但主流把它们**内联进事务、收在驱动层、以有界超时兜底**, +> 而不是在 net 层养一组常驻定时器。 + +### 3.1 研究范围与方法 + +- 一手源码:WIZnet ioLibrary_Driver `socket.c`(全文通读)、Zephyr `eth_w5500.c` + `eth_wiznet.c`(全文)、 + FreeRTOS-Plus-TCP `FreeRTOS_Sockets.c`(accept/listen/reuse-socket 路径)、mbed WIZnetInterface + `WIZnetInterface.cpp`(socket_listen/socket_accept/socket_bind 路径)、WIZnet 官方 lwIP 移植 + `w5x00_lwip.c`、PeterBorisenko `spiif.c`(W5500+lwIP+FreeRTOS)。 +- 文档:W5500 官方 TCP 应用笔记、W5500 socket API 文档、CH395 数据手册、FreeRTOS+TCP API 参考、Zephyr 文档。 +- 本项目对照:`net_socket.h` 公共 API 全量、`net_listener.c` 模块头与策略常量、 + `BSD_Socket_API_使用指南.md` 第 3.6 节与第 12.10 节(五层自愈现状表)。 + +### 3.2 各主流方案状态同步机制对照 + +| 方案 | 状态事实源在哪 | 事件 vs 轮询 | 谁拥有状态机 | 监听/accept 语义 | 兜底机制 | +|---|---|---|---|---|---| +| **WIZnet ioLibrary**(W5500/W6100,硬 TCP 栈,与 CH395F 同类) | **芯片 Sn_SR 寄存器,每次调用直接回读**;软件里只有传输记账(`sock_io_mode`/`sock_is_sending`/`sock_remained_size`/`sock_pack_info`),**无 socket 状态镜像** | 调用内阻塞轮询(`while(getSn_CR)`、`while(getSn_SR != …)`);Sn_IR 标志可读可清,INT 引脚电平驱动、可选 | **芯片**(硬件 TCP 状态机);host 端没有状态机 | **没有 accept()**——监听 socket 本身就是连接 socket(与本项目单连接复用同形);重监听 = 应用主循环每轮 `switch(getSn_SR)` 回读后 open/listen | 无需对账(无镜像可对不上);命令完成/状态迁移一律有界轮询 | +| **W5500 + lwIP**(官方 RP2040-HAT-LWIP-C、PeterBorisenko、d-a-v、QNEthernet) | 芯片**不持有 socket 状态**(socket 0 配成 MACRAW,纯网卡);socket/TCP 状态全在 lwIP pcb | 驱动任务 = INT 引脚信号量 + **500 ms 超时兜底**(超时则清 IR + 回读 PHYCFGR 对账链路) | **lwIP**(软件栈单一所有者) | 标准 BSD:listen pcb 常驻,`tcp_accept` 回调交付**新 pcb** | 驱动层超时兜底,netif 层无感 | +| **mbed OS WIZnetInterface**(硬 TCP 栈 + nsapi BSD 门面) | **芯片 Sn_SR**(`is_connected`/`is_closed` 每次回读芯片) | accept 内轮询 `is_connected` 带超时;无状态镜像 | **芯片**;门面只翻译 API | **accept 内联重建监听**:连接继承原芯片 socket,随即 `new_socket()`+bind+listen 换一颗芯片 socket 继续监听(用 8 颗硬件 socket 模拟"监听常驻") | 重建失败处理粗糙(注释"监听已不存在"仍返回 0)——说明内联重建仍需有界重试,但重试在同一事务/模块内 | +| **FreeRTOS+TCP**(纯软件栈) | pcb 内 `eTCPState`,**唯一写者 = IP task** | 每 socket 一个 `xEventGroup`(`eSOCKET_ACCEPT`/`eSOCKET_CONNECT`/…),调用方阻塞在事件上,不轮询 | **IP task(单所有者)** | 默认:listen socket 常驻,accept 生成**子 socket**;`FREERTOS_SO_REUSE_LISTEN_SOCKET` 模式:监听 socket 自身转连接(与本项目同形),回 LISTEN 由栈状态机处理 | 无对账(状态即事实);reuse 模式 `listen()` **同步清空 rx/tx 流并复位状态位** | +| **Zephyr**(原生 net 栈 + WIZnet 驱动) | 驱动 = **MACRAW 网卡**(socket 0),socket 状态全在 Zephyr net 栈;**W5500 硬 socket offload 未实现**(feature request 未落地) | 驱动线程:INT 引脚 GPIO 回调给信号量 + `k_sem_take` **超时兜底**(超时 → 回读 PHYCFGR 更新链路);TX 完成 = SENDOK 信号量 + 超时 | **net 栈**(socket);**驱动**(链路态缓存 `ctx->state.is_up`,边沿通知 `net_eth_carrier_on/off`) | 标准 BSD:listen 常驻、accept 返回新 socket | 事件 + 周期回读兜底,全部收在驱动层 | + +关键来源(均为上表证据出处): + +- ioLibrary `socket.c`(socket/listen/connect/recv/send 全部直接 `getSn_SR` 回读;`socket()` 先 `close()`;`close()` 末尾 `while(getSn_SR != SOCK_CLOSED)`): +- W5500 官方 TCP 应用笔记(每个阶段给出"方法一:Sn_IR 中断标志 / 方法二:Sn_SR 回读"双轨;明确 Sn_IR 会重叠丢失、不推荐纯事件): +- WIZnet socket API 文档("Berkeley API 有 bind()/accept(),WIZnet API 没有"): +- ioLibrary issue 127(listen 后立即来 SYN:`getSn_SR` 观察不到 LISTEN 瞬态——纯回读模型同样要处理瞬态,ioLibrary 的 listen() 直接 close 重来): +- W5500 状态机由应用每轮 `getSn_SR` 驱动的典型参考流程: 与 +- W5500 INT 引脚为电平驱动(事件不可靠的硬件根因之一): +- Zephyr 驱动: 与 (`wiznet_thread_interrupt`:信号量超时 → `update_link_status`;`wiznet_command`:写 Sn_CR 后有界轮询回读) +- Zephyr W5500 硬 socket offload 未实现(feature request):;MACRAW 模式说明: +- WIZnet 官方 lwIP 移植(MACRAW + lwIP 拥有全部 TCP 状态): +- PeterBorisenko W5500+lwIP+FreeRTOS(`spiif.c`:信号量 500 ms 超时 → `spi_if_clr()` 清 IR + `spi_if_check_link_state()` 回读 PHYCFGR 边沿通知 lwIP): +- 同类移植:、 +- mbed WIZnetInterface(`socket_accept` 内联换芯片 socket 重监听): +- FreeRTOS+TCP `FreeRTOS_Sockets.c`(`FreeRTOS_accept` 阻塞在 `eSOCKET_ACCEPT` 事件;`prvAcceptWaitClient` 交付子 socket 或 reuse 模式返回父 socket;reuse 模式 `listen()` 内 `vStreamBufferClear(rx/tx)` + 位复位 + `vTCPStateChange(eTCP_LISTEN)`): +- FreeRTOS accept 文档("默认创建子 socket 处理被接受连接"): +- CH395 数据手册(TCP Server 模式:Socket 持续检测连接事件、每 Socket 同时只允许一条连接;多连接模式使能位;Socket 状态枚举含 SYN_RCVD/TIME_WAIT 等 11 态;SINT_STAT_* 事件位):(镜像 ) + +### 3.3 提炼的设计原则 + +1. **单一事实源(P1)**:连接状态的权威副本只有一份——硬栈芯片在 Sn_SR/芯片状态寄存器,软栈在协议栈 pcb。 + 任何"决策点"(accept 前、recv 前、重监听前)要状态就**当场回读**,而不是查一份可能滞后的镜像。 + ioLibrary 每个 API 内部都在回读;mbed 门面 `is_connected` 也回读。镜像只允许作为**展示缓存**,不得作为决策依据。 +2. **状态机单所有者(P2)**:芯片(硬栈)或协议栈任务(软栈)是唯一写者;上层通过**事件订阅**获知迁移 + (FreeRTOS 每 socket 事件组、Zephyr carrier on/off、lwIP netif link 回调)。本项目 netTask 串行化已满足"单写者", + 缺的是"镜像降级为投影"。 +3. **事件只负责提前唤醒,正确性靠回读 + 有界超时兜底(P3)**:W5500 官方应用笔记明确 Sn_IR 会因处理不及时 + 而重叠丢失、不推荐纯事件;Zephyr/PeterBorisenko 移植全部是"INT 信号量 + 超时后回读对账"。 + 区别在于:**兜底回读收在驱动层、且回读的是芯片事实本身**,所以不存在"对账修正镜像"这种动作。 +4. **监听常驻是 API 层承诺,不是芯片属性(P4)**:芯片单 socket 事实由适配层补齐——子 socket(软栈)、 + 换芯片 socket 内联重监听(mbed)、或栈状态机内联回 LISTEN(FreeRTOS reuse)。**重监听发生在 + accept/close 事务内部**,不是独立周期机制;失败重试也在同一事务/模块内有界进行。 +5. **"复用前清理"属于资源所有者、内联进事务(P5)**:ioLibrary `socket()` 先 `close()`、`close()` 内联等待 + CLOSED;W5300 erratum 内联在 `close()`;FreeRTOS reuse 模式 `listen()` 同步清空 rx/tx 流。 + 芯片残渣/沉降怪癖的主流处理 = 打开/监听事务的前置步骤(有界超时),不是常驻排渣定时器。 +6. **分层边界(P6)**:驱动层独占全部芯片交互 + 芯片侧缓存(链路态、命令完成)+ 怪癖内联处理; + net/栈层只见事件与数据。主流方案中**没有 net 层直接读芯片寄存器做对账**这回事——要么 net 层根本不碰芯片 + (软栈),要么决策即回读(硬栈门面)。 + +### 3.4 映射到本项目:目标架构草案 + +#### 3.4.1 分层职责 + +| 层 | 应持有 | 现状偏差 | +|---|---|---| +| `ch395f` 驱动层 | 全部 SPI 事务;芯片 socket 状态回读(`GET_SOCKET_STATUS_SN` 已有);每 socket 事件读取即清(`GET_INT_STATUS_SN` 已有);全局中断汇聚(现在在 net 层 `net_poll`,应下沉);链路态缓存 + 边沿事件(`GET_PHY_STATUS` + PHY_CHANGE 位,Zephyr 模型);**复合复位原语**(沉降/排渣内联) | 中断汇聚在 net 层;无复合复位原语;沉降/排渣策略在 net 层 | +| net 层(netTask 单所有者) | fd ↔ 芯片 socket 映射、消息队列串行化(保留)、L3 pending recv(保留)、事件分发/回调;**socket 状态 = 按需回读芯片,或事件推送的展示缓存** | `net_sock_state_t` 8 态镜像是决策依据(`net_can_accept`/`net_can_read`/accept/recv 全查镜像)→ 镜像滞后即产生五层补丁 | +| 应用/服务层 | 只管 fd 与数据;监听常驻由 net 层承诺 | 已符合(EOF 不得 close 监听 fd 的约定正确) | + +#### 3.4.2 驱动层应新增/收编的原语 + +1. `ch395f_sock_state(sn)`:`GET_SOCKET_STATUS_SN` 的薄封装(返回芯片 11 态枚举,映射到 net 层语义由 net 层做)。 +2. `ch395f_sock_events(sn)`:读即清每 socket 中断位;`GET_GLOB_INT_STATUS` 汇聚从 `net_poll` 下沉驱动。 +3. `ch395f_sock_reset(sn, mode, timeout_ms)`:**复合原语** = 排空 RX(`CLEAR_RECV_BUF_SN`/分块读空)→ + `TCP_DISCONNECT`/`CLOSE_SOCKET_SN` → 排空中断位 → 有界等待芯片回 CLOSED。把 TRAP-NET-09 沉降窗口与 + TRAP-NET-18 排渣隔离收进这一个同步原语(对应 ioLibrary `close()` 内联等待、FreeRTOS `listen()` 内联清流)。 +4. `ch395f_wait_cmd(sn, cmd, timeout_ms)`:命令完成有界等待(`ch395f_poll_cmd_status` 已有雏形,补齐到所有命令路径)。 +5. ~~多连接模式语义利用~~(**T4 更正,T2 代码实证**):`FUN_PARA_TCP_SERVER`(bit1)**未启用**—— + `net_init` 实传裸 `0x08`、bit1=0(net_socket.c:261-270),设计文档 A′ 出局论证(多连接模式 + 不支持多监听)成立。"芯片自己保证监听常驻"**不成立**,监听常驻只能由软件承诺(auto_relisten); + 重监听应收敛为"reset 原语 + 一次 LISTEN 命令",由 accept/close 事务内联或单一 reconciler 触发。 + +#### 3.4.3 net 层可整体删除/降级的补丁 + +| 现有机制 | 裁决 | 依据(主流对照) | +|---|---|---| +| TRAP-NET-17 周期对账 `net_reconcile_tick`(200 ms + 去抖 + 冷却) | **整体删除** | 无镜像即无对账;ioLibrary/mbed 决策即回读(P1) | +| N-32 监听器 pump `net_listener_pump`(100 ms,"唯一重建入口") | **整体删除**,改为 accept 事务内联重建(mbed 模型)或 close 事务内联回 LISTEN(FreeRTOS reuse 模型) | P4:重监听是事务步骤,不是周期机制 | +| D10 重监听重试(30 ms/2 s 独立定时器) | **降级**:内联重建失败时"下次 accept 再试"或 reset 原语内部有界重试;独立定时器删除 | mbed 内联重建 + 有界超时;TRAP-NET-09 沉降收进 `ch395f_sock_reset` | +| `net_resync_listen`(公共 API,"静默回收"对齐) | **整体删除** | "静默回收"是 CH395 TCP Server 模式**文档行为**(芯片自动回监听),回读即对齐,不需要"仅修 MCU 状态"的 API | +| `net_lost_connect_probe`(丢 CONNECT 中断补 accept) | **降级**:accept 判据改为回读芯片状态(SYN_RCVD/ESTABLISHED 即有连接),丢中断只影响时延不影响正确性 | P3;ioLibrary 观察不到 LISTEN 瞬态也照样靠回读自愈(issue 127 场景) | +| `auto_relisten` 标志 + `handle_disconnect_event`/`handle_timeout_event` 重建路径 | **简化**:断开事务末尾调 `ch395f_sock_reset` + LISTEN(FreeRTOS reuse 回 LISTEN 的等价物);标志位可保留作"该 fd 是监听器"的类型标记 | P4/P5 | +| TRAP-NET-18 沉降/隔离状态机(`net_listener_settle` 400/100/800 ms 三档) | **下沉驱动**:并入 `ch395f_sock_reset(sn, mode, timeout)`;net 层只选 mode(异常/正常) | ioLibrary `close()` 内联等待、W5300 erratum 内联先例 | +| `net_can_accept`/`net_can_read`/`net_listener_armed` 镜像门控 | **保留但改语义**:门控值 = 驱动层事件缓存(readable 由 RECV_OK 事件置位可保留),但"非 ESTABLISHED 一律返回 1"这类镜像补偿逻辑删除——决策点回读芯片后不再需要 | P1 | +| L3 pending recv、消息队列串行化、网络代次号 | **保留**(与主流不冲突:FreeRTOS/Zephyr 同样单栈任务 + 阻塞事件模型) | — | + +#### 3.4.4 必须保留、但应下沉/换形的芯片怪癖 + +| 怪癖 | 任何架构都要处理 | 主流收法 | +|---|---|---| +| 丢中断(CONNECT/DISCONNECT/RECV_OK,INT 电平驱动 + IR 位重叠) | 是(W5500 官方笔记同款警告) | 事件唤醒 + **有界超时回读**,收在驱动层(Zephyr `k_sem_take` 超时、PeterBorisenko 500 ms);net 层不做周期对账 | +| close 沉降窗口(TRAP-NET-09:close 后立即 open 失败) | 是(ioLibrary `close()` 内 `while(Sn_SR != CLOSED)`;listen 竞态 issue 127) | `close()/socket()` 事务内联等待 + 有界超时(驱动原语内部) | +| RX 残渣(TRAP-NET-18:中止后残留 ~2 KB 被下一条连接读走) | 是(软栈对应物 = FreeRTOS reuse `listen()` 清流;硬栈 = 打开前 close/清缓冲) | `ch395f_sock_reset` 前置排空(`CLEAR_RECV_BUF_SN` 命令现成) | +| 芯片自动回收/回监听(现被当作"静默回收"陷阱) | 不是缺陷——bit1=0 下的台架实证芯片行为(run8;⚠ T4 更正:非 Server 模式条款,Server 模式未启用) | 回读对齐后此"陷阱"消失;仅需在文档里把它登记为芯片语义 | +| PHY/链路变化 | 是 | 驱动层缓存 + 边沿事件(Zephyr carrier on/off),net 层订阅;不进对账 | + +#### 3.4.5 结构性根源判定 + +本项目偏离主流的不是"单连接复用"(ioLibrary 无 accept、CH395 每 socket 一连接、FreeRTOS reuse 模式全部同形), +而是三件事的组合:**① net 层维护 8 态镜像并作为决策依据;② 状态迁移只靠中断事件推进;③ 监听重建与事务脱钩**。 +镜像滞后 → 需要周期对账(TRAP-NET-17);事件丢失 → 需要探针与门控补偿;重建脱钩 → 需要 pump/重试/沉降三件套。 +按 P1~P6 重构后,五层中的四层(对账、pump、resync、probe)失去存在理由,一层(沉降/排渣)下沉为驱动原语。 + +### 3.5 编号发现 + +- **F-MS-1** ioLibrary(WIZnet 官方,硬 TCP 栈权威参考)**没有任何 socket 状态镜像**:`socket()/listen()/connect()/recv()/send()` + 每次调用直接回读 Sn_SR/Sn_IR/Sn_RX_RSR;软件状态仅四组传输记账。"周期对账"在该模型中不存在也不必要。 + 证据:socket.c 全文(见第 2 节链接)。 +- **F-MS-2** "监听 socket = 数据通道"不是对 BSD 的偏离,而是硬 TCP 栈芯片的原生主流模型(WIZnet API 明确没有 + accept();CH395 手册"每 Socket 只允许一条连接")。本项目的偏离在**镜像 + 纯事件驱动**,不在单连接复用本身。 +- **F-MS-3** 主流对丢中断的处理范式 = **事件唤醒 + 有界超时回读兜底,且兜底收在驱动/netif 层** + (Zephyr:INT 信号量超时 → 回读 PHYCFGR;PeterBorisenko:500 ms 超时 → 清 IR + 链路对账;W5500 官方笔记: + 事件法与回读法双轨、事件法明示不可靠)。不存在"net 层周期对账修正软件镜像"的做法。 +- **F-MS-4** CH395 的"静默回收"(peer FIN 后芯片自行回监听、不发 DISCONNECT 中断)为**台架实证芯片行为** + (run8,当时 FUN_PARA=0x09 即 bit3=1)。⚠ T4 更正:原文称其为"TCP Server 模式文档行为"有误—— + Server 模式(bit1)未启用(net_socket.c:261-270),该行为是 bit1=0 下的实测现象而非 Server 模式条款。 + 结论不变:`net_resync_listen` 这类"仅修 MCU 状态"的 API 是镜像滞后的直接产物,回读模型下无存在理由。 +- **F-MS-5** FreeRTOS+TCP `FREERTOS_SO_REUSE_LISTEN_SOCKET` 给出与本项目模型同形的**主流参考实现**: + 监听 socket 自身转连接、回 LISTEN 由栈的单一状态机在事务内完成;且 `listen()` **同步清空 rx/tx 流**—— + 这正是 TRAP-NET-18 排渣隔离的主流对应物(一行内联清理,而非 400/100/800 ms 三档状态机)。 +- **F-MS-6** mbed WIZnetInterface 的 accept 模型:连接继承原芯片 socket,**accept 事务内联**在另一颗芯片 socket 上 + bind+listen 重建监听(用芯片 8 socket 兑现"监听常驻"承诺)。其失败处理粗糙(可静默失去监听)说明: + 内联重建仍需有界重试,但重试应长在事务/模块内部,而不是独立周期机制。CH395F 多连接模式已启用,该模型可移植。 +- **F-MS-7** 主流 RTOS 对硬 TCP 栈只有两种姿势:**MACRAW + 原生栈**(状态全在 MCU,Zephyr/lwIP 全部移植如此)或 + **完全 offload**(状态全在芯片,驱动代理 BSD 调用;Zephyr 对 W5500 的 offload 仍是未落地 feature request)。 + "芯片持有状态 + MCU 镜像状态"的混合姿势正是五层补丁的结构性缺口。 +- **F-MS-8** CH395 芯片状态枚举有 11 个 TCP 态(含 SYN_RCVD、FIN_WAIT、TIME_WAIT),net 层 8 态镜像是**有损投影**; + 有损镜像必然产生"镜像说 A、芯片是 B"的窗口,五层补丁全在为这个窗口付税。决策点回读芯片 11 态 + (`GET_SOCKET_STATUS_SN` 单命令、SPI 成本微秒级)即可让窗口归零。 +- **F-MS-9** 纯回读模型同样要处理瞬态(ioLibrary issue 127:listen 后立即 SYN,Sn_SR 观察不到 LISTEN), + 但主流把瞬态处理写成**事务内的有界重试/重来**(ioLibrary listen() 直接 close 重来), + 不需要"连续 2 次失配去抖 + 1 s 冷却"这类镜像世界的防抖装置——去抖与冷却是对账机制的自偿成本。 + +### 3.6 对 T4 汇总的建议输入 + +1. 目标架构一句话:**驱动持有芯片事实并内联处理怪癖(reset 原语),net 层单所有者只做映射/串行/事件分发, + 决策点回读或订阅,监听重建内联进 accept/close 事务**。 +2. 分阶段建议(风险从低到高): + ① 驱动新增 `sock_state` 回读 + `sock_reset` 复合原语(现补丁原参数原样收编,行为不变); + ② `net_can_*`/accept/recv 判据改回读(删 reconcile/pump/probe 的触发面); + ③ 监听重建内联化(删 auto_relisten 定时器路径、D10 独立定时器); + ④ (远期、低优先)评估 mbed 式"换芯片 socket 重监听"。⚠ T4 更正:其前提"多连接模式已启用"不成立 + (bit1=0,net_socket.c:261-270),且多连接模式不支持多监听(A′ 出局论证)——仅列为 V0.2 级评估项。 +3. 回归判据沿用现成探针(快速复连、中止复连首上传),对照 12.10 节生产验证数据即可裁决。 + + +--- + +## 4. 三方汇总裁决与目标架构(T4) + +> 审计对象:BSD Socket API 的「芯片/本层状态对账与重监听自愈」五层机制(auto_relisten / D10 重试 / +> TRAP-NET-17 对账 / N-32 pump / TRAP-NET-18 排渣隔离)及其显式 API(`net_relisten` / `net_resync_listen`)。 +> 方法:三路独立审计汇总——net 层盘点(`docs/网络/N34-自愈审计.md` §1,F-NET-1..14)、 +> CH395F 驱动层审计(`docs/网络/N34-自愈审计.md` §2,F-DRV-1..16)、 +> 主流方案对照(`docs/网络/N34-自愈审计.md` §3,F-MS-1..9,含 T4 更正两处:多连接模式未启用、 +> "静默回收"非 Server 模式条款)。全部证据带 文件:行号,HEAD `09d6195` 实测。 +> 性质:纯审计,未改任何代码。本部分(T4)是裁决与路线,不是实施记录。 + +### 实施状态(2026-10-13 更新) + +> 想先要"为什么一直不稳"的人话版(比喻 + 全景图):先读 +> `docs/网络/N34-自愈审计.md` §0。 + +| 提交 | 内容 | 结果 | +|---|---|---| +| `9ac9879` | P0 止血包(M1+M2+M4-lite) | 台架验证(E7b/E8) | +| `ae17d36` | M3 矛盾态可疑门(风暴态免疫,资产门版) | E9/E9b 全绿 | +| `4c0a0af` | **阶段 0** 零风险清理 9 条 | E10:0/20 + 20/20 + 20/20 + 11/11 | +| `b8c6a3d` | **阶段 1** API 面收口(对账⑤ + 删 3 API + time_sync/lftpd) | E10:全绿回归(详见 §6 E10) | +| `13a4a40` `d346e4e` | KeepAlive 转正(`docs/芯片手册/CH395F.md` §5.56)+ **阶段 1.5** recv 语义主流化 | E11/E12 台架 | +| `f7f1793` `83a7072` | **阶段 2** ch395f 五原语(只加不接线)+ E2/E3/E4 定案 | Phase 15 套件 20/20 | +| `6394853` `f51c083` | **阶段 3** net 层收敛:单一 reconciler + 忙等统一 + 镜像降级 + E-02 收口(`net_drop_conn`) | 终版回归全绿(TRAP-NET-17/-18/-20、E-02 双 PASS、104、对时、integrity 15/15) | +| `123d80a` | E6 插桩 `NET_U15_PROBE`(默认关,零成本) | ✅ E6 已裁决(2026-10-13:命中=0 → 项 4 不立项,U15 观察项) | +| `925058a` | **收口**:lftpd 对端地址修复(`net_getpeername` + ntoa 段序)+ E6 落档 + 全库编号重排 TRAP-NET-01~21 | @test PASS + ftp_abort 10/10 + relisten 20/20 + ci_local 全绿 | + +阶段 0~3 与 E6 均已完成并转正;仅剩延后项 +(relisten 沉降段统一,见 §5.5.2)。阶段 1 实施过程中的一条关键台架发现 +已留痕(§6 E10):**`net_listener_armed` 必须放行 ESTABLISHED**——单连接模型 +的"待 accept"电平,漏放行时 i104(200ms 周期)抢不过对账② 的 ~400ms 去抖而整场 +饿死(18/18 FAIL → 修复后 18/18 PASS,127s→8.3s)。 + +### 4.0 结论先行(直接回答本次审计的问题) + +**问:「芯片/本层状态对账与重监听自愈」可以简化或删除吗?可以内聚到 ch395f 驱动层吗?** + +1. **不能整体删除。** 催生五层自愈的 6 条 Trap(09/14/17/22/23/25)中,只有 2 个被根因修复消掉 + (TRAP-NET-09 的启动形态→就绪屏障;TRAP-NET-20 的②accept 竞态→closed-with-data)。其余全部落在**芯片行为**上: + TRAP-NET-20 的裸机芯片台架(剥离 OS/net/FTP)独立复现"握手完成但 CONNECT 中断被丢"——这是 + "对账不可因固件修 bug 而删"的决定性证据(F-NET-8)。 +2. **用户的假设部分成立,但打不中五层。** 24 条 Trap 根因归类:芯片行为 12、引用/数值错误 4、 + 判读假象 3、时序误用 2、设计缺口 2(F-DRV 总表)。寄存器数值/引用错误确实存在过 + (TRAP-NET-14 字节序、TRAP-NET-02 缓冲重叠、TRAP-NET-16 FIFO 上限、B3 全量读清),但它们**不是**五层自愈的成因—— + B3(全 Socket SINT 读清)只是"自家制造的丢中断"放大器,修复后对账仍必要,只是命中数应下降(可台架量化)。 +3. **但"五层补丁"这个形态不是终态,可以大幅简化并部分内聚。** 结构性根源不是单连接复用模型 + (ioLibrary 无 accept、CH395 每 Socket 一连接、FreeRTOS+TCP reuse 模式全部同形,F-MS-2/F-DRV-16), + 而是三件事的组合:**① 驱动完全不持事实**(ch395f.c 仅 4 个 static,纯命令转发层); + **② net 层维护有损 8 态镜像并作为决策依据**(芯片 11 态→8 态有损投影,F-MS-8); + **③ 同一芯片事实被 7 处独立回读、三套去抖/冷却参数并存**(F-DRV-1)。 + 镜像滞后 → 需要周期对账;事件丢失 → 需要探针与门控补偿;重建脱钩 → 需要 pump/重试/沉降三件套。 +4. **"内聚到 ch395f"对一半。** 该下沉驱动的是**事实与机械**:状态回读+缓存、事件读即清护栏、 + 排渣/沉降/等 CLOSED 复合原语、非阻塞命令完成查询、链路态缓存。 + 该留在 net 层的是**策略与所有权**:监听重建的单一决策者(pump 是 N-32 的架构解,不是补丁, + 且消费者驱动即时重建有 P2a 台架实测支撑,删了会 `task stall`)。 +5. **立即可删的是 API 面,不是机制**:`net_relisten`(footgun,生产零调用)、 + `net_resync_listen`(语义并入对账后删)、`net_can_accept`(iec104 迁移后退役)—— + 这三个"上层手动自愈"入口正是"补丁式代码难维护"的最直观来源。 + +### 4.1 三方裁决的调和 + +T1(net 层视角)判"机制主体保留、API 可删";T3(主流对照视角)判"对账/pump/探针/resync 整体删除"。 +两者不矛盾,差在参照系:**T1 评的是现架构下的必要性,T3 评的是目标架构下的存在理由**。 +统一裁决(T2 的调和,F-DRV 总表): + +> **删除这些机制的前提不是"根因已修",而是"驱动成为唯一事实源"。** +> 需求(丢中断兜底、监听常驻承诺、残渣清理、沉降窗口)是永恒的;形态(五层常驻定时器 + 镜像对账) +> 是可替换的——替换为"驱动持事实 + 决策点读事实 + 重建内联进事务 + 单一 reconciler"。 + +### 4.2 逐机制汇总裁决 + +| 机制 | 位置(证据见 §1/§2/§3) | 汇总裁决 | 依据 | +|---|---|---|---| +| `auto_relisten` | net_types.h:172 | **保留**(模型基石) | 芯片永不自动回 LISTEN,监听常驻是软件承诺(P4) | +| D10 `net_listener_retry_tick` | net_listener.c:394-432 | **短期保留+小修;长期降级** | 小修=补 pump 同型状态门(F-NET-10)+ give-up 文案(F-NET-14);长期=驱动 `cmd_done()` 后内联进事务、删独立定时器(F-DRV-8) | +| TRAP-NET-17 `net_reconcile_tick` | net_socket.c:548-641 | **功能保留,形态重构**(阶段 1 已做①扩展=⑤) | 与 pump 判据合并为**单一 reconciler**、读驱动缓存、一套去抖(F-DRV-1);①扩展覆盖"本层 ESTABLISHED+芯片已回 LISTEN"缺口(F-NET-2)✅ 已实施(bad=5,`b8c6a3d`) | +| N-32 `net_listener_pump` | net_listener.c:465-553 | **并入 `net_reconcile_tick`(阶段 3,2026-10-13)** | 唯一周期重建决策者=不变量 II **不变**,只是与对账合一为单一 100ms tick(形态③④/U11,判据读驱动缓存);T3"改内联"与 P2a 台架实测冲突(删消费者驱动→`[WD] task stall`)故消费者三处仍保留 | +| TRAP-NET-18 排渣隔离 | net_listener.c:102-121/:149-204 | **下沉驱动** | 排空/等 CLOSED 是纯芯片机械;与 net_close_locked:2326-2345 是同一契约两套变体(F-NET-9/F-DRV-4)→ 收口 `ch395f_sock_reset(sn, mode, wait_ms)`,net 层只选 mode | +| `net_relisten` | net_socket.c:3080 | **删除** ✅ 已实施(`b8c6a3d`) | 生产零调用;对健康监听器无条件 open→USE→close,自毁 ~1s(footgun) | +| `net_resync_listen` | net_socket.c:3026 | **删除**(先并入对账①)✅ 已实施(语义=对账⑤) | 语义正确但载体错位:应是自动对账,不是手动 API(F-NET-2/F-MS-4) | +| `net_can_accept` | net_socket.c:1427 | **删除**(先迁 iec104 到 `net_listener_armed`)✅ 已实施 | 过期门控致 :2404 丢 CONNECT 探针从不运行(F-NET-4,与已修的 :7003 探针失效同型)。⚠ 迁移时 **armed 必须补 ESTABLISHED 放行**,否则 i104 整场饿死(§6 E10) | +| `net_lost_connect_probe` | net_socket.c:1228-1267 | **保留**;驱动持事实后去可去抖 | 丢 CONNECT 是芯片行为(裸机实证);去抖/冷却是对账世界的自偿成本(F-MS-9) | +| L3 pending recv + 看门狗 | net_socket.c:2024-2190 | **保留** | 阻塞模型主体,与主流(FreeRTOS 事件组阻塞)同构 | +| `session_dead` / `released_abnormal` / 幽灵甄别 / closed-with-data | net_types.h | **保留** | 芯片行为的必要建模,P1 不变量 I | +| `net_listen_locked` 3×osDelay(500) / `net_close_locked` 500ms 忙等 | net_socket.c:1092-1104/:2326-2345 | **简化**(P2b) | 根在驱动阻塞原语(F-DRV-8);先补 `cmd_done()` 再统一走 D10 | +| 上层每 32 轮盲收(U15) | ui_net_sink.c:552-558、iec104_serv.c:246-253 | **中期下沉** | "RX 非空但 readable=0"并入 reconciler 后上层退役(F-NET-12) | +| time_sync 每会话 close 监听重建 | time_sync.c:182 | **简化(上层)** ✅ 已实施(`b8c6a3d`) | 违反自家"EOF 不得 close 监听 fd"铁律,主动制造 TRAP-NET-09 沉降窗(F-NET-13);改 lftpd 模式同 fd 回 accept。实测连续 6 轮推送正常(E10) | +| lftpd 应用层阻塞重试 | lftpd_inet.c:39-70、lftpd.c:473-477 | **删除/简化** ✅ 已实施 | TRAP-NET-09 启动形态已被就绪屏障消除,重试理由不存在(F-DRV-9)。实施偏差:未走 `net_listener_try` 登记(会把 BOUND/D10 状态语义一并拖入),改单次尝试+失败即 425(E10 登记) | +| 三处全局读清残留 | net_socket.c:1668/:2332、net_listener.c:176 | **收窄**(与驱动护栏合并做) | B3 已证"只读本 Socket SINT"可行;这三处仍在吞别的 Socket 边沿事件(F-NET-7) | + +### 4.3 目标架构(高内聚低耦合的落点) + +| 层 | 应持有 | 现状偏差 | +|---|---|---| +| `ch395f` 驱动 | **唯一事实源**:每 socket 状态缓存 + 类型化回读(`{open, tcp_state}` + `is_listening()/is_established()` 谓词,byte0 门控 byte1 收口);事件护栏 `sock_events(sn)`(永不碰全局寄存器);复合原语 `sock_reset(sn, mode, wait_ms)`(排空 RX→close→排空事件→有界等 CLOSED,收编 TRAP-NET-09/-18 参数);非阻塞 `cmd_done()`;链路态缓存+边沿事件;IP 字节序封装;版本门控 | 驱动零状态(4 个 static);读即清裸暴露;命令原语全阻塞(`HAL_Delay(20)` 忙等、4s 上限);字节序注释与实测相反 | +| net 层(netTask 单所有者) | fd↔socket 映射、消息队列串行化、L3 pending、事件分发;**状态镜像降级为展示投影**;**单一 reconciler**(对账+pump 判据合并、读驱动缓存、一套去抖);重建=`sock_reset`+LISTEN(pump 周期 + 消费者即时,两处入口语义统一) | 8 态镜像是决策依据;7 处手拼回读三套参数;两套排空变体;`net_can_*` 镜像门控 | +| 应用/服务层 | 只管 fd 与数据;监听常驻由 net 层承诺 | 32 轮盲收、time_sync close、lftpd 重试——三层兜底散在应用 | + +一句话:**驱动持有芯片事实并内联处理怪癖,net 层单所有者只做映射/串行/分发且决策读事实, +监听重建收敛为"reset 原语 + LISTEN"由 pump 与事务共同触发,应用层零自愈责任。** + +### 4.4 分阶段路线(风险从低到高;每阶段独立可回退) + +- **阶段 0 · 零行为风险清理**(纯注释/宏/一行判定,可先行提交): + F-DRV-5 字节序注释更正、F-DRV-6 FUN_PARA 用宏、F-DRV-11 撤旧"部分读归零"注释约束、F-DRV-12 删死声明、 + F-NET-11 注释漂移(50ms→100ms)、F-NET-14 give-up 文案、F-DRV-10 版本门控(一行)、 + F-NET-6 U12 LISTEN 失败补 close(一行)、F-NET-10 retry_tick 状态门(一行)。 +- **阶段 1 · API 面收口**(小 diff,行为可测):删 `net_relisten`;iec104 门控迁 `net_listener_armed` + → 退役 `net_can_accept`;对账①扩展覆盖静默回收形态 → 删 `net_resync_listen`; + time_sync 会话收尾不 close;删 lftpd 应用层重试。 +- **阶段 2 · 驱动原语**(新增不删旧,行为不变):类型化回读+每 socket 缓存、`sock_events` 护栏、 + `sock_reset` 复合原语(现补丁参数原样收编)、`cmd_done()` 非阻塞、链路缓存。 +- **阶段 3 · net 层收敛**(核心重构,需完整台架回归):对账+pump 合并单一 reconciler(读缓存、 + 一套去抖)、探针去抖删除、两处忙等改走 `cmd_done`、U15 数据面对账内化 → 上层 32 轮盲收退役。 +- **贯穿验证腿**:B3 修复前后各跑 `:21 100ms×50` 统计 `reconcile:` 命中数,量化"自家制造的丢中断" + 占比(F-NET-8,T1/T2 同一实验勿重复做);阶段 3 回归判据沿用生产验证数据 + (快速复连 :7003 10/10、:21 100ms/0ms/600ms 各 50/50、中止 RST 10/10 + FIN 5/5、SHA-256 全匹配)。 + +### 4.5 红线(本次审计明确"不做"的事) + +1. **不改 EXTI/ISR**:全局事件寄存器整体读即清,ISR 读寄存器会把事件从 netTask 抢走——正是 B3 事故形状(F-DRV-14)。 +2. **不启用多连接模式(FUN_PARA bit1)**:不支持多监听,A′ 出局论证成立;T3 原文误称"已启用"已更正。 +3. **不删消费者驱动即时重建**(accept/recv/pending 三处):P2a 台架实测删除→CLOSED 窗口拉长→`[WD] task stall`。 +4. **不改单连接复用模型**:这是硬 TCP 栈原生主流形态,不是对 BSD 的偏离。 +5. **SPI 分频 8、中断经 `net_poll` 集中读清**:PCB 约束与既有约定不变(护栏是收窄读法,不是改路径)。 +6. 驱动缓存成本每 socket 数字节可忽略,但按平台约束前后跑 `scripts/ram_report.py` / `stack_report.py`。 + +### 4.6 发现索引与后续登记建议 + +- §1~§3 共 **39 条编号发现**:F-NET-1..14(net 层)、F-DRV-1..16(驱动层)、F-MS-1..9(主流对照)。 + 每条带 文件:行号 证据与裁决,后续排期直接按编号引用。 +- 建议登记进 `docs/任务规划/README.md`(**按铁律在 done 时统一处理,任务期不改登记簿**): + 阶段 0 合并为一条 N-x(零风险清理簇);阶段 1 一条(API 收口簇);阶段 2/3 各一条(驱动原语 / net 层收敛, + 后者需完整台架回归);F-NET-2 覆盖缺口建议**单独登记并优先**——它是当前唯一"无自动机制认领"的失同步形态。 +- §1~§3 即审计留痕;销项时结论沉淀进对应模块文档,本合并文档可归档。 + + +--- + +## 5. 实施方案 + +> 依据:`docs/网络/N34-自愈审计.md` §4(裁决与路线)+ §1~§3(F-NET/F-DRV/F-MS 编号引用)。 +> 纪律:**每个阶段的实施以对应实验拿到明确结果为前置门**(映射见 §5.1);实验用实验分支/实验宏, +> 不污染产品路径。所有改动只发生在 `task/N34-bsd-selfheal-audit` 分支。 +> 每阶段收尾三件套:`@build` 0 错 0 警 + `ci_local.py` + 对应台架回归(判据见各阶段)。 + +### 5.1 实验 → 实施阶段 的门禁映射 + +| 实施项 | 前置实验 | 实验要回答的问题 | +|---|---|---| +| 阶段 0(零风险清理) | 无(E0 已佐证) | — | +| **P0 止血包(M1+M2+M4-lite)** | **E7/E7b ✅ 已完成** | 背靠背 STOR 失效循环能否断链(结论:8%→96.7%,簇已断,见 §6 E7b 节) | +| 阶段 1(API 收口 + F-NET-2 缺口修复 + **M3**) | E5 ✅(形态罕见,顺带修) | 静默回收形态在生产路径是否真实可达、检测延迟多大 | +| 阶段 1.5(recv 语义主流化,**待用户确认排期**) | 依赖阶段 1 对账①先落地 | — | +| 阶段 2(驱动原语) | E2、E3、E4 | 回读成本;GET_CMD_STATUS 判别力;reset 序列的正确内部顺序 | +| 阶段 3(net 层收敛) | E1 ✅、E6 | 对账/pump 真实命中频率(已量化);U15 失配频率 | + +### 5.1.5 P0 止血包(E7b 已验证,✅ 已转正 `9ac9879`) + +三处一行级改动(分支已实施并台架验证,**已提交**): + +| # | 改动 | 位置 | 效果(E7b 实测) | +|---|---|---|---| +| M1 | GRACEFUL 沉降无条件化(删 `recv_bytes!=0` 门) | net_socket.c:3424 | 断"被杀会话免沉降→残渣杀下一条"繁殖链 | +| M2 | lftpd 数据等待门控 `p_sock->state` 直读 → `net_listener_armed` | lftpd_inet.c:117 | 镜像门控退役首例;救回窗可用 | +| M4-lite | 删沉降段全局读即清(模块约定违规点) | net_listener.c:178 | 断"沉降热路径搁浅他 Socket 事件"放大器(**必须与 M1 同批**,E7 实证) | + +回归:n30 116/120(残余偶发自愈)+ ftp_regression 11/11 + :7003 50/50 + iec104 18/18 + +TRAP-NET-18 RST/FIN 5/5+5/5 + @test PASS + 0 错 0 警。**E8 深查修正**(见 §6 E8 节与 +盘点 F-NET-17):残余并非稳定 3%,而是**风暴态**——同固件故障率随残渣存量 2%~54% 摆动; +治本 = M3(`0x05/0x00`+年轻连接的电平确认,交对账①去抖)+ M5(drain-until-quiescent 的 +`sock_reset`,沉降/pump 让位 closed-with-data 救回)。M3/M5 属阶段 1/2,非止血包范围。 +**常备回归补挂**:`n30_repro --n 20` 纳入 Ch395F 回归集(本轮 46/50 潜伏的测试面根因); +`ftp_abort_probe.py` 已迁 `test/host/`(2026-10-13,N34 阶段 3 收尾,`rename_ref.py` 全库引用同步)。 + +### 5.2 阶段 0 · 零风险清理(先行,不依赖实验)✅ 已完成(`4c0a0af`,E10 验证) + +> 实施结果与下表逐条对应;提交切分因 0-A 与 0-5 改动在同一文件内交织而 +> **合并为一笔**(8 项,`4c0a0af`),0-3/0-4/0-6 的逻辑改动在提交信息中逐条 +> 列出并有冒烟证据。实施中额外抓出并修复:`CH395F_FUN_PARA_SOCKET_CLOSE` +> 重复定义(首版增量构建 9 条 `#47-D` 警告,已并入既有宏块)。 +> 验证:全量重编 0 错 0 警 + @test PASS + n30 0/20、:21 @0ms 20/20、 +> :7003 20/20、ftp_regression 11/11。 + +| # | 改动 | 位置 | 验证 | +|---|---|---|---| +| 0-1 | 注释漂移:pump "每 50 ms"→100 ms | net_socket.c:3384 | @build | +| 0-2 | give-up 文案改"重试预算耗尽,交 pump 周期接管" | net_listener.c:414-419 | @build + @test 冒烟 | +| 0-3 | U12 对称:TCP_LISTEN 失败补 `ch395f_close_socket` | net_listener.c:227-230、net_socket.c:1112-1114 | @build + @test(TRAP-NET-17 快速复连子集) | +| 0-4 | retry_tick 补状态门(仅 LISTENING/CLOSED 才重试) | net_listener.c:400-412 | @build + @test | +| 0-5 | FUN_PARA 用宏 `CH395F_FUN_PARA_SOCKET_CLOSE` 替代裸 0x08;删 `CH395F_GINT_STAT_SOCK0..3` 死宏 | net_socket.c:270、ch395f.h:244-247 | @build | +| 0-6 | 版本门控:ver<0x46 则拒置 bit3 + 自检留痕 | net_socket.c:257-270 | @build + @test(启动锚点) | +| 0-7 | IP 字节序注释更正(set_ip/gw/mask 实为点分序;get_remot_ipp 小端) | ch395f.h:380/:387/:394/:479-484 | @build | +| 0-8 | 旧"部分读归零"注释降级为"未证实历史观察";DMA 缓冲 sizing 理由改"接收缓冲 2KB" | ch395f.h:143-153、net_socket.c:1908/:1969/:3466/:3470 | @build | +| 0-9 | 删死声明:`CH395F_CMD_GET_GLOB_INT_STATUS`(0x29)、`CH395F_SPI_BEGIN_DELAY_MS`、EEPROM/GPIO/SLEEP 命令宏 | ch395f.h:36/:42/:163-167 | @build | + +提交切分:0-1/0-2/0-7/0-8/0-9 一笔(纯注释/死码);0-3/0-4 一笔(一行逻辑,需 @test);0-5/0-6 一笔(init 路径,需 @test)。 + +### 5.3 阶段 1 · API 面收口(门:E5 结果)✅ 已完成(`b8c6a3d`,E10 验证) + +> 6 项全部实施,两个偏差与一条关键台架发现见**§6 E10**: +> ① `net_listener_armed` 必须补 **ESTABLISHED 放行**(首版漏放行 → iec104 +> 18/18 全 FAIL,修复后 18/18 PASS 且 127s→8.3s);② 第 6 项未走 +> `net_listener_try` 登记(会把 BOUND/D10 语义拖入),改单次尝试+失败即 425。 +> 验证:iec104 18/18、n30 0/50、:21 @0ms 50/50、:7003 50/50、 +> ftp_regression 11/11、Trap23 RST/FIN 5/5+5/5、time_sync 连续 6 轮正常。 + +1. **对账①扩展**(先修缺口再删 API):`net_reconcile_tick` 判据①增一形态—— + 本层 ESTABLISHED 且芯片 `open=1 && TCP=LISTEN` → 按 RESYNC 语义只改本层 + (ESTABLISHED→LISTENING + 转登 `last_peer_port` + 清 `lost_conn_seen` + 取消挂起 recv), + **不发芯片命令**(纪律同 pump③ U11)。位置:net_socket.c:588-590 判据处 + 复用 :789-797 的 resync 分派体。 +2. **删 `net_relisten`**(net_socket.c:3080-3120 + 消息分派 :800-814 + 头文件声明 + 指南同步)。 + 台架 Phase 13 的调用(net_latency_test.c:784/:787/:800)改用 `net_listener_try` 语义 + (经 test 专用内部入口或删该调用——Phase 13 本意是"确认连接已死后的确定性重建",pump 已覆盖)。 +3. **删 `net_resync_listen`**(第 1 步并入后;net_socket.c:3026-3066 + 分派 :774-799 + 声明)。 + net_latency_test.c:783 同步删。 +4. **iec104 门控迁移**:iec104_serv.c:227 `net_can_accept` → `net_listener_armed`(sim 已有实现); + 随后删 `net_can_accept`(net_socket.c:1427)。 +5. **time_sync 会话收尾不 close**:time_sync.c:182 删 `net_close`,回外层循环同 fd `net_accept`(lftpd 样板)。 + ⚠ 需确认 :8888 协议语义(对端每次会话新建 TCP)——E5 顺带验证。 +6. **删 lftpd 应用层阻塞重试**:lftpd_inet.c:39-70(5×2000ms)、lftpd.c:473-477(3×200ms)—— + 就绪屏障已消除 TRAP-NET-09 启动形态;PASV 监听失败改走 `net_listener_try` 登记。 + +验收:`@build` + `ci_local` + 台架回归全套现成脚本(`ftp_regression.py`、`n30_repro.py --n 50`、 +:7003 快速复连、`iec104_master.py`、`time_sync_server.py`)+ `@test` 锚点。 +风险:删 API 是**公开面收缩**——模拟器 `net_socket_sim.c` 同名实现同步删,双端编译验证。 + +### 5.3.5 阶段 1.5 · recv 语义主流化(用户提议 2026-10-12,方向已确认,排期待批) + +对齐 Python/POSIX:**模式归使用者、超时归调用方、API 不强加默认预算**。 + +1. `net_recv()` 默认**无限阻塞**(删 `NET_RECV_TIMEOUT_MS` 30s 默认);唤醒源=数据/EOF/close/ + 网络重建(`CONNRESET`)——会话死亡由对账①在 ~400ms 内判死兜底(**前置:阶段 1 对账①扩展先落地**, + 否则 F-NET-2 形态是真实挂死窗)。 +2. `net_recv_nb()`(现 DONTWAIT 转正)+ `net_setblocking(fd, on)` 模式挂 socket 属性。 +3. 超时=调用方策略:首选 `net_select(r,w,e,timeout_ms)`(已有);`net_recv_to(fd,buf,len,ms)` + 保留为**显式预算**便捷入口(等价 `settimeout`,默认值删除)。 +4. 内部 watchdog 降级为 tripwire:pending 超 60s 只 `[ERR]` 留痕(完成逻辑缺位探针), + **不再伪造 TIMEDOUT 返回值**。 +5. **文档契约(嵌入式特有,Python 没有)**:IWDG `required_mask` 内任务**禁止无限阻塞 recv**, + 必须 select 超时或显式预算——"超时归使用者"对受监控任务是义务不是选项。 +6. 收益:32 轮盲收(U15 上层兜底)随"阻塞为主 + 决策点回读"直接退役(阻塞 recv 的数据检测走 + `GET_RECV_LEN` 回读,对丢 RECV_OK 天然免疫——E1/E5 实证);调用点迁移面=全仓 grep + `net_recv`/`net_recv_to`(time_sync/iec104/ui_net_sink/lftpd 数处),模拟器双端同步。 + +### 5.4 阶段 2 · 驱动原语(门:E2/E3/E4 结果) + +新增到 ch395f.c/.h(无新文件,无需双端注册;模拟器零依赖): + +1. **类型化状态回读 + 每 socket 缓存**(F-DRV-1/3): + ```c + typedef struct { uint8_t open; uint8_t tcp_state; } ch395f_sock_state_t; + ch395f_sock_state_t ch395f_get_sock_state(uint8_t sn); /* 主动回读 */ + ch395f_sock_state_t ch395f_sock_state_cached(uint8_t sn); /* 读缓存 */ + int ch395f_sock_is_listening(uint8_t sn); /* open=1 && TCP=LISTEN */ + ``` + 缓存刷新点唯一:`net_poll` 每轮对在用 socket 各刷一次(成本由 E2 实测背书; + 预计 4 监听 × ~15µs ≪ 1ms 轮询周期)。`open=0` 时 `tcp_state` 返回哨兵 `0xFF`("无意义")。 + 边界校验:sn>7 直接返回 CLOSED+哨兵并记一次 fault 计数。 +2. **事件护栏**(F-DRV-2):`uint8_t ch395f_sock_events(uint8_t sn)`——只读本 Socket SINT(读即清), + 永不触碰全局寄存器;全局寄存器唯一读者写成驱动侧断言(文档契约 + 调试断言)。 + 三处"两级预清"(net_socket.c:1668/:2332、net_listener.c:176)改调本原语(F-NET-7 收窄)。 +3. **复合复位原语**(F-DRV-4,内部顺序由 E4 定案): + ```c + typedef enum { CH395F_RESET_GRACEFUL, CH395F_RESET_QUARANTINE } ch395f_reset_mode_t; + int ch395f_sock_reset_begin(uint8_t sn, ch395f_reset_mode_t mode); + int ch395f_sock_reset_poll(uint8_t sn); /* 0=完成 / 1=进行中 / -1=失败 */ + ``` + ⚠ **必须是非阻塞状态机**(每 net_poll 步进),不得复制 `poll_cmd_status` 式忙等—— + 400/800ms 沉降阻塞 netTask = task stall(P2a 实测)。net 层只选 mode + 读结果。 +4. **非阻塞命令完成查询**(F-DRV-8,形态由 E3 定案):若 GET_CMD_STATUS 对目标命令有判别力 → + `int ch395f_cmd_done(uint8_t sn)`;若无判别力(Trap 记录已证 SET 类无判别力 + RESET_ALL 陈旧 BUSY 残留)→ + **完成判据改用状态回读**(open 完成=byte0 变 05H;listen 完成=TCP=01H;close 完成=byte0=00H)。 + `poll_cmd_status` 保留给内核前/测试路径,产品 socket 路径逐步弃用。 +5. **链路态缓存**(F-DRV-13):`net_poll` 读全局后把 bit0~3 喂驱动 + (`ch395f_glob_event_feed(uint16_t bits)`);驱动持 `link_up` 缓存 + 变化计数; + net 层订阅(链路变化 → 日志 + 可选触发重配)。运行期拔网线不再静默。 + +验收:新增原语先**不接线**(只加不删),`@build` + 单元级板测(ch395f_test_task 加 TC 用例: +状态回读一致性、reset 状态机跑通、cmd_done 语义)+ `@test` 冒烟。RAM 增量 <64B,跑 `ram_report.py` 确认。 + +### 5.5 阶段 3 · net 层收敛(门:E1/E6 结果;需完整台架回归)—— **已实施 2026-10-13**(最终构建 Code=211348,回归全绿) + +1. **单一 reconciler**:`net_reconcile_tick` + `net_listener_pump` 合并为一个 100ms tick, + 判据读**驱动缓存**(不再各自发 SPI),统一一套去抖(连续 2 次)+ 冷却(1s); + 覆盖形态:①本层 ESTABLISHED 而芯片 CLOSED/回收 ②本层 LISTENING 而芯片已握手(补 accept) + ③监听 fd 芯片失位(重建)④本层 CLOSED 而芯片在听(只重同步)。 + 消费者即时重建三处保留(P2a 定案)。 + ✅ **已实施(2026-10-13)**:`net_listener_pump` 删除、并入 `net_reconcile_tick`(`NET_RECONCILE_PERIOD_MS=100`); + 判据读 `ch395f_sock_state_cached`(唯一刷新点=`net_poll` 每轮全量刷新,E2 实测单次回读 18.1µs); + 去抖**分级落地**(原计划"统一连续 2 次"按 E2/E9a 数据细化):②=1 轮 / ③④⑤=2 轮 / ①=4 轮(M3 让位窗 400ms)+ 冷却 1s; + 主循环自愈 tick 由 4 个回到 **3 个**;形态② 命中时的对端端口回读是本 tick 唯一按需 SPI。 +2. **探针去抖删除**(读缓存无瞬态噪声,F-MS-9);`net_lost_connect_probe` 判据改读缓存。 + ✅ **已实施(2026-10-13)**:探针判据读驱动缓存(零 SPI),`NET_LOST_CONN_STABLE_N` 下线; + 两类瞬态结构性消除("刚 listen 未生效"显示 TCP=00 永不命中握手门,E2:LISTEN 可见 ≤10ms 无长尾; + 拆链窗假 ESTABLISHED 由幽灵甄别拦截),`lost_conn_seen` 退化为 M3 可疑电平标志。 +3. **两处忙等统一**:`net_listen_locked` 3×500ms、`net_close_locked` 500ms 改走 + `sock_reset_poll`/`cmd_done` 非阻塞路径(P2b 收口)。 + ✅ **已实施(2026-10-13)**:三处(含 `net_connect_locked` open 重试)统一走 `net_sock_reset_sync` + (驱动 `sock_reset` 原语:读空 RX→CLOSE→排空事件(quiescent)→等 byte0=00H,内置 LISTEN 态**二段 CLOSE** 补发, + E2 补充发现;GRACEFUL 100ms 底 / 600ms 封顶);`net_close_locked` 另加 `ch395f_cmd_done(CLOSE)` 快路径, + 并**不再读清全局中断寄存器**(F-NET-7:全局唯一读者=`net_poll`)。 +4. **U15 内化**(若 E6 证实频率>0):reconciler 增一条"GET_RECV_LEN>0 但 readable=0 → 置 readable" + 低频对账;随后 ui_net_sink.c:552-558、iec104_serv.c:246-253 的 32 轮盲收退役。 + ✅ **E6 已裁决(2026-10-13,§6 E6 结果):命中=0(含压测)→ 不立项**,U15 登记观察项, + 上层盲收保留现状(与 TRAP-NET-20「不留未证明的复杂度」一致;插桩代码留仓宏=0 零成本,备再武装)。 +5. **镜像降级**:`net_sock_state_t` 保留为展示/门控投影,accept/recv/can_* 决策点改读驱动缓存。 + ✅ **已实施(2026-10-13)**:`net_types.h` 注释把 8 态明确声明为"**本层会话投影**,非芯片状态事实源" + (芯片事实源=驱动缓存,刷新点=net_poll);reconciler / 探针 / U11 重同步等按芯片状态决策的点全部改读缓存, + 事件路径(`handle_*_event`)保留**主动 SPI**(缓存刷新在 net_poll 末尾,事件路径上必然滞后)。 + +验收:完整台架回归 = TRAP-NET-17(:7003 快速复连 10/10、:21 100ms/0ms/600ms 各 50/50)、 +TRAP-NET-18(RST 10/10 + FIN 5/5,SHA-256)、TRAP-NET-20(n30_repro 11/11)、E-02(e02_probe.py)、 +104(iec104_master.py)、对时(time_sync_server.py)、`@test` 锚点 + `[STACK]` 水位对比。 +✅ **2026-10-13 全绿**:`@test` PASS;`[STACK]` net peak 1116/3840(70% 余量);TRAP-NET-17 `:7003` 50/50 + +`:21` 100ms/0ms/600ms 各 50/50;TRAP-NET-18 RST 10/10 + FIN 5/5;TRAP-NET-20 n30 11/11;E-02 双 PASS; +iec104 18/18;对时跨会话连续更新;`ftp_regression` 11/11;integrity 15/15 SHA; +`stack_report` netTask 静态链 **1944 B 不变**(链尾改经 `net_listener_retry_tick`→`net_relisten_locked`)。 + +#### 5.5.1 E-02 收口(N34 阶段 3 追加,2026-10-13) + +E-02 的最后一环不是"挂起 recv 不冻结 netTask"(阶段 1.5 已解决),而是**应用放弃会话后单连接监听被挂起对端永久占住**: + +- **新 API `net_drop_conn(sockfd)`**(`NET_MSG_DROP_CONN`):拆会话**保监听**——netTask 侧 + `net_pending_settle_close` 先了结挂起 recv,再 `net_sock_reset_sync(QUARANTINE)`(400ms 底), + 本层收口 CLOSED(不清 fd),`auto_relisten` fd 即时 `net_listener_try`(失败交 D10 + reconciler 形态④); + **幂等**(非 ESTABLISHED/CONNECTING 直接返 0);**EOF 路径不得调**(`auto_relisten` 已接管)。 + timeSync 30s 预算耗尽路径已接入;模拟器(conn≠listen fd)实现为关数据连接。 +- **`net_poll` 每轮步进 `ch395f_sock_reset_poll`**(驱动契约"在 net_poll 步进";IDLE 槽纯 RAM 判零 SPI): + 同步包装 600ms 放弃后复位机不搁浅,对端 FIN 到达即自动走完、槽位可复用。 +- **芯片定案(E-02 台架,2026-10-13)**:CH395F `close` **无 RST 语义**(TRAP-NET-07 定案更正已载)—— + 对**内核存活但不 close 的对端**发 FIN 后芯片停 `FIN_WAIT_2`(实测 0x05/0x06 长尾),监听恢复受限于对端 close; + 对端配合则 `TIME_WAIT` 过渡 ~150ms → reconciler 形态④ relisten → 新连接接入。 + 不配合场景兜底 = 芯片 TIMEOUT/KeepAlive 拆僵尸(E13 定案 30~90s)。详见 `docs/问题记录/CH395F_Trap_Records.md` TRAP-NET-07 补充。 + +#### 5.5.2 relisten 沉降段统一(延后项,登记不做强行收口) + +`net_relisten_locked` 隔离段仍是**手拼**序列(排空 RX + 主动 close + 读本 Socket SINT + 轮询状态 + 30ms 重试轮), +未统一改走 `sock_reset` 原语。判定:它**不是忙等**(每轮短 SPI + 交重试轮询,netTask 无长阻塞), +且 TRAP-NET-18 的沉降等级(GRACEFUL 100ms / QUARANTINE 400ms / 800ms 强制放行 < 2s 时限)是**生产已调优**的时序, +统一改造的回归面覆盖每次断连热路径,收益只是"少一段重复代码"——按 TRAP-NET-20 纪律**延后**, +待下一次触碰该热路径的任务顺路处理(`sock_reset` 原语已具备承接条件:二段 CLOSE、quiescent 排空、等级化最短等待)。 + +### 5.6 横向纪律 + +- **台架锁**:所有烧录/`@test`/探针前后 `task_worktree.py lock acquire/release bench --task N34`。 +- **烧录**:`-Rst -Run` + 回读校验(`stlink_flash.py cmp`),`-CmpFile` 排在采日志之后。 +- **实验代码**:一律实验宏门控(如 `NET_U15_PROBE`),产品构建编译消失;实验结论写进 + `docs/网络/N34-自愈审计.md` §6 后再实施,实验宏代码在对应阶段实施时删除或转正。 +- **文档同步**:每阶段合入后同步 BSD_Socket_API_使用指南、网络Socket监听生命周期设计说明、 + CH395F_Trap_Records(U12 登记)、AGENTS 模块约定(若 API 面变化)——登记簿销项在 done 时统一做。 +- **提交**:每阶段独立提交(标题 ≤100、正文 ≤80/行,`git commit -F` UTF-8 无 BOM 文件); + 不 push(等用户指令)。 + +### 5.7 工作量与风险 + +| 阶段 | 预估 diff | 风险 | 回退 | +|---|---|---|---| +| 0 | ~80 行 | 极低 | revert 单笔提交 | +| 1 | ~250 行(删多于增) | 中(公开 API 收缩,模拟器双端) | 按子项独立提交可逐个 revert | +| 2 | ~350 行(纯新增) | 低(不接线) | revert | +| 3 | ~400 行(删 3 个 tick/忙等) | **高**(核心时序重构) | 独立提交 + 完整回归门禁,失败即 revert | + + +--- + +## 6. 实验方案与记录 + +> 目的:在改任何产品代码之前,用实验把 §5 的关键假设变成**明确结果**。 +> 每个实验有:假设 → 方法 → 判据(PASS/FAIL/数值)→ 它门禁哪个实施决策。 +> 结果一律追加记录在 §6 实验记录节(含原始日志片段与统计),实施提交正文引用实验编号。 +> 台架纪律:`task_worktree.py lock acquire bench --task N34` → 实验 → `lock release bench --task N34 --bench-pass`; +> 烧录必 `-Rst -Run` + 回读校验;串口采集先于烧录(serial-assistant `open→clear→烧录→wait-idle`); +> 日志出现 `[ERR]`/Fault 即本轮作废。 + +### 实验清单 + +#### E0 · 历史串口日志分析(零成本,无需台架独占)✅ 已跑 + +- **假设**:五层机制在生产常态下命中极少(冷路径),日志噪声主要来自 D10 重试循环。 +- **方法**:直接 grep 主工作树 serial-assistant 日志(gen=149 窗口,2026-10-12 19:38→23:05, + 产品构建 fw build Oct 12 2026 19:31:06 suite=NONE,开机后 ~3.5 h 常态运行)。 +- **判据**:各机制命中计数 + 每次恢复的日志行数。 + +#### E1 · 压力下对账/pump/D10 命中量化(台架,不烧录,用现网固件) + +- **假设**:常态下 `reconcile` 命中≈0(E0 已证 3.5h 零命中);快速复连压力下命中才出现, + 且 B3 修复后"自家制造的丢中断"占比应显著低于 TRAP-NET-17 时代。 +- **方法**(设备跑当前产品固件,PC 侧压测): + 1. `n30_repro.py --n 50 --size 1024`(:21 背靠背 STOR); + 2. `ftp_regression.py`(多数据连接会话); + 3. :7003 快速复连循环(connect→立即 close,间隔 100ms×50——新写 `test/host/relisten_stress.py`, + 纯 stdlib,connect 成功即 shutdown(SHUT_RDWR)+close); + 4. `iec104_master.py` 连接/断开 ×10。 + 每轮之间 serial `clear`,跑完 grep `reconcile|pump:|relisten|give up|lost` 计数 + 恢复延迟。 +- **判据**: + - reconcile 命中数 / 50 轮(目标基线:TRAP-NET-17 时代 2/10 死亡 → 现在应 0 死亡); + - `pump: chip not listening` 与 `relisten retry ok` 次数与间隔(量化 D10 恢复窗); + - 端口拒绝窗(connect 被拒的间隔分布)→ 决定阶段 3 后"恢复窗 ~1s 拒绝新连接"是否仍需改善。 +- **门禁**:阶段 3 单一 reconciler 的周期/去抖参数;F-NET-7 收窄优先级。 + +#### E5 · 静默回收形态可达性(F-NET-2 缺口,台架,不烧录) + +- **假设**:对端 FIN 后芯片可静默回收 Socket 回监听且不发 DISCONNECT → + 本层滞留假 ESTABLISHED,新连接被芯片接住但本层无感知(对账①不命中,因芯片态=LISTEN 非 CLOSED)。 +- **方法**:对 :7003(或 :8888)——connect → 发一帧 → 对端立即 FIN(close)→ 等 3s → + 立即再 connect → 发数据 → 观察设备是否响应;同时看串口日志有无 DISCONNECT/reconcile 痕迹。 + 对照组:同样序列但第二次连接前等 10s(看是否被某机制自愈)。 +- **判据**: + - 复连后设备**无响应且无日志** → 缺口坐实(PASS=缺口存在),记录卡死时长; + - 若被自动恢复 → 记录是哪条机制救的(日志)→ 修正 F-NET-2 结论。 +- **门禁**:阶段 1 第 1 步(对账①扩展)的判据设计与优先级;time_sync close 语义确认。 + +#### E2 · 状态回读成本与瞬态窗口(裸机芯片台架,需烧实验固件) + +- **假设**:`GET_SOCKET_STATUS` 单次 SPI 成本为微秒级 → "决策点回读/每 poll 刷新"成本可忽略; + 且"listen 后立即 SYN 观察不到 LISTEN"瞬态窗口有界(ioLibrary issue 127 同形)。 +- **方法**:复活 `test/board/ch395f_reuse_bench.c`(存档态:注册进 uvprojx Test 组 + + main.c USER CODE 2 区调用,实验分支不入库该注册);DWT 周期计数器测 + `ch395f_get_socket_status` 单次耗时 min/avg/max;矩阵中在 LISTEN 命令后 0/10/50/100ms 回读 + 记录首次观察到 TCP=01H 的延迟。配套 `ch395f_reuse_host.py` 打连接。 +- **判据**:单次回读 ≤100µs(预计 ~15µs)→ 每 poll 全量刷新成立; + LISTEN 可见延迟分布 → 决定 reconciler 去抖是否可减为 1 次。 +- **门禁**:阶段 2 缓存刷新策略;阶段 3 去抖简化。 + +#### E3 · GET_CMD_STATUS 判别力矩阵(裸机芯片台架,同场烧录) + +- **假设**:GET_CMD_STATUS 对 OPEN/LISTEN/CLOSE 有判别力;对 SET 类无判别力 + (Trap 记录已证 SET 类 + RESET_ALL 陈旧 BUSY 残留)。 +- **方法**:同 E2 实验固件:对每条命令(OPEN_SOCKET/TCP_LISTEN/CLOSE_SOCKET/SET_SOUR_PORT/ + SET_PROTO_TYPE)发命令后以 ~1ms 节奏轮询 cmd status,记录"变 BUSY→终态"延迟与假阳性 + (命令未发时读到的残留态)。 +- **判据**:某命令判别力成立(发后 ≤N ms 内变终态、无残留假阳性)→ `ch395f_cmd_done` 可用该判据; + 不成立 → 完成判据改用状态回读(`docs/网络/N34-自愈审计.md` §5.4 第 4 项的回退方案)。 +- **门禁**:阶段 2 第 4 项形态。 + +#### E4 · sock_reset 内部顺序 A/B(裸机芯片台架 + 生产路径复现) + +- **假设**:TRAP-NET-18 排渣只需"排空 RX(读空)→ close → 排空事件 → 等 CLOSED", + `CLEAR_RECV_BUF` 命令可不用(TRAP-NET-20 已证缓冲重分配无效);GRACEFUL 与 QUARANTINE 差异仅在 + 是否排 RX + 最短等待。 +- **方法**:E2 实验固件加变体:close 前 (a)读空 RX (b)CLEAR_RECV_BUF (c)什么都不做, + 随后复监听 + 立即上传,校验载荷完整性(SHA/图案);生产路径侧用 `n30_repro.py` 变体 + (RST 中止 → 立即复连 STOR)对照现固件行为。 +- **判据**:(a) 与现实现等价(10/10 载荷完整)、(b) 不等价则记录差异 → 定案 `sock_reset` 内部序列。 +- **门禁**:阶段 2 第 3 项实现细节。 + +#### E6 · U15 失配频率(生产路径插桩,需烧实验固件) + +- **假设**:`RX 非空但 readable=0`(RECV_OK 中断丢失的数据面形态)在生产真实发生, + 上层 32 轮盲收是必要兜底而非保险。 +- **方法**:实验宏 `NET_U15_PROBE` 门控:net_poll 每 32 轮对在用 fd 发一条 GET_RECV_LEN, + 若 >0 且 !readable → 计数 + 低频日志(每小时 ≤1 条防刷屏);跑 24h 常态 + E1 压测。 +- **判据**:命中 >0 → 阶段 3 第 4 项(U15 内化)立项;命中 =0(含压测)→ 上层盲收保留现状、 + U15 登记观察项。 +- **门禁**:阶段 3 第 4 项做不做。 + +### 实验顺序与成本 + +| 顺序 | 实验 | 需烧录 | 预估耗时 | +|---|---|---|---| +| 1 | E0 | 否 | ✅ 已完成 | +| 2 | E1 | 否(现网固件) | ~40 min(含写 relisten_stress.py) | +| 3 | E5 | 否 | ~20 min | +| 4 | E2+E3+E4 | 是(一次实验固件全收) | ~2 h(含注册/烧录/矩阵跑完/还原) | +| 5 | E6 | 是(插桩固件) | 烧录 20 min + 24h 挂机(期间可做阶段 0) | + +E1/E5 不烧录、风险最低,先跑;E2/E3/E4 一次实验固件合并跑(实验分支,注册不入库); +E6 挂机期间并行实施阶段 0。 + +--- + +### 实验记录 + +#### E0 结果(2026-10-12 23:05,gen=149 窗口 19:38→23:05,产品构建,常态运行 ~3.5 h) + +命中计数(grep 大小写不敏感,SimpleMatch): + +| 模式 | 命中 | 说明 | +|---|---|---| +| `reconcile` | **0** | TRAP-NET-17 对账整窗口零命中(冷路径坐实) | +| `chip not listening` | 2 | 均为 `sock2 pump: chip not listening (0x00/0x00, was state=0) -> relisten`(pump 情形②) | +| `relisten` | 36 | 其中 ~24 条是 `relisten quarantine/settle (sock=0x00 tcp=0x00)` **重试循环逐轮留痕**(30ms 节奏),其余为 `auto-relisten ok`/`relisten retry ok` 成功尾 | +| `settle` / `quarantine` | 12 / 14 | sock1(GRACEFUL)与 sock2(QUARANTINE)各若干次会话收尾 | +| `give up` / `resync` / `lost connect` | 0 | 无放弃、无探针触发 | + +恢复形态(原始片段归纳):会话结束 → settle/quarantine 窗口内每 30ms 一条留痕 +(sock2 QUARANTINE ≈15 条 ≈450ms;sock1 GRACEFUL ≈4 条 ≈120ms)→ `auto-relisten ok` + +`relisten retry ok`。**每次恢复产生 5~16 行日志**——D10 重试循环的逐轮留痕是主要日志噪声源。 + +结论: +1. 常态下对账零命中 → 阶段 3"单一 reconciler 读缓存"不会增加热路径成本;E1 压测补压力面。 +2. 沉降/隔离与 D10 重试是**每次会话收尾都走的热路径** → `sock_reset` 复合原语(阶段 2)收益直接。 +3. 日志留痕策略需随阶段 2 收口(重试循环只留首末 + 计数,勿逐轮)。 + +#### E1 结果(2026-10-12 23:09–23:16,现网产品固件 fw=Oct 12 19:31:06,net 代码=HEAD) + +压测腿与结果: + +| 腿 | 结果 | 延迟分布 | +|---|---|---| +| :7003 复连 100ms×50 | **50/50 成功,0 拒连** | p50=**1020ms** min=2ms max=1049ms | +| :7003 复连 0ms hold50×20 | 20/20 | p50=527ms | +| `n30_repro --n 50`(背靠背 STOR) | **4/50 成功(46 次 425)** | — | +| `n30_repro --n 10`(复跑确认) | **1/10 成功(9 次 425)** | — | +| `ftp_regression`(有间隔) | 11/11 | 正常 | + +串口命中(leg1 窗口):`reconcile`=1、`pump: chip not listening`=61、`auto-relisten ok`=70、 +`give up`=0、`listen lost`=1、`lost-CONNECT self-accept`=1。 + +**发现 A(热路径时序,量化"恢复窗")**:正常断连的完整链 = `DISCONNECTED`(芯片 0x05/0x00, +bit3=1 契约)→ 释放路径**只置事实不重建**(N-32 定案)→ 等 pump(实测 115–900ms)→ 首次 +`net_listener_try` 失败 → 30ms 后 D10 重试成功(~75ms)。**每次会话收尾产生 ~0.3–1s 监听空窗**; +空窗内 SYN 被静默丢弃(无 RST),客户端 1s RTO 重传 → :7003 复连 p50≈1020ms。 +→ 直接支撑"重建内联进释放事务"(mbed/FreeRTOS reuse 模型)与 `sock_reset` 原语; +也解释 TRAP-NET-17 时代"恢复窗拒绝新连接"的口径。 + +**发现 B(P0,新):背靠背数据连接失效循环,现固件可复现 9/10**: +失败签名(sock4=FTP 数据监听):数据连接 accept 后 **~20ms** 被**上一条连接的陈旧 DISCONNECT** +判死(`DISCONNECTED, releasing (chip st=0x05/0x00)`——芯片 TCP=0x00 非 ESTABLISHED, +**通过 D20 陈旧甄别**)→ 被杀会话 `recv_bytes=0` → **不登记 GRACEFUL 沉降**(门=收过数据)→ +立即 relisten → 下一条连接仍落在残渣投递窗内 → 再被杀——**自持循环**。`closed-with-data accept` +偶发救回(窗口内仅 2 次)但 lftpd 数据门控(`lftpd_inet_accept_timeout` 直读 `p_sock->state` 镜像) +错过窗口 → 5s 超时 → 425。 +对照台账:TRAP-NET-20 定案回归 = `ftp_regression`(有间隔)11/11,**n30_repro(背靠背)不在终版回归集**; +Trap 记录证据通则节自认"历史台架计数日志未入库不可复核"——本次 9/10 是**首个入库可复核的背靠背数据**。 +初步归因指向:G1 GRACEFUL 沉降门 `recv_bytes≠0` 对"被陈旧事件杀死的零数据会话"失效(新缺口, +建议登记 F-NET-15);G2 三处全局读清残留(F-NET-7)与事件护栏缺失(F-DRV-2)是残渣投递的供给面。 +→ 根因深挖已派 net-auditor(代码级),修复属阶段 0/1 范围(一行级沉降门修正候选)+ 阶段 2 护栏。 + +#### E5 结果(2026-10-12 23:2x,:7003 connect→send 1B→FIN→3s→复连 ×10) + +20 次 FIN **全部产生 `DISCONNECTED` 事件(20/20)**;pump 重建 10 次;`reconcile`=1。 +**"静默回收"形态(本层 ESTABLISHED + 芯片回 LISTEN + 无 DISCONNECT)在 E1+E5 累计 ~110 次 +连接/断开中零复现**。 +结论:F-NET-2 缺口在代码上真实(无任何机制认领该形态),但触发在本环境罕见 +(run8 台架曾实证,非日常路径)→ 阶段 1 顺带修(对账①扩展),不单独立急项。 + +#### E7 / E7b 结果(2026-10-12 23:36–23:5x,M1+M2 ± M4-lite 实验固件,台架锁 N34) + +**实验改动**(分支实验性,未提交): +- M1 `net_socket.c:3424` GRACEFUL 沉降无条件化(删 `recv_bytes!=0` 门) +- M2 `lftpd_inet.c:117` 镜像门控 → `net_listener_armed` +- M4-lite `net_listener.c:178` 删沉降段全局读即清(E7 中途追加,见下) + +**E7(M1+M2)**:`@build` 0/0(Code=209488);`@test` PASS(戳 23:35:58,回读无差异)。 + +| 测试 | 基线(HEAD 固件) | E7(M1+M2) | +|---|---|---| +| n30 ×50 | 4/50 | 37/50(尾部 13 连败**持续簇**) | +| n30 ×20 立即复跑 | — | **20/20 全败**(簇自持且跨轮持续) | +| ftp_regression | 11/11 | 11/11 | +| :7003 100ms×50 | 50/50 p50=1020ms | 50/50 p50=1022ms(M1 的 +100ms 沉降被 SYN-RTO 窗掩盖) | + +**E7 中途根因(新发现 F-NET-16)**:失败簇期间 `disarmed`=0(M2 从未早退,425=lftpd 满 5s 超时)、 +`closed-with-data` 38 次**全部迟到**、控制通道 sock2 也被陈旧 DISCONNECT 杀。代码定位: +`net_listener.c:178` 沉降段每 30ms 重试一轮 `GET_GLOB_INT_STATUS_ALL`(全局读即清)—— +M1 使沉降成为每次断连热路径后,**每次关闭产生 ~4 次全局读清**,按 B3 机制搁浅其它 Socket 的 +CONNECT/RECV_OK(net_poll 门控焊死)→ 数据连接事件永不到达 lftpd → 满 5s 超时。 +该行同时违反 AGENTS 模块约定「所有 CH395F 中断在 net_poll 内集中读取并清除」。 +**教训:M1 单独上会放大 F-NET-7 供给面——M1 与 M4-lite 必须同批**(net-auditor 的 M1+M2 组合 +建议需修正为 M1+M2+M4-lite)。 + +**E7b(M1+M2+M4-lite)**:`@build` 0/0(Code=209484,−4B);`@test` PASS(回读无差异; +注:固件戳未变=戳所在 TU 未重编,镜像身份以 Code 尺寸变化+回读无差异为准)。 + +| 测试 | E7b 结果 | +|---|---| +| n30 ×50 第 1 轮 | **47/50**(#18-20 偶发,**自愈无簇**) | +| n30 ×20 立即复跑 | **20/20** | +| n30 ×50 第 2 轮 | **49/50**(#47 偶发) | +| ftp_regression | 11/11 | +| :7003 100ms×50 | 50/50 p50=1011ms | +| iec104_master | 18/18 PASS | +| TRAP-NET-18 RST×5 / FIN×5(`test/host/ftp_abort_probe.py --check-abort`) | 5/5(450)/ 5/5(226) | + +**裁决**:M1+M2+M4-lite 把背靠背 STOR 从 8% 拉到 **96.7%(116/120)**,残余 ~3% 偶发 += 原始"滞后 DISCONNECT 杀新连接"形态(D20 结构性失明,M3 域)——**自愈型、无簇、无数据损坏**。 +自持循环已断,P0 降级为 P2 尾巴。 +**遗留**:① M3(D20 年轻连接电平确认,并入阶段 1 对账①扩展)治本剩余偶发; +② `n30_repro` 纳入常备回归集(本轮 4 连败的测试面根因);③ `ftp_abort_probe.py` 落在 +`MDK-ARM/` 目录(应迁 `test/host/`,与"脚本在库"证据通则一致); +④ 设备现跑实验固件,转正或回退由用户裁决。 + +#### E8 残余偶发深度排查(2026-10-13 00:06–00:2x,提交 9ac9879 后固件,台架锁 N34) + +**背景**:E7b 报"残余 ~3%"后,TRAP-NET-18 abort 探针(RST×5+FIN×5)再跑 n30 → **27/50** +(两个持续簇 #6-25、#44-50)——"3%"不是稳定值,故障率随设备状态在 2%~54% 间摆动。 + +**实验矩阵(同固件、同负载,仅设备状态不同)**: + +| 设备状态 | n30 结果 | 簇形态 | +|---|---|---| +| 烧录后全新启动(E7b) | 3/50、1/50、0/20 | 偶发、自愈 | +| abort 探针 + 多轮压测累积后 | **27/50** | 两个持续簇 | +| 仅 ST-Link 复位(不重烧) | **6/50** | 两小簇(#37-38、#47-50) | + +→ **状态依赖确证**:故障率 ∝ 芯片侧残留(OPEN 卡死 socket + 粘滞 DISCONNECT 锁存)存量。 + +**风暴签名(污染态,00:07:14.5–15.2 窗口,全部在 0.75s 内)**: +``` +closed-with-data accept (drain pending) ← 救回:RX 有数据,accept 返 fd 给 lftpd +settle ×2 ← pump 的沉降插进来 +standalone accept ← 下一条 SYN 进卡死芯片 socket +DISCONNECTED, releasing (chip st=0x05/0x00) ← accept 后 11ms 被杀 +closed-with-data accept (drain pending) ← 又救回……循环 ×3 +pump → settle ×4 → auto-relisten ok ← 800ms 强制放行才出簇 +→ 5.6s 后 accept timeout (425) +``` + +**机制链(三个叠加缺陷,登记 F-NET-17)**: +1. **沉降自再生锁存**:沉降段 `net_listener.c:174` 的 `close_socket` 在芯片已 CLOSED + 后再 close 仍会(经拆链收尾)重新锁存 DISCONNECT;`:176` 的 SINT 读清发生在 + close 后**同一轮**,而锁存在 close 后 ~10-20ms 才落(TRAP-NET-12 实证)→ 沉降最后一轮 + 读清后残留一枚"迟到锁存",relisten 完成后 11ms 杀死首条连接。30ms 重试粒度 vs + 10-20ms 锁存延迟 = 每次重建掷一次硬币(E7b 的 ~2% 偶发即此)。 +2. **泵与救回互抢**:沉降段 `:172` `net_drain_chip_rx` 排空的正是 closed-with-data + 救回(`net_socket.c:1317`)依赖的 RX 数据;pump 沉降与 lftpd 救回在同一 socket 上 + 竞速——救回 accept 成功返回(`:1352`)但 lftpd 拿到的是**被杀会话/被排空半份的数据** + → 短读 → 425。风暴期 31 次 accept timeout 与 38 次救回并存即此矛盾的直接证据。 +3. **芯片 OPEN 卡死放大**:被杀会话芯片侧仍 OPEN(0x05),新 SYN 被芯片接进卡死 + socket(TCP 槽仍 0x00)→ net 层 accept → 又被下一枚锁存杀 → 风暴自持,直到 + 800ms 强制放行 + close 真正落拆链才出簇。 + +**对 M3/M5 设计的修正(比 T5 版更锐)**: +- **M3 判据升级**:杀事件时刻 `chip=0x05/0x00`(socket OPEN + TCP 槽 0x00)与 + "连接年轻(<1s)"组合 = **逻辑矛盾态**(活连接应 0x04)→ 不释放,交对账①电平 + 去抖二次确认。该判据在风暴窗口 100% 命中、健康路径 0 误伤(健康断连芯片为 + 0x00/0x00 或 0x07)。 +- **M5 语义升级**:`sock_reset` 必须 **drain-until-quiescent**(close 后轮询 SINT + 直到 DISCONNECT 锁存出现并读清、连续两轮无新锁存才 open+listen),固定 100ms + 窗在锁存延迟抖动下必输硬币。 +- **新增红线**:沉降/pump 重建前**必须让位**于 closed-with-data 救回(RX 有数据 + = 消费者资产,先交还 accept 路径,排空后才准 reset)——泵与救回必须单一所有者, + 归 `sock_reset` 内联裁决。 + +#### E9 结果(2026-10-13 00:30–00:47,M3 原型三轮迭代,台架锁 N34) + +**T6 先修正了 E8 的机制表述**(详见盘点「F-NET-17 补充」):close 已 CLOSED socket +**不产生**锁存(TC-NET-204 模式 C 证伪强形式);锁存生成者=拆链完成事件(滞后+粘滞); +"沉降末轮读清不保证静默"仍成立,经"拆链滞后于末轮读清"+"剩余两处全局读清搁浅"两路径。 +M3 因此从"可选加固"升格为**结构性必需**(静默可被新连接自身流量打破=无界窗)。 + +**M3 判据三轮迭代(每轮都是台架数据说话)**: + +| 轮 | 判据 | n30(预置残渣后) | Trap23 探针 | :21 @0ms×50 | +|---|---|---|---|---| +| R1 | `0x05/0x00 + flag==0` | **0/50** ✓ | **0/5+0/5** ✗ | — | +| R2 | +`recv_bytes==0` | **0/50** ✓ | 5/5+5/5 ✓ | **46/50**(4 refused,p90=1540)✗ | +| R3 | +`芯片 RX>0` | **0/50** ✓ | 5/5+5/5 ✓ | **50/50** ✓ | + +- **R1→R2 教训**:已读数据的会话(探针控制通道)遇 0x05/0x00 多为真死(RST/槽污染), + 延迟它实测 **~1.2s**(对账 1s 冷却,非 T6 估的 400ms)打破应用窗口。 +- **R2→R3 语义修正**:延迟保护的本质=**"芯片 RX 有在途数据"=不可毁的消费者资产** + (closed-with-data 救回同源红线)。@0ms 快复连(连上即关零数据)RX 空 → 立即释放, + 误杀也无数据损失,后到数据由救回接住。 +- 最终判据四条件:`st=0x05/0x00 && lost_conn_seen==0 && recv_bytes==0 && RX>0`; + 前置补 `handle_connect_event` 清 `lost_conn_seen`(两处,T6 发现的必要前置); + 对账①**零改动**认领(E9 实测 71 suspect → 全部正确接管/释放)。 + +**E9b 全套回归(R3 固件,Code=209552,0 错 0 警,@test PASS 回读无差异)**: +n30 ×50(预置后)**0/50** + ×20 **0/20**;ftp_regression 11/11;:21 @0ms×50 **50/50**; +:7003 50/50 p50=1007/p90=1037(回到 E7b 水平);iec104 18/18;Trap23 RST/FIN 5/5+5/5。 + +**遗留观察(非 E9 阻塞,已归因)**::21 @0ms×50 窗口有 `[WD] task stall`(多任务 miss) +——该窗 M3 suspect **零触发**(资产门正确让位),归因于 :21 暴力用例本身的断连 churn +(40 DISCONNECTED + 157 settle 行/20s,E7b 时代已有 MISS start 前例);列入阶段 2 +`sock_reset` 沉降内联后的复验项。B10 的 :21 守护口径(50/50)通过;stall 是否先于 +M1 存在无基线可考(历史日志未入库,又一"脚本在库、日志未入库"实例)。 + +**裁决**:M3(R3 资产门版)具备转正条件;风暴态免疫在"探针预置残渣"最恶劣前置下实证。 + +#### E10 结果(2026-10-13 05:40–06:50,阶段 0 + 阶段 1 转正验证,台架锁 N34) + +**范围**:M3 转正提交(`ae17d36`)+ 阶段 0 九项(`4c0a0af`)+ 阶段 1 六项 +(`b8c6a3d`,16 文件 +193/-371)。三笔均 `@build` 0 错 0 警 + `@test` PASS +(回读无差异)。 + +**阶段 0 验证**(Code=209580):全量重编 0 错 0 警(含强制重编依赖 TU 以暴露 +头文件改动引入的警告——首次增量构建 9 条 `#47-D` 宏重定义警告即由此抓出: +`CH395F_FUN_PARA_SOCKET_CLOSE` 与既有宏块重复定义,已并入原宏块); +冒烟 n30 0/20、:21 @0ms 20/20、:7003 20/20、ftp_regression 11/11。 + +**阶段 1 首版台架失败与根因(本记录的核心留痕)**: + +| 项 | 首版 | 根因 | 修复后 | +|---|---|---|---| +| iec104_master | **18/18 全 FAIL**(127s 跑满超时) | `net_listener_armed` 首版只放行 LISTENING / CLOSED+残留,**漏了 ESTABLISHED** | 18/18 PASS,**8.3s** | +| n30 / :21 / :7003 / ftp / Trap23 | 全绿 | 未受影响(见下) | 全绿 | + +**机制**(为什么只有 i104 中招,是一条非显然的坑): + +- 单连接模型下"连接已到、应用未 accept"的电平就是 **ESTABLISHED**(CONNECT + 处理把监听 fd 自身转数据通道)——这正是旧 `net_can_accept` 唯一放行的状态。 +- 漏放行后能否活下来纯看**消费者任务周期与对账②去抖赛跑**: + ui_net_sink 10ms 周期 → `net_accept_locked` 内自带 `net_poll` 通常先处理 + CONNECT 并在同一次调用里返回 fd(探针/在途 accept 抢先); + **i104 200ms 周期抢不过对账② 的 ~400ms 去抖** → 对账② standalone accept + 先转 ESTABLISHED → armed=0 → 应用整场不 accept、芯片 RX 数据无人读。 +- 设备日志证据链:`sock1 standalone accept`(06:24:23)→ 之后整场无 + `i104: accepted`,直到对端 FIN 才由 `closed-with-data accept` 救回 + (`i104: accepted fd=1` 06:26:30)。期间主站 I 帧全部无响应。 +- **修复语义**:armed = 旧 `net_can_accept` 的 ESTABLISHED 放行态 ∪ LISTENING + ∪ CLOSED+残留。此态 accept 必返 fd,一次调用即离开分支,**无 RPC 风暴风险** + (P2a 的 task stall 约束针对 CLOSED 无残留态,不适用)。 + +**M3 无辜旁证(顺带实证)**:同一窗口 `sock1 suspect DISCONNECT on live conn +(chip 0x05/0x00), defer to reconcile` 命中 → 对账① 1.2s 后接管 → +`closed-with-data accept (drain pending)` 把**未被 accept 的 RX 数据**救回 +——正是 M3 资产门的设计意图(芯片 RX 有在途数据 = 不可毁的消费者资产)。 + +**阶段 1 全绿回归(修复版,Code=209580,@test PASS)**: +iec104 **18/18**(8.3s,较基线 127s 大幅缩短:连接即刻被 accept); +n30 ×50 **0/50**;:21 @0ms×50 **50/50** p50=512;:7003 100ms×50 **50/50** +p50=517;ftp_regression **11/11**;Trap23 RST **5/5** + FIN **5/5**。 + +**time_sync 第 5 项(会话收尾不 close)实测**:连续 6 次推送成功(RTC 逐次 +更新),每轮 `standalone accept → DISCONNECTED, releasing → relisten settle → +auto-relisten ok → 下一轮 accept`(周期 ~4s = PC 侧 2s 宽限 + 2s 间隔), +零 `[ERR]`。契约变化已同步 `test/host/time_sync_server.py` 注释:板子不再先 +关,PC 侧 2s 宽限后自行关闭。⚠ 注意:若 PC 进程被**强杀**(非正常关闭), +芯片侧会走 `[ERR] sock0 TIMEOUT` + QUARANTINE 沉降(400ms)再重建——本轮 +首次验证即因强杀验证进程复现一次,属异常收尾,非正常路径。 + +**阶段 1 与 §5 的两处偏差(诚实登记)**: + +1. 第 6 项原计划"PASV 监听失败改走 `net_listener_try` 登记",实施时判定该路 + 径会把 armed/BOUND/D10 状态语义一并拖入(需给 `net_relisten_locked` 补 + `auto_relisten`、给 armed 补 BOUND 放行、改 0-4 状态门),代价与一个罕见 + 故障不匹配 → 改为**单次尝试 + 失败即 425**(主流 FTP 服务器 passive + listen 失败亦如此,客户端自带传输重试语义),并删除 `cmd_pasv` 的 + 3×200ms 重试。 +2. 第 2/3 项原计划拆分两笔提交,实施时因 0-A 与 0-5 的改动在同一文件内交织 + → 阶段 0 合并为一笔(0-3/0-4/0-6 的逻辑改动在提交信息中逐条列出并有 + 冒烟证据),阶段 1 一笔。判据与验证不受影响。 + +**编译面验证(阶段 1 附带)**:`net_latency_test.c` 的 Phase 13 清理位于 +`#ifdef ENABLE_PHASE13_TESTS` 内、产品构建不编译 → 临时开 CH395F 套件 + +Phase13 宏全量重编 **0 错 0 警**(Code=216632)后还原宏再重编产品镜像, +确认死代码无语法/符号残留。 + +**收尾门禁(`ci_local.py`,退出码 0)**:L0 pre-commit 钩子 **32/32 通过 +(0 失败 0 跳过)**、L0b 提交信息 gitlint **10 条 0 失败**、L0.5 主机侧单测 +**15 runner / 951 断言全过**。过程中修掉两处真实收尾问题(各一笔提交): + +1. **time_sync 单测契约残留**(`b3ef728`):阶段 1 第 5 项删掉 + `time_sync_handle_conn` 的 `net_close` 后,4 条主机侧断言仍要求"处理完关闭 + 连接"(TC-TS-020e/023b/024b/027c)→ 改为 `s_close_calls == 0` 并补契约变更 + 注释(任务级关闭由 `conn != ls` 分支负责,TC-TS-040e 仍覆盖)。80/80 PASSED。 +2. **分支级 RAM 台账漂移**(`bfbe24e`):本分支自身代码增长(net_listener.c 等) + 从未刷新 RAM 文档受管区 → mem-numbers / mem-budget 在分支上长期失败 + (任何 App/*.c 提交都会撞),此前靠 `--no-verify` 绕过。用项目工具 + `check_mem_numbers.py --write` + `ram_baseline.py --write` 同步:ER_IROM1 + 227,824→228,172 B、Flash 合计 228,928→229,276 B;**RAM 两区未变** + (RW_IRAM1 97,864 B/74.7%、RW_CCM 57,376 B/87.9%),印证阶段 0/1 未动 RAM。 + 此后本分支常规提交不再需要 `--no-verify`。 + +#### E11 结果(2026-10-13 07:01–07:18,CH395F 服务器侧 KeepAlive,临时实验固件) + +**问题**:`CMD_SET_KEEPALIVE_SN`(手册 §5.56)——芯片在"IDLE 无收发 → 探测 +CNT 次无 ACK"后产生 SINT_STAT_TIM_OUT / DISCONNECT。怀疑本仓没有利用它。 + +**静态证据(怀疑成立)**: + +| 事实 | 证据 | +|---|---| +| 参数配了:idle 60s / intvl 5s / cnt 3(均 500 倍数、IDLE>INTVL) | `net_config.h:61-70`,`net_socket.c:298-302`(INIT_CH395 之前) | +| 开关**全仓仅一处**,且在 TCP **客户端**分支 `CONNECTING` | `net_socket.c:3257` | +| `App/` 目录**零个 `net_connect()`** → 生产无任何 socket 会走到该行 | grep 全仓 | +| 手册明确:TCP **服务器**应在 `SINT_STAT_CONNECT` 后开启 | `docs/芯片手册/CH395F.md` §5.56(:815-821) | +| 服务器侧调用 2026-10-12 曾加后**撤回**,理由仅覆盖"物理链路失效";注释自认"链路在、对端静默"未覆盖且**无法注入** | `net_socket.c:3223-3229` | +| 唯一能发现该缺口的 Phase 11 测试**默认关闭** | `test/board/ch395f_test.h`(`//#define ENABLE_PHASE11_TESTS`) | + +**注入手段的两次修正(方法学留痕)**: + +1. **Windows 防火墙入站 DROP **无效****:连接建立后再加规则,对该已建立连接 + 不生效(ALE 层在连接建立时判定)。正对照实测:装规则后 KEEPLIVE 回显**照常 + 收到** → `VERDICT: injection INEFFECTIVE`(`e11_inject_check.py`)。这也解释了 + 首轮 235s 窗口无 TIMEOUT(对端一直在 ACK,并非 keepalive 失效)。 +2. **有效注入 = 禁用 PC 网卡**:中间有交换机(判别依据:ESTABLISHED 会话下 + 禁用 PC 网卡 12s,设备**零网络事件** → 设备侧 PHY 链路保持)。此时对端不可达、 + 设备链路在、且不产生 FIN/RST —— 正是"链路在、对端静默"。 + +**实测(实验固件:`handle_connect_event` LISTENING 分支加 +`ch395f_set_keepalive_enable(sock,1)` + 调用点 DBG 留痕;TCP :7003)**: + +| 试验 | 配置/注入 | 结果 | +|---|---|---| +| T2 正常空闲存活 | 生产参数 60/5/3,对端存活但静默 90s | KEEPLIVE 回显正常、**无 TIMEOUT** → 不误杀空闲会话 ✓ | +| T4 定标(短参数) | 6s/2s/2,禁用 PC 网卡 40s | 留痕 `keepalive enabled (idle=6000 intvl=2000 cnt=2)`;**T+5.1s** 报 `[ERR] sock3 TIMEOUT` → 参数按配置缩放,芯片默认值(20/15/9)假设**被否** ✓ | +| T5 生产参数 | 60/5/3,禁用 PC 网卡 90s | 留痕 `idle=60000 intvl=5000 cnt=3`;**T+30.0s** 报 `[ERR] sock3 TIMEOUT` → QUARANTINE → relisten → **新连接成功恢复** ✓ | + +**结论**: + +- KeepAlive 能力**真实可用**,且本层**已有现成消费者**(`net_poll` 派发 + TIM_OUT → `handle_timeout_event` QUARANTINE → pump 重建)——打开开关即可闭环。 +- 生产参数下实测判死 **≈30.0s**(早于手册公式 75s;芯片定时语义与手册公式 + 不一致,**以实测值为准**)。短参数实测 ≈5.1s,证明参数生效且可按需调优。 +- 静默但**存活**的空闲会话不被误杀(T2)——因为对端会 ACK 探测包。 +- 2026-10-12 撤回的盲区(链路在、对端静默)**正是本机制覆盖的场景**;当年 + "+2s 即报 TIM_OUT"只覆盖了物理断链。 +- **已采纳转正(2026-10-13,用户批准)**:在 `handle_connect_event` 的 LISTENING + 分支加回一行 `ch395f_set_keepalive_enable(sock, 1)`(手册规定位置),并重新打开 + Phase 11 测试纳入回归;参数是否调优(如缩短 IDLE)由实测判死窗口需求决定。 +- ⚠ 台架副作用:本轮反复实验后设备落入当时称为"残渣风暴"的状态(详见 E12)。 + **该状态已由 E13 重新定性为僵尸会话,30~90s 自动清除,无需冷启动。** +- **产品路径验证(2026-10-13 10:32,转正提交后产品固件)**::7003 连接 → + 禁用 PC 网卡 51s → `[ERR] sock3 TIMEOUT`(**禁用后 17s**)→ QUARANTINE → + pump → `auto-relisten ok` → 网卡恢复后新连接 PASS(KEEPLIVE 回显正常) + → 转正的一行在**产品固件**上闭环生效 ✓ + +#### E12 结果(2026-10-13 07:18–08:05,SPI 缓存区 TSL 时序 A/B,临时实验固件) + +**问题**:手册 §7.4——SPI/并口模式下读写收发缓存区命令**只需**在"长度与数据 +之间"留 TSL(读 0.4us / 写 0.2us),其余字节间无需间隔;**非缓存区命令**另有 +TSC=0.6us(命令→数据)、TSD=0.3us(数据字节间)。本仓是否满足? + +**静态证据(没考虑过,且缓存区路径零间隔)**: + +| 事实 | 证据 | +|---|---| +| 全仓无任何 TSL 相关代码/注释(grep 只命中手册) | — | +| 两个缓存区函数把 `[cmd, sock, lenL, lenH, data...]` 打包进**同一 DMA 缓冲、一次连续突发**(CS 全程拉低)→ 长度→数据**间隔 = 0** | `ch395f.c` `write_send_buf` / `read_recv_buf`(原实现) | +| 其余命令走逐字节 `HAL_SPI_TransmitReceive(...,1,...)` → TSC/TSD 有 µs 级余量 ✓ | 同上 | +| 量化:SPI2 分频 8 @APB1 42MHz = **5.25MHz**(单字节 1.52us);TSL 0.4us ≈ 2.1 个 SCK 周期 | `Src/spi.c:84` | + +**A/B 设计**:A = 现状(零间隔单突发);B = 头/数据两段传输 + 段间显式忙等 +(`CH395F_TSL_LOOPS=40` ≈1.0–1.4us,另叠加两段间 HAL/DMA 重配开销;CS 全程拉低, +帧形与手册图一致)。B 代价:代码 +28B(Code 209580→209608)、写路径多一次 +memcpy;**吞吐无回退**(A 40×128KB 51.8s vs B 47.9s)。 + +**字节级校验(`test/host/ftp_integrity_test.py`:STOR→RETR 全量 SHA-256 比对, +差异自动二次回读归因)**: + +| 臂 | 样本 | 结果 | +|---|---|---| +| A(零间隔) | 260 轮(20×64KB + 240×128KB ≈32MB/方向) | **1 次不一致**(A2 轮,细节未留档,A3/A4/A5 未复现) | +| B(两段+TSL) | 200 轮(40+100+60 ×128KB ≈25.6MB/方向) | **0 次不一致** | + +→ **未观察到 TSL 造成的可复现数据错位**;两臂均接近"零错位"。 + +**连接级 n30 ×50(每次运行前重烧录 = MCU 复位 + RESET_ALL)**: + +| 臂 | 各轮失败数 | 有失败的轮次 | +|---|---|---| +| A | 8, 22, 0, 0, 12, 0, 0, 39 | 4/8 | +| B | 0, 0, 0, 0, 0, 47 | 1/6 | + +**污染控制与关键反证**:序列末恢复产品镜像后再跑 n30 ×20 = **16/20 失败**, +日志签名 = PASV 数据连接被芯片接受后立即关闭(`sock4 st=0x00/0x00`)+ 控制连接 +TIMEOUT → 即 E8 记录的**残渣风暴态**,且**跨烧录存活**(重烧 + @test PASS 后依旧) +→ 与 E8"普通复位后仍 12%"一致。A 39/50 与 B 47/50 **两臂都出现**灾难轮 +→ 该差异**不能归因于 TSL**,E12 的连接级对比**被此状态噪声吞没,无结论**。 +> ⚠ **"跨烧录存活"口径已被 E13(2026-10-13 10:55)推翻**:板上 CH395F 的 +> RSTI 脚接 MCU NRST(`docs/硬件平台/V0.0_网表详细分析报告.md` :174 +> "RSTI←NRST")→ 每次 ST-LINK 烧录/复位都会**硬件复位芯片**;该状态实为 +> **僵尸会话**(E13),锤击几分钟内即再生,"跨烧录存活"是再生造成的错觉。 + +**结论(诚实口径)**: + +1. 现状驱动**确属规格偏离**(零 TSL 间隔,按"间隔"语义解读),但**未测得实际危害**: + 460 轮/≈58MB 双向字节级校验仅 1 次未复现异常,且两臂无差异。 +2. 依 TRAP-NET-20 铁律「不留未证明的复杂度」:**当前不建议改产品路径**,而是 + **登记该偏离 + 本轮证据**;B 变体作为**待用补丁**保留(若将来出现芯片批次 + 差异、提高 SCK、或不明数据错位,可直接采用——它已证明无吞吐/正确性回退)。 +3. **方法学教训(E13 修正版,后续时序/连接类实验必读)**:本台架的**僵尸会话 + 态**(曾称"芯片残渣风暴")必须在实验前后受控:由"对端静默死亡"(网卡禁用/ + 拔线/PC 崩溃)触发,芯片 TIMEOUT(有在途 TX ≈30s)/ keepalive(纯空闲 + ≈75-90s)自动清除;锤击会在几分钟内再生僵尸 → 同一镜像重烧后仍可 + 0/50..47/50 摆动、跨臂污染使 A/B 失效。**正确控制 = 下一臂开跑前静置 + ≥90s 让僵尸清窗**,不是断电冷启动(RSTI←NRST,烧录本身就是芯片硬件复位)。 + +#### E13 结果(2026-10-13 10:24–10:59,风暴态根因=僵尸会话:确定性复现,产品固件+网卡抖动注入) + +**假设**:E8/E12 记录的"残渣风暴态"不是跨烧录存活的芯片内部残渣,而是 +**僵尸会话**(对端静默死亡 → 芯片侧 ESTABLISHED 未清)+ 单连接监听被僵尸占用 + +PASV 重监听被垂死僵尸 accept 的竞态——且可由网卡抖动**确定性复现**。 + +**推翻"跨烧录存活"的硬件事实**:板上 CH395F 的 **RSTI 脚接 MCU NRST** +(`docs/硬件平台/V0.0_网表详细分析报告.md` :174 "RSTI←NRST";手册 +`docs/芯片手册/CH395F.md` :935 亦载"通过 RSTI 引脚拉低控制复位") +→ 每次 ST-LINK 烧录/复位(`-Rst -Run`)都会**硬件复位芯片**。 +"风暴态跨烧录存活"与接线事实矛盾——存活的不是芯片内部状态。 + +**排除 4 元组复用假设**(`test/host/pasv_probe.py`,本轮新增):5 轮干净 STOR +打印每轮 PASV 端口 + PC 源端口 → PASV 端口**递增**(49157→49161)、PC 源端口 +随机变化 → "芯片复用 PASV 端口 + TIME_WAIT 4 元组冲突"假设**被否**。 + +**确定性复现(100%,产品固件,零插桩)**: +`ftp_integrity_test.py --rounds 40 --size 131072` 正常跑到第 21 轮(前 20 轮 +全 OK)→ **禁用 PC 网卡 8s**(10:55:01,打断传输中会话)→ 恢复网卡 → +`n30_repro.py` 首连 :21 **ConnectionRefused**。 + +**因果链全程(串口日志)**: + +| 时刻 | 事件 | 解读 | +|---|---|---| +| 10:55:01 | 网卡禁用:PC 内核直接丢弃全部 socket,**不发任何 FIN/RST** | 芯片毫不知情 → sock2/sock4 成 **ESTABLISHED 僵尸**;设备侧 30s 内零网络事件 | +| 10:55:30 | `[ERR][NET] sock2 TIMEOUT` | 芯片**重传耗尽**(设备有未 ACK 的在途 TX)→ 僵尸清除,**+30s** | +| 10:55:31 | `sock2 pump: chip not listening (0x05/0x00) -> relisten` → `auto-relisten ok` | 本层 pump 接住 → 监听复活 | +| 10:55:43 | 新连接 accept → PASV → `sock4 standalone accept` → **20ms** 后 `suspect DISCONNECT on live conn (chip 0x05/0x00), defer to reconcile` | **PASV 竞态**:sock4 的僵尸(被打断轮遗留)未清,新监听器的 accept 为垂死僵尸开枪 → lftpd 5s accept 窗空转 → 425 | +| ~10:58 | n30 ×3 全 OK | sock4 僵尸被 TIMEOUT/reconcile 清除 → 自愈完成 | + +**结论**: + +1. **风暴态根因 = 僵尸会话**,不是芯片残渣:对端静默死亡(网卡禁用/拔线/PC + 崩溃/进程被杀)是注入源;上午 E11 反复网卡抖动 + E12 460 轮锤击 + = 完美风暴生成器。失败率随时间摆动(E8 的 2%~54%)= 僵尸生成/清除的节奏。 +2. **"跨烧录存活"是错觉**:重烧后芯片干净,但锤击几分钟内再生半死会话。 + E8/E12 的"需断电冷启动"口径作废(见上方修正标记)。 +3. **现有自愈链端到端有效**(② keepalive 转正 + pump + 对账① 的合力, + 本轮首次在同一场景全链验证): + - 有在途 TX 的僵尸:芯片 TIMEOUT(重传耗尽)≈30s; + - 纯空闲僵尸:keepalive 60/5/3(E11 T5 实测 30s、产品验证 17s); + - 本层 pump/对账接住事件 → relisten → 端口恢复。 +4. **剩余缺口(阶段 2 收口)**:PASV 重监听**不预清**数据 Socket——僵尸未清 + 就在同一 Socket 上开新监听 → accept 为垂死僵尸开枪 → 5s 窗。阶段 2 + `sock_reset`(open 前 close+等 CLOSED)直接消除该竞态。 +5. **e02_probe listener=FAIL 重解读**(非阶段 1.5 回归):探针的挂起客户端 + socket 在 PC 侧仍打开(脚本在第二次连接尝试之后才 close),PC **内核**会对 + 芯片 keepalive 探测自动 ACK → 僵尸会话"活着" → 单连接监听被占、拒新连接是 + **正确行为**;探针预期("预算耗尽 → 设备拆连接")与阶段 1-5"EOF 不 close" + 设计冲突。真实缺口 = **应用层无法主动拆掉自己放弃的会话** → 也是阶段 2 + `sock_reset` 的职责(应用侧形态)。 +6. 顺带登记(显示层,低):`[FTP] connection received from 100.1.168.192:21` + ——对端地址显示为设备自身 IP 的字节序反转 + 端口 21,疑似 lftpd 日志把 + local 地址当 peer 地址打印(不影响功能,未立项)。 + +**工具与留痕**:`test/host/pasv_probe.py`(新增);pktmon 抓包留存 +`%TEMP%\n30_cap.etl`(3.9MB,含网卡抖动窗口,设备 IP 过滤)。 + +#### E2 结果(2026-10-13 11:43–11:50,bench 实验固件 Code=213412,跑两遍,台架锁 N34) + +**固件**:复活 `test/board/ch395f_reuse_bench.c`(N30_CHIP_BENCH 门控,内核前裸机; +旧 N-30 定案矩阵由 `s_bench_legacy=0` 停泊保留);注册 uvprojx Test 组 + `Src/main.c` +USER CODE 2 区(注册不入库,实验完 `git restore`)。DWT CYCCNT @168MHz(1 cycle≈5.95ns)。 +两遍独立运行,本节数字两遍一致(第二遍为第一遍 C 变体级联污染后的干净重跑,E2/E3 复验)。 + +**单次回读成本**(`ch395f_get_socket_status`,N=1000×2 遍,两遍逐值相同): + +| 统计 | cycles | µs | +|---|---|---| +| min | 3048 | 18.1 | +| avg | 3057 | 18.1 | +| max | 3481 | 20.7 | + +**LISTEN 可见性矩阵**(裸 TCP_LISTEN 不轮询命令态,发命令后 +0/10/50/100ms 回读; +100ms 仍不可见则 10ms 节奏续查到 1000ms;12 轮×2 遍=24,轮间换端口): + +| 首次观察到 TCP=01H | 计数 | +|---|---| +| +0ms | 14 | +| +10ms | 10 | +| >10ms | **0** | + +**判据结论**: +1. 单次回读 18.1µs ≪ 100µs → **"每 poll 全量刷新"成立**(4 监听 × ~18µs ≈ 72µs ≪ + 1ms 轮询周期;`docs/网络/N34-自愈审计.md` §5.4 第 1 项预估 ~15µs 量级坐实)。 +2. LISTEN 可见延迟有界:100% 在 +10ms 内可见、无长尾 → **reconciler 去抖可减为 1 次** + (决策点后 ≥10ms 单次回读即可裁定 LISTEN 形态;现"连续 2 次"去抖对该形态无必要—— + 瞬态窗口仅存在于 0~10ms 内,且第二次回读必在窗口外)。 + +**E2 补充发现(2026-10-13 12:23,Phase 15 板测 TC1501/1502b 实证):LISTEN 态 +CLOSE_SOCKET 是二段关闭**。CLOSE 命令对 LISTEN 态 Socket 返回 SUCCESS(0x00), +但只拆监听(TCP 01H→00H),**byte0 恒留 05H(Socket 仍 OPEN),2s 内不自落**; +需再发一次 CLOSE(此时 TCP 已 00H)byte0 才落 00H(二段间隔实测 0ms 即落)。 +生产 net 层早已在**无意中吸收**该语义:`net_close_locked` 等 CLOSED 的 500ms 窗 +超时后照常放行,随后重 listen 时 `net_listen_locked` 的 TRAP-NET-09 open 重试 +(3 次 × 500ms 沉降)第二次 open 前的 close 恰好充当二段——这就是 TRAP-NET-09 +"关闭后需延时才能重开"重试环存在的芯片级原因之一。阶段 2 `sock_reset_poll` +的 WAIT_CLOSE 阶段已内置二段补发(标记=drain 字段 0xF,只补发一次), +TC1502b 实证 LISTEN 态复位 GRACEFUL ~180ms 落定。 +**对阶段 3 的含义**:任何以 `cmd_done(DONE_CLOSE)` 为判据的等待,若目标 Socket +可能处于 LISTEN,必须容忍"一次 CLOSE 不落 00H"——要么走 sock_reset(已内置), +要么保留 TRAP-NET-09 式重试,不得把 DONE_CLOSE 当单次 CLOSE 的充分条件。 + +#### E3 结果(同场同固件,5 命令 × 5 轮 × 2 遍 = 50 样本,两遍签名逐条一致) + +**方法**:发命令前对照读 GET_CMD_STATUS(记录残留假阳性);发命令后按手册 +`docs/芯片手册/CH395F.md` §9.2.1 节奏(≥2ms 首查)以 ~1ms 轮询至终态(2s 上限)。 +`pre`=对照读值,`first`=首个非 BUSY 终值,`busy`=是否观察到 BUSY→终态转变。 + +| 命令 | pre(对照读) | busy | first | done | 判别力判定 | +|---|---|---|---|---|---| +| OPEN_SOCKET | 0x00(r0) / **0x17**(r1-4) | 否(0/10) | 0x00 | 3ms | **部分**:状态确被 OPEN 更新(0x17→0x00),但 pre=SUCCESS 时完成与残留不可分 | +| TCP_LISTEN | 0x00(残留 SUCCESS) | 否(0/10) | 0x00 | 3ms | **不成立**:无转变,残留 SUCCESS 不可甄别 | +| CLOSE_SOCKET | 0x00(残留 SUCCESS) | 否(0/10) | 0x00 | 3ms | **不成立**:同上 | +| SET_SOUR_PORT | **0x17** | 否 | **0x17(不变)** | — | **不成立**:SET 类根本不更新命令状态 | +| SET_PROTO_TYPE | **0x17** | 否 | **0x17(不变)** | — | **不成立**:同上 | + +**三条实证**: +1. **残留假阳性坐实**:陈旧错误码 0x17(ERR_VAL,源自"对已关闭 Socket 再 close") + 跨后续 SET 命令(不更新状态)持续存活——复核 `docs/问题记录/CH395F_Trap_Records.md` + TRAP-NET-20 定案轮的"陈旧残留"形态(RESET_ALL 陈旧 BUSY 变体本矩阵未复现,同类 + 陈旧值持续存活已证)。 +2. **OPEN/LISTEN/CLOSE 均在 <2ms 完成**,BUSY 从未观察到——"BUSY→终态"转变对这三条 + 命令不可观测;驱动 `poll_cmd_status` 20ms 粒度对它们恒白等 ≥20ms(无害但无判别价值)。 +3. **SET 类无判别力**(命令状态完全不被更新)——Trap 记录结论复核成立。 + +**判据结论(定案 `docs/网络/N34-自愈审计.md` §5.4 第 4 项)**:五条命令均不满足 +"发后 ≤N ms 变终态 **且** 无残留假阳性" → **`ch395f_cmd_done` 完成判据改用状态回读**: +open 完成=byte0 变 05H;listen 完成=TCP=01H;close 完成=byte0=00H +(回读成本 18.1µs 由 E2 实测背书;listen 可见延迟 ≤10ms 由 E2 矩阵背书,成本与时效均成立)。 +GET_CMD_STATUS 仅保留两用途:INIT_CH395 类慢命令(内核前/测试路径)与**命令级被拒** +检测(非 SUCCESS 非 BUSY 错误码仍会被芯片写入命令状态,B2 判据仍有效——被拒码是 +"新值覆盖残留"方向,不受残留歧义影响)。`poll_cmd_status` 保留给内核前/测试路径, +产品 socket 路径弃用。 + +#### E4 结果(同场同固件 + 生产路径基线;3 变体 ×10 轮 ×2 遍 = 60 轮) + +**方法**:每轮先造污染会话(主机 `test/host/ch395f_reuse_host.py --mode e4`:连上 → +发 3000B → RST,TRAP-NET-18 中止形态),芯片侧按变体执行复位序列,**立即**原端口重开 ++ LISTEN,主机立即上传 4096B 图案载荷(图案种子随全局轮次变,上一轮残渣必失配)。 +PASS 口径 = 消费者在 DISCONNECT/TIM_OUT 前实际读到的字节数=4096 且图案逐字节匹配 +(= 生产 net 层释放语义;事后读空仅作取证,不计入 PASS)。 + +| 变体 | 第 1 遍 | 第 2 遍 | 观察 | +|---|---|---|---| +| A 读空 RX→CLOSE→排事件→等 byte0=00H | **10/10** | **10/10** | 19/20 轮 rxdrain=**2048**(残渣恰等于芯片单 socket RX 缓冲整容 = TRAP-NET-18 机制直接证据) | +| B CLEAR_RECV_BUF→CLOSE→排事件→等 CLOSED | **10/10** | **10/10** | 与 A 等价,无差异 | +| C 什么都不做→CLOSE→等 CLOSED | **0/10** | **0/10** | 首 C 轮(rg20)两遍同签名,随后**自持级联** | + +**C 变体失败签名(两遍逐字节复现)**:重开后主机载荷连接 SYN 被吞(主机侧 +connect 超时);bench 首次 SINT 读 = **0x04**(陈旧 RECV 位粘滞——TRAP-NET-12 +"粘滞到被读",close/open 不清 SINT);后续轮污染连接亦 SYN 被吞(主机 +`contam CONNECT timeout`、bench `no death event`)——每轮 close 又携带未排空数据 +→ **级联自持**,open+listen 命令照常返回 SUCCESS 也无法恢复,唯芯片硬件复位 +(E13 RSTI←NRST)清除。 + +**机制定案**:决定性一步 = **close 前 RX 缓冲必须为空**(读空与 CLEAR_RECV_BUF +等价——TRAP-NET-20"缓冲重分配无效"指的是 set_send/recv_buf 重分配,不是 CLEAR)。 +带非空 RX 关闭违反 `docs/芯片手册/CH395F.md` §5.52 bit3=1"读出残留数据后再关闭" +契约 → 芯片拆链停滞 → 下一会话 SYN 被吞/CONNECT 丢失,且自持。排空事件(A/B 有、 +C 无)清的是粘滞 SINT 位——对**事件驱动 accept 的正确性**必需(0x04 陈旧 RECV 会 +误导消费者),但不是 TCP 层吞 SYN 的根因。所有轮 closed_ms=0(close 命令落定后 +byte0 已是 00H)——"等 CLOSED"几乎零成本,但为契约必需步。 + +**生产路径基线**(烧 bench 固件前,Phase 1.5 产品固件 Code=209780): +`test/host/ftp_abort_probe.py --check-abort --rounds 5` → **5/5**(中止 STOR→450; +设备日志 `sock4 TIMEOUT` → quarantine → `UPLOAD '/ab000N.bin' FAILED` → +followup `UPLOAD '/ab_ok.bin' 4096 bytes OK`)——现实现(隔离期持续排空 RX + +close + 排空事件 + 等 CLOSED)行为基线全绿,与变体 A 序列一致。 + +**判据结论(定案 `docs/网络/N34-自愈审计.md` §5.4 第 3 项)**: +(a) 与现实现等价成立(A 20/20 载荷完整);(b) B 与 A 等价(无差异记录); +C 不等价(0/20 + 级联)→ **`ch395f_sock_reset` 内部序列定案**: + +1. 读空 RX(GET_RECV_LEN→READ_RECV_BUF 直到 0;CLEAR_RECV_BUF 等价可用,**取读空** + ——保手册 §5.52 契约且排空字节数可观测可留痕); +2. CLOSE_SOCKET; +3. 排空本 socket 事件(drain-until-quiescent,E8 M5 口径:连续两轮无新位); +4. 轮询 byte0=00H(非阻塞状态机步进,约束不变)。 + +GRACEFUL 与 QUARANTINE **都必须排 RX**——E4 假设"差异仅在是否排 RX + 最短等待"中 +"是否排 RX"一半被否(带脏缓冲关闭在任何释放形态下都危险),两模式差异只剩 +最短等待(100ms/400ms,TRAP-NET-18 二阶段实测值)。 + +#### E-02 收口台架(2026-10-13,阶段 3 产品固件 Code=211348,`test/host/e02_probe.py`) + +**背景**:E13 第 5 条 把 `e02_probe` 的 `listener=FAIL` 重解读为"探针预期与'EOF 不 close'设计冲突", +并指出真实缺口 = **应用层无法主动拆掉自己放弃的会话**。阶段 3 以 `net_drop_conn()` 收口该缺口, +本节记录收口后的台架定案时序。 + +**drop_conn 时序实测**(timeSync 30s 预算耗尽路径,:8888 单连接监听): + +| 阶段 | 观察 | +|---|---| +| 预算耗尽 | `time_sync: no TIME packet in 30s, drop conn` → netTask 侧 `sock_reset QUARANTINE`(FIN + 排渣 + 排空事件 + 等 byte0=00H,400ms 底)+ 本层收口 CLOSED + `auto_relisten` 即时 `net_listener_try` | +| 对端不配合(PC socket 仍开) | 芯片停 **`FIN_WAIT_2`**(byte0=`05H` / byte1=`06H`)长尾——`close` 无 RST(TRAP-NET-07 定案更正),监听恢复**受限于对端 close**;复位机在 `net_poll` 步进里等待,不搁浅 | +| 对端配合(探针 close 自己的 socket) | 芯片 `TIME_WAIT` 过渡 ~150ms(TC-NET-204 模式 A 同形)→ byte0 回 `00H` → **reconciler 形态④**(本层 CLOSED + 芯片不在 LISTEN)relisten | +| 恢复 | 对端 close 后 **~500ms** 内 :8888 重新接入(形态④ 去抖 2 轮 = 200ms + TIME_WAIT ~150ms + open/listen);新连接发完整 8 字节 TIME 包 → 设备落 **`wall clock updated`** | +| 探针判定 | `OVERALL: queue=PASS listener=PASS`(双 PASS) | + +**探针口径更新**(`test/host/e02_probe.py`,本轮改): + +1. **模拟守规矩对端**:第 3 步预算耗尽后,探针**主动 close 自己的 socket**(配合设备 FIN)—— + 这是"可恢复边界"的显式模拟;**不配合**(内核存活、自动 ACK FIN、socket 不关)场景**不在探针判据内**, + 其兜底 = 芯片 TIMEOUT/KeepAlive 拆僵尸(E13 定案 30~90s),探针头部已写明该定案口径。 +2. **3s 重试窗**:第二次连接由"单次 5s 尝试"改为 **3s 窗内 0.3s 节奏重试**—— + 恢复本身要经过 TIME_WAIT + 形态④ 去抖(合计 ~500ms 量级),单次即时尝试会把 + "正在恢复"误判成 FAIL;重试窗只放宽**时机**,不放宽"必须重新接入 + 对时落地"的判据。 +3. 顺序修正:close 移到第二次连接**之前**(旧脚本在第二次尝试之后才 close → 内核替挂起 socket + ACK keepalive → 监听被合法占用 → 旧 FAIL 的根因,见 E13 第 5 条)。 + +**结论**:E-02 三段收口齐备——① 挂起 recv 不冻结 netTask(阶段 1.5);② 应用可主动拆会话保监听 +(`net_drop_conn`,幂等、EOF 路径不得调);③ 复位机不搁浅(`net_poll` 步进 `ch395f_sock_reset_poll`)。 +芯片侧物理边界(无 RST、`FIN_WAIT_2` 长尾受限于对端)已登记 `docs/问题记录/CH395F_Trap_Records.md` TRAP-NET-07 补充。 + +#### E6 结果(2026-10-13 15:20–19:25,U15 插桩固件 Code=211692;用户裁决提前收线 ~4h,原规格 24h) + +- **配置**:`NET_U15_PROBE=1` 两段确认插桩(period=32、log_per_h=1),`U15 probe armed` ×2 确认 + (15:19/15:20 两次 boot);窗口内常态运行 + 每小时 `:7003×10`(gap=100ms)全过 + hour04 E1 压测 + (`:7003×50` gap=0 → 50/50、`:21×50` gap=100 → 50/50,拒连 0)。 +- **判据**:`U15 probe:` 命中 = **0**(含压测)→ 按 §E6 判据,U15 登记**观察项**,阶段 3 第 4 项 + (U15 内化)**不立项**;上层 32 轮盲收保留现状。 +- **零复位**:窗口内除 armed 两次 boot 外无启动锚点、无 Fault。 +- **附带观察(非 U15,登记备查)**:E1 `:21×50` 压测尾 25s 内 7 条 `[ERR][WD] task stall` tripwire + (miss≈2.5s 级、缺失位在 0x80/0x14/0x4 间漂移),无复位自恢复、50/50 连接全过;同窗 + `sock2 TIMEOUT` + `[FTP] error reading next command` = 客户端放弃连接的正常形态。 + 若产品构建复现再查 FTP hammering 下的任务饿死。 +- **收尾**:宏已回 0、产品构建重编烧录回归;插桩代码留仓(宏=0 零成本),需要时重新武装。 diff --git a/docs/网络/N34-自愈审计/CH395F驱动层审计.md b/docs/网络/N34-自愈审计/CH395F驱动层审计.md deleted file mode 100644 index c8b74a92..00000000 --- a/docs/网络/N34-自愈审计/CH395F驱动层审计.md +++ /dev/null @@ -1,293 +0,0 @@ -# N34 自愈审计 · CH395F 驱动层审计(T2) - -> 审计对象:`Drivers/BSP/CH395F/ch395f.c`(1411 行)/ `ch395f.h`(750 行)、`Src/` 中断与引脚配置、 -> `Drivers/BSP/NET/net_socket.c`(3671 行)与 `net_listener.c`(554 行)中**由驱动层契约决定的部分**。 -> 方法:驱动 API 全量清点 + 中断路径实证 + 全部 Trap 台账通读归类 + git 考古定位根因修复落点。 -> 行号均为本工作树 HEAD(`09d6195`,分支 `task/N34-bsd-selfheal-audit`)实测行号。纯只读,未改任何代码。 -> 参照文档:CH395F_Trap_Records.md(含 2026-10-11 寄存器审计 B1~B10)、 -> 网络Socket监听生命周期设计说明.md、BSD_Socket_API_使用指南.md、主流方案对照.md(T3)、 -> net层自愈机制盘点.md(T1)。 - -## 1 驱动层 API 面盘点 - -### 1.1 驱动"知道"什么(可回读的芯片事实) - -| 芯片事实 | 驱动 API | 位置 | 语义要点 | -|---|---|---|---| -| Socket 是否打开 + TCP 态 | `ch395f_get_socket_status` | ch395f.c:1356 | **2 字节**输出:`[0]`=`00H`CLOSED/`05H`OPEN,`[1]`=TCP 态;`[1]` 仅在 `[0]`=OPEN 时有意义 | -| 单 Socket 事件 | `ch395f_get_sock_int_status` | ch395f.c:1324 | **读即清**(GET_INT_STATUS_SN) | -| 全局事件汇聚(16 位) | `ch395f_get_glob_int_status_all` | ch395f.c:1305 | **整体读即清**,芯片无按位清除手段 | -| RX 积压字节 | `ch395f_get_recv_len` | ch395f.c:1274 | 低字节在前;可反复查询(手册 5.34) | -| 对端 IP:Port | `ch395f_get_remot_ipp` | ch395f.c:336 | 6 字节,**小端**(与 SET_DES_IP 的点分序相反) | -| PHY/链路 | `ch395f_get_phy_status` | ch395f.c:875 | 返回 `01H`DISCONN…`08H`100M FULL | -| 命令完成 | `ch395f_get_cmd_status` / `ch395f_poll_cmd_status` | ch395f.c:629 / :654 | 后者 `HAL_Delay(20)` **阻塞忙等** | -| SPI 通道健康 | `ch395f_spi_byte_fault` | ch395f.c:265 | 锁存式(复位前不清),仅首条故障记日志 | -| 芯片版本 | `ch395f_get_version` | ch395f.c:310 | 仅 net_init 打印一次(net_socket.c:259),**不参与门控** | -| MAC/IP/掩码/网关 | `ch395f_get_mac_addr` / `ch395f_get_ip_inf` | ch395f.c:819 / :847 | 回读用于自检留痕 | -| ARP 不可达现场 | `ch395f_get_unreach_info` | ch395f.c:1402 | **全仓零调用**(B4) | - -### 1.2 驱动暴露的原语(51 个公共函数,按职责分组) - -| 组 | 函数 | 位置 | -|---|---|---| -| SPI 事务 | `write_cmd` / `write_data` / `read_data`(**公开**)、`spi_byte_fault`;`spi_begin`/`spi_end`/`spi_dma_transfer`/`spi_fault_set` 为 static | ch395f.c:212/229/246/265;:76/:87/:120/:100 | -| 芯片检测与初始化 | `check_exist` / `get_version` / `reset` / `get_cmd_status` / `poll_cmd_status` / `init` | :278/:310/:613/:629/:654/:680 | -| 网络参数 | `set_ip_addr` / `set_gwip_addr` / `set_mask_addr` / `set_mac_addr` / `get_mac_addr` / `get_ip_inf` / `set_phy` / `get_phy_status` / `ping_enable` / `set_dhcp` / `get_dhcp_status` / `set_arp` / `set_ttl` / `get_unreach_info` | :702/:731/:760/:791/:819/:847/:898/:875/:918/:937/:956/:1375/:1389/:1402 | -| 缓冲几何 | `set_recv_buf` / `set_send_buf` / `clear_recv_buf` | :416/:443/:468 | -| TCP 全局参数 | `set_fun_para` / `set_tcp_mss` / `set_retrans_count` / `set_retrans_period` / `set_keepalive_idle` / `_intvl` / `_cnt` / `_enable` | :372/:394/:573/:592/:487/:509/:531/:551 | -| Socket 生命周期 | `set_proto_type` / `set_des_ip` / `set_des_port` / `set_sour_port` / `open_socket` / `close_socket` / `tcp_listen` / `tcp_connect` / `tcp_disconnect` | :980/:1003/:1034/:1059/:1085/:1100/:1115/:1130/:1145 | -| 数据面 | `write_send_buf` / `read_recv_buf` / `get_recv_len` | :1166/:1222/:1274 | -| 状态回读 | `get_glob_int_status_all` / `get_sock_int_status` / `get_socket_status` / `get_remot_ipp` | :1305/:1324/:1356/:336 | - -**驱动持有的状态 = 零。** 全文件仅 4 个 static(ch395f.c:50-56):`s_spi2_dma_done`、`s_spi_fault`、 -两个 DMA 缓冲。没有任何 socket 状态缓存、没有链路态缓存、没有事件缓存——驱动是纯命令转发层。 - -### 1.3 SPI 事务包裹与集中中断读取约定 - -- 每次事务由 static 的 `ch395f_spi_begin()`(CS PB12 SET→RESET)/ `ch395f_spi_end()` 包裹(ch395f.c:76/:87), - 但 `write_cmd/write_data/read_data` 是**公开**的(ch395f.h:301/:308)——任何调用方都能拼一条未包裹的事务。 -- SPI2 分频 **8**(Src/spi.c:77-88,CPOL LOW / CPHA 1EDGE / 软 NSS),PCB 约束,勿改。 -- 集中中断读取约定落在 **net 层**:`net_poll()` 先读 INT# 引脚(net_socket.c:435),低电平才读 - `GET_GLOB_INT_STATUS_ALL`(:441),再按 `1U<<(i+4)` 门控去读该 Socket 的 SINT(:464/:469)。 - 驱动只提供两个裸的读即清原语,**没有任何护栏**。 - -### 1.4 中断路径结论:纯轮询,无 ISR - -- `CH395_INT_Pin`(PE5)在 `Src/gpio.c:76-80` 配为 `GPIO_MODE_INPUT` + `GPIO_NOPULL` → **无 EXTI、无 ISR**。 -- `EXTI9_5_IRQn` 属于 `TP_BUSY_Pin`(PC7,Src/main.h:124-125、gpio.c:170-171、stm32f4xx_it.c:391),与 CH395F 无关。 -- 事件消费节奏 = netTask 主循环 `net_poll()` + `net_process_messages()`(App/task/net_task.c:241-279,约 1ms 一轮)。 - -**判定:现状正确,不应改 EXTI。** 芯片的全局事件寄存器是"整体读即清",ISR 内读寄存器会把事件从 -netTask 手里抢走(正是 B3 事故的形状,见 F-DRV-2)。缺的不是中断,而是**驱动侧的事件分发与缓存**。 - -## 2 Trap 根因归类总表 - -台账共 **24 条**(旧编号 01–25,**旧 19 从未存在**,属悬空引用)。根因类别: -**(a)** 芯片真实行为(不可在固件侧根治);**(b)** 固件寄存器值/常量错误;**(c)** 引用错误 -(字节序/偏移/索引/单位);**(d)** 时序误用(顺序/忙等/沉降);**(e)** 设计缺口;**(f)** 判读或台架假象。 - -| Trap | 症状(一句话) | 类别 | 根因修复落点 | 催生自愈补丁 | 根因修好后补丁是否仍必要 | -|---|---|---|---|---|---| -| 01 | Socket 4~7 的 CONNECT 中断不触发 | (d)+(a) | net_socket.c:333-340(net_init 内 7→0 逆序给全部 8 个 Socket 显式配缓冲) | 否 | — | -| 02 | Socket 4~7 发送固定垃圾/接收不落数据(缓冲重叠) | (c) | net_socket.c:334-340 + ch395f.h:136-141(固定 6 块/Socket,用满 48 块) | 否 | — | -| 03 | DHCP 包 xid 偏移错 | (c) | 主机侧测试工具(历史) | 否 | — | -| 04 | UDP 写完不等 SENBUF_FREE → 下次写被静默丢弃 | (a) | net_socket.c:2721-2753(`send_ready` 门控) | 否(属正常流控) | 是 | -| 05 | 自动协商与 RTL8305NBI 不兼容 | (a) | net_task.c:110(强制 100M 全双工) | 否 | 是 | -| 06 | KeepAlive 非 500ms 倍数 → 内部定时器异常 | (a) | net_config.h:61-71 + net_socket.c:288-290 | 否 | 是 | -| 07 | 只 disconnect 不 close → 重开必返 `0x20` | (f)→(a) | 原判"FIN_WAIT_2/RST"已证伪;真因 `FUN_PARA` bit3=1 芯片不自动关 Socket。轮询 **byte0**:net_socket.c:2326-2345 | 间接(催生"close→等 CLOSED→排空"序列) | 是(bit3=1 契约) | -| 08 | RECV 中断无限触发 | (a) | net_socket.c:429(`do{}while(0)` 单批处理) | 否 | 是 | -| 09 | PHY link up 同一时刻 `open_socket` 失败 | (a)+(d) | 根因侧已修:就绪屏障 net_task.c:232 + 启动期等 PHY link net_task.c:118-130 | **是**(D10 层) | **部分**:启动形态已被根因消除;close→reopen 沉降仍是芯片行为(TRAP-NET-20 实证窗口不定长)→ 重试保留,**应用层重复重试可删** | -| 10 | 6 个 API 永远返回 -1(消息队列按值拷贝) | (e) | 错误码改经 `xTaskNotify` 通知值回传 | 否 | — | -| 11 | accept 拒绝 standalone_accept 的 ESTABLISHED | (e) | net_accept_locked(net_socket.c:1281-1342) | 否 | — | -| 12 | FatFs `FR_INVALID_NAME` | 非 CH395F | lftpd_io.c | 否 | — | -| 13 | 分批读后 `recv_len` 归零 | (f) 存疑 | 无(机理未证实,与手册 5.34 及本仓 TC-NET-201 相反) | 否 | **约束已事实作废**(见 F-DRV-11) | -| 14 | close 后 DISCONNECT 滞后锁存,污染下次首读 | (a) | 预清 + 反核实:net_socket.c:2326-2345 / :1137、net_listener.c:262、net_socket.c:1670 | **是** | 是(TRAP-NET-18 实测伪 DISCONNECT 仍投递给下一条连接) | -| 15 | DHCP 成功后 IP 寄存器提交滞后 | (a) | 主机侧判据修正(历史) | 否 | — | -| 16 | TCP Client 目的 IP 字节序写反 → CONNECT TIMEOUT | (c) | net_socket.c:1636-1641 + :2691-2701(改大端),commit `db64e6ac` | 否 | 根因已修;**驱动 API 未封装字节序 → 复发面仍在**(F-DRV-5) | -| 17 | client 重连被上一条连接的残留 DISCONNECT 秒断 | (a) | 预清 + 反核实:net_socket.c:1660-1726 | **是** | 是 | -| 18 | "bit0=1 才是多 Socket 模式" | (f) 伪根因 | net_socket.c:270 回到 `0x08`(commit `d758d5c`);真正起作用的是 TRAP-NET-02 的缓冲分配 | 否 | 驱动侧残留:语义宏未被使用(F-DRV-6) | -| 19 | — | — | **不存在**(悬空引用) | — | — | -| 20 | 单次写 >1KB 写穿发送 FIFO → 数据错乱/卡死 | (c) | ch395f.h:138-141 + net_socket.c:1745/1788/2717(`NET_SEND_CHUNK_MAX`) | 否 | 根因已修;驱动只告警不拦截(F-DRV-7) | -| 21 | 接收缓冲"恰好填满+排空"窗口冻结 | (f) 非缺陷 | 台架自吞数据 + 判读工具误判;保留金丝雀 ch395f.c:1176-1189 与布局解耦 ch395f.h:130-153 | 否 | — | -| 22 | 快速复连丢 CONNECT/DISCONNECT → 监听静默死亡 | (a)(B3 曾自造同类症状,commit `4f2cc8b` 修) | net_reconcile_tick net_socket.c:548-641 | **是**(对账层) | **是**(TRAP-NET-20 裸机台架独立实证芯片丢中断);但命中数应重测 | -| 23 | 上传中止后首笔复连被静默截断(≤2KB) | (a)+(e) | net_listener.c:102-121(排空)+ :149-204(隔离段);登记点 net_socket.c:3430/:3523 | **是**(隔离排渣层) | 是 | -| 24 | MAC 写入顺序违反手册(SET_MAC 须在 INIT 前) | (d) | net_socket.c:292-299(commit `de1b50e`) | 否 | 已根治 | -| 25 | FTP 数据连接三连:丢 CONNECT / accept 竞态自毁缓冲 / 背靠背小段毒化 TX | ①(a) ②(e) ③(a) | ①探针 net_lost_connect_probe net_socket.c:1228-1267 + 幽灵甄别 `last_peer_port`;②closed-with-data accept net_socket.c:1306-1334(**我方 bug,已根治**);③批量发送 lftpd.c:107-155 | **是** | ①③是;②属契约实现,保留 | - -**归类小结**(按定案后的主因计,跨类条目只计一次):(a) 芯片行为 12 条、(c) 引用/数值错误 4 条、 -(f) 判读假象 3 条、(d) 时序误用 2 条、(e) 设计缺口 2 条;另旧 12 非 CH395F、旧 19 不存在。 - -**催生五层自愈补丁的 6 条(09/14/17/22/23/25)中,只有 TRAP-NET-09 的 -"启动形态"和 TRAP-NET-20 的"②accept 竞态"被根因修复消掉;其余全部落在芯片行为上,不可从根上消除。** - -## 3 五层自愈 × 驱动层契约 - -| 自愈层 | 参数与位置 | 驱动层是否已有对应能力 | 结论 | -|---|---|---|---| -| `auto_relisten`(监听常驻) | net_types.h:172;置位 net_socket.c:1119 | 无——芯片永不自动回 LISTEN(单连接复用模型),必须软件承诺 | **保留**(模型基石) | -| D10 非阻塞重监听重试 | net_listener.c:77-79(30ms/2s)、:394-432 | 驱动只有**阻塞**的 `poll_cmd_status`(ch395f.c:654)→ 非阻塞重试是在 net 层绕开驱动阻塞原语 | **保留**,但真正的解法是驱动提供非阻塞命令完成查询(F-DRV-8) | -| TRAP-NET-17 周期对账 | net_socket.c:544-546(200ms/去抖 2/冷却 1s)、:548-641 | 驱动无状态缓存 → 对账只能每周期重新发 SPI | **保留**(根因是芯片丢中断);参数与 pump 重复,应收敛(F-DRV-1) | -| N-32 监听器 pump | net_listener.c:437-441(100ms/去抖 2/冷却 1s)、:465-553 | 同上 | **保留**(不变量 II 的落点,不是补丁) | -| TRAP-NET-18 排渣隔离 | net_listener.c:53-56(400/100/800ms)、:102-121、:149-204 | 排空 RX 的**机械**(`get_recv_len`+`read_recv_buf` 循环)住在 net 层;驱动只有裸 `clear_recv_buf`(且产品零调用) | **下沉驱动**(F-DRV-4) | - -## 4 驱动层缺口清单(缺什么原语) - -1. **无 socket 状态缓存 / 无权威回读单一来源**:7 处各自手拼 `GET_SOCKET_STATUS` - (net_socket.c:586/1235/1694/2330/3391/3408、net_listener.c:516),三套去抖/冷却参数并存。 -2. **无类型化状态谓词**:`byte0 门控 byte1` 只写在 ch395f.h:592-601 注释里,仍有 3 处只判 `byte1` - (net_socket.c:592/1237/3410)→ B6 类误读未收口。 -3. **无"是否在听"谓词**:`chip_listening` 判据手拼在 net_listener.c:517-518。 -4. **无复合复位原语**(排空 RX → close → 排空事件 → 有界等 CLOSED):net 层两套同构变体。 -5. **无 RX 排空原语**:`net_drain_chip_rx`(net_listener.c:102-121)是纯芯片机械。 -6. **读即清原语无护栏**:驱动裸暴露"整体读即清"的全局寄存器,B3 事故即此 API 形状的产物。 -7. **无非阻塞命令完成查询**:`poll_cmd_status` 用 `HAL_Delay(20)` 忙等,最长 4s。 -8. **无链路态缓存与边沿事件**:PHY_CHANGE/DHCP/UNREACH/IP_CONFLI 四个全局位读清后无人消费(B4)。 -9. **无版本门控**:`get_version` 只打印不判(net_socket.c:259);bit3 需 ≥0x46、bit1 需 ≥0x44(B9)。 -10. **无发送缓冲空闲查询**:SENBUF_FREE 只存在于读即清的 SINT 里 → `send_ready` 镜像无处下沉。 -11. **无 socket 索引边界校验**:51 个函数中 `sock` 一律不校验 0~7。 -12. **无 SPI 总线互斥**:串行化仅靠"只有 netTask 碰驱动"的约定(AGENTS 模块约定)。 -13. **IP 字节序契约未封装**:4 处调用点手拼大端数组,驱动注释还与实测相反。 -14. **无事件分发中枢**:`net_poll` 里的 glob→SINT→handler 分发是 net 层手拼,驱动无 dispatch 表。 - -## 5 编号发现 - -**F-DRV-1 · 同一芯片事实被 7 处独立回读、三套去抖参数并存——"单一事实源"缺失是五层膨胀的地基** -证据:`ch395f_get_socket_status` 调用点 net_socket.c:586(对账 200ms/去抖 2/冷却 1s,:544-546)、 -:1235(探针 100ms/去抖 2,`NET_LOST_CONN_STABLE_N`)、:2330(close 等待 500ms)、:3391/:3408(事件留痕)、 -net_listener.c:516(pump 100ms/去抖 2/冷却 1s,:437-441)。驱动 static 变量只有 4 个(ch395f.c:50-56), -无任何缓存。 -影响:同一 socket 的状态在 100ms 窗口内可被读 3~4 次,判读口径与防抖参数各不相同;任何一处口径漂移 -就产生新的"镜像说 A、芯片是 B"窗口(与 T3 的 F-MS-8 同判)。 -裁决:**下沉驱动**——驱动持每 socket 的最近一次回读 + 一个权威刷新入口(每 net_poll 周期一次), -对账/pump/探针/close 共用同一份事实与同一套去抖。 - -**F-DRV-2 · 读即清原语无护栏:B3 事故是驱动 API 形状的直接产物,修复落在 net 层,API 形状未变** -证据:ch395f.c:1305(全局 16 位整体读即清)与 :1324(单 Socket 读即清)裸暴露;B3 修复只在 -net_socket.c:1122-1137 用注释收口("原为遍历 8 个 Socket 全清 + 再读清全局"→ 仅清本 Socket), -git 考古链 `e862009bc`→`319d00e3`→`db64e6acf`(commit `4f2cc8b` 修)。 -影响:同类事故(吞掉其它在用 Socket 的 CONNECT/DISCONNECT 边沿事件)可在新调用点复发; -这正是"看起来像 TRAP-NET-17 的自家 bug"的来源。 -裁决:**下沉驱动**——提供 `ch395f_sock_events(sn)`(只读该 Socket,永不碰全局), -并把"全局寄存器唯一读者 = net_poll"写成驱动侧断言/文档契约。 - -**F-DRV-3 · 无类型化状态回读:`byte0 门控 byte1` 只活在注释里,3 处只判 byte1** -证据:ch395f.h:592-601 注释规定 `[1]` 仅在 `[0]`=OPEN 时有意义;违反口径的判读点 -net_socket.c:592(`st[1]==ESTABLISHED` 未先看 `st[0]`)、:1237(探针同型)、:3410(陈旧 DISCONNECT 甄别同型)。 -TRAP-NET-07 定案更正第 3 条正是把这类读数读成"卡在某个 TCP 态"。 -影响:幽灵态/孤儿态判定的正确性依赖每个调用点都记得这条规则——已三次失守。 -裁决:**下沉驱动**——返回 `{open, tcp_state}` 结构 + `ch395f_sock_is_listening()/is_established()` 谓词, -`open=0` 时 TCP 态返回"无意义"哨兵值。 - -**F-DRV-4 · "close + 排空 RX + 排空事件 + 等 CLOSED"在 net 层有两套变体,是下沉驱动的最佳落点** -证据:net_socket.c:2326-2345(close→循环读状态 + 读 SINT + 读全局→等 CLOSED,上限 500ms)与 -net_listener.c:149-204(排空 RX→close→读 SINT→读全局→读状态→等 `CLOSED/TCP_CLOSED` + 最短等待 -400/100ms,上限 800ms)。语义同构(bit3=1 排空契约),参数漂移,**且只有后者排空 RX**。 -影响:同一芯片契约两处实现;close 路径不排空 RX,依赖"上层先排"的隐含前提。 -裁决:**下沉驱动**——`ch395f_sock_reset(sn, mode, wait_ms)`(mode=异常/正常), -net 层只选 mode。与 T1 的 F-NET-9 同判,签名归 T4 定。 - -**F-DRV-5 · IP 字节序契约未封装,且驱动注释与实测相反(TRAP-NET-14 的复发面仍在)** -证据:TRAP-NET-14 根因是照抄手册"所有 IP 命令低字节在前"的总括,实测 SET_IP/SET_DES_IP 收**点分序**; -修复落在 net 层(net_socket.c:1636-1641、:2691-2701,commit `db64e6ac`)。但驱动侧: -`ch395f_set_des_ip` 头注释完全不写字节序(ch395f.h:479-484),`set_ip_addr/set_gwip/set_mask` -仍写"低字节在前"(ch395f.h:380/:387/:394),而 `get_remot_ipp` 是**小端**(ch395f.h:612)。 -影响:同一驱动内两种字节序、注释还指错方向;4 个调用点各自手拼数组,新调用点极易再犯。 -裁决:**下沉驱动**——驱动内做字节序转换(对外收 `uint32_t` 网络序或明确点分序), -并改正 ch395f.h 三处注释;这是"让 BSD API 更主流"里成本最低、收益最直接的一步。 - -**F-DRV-6 · FUN_PARA 语义宏是死代码,net_init 传裸 `0x08`** -证据:ch395f.h:204-206 定义 `CH395F_FUN_PARA_TCP_SERVER/SOCKET_CLOSE/DISABLE_SEND_OK`, -全仓零引用(grep 仅命中定义处);net_socket.c:270 写 `ch395f_set_fun_para(0x08)`。 -影响:TRAP-NET-15 的"bit0=1 多 Socket 模式"伪根因能存活一年多,正是因为语义没有类型化到宏/枚举层, -只能靠注释与手册对质。 -裁决:**简化**——net_init 改用宏组合(`CH395F_FUN_PARA_SOCKET_CLOSE`),删掉未使用的 -`CH395F_GINT_STAT_SOCK0..3`(net_poll 用 `1U<<(i+4)`,net_socket.c:464)。 - -**F-DRV-7 · 驱动对超限写只告警不拦截——现状正确,但契约必须显式化** -证据:ch395f.c:1183-1189 明确"超出不会被命令拒绝,而是写穿发送缓冲(TRAP-NET-16)→ 只告警不拦截, -分块责任在调用方";分块上限 `NET_SEND_CHUNK_MAX` 在 net 层(net_socket.c:1745,应用于 :1788/:2717)。 -影响:若驱动内自动分块,需在分块间等 SENBUF_FREE,而该事件由 net_poll 集中读清 → 驱动内等待会自锁。 -裁决:**保留**(现状合理);把"单次写 ≤ `CH395F_SEND_BUF_SIZE_BYTES`"从注释升格为 -驱动 README 的显式契约,并把 OVERSIZE 金丝雀计数暴露给上层(当前只打日志)。 - -**F-DRV-8 · 驱动的命令完成等待是阻塞原语:每条 socket 命令至少 20ms、最坏 4s 的 netTask 忙等** -证据:ch395f.c:654-670 `poll_cmd_status` 用 `HAL_Delay(20U)` 忙等(不让出 CPU); -`open_socket`/`close_socket`/`tcp_listen`/`tcp_connect`/`tcp_disconnect` 全部以 4000ms 上限调用它 -(:1091/:1106/:1121/:1136/:1151);DMA 路径另有 1s 忙等(:138-160)。 -影响:D10 的"非阻塞"只覆盖 net 层策略层,驱动层仍是阻塞原语——netTask 停摆指纹 -(`req=0x77`/`mask=0x41`)与 P2b 暂缓的"忙等"问题,真正的根在驱动层。 -裁决:**下沉驱动**——新增非阻塞 `ch395f_cmd_done()`(单次查询,返回 BUSY/终态), -把 open/listen/connect 拆成"发命令"+"查完成"两步;net 层的 D10 重试即可直接复用。 - -**F-DRV-9 · 阻塞重试三处残留,其中两处属应用层——与 D10 非阻塞设计矛盾,构成第 6/7 层自愈** -证据:net_socket.c:1092-1104(`net_listen_locked` 3×`osDelay(500)`)、:1647-1659 -(`net_connect_locked` 同型);应用层 lftpd_inet.c:39-70(5 轮,`vTaskDelay(2000)`)、 -lftpd.c:473-477(3 轮 ×200ms)。 -影响:TRAP-NET-09 的"启动形态"已被就绪屏障 + 启动期等 PHY link 消除(net_task.c:118-130/:232), -应用层重试的原始理由已不存在;保留它只会把恢复时间拉长并制造额外的重建所有者。 -裁决:**简化**——net 层两处并入 D10(P2b);应用层两处**可删**(保留 lftpd 的重试前先确认 -`net_relisten` 语义已覆盖,见 T1 的裁决)。 - -**F-DRV-10 · 无版本门控:`get_version` 只打印不判** -证据:net_socket.c:259 `DBG_INFO("ch395f ver=0x%02X fun_para=0x08", ch395f_get_version())`; -驱动无版本判断(ch395f.c:310)。FUN_PARA bit3 需芯片 ≥0x46、bit1 需 ≥0x44(B9)。 -影响:换料/降版本时 `0x08` 静默失效,而整套排空契约(bit3=1)正是建立在 bit3 生效之上。 -裁决:**简化**——`net_init` 内门控(版本不足则拒绝置 bit3 并留自检记录),一行判定消掉整类静默。 - -**F-DRV-11 · 旧机理的"必须一次读空整个接收缓冲"约束已事实作废,但注释仍在四处约束后人** -证据:ch395f.h:146(旧注释"接收方向需一次性读空整个 Socket 接收缓冲…避免部分读导致剩余数据不可见")、 -net_socket.c:1908/:1969/:3466/:3470。而 `net_recv_locked` 常态按 `min(len, recv_len)` 部分读 -(net_socket.c:1882-1952),TC-NET-604 结案已撤回"部分排空致命",TRAP-NET-18 回归 10/10+5/5 全在部分读下通过。 -影响:注释与代码事实相反;`CH395F_SPI_DMA_BUF_SIZE = 2048+4` 的 sizing 理由(ch395f.h:143-153) -仍挂在一条未证实的机理上。 -裁决:**可删**(注释级)——把旧机理降级为"未证实的历史观察",补一条"部分读→再查 recv_len" -定向 TC 结案;DMA 缓冲 sizing 改以"接收缓冲 2KB"为唯一理由。 - -**F-DRV-12 · 死声明与零调用 API 清单(驱动 API 面虚胖)** -证据:`CH395F_CMD_GET_GLOB_INT_STATUS` 0x29(ch395f.h:36,只有 2 字节版 0x19 有实现)、 -`CH395F_SPI_BEGIN_DELAY_MS`(ch395f.c:42,从未使用)、EEPROM/GPIO/SLEEP 命令 -(ch395f.h:163-167 等)、`CH395F_GINT_STAT_SOCK0..3`(:244-247)、 -`ch395f_tcp_disconnect`(ch395f.c:1145,产品零调用,仅 TC-NET-204 用例 ch395f_test_task.c:783)、 -`ch395f_get_unreach_info`(:1402,全仓零调用)、`ch395f_clear_recv_buf`(:468,仅未注册的 -ch395f_reuse_bench.c:169)。 -影响:API 面越大,"哪个是产品路径"越难判;TRAP-NET-07 的"disconnect→close"错误序列正是从 -一个零调用的 API 出发的。 -裁决:**可删/登记**——删死宏与未实现命令声明;`tcp_disconnect` 保留但注释标注"产品路径禁用"; -`clear_recv_buf` 保留(TRAP-NET-20 已证"缓冲重分配无效",排渣用读空而非 CLEAR_RECV_BUF)。 - -**F-DRV-13 · 链路事件静默:四个全局中断位读清后无人消费,驱动无链路缓存** -证据:net_socket.c:441 读全局后只按 `bit4..bit11` 分派 socket 事件(:464), -bit0~bit3(UNREACH/IP_CONFLI/PHY_CHANGE/DHCP)被丢弃;`ch395f_get_unreach_info` 零调用; -链路只在启动期等一次(net_task.c:118-130)。 -影响:运行期拔网线/链路抖动/IP 冲突/ARP 不可达完全静默,只能靠芯片 TIMEOUT(≈1.6s)间接暴露。 -裁决:**下沉驱动**——驱动持链路态缓存 + 变化计数 + `ch395f_link_up()` 谓词,net 层订阅; -这是 T3 的"PHY/链路变化收在驱动层"结论的落点。 - -**F-DRV-14 · 中断路径实证:纯轮询无 ISR,且不应改为 EXTI** -证据:`Src/gpio.c:76-80` PE5 = `GPIO_MODE_INPUT`/`GPIO_NOPULL`;`EXTI9_5_IRQn` 归 TP_BUSY(PC7, -gpio.c:170-171、stm32f4xx_it.c:391);消费节奏 = net_task.c:241-279 主循环。 -影响:与 AGENTS 模块约定一致;改 EXTI 会让 ISR 与 netTask 争抢"整体读即清"的全局寄存器(F-DRV-2 形状)。 -裁决:**保留**——但把"为什么不用 EXTI"从口头约定升格为驱动 README 的显式理由(当前只有一句约定)。 - -**F-DRV-15 · HAL 全局回调跨模块耦合:CH395F 驱动代管 SPI1 完成标志** -证据:ch395f.c:180-199 的 `HAL_SPI_TxRxCpltCallback`/`HAL_SPI_ErrorCallback` 同时处理 SPI2 与 SPI1, -并 `extern volatile uint8_t g_spi1_dma_done`(:178,定义在 GD5F 驱动里)。 -影响:HAL 全局回调只能有一份,谁定义谁就代管别人;改 CH395F 驱动可能静默破坏 NAND 的 DMA 完成路径。 -裁决:**简化**——收进一个 SPI 适配层(或按实例注册),至少把该耦合写进两个模块的 README。 - -**F-DRV-16 · 单连接复用不是偏离主流;差距在驱动不持事实、不推事件、无复合事务原语** -证据:T3 的 F-MS-2/F-MS-5(ioLibrary 无 accept、CH395 每 Socket 一连接、FreeRTOS+TCP -`REUSE_LISTEN_SOCKET` 同形);本仓驱动 static 变量仅 4 个(ch395f.c:50-56)、无事件分发、 -无复合原语(§4 缺口 1/6/7/14)。 -影响:把"要不要改成 per-socket 独立状态机"当成问题是问错了方向——芯片侧本来就是 -每 Socket 一连接、监听与数据通道复用;缺的是驱动把芯片事实**收口**成一份可查询的真相。 -裁决:**保留模型**;按 F-DRV-1/2/3/4/8/13 补驱动原语后,五层里的对账/pump/探针失去存在理由, -隔离排渣下沉为驱动原语(与 T3 的 4.3/4.4 一致)。 - -## 6 内聚评估:驱动持缓存 + 权威回读后,reconcile/pump/drain 能否下沉或删除 - -| 机制 | 能否因"驱动持事实"而删 | 理由 | -|---|---|---| -| TRAP-NET-17 对账①② | **不能删,但可大幅收窄** | 根因是芯片丢中断(TRAP-NET-20 裸机台架独立实证)。驱动持缓存后,对账的"回读"部分变成读缓存;"发现失配"仍需存在,只是从"周期 SPI 轮询"降为"每 poll 周期一次刷新 + 比对" | -| N-32 pump | **不能删** | pump 是"单一重建所有者"(不变量 II),是架构解不是补丁;驱动持事实后它的判据读缓存,动作仍在 net 层(策略) | -| D10 重试 | **可降级** | 若驱动提供非阻塞 `cmd_done()` + `sock_reset()` 内联有界等待(F-DRV-4/8),独立 30ms/2s 定时器可退化为事务内重试(T3 同判) | -| TRAP-NET-18 排渣隔离 | **可下沉** | 排空/等待是纯芯片机械;net 层只保留"异常/正常"两档 mode | -| 探针 `net_lost_connect_probe` | **不能删,可去抖简化** | 丢 CONNECT 是芯片行为;驱动持事实后去抖可去掉(读缓存无瞬态噪声),保留"状态已 ESTABLISHED 而本层 LISTENING → 补 accept" | -| `auto_relisten` | **不能删** | 芯片永不自动回 LISTEN,这是模型承诺 | - -**一句话结论**:五层里有四层(对账、pump、探针、resync)存在的直接理由是"驱动不持事实、net 层只能自建镜像"; -把它们删掉的前提不是"根因已修",而是"驱动成为唯一事实源"。TRAP-NET-18 的排渣隔离则相反—— -它的根因(在途数据落 RX)不可修,但它的**实现位置**应当从 net 层下沉到驱动。 - -## 7 对 T4 汇总的输入 - -1. 驱动层最该补的三个原语(按收益/成本排序): - ① 类型化状态回读 + 每 socket 缓存(F-DRV-1/3);② `ch395f_sock_reset()` 复合复位(F-DRV-4); - ③ 非阻塞 `ch395f_cmd_done()`(F-DRV-8)。三者都不改芯片行为,只把事实与机械收进驱动。 -2. 立即可做、零风险的清理:F-DRV-6(用宏)、F-DRV-10(版本门控)、F-DRV-11(撤旧"部分读归零"注释约束)、 - F-DRV-12(删死声明)、F-DRV-5 的注释更正。 -3. 需要重测的量化项:B3 修复(`4f2cc8b`)前后各跑 `:21 100ms×50`,统计 `reconcile:` 命中数, - 量化"自家制造的丢中断"占比(与 T1 的 F-NET-8 同一实验,勿重复做)。 -4. 与 T3 的分歧点:T3 的 4.2 第 5 条称"FUN_PARA_TCP_SERVER(多连接模式)已在 net_init 启用"—— - **与代码不符**。net_socket.c:261-270 明确 `bit1=0`(多连接模式不用),实传 `0x08`; - 设计文档的 A′ 出局论证(不支持多监听)成立。T4 汇总时须以代码为准。 diff --git a/docs/网络/N34-自愈审计/README.md b/docs/网络/N34-自愈审计/README.md deleted file mode 100644 index e41ea261..00000000 --- a/docs/网络/N34-自愈审计/README.md +++ /dev/null @@ -1,135 +0,0 @@ -# N34 自愈审计 · 汇总报告(T4) - -> 审计对象:BSD Socket API 的「芯片/本层状态对账与重监听自愈」五层机制(auto_relisten / D10 重试 / -> TRAP-NET-17 对账 / N-32 pump / TRAP-NET-18 排渣隔离)及其显式 API(`net_relisten` / `net_resync_listen`)。 -> 方法:三路独立审计汇总——net 层盘点(`docs/网络/N34-自愈审计/net层自愈机制盘点.md`,F-NET-1..14)、 -> CH395F 驱动层审计(`docs/网络/N34-自愈审计/CH395F驱动层审计.md`,F-DRV-1..16)、 -> 主流方案对照(`docs/网络/N34-自愈审计/主流方案对照.md`,F-MS-1..9,含 T4 更正两处:多连接模式未启用、 -> "静默回收"非 Server 模式条款)。全部证据带 文件:行号,HEAD `09d6195` 实测。 -> 性质:纯审计,未改任何代码。本报告是裁决与路线,不是实施记录。 - -## 实施状态(2026-10-13 更新) - -> 想先要"为什么一直不稳"的人话版(比喻 + 全景图):先读 -> `docs/网络/N34-自愈审计/网络不稳根因总览.md`。 - -| 提交 | 内容 | 结果 | -|---|---|---| -| `9ac9879` | P0 止血包(M1+M2+M4-lite) | 台架验证(E7b/E8) | -| `ae17d36` | M3 矛盾态可疑门(风暴态免疫,资产门版) | E9/E9b 全绿 | -| `4c0a0af` | **阶段 0** 零风险清理 9 条 | E10:0/20 + 20/20 + 20/20 + 11/11 | -| `b8c6a3d` | **阶段 1** API 面收口(对账⑤ + 删 3 API + time_sync/lftpd) | E10:全绿回归(详见实验方案 E10) | -| `13a4a40` `d346e4e` | KeepAlive 转正(`docs/芯片手册/CH395F.md` §5.56)+ **阶段 1.5** recv 语义主流化 | E11/E12 台架 | -| `f7f1793` `83a7072` | **阶段 2** ch395f 五原语(只加不接线)+ E2/E3/E4 定案 | Phase 15 套件 20/20 | -| `6394853` `f51c083` | **阶段 3** net 层收敛:单一 reconciler + 忙等统一 + 镜像降级 + E-02 收口(`net_drop_conn`) | 终版回归全绿(TRAP-NET-17/-18/-20、E-02 双 PASS、104、对时、integrity 15/15) | -| `123d80a` | E6 插桩 `NET_U15_PROBE`(默认关,零成本) | ✅ E6 已裁决(2026-10-13:命中=0 → 项 4 不立项,U15 观察项) | -| `925058a` | **收口**:lftpd 对端地址修复(`net_getpeername` + ntoa 段序)+ E6 落档 + 全库编号重排 TRAP-NET-01~21 | @test PASS + ftp_abort 10/10 + relisten 20/20 + ci_local 全绿 | - -阶段 0~3 与 E6 均已完成并转正;仅剩延后项 -(relisten 沉降段统一,见实施方案 §5.2)。阶段 1 实施过程中的一条关键台架发现 -已留痕(实验方案 E10):**`net_listener_armed` 必须放行 ESTABLISHED**——单连接模型 -的"待 accept"电平,漏放行时 i104(200ms 周期)抢不过对账② 的 ~400ms 去抖而整场 -饿死(18/18 FAIL → 修复后 18/18 PASS,127s→8.3s)。 - -## 0. 结论先行(直接回答本次审计的问题) - -**问:「芯片/本层状态对账与重监听自愈」可以简化或删除吗?可以内聚到 ch395f 驱动层吗?** - -1. **不能整体删除。** 催生五层自愈的 6 条 Trap(09/14/17/22/23/25)中,只有 2 个被根因修复消掉 - (TRAP-NET-09 的启动形态→就绪屏障;TRAP-NET-20 的②accept 竞态→closed-with-data)。其余全部落在**芯片行为**上: - TRAP-NET-20 的裸机芯片台架(剥离 OS/net/FTP)独立复现"握手完成但 CONNECT 中断被丢"——这是 - "对账不可因固件修 bug 而删"的决定性证据(F-NET-8)。 -2. **用户的假设部分成立,但打不中五层。** 24 条 Trap 根因归类:芯片行为 12、引用/数值错误 4、 - 判读假象 3、时序误用 2、设计缺口 2(F-DRV 总表)。寄存器数值/引用错误确实存在过 - (TRAP-NET-14 字节序、TRAP-NET-02 缓冲重叠、TRAP-NET-16 FIFO 上限、B3 全量读清),但它们**不是**五层自愈的成因—— - B3(全 Socket SINT 读清)只是"自家制造的丢中断"放大器,修复后对账仍必要,只是命中数应下降(可台架量化)。 -3. **但"五层补丁"这个形态不是终态,可以大幅简化并部分内聚。** 结构性根源不是单连接复用模型 - (ioLibrary 无 accept、CH395 每 Socket 一连接、FreeRTOS+TCP reuse 模式全部同形,F-MS-2/F-DRV-16), - 而是三件事的组合:**① 驱动完全不持事实**(ch395f.c 仅 4 个 static,纯命令转发层); - **② net 层维护有损 8 态镜像并作为决策依据**(芯片 11 态→8 态有损投影,F-MS-8); - **③ 同一芯片事实被 7 处独立回读、三套去抖/冷却参数并存**(F-DRV-1)。 - 镜像滞后 → 需要周期对账;事件丢失 → 需要探针与门控补偿;重建脱钩 → 需要 pump/重试/沉降三件套。 -4. **"内聚到 ch395f"对一半。** 该下沉驱动的是**事实与机械**:状态回读+缓存、事件读即清护栏、 - 排渣/沉降/等 CLOSED 复合原语、非阻塞命令完成查询、链路态缓存。 - 该留在 net 层的是**策略与所有权**:监听重建的单一决策者(pump 是 N-32 的架构解,不是补丁, - 且消费者驱动即时重建有 P2a 台架实测支撑,删了会 `task stall`)。 -5. **立即可删的是 API 面,不是机制**:`net_relisten`(footgun,生产零调用)、 - `net_resync_listen`(语义并入对账后删)、`net_can_accept`(iec104 迁移后退役)—— - 这三个"上层手动自愈"入口正是"补丁式代码难维护"的最直观来源。 - -## 1. 三方裁决的调和 - -T1(net 层视角)判"机制主体保留、API 可删";T3(主流对照视角)判"对账/pump/探针/resync 整体删除"。 -两者不矛盾,差在参照系:**T1 评的是现架构下的必要性,T3 评的是目标架构下的存在理由**。 -统一裁决(T2 的调和,F-DRV 总表): - -> **删除这些机制的前提不是"根因已修",而是"驱动成为唯一事实源"。** -> 需求(丢中断兜底、监听常驻承诺、残渣清理、沉降窗口)是永恒的;形态(五层常驻定时器 + 镜像对账) -> 是可替换的——替换为"驱动持事实 + 决策点读事实 + 重建内联进事务 + 单一 reconciler"。 - -## 2. 逐机制汇总裁决 - -| 机制 | 位置(证据见三份发现文件) | 汇总裁决 | 依据 | -|---|---|---|---| -| `auto_relisten` | net_types.h:172 | **保留**(模型基石) | 芯片永不自动回 LISTEN,监听常驻是软件承诺(P4) | -| D10 `net_listener_retry_tick` | net_listener.c:394-432 | **短期保留+小修;长期降级** | 小修=补 pump 同型状态门(F-NET-10)+ give-up 文案(F-NET-14);长期=驱动 `cmd_done()` 后内联进事务、删独立定时器(F-DRV-8) | -| TRAP-NET-17 `net_reconcile_tick` | net_socket.c:548-641 | **功能保留,形态重构**(阶段 1 已做①扩展=⑤) | 与 pump 判据合并为**单一 reconciler**、读驱动缓存、一套去抖(F-DRV-1);①扩展覆盖"本层 ESTABLISHED+芯片已回 LISTEN"缺口(F-NET-2)✅ 已实施(bad=5,`b8c6a3d`) | -| N-32 `net_listener_pump` | net_listener.c:465-553 | **并入 `net_reconcile_tick`(阶段 3,2026-10-13)** | 唯一周期重建决策者=不变量 II **不变**,只是与对账合一为单一 100ms tick(形态③④/U11,判据读驱动缓存);T3"改内联"与 P2a 台架实测冲突(删消费者驱动→`[WD] task stall`)故消费者三处仍保留 | -| TRAP-NET-18 排渣隔离 | net_listener.c:102-121/:149-204 | **下沉驱动** | 排空/等 CLOSED 是纯芯片机械;与 net_close_locked:2326-2345 是同一契约两套变体(F-NET-9/F-DRV-4)→ 收口 `ch395f_sock_reset(sn, mode, wait_ms)`,net 层只选 mode | -| `net_relisten` | net_socket.c:3080 | **删除** ✅ 已实施(`b8c6a3d`) | 生产零调用;对健康监听器无条件 open→USE→close,自毁 ~1s(footgun) | -| `net_resync_listen` | net_socket.c:3026 | **删除**(先并入对账①)✅ 已实施(语义=对账⑤) | 语义正确但载体错位:应是自动对账,不是手动 API(F-NET-2/F-MS-4) | -| `net_can_accept` | net_socket.c:1427 | **删除**(先迁 iec104 到 `net_listener_armed`)✅ 已实施 | 过期门控致 :2404 丢 CONNECT 探针从不运行(F-NET-4,与已修的 :7003 探针失效同型)。⚠ 迁移时 **armed 必须补 ESTABLISHED 放行**,否则 i104 整场饿死(实验方案 E10) | -| `net_lost_connect_probe` | net_socket.c:1228-1267 | **保留**;驱动持事实后去可去抖 | 丢 CONNECT 是芯片行为(裸机实证);去抖/冷却是对账世界的自偿成本(F-MS-9) | -| L3 pending recv + 看门狗 | net_socket.c:2024-2190 | **保留** | 阻塞模型主体,与主流(FreeRTOS 事件组阻塞)同构 | -| `session_dead` / `released_abnormal` / 幽灵甄别 / closed-with-data | net_types.h | **保留** | 芯片行为的必要建模,P1 不变量 I | -| `net_listen_locked` 3×osDelay(500) / `net_close_locked` 500ms 忙等 | net_socket.c:1092-1104/:2326-2345 | **简化**(P2b) | 根在驱动阻塞原语(F-DRV-8);先补 `cmd_done()` 再统一走 D10 | -| 上层每 32 轮盲收(U15) | ui_net_sink.c:552-558、iec104_serv.c:246-253 | **中期下沉** | "RX 非空但 readable=0"并入 reconciler 后上层退役(F-NET-12) | -| time_sync 每会话 close 监听重建 | time_sync.c:182 | **简化(上层)** ✅ 已实施(`b8c6a3d`) | 违反自家"EOF 不得 close 监听 fd"铁律,主动制造 TRAP-NET-09 沉降窗(F-NET-13);改 lftpd 模式同 fd 回 accept。实测连续 6 轮推送正常(E10) | -| lftpd 应用层阻塞重试 | lftpd_inet.c:39-70、lftpd.c:473-477 | **删除/简化** ✅ 已实施 | TRAP-NET-09 启动形态已被就绪屏障消除,重试理由不存在(F-DRV-9)。实施偏差:未走 `net_listener_try` 登记(会把 BOUND/D10 状态语义一并拖入),改单次尝试+失败即 425(E10 登记) | -| 三处全局读清残留 | net_socket.c:1668/:2332、net_listener.c:176 | **收窄**(与驱动护栏合并做) | B3 已证"只读本 Socket SINT"可行;这三处仍在吞别的 Socket 边沿事件(F-NET-7) | - -## 3. 目标架构(高内聚低耦合的落点) - -| 层 | 应持有 | 现状偏差 | -|---|---|---| -| `ch395f` 驱动 | **唯一事实源**:每 socket 状态缓存 + 类型化回读(`{open, tcp_state}` + `is_listening()/is_established()` 谓词,byte0 门控 byte1 收口);事件护栏 `sock_events(sn)`(永不碰全局寄存器);复合原语 `sock_reset(sn, mode, wait_ms)`(排空 RX→close→排空事件→有界等 CLOSED,收编 TRAP-NET-09/-18 参数);非阻塞 `cmd_done()`;链路态缓存+边沿事件;IP 字节序封装;版本门控 | 驱动零状态(4 个 static);读即清裸暴露;命令原语全阻塞(`HAL_Delay(20)` 忙等、4s 上限);字节序注释与实测相反 | -| net 层(netTask 单所有者) | fd↔socket 映射、消息队列串行化、L3 pending、事件分发;**状态镜像降级为展示投影**;**单一 reconciler**(对账+pump 判据合并、读驱动缓存、一套去抖);重建=`sock_reset`+LISTEN(pump 周期 + 消费者即时,两处入口语义统一) | 8 态镜像是决策依据;7 处手拼回读三套参数;两套排空变体;`net_can_*` 镜像门控 | -| 应用/服务层 | 只管 fd 与数据;监听常驻由 net 层承诺 | 32 轮盲收、time_sync close、lftpd 重试——三层兜底散在应用 | - -一句话:**驱动持有芯片事实并内联处理怪癖,net 层单所有者只做映射/串行/分发且决策读事实, -监听重建收敛为"reset 原语 + LISTEN"由 pump 与事务共同触发,应用层零自愈责任。** - -## 4. 分阶段路线(风险从低到高;每阶段独立可回退) - -- **阶段 0 · 零行为风险清理**(纯注释/宏/一行判定,可先行提交): - F-DRV-5 字节序注释更正、F-DRV-6 FUN_PARA 用宏、F-DRV-11 撤旧"部分读归零"注释约束、F-DRV-12 删死声明、 - F-NET-11 注释漂移(50ms→100ms)、F-NET-14 give-up 文案、F-DRV-10 版本门控(一行)、 - F-NET-6 U12 LISTEN 失败补 close(一行)、F-NET-10 retry_tick 状态门(一行)。 -- **阶段 1 · API 面收口**(小 diff,行为可测):删 `net_relisten`;iec104 门控迁 `net_listener_armed` - → 退役 `net_can_accept`;对账①扩展覆盖静默回收形态 → 删 `net_resync_listen`; - time_sync 会话收尾不 close;删 lftpd 应用层重试。 -- **阶段 2 · 驱动原语**(新增不删旧,行为不变):类型化回读+每 socket 缓存、`sock_events` 护栏、 - `sock_reset` 复合原语(现补丁参数原样收编)、`cmd_done()` 非阻塞、链路缓存。 -- **阶段 3 · net 层收敛**(核心重构,需完整台架回归):对账+pump 合并单一 reconciler(读缓存、 - 一套去抖)、探针去抖删除、两处忙等改走 `cmd_done`、U15 数据面对账内化 → 上层 32 轮盲收退役。 -- **贯穿验证腿**:B3 修复前后各跑 `:21 100ms×50` 统计 `reconcile:` 命中数,量化"自家制造的丢中断" - 占比(F-NET-8,T1/T2 同一实验勿重复做);阶段 3 回归判据沿用生产验证数据 - (快速复连 :7003 10/10、:21 100ms/0ms/600ms 各 50/50、中止 RST 10/10 + FIN 5/5、SHA-256 全匹配)。 - -## 5. 红线(本次审计明确"不做"的事) - -1. **不改 EXTI/ISR**:全局事件寄存器整体读即清,ISR 读寄存器会把事件从 netTask 抢走——正是 B3 事故形状(F-DRV-14)。 -2. **不启用多连接模式(FUN_PARA bit1)**:不支持多监听,A′ 出局论证成立;T3 原文误称"已启用"已更正。 -3. **不删消费者驱动即时重建**(accept/recv/pending 三处):P2a 台架实测删除→CLOSED 窗口拉长→`[WD] task stall`。 -4. **不改单连接复用模型**:这是硬 TCP 栈原生主流形态,不是对 BSD 的偏离。 -5. **SPI 分频 8、中断经 `net_poll` 集中读清**:PCB 约束与既有约定不变(护栏是收窄读法,不是改路径)。 -6. 驱动缓存成本每 socket 数字节可忽略,但按平台约束前后跑 `scripts/ram_report.py` / `stack_report.py`。 - -## 6. 发现索引与后续登记建议 - -- 三份发现文件共 **39 条编号发现**:F-NET-1..14(net 层)、F-DRV-1..16(驱动层)、F-MS-1..9(主流对照)。 - 每条带 文件:行号 证据与裁决,后续排期直接按编号引用。 -- 建议登记进 `docs/任务规划/README.md`(**按铁律在 done 时统一处理,任务期不改登记簿**): - 阶段 0 合并为一条 N-x(零风险清理簇);阶段 1 一条(API 收口簇);阶段 2/3 各一条(驱动原语 / net 层收敛, - 后者需完整台架回归);F-NET-2 覆盖缺口建议**单独登记并优先**——它是当前唯一"无自动机制认领"的失同步形态。 -- 本目录三份发现文件即审计留痕,销项时结论沉淀进对应模块文档后本目录可归档。 diff --git a/docs/网络/N34-自愈审计/net层自愈机制盘点.md b/docs/网络/N34-自愈审计/net层自愈机制盘点.md deleted file mode 100644 index 9e2fa0d0..00000000 --- a/docs/网络/N34-自愈审计/net层自愈机制盘点.md +++ /dev/null @@ -1,346 +0,0 @@ -# N34 自愈审计 · net 层自愈机制盘点(T1) - -> 审计对象:`Drivers/BSP/NET/net_socket.c`(3671 行)与 `net_listener.c`(554 行)/ `net_listener.h`。 -> 方法:全文通读 + 调用点 grep + git 考古(`git log -S`)+ 问题记录与设计文档对照。纯只读,未改任何代码。 -> 行号均为本工作树 HEAD(`09d6195`)实测行号。 -> 参照文档:CH395F_Trap_Records.md(TRAP-NET-09/-17/-18/-20 与 2026-10-11 寄存器审计章节 B1~B10 条目)、 -> 网络Socket监听生命周期设计说明.md(B10 根治设计,含 P0/P1/P2 落地记录与模块化边界)、 -> BSD_Socket_API_使用指南.md(五层自愈口径)。 - -## 1. 机制 × 属性总表 - -核心七机制(用户指定盘点范围): - -| # | 机制 | 代码位置 | 触发条件 | 改本层哪些状态 | 发哪些芯片命令 | 定时器/去抖/冷却 | 重叠/互为兜底的对象 | 根因分类 | 初步裁决 | -|---|---|---|---|---|---|---|---|---|---| -| 1 | **auto_relisten**(监听常驻标志) | 置位 net_socket.c:1119(`net_listen_locked`);字段 net_types.h:172;消费点 net_socket.c:1316/1924-1938/2136-2162/1483、net_listener.c:501 | `net_listen` 成功即置 1;此后任何"会话已结束"路径(DISCONNECT/TIMEOUT/对账①)把 fd 置 CLOSED 后,由消费点或 pump 依据该标志重建 | 本身不改状态,是**门控标志**;`net_close`(memset net_socket.c:2351)与 `net_init`(:244)清零 | 无(纯标志) | 无 | 是所有重建路径(机制 2/4/6 与 accept/recv/pending 三处即时重建)的共同前提门 | (a) 芯片永不自动回 LISTEN(手册无此条款,设计文档认定;单连接复用模型锁死) | **保留**(模型基石,不可删) | -| 2 | **D10 非阻塞重监听重试** `net_listener_retry_tick` | net_listener.c:394-432;登记 net_listener.c:374-378;常量 :77-79 | 任何 `net_listener_try` 单次重建失败(open 失败 / LISTEN 失败 / 隔离未满)即登记 `s_relisten_wait`,到点重试 | 成功经 `net_relisten_locked` 置 `state=LISTENING`(net_listener.c:233)、清 `released_abnormal`、转登 `last_peer_port`、清 `lost_conn_seen`、清对端簿记(:239-255);**刻意不清 `session_dead`**(:234-238 契约注释) | 隔离段见机制 5;正常段:SET_PROTO_TYPE、SET_SOUR_PORT、OPEN_SOCKET、TCP_LISTEN(:207-230);open 失败补 CLOSE_SOCKET(:218);成功尾读本 Socket SINT 读即清(:262) | 重试间隔 30ms(:77)、放弃时限 2s(:79,自首次尝试);无去抖(单次尝试);**L3 保险**:同 fd 有在途挂起 recv 则跳过(:405-408,经 net_socket.c:2202 `net_pending_active`) | 是机制 4(pump)与三处消费者即时重建的**执行器**(它们调 `net_listener_try`,失败落到这里);"2s 放弃"并非终态——pump 冷却后会再次触发(见 F-NET-4) | (a) 芯片 close 后重开需沉降(TRAP-NET-09 PHY 形态 + TRAP-NET-20 裸机台架实证拆链窗口 >2s 不定长);旧实现 osDelay(500) 忙等属 (b) 已修(e5733b3) | **保留**;"give up" 语义需简化(F-NET-4) | -| 3 | **TRAP-NET-17 周期对账** `net_reconcile_tick` | net_socket.c:548-641;常量 :544-546 | 每 200ms 对**认领范围内**(仅本层 ESTABLISHED / LISTENING,:579-581)的 fd 回读芯片两字节状态:① 本层 ESTABLISHED 而芯片 SOCKET_CLOSED 或 TCP_CLOSED(:588-590);② 本层 LISTENING 而芯片 TCP_ESTABLISHED(:591-592) | ① 复用 `handle_disconnect_event`(:633)→ 置 `session_dead=1`、`last_peer_port`、`state=CLOSED`、可能登记 GRACEFUL 沉降;② 复用 `handle_connect_event`(:638)→ 置 ESTABLISHED、清 `session_dead`、记对端 | GET_SOCKET_STATUS(每认领 fd 一条,:586);② 前 GET_REMOT_IPP 幽灵甄别(:597-602);后续按事件路径的命令 | 周期 200ms(:544)、连续 2 次失配去抖(:545,≈400ms)、动手后 1s 冷却(:546) | ② 与 accept 路径内联探针 `net_lost_connect_probe`(net_socket.c:1228-1267)职责重叠(同一失同步双兜底,TRAP-NET-20 定案"探针快路径 + 对账兜底网");① 与 `net_resync_listen` 语义相邻但判据不同(见 F-NET-2);原③④ 已移交 pump(:536-537/606-607) | (a) 芯片丢 CONNECT/DISCONNECT 中断(TRAP-NET-17 生产实测 + TRAP-NET-20 裸机台架独立实证,**与 B3 固件 bug 无关**);幽灵态 (a) | **保留**;建议扩展①(F-NET-2) | -| 4 | **N-32 监听器 pump** `net_listener_pump` | net_listener.c:465-553;常量 :437-441 | 每 100ms 对**监听型 fd**(in_use + auto_relisten + STREAM + local_port≠0,:501-507)且本层处于 LISTENING/CLOSED(:509-513)者回读芯片:① 本层 LISTENING 而芯片不在 LISTEN → 重建;② 本层 CLOSED 而芯片不在 LISTEN → 重建;③ 本层 CLOSED 而芯片**正在** LISTEN(U11)→ 只重同步 | ③:仅 `state: CLOSED→LISTENING`(:528)+ 转登 `last_peer_port`、清 `lost_conn_seen`(:532-533),**不发任何芯片命令**;①②:经 `net_listener_try` 走机制 2 的状态改动;`session_dead` 不清(:462-463 契约) | 每监听型 fd 一条 GET_SOCKET_STATUS(:516);③ 零命令;①② 同机制 2 | 周期 100ms(:437)、连续 2 次去抖(:440)、1s 冷却(:441) | 本层**唯一周期性重建决策者**(不变量 II,设计文档第 4 节);与三处消费者即时重建(accept net_socket.c:1327 / recv :1938 / pending :2162)构成"周期兜底 + 消费者驱动"双层——该分工是 P2a 台架实测推翻原计划后的定案(删消费者驱动会致 `[WD] task stall`) | (c) 设计缺口(多所有者竞争)的**架构解**;其底层需求(芯片不回 LISTEN + 事件会丢)为 (a) | **保留**(这就是"内聚"的落点,不是补丁) | -| 5 | **TRAP-NET-18 排渣隔离** `net_drain_chip_rx` + 隔离段 | 排空 net_listener.c:102-121;隔离段 net_listener.c:149-204;登记 net_listener.c:282-289(`net_listener_settle`),登记点 net_socket.c:3430(GRACEFUL,仅 `recv_bytes≠0`)/:3523(QUARANTINE) | 异常释放(芯片 TIMEOUT,net_socket.c:3512-3524)登记 QUARANTINE;正常释放(芯片 DISCONNECT 且本连接收过数据,:3424-3432)登记 GRACEFUL;下一次 `net_relisten_locked` 进入隔离段 | 隔离期满清 `s_relisten_quarantine/_settle_start/_settle_dead`(:199-203);不改 socket 状态(状态改动仍由机制 2 成功尾做) | 隔离期内每轮:GET_RECV_LEN + READ_RECV_BUF(排空,:172→:107-120)、CLOSE_SOCKET(主动关,:174)、读本 Socket SINT 读即清(:176)、**读全局中断寄存器**(:178,见 F-NET-7)、GET_SOCKET_STATUS(:180) | QUARANTINE 最短 400ms / GRACEFUL 最短 100ms / 上限 800ms 强制放行(net_listener.c:53-55;上限必须 < 2s 重试时限,实测 2000ms 会把监听耗死) | 与 `net_close_locked` 的"close→等 CLOSED→排空中断"循环(net_socket.c:2326-2345)是同一芯片契约的**两个变体实现**(下沉候选,见 F-NET-9);GRACEFUL 的 `recv_bytes≠0` 门是为保护 TRAP-NET-17 快速复连时序而设(两机制互相制衡) | (a) FUN_PARA bit3=1(0x08,D4 定案必须保留)下芯片保留现场 + 在途数据在 TIMEOUT 之后才落缓冲 + 残留 DISCONNECT 投递给下一条连接(TRAP-NET-18 两阶段台架实证;0ms/30ms 沉降无效、100ms 通过) | **保留**;"close+排空+等 CLOSED"原语可**下沉**驱动层合并(F-NET-9) | -| 6 | **net_resync_listen / net_relisten** 显式自愈 API | API net_socket.c:3026-3066 / :3080-3120;分派 :774-799 / :800-814 | 应用层判定"连接已死/监听器可能失位"后主动调用;**生产代码零调用者**,唯一调用者 test/board/net_latency_test.c:783-784/787/800(Phase 13 台架,产品构建不启用) | RESYNC:仅本层 `state: ESTABLISHED→LISTENING` + 转登 `last_peer_port` + 清 `lost_conn_seen` + 取消挂起 recv(:789-797),**不发芯片命令**;RELISTEN:经 `net_listener_try` 无条件芯片侧重建(机制 2 全套) | RESYNC:无;RELISTEN:机制 2 全套命令(含隔离段) | 无自身定时;RELISTEN 失败落 D10 重试 | RESYNC 与 pump③(U11)语义相邻但覆盖的本地状态不同(ESTABLISHED vs CLOSED);RELISTEN 与 pump①② 完全重叠且**更危险**——对健康监听器无条件 open→USE→close 会造成 ~1s 监听中断(net_socket.c:3078 注释自认) | RESYNC 针对的"芯片静默回收"为 (a)(run8 台架实证,当时 FUN_PARA 已是 0x09);但 API 形态属 (c) 时代遗留(上层镜像芯片状态、手动修);RELISTEN 属 (c) 冗余 | **可删**(生产路径):RESYNC 语义并入对账①扩展后删 API;RELISTEN 直接删(F-NET-2/F-NET-3) | -| 7 | **L3 挂起 recv**(pending)及其交互保险 | 结构 net_socket.c:184-193;登记 :2024-2063;完成 tick :2074-2190;了结 :1985-2007;取消 :2237-2256;close 前了结 :2267-2298;查询 :2202-2208 | 阻塞 recv 无数据即登记;`net_pending_tick` 每轮按 ①数据 ②到点 ③断连(`CLOSED ∥ session_dead`,:2136)顺序了结 | ③ 断连分支:排空后按 `released_abnormal` 回 CONNRESET 或 EOF(0)(:2164-2174);`state==CLOSED` 且 auto_relisten 时**消费者驱动即时重建**(:2159-2162);close 路径先 settle(排空或取消,必通知,:2316-2318);`net_init` 重建前 `cancel_all`(:241) | GET_RECV_LEN + READ_RECV_BUF(①③ 读数据);③ 即时重建走机制 2 命令 | 每 fd 至多一个挂起(:2038-2041 防覆盖挂死);deadline=登记+预算(默认 30s,net_config.h:39);**看门狗**:超 deadline 后 1s 强制了结 + DBG_ERROR(:182/:2181-2188);与机制 2 的交互保险 = retry_tick 的 `net_pending_active` 跳过(net_listener.c:405-408) | 与机制 2 互为保险(挂起在途则 D10 不动该 fd);③ 的即时重建与 recv_locked DONTWAIT 的即时重建同型(:1936-1938);`cancel_all`(net_init)与 tick 的"fd 已关闭兜底"(:2092-2096)双保险 | 阻塞模型演进(E-02 头阻塞修复),非严格自愈;③ 的排空+EOF 语义对应 (a)(旧观察注:FIN 先于数据、bit3=1 保留现场);看门狗属 (c) 完成逻辑缺位的 tripwire | **保留**(含看门狗,留痕成本极低);它是"消费者驱动重建"三处之一,P2a 已定案不可删 | - -配套机制(不在七项内但参与同一失同步网,盘点完整性所需): - -| 机制 | 位置 | 一句话属性 | 根因 | 裁决 | -|---|---|---|---|---| -| `net_lost_connect_probe`(TRAP-NET-20 双信源探针) | net_socket.c:1228-1267,调用 :1298-1300 | accept 轮询内联补 accept;去抖 2 轮(:1214)无冷却;幽灵甄别 `last_peer_port` | (a) 拆链窗口丢 CONNECT(裸机台架 15/15) | 保留 | -| closed-with-data accept | net_socket.c:1305-1335 | CLOSED + RX 有数据 → 接受"已关但可读"连接排空(bit3=1 排空契约) | (a)(TRAP-NET-20 三次定案:425 真根因是我方 relisten 自毁缓冲) | 保留 | -| `session_dead` 电平(N-32 P1 不变量 I) | net_types.h:192-206;置位 net_socket.c:1723/3439/3535;清零 :3310/3347;不清 net_listener.c:234-238 | "会话已结束"从边沿改粘滞电平,消费者不再被抢先 relisten 饿死 | (c) 设计缺口的**架构解** | 保留(这是根治本身) | -| `released_abnormal` | net_types.h:174-180;置位 net_socket.c:3529/3434 | 异常/正常释放区分 → 挂起 recv 回 CONNRESET 还是 EOF | (a) 协议边界(在途 FIN 与正常完成不可区分,TRAP-NET-18 硬化节留痕) | 保留 | -| `net_listen_locked` 3×osDelay(500) 阻塞重试 | net_socket.c:1092-1108 | 首次监听路径仍忙等(P2b 暂缓项),与 D10 非阻塞模式并存 | (b) 时代遗留(D10 已修 relisten 侧,listen 侧未统一) | 简化(统一走 `net_listener_try`) | -| `net_close_locked` 500ms 等 CLOSED 忙等 | net_socket.c:2326-2345 | 关闭热路径同步忙等(P2b 暂缓项) | (c) 同步等待设计债;台架实测危害理论性(设计文档 P2b 决策) | 简化(暂缓有据,留观察) | -| 陈旧 DISCONNECT 甄别(D20) | net_socket.c:3400-3415 | ESTABLISHED 收 DISCONNECT 时回读芯片,TCP 仍 ESTABLISHED 判陈旧丢弃 | (a) TRAP-NET-12 滞后锁存 | 保留 | -| 代次号 `s_net_generation` | net_socket.c:49/358-374;递增 net_task.c:145 | 网络重建后服务层重建监听的依据(应用层重建所有者通道) | (c) 但边界清晰(芯片复位后 fd 全灭是事实) | 保留 | - -## 2. 根因分类汇总 - -**(a) 芯片真实行为(手册条文或裸机台架实证,无法从根上消除)——对应机制必须保留:** - -| 芯片行为 | 证据 | 支撑的机制 | -|---|---|---| -| 连接结束后芯片永不自动回 LISTEN(单连接复用模型下监听槽位被消耗) | 设计文档第 2.1 节引手册;官方 EXAM10 例程在事件现场当场重建 | auto_relisten、pump、D10、resync 语义 | -| 快速复连/拆链窗口内丢 CONNECT/DISCONNECT 中断 | TRAP-NET-17 生产实测;**TRAP-NET-20 裸机台架剥离 OS/net 层仍复现**(关键:证明与固件层 bug 无关) | 对账①②、探针、pump | -| 拆链收尾窗口变长且不可观测(>2s 不定长),窗口内假报 ESTABLISHED(幽灵态,对端端口=旧连接) | TRAP-NET-20 定案实验 | 幽灵甄别 `last_peer_port`、去抖+冷却纪律 | -| FUN_PARA bit3=1(0x08,D4 定案保留):DISCONNECT/TIMEOUT 后芯片不关 Socket 不清缓冲(停 0x05/0x00),MCU 必须排空后显式 close | 手册 §5.52②/§5.23;TRAP-NET-20 三次定案判别探针(recv_len=1027 完整载荷) | closed-with-data accept、TRAP-NET-18 排渣、close_locked 排空循环 | -| 中止时段的在途数据在 TIMEOUT **之后**才落进 RX 缓冲(~2KB 残渣被下条连接读走) | TRAP-NET-18 一阶段(RST 10/10 复现→修复 10/10) | QUARANTINE 400ms + 持续排空 | -| 上一条连接残留 DISCONNECT 会在复用同 fd 的下一条连接 accept 后 10~20ms 投递 | TRAP-NET-18 二阶段(0ms/30ms 沉降无效、100ms 通过) | GRACEFUL 100ms(仅收过数据时登记) | -| close 后重开 Socket 需沉降(PHY 形态 + 拆链形态) | TRAP-NET-09;TRAP-NET-20 台架(固定沉降 100~3000ms 均证伪"等就好") | D10 到点重试(而非固定延时) | -| 芯片可静默回收连接回监听且不发 DISCONNECT | run8 台架实证(当时 FUN_PARA=0x09,bit3 已=1,git 核实) | **阶段 1 起由对账⑤自动认领**(原仅 `net_resync_listen` 覆盖 = F-NET-2 缺口,已闭环) | -| 全局中断寄存器整体读即清、无按位清除;SINT 粘滞到被读 | 手册 §5.15;TRAP-NET-12 | net_poll 集中读清纪律;也解释了为何"丢弃式读全局"有害(F-NET-7) | - -**(b) 早期固件 bug(寄存器数值/引用/时序误用)——根因已修,评估补丁是否仍必要:** - -| 早期 bug | 修复 | 曾支撑的补丁 | 根因修复后补丁是否仍必要? | -|---|---|---|---| -| B3:listen/relisten 对全部 8 个 Socket 读 SINT + 丢弃式读清全局(多连接时代调试遗留,git 考古 e862009bc→319d00e3→db64e6acf) | 4f2cc8b(2026-10-12)收窄为只清本 Socket | TRAP-NET-17 对账在生产中的部分命中 | **仍必要**:TRAP-NET-20 裸机台架(无 net 层代码)独立实证芯片自身丢中断;但 B3 修复后对账在生产日志的命中数应显著下降——可台架统计验证(F-NET-8) | -| `get_socket_status` 曾给 1 字节缓冲(栈越界、状态误读) | e5733b3 | 曾可能污染对账/甄别读数 | 已修,无遗留补丁 | -| TRAP-NET-15:FUN_PARA bit0=1(保留位,无出处) | d758d5c 定案 0x08 | 多 Socket 模式误判相关 | 已修;对自愈机制无直接影响 | -| B2:`ch395f_tcp_connect` 返回值被丢弃 → CONNECTING 滞留 30s | 794a688 + a844241(命令级失败置 session_dead) | 无独立补丁,靠 recv 超时暴露 | 已修,无遗留补丁 | -| relisten 旧实现 osDelay(500) 忙等(全网冻结 500ms) | e5733b3(D10 非阻塞化) | — | 忙等已修;**同族的 `net_listen_locked` 3×500ms 与 `net_close_locked` 500ms 忙等仍在**(P2b 暂缓,F-NET-5) | -| B1:服务器侧 KeepAlive 使能(按手册补的"未证明复杂度") | 9a45be1 撤回(实测芯片 2s 重传超时已覆盖静默死亡) | — | 正面案例:根因复核后主动删除,应作为本次审计的裁决范式 | -| **U12(未修)**:relisten 的 TCP_LISTEN 失败不 close 芯片 Socket(与 open 失败路径不对称) | 未修(设计文档 P0 登记项未勾选) | 靠下一轮重试 open→USE→close 兜住 | 见 F-NET-6 | - -**(c) 设计缺口(本层镜像芯片状态导致必须对账/多处所有者)——可由架构消解(多数已消解):** - -| 缺口 | 现状 | -|---|---| -| 重建多所有者(曾 12 个) | P2a pump 收敛周期性路径;消费者驱动 3 处经台架定案保留;显式 API 2 处(RELISTEN/RESYNC)与 net_can_accept 旧门控**已于阶段 1 删除**(F-NET-2/3 ✅) | -| EOF 边沿化(抢先 relisten 吞 EOF) | P1 `session_dead` 电平化已消解 | -| 上层直接读 net 层状态镜像 | `lftpd_inet_accept_timeout` 直读 `p_sock->state`(lftpd_inet.c:117)、`net_listener_armed` 用 `readable` 代理"RX 可能有残留"(net_socket.c:1480-1485)、`net_can_read` 自认"对账不含 RX 非空但 readable=0"(:1500-1501)迫使上层保留每 32 轮兜底 recv(ui_net_sink.c:552-558、iec104_serv.c:246-253)——**第三层兜底散在应用层**(F-NET-12) | -| 应用层仍是重建所有者 | ~~time_sync 每会话 close 监听 fd 全量重建~~(**阶段 1 已修** `b8c6a3d`:同 fd 跨会话常驻);lftpd 应用层阻塞重试已删(阶段 1);iec104 `s_drop_conn` close 监听重建(协议违例场景,有意例外)(F-NET-13) | -| fd 别名(监听=连接) | 硬件上限(多连接模式不支持多监听,D1 出局),不可消解;只能靠不变量 I/II 约束 | - -## 3. 编号发现 - -**F-NET-1 · 重建入口实测 5 处,与设计文档定案一致;"唯一重建入口"仅指周期性** -证据:`net_listener_try` 调用点共 4(阶段 1 删 `NET_MSG_RELISTEN` 后)——net_listener.c:552(pump,唯一周期性)、net_socket.c:1327(accept CLOSED 分支)、:1938(recv DONTWAIT EOF 分支)、:2162(pending_tick ③)。后三处为"消费者动作触发",P2a 台架实测删除会把 CLOSED 窗口拉长到 pump 周期+隔离期并触发 `[WD] task stall`(设计文档第 9 节 P2a 修正记录)。 -影响:这不是缺陷,是**已实测裁决的重叠**;但"唯一重建入口"的表述在 BSD 指南与注释中易被读成"仅 pump 一处",误导后续维护者删消费者驱动路径。 -建议:保留现状;文档口径统一为"唯一**周期性**入口 + 三处消费者驱动"。 - -**F-NET-2 · "本层 ESTABLISHED 而芯片已回 LISTEN"失同步无任何自动机制覆盖;唯一解药 API 生产零调用** -证据:对账①判据要求芯片 SOCKET_CLOSED 或 TCP_CLOSED(net_socket.c:588-589),芯片回 LISTEN(0x05/0x01)时不命中;对账②要求本层 LISTENING(:591);pump 只裁本层 LISTENING/CLOSED(net_listener.c:509-513);`handle_connect_event` 对本层 ESTABLISHED 的新 CONNECT 中断两个分支都不命中、静默丢弃(net_socket.c:3306 起)。该场景为芯片真实行为(run8 实证"芯片静默回收、监听器健康",git 核实当时 FUN_PARA=0x09 即 bit3=1)。现仅 `net_resync_listen`(net_socket.c:774-799)能修,而其生产调用者为零(仅 net_latency_test.c:783)。 -影响:一旦生产出现"静默回收 + 无 DISCONNECT",端口将滞留假 ESTABLISHED,新连接被芯片接住但本层无感知,直到下一次应用 close 或网络重建。 -建议:把该形态并入对账①——本层 ESTABLISHED 且芯片 TCP=LISTEN → 按 RESYNC 语义只改本层状态(不发芯片命令,纪律同 pump③ U11 与 :785-788 注释);并入后 `net_resync_listen` API 可删(见 F-NET-3)。 -**✅ 已实施(阶段 1,`b8c6a3d`)**:对账判据新增 **bad=5**(本层 ESTABLISHED 且 `st[0]!=SOCKET_CLOSED && st[1]==TCP_LISTEN`)→ RESYNC 语义(state→LISTENING + `last_peer_port` 转登 + `lost_conn_seen` 清零 + `net_pending_cancel(CONNRESET)`),不发任何芯片命令;手动 API 同批删除。台架验证见实验方案 E10。 - -**F-NET-3 · `net_relisten` / `net_resync_listen` / `net_can_accept` 三个 API 是冗余或过期面** -证据:`net_relisten`(net_socket.c:3080)与 `net_resync_listen`(:3026)生产零调用(grep 全仓仅 test/board/net_latency_test.c:776-801);`net_relisten` 对健康监听器无条件 open→USE→close,自认造成 ~1s 监听中断(:3078 注释),与 pump③"芯片在听绝不重下发"的 U12 教训直接冲突。`net_can_accept`(:1427)唯一生产调用者 iec104_serv.c:227。 -影响:公开 API 面越大,上层"手动自愈"路径越多,正是用户怀疑的"补丁式代码难维护"的来源;`net_relisten` 是 footgun。 -建议:`net_relisten` 删除(台架 Phase 13 改用 pump 语义或测试专用内部入口);`net_resync_listen` 在 F-NET-2 并入对账后删除;`net_can_accept` 在 F-NET-4 迁移后删除。 -**✅ 已实施(阶段 1,`b8c6a3d`)**:三 API 与 `NET_MSG_RELISTEN` / `NET_MSG_RESYNC_LISTEN` 消息类型全部删除(MCU + sim 双端),生产/测试调用者归零;Phase 13 的自愈段交回 net 层自动机制。**⚠ 遗留铁律**:门控迁移必须给 `net_listener_armed` 补 ESTABLISHED 放行,见 F-NET-4 实施注。 - -**F-NET-4 · iec104(:2404)仍用过期门控 `net_can_accept` → 丢 CONNECT 探针对该端口从不运行** -证据:iec104_serv.c:227 以 `net_can_accept`(只认 ESTABLISHED,net_socket.c:1434)门控 accept;`net_lost_connect_probe` 只在 `net_accept_locked` 内 LISTENING 态运行(:1298-1300),门控挡在门外则永不触发——与 N-32 P1 为 :7003 修复的"探针失效"完全同型(ui_net_sink.c:521-526 注释记载原委)。:2404 的丢 CONNECT 只剩对账②兜底,而②带 400ms 去抖 + 1s 冷却(TRAP-NET-20 已实证该节奏对短命监听器"救不全")。 -影响:104 上行端口的丢中断恢复被拖慢到秒级;三个常驻监听里 :21/:8888 无条件轮询 accept、:7003 已换 `net_listener_armed`,唯独 :2404 漏换。 -建议:iec104 门控换 `net_listener_armed`(双端 sim 已有实现);随后 `net_can_accept` 可退役,消除"两个相似门控语义不同"的长期困惑。 -**✅ 已实施(阶段 1,`b8c6a3d`)**:门控已迁 `net_listener_armed`,`net_can_accept` 三处(MCU / 头文件 / sim)删除。 -**⚠⚠ 实施中发现的坑(必读,实验方案 E10 有完整证据链)**:`net_listener_armed` 首版只放行 LISTENING / CLOSED+残留,**漏放行 ESTABLISHED**——而单连接模型下"连接已到、应用未 accept"的电平正是 ESTABLISHED(CONNECT 处理把监听 fd 自身转数据通道),也就是旧 `net_can_accept` 唯一放行的状态。后果:i104 200ms 任务周期抢不过对账② 的 ~400ms 去抖,对账②先 standalone accept 转 ESTABLISHED → armed=0 → 应用整场不 accept,**iec104 18/18 全 FAIL**(127s 跑满超时;设备日志:`sock1 standalone accept` 后整场无 `i104: accepted`,直至对端 FIN 才由 closed-with-data 救回)。ui_net_sink 之所以不受影响,是 10ms 周期 + `net_accept_locked` 自带 `net_poll` 通常在状态翻转前就拿到 fd——**属竞态幸存,不是设计正确**。修复:armed 放行 ESTABLISHED(此态 accept 必返 fd,一次调用即离开,无 RPC 风暴风险);修复后 18/18 PASS 且耗时 127s→8.3s。 - -**F-NET-5 · 两套重监听重试策略并存:D10 非阻塞 vs listen 路径 3×osDelay(500) 阻塞** -证据:net_listener.c:71-79(D10,注释明言旧 osDelay(500) 忙等致全网冻结 500ms、实测恢复 588ms);但 net_socket.c:1093-1104 `net_listen_locked` 仍是 open 失败→close→osDelay(500)→重试 ×3(最坏 netTask 冻结 1.5s)。设计文档 P2b 已登记该忙等并**台架依据暂缓**。 -影响:同一芯片沉降问题两套答案;首次监听/PASV 监听落在沉降窗时 netTask 停摆(回执全链排队)。 -建议:裁决"简化"——`net_listen_locked` 的失败路径改走 `net_listener_try` 登记 + D10 到点重试(P2b 范围,非紧急,但两套策略不应长期并存)。 - -**F-NET-6 · U12 不对称仍在:TCP_LISTEN 命令失败不 close 芯片 Socket** -证据:net_listener.c:214-222 open 失败→close→交重试;:227-230 LISTEN 失败→直接返 BUSY,**不 close**;net_socket.c:1112-1114 `net_listen_locked` 同样不 close。设计文档第 5 节 P2 曾列"补 U12 的 close 对称",第 9 节 P0 登记 U12 进 Trap 记录的复选框未勾选,代码亦未修。 -影响:LISTEN 失败后芯片处于 OPEN 但未监听;下一轮重试 open 返 USE 才走 close 分支兜回(多一轮 30ms + 一次 USE 竞态面);与 pump③ 防的"把正听着的 Socket 关掉"是同源机理的反向形态。 -建议:小修——两处 LISTEN 失败补 `ch395f_close_socket`,与 open 失败路径对称;同时把 U12 正式登记进 Trap 记录(P0 欠账)。 - -**F-NET-7 · 三处无条件"读清全局中断寄存器"仍在,会搁浅其它 Socket 的挂起事件——对账/pump 因此仍部分背锅** -证据:net_connect_locked net_socket.c:1668-1670、net_close_locked :2332-2334、relisten 隔离段 net_listener.c:176-178。B3 修复(4f2cc8b)只收窄了 listen/relisten 成功尾两处;Trap 记录 B3 行残余注记明确点名这三处"仍会搁浅(延后、非销毁)其它 Socket 挂起事件——它们是 TRAP-NET-12/-18 指定的两级预清,本次不动"。 -影响:全局寄存器整体读即清(手册 §5.15),这三处每次执行都可能吞掉别的在用 Socket 的 CONNECT/DISCONNECT 边沿位——即"丢中断"有一部分仍是**自家制造**,对账①②与 pump 的必要性里混入了这部分本可消除的噪声。 -建议:与驱动层审计(T2)合并处置:评估两级预清能否收窄为"只读本 Socket SINT"(B3 已证该做法成立且官方例程一律如此);若可行,对账①②的生产命中应同步下降(见 F-NET-8 的验证腿)。 - -**F-NET-8 · TRAP-NET-17 对账不是 B3 固件 bug 的补丁——裸机台架已独立实证芯片丢中断,故 B3 修复后对账不可删;但命中数应可测得下降** -证据:TRAP-NET-20 定案实验在**剥离 OS/net/FTP 的裸机芯片台架**上复现"握手完成但 CONNECT 中断被丢"(修复组 15/15,机制触发 5 次全救回);B3 修复在其后(4f2cc8b)。 -影响:直接回答本次审计的核心疑问——"根因已修的补丁是否仍必要":对 TRAP-NET-17 而言,其根因是芯片行为而非固件 bug,**必要**。 -建议:保留对账①②;补一条低成本台架验证腿:B3 修复前后各跑 `:21 100ms×50`,serial-assistant 统计 `reconcile:` 日志命中数,量化"自家制造的丢中断"占比,为 F-NET-7 收窄决策提供数据。 - -**F-NET-9 · "close + 排空 RX + 排空中断 + 等 CLOSED"在 net 层有两套变体实现——下沉驱动层的最佳候选** -证据:net_close_locked net_socket.c:2326-2345(close→循环 GET_SOCKET_STATUS + 读 SINT + 读全局→等 CLOSED,500ms 上限)与 net_relisten_locked 隔离段 net_listener.c:171-184(排空 RX→close→读 SINT→读全局→读状态→等 CLOSED/TCP_CLOSED + 最短等待)。两者语义同构(bit3=1 排空契约),参数不同(500ms vs 400/100/800ms),后者多排空 RX。 -影响:同一芯片契约两处实现,参数漂移风险(例如只有一处排 RX——close_locked 路径不排空 RX,依赖"close 前可读"契约由上层先排)。 -建议:裁决"下沉":在 CH395F 驱动层收口一个 `ch395f_close_flushed(sock, drain_rx, wait_ms)` 原语(具体签名归 T2 评估),net 层两处调用它并各自保留策略参数;BSD 层不再手拼命令序列。这是"让 BSD API 更主流"最实的一步。 - -**F-NET-10 · retry_tick 的 L3 保险只挡"在途挂起 recv",不挡"ESTABLISHED 无挂起"——与 pump 的状态门不对称** -证据:net_listener.c:400-412 重试循环无本层状态门(不看 state),仅 :405-408 跳过有挂起 recv 的 fd;对比 pump 明确只裁 LISTENING/CLOSED(net_listener.c:509-513),且 recv/accept/pending 三处即时重建都带内层 `state==CLOSED` 门(:1936/:2159 注释明言防 U12 同源事故)。 -影响:fd 在重试队列中且已因新 CONNECT 转 ESTABLISHED(无挂起 recv)时,到点重试会 close+open 摧毁新连接。可达性存疑(芯片不在听时新 SYN 难成,需 open 成/LISTEN 败的 U12 窗口配合),但不对称本身是隐患。 -建议:给 retry_tick 补与 pump 同型的状态门(仅 LISTENING/CLOSED 才重试),一行条件消掉整类竞态;补后 `net_pending_active` 保险可保留作第二道。 - -**F-NET-11 · 注释漂移:handle_disconnect_event 写"pump 每 50 ms 回读",实际 100ms** -证据:net_socket.c:3384("每 50 ms")vs net_listener.c:437(`NET_LISTENER_PUMP_PERIOD_MS 100U`,注释记载 50ms 曾把 uiTask 最坏单轮推到 1034ms 的实测)。 -影响:排障时按 50ms 预期判读日志时序会错判。 -建议:改注释;顺带该 LISTENING 分支(:3380-3399)现已纯留痕不动作,其 GET_SOCKET_STATUS 回读可裁决"简化"(pump 的 `chip not listening` 日志已含同等信息),删分支则少一条 SPI 与一处重复判读口径——低优先级。 - -**F-NET-12 · "RX 非空但 readable=0"无人对账(U15),兜底被推给上层的低频盲收——三层兜底中最隐蔽的补丁** -证据:net_can_read 注释自认"readable 仅由 RECV_OK 置位,TRAP-NET-17 对账不含 RX 非空但 readable=0 → 中断丢失时恒返 0,调用方须保留低频兜底 recv"(net_socket.c:1500-1501);ui_net_sink.c:552-558 与 iec104_serv.c:246-253 各自实现"每 32 轮无条件收一次";`net_listener_armed` 又用 `readable` 代理"芯片 RX 可能残留"(:1482-1485)。Trap 记录 B6/B10 一带将 U15 登记为未修残余。 -影响:数据面丢中断的兜底散落在两个应用任务里,net 层无对账——与"自愈内聚进 net/驱动层"的目标方向相反。 -建议:中期把"GET_RECV_LEN>0 但 readable=0"并入 pump 或独立低频对账(每 fd 一条命令,成本与 pump 同量级),随后上层 32 轮兜底可退役;短期保留现状但在两处上层注释里指向同一编号(U15),避免第三处复制。 - -**F-NET-13 · time_sync 每会话 close 监听 fd 全量重建,违反自家铁律,是应用层重建所有者的最大残留** -证据:time_sync.c:182 `net_close(conn_sock)`(单连接下 conn==listen)后外层循环 socket+bind+listen 重建(:224);设计文档第 7.4 节契约表已点名"违反『EOF 不得 close 监听 fd』→ close→重建沉降窗(TRAP-NET-09)+ 最坏 ~10s 端口下线"。对照正面样板:lftpd 控制通道明确不 close(lftpd.c:746-750/826-827)、iec104 EOF 分支带 `s_client != s_listen` 护栏(iec104_serv.c:259-262)。 -影响::8888 每次会话结束都主动制造一次 TRAP-NET-09 沉降窗;也是"应用层还能毁监听器"的活例。 -建议:裁决"简化(上层)":time_sync 会话收尾不 close,直接回外层在同一 fd 上 accept(lftpd 模式);iec104 `s_drop_conn` 的 close 属协议违例强制断连,语义正当,保留但注释标注为"有意例外"。 -**✅ 已实施(阶段 1,`b8c6a3d`)**:`net_close(conn_sock)` 删除,`ls` 提到外层循环跨会话常驻(仅网络代次变化/被毁时重建),`conn != ls`(模拟器多连接后端)才关数据 fd。台架实测连续 6 轮推送正常(周期 ~4s),每轮走 `standalone accept → DISCONNECTED, releasing → settle → auto-relisten ok`,零 `[ERR]`。契约变化已同步 `test/host/time_sync_server.py`(板子不再先关,PC 侧 2s 宽限后自关);⚠ 强杀 PC 进程会走芯片 `[ERR] TIMEOUT` + QUARANTINE 沉降(异常收尾,非正常路径)。 - -**F-NET-14 · D10 的"2s 放弃"不是终态,`relisten give up` 日志误导排障** -证据:net_listener.c:414-419 到期清队列 + DBG_ERROR;但 pump 在冷却后看到"芯片不在听"会再次 `net_listener_try`(net_listener.c:538-552),监听器实际会无限期重试(每 ~1.2s 一轮)。 -影响:日志出现 give up 时端口并非永久死亡,排障者可能误判严重度;同时"无限重试"缺少上限留痕(每轮一条 pump 日志)。 -建议:give up 文案改为"重试预算耗尽,交 pump 周期接管";或把 pump 的重触发也纳入同一预算计数(二选一,倾向改文案——保留最终兜底)。 -**✅ 已实施(阶段 0,`4c0a0af`)**:文案改为 `relisten retry budget exhausted, pump takes over`;同批补 0-4 retry_tick 状态门(仅 LISTENING/CLOSED/BOUND 之外的状态清陈旧登记,防 open→close 摧毁活连接)。 - -## 4. 重叠/兜底关系速览(谁兜谁) - -- 监听器重建:pump(周期唯一)→ 失败落 D10(30ms×2s)→ D10 放弃后 pump 冷却再触发(无限);消费者三处即时重建把 CLOSED 窗口压到 ~0(防上层 RPC 空转)。 -- 丢 CONNECT:探针(accept 内联,快)↔ 对账②(周期,兜底网);两者共享幽灵甄别;:2404 因门控过期只剩②(F-NET-4)。 -- 丢 DISCONNECT/静默回收:对账①(芯片 CLOSED 形态)↔ 对账⑤(芯片回 LISTEN 形态,阶段 1 起自动认领 = F-NET-2 已闭环)。 -- 会话结束感知:session_dead 电平(P1)兜住"抢先 relisten 吞 EOF";pending_tick ③ 与 recv DONTWAIT 双路径给 EOF;看门狗兜"完成逻辑缺位"。 -- 芯片收尾残留:TRAP-NET-18 沉降/隔离(重建前)↔ close_locked 排空循环(显式关闭)↔ listen/relisten 成功尾读本 Socket SINT——三处清理纪律,两套命令变体(F-NET-9 下沉候选)。 -- 数据面丢中断:RECV_OK 电平重放(TRAP-NET-08)+ readable 门控 + 上层 32 轮盲收(F-NET-12,唯一未内聚的一层)。 - -## 5. 裁决汇总 - -| 机制 | 裁决 | 一句话依据 | -|---|---|---| -| auto_relisten / session_dead / released_abnormal / 幽灵甄别 / closed-with-data / 探针 / 对账①② / pump / D10 / TRAP-NET-18 沉降隔离 / L3 pending + 看门狗 | **保留** | 根因是芯片行为或已实测裁决的架构解;TRAP-NET-20 裸机台架是"不可从根上消除"的判据 | -| net_relisten | **可删** | 生产零调用 + 对健康监听器是 footgun | -| net_resync_listen | **可删**(先并入对账①) | 语义正确但载体错位:应是自动对账,不是手动 API | -| net_can_accept | **可删**(先迁 iec104 到 armed 门控) | 过期门控致 :2404 探针失效 | -| handle_disconnect_event LISTENING 分支回读 | **简化**(可选) | 已纯留痕,pump 日志可替代 | -| net_listen_locked 3×500ms / net_close_locked 500ms 忙等 | **简化**(P2b,暂缓有据) | 与 D10 策略统一;等 task stall 因果证据 | -| U12 LISTEN 失败不 close | **修复**(小) | 一行对称补齐 | -| retry_tick 状态门 | **修复**(小) | 与 pump 对称,一行条件 | -| 三处全局读清残留 | **下沉/收窄**(与 T2 合并) | B3 已证单 Socket 收窄可行 | -| close+排空+等 CLOSED 命令序列 | **下沉驱动层**(与 T2 合并) | 两套变体一套契约 | -| 上层 32 轮盲收兜底 | **下沉 net 层**(中期) | U15 并入对账后上层退役 | -| time_sync 会话收尾 close | **简化(上层)** | 改复用同 fd accept(lftpd 样板) | -| GRACEFUL 沉降门 `recv_bytes≠0` | **修正(一行级,见 F-NET-15)** | E1 背靠背失效循环的放大器;门的原保护目标已被 N-32 pump 延迟化架空 | - -## F-NET-15 背靠背数据连接失效循环(E1 发现 B 代码级根因,T5) - -失败签名(完整时间线见 实验方案.md 的 E1 结果一节):`n30_repro` 背靠背 STOR 4/50、复跑 1/10 成功,其余 425;sock4(FTP 数据监听)上数据连接 accept 后 ~20ms 被上一条连接的陈旧 DISCONNECT 判死(`DISCONNECTED, releasing (chip st=0x05/0x00)`),被杀会话 `recv_bytes=0` → 不登记 GRACEFUL → 立即 relisten → 下一条连接继续被杀(自持循环);closed-with-data accept 偶发救回 2 次但被 lftpd 镜像门控错过。 - -### Q1 · D20 陈旧甄别为何在芯片 TCP=0x00 时放行 - -判据本体:net_socket.c:3404-3415(回读 :3408,判别 :3410)—— 单一规则:芯片 TCP==0x04(ESTABLISHED)判陈旧丢弃,**否则(含 CLOSE_WAIT/CLOSED)一律按真断开**;CLOSED 是刻意放进"真"集合的(注释 :3403「真断开时芯片处于 CLOSE_WAIT(0x07)/CLOSED(0x00) 等状态照常处理」)。 - -放行的根因是**判据的证据通道被事件自身污染**: - -1. 滞后 DISCONNECT 在**拆链收尾完成时刻**锁存(TRAP-NET-12:DISCONNECT 在 FIN/ACK 完成后才上报),而拆链收尾完成的标志正是把 TCP_CLOSED(0x00) 写进该 Socket 的状态寄存器。事件投递与寄存器写 0x00 是同一时刻的两个面——甄别读到的 0x00 无法区分「真断开」与「陈旧事件 + 新连接仍活着」。 -2. 每 Socket 只有一个状态槽,存的是**最后一次状态迁移**(TRAP-NET-20 已证该寄存器在拆链窗口内双向不可信:既会假报 ESTABLISHED 幽灵,也会假报 CLOSED——新连接的 0x04 迁移被旧连接的 0x00 迁移覆盖)。E1 签名 `st=0x05/0x00` 即:Socket 仍 OPEN(0x05,被杀连接在芯片侧还活着),TCP 槽显示旧拆链的 0x00。 -3. **事件无连接身份**:芯片事件位不带连接 ID;幽灵甄别那套对端端口比对(:597-604、:1249)在此不可用——新握手已把芯片对端寄存器覆写为新端口,两种假设下都读到 `peer == remote_port(新) ≠ last_peer_port(旧)`,无判别力。 -4. 投递时序吻合:net_poll 同一次 SINT 读(:469)内 CONNECT 先于 DISCONNECT 派发(:480-489);`handle_connect_event` 在 accept 尾部读清 SINT(:3314-3316),所以致死位必须在 accept **之后**锁存——正是拆链收尾完成时刻(10~20ms,TRAP-NET-18 二阶段与 TRAP-NET-20 现象 3 均有记载)。新连接自己的握手 ACK 流量往往正是完成旧拆链最后一次 FIN/ACK 交换的那笔流量 → 背靠背强相关。 - -结论:D20 在该时刻**结构性失明**——它的规则是在 TRAP-NET-16 时代标定的(当时事件先于寄存器写投递,芯片显示 0x04 可辨)。剩余可用判别轴只有两条:时间上的电平确认(对账去抖语义)或"年轻连接+零数据"启发式。 - -### Q2 · GRACEFUL 沉降门 `recv_bytes≠0` 对失效循环为何失效 - -门本体:net_socket.c:3424 `recv_bytes != 0U`(外层已是 ESTABLISHED 状态门 :3400)才登记 GRACEFUL(:3430);不登记则下一次 relisten 跳过整个隔离段(net_listener.c:149 以 `quarantine != 0` 为门)。 - -1. **受害者类别由事件时序定义,与数据无关**:连接在 accept 后 ~20ms 被杀,MCU 尚未读任何字节 → `recv_bytes==0` 是构造性必然(即便客户端已把 STOR 数据灌进芯片 RX——门统计的是 MCU 已读字节,不是芯片 RX 存量)。门按"收过数据"划危险类别,恰好把每一任受害者排除在外。 -2. **循环自持的关键:每一轮都重新制造残渣**。被杀连接在芯片侧仍 OPEN 活着(st[0]=0x05);下一次 relisten 的 open 返 USE → 失败分支 close(net_listener.c:214-222)→ 在芯片侧摧毁该连接 → 新拆链 → TRAP-NET-12 锁存新的滞后 DISCONNECT → 下一条连接再被杀。最初的残渣只是种子,循环靠"杀→close→拆链→滞后事件"自我繁殖。 -3. **为什么登记沉降就能断环**:登记后 relisten 走隔离段——提前 close 芯片 Socket(:174)、每 30ms 重试轮读清 SINT(:176)、≥100ms 保持期满才 open+listen(:182-197)。滞后 DISCONNECT 在"芯片 Socket 已关、本层无会话"的窗口内被消费,无害(对照:落在 LISTENING 上也只是被动留痕,net_socket.c:3380-3399)。 -4. **门的原保护目标已被架空**:注释 :3420-3422 写明门是为保住 TRAP-NET-17 的 `:21 @0ms×50` 快速复连时序而设(零数据连接免 100ms 沉降)。但 E1 发现 A 实测:N-32 P2a 之后**每次**会话收尾都要等 pump(115–900ms)+ D10,恢复窗 0.3–1s,`:7003` 复连 p50=1020ms(SYN 静默丢 → 客户端 RTO)。门如今省下的 100ms 落在 0.3–1s 的窗里不可见,换来的却是 P0 失效循环。 -5. 对照佐证:TIMEOUT 路径的 QUARANTINE **无条件**登记(net_socket.c:3517-3524)——所以 TRAP-NET-18 的 RST 中止修复从未循环过;循环是 DISCONNECT 有门路径独有的。 - -### Q3 · closed-with-data 救回后 lftpd 镜像门控为何错过 - -门本体:lftpd_inet.c:116-120 —— 每次 accept 失败后 ftpTask **直读** `p_sock->state`(跨任务裸镜像,F-NET-12 家族),非 LISTENING 立即 `return -1` → 425(预算 5s,lftpd.c:369/401/532/599)。 - -1. 每轮 `net_accept` 内部先走 CLOSED 分支(net_socket.c:1316-1328):RX 有数据 → closed-with-data 救回(返 fd);否则即时 relisten。所以镜像检查看到的是 accept 之后的状态:relisten 成功 → LISTENING(继续轮询);**relisten 失败 → 仍 CLOSED → 立即中止**。 -2. 而 relisten 失败恰恰集中发生在芯片拆链忙期(open 返 USE/BUSY)——镜像门在**恢复正在进行、剩余预算最值钱**的时刻中止,把最多 ~4.9s 预算直接扔掉。N-32 不变量 II 已把 `CLOSED + auto_relisten` 定义为"监听器仍在服役"(`net_listener_armed`,net_socket.c:1459-1489),lftpd 数据门控早于该语义、未迁移。 -3. 救回窗口本身很窄:被杀 → pump/D10/accept 轮在 ~30–130ms 内 relisten,其 close 顺带清掉芯片 RX;lftpd 100ms 轮询通常落在 RX 已清之后 → 救回输给 relisten(E1:46 败仅 2 救)。 - -### 因果链(一轮迭代,全部落行号) - -1. conn N accept:CONNECT → ESTABLISHED,SINT 预清(net_socket.c:3314-3316)。 -2. 旧拆链于 10~20ms 后完成(常由 conn N 自身 ACK 流量促成):状态寄存器写 0x00 + 锁存 DISCONNECT。 -3. net_poll 派发 → D20 读 0x05/0x00 → 判真断开 → CLOSED/session_dead/last_peer_port(:3434-3444);`recv_bytes==0` → 不登记 GRACEFUL(:3424)。 -4. relisten:open 返 USE(Socket 仍 OPEN 且 conn N 活着)→ close(net_listener.c:218)→ conn N 被芯片侧摧毁 → 拆链 N 滞后锁存;30ms 后 D10 重试 open+listen 成功(:262 成功尾 SINT 只清"已锁存"位)。 -5. 客户端下一条命令重连 → CONNECT → ESTABLISHED;拆链 N 完成 → DISCONNECT N → 回到第 2 步。 -6. 期间 lftpd accept_timeout 轮询撞上"relisten 失败的 CLOSED" → 镜像门中止 → 425。 - -### 最小修复候选(按侵入度递增) - -- **M1(一行级,net 层,首选先手)**:net_socket.c:3424 删去 `recv_bytes != 0U` 门(使 :3430 的 GRACEFUL 登记无条件化,外层 ESTABLISHED 门 :3400 保留)。断环位置=因果链第 3 步:登记后第 4 步自动走隔离段(提前 close + 逐轮 SINT 排空 + ≥100ms 保持),下一条连接出生干净。代价:每次会话收尾恢复窗 +100ms——相对实测 0.3–1s 的 pump 主导窗为边际量。必跑回归:`:21 @0ms×50`(门当初的守护用例)、`ftp_regression` 11/11、`ftp_abort_probe` RST/FIN、`n30_repro --n 50`。 -- **M2(一行级,应用层)**:lftpd_inet.c:117 改判 `net_listener_armed(socket) == 0`(模拟器双端已有实现)。让 5s 预算穿过"恢复中的 CLOSED"窗口,救回/pump 重建得以在窗内落地;同时消灭一处裸镜像。注意 M2 单独不杀循环(连接仍被杀),只把"立即 425"换成"预算内恢复";与 M1 合用后循环死亡,M2 退化为时延/健壮性修正。 -- **M3(结构性,阶段 1)**:D20 年轻连接电平确认——对"刚转 ESTABLISHED 且零数据"的连接上的 DISCONNECT 不吃边沿,交给对账①电平确认(200ms×2 去抖):连接真活则下轮读到 0x04 存活;真断则 ~400ms 内由对账①释放。需一个"年轻"标记(复用 `lost_conn_seen` 或加 accept 时刻字段)。直击"杀"本身,改动面大于 M1,建议 M1 台架数据出来后再做。 -- **M4(供给面,即 F-NET-7)**:三处全局读清(net_socket.c:1668-1670、:2332-2334,net_listener.c:176-178)收窄为单 Socket SINT 纪律。全局位被丢弃式读清 + SINT 粘滞 = **搁浅**:滞后 DISCONNECT 可被推迟到任意未来连接上投递,把有界的 10~20ms 危险窗变成无界。实验方案 E1 的 G2 归因即此。M1 不依赖 M4 生效,但 M4 缩小危险供给面。 -- **M5(结构性,即 F-NET-9)**:`sock_reset` 复合原语(close+排空 RX+排空中断+沉降+relisten 收进一个驱动层事务)让沉降**无条件且内联**,同时消掉发现 A 的 pump 延迟窗(`:7003` p50 1020ms 的根源)——M1 的 +100ms 代价在该形态下归零。 - -### 与此前发现的关系 - -- **F-NET-7**:全局读清是本循环的**供给放大器**(把滞后事件从"有界 10~20ms"放大为"无界搁浅");M1 是需求侧止血,M4 是供给侧收口,两者正交。 -- **F-NET-9**:M1 是在现有"两套沉降变体"之上打的一行补丁;F-NET-9 的复合原语是同一方向的深水区——"沉降无条件化"在事务化后不再需要门来平衡时延。一行级修正与下沉原语不矛盾,是同一裁决的先后两步。 -- **F-NET-12**:lftpd 镜像门控是"上层直读 net 状态镜像"的第一例**实测 P0 代价**,为 F-NET-4 的 armed 门控迁移提供了数据支撑。 -- **F-NET-11** 结论仍立:落在 LISTENING 上的滞后 DISCONNECT 无害(被动留痕);循环需要它落在 ESTABLISHED 上,即投递必须晚于 accept——这正是拆链完成时刻的投递形态。 - -### 回归集建议 - -把 `n30_repro`(背靠背 STOR)纳入常备回归集——TRAP-NET-20 终版回归只含有间隔的 `ftp_regression`,背靠背形态不在集内,这是本次 9/10 能潜伏的测试面原因(实验方案对照台账已登记)。验收线建议:`n30_repro --n 50` 通过数与 `:21 @0ms×50`、`ftp_regression`、`ftp_abort_probe` 同时全绿,由台架定具体门槛。 - -## F-NET-17 残余偶发的真身:风暴态(E8 深查,2026-10-13) - -E7b 的"残余 ~3%"不是稳定值:同固件同负载,故障率随设备状态在 **2%(全新启动)~ 54%(残渣累积)~ 12%(仅复位)** 间摆动(实验方案 E8 矩阵)。"偶发"的真身是**可自持的风暴态**,三个缺陷叠加: - -1. **沉降自再生锁存**(M1 的硬币):沉降段 `net_listener.c:174` close 之后,拆链收尾在 ~10-20ms 后(TRAP-NET-12 延迟锁存)**重新置位** DISCONNECT;`:176` 的 SINT 读清与 close 同轮发生,读不清这枚迟到的锁存 → 沉降最后一轮"清干净"的假象下,relisten 完成后 11ms 首条连接被杀。30ms 重试粒度 vs 10-20ms 锁存延迟 = 每次重建一次硬币——E7b 的 2% 即此。 -2. **泵与救回互抢**(风暴燃料):沉降段 `:172` `net_drain_chip_rx` 排空的正是 closed-with-data 救回(net_socket.c:1317)依赖的 RX 数据;救回 accept 成功返回 fd(:1352)但会话随即被杀/数据被排空 → lftpd 短读 → 425。E8 窗口 31 次 accept timeout 与 38 次救回并存为直接证据。 -3. **芯片 OPEN 卡死放大**(自持结构):被杀会话芯片侧仍 OPEN(0x05),新 SYN 被接进卡死 socket(TCP 槽 0x00)→ accept → 下一枚锁存再杀 → 直到 800ms 强制放行 + close 真正落拆链才出簇。风暴期时间线(0.75s 内 3 轮"救回→杀→救回")见实验方案 E8 节。 - -**设计修正(覆盖 T5 版 M3/M5 表述)**: -- **M3 判据**:`chip=0x05/0x00`(Socket OPEN + TCP 槽 0x00)+ 连接年轻(<1s)= 逻辑矛盾态(活连接应 0x04)→ 不吃边沿,交对账①电平去抖二次确认。风暴窗口 100% 命中、健康路径(0x00/0x00 或 0x07)零误伤。 -- **M5 语义**:`sock_reset` 必须 **drain-until-quiescent**(close 后轮询 SINT 直到 DISCONNECT 锁存出现并读清、连续两轮无新锁存才 open+listen);固定时间窗在锁存延迟抖动下必输硬币。 -- **新红线**:RX 有数据 = 消费者资产——沉降/pump 重建前必须让位 accept 救回路径,排空后才准 reset;救回与重建必须单一所有者(`sock_reset` 内联裁决)。 - -## F-NET-17 补充:风暴态三机制代码级核实与 M3/M5 设计(T6,2026-10-13) - -> 行号 = 提交 9ac9879(M1+M2+M4-lite)后的实测行号。只读核实,未改产品代码。 - -### T6-① 「close 已 CLOSED 的芯片 Socket 产生 DISCONNECT 锁存」——基石**修正**而非照单确认 - -**手册口径**:§5.35「关闭 Socket 前 CH395 会自动断开 TCP 连接」(Trap 记录定案更正引用)——close 的动作序是"先断链再关 Socket";若 TCP 已 CLOSED,断链环节无事可做。§5.52② bit3=1 只豁免 DISCONNECT/TIMEOUT 场景的自动关闭/清缓冲,**不豁免 CLOSE_SOCKET 命令本身**(close 仍清缓冲——net_socket.c:1311 注释"close+open 清缓冲"同此)。SINT bit4=DISCONNECT(ch395f.h:268)。 - -**证伪强形式(干净情形)**:TC-NET-204(TRAP-NET-07 定案更正,Trap 记录实测表)模式 C 正是本问题的受控实验——DISCONNECT 已发生、芯片停在 `0x05/0x00`(与风暴签名逐位相同),补 `close` → **+0 ms 回 CLOSED**、重开重连 5/5 OK;且"15 轮重开均未观察到残留 DISCONNECT(0x10)——close 顺带清掉了锁存"。即:**close 一个已 CLOSED 的 Socket 不产生新锁存,反而顺带清掉已锁存的位**。模式 A(close 一条活连接,主动拆链)5 轮同样无残留锁存。 - -**与 TRAP-NET-12 的张力消解**:TRAP-NET-12 的残留锁存不是 close 命令制造的,而是**拆链完成事件**——"DISCONNECT 可能在与 CLOSED 状态达成几乎同时、甚至之后才锁存"(Trap 记录 TRAP-NET-12 根因节),粘滞跨会话(B3 条目"SINT 粘滞到被读")。锁存的生成者是拆链过程(多为对端 FIN 的被动拆链收尾),不是 close 命令。 - -**风暴窗口的直接反证**:E8 时间线里 kill 发生在 `auto-relisten ok` **之前**(窗口内 relisten 尚未完成,沉降段 close 还没跑或没再跑)——那几枚 11ms 杀连接锁存**不可能由沉降段 close 产生**。真实来源两条:(a) 上一条 STOR 的客户端 FIN(FTP 正常行为,发完即关,TRAP-NET-20 定案)的被动拆链收尾迟到完成(TRAP-NET-12/-18 的 10~20ms,TRAP-NET-20 的 >2s 不可观测窗);(b) 卡死 socket(0x05/0x00)把新 SYN 接进来又自行拆掉(`docs/芯片手册/CH395F.md` §5.29 语义域),每轮自产一对 CONNECT/DISCONNECT。 - -**但"沉降末轮 SINT 读清不能保证静默"的实操结论仍成立**,经两条路径: -1. 拆链完成滞后于末轮读清(lead 说的硬币——生成者是拆链,不是 close); -2. **搁浅**:剩余两处全局读清(net_socket.c:1670 `net_connect_locked`、:2334 `net_close_locked` 等待环)焊死 net_poll 全局门(:441→:464)→ 锁存被搁到下一条连接的 CONNECT 全局位被读到那一刻,net_poll 同轮读 SINT 见 CONNECT|DISCONNECT 双置 → 先 CONNECT(:480-483)后 DISCONNECT(:486-489)→ accept 后毫秒级被杀。这是"锁存跨越整个沉降窗落到下条连接"的投递机制(F-NET-7 残余的实测代价)。 - -**基石判定**:现象成立、机制表述须修正——"close 已 CLOSED Socket 会再锁存"应改写为"**TCP 槽 0x00 不证明拆链已完成;拆链完成事件滞后且粘滞;close 命令本身对死 Socket 不产生锁存(TC-NET-204 模式 C),对活连接的主动拆链在该实验窗口内亦无残留(模式 A)**"。此修正直接改写 M5:静默判据必须是"**等到预期锁存出现并读清 + 连续静默轮**",而不是"close 后假设会生成锁存然后清掉";且静默可被新连接自身流量打破(无界窗)→ **M3 是结构性必需,不是可选加固**。 - -### T6-② 泵与救回抢同一 RX 缓冲——**证实**,排空后救回判据必然失败(对已存在数据) - -- 同一缓冲:`net_drain_chip_rx`(net_listener.c:102-121)经 GET_RECV_LEN+READ_RECV_BUF 消费(读即推进);closed-with-data 救回判据 net_socket.c:1317 `ch395f_get_recv_len(sockfd) > 0` 读同一寄存器源。两者都在 netTask 上下文(accept RPC 与 pump/relisten 串行)→ **不是数据竞态,是先后竞态**:谁先跑谁定生死。 -- 排空后 `recv_len==0` → :1317 判据对"排空时已存在的数据"**必然失败** → accept CLOSED 分支落到 :1327 即时 relisten(同样进沉降段)→ 资产二次销毁(close 清缓冲)。唯一例外:引擎仍活、客户端续发,可在两次排空间隙回填 RX——但沉降段每 30ms 排一次(:172 每重试轮)、lftpd 100ms 轮询一次 → 泵压倒性赢。 -- E8 佐证:风暴窗 31 次 accept timeout 与 38 次救回并存 = 竞态双峰的直接证据(赢一次的救回拿到的也常是被排空半份的数据 → 短读 425)。 -- 结论:lead 的新红线成立。裁决点收进 sock_reset 第一步(见 T6-④ QUIESCE 第 1 步):RX>0 且芯片 st[0]==OPEN 且本层 CLOSED+auto_relisten → **让位 accept**(返 BUSY 不排空),RX==0 或强制放行后才准排空+reset。 - -### T6-③ M3 最小补丁设计(一行级判据 + 一行级前置清零) - -**字段选型:复用 `lost_conn_seen`(net_types.h:190)作 ESTABLISHED 期"可疑电平"(0/1),零 RAM**。 -- 生命周期兼容:探针只在 LISTENING 期跑(accept 门 :1298),与 ESTABLISHED 期不重叠;relisten(net_listener.c:249)/pump③(:538)/listen(:1121)/RESYNC(:792)已有清零点。 -- **前置一行(必须)**:`handle_connect_event` 现**不清** lost_conn_seen(grep 实测:清零点无此函数)——LISTENING 期探针残留的计数会带进新连接,使可疑判据首轮即被旁路、M3 失效。须在两个转 ESTABLISHED 分支(:3306/:3344 附近)补 `p_sock->lost_conn_seen = 0U;`。 -- 备选(新增 `uint32_t estab_ms` 支持"<1s 年轻窗",32B/8 sock)不推荐:矛盾判据本身即判别器,flag 比时钟更准——滞后锁存同样能杀老年连接,age 窗会漏;代价仅为真 RST 释放也延 ~400ms(可接受,见误伤分析)。 - -**判据插入点**:`handle_disconnect_event` ESTABLISHED 分支,D20 甄别(:3410-3415)之后、释放留痕(:3418)之前——`st[]` 已在 :3408 读过,**零额外 SPI**: - -```c -if ((st[0] == CH395F_SOCKET_OPEN) && (st[1] == CH395F_TCP_CLOSED) && - (p_sock->lost_conn_seen == 0U)) { - p_sock->lost_conn_seen = 1U; - DBG_WARN("sock%d suspect DISCONNECT on live conn (chip 0x05/0x00), defer to reconcile", sockfd); - return; -} -``` - -**交对账①的衔接语义——现有代码可直接认领,对账零改动**:对账①判据(:588-589)`ESTABLISHED && (st[0]==CLOSED || st[1]==CLOSED)`,0x05/0x00 经 `st[1]==CLOSED` 命中;去抖 `s_bad` 连续 2 轮(200ms×2≈400ms)后复用 `handle_disconnect_event`(:633)再入——此时 `lost_conn_seen==1` 旁路可疑判据 → 正常释放(GRACEFUL 登记、session_dead、CLOSED、回调全套不变)。双锁存场景(第二枚边沿到来)同样因 flag 已置而直接释放:双锁存=强证据,不再三确认。 - -**去抖期间的状态摆位——保持 ESTABLISHED、session_dead 保持 0,这正是收益所在**: -1. lftpd accept 轮询见 ESTABLISHED → :1305 门通过 → **accept 正常返 fd**(不再依赖 closed-with-data 救回); -2. recv 快路径无状态门槛 → 消费者在 400ms 租约内排空 RX(引擎活则数据续到); -3. 对 n30 的小段 STOR:400ms 租约足以让传输完成 + 客户端正常 FIN(0x07 健康释放)→ **风暴链被"会话正常完成"掐断**; -4. 槽恢复 0x04 的形态(TRAP-NET-16 时代可恢复类):对账① bad=0 → 连接存活,flag 由 relisten/connect 清; -5. 引擎真死:消费者排空后 WOULDBLOCK,对账①释放后得 EOF——EOF 晚 ~400ms,语义不变。 - -**:21 @0ms 快复连为何零误伤**: -1. 健康 FIN 释放:芯片处 CLOSE_WAIT(0x07)(bit3=1 芯片不关 Socket,CLOSE_WAIT 等 MCU 关——D20 注释 :3403),判据要求恰 `0x05/0x00`,0x07 不命中; -2. 芯片静默回收形态 `0x00/0x00`:判据要求 st[0]==0x05,不命中; -3. 真 RST(恰 0x05/0x00)会命中可疑 → 代价仅释放延 ~400ms(对账①两轮),不是错状态;且 @0ms 复连时序已被 pump 窗主导(E7b `:7003` p50=1011ms),400ms 落在窗内不可见; -4. E5 健康 FIN 20/20 样本走 ①②的形态,均在可疑集合之外。 - -**成本**:判据 4 行 + connect 清零 1 行 + 1 条 DBG(~90B Flash),零 RAM。 - -### T6-④ M5 `sock_reset` drain-until-quiescent 状态机草案(阶段 2) - -替换现三数组 `s_relisten_quarantine/_settle_start/_settle_dead`(net_listener.c:150-208)为 per-socket 相位机;五个重建入口(pump/D10/accept CLOSED 分支/recv EOF/pending③)已全部收敛到 `net_listener_try`,状态机天然单所有者。 - -``` -IDLE ──(settle 登记或 try 进入)──▶ CLOSE - CLOSE: 发一次 CLOSE_SOCKET(TC-NET-204:顺带清已锁存位,保留此动作);t0=now;latch_seen=0;quiet=0 -QUIESCE(每 30ms 重试轮,非阻塞): - 1. RX 裁决(红线): GET_RECV_LEN>0 且芯片 st[0]==OPEN 且本层 CLOSED+auto_relisten - → 让位 accept:返 BUSY,本轮不排空不 close - 否则 → net_drain_chip_rx 排空 - 2. GET_INT_STATUS_SN 读即清: 见 DISCONNECT 位 → latch_seen=1, quiet=0; - 否则若 latch_seen → quiet++ - 3. GET_SOCKET_STATUS: 记 st - 4. 静默成立 := st[0]==CLOSED - && (latch_seen || now-t0 >= LATCH_GRACE_MS) // 等过锁存延迟窗 - && quiet >= QUIET_ROUNDS // 连续两轮无新锁存 - && RX==0 - && now-t0 >= hold_min // GRACEFUL 100 / QUARANTINE 400(沿用) - → OPEN - 5. now-t0 >= RESET_MAX_MS → 强制放行 OPEN(留痕,沿用现兜底) -OPEN: SET_PROTO_TYPE/SET_SOUR_PORT/OPEN/TCP_LISTEN + 成功尾(现 :207-267 不动); - 失败 → D10 30ms 重试,每轮先回 QUIESCE 第 2/3 步轻量复核(1 条 SINT + 1 条状态) -``` - -- 参数建议:`LATCH_GRACE_MS=50`(实测锁存延迟 10~20ms,×2.5 余量)、`QUIET_ROUNDS=2`(2×30ms)、`RESET_MAX_MS=800`(保持"必须 < 2000 放弃时限"规则)、`hold_min` 沿用两级。 -- **诚实边界(来自 T6-① 修正)**:静默不是有界保证——拆链完成可被**新连接自身流量**触发(TRAP-NET-20 窗口 >2s 不可观测),状态机只能静默"已在途的拆链";"未来流量触发的拆链"只有 M3 的电平确认能接住。**M5 与 M3 互补成对,不是替代关系**:M5 消灭"沉降窗内可静默"的绝大多数锁存(E7b 的 2% 硬币),M3 兜住无界残余(风暴态)。 -- RAM:三数组(uint8 + 2×uint32 ≈ 9B/sock)→ 相位 uint8 + t0 uint32 + 打包 flags ≈ 6B/sock,微负,预算友好。 -- 与 T6-② 的闭环:QUIESCE 第 1 步即红线裁决点——"泵与救回互抢"在状态机内单点裁决,五入口不再各自竞速。 diff --git a/docs/网络/N34-自愈审计/主流方案对照.md b/docs/网络/N34-自愈审计/主流方案对照.md deleted file mode 100644 index 50c7dee3..00000000 --- a/docs/网络/N34-自愈审计/主流方案对照.md +++ /dev/null @@ -1,162 +0,0 @@ -# N34 自愈审计 · T3 主流嵌入式 socket 栈对照研究 - -> 审计组发现文件(mainstream-benchmark)。性质:web 检索 + 一手源码阅读 + 本项目代码对照。 -> 结论先行:**五层自愈补丁不是"芯片怪癖的必然代价",而是"软件镜像 + 纯事件驱动 + 决策不看芯片"这一组合的补偿税**。 -> 主流方案(含同为硬 TCP 栈的 WIZnet 官方库)没有一处维护第二份 socket 状态、也没有一处做周期对账; -> 芯片怪癖(丢中断、close 沉降、RX 残渣)在任何架构下都存在,但主流把它们**内联进事务、收在驱动层、以有界超时兜底**, -> 而不是在 net 层养一组常驻定时器。 - -## 1 研究范围与方法 - -- 一手源码:WIZnet ioLibrary_Driver `socket.c`(全文通读)、Zephyr `eth_w5500.c` + `eth_wiznet.c`(全文)、 - FreeRTOS-Plus-TCP `FreeRTOS_Sockets.c`(accept/listen/reuse-socket 路径)、mbed WIZnetInterface - `WIZnetInterface.cpp`(socket_listen/socket_accept/socket_bind 路径)、WIZnet 官方 lwIP 移植 - `w5x00_lwip.c`、PeterBorisenko `spiif.c`(W5500+lwIP+FreeRTOS)。 -- 文档:W5500 官方 TCP 应用笔记、W5500 socket API 文档、CH395 数据手册、FreeRTOS+TCP API 参考、Zephyr 文档。 -- 本项目对照:`net_socket.h` 公共 API 全量、`net_listener.c` 模块头与策略常量、 - `BSD_Socket_API_使用指南.md` 第 3.6 节与第 12.10 节(五层自愈现状表)。 - -## 2 各主流方案状态同步机制对照 - -| 方案 | 状态事实源在哪 | 事件 vs 轮询 | 谁拥有状态机 | 监听/accept 语义 | 兜底机制 | -|---|---|---|---|---|---| -| **WIZnet ioLibrary**(W5500/W6100,硬 TCP 栈,与 CH395F 同类) | **芯片 Sn_SR 寄存器,每次调用直接回读**;软件里只有传输记账(`sock_io_mode`/`sock_is_sending`/`sock_remained_size`/`sock_pack_info`),**无 socket 状态镜像** | 调用内阻塞轮询(`while(getSn_CR)`、`while(getSn_SR != …)`);Sn_IR 标志可读可清,INT 引脚电平驱动、可选 | **芯片**(硬件 TCP 状态机);host 端没有状态机 | **没有 accept()**——监听 socket 本身就是连接 socket(与本项目单连接复用同形);重监听 = 应用主循环每轮 `switch(getSn_SR)` 回读后 open/listen | 无需对账(无镜像可对不上);命令完成/状态迁移一律有界轮询 | -| **W5500 + lwIP**(官方 RP2040-HAT-LWIP-C、PeterBorisenko、d-a-v、QNEthernet) | 芯片**不持有 socket 状态**(socket 0 配成 MACRAW,纯网卡);socket/TCP 状态全在 lwIP pcb | 驱动任务 = INT 引脚信号量 + **500 ms 超时兜底**(超时则清 IR + 回读 PHYCFGR 对账链路) | **lwIP**(软件栈单一所有者) | 标准 BSD:listen pcb 常驻,`tcp_accept` 回调交付**新 pcb** | 驱动层超时兜底,netif 层无感 | -| **mbed OS WIZnetInterface**(硬 TCP 栈 + nsapi BSD 门面) | **芯片 Sn_SR**(`is_connected`/`is_closed` 每次回读芯片) | accept 内轮询 `is_connected` 带超时;无状态镜像 | **芯片**;门面只翻译 API | **accept 内联重建监听**:连接继承原芯片 socket,随即 `new_socket()`+bind+listen 换一颗芯片 socket 继续监听(用 8 颗硬件 socket 模拟"监听常驻") | 重建失败处理粗糙(注释"监听已不存在"仍返回 0)——说明内联重建仍需有界重试,但重试在同一事务/模块内 | -| **FreeRTOS+TCP**(纯软件栈) | pcb 内 `eTCPState`,**唯一写者 = IP task** | 每 socket 一个 `xEventGroup`(`eSOCKET_ACCEPT`/`eSOCKET_CONNECT`/…),调用方阻塞在事件上,不轮询 | **IP task(单所有者)** | 默认:listen socket 常驻,accept 生成**子 socket**;`FREERTOS_SO_REUSE_LISTEN_SOCKET` 模式:监听 socket 自身转连接(与本项目同形),回 LISTEN 由栈状态机处理 | 无对账(状态即事实);reuse 模式 `listen()` **同步清空 rx/tx 流并复位状态位** | -| **Zephyr**(原生 net 栈 + WIZnet 驱动) | 驱动 = **MACRAW 网卡**(socket 0),socket 状态全在 Zephyr net 栈;**W5500 硬 socket offload 未实现**(feature request 未落地) | 驱动线程:INT 引脚 GPIO 回调给信号量 + `k_sem_take` **超时兜底**(超时 → 回读 PHYCFGR 更新链路);TX 完成 = SENDOK 信号量 + 超时 | **net 栈**(socket);**驱动**(链路态缓存 `ctx->state.is_up`,边沿通知 `net_eth_carrier_on/off`) | 标准 BSD:listen 常驻、accept 返回新 socket | 事件 + 周期回读兜底,全部收在驱动层 | - -关键来源(均为上表证据出处): - -- ioLibrary `socket.c`(socket/listen/connect/recv/send 全部直接 `getSn_SR` 回读;`socket()` 先 `close()`;`close()` 末尾 `while(getSn_SR != SOCK_CLOSED)`): -- W5500 官方 TCP 应用笔记(每个阶段给出"方法一:Sn_IR 中断标志 / 方法二:Sn_SR 回读"双轨;明确 Sn_IR 会重叠丢失、不推荐纯事件): -- WIZnet socket API 文档("Berkeley API 有 bind()/accept(),WIZnet API 没有"): -- ioLibrary issue 127(listen 后立即来 SYN:`getSn_SR` 观察不到 LISTEN 瞬态——纯回读模型同样要处理瞬态,ioLibrary 的 listen() 直接 close 重来): -- W5500 状态机由应用每轮 `getSn_SR` 驱动的典型参考流程: 与 -- W5500 INT 引脚为电平驱动(事件不可靠的硬件根因之一): -- Zephyr 驱动: 与 (`wiznet_thread_interrupt`:信号量超时 → `update_link_status`;`wiznet_command`:写 Sn_CR 后有界轮询回读) -- Zephyr W5500 硬 socket offload 未实现(feature request):;MACRAW 模式说明: -- WIZnet 官方 lwIP 移植(MACRAW + lwIP 拥有全部 TCP 状态): -- PeterBorisenko W5500+lwIP+FreeRTOS(`spiif.c`:信号量 500 ms 超时 → `spi_if_clr()` 清 IR + `spi_if_check_link_state()` 回读 PHYCFGR 边沿通知 lwIP): -- 同类移植:、 -- mbed WIZnetInterface(`socket_accept` 内联换芯片 socket 重监听): -- FreeRTOS+TCP `FreeRTOS_Sockets.c`(`FreeRTOS_accept` 阻塞在 `eSOCKET_ACCEPT` 事件;`prvAcceptWaitClient` 交付子 socket 或 reuse 模式返回父 socket;reuse 模式 `listen()` 内 `vStreamBufferClear(rx/tx)` + 位复位 + `vTCPStateChange(eTCP_LISTEN)`): -- FreeRTOS accept 文档("默认创建子 socket 处理被接受连接"): -- CH395 数据手册(TCP Server 模式:Socket 持续检测连接事件、每 Socket 同时只允许一条连接;多连接模式使能位;Socket 状态枚举含 SYN_RCVD/TIME_WAIT 等 11 态;SINT_STAT_* 事件位):(镜像 ) - -## 3 提炼的设计原则 - -1. **单一事实源(P1)**:连接状态的权威副本只有一份——硬栈芯片在 Sn_SR/芯片状态寄存器,软栈在协议栈 pcb。 - 任何"决策点"(accept 前、recv 前、重监听前)要状态就**当场回读**,而不是查一份可能滞后的镜像。 - ioLibrary 每个 API 内部都在回读;mbed 门面 `is_connected` 也回读。镜像只允许作为**展示缓存**,不得作为决策依据。 -2. **状态机单所有者(P2)**:芯片(硬栈)或协议栈任务(软栈)是唯一写者;上层通过**事件订阅**获知迁移 - (FreeRTOS 每 socket 事件组、Zephyr carrier on/off、lwIP netif link 回调)。本项目 netTask 串行化已满足"单写者", - 缺的是"镜像降级为投影"。 -3. **事件只负责提前唤醒,正确性靠回读 + 有界超时兜底(P3)**:W5500 官方应用笔记明确 Sn_IR 会因处理不及时 - 而重叠丢失、不推荐纯事件;Zephyr/PeterBorisenko 移植全部是"INT 信号量 + 超时后回读对账"。 - 区别在于:**兜底回读收在驱动层、且回读的是芯片事实本身**,所以不存在"对账修正镜像"这种动作。 -4. **监听常驻是 API 层承诺,不是芯片属性(P4)**:芯片单 socket 事实由适配层补齐——子 socket(软栈)、 - 换芯片 socket 内联重监听(mbed)、或栈状态机内联回 LISTEN(FreeRTOS reuse)。**重监听发生在 - accept/close 事务内部**,不是独立周期机制;失败重试也在同一事务/模块内有界进行。 -5. **"复用前清理"属于资源所有者、内联进事务(P5)**:ioLibrary `socket()` 先 `close()`、`close()` 内联等待 - CLOSED;W5300 erratum 内联在 `close()`;FreeRTOS reuse 模式 `listen()` 同步清空 rx/tx 流。 - 芯片残渣/沉降怪癖的主流处理 = 打开/监听事务的前置步骤(有界超时),不是常驻排渣定时器。 -6. **分层边界(P6)**:驱动层独占全部芯片交互 + 芯片侧缓存(链路态、命令完成)+ 怪癖内联处理; - net/栈层只见事件与数据。主流方案中**没有 net 层直接读芯片寄存器做对账**这回事——要么 net 层根本不碰芯片 - (软栈),要么决策即回读(硬栈门面)。 - -## 4 映射到本项目:目标架构草案 - -### 4.1 分层职责 - -| 层 | 应持有 | 现状偏差 | -|---|---|---| -| `ch395f` 驱动层 | 全部 SPI 事务;芯片 socket 状态回读(`GET_SOCKET_STATUS_SN` 已有);每 socket 事件读取即清(`GET_INT_STATUS_SN` 已有);全局中断汇聚(现在在 net 层 `net_poll`,应下沉);链路态缓存 + 边沿事件(`GET_PHY_STATUS` + PHY_CHANGE 位,Zephyr 模型);**复合复位原语**(沉降/排渣内联) | 中断汇聚在 net 层;无复合复位原语;沉降/排渣策略在 net 层 | -| net 层(netTask 单所有者) | fd ↔ 芯片 socket 映射、消息队列串行化(保留)、L3 pending recv(保留)、事件分发/回调;**socket 状态 = 按需回读芯片,或事件推送的展示缓存** | `net_sock_state_t` 8 态镜像是决策依据(`net_can_accept`/`net_can_read`/accept/recv 全查镜像)→ 镜像滞后即产生五层补丁 | -| 应用/服务层 | 只管 fd 与数据;监听常驻由 net 层承诺 | 已符合(EOF 不得 close 监听 fd 的约定正确) | - -### 4.2 驱动层应新增/收编的原语 - -1. `ch395f_sock_state(sn)`:`GET_SOCKET_STATUS_SN` 的薄封装(返回芯片 11 态枚举,映射到 net 层语义由 net 层做)。 -2. `ch395f_sock_events(sn)`:读即清每 socket 中断位;`GET_GLOB_INT_STATUS` 汇聚从 `net_poll` 下沉驱动。 -3. `ch395f_sock_reset(sn, mode, timeout_ms)`:**复合原语** = 排空 RX(`CLEAR_RECV_BUF_SN`/分块读空)→ - `TCP_DISCONNECT`/`CLOSE_SOCKET_SN` → 排空中断位 → 有界等待芯片回 CLOSED。把 TRAP-NET-09 沉降窗口与 - TRAP-NET-18 排渣隔离收进这一个同步原语(对应 ioLibrary `close()` 内联等待、FreeRTOS `listen()` 内联清流)。 -4. `ch395f_wait_cmd(sn, cmd, timeout_ms)`:命令完成有界等待(`ch395f_poll_cmd_status` 已有雏形,补齐到所有命令路径)。 -5. ~~多连接模式语义利用~~(**T4 更正,T2 代码实证**):`FUN_PARA_TCP_SERVER`(bit1)**未启用**—— - `net_init` 实传裸 `0x08`、bit1=0(net_socket.c:261-270),设计文档 A′ 出局论证(多连接模式 - 不支持多监听)成立。"芯片自己保证监听常驻"**不成立**,监听常驻只能由软件承诺(auto_relisten); - 重监听应收敛为"reset 原语 + 一次 LISTEN 命令",由 accept/close 事务内联或单一 reconciler 触发。 - -### 4.3 net 层可整体删除/降级的补丁 - -| 现有机制 | 裁决 | 依据(主流对照) | -|---|---|---| -| TRAP-NET-17 周期对账 `net_reconcile_tick`(200 ms + 去抖 + 冷却) | **整体删除** | 无镜像即无对账;ioLibrary/mbed 决策即回读(P1) | -| N-32 监听器 pump `net_listener_pump`(100 ms,"唯一重建入口") | **整体删除**,改为 accept 事务内联重建(mbed 模型)或 close 事务内联回 LISTEN(FreeRTOS reuse 模型) | P4:重监听是事务步骤,不是周期机制 | -| D10 重监听重试(30 ms/2 s 独立定时器) | **降级**:内联重建失败时"下次 accept 再试"或 reset 原语内部有界重试;独立定时器删除 | mbed 内联重建 + 有界超时;TRAP-NET-09 沉降收进 `ch395f_sock_reset` | -| `net_resync_listen`(公共 API,"静默回收"对齐) | **整体删除** | "静默回收"是 CH395 TCP Server 模式**文档行为**(芯片自动回监听),回读即对齐,不需要"仅修 MCU 状态"的 API | -| `net_lost_connect_probe`(丢 CONNECT 中断补 accept) | **降级**:accept 判据改为回读芯片状态(SYN_RCVD/ESTABLISHED 即有连接),丢中断只影响时延不影响正确性 | P3;ioLibrary 观察不到 LISTEN 瞬态也照样靠回读自愈(issue 127 场景) | -| `auto_relisten` 标志 + `handle_disconnect_event`/`handle_timeout_event` 重建路径 | **简化**:断开事务末尾调 `ch395f_sock_reset` + LISTEN(FreeRTOS reuse 回 LISTEN 的等价物);标志位可保留作"该 fd 是监听器"的类型标记 | P4/P5 | -| TRAP-NET-18 沉降/隔离状态机(`net_listener_settle` 400/100/800 ms 三档) | **下沉驱动**:并入 `ch395f_sock_reset(sn, mode, timeout)`;net 层只选 mode(异常/正常) | ioLibrary `close()` 内联等待、W5300 erratum 内联先例 | -| `net_can_accept`/`net_can_read`/`net_listener_armed` 镜像门控 | **保留但改语义**:门控值 = 驱动层事件缓存(readable 由 RECV_OK 事件置位可保留),但"非 ESTABLISHED 一律返回 1"这类镜像补偿逻辑删除——决策点回读芯片后不再需要 | P1 | -| L3 pending recv、消息队列串行化、网络代次号 | **保留**(与主流不冲突:FreeRTOS/Zephyr 同样单栈任务 + 阻塞事件模型) | — | - -### 4.4 必须保留、但应下沉/换形的芯片怪癖 - -| 怪癖 | 任何架构都要处理 | 主流收法 | -|---|---|---| -| 丢中断(CONNECT/DISCONNECT/RECV_OK,INT 电平驱动 + IR 位重叠) | 是(W5500 官方笔记同款警告) | 事件唤醒 + **有界超时回读**,收在驱动层(Zephyr `k_sem_take` 超时、PeterBorisenko 500 ms);net 层不做周期对账 | -| close 沉降窗口(TRAP-NET-09:close 后立即 open 失败) | 是(ioLibrary `close()` 内 `while(Sn_SR != CLOSED)`;listen 竞态 issue 127) | `close()/socket()` 事务内联等待 + 有界超时(驱动原语内部) | -| RX 残渣(TRAP-NET-18:中止后残留 ~2 KB 被下一条连接读走) | 是(软栈对应物 = FreeRTOS reuse `listen()` 清流;硬栈 = 打开前 close/清缓冲) | `ch395f_sock_reset` 前置排空(`CLEAR_RECV_BUF_SN` 命令现成) | -| 芯片自动回收/回监听(现被当作"静默回收"陷阱) | 不是缺陷——bit1=0 下的台架实证芯片行为(run8;⚠ T4 更正:非 Server 模式条款,Server 模式未启用) | 回读对齐后此"陷阱"消失;仅需在文档里把它登记为芯片语义 | -| PHY/链路变化 | 是 | 驱动层缓存 + 边沿事件(Zephyr carrier on/off),net 层订阅;不进对账 | - -### 4.5 结构性根源判定 - -本项目偏离主流的不是"单连接复用"(ioLibrary 无 accept、CH395 每 socket 一连接、FreeRTOS reuse 模式全部同形), -而是三件事的组合:**① net 层维护 8 态镜像并作为决策依据;② 状态迁移只靠中断事件推进;③ 监听重建与事务脱钩**。 -镜像滞后 → 需要周期对账(TRAP-NET-17);事件丢失 → 需要探针与门控补偿;重建脱钩 → 需要 pump/重试/沉降三件套。 -按 P1~P6 重构后,五层中的四层(对账、pump、resync、probe)失去存在理由,一层(沉降/排渣)下沉为驱动原语。 - -## 5 编号发现 - -- **F-MS-1** ioLibrary(WIZnet 官方,硬 TCP 栈权威参考)**没有任何 socket 状态镜像**:`socket()/listen()/connect()/recv()/send()` - 每次调用直接回读 Sn_SR/Sn_IR/Sn_RX_RSR;软件状态仅四组传输记账。"周期对账"在该模型中不存在也不必要。 - 证据:socket.c 全文(见第 2 节链接)。 -- **F-MS-2** "监听 socket = 数据通道"不是对 BSD 的偏离,而是硬 TCP 栈芯片的原生主流模型(WIZnet API 明确没有 - accept();CH395 手册"每 Socket 只允许一条连接")。本项目的偏离在**镜像 + 纯事件驱动**,不在单连接复用本身。 -- **F-MS-3** 主流对丢中断的处理范式 = **事件唤醒 + 有界超时回读兜底,且兜底收在驱动/netif 层** - (Zephyr:INT 信号量超时 → 回读 PHYCFGR;PeterBorisenko:500 ms 超时 → 清 IR + 链路对账;W5500 官方笔记: - 事件法与回读法双轨、事件法明示不可靠)。不存在"net 层周期对账修正软件镜像"的做法。 -- **F-MS-4** CH395 的"静默回收"(peer FIN 后芯片自行回监听、不发 DISCONNECT 中断)为**台架实证芯片行为** - (run8,当时 FUN_PARA=0x09 即 bit3=1)。⚠ T4 更正:原文称其为"TCP Server 模式文档行为"有误—— - Server 模式(bit1)未启用(net_socket.c:261-270),该行为是 bit1=0 下的实测现象而非 Server 模式条款。 - 结论不变:`net_resync_listen` 这类"仅修 MCU 状态"的 API 是镜像滞后的直接产物,回读模型下无存在理由。 -- **F-MS-5** FreeRTOS+TCP `FREERTOS_SO_REUSE_LISTEN_SOCKET` 给出与本项目模型同形的**主流参考实现**: - 监听 socket 自身转连接、回 LISTEN 由栈的单一状态机在事务内完成;且 `listen()` **同步清空 rx/tx 流**—— - 这正是 TRAP-NET-18 排渣隔离的主流对应物(一行内联清理,而非 400/100/800 ms 三档状态机)。 -- **F-MS-6** mbed WIZnetInterface 的 accept 模型:连接继承原芯片 socket,**accept 事务内联**在另一颗芯片 socket 上 - bind+listen 重建监听(用芯片 8 socket 兑现"监听常驻"承诺)。其失败处理粗糙(可静默失去监听)说明: - 内联重建仍需有界重试,但重试应长在事务/模块内部,而不是独立周期机制。CH395F 多连接模式已启用,该模型可移植。 -- **F-MS-7** 主流 RTOS 对硬 TCP 栈只有两种姿势:**MACRAW + 原生栈**(状态全在 MCU,Zephyr/lwIP 全部移植如此)或 - **完全 offload**(状态全在芯片,驱动代理 BSD 调用;Zephyr 对 W5500 的 offload 仍是未落地 feature request)。 - "芯片持有状态 + MCU 镜像状态"的混合姿势正是五层补丁的结构性缺口。 -- **F-MS-8** CH395 芯片状态枚举有 11 个 TCP 态(含 SYN_RCVD、FIN_WAIT、TIME_WAIT),net 层 8 态镜像是**有损投影**; - 有损镜像必然产生"镜像说 A、芯片是 B"的窗口,五层补丁全在为这个窗口付税。决策点回读芯片 11 态 - (`GET_SOCKET_STATUS_SN` 单命令、SPI 成本微秒级)即可让窗口归零。 -- **F-MS-9** 纯回读模型同样要处理瞬态(ioLibrary issue 127:listen 后立即 SYN,Sn_SR 观察不到 LISTEN), - 但主流把瞬态处理写成**事务内的有界重试/重来**(ioLibrary listen() 直接 close 重来), - 不需要"连续 2 次失配去抖 + 1 s 冷却"这类镜像世界的防抖装置——去抖与冷却是对账机制的自偿成本。 - -## 6 对 T4 汇总的建议输入 - -1. 目标架构一句话:**驱动持有芯片事实并内联处理怪癖(reset 原语),net 层单所有者只做映射/串行/事件分发, - 决策点回读或订阅,监听重建内联进 accept/close 事务**。 -2. 分阶段建议(风险从低到高): - ① 驱动新增 `sock_state` 回读 + `sock_reset` 复合原语(现补丁原参数原样收编,行为不变); - ② `net_can_*`/accept/recv 判据改回读(删 reconcile/pump/probe 的触发面); - ③ 监听重建内联化(删 auto_relisten 定时器路径、D10 独立定时器); - ④ (远期、低优先)评估 mbed 式"换芯片 socket 重监听"。⚠ T4 更正:其前提"多连接模式已启用"不成立 - (bit1=0,net_socket.c:261-270),且多连接模式不支持多监听(A′ 出局论证)——仅列为 V0.2 级评估项。 -3. 回归判据沿用现成探针(快速复连、中止复连首上传),对照 12.10 节生产验证数据即可裁决。 diff --git a/docs/网络/N34-自愈审计/实施方案.md b/docs/网络/N34-自愈审计/实施方案.md deleted file mode 100644 index d4bcb08b..00000000 --- a/docs/网络/N34-自愈审计/实施方案.md +++ /dev/null @@ -1,223 +0,0 @@ -# N34 自愈重构 · 实施方案 - -> 依据:`docs/网络/N34-自愈审计/README.md`(裁决与路线)+ 三份发现文件(F-NET/F-DRV/F-MS 编号引用)。 -> 纪律:**每个阶段的实施以对应实验拿到明确结果为前置门**(映射见 §1);实验用实验分支/实验宏, -> 不污染产品路径。所有改动只发生在 `task/N34-bsd-selfheal-audit` 分支。 -> 每阶段收尾三件套:`@build` 0 错 0 警 + `ci_local.py` + 对应台架回归(判据见各阶段)。 - -## 1. 实验 → 实施阶段 的门禁映射 - -| 实施项 | 前置实验 | 实验要回答的问题 | -|---|---|---| -| 阶段 0(零风险清理) | 无(E0 已佐证) | — | -| **P0 止血包(M1+M2+M4-lite)** | **E7/E7b ✅ 已完成** | 背靠背 STOR 失效循环能否断链(结论:8%→96.7%,簇已断,见实验方案 E7b 节) | -| 阶段 1(API 收口 + F-NET-2 缺口修复 + **M3**) | E5 ✅(形态罕见,顺带修) | 静默回收形态在生产路径是否真实可达、检测延迟多大 | -| 阶段 1.5(recv 语义主流化,**待用户确认排期**) | 依赖阶段 1 对账①先落地 | — | -| 阶段 2(驱动原语) | E2、E3、E4 | 回读成本;GET_CMD_STATUS 判别力;reset 序列的正确内部顺序 | -| 阶段 3(net 层收敛) | E1 ✅、E6 | 对账/pump 真实命中频率(已量化);U15 失配频率 | - -## 1.5 P0 止血包(E7b 已验证,✅ 已转正 `9ac9879`) - -三处一行级改动(分支已实施并台架验证,**已提交**): - -| # | 改动 | 位置 | 效果(E7b 实测) | -|---|---|---|---| -| M1 | GRACEFUL 沉降无条件化(删 `recv_bytes!=0` 门) | net_socket.c:3424 | 断"被杀会话免沉降→残渣杀下一条"繁殖链 | -| M2 | lftpd 数据等待门控 `p_sock->state` 直读 → `net_listener_armed` | lftpd_inet.c:117 | 镜像门控退役首例;救回窗可用 | -| M4-lite | 删沉降段全局读即清(模块约定违规点) | net_listener.c:178 | 断"沉降热路径搁浅他 Socket 事件"放大器(**必须与 M1 同批**,E7 实证) | - -回归:n30 116/120(残余偶发自愈)+ ftp_regression 11/11 + :7003 50/50 + iec104 18/18 + -TRAP-NET-18 RST/FIN 5/5+5/5 + @test PASS + 0 错 0 警。**E8 深查修正**(见实验方案 E8 节与 -盘点 F-NET-17):残余并非稳定 3%,而是**风暴态**——同固件故障率随残渣存量 2%~54% 摆动; -治本 = M3(`0x05/0x00`+年轻连接的电平确认,交对账①去抖)+ M5(drain-until-quiescent 的 -`sock_reset`,沉降/pump 让位 closed-with-data 救回)。M3/M5 属阶段 1/2,非止血包范围。 -**常备回归补挂**:`n30_repro --n 20` 纳入 Ch395F 回归集(本轮 46/50 潜伏的测试面根因); -`ftp_abort_probe.py` 已迁 `test/host/`(2026-10-13,N34 阶段 3 收尾,`rename_ref.py` 全库引用同步)。 - -## 2. 阶段 0 · 零风险清理(先行,不依赖实验)✅ 已完成(`4c0a0af`,E10 验证) - -> 实施结果与下表逐条对应;提交切分因 0-A 与 0-5 改动在同一文件内交织而 -> **合并为一笔**(8 项,`4c0a0af`),0-3/0-4/0-6 的逻辑改动在提交信息中逐条 -> 列出并有冒烟证据。实施中额外抓出并修复:`CH395F_FUN_PARA_SOCKET_CLOSE` -> 重复定义(首版增量构建 9 条 `#47-D` 警告,已并入既有宏块)。 -> 验证:全量重编 0 错 0 警 + @test PASS + n30 0/20、:21 @0ms 20/20、 -> :7003 20/20、ftp_regression 11/11。 - -| # | 改动 | 位置 | 验证 | -|---|---|---|---| -| 0-1 | 注释漂移:pump "每 50 ms"→100 ms | net_socket.c:3384 | @build | -| 0-2 | give-up 文案改"重试预算耗尽,交 pump 周期接管" | net_listener.c:414-419 | @build + @test 冒烟 | -| 0-3 | U12 对称:TCP_LISTEN 失败补 `ch395f_close_socket` | net_listener.c:227-230、net_socket.c:1112-1114 | @build + @test(TRAP-NET-17 快速复连子集) | -| 0-4 | retry_tick 补状态门(仅 LISTENING/CLOSED 才重试) | net_listener.c:400-412 | @build + @test | -| 0-5 | FUN_PARA 用宏 `CH395F_FUN_PARA_SOCKET_CLOSE` 替代裸 0x08;删 `CH395F_GINT_STAT_SOCK0..3` 死宏 | net_socket.c:270、ch395f.h:244-247 | @build | -| 0-6 | 版本门控:ver<0x46 则拒置 bit3 + 自检留痕 | net_socket.c:257-270 | @build + @test(启动锚点) | -| 0-7 | IP 字节序注释更正(set_ip/gw/mask 实为点分序;get_remot_ipp 小端) | ch395f.h:380/:387/:394/:479-484 | @build | -| 0-8 | 旧"部分读归零"注释降级为"未证实历史观察";DMA 缓冲 sizing 理由改"接收缓冲 2KB" | ch395f.h:143-153、net_socket.c:1908/:1969/:3466/:3470 | @build | -| 0-9 | 删死声明:`CH395F_CMD_GET_GLOB_INT_STATUS`(0x29)、`CH395F_SPI_BEGIN_DELAY_MS`、EEPROM/GPIO/SLEEP 命令宏 | ch395f.h:36/:42/:163-167 | @build | - -提交切分:0-1/0-2/0-7/0-8/0-9 一笔(纯注释/死码);0-3/0-4 一笔(一行逻辑,需 @test);0-5/0-6 一笔(init 路径,需 @test)。 - -## 3. 阶段 1 · API 面收口(门:E5 结果)✅ 已完成(`b8c6a3d`,E10 验证) - -> 6 项全部实施,两个偏差与一条关键台架发现见**实验方案 E10**: -> ① `net_listener_armed` 必须补 **ESTABLISHED 放行**(首版漏放行 → iec104 -> 18/18 全 FAIL,修复后 18/18 PASS 且 127s→8.3s);② 第 6 项未走 -> `net_listener_try` 登记(会把 BOUND/D10 语义拖入),改单次尝试+失败即 425。 -> 验证:iec104 18/18、n30 0/50、:21 @0ms 50/50、:7003 50/50、 -> ftp_regression 11/11、Trap23 RST/FIN 5/5+5/5、time_sync 连续 6 轮正常。 - -1. **对账①扩展**(先修缺口再删 API):`net_reconcile_tick` 判据①增一形态—— - 本层 ESTABLISHED 且芯片 `open=1 && TCP=LISTEN` → 按 RESYNC 语义只改本层 - (ESTABLISHED→LISTENING + 转登 `last_peer_port` + 清 `lost_conn_seen` + 取消挂起 recv), - **不发芯片命令**(纪律同 pump③ U11)。位置:net_socket.c:588-590 判据处 + 复用 :789-797 的 resync 分派体。 -2. **删 `net_relisten`**(net_socket.c:3080-3120 + 消息分派 :800-814 + 头文件声明 + 指南同步)。 - 台架 Phase 13 的调用(net_latency_test.c:784/:787/:800)改用 `net_listener_try` 语义 - (经 test 专用内部入口或删该调用——Phase 13 本意是"确认连接已死后的确定性重建",pump 已覆盖)。 -3. **删 `net_resync_listen`**(第 1 步并入后;net_socket.c:3026-3066 + 分派 :774-799 + 声明)。 - net_latency_test.c:783 同步删。 -4. **iec104 门控迁移**:iec104_serv.c:227 `net_can_accept` → `net_listener_armed`(sim 已有实现); - 随后删 `net_can_accept`(net_socket.c:1427)。 -5. **time_sync 会话收尾不 close**:time_sync.c:182 删 `net_close`,回外层循环同 fd `net_accept`(lftpd 样板)。 - ⚠ 需确认 :8888 协议语义(对端每次会话新建 TCP)——E5 顺带验证。 -6. **删 lftpd 应用层阻塞重试**:lftpd_inet.c:39-70(5×2000ms)、lftpd.c:473-477(3×200ms)—— - 就绪屏障已消除 TRAP-NET-09 启动形态;PASV 监听失败改走 `net_listener_try` 登记。 - -验收:`@build` + `ci_local` + 台架回归全套现成脚本(`ftp_regression.py`、`n30_repro.py --n 50`、 -:7003 快速复连、`iec104_master.py`、`time_sync_server.py`)+ `@test` 锚点。 -风险:删 API 是**公开面收缩**——模拟器 `net_socket_sim.c` 同名实现同步删,双端编译验证。 - -## 3.5 阶段 1.5 · recv 语义主流化(用户提议 2026-10-12,方向已确认,排期待批) - -对齐 Python/POSIX:**模式归使用者、超时归调用方、API 不强加默认预算**。 - -1. `net_recv()` 默认**无限阻塞**(删 `NET_RECV_TIMEOUT_MS` 30s 默认);唤醒源=数据/EOF/close/ - 网络重建(`CONNRESET`)——会话死亡由对账①在 ~400ms 内判死兜底(**前置:阶段 1 对账①扩展先落地**, - 否则 F-NET-2 形态是真实挂死窗)。 -2. `net_recv_nb()`(现 DONTWAIT 转正)+ `net_setblocking(fd, on)` 模式挂 socket 属性。 -3. 超时=调用方策略:首选 `net_select(r,w,e,timeout_ms)`(已有);`net_recv_to(fd,buf,len,ms)` - 保留为**显式预算**便捷入口(等价 `settimeout`,默认值删除)。 -4. 内部 watchdog 降级为 tripwire:pending 超 60s 只 `[ERR]` 留痕(完成逻辑缺位探针), - **不再伪造 TIMEDOUT 返回值**。 -5. **文档契约(嵌入式特有,Python 没有)**:IWDG `required_mask` 内任务**禁止无限阻塞 recv**, - 必须 select 超时或显式预算——"超时归使用者"对受监控任务是义务不是选项。 -6. 收益:32 轮盲收(U15 上层兜底)随"阻塞为主 + 决策点回读"直接退役(阻塞 recv 的数据检测走 - `GET_RECV_LEN` 回读,对丢 RECV_OK 天然免疫——E1/E5 实证);调用点迁移面=全仓 grep - `net_recv`/`net_recv_to`(time_sync/iec104/ui_net_sink/lftpd 数处),模拟器双端同步。 - -## 4. 阶段 2 · 驱动原语(门:E2/E3/E4 结果) - -新增到 ch395f.c/.h(无新文件,无需双端注册;模拟器零依赖): - -1. **类型化状态回读 + 每 socket 缓存**(F-DRV-1/3): - ```c - typedef struct { uint8_t open; uint8_t tcp_state; } ch395f_sock_state_t; - ch395f_sock_state_t ch395f_get_sock_state(uint8_t sn); /* 主动回读 */ - ch395f_sock_state_t ch395f_sock_state_cached(uint8_t sn); /* 读缓存 */ - int ch395f_sock_is_listening(uint8_t sn); /* open=1 && TCP=LISTEN */ - ``` - 缓存刷新点唯一:`net_poll` 每轮对在用 socket 各刷一次(成本由 E2 实测背书; - 预计 4 监听 × ~15µs ≪ 1ms 轮询周期)。`open=0` 时 `tcp_state` 返回哨兵 `0xFF`("无意义")。 - 边界校验:sn>7 直接返回 CLOSED+哨兵并记一次 fault 计数。 -2. **事件护栏**(F-DRV-2):`uint8_t ch395f_sock_events(uint8_t sn)`——只读本 Socket SINT(读即清), - 永不触碰全局寄存器;全局寄存器唯一读者写成驱动侧断言(文档契约 + 调试断言)。 - 三处"两级预清"(net_socket.c:1668/:2332、net_listener.c:176)改调本原语(F-NET-7 收窄)。 -3. **复合复位原语**(F-DRV-4,内部顺序由 E4 定案): - ```c - typedef enum { CH395F_RESET_GRACEFUL, CH395F_RESET_QUARANTINE } ch395f_reset_mode_t; - int ch395f_sock_reset_begin(uint8_t sn, ch395f_reset_mode_t mode); - int ch395f_sock_reset_poll(uint8_t sn); /* 0=完成 / 1=进行中 / -1=失败 */ - ``` - ⚠ **必须是非阻塞状态机**(每 net_poll 步进),不得复制 `poll_cmd_status` 式忙等—— - 400/800ms 沉降阻塞 netTask = task stall(P2a 实测)。net 层只选 mode + 读结果。 -4. **非阻塞命令完成查询**(F-DRV-8,形态由 E3 定案):若 GET_CMD_STATUS 对目标命令有判别力 → - `int ch395f_cmd_done(uint8_t sn)`;若无判别力(Trap 记录已证 SET 类无判别力 + RESET_ALL 陈旧 BUSY 残留)→ - **完成判据改用状态回读**(open 完成=byte0 变 05H;listen 完成=TCP=01H;close 完成=byte0=00H)。 - `poll_cmd_status` 保留给内核前/测试路径,产品 socket 路径逐步弃用。 -5. **链路态缓存**(F-DRV-13):`net_poll` 读全局后把 bit0~3 喂驱动 - (`ch395f_glob_event_feed(uint16_t bits)`);驱动持 `link_up` 缓存 + 变化计数; - net 层订阅(链路变化 → 日志 + 可选触发重配)。运行期拔网线不再静默。 - -验收:新增原语先**不接线**(只加不删),`@build` + 单元级板测(ch395f_test_task 加 TC 用例: -状态回读一致性、reset 状态机跑通、cmd_done 语义)+ `@test` 冒烟。RAM 增量 <64B,跑 `ram_report.py` 确认。 - -## 5. 阶段 3 · net 层收敛(门:E1/E6 结果;需完整台架回归)—— **已实施 2026-10-13**(最终构建 Code=211348,回归全绿) - -1. **单一 reconciler**:`net_reconcile_tick` + `net_listener_pump` 合并为一个 100ms tick, - 判据读**驱动缓存**(不再各自发 SPI),统一一套去抖(连续 2 次)+ 冷却(1s); - 覆盖形态:①本层 ESTABLISHED 而芯片 CLOSED/回收 ②本层 LISTENING 而芯片已握手(补 accept) - ③监听 fd 芯片失位(重建)④本层 CLOSED 而芯片在听(只重同步)。 - 消费者即时重建三处保留(P2a 定案)。 - ✅ **已实施(2026-10-13)**:`net_listener_pump` 删除、并入 `net_reconcile_tick`(`NET_RECONCILE_PERIOD_MS=100`); - 判据读 `ch395f_sock_state_cached`(唯一刷新点=`net_poll` 每轮全量刷新,E2 实测单次回读 18.1µs); - 去抖**分级落地**(原计划"统一连续 2 次"按 E2/E9a 数据细化):②=1 轮 / ③④⑤=2 轮 / ①=4 轮(M3 让位窗 400ms)+ 冷却 1s; - 主循环自愈 tick 由 4 个回到 **3 个**;形态② 命中时的对端端口回读是本 tick 唯一按需 SPI。 -2. **探针去抖删除**(读缓存无瞬态噪声,F-MS-9);`net_lost_connect_probe` 判据改读缓存。 - ✅ **已实施(2026-10-13)**:探针判据读驱动缓存(零 SPI),`NET_LOST_CONN_STABLE_N` 下线; - 两类瞬态结构性消除("刚 listen 未生效"显示 TCP=00 永不命中握手门,E2:LISTEN 可见 ≤10ms 无长尾; - 拆链窗假 ESTABLISHED 由幽灵甄别拦截),`lost_conn_seen` 退化为 M3 可疑电平标志。 -3. **两处忙等统一**:`net_listen_locked` 3×500ms、`net_close_locked` 500ms 改走 - `sock_reset_poll`/`cmd_done` 非阻塞路径(P2b 收口)。 - ✅ **已实施(2026-10-13)**:三处(含 `net_connect_locked` open 重试)统一走 `net_sock_reset_sync` - (驱动 `sock_reset` 原语:读空 RX→CLOSE→排空事件(quiescent)→等 byte0=00H,内置 LISTEN 态**二段 CLOSE** 补发, - E2 补充发现;GRACEFUL 100ms 底 / 600ms 封顶);`net_close_locked` 另加 `ch395f_cmd_done(CLOSE)` 快路径, - 并**不再读清全局中断寄存器**(F-NET-7:全局唯一读者=`net_poll`)。 -4. **U15 内化**(若 E6 证实频率>0):reconciler 增一条"GET_RECV_LEN>0 但 readable=0 → 置 readable" - 低频对账;随后 ui_net_sink.c:552-558、iec104_serv.c:246-253 的 32 轮盲收退役。 - ✅ **E6 已裁决(2026-10-13,实验方案 E6 结果):命中=0(含压测)→ 不立项**,U15 登记观察项, - 上层盲收保留现状(与 TRAP-NET-20「不留未证明的复杂度」一致;插桩代码留仓宏=0 零成本,备再武装)。 -5. **镜像降级**:`net_sock_state_t` 保留为展示/门控投影,accept/recv/can_* 决策点改读驱动缓存。 - ✅ **已实施(2026-10-13)**:`net_types.h` 注释把 8 态明确声明为"**本层会话投影**,非芯片状态事实源" - (芯片事实源=驱动缓存,刷新点=net_poll);reconciler / 探针 / U11 重同步等按芯片状态决策的点全部改读缓存, - 事件路径(`handle_*_event`)保留**主动 SPI**(缓存刷新在 net_poll 末尾,事件路径上必然滞后)。 - -验收:完整台架回归 = TRAP-NET-17(:7003 快速复连 10/10、:21 100ms/0ms/600ms 各 50/50)、 -TRAP-NET-18(RST 10/10 + FIN 5/5,SHA-256)、TRAP-NET-20(n30_repro 11/11)、E-02(e02_probe.py)、 -104(iec104_master.py)、对时(time_sync_server.py)、`@test` 锚点 + `[STACK]` 水位对比。 -✅ **2026-10-13 全绿**:`@test` PASS;`[STACK]` net peak 1116/3840(70% 余量);TRAP-NET-17 `:7003` 50/50 + -`:21` 100ms/0ms/600ms 各 50/50;TRAP-NET-18 RST 10/10 + FIN 5/5;TRAP-NET-20 n30 11/11;E-02 双 PASS; -iec104 18/18;对时跨会话连续更新;`ftp_regression` 11/11;integrity 15/15 SHA; -`stack_report` netTask 静态链 **1944 B 不变**(链尾改经 `net_listener_retry_tick`→`net_relisten_locked`)。 - -### 5.1 E-02 收口(N34 阶段 3 追加,2026-10-13) - -E-02 的最后一环不是"挂起 recv 不冻结 netTask"(阶段 1.5 已解决),而是**应用放弃会话后单连接监听被挂起对端永久占住**: - -- **新 API `net_drop_conn(sockfd)`**(`NET_MSG_DROP_CONN`):拆会话**保监听**——netTask 侧 - `net_pending_settle_close` 先了结挂起 recv,再 `net_sock_reset_sync(QUARANTINE)`(400ms 底), - 本层收口 CLOSED(不清 fd),`auto_relisten` fd 即时 `net_listener_try`(失败交 D10 + reconciler 形态④); - **幂等**(非 ESTABLISHED/CONNECTING 直接返 0);**EOF 路径不得调**(`auto_relisten` 已接管)。 - timeSync 30s 预算耗尽路径已接入;模拟器(conn≠listen fd)实现为关数据连接。 -- **`net_poll` 每轮步进 `ch395f_sock_reset_poll`**(驱动契约"在 net_poll 步进";IDLE 槽纯 RAM 判零 SPI): - 同步包装 600ms 放弃后复位机不搁浅,对端 FIN 到达即自动走完、槽位可复用。 -- **芯片定案(E-02 台架,2026-10-13)**:CH395F `close` **无 RST 语义**(TRAP-NET-07 定案更正已载)—— - 对**内核存活但不 close 的对端**发 FIN 后芯片停 `FIN_WAIT_2`(实测 0x05/0x06 长尾),监听恢复受限于对端 close; - 对端配合则 `TIME_WAIT` 过渡 ~150ms → reconciler 形态④ relisten → 新连接接入。 - 不配合场景兜底 = 芯片 TIMEOUT/KeepAlive 拆僵尸(E13 定案 30~90s)。详见 `docs/问题记录/CH395F_Trap_Records.md` TRAP-NET-07 补充。 - -### 5.2 relisten 沉降段统一(延后项,登记不做强行收口) - -`net_relisten_locked` 隔离段仍是**手拼**序列(排空 RX + 主动 close + 读本 Socket SINT + 轮询状态 + 30ms 重试轮), -未统一改走 `sock_reset` 原语。判定:它**不是忙等**(每轮短 SPI + 交重试轮询,netTask 无长阻塞), -且 TRAP-NET-18 的沉降等级(GRACEFUL 100ms / QUARANTINE 400ms / 800ms 强制放行 < 2s 时限)是**生产已调优**的时序, -统一改造的回归面覆盖每次断连热路径,收益只是"少一段重复代码"——按 TRAP-NET-20 纪律**延后**, -待下一次触碰该热路径的任务顺路处理(`sock_reset` 原语已具备承接条件:二段 CLOSE、quiescent 排空、等级化最短等待)。 - -## 6. 横向纪律 - -- **台架锁**:所有烧录/`@test`/探针前后 `task_worktree.py lock acquire/release bench --task N34`。 -- **烧录**:`-Rst -Run` + 回读校验(`stlink_flash.py cmp`),`-CmpFile` 排在采日志之后。 -- **实验代码**:一律实验宏门控(如 `NET_U15_PROBE`),产品构建编译消失;实验结论写进 - `docs/网络/N34-自愈审计/实验记录.md` 后再实施,实验宏代码在对应阶段实施时删除或转正。 -- **文档同步**:每阶段合入后同步 BSD_Socket_API_使用指南、网络Socket监听生命周期设计说明、 - CH395F_Trap_Records(U12 登记)、AGENTS 模块约定(若 API 面变化)——登记簿销项在 done 时统一做。 -- **提交**:每阶段独立提交(标题 ≤100、正文 ≤80/行,`git commit -F` UTF-8 无 BOM 文件); - 不 push(等用户指令)。 - -## 7. 工作量与风险 - -| 阶段 | 预估 diff | 风险 | 回退 | -|---|---|---|---| -| 0 | ~80 行 | 极低 | revert 单笔提交 | -| 1 | ~250 行(删多于增) | 中(公开 API 收缩,模拟器双端) | 按子项独立提交可逐个 revert | -| 2 | ~350 行(纯新增) | 低(不接线) | revert | -| 3 | ~400 行(删 3 个 tick/忙等) | **高**(核心时序重构) | 独立提交 + 完整回归门禁,失败即 revert | diff --git a/docs/网络/N34-自愈审计/实验方案.md b/docs/网络/N34-自愈审计/实验方案.md deleted file mode 100644 index 7c874cec..00000000 --- a/docs/网络/N34-自愈审计/实验方案.md +++ /dev/null @@ -1,735 +0,0 @@ -# N34 自愈重构 · 实验方案与记录 - -> 目的:在改任何产品代码之前,用实验把实施方案的关键假设变成**明确结果**。 -> 每个实验有:假设 → 方法 → 判据(PASS/FAIL/数值)→ 它门禁哪个实施决策。 -> 结果一律追加记录在本文件"实验记录"节(含原始日志片段与统计),实施提交正文引用实验编号。 -> 台架纪律:`task_worktree.py lock acquire bench --task N34` → 实验 → `lock release bench --task N34 --bench-pass`; -> 烧录必 `-Rst -Run` + 回读校验;串口采集先于烧录(serial-assistant `open→clear→烧录→wait-idle`); -> 日志出现 `[ERR]`/Fault 即本轮作废。 - -## 实验清单 - -### E0 · 历史串口日志分析(零成本,无需台架独占)✅ 已跑 - -- **假设**:五层机制在生产常态下命中极少(冷路径),日志噪声主要来自 D10 重试循环。 -- **方法**:直接 grep 主工作树 serial-assistant 日志(gen=149 窗口,2026-10-12 19:38→23:05, - 产品构建 fw build Oct 12 2026 19:31:06 suite=NONE,开机后 ~3.5 h 常态运行)。 -- **判据**:各机制命中计数 + 每次恢复的日志行数。 - -### E1 · 压力下对账/pump/D10 命中量化(台架,不烧录,用现网固件) - -- **假设**:常态下 `reconcile` 命中≈0(E0 已证 3.5h 零命中);快速复连压力下命中才出现, - 且 B3 修复后"自家制造的丢中断"占比应显著低于 TRAP-NET-17 时代。 -- **方法**(设备跑当前产品固件,PC 侧压测): - 1. `n30_repro.py --n 50 --size 1024`(:21 背靠背 STOR); - 2. `ftp_regression.py`(多数据连接会话); - 3. :7003 快速复连循环(connect→立即 close,间隔 100ms×50——新写 `test/host/relisten_stress.py`, - 纯 stdlib,connect 成功即 shutdown(SHUT_RDWR)+close); - 4. `iec104_master.py` 连接/断开 ×10。 - 每轮之间 serial `clear`,跑完 grep `reconcile|pump:|relisten|give up|lost` 计数 + 恢复延迟。 -- **判据**: - - reconcile 命中数 / 50 轮(目标基线:TRAP-NET-17 时代 2/10 死亡 → 现在应 0 死亡); - - `pump: chip not listening` 与 `relisten retry ok` 次数与间隔(量化 D10 恢复窗); - - 端口拒绝窗(connect 被拒的间隔分布)→ 决定阶段 3 后"恢复窗 ~1s 拒绝新连接"是否仍需改善。 -- **门禁**:阶段 3 单一 reconciler 的周期/去抖参数;F-NET-7 收窄优先级。 - -### E5 · 静默回收形态可达性(F-NET-2 缺口,台架,不烧录) - -- **假设**:对端 FIN 后芯片可静默回收 Socket 回监听且不发 DISCONNECT → - 本层滞留假 ESTABLISHED,新连接被芯片接住但本层无感知(对账①不命中,因芯片态=LISTEN 非 CLOSED)。 -- **方法**:对 :7003(或 :8888)——connect → 发一帧 → 对端立即 FIN(close)→ 等 3s → - 立即再 connect → 发数据 → 观察设备是否响应;同时看串口日志有无 DISCONNECT/reconcile 痕迹。 - 对照组:同样序列但第二次连接前等 10s(看是否被某机制自愈)。 -- **判据**: - - 复连后设备**无响应且无日志** → 缺口坐实(PASS=缺口存在),记录卡死时长; - - 若被自动恢复 → 记录是哪条机制救的(日志)→ 修正 F-NET-2 结论。 -- **门禁**:阶段 1 第 1 步(对账①扩展)的判据设计与优先级;time_sync close 语义确认。 - -### E2 · 状态回读成本与瞬态窗口(裸机芯片台架,需烧实验固件) - -- **假设**:`GET_SOCKET_STATUS` 单次 SPI 成本为微秒级 → "决策点回读/每 poll 刷新"成本可忽略; - 且"listen 后立即 SYN 观察不到 LISTEN"瞬态窗口有界(ioLibrary issue 127 同形)。 -- **方法**:复活 `test/board/ch395f_reuse_bench.c`(存档态:注册进 uvprojx Test 组 + - main.c USER CODE 2 区调用,实验分支不入库该注册);DWT 周期计数器测 - `ch395f_get_socket_status` 单次耗时 min/avg/max;矩阵中在 LISTEN 命令后 0/10/50/100ms 回读 - 记录首次观察到 TCP=01H 的延迟。配套 `ch395f_reuse_host.py` 打连接。 -- **判据**:单次回读 ≤100µs(预计 ~15µs)→ 每 poll 全量刷新成立; - LISTEN 可见延迟分布 → 决定 reconciler 去抖是否可减为 1 次。 -- **门禁**:阶段 2 缓存刷新策略;阶段 3 去抖简化。 - -### E3 · GET_CMD_STATUS 判别力矩阵(裸机芯片台架,同场烧录) - -- **假设**:GET_CMD_STATUS 对 OPEN/LISTEN/CLOSE 有判别力;对 SET 类无判别力 - (Trap 记录已证 SET 类 + RESET_ALL 陈旧 BUSY 残留)。 -- **方法**:同 E2 实验固件:对每条命令(OPEN_SOCKET/TCP_LISTEN/CLOSE_SOCKET/SET_SOUR_PORT/ - SET_PROTO_TYPE)发命令后以 ~1ms 节奏轮询 cmd status,记录"变 BUSY→终态"延迟与假阳性 - (命令未发时读到的残留态)。 -- **判据**:某命令判别力成立(发后 ≤N ms 内变终态、无残留假阳性)→ `ch395f_cmd_done` 可用该判据; - 不成立 → 完成判据改用状态回读(`docs/网络/N34-自愈审计/实施方案.md` §4 第 4 项的回退方案)。 -- **门禁**:阶段 2 第 4 项形态。 - -### E4 · sock_reset 内部顺序 A/B(裸机芯片台架 + 生产路径复现) - -- **假设**:TRAP-NET-18 排渣只需"排空 RX(读空)→ close → 排空事件 → 等 CLOSED", - `CLEAR_RECV_BUF` 命令可不用(TRAP-NET-20 已证缓冲重分配无效);GRACEFUL 与 QUARANTINE 差异仅在 - 是否排 RX + 最短等待。 -- **方法**:E2 实验固件加变体:close 前 (a)读空 RX (b)CLEAR_RECV_BUF (c)什么都不做, - 随后复监听 + 立即上传,校验载荷完整性(SHA/图案);生产路径侧用 `n30_repro.py` 变体 - (RST 中止 → 立即复连 STOR)对照现固件行为。 -- **判据**:(a) 与现实现等价(10/10 载荷完整)、(b) 不等价则记录差异 → 定案 `sock_reset` 内部序列。 -- **门禁**:阶段 2 第 3 项实现细节。 - -### E6 · U15 失配频率(生产路径插桩,需烧实验固件) - -- **假设**:`RX 非空但 readable=0`(RECV_OK 中断丢失的数据面形态)在生产真实发生, - 上层 32 轮盲收是必要兜底而非保险。 -- **方法**:实验宏 `NET_U15_PROBE` 门控:net_poll 每 32 轮对在用 fd 发一条 GET_RECV_LEN, - 若 >0 且 !readable → 计数 + 低频日志(每小时 ≤1 条防刷屏);跑 24h 常态 + E1 压测。 -- **判据**:命中 >0 → 阶段 3 第 4 项(U15 内化)立项;命中 =0(含压测)→ 上层盲收保留现状、 - U15 登记观察项。 -- **门禁**:阶段 3 第 4 项做不做。 - -## 实验顺序与成本 - -| 顺序 | 实验 | 需烧录 | 预估耗时 | -|---|---|---|---| -| 1 | E0 | 否 | ✅ 已完成 | -| 2 | E1 | 否(现网固件) | ~40 min(含写 relisten_stress.py) | -| 3 | E5 | 否 | ~20 min | -| 4 | E2+E3+E4 | 是(一次实验固件全收) | ~2 h(含注册/烧录/矩阵跑完/还原) | -| 5 | E6 | 是(插桩固件) | 烧录 20 min + 24h 挂机(期间可做阶段 0) | - -E1/E5 不烧录、风险最低,先跑;E2/E3/E4 一次实验固件合并跑(实验分支,注册不入库); -E6 挂机期间并行实施阶段 0。 - ---- - -## 实验记录 - -### E0 结果(2026-10-12 23:05,gen=149 窗口 19:38→23:05,产品构建,常态运行 ~3.5 h) - -命中计数(grep 大小写不敏感,SimpleMatch): - -| 模式 | 命中 | 说明 | -|---|---|---| -| `reconcile` | **0** | TRAP-NET-17 对账整窗口零命中(冷路径坐实) | -| `chip not listening` | 2 | 均为 `sock2 pump: chip not listening (0x00/0x00, was state=0) -> relisten`(pump 情形②) | -| `relisten` | 36 | 其中 ~24 条是 `relisten quarantine/settle (sock=0x00 tcp=0x00)` **重试循环逐轮留痕**(30ms 节奏),其余为 `auto-relisten ok`/`relisten retry ok` 成功尾 | -| `settle` / `quarantine` | 12 / 14 | sock1(GRACEFUL)与 sock2(QUARANTINE)各若干次会话收尾 | -| `give up` / `resync` / `lost connect` | 0 | 无放弃、无探针触发 | - -恢复形态(原始片段归纳):会话结束 → settle/quarantine 窗口内每 30ms 一条留痕 -(sock2 QUARANTINE ≈15 条 ≈450ms;sock1 GRACEFUL ≈4 条 ≈120ms)→ `auto-relisten ok` + -`relisten retry ok`。**每次恢复产生 5~16 行日志**——D10 重试循环的逐轮留痕是主要日志噪声源。 - -结论: -1. 常态下对账零命中 → 阶段 3"单一 reconciler 读缓存"不会增加热路径成本;E1 压测补压力面。 -2. 沉降/隔离与 D10 重试是**每次会话收尾都走的热路径** → `sock_reset` 复合原语(阶段 2)收益直接。 -3. 日志留痕策略需随阶段 2 收口(重试循环只留首末 + 计数,勿逐轮)。 - -### E1 结果(2026-10-12 23:09–23:16,现网产品固件 fw=Oct 12 19:31:06,net 代码=HEAD) - -压测腿与结果: - -| 腿 | 结果 | 延迟分布 | -|---|---|---| -| :7003 复连 100ms×50 | **50/50 成功,0 拒连** | p50=**1020ms** min=2ms max=1049ms | -| :7003 复连 0ms hold50×20 | 20/20 | p50=527ms | -| `n30_repro --n 50`(背靠背 STOR) | **4/50 成功(46 次 425)** | — | -| `n30_repro --n 10`(复跑确认) | **1/10 成功(9 次 425)** | — | -| `ftp_regression`(有间隔) | 11/11 | 正常 | - -串口命中(leg1 窗口):`reconcile`=1、`pump: chip not listening`=61、`auto-relisten ok`=70、 -`give up`=0、`listen lost`=1、`lost-CONNECT self-accept`=1。 - -**发现 A(热路径时序,量化"恢复窗")**:正常断连的完整链 = `DISCONNECTED`(芯片 0x05/0x00, -bit3=1 契约)→ 释放路径**只置事实不重建**(N-32 定案)→ 等 pump(实测 115–900ms)→ 首次 -`net_listener_try` 失败 → 30ms 后 D10 重试成功(~75ms)。**每次会话收尾产生 ~0.3–1s 监听空窗**; -空窗内 SYN 被静默丢弃(无 RST),客户端 1s RTO 重传 → :7003 复连 p50≈1020ms。 -→ 直接支撑"重建内联进释放事务"(mbed/FreeRTOS reuse 模型)与 `sock_reset` 原语; -也解释 TRAP-NET-17 时代"恢复窗拒绝新连接"的口径。 - -**发现 B(P0,新):背靠背数据连接失效循环,现固件可复现 9/10**: -失败签名(sock4=FTP 数据监听):数据连接 accept 后 **~20ms** 被**上一条连接的陈旧 DISCONNECT** -判死(`DISCONNECTED, releasing (chip st=0x05/0x00)`——芯片 TCP=0x00 非 ESTABLISHED, -**通过 D20 陈旧甄别**)→ 被杀会话 `recv_bytes=0` → **不登记 GRACEFUL 沉降**(门=收过数据)→ -立即 relisten → 下一条连接仍落在残渣投递窗内 → 再被杀——**自持循环**。`closed-with-data accept` -偶发救回(窗口内仅 2 次)但 lftpd 数据门控(`lftpd_inet_accept_timeout` 直读 `p_sock->state` 镜像) -错过窗口 → 5s 超时 → 425。 -对照台账:TRAP-NET-20 定案回归 = `ftp_regression`(有间隔)11/11,**n30_repro(背靠背)不在终版回归集**; -Trap 记录证据通则节自认"历史台架计数日志未入库不可复核"——本次 9/10 是**首个入库可复核的背靠背数据**。 -初步归因指向:G1 GRACEFUL 沉降门 `recv_bytes≠0` 对"被陈旧事件杀死的零数据会话"失效(新缺口, -建议登记 F-NET-15);G2 三处全局读清残留(F-NET-7)与事件护栏缺失(F-DRV-2)是残渣投递的供给面。 -→ 根因深挖已派 net-auditor(代码级),修复属阶段 0/1 范围(一行级沉降门修正候选)+ 阶段 2 护栏。 - -### E5 结果(2026-10-12 23:2x,:7003 connect→send 1B→FIN→3s→复连 ×10) - -20 次 FIN **全部产生 `DISCONNECTED` 事件(20/20)**;pump 重建 10 次;`reconcile`=1。 -**"静默回收"形态(本层 ESTABLISHED + 芯片回 LISTEN + 无 DISCONNECT)在 E1+E5 累计 ~110 次 -连接/断开中零复现**。 -结论:F-NET-2 缺口在代码上真实(无任何机制认领该形态),但触发在本环境罕见 -(run8 台架曾实证,非日常路径)→ 阶段 1 顺带修(对账①扩展),不单独立急项。 - -### E7 / E7b 结果(2026-10-12 23:36–23:5x,M1+M2 ± M4-lite 实验固件,台架锁 N34) - -**实验改动**(分支实验性,未提交): -- M1 `net_socket.c:3424` GRACEFUL 沉降无条件化(删 `recv_bytes!=0` 门) -- M2 `lftpd_inet.c:117` 镜像门控 → `net_listener_armed` -- M4-lite `net_listener.c:178` 删沉降段全局读即清(E7 中途追加,见下) - -**E7(M1+M2)**:`@build` 0/0(Code=209488);`@test` PASS(戳 23:35:58,回读无差异)。 - -| 测试 | 基线(HEAD 固件) | E7(M1+M2) | -|---|---|---| -| n30 ×50 | 4/50 | 37/50(尾部 13 连败**持续簇**) | -| n30 ×20 立即复跑 | — | **20/20 全败**(簇自持且跨轮持续) | -| ftp_regression | 11/11 | 11/11 | -| :7003 100ms×50 | 50/50 p50=1020ms | 50/50 p50=1022ms(M1 的 +100ms 沉降被 SYN-RTO 窗掩盖) | - -**E7 中途根因(新发现 F-NET-16)**:失败簇期间 `disarmed`=0(M2 从未早退,425=lftpd 满 5s 超时)、 -`closed-with-data` 38 次**全部迟到**、控制通道 sock2 也被陈旧 DISCONNECT 杀。代码定位: -`net_listener.c:178` 沉降段每 30ms 重试一轮 `GET_GLOB_INT_STATUS_ALL`(全局读即清)—— -M1 使沉降成为每次断连热路径后,**每次关闭产生 ~4 次全局读清**,按 B3 机制搁浅其它 Socket 的 -CONNECT/RECV_OK(net_poll 门控焊死)→ 数据连接事件永不到达 lftpd → 满 5s 超时。 -该行同时违反 AGENTS 模块约定「所有 CH395F 中断在 net_poll 内集中读取并清除」。 -**教训:M1 单独上会放大 F-NET-7 供给面——M1 与 M4-lite 必须同批**(net-auditor 的 M1+M2 组合 -建议需修正为 M1+M2+M4-lite)。 - -**E7b(M1+M2+M4-lite)**:`@build` 0/0(Code=209484,−4B);`@test` PASS(回读无差异; -注:固件戳未变=戳所在 TU 未重编,镜像身份以 Code 尺寸变化+回读无差异为准)。 - -| 测试 | E7b 结果 | -|---|---| -| n30 ×50 第 1 轮 | **47/50**(#18-20 偶发,**自愈无簇**) | -| n30 ×20 立即复跑 | **20/20** | -| n30 ×50 第 2 轮 | **49/50**(#47 偶发) | -| ftp_regression | 11/11 | -| :7003 100ms×50 | 50/50 p50=1011ms | -| iec104_master | 18/18 PASS | -| TRAP-NET-18 RST×5 / FIN×5(`test/host/ftp_abort_probe.py --check-abort`) | 5/5(450)/ 5/5(226) | - -**裁决**:M1+M2+M4-lite 把背靠背 STOR 从 8% 拉到 **96.7%(116/120)**,残余 ~3% 偶发 -= 原始"滞后 DISCONNECT 杀新连接"形态(D20 结构性失明,M3 域)——**自愈型、无簇、无数据损坏**。 -自持循环已断,P0 降级为 P2 尾巴。 -**遗留**:① M3(D20 年轻连接电平确认,并入阶段 1 对账①扩展)治本剩余偶发; -② `n30_repro` 纳入常备回归集(本轮 4 连败的测试面根因);③ `ftp_abort_probe.py` 落在 -`MDK-ARM/` 目录(应迁 `test/host/`,与"脚本在库"证据通则一致); -④ 设备现跑实验固件,转正或回退由用户裁决。 - -### E8 残余偶发深度排查(2026-10-13 00:06–00:2x,提交 9ac9879 后固件,台架锁 N34) - -**背景**:E7b 报"残余 ~3%"后,TRAP-NET-18 abort 探针(RST×5+FIN×5)再跑 n30 → **27/50** -(两个持续簇 #6-25、#44-50)——"3%"不是稳定值,故障率随设备状态在 2%~54% 间摆动。 - -**实验矩阵(同固件、同负载,仅设备状态不同)**: - -| 设备状态 | n30 结果 | 簇形态 | -|---|---|---| -| 烧录后全新启动(E7b) | 3/50、1/50、0/20 | 偶发、自愈 | -| abort 探针 + 多轮压测累积后 | **27/50** | 两个持续簇 | -| 仅 ST-Link 复位(不重烧) | **6/50** | 两小簇(#37-38、#47-50) | - -→ **状态依赖确证**:故障率 ∝ 芯片侧残留(OPEN 卡死 socket + 粘滞 DISCONNECT 锁存)存量。 - -**风暴签名(污染态,00:07:14.5–15.2 窗口,全部在 0.75s 内)**: -``` -closed-with-data accept (drain pending) ← 救回:RX 有数据,accept 返 fd 给 lftpd -settle ×2 ← pump 的沉降插进来 -standalone accept ← 下一条 SYN 进卡死芯片 socket -DISCONNECTED, releasing (chip st=0x05/0x00) ← accept 后 11ms 被杀 -closed-with-data accept (drain pending) ← 又救回……循环 ×3 -pump → settle ×4 → auto-relisten ok ← 800ms 强制放行才出簇 -→ 5.6s 后 accept timeout (425) -``` - -**机制链(三个叠加缺陷,登记 F-NET-17)**: -1. **沉降自再生锁存**:沉降段 `net_listener.c:174` 的 `close_socket` 在芯片已 CLOSED - 后再 close 仍会(经拆链收尾)重新锁存 DISCONNECT;`:176` 的 SINT 读清发生在 - close 后**同一轮**,而锁存在 close 后 ~10-20ms 才落(TRAP-NET-12 实证)→ 沉降最后一轮 - 读清后残留一枚"迟到锁存",relisten 完成后 11ms 杀死首条连接。30ms 重试粒度 vs - 10-20ms 锁存延迟 = 每次重建掷一次硬币(E7b 的 ~2% 偶发即此)。 -2. **泵与救回互抢**:沉降段 `:172` `net_drain_chip_rx` 排空的正是 closed-with-data - 救回(`net_socket.c:1317`)依赖的 RX 数据;pump 沉降与 lftpd 救回在同一 socket 上 - 竞速——救回 accept 成功返回(`:1352`)但 lftpd 拿到的是**被杀会话/被排空半份的数据** - → 短读 → 425。风暴期 31 次 accept timeout 与 38 次救回并存即此矛盾的直接证据。 -3. **芯片 OPEN 卡死放大**:被杀会话芯片侧仍 OPEN(0x05),新 SYN 被芯片接进卡死 - socket(TCP 槽仍 0x00)→ net 层 accept → 又被下一枚锁存杀 → 风暴自持,直到 - 800ms 强制放行 + close 真正落拆链才出簇。 - -**对 M3/M5 设计的修正(比 T5 版更锐)**: -- **M3 判据升级**:杀事件时刻 `chip=0x05/0x00`(socket OPEN + TCP 槽 0x00)与 - "连接年轻(<1s)"组合 = **逻辑矛盾态**(活连接应 0x04)→ 不释放,交对账①电平 - 去抖二次确认。该判据在风暴窗口 100% 命中、健康路径 0 误伤(健康断连芯片为 - 0x00/0x00 或 0x07)。 -- **M5 语义升级**:`sock_reset` 必须 **drain-until-quiescent**(close 后轮询 SINT - 直到 DISCONNECT 锁存出现并读清、连续两轮无新锁存才 open+listen),固定 100ms - 窗在锁存延迟抖动下必输硬币。 -- **新增红线**:沉降/pump 重建前**必须让位**于 closed-with-data 救回(RX 有数据 - = 消费者资产,先交还 accept 路径,排空后才准 reset)——泵与救回必须单一所有者, - 归 `sock_reset` 内联裁决。 - -### E9 结果(2026-10-13 00:30–00:47,M3 原型三轮迭代,台架锁 N34) - -**T6 先修正了 E8 的机制表述**(详见盘点「F-NET-17 补充」):close 已 CLOSED socket -**不产生**锁存(TC-NET-204 模式 C 证伪强形式);锁存生成者=拆链完成事件(滞后+粘滞); -"沉降末轮读清不保证静默"仍成立,经"拆链滞后于末轮读清"+"剩余两处全局读清搁浅"两路径。 -M3 因此从"可选加固"升格为**结构性必需**(静默可被新连接自身流量打破=无界窗)。 - -**M3 判据三轮迭代(每轮都是台架数据说话)**: - -| 轮 | 判据 | n30(预置残渣后) | Trap23 探针 | :21 @0ms×50 | -|---|---|---|---|---| -| R1 | `0x05/0x00 + flag==0` | **0/50** ✓ | **0/5+0/5** ✗ | — | -| R2 | +`recv_bytes==0` | **0/50** ✓ | 5/5+5/5 ✓ | **46/50**(4 refused,p90=1540)✗ | -| R3 | +`芯片 RX>0` | **0/50** ✓ | 5/5+5/5 ✓ | **50/50** ✓ | - -- **R1→R2 教训**:已读数据的会话(探针控制通道)遇 0x05/0x00 多为真死(RST/槽污染), - 延迟它实测 **~1.2s**(对账 1s 冷却,非 T6 估的 400ms)打破应用窗口。 -- **R2→R3 语义修正**:延迟保护的本质=**"芯片 RX 有在途数据"=不可毁的消费者资产** - (closed-with-data 救回同源红线)。@0ms 快复连(连上即关零数据)RX 空 → 立即释放, - 误杀也无数据损失,后到数据由救回接住。 -- 最终判据四条件:`st=0x05/0x00 && lost_conn_seen==0 && recv_bytes==0 && RX>0`; - 前置补 `handle_connect_event` 清 `lost_conn_seen`(两处,T6 发现的必要前置); - 对账①**零改动**认领(E9 实测 71 suspect → 全部正确接管/释放)。 - -**E9b 全套回归(R3 固件,Code=209552,0 错 0 警,@test PASS 回读无差异)**: -n30 ×50(预置后)**0/50** + ×20 **0/20**;ftp_regression 11/11;:21 @0ms×50 **50/50**; -:7003 50/50 p50=1007/p90=1037(回到 E7b 水平);iec104 18/18;Trap23 RST/FIN 5/5+5/5。 - -**遗留观察(非 E9 阻塞,已归因)**::21 @0ms×50 窗口有 `[WD] task stall`(多任务 miss) -——该窗 M3 suspect **零触发**(资产门正确让位),归因于 :21 暴力用例本身的断连 churn -(40 DISCONNECTED + 157 settle 行/20s,E7b 时代已有 MISS start 前例);列入阶段 2 -`sock_reset` 沉降内联后的复验项。B10 的 :21 守护口径(50/50)通过;stall 是否先于 -M1 存在无基线可考(历史日志未入库,又一"脚本在库、日志未入库"实例)。 - -**裁决**:M3(R3 资产门版)具备转正条件;风暴态免疫在"探针预置残渣"最恶劣前置下实证。 - -### E10 结果(2026-10-13 05:40–06:50,阶段 0 + 阶段 1 转正验证,台架锁 N34) - -**范围**:M3 转正提交(`ae17d36`)+ 阶段 0 九项(`4c0a0af`)+ 阶段 1 六项 -(`b8c6a3d`,16 文件 +193/-371)。三笔均 `@build` 0 错 0 警 + `@test` PASS -(回读无差异)。 - -**阶段 0 验证**(Code=209580):全量重编 0 错 0 警(含强制重编依赖 TU 以暴露 -头文件改动引入的警告——首次增量构建 9 条 `#47-D` 宏重定义警告即由此抓出: -`CH395F_FUN_PARA_SOCKET_CLOSE` 与既有宏块重复定义,已并入原宏块); -冒烟 n30 0/20、:21 @0ms 20/20、:7003 20/20、ftp_regression 11/11。 - -**阶段 1 首版台架失败与根因(本记录的核心留痕)**: - -| 项 | 首版 | 根因 | 修复后 | -|---|---|---|---| -| iec104_master | **18/18 全 FAIL**(127s 跑满超时) | `net_listener_armed` 首版只放行 LISTENING / CLOSED+残留,**漏了 ESTABLISHED** | 18/18 PASS,**8.3s** | -| n30 / :21 / :7003 / ftp / Trap23 | 全绿 | 未受影响(见下) | 全绿 | - -**机制**(为什么只有 i104 中招,是一条非显然的坑): - -- 单连接模型下"连接已到、应用未 accept"的电平就是 **ESTABLISHED**(CONNECT - 处理把监听 fd 自身转数据通道)——这正是旧 `net_can_accept` 唯一放行的状态。 -- 漏放行后能否活下来纯看**消费者任务周期与对账②去抖赛跑**: - ui_net_sink 10ms 周期 → `net_accept_locked` 内自带 `net_poll` 通常先处理 - CONNECT 并在同一次调用里返回 fd(探针/在途 accept 抢先); - **i104 200ms 周期抢不过对账② 的 ~400ms 去抖** → 对账② standalone accept - 先转 ESTABLISHED → armed=0 → 应用整场不 accept、芯片 RX 数据无人读。 -- 设备日志证据链:`sock1 standalone accept`(06:24:23)→ 之后整场无 - `i104: accepted`,直到对端 FIN 才由 `closed-with-data accept` 救回 - (`i104: accepted fd=1` 06:26:30)。期间主站 I 帧全部无响应。 -- **修复语义**:armed = 旧 `net_can_accept` 的 ESTABLISHED 放行态 ∪ LISTENING - ∪ CLOSED+残留。此态 accept 必返 fd,一次调用即离开分支,**无 RPC 风暴风险** - (P2a 的 task stall 约束针对 CLOSED 无残留态,不适用)。 - -**M3 无辜旁证(顺带实证)**:同一窗口 `sock1 suspect DISCONNECT on live conn -(chip 0x05/0x00), defer to reconcile` 命中 → 对账① 1.2s 后接管 → -`closed-with-data accept (drain pending)` 把**未被 accept 的 RX 数据**救回 -——正是 M3 资产门的设计意图(芯片 RX 有在途数据 = 不可毁的消费者资产)。 - -**阶段 1 全绿回归(修复版,Code=209580,@test PASS)**: -iec104 **18/18**(8.3s,较基线 127s 大幅缩短:连接即刻被 accept); -n30 ×50 **0/50**;:21 @0ms×50 **50/50** p50=512;:7003 100ms×50 **50/50** -p50=517;ftp_regression **11/11**;Trap23 RST **5/5** + FIN **5/5**。 - -**time_sync 第 5 项(会话收尾不 close)实测**:连续 6 次推送成功(RTC 逐次 -更新),每轮 `standalone accept → DISCONNECTED, releasing → relisten settle → -auto-relisten ok → 下一轮 accept`(周期 ~4s = PC 侧 2s 宽限 + 2s 间隔), -零 `[ERR]`。契约变化已同步 `test/host/time_sync_server.py` 注释:板子不再先 -关,PC 侧 2s 宽限后自行关闭。⚠ 注意:若 PC 进程被**强杀**(非正常关闭), -芯片侧会走 `[ERR] sock0 TIMEOUT` + QUARANTINE 沉降(400ms)再重建——本轮 -首次验证即因强杀验证进程复现一次,属异常收尾,非正常路径。 - -**阶段 1 与实施方案的两处偏差(诚实登记)**: - -1. 第 6 项原计划"PASV 监听失败改走 `net_listener_try` 登记",实施时判定该路 - 径会把 armed/BOUND/D10 状态语义一并拖入(需给 `net_relisten_locked` 补 - `auto_relisten`、给 armed 补 BOUND 放行、改 0-4 状态门),代价与一个罕见 - 故障不匹配 → 改为**单次尝试 + 失败即 425**(主流 FTP 服务器 passive - listen 失败亦如此,客户端自带传输重试语义),并删除 `cmd_pasv` 的 - 3×200ms 重试。 -2. 第 2/3 项原计划拆分两笔提交,实施时因 0-A 与 0-5 的改动在同一文件内交织 - → 阶段 0 合并为一笔(0-3/0-4/0-6 的逻辑改动在提交信息中逐条列出并有 - 冒烟证据),阶段 1 一笔。判据与验证不受影响。 - -**编译面验证(阶段 1 附带)**:`net_latency_test.c` 的 Phase 13 清理位于 -`#ifdef ENABLE_PHASE13_TESTS` 内、产品构建不编译 → 临时开 CH395F 套件 + -Phase13 宏全量重编 **0 错 0 警**(Code=216632)后还原宏再重编产品镜像, -确认死代码无语法/符号残留。 - -**收尾门禁(`ci_local.py`,退出码 0)**:L0 pre-commit 钩子 **32/32 通过 -(0 失败 0 跳过)**、L0b 提交信息 gitlint **10 条 0 失败**、L0.5 主机侧单测 -**15 runner / 951 断言全过**。过程中修掉两处真实收尾问题(各一笔提交): - -1. **time_sync 单测契约残留**(`b3ef728`):阶段 1 第 5 项删掉 - `time_sync_handle_conn` 的 `net_close` 后,4 条主机侧断言仍要求"处理完关闭 - 连接"(TC-TS-020e/023b/024b/027c)→ 改为 `s_close_calls == 0` 并补契约变更 - 注释(任务级关闭由 `conn != ls` 分支负责,TC-TS-040e 仍覆盖)。80/80 PASSED。 -2. **分支级 RAM 台账漂移**(`bfbe24e`):本分支自身代码增长(net_listener.c 等) - 从未刷新 RAM 文档受管区 → mem-numbers / mem-budget 在分支上长期失败 - (任何 App/*.c 提交都会撞),此前靠 `--no-verify` 绕过。用项目工具 - `check_mem_numbers.py --write` + `ram_baseline.py --write` 同步:ER_IROM1 - 227,824→228,172 B、Flash 合计 228,928→229,276 B;**RAM 两区未变** - (RW_IRAM1 97,864 B/74.7%、RW_CCM 57,376 B/87.9%),印证阶段 0/1 未动 RAM。 - 此后本分支常规提交不再需要 `--no-verify`。 - -### E11 结果(2026-10-13 07:01–07:18,CH395F 服务器侧 KeepAlive,临时实验固件) - -**问题**:`CMD_SET_KEEPALIVE_SN`(手册 §5.56)——芯片在"IDLE 无收发 → 探测 -CNT 次无 ACK"后产生 SINT_STAT_TIM_OUT / DISCONNECT。怀疑本仓没有利用它。 - -**静态证据(怀疑成立)**: - -| 事实 | 证据 | -|---|---| -| 参数配了:idle 60s / intvl 5s / cnt 3(均 500 倍数、IDLE>INTVL) | `net_config.h:61-70`,`net_socket.c:298-302`(INIT_CH395 之前) | -| 开关**全仓仅一处**,且在 TCP **客户端**分支 `CONNECTING` | `net_socket.c:3257` | -| `App/` 目录**零个 `net_connect()`** → 生产无任何 socket 会走到该行 | grep 全仓 | -| 手册明确:TCP **服务器**应在 `SINT_STAT_CONNECT` 后开启 | `docs/芯片手册/CH395F.md` §5.56(:815-821) | -| 服务器侧调用 2026-10-12 曾加后**撤回**,理由仅覆盖"物理链路失效";注释自认"链路在、对端静默"未覆盖且**无法注入** | `net_socket.c:3223-3229` | -| 唯一能发现该缺口的 Phase 11 测试**默认关闭** | `test/board/ch395f_test.h`(`//#define ENABLE_PHASE11_TESTS`) | - -**注入手段的两次修正(方法学留痕)**: - -1. **Windows 防火墙入站 DROP **无效****:连接建立后再加规则,对该已建立连接 - 不生效(ALE 层在连接建立时判定)。正对照实测:装规则后 KEEPLIVE 回显**照常 - 收到** → `VERDICT: injection INEFFECTIVE`(`e11_inject_check.py`)。这也解释了 - 首轮 235s 窗口无 TIMEOUT(对端一直在 ACK,并非 keepalive 失效)。 -2. **有效注入 = 禁用 PC 网卡**:中间有交换机(判别依据:ESTABLISHED 会话下 - 禁用 PC 网卡 12s,设备**零网络事件** → 设备侧 PHY 链路保持)。此时对端不可达、 - 设备链路在、且不产生 FIN/RST —— 正是"链路在、对端静默"。 - -**实测(实验固件:`handle_connect_event` LISTENING 分支加 -`ch395f_set_keepalive_enable(sock,1)` + 调用点 DBG 留痕;TCP :7003)**: - -| 试验 | 配置/注入 | 结果 | -|---|---|---| -| T2 正常空闲存活 | 生产参数 60/5/3,对端存活但静默 90s | KEEPLIVE 回显正常、**无 TIMEOUT** → 不误杀空闲会话 ✓ | -| T4 定标(短参数) | 6s/2s/2,禁用 PC 网卡 40s | 留痕 `keepalive enabled (idle=6000 intvl=2000 cnt=2)`;**T+5.1s** 报 `[ERR] sock3 TIMEOUT` → 参数按配置缩放,芯片默认值(20/15/9)假设**被否** ✓ | -| T5 生产参数 | 60/5/3,禁用 PC 网卡 90s | 留痕 `idle=60000 intvl=5000 cnt=3`;**T+30.0s** 报 `[ERR] sock3 TIMEOUT` → QUARANTINE → relisten → **新连接成功恢复** ✓ | - -**结论**: - -- KeepAlive 能力**真实可用**,且本层**已有现成消费者**(`net_poll` 派发 - TIM_OUT → `handle_timeout_event` QUARANTINE → pump 重建)——打开开关即可闭环。 -- 生产参数下实测判死 **≈30.0s**(早于手册公式 75s;芯片定时语义与手册公式 - 不一致,**以实测值为准**)。短参数实测 ≈5.1s,证明参数生效且可按需调优。 -- 静默但**存活**的空闲会话不被误杀(T2)——因为对端会 ACK 探测包。 -- 2026-10-12 撤回的盲区(链路在、对端静默)**正是本机制覆盖的场景**;当年 - "+2s 即报 TIM_OUT"只覆盖了物理断链。 -- **已采纳转正(2026-10-13,用户批准)**:在 `handle_connect_event` 的 LISTENING - 分支加回一行 `ch395f_set_keepalive_enable(sock, 1)`(手册规定位置),并重新打开 - Phase 11 测试纳入回归;参数是否调优(如缩短 IDLE)由实测判死窗口需求决定。 -- ⚠ 台架副作用:本轮反复实验后设备落入当时称为"残渣风暴"的状态(详见 E12)。 - **该状态已由 E13 重新定性为僵尸会话,30~90s 自动清除,无需冷启动。** -- **产品路径验证(2026-10-13 10:32,转正提交后产品固件)**::7003 连接 → - 禁用 PC 网卡 51s → `[ERR] sock3 TIMEOUT`(**禁用后 17s**)→ QUARANTINE → - pump → `auto-relisten ok` → 网卡恢复后新连接 PASS(KEEPLIVE 回显正常) - → 转正的一行在**产品固件**上闭环生效 ✓ - -### E12 结果(2026-10-13 07:18–08:05,SPI 缓存区 TSL 时序 A/B,临时实验固件) - -**问题**:手册 §7.4——SPI/并口模式下读写收发缓存区命令**只需**在"长度与数据 -之间"留 TSL(读 0.4us / 写 0.2us),其余字节间无需间隔;**非缓存区命令**另有 -TSC=0.6us(命令→数据)、TSD=0.3us(数据字节间)。本仓是否满足? - -**静态证据(没考虑过,且缓存区路径零间隔)**: - -| 事实 | 证据 | -|---|---| -| 全仓无任何 TSL 相关代码/注释(grep 只命中手册) | — | -| 两个缓存区函数把 `[cmd, sock, lenL, lenH, data...]` 打包进**同一 DMA 缓冲、一次连续突发**(CS 全程拉低)→ 长度→数据**间隔 = 0** | `ch395f.c` `write_send_buf` / `read_recv_buf`(原实现) | -| 其余命令走逐字节 `HAL_SPI_TransmitReceive(...,1,...)` → TSC/TSD 有 µs 级余量 ✓ | 同上 | -| 量化:SPI2 分频 8 @APB1 42MHz = **5.25MHz**(单字节 1.52us);TSL 0.4us ≈ 2.1 个 SCK 周期 | `Src/spi.c:84` | - -**A/B 设计**:A = 现状(零间隔单突发);B = 头/数据两段传输 + 段间显式忙等 -(`CH395F_TSL_LOOPS=40` ≈1.0–1.4us,另叠加两段间 HAL/DMA 重配开销;CS 全程拉低, -帧形与手册图一致)。B 代价:代码 +28B(Code 209580→209608)、写路径多一次 -memcpy;**吞吐无回退**(A 40×128KB 51.8s vs B 47.9s)。 - -**字节级校验(`test/host/ftp_integrity_test.py`:STOR→RETR 全量 SHA-256 比对, -差异自动二次回读归因)**: - -| 臂 | 样本 | 结果 | -|---|---|---| -| A(零间隔) | 260 轮(20×64KB + 240×128KB ≈32MB/方向) | **1 次不一致**(A2 轮,细节未留档,A3/A4/A5 未复现) | -| B(两段+TSL) | 200 轮(40+100+60 ×128KB ≈25.6MB/方向) | **0 次不一致** | - -→ **未观察到 TSL 造成的可复现数据错位**;两臂均接近"零错位"。 - -**连接级 n30 ×50(每次运行前重烧录 = MCU 复位 + RESET_ALL)**: - -| 臂 | 各轮失败数 | 有失败的轮次 | -|---|---|---| -| A | 8, 22, 0, 0, 12, 0, 0, 39 | 4/8 | -| B | 0, 0, 0, 0, 0, 47 | 1/6 | - -**污染控制与关键反证**:序列末恢复产品镜像后再跑 n30 ×20 = **16/20 失败**, -日志签名 = PASV 数据连接被芯片接受后立即关闭(`sock4 st=0x00/0x00`)+ 控制连接 -TIMEOUT → 即 E8 记录的**残渣风暴态**,且**跨烧录存活**(重烧 + @test PASS 后依旧) -→ 与 E8"普通复位后仍 12%"一致。A 39/50 与 B 47/50 **两臂都出现**灾难轮 -→ 该差异**不能归因于 TSL**,E12 的连接级对比**被此状态噪声吞没,无结论**。 -> ⚠ **"跨烧录存活"口径已被 E13(2026-10-13 10:55)推翻**:板上 CH395F 的 -> RSTI 脚接 MCU NRST(`docs/硬件平台/V0.0_网表详细分析报告.md` :174 -> "RSTI←NRST")→ 每次 ST-LINK 烧录/复位都会**硬件复位芯片**;该状态实为 -> **僵尸会话**(E13),锤击几分钟内即再生,"跨烧录存活"是再生造成的错觉。 - -**结论(诚实口径)**: - -1. 现状驱动**确属规格偏离**(零 TSL 间隔,按"间隔"语义解读),但**未测得实际危害**: - 460 轮/≈58MB 双向字节级校验仅 1 次未复现异常,且两臂无差异。 -2. 依 TRAP-NET-20 铁律「不留未证明的复杂度」:**当前不建议改产品路径**,而是 - **登记该偏离 + 本轮证据**;B 变体作为**待用补丁**保留(若将来出现芯片批次 - 差异、提高 SCK、或不明数据错位,可直接采用——它已证明无吞吐/正确性回退)。 -3. **方法学教训(E13 修正版,后续时序/连接类实验必读)**:本台架的**僵尸会话 - 态**(曾称"芯片残渣风暴")必须在实验前后受控:由"对端静默死亡"(网卡禁用/ - 拔线/PC 崩溃)触发,芯片 TIMEOUT(有在途 TX ≈30s)/ keepalive(纯空闲 - ≈75-90s)自动清除;锤击会在几分钟内再生僵尸 → 同一镜像重烧后仍可 - 0/50..47/50 摆动、跨臂污染使 A/B 失效。**正确控制 = 下一臂开跑前静置 - ≥90s 让僵尸清窗**,不是断电冷启动(RSTI←NRST,烧录本身就是芯片硬件复位)。 - -### E13 结果(2026-10-13 10:24–10:59,风暴态根因=僵尸会话:确定性复现,产品固件+网卡抖动注入) - -**假设**:E8/E12 记录的"残渣风暴态"不是跨烧录存活的芯片内部残渣,而是 -**僵尸会话**(对端静默死亡 → 芯片侧 ESTABLISHED 未清)+ 单连接监听被僵尸占用 + -PASV 重监听被垂死僵尸 accept 的竞态——且可由网卡抖动**确定性复现**。 - -**推翻"跨烧录存活"的硬件事实**:板上 CH395F 的 **RSTI 脚接 MCU NRST** -(`docs/硬件平台/V0.0_网表详细分析报告.md` :174 "RSTI←NRST";手册 -`docs/芯片手册/CH395F.md` :935 亦载"通过 RSTI 引脚拉低控制复位") -→ 每次 ST-LINK 烧录/复位(`-Rst -Run`)都会**硬件复位芯片**。 -"风暴态跨烧录存活"与接线事实矛盾——存活的不是芯片内部状态。 - -**排除 4 元组复用假设**(`test/host/pasv_probe.py`,本轮新增):5 轮干净 STOR -打印每轮 PASV 端口 + PC 源端口 → PASV 端口**递增**(49157→49161)、PC 源端口 -随机变化 → "芯片复用 PASV 端口 + TIME_WAIT 4 元组冲突"假设**被否**。 - -**确定性复现(100%,产品固件,零插桩)**: -`ftp_integrity_test.py --rounds 40 --size 131072` 正常跑到第 21 轮(前 20 轮 -全 OK)→ **禁用 PC 网卡 8s**(10:55:01,打断传输中会话)→ 恢复网卡 → -`n30_repro.py` 首连 :21 **ConnectionRefused**。 - -**因果链全程(串口日志)**: - -| 时刻 | 事件 | 解读 | -|---|---|---| -| 10:55:01 | 网卡禁用:PC 内核直接丢弃全部 socket,**不发任何 FIN/RST** | 芯片毫不知情 → sock2/sock4 成 **ESTABLISHED 僵尸**;设备侧 30s 内零网络事件 | -| 10:55:30 | `[ERR][NET] sock2 TIMEOUT` | 芯片**重传耗尽**(设备有未 ACK 的在途 TX)→ 僵尸清除,**+30s** | -| 10:55:31 | `sock2 pump: chip not listening (0x05/0x00) -> relisten` → `auto-relisten ok` | 本层 pump 接住 → 监听复活 | -| 10:55:43 | 新连接 accept → PASV → `sock4 standalone accept` → **20ms** 后 `suspect DISCONNECT on live conn (chip 0x05/0x00), defer to reconcile` | **PASV 竞态**:sock4 的僵尸(被打断轮遗留)未清,新监听器的 accept 为垂死僵尸开枪 → lftpd 5s accept 窗空转 → 425 | -| ~10:58 | n30 ×3 全 OK | sock4 僵尸被 TIMEOUT/reconcile 清除 → 自愈完成 | - -**结论**: - -1. **风暴态根因 = 僵尸会话**,不是芯片残渣:对端静默死亡(网卡禁用/拔线/PC - 崩溃/进程被杀)是注入源;上午 E11 反复网卡抖动 + E12 460 轮锤击 - = 完美风暴生成器。失败率随时间摆动(E8 的 2%~54%)= 僵尸生成/清除的节奏。 -2. **"跨烧录存活"是错觉**:重烧后芯片干净,但锤击几分钟内再生半死会话。 - E8/E12 的"需断电冷启动"口径作废(见上方修正标记)。 -3. **现有自愈链端到端有效**(② keepalive 转正 + pump + 对账① 的合力, - 本轮首次在同一场景全链验证): - - 有在途 TX 的僵尸:芯片 TIMEOUT(重传耗尽)≈30s; - - 纯空闲僵尸:keepalive 60/5/3(E11 T5 实测 30s、产品验证 17s); - - 本层 pump/对账接住事件 → relisten → 端口恢复。 -4. **剩余缺口(阶段 2 收口)**:PASV 重监听**不预清**数据 Socket——僵尸未清 - 就在同一 Socket 上开新监听 → accept 为垂死僵尸开枪 → 5s 窗。阶段 2 - `sock_reset`(open 前 close+等 CLOSED)直接消除该竞态。 -5. **e02_probe listener=FAIL 重解读**(非阶段 1.5 回归):探针的挂起客户端 - socket 在 PC 侧仍打开(脚本在第二次连接尝试之后才 close),PC **内核**会对 - 芯片 keepalive 探测自动 ACK → 僵尸会话"活着" → 单连接监听被占、拒新连接是 - **正确行为**;探针预期("预算耗尽 → 设备拆连接")与阶段 1-5"EOF 不 close" - 设计冲突。真实缺口 = **应用层无法主动拆掉自己放弃的会话** → 也是阶段 2 - `sock_reset` 的职责(应用侧形态)。 -6. 顺带登记(显示层,低):`[FTP] connection received from 100.1.168.192:21` - ——对端地址显示为设备自身 IP 的字节序反转 + 端口 21,疑似 lftpd 日志把 - local 地址当 peer 地址打印(不影响功能,未立项)。 - -**工具与留痕**:`test/host/pasv_probe.py`(新增);pktmon 抓包留存 -`%TEMP%\n30_cap.etl`(3.9MB,含网卡抖动窗口,设备 IP 过滤)。 - -### E2 结果(2026-10-13 11:43–11:50,bench 实验固件 Code=213412,跑两遍,台架锁 N34) - -**固件**:复活 `test/board/ch395f_reuse_bench.c`(N30_CHIP_BENCH 门控,内核前裸机; -旧 N-30 定案矩阵由 `s_bench_legacy=0` 停泊保留);注册 uvprojx Test 组 + `Src/main.c` -USER CODE 2 区(注册不入库,实验完 `git restore`)。DWT CYCCNT @168MHz(1 cycle≈5.95ns)。 -两遍独立运行,本节数字两遍一致(第二遍为第一遍 C 变体级联污染后的干净重跑,E2/E3 复验)。 - -**单次回读成本**(`ch395f_get_socket_status`,N=1000×2 遍,两遍逐值相同): - -| 统计 | cycles | µs | -|---|---|---| -| min | 3048 | 18.1 | -| avg | 3057 | 18.1 | -| max | 3481 | 20.7 | - -**LISTEN 可见性矩阵**(裸 TCP_LISTEN 不轮询命令态,发命令后 +0/10/50/100ms 回读; -100ms 仍不可见则 10ms 节奏续查到 1000ms;12 轮×2 遍=24,轮间换端口): - -| 首次观察到 TCP=01H | 计数 | -|---|---| -| +0ms | 14 | -| +10ms | 10 | -| >10ms | **0** | - -**判据结论**: -1. 单次回读 18.1µs ≪ 100µs → **"每 poll 全量刷新"成立**(4 监听 × ~18µs ≈ 72µs ≪ - 1ms 轮询周期;`docs/网络/N34-自愈审计/实施方案.md` §4 第 1 项预估 ~15µs 量级坐实)。 -2. LISTEN 可见延迟有界:100% 在 +10ms 内可见、无长尾 → **reconciler 去抖可减为 1 次** - (决策点后 ≥10ms 单次回读即可裁定 LISTEN 形态;现"连续 2 次"去抖对该形态无必要—— - 瞬态窗口仅存在于 0~10ms 内,且第二次回读必在窗口外)。 - -**E2 补充发现(2026-10-13 12:23,Phase 15 板测 TC1501/1502b 实证):LISTEN 态 -CLOSE_SOCKET 是二段关闭**。CLOSE 命令对 LISTEN 态 Socket 返回 SUCCESS(0x00), -但只拆监听(TCP 01H→00H),**byte0 恒留 05H(Socket 仍 OPEN),2s 内不自落**; -需再发一次 CLOSE(此时 TCP 已 00H)byte0 才落 00H(二段间隔实测 0ms 即落)。 -生产 net 层早已在**无意中吸收**该语义:`net_close_locked` 等 CLOSED 的 500ms 窗 -超时后照常放行,随后重 listen 时 `net_listen_locked` 的 TRAP-NET-09 open 重试 -(3 次 × 500ms 沉降)第二次 open 前的 close 恰好充当二段——这就是 TRAP-NET-09 -"关闭后需延时才能重开"重试环存在的芯片级原因之一。阶段 2 `sock_reset_poll` -的 WAIT_CLOSE 阶段已内置二段补发(标记=drain 字段 0xF,只补发一次), -TC1502b 实证 LISTEN 态复位 GRACEFUL ~180ms 落定。 -**对阶段 3 的含义**:任何以 `cmd_done(DONE_CLOSE)` 为判据的等待,若目标 Socket -可能处于 LISTEN,必须容忍"一次 CLOSE 不落 00H"——要么走 sock_reset(已内置), -要么保留 TRAP-NET-09 式重试,不得把 DONE_CLOSE 当单次 CLOSE 的充分条件。 - -### E3 结果(同场同固件,5 命令 × 5 轮 × 2 遍 = 50 样本,两遍签名逐条一致) - -**方法**:发命令前对照读 GET_CMD_STATUS(记录残留假阳性);发命令后按手册 -`docs/芯片手册/CH395F.md` §9.2.1 节奏(≥2ms 首查)以 ~1ms 轮询至终态(2s 上限)。 -`pre`=对照读值,`first`=首个非 BUSY 终值,`busy`=是否观察到 BUSY→终态转变。 - -| 命令 | pre(对照读) | busy | first | done | 判别力判定 | -|---|---|---|---|---|---| -| OPEN_SOCKET | 0x00(r0) / **0x17**(r1-4) | 否(0/10) | 0x00 | 3ms | **部分**:状态确被 OPEN 更新(0x17→0x00),但 pre=SUCCESS 时完成与残留不可分 | -| TCP_LISTEN | 0x00(残留 SUCCESS) | 否(0/10) | 0x00 | 3ms | **不成立**:无转变,残留 SUCCESS 不可甄别 | -| CLOSE_SOCKET | 0x00(残留 SUCCESS) | 否(0/10) | 0x00 | 3ms | **不成立**:同上 | -| SET_SOUR_PORT | **0x17** | 否 | **0x17(不变)** | — | **不成立**:SET 类根本不更新命令状态 | -| SET_PROTO_TYPE | **0x17** | 否 | **0x17(不变)** | — | **不成立**:同上 | - -**三条实证**: -1. **残留假阳性坐实**:陈旧错误码 0x17(ERR_VAL,源自"对已关闭 Socket 再 close") - 跨后续 SET 命令(不更新状态)持续存活——复核 `docs/问题记录/CH395F_Trap_Records.md` - TRAP-NET-20 定案轮的"陈旧残留"形态(RESET_ALL 陈旧 BUSY 变体本矩阵未复现,同类 - 陈旧值持续存活已证)。 -2. **OPEN/LISTEN/CLOSE 均在 <2ms 完成**,BUSY 从未观察到——"BUSY→终态"转变对这三条 - 命令不可观测;驱动 `poll_cmd_status` 20ms 粒度对它们恒白等 ≥20ms(无害但无判别价值)。 -3. **SET 类无判别力**(命令状态完全不被更新)——Trap 记录结论复核成立。 - -**判据结论(定案 `docs/网络/N34-自愈审计/实施方案.md` §4 第 4 项)**:五条命令均不满足 -"发后 ≤N ms 变终态 **且** 无残留假阳性" → **`ch395f_cmd_done` 完成判据改用状态回读**: -open 完成=byte0 变 05H;listen 完成=TCP=01H;close 完成=byte0=00H -(回读成本 18.1µs 由 E2 实测背书;listen 可见延迟 ≤10ms 由 E2 矩阵背书,成本与时效均成立)。 -GET_CMD_STATUS 仅保留两用途:INIT_CH395 类慢命令(内核前/测试路径)与**命令级被拒** -检测(非 SUCCESS 非 BUSY 错误码仍会被芯片写入命令状态,B2 判据仍有效——被拒码是 -"新值覆盖残留"方向,不受残留歧义影响)。`poll_cmd_status` 保留给内核前/测试路径, -产品 socket 路径弃用。 - -### E4 结果(同场同固件 + 生产路径基线;3 变体 ×10 轮 ×2 遍 = 60 轮) - -**方法**:每轮先造污染会话(主机 `test/host/ch395f_reuse_host.py --mode e4`:连上 → -发 3000B → RST,TRAP-NET-18 中止形态),芯片侧按变体执行复位序列,**立即**原端口重开 -+ LISTEN,主机立即上传 4096B 图案载荷(图案种子随全局轮次变,上一轮残渣必失配)。 -PASS 口径 = 消费者在 DISCONNECT/TIM_OUT 前实际读到的字节数=4096 且图案逐字节匹配 -(= 生产 net 层释放语义;事后读空仅作取证,不计入 PASS)。 - -| 变体 | 第 1 遍 | 第 2 遍 | 观察 | -|---|---|---|---| -| A 读空 RX→CLOSE→排事件→等 byte0=00H | **10/10** | **10/10** | 19/20 轮 rxdrain=**2048**(残渣恰等于芯片单 socket RX 缓冲整容 = TRAP-NET-18 机制直接证据) | -| B CLEAR_RECV_BUF→CLOSE→排事件→等 CLOSED | **10/10** | **10/10** | 与 A 等价,无差异 | -| C 什么都不做→CLOSE→等 CLOSED | **0/10** | **0/10** | 首 C 轮(rg20)两遍同签名,随后**自持级联** | - -**C 变体失败签名(两遍逐字节复现)**:重开后主机载荷连接 SYN 被吞(主机侧 -connect 超时);bench 首次 SINT 读 = **0x04**(陈旧 RECV 位粘滞——TRAP-NET-12 -"粘滞到被读",close/open 不清 SINT);后续轮污染连接亦 SYN 被吞(主机 -`contam CONNECT timeout`、bench `no death event`)——每轮 close 又携带未排空数据 -→ **级联自持**,open+listen 命令照常返回 SUCCESS 也无法恢复,唯芯片硬件复位 -(E13 RSTI←NRST)清除。 - -**机制定案**:决定性一步 = **close 前 RX 缓冲必须为空**(读空与 CLEAR_RECV_BUF -等价——TRAP-NET-20"缓冲重分配无效"指的是 set_send/recv_buf 重分配,不是 CLEAR)。 -带非空 RX 关闭违反 `docs/芯片手册/CH395F.md` §5.52 bit3=1"读出残留数据后再关闭" -契约 → 芯片拆链停滞 → 下一会话 SYN 被吞/CONNECT 丢失,且自持。排空事件(A/B 有、 -C 无)清的是粘滞 SINT 位——对**事件驱动 accept 的正确性**必需(0x04 陈旧 RECV 会 -误导消费者),但不是 TCP 层吞 SYN 的根因。所有轮 closed_ms=0(close 命令落定后 -byte0 已是 00H)——"等 CLOSED"几乎零成本,但为契约必需步。 - -**生产路径基线**(烧 bench 固件前,Phase 1.5 产品固件 Code=209780): -`test/host/ftp_abort_probe.py --check-abort --rounds 5` → **5/5**(中止 STOR→450; -设备日志 `sock4 TIMEOUT` → quarantine → `UPLOAD '/ab000N.bin' FAILED` → -followup `UPLOAD '/ab_ok.bin' 4096 bytes OK`)——现实现(隔离期持续排空 RX + -close + 排空事件 + 等 CLOSED)行为基线全绿,与变体 A 序列一致。 - -**判据结论(定案 `docs/网络/N34-自愈审计/实施方案.md` §4 第 3 项)**: -(a) 与现实现等价成立(A 20/20 载荷完整);(b) B 与 A 等价(无差异记录); -C 不等价(0/20 + 级联)→ **`ch395f_sock_reset` 内部序列定案**: - -1. 读空 RX(GET_RECV_LEN→READ_RECV_BUF 直到 0;CLEAR_RECV_BUF 等价可用,**取读空** - ——保 §5.52 契约且排空字节数可观测可留痕); -2. CLOSE_SOCKET; -3. 排空本 socket 事件(drain-until-quiescent,E8 M5 口径:连续两轮无新位); -4. 轮询 byte0=00H(非阻塞状态机步进,约束不变)。 - -GRACEFUL 与 QUARANTINE **都必须排 RX**——E4 假设"差异仅在是否排 RX + 最短等待"中 -"是否排 RX"一半被否(带脏缓冲关闭在任何释放形态下都危险),两模式差异只剩 -最短等待(100ms/400ms,TRAP-NET-18 二阶段实测值)。 - -### E-02 收口台架(2026-10-13,阶段 3 产品固件 Code=211348,`test/host/e02_probe.py`) - -**背景**:E13 §5 把 `e02_probe` 的 `listener=FAIL` 重解读为"探针预期与'EOF 不 close'设计冲突", -并指出真实缺口 = **应用层无法主动拆掉自己放弃的会话**。阶段 3 以 `net_drop_conn()` 收口该缺口, -本节记录收口后的台架定案时序。 - -**drop_conn 时序实测**(timeSync 30s 预算耗尽路径,:8888 单连接监听): - -| 阶段 | 观察 | -|---|---| -| 预算耗尽 | `time_sync: no TIME packet in 30s, drop conn` → netTask 侧 `sock_reset QUARANTINE`(FIN + 排渣 + 排空事件 + 等 byte0=00H,400ms 底)+ 本层收口 CLOSED + `auto_relisten` 即时 `net_listener_try` | -| 对端不配合(PC socket 仍开) | 芯片停 **`FIN_WAIT_2`**(byte0=`05H` / byte1=`06H`)长尾——`close` 无 RST(TRAP-NET-07 定案更正),监听恢复**受限于对端 close**;复位机在 `net_poll` 步进里等待,不搁浅 | -| 对端配合(探针 close 自己的 socket) | 芯片 `TIME_WAIT` 过渡 ~150ms(TC-NET-204 模式 A 同形)→ byte0 回 `00H` → **reconciler 形态④**(本层 CLOSED + 芯片不在 LISTEN)relisten | -| 恢复 | 对端 close 后 **~500ms** 内 :8888 重新接入(形态④ 去抖 2 轮 = 200ms + TIME_WAIT ~150ms + open/listen);新连接发完整 8 字节 TIME 包 → 设备落 **`wall clock updated`** | -| 探针判定 | `OVERALL: queue=PASS listener=PASS`(双 PASS) | - -**探针口径更新**(`test/host/e02_probe.py`,本轮改): - -1. **模拟守规矩对端**:第 3 步预算耗尽后,探针**主动 close 自己的 socket**(配合设备 FIN)—— - 这是"可恢复边界"的显式模拟;**不配合**(内核存活、自动 ACK FIN、socket 不关)场景**不在探针判据内**, - 其兜底 = 芯片 TIMEOUT/KeepAlive 拆僵尸(E13 定案 30~90s),探针头部已写明该定案口径。 -2. **3s 重试窗**:第二次连接由"单次 5s 尝试"改为 **3s 窗内 0.3s 节奏重试**—— - 恢复本身要经过 TIME_WAIT + 形态④ 去抖(合计 ~500ms 量级),单次即时尝试会把 - "正在恢复"误判成 FAIL;重试窗只放宽**时机**,不放宽"必须重新接入 + 对时落地"的判据。 -3. 顺序修正:close 移到第二次连接**之前**(旧脚本在第二次尝试之后才 close → 内核替挂起 socket - ACK keepalive → 监听被合法占用 → 旧 FAIL 的根因,见 E13 §5)。 - -**结论**:E-02 三段收口齐备——① 挂起 recv 不冻结 netTask(阶段 1.5);② 应用可主动拆会话保监听 -(`net_drop_conn`,幂等、EOF 路径不得调);③ 复位机不搁浅(`net_poll` 步进 `ch395f_sock_reset_poll`)。 -芯片侧物理边界(无 RST、`FIN_WAIT_2` 长尾受限于对端)已登记 `docs/问题记录/CH395F_Trap_Records.md` TRAP-NET-07 补充。 - -### E6 结果(2026-10-13 15:20–19:25,U15 插桩固件 Code=211692;用户裁决提前收线 ~4h,原规格 24h) - -- **配置**:`NET_U15_PROBE=1` 两段确认插桩(period=32、log_per_h=1),`U15 probe armed` ×2 确认 - (15:19/15:20 两次 boot);窗口内常态运行 + 每小时 `:7003×10`(gap=100ms)全过 + hour04 E1 压测 - (`:7003×50` gap=0 → 50/50、`:21×50` gap=100 → 50/50,拒连 0)。 -- **判据**:`U15 probe:` 命中 = **0**(含压测)→ 按 §E6 判据,U15 登记**观察项**,阶段 3 第 4 项 - (U15 内化)**不立项**;上层 32 轮盲收保留现状。 -- **零复位**:窗口内除 armed 两次 boot 外无启动锚点、无 Fault。 -- **附带观察(非 U15,登记备查)**:E1 `:21×50` 压测尾 25s 内 7 条 `[ERR][WD] task stall` tripwire - (miss≈2.5s 级、缺失位在 0x80/0x14/0x4 间漂移),无复位自恢复、50/50 连接全过;同窗 - `sock2 TIMEOUT` + `[FTP] error reading next command` = 客户端放弃连接的正常形态。 - 若产品构建复现再查 FTP hammering 下的任务饿死。 -- **收尾**:宏已回 0、产品构建重编烧录回归;插桩代码留仓(宏=0 零成本),需要时重新武装。 diff --git a/docs/网络/N34-自愈审计/网络不稳根因总览.md b/docs/网络/N34-自愈审计/网络不稳根因总览.md deleted file mode 100644 index 349400f1..00000000 --- a/docs/网络/N34-自愈审计/网络不稳根因总览.md +++ /dev/null @@ -1,119 +0,0 @@ -# 为什么网络「一直不稳」——七个误会与逐个拆掉(N34 复盘总览) - -> 定位:本文是 N34 自愈审计的**人类可读复盘总览**——把散在 Trap 记录、实验台账、 -> 实施方案里的根因,用一个贯穿比喻串成一条因果链,并逐环给出已落地的拆法与证据出处。 -> 技术细节与行号证据一律指向本目录其余文档与 `docs/问题记录/CH395F_Trap_Records.md`, -> 此处不重复。适合新成员第一遍读,也适合对外解释"我们的网络为什么曾经时好时坏"。 - -**一句话回答**:不稳从来不是"网断了",而是**一个会丢门铃、只会礼貌送客的芯片**, -配上一套**靠门铃驱动、人人插手重建、唯一传达员还会被按在椅子上半分钟**的软件—— -每个环节单看都"正常工作",叠起来就是随机、无日志、时好时坏的"玄学"。 - -## 0. 角色表 - -把 CH395F 芯片想成一个**外国籍传达室大爷**:他会收发信件(TCP 栈),但**门铃会坏** -(中断会丢)、**只会礼貌送客不会轰人**(close 无 RST)。netTask 是**全楼唯一能进传达室 -的传达员**(所有 CH395F 事件只在 `net_poll` 集中读清)。FTP、对时、UI 推流、IEC104 -是**楼里等信的住户**(各自监听一个端口,但共用这一个传达员)。 - -## 1. 七个误会(叠加才成为"玄学") - -### ① 门铃会丢(TRAP-NET-17:丢中断) - -快速复连时芯片本应"按门铃"(INT# 拉低)通知有新来访/客人离开,但门铃**会哑**—— -人已经站在屋里,铃没响。传达员只靠门铃干活,于是本层状态**静默滞留**:端口其实 -死了,日志里**一个错都不报**。当年"待机后 UI 复连 2/10 永久拒连、FTP 第 29 轮自己 -死掉"就是它——不是断网,是**没人报信**。裸机芯片台架(剥离 OS/net/FTP)独立复现, -证明这是芯片天性,不是固件 bug(见 `docs/网络/N34-自愈审计/README.md` 结论 1)。 - -### ② 门铃总闸是"看一眼就清零"(TRAP-NET-20 B3) - -全局中断寄存器是**一整排灯,读一次全部熄灭**。历史上 open/relisten 里有两处 -"丢弃式读一遍"——等于**查自己门铃时把全楼别的住户的门铃线也拔了**,别人的 -CONNECT/DISCONNECT 从此永久失踪。这是"自家制造的丢中断"放大器。 - -### ③ 柜台和货梯是同一台(单连接模型) - -监听 fd 就是数据 fd——**商店柜台平时兼货运电梯**。住户看到"客人走了"(EOF)习惯性 -`net_close` → **把柜台连电梯一起拆了** → 端口"连接被拒"。而芯片侧其实已自动恢复 -(auto_relisten),应用这一刀纯属帮倒忙。注意:单连接复用本身**不是**错误(主流硬 -TCP 栈全部同形),错的是住户不懂模型就动手。 - -### ④ 三个临时工都在修电梯(多所有者 relisten) - -accept、recv、pending 三处都会"顺手重建监听",谁先看到断连谁动手 → 互相踩脚: -重建风暴、慢消费者还没读完的 EOF 被吞、新客户端数据被旧会话静默吸收。修得越多越乱。 - -### ⑤ 传达员被按在椅子上(队头阻塞) - -一个挂起的对时客户端不发数据 → 旧代码 **30 秒阻塞收**;而 netTask 是**全楼唯一 -传达员**——这 30 秒里 FTP 的开门请求、UI 的连接请求全部排队冻结(E-02 实测 FTP -banner 迟到 25~30 s)。各处还有 500 ms 盲等忙等,传达员频繁"冻结半秒"。 - -### ⑥ 僵尸客人赖着不走(E13:网卡闪断) - -PC 网卡闪断一下 → PC 内核**悄悄把自己的连接记录撕了**(不发 FIN),芯片那头还认为 -"客人正在屋里聊天"(ESTABLISHED)。单连接模型下**唯一的柜台被这个不存在的客人永久 -占用**,真客人来了只得到"连接被拒"。当时看起来像"设备网络莫名死了",其实是 -**和幽灵做生意**。 - -### ⑦ 大爷只会礼貌送客,不会轰人(close 无 RST,手册定案) - -芯片的 close 只发 FIN("您请回吧")。对面**内核活着但程序挂死**时——内核礼貌回 -ACK,但客人**就是不起身**——芯片永远站在门口等(FIN_WAIT_2 长尾,E-02 定案:监听 -恢复受限于对端 close)。它**没有"轰出去"(RST)这个动作**——手册全文 RST 只出现在 -listen 占用与多连接两处。这是芯片天性。 - -### ⑧ 垃圾不清,越扫越脏(残渣风暴态) - -上面每一条的残骸(迟到的 DISCONNECT 锁存、半死 Socket、粘滞中断位)都会**带进下 -一条连接**。所以故障率不是常数:残渣少时 2%,堆起来能摆到 54%(E8 实测)——同一份 -固件时好时坏,看起来像"玄学"。 - -## 2. 全景图 - -![七个误会与逐个拆掉](../../images/网络不稳根因总览.svg) - -## 3. 僵尸会话时间线(误会⑥ 的完整一生) - -```mermaid -sequenceDiagram - participant PC as PC(网卡+内核) - participant CH as CH395F(大爷) - participant NT as netTask(传达员) - Note over PC,CH: 会话正常通信(ESTABLISHED) - Note over PC: 网卡闪断(驱动重启/电源管理) - PC--)CH: 内核撕掉 socket,不发 FIN(静默) - Note over CH: 仍认为 ESTABLISHED,唯一柜台被幽灵占用 - Note over NT: 新连接到达 → 连接被拒(端口"死了") - CH->>PC: keepalive 探测(空闲 60s 起,每 5s,共 3 次) - PC--xCH: 无人认领 → 探测无响应 - CH->>NT: TIM_OUT 中断(≈30~90 s) - NT->>CH: QUARANTINE:sock_reset 排渣(读空→CLOSE→排空→确认) - NT->>CH: relisten → LISTEN - Note over NT: 端口恢复,新客人正常进门 -``` - -## 4. 环 → 拆法 → 落点(对照表) - -| 环 | 拆法(已落地) | 证据/落点 | -|---|---|---| -| ① 丢中断 | 单一 reconciler:100 ms 巡逻对账五种形态,判据读驱动状态缓存 | TRAP-NET-17;实验方案 E2(回读 18.1 µs 背书);实施方案 阶段 3 项 1 | -| ② 总闸读清 | 全局寄存器唯一读者=net_poll(F-NET-7);open/relisten 只清本 Socket SINT | TRAP-NET-20 B3;实施方案 阶段 0/2 | -| ③ 拆柜台 | 铁律"EOF 不得 close 监听 fd" + 新 API `net_drop_conn`(拆会话保监听) | 生命周期设计说明 §10.10;实验方案 E-02 | -| ④ 多所有者 | 重建收敛唯一所有者(net_listener + reconciler),其余只置事实不动手 | README 结论 4(不变量 II);实施方案 阶段 3 项 1 | -| ⑤ 队头阻塞 | 忙等统一 `net_sock_reset_sync` 有界排渣(600 ms 硬顶);recv 非阻塞消息化 | 实验方案 E-02(banner 39 ms);阶段 1.5 | -| ⑥ 僵尸会话 | keepalive 60/5/3 + 芯片 TIMEOUT → QUARANTINE → 重建(30~90 s 自愈) | 实验方案 E11/E13(用户判断"不该依赖冷启动"被证实) | -| ⑦ 无 RST | 定案入 TRAP-NET-07:兜底靠查岗不靠 RST;`net_drop_conn` 本地拆除 + 对账③④ 认领 | TRAP-NET-07 定案更正;实验方案 E-02 收口 | -| ⑧ 残渣风暴 | `sock_reset` 标准排渣(读空→CLOSE→排空事件→byte0=00H 确认)+ M3 矛盾态门 | TRAP-NET-18/-20;实施方案 M3/M5(E8/E9 风暴态免疫) | - -## 5. 一句话总结 - -原来的"不稳" = 芯片天性(丢门铃、无 RST)× 软件形态(靠门铃驱动、人人插手、 -唯一传达员会冻结)× 残渣复利(脏态带进下条连接)。N34 做的事不是修某一个 bug, -而是把每一环改成**不依赖门铃(100 ms 对账)、只有一个主人(单所有者)、永远不冻结 -(有界排渣+消息化)、开张前扫垃圾(sock_reset)**——所以现在的"稳"是结构性的。 - -> 遗留观察项:E6 正在挂机量化"RX 非空但 readable=0"(RECV_OK 中断丢失的数据面 -> 形态)在生产是否真实发生(`NET_U15_PROBE`,实验方案 E6 节)——命中>0 则上层 -> 32 轮盲收内化进 net 层(阶段 3 项 4);=0 则保留现状登记观察。 diff --git a/docs/网络/Net层阻塞模型架构设计.md b/docs/网络/Net层阻塞模型架构设计.md index 051b3c90..8846feed 100644 --- a/docs/网络/Net层阻塞模型架构设计.md +++ b/docs/网络/Net层阻塞模型架构设计.md @@ -57,7 +57,7 @@ netTask 是**全部 socket 操作的唯一执行者**——CH395F 的 SPI 总线 (**N34 阶段 1.5 更新**:本条描述 L3 改造当时口径;2026-10-13 起 `net_recv` 默认预算 30s 删除、改为无限阻塞,`net_recv_to` 成为唯一显式预算入口—— 挂起模型与"等待不占 netTask"性质不变,变的是超时归属:见 - `docs/网络/N34-自愈审计/实施方案.md` §3.5 与 BSD 指南 §3.5。) + `docs/网络/N34-自愈审计.md` §5.3.5 与 BSD 指南 §3.5。) > 图形由 `tools/arch_diagram/gen_net_pending_svg.py` 生成(改流程后重跑;见 > `docs/架构设计/图形绘制流程.md`)。 @@ -226,4 +226,4 @@ CONNECT → ESTABLISHED 是合法迁移,推断取消会误杀正在建链的 | 2026-09-29 | **改写为架构设计文档**:以"当前如何设计/为何如此"为主线重组织;历史演进与门禁实测降级为附录 A;迁入 `docs/网络/` | | 2026-10-03 | **网络重建失败恢复(P1-8 / A2)落地并真机验证**:首启组网失败 = 自检 `net init fail` + 30s 周期重试(1s 分片喂心跳,**不自驻留**);重建(reconfig)失败 = 自检 `net reinit fail` + 30s 延迟重触发事件位(tick 回绕安全),成功即清零;PHY 链路超时仍走既有 `comms fault` 降级(语义不变)。真机:开机 `net init: OK` / `PHY: 100M FULL` / `netTask: started`,无 `init FAIL` / `retry in 30s` 误触发(**正常路径零回归**);芯片级 `net_init` 失败分支 / 拔线负向场景待专项窗口(`待处理内容.md` N-16)。**执行判据见 `测试说明/实机测试实施指南.md` §7.1 / §10** | | 2026-10-12 | **N-32 监听生命周期模块化 + 单一所有者**:监听生命周期抽为 `net_listener.c`(`net_listener_try` / `net_listener_clear` / `net_listener_retry_tick` / `net_listener_pump` / `net_listener_settle`,真机专属、模拟器不编译);**重建动作收敛到唯一所有者 `net_listener_pump`**(100ms,去抖+冷却),对账 `net_reconcile_tick` 只剩①②;主循环自愈 tick 由 3 个增至 **4 个**。详见 `docs/子系统设计/网络Socket监听生命周期设计说明.md` §10 | -| 2026-10-13 | **N34 阶段 3:对账与 pump 合并 + 忙等统一 + E-02 收口**(实施规格 `docs/网络/N34-自愈审计/实施方案.md` §5):`net_listener_pump` 删除、并入 `net_reconcile_tick`(单一 100ms tick,形态①②③④⑤+U11,判据读驱动状态缓存,分级去抖 ②1/③④⑤2/①4 轮 + 1s 冷却)——**主循环自愈 tick 由 4 个回到 3 个**,单一重建所有者不变量不变(=形态③④);`net_listen_locked`/`net_connect_locked`/`net_close_locked` 的盲等改 `net_sock_reset_sync` 确定性排渣(GRACEFUL 100ms 底 / 600ms 封顶),close/connect 不再读清全局中断寄存器(F-NET-7);新增 `net_drop_conn()`(拆会话保监听,`NET_MSG_DROP_CONN`)+ `net_poll` 每轮步进 `ch395f_sock_reset_poll`。挂起 recv 模型本身不变(`net_drop_conn` 复用 `net_pending_settle_close` 先了结挂起)。台架回归全绿(Code=211348) | +| 2026-10-13 | **N34 阶段 3:对账与 pump 合并 + 忙等统一 + E-02 收口**(实施规格 `docs/网络/N34-自愈审计.md` §5.5):`net_listener_pump` 删除、并入 `net_reconcile_tick`(单一 100ms tick,形态①②③④⑤+U11,判据读驱动状态缓存,分级去抖 ②1/③④⑤2/①4 轮 + 1s 冷却)——**主循环自愈 tick 由 4 个回到 3 个**,单一重建所有者不变量不变(=形态③④);`net_listen_locked`/`net_connect_locked`/`net_close_locked` 的盲等改 `net_sock_reset_sync` 确定性排渣(GRACEFUL 100ms 底 / 600ms 封顶),close/connect 不再读清全局中断寄存器(F-NET-7);新增 `net_drop_conn()`(拆会话保监听,`NET_MSG_DROP_CONN`)+ `net_poll` 每轮步进 `ch395f_sock_reset_poll`。挂起 recv 模型本身不变(`net_drop_conn` 复用 `net_pending_settle_close` 先了结挂起)。台架回归全绿(Code=211348) | diff --git a/docs/问题记录/CH395F_Trap_Records.md b/docs/问题记录/CH395F_Trap_Records.md index ae7cf8a8..8a9d89df 100644 --- a/docs/问题记录/CH395F_Trap_Records.md +++ b/docs/问题记录/CH395F_Trap_Records.md @@ -153,7 +153,7 @@ CH395F 以太网驱动与 NET 层的实证陷阱:现象、定案根因、修 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)——**不是**应用可加速的路径。 +- **边界(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` §6 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-NET-09 · 沉降窗:PHY link up 后 / close 后不能立即 open @@ -308,9 +308,9 @@ CH395F 以太网驱动与 NET 层的实证陷阱:现象、定案根因、修 # 附录 A · 驱动关键点速查(细节以对应条目为准) - **初始化顺序**:`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)。 +- **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` §6 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)。 +- **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` §6 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 链)。 diff --git a/test/board/ch395f_reuse_bench.c b/test/board/ch395f_reuse_bench.c index 4846704e..86d0951e 100644 --- a/test/board/ch395f_reuse_bench.c +++ b/test/board/ch395f_reuse_bench.c @@ -19,7 +19,7 @@ * 含"命令未发时"对照读,复核 Trap 记录的 SET 类无判别力 + 陈旧 BUSY 残留); * E4 = sock_reset 内部顺序 A/B/C 变体 ×10 轮(轮间换端口)+ 立即复连载荷完整性自证 * (配套 test/host/ch395f_reuse_host.py --mode e4)。 - * 判据与结果 → docs/网络/N34-自愈审计/实验方案.md E2/E3/E4 条目与实验记录节。 + * 判据与结果 → docs/网络/N34-自愈审计.md §6 E2/E3/E4 条目与实验记录节。 * * close_mode:0=主机先关(芯片等 DISCONNECT);1=芯片先关;2=无数据连上即关 * verify:0=无修复;1=listen 后确认 LISTEN 重试;2=缓冲重分配; diff --git a/test/board/ch395f_test_task.c b/test/board/ch395f_test_task.c index 5ce02b06..ab911f21 100644 --- a/test/board/ch395f_test_task.c +++ b/test/board/ch395f_test_task.c @@ -1657,7 +1657,7 @@ static void phase5_run(void) { /* =================================================================== * Phase 15 — N34 阶段 2 驱动原语(无需 PC) * - * 测试目的:验证 ch395f 新增原语(实施方案 docs/网络/N34-自愈审计/实施方案.md §4) + * 测试目的:验证 ch395f 新增原语(实施方案 docs/网络/N34-自愈审计.md §5.4) * 语义正确:类型化状态回读一致性 / cmd_done(E3 定案=状态回读)/ * sock_reset 非阻塞状态机跑通(E4 定案序列)/ 链路态缓存 / 事件护栏。 * 污染会话下的复位载荷完整性已由 E4 bench 60 轮实证(实验方案 E4), diff --git a/test/host/e6_soak.py b/test/host/e6_soak.py index 3fec8fe1..79b42dbf 100644 --- a/test/host/e6_soak.py +++ b/test/host/e6_soak.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """E6 24h soak driver (U15 mismatch frequency, N34 experiment plan E6). -Protocol (docs/网络/N34-自愈审计/实验方案.md §E6): +Protocol (docs/网络/N34-自愈审计.md §6 E6): 24h normal operation + periodic E1 stress injection: - every hour : relisten_stress :7003 n=10 gap=100 (UI-like churn) - every 4th hour : full E1 (:7003 n=50 gap=0 + :21 n=50 gap=100) diff --git a/tools/arch_diagram/gen_net_instability_svg.py b/tools/arch_diagram/gen_net_instability_svg.py index 70aa6ae0..1be62011 100644 --- a/tools/arch_diagram/gen_net_instability_svg.py +++ b/tools/arch_diagram/gen_net_instability_svg.py @@ -105,7 +105,7 @@ SUM_RIGHT = ["逐环拆掉 = 稳(N34 阶段 0~3 已实施)", "不依赖门铃 · 唯一主人 · 永不冻结 · 100ms 对账 · 开张前排渣——台架回归全绿"] FOOT = ("证据链:docs/问题记录/CH395F_Trap_Records.md(TRAP-NET-07/-09/-12/-17/-18/-20) · " - "docs/网络/N34-自愈审计/实验方案.md(E1~E13) · 修复落地见 docs/网络/N34-自愈审计/实施方案.md") + "docs/网络/N34-自愈审计.md §6 实验记录(E1~E13) · 修复落地见同文档 §5 实施方案") # ----------------------------- 工具函数 -----------------------------