feat(NET): SEND pending 二期落地 + 末块挂起越界缺陷修复(台架复测闭环)

net_pending.c/h 增设第二张挂起表 s_net_send[](与 recv 表完全独立,收发
互不阻塞);net_send_locked 快速路径改为有界自旋,单次等待超出
NET_SEND_SPIN_MS(2ms) 即登记挂起 send,由 net_send_pending_tick 按主循环
节拍分块推进,netTask 最坏停滞由 1s 压到 2ms。net_listener_retry_tick 的
挂起保险扩展到 send 侧;net_close_locked / 网络重建 / 状态对账三路均先
取消再动作(取消必通知,调用方永不挂死)。

台架复测(A/B 对照 + 插桩定位)发现并修复末块挂起越界缺陷:内层"等本块
发完(SENBUF_FREE)"位于写块之后,写完末块时 sent 已 == len,若该次等待越过
2ms 预算便会调用 net_send_pending_register,而登记函数按 sent >= len 拒收
并回 -1,把一次实际已完整交付芯片的发送报成失败。修复为内层等待开头判
sent >= len 即跳出(末块已交付、SENBUF_FREE 等待是纯保守自旋),并置于
超时/自旋判定之前;该守卫同时保证后续登记必伴随 sent < len。

复验:ftp_regression 连续 19 次 11/11;ftp_integrity_test 20 轮 x 64KB
STOR/RETR SHA-256 20/20 一致(两轮),吞吐稳定约 80KB/s;设备日志零
write error;ST-LINK 回读 No difference found。

文档:Net层阻塞模型架构设计.md 补二期设计与新增 9.1 缺陷记录,netTask
栈实测峰值按本次台架 920B 订正为 1116/3840(FTP 传输期 1164);网络自愈
架构设计.md 吸收原监听生命周期文档、新增编号术语导读表,并把 5.2
"实验锚点速查"的 E 编号含义与 E-02 审计编号歧义写在表头(用户反馈该表
E 编号无法读懂)。

门禁:Keil 0 错 0 警;cppcheck / 编码规范 / 乱码零告警;
check_mem_numbers PASS;check_doc_links --all --sections-strict 142 篇 PASS。
This commit is contained in:
2026-10-14 12:05:21 +08:00
parent df13dec479
commit f849c79b7a
24 changed files with 1626 additions and 882 deletions
+3 -2
View File
@@ -94,8 +94,8 @@ docs/
| [`IEC104上行设计说明.md`](子系统设计/IEC104上行设计说明.md) | **B4 上行规约**:IEC 60870-5-104 被控站(TCP 2404)范围/点表(V0.1 有效子集)/链路流程(k/w/t1~t3、总召、COV、遥控两段式、定值)/netTask 接入决策/预算与测试方案;协议核心纯逻辑 + L0.5 全栈单测 |
| [`网络/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 请求架构、五路完成语义、设计边界与性能特征 |
| [`网络/网络自愈架构设计.md`](网络/网络自愈架构设计.md) | **网络自愈架构**:现状权威描述——设计原则(不变量 I1~I8)、分层职责与三 tick、reconciler 形态①~⑤ + M3 门、驱动原语、recv 语义、KeepAlive、参数表与 E 锚点速查、红线与延后项;审计过程(T1~T4/E0~E13)已归档 |
| [`网络/Net层阻塞模型架构设计.md`](网络/Net层阻塞模型架构设计.md) | **等待模型**:recv/send 挂起请求架构、数据结构与挂起槽状态机、recv 五路 + send 四路完成语义、模块接口契约(14 函数)、实现落点(20 集成点)、调用方契约、设计边界与性能特征 |
| [`网络/网络自愈架构设计.md`](网络/网络自愈架构设计.md) | **网络自愈架构 + 监听生命周期**:现状权威描述——根因三层证据与 A′ 出局、设计原则(不变量 I1~I8)、分层职责与三 tick、reconciler 形态①~⑤ + M3 门、驱动原语、FUN_PARA bit3 契约、recv 语义、KeepAlive、参数表与 E 锚点速查、红线、代码级事实清点、决策记录 D1~D7、`net_listener.c` 模块化边界、延后项;审计与 B10 根治过程已归档 |
---
@@ -272,6 +272,7 @@ docs/
| 日期 | 变更 | 说明 |
|---|---|---|
| 2026-10-14 | 网络文档合并 | 《网络Socket监听生命周期设计说明.md》(B10 根治,587 行)并入 [`网络/网络自愈架构设计.md`](网络/网络自愈架构设计.md):原理与规范层论证 → §1.1~§1.4、FUN_PARA bit3 契约 → §4.9、代码级事实清点 → §9、决策记录 → §10、`net_listener.c` 模块化边界 → §11,原「引用与归档约定」顺延为 §12;过程类内容(分阶段勾选清单、台架验收记录)不迁、留 git 历史;原文档删除。全库引用同步 13 处(`net_listener.c`/`.h` 注释、Net层阻塞模型、BSD_Socket_API 两篇、陷阱记录 B10、编号规范 §4.2/§5.2/§7.2);本索引 §5.1 补登记 |
| 2026-10-14 | E14"丢中断"复核与归因更正 | 48 轮直读 SINT 矩阵复核历史"丢中断"说法:**芯片侧事件丢失 = 0**("丢失"= drain 读清消费 3/48 + 门控竞态 2/48 + 主机同步 1/48,幽灵态 0/48 未复现);根除性修复 = listen/relisten 路径排事件挪至 LISTEN 命令**之前**(吞新会话 CONNECT 的生产侧自伤窗);全局读收口(全局寄存器唯一读者=`net_poll`,I7 护栏);全库 17 文件约 40 处"丢中断"归因更正为"事件未收到/读清消费竞态",`docs/网络/网络自愈架构设计.md` §5.2 新增 E14 锚点;台架保留复核探针(drain-int/SINT 补读×3/超时归类);同日 E15 幽灵态归因复核(官网+手册实锤:0x04+旧端口 = 芯片**如实报告**旧连接拆链进行中——bit3=1 保留现场为自家配置、拆链窗与 §5.17 重传默认值吻合、WCH 官方博客确认断连即重连被拒且官方方案 = 关闭完成后重开,即 settle 等待) |
| 2026-10-13 | 网络自愈架构文档瘦身与改名 | 合并文档 `网络/N34-自愈审计.md`(2062 行审计全记录)瘦身为**终版设计描述**(281 行:不变量 I1~I8 / 架构与机制详解 / 参数与 E 锚点速查 / 红线 / 延后项),并经 `rename_ref.py` 改名 [`网络/网络自愈架构设计.md`](网络/网络自愈架构设计.md)(全库 16 文件 30 处引用同步);审计过程(T1~T4、E0~E13)归档 git 历史,考古命令见该文档「引用与归档约定」 |
| 2026-10-13 | 网络自愈审计归档 | 原 `docs/网络/N34-自愈审计/` 目录 7 份文档(根因总览 / net 层盘点 / 驱动层审计 / 主流对照 / 汇总裁决 / 实施方案 / 实验方案)**合并为单文档 [`网络/网络自愈架构设计.md`](网络/网络自愈架构设计.md)**,原目录删除;全库引用同步(`docs/` 4 篇 + 驱动/网络代码注释 + 配图生成器与 SVG)。本索引 §5.1 补登记 |
+31 -27
View File
@@ -1,7 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="1030" viewBox="0 0 1440 1030" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif">
<rect x="0" y="0" width="1440" height="1030" fill="#ffffff"/>
<svg xmlns="http://www.w3.org/2000/svg" width="1620" height="1074" viewBox="0 0 1620 1074" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif">
<rect x="0" y="0" width="1620" height="1074" fill="#ffffff"/>
<text x="40" y="44" font-size="21" font-weight="700" fill="#0f172a">Net 层阻塞模型 · pending 请求架构</text>
<text x="40" y="70" font-size="13" fill="#64748b">把「等数据」从执行者内的自旋,改为执行者外的登记 —— 调用方在自己上下文睡眠,netTask 零占用地服务整个队列</text>
<text x="40" y="70" font-size="13" fill="#64748b">把「等数据 / 等发送缓冲」从执行者内的自旋,改为执行者外的登记 —— 调用方在自己上下文睡眠,netTask 零占用地服务整个队列</text>
<rect x="40.0" y="100" width="16" height="16" rx="4" fill="#ffffff" stroke="#2563eb" stroke-width="2"/>
<text x="64.0" y="112" font-size="12.5" fill="#475569">调用方任务</text>
<rect x="150.5" y="100" width="16" height="16" rx="4" fill="#ffffff" stroke="#7c3aed" stroke-width="2"/>
@@ -32,7 +32,7 @@
<text x="80.0" y="564.0" font-size="12.0" fill="#64748b">与 LwIP 消费者线程模型同构 · 取消必通知,永不挂死</text>
<text x="468.0" y="330.0" font-size="12" font-weight="700" fill="#16a34a">是</text>
<text x="78.0" y="396.0" font-size="12" font-weight="700" fill="#dc2626">否,timeout &gt; 0</text>
<rect x="540.0" y="300.0" width="830.0" height="74.0" rx="10" fill="#f0fdfa" stroke="#0d9488" stroke-opacity="0.55" stroke-width="1.6"/>
<rect x="540.0" y="300.0" width="1010.0" height="74.0" rx="10" fill="#f0fdfa" stroke="#0d9488" stroke-opacity="0.55" stroke-width="1.6"/>
<text x="556.0" y="332.0" font-size="14.0" font-weight="700" fill="#0f172a">立即 SPI 读出</text>
<text x="556.0" y="356.0" font-size="12.0" fill="#64748b">GET_REMV_LEN / READ → 通知调用方(结果 = 字节数)</text>
<polyline points="456.0,337.0 538.0,337.0" fill="none" stroke="#16a34a" stroke-width="2.2" stroke-linejoin="round"/>
@@ -41,24 +41,27 @@
<path d="M 538.0 441.0 L 527.0 446.5 L 527.0 435.5 z" fill="#7c3aed"/>
<text x="498.0" y="418.0" font-size="11.5" fill="#64748b" text-anchor="middle">登记即返回</text>
<text x="498.0" y="432.0" font-size="11.5" fill="#64748b" text-anchor="middle">(微秒级)</text>
<rect x="540" y="392" width="830" height="100" rx="12" fill="#fffbeb" stroke="#d97706" stroke-opacity="0.5" stroke-width="1.6"/>
<rect x="540" y="392" width="1010" height="100" rx="12" fill="#fffbeb" stroke="#d97706" stroke-opacity="0.5" stroke-width="1.6"/>
<text x="556" y="418" font-size="14.0" font-weight="700" fill="#b45309">netTask 主循环(~1ms 节拍)—— 挂起期间照常服务整个队列</text>
<rect x="556.0" y="434.0" width="190.5" height="46" rx="8" fill="#ffffff" stroke="#0d9488" stroke-opacity="0.6"/>
<text x="566.0" y="454.0" font-size="12.5" font-weight="700" fill="#0d9488">net_poll()</text>
<rect x="556.0" y="434.0" width="186.0" height="46" rx="8" fill="#ffffff" stroke="#0d9488" stroke-opacity="0.6"/>
<text x="566.0" y="454.0" font-size="11.5" font-weight="700" fill="#0d9488">net_poll()</text>
<text x="566.0" y="472.0" font-size="11" fill="#64748b">刷新 readable</text>
<rect x="758.5" y="434.0" width="190.5" height="46" rx="8" fill="#ffffff" stroke="#d97706" stroke-opacity="0.6"/>
<text x="768.5" y="454.0" font-size="12.5" font-weight="700" fill="#d97706">net_pending_tick()</text>
<text x="768.5" y="472.0" font-size="11" fill="#64748b">结束挂起</text>
<rect x="961.0" y="434.0" width="190.5" height="46" rx="8" fill="#ffffff" stroke="#7c3aed" stroke-opacity="0.6"/>
<text x="971.0" y="454.0" font-size="12.5" font-weight="700" fill="#7c3aed">net_listener_retry_tick()</text>
<text x="971.0" y="472.0" font-size="11" fill="#64748b">重试对账</text>
<rect x="1163.5" y="434.0" width="190.5" height="46" rx="8" fill="#ffffff" stroke="#64748b" stroke-opacity="0.6"/>
<text x="1173.5" y="454.0" font-size="12.5" font-weight="700" fill="#64748b">排空消息队列</text>
<text x="1173.5" y="472.0" font-size="11" fill="#64748b">服务所有消费者</text>
<rect x="40" y="766" width="1360" height="234" rx="14" fill="#f8fafc" stroke="#64748b" stroke-opacity="0.28"/>
<text x="58" y="794" font-size="15.0" font-weight="700" fill="#334155">net_pending_tick():五路完成 / 取消判定(每轮对每个挂起 fd 按序)</text>
<polyline points="955.0,492.0 955.0,764.0" fill="none" stroke="#d97706" stroke-width="2.2" stroke-linejoin="round"/>
<path d="M 955.0 764.0 L 949.5 753.0 L 960.5 753.0 z" fill="#d97706"/>
<rect x="754.0" y="434.0" width="186.0" height="46" rx="8" fill="#ffffff" stroke="#d97706" stroke-opacity="0.6"/>
<text x="764.0" y="454.0" font-size="11.5" font-weight="700" fill="#d97706">net_pending_tick()</text>
<text x="764.0" y="472.0" font-size="11" fill="#64748b">结束挂起 recv</text>
<rect x="952.0" y="434.0" width="186.0" height="46" rx="8" fill="#ffffff" stroke="#d97706" stroke-opacity="0.6"/>
<text x="962.0" y="454.0" font-size="11.5" font-weight="700" fill="#d97706">net_send_pending_tick()</text>
<text x="962.0" y="472.0" font-size="11" fill="#64748b">推进挂起 send</text>
<rect x="1150.0" y="434.0" width="186.0" height="46" rx="8" fill="#ffffff" stroke="#7c3aed" stroke-opacity="0.6"/>
<text x="1160.0" y="454.0" font-size="11.5" font-weight="700" fill="#7c3aed">net_listener_retry_tick()</text>
<text x="1160.0" y="472.0" font-size="11" fill="#64748b">重试对账</text>
<rect x="1348.0" y="434.0" width="186.0" height="46" rx="8" fill="#ffffff" stroke="#64748b" stroke-opacity="0.6"/>
<text x="1358.0" y="454.0" font-size="11.5" font-weight="700" fill="#64748b">排空消息队列</text>
<text x="1358.0" y="472.0" font-size="11" fill="#64748b">服务所有消费者</text>
<rect x="40" y="766" width="1540" height="264" rx="14" fill="#f8fafc" stroke="#64748b" stroke-opacity="0.28"/>
<text x="58" y="794" font-size="15.0" font-weight="700" fill="#334155">挂起 recv:五路完成 / 取消判定(net_pending_tick 每轮对每个挂起 fd 按序)</text>
<polyline points="1045.0,492.0 1045.0,764.0" fill="none" stroke="#d97706" stroke-width="2.2" stroke-linejoin="round"/>
<path d="M 1045.0 764.0 L 1039.5 753.0 L 1050.5 753.0 z" fill="#d97706"/>
<text x="66" y="832.0" font-size="14" font-weight="700" fill="#0d9488">①</text>
<text x="96" y="832.0" font-size="12.5" font-weight="700" fill="#0f172a">数据到达</text>
<text x="270" y="832.0" font-size="12.5" fill="#475569">→ SPI 读出,通知调用方(结果 = 字节数)</text>
@@ -75,12 +78,13 @@
<text x="96" y="940.0" font-size="12.5" font-weight="700" fill="#0f172a">网络重建(芯片复位)</text>
<text x="270" y="940.0" font-size="12.5" fill="#475569">→ 取消 + 必须通知(否则调用方永久挂死)</text>
<text x="96" y="969" font-size="12" font-weight="700" fill="#dc2626">挂起超时留痕探针(编译期选项,默认关):挂起超阈值仍未结束 → 只 DBG_ERROR 留痕(不强制结束、不伪造返回值)</text>
<rect x="1120" y="812" width="250" height="62" rx="10" fill="#eff6ff" stroke="#2563eb" stroke-opacity="0.55" stroke-width="1.6"/>
<text x="1134" y="838" font-size="13.5" font-weight="700" fill="#2563eb">xTaskNotify</text>
<text x="1134" y="858" font-size="11.5" fill="#64748b">唤醒调用方(结果 = 字节数 / 负错误码)</text>
<polyline points="1080.0,843.0 1118.0,843.0" fill="none" stroke="#2563eb" stroke-width="2.2" stroke-linejoin="round"/>
<path d="M 1118.0 843.0 L 1107.0 848.5 L 1107.0 837.5 z" fill="#2563eb"/>
<polyline points="1245.0,874.0 1245.0,988.0 50.0,988.0 50.0,545.0 56.0,545.0" fill="none" stroke="#2563eb" stroke-width="2.2" stroke-linejoin="round" stroke-dasharray="7 5"/>
<text x="66" y="999" font-size="12" font-weight="700" fill="#d97706">挂起 send 四路:①断连(以已发字节数结束)②推进(每轮至多一块 ≤ NET_SEND_CHUNK_MAX)③写完(以总长度结束)④无进展超时(短写)—— net_send_pending_tick 每轮判定</text>
<rect x="1300" y="812" width="250" height="62" rx="10" fill="#eff6ff" stroke="#2563eb" stroke-opacity="0.55" stroke-width="1.6"/>
<text x="1314" y="838" font-size="13.5" font-weight="700" fill="#2563eb">xTaskNotify</text>
<text x="1314" y="858" font-size="11.5" fill="#64748b">唤醒调用方(结果 = 字节数 / 负错误码)</text>
<polyline points="1260.0,843.0 1298.0,843.0" fill="none" stroke="#2563eb" stroke-width="2.2" stroke-linejoin="round"/>
<path d="M 1298.0 843.0 L 1287.0 848.5 L 1287.0 837.5 z" fill="#2563eb"/>
<polyline points="1425.0,874.0 1425.0,1018.0 50.0,1018.0 50.0,545.0 56.0,545.0" fill="none" stroke="#2563eb" stroke-width="2.2" stroke-linejoin="round" stroke-dasharray="7 5"/>
<path d="M 56.0 545.0 L 45.0 550.5 L 45.0 539.5 z" fill="#2563eb"/>
<text x="40" y="1006" font-size="12" fill="#64748b">关键性质:挂起期间 netTask 零额外占用(判据复用主循环既有 net_poll 的 readable,不引入额外 SPI 事务);公开 API 不变,既有调用方 / 测试 / 模拟器零改动。</text>
<text x="40" y="1050" font-size="12" fill="#64748b">关键性质:挂起期间 netTask 零额外占用(recv 判据复用 net_poll 的 readable,不引入额外 SPI;send 有界自旋 ≤ NET_SEND_SPIN_MS,netTask 最坏停滞由 1s 压到 2ms);公开 API 不变。</text>
</svg>

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

+14 -11
View File
@@ -11,21 +11,21 @@
## 2. 设计基线(供后续接入参考)
### 2.1 pending 二期设计基线(ACCEPT + SEND)
### 2.1 pending 二期设计基线(SEND 已落地 / ACCEPT 待办)
> **来源**:一期(RECV)架构现状见 `docs/网络/Net层阻塞模型架构设计.md`(演进史查 git 历史);
> **来源**:recv/send 架构现状见 `docs/网络/Net层阻塞模型架构设计.md`(演进史查 git 历史);
> 本节仅保留**二期仍要用的设计基线**。
**一期已验证的机制骨架**(二期沿用,勿重新发明):独立静态 pending 表 + `net_pending_tick`
主循环按节拍结束挂起(五路:数据/到点/断连/同 fd 串行/重建取消必通知)+ 登记守卫(每 fd 至多一个、
不覆盖、拒绝 netTask 自等待)+ 1s 看门狗强结算。
主循环按节拍结束挂起(recv 五路:数据/到点/断连/同 fd 串行/重建取消必通知)+ 登记守卫(每 fd 至多一个、
不覆盖、拒绝 netTask 自等待)+ 挂起超时留痕探针(tripwire,只留痕不伪造返回值)。
**二期扩展点**:
| 项 | 目标 | 对应缺陷 |
|---|---|---|
| **ACCEPT pending** | accept 等待改为"登记 → CONNECT 事件完成",退役 100ms/200ms 轮询循环 | E-06(accept 轮询计时失真) |
| **SEND pending** | 发送续传改为"登记 → send_ready 完成",消除 1s/分片 stall 对队列的冻结 | E-02 残余(SEND 位置) |
| 项 | 目标 | 对应缺陷 | 状态 |
|---|---|---|---|
| **SEND pending** | 发送续传改为"登记 → send_ready 完成",消除 1s/分片 stall 对队列的冻结 | E-02 残余(SEND 位置) | ✅ **2026-10-14 已落地**(`net_send_pending_tick` + 有界自旋 `NET_SEND_SPIN_MS`;架构见 `docs/网络/Net层阻塞模型架构设计.md` §4.5/§5.5/§7) |
| **ACCEPT pending** | accept 等待改为"登记 → CONNECT 事件完成",退役 100ms/200ms 轮询循环 | E-06(accept 轮询计时失真) | ⬜ 待做 |
**二期门禁**(一期门禁的加强版):
@@ -39,10 +39,13 @@
8. 看门狗接 selftest 留痕 + 模拟器差距清单 +1(`net_socket_sim.c` 无法建模 pending)。
**已知代价(延续一期,需文档化)**:pending 完成判定随主循环节拍运行,被长操作占用时超时
顺延(~1ms 级);二期把 SEND 也 pending 化后,该顺延面反而**收窄**。
顺延(~1ms 级);**SEND 已 pending 化(2026-10-14)**,该顺延面已收窄——发送停滞不再顺延整层;
剩余 ACCEPT 化不改变此结论。
**改动量估算**:再 ~300-500 行,集中在 `net_socket.c`;公开 API 不变。风险评级**中**
(执行器核心 + D 系列前科)——**大的是验证,不是代码**。
**改动量估算**:SEND 实际落地约 300 行(`net_pending.c` +287 行、`net_socket.c` 10 处集成点、
`net_config.h` 新增 `NET_SEND_SPIN_MS`);公开 API 不变。风险评级**中**(执行器核心 + D 系列前科)
——**大的是验证,不是代码**。SEND 已完成构建门禁(0 错误 0 警告)与静态核对,**台架复测待做**
(netTask 栈峰值须重测)。剩余 ACCEPT 预计再 ~100-200 行。
### 2.2 错误码通道四要素(CH395F 范式)
+1 -1
View File
@@ -10,7 +10,7 @@
|---|---|---|---|---|
| N-2 | **49s 全系统日志静默(defaultTask/NAND syslog 排空停滞)**——串口日志 49s 无行,期间 net 栈照常处理外部连接 → 日志管线停滞,队列满后各任务日志行被丢;系统功能不受损 | 2026-09-22(L3 台架 F2,gen 181) | **A/B 归因:既有存储侧现象**。**2026-10-03 降级为低风险观察项**:① 丢弃可观测性**代码已具备**(`console_log.c` `s_con_drop` + `[ERR] CONSOLE dropped`;`sys_log_mcu.c` 队列满 `DBG_ERROR`;UART TX 僵尸通道自愈);② V5 30 min **未复现 49s**(>10s 静默全为 ~28s、均在 `e02_probe` 窗口内=N-17)。**残余待办**:与 S8(非阻塞日志)合并评估(C3) | `错误码传播审计.md` E-02 阶段二 |
| N-3 | **L5 队列往返基线(1223/988µs)的 L3 后重验**——L5 测 DONTWAIT 队列全程;L3 对该路径逐位不变 → 机制上无影响,但基线数值属分片时代,严格重锁需跑一轮 Phase 13(需 PC peer) | 2026-09-22(L3 收尾留档) | 低风险:10ms/100ms 阈值余量 >8×;下次 Phase 13 窗口顺带重锁即可 | `链路延迟测试说明.md` §6.4 L5 注¹ |
| N-4 | **pending 机制二期:ACCEPT + SEND 阻塞路径 pending 化**——E-06、E-20、E-21 剩余分量共用该机制扩展;届时 L3/L5 基线再重锁、探针族扩充、看门狗接 selftest 留痕、模拟器差距清单 +1 | — | 一期(RECV)已落地并入主线;架构现状见 `Net层阻塞模型架构设计.md`,二期设计基线见 `基线与待确认.md` §2.1 | `错误码传播审计.md` 第二档 #2 |
| N-4 | **pending 机制二期:ACCEPT 阻塞路径 pending 化**(SEND 部分已完成)——E-06、E-20、E-21 剩余分量共用该机制扩展;届时 L3/L5 基线再重锁、探针族扩充、看门狗接 selftest 留痕、模拟器差距清单 +1 | — | 一期(RECV)已落地;二期 **SEND 已于 2026-10-14 落地**(构建门禁过、台架复测待做),**ACCEPT 仍待做**(收益性质不同,优先级低——见 `Net层阻塞模型架构设计.md` §10 判据)。架构现状见 `Net层阻塞模型架构设计.md`,ACCEPT 设计基线见 `基线与待确认.md` §2.1 | `错误码传播审计.md` 第二档 #2 |
| N-16 | **A2/A4 专项场景台架验证待办**(正常路径已实测零回归):① PHY 拔线负向测试——断链需断 **DTU 侧**线缆或关**受管交换机端口**;② A2 芯片级 `net_init` 失败分支(需 SPI 故障注入);③ A4 短写压测(需远显客户端 + UI 推流压力) | 2026-10-03(台架拓扑 = CH395↔交换机,拔线操作受限) | 待专项台架窗口 | `实机测试实施指南.md` §10 |
| N-17 | **`time_sync` 30s 挂起客户端掉线 + 监听重建期间,网络任务心跳缺口达 800ms~1s(声明 stall 但未复位)。**待办**:定位监听重建为何阻塞 NET/FTP/UI;评估缩短重建重试(5×2s)或让掉线路径不阻塞其它任务 | 2026-10-03(V5 台架) | V5 长跑 44 次 `task stall: mask=0x00000001`,**100% 与 `e02_probe` 的 30s 挂起客户端触发 `time_sync: no TIME packet in 30s, drop conn` → 监听重建(约 4s)重合**;隔离实验(仅 FTP,120s)**0 次 stall** → 探针场景专属。缺口(~1s)< IWDG 最坏超时(1701ms),余量仅 ~0.7s。 | `实机测试实施指南.md` §4;`../子系统设计/看门狗设计说明.md` |
@@ -36,10 +36,10 @@
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
| 区域 | 用量 | 容量 | 占比 |
|---|---|---|---|
| ER_IROM1 | 230,048 B (224.66 KB) | 1,048,576 B (1024 KB) | 21.9% |
| RW_IRAM1 | 97,912 B (95.62 KB) | 131,072 B (128 KB) | 74.7% |
| ER_IROM1 | 230,988 B (225.57 KB) | 1,048,576 B (1024 KB) | 22.0% |
| RW_IRAM1 | 98,136 B (95.84 KB) | 131,072 B (128 KB) | 74.9% |
| RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% |
| **Flash (ROM 合计)** | **231,152 B (225.7 KB)** | 1,048,576 B (1024 KB) | **22.0%** |
| **Flash (ROM 合计)** | **232,092 B (226.7 KB)** | 1,048,576 B (1024 KB) | **22.1%** |
<!-- MEM_SNAPSHOT:END -->
各区的容量与常量来源:
@@ -110,7 +110,7 @@
| 任务 | 配置栈 | 字节 | 实际职责 | 2026-10-10 实测峰值 / 静态最坏链 |
|---|---|---|---|---|
| uiTask | `4096*4` | 16KB | LVGL 1bpp 离屏渲染(渲染缓冲在静态区,栈只走调用链) | 4016 B(余 75%)/ 静态 1336 B → 可大幅缩减 |
| netTask | `960*4` | 3.75KB | CH395F 轮询(DMA 搬运,栈压力小) | 920 B(余 76%)/ 静态 **1944 B**(2026-10-12 N32 监听模块化后复测;自愈收敛阶段 3 合并后复测 **1944 B** 不变,链尾经 net_listener_retry_tick→net_relisten_locked) |
| netTask | `960*4` | 3.75KB | CH395F 轮询(DMA 搬运,栈压力小) | **1116 B(余 70%)**/ 静态 **1944 B**(2026-10-14 SEND pending 台架复测,含 `net_send_pending_tick` 新增栈帧;FTP 传输期峰 **1164 B**。前值 920 B 为一期 recv 挂起时实测,N32 监听模块化后复测 **1944 B** 不变,链尾经 net_listener_retry_tick→net_relisten_locked) |
| ftpTask | `1024*4` | 4KB | lftpd FTP 服务 | 740 B(余 81%)/ 静态 1216 B |
| testTask | `2048*4` | 8KB | 测试套件(产品构建不创建;存储套件深链需要) | 未创建 / 静态 496 B |
| defaultTask | `1280*4` | 5KB | FatFS 挂载/探针 + 配置引擎 + syslog + 栈水位探针 | 2152 B(余 41%)/ 静态 **3016 B**(N-33 抬栈 1024→1280,2026-10-12) |
+3 -3
View File
@@ -15,14 +15,14 @@
<!-- MEM_BUDGET:BEGIN generated by scripts/ram_baseline.py -->
| 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) |
|---|---|---|---|---|
| ER_IROM1 (Flash) | 230,048 B | 1,048,576 B | 21.9% | 参考 |
| ER_IROM1 (Flash) | 230,988 B | 1,048,576 B | 22.0% | 参考 |
| RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS |
| UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 |
| RW_IRAM1 | 97,912 B | 131,072 B | 74.7% | PASS |
| RW_IRAM1 | 98,136 B | 131,072 B | 74.9% | PASS |
| 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 |
|---|---|---|---|
| 网络 | 13,458 B | 12 KB | ⚠ 超 1.1 KB |
| 网络 | 13,682 B | 12 KB | ⚠ 超 1.4 KB |
| 存储 | 7,056 B | 12 KB | OK |
| UI | 54,988 B | 16 KB | ⚠ 超 37.7 KB |
| RTOS 堆 | 57,376 B | 56 KB | 堆=配额 |
@@ -1,587 +0,0 @@
# 网络 Socket 监听生命周期设计说明(B10 根治)
> 定位:回答"为什么 CH395F 上监听会被毁、端口会永久拒连",并给出根治路线。
> 事实来源:`docs/芯片手册/CH395F.md`、`docs/问题记录/CH395F_Trap_Records.md`、
> `Drivers/BSP/NET/net_socket.c`(3921 行)、官方 CH395EVT V2.0 例程(主机侧核对)。
> 本文只定原理与方案,不含已落地代码;落地按 §5 分阶段,每阶段独立可回滚。
## 1. 一句话根因
**监听器是一个长期服务对象,却被实现成一个短暂的芯片 Socket;而"这条会话结束了"这个事实,
是用一个对时序敏感的边沿来通知的。**
由此必然派生两类事故:① 没人接住边沿 → 监听器无人重建(B10 孤儿监听);
② 别人抢先接住 → 消费者永远收不到结束信号(会话卡死、新连接被旧会话静默吸收)。
## 2. 原理:三层证据
### 2.1 芯片层——"监听"不是能力,是一个会被消耗掉的槽位状态
| 事实 | 出处 |
|---|---|
| LISTEN 与 ESTABLISHED 是同一 TCP 状态字节的**互斥取值**(01H/04H) | `docs/芯片手册/CH395F.md` §5.22 L535-545 |
| 「Socket 会一直检测连接事件,**直到连接成功后**……每个 Socket 只可以建立一个连接」 | `docs/芯片手册/CH395F.md` §5.29 L609 |
| 连接结束后芯片**永不**自动回 LISTEN——全手册无此条款;重连必须"重新打开 Socket 再 CONNECT" | `docs/芯片手册/CH395F.md` §5.30 L615、§9.2.5 L1266 |
| 断开后芯片行为由 FUN_PARA bit3 分流:=0 芯片自动关 Socket(→`0x00/0x00`);**=1 芯片什么都不做(→`0x05/0x00`),MCU 必须自己 CLOSE** | `docs/芯片手册/CH395F.md` §5.23 L572、§5.52 L795② |
⚠ **本仓 `ch395f_set_fun_para(0x08)`(`Drivers/BSP/NET/net_socket.c:275`,bit3 SOCKET_CLOSE=1,
TRAP-NET-15/-20 定案为保留排空现场)⇒ 断开后芯片必然停在 `0x05/0x00`。
B10 探针看到的"孤儿芯片态"不是芯片异常,而是本仓这个配置的**直接且合法的后果**。**
⚠ 口径错误待修:`Drivers/BSP/NET/net_socket.c:3627` 与 `:3641` 的函数头注释写
「CH395F 在 DISCONNECT 后自动关闭 Socket(FUN_PARA 默认)」——与实际 `0x08` 相反。
### 2.2 规范层——本仓的做法与 TCP/POSIX 规范正面冲突
- RFC 1122 §4.2.2.18:被动 OPEN「**MUST NOT affect any previously created connection record**」。
- POSIX `accept()`:「The original socket remains open and can accept more connections」。
- lwIP 把"监听器归谁"直接写进 `tcp_close()` 文档:只有 socket 层创建的监听器才会被连带关闭,
raw API 自建的不会——**所有权是 API 契约的一部分,不是调用方的自觉**。
本仓让同一个 fd 兼任"监听器"和"连接"两个角色(handle/resource aliasing),
于是"关掉一条连接"和"毁掉一个监听器"在类型上无法区分——
这正是 AGENTS 里「应用层不得在 EOF 时 `net_close` 该 fd」这条铁律存在的唯一原因。
**需要靠铁律维持的正确性,就是架构欠债的凭证。**
### 2.3 实现层——别名导致"边沿通知",进而导致多所有者
`net_recv` 的 EOF 只在 **recv 那一刻 `state==CLOSED`** 才给出
(`Drivers/BSP/NET/net_socket.c:1880-1888`);若该 fd 已被别的路径 relisten 回 LISTENING,
同一个 recv 恒返 `WOULDBLOCK`(`:1890`)。这是一个**边沿**,不是一个**电平**。
于是"谁重建监听"散成 **12 个所有者**:栈内 8 处(对账③④、`handle_disconnect_event` 监听分支、
`net_accept_locked` CLOSED 分支、`net_recv_locked`、`net_pending_tick`、`NET_MSG_RELISTEN`、
`net_resync_listen`、`net_listen_locked`)+ 应用层 4 处(lftpd、time_sync、ui_net_sink、net_task 代次)。
状态迁移点 15 个,其中 **15 个"无人负责"组合**(U1~U15,见 `docs/问题记录/CH395F_Trap_Records.md` B10 行)。
三个已实测的派生失效:
| 失效 | 机理 | 现状 |
|---|---|---|
| 孤儿监听(B10) | 边沿没人接,四方互等 | 对账④兜底 ≈1 s(已提交 `4f2cc8b`) |
| 消费者卡死 | ④/D10 抢先 relisten → recv 永久 `WOULDBLOCK` → `g_client` 不清、新连接被旧会话静默吸收 | **④ 扩大了此窗口**,未修 |
| 监听被误关 | `net_can_read` 在非 ESTABLISHED(含 LISTENING)返 1 → recv 得 `WOULDBLOCK` → `Drivers/BSP/UI/ui_net_sink.c:563` 无护栏 `net_close(g_client)` = 关掉监听 fd | 靠随后 `bind_listen` 自救,落 TRAP-NET-09 沉降窗 |
另有一条**探针失效**:`net_can_accept`(`:1453`)要求 ESTABLISHED 才返 1,
而 `Drivers/BSP/UI/ui_net_sink.c:512` 用它门控 accept → :7003 **永不进 `net_accept`**
→ TRAP-NET-20 的 `net_lost_connect_probe`(`:1250-1289`)对这个端口**从不运行**,
CONNECT 事件未收到时只剩对账②。:21/:8888 因为无条件轮询 accept,探针才活着。
### 2.4 FUN_PARA bit3=1 的必要性与安全边界(2026-10-12 评估,结论:**必须保留 0x08**)
> 用户提问:「bit3 在本项目设置为 1 的必要性大吗?会导致这么多问题;如果设为 1,
> 如何安全处理?」本节以手册原文 + 实测证据回答,并把它并入决策记录 D4。
**① 必要性 = 硬性必需,不是可调项。** 手册对 bit3 的分流写得很直白:
- `docs/芯片手册/CH395F.md` §5.52 注释②:「此位为 0 表示在产生 SINT_STAT_DISCONNECT 或者 SINT_STAT_TIM_OUT 中断后,
CH395 会主动关闭 Socket。……**关闭 Socket 就意味着 CH395 会将此 Socket 内部缓冲区和
一些相关变量清除**,在某些应用中,如果 CH395 内部自行关闭 Socket 可能会导致残留的数据
被强行清除。这种情况下可以将该位置 1,**由单片机将数据读出来,然后再关闭 Socket**。」
- `docs/芯片手册/CH395F.md` §5.23 L572 同向:「为 0 则……主动将 Socket 状态置为关闭状态**并将相关缓冲区全部清除**;
反之则不会对 Socket 状态和相关缓冲区做任何操作,以方便外部单片机……**将残留的数据读出**。」
实测现场(TRAP-NET-20 三次定案的判别探针):`st=0x05/0x00` 且 `recv_len=1027`——
Socket 仍 OPEN、芯片 TCP 已 CLOSED、**RX 缓冲里 STOR 载荷一个不少**。这正是 bit3=1 按规格
保留现场,不是芯片异常(`docs/问题记录/CH395F_Trap_Records.md` TRAP-NET-20「定案二」)。
**② bit3=0 的代价是「硬件保证的数据丢失」,且不可在软件层补救。**
本产品最典型的流量形态是「发完即 FIN」:ftplib `storbinary` 语义 = `sendall` 发完立即关
数据连接、回控制通道等 226。芯片把最后一段数据落进 RX 的同时抛出 DISCONNECT:
- bit3=1:RX 数据在,MCU 来得及读(TRAP-NET-18 的「排空 RX 再 close」修复才成立);
- bit3=0:芯片**当场**关 Socket 并清缓冲 → MCU 永远读不到尾部 → 文件被截断却仍回报 226
(静默损坏)。这是 bit3=0 的**必然结果**,不是概率问题——软件再聪明也拿不回已被硬件擦掉的数据。
即:改 bit3=0 等于用「一个硬件保证、不可修的数据丢失」去换「一个软件侧可修的关闭义务」,
严格更差。:8888 对时包、:7003 远显推流同样是「数据 + 随即关闭」形态,一并受害。
**③ bit3=0 也并不能真正消除孤儿态。** 芯片自动关以**收到** DISCONNECT/TIM_OUT 为前提,
而 TRAP-NET-17 已实证快速复连场景芯片会**丢**这两个中断(`docs/问题记录/CH395F_Trap_Records.md`
B10 行)→ 中断丢了,bit3=0 同样停在已关/半关态,孤儿 Socket 照样存在。
它只是降低出现概率,代价却是上面第②条。
**④ 所以「这么多问题」不归 bit3=1。** 断开后停 `0x05/0x00` 是 bit3=1 的**合法后果**(D4);
真正造成 B10 永久拒连、消费者卡死、EOF 被吞没的,是软件侧没把「该由 MCU 关 Socket」这条
义务落到唯一所有者——重建所有者 12 个、无人负责组合 15 个(§2.3),以及 U12 的「relisten
的 LISTEN 失败不 close 芯片 Socket」与 open 失败路径不对称。bit3=1 只是把义务显式化,
把原本被芯片掩盖的架构欠债照了出来。
**⑤ 安全处理 = 把 bit3=1 的契约在软件侧收口**(即 §4 的两条不变量,逐条对应上面的缺口):
| 契约要求 | 对应缺口 | 收口手段 |
|---|---|---|
| 「会话结束」必须被**谁都能看见**,不能只在边沿那一刻 | EOF 边沿化:抢先 relisten → recv 永久 `WOULDBLOCK`(§2.3) | 不变量 I:`net_sock_t.session_dead` 粘滞电平,`net_recv_locked` DONTWAIT 判据改 `state==CLOSED \|\| session_dead`(P1) |
| 关闭 Socket 必须**一定有人做**、且只做一次 | 重建所有者 12 个 + U12 的 LISTEN 失败不回 close | 不变量 II:`net_listener_pump` 单点重建 + 补 U12 的 close 对称(P2)(自愈收敛阶段 3:pump 已并入 `net_reconcile_tick` 形态③④,单一 100ms tick——不变量 II 不变,见 §10.10) |
| 关闭姿势必须正确 | TRAP-NET-12:陈旧 DISCONNECT 锁存秒断下一条连接 | `net_close_locked` 既有「close → 等 `CLOSED` → 排空本 Socket 中断」,是 bit3=1 下唯一正确的关闭序列 |
| 异常释放须先排空残渣再关 | TRAP-NET-18:~2KB RX 残渣被下条连接当自己的数据读走 | `net_relisten_locked` 隔离期(400 ms)持续排空 RX + 主动 close + 等 `CLOSED/TCP_CLOSED` |
> **命名注(自愈收敛阶段 1.5,2026-10-13)**:上表与下文所说的「DONTWAIT」机制本体不变
> (`NET_MSG_DONTWAIT` 仍是 `net_recv_locked` 的非阻塞路径与 EOF 判据载体),但对外
> 入口已主流化:具名 API `net_recv_nb()`(单次非阻塞)+ `net_setblocking(fd, on)`
> (模式挂控制块 `blocking` 属性);`net_recv()` 默认改为**无限阻塞**(30s 默认预算
> 删除),受 IWDG 监控任务的有界等待契约见 `docs/网络/BSD_Socket_API_使用指南.md`
> §10.1 与 `docs/网络/网络自愈架构设计.md` §4.5。本文所有「DONTWAIT 判据 /
> 返 WOULDBLOCK」表述按新命名逐字仍成立,不逐处改写。
**⑥ 本次(P0)已落地的口径修正**:`net_socket.c:3627` / `:3641` 原注释写「DISCONNECT 后
芯片自动关闭 Socket(FUN_PARA 默认)」——与实际 `0x08` **相反**。这类注释正是 U12 这类
「跳过显式 close」缺陷的观念源头,已改正并留痕。
## 3. 为什么不能走"芯片原生多连接模式"(A′)
芯片确实自带 lwIP 式模型:FUN_PARA bit1(≥0x44,本仓实测 0x4A)下
「服务器的 Socket **仅用来监听**,单片机需要分配其它 Socket 用于该连接」(§9.2.6 L1295)。
但本产品**不可用**,三条硬证据:
1. **不支持多监听**——`docs/问题记录/CH395F_Trap_Records.md` 附 FTP 改造清单第 4 条
「CH395F 多连接模式不支持多监听(类型:架构)」,处置即 `fun_para=0x00`。
本产品有 21/8888/7003 **三个常驻监听** + 会话期动态 PASV 监听。
2. 池分配按**源端口匹配**(`docs/芯片手册/CH395F.md` §9.2.6 L1295),多监听器的池会互相串。
3. 历史实测:「多连接模式下最多接受 3 个客户端(Socket 1~3),Socket 4~7 的 CONNECT 中断从不触发」(:10)、
「多个 Socket 数据互相覆盖」(:35)——即 TRAP-NET-02/-11。
且 FUN_PARA **初始化后不可改**(`docs/芯片手册/CH395F.md` §5.52 L795⑥),这是开机一次性决策,不能运行时降级。
**结论:A′ 出局;根治必须在单连接复用模型内做。**
## 4. 根治原理:两条不变量
- **不变量 I(电平化)**:「这条会话已结束」必须是**可重复查询、幂等**的状态,
而不是"恰好在那一刻观察到才算"。谁先看到不重要,**谁都饿不着**。
- **不变量 II(单一所有者)**:监听器的重建**只有一个决定者**(net 层的 listener pump)。
其它所有位置只**置事实**,不**做动作**。(自愈收敛阶段 3 起该决定者 = `net_reconcile_tick` 形态③④,pump 已并入,见 §10.10)
两条不变量成立后,"谁重建""会不会错过"这两类问题在结构上消失,
对账④从"唯一救命稻草"降级为"事件面未收到时的补漏"(TRAP-NET-17 实证事件面不是可靠唯一信源——E14 复核:事件非芯片侧丢失、被读清消费——故对账**必须保留**)。
## 5. 分阶段方案
### P0 口径与登记(零风险,纯文档/注释)
1. 修 `net_socket.c:3627`、`:3641` 的 FUN_PARA 口径错误注释(实际 `0x08`,芯片**不**自动关)。
2. 把 U11(本层 CLOSED + 芯片正在 LISTEN,④判据缺口)、U12(relisten 的 LISTEN 失败不 close 芯片 Socket,
与 open 失败路径不对称)、U14(`NET_SOCK_STATE_TCP_CLIENT_RECONNECTING` 死枚举)、
U15(丢 RECV_OK 无人对账)登记进 `docs/问题记录/CH395F_Trap_Records.md`。
3. 在 B10 行补记残余:**④ 扩大了"EOF 被吞没"窗口**(见 §2.3)。
### P1 边沿 → 电平(小改,收益最大)
1. `net_sock_t` 增 1 字节 `session_dead`(粘滞"本会话已结束"):
- **置位**:`handle_disconnect_event` 释放分支、`handle_timeout_event` ESTABLISHED 分支、
对账①(复用前者)、`net_connect_locked` 命令被拒。
- **清零**:新的 CONNECT 成功(`handle_connect_event`)、`net_close`/`net_socket`(memset)。
- **不清**:`net_relisten_locked`——**这正是关键**,重建监听与"应用是否已确认会话结束"解耦。
2. `net_recv_locked` DONTWAIT:判据由 `state==CLOSED` 改为 `state==CLOSED || session_dead`
→ EOF **可重复获得**,任何消费者都不会再饿着。
3. `ui_net_sink` 三处收口:
- `:563` 真错误分支加 `g_client != g_listen` 护栏(与 `:552` 一致,MCU 上不关监听);
- EOF/错误后**不再**自行 `bind_listen`(重建交回 net 层,落实不变量 II);
- accept 门控从 `net_can_accept`(ESTABLISHED)改为新的 `net_listener_armed(fd)`
(LISTENING 或 CLOSED+auto_relisten)→ 让 :7003 重新吃到 `lost_connect_probe`。
4. 验收(新增腿,专打"抢先 relisten"):客户端连上 :7003 → 拖住 uiTask 使 relisten 先发生 →
断连 → 断言 sink 仍拿到 EOF、且**下一个客户端能被服务**(当前会被旧会话静默吸收)。
回归矩阵沿用 B10 那套(`:7003` 10/10、`:21` 50/50×2、`:8888` 20/20、`ftp_regression` 11/11、TRAP-NET-18 5/5×2)。
### P2 单一所有者(中改,消灭多所有者)—— P2a ✅ 2026-10-12 / P2b 建议暂缓
1. 新增 `net_listener_pump()`(100 ms 周期),接管**周期性**重建,是唯一做周期性
`net_relisten_try` 的地方。✅ 已落地(自愈收敛阶段 3 已并入 `net_reconcile_tick` 形态③④,单一 100ms tick,见 §10.10)
2. ⚠ **原第 2 条已被实测推翻**:原计划「删除 `net_accept_locked` / `net_recv_locked` /
`net_pending_tick` 三处即时重建」。实测删掉会把 CLOSED 窗口从 ~0 拉长到 ~100 ms,
调用方高频空转 `net_accept`(RPC 无上界)→ uiTask 最坏单轮 172 ms → 1034 ms + `[WD] task stall`。
**故保留这三处**(消费者动作触发,非周期性抢先),只删对账③④ 两处**周期性**路径。
调用点 7 → 5。✅ 已按修正版落地(详见 §9)
3. P2b:消灭 netTask 内的同步忙等(U6):`net_close_locked` 的 500 ms `osDelay` 循环、
`net_listen_locked` 的 3×500 ms,改用既有 D10 非阻塞重试模式。
**建议暂缓** —— 实测 `task stall` 在忙等未改造时同样存在,危害是理论性的(详见 §9)
4. 补 U11:本层 CLOSED 而芯片**正在** LISTEN → 只做状态重同步,不重下发命令。✅ 已落地
5. 验收:`:21` 0ms×50 仍 50/50;`netp`(`net_poll` 段最长耗时,µs)不劣化。✅ 见 §9
(⚠ 原判据「close/listen 风暴期间无 `[WD] task stall`」**不可用**:该现象在基线同样存在,
见 §9 的 P1/P2a 对比数据)
### P3 远期(**不建议现在做**,仅登记方向)
形态 A:listener 成为一等对象 + `accept` 返回虚拟句柄(与 `simulator/sim/net_socket_sim.c`
已是同一模型,消除真机/模拟器双模型分裂)。代价:控制面重写 15~25 函数、RAM +<1 KB、Flash +2~6 KB、
回归面覆盖全部服务与板级套件。只有当产品需要"同一端口并发多连接"时才值得立项。
## 6. 顺序与回滚
P0 → P1 → P2 严格串行,每阶段独立提交、独立回归、可单独回滚。
P1 是性价比最高的一步:它不改变 fd 模型,却把 B10 与"消费者卡死"两类事故同时从结构上消掉。
P2 之后,对账④保留但只作事件面未收到的补漏(TRAP-NET-17 实证事件面不是可靠唯一信源,不可删)。
## 7. 证据附录(2026-10-12 实测与调研,供后续执行时复核)
### 7.1 B10 现场与修复实测
| 项 | 数据 |
|---|---|
| 孤儿态探针(临时,测完已删) | `TEMP orphan sock2 state=0 port=7003 chip=0x05/0x00` 每秒一条持续 12 s+;`:7003 300ms×10` → `SUMMARY rounds=10 ok=0 fail=10 unrecovered=10`;+30 s 仍拒连 |
| 判"是否 B3 引起"的 A/B | 基线 `794a688` = 5/1/1/5(均值 3.0)vs B3 构建 = 3/7/2/1(均值 3.25)→ **统计不可区分**;每个样本前硬复位 + 等 12 s 干净启动 |
| ④ 修复后 | `:7003 300ms×10` **10/10**(+20 s 存活);`:8888 0ms×20` 20/20;`:21 0ms×50` 50/50;`:21 100ms×50` 50/50;`ftp_regression` 11/11 ×2;`ftp_abort_probe --check-abort` RST 5/5、`--graceful` FIN 5/5;`@test` PASS(600 ms、回读无差异);`Code` 197008→197084 |
| 救援日志(生效证明) | `sock2 reconcile: orphan listener (chip=0x05/0x00) -> relisten` → `auto-relisten ok` / `relisten retry ok` |
| 台架方法论坑 | ① 一轮把监听打死不自恢复 → **每腿前必须硬复位**,否则 A/B 被污染(本次第一腿就是这么废的);② 判固件回归前先核对主机侧工具凭据(`ftp_abort_probe.py` 用 anonymous 登录,N-19 后 530 拒 → 假 0/5) |
### 7.2 官方例程的所有权模型(CH395EVT V2.0,主机路径 `D:\Code\DTU\使用的芯片手册\CH395EVT\EVT\`,不在仓内)
- **EXAM5(多连接)**:`main.c:296 SetFunPapr(0x02)`(bit1=1);Socket0 `OPEN+TCPListen` 后再不碰(:117-127),Socket1~7 只配端口/协议**不 OPEN**(:129-133);DISCONNECT/TIM_OUT **只打印、不 close 不重 LISTEN**(:203-212)——恢复完全由芯片自动,**只在多连接模式成立**。
- **EXAM10(单连接,FTP Server)**:DISCONNECT → `CH395.C:234` 显式 `CloseSocket` → `:237-238` **立即重新 OPEN + 重新 TCP_LISTEN**;TIM_OUT 同路径(`:250`/`:254`);调用者是中断处理器;close→reopen **无延时无重试**,失败 `mStopIfError` 挂死。
- **结论**:单连接模式下官方**零自动重监听**,且原生做法是**在事件现场当场重建**;本仓对账④是周期兜底,方向一致但不可互替(TRAP-NET-17 实证事件面不是可靠唯一信源)。
- **中断所有权**:全局寄存器整体读即清、无按位清除(`docs/芯片手册/CH395F.md` §5.15 L441);CONNECT/DISCONNECT/TIM_OUT 是**事件位**(锁存但读即清,可被其它读者消费掉),RECV 是**条件位**(电平语义、可补)→ 任何"只靠事件重建监听"的方案天然不完备。
### 7.3 状态机事实(`Drivers/BSP/NET/net_socket.c` 3921 行)
- `->state =` 赋值 **13 处**(:815 :953 :1069 :1072 :1139 :1685 :1693 :2075 :3577 :3611 :3698 :3765 :3787)+ 2 处 memset(:249 全表、:2622 单块);`state` 的**全仓唯一写者**是本文件。
- **重建所有者 12 个**:栈内 8(对账③④、`handle_disconnect_event` 监听分支、`net_accept_locked` CLOSED 分支、`net_recv_locked`、`net_pending_tick`、`NET_MSG_RELISTEN`、`net_resync_listen`、`net_listen_locked`)+ 应用层 4(lftpd、time_sync、ui_net_sink、net_task 代次)。
- 做决策必须知道的判据:`net_listen_locked:1102-1104` 状态门**只允许 BOUND/CREATED**(对已 CLOSED 的监听 fd 直接 `net_listen` 返 INVAL);`net_connect_locked:1583` **无状态门**(可在 LISTENING 上改成 CONNECTING);`net_recv_locked:1862-1873` 快路径**无状态门**;`net_can_accept:1453` 要求 ESTABLISHED。
- M6(`handle_disconnect_event:3659-3701` 置 CLOSED)之后,**唯一不依赖应用调用的收口是对账④**;`:1346`/`:1884`/`:2451` 三条内部路径全部以"应用先发起一次 accept/recv"为前提,而 `ui_net_sink.c:512`/`:536` 的门控恰好把这两类调用在 CLOSED 态下全部掐断——这就是四方互等的成因。
### 7.4 三个服务的契约互不一致
| 服务 | 认为的连接结束信号 | net 层实际给什么 | 监听重建者 | 是否留无人重建组合 |
|---|---|---|---|---|
| `lftpd` 控制 :21 | `read_line!=0`(EOF→-1,`lftpd_inet.c:154-156`) | 阻塞 pending 保证 CLOSED 时挂起 recv 必被结束(FIN→0 / 异常→CONNRESET,`net_socket.c:2457-2462`) | net 层(`:2449-2451`),应用**永不** close 控制 fd(`lftpd.c:746-750`) | 无(契约一致,是正面样板) |
| `time_sync` :8888 | EOF/30 s 预算/错误,随后**无条件** `net_close(conn==ls)`(`time_sync.c:182`) | 同上 | 应用自己每会话全量重建(`:224`) | 不留孤儿,但违反「EOF 不得 close 监听 fd」→ close→重建沉降窗(TRAP-NET-09)+ 最坏 ~10 s 端口下线 |
| `ui_net_sink` :7003 | `net_recv` 返 0 或真错误;重建条件 `net_get_sock(g_listen)==NULL`(`:557`/`:566`) | 该 0 只在 recv 时刻 `state==CLOSED` 才给 | 名义 net 层,实际自身条件在 MCU **恒假**(fd 不释放) | **有**(B10 与"消费者卡死"都出自这里) |
| 模拟器后端 | `recv` 0 恒可得、fd 分离(`net_socket_sim.c:293-336`) | Winsock 原生 | 无需 | 无——且对上述全部失效模式**零覆盖** |
### 7.5 ④ 副作用的判据链(P1 要消除的对象)
`net_can_read:1477-1478` 在非 ESTABLISHED(**含 LISTENING**)返 1 → 消费者去 recv →
DONTWAIT 在 LISTENING 下返 `WOULDBLOCK`(`:1890`)→ `ui_net_sink.c:560-569` 判为"真错误"→
`:563` **无 `g_client != g_listen` 护栏**的 `net_close(g_client)` 在 MCU 上等于**关掉监听 fd**
(与同文件 `:134` 注释"严禁"自相矛盾)→ 靠随后 `bind_listen` 自救,落 TRAP-NET-09 沉降窗。
## 8. 决策记录
| # | 决定 | 依据 |
|---|---|---|
| D1 | **A′(启用芯片多连接模式)出局**,根治必须在单连接复用模型内做 | `docs/问题记录/CH395F_Trap_Records.md` 附 FTP 改造清单第 4 条「多连接模式不支持多监听」;本产品 3 常驻监听 + 动态 PASV;§9.2.6 L1295 池按源端口匹配会互串;历史实测 TRAP-NET-02/-11;FUN_PARA 初始化后不可改(`docs/芯片手册/CH395F.md` §5.52 L795⑥) |
| D2 | 采用**形态 B 深化**(电平化 + 单一所有者),**不做 P3** | P3 需控制面重写、RAM/Flash 上升,只有"同端口并发多连接"需求才值得 |
| D3 | 对账④**保留**(`4f2cc8b` 不撤回),其副作用由 P1 消除 | TRAP-NET-17 实证事件面不是可靠唯一信源(E14 复核:非芯片侧丢失,是读清消费)→ 周期对账不可删;④ 已把 `:7003` 从 0/10 永久死拉到 10/10 |
| D4 | `FUN_PARA` **保持 `0x08`**,不因孤儿态改配置(2026-10-12 复核确认) | bit3=1 是 TRAP-NET-15/-20 定案的"保留排空现场"契约;bit3=0 会令芯片在 DISCONNECT/TIM_OUT 时**当场清缓冲**(`docs/芯片手册/CH395F.md` §5.52② / §5.23 L572)→「发完即 FIN」流量的尾部数据必被硬件擦除、**不可补救**;且 bit3=0 也挡不住事件面未收到(TRAP-NET-17)→ 孤儿态照样有。孤儿态是 bit3=1 的合法后果,应在软件层收口而非回退契约。完整论证见 §2.4 |
| D5 | 顺序 **P0 → P1 → P2**,每阶段独立提交、独立回归、可单独回滚 | P1 不动 fd 模型却消掉两类事故,性价比最高 |
| D6 | 真机/模拟器**双模型分裂暂不消除** | 消除即等于做 P3;先靠 P1 把应用侧契约对齐 |
| D7 | 同族残留**不在本次根治范围**,各自单列:U1(CONNECTING 滞留,B2 残余)、U11、U12、U14、U15,以及审计项 B4/B5/B6/B7/B8/B9 | 与监听生命周期不同因,混做会让回归面失控 |
## 9. 执行清单(后续按此执行,逐项勾选)
**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`——**实际落点改 《网络自愈架构设计》**:U11/U12/U14 机制归属见 `docs/网络/网络自愈架构设计.md` §4.1;U15 经 E6 裁决(命中=0)登记观察项(`docs/网络/网络自愈架构设计.md` §4.8)
- [x] 在 B10 行确认已记「④ 扩大 EOF 吞没窗口」——2026-10-13 复核:新 TRAP-NET 文档 B10 行在位(✅ 已修 + 副作用注记 + `net_drop_conn` 根治出口)
**P1(边沿→电平)**
- [x] `net_types.h` 的 `net_sock_t` 增 `uint8_t session_dead;`
- [x] 置位:`handle_disconnect_event` 释放分支、`handle_timeout_event` ESTABLISHED 分支、对账①(复用前者)、`net_connect_locked:1685`
- [x] 清零:`handle_connect_event`(新 CONNECT,两支)、`net_close`/`net_socket` 的 memset 与显式清零;**`net_relisten_locked` 不清**(关键,已加注释钉住契约)
- [x] `net_recv_locked` DONTWAIT 判据改 `state==CLOSED || session_dead`
- [x] `ui_net_sink.c:563` 加 `g_client != g_listen` 护栏;EOF/错误后不再自行 `bind_listen`
- [x] 新增 `net_listener_armed(fd)`(LISTENING 或 CLOSED+auto_relisten),替换 `ui_net_sink.c:512` 的 `net_can_accept` 门控(MCU + `net_socket_sim.c` 双端实现)
- [x] **超出清单的必要扩展(设计时未预见)**:`net_pending_tick` ③ 断连判据同步改 `CLOSED || session_dead`。不改则 lftpd 的**阻塞** recv 仍会被抢先 relisten 饿死——①要求 ESTABLISHED、③要求 CLOSED,抢先 relisten 后两头落空,只能干等 deadline 超时。判据放宽后,两处 relisten 调用均须补内层 `state == CLOSED` 门:`net_relisten_try` 无状态门,对已 LISTENING 的 fd 再 open+listen 会因 USE 错误走进关闭分支、**关掉正听着新 SYN 的 Socket**(U12 同源事故)
- [x] 全量回归矩阵(§7.1 那一套)—— 2026-10-12 真机全绿,见下「P1 台架验收记录」
- [!] 「抢先 relisten」腿 —— **外部注入不可构造,已放弃**:见下「验收腿的否定结论」
- [x] `@test` PASS → 回读校验(`No difference found`)
**P1 台架验收记录(2026-10-12,`@build` Code=197096 / 基线 197084,+12 B)**
| 腿 | 结果 | 说明 |
|---|---|---|
| `@test` 启动闭环 | **PASS** | 启动锚点齐、无 `[ERR]`、reset cause ×1、7.2 s 到 net ready |
| `ftp_regression` | **11/11** | 含 STOR 32KB —— 「发完即 FIN」链路,`session_dead` 直接作用面 |
| TRAP-NET-18 `--check-abort`(RST) | **5/5** | 450 + 5xx 正确回报,无误报 226 |
| TRAP-NET-18 `--graceful`(FIN) | **5/5** | 226 正常 |
| `:21` 快速复连 0 ms ×50 | **50/50** | TRAP-NET-17 基线不回归 |
| `:21` 快速复连 100 ms ×50 | **50/50** | 同上 |
| `:8888` 0 ms ×20 | **20/20** | 对时服务不回归 |
| `:7003` 300 ms ×10 | **10/10** | B10 原始失败场景(修复前 0/10) |
| 设备侧孤儿对账 `orphan listener` | **0 次** | 探针时段经 serial-assistant `clear` 划界后统计 |
> 台架观测用 **`serial-assistant` skill**(daemon 独占串口、日志即接口)——
> 设备侧簿记(`standalone accept` / `auto-relisten ok` / `DISCONNECTED, releasing` /
> `orphan listener`)直接 `log --grep` 取增量,无需自建串口客户端。
> ⚠ 自建 pyserial 客户端会撞 `PermissionError(拒绝访问)`:daemon 独占绑定串口。
**验收腿的否定结论(重要,勿再重复踩)**:设计文档原计划的新增腿是
「连上 :7003 → 拖住 uiTask 使 relisten 先发生 → 断连 → 断言 sink 仍得 EOF」。
2026-10-12 实现后发现**真机外部无法构造**,且据此写的探针**无判别力**:
- 外部唯一可注入的是「A 发完数据立刻 FIN + 立即 B 抢占」。实测该形态下
**P1 版与对照组(撤掉 `g_client != g_listen` 护栏)双双 PASS**,设备侧
`accept/relisten/disconnect` 簿记与 `orphan` 计数完全一致 → 探针测不到它声称要测的东西。
- 机理:真实「抢先」要求 relisten 发生在**消费者下一次 recv 之前**;而对账④ 周期
200 ms + 去抖(`NET_RECONCILE_STABLE_N`),`ui_net_sink` 轮询 100 ms ——
正常时序下消费者总是先 recv 到 EOF,对账抢不到。**只有 uiTask 被拖住**才可能出现,
而那是设备内部状态。
- 处置:按 TRAP-NET-20「不留未证明的复杂度」**删除该探针**;该场景登记为**残余验证项**,
需设备内注入点(参照 `TEST_LATENCY_PROBE` 的编译开关模式)才能在真机复现。
- 因此 P1 的**正确性依据**是:① 代码级论证(判据由边沿改电平、置位/清零完备、
两处 relisten 补状态门);② 上表全量回归矩阵**不回归**;③ 不变量 I 的结构性论证。
「抢先」的前瞻场景**未被真机证明**,如实标注,不冒充已验收。
**P1 落成状态(2026-10-12)**:代码已实现、`@build` 0 错 0 警(`Code=197096`,较基线 +12 B)、
`mem-numbers` 快照已 `--write` 刷新、除下述基线问题外静态门禁全绿。
⚠ **已知基线问题(非本改动引入)**:`doc-links` 在**任何从当前 `v2-on-ui` 切出的工作树**上都会报
2 处失效引用——`RAM使用分析与优化方案.md` 引用了两个**尚未提交**的文件(RAM 基线 `.tsv` 与
RAM 预算台账 `.md`,属另一在途任务)。这两个文件在主工作树磁盘上存在故主线门禁呈绿,
工作树里没有故必报红(机理见 `docs/环境与部署/并行任务工作树方案.md` §10.2)。
待该任务合入后自动消解,**不应**由本分支代为提交他人半成品。
**P2(单一所有者)—— 已拆为 P2a / P2b**
**P2a(✅ 2026-10-12 落地,提交 `7cb5af0`)**
- [x] 新增 `net_listener_pump()`(100 ms 周期 + 去抖 + 冷却),接管**周期性**重建,
覆盖:① LISTENING 失位 ② CLOSED 不在 LISTEN ③ CLOSED 但芯片**正在** LISTEN(U11)
(自愈收敛阶段 3 已并入 `net_reconcile_tick` 形态③④/U11,单一 100ms tick,见 §10.10)
- [x] 删对账③④ 及其孤儿态认领门槛(对账只剩①②;认领收窄为 ESTABLISHED/LISTENING)
- [x] 补 U11:本层 CLOSED 而芯片正在 LISTEN → **只做状态重同步**(含 `last_peer_port`
转登 + `lost_conn_seen` 清零,同 `net_resync_listen`),不发任何芯片命令
- [x] `net_listener_armed` 收紧:CLOSED 态仅在 `readable≠0`(有残留)时放行
- [x] 验收:`:21` 0ms×50 / 100ms×50 各 **50/50**;`:8888` **20/20**;`:7003` 300ms×10
**10/10**;`ftp_regression` **11/11**;Trap23 abort/graceful 各 **5/5**;
设备侧 `orphan listener` 增量 **0**(P1 基线同条件为 **2**)→ 目标达成
- [ ] **P2b(同步忙等改造)—— 建议暂缓**:`net_close_locked` 的 500 ms `osDelay` 循环、
`net_listen_locked` 的 3×500 ms。**实测依据**:全量历史 `task stall` 在忙等未改造时
同样存在(P1 基线同条件 53 条),忙等的危害是**理论性**的;而 `net_close` 是每次会话
结束都走的热路径。按 TRAP-NET-20「不留未证明的复杂度」,为一个未发生的故障去动热路径,
性价比存疑 —— 建议等有 `task stall` 与忙等的**因果证据**后再做。
(**自愈收敛阶段 3 已实施 2026-10-13**:不是"为忙等而改",而是驱动 `sock_reset` 原语落地后
顺路收口——`net_sock_reset_sync` 确定性排渣替代盲等(GRACEFUL 100ms 底 / 600ms 封顶),
并删除 `net_close_locked` / `net_connect_locked` 的全局中断读清(F-NET-7),见 §10.10)
> ⚠ **P2a 与设计文档原清单的关键差异(实测推翻原计划,务必据实执行)**:
> 原清单第 2 条要求把 `net_recv_locked` / `net_pending_tick` / `net_accept_locked`
> **三处即时重建一并删除**。台架实测证明**删掉会引入回归**:
> - CLOSED 窗口从 ~0 拉长到 pump 周期 + 隔离期(~100 ms);
> - 期间调用方高频发起 `net_accept`(RPC:入队 + `xTaskNotifyWait` 回执**无上界**)
> → 队列积压 → 实测 uiTask 最坏单轮 **172 ms → 1034 ms**,`[WD] task stall` 出现;
> - 故**保留这三处**:它们是「**消费者动作触发**」(消费者正在等这条连接的结局),
> 不是周期性「抢先」;真正该收敛的只有**周期性**路径(对账③④)。
> - 收敛后 `net_relisten_try` 调用点 **7 → 5**(pump + 显式消息 + 3 处消费者驱动)。
> ⚠ **如实记录的观测**:`task stall` 是**既有现象**(P1 基线同条件 53 条 vs P2a 32 条,
> 未恶化);uiTask 最坏单轮的历史最大值 P2a 偏高(1054 ms vs P1 172 ms),但 `uil`
> (最近一次)为 10999 µs 即**当前正常**,判为一次性卡顿,**留待观察**。
**回滚点**:P0 纯注释可直接 revert;P1 若回归失败 revert 单提交即可(④ 仍在,退回到"端口活着但慢消费者可能卡死"的已知状态);P2a 独立成提交(`7cb5af0`),revert 后回到"对账③④ 兜底 + 12 所有者"的 P1 状态;P2b 未做。
---
## 10. 监听生命周期模块化边界设计(**已执行 2026-10-12**,提交 `c1ab902`)
> **动机**:真机 net 层 `net_socket.c` 4126 行,而模拟器做同样的事只有 807 行(5 倍);
> `relisten` 一词在文件里出现 110 次。用户诉求 = **高内聚低耦合**。
> P2a 把「周期性重建」收敛到 `net_listener_pump`(所有者 7→5)之后,搬移边界才**第一次清晰**——
> 这正是 P2a 相对"直接抽模块"的价值:先收敛,再搬移。(自愈收敛阶段 3 起 pump 已并入 `net_reconcile_tick` 形态③④,见 §10.10)
>
> 本节只定**边界与接口**,不含任何代码;执行须过 §10.6 的验证门。
### 10.1 目标与可验证判据
「高内聚」在本项目落成**三条可机械检验**的判据(不是感觉):
| # | 判据 | 现状 | 目标 |
|---|---|---|---|
| 1 | 模块外调用「重建监听」的**入口数** | 5(pump / 显式消息 / recv / accept / pending) | **1**(新模块导出) |
| 2 | `s_relisten_*` 静态状态被**模块外直接读写** | 2 处外部写 | **0** |
| 3 | 依赖方向 | 无 | 新模块 → `net_socket.c` **单向**,无反向 include |
⚠ **明确不承诺**:本重构**不消除 fd 别名**(同一 Socket 既当监听器又当连接)。
那是 CH395F 单连接模型 + bit1 多连接模式**不支持多监听**(Trap 记录改造清单第 4 条)锁死的,
属硬件上限(§3 D1)。重构只提升**组织度**,不改变架构上限——**别指望补丁消失,只指望补丁不再重复**。
### 10.2 现状清点(2026-10-12,`net_socket.c` 4126 行)
| 类别 | 数量 | 明细 |
|---|---|---|
| 纯监听生命周期函数 | **7** | `net_relisten_locked`(139) / `net_listener_pump`(107) / `net_relisten_retry_tick`(47) / `net_relisten_try`(45) / `net_drain_chip_rx`(32) / `net_relisten_clear`(28) / `net_relisten_done`(16) |
| 静态状态数组 | **7** | `s_relisten_quarantine` / `_settle_start` / `_settle_dead` / `_wait` / `_next` / `_dead` / `_mark` |
| 常量 | **8** | `NET_RELISTEN_{QUARANTINE,GRACEFUL,SETTLE}_MS` / `NET_RELISTEN_SETTLE_{QUARANTINE,GRACEFUL}` / `NET_RELISTEN_{RETRY,DEADLINE}_MS` / `NET_LISTENER_PUMP_PERIOD_MS` |
| **搬移量小计** | **≈470 行** | 函数块 414 + 常量/状态注释块 55 |
> ⚠ **自愈收敛阶段 3(2026-10-13)追注**:上表是 2026-10-12 搬移时的历史清点,原文保留。此后 `net_listener_pump` 已**删除**(重建决策并入 `net_socket.c` 的 `net_reconcile_tick` 形态③④/U11,单一 100ms tick),`NET_LISTENER_PUMP_PERIOD_MS` 等 pump 常量同删、新增 `NET_RECONCILE_PERIOD_MS` / `NET_RECON_STABLE_*`——见 §10.10。
**跨边界引用点**(这个数字决定接口面大小,是本设计的核心依据):
| 引用 | 处数 | 位置 |
|---|---|---|
| `s_relisten_quarantine[fd] = <level>` 外部**写** | **2** | `handle_disconnect_event`(GRACEFUL)、`handle_timeout_event`(QUARANTINE) |
| `net_relisten_try()` 外部调用 | **4** | `NET_MSG_RELISTEN` 处理、`net_recv_locked`、`net_accept_locked`、`net_pending_tick` |
| `net_relisten_clear()` 外部调用 | **2** | `net_socket`(新 fd)、`net_close_locked` |
| 自愈 tick 调用 | **2** | `net_process_messages`(`retry_tick` + `pump`) |
> **关键观察**:状态数组的**外部写只有 2 处**、且都是「登记沉降等级」这一个语义。
> 这说明模块边界天然干净——**接口面只需 4 个函数**,不需要暴露任何数组。
### 10.3 边界划分
**搬入新模块** `Drivers/BSP/NET/net_listener.c`(**真机专属**,sim 不编):
| 搬入 | 理由 |
|---|---|
| `net_relisten_locked` / `net_relisten_try` / `net_relisten_retry_tick` / `net_relisten_clear` / `net_relisten_done` | 「维持 fd 处于 LISTEN 态」的全部机制 |
| `net_listener_pump` | 唯一的周期性重建决策者(P2a 成果)(自愈收敛阶段 3 已并入 `net_reconcile_tick` 形态③④,单一 100ms tick,见 §10.10) |
| `net_drain_chip_rx` | 重建前的排空(只被 relisten 调用) |
| 7 个 `s_relisten_*` + 8 个 `NET_RELISTEN_*` / `PUMP_PERIOD` 常量 | 状态与策略私有化 |
**留在 `net_socket.c`**(并说明理由,避免"看着像就搬"):
| 留下 | 理由 |
|---|---|
| `net_listen_locked` | 是**应用 API 路径**(首次监听,含 §9 待议的忙等 P2b),且要处理 BOUND/CREATED 状态门与缓冲——属"连接层建监听",不是"维持监听" |
| `net_accept_locked` | 产出的是**连接**(LISTENING→ESTABLISHED 迁移),属连接层;它只**调用**新模块 |
| `net_lost_connect_probe` | 做的是"补 accept"(连接建立),不是重建监听 |
| `net_listener_armed` | **双端公开 API**(sim 也实现);纯 RAM 查询,很薄,留在原处避免 sim 缺符号 |
| ~~`net_resync_listen`~~ | **自愈收敛阶段1 已删除**:其"只改本层状态"语义并入对账⑤(静默回收形态自动认领,纪律"不发芯片命令"随动作沉淀进 `net_reconcile_tick`),手动 API 面退役 |
| `net_reconcile_tick` | ①②⑤ 是**连接**失同步自愈(③④ 已在 P2a 移交 pump;⑤ 为 自愈收敛阶段1 新增,吸收原 `net_resync_listen` 语义)(自愈收敛阶段 3:③④/U11 已**回到本 tick**,单一 100ms tick 覆盖①②③④⑤/U11,见 §10.10) |
| `net_sock_t` 全部字段 | 数据归 `net_types.h`(双端共享核心),**不随模块走** |
> ⚠ `net_sock_t` 的 `last_peer_port` / `lost_conn_seen` 是**跨层字段**:新模块的
> `net_relisten_locked` 要写它们(转登幽灵甄别基准),`net_lost_connect_probe` 与对账②
> 也要读写。**这是本次重构无法消除的残余耦合**(单连接模型的固有结果),
> 明确登记而非假装不存在。
### 10.4 接口设计(4 函数 + 1 枚举,全部窄接口)
```c
/* net_listener.h —— 只被 net_socket.c include(内部头,不进公开 net_socket.h,故 sim 无依赖) */
/* 沉降/隔离等级(替代外部直接写 s_relisten_quarantine) */
typedef enum {
NET_LISTENER_SETTLE_GRACEFUL = 2, /* 正常释放(DISCONNECT):等芯片收尾 100ms */
NET_LISTENER_SETTLE_QUARANTINE = 1, /* 异常释放(TIMEOUT/RST):等在途落尽 + 排空残渣 400ms */
} net_listener_settle_t;
/* 登记「本 fd 的会话已结束,重建前需沉降」—— 由 handle_disconnect/timeout_event 调用 */
void net_listener_settle(int sockfd, net_listener_settle_t level);
/* 尝试重建一次(不阻塞);已在重试队列则返回 -1 —— 替代 4 处 net_relisten_try */
int net_listener_try(net_sock_t *p_sock);
/* 清本 fd 的重建/沉降状态(新 fd 或 close 时)—— 替代 2 处 net_relisten_clear */
void net_listener_clear(int sockfd);
/* 自愈节拍:D10 重试轮询 + 监听器 pump(合二为一,调用方只记一个入口) */
void net_listener_tick(void);
```
**反向依赖(新模块需要 `net_socket.c` 提供的)**:
- `net_sock_t` 定义 + 字段(`net_types.h`,共享头,**不算反向依赖**)
- `s_net_socks` 数组 —— **问题点**:它是 `net_socket.c` 的 static。
**解法**:新模块**不反推 fd**,一律用 `p_sock->ch395_sock`(值等于 fd,`net_socket` 初始化时保证)
或调用方传入的 `sockfd`。这样**完全不需要访问 `s_net_socks`**,依赖面进一步收窄。
- `net_set_errno` / `NET_ERR_RETURN_1`(错误码)
- `ch395f_*`(驱动,本来就是外部依赖)
- `HAL_GetTick` / `lat_probe_*`(TEST_LATENCY_PROBE 构建)
### 10.5 构建与注册
| 项 | 动作 |
|---|---|
| `MDK-ARM/STM32F407-Demo.uvprojx` | 加 `<File>` 条目;`Drivers/BSP/NET` 已在 IncludePath(同目录,**无需新增路径**) |
| `simulator/CMakeLists.txt` | **不动** —— 新模块真机专属;sim 用 `sim/net_socket_sim.c` 自洽(无孤儿态、无 relisten) |
| 注册校验 | 跑 `registration-check`(双端注册反向校验)确认没有单边注册 |
> ⚠ 按项目约定,新 `.c` 文件的注册走 **`new-source-file` skill**(uvprojx + CMakeLists 双端),
> 不要手工编辑工程文件。
### 10.6 验证门(执行时必须逐条过)
1. **纯搬移可证**:搬移前后 `@build` 的 `Code`/`RO-data`/`RW-data`/`ZI-data` **完全不变**
(只挪位置、不加逻辑;任何数字变化都说明搬错了)。
2. `@test` PASS + 回读 `No difference found`。
3. 全量回归矩阵:`:21` 0ms×50 / 100ms×50、`:8888` 0ms×20、`:7003` 300ms×10、
`ftp_regression` 11/11、Trap23 abort/graceful 各 5/5。
4. 设备侧 `orphan listener` 增量 **0**(P2a 已达成的基线,不得回退)。
5. `net_listener.c` **不 include** `net_socket.c` 的任何符号(除 `net_types.h`)——
用 `grep -n '#include' net_listener.c` 人工核对。
6. 判据 §10.1 三条**逐条量化复核**(入口数 / 外部写 / 依赖方向)。
### 10.7 明确不做(边界外,避免范围失控)
- **不动** `net_listen_locked` 的忙等(那是 P2b,已判暂缓)。
- **不动** `net_sock_t` 字段布局(`last_peer_port` / `lost_conn_seen` 的跨层耦合本次保留)。
- **不动** sim 后端(D6:双模型分裂暂不消除;P1 已让**应用层契约**对齐)。
- **不做** P3 形态 A(listener 一等对象 + accept 返回虚拟句柄)—— 硬件不支持多监听,已出局(D1)。
- **不引入**第二套并行机制(沿用「同一能力只有一个实现」的收敛原则)。
### 10.8 执行顺序建议
1. **先搬状态与常量**(7 数组 + 8 常量)→ 编译验证(此时功能等价,只换位置);
2. **再搬 7 个函数**,同步把 8 处外部引用改成 4 个窄接口;
3. **最后合并 tick**(`net_listener_tick` 内部调 retry + pump),`net_process_messages` 少一个调用点;
4. 每步独立提交、独立 `@build`;**第 3 步后**才跑完整台架矩阵(前两步是纯组织变更,可只跑 `@build` + `@test`)。
### 10.9 执行结果(2026-10-12,提交 `c1ab902`)
**执行顺序调整(与 §10.8 的差异,据实记录)**:§10.8 的「先搬状态与常量」**单独做不可行**——
状态数组被 `net_relisten_*` 函数直接读写,只搬状态不搬函数会**编译失败**。
故第 1 步改为「**状态 + 常量 + 7 函数一次性纯搬移**」,第 3 步(合并 tick)**未做**
(收益仅少一个调用点,却会破坏「尺寸不变」这条硬判据,性价比不足)。
| 判据 | 结果 |
|---|---|
| `net_socket.c` 行数 | 4126 → **3656**(-471) |
| 新增模块 | `net_listener.c` / `net_listener.h`(真机专属) |
| **RW-data** | 980 → **980**(不变 ✓) |
| **ZI-data** | 143060 → **143060**(不变 ✓) |
| Code / RO-data | 197236 → 197228(-8)/ 17012 → 17000(-12) |
| 接口 | 8 处散点直调 + 2 处直接写数组 → **4 个窄接口** |
| `s_net_socks` 依赖 | **已消除**(改用 `net_get_sock` + `ch395_sock`) |
| 静态门禁 | 全绿(cppcheck / C 规范 / 双端注册 / 主机侧单测 12 套 / mem-numbers) |
| `@test` + 回读 | PASS + `No difference found` |
| 全量回归 | `:21` 0ms×50 与 100ms×50 各 **50/50**;`:8888` **20/20**;`:7003` **10/10**;`ftp_regression` **11/11**;Trap23 abort/graceful 各 **5/5** |
| 设备侧 `orphan listener` | **0**(保持 P2a 基线) |
> **关于「尺寸完全不变」判据的修订**:实际执行发现该判据**过严** —— 接口改造
> (fd 改传参、`net_set_errno` 提升可见性、新增 `net_pending_active`/`net_listener_settle`)
> **必然**改变代码生成。故修订为:**RW-data / ZI-data 必须完全不变**(数据总量不变),
> **Code / RO-data 允许极小变化**(本次各 -8 / -12 字节,相对 471 行搬移可忽略)。
**执行中暴露的三处「非纯搬移」(必要,均已注释留痕)**:
1. **`net_set_errno` 由 `static` 提升为模块间可见** —— 它是 `static`,新模块链接期
报 `L6218E: Undefined symbol`;`NET_ERR_RETURN_1` 宏随之迁到 `net_socket.h`
(它依赖前者,不能只移宏)。
2. **新增 `net_pending_active(fd)`** —— `net_listener_retry_tick` 原有「本 fd 有在途
挂起 recv 则跳过」(L3 保险,防重开 Socket 摧毁在途连接)直接读 `s_net_pending[i].active`;
新模块不能碰该 static 数组,故加薄访问器。**语义原样保留**。
3. **`net_listener.c` 必须真机专属** —— 它依赖 `ch395f_*` SPI 驱动,而 sim 是 Winsock
后端(无 `ch395f_*`、无孤儿态、无重监听概念)→ **无法**在 sim 编译。故:
只注册 Keil 端;并在 `scripts/check_registration.py` 的 `SIM_EXEMPT` **显式点名**
(该脚本既定的豁免原则正是「显式清单 + 每行理由,不整目录一刀切」,本例遵此)。
**教训(供后续搬移复用)**:
- 搬移前先查**跨边界引用面**(本次发现「状态数组外部写仅 2 处」),它直接决定接口面与风险;
- 「纯搬移可证」判据要选**对改造不敏感的量**(RW/ZI 数据总量),否则会被接口改造的
正常开销误报;
- 改完文件**必须重新 `git add`** —— pre-commit 校验的是**暂存区快照**,漏 add 会让
「本地已修好」与「提交时仍报错」自相矛盾(本次踩到,白跑两轮)。
### 10.10 自愈收敛阶段 3:pump 并入 reconciler(**已执行 2026-10-13**)
自愈架构审计(`docs/网络/网络自愈架构设计.md` §4.1 表注留痕沿革)指出:`net_reconcile_tick`(200ms,①②⑤)与
`net_listener_pump`(100ms,③④/U11)是**同一件事的两个半片**——都在"读芯片状态 → 比对镜像 → 分级处置",
拆两个 tick 只是历史演化(P2a 从对账里拆出去),既多一份去抖/冷却状态机,又让"对账① 置 CLOSED 后
要等下一个 pump 轮才重建"多一跳延迟。阶段 3 合并为**单一 100ms `net_reconcile_tick`**:
- **`net_listener_pump()` 删除**;三种情形作为**形态③④ + U11** 回到 `net_reconcile_tick`,
认领门槛按原 pump 口径以 `listener_fd` 门(`auto_relisten` + TCP + 已绑端口)恢复——
**不变量 II(单一所有者)不变**,重建决策仍只此一处;
- **判据改读驱动状态缓存** `ch395f_sock_state_cached`(唯一刷新点 = `net_poll` 每轮全量刷新,
E2 实测单次回读 18.1µs)——reconciler 不再自发状态 SPI,形态② 命中时的对端端口回读是
本 tick 唯一按需 SPI;
- **分级去抖**(100ms/轮):②=1 轮(`NET_RECON_STABLE_ACCEPT`)、③④⑤=2 轮
(`NET_RECON_STABLE_RELISTEN`)、①=4 轮(`NET_RECON_STABLE_DEATH`,M3 让位窗 400ms 不变);
冷却 1s(`NET_RECONCILE_COOLDOWN_MS`);U11 被动重同步不去抖不占冷却;
- 常量 `NET_LISTENER_PUMP_PERIOD_MS` / `NET_LISTENER_STABLE_N` / `NET_LISTENER_COOLDOWN_MS` 删除;
主循环自愈 tick 由 4 个回到 **3 个**(retry / pending / reconcile);
- 同阶段顺路收口 P2b(§9 曾"建议暂缓"):`net_listen_locked` / `net_connect_locked` 的 open 失败
重试与 `net_close_locked` 的排空循环改走驱动 `sock_reset` 原语(`net_sock_reset_sync`,
GRACEFUL 100ms 底 / 600ms 封顶),并删除 close/connect 路径的全局中断读清(F-NET-7)。
**台架回归(2026-10-13,最终构建 Code=211348)**:`:7003` 50/50、`:21` 100ms/0ms/600ms 各 50/50、
TRAP-NET-18 RST 10/10 + FIN 5/5、TRAP-NET-20 n30 11/11、iec104 18/18、`ftp_regression` 11/11、E-02 双 PASS;
netTask 静态最坏链 1944 B 不变(链尾改经 `net_listener_retry_tick`→`net_relisten_locked`)。
+23 -8
View File
@@ -36,7 +36,7 @@
| **跨文档同号不同义** | `Trap 02` 在 CH395F 是"缓冲区重叠"、在 GD5F 是"测试缓冲越界";`B10` 在规划是"送检窗口"、在陷阱审计是"孤儿监听" |
| **语义进编号 → 事实一变编号就撒谎** | `P1-x` 把严重度写进号里,而审计中大量条目事后降级(`E-59` P0→P1、dhara `is_free` P1→P2):改号则引用全断,不改则号实不符 |
| **孤儿编号(代码有、文档无)** | `I-1`/`I-2`、`Task #32` 会话号、`P1-01~P1-09`、`TC-GD5F-1100/1101/1102`、`TC-STO-06`、`TC-AH-*`、`TC-PE-*`(前两项项目已在 `docs/测试说明/实机测试实施指南.md` 自行承认) |
| **悬空引用(引用了不存在的号)** | `Trap 19` 从未存在却被 `docs/测试说明/CH395F_Test_Guide.md` 引用;`N-5/N-8/N-19/N-30/N-31` 销项删行后仍被引用 |
| **悬空引用(引用了不存在的号)** | `Trap 19` 从未存在却被 `docs/测试说明/CH395F_Test_Guide.md` 引用;`N-5/N-8/N-19/N-30/N-31/N-32` 销项删行后仍被引用(`N-32` = 监听生命周期模块化,被 `docs/网络/` 三篇与代码注释多处引用) |
| **同一事物多种字面** | `Trap 07` / `TRAP-07` / `陷阱 1`;`gen 181` / `gen-288` / `gen68`;`TC-NET-704` / `TC704` / `TC-203` |
| **规范自身就不自洽** | `docs/测试说明/测试文档模板规范.md` §6.3 同一行并列 `TC-STO-03`(2 位)与 `TC-RTC-001`(3 位);`docs/测试说明/测试文档模板规范.md` §6.2 已废止 `PHY` 模块码,而阶段 ID 至今仍是 `TM-PHY-01` |
| **机器校验只覆盖一个族** | 31 个 pre-commit 钩子中只有 `TC` 有 `scripts/check_test_traceability.py`,且**未挂钩子、warn-only、正则 `TC-[A-Za-z0-9]+-\d+` 匹配不到 `TC-AI-01A`**;`E/N/S/B/C/D/A/U/O/gen/Trap` 全部裸奔 |
@@ -45,7 +45,7 @@
---
## 2. 六条设计原则(强制)
## 2. 七条设计原则(强制)
| # | 原则 | 理由 |
|---|---|---|
@@ -55,6 +55,7 @@
| **P-4** | **形态唯一**:`<前缀>-<序号>`;前缀 1~3 位大写字母;序号位数按类别固定并左补零;子项只允许一级 `-<小写字母>`。**禁止**裸形(`S3`)、空格形(`Trap 07`)、中文形(`陷阱 1`)、圈号(`①②③④`) | 连字符与补零不得承载语义;字面唯一才能被机器判定 |
| **P-5** | **只增不复用**;每个类别有「下一个可用号」游标(`[计划] scripts/id_registry.json`),新增先取游标 | 并行任务与多文档同时新增时防撞号(`docs/问题记录/项目审查与优化建议报告.md` 变更记录自述"并轨时未做全局唯一性检查"就是没有游标) |
| **P-6** | **引用必须机器可判定**:跨文档引用用行内代码,首次出现紧邻属主文档名;文档内编号**必须**带文档名 | 与 `§N.M` 同一条路:能校验的约定才算存在 |
| **P-7** | **编号不语义,引用必语义**:编号本体保持无语义并冻结(同 P-2);但**正文引用必须带语义括注**——`TRAP-NET-18(异常释放后排空 RX 残渣)`、`docs/网络/网络自愈架构设计.md` §4.6(应用主动拆会话保监听);**禁止裸 ID 单独承担信息**。语义放括注,事实变了只改括注、编号不变 | 读者不该为看懂一句话去跳三篇文档。可读性由**引用方式**解决,不由**编号命名**解决——命名带语义会重蹈 `P1-x` 覆辙(§1.2)。落地见 §5.6 |
---
@@ -90,8 +91,9 @@
|---|---|---|---|
| `FR-[A-Z0-9]+-\d{2}` | 功能需求 | `docs/产品需求/产品需求设计文档.md` §5 | 各域独立递增;**新增域写进该文 §1.2 术语与缩写** |
| `NFR-[A-Z0-9]+-\d{2}` | 非功能需求(**新建**,现 §7 全是散文无号) | `docs/产品需求/产品需求设计文档.md` §7 | 从 `NFR-<域>-01` 起 |
| `N-\d{2}` | **登记簿条目**(技术债 / 长期观察 / 硬件改版项;含义由"网络项"重定义) | `docs/任务规划/登记簿.md` | 在册最大 `N-33` → 游标 `N-34`;原 `S-x`/`T-x` 并入本族(§7.1) |
| `N-\d{2}` | **登记簿条目**(技术债 / 长期观察 / 硬件改版项;含义由"网络项"重定义) | `docs/任务规划/登记簿.md` | **曾用**最大 `N-33`(`N-30`~`N-33` 均已销项、登记簿行已删,**现存至 `N-29`**)→ 游标 `N-34`(号不回收);原 `S-x`/`T-x` 并入本族(§7.1) |
| `E-\d{2}` | 错误码/返回值传播审计发现 | `docs/问题记录/错误码传播审计.md` | **冻结**:`E-63` 为最大号,**不再新增**;新发现一律 `DEF` |
| `E\d+` | **自愈实验编号**(N34 台架实验,**无连字符**) | `docs/网络/网络自愈架构设计.md` §5.2 | **已封闭**:`E0`~`E15`,**不再新增**;原始实验记录已归档(该文档 §12)。⚠ 与上一行靠**连字符**区分:带连字符 = 审计发现 |
| `TC-[A-Z]{1,4}-\d{3,4}` | 测试用例 | 各测试文档 | 3 位起;分阶段套件序号 = 阶段号×100 + 项号(阶段 ≥10 自然进位 4 位,`TC-NET-1001` 合规);后缀只允许 `-<小写字母>` |
| `TM-[A-Z]{1,4}-\d{2}` | 测试阶段 | 各测试文档 | 模块码取 §6 统一表(`TM-PHY-01` → `TM-NET-01`) |
| `DP-[A-Z]{1,4}-\d{2}` | 设计原则 | 各测试文档 | **加模块码**(现 `DP-01` 跨文档会撞) |
@@ -104,7 +106,7 @@
|---|---|---|---|---|
| `DEF-<nnn>` | 缺陷 / 问题发现 | `P0-x`~`P3-x`(在册项)、新发现 | `[计划] docs/问题记录/缺陷登记簿.md` | `DEF-001` |
| `WI-<nnn>` | 工作项 / 任务卡 | `B\d`(功能主线)、`C\d`(结构债)、`D\d`(验证窗口)、`W1-0`、`S1~S14`、`A-1~A-7`、`P1-10`、报告 `R-x`(内存优化)、`F-x`、`U1~U5` 的未闭环项 | `docs/任务规划/` 波次与排期文件(类别做列) | `WI-001` |
| `DR-<nnn>` | 决策记录 | `docs/子系统设计/网络Socket监听生命周期设计说明.md` 的 `D1~D10` | `[计划] docs/架构设计/决策记录登记簿.md` | `DR-001` |
| `DR-<nnn>` | 决策记录 | `docs/网络/网络自愈架构设计.md` 的 `D1~D7` | `[计划] docs/架构设计/决策记录登记簿.md` | `DR-001` |
| `OQ-<nnn>` | 待确认 / 开放问题 | `B0-a/b/c`、`待确认-N` | `docs/任务规划/基线与待确认.md` | `OQ-001` |
| `RSK-<nn>` | 风险(**文档内编号**) | 保护规划 `R1~R8`、看门狗 `R1~R7` | 各文档风险表(引用必须带文档名) | 各文档自 `RSK-01` |
| `TRAP-<MOD>-<nn>` | 陷阱记录 | `Trap 07`、`TRAP-07`、`陷阱 1` | `docs/问题记录/<模块>_Trap_Records.md` | 保留原号(`TRAP-NET-01~21`——NET 域 2026-10-13 全库迁移并重排连续,沿革见该文档、`TRAP-GD5F-01~06`、`TRAP-RTC-01~`) |
@@ -115,7 +117,7 @@
| `GEN-<nnn>` | 台架日志代次 | `gen 181`、`gen-288`、`gen68` | `docs/测试说明/串口助手设计说明.md` | 数字不变,仅改字面 |
| `RUN-<yyyymmdd>-<n>` | 台架轮次 | `round 1~9` | `docs/测试说明/实机测试实施指南.md` | 日期制 |
| `MS-<nn>` | 里程碑 | `M1~M3` | 波次文件 | `MS-01` |
| `RC-<nn>` | net 周期对账类型 | `①②③④`(唯一被代码逻辑引用的圈号链) | `docs/子系统设计/网络Socket监听生命周期设计说明.md` | `RC-01`~`RC-04` |
| `RC-<nn>` | net 周期对账类型 | `①②③④⑤`(唯一被代码逻辑引用的圈号链) | `docs/网络/网络自愈架构设计.md` | `RC-01`~`RC-05` |
| `PAT-<nn>` | 缺陷模式(九类) | `docs/问题记录/错误码传播审计.md` §1.1 的 `P1~P9` | 同文 | `PAT-01`~`PAT-09` |
| `ST-<n>` | 专项内部实施阶段(**文档内编号**) | 保护规划 §7.1 的 `P0~P5`、方案阶段 `P0~P3` | 各专项文档 | 各文档自 `ST-0` |
| `UL-<n>` | UI 标准化四层(**文档内编号**) | 报告 UI 四层 `L0~L3` | `docs/UI设计/UI_设计架构.md` | `UL-0` |
@@ -149,6 +151,8 @@
### 5.1 新增编号
0. **先问"能不能不加号"**:能用 § 小节引用、表格列、语义标题表达清楚的,就不要分配编号——
编号是"确需跨文档长期引用"时才付的成本(P-1 / P-3 / P-7)。确需新增才往下走。
1. 查 §4 总表确定类别与属主登记处;**表里没有的类别 → 先改本规范,不得自造前缀**。
2. 到属主登记处取游标号(`[计划] scripts/id_registry.json` 记录每类下一个可用号),登记处新增一行。
3. 严重度 / 状态 / 波次 / 出处写进表格列,**不进编号**。
@@ -159,7 +163,7 @@
|---|---|
| 全局编号,跨文档首次出现 | `` `docs/任务规划/登记簿.md` N-21 ``(紧邻,同 `§N.M` 规则) |
| 全局编号,同段后续 | `` `N-21` `` |
| 文档内编号 | **必须** `` `docs/子系统设计/网络Socket监听生命周期设计说明.md` DR-003 `` |
| 文档内编号 | **必须** `` `docs/网络/网络自愈架构设计.md` DR-003 `` |
| 尚未创建的条目文件 | 加 `[计划] ` 前缀(`docs/文档规范.md` §3.3) |
| 引用报错样例/历史字面 | 该行加 `[示例]` 标记 |
@@ -179,6 +183,16 @@
- 号段用尽 → 扩位(`N-99`→`N-100`),**不换字母**。
- 历史文档(审计报告、评审报告、已闭环陷阱)不回改,走别名表。
### 5.6 编号密集文档的对照节(强制)
一篇文档若引用 **≥3 个编号族**(如 `I*` + `E*` + `F-*` + `M*` + `U*` + `TRAP-*`),**必须**在正文之前
加一节「导读:本文用到的编号与术语」——每个记号一行,给出「是什么 + 在哪查」,让读者不必跳出本文拼语义。
- **首次实践**:`docs/网络/网络自愈架构设计.md` 的 `## 导读:本文用到的编号与术语`。
- 对**已封闭 / 已归档**的编号族尤其必要:属主登记表可能已删行(`N-30`~`N-33` 销项、`E0`~`E15` 随审计归档),
此时该文档即其**唯一可查处**。
- 与 P-7 配套:**正文引用带语义括注**(局部可读)+ **对照节给编号族总览**(全局可查),两者合起来才算看得懂。
---
## 6. 模块码表(`TC` / `TM` / `DP` / `TRAP` 共用)
@@ -209,7 +223,7 @@
| 旧族 | 新族 | 规则 |
|---|---|---|
| 登记簿 `N-2`…`N-33` | `N-02`…`N-33` | 仅补零,数字不变 |
| 登记簿 `N-2`…`N-33`(`N-30`~`N-33` 已销项,现存至 `N-29`) | `N-02`…`N-33` | 仅补零,数字不变;号不回收,游标仍 `N-34` |
| 登记簿 `S-3/S-4/S-5`、`T-7` | `N-34`… | 续号并入,别名表保留旧号 |
| 主线 `B0~B10`、结构债 `C1~C6`、验证窗口 `D2~D7`、`W1-0` | `WI-001`… | 按波次顺序连续重编,映射表逐条列出并落别名表 |
| 复审 `S1~S14`、`A-1~A-7`、报告 `R-x`/`F-x`/`U1~U5` 未闭环项 | `WI-*` | 同上;已闭环项留历史文档 |
@@ -236,7 +250,7 @@
| 孤儿 | 处置 |
|---|---|
| `I-1`/`I-2`(`ui_net_sink.c`) | 在 `docs/子系统设计/网络Socket监听生命周期设计说明.md` 补定义行,或就地展开成事实描述 |
| `I-1`/`I-2`(`ui_net_sink.c`) | 就地展开成事实描述(原属主《网络Socket监听生命周期设计说明》已并入 `docs/网络/网络自愈架构设计.md` §9.3,不再补编号定义行) |
| `Task #32` 会话号及其小节引用 | 会话号,改写为可解析引用(结论已沉淀进 `docs/子系统设计/看门狗设计说明.md` §11) |
| `P1-01~P1-09`(`ch395f_test_task.c`) | 改指 `TC-NET-101`…,或补文档条目 |
| `TC-GD5F-1100/1101/1102`、`TC-STO-06` | 补进 `docs/测试说明/GD5F2GQ5UE_Test_Guide.md` |
@@ -311,4 +325,5 @@ git grep -n '<旧字面量>' # 零残留
| 日期 | 变更 | 说明 |
|---|---|---|
| 2026-10-14 | 新增 P-7 与引用可读性规则 | §2 加 **P-7「编号不语义,引用必语义」**(编号本体保持无语义并冻结,但正文引用必须带语义括注、禁裸 ID 独担信息)→ 六条原则变**七条**;§5.1 加前置步"先问能不能不加号";新增 **§5.6「编号密集文档的对照节(强制)」**(≥3 个编号族必须加「导读:编号与术语」);§4.1 补登记 **`E\d+`**(N34 自愈实验编号族,已封闭)并把 `N-\d{2}` 的在册范围订正为「曾用最大 `N-33`,现存至 `N-29`」;§1.2 悬空引用示例补 `N-32`;§7.1 同步订正。起因:`docs/网络/网络自愈架构设计.md` 合并与导读节实践暴露"裸编号引用不可读",用户裁定以后尽量用语义标识 |
| (规划稿) | 建立本文档 | 全库扫描 1,385 个不同编号 / 19,306 处出现后定稿的两层编号体系(全局编号 + 文档内编号)、六条原则、目标前缀总表、三批迁移映射与 `check_ids.py` 门禁设计;用户决策:`E-xx` 冻结保留、严重度档位词 `P0~P3` 不变、文档落 `docs/编号规范.md`、阶段 1 三批全做 |
+12 -9
View File
@@ -158,7 +158,7 @@ sequenceDiagram
| **`netTask` 外(任意应用任务)** | 公共 API:`net_send()` / `net_recv()` / `net_recv_to()` / `net_accept()` / `net_connect()` / `net_close()` / `net_listen()` / `net_drop_conn()`——请求入队列,netTask 串行执行,调用方阻塞等回执。**自愈无显式 API**(自愈收敛阶段1:`net_relisten()` / `net_resync_listen()` 已退役,静默回收/监听失位由对账⑤与 reconciler 形态③④自动认领,见 §3.6) |
| **`netTask` 内** | 内部 API `net_send_sock()` / `net_recv_sock()` / `net_close_sock()` / `net_listen_locked()` |
| **`net_socket()` / `net_bind()`** | 直接调用,无需队列;实际在就绪屏障后的服务任务(timeSyncTask / ftpTask / uiTask)中创建监听 |
| **`net_poll()` 和 `net_process_messages()`** | 必须在 `netTask` 主循环调用;`net_process_messages()` 每轮先跑 **3 个非阻塞 tick**(D10 重监听重试 → L3 挂起 recv 完成 → TRAP-NET-17 对账+监听重建合一 reconciler 100ms),再处理队列 |
| **`net_poll()` 和 `net_process_messages()`** | 必须在 `netTask` 主循环调用;`net_process_messages()` 每轮先跑 **4 个非阻塞 tick**(D10 重监听重试 → L3 挂起 recv 完成 → L3 挂起 send 推进 → TRAP-NET-17 对账+监听重建合一 reconciler 100ms),再处理队列 |
### 3.4 内部 API 说明
@@ -178,7 +178,8 @@ int net_close_sock(net_sock_t *p_sock);
- **`netTask` 不可向自身发消息**:`netTask` 中直接调 `net_send()` 会死锁(等待自己处理消息),必须在 `netTask` 中使用 `net_send_sock()` 等内部版本
- **阻塞 recv 不再独占 netTask(L3 pending 化,2026-09-22)**:无数据时公共 `net_recv()` / `net_recv_to()` **登记挂起请求后返回**(每 fd 至多一个,禁止覆盖——覆盖即丢失前调用方通知 = 永久挂死);完成由 `net_pending_tick()` 按主循环节拍检查(①数据 ②到点[仅显式预算] ③断连)并经任务通知唤醒调用方——等待期间调用方在自己上下文睡通知,netTask 零占用(消除 E-02 头阻塞)。副作用:`buf` 指针须保持有效至返回;close / 网络重建 / 陈旧重同步会**显式取消并通知**(`NET_ERR_CONNRESET` 等),调用方永不挂死
- **recv 语义主流化(自愈收敛阶段 1.5,2026-10-13)**:对齐 POSIX/Python——**模式归使用者、超时归调用方、API 不强加默认预算**。`net_recv()` 默认**无限阻塞**(`NET_RECV_TIMEOUT_MS` 30s 默认已删),唤醒源 = 数据 / EOF / close / 网络重建(CONNRESET),会话死亡由对账① ~400ms 判死兜底;非阻塞具名入口 `net_recv_nb()`(原 `NET_MSG_DONTWAIT` 路径转正),模式属性 `net_setblocking(fd, on)` 挂 socket 控制块;超时首选 `net_select(...)`,`net_recv_to(...)` 保留为**显式预算**便捷入口。内部看门狗降级为 **tripwire**:挂起超 60s 只 `[ERR]` 留痕一条(完成逻辑缺位探针),不再伪造 TIMEDOUT 返回值。⚠ **文档契约(嵌入式特有)**:IWDG `required_mask` 内任务**禁止无限阻塞 recv**——必须 `net_select` 超时或 `net_recv_to` 显式预算("超时归使用者"对受监控任务是义务不是选项)
- **队列阻塞语义**:只有 `osMessageQueuePut` 失败(8 长度固定队列,极端情形)时公共 API 阻塞;recv 挂起等待**不计入**队列占用(完成前队列中无消息滞留)
- **阻塞 send 不再独占 netTask(L3 send pending 化,2026-10-14)**:`net_send()` 快路径**有界自旋**等发送缓冲腾出(预算 `NET_SEND_SPIN_MS` = 2ms;健康链路实测块间等待 med 219µs ≪ 2ms,故正常发送不会触发挂起),超预算即登记挂起 send 返回;推进由 `net_send_pending_tick()` 按主循环节拍分块完成(①断连 ②推进 ③写完 ④无进展超时),调用方睡任务通知——netTask 最坏停滞由 `NET_SEND_TIMEOUT_MS`(1s)压到 2ms。返回值恒为**已写入字节数**:`= len` 全部写入,`< len` 为**短写**,调用方**必须**检查并重试剩余(断连/超时/取消均以已发字节数结束,不会伪造全部成功)。停滞计时按**无进展**刷新(每次成功写块重置),大文件传输不会被误判超时
- **队列阻塞语义**:只有 `osMessageQueuePut` 失败(8 长度固定队列,极端情形)时公共 API 阻塞;recv / send 挂起等待**不计入**队列占用(完成前队列中无消息滞留)
- **哪些操作要求 netTask 已运行**:`net_listen()` / `net_accept()` / `net_connect()` / `net_send()` / `net_recv()` / `net_close()` 全经消息队列 → 只能在就绪屏障后的任务调用;`net_socket()` / `net_bind()` 为直接调用,但同样建议屏障后;**`net_listener_armed()` / `net_can_read()` 是纯 RAM 读控制块**(§10.1),不入队、不等回执,**任意任务可安全周期调用**(用途 = 门控,把"无谓的 RPC"降为 0)
### 3.6 芯片/本层状态对账与重监听自愈(TRAP-NET-09/-17/-18)
@@ -188,7 +189,7 @@ int net_close_sock(net_sock_t *p_sock);
| 机制 | 触发 | 动作 | 关键参数 |
|---|---|---|---|
| **`auto_relisten`** | 数据通道正常 EOF / DISCONNECT | 重开 + 重新 LISTEN,回 LISTENING;应用层无需 close/重 listen | `net_listen()` 置位;`net_close()` 清 |
| **D10 非阻塞重监听重试** | 重开失败(TRAP-NET-09:芯片 close 后需沉降) | 单次尝试失败即登记待重试,每轮到点重试;超时限放弃并记 `relisten give up` | 重试间隔 30ms / 时限 2s(自首次尝试);同 fd 有在途挂起 recv 时跳过(L3 保险,防毁在途连接) |
| **D10 非阻塞重监听重试** | 重开失败(TRAP-NET-09:芯片 close 后需沉降) | 单次尝试失败即登记待重试,每轮到点重试;超时限放弃并记 `relisten give up` | 重试间隔 30ms / 时限 2s(自首次尝试);同 fd 有在途挂起 recv / send 时跳过(L3 保险,防毁在途连接 / 截断在途传输) |
| **TRAP-NET-17 周期对账 + 监听重建(合一 `net_reconcile_tick`,自愈收敛阶段 3;对账①②⑤ + 原 pump③④/U11)** | 芯片 CONNECT/DISCONNECT 事件未收到(快速复连,读清消费竞态——E14 复核:非芯片侧丢失);静默回收 Socket(peer FIN,无 DISCONNECT 事件);监听型 fd 芯片监听失位 / 本层状态滞后 | 每 100ms 对在用 socket 读**驱动状态缓存**(`ch395f_sock_state_cached`,唯一刷新点 = `net_poll` 每轮全量刷新,E2 实测单次回读 18.1µs),修正**五类**失同步 + 一类被动重同步:① 本层 ESTABLISHED 而芯片已断 → 按断开处理(只置事实);② 本层 LISTENING 而芯片已握手 → 补 accept(N-30 幽灵甄别);③ 本层 LISTENING 而芯片监听失位 → 重建(`net_listener_try`);④ 本层 CLOSED(监听型 fd)而芯片不在 LISTEN → 重建;⑤ 本层 ESTABLISHED 而芯片已静默回收回 LISTEN(0x05/0x01)→ RESYNC 语义只修本层、不发芯片命令(自愈收敛阶段1 新增,原手动 API `net_resync_listen` 的语义并入);U11 本层 CLOSED 而芯片**正在** LISTEN → 只做状态重同步、不发芯片命令 | 周期 100ms;**分级去抖**:②=1 轮、③④⑤=2 轮、①=4 轮(M3 让位窗 400ms,**勿改小**)+ 动手后 **1s 冷却**(防"刚 open 未生效"瞬态重建风暴;U11 被动重同步不去抖、不占冷却);其它位置只置事实、不做重建动作 |
| **TRAP-NET-18 排渣隔离** | 异常释放(芯片 TIMEOUT,如客户端中止上传):在途数据落进 RX 缓冲,直接复监听 → 下条连接把 ~2048B 残渣当自己的数据读走(上传静默截断);正常 FIN 释放亦有残留 DISCONNECT 收尾问题 | 复监听前沉降/隔离:持续排空 RX 残渣 + 主动 close + 排空中断,等芯片回 CLOSED/TCP_CLOSED 才 open+listen;到期强制放行 | 沉降等级(`net_listener_settle`):异常释放 QUARANTINE **400ms** / 正常释放 GRACEFUL **100ms**(2026-09-24 起正常 FIN 亦需沉降);上限 **800ms** 强制放行(**必须 < 2s 时限**,否则监听被放弃) |
@@ -247,7 +248,7 @@ int main(void)
- 更新所有 Socket 的连接状态(CONNECT / DISCONNECT / RECV / TIMEOUT)
- 触发事件回调
- 处理数据接收
- `net_process_messages()` 每轮先跑 3 个**非阻塞**检查再取队列:D10 重监听重试(断连后监听器恢复)→ L3 挂起 recv 完成检查(阻塞 recv 唤醒,E-02 修复)→ TRAP-NET-17 芯片/本层对账 + 监听重建合一 reconciler(100ms 周期,读驱动状态缓存,事件未收到/静默回收/监听失位自愈,§3.6)
- `net_process_messages()` 每轮先跑 4 个**非阻塞**检查再取队列:D10 重监听重试(断连后监听器恢复)→ L3 挂起 recv 完成检查(阻塞 recv 唤醒,E-02 修复)→ L3 挂起 send 推进(阻塞 send 唤醒)→ TRAP-NET-17 芯片/本层对账 + 监听重建合一 reconciler(100ms 周期,读驱动状态缓存,事件未收到/静默回收/监听失位自愈,§3.6)
- **监听 Socket 建立**:由就绪屏障后的服务任务(timeSyncTask / ftpTask / uiTask)经队列 API 建立(`net_socket` → `net_bind` → `net_listen`);断连后 net 层自动重监听(§3.6),服务层只需循环 `net_accept`,无需维护重监听逻辑
```c
@@ -1054,11 +1055,12 @@ Socket 已创建或已绑定时可用;port 为 0(动态分配)时返回已
void net_process_messages(void);
```
处理其他任务投递的请求(send/recv/connect/accept/close/drop_conn)。每次调用先跑 3 个**非阻塞** tick,再取队列消息(0 超时出队):
处理其他任务投递的请求(send/recv/connect/accept/close/drop_conn)。每次调用先跑 4 个**非阻塞** tick,再取队列消息(0 超时出队):
1. **D10 重监听重试**(`net_listener_retry_tick`):断连后监听器恢复的到点重试(30ms 间隔 / 2s 时限,§3.6)
2. **L3 挂起 recv 完成检查**(`net_pending_tick`):按 ①数据 ②到点(仅显式预算) ③断连 检查每个挂起请求并通知调用方;④ 挂起超时留痕探针(编译期选项,默认关):挂起超阈值只 `[ERR]` 留痕一次,不强制结束
3. **TRAP-NET-17 芯片/本层对账 + 监听重建(合一 `net_reconcile_tick`)**:100ms 周期,判据读驱动状态缓存(`ch395f_sock_state_cached`),修正**五类**失同步(①②③④⑤)+ U11 被动重同步——原 200ms 对账与 100ms 监听器 pump 已合并为单一 tick,**唯一重建入口**=形态③④(§3.6)
3. **L3 挂起 send 推进**(`net_send_pending_tick`):按 ①断连 ②推进(每轮至多一块 `NET_SEND_CHUNK_MAX`) ③写完 ④无进展超时 推进挂起 send 并通知调用方(结果恒为已发字节数,短写由调用方检查)
4. **TRAP-NET-17 芯片/本层对账 + 监听重建(合一 `net_reconcile_tick`)**:100ms 周期,判据读驱动状态缓存(`ch395f_sock_state_cached`),修正**五类**失同步(①②③④⑤)+ U11 被动重同步——原 200ms 对账与 100ms 监听器 pump 已合并为单一 tick,**唯一重建入口**=形态③④(§3.6)
> `net_msg_t.timeout_ms`:L3 显式预算(`net_recv_to()`),0=**无限等待**(自愈收敛阶段 1.5 删除 `NET_RECV_TIMEOUT_MS` 30s 默认预算——超时归调用方)。消息类型 7 种(send/recv/close/connect/listen/accept/drop_conn)——自愈类消息 `NET_MSG_RESYNC_LISTEN` / `NET_MSG_RELISTEN` 已随 API 退役删除(自愈收敛阶段1)。
@@ -1275,7 +1277,7 @@ static int read_time_frame(int conn_sock, uint8_t buf[FRAME_LEN]) {
**是的**,且 `net_poll()` 与 `net_process_messages()` 缺一不可:
- `net_poll()`:GPIO 轮询 INT#,**每轮批处理一批**中断,更新各 Socket 状态、触发事件回调(RECV 为电平触发,每轮只处理一批,避免饿死主循环)。
- `net_process_messages()`:除串行处理队列请求外,每轮先跑 3 个**非阻塞**检查——D10 重监听重试(断连后监听器恢复)→ L3 挂起 recv 完成检查(阻塞 recv 的唤醒)→ TRAP-NET-17 芯片/本层对账 + 监听重建合一 reconciler(100ms 周期,读驱动状态缓存,事件未收到/静默回收/监听失位自愈,§3.6)。
- `net_process_messages()`:除串行处理队列请求外,每轮先跑 4 个**非阻塞**检查——D10 重监听重试(断连后监听器恢复)→ L3 挂起 recv 完成检查(阻塞 recv 的唤醒)→ L3 挂起 send 推进(阻塞 send 的唤醒)→ TRAP-NET-17 芯片/本层对账 + 监听重建合一 reconciler(100ms 周期,读驱动状态缓存,事件未收到/静默回收/监听失位自愈,§3.6)。
建议主循环周期 ≤ 10ms(本项目 1ms)。若 netTask 主循环停摆(断点、任务被挂起),监听器不会恢复、阻塞 recv 不会完成——这是软件侧"监听死机"的最常见原因。
@@ -1458,10 +1460,11 @@ CH395F 底层驱动返回的状态码(`ch395f_get_cmd_status()` 或 `ch395f_po
| 日期 | 变更 |
|------|------|
| 2026-10-14 | **二期 SEND pending 化**(架构见 `docs/网络/Net层阻塞模型架构设计.md` §4.5/§5.5/§7):`net_send` 快路径改为**有界自旋**(`NET_SEND_SPIN_MS` = 2ms,健康链路块间等待 med 219µs ≪ 2ms,行为不变),超预算登记挂起 send,由新增 `net_send_pending_tick` 按主循环节拍分块推进(①断连 ②推进 ③写完 ④无进展超时),调用方睡任务通知——netTask 最坏停滞由 `NET_SEND_TIMEOUT_MS`(1s)压到 2ms。返回值恒为**已写入字节数**(短写须检查);`NET_SEND_TIMEOUT_MS` 降级为**无进展**超时(每次成功写块刷新)。主循环 tick 由 3 个增至 **4 个**(新增 `net_send_pending_tick`,与 `net_pending_tick` 并列);§3.3 / §3.5 / §3.6 / §10.2 / §12 同步 |
| 2026-10-13 | **自愈收敛阶段 3 自愈合并 + E-02 收口**(实施规格现沉淀 `docs/网络/网络自愈架构设计.md` §4.1):`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/网络/网络自愈架构设计.md` §5.2 E-02) |
| 2026-10-13 | **自愈收敛阶段 1.5 recv 语义主流化**(规格现沉淀 `docs/网络/网络自愈架构设计.md` §4.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 | **自愈收敛阶段1 API 面收敛**(审计过程已归档 `docs/网络/网络自愈架构设计.md` §9,现状见其 §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-13 | **自愈收敛阶段1 API 面收敛**(审计过程已归档 `docs/网络/网络自愈架构设计.md` §12,现状见其 §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/网络/网络自愈架构设计.md` §11 |
| 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 结案) |
| 2026-09-21 | **修正公开 API 的错误码回传**:`net_msg_t` 按值拷入队列 → netTask 对消息的写入调用方不可见,改用 `xTaskNotify()` 通知值回传具体错误码(负值),公开 API 恢复 errno 后统一返回 -1(此前被覆盖成 `NET_ERR`,使 `net_get_errno() != NET_ERR_WOULDBLOCK` 的"空轮询"判断恒成立 → `ui_net_sink` 连上即 `net_close`);§12.4 补单连接模型断开处理(EOF 时不得 `net_close` 监听 socket);§12.9 补错误码回传约束 |
@@ -5,10 +5,10 @@
>
> **范围**:MCU 端 `Drivers/BSP/NET/net_socket.c`(约 3660 行)+ 其拆出的
> `Drivers/BSP/NET/net_listener.c`(约 550 行,**监听生命周期**,N-32 §10 模块化)+
> `Drivers/BSP/NET/net_pending.c`(约 390 行,**挂起 recv 请求表**,2026-10-14 拆出)。
> `Drivers/BSP/NET/net_pending.c`(约 690 行,**挂起 recv / send 请求表**;send 组为 2026-10-14 二期新增)。
> 模拟器端后端 `simulator/sim/net_socket_sim.c`(Winsock)是另一套实现,见 `docs/网络/BSD_Socket_API_使用指南.md` §2.3;`net_select.c` 不在此展开,见 `docs/网络/BSD_Socket_API_使用指南.md` §7 / §10.5。
> `net_listener.c` / `net_pending.c` 真机专属(模拟器不编译);设计依据分别见
> `docs/子系统设计/网络Socket监听生命周期设计说明.md` §10 与
> `docs/网络/网络自愈架构设计.md` §11 与
> `docs/网络/Net层阻塞模型架构设计.md`。
>
> **交叉引用约定**:机制标 Trap 号 → `docs/问题记录/CH395F_Trap_Records.md`;用法语义引用使用指南 §;关键参数值与 `net_config.h` / `net_socket.c` 内宏一致。
@@ -25,7 +25,7 @@
| 执行域 | 运行位置 / 节奏 | 允许的操作 | 典型调用者 |
|---|---|---|---|
| **netTask 主循环** | 独立 FreeRTOS 任务,1ms/tick | **唯一**可碰芯片(SPI2)的地方;3 个非阻塞 tick(D10 重试 / L3 挂起 / TRAP-NET-17 对账+监听重建合一 reconciler);队列分派;事件处理器;用户回调 | `net_poll` / `net_process_messages` 及其下游全部 `_locked` / `_tick` / `handle_*` |
| **netTask 主循环** | 独立 FreeRTOS 任务,1ms/tick | **唯一**可碰芯片(SPI2)的地方;4 个非阻塞 tick(D10 重试 / L3 挂起 recv / L3 挂起 send / TRAP-NET-17 对账+监听重建合一 reconciler);队列分派;事件处理器;用户回调 | `net_poll` / `net_process_messages` 及其下游全部 `_locked` / `_tick` / `handle_*` |
| **应用任务侧** | ftpTask / uiTask / timeSyncTask / testTask 等 | 经队列的公共 API(入队 + `xTaskNotifyWait` 等回执);纯 RAM 直调 API;**禁止直碰芯片** | 各服务任务 |
| **直调快路径** | 同步调用,不经过队列 | `net_socket` / 查询族 / 回调注册(纯 RAM);**直碰 SPI 的三例外**:`net_bind`(UDP) / `net_sendto` / `net_recvfrom` | 初始化阶段、netTask 上下文、测试套件(硬件层测试模式下 netTask 自挂起,testTask 独占芯片) |
@@ -48,16 +48,17 @@ flowchart TB
XN["xTaskNotify / xTaskNotifyWait<br/>(结果回传的唯一通道)"]
subgraph NT["netTask 主循环 — 1ms/tick;芯片操作唯一执行者"]
NPM["netProcessMessages<br/>先跑 3 个 tick,再排空队列分派"]
NPM["netProcessMessages<br/>先跑 4 个 tick,再排空队列分派"]
NP["netPoll<br/>INT# GPIO 轮询 → 全局中断 → 各 Socket SINT → 状态缓存全量刷新 + sock_reset 步进 → 每轮一批"]
T1["① netListenerRetryTick<br/>D10 重监听重试:30ms / 2s 时限"]
T2["② netPendingTick<br/>L3 挂起 recv:①数据 ②到点(仅显式预算) ③断连 ④tripwire 60s 只留痕"]
T3["③ netReconcileTick<br/>TRAP-NET-17 对账 + 监听重建合一(自愈收敛阶段 3):100ms / 分级去抖 / 冷却<br/>读驱动状态缓存(①②③④⑤ + U11)"]
T2B["③ netSendPendingTick<br/>L3 挂起 send:①断连 ②推进(每轮≤1块) ③写完 ④无进展超时"]
T3["④ netReconcileTick<br/>TRAP-NET-17 对账 + 监听重建合一(自愈收敛阶段 3):100ms / 分级去抖 / 冷却<br/>读驱动状态缓存(①②③④⑤ + U11)"]
DISP["队列分派 switch<br/>SEND · RECV · CLOSE<br/>CONNECT · LISTEN · ACCEPT · DROP_CONN"]
subgraph LK["_locked / 内部分派目标(仅 netTask 上下文)"]
LL["netListenLocked<br/>初次监听(open 重试 3× + sock_reset 排渣)"]
LN["netConnectLocked<br/>open 重试 3×(sock_reset 排渣)+ tcpConnect"]
LS["netSendLocked<br/>分块 ≤1KB + sendReady 门控"]
LS["netSendLocked<br/>分块 ≤1KB + sendReady 门控;超 NET_SEND_SPIN_MS 登记挂起 send"]
LR["netRecvLocked<br/>快路径 / DONTWAIT / 登记挂起"]
LA["netAcceptLocked<br/>先 netPoll + 状态判定"]
LC["netCloseLocked<br/>结束挂起 + cmd_done 快路径 / sock_reset(GRACEFUL)"]
@@ -67,14 +68,15 @@ flowchart TB
EV["事件处理器<br/>handleConnect · handleDisconnect<br/>handleRecv · handleSend · handleTimeout"]
ST["netListenerSettle<br/>只置沉降/隔离事实(100ms / 400ms)"]
FE["fireEvent → 用户回调"]
PR["s_net_pending<br/>(每 fd 至多一个挂起)"]
PR["s_net_pending<br/>(每 fd 至多一个挂起 recv)"]
SR["s_net_send<br/>(每 fd 至多一个挂起 send)"]
CH[("ch395f.* 驱动<br/>SPI2 串行事务")]
end
PUB -->|入队| MQ
MQ --> DISP
DISP --> LL & LN & LS & LR & LA & LC & DC & RLS
NPM --> T1 & T2 & T3 & NP
NPM --> T1 & T2 & T2B & T3 & NP
T1 --> RLS
T3 -->|"① 已断"| EV
T3 -->|"② 补 accept"| EV
@@ -89,6 +91,9 @@ flowchart TB
LR -->|无数据+阻塞| PR
PR --> T2
T2 -->|xTaskNotify| XN
LS -->|等发送缓冲超预算| SR
SR --> T2B
T2B -->|xTaskNotify| XN
XN -->|唤醒| PUB
NP --> CH
LL & LN & LS & LR & LA & LC & DC & RLS --> CH
@@ -98,11 +103,11 @@ flowchart TB
**读图要点**:
1. 应用侧只有两种"等":① 入队后 `xTaskNotifyWait` 等回执(同步语义);② 挂起 recv 的等待(同一通道,只是"完成"由 `netPendingTick` 判定,不由 netTask 立即处理)。
2. `net_process_messages()` 开头的 **3 个 tick 是网络层自愈主干**:没有任何应用消息时,重试 / 挂起完成 / 状态对账 + 监听重建照常运转——"断连后即使没人发消息,监听器也能自己活过来"靠的就是它们。
1. 应用侧只有两种"等":① 入队后 `xTaskNotifyWait` 等回执(同步语义);② 挂起 recv / send 的等待(同一通道,只是"完成"由 `netPendingTick` / `netSendPendingTick` 判定,不由 netTask 立即处理)。
2. `net_process_messages()` 开头的 **4 个 tick 是网络层自愈主干**:没有任何应用消息时,重试 / 挂起 recv 完成 / 挂起 send 推进 / 状态对账 + 监听重建照常运转——"断连后即使没人发消息,监听器也能自己活过来"靠的就是它们。
3. 事件处理器**只由 `net_poll` 驱动**,例外是 `net_reconcile_tick`(分级去抖 + 1s 冷却)可再入 `handle_disconnect_event` / `handle_connect_event`——对账是事件未收到的自愈通道(TRAP-NET-17)。**注意职责边界(N-32 P2,自愈收敛阶段 3 合并后)**:对账与事件处理器**只置事实**(置 CLOSED / 登记沉降),**重建监听的动作统一由 `net_reconcile_tick` 形态③④ 做**(本层唯一重建入口)——"谁先看到 DISCONNECT 就谁重建"的竞争在结构上消失。
4. 回调 `event_cb` 在 **netTask 上下文**执行:回调内不得阻塞、不得发网络操作。
5. **模块归属**:`T1`(D10 重试)/ `RLS` 的 `net_listener_try` / `ST`(settle)在 `net_listener.c`(真机专属、模拟器不编译);挂起 recv 完成检查(`net_pending_tick`)与挂起表在 `net_pending.c`(真机专属);其余 tick(含合并后的 `T3` reconciler)与 `_locked` 在 `net_socket.c`。
5. **模块归属**:`T1`(D10 重试)/ `RLS` 的 `net_listener_try` / `ST`(settle)在 `net_listener.c`(真机专属、模拟器不编译);挂起 recv 完成检查(`net_pending_tick`)、挂起 send 推进(`net_send_pending_tick`)与两张挂起表(`PR` / `SR`)在 `net_pending.c`(真机专属);其余 tick(含合并后的 `T3` reconciler)与 `_locked` 在 `net_socket.c`。
---
@@ -121,7 +126,7 @@ flowchart TB
| 函数 | 设计作用 | 主要调用 | 意图 / 注意 |
|---|---|---|---|
| `net_poll()` | 事件泵:GPIO 轮询 INT#(低=有待处理)→ `GET_GLOB_INT_STATUS_ALL`(读=清)→ 按位图逐 Socket 读 SINT → 分派 5 类事件;**每轮末尾**对在用 Socket 全量刷新**驱动状态缓存** + 步进 `ch395f_sock_reset_poll`(自愈收敛阶段 3) | `HAL_GPIO_ReadPin` → `ch395f_get_glob_int_status_all` → `ch395f_get_sock_int_status` ×n → `handle_connect/disconnect/recv/send/timeout_event` → `ch395f_get_sock_state` ×n(刷新缓存)+ `ch395f_sock_reset_poll` ×n | **RECV 中断是电平触发**:每次调用**只处理一批**(do-while(0)),处理完立即返回,否则主循环(echo/应用逻辑)饿死(TRAP-NET-08);SENBUF_FREE 只置 `send_ready` 不记事件;状态缓存**唯一刷新点**在此(reconciler 只读缓存,E2 实测单次回读 18.1µs);`sock_reset_poll` 步进使同步包装 600ms 放弃后的复位机不搁浅(IDLE 槽纯 RAM 判零 SPI) |
| `net_process_messages()` | 队列入口 + 自愈 tick 宿主:每轮**先**跑 3 个非阻塞 tick,再排空队列逐条分派,结果 `xTaskNotify` 回传 | `net_listener_retry_tick` → `net_pending_tick` → `net_reconcile_tick`(对账+重建合一)→ `osMessageQueueGet`(0 超时) ×n → switch 分派 → `xTaskNotify` | ① tick 放在队列**之前**:断连后即使没有任何应用消息,监听器也能自主恢复、挂起 recv 也能完成;② RECV 若返回 `NET_PENDING_REGISTERED`(哨兵 0x70000000)则**不发即时回执**(`do_notify=0`),完成时才通知;③ 失败结果把 `s_net_errno`(具体负错误码)压入通知值——调用方恢复 errno 后对外统一 -1(BSD 语义);④ 通知值**整个 32 位**都是结果(不压位域标签,否则 32768 这类大字节数会溢出为负);⑤ **对账①置 CLOSED 后形态③④同轮即可重建**(自愈收敛阶段 3 合并,原"pump 置于对账之后"的顺序依赖在单 tick 内自然成立) |
| `net_process_messages()` | 队列入口 + 自愈 tick 宿主:每轮**先**跑 4 个非阻塞 tick,再排空队列逐条分派,结果 `xTaskNotify` 回传 | `net_listener_retry_tick` → `net_pending_tick` → `net_send_pending_tick` → `net_reconcile_tick`(对账+重建合一)→ `osMessageQueueGet`(0 超时) ×n → switch 分派 → `xTaskNotify` | ① tick 放在队列**之前**:断连后即使没有任何应用消息,监听器也能自主恢复、挂起 recv / send 也能完成;② RECV 若返回 `NET_PENDING_REGISTERED`(哨兵 0x70000000)则**不发即时回执**(`do_notify=0`),完成时才通知;③ 失败结果把 `s_net_errno`(具体负错误码)压入通知值——调用方恢复 errno 后对外统一 -1(BSD 语义);④ 通知值**整个 32 位**都是结果(不压位域标签,否则 32768 这类大字节数会溢出为负);⑤ **对账①置 CLOSED 后形态③④同轮即可重建**(自愈收敛阶段 3 合并,原"pump 置于对账之后"的顺序依赖在单 tick 内自然成立) |
### 4.3 经队列的公共 API 及其 netTask 侧实现
@@ -132,7 +137,7 @@ flowchart TB
| `net_listen` | `NET_MSG_LISTEN` | `net_listen_locked` | `set_proto_type` + `set_sour_port` + `open_socket`(**open 重试 3×,失败走 `net_sock_reset_sync` 确定性排渣**,自愈收敛阶段 3)+ `tcp_listen` + 清**本 Socket** 中断状态 | 0 / -1 | **初次**监听允许有界重试(启动期无竞争);成功后置 `auto_relisten=1`——监听常驻的源头;启动前只清本 Socket SINT(B3/F-NET-7:全局寄存器唯一读者 = `net_poll`,全量读清会焊死其它 Socket 事件位);open 失败重试原"close+盲等 500ms"已换 sock_reset(GRACEFUL 100ms 底,E4 定案复位后 open 必成) |
| `net_accept` | `NET_MSG_ACCEPT` | `net_accept_locked` | 无(先 `net_poll` 泵一次 + 状态判定) | 监听 fd 自身(单连接)/ -1(WOULDBLOCK) | 单连接:监听 Socket 在 CONNECT 后**自身即数据通道**,返回的就是它;CLOSED + auto_relisten 时触发**非阻塞** `net_listener_try`;**accept 路径不做自愈**——单次状态读会误判"刚 listen 未生效"致重建风暴(2026-09-23 实测),统一交给 `net_reconcile_tick` 形态③④(自愈收敛阶段 3 合并后的单一重建入口) |
| `net_connect` | `NET_MSG_CONNECT` | `net_connect_locked` | `set_proto_type` + `open_socket`(open 重试 3× + `net_sock_reset_sync` 排渣)+ 清残留中断(只清本 Socket SINT,`ch395f_sock_events`)+ `set_sour_port` / `set_des_ip`(大端数组)/ `set_des_port` + `tcp_connect` | 0 = 请求已发出(**异步**)/ -1 | open **先于**寄存器写入(open 会复位寄存器);open 后清残留 DISCONNECT,防新连接秒断(TRAP-NET-12);目的 IP 字节序写反 = CONNECT TIMEOUT(TRAP-NET-14);返回 0 ≠ 已建立——调用方以 `net_sock_state` 轮询等 ESTABLISHED |
| `net_send` | `NET_MSG_SEND` | `net_send_locked` | `ch395f_write_send_buf`(分块)+ 等 SENBUF_FREE(`net_poll` 轮询) | 实际字节数 / -1 | 分块 ≤ `NET_SEND_CHUNK_MAX` = 1KB(**硬件发送 FIFO 硬上限**,TRAP-NET-16:写穿 FIFO = 数据错位);`send_ready` 门控用 `net_poll` 轮询而非 osDelay;短写**必须**告警——静默截断会让对端永久等待剩余字节(2026-09-21 实证:UI 推流应答截断 → 客户端失联) |
| `net_send` | `NET_MSG_SEND` | `net_send_locked` | `ch395f_write_send_buf`(分块)+ 等 SENBUF_FREE(`net_poll` 轮询,**有界自旋 ≤ `NET_SEND_SPIN_MS`**);超预算登记挂起 send(`net_send_pending_register`) | 实际字节数 / -1 | 分块 ≤ `NET_SEND_CHUNK_MAX` = 1KB(**硬件发送 FIFO 硬上限**,TRAP-NET-16:写穿 FIFO = 数据错位);`send_ready` 门控用 `net_poll` 轮询而非 osDelay,**自旋超 `NET_SEND_SPIN_MS`(2ms)即转挂起**(netTask 最坏停滞由 1s 压到 2ms);短写**必须**告警——静默截断会让对端永久等待剩余字节(2026-09-21 实证:UI 推流应答截断 → 客户端失联) |
| `net_recv` | `NET_MSG_RECV`(timeout=0→**无限等待**,自愈收敛阶段 1.5 删 30s 默认) | `net_recv_locked` | `net_poll` + `get_recv_len` / `read_recv_buf` | >0 / 0(EOF) / -1(errno = WOULDBLOCK / CONNRESET;阻塞形态无 TIMEDOUT 一档) | 快路径:数据已在 → 立即读出返回(允许部分读);无数据 + 阻塞(`blocking` 属性=1 且未 DONTWAIT)→ **登记挂起后立即返回**(L3,netTask 零占用),唤醒源=数据/EOF/close/网络重建;DONTWAIT / `blocking=0` + CLOSED/session_dead → EOF + 触发重监听;⚠ IWDG 受监控任务禁用无限形态(`net_socket.h` 文档契约) |
| `net_recv_nb` | `NET_MSG_RECV`(flags=DONTWAIT) | `net_recv_locked` | 同上 | >0 / 0 / -1(WOULDBLOCK 等) | 原 `NET_MSG_DONTWAIT` 路径转正的具名入口(自愈收敛阶段 1.5);不改变 `blocking` 属性;受 IWDG 监控 / 有周期职责任务的推荐默认(ui_net_sink / iec104_serv 已迁) |
| `net_recv_to` | `NET_MSG_RECV`(timeout>0) | `net_recv_locked` | 同上 | >0 / 0 / -1 / **-2(本片超时无数据)** | E-02 头阻塞修复配套 + **显式预算**入口(自愈收敛阶段 1.5,等价 Python settimeout,无隐藏默认值):调用方把长等待切成有界片(如剩余预算),**总超时语义不变、单次占用有界**;`timeout_ms=0` 直接拒(0 = 无限等待,与本接口"有界"存在理由相反);超时**只凭 -2 判定、不得依赖 errno**(`s_net_errno` 是跨任务共享全局量,随时被别的任务覆盖) |
@@ -181,7 +186,7 @@ flowchart TB
| `net_listener_settle` | 登记"本 fd 会话已结束,重建前需沉降/隔离"(**只置事实**) | 写 `s_relisten_quarantine[fd]`(等级:GRACEFUL 100ms / QUARANTINE 400ms) | N-32 P1:取代此前外部直接写 `s_relisten_quarantine[]`;由 `handle_disconnect_event`(GRACEFUL)/ `handle_timeout_event`(QUARANTINE)调用。**只置事实,动作交 reconciler 形态③④ / `net_listener_try`**;越界 fd 静默忽略 |
| `net_listener_try` | 尝试重建一次;失败登记重试(**不阻塞**) | `net_relisten_locked` → 成功 `net_relisten_done` / 失败置 `s_relisten_wait/next/dead` | 已在重试队列 → 直接返回 -1(**不重置重试时刻**):否则 10ms 一次的 accept 轮询会不断把重试时刻往后推 → 永不重试(饥饿) |
| `net_relisten_locked` | 真正的 open + listen(**单次尝试**) | (沉降期)`net_drain_chip_rx` + `close_socket` + `get_sock_int_status` / `get_glob_int_status_all` + `get_socket_status` → `set_proto_type` + `set_sour_port` + `open_socket` + `tcp_listen` + 清中断 | ① **严禁 osDelay 忙等**:失败只清理并返回,沉降交给重试轮询;② **TRAP-NET-18 沉降门**:带沉降标记的 socket(异常释放 TIMEOUT → QUARANTINE 400ms;正常释放 DISCONNECT → GRACEFUL 100ms)→ 排空 RX 残渣 + 主动 close + 排空中断 + 等芯片回 CLOSED/TCP_CLOSED;**上限强制放行**(必须 < 2s 重试时限,否则沉降耗尽重试预算 → `relisten give up` 监听被弃——2026-09-23 实测修正);③ 成功只置 state=LISTENING,**不 memset**(fd 与 `auto_relisten` 保留——BSD 风格监听常驻) |
| `net_listener_retry_tick` | 每轮到点重试(D10) | `net_relisten_locked` → `net_relisten_done` / 顺延 / 放弃 | **同 fd 有在途挂起 recv 时跳过**(重开 Socket 会摧毁在途连接,L3 保险);到 2s 时限 → 清状态 + `relisten give up` 错误日志(应用可见) |
| `net_listener_retry_tick` | 每轮到点重试(D10) | `net_relisten_locked` → `net_relisten_done` / 顺延 / 放弃 | **同 fd 有在途挂起 recv / send 时跳过**(重开 Socket 会摧毁在途连接 / 截断在途传输,L3 保险);到 2s 时限 → 清状态 + `relisten give up` 错误日志(应用可见) |
| `net_relisten_done` | 标记完成 | `net_listener_clear` +(探针构建)恢复计时样本 | 恢复时延 = 首次尝试 → 完成(L3 探针 `relisten` 段) |
| `net_listener_clear` | 取消该 fd 的待重试/沉降 | 清 `wait/next/dead` + `settle_dead/settle_start` + `quarantine` | 触发点:`net_socket`(新 fd 不继承)/ `net_close_locked`(关闭即取消);沉降/隔离状态必须一并清,否则残留标记会污染下一次复监听 |
| `net_drain_chip_rx` | 排空芯片 RX 残留(256B 分块,≤16 次 = 4KB 上限) | `get_recv_len` + `read_recv_buf` | TRAP-NET-18:中止/异常断开时芯片 RX 缓冲残留上一条连接的数据(实测 ~2048B),不排空会被下一条连接当自己的数据读走(静默截断);分块读避免新增静态缓冲(内存预算紧张) |
@@ -265,12 +270,20 @@ sequenceDiagram
Note over A: net_wait_result:零等待拾取 → 无则阻塞
Q->>N: NET_MSG_SEND
N->>C: net_send_locked:write_send_buf(≤1KB/块)
loop 每块
N->>C: 等 SENBUF_FREE(net_poll 轮询,不睡)
alt 健康路径(块间等待 < NET_SEND_SPIN_MS=2ms)
loop 每块
N->>C: 等 SENBUF_FREE(net_poll 有界自旋,不睡)
end
else 超预算(对端零窗口 / 不读)
N->>N: net_send_pending_register(每 fd 至多 1;do_notify=0)
Note over A,N: netTask 立即解放;A 在自己上下文睡通知
loop 每个主循环节拍(1ms)
N->>C: net_send_pending_tick:①断连 ②推进 ③写完 ④无进展超时
end
end
C-->>N: SENBUF_FREE / SEND_OK(net_poll 置 send_ready)
N->>A: xTaskNotify(实际字节数 / 负错误码)
A->>A: 返回字节数 / -1(errno 已恢复)
A->>A: 返回字节数 / -1(errno 已恢复;短写按 ret < len 处理)
```
### 5.2 阻塞接收无数据(L3 pending 路径)
@@ -401,7 +414,7 @@ sequenceDiagram
| `ch395f_open_socket` / `ch395f_close_socket` | `net_bind`(UDP)、`net_listen_locked`、`net_connect_locked`、`net_relisten_locked`(含沉降期主动 close);`net_close_locked` 的 CLOSE 已改经 `ch395f_cmd_done` / `ch395f_sock_reset_*`(自愈收敛阶段 3) | TRAP-NET-09:close 后需沉降才能重开 |
| `ch395f_tcp_listen` / `ch395f_tcp_connect` | `net_listen_locked` / `net_relisten_locked`;`net_connect_locked` | — |
| `ch395f_set_proto_type` / `set_sour_port` / `set_des_ip` / `set_des_port` | `net_bind`(UDP)、`net_listen_locked`、`net_connect_locked`、`net_relisten_locked`、`net_sendto` | open 之后写端口/IP(open 会复位寄存器);set_des_ip 期望**大端数组** |
| `ch395f_write_send_buf` | `net_send_locked` / `net_sendto` | 单次 ≤1KB(硬件 FIFO 上限,TRAP-NET-16) |
| `ch395f_write_send_buf` | `net_send_locked` / `net_send_pending_tick`(二期挂起 send 推进)/ `net_sendto` | 单次 ≤1KB(硬件 FIFO 上限,TRAP-NET-16) |
| `ch395f_get_recv_len` / `ch395f_read_recv_buf` | `net_recv_locked`、`net_pending_tick`、`net_pending_settle_close`、`net_recvfrom`、`net_drain_chip_rx`、`handle_disconnect_event`(M3 可疑门 RX 非空判据) | `handle_recv_event` **故意不读**(防竞争,旧机理未证实) |
| `ch395f_get_socket_status` | `handle_disconnect_event`(两处甄别——事件路径**主动 SPI**,缓存必然滞后于刚到达的中断)、`net_relisten_locked`(沉降门)、`net_connect_locked`(诊断回读)。⚠ **周期对账不再直接调**——改读缓存(下行) | 输出 **2 字节**([0]=Socket 状态 [1]=TCP 状态),缓冲必须给 2 字节 |
| `ch395f_get_sock_state` / `ch395f_sock_state_cached` | 刷新**仅 `net_poll`** 每轮对在用 Socket 调 `get_sock_state`(唯一刷新点);`sock_state_cached` 读缓存 = `net_reconcile_tick`(形态①②③④⑤/U11 判据) | 自愈收敛阶段 3:芯片状态唯一事实源;E2 实测单次回读 18.1µs,缓存新鲜度 ≤1ms |
@@ -419,6 +432,7 @@ sequenceDiagram
|---|---|
| 监听恢复策略(节奏/时限/沉降) | `net_listener_try` / `net_relisten_locked` / `net_listener_retry_tick`;常量 `NET_RELISTEN_*_MS`(`net_listener.c` 内);重建决策在 `net_reconcile_tick` 形态③④(下行) |
| 阻塞接收等待语义 | `net_pending_register` / `net_pending_tick` / `net_pending_complete`;常量 `NET_PENDING_WATCHDOG_MS` |
| 阻塞发送等待语义(二期 SEND pending) | `net_send_pending_register` / `net_send_pending_tick` / `net_send_pending_complete`;常量 `NET_SEND_SPIN_MS`(有界自旋预算)/ `NET_SEND_TIMEOUT_MS`(无进展超时)/ `NET_SEND_CHUNK_MAX`(分块上限,`net_pending.h` 唯一定义处) |
| 事件未收到自愈 + 监听重建策略(合一 tick,自愈收敛阶段 3) | `net_reconcile_tick`(`NET_RECONCILE_PERIOD_MS` / `NET_RECONCILE_COOLDOWN_MS` / `NET_RECON_STABLE_ACCEPT|RELISTEN|DEATH`,①②③④⑤+U11,唯一重建入口=形态③④)+ `handle_disconnect_event` / `handle_connect_event` |
| 新增一个公共 API | 模式:`net_socket.h` 声明(`NET_NO_OS` 条件编译)→ `net_msg_type_t` 加类型(如需要)→ `net_process_messages` 加 dispatch 分支 → netTask 侧 `_locked` 实现;调用侧复制"参数检查 → 入队 → `net_wait_result` → errno 恢复"骨架 |
| 缓冲布局 / MSS / 重传 / KeepAlive 参数 | `net_init` + `net_config.h` + `ch395f.h`(`CH395F_RECV/SEND_BLOCKS`、`CH395F_TCP_MSS`);改完必须 0 错 0 警构建 + 全测试套件回归 |
@@ -430,7 +444,7 @@ sequenceDiagram
## 9. 相关文档
- `docs/网络/BSD_Socket_API_使用指南.md` —— 公开 API 语义、示例、常见问题(本文的上半部)
- `docs/子系统设计/网络Socket监听生命周期设计说明.md` —— **监听生命周期权威设计**(N-32:边沿→电平、单一所有者重建(自愈收敛阶段 3 起为 `net_reconcile_tick` 形态③④)、`net_listener.c` 模块化边界 §10)
- `docs/网络/网络自愈架构设计.md` —— **监听生命周期权威设计**(N-32:边沿→电平、单一所有者重建(自愈收敛阶段 3 起为 `net_reconcile_tick` 形态③④)、`net_listener.c` 模块化边界 §11)
- `docs/问题记录/CH395F_Trap_Records.md` —— TRAP-NET-01~23 机制细节(本文引用处)
- `docs/网络/Net层阻塞模型架构设计.md` —— 阻塞等待模型的现状架构设计(pending 请求模型 / 五路完成语义)
- `docs/问题记录/错误码传播审计.md` —— E-02 头阻塞与错误码回传全审计
+635 -92
View File
@@ -1,16 +1,35 @@
# Net 层阻塞模型架构设计
> **模块名称**:Net 层挂起 recv 模型(`net_pending.c` / netTask 主循环节拍)
> **模块名称**:Net 层挂起 recv / send 模型(`Drivers/BSP/NET/net_pending.c` + netTask 主循环节拍)
> **文档定位**:**现状架构设计说明**——描述本层**当前如何组织等待与完成**、为什么这样设计、
> 边界条件与已验证的性能特征。**不是历史记录**:历史演进、门禁实测记录与修订日志
> 一律不保留在文档内(需要时查 git 历史)。
> **状态**:设计已落地并全量台架验证(`v2-on-ui`)。
> **日期**:2026-09-29
> 数据结构与接口契约、实现落点与集成步骤、边界条件与已验证的性能特征。**不是历史记录**:
> 历史演进、门禁实测记录与修订日志一律不保留在文档内(需要时查 git 历史)。
> **状态**:recv 挂起(一期)与 send 挂起(二期)设计均已落地;recv 全量台架验证,
> send 于 2026-10-14 完成台架复测(含本次复测发现并修复的末块挂起越界缺陷,见 §9.1)。
> **日期**:2026-10-14
> **相关文档**:
> `docs/网络/BSD_Socket_API_使用指南.md`(应用视角 API 语义)、
> `docs/网络/BSD_Socket_API_函数调用关系.md`(调用链与执行域)、
> `docs/网络/网络自愈架构设计.md`(周期对账与监听生命周期——与本模型的分工见 §2.4;模块化边界见 §11)、
> `docs/测试说明/链路延迟测试说明.md`(延迟基线与故障对照)、
> `docs/任务规划/基线与待确认.md` §2.1(二期 ACCEPT/SEND 扩展设计)
> `docs/问题记录/错误码传播审计.md`(E-02 队头阻塞审计)、
> `docs/任务规划/基线与待确认.md` §2.1(二期扩展设计:SEND 已落地、ACCEPT 待办)
## 目录
- [1. 问题域](#1-问题域)
- [2. 架构总览](#2-架构总览)
- [3. 一次阻塞 recv 的全过程](#3-一次阻塞-recv-的全过程)
- [4. 数据结构](#4-数据结构)
- [5. 完成与取消语义](#5-完成与取消语义)
- [6. 模块接口契约](#6-模块接口契约)
- [7. 实现落点(集成步骤)](#7-实现落点集成步骤)
- [8. 调用方契约](#8-调用方契约)
- [9. 性能特征](#9-性能特征)
- [10. 设计边界与已知代价](#10-设计边界与已知代价)
- [11. 设计铁律](#11-设计铁律)
- [12. 排查辅助:挂起超时留痕探针](#12-排查辅助挂起超时留痕探针)
- [13. 源码索引](#13-源码索引)
---
@@ -29,11 +48,28 @@ netTask 是**全部 socket 操作的唯一执行者**——CH395F 的 SPI 总线
> → **观测本身占用执行者**,等待无法挪出 netTask。
设计自由度因此只剩两条:① 每次占用执行者多久 ② 等待期间执行者还能做什么。
本架构的答案:**占用降到登记一次(微秒级),等待期间执行者照常服务整个队列**。
三条候选路线的取舍:
| 路线 | 单次占用执行者 | 等待期间执行者 | 落地结论 |
|---|---|---|---|
| A 执行者内自旋 | 整个等待期(可无限) | **完全停摆** | ❌ 队头阻塞(实测一条空闲 FTP 连接冻结远端显示 17.7s) |
| B 有界分片轮询(`net_recv_to` 分片) | ≤ `timeout_ms` | 分片间隙可用 | ⚠️ 止血:仍周期性占用,调用方须自管总预算 |
| C **挂起登记(本模型)** | 微秒级(登记一次) | 照常服务整个队列 | ✅ 结构性消灭 |
路线 A→B→C 的落地证据与量化见 `docs/问题记录/错误码传播审计.md` E-02。
路线 B 的入口 `net_recv_to` 在路线 C 下**继续保留**,但语义已从"分期占用执行者"
变为"调用方显式预算"(见 §6.2、§8)。
> **SEND 是同一问题的第二个面**:`net_send` 写入 CH395F 发送 FIFO 后需等 `send_ready`
> (由 `SENBUF_FREE` 中断经 `net_poll` 重新置位),旧的 `net_send_locked` 在 netTask 内
> **忙轮询等它**,单次停滞上限 `NET_SEND_TIMEOUT_MS`(1 s)——同属执行者占用型队头阻塞,
> 量级比 recv 小一个数量级,但同样会冻结整个消息队列并顺延挂起 recv 的唤醒。二期把
> send 也纳入挂起模型(§4.5、§5.5),**快路径保留有界自旋**(健康链路 219 µs ≪ 2 ms 预算,
> 行为不变),超预算才登记挂起。收益判据仍是"执行者被占多久"(§10)。
---
## 2. 架构总览:pending 请求模型
## 2. 架构总览
核心思想:把"等数据"从**执行者内的自旋**改为**执行者外的登记**。
@@ -43,75 +79,89 @@ netTask 是**全部 socket 操作的唯一执行者**——CH395F 的 SPI 总线
| 区域 | 内容 |
|---|---|
| **左栏 · 调用方任务** | `net_recv` → 判"数据已在?" → **是**走快速路径;**否**(`timeout > 0`)登记 pending 后在自己上下文睡任务通知 |
| **中栏 · netTask 主循环** | ~1ms 节拍:`net_poll`(刷新 readable + 全量刷新驱动状态缓存)→ 3 个非阻塞 tick(`net_listener_retry_tick` D10 → `net_pending_tick` 结束挂起 → `net_reconcile_tick` TRAP-NET-17 对账 + 监听重建唯一入口)→ 排空消息队列 |
| **下带 · 五路判定** | 每轮对每个挂起 fd 按序判定:数据到达 / 到点(仅显式预算)/ EOF / 同 fd 后续操作 / 网络重建;挂起超时留痕探针(编译期选项,默认关,见 §9)只留痕 |
| **左栏 · 调用方任务** | `net_recv` → 判"数据已在?" → **是**走快速路径;**否**(`timeout > 0`)登记挂起 recv 后在自己上下文睡任务通知。`net_send` 同构:快路径短暂自旋写块,超预算则登记挂起 send 后睡 |
| **中栏 · netTask 主循环** | ~1ms 节拍:`net_poll`(刷新 `readable` + 全量刷新驱动状态缓存)→ 4 个非阻塞 tick(`net_listener_retry_tick` D10 → `net_pending_tick` 结束挂起 recv → `net_send_pending_tick` 推进/结束挂起 send → `net_reconcile_tick` 对账 + 监听重建唯一入口)→ 排空消息队列 |
| **下带 · 判定** | recv 每轮按序判定:数据到达 / 到点(仅显式预算)/ EOF / 同 fd 后续操作 / 网络重建(**五路**);send 每轮按序判定:断连 / 推进 / 写完 / 无进展超时(**四路**);挂起超时留痕探针(编译期选项,默认关,见 §12)只留痕 |
| **回环** | 判定完成经 `xTaskNotify` 唤醒调用方(结果 = 字节数 / 负错误码) |
**关键性质**:
- 等待期间 netTask **零额外占用**——`net_pending_tick` 只读标志与到期时间,
数据到达判据 `readable` 由主循环既有的 `net_poll()` 每轮刷新,**不引入额外 SPI 事务**;
- 调用方**在自己的上下文睡眠**(任务通知),与 LwIP 的消费者线程模型同构;
- 公开 API 与调用契约**稳定**——`net_recv`(默认无限阻塞)/ `net_recv_nb` +
`net_setblocking` / `net_recv_to`(唯一显式预算入口)的签名与返回值不变,
既有调用方、测试与模拟器零改动;超时归属语义详见
`docs/网络/网络自愈架构设计.md` §4.5 与 `docs/网络/BSD_Socket_API_使用指南.md` §3.5。
> 图形由 `tools/arch_diagram/gen_net_pending_svg.py` 生成(改流程后重跑;见
> `docs/架构设计/图形绘制流程.md`)。
---
### 2.1 关键性质
## 3. 数据结构
- 等待期间 netTask **零额外占用**——`net_pending_tick` 只读标志与到期时间,
数据到达判据 `readable` 由主循环既有的 `net_poll()` 每轮刷新,**不引入额外 SPI 事务**;
- **挂起 send 的占用有界且非阻塞**——`net_send_pending_tick` 每轮至多写一块(`≤ NET_SEND_CHUNK_MAX`),
写完即返回,绝不 `osDelay` 忙等;故 netTask 最坏停滞由 `NET_SEND_TIMEOUT_MS`(1 s)
压到 `NET_SEND_SPIN_MS`(2 ms);
- 调用方**在自己的上下文睡眠**(任务通知),与 LwIP 的消费者线程模型同构;
- 公开 API 与调用契约**稳定**——`net_recv`(默认无限阻塞)/ `net_recv_nb` +
`net_setblocking` / `net_recv_to`(唯一显式预算入口)/ `net_send` 的签名不变;
既有调用方、测试与模拟器零改动;`net_send` 的返回值语义(已发字节数,短写须检查)
与挂起化前一致,超时归属语义详见
`docs/网络/网络自愈架构设计.md` §4.5 与 `docs/网络/BSD_Socket_API_使用指南.md` §3.5。
### 3.1 挂起请求表
### 2.2 模块边界
| 项 | 设计 |
|---|---|
| 存储 | **独立静态数组** `s_net_pending[NET_MAX_SOCKETS]`(不置于 `net_sock_t` 内) |
| 字段 | `active` / `infinite` / `buf` / `len` / `deadline` / `registered` / `caller`(`infinite`:0 预算 = 无限等待;留痕开启时另有 `tripwire_logged`:保证只留痕一次) |
| 容量 | 每 fd **至多一个**挂起请求 |
| 占用 | ≈24B/fd × 8 fd = **192B**(+4B 任务句柄),实测 SRAM +200B |
本模型是 `net_socket.c` 内部的**一层**,实现独立成模块,只被少数确定位置消费:
**为什么独立成表而不是塞进 `net_sock_t`**:
| 文件 | 持有 | 对外接口 |
|---|---|---|
| `Drivers/BSP/NET/net_pending.c` / `.h` | 挂起 recv 表 `s_net_pending[]`、挂起 send 表 `s_net_send[]`、执行者句柄 `s_owner_task`、全部完成/取消逻辑 | 14 个函数(recv 组 9 + send 组 7,含 2 个 static,§6.1) |
| `Drivers/BSP/NET/net_socket.c` | 消息队列与分派、`net_recv_locked` / `net_send_locked` 的挂起登记、close/drop_conn/重建/对账的结算调用 | 调用方(§7) |
| `Drivers/BSP/NET/net_listener.c` | 监听器重开 | 消费 `net_pending_active()` / `net_send_pending_active()` 作"挂起保险" |
| `App/task/net_task.c` | 主循环节拍(`net_poll` → `net_process_messages` → `osDelay(1)`) | 提供 tick 节拍 |
`net_close_locked` 对 `net_sock_t` 做**整块 memset**。若挂起状态置于其中,`close` 会
静默抹掉 pending——调用方永远等不到通知,**永久挂死**。独立表把"跨时间状态"与
socket 生命周期解耦,与 `net_listener_retry_tick` 的重试表同构。
依赖方向**单向**:`net_pending.c` → `net_types.h` / `net_socket.h` / `net_listener.h` /
`ch395f.h`(send 侧分块写与缓冲常量)/ `main.h`;无反向 include。`net_listener.c` 经
`net_pending.h` 反向消费 `net_pending_active()` / `net_send_pending_active()`,二者均为
**纯 RAM 读**,任意上下文安全。
### 3.2 登记守卫(两条硬防线)
### 2.3 与主循环的关系
| 守卫 | 理由 |
|---|---|
| 每 fd 至多一个,**不覆盖** | 覆盖 = 丢掉首个调用方的通知 = 该调用方永久挂起 |
| `caller == NULL` 或 `caller == netTask` 时**拒绝**(返回 BUSY) | netTask 上下文自等待 = 无人能完成它 = 死锁;netTask 内必须用 DONTWAIT |
netTask 主循环(`App/task/net_task.c` → `net_task_func`)每轮:
---
```text
task_wd_beat() → 处理 SYS_EVT_NET_RECONFIG → net_poll() → net_process_messages() → osDelay(1)
```
## 4. 挂起的 recv 会被什么结束(5 种触发条件)
`net_process_messages()`(`Drivers/BSP/NET/net_socket.c`)内部**先跑四个非阻塞 tick,
再排空消息队列**,顺序固定:
挂起 recv 被结束的方式只有 5 种,下文简称**五路**——①②③ 由 `net_pending_tick()`
每轮对每个挂起 fd 按序判定,④⑤ 由显式操作触发:
| # | 条件 | 动作 | 设计要点 |
| 顺序 | tick | 职责 | 与挂起模型的关系 |
|---|---|---|---|
| 1 | **数据到达** | SPI 读出 + 通知(结果 = 字节数) | 由 `net_poll` 维护的 `readable` 门控,**零额外 SPI**;仅 ESTABLISHED / UDP_OPEN 态可读;spurious readable 需清除 |
| 2 | **到点** `now >= deadline`(仅显式预算登记;无限等待登记无 deadline) | 通知 `NET_ERR_TIMEDOUT`(对外 -2 映射保持) | 状态无关;用有符号回绕安全差值,防 tick 回绕;这是 `net_recv_to` 调用方策略的正常兑现,不是伪造 |
| 3 | **CLOSED**(EOF/DISCONNECT) | 排空:有数据则读出;否则按 0(EOF) 完成 + `auto_relisten` + 通知 | CLOSE 先行完成挂起 = `net_close` 的 settle 钩子(排空→数据否则 CONNRESET) |
| 4 | **同 fd 后续操作**:close / drop_conn / 对账 resync(芯片静默回收) | 先结束挂起 recv(排空 → 有数据按字节数完成;否则取消 CONNRESET),再执行新操作 | per-fd 串行:pending 模型唯一留下的次序面就在本 fd 内,其他 fd 不受影响。同 fd 第二个 recv 被登记守卫拒绝(BUSY);send 无需结算(收发不冲突,SPI 由 netTask 串行) |
| 5 | **网络重建**(`net_stack_bring_up`,芯片复位) | **取消 + 必须通知** | `net_init` 顶部 `net_pending_cancel_all(CONNRESET)`,先于芯片复位;**取消不通知 = 调用方永久挂死** |
| ① | `net_listener_retry_tick()` | D10 重监听重试(30ms/2s) | 跳过有在途挂起 recv / send 的 fd(§7.2 集成点 8 / 20) |
| ② | `net_pending_tick(s_net_socks)` | **结束挂起 recv**(数据/到点/断连) | recv 的主完成检查入口 |
| ③ | `net_send_pending_tick(s_net_socks)` | **推进/结束挂起 send**(断连/推进/写完/超时) | send 的主推进入口(二期) |
| ④ | `net_reconcile_tick()` | 100ms 对账 + 监听重建唯一入口 | 形态⑤(芯片静默回收)取消本 fd 挂起 recv / send(§7.2 集成点 7 / 17) |
**状态推断的克制**:CONNECTING / LISTENING 等中间态**保持等待**,**不做**状态推断式取消——
CONNECT → ESTABLISHED 是合法迁移,推断取消会误杀正在建链的等待。
四个 tick 均非阻塞、**绝不 `osDelay` 忙等**;tick 先于消息分派,保证"断连后即使无应用
消息,监听器与挂起 recv / send 也能自主收敛"。②③ 相对顺序无关(两张独立表),
但都必须在 ④ 之前——对账可能取消同一 fd 的两侧挂起。
### 4.1 一次阻塞 recv 从调用到返回的全过程
### 2.4 与网络自愈架构的分工
一句话链:**登记(μs 级)→ 调用方自己睡 → 每轮判定(只读标志与到期时间)→ 公共出口(通知+清槽)→ 两段式等待取结果**。以 `net_recv(sockfd, buf, len)` 阻塞路径为例:
两篇文档描述同一段代码的两个正交面,边界如下:
| 关注点 | 归属文档 |
|---|---|
| "一次阻塞 recv 如何被唤醒/结束"(本模型:登记、五路完成、接口、集成点) | **本文** |
| "芯片与本层状态失同步后如何周期收敛"(对账五形态、沉降隔离、D10 重试、KeepAlive) | `docs/网络/网络自愈架构设计.md` |
| 监听生命周期的模块化边界(`net_listener.c` 拆分依据) | `docs/网络/网络自愈架构设计.md` §11 |
交叉点只有两处:`net_reconcile_tick` 形态⑤ 取消挂起(§5.2 第 5 路)、
`net_listener_retry_tick` 的挂起 recv 保险(§7.2 集成点 8)。
---
## 3. 一次阻塞 recv 的全过程
一句话链:**登记(μs 级)→ 调用方自己睡 → 每轮判定(只读标志与到期时间)→
公共出口(通知 + 清槽)→ 两段式等待取结果**。以 `net_recv(sockfd, buf, len, 0)` 阻塞路径为例:
```mermaid
flowchart TD
A["调用方任务:net_recv(sockfd, buf, len)"] --> B["消息入队,随后在<br/>自己上下文睡(两段式等待)"]
A["调用方任务:net_recv(sockfd, buf, len, 0)"] --> B["消息入队,随后在<br/>自己上下文睡(两段式等待)"]
B -.-> C
subgraph NT["netTask —— 唯一执行者(~1ms 节拍)"]
@@ -133,75 +183,539 @@ flowchart TD
I["通知调用方(同一回传通道):通知值 = 结果原值<br/>挂起路径经公共出口 net_pending_complete:清槽、防重复通知"]
I --> J["调用方醒来取结果<br/>0 = EOF 也是有效通知(以 pdTRUE 判定)"]
K["取消路径:close / drop_conn / 网络重建"] -. "先结束挂起、再动芯片" .-> I
K["取消路径:close / drop_conn / 网络重建 / 对账 resync"] -. "先结束挂起、再动芯片" .-> I
```
1. **入队**(调用方任务):`net_recv` 把请求打包进消息入 `netMsgQueue`,随后进入两段式等待(步骤 6)。netTask 不被占用。
2. **快速路径判定**(netTask,`net_recv_locked`):数据已在(`readable` / recv_len>0)→ 直接读出、经通知值回传字节数——不产生挂起,常见路径到此为止。
3. **登记**(netTask):无数据且阻塞模式 → `net_pending_register` 过两条守卫(§3.2:每 fd 至多一个不覆盖;拒绝 netTask 自等待)→ 记 `buf`/`len`/`caller` 与预算(`net_recv_to` → `deadline`;无限等待 → `infinite=1` 无 deadline)→ 返回"已登记"哨兵,netTask 立即处理下一条消息。
4. **每轮判定**(netTask,`net_pending_tick`):对每个挂起 fd 按序 ①数据 → ②到点 → ③断连(判据见上表;③ 的判据是 `state==CLOSED || session_dead`,漏 ③ = 无限 recv 永久挂死,勿动)。命中任一条件进入公共出口;挂起超时留痕探针只留痕不完成(编译期选项,默认关,见 §9)。
5. **公共完成出口**(`net_pending_complete`,所有完成/取消路径的唯一出口):`xTaskNotify` 覆盖式写入**通知值 = 结果原值**(>0 字节数 / 0 EOF / 负错误码)→ 清 `active`;无在位挂起则静默返回(防重复通知)。失败结果同步记入 netTask 自己的 errno 槽位(按任务槽位,非跨任务共享全局)。
6. **唤醒与取结果**(调用方任务):所有 `net_*` API 的回执共用同一条任务通知通道,**两段式等待**防"入队→等待入口"间隙内通知丢失——先零等待拾取(清除掩码全 1;以 pdTRUE/pdFALSE 判定已落地,不能判值≠0——合法的 0 结果=EOF 也是有效通知),未拾取到再阻塞等待。通知值 32 位整体作为结果,不切分位域(大单次收发不被截断)。
7. **取消路径**(五路中的 ④⑤,共同规则"**先结束挂起、再动芯片**"):
- **同 fd 后续操作**(close / drop_conn):`net_pending_settle_close` 先结算——芯片 RX 有数据(bit3=1 契约:断连后残数据留缓冲待 MCU 读)→ 按字节数完成;无数据 → 取消(CONNRESET)——然后才执行 close/拆会话。对账发现芯片静默回收(resync)时走 `net_pending_cancel`(连接已被芯片收走,无排空窗口,直接取消必通知)。
- **网络重建**:`net_init` 顶部 `net_pending_cancel_all(CONNRESET)`,先于芯片复位;取消必通知,否则调用方永久挂死。
8. **buf 生命周期**:挂起期间 `buf` 指针须保持有效至调用返回(挂起表按指针写入,§5)。
1. **入队**(调用方任务):`net_recv` 把请求打包进消息入 `netMsgQueue`,随后进入两段式
等待(步骤 6)。netTask 不被占用。
2. **快速路径判定**(netTask,`net_recv_locked`):函数入口先 `net_poll()` 一次刷新状态机
与可读标志,再查 `ch395f_get_recv_len`。数据已在(`recv_len > 0`)→ 直接读出、经通知值
回传字节数——不产生挂起,常见路径到此为止。
3. **登记**(netTask):无数据且阻塞模式 → `net_pending_register` 过两条守卫(§4.3)→ 记
`buf`/`len`/`caller` 与预算(`net_recv_to` → `deadline`;无限等待 → `infinite=1` 无
deadline)→ 返回哨兵 `NET_PENDING_REGISTERED`,netTask 立即处理下一条消息。
4. **每轮判定**(netTask,`net_pending_tick`):对每个挂起 fd 按序 ①数据 → ②到点 →
③断连(判据见 §5);命中任一条件进入公共出口。挂起超时留痕探针只留痕不完成
(编译期选项,默认关,见 §12)。
5. **公共完成出口**(`net_pending_complete`,所有完成/取消路径的**唯一**出口):
`xTaskNotify` 覆盖式写入**通知值 = 结果原值**(>0 字节数 / 0 EOF / 负错误码)→ 清
`active`;无在位挂起则静默返回(防重复通知)。失败结果同步记入 netTask 自己的 errno
槽位(按任务槽位,非跨任务共享全局)。
6. **唤醒与取结果**(调用方任务):所有 `net_*` API 的回执共用同一条任务通知通道,
**两段式等待**防"入队→等待入口"间隙内通知丢失——先零等待拾取(清除掩码全 1;
以 `pdTRUE`/`pdFALSE` 判定已落地,**不能判值≠0**——合法的 0 结果 = EOF 也是有效通知),
未拾取到再阻塞等待。通知值 32 位整体作为结果,不切分位域(大单次收发不被截断)。
7. **取消路径**(第 4/5 路,共同规则"**先结束挂起、再动芯片**"):
- **同 fd 后续操作**(close / drop_conn):`net_pending_settle_close` 先结算——芯片 RX
有数据(bit3=1 契约:断连后残数据留缓冲待 MCU 读)→ 按字节数完成;无数据 → 取消
(CONNRESET)——然后才执行 close/拆会话。对账发现芯片静默回收(resync)时走
`net_pending_cancel`(连接已被芯片收走,无排空窗口,直接取消必通知)。
- **网络重建**:`net_init` 顶部 `net_pending_cancel_all(CONNRESET)`,先于芯片复位;
取消必通知,否则调用方永久挂死。
8. **`buf` 生命周期**:挂起期间 `buf` 指针须保持有效至调用返回(挂起表按指针写入,§8)。
---
## 5. 调用方契约
## 4. 数据结构
调用方**不改协议、不改 API**,但需知悉三条:
### 4.1 挂起 recv 请求表 `net_pending_t`
| 字段 | 类型 | 含义 | 生命周期 |
|---|---|---|---|
| `active` | `uint8_t` | 1 = 本 fd 有挂起 recv | 登记置 1;`net_pending_complete` / `clear` 清 0 |
| `infinite` | `uint8_t` | 1 = 无限等待(无 deadline) | 登记按 `to_ms == 0` 定;每轮 ② 判定据此跳过 |
| `tripwire_logged` | `uint8_t` | 1 = 本轮挂起已留痕(保证只留一条) | **仅 `NET_PENDING_TRIPWIRE` 编译期存在**;登记清 0 |
| `buf` | `void *` | 调用方接收缓冲(仅指针,不拷贝) | 登记写入;调用方保证有效至返回 |
| `len` | `int` | 缓冲容量 | 登记写入 |
| `deadline` | `uint32_t` | 截止时刻(tick)= 登记时刻 + 本次预算 | 无限等待时置 0(可观测性用,② 判定不看) |
| `registered` | `uint32_t` | 登记时刻(tick) | 留痕探针基准 |
| `caller` | `TaskHandle_t` | 调用方任务(完成通知目标) | 登记写入 |
- **存储**:**独立静态数组** `s_net_pending[NET_MAX_SOCKETS]`(不置于 `net_sock_t` 内)。
- **容量**:每 fd **至多一个**挂起请求。
- **占用**:结构 24 B/fd × 8 fd = **192 B**(`tripwire_logged` 落在对齐填充里,开启探针
不增加占用);另 `s_owner_task` 4 B。实测 SRAM +200 B。
- **`infinite` 与 `deadline` 正交**:`to_ms == 0` 即"0 预算 = 无限等待"(无默认预算)。
### 4.2 为什么独立成表而不是塞进 `net_sock_t`
`net_close_locked` 对 `net_sock_t` 做**整块 `memset`**。若挂起状态置于其中,`close` 会
静默抹掉 pending——调用方永远等不到通知,**永久挂死**。独立表把"跨时间状态"与
socket 生命周期解耦,与 `net_listener_retry_tick` 的重试表同构。
### 4.3 登记守卫(两条硬防线,recv / send 共用)
recv 与 send 两张表的登记函数(`net_pending_register` / `net_send_pending_register`)
执行**完全相同**的两条守卫:
| 守卫 | 判据 | 理由 |
|---|---|---|
| 每 fd 至多一个,**不覆盖** | `p->active != 0` → 返回 `BUSY` | 覆盖 = 丢掉首个调用方的通知 = 该调用方永久挂起 |
| 拒绝执行者自等待 | `caller == NULL` 或 `caller == s_owner_task` → 返回 `BUSY` | netTask 上下文自等待 = 无人能完成它 = 死锁;netTask 内必须用 `NET_MSG_DONTWAIT` / 非阻塞发送 |
`s_owner_task` 由 `net_pending_capture_owner()` 在 `net_init` 顶部捕获(= 调用者,
NET 层为 netTask,硬件层为 testTask)。
### 4.4 挂起槽状态机
```mermaid
stateDiagram-v2
[*] --> IDLE
IDLE --> WAITING: net_pending_register(过两守卫)
WAITING --> WAITING: tick 每轮判定未命中<br/>(含中间态 CONNECTING/LISTENING 保持等待)
WAITING --> IDLE: net_pending_complete<br/>(数据 / 到点 / EOF / 取消)
IDLE --> IDLE: complete 对非在位槽位静默返回(防重复通知)
note right of WAITING
等待期零执行者占用:
tick 只读标志与到期时间
end note
```
槽位只有两个状态:**IDLE**(无挂起)与 **WAITING**(有挂起)。所有出口都汇聚到
`net_pending_complete`(或幂等的 `net_pending_clear`),因此**不存在"完成两次"或
"漏通知"的中间态**。挂起 send 槽位(§4.5)同构,只是"完成条件"由"数据到达"换为"全部写完"。
### 4.5 挂起 send 请求表 `net_send_pending_t`
| 字段 | 类型 | 含义 | 生命周期 |
|---|---|---|---|
| `active` | `uint8_t` | 1 = 本 fd 有挂起 send | 登记置 1;`net_send_pending_complete` / `clear` 清 0 |
| `buf` | `const void *` | 调用方发送缓冲基址(**仅指针,不拷贝**) | 登记写入;调用方保证有效至返回 |
| `len` | `int` | 本次发送**总**长度(含登记前已写入芯片的部分) | 登记写入 |
| `sent` | `int` | 已写入芯片的字节数(**进度**;登记时可 > 0) | 每次成功写块累加;完成时即结果值 |
| `deadline` | `uint32_t` | 无进展超时时刻 = **上次成功写块时刻** + `NET_SEND_TIMEOUT_MS` | 每次成功写块刷新;`NET_SEND_TIMEOUT_MS == 0` 时置 0(不限) |
| `registered` | `uint32_t` | 登记时刻(tick) | 日志基准 |
| `caller` | `TaskHandle_t` | 调用方任务(完成通知目标) | 登记写入 |
- **存储**:**独立静态数组** `s_net_send[NET_MAX_SOCKETS]`,与 `s_net_pending[]` **完全独立**
(各自一张表、各占一槽)。
- **容量**:每 fd **至多一个**挂起 send。
- **占用**:结构 28 B/fd × 8 fd = **224 B**(实测 RW_IRAM1 +224 B,与 `s_net_pending` 的 192 B 并列)。
- **与 recv 表可并存**:同一 fd 可同时有一个挂起 recv 与一个挂起 send——收发两个方向互不阻塞
(SPI 由 netTask 串行,本就支持该并发)。
- **`deadline` 按无进展刷新**:这是与 recv 表 `deadline`(一次预算、不刷新)的**关键差异**,
理由见 §5.5。
---
## 5. 完成与取消语义
recv 与 send 两张挂起表**各自独立**,完成语义分别定义:recv 是"等一次到达即完成"(五路),
send 是"分块推进直到全部写完"(四路 + 显式取消)。两表可同一 fd 并存,互不阻塞(§4.5)。
### 5.1 挂起 recv 五路总表
挂起 recv 被结束的方式只有 5 种,下文简称**五路**——①②③ 由 `net_pending_tick()`
每轮对每个挂起 fd 按序判定,④⑤ 由显式操作触发。
| # | 条件 | 动作 | 设计要点 |
|---|---|---|---|
| 1 | **数据到达** | SPI 读出 + 通知(结果 = 字节数) | 由 `net_poll` 维护的 `readable` 门控,**零额外 SPI**;仅 `ESTABLISHED` / `UDP_OPEN` 态可读;spurious readable 需清除 |
| 2 | **到点** `now >= deadline`(仅显式预算登记) | 通知 `NET_ERR_TIMEDOUT`(对外 -2 映射保持) | 状态无关;用有符号回绕安全差值,防 tick 回绕;这是 `net_recv_to` 调用方策略的正常兑现,不是伪造 |
| 3 | **断连**(`CLOSED` 或 `session_dead`) | 排空:有数据则读出;否则按 `released_abnormal` 给 `CONNRESET` 或 0(EOF),并 `auto_relisten` + 通知 | CLOSE 先行完成挂起 = `net_close` 的 settle 钩子 |
| 4 | **同 fd 后续操作**:close / drop_conn / 对账 resync | 先结束挂起 recv,再执行新操作 | per-fd 串行;同 fd 第二个 recv 被登记守卫拒绝(BUSY);send 无需结算(收发不冲突,SPI 由 netTask 串行) |
| 5 | **网络重建**(`net_stack_bring_up`,芯片复位) | **取消 + 必须通知** | `net_init` 顶部 `net_pending_cancel_all(CONNRESET)`,先于芯片复位;**取消不通知 = 调用方永久挂死** |
### 5.2 挂起 recv 逐路判据与动作
**第 1 路 · 数据到达**(`net_pending_tick` 内):
- 门控:`state == ESTABLISHED || state == UDP_OPEN` 且 `readable != 0`;
- 命中后 `ch395f_get_recv_len` → 有数据则 `min(可用, len)` 读出、累加 `recv_bytes`、
清 `readable`、以字节数完成;`avail == 0`(虚假标志)只清 `readable` 继续等待。
- **零额外 SPI 的来源**:`readable` 由 `net_poll` 每轮刷新(INT# 电平触发链保证缓冲有数据时
每轮重读 SINT 重位置位),`net_pending_tick` 不发起探测事务。
**第 2 路 · 到点**:
- 判据 `(p->infinite == 0) && ((int32_t)(now - p->deadline) >= 0)`;有符号差值抗 tick 回绕。
- 无限等待登记(`infinite == 1`)**无 deadline,永不因时间结束**——这是调用方策略。
**第 3 路 · 断连**:
- 判据 `state == CLOSED || session_dead != 0`(`session_dead` 是"会话已结束"的**粘滞电平**,
定义见 `Drivers/BSP/NET/net_types.h`)。
⚠ **判据不可收窄**:只判 `CLOSED` 时,抢先 relisten 后第 1 路(要求 `ESTABLISHED`)与
第 3 路(要求 `CLOSED`)两头落空,挂起只能干等第 2 路;无限等待登记没有第 2 路,
**漏判 = 永久挂死**(留痕探针只留痕、不唤醒)。
- 动作:查芯片残留 → 有则按数据完成;无则内层 `state == CLOSED && auto_relisten` 时
`net_listener_try()` 即时重建,随后按 `released_abnormal` 决定 `CONNRESET`(异常释放)
或 `0`(正常 FIN)。
- ⚠ 内层 `state == CLOSED` 门是**必需的**,不是冗余:`net_listener_try` 无状态门,
对已 `LISTENING` 的 fd 再 `open+listen` 会因 `USE` 错误走进关闭分支、关掉正听着的 Socket。
- 该处**保留即时重建**(而非只交 reconciler):它只在 `p->active != 0`(有挂起 recv)时
到达,属"消费者在等这条连接结局"的**消费者驱动路径**;删掉会把 CLOSED 窗口拉长到
reconciler 周期 + 隔离期(~100ms),期间调用方高频 `net_accept` → `[WD] task stall`。
**第 4 路 · 同 fd 后续操作**:
- `net_close_locked` / `net_drop_conn_locked` 的**首行**调 `net_pending_settle_close`
(有数据先送达、无数据取消,**必通知**),再动芯片;
- 对账形态⑤(芯片静默回收回 LISTEN)走 `net_pending_cancel(i, CONNRESET)`——连接已被芯片
收走,无排空窗口,直接取消。
**第 5 路 · 网络重建**:
- `net_init` 顶部(控制块清零与芯片复位**之前**)`net_pending_cancel_all(NET_ERR_CONNRESET)`;
- 遍历 8 个 fd 逐一通知,未在位者静默跳过。
### 5.3 为什么 recv 判据顺序是 ①→②→③
1. **数据优先于断连**:断连时芯片缓冲可能仍有残数据(bit3=1 契约),先判数据可让
最后一次读到完整内容,避免"断连即丢尾包";
2. **数据优先于到点**:数据与到点同时成立时交付数据(更贴近调用方意图,且
`net_recv_to` 的调用方以"本片有数据"为准继续累计预算);
3. **到点优先于断连**:显式预算登记在超时时刻应兑现超时语义,即便此刻连接恰好已断——
调用方拿到 `-2` 后按自己的预算循环决定是否续轮,下一轮会拿到断连结果。
### 5.4 recv 状态推断的克制
`CONNECTING` / `LISTENING` 等中间态**保持等待**,**不做**状态推断式取消——
`CONNECT → ESTABLISHED` 是合法迁移,推断取消会误杀正在建链的等待。陈旧 resync
(芯片静默回收)由显式 `net_pending_cancel` 处理,不靠推断。
### 5.5 挂起 send 的完成与取消(四路 + 显式取消)
send 与 recv 语义不同:recv 是"等一次到达即完成",send 是"**分块推进直到全部写完**"。
因此 `net_send_pending_tick()` 每轮对每个挂起 send 按序判定 **①断连 → ②推进 → ③写完 → ④无进展超时**,
另有显式取消路径(close / drop_conn / resync / 重建)。
| # | 条件 | 动作 | 设计要点 |
|---|---|---|---|
| 1 | **断连**(`state != ESTABLISHED` 或 `session_dead`) | 以**已发字节数**结束 | 剩余字节不可能送达;判据同快速路径——离开 `ESTABLISHED` 后即便再回来也是**新会话**,旧字节绝不投到新连接;`session_dead` 为冗余保险 |
| 2 | **推进**:`sent < len` 且 `send_ready != 0` | 写下一块(`min(剩余, NET_SEND_CHUNK_MAX)`),清 `send_ready`,刷新无进展计时 | **每轮至多一块**——写完即清 `send_ready`,下一块须等 `SENBUF_FREE` 中断经 `net_poll` 重新置位;与快速路径共用分块粒度(TRAP-NET-16) |
| 3 | **全部写完**(`sent >= len`) | 以**总长度**结束 | 调用方见 `ret == len` 即成功 |
| 4 | **无进展超时**(`now >= deadline`) | 以**已发字节数**结束(短写)+ `TIMEDOUT` | 停滞计时按**无进展**刷新(每次成功写块重置)——大文件传输总时长必然超过单块预算,按总时长判会误杀;这与 recv 侧"到点"是**有意的语义差异** |
| — | **显式取消**:close / drop_conn / resync / 重建 | 以**已发字节数**结束,原因记 `net_set_errno` + `[ERR]` | 已写入芯片的字节**不回收**(对端可能已收到);未发部分放弃,由调用方按短写处理。**取消必通知**(同 recv 铁律) |
**为什么 send 结果恒为"已发字节数"而非错误码**:`net_send` 的契约是"返回实际写入字节数",
短写是**一等公民**(`ret < len` 即短写,调用方重试剩余)。断连/超时/取消都不伪造"全部成功",
也不把结果改写为负错误码——原因经 `net_set_errno` 记录在 netTask 自己的槽位,调用方以
`ret != len` 判定短写(详见 `docs/网络/BSD_Socket_API_使用指南.md` §3.5)。
**为什么 send 用"无进展"而非"总时长"计时**:recv 的"到点"是调用方**显式预算**的兑现(一次等待
一个 deadline 就够);send 的等待是**多次块间等待的累积**,一个 100 KB 传输在健康链路下也要
经历上百次 `send_ready` 等待,总时长远超单块预算。若按总时长判,正常大传输会被误杀成超时——
故 `deadline` 每次成功写块后重置为"当前 + `NET_SEND_TIMEOUT_MS`",只判"块间停滞"。
---
## 6. 模块接口契约
### 6.1 接口总表
全部声明在 `Drivers/BSP/NET/net_pending.h`,实现见 `Drivers/BSP/NET/net_pending.c`。
recv 与 send 各一组(send 组前缀 `net_send_pending_`),共 14 个函数。
**公共 / recv 组**:
| 函数 | 上下文 | 前置条件 | 返回 | 副作用 | 调用者 |
|---|---|---|---|---|---|
| `net_pending_capture_owner(void)` | 执行任务 | 创建消息队列前 | — | 捕获 `s_owner_task` | `net_init` 顶部 |
| `net_pending_register(fd, buf, len, to_ms, caller)` | netTask | 无在位挂起;`caller` 非空且非执行者 | `NET_PENDING_REGISTERED` / `-1`(BUSY/BADF) | 占槽、写字段 | `net_recv_locked` 阻塞无数据路径 |
| `net_pending_tick(p_socks)` | netTask | `p_socks = s_net_socks` | — | 完成命中的挂起(通知 + 清槽) | `net_process_messages` 开头 |
| `net_pending_complete(fd, result)`(static) | netTask | — | — | 通知调用方、清槽、失败记 errno | 本模块内部所有完成/取消路径 |
| `net_pending_clear(fd)` | **任意** | — | — | 静默清槽(不通知) | `alloc_socket` / `net_close_locked` 尾部 |
| `net_pending_cancel(fd, err)` | netTask | — | — | 经 `complete` 通知 | `net_reconcile_tick` 形态⑤ |
| `net_pending_cancel_all(err)` | netTask | 芯片复位之前 | — | 逐 fd 通知 | `net_init` 顶部 |
| `net_pending_settle_close(fd, p_sock)` | netTask | `ch395f_close_socket` 之前 | — | 排空或取消 + 通知 | `net_close_locked` / `net_drop_conn_locked` 首行 |
| `net_pending_active(fd)` | **任意**(纯 RAM 读) | — | `1`/`0` | 无 | `net_listener_retry_tick` 挂起保险 |
**send 组**(与 recv 组一一对称):
| 函数 | 上下文 | 前置条件 | 返回 | 副作用 | 调用者 |
|---|---|---|---|---|---|
| `net_send_pending_register(fd, buf, len, sent, caller)` | netTask | 无在位挂起 send;`caller` 非空且非执行者;`0 ≤ sent < len` | `NET_PENDING_REGISTERED` / `-1`(BUSY/BADF/INVAL) | 占槽、写字段 | `net_send_locked` 超预算路径 |
| `net_send_pending_tick(p_socks)` | netTask | `p_socks = s_net_socks` | — | 推进/完成命中的挂起 send(通知 + 清槽) | `net_process_messages` 开头 |
| `net_send_pending_complete(fd, result)`(static) | netTask | — | — | 通知调用方、清槽 | 本模块内部所有 send 完成/取消路径 |
| `net_send_pending_clear(fd)` | **任意** | — | — | 静默清槽(不通知) | `alloc_socket` / `net_close_locked` 尾部 |
| `net_send_pending_cancel(fd, err)` | netTask | — | — | 经 `complete` 通知(结果 = 已发字节数) | `net_close_locked` / `net_drop_conn_locked` / `net_reconcile_tick` 形态⑤ |
| `net_send_pending_cancel_all(err)` | netTask | 芯片复位之前 | — | 逐 fd 通知 | `net_init` 顶部 |
| `net_send_pending_active(fd)` | **任意**(纯 RAM 读) | — | `1`/`0` | 无 | `net_listener_retry_tick` 挂起 send 保险 |
> `net_pending_complete` / `net_send_pending_complete` 是各自方向**唯一**的通知出口,
> `clear` 是**唯一**静默清槽出口——任何新增路径必须二选一,不得自行写
> `s_net_pending[fd].active = 0` / `s_net_send[fd].active = 0`。
### 6.2 关键契约细则
- **通知值即结果**:`xTaskNotify(caller, (uint32_t)result, eSetValueWithOverwrite)`,
32 位整体不切分;`result < 0` 时先 `net_set_errno(result)`(记入 netTask 任务槽位)。
- **失败码回传路径(两条)**:快速路径由 `net_process_messages` 回传该条消息**自身**的
错误码;挂起路径由 `net_pending_complete` 记 errno 并把负值作为通知值——两处取的都是
netTask 自己的 errno 槽位(E-03 后按任务隔离),不受其他任务影响。
- **`NET_PENDING_REGISTERED` 哨兵**:`0x70000000`,取值不与字节数空间冲突;分派器据此
置 `do_notify = 0`,把回执推迟到真正的完成时刻,避免"登记成功"被误报为处理结果。
- **`net_recv_to` 与 `timeout_ms = 0`**:`net_recv_to` 拒收 `timeout_ms == 0`(`INVAL`),
因为 0 在 netTask 侧意味着"无限等待",与该接口"有界等待"的存在理由相反。
- **`-2` 映射**:`net_recv_to` 见到 `NET_ERR_TIMEDOUT` 返回 `-2`(**先于**通用 `< 0` 分支
判断,否则 `-9` 会被吞成 `-1`);超时只凭返回码判定,不依赖 errno(无跨任务竞态)。
- **send 短写契约**:`net_send` 返回值恒为**已写入字节数**(`≥ 0`);`= len` 为全部写入,
`< len` 为短写,调用方**必须**检查并重试剩余(`ui_net_sink` / `iec104_serv` 按
`ret != total` 判短写;`lftpd_inet_write` 循环 `while (n > 0)`、`n <= 0` 视为致命)。
断连/超时/取消均以已发字节数结束,**不伪造"全部成功"、不改写为负错误码**——原因记
netTask 自己的 errno 槽位。
- **`net_send_sock`(netTask 内部版)**:其阻塞用法会把 `caller` 置为 netTask 自身,
登记必被"拒绝执行者自等待"守卫拒(`BUSY`)→ 返回 `-1`。故该接口只适合"发送缓冲可用"
的场合;netTask 内需长时间等待的发送须自行分片(见 §7.2 集成点 13)。
### 6.3 编译期开关
| 宏 | 位置 | 默认 | 作用 |
|---|---|---|---|
| `NET_PENDING_TRIPWIRE` | `Drivers/BSP/NET/net_pending.h` | `0`(产品构建编译剔除,零成本) | 开启挂起超时留痕探针(见 §12) |
| `NET_RECV_TRIPWIRE_MS` | 同上(仅探针开启时定义) | `60000` | 留痕阈值 |
| `NET_SEND_SPIN_MS` | `Drivers/BSP/NET/net_config.h` | `2` | **发送快速路径**的有界自旋预算(ms):单次"等发送缓冲腾出"超过本值即登记挂起 send,把 netTask 最坏停滞由 `NET_SEND_TIMEOUT_MS`(1 s)压到本值。健康链路实测块间等待 med 219 µs / max 378 µs(≈5× 余量),故正常发送不会触发挂起 |
| `NET_SEND_TIMEOUT_MS` | `Drivers/BSP/NET/net_config.h` | `1000` | 发送**无进展**超时(ms):快速路径的块间停滞上限;挂起路径的 `deadline` 亦按本值刷新。`0` = 不限 |
---
## 7. 实现落点(集成步骤)
本节是"**改/查本模型时看哪里**"的落地清单:每个集成点若缺失或改动,后果都是确定的。
### 7.1 集成点总表
**recv 组**(编号 1–12 沿用一期):
| # | 集成点 | 文件 / 函数 | 调用 | 缺失后果 |
|---|---|---|---|---|
| 1 | 执行者句柄捕获 | `net_socket.c` `net_init` 顶部 | `net_pending_capture_owner()` | 自等待守卫失效(`s_owner_task` 恒 NULL)→ netTask 内阻塞 recv/send 登记成功 → **死锁** |
| 2 | 重建前取消 | `net_socket.c` `net_init` 顶部(芯片复位前) | `net_pending_cancel_all(NET_ERR_CONNRESET)` | 重建前挂起的调用方**永久挂死** |
| 3 | 每轮完成检查 | `net_socket.c` `net_process_messages` 开头 | `net_pending_tick(s_net_socks)` | 挂起**永不完成** |
| 4 | 挂起登记与回执抑制 | `net_socket.c` `net_process_messages` 的 `NET_MSG_RECV` 分支 | `net_recv_locked` 返回哨兵 → `do_notify = 0` | "登记成功"被当结果回执(误报) |
| 5 | 关闭结算 | `net_socket.c` `net_close_locked` 首行 | `net_pending_settle_close` | close 丢已到数据 / 调用方挂死 |
| 6 | 拆会话结算 | `net_socket.c` `net_drop_conn_locked` 首行 | `net_pending_settle_close` | 同上 |
| 7 | 静默回收取消 | `net_socket.c` `net_reconcile_tick` 形态⑤ | `net_pending_cancel(i, NET_ERR_CONNRESET)` | 芯片静默回收后挂起 recv 无唤醒源 |
| 8 | 重开 Socket 保险 | `net_listener.c` `net_listener_retry_tick` | `net_pending_active(i)` 命中则 `continue` | 重开 Socket **摧毁在途连接** |
| 9 | 新 fd 清槽 | `net_socket.c` `alloc_socket` | `net_pending_clear(sockfd)` | fd 复用继承旧挂起 → 新会话立即 EOF / 结果错位 |
| 10 | close 幂等清槽 | `net_socket.c` `net_close_locked` 尾部 | `net_pending_clear(sockfd)` | 槽位残留(虽经 settle 已通知,仍须归零) |
| 11 | 唤醒通道(两段式) | `net_socket.c` `net_wait_result` | `xTaskNotifyWait` 零等待 + 阻塞 | 通知丢失 → 调用方挂死 |
| 12 | 双端注册 | `MDK-ARM/STM32F407-Demo.uvprojx` + `simulator/CMakeLists.txt` | `net_pending.c` 只入真机 | 编译缺失 / 模拟器误编(§7.4) |
**send 组**(编号 13–19,二期;与 recv 组一一对称):
| # | 集成点 | 文件 / 函数 | 调用 | 缺失后果 |
|---|---|---|---|---|
| 13 | 发送有界自旋 | `net_socket.c` `net_send_locked` 两个等待循环 | 超 `NET_SEND_SPIN_MS` 即 `net_send_pending_register(...)` | netTask 停滞回到 1 s(队头阻塞);须在 1 s 超时判定**之后**再判自旋预算,保证 `SPIN > TIMEOUT` 误配时仍以超时收口 |
| 14 | 每轮推进 | `net_socket.c` `net_process_messages` 开头(`net_pending_tick` 之后) | `net_send_pending_tick(s_net_socks)` | 挂起 send **永不推进 / 永不完成** |
| 15 | send 登记与回执抑制 | `net_socket.c` `net_process_messages` 的 `NET_MSG_SEND` 分支 | `net_send_locked` 返回哨兵 → `do_notify = 0` | "登记成功"被当结果回执(误报) |
| 16 | 关闭 / 拆会话取消 | `net_socket.c` `net_close_locked` / `net_drop_conn_locked` | `net_send_pending_cancel(fd, NET_ERR_CONNRESET)` | 挂起 send 的调用方**永久挂死**(已发字节数收不回来) |
| 17 | 静默回收取消 | `net_socket.c` `net_reconcile_tick` 形态⑤ | `net_send_pending_cancel(i, NET_ERR_CONNRESET)` | 芯片静默回收后挂起 send 无唤醒源 |
| 18 | 重建前取消 | `net_socket.c` `net_init` 顶部(芯片复位前) | `net_send_pending_cancel_all(NET_ERR_CONNRESET)` | 重建前挂起的调用方**永久挂死** |
| 19 | 新 fd / close 清槽 | `net_socket.c` `alloc_socket` / `net_close_locked` 尾部 | `net_send_pending_clear(sockfd)` | fd 复用继承旧挂起 send → 新会话误写旧缓冲 |
| 20 | send 重开保险 | `net_listener.c` `net_listener_retry_tick` | `net_send_pending_active(i)` 命中则 `continue` | 重开 Socket **摧毁在途传输**(半写的数据流被截断) |
### 7.2 逐集成点要点
**1–2. `net_init` 顶部顺序**(`Drivers/BSP/NET/net_socket.c`):
```text
net_pending_cancel_all(NET_ERR_CONNRESET) ← 必须最早(芯片复位前)
memset(s_net_socks, 0, ...) ← 控制块清零
net_pending_capture_owner() ← 捕获执行者
... ch395f_check_exist / ch395f_reset ... ← 芯片复位
```
顺序不可交换:取消必须先于清零与复位,否则挂起调用方既收不到通知、其 `caller`
也可能随控制块一起失效。
**3–4 / 14–15. `net_process_messages` 开头与分派**:
```text
net_listener_retry_tick() ← D10
net_pending_tick(s_net_socks) ← 挂起 recv 完成检查
net_send_pending_tick(s_net_socks) ← 挂起 send 推进/完成检查
net_reconcile_tick() ← 对账 + 重建
while (osMessageQueueGet(..., 0) == osOK) { ... }
```
`NET_MSG_RECV` 分支:`msg.result = net_recv_locked(...)`;`NET_MSG_SEND` 分支:
`msg.result = net_send_locked(..., msg.caller)`;两者若等于 `NET_PENDING_REGISTERED`
则 `do_notify = 0`(**不发即时回执**)。send 分支还须把 `msg.caller`(调用方任务句柄)
透传给 `net_send_locked`——挂起登记的目标。四个 tick 均先于消息分派。
**5–6. 结算先于拆链**:`net_close_locked` / `net_drop_conn_locked` 的**第一行**就是
`net_pending_settle_close(...)`——"关闭不得让等待中的调用方永久悬挂",且"关闭不丢已到达
数据(芯片缓冲在 close 前可读)"。
**8 / 20. 挂起保险(recv 与 send)**:`net_listener_retry_tick` 对每个待重试 fd,先做状态门
(仅 `LISTENING`/`CLOSED` 才重试),再 `net_pending_active(i) || net_send_pending_active(i)`
命中即 `continue`——重开 Socket 会摧毁在途连接(recv)与在途传输(send 的半写数据流)。
**11. 两段式等待**(`net_wait_result`,所有 `net_*` API 共用):
- 第一段零等待拾取,消费"入队 → 等待入口"间隙内已落地的通知;
- 判定必须用返回值 `pdTRUE`/`pdFALSE`,**不能判值 ≠ 0**(0 = EOF 是合法通知);
- 清除掩码必须全 1(传 0 = 不清除,会残留上次结果被误读为本次结果);
- 禁止在本路径加逐调用日志(曾占日志 98.6%,冲掉 `[LAT]` 证据行)。
**13. 发送有界自旋**(`net_send_locked`):两个等待循环(首块等待 + 每块发完等待)在
`net_poll()` 之后判两件事,**顺序不可交换**:先判 `NET_SEND_TIMEOUT_MS`(1 s 停滞 → 记
`TIMEDOUT` 收口),再判 `NET_SEND_SPIN_MS`(超预算 → `net_send_pending_register(...)` 返回
哨兵)。把超时判定放在自旋判定**之前**,是为了保证 `NET_SEND_SPIN_MS > NET_SEND_TIMEOUT_MS`
的误配下仍以超时收口,不会"永远登记不出去"。健康路径块间等待 med 219 µs ≪ 2 ms,**永不触发**
挂起——故快速路径行为与挂起化前逐字节一致。
**14–19. send 组生命周期**:`net_send_pending_tick` 每轮按 ①断连 → ②推进 → ③写完 → ④超时
(§5.5)推进;`net_close_locked` / `net_drop_conn_locked` / `net_reconcile_tick` 形态⑤ /
`net_init` 顶部四处调 `net_send_pending_cancel(_all)`(**必通知**,否则调用方永久挂死);
`alloc_socket` / `net_close_locked` 尾部调 `net_send_pending_clear`(静默归零)。
### 7.3 改动检查清单
**新增一种"结束挂起 recv"的条件**:
- [ ] 在 `net_pending_tick` 的 ①②③ 判定链中插入,或走显式 `net_pending_cancel*`;
- [ ] 所有出口**必须**经 `net_pending_complete`(不得直接清 `active`);
- [ ] 若新条件是"取消",**必须通知**(取消不通知 = 永久挂死);
- [ ] 复核第 1 路的 `readable` 门控与第 3 路的判据未被新条件破坏(两头落空即挂死);
- [ ] 更新本文 §5 与 `docs/网络/BSD_Socket_API_使用指南.md` §3.5。
**新增一个使用阻塞 recv 的消费者**:
- [ ] 确认该任务**不在** IWDG `required_mask` 内;若在,**禁止无限阻塞**,必须
`net_select` 超时或 `net_recv_to` 显式预算(契约见 §8);
- [ ] `buf` 保持有效至调用返回;
- [ ] 保留"总预算 − 已耗时"循环骨架(`n == -2` 续轮)。
**新增一种"结束挂起 send"的条件**:
- [ ] 在 `net_send_pending_tick` 的 ①②③④ 判定链中插入,或走显式 `net_send_pending_cancel*`;
- [ ] 所有出口**必须**经 `net_send_pending_complete`(不得直接清 `active`);
- [ ] 若新条件是"取消",**必须通知**(取消不通知 = 永久挂死);
- [ ] 结果恒为**已发字节数**(短写语义),**不得**改写为负错误码——原因走 `net_set_errno`;
- [ ] 复核"无进展计时刷新"未被新条件破坏(按总时长判会误杀大传输);
- [ ] 更新本文 §5.5 与 `docs/网络/BSD_Socket_API_使用指南.md` §3.5。
**新增一个 socket fd 生命周期路径**:任何 `memset(p_sock)` / 释放 fd 的路径都须
配套 `net_pending_clear(fd)`(新 fd 不得继承)或 `net_pending_settle_close`(关闭前结算),
**两个方向都要**(recv 与 send 各自一张表)——漏掉 send 侧会让新 fd 误写旧缓冲。
### 7.4 双端注册与模拟器边界
- **真机**:`Drivers/BSP/NET/net_pending.c` 必须注册进 `MDK-ARM/STM32F407-Demo.uvprojx`
的 NET 分组(`net_listener.c` 旁)。
- **模拟器**:`simulator/CMakeLists.txt` **不编译** `net_pending.c` 与 `net_listener.c`
——模拟器走 `simulator/sim/net_socket_sim.c`(单线程 Winsock,无法复现挂起模型内部机制;
外部语义已由包装层按 blocking 属性/预算模拟)。故挂起表 / tick / 通知路径在模拟器上
**零覆盖**,只能硬件验证。
---
## 8. 调用方契约
调用方**不改协议、不改 API**,但需知悉五条:
| 契约 | 说明 |
|---|---|
| `buf` 生命周期 | 挂起期间 `buf` 指针须**保持有效**至调用返回(挂起表按指针写入) |
| `buf` 生命周期 | 挂起期间 `buf` 指针须**保持有效**至调用返回(挂起表按指针写入,recv / send 同) |
| **send 短写** | `net_send` 返回值恒为已写入字节数:`= len` 全部写入,`< len` 为短写,调用方**必须**检查并重试剩余(`ui_net_sink` / `iec104_serv` 按 `ret != total` 判短写;`lftpd_inet_write` 循环 `while (n > 0)`、`n <= 0` 视为致命)。挂起 send 以已发字节数结束,不会伪造全部成功 |
| 剩余预算循环 | 调用方保留"总预算 − 已耗时"循环骨架(`n == -2` 续轮,预算耗尽退出)——这是**公开契约要求**(30s 总超时语义 + 跨 TCP 分段累积),与本架构无关 |
| 超时精度 | 完成判定随主循环节拍(~1ms);主循环被长操作占用时,超时**最多顺延**相应时长(已知代价,见 §7) |
| IWDG 内禁无限阻塞 | IWDG `required_mask` 内任务**禁止无限阻塞 recv**——必须 `net_select` 超时或 `net_recv_to` 显式预算("超时归使用者"对受监控任务是义务不是选项,否则"对端连上不发数据" = IWDG 复位) |
| 超时精度 | 完成判定随主循环节拍(~1ms);主循环被长操作占用时,超时**最多顺延**相应时长(已知代价,见 §10) |
---
## 6. 性能特征(已验证)
## 9. 性能特征
| 指标 | 值 | 条件 |
| 指标 | 值 | 来源 / 条件 |
|---|---|---|
| 队列响应性(挂起连接下) | **92.5ms** | 挂起对时客户端 + FTP banner 探测 |
| M3 静默档帧周期 | med 58.9 / p95 69.9 / max 72.1ms | 零尖峰 |
| M3 压力档帧周期 | med 60.1 / p95 66.4 / max 69.3ms | 零尖峰,p99 尾部 133.5→69.3ms |
| netTask 栈峰值 | 488B / 4096(余量 88%) | 零占用设计落点 |
| 队列响应性(挂起连接下) | **92.5 ms** | `test/host/e02_probe.py`:挂起对时客户端 + FTP banner 探测 |
| M3 静默档帧周期 | med 58.9 / p95 69.9 / max 72.1 ms | `docs/测试说明/链路延迟测试说明.md` §6.3(零尖峰) |
| M3 压力档帧周期 | med 60.1 / p95 66.4 / max 69.3 ms | 同上(零尖峰;p99 尾部 133.5 → 69.3 ms) |
| netTask 栈实测峰值 | **1116 B / 3840 B(余 70%)**,FTP 传输期峰 **1164 B**;静态最坏链 ≈1.9 KB | `docs/内存管理/RAM使用分析与优化方案.md` §2.3;2026-10-14 台架复测(含 `net_send_pending_tick` 新增栈帧) |
**结构性收益(为什么这些数字成立)**:压力档下 MCU 侧 `prod_cycle_mcu` 6706 µs 纹丝不动
(min 6698 / max 6720)——队头阻塞工况不再向 UI 帧周期传导;`net_pending_tick` 在无挂起
时是纯指针读(零 SPI),故 L5 队列往返段与挂起化前逐位不变。
**SEND pending 的结构性收益**(二期,`net_send_pending_tick`):发送停滞期 netTask 不再
被冻结——netTask 最坏停滞由 `NET_SEND_TIMEOUT_MS`(1 s)压到 `NET_SEND_SPIN_MS`(2 ms),
且停滞不再顺延 `net_pending_tick` 的挂起 recv 唤醒。健康链路块间等待 med 219 µs / max 378 µs
(≈5× 余量),故**正常发送完全不触发挂起**,快路径逐字节不变。
静态代价:`s_net_send[]` **+224 B**(RW_IRAM1,8×28 B,见 §4.5)。
> ✅ **2026-10-14 台架复测已补测**:SEND pending 改动后 netTask 栈峰值 **1116 B / 3840 B**
> (开机态)~ **1164 B**(FTP 传输期),较此前 920 B 增加 ≈200 B(`net_send_pending_tick`
> 新增一层调用帧),余量 69~70%,远低于 1.9 KB 静态最坏链。复测同步发现并修复了
> 末块挂起越界缺陷(见 §9.1)。
### 9.1 末块挂起越界缺陷(2026-10-14 台架复测发现并修复)
**现象**:SEND pending 二期上线后台架复测,`ftp_regression.py` 间歇性 1/11(部分步骤
`write error, ret=-1`),偶可 11/11;基线固件同测 6/6 恒过 → 判定为**本次改动引入的回归**。
**根因**:`net_send_locked` 的内层"等本块发完(SENBUF_FREE)"循环位于**写块之后**。
写完**末块**时 `sent` 已累加到 `len`,若该次 SENBUF_FREE 等待超过 `NET_SEND_SPIN_MS`
(2 ms),代码径自调用 `net_send_pending_register(..., sent, ...)`。而登记函数的参数
校验含 `sent >= len → INVAL` 拒绝(本模型下 `sent == len` 意味着**无剩余块可挂起**),
返回 -1 被 `net_send_locked` 直接透传 → 一次**实际已完整交付芯片**的发送被报成失败。
`ftp_regression` 的 RETR 1KB 是单块帧(一次即 `sent == len`),故首当其冲。
**修复**:内层等待循环**开头**判 `sent >= len` → 直接跳出等待返回 `sent`
(== `len`,完整交付)——末块写完后已无下一块,"等 SENBUF_FREE"是纯保守自旋,
其等待顺延为下一批的责任。该守卫**置于超时/自旋判定之前**,末块路径零额外延迟;
同时它保证后面的 `net_send_pending_register` 调用必伴随 `sent < len`,登记必获受理。
外层写块前的挂起判定不受影响(循环条件 `sent < len` 未被改写)。
**为什么此前没暴露**:健康链路块间等待 med 219 µs ≪ 2 ms,末块等待极少越过预算;只有
在末块 SENBUF_FREE 变慢(FTP 数据连接刚建立、客户端读速不匹配等)时才触发,故表现为
间歇性。**教训**:自旋预算与"末块"边界是两个正交维度,二者的组合必须显式覆盖——
写完末块后"还差多少没写"的答案已是 0,此时任何"等缓冲区"型预算分支都不该继续走。
**复测结论**(修复后):`ftp_regression` 连续 **19 次 11/11**(含守卫重排版后重烧 5 次)、
`ftp_integrity_test` 20 轮 × 64 KB STOR/RETR SHA-256 **20/20 一致**(两轮,吞吐稳定
≈80 KB/s)、设备日志零 write error、无 forbidden 标记、ST-LINK 回读 `No difference found`。
---
## 7. 设计边界与已知代价
## 10. 设计边界与已知代价
| 项 | 现状 | 说明 |
|---|---|---|
| **SEND 路径未覆盖** | 发送仍为分块阻塞(1s/chunk stall) | 该期间队列仍被冻结;二期 pending 化解决 |
| **ACCEPT 路径未覆盖** | accept 仍用轮询计时 | 需 pending 化(二期) |
| **超时精度** | 随主循环节拍(~1ms),被长操作占用时顺延 | 一期文档化、不修;二期 SEND 化后该顺延面**收窄** |
| **SEND 路径**(执行者占用问题)✅ **二期已覆盖** | `net_send_locked` 快路径有界自旋 ≤ `NET_SEND_SPIN_MS`(2 ms);超预算登记挂起 send,由 `net_send_pending_tick` 按主循环节拍分块推进(`NET_SEND_CHUNK_MAX` = 1 KB/块),调用方在自己上下文睡 | 停滞期 netTask **不再冻结**:挂起 send 每轮至多写一块,`net_pending_tick` 与消息队列照常推进——挂起 recv 的最坏唤醒延迟不再被 ≈1 s 顺延。健康链路块间等待 med 219 µs ≪ 2 ms,快速路径行为与挂起化前逐字节一致。`NET_SEND_TIMEOUT_MS` 降级为**无进展**超时(每次成功写块刷新,见 §5.5) |
| **ACCEPT 路径未覆盖**(调用方轮询问题) | `net_accept_locked` 单次 `net_poll` 后**立即**返回 `ESTABLISHED` 或 `WOULDBLOCK`——**不占用 netTask**(0 停滞) | 代价不在执行者,而在**调用方轮询**:没有"阻塞到有连接"的入口,各服务(`ui_net_sink` / `iec104_serv` 100 ms / `time_sync` / `lftpd`)各自手写轮询循环,每轮一次完整 RPC 往返(入队 + 通知 + 唤醒),并带来一个轮询周期级的 accept 延迟抖动。pending 化的收益是**消除轮询 churn 与抖动**,不是解队头阻塞 |
| **超时精度** | 随主循环节拍(~1ms),被长操作占用时顺延 | 一期文档化、不修;SEND 化后该顺延面**收窄**(发送停滞不再顺延整层) |
| **挂起超时留痕探针** | 编译期选项 `NET_PENDING_TRIPWIRE` 默认关(排查挂死时开);未接自检 selftest | 二期补齐自检 |
| **模拟器不可建模** | `net_socket_sim.c` 单线程 Winsock 无法复现**挂起模型内部机制**(外部语义已由包装层按 blocking 属性/预算模拟) | 挂起表/tick/通知路径在模拟器上零覆盖,只能硬件验证;挂起模型实现独立在 `net_pending.c`(真机专属,CMakeLists 不编译;**监听生命周期 `net_listener.c` 同此**) |
| **模拟器不可建模** | `net_socket_sim.c` 单线程 Winsock 无法复现**挂起模型内部机制**(外部语义已由包装层按 blocking 属性/预算模拟) | 挂起表 / tick / 通知路径在模拟器上零覆盖,只能硬件验证(§7.4) |
| **单 fd 单挂起** | 每 fd 至多一个挂起 recv,第二个被 `BUSY` 拒绝 | 由登记守卫保证;同 fd 并发 recv 本就无意义(per-fd 串行语义) |
**二期扩展设计**(ACCEPT / SEND 的挂起化、门禁清单、改动量)见
`docs/任务规划/基线与待确认.md` §2.1。
> **SEND 与 ACCEPT 的 pending 化收益性质不同,勿混为一谈**。判据只有一个:
> **执行者被占多久**——SEND 挂起化前 ≤1 s/次(占用),ACCEPT 0(不占用)。
> - SEND 是**执行者占用**问题,与 RECV 同源:pending 化直接解队头阻塞,收益真实,
> **二期已落地**(§5.5、§7.1 send 组);
> - ACCEPT 是**调用方轮询**问题:执行者本就零占用,"accept 慢"来自调用方轮询周期,
> 不是 netTask 卡住——pending 化换的是 RPC 开销与延迟抖动,**不解决任何队头阻塞**。
> 故 ACCEPT 优先级低于 SEND,**仍未落地**(登记项见 `docs/任务规划/登记簿.md` N-4)。
**二期扩展设计**(ACCEPT 挂起化、门禁清单、改动量)见
`docs/任务规划/基线与待确认.md` §2.1。SEND 的挂起化已按该设计落地,见本文 §4.5 / §5.5 / §7.1。
---
## 8. 设计铁律(为什么是这样)
## 11. 设计铁律(为什么是这样)
1. **执行者永不在等待中自旋**——等待必须在调用方上下文完成,netTask 只负责"登记"与"结束"。
send 侧的"快路径有界自旋"是该铁律的**受控例外**:自旋被 `NET_SEND_SPIN_MS` 硬限(2 ms),
超限即转为挂起登记,故执行者占用有界。
2. **取消必通知**——任何取消路径(close / 重建 / 陈旧重同步)都必须唤醒调用方,
否则是永久挂死而非错误返回。
3. **挂起状态与 socket 表解耦**——`net_sock_t` 会被整块 memset,跨时间状态不能寄居其中。
4. **不做状态推断式取消**——中间态(CONNECTING/LISTENING)合法迁移存在,
否则是永久挂死而非错误返回(recv / send 同)。
3. **挂起状态与 socket 表解耦**——`net_sock_t` 会被整块 `memset`,跨时间状态不能寄居其中。
4. **不做状态推断式取消**——中间态(`CONNECTING`/`LISTENING`)合法迁移存在,
推断取消会误杀正常建链等待。
5. **数据到达不引入额外 SPI**——判据复用主循环既有 `net_poll` 维护的 `readable` 标志。
6. **唯一完成出口**——所有完成/取消都经 `net_pending_complete`(recv)/ `net_send_pending_complete`
(send);任何路径不得自行清 `active`。
7. **send 结果恒为已发字节数**——短写是一等公民;断连 / 超时 / 取消**不伪造**全部成功、
**不把结果改写为负错误码**,原因只经 `net_set_errno` 留痕。send 停滞计时按**无进展**
刷新(每次成功写块重置),绝不按总时长判(会误杀大传输)。
---
## 9. 排查辅助:挂起超时留痕探针(编译期选项,默认关)
## 12. 排查辅助:挂起超时留痕探针(编译期选项,默认关)
五路判定 + 显式 cancel 是挂起 recv 的**全部唤醒责任**(§4)。一个挂起请求若超过阈值
五路判定 + 显式 cancel 是挂起 recv 的**全部唤醒责任**(§5)。一个挂起请求若超过阈值
(60s,`NET_RECV_TRIPWIRE_MS`)还没被任何一路结束,只有一种解释:**完成路径漏了它**
(代码缺陷)——而不是"数据还没到":无限等待本来就是调用方的合法策略,不该被强行打断。
@@ -210,6 +724,35 @@ flowchart TD
"强制超时"兜底:强制结束会把"唤醒路径有 bug"这个**缺陷**伪装成一次**普通超时**——
调用方拿到假结果,缺陷永远潜伏(旧实现正是"到点 +1s 伪造 TIMEDOUT",已废除)。
开关:`NET_PENDING_TRIPWIRE`(`net_socket.c`,默认 0 = 编译剔除、产品零成本)——
排查"调用方挂死唤不醒"时置 1 重烧。**开与关语义完全一致**:探针只留痕,不参与完成。
代码里该机制名为 tripwire(绊线:挂住 = 踩到缺陷 wiring)。
开关:`NET_PENDING_TRIPWIRE`(`Drivers/BSP/NET/net_pending.h`,默认 0 = 编译剔除、产品
零成本)——排查"调用方挂死唤不醒"时置 1 重烧。**开与关语义完全一致**:探针只留痕,
不参与完成。代码里该机制名为 tripwire(绊线:挂住 = 踩到缺陷 wiring)。
判据基准:无限登记按 `registered` 计,显式登记按 `deadline` 计。
---
## 13. 源码索引
| 文件 | 符号 | 职责 |
|---|---|---|
| `Drivers/BSP/NET/net_pending.h` | `NET_PENDING_REGISTERED` / `NET_PENDING_TRIPWIRE` / `NET_SEND_CHUNK_MAX` | 哨兵、编译期开关、分块上限(唯一定义处) |
| `Drivers/BSP/NET/net_config.h` | `NET_SEND_SPIN_MS` / `NET_SEND_TIMEOUT_MS` | 发送自旋预算与无进展超时 |
| `Drivers/BSP/NET/net_pending.c` | `net_pending_capture_owner` | 捕获执行者句柄(防自等待,recv/send 共用) |
| `Drivers/BSP/NET/net_pending.c` | `net_pending_register` | 登记挂起 recv(两守卫) |
| `Drivers/BSP/NET/net_pending.c` | `net_pending_tick` | 每轮 recv 完成检查(①②③ + 探针) |
| `Drivers/BSP/NET/net_pending.c` | `net_pending_complete`(static) | recv 唯一通知出口(通知 + 清槽 + 记 errno) |
| `Drivers/BSP/NET/net_pending.c` | `net_pending_clear` | recv 幂等静默清槽 |
| `Drivers/BSP/NET/net_pending.c` | `net_pending_cancel` / `net_pending_cancel_all` | recv 显式取消(resync / 重建) |
| `Drivers/BSP/NET/net_pending.c` | `net_pending_settle_close` | recv 关闭前结算(排空或取消) |
| `Drivers/BSP/NET/net_pending.c` | `net_pending_active` | recv 挂起查询(纯 RAM 读,监听器保险) |
| `Drivers/BSP/NET/net_pending.c` | `net_send_pending_register` | 登记挂起 send(两守卫;记录进度起点 `sent`) |
| `Drivers/BSP/NET/net_pending.c` | `net_send_pending_tick` | 每轮 send 推进/完成检查(①断连②推进③写完④超时) |
| `Drivers/BSP/NET/net_pending.c` | `net_send_pending_complete`(static) | send 唯一通知出口(结果 = 已发字节数) |
| `Drivers/BSP/NET/net_pending.c` | `net_send_pending_clear` / `net_send_pending_cancel` / `net_send_pending_cancel_all` | send 静默清槽 / 显式取消 / 重建取消 |
| `Drivers/BSP/NET/net_pending.c` | `net_send_pending_active` | send 挂起查询(纯 RAM 读,监听器保险) |
| `Drivers/BSP/NET/net_socket.c` | `net_recv_locked` / `net_recv` / `net_recv_nb` / `net_recv_to` | 快速路径与挂起 recv 登记入口 |
| `Drivers/BSP/NET/net_socket.c` | `net_send_locked` / `net_send` / `net_send_sock` | 快速路径(有界自旋)与挂起 send 登记入口 |
| `Drivers/BSP/NET/net_socket.c` | `net_process_messages` / `net_wait_result` | 分派(含 `NET_MSG_SEND` 透传 `caller`)、哨兵抑制、两段式唤醒 |
| `Drivers/BSP/NET/net_socket.c` | `net_close_locked` / `net_drop_conn_locked` / `alloc_socket` / `net_init` | 结算、清槽、取消、捕获执行者(recv + send 双侧) |
| `Drivers/BSP/NET/net_listener.c` | `net_listener_retry_tick` | 消费 `net_pending_active` / `net_send_pending_active` 作挂起保险 |
| `App/task/net_task.c` | `net_task_func` | 主循环节拍(`net_poll` → `net_process_messages`) |
+318 -26
View File
@@ -1,13 +1,49 @@
# 网络自愈架构设计
> **定位**:本文是 CH395F 网络自愈架构的**现状权威描述**——设计结构、原理、关键参数及其实证锚点。
> 文中「收敛阶段 0~3」指 2026-10 自愈架构的分阶段实施(过程已归档,见 §9)。
> 文中「收敛阶段 0~3」指 2026-10 自愈架构的分阶段实施(过程已归档,见 §12)。
> **本文已吸收原《网络Socket监听生命周期设计说明》(B10 根治,2026-10-14 合并)**:
> 其原理与规范层论证并入 §1.1~§1.4、FUN_PARA bit3 契约并入 §4.9、代码级事实并入 §9、
> 决策记录并入 §10、模块化边界并入 §11;原文档已删除,过程全文在 git 历史
> (`git log --oneline -- docs/子系统设计/网络Socket监听生命周期设计说明.md`)。
> 架构审计过程(T1~T4 三路审计与 39 条编号发现、E0~E15 实验方法与原始记录、分阶段实施清单)
> 已完成使命并删除——过程全文在 git 历史(`git show 1a35b36^:docs/网络/N34-自愈审计/`,目录合并前
> 7 份文档)。实证结论已沉淀进:`docs/问题记录/CH395F_Trap_Records.md`(TRAP-NET-01~21 与 B 系列)、
> `docs/子系统设计/网络Socket监听生命周期设计说明.md`(D1~D10 与模块化边界)、`docs/芯片手册/CH395F.md`。
> `docs/芯片手册/CH395F.md`。
> 仍被代码与文档引用的 12 条编号发现保留在 §7 速查表维持可寻址。
## 导读:本文用到的编号与术语
本文跨越 2026-10 的多轮审计与实验,正文里混用了**多套编号**与**机制代号**。
它们都是**冻结标识**——代码注释、`test/host/` 探针脚本、`docs/问题记录/` 都直接引用,
**看懂即可,不要重排或改名**(改号即断链)。查下表:
| 记号 | 是什么 | 在哪查 |
|---|---|---|
| `I1`~`I8` | 本架构的**不变量**(Invariant)——违反即有实测后果 | §2 |
| `E0`~`E15`、`E-02` | **台架实验**编号(E = Experiment):每个实验是「假设 → 方法 → 判据 → 门禁哪个实施决策」 | §5.2(原始记录已归档,见 §12) |
| `F-NET-nn` / `F-DRV-nn` / `F-MS-nn` | 审计的**编号发现**:NET 层 / 驱动层 / 状态镜像层 | §7(只保留仍被引用的 12 条) |
| `M1`~`M5` | 实验固件里的**候选修改代号**:M1 沉降无条件化 · M2 门控换 `net_listener_armed` · M3 可疑门 · M4/M4-lite 删沉降段全局读即清 · M5 `sock_reset` quiescent 排空 | §4.1、§4.3 |
| `形态①`~`形态⑤` | `net_reconcile_tick` 的五类**失同步判据** | §4.1 |
| `U1`~`U15` | 状态迁移的**"无人负责"组合**(审计枚举出的架构欠债) | `docs/问题记录/CH395F_Trap_Records.md` B10 行 |
| `TRAP-NET-nn`、`B3`/`B10` | **陷阱记录**条目与 B 系列(待修/已修清单) | 同上 |
| `N-nn` | `docs/任务规划/登记簿.md` 的**任务/发现编号** | 该登记簿(⚠ `N-32` 等**已销项**——登记簿行按"规划只留待办、完成项删除"约定已删,本文引用处即其最终事实描述) |
| `D1`~`D7` | 本文**决策记录** | §10 |
| `T1`~`T4` | 审计的**三个视角**(已归档,勿再引用) | git 历史(§12) |
| `P0`~`P3`、`阶段 0`~`阶段 3` | 已完成的**实施阶段**(前者 = B10 根治,后者 = 自愈收敛),其勾选清单勿再引用 | §12 |
| `TSL` / `TSC` / `TSD` | SPI 命令与数据之间的**时序间隔**要求 | `docs/芯片手册/CH395F.md` §7.4 |
| `n30` | 压测脚本 `n30_repro.py`(:21 背靠背 STOR 30 轮),自愈回归的**主判据** | `test/host/` |
| 沉降窗 / 隔离期 | 释放会话后、重建监听前的**最短等待**(GRACEFUL 100 ms / QUARANTINE 400 ms) | §4.3 |
| 让位窗 | M3 可疑门交给形态① 电平二次确认的 **400 ms** | §4.1 |
| 镜像 | net 层持有的 8 态 `net_sock_state_t`——**会话投影**,不是芯片事实源 | §3.3 |
> ⚠ **字母 `E` 有两套编号,靠连字符区分**:
> - `E0`~`E15`(**无**连字符)= 本文的**自愈实验**编号,逐条见 §5.2;
> - `E-02`(**带**连字符)= `docs/问题记录/错误码传播审计.md` 的**审计发现**编号——
> §5.2 末行引用它是为了记录该发现的**收口台架**,**不是**实验编号
> (证据:`test/host/e02_probe.py` 文档串自述"错误码传播审计.md E-02 的 timeSync 触发形态");
> - 该审计编号族冻结于 `E-63`,不再新增。
## 1. 为什么是这个架构
三个芯片事实决定架构形态(全部台架实证,见 `docs/问题记录/CH395F_Trap_Records.md` TRAP-NET-09/-17/-18/-20
@@ -26,6 +62,69 @@
组合:net 层持有损 8 态镜像并作为决策依据 + 状态迁移只靠中断事件 + 重建与事务脱钩。
现架构(§2~§4)即按此收敛的结果。
### 1.1 芯片层事实:LISTEN 不是能力,是一个会被消耗掉的槽位状态
| 事实 | 出处 |
|---|---|
| LISTEN 与 ESTABLISHED 是同一 TCP 状态字节的**互斥取值**(01H/04H) | `docs/芯片手册/CH395F.md` §5.22 L535-545 |
| 「Socket 会一直检测连接事件,**直到连接成功后**……每个 Socket 只可以建立一个连接」 | `docs/芯片手册/CH395F.md` §5.29 L609 |
| 连接结束后芯片**永不**自动回 LISTEN——全手册无此条款;重连必须"重新打开 Socket 再 CONNECT" | `docs/芯片手册/CH395F.md` §5.30 L615、§9.2.5 L1266 |
| 断开后芯片行为由 FUN_PARA bit3 分流:=0 芯片自动关 Socket(→`0x00/0x00`);**=1 芯片什么都不做(→`0x05/0x00`),MCU 必须自己 CLOSE** | `docs/芯片手册/CH395F.md` §5.23 L572、§5.52 L795② |
⚠ 本仓 `ch395f_set_fun_para(0x08)`(`Drivers/BSP/NET/net_socket.c`,bit3 `SOCKET_CLOSE`=1,
TRAP-NET-15/-20 定案为"保留排空现场")⇒ **断开后芯片必然停在 `0x05/0x00`**。
探针看到的"孤儿芯片态"不是芯片异常,而是本仓这个配置的**直接且合法的后果**;
bit3=1 的必要性与安全边界见 §4.9。
### 1.2 规范层:本仓做法与 TCP/POSIX 的所有权契约正面冲突
- RFC 1122 §4.2.2.18:被动 OPEN「**MUST NOT affect any previously created connection record**」。
- POSIX `accept()`:「The original socket remains open and can accept more connections」。
- lwIP 把"监听器归谁"直接写进 `tcp_close()` 文档:只有 socket 层创建的监听器才会被连带关闭,
raw API 自建的不会——**所有权是 API 契约的一部分,不是调用方的自觉**。
本仓让同一个 fd 兼任"监听器"和"连接"两个角色(handle/resource aliasing),
于是"关掉一条连接"和"毁掉一个监听器"在类型上无法区分——这正是红线 8
(「EOF 时不得 `net_close` 监听 fd」,§6)存在的唯一原因。
**需要靠铁律维持的正确性,就是架构欠债的凭证。**
### 1.3 实现层:fd 别名 → 边沿通知 → 多所有者
EOF 只在 **recv 那一刻**本层为 CLOSED 才给出(`net_recv_locked`,`Drivers/BSP/NET/net_socket.c`);
若该 fd 已被别的路径 relisten 回 LISTENING,同一个 recv 恒返 `WOULDBLOCK`。
这是一个**边沿**,不是一个**电平**——正是不变量 I2/I3 要消除的形状。
由此派生出"谁重建监听"的**12 个所有者**:栈内 8 处(对账、`handle_disconnect_event` 监听分支、
`net_accept_locked` CLOSED 分支、`net_recv_locked`、`net_pending_tick`、`NET_MSG_RELISTEN`、
`net_resync_listen`、`net_listen_locked`)+ 应用层 4 处(lftpd、time_sync、ui_net_sink、net_task 代次);
状态迁移点 15 个,其中 **15 个"无人负责"组合**(U1~U15,见 `docs/问题记录/CH395F_Trap_Records.md` B10 行)。
收敛后周期性决策已唯一化为 §4.1 形态③④、消费者驱动即时重建按 §4.2 保留(3 处)——
上段是欠债的**原始形状**,保留以说明"为什么需要 I2"。
三个已实测的派生失效(机理保留,供判读 I2/I3 被违反时的症状):
| 失效 | 机理 | 现状 |
|---|---|---|
| 孤儿监听(B10) | 边沿没人接 → net 层等应用重建、应用等 net 层给 EOF、对账只裁 ESTABLISHED/LISTENING 直接跳过、芯片不会自己回 LISTEN → **四方互等** | 已收口(§4.1 形态④ + §4.6 `net_drop_conn`) |
| 消费者卡死 | 抢先 relisten 把慢消费者的 EOF **永久**吞掉 → `g_client` 不清、新连接被旧会话静默吸收 | 已由 `session_dead` 粘滞电平消除(§3.3 不变量 I) |
| 监听被误关 | `net_can_read` 在非 ESTABLISHED(**含 LISTENING**)返 1 → 消费者 recv 得 `WOULDBLOCK` → 判"真错误"后无护栏 `net_close` 等于**关掉监听 fd** | 已加 `g_client != g_listen` 护栏(`Drivers/BSP/UI/ui_net_sink.c`);落 TRAP-NET-09 沉降窗 |
### 1.4 为什么不走"芯片原生多连接模式"(A′ 出局)
芯片确实自带 lwIP 式模型:FUN_PARA bit1(≥0x44,本仓实测 0x4A)下
「服务器的 Socket **仅用来监听**,单片机需要分配其它 Socket 用于该连接」(`docs/芯片手册/CH395F.md` §9.2.6 L1295)。
但本产品**不可用**,三条硬证据:
1. **不支持多监听**——`docs/问题记录/CH395F_Trap_Records.md` 附 FTP 改造清单第 4 条
「CH395F 多连接模式不支持多监听(类型:架构)」,处置即 `fun_para=0x00`。
本产品有 21/8888/7003 **三个常驻监听** + 会话期动态 PASV 监听。
2. 池分配按**源端口匹配**(`docs/芯片手册/CH395F.md` §9.2.6 L1295),多监听器的池会互相串。
3. 历史实测:「多连接模式下最多接受 3 个客户端(Socket 1~3),Socket 4~7 的 CONNECT 中断从不触发」、
「多个 Socket 数据互相覆盖」——即 TRAP-NET-02/-11。
且 FUN_PARA **初始化后不可改**(`docs/芯片手册/CH395F.md` §5.52 L795⑥),这是开机一次性决策,
不能运行时降级。**结论:A′ 出局;根治只能在单连接复用模型内做**(红线 2,§6)。
## 2. 设计原则(不变量)
| # | 不变量 | 违反的后果(均实测) |
@@ -196,6 +295,55 @@ reconciler 保留分级去抖,因为形态①要让位 M3 窗、形态②要
- **E6 裁决(2026-10-13,~4h 常态 + 每小时 :7003×10 + E1 压测):命中=0 → 内化不
立项**,上层盲收保留现状(I8);将来出现数据面失配再武装重裁。
### 4.9 FUN_PARA bit3=1 的必要性与安全边界(D4)
> 结论:**必须保留 `0x08`**。本节以手册原文 + 实测证据回答"bit3=1 引出这么多问题,
> 是否该改回 0",并给出软件侧的收口位置。
**① 必要性 = 硬性必需,不是可调项。** 手册对 bit3 的分流写得很直白:
- `docs/芯片手册/CH395F.md` §5.52 注释②:「此位为 0 表示在产生 SINT_STAT_DISCONNECT 或者 SINT_STAT_TIM_OUT 中断后,
CH395 会主动关闭 Socket。……**关闭 Socket 就意味着 CH395 会将此 Socket 内部缓冲区和
一些相关变量清除**,在某些应用中,如果 CH395 内部自行关闭 Socket 可能会导致残留的数据
被强行清除。这种情况下可以将该位置 1,**由单片机将数据读出来,然后再关闭 Socket**。」
- `docs/芯片手册/CH395F.md` §5.23 L572 同向:「为 0 则……主动将 Socket 状态置为关闭状态**并将相关缓冲区全部清除**;
反之则不会对 Socket 状态和相关缓冲区做任何操作,以方便外部单片机……**将残留的数据读出**。」
实测现场(TRAP-NET-20 判别探针):`st=0x05/0x00` 且 `recv_len=1027`——
Socket 仍 OPEN、芯片 TCP 已 CLOSED、**RX 缓冲里 STOR 载荷一个不少**。这正是 bit3=1 按规格
保留现场,不是芯片异常(`docs/问题记录/CH395F_Trap_Records.md` TRAP-NET-20「定案二」)。
**② bit3=0 的代价是「硬件保证的数据丢失」,且不可在软件层补救。**
本产品最典型的流量形态是「发完即 FIN」:ftplib `storbinary` 语义 = `sendall` 发完立即关
数据连接、回控制通道等 226。芯片把最后一段数据落进 RX 的同时抛出 DISCONNECT:
- bit3=1:RX 数据在,MCU 来得及读(TRAP-NET-18 的「排空 RX 再 close」修复才成立);
- bit3=0:芯片**当场**关 Socket 并清缓冲 → MCU 永远读不到尾部 → 文件被截断却仍回报 226
(静默损坏)。这是 bit3=0 的**必然结果**,不是概率问题。
即:改 bit3=0 等于用「一个硬件保证、不可修的数据丢失」去换「一个软件侧可修的关闭义务」,
严格更差。:8888 对时包、:7003 远显推流同样是「数据 + 随即关闭」形态,一并受害。
**③ bit3=0 也并不能真正消除孤儿态。** 芯片自动关以**收到** DISCONNECT/TIM_OUT 为前提,
而 TRAP-NET-17(E14 复核)已实证快速复连场景这两个中断会被读清消费掉
(`docs/问题记录/CH395F_Trap_Records.md` B10 行)→ 事件丢了,bit3=0 同样停在已关/半关态,
孤儿 Socket 照样存在。它只是降低出现概率,代价却是第②条。
**④ 所以「这么多问题」不归 bit3=1。** 断开后停 `0x05/0x00` 是 bit3=1 的**合法后果**(D4);
真正造成 B10 永久拒连、消费者卡死、EOF 被吞没的,是软件侧没把「该由 MCU 关 Socket」这条
义务落到唯一所有者(§1.3 的 12 所有者 / 15 组合),以及 U12 的「relisten 的 LISTEN 失败
不 close 芯片 Socket」与 open 失败路径不对称。bit3=1 只是把义务显式化,把原本被芯片
掩盖的架构欠债照了出来。
**⑤ 安全处理 = 把 bit3=1 的契约在软件侧收口**(逐条对应上面的缺口):
| 契约要求 | 对应缺口 | 收口手段(现状) |
|---|---|---|
| 「会话结束」必须**谁都能看见**,不能只在边沿那一刻 | EOF 边沿化:抢先 relisten → recv 永久 `WOULDBLOCK` | 不变量 I:`net_sock_t.session_dead` 粘滞电平,`net_recv_locked` 判据 `state==CLOSED \|\| session_dead`(§3.3) |
| 关闭 Socket 必须**一定有人做**、且只做一次 | 重建所有者 12 个 + U12 的 LISTEN 失败不回 close | 不变量 II:重建决策唯一化为 `net_reconcile_tick` 形态③④ + `net_listener_try`(§4.1 / §4.2) |
| 关闭姿势必须正确 | TRAP-NET-12:陈旧 DISCONNECT 锁存秒断下一条连接 | `sock_reset` 序列 = 排 RX → CLOSE → 排事件 → 等 `byte0=00H`(§4.4) |
| 异常释放须先排空残渣再关 | TRAP-NET-18:~2KB RX 残渣被下条连接当自己的数据读走 | 隔离期持续排空 RX + 主动 close + 等 `CLOSED/TCP_CLOSED`(§4.3) |
## 5. 关键参数与实证锚点
### 5.1 参数表
@@ -212,27 +360,36 @@ reconciler 保留分级去抖,因为形态①要让位 M3 窗、形态②要
| recv tripwire | 60s | 阶段 1.5(完成逻辑缺位探针,不改语义) |
| 恢复窗(正常断连→监听可用) | 0.3~1s | E1 发现 A:释放只置事实 → 等周期 tick → SYN 静默丢 → 客户端 1s RTO → `:7003` 复连 p50≈1020ms |
### 5.2 实验锚点速查(E 编号被代码与陷阱记录引用,结论一句话)
### 5.2 实验锚点速查(E 编号 = 台架实验序号)
| E | 结论 | 落点 |
|---|---|---|
| E0 | 常态 3.5h 对账零命中(冷路径);D10 逐轮留痕是主要日志噪声源 | reconciler 热路径成本可接受;重试日志只留首末 |
| E1 | `:7003` 复连 50/50 p50≈1020ms(恢复窗 0.3~1s);背靠背 STOR 4/50 失效循环(发现 B) | P0 止血包;`n30_repro` 纳入常备回归 |
| E5 | 静默回收形态 ~110 次连接零复现(代码缺口真实、触发罕见) | 阶段 1 顺带修(形态⑤),不单独立项 |
| E7/E7b | M1+M2+M4-lite:n30 8%→96.7%(116/120);M1 不带 M4-lite 会放大 F-NET-7 | P0 转正(`9ac9879`);M1/M4-lite 同批纪律 |
| E8 | 故障率 2%~54% 状态依赖(残渣存量);风暴三机制(F-NET-17) | M3/M5 设计 + "沉降让位救回"红线 |
| E9 | M3 判据三轮迭代定案(资产门版);风暴免疫 + 零误伤 | §4.1 M3 门(`ae17d36`) |
| E10 | 阶段 0/1 转正全绿;**armed 必须放行 ESTABLISHED**(i104 18/18,127s→8.3s) | §4.2 门控 |
| E11 | 服务器侧 KeepAlive 真实可用;判死 ≈30.0s(手册公式不成立);静默存活不误杀 | §4.7 转正 |
| E12 | TSL 规格偏离:460 轮/≈58MB 双向 SHA-256 无可复现危害、两臂无差异 | §6 红线 6;B 变体留作待用补丁 |
| E13 | 风暴态根因 = 僵尸会话(确定性复现);RSTI←NRST 推翻"跨烧录存活";自愈链端到端有效 30~90s | §4.7;PASV 预清缺口由 `sock_reset` 收口 |
| E2 | 单次回读 18.1µs;LISTEN 可见 ≤10ms 无长尾;LISTEN 态 CLOSE 二段(byte0 留 05H) | 缓存刷新策略;② 去抖 1 轮;`sock_reset` 二段补发 |
| E3 | OPEN/LISTEN/CLOSE 命令状态无判别力 + 陈旧 0x17 跨命令存活;SET 类完全不更新 | `cmd_done` 判据 = 状态回读 |
| E4 | 复位序列 A(排RX→CLOSE→排事件→等00H)20/20 vs C(什么都不做)0/20 级联自持;rxdrain=2048 | §4.4 序列定案;两模式都必须排 RX |
| E6 | U15 命中=0(~4h 常态 + 压测;用户裁决提前收线) | §4.8 观察项 |
| E-02 | `drop_conn` 时序定案:配合对端 TIME_WAIT ~150ms→形态④;不配合 FIN_WAIT_2 长尾 | §4.6 |
| E14 | "丢中断"复核(2026-10-14,48 轮直读 SINT):芯片侧事件丢失 = 0;"丢失"全部 = drain 读清消费(3/48,drain-int 现场抓获)+ 门控竞态(2/48)+ 主机同步(1/48);幽灵态本轮 0/48 未复现 | §1 事实 2 改写;TRAP-NET-17/-20 归因更正(机制保留不变);生产 listen/relisten 排事件挪至 LISTEN 命令**前**(根除自伤窗) |
| E15 | 幽灵态归因复核(2026-10-14,官方出处):0x04+旧端口 = 芯片**如实报告**旧连接拆链进行中——① bit3 `SOCKET_CLOSE=1` 为我方设置(版本门控,§5.52②:DISCONNECT/TIM_OUT 后芯片按设计不动状态与缓冲);② 拆链走 RFC 9293 §3.3.2 状态机:主动关闭 FIN_WAIT_1→FIN_WAIT_2→TIME_WAIT(2MSL 驻留;TIME_WAIT 协议目的=防旧连接迟到报文伤新连接——"断连即重连被拒"的协议根源),芯片自动走完(手册 §5.22);§5.17 默认重传 12 次、实测耗尽 ≥3s,与拆链窗实测 >2s 同量级;③ WCH 官方博客确认"断连后立即重连被拒、需程序侧特殊处理" | TRAP-NET-20"谎报"→"如实报告";settle 等待 = 官方方案;幽灵甄别 = 强制放行路径的护栏;I3 就此加强 |
> **`E` 是什么**:`E` + 序号 = **台架实验编号**(E = Experiment),不是错误码、也不是小节号。
> 每个实验遵循「假设 → 方法 → 判据 → 它门禁哪个实施决策」四段式;下表给出方法与结论,
> 原始记录(假设、原始日志、统计)随审计过程归档,取用命令见 §12。
> ⚠ **这些编号是冻结标识**:`net_socket.c` / `net_listener.c` 注释、`test/host/e02_probe.py`、
> `test/host/e6_soak.py`、`docs/问题记录/CH395F_Trap_Records.md` 都直接引用它们,**重排或改名会断链**。
> ⚠ 本表**末行 `E-02` 不是实验编号**——它是 `docs/问题记录/错误码传播审计.md` 的**审计发现**编号
> (`E-<nn>` 族,冻结于 `E-63`),列在此处只为记录该发现的**收口台架**。
> 区分规则:**带连字符 = 审计发现,无连字符 = 自愈实验**。
| 编号 | 实验做了什么(方法) | 结论 | 落点 |
|---|---|---|---|
| **E0** | 直接 grep 生产常态串口日志(gen=149 窗口,开机后 ~3.5 h,零台架成本) | 常态下对账**零命中**(冷路径);日志噪声主要来自 D10 重试的逐轮留痕 | reconciler 热路径成本可接受;重试日志只留首末 |
| **E1** | 现网固件 + PC 侧压测:`n30_repro.py` 背靠背 STOR、`ftp_regression`、`:7003` 100 ms×50 快速复连、iec104 连断 ×10 | `:7003` 复连 50/50、p50≈1020 ms(恢复窗 0.3~1 s);背靠背 STOR 出现 4/50 失效循环 | 促成止血;`n30_repro` 纳入常备回归 |
| **E2** | 裸机实验固件(`ch395f_reuse_bench.c` + DWT 周期计数器):测 `GET_SOCKET_STATUS` 单次 SPI 耗时;LISTEN 命令后 0/10/50/100 ms 回读,看首次观察到 TCP=01H 的延迟 | 单次回读 **18.1 µs**(每轮全量刷新成本可忽略);LISTEN 可见延迟 ≤10 ms 无长尾;**LISTEN 态 CLOSE 是二段**——一次只拆监听,byte0 恒留 05H 不自落 | 驱动缓存刷新策略;reconciler 形态② 去抖降到 1 轮;`sock_reset` 内置二段补发 |
| **E3** | 同场固件,对 OPEN / LISTEN / CLOSE / SET_* 五条命令各 5 轮 ×2 遍 = 50 样本,看 `GET_CMD_STATUS` 有无判别力 | OPEN / LISTEN / CLOSE **无判别力**,且陈旧错误码 0x17 跨命令持续存活;SET 类**完全不更新**状态 | 命令完成判据改用**状态回读**(`ch395f_cmd_done`) |
| **E4** | 裸机 3 变体 ×10 轮 ×2 遍 = 60 轮:先造污染会话(发 3000B 后 RST),按变体复位后**立即**重开监听并上传 4096B 图案载荷,逐字节校验 | 变体 A(排空 RX→CLOSE→排空事件→等 byte0=00H)**20/20**;变体 C(什么都不做)**0/20** 级联自持;实测 `rxdrain=2048`(芯片单 Socket RX 缓冲满容) | §4.4 复位序列定案;两档沉降**都必须排 RX** |
| **E5** | :7003 上 connect→发 1B→对端 FIN→等 3 s→复连 ×10,查"芯片静默回收回 LISTEN 却不发 DISCONNECT"能否复现 | 20 次 FIN **全部**产生 `DISCONNECTED`;静默回收形态在 E1+E5 累计 ~110 次连接中断里**零复现** | 缺口代码上真实但触发罕见 → 顺带修(对账形态⑤),不单独立项 |
| **E6** | 生产路径插桩固件(`NET_U15_PROBE=1` + `test/host/e6_soak.py`):常态运行 + 每小时 :7003×10 + 压测,探"`GET_RECV_LEN>0` 但 `!readable`"失配 | 命中 **0**(~4 h,用户裁决提前收线,原规格 24 h) | U15 登记**观察项**、不立项(I8);上层盲收保留现状 |
| **E7 / E7b** | 实验固件加候选修改 M1(GRACEFUL 沉降无条件化)+ M2(门控换 `net_listener_armed`),再叠加/去掉 M4-lite(删沉降段全局读即清),跑 `n30` 压测 | M1+M2+M4-lite 把 `n30` 成功率从 **8% → 96.7%**(116/120);**M1 单独上会放大 F-NET-7** | 转正(`9ac9879`);立下「M1 与 M4-lite 必须同批」纪律 |
| **E8** | 同固件、同负载,**只变设备状态**跑 `n30` 矩阵(全新启动 / 探针+压测累积后 / 仅 ST-Link 复位) | 故障率在 **2%~54%** 间随状态摆动 → 故障率 ∝ 芯片侧残留存量;抓到"风暴签名"三机制(F-NET-17) | M3 可疑门 + `sock_reset` quiescent 排空;"沉降让位救回"红线 |
| **E9** | M3 原型三轮迭代(可疑门判据:芯片 `0x05/0x00` + `lost_conn_seen==0` + `recv_bytes==0` + 芯片 RX 有在途数据) | 判据定案为**资产门版**;风暴态免疫 + 零误伤 | §4.1 的 M3 门(`ae17d36`) |
| **E10** | 阶段 0 九项 + 阶段 1 六项转正后的全量台架验证 | 全绿;暴露 **`net_listener_armed` 必须放行 ESTABLISHED**——漏放行时 i104 的 200 ms 轮询抢不过形态② 去抖而整场饿死(18/18 FAIL,127 s→8.3 s) | §4.2 门控定案 |
| **E11** | 临时固件开启服务器侧 KeepAlive(`CMD_SET_KEEPALIVE_SN`,60s/5s/3),用"禁用 PC 网卡"造"链路在、对端静默" | 真实可用;判死 **≈30.0 s**(早于手册公式 75 s,以实测为准);静默但**存活**的对端不误杀 | §4.7 转正 |
| **E12** | SPI 缓存区 TSL 时序 A/B:手册要求"长度与数据之间"留 TSL(读 0.4 µs / 写 0.2 µs),本仓把命令头与数据打包进同一 DMA 突发(间隔=0),对照两臂 | 460 轮 / ≈58 MB 双向 SHA-256 **无可复现危害**、两臂无差异 | §6 红线 6;"两段+TSL"变体留作**待用补丁** |
| **E13** | 产品固件 + PC 网卡抖动注入,尝试确定性复现 E8/E12 记录的"残渣风暴态" | 根因 = **僵尸会话**(对端静默死亡、芯片侧 ESTABLISHED 未清、单连接监听被占),而非跨烧录残渣;`RSTI←NRST` 推翻"跨烧录存活";自愈链端到端 30~90 s 有效 | §4.7;PASV 预清缺口由 `sock_reset` 收口 |
| **E14** | 48 轮直读 SINT 矩阵复核"丢中断"说法(drain-int 现场抓取 + SINT 补读 ×3 + 超时归类) | **芯片侧事件丢失 = 0**;"丢失"实为 drain 读清消费 3/48 + 门控竞态 2/48 + 主机同步 1/48;幽灵态本轮 0/48 未复现 | §1 事实 2 改写;生产 listen/relisten 排事件挪到 LISTEN 命令**之前**(根除自伤窗);TRAP-NET-17/-20 归因更正(机制保留不变) |
| **E15** | 幽灵态归因复核(WCH 官网 + 手册出处) | `0x04` + 旧端口 = 芯片**如实报告**旧连接拆链进行中,不是谎报:bit3=1 是自家配置(手册 §5.52②);拆链走 RFC 9293 §3.3.2 状态机(FIN_WAIT_1→FIN_WAIT_2→TIME_WAIT,2MSL 驻留);拆链窗实测 >2 s 与手册 §5.17 默认重传 12 次耗尽 ≥3 s 同量级;WCH 官方博客确认"断连即重连被拒"、官方方案 = 关闭完成后重开 | TRAP-NET-20 口径"谎报"→"如实报告";settle 等待 = 官方方案;幽灵甄别 = 强制放行路径的护栏;I3 就此加强 |
| **E-02**(审计编号) | `test/host/e02_probe.py` 台架定案 `net_drop_conn` 收口后的时序(阶段 3 产品固件) | 对端配合(收到 FIN 后其内核 close):`TIME_WAIT` ~150 ms → 形态④ relisten,恢复全程 ~500 ms;对端内核存活但不 close:芯片停 `FIN_WAIT_2` 长尾,**监听恢复受限于对端 close** | §4.6 |
## 6. 红线(明确不做的事)
@@ -280,10 +437,145 @@ reconciler 保留分级去抖,因为形态①要让位 M3 窗、形态②要
3. **U15 观察项**:若将来出现数据面失配(RX>0 但 `!readable`),重新武装探针重裁(§4.8)。
4. **链路事件业务消费**:缓存与留痕已就绪;PHY_CHANGE → 自动重配的触发待产品需求。
## 9. 引用与归档约定
## 9. 代码级事实清点
> 判读本架构或做决策时需要的**源码事实**。⚠ 只给符号名、不固化行号——行号随重构漂移
> (本仓曾出现文档内两处行数快照自相矛盾)。行数快照(2026-10-14):
> `net_socket.c` 3702 行、`net_listener.c` 467 行、`net_listener.h` 97 行。
### 9.1 会话投影的状态写入面
`net_sock_t.state` 的**全仓唯一写者**是 `net_socket.c`(13 处显式赋值 + 2 处 `memset`);
`net_types.h` 只定义、不写。因此"镜像说 A、芯片是 B"的窗口只可能由这一个文件制造——
这是 I1(镜像降级为会话投影)收敛成本低的根本原因。
### 9.2 关键状态门(做决策必须知道)
| 函数 | 状态门 |
|---|---|
| `net_listen_locked` | **只允许 BOUND / CREATED**——对已 CLOSED 的监听 fd 直接 `net_listen` 返 INVAL |
| `net_connect_locked` | **无状态门**(可在 LISTENING 上改成 CONNECTING) |
| `net_recv_locked` 快路径 | **无状态门** |
| `net_listener_armed` | 唯一监听门控;**必须放行 ESTABLISHED**(§4.2) |
| `net_listener_try` | **无状态门**——对已 LISTENING 的 fd 再 open+listen 会因 USE 错误走进关闭分支、**关掉正听着新 SYN 的 Socket**(U12 同源事故);故两处调用点须自补内层 `state == CLOSED` 门 |
### 9.3 三个服务的契约差异(历史成因,现状已对齐)
| 服务 | 曾认为的连接结束信号 | net 层实际给什么 | 监听重建者 | 是否留无人重建组合 |
|---|---|---|---|---|
| `lftpd` 控制 :21 | `read_line!=0`(EOF→-1) | 阻塞 pending 保证 CLOSED 时挂起 recv 必被结束(FIN→0 / 异常→CONNRESET) | net 层;应用**永不** close 控制 fd | 无(契约一致,**正面样板**) |
| `time_sync` :8888 | EOF / 预算耗尽 / 错误,随后**无条件** `net_close(conn==ls)` | 同上 | 应用自己每会话全量重建 | 不留孤儿,但违反红线 8 → close→重建沉降窗(TRAP-NET-09)+ 最坏 ~10 s 端口下线;**已改走 `net_drop_conn`**(§4.6) |
| `ui_net_sink` :7003 | `net_recv` 返 0 或真错误;重建条件 `net_get_sock(g_listen)==NULL`(fd 不释放 → **恒假**) | 该 0 只在 recv 时刻 `state==CLOSED` 才给 | 名义 net 层,实际自身条件在 MCU 恒假 | **有**(B10 与"消费者卡死"都出自这里);**已收口**(§1.3 / §4.1) |
| 模拟器后端 | `recv` 0 恒可得、fd 分离 | Winsock 原生 | 无需 | 无——且对上述全部失效模式**零覆盖**(D6:双模型分裂暂不消除) |
## 10. 决策记录
| # | 决定 | 依据 |
|---|---|---|
| D1 | **A′(启用芯片多连接模式)出局**,根治必须在单连接复用模型内做 | §1.4:多监听不支持 / 池按源端口匹配会互串 / 历史实测 TRAP-NET-02、-11 / FUN_PARA 初始化后不可改 |
| D2 | 采用**形态 B 深化**(电平化 + 单一所有者),**不做 P3**(listener 一等对象 + `accept` 返回虚拟句柄) | P3 需控制面重写 15~25 函数、RAM/Flash 上升,只有"同端口并发多连接"需求才值得 |
| D3 | 周期对账**保留**,其副作用由不变量 I 消除 | TRAP-NET-17 实证事件面不是可靠唯一信源(E14 复核:非芯片侧丢失,是读清消费)→ 周期对账不可删 |
| D4 | `FUN_PARA` **保持 `0x08`**,不因孤儿态改配置 | bit3=1 是 TRAP-NET-15/-20 定案的"保留排空现场"契约;bit3=0 会令芯片在 DISCONNECT/TIM_OUT 时**当场清缓冲** →「发完即 FIN」的尾部数据必被硬件擦除、**不可补救**。完整论证见 §4.9 |
| D5 | 顺序 **P0 → P1 → P2**,每阶段独立提交、独立回归、可单独回滚 | P1 不动 fd 模型却同时消掉 B10 与"消费者卡死"两类事故,性价比最高 |
| D6 | 真机/模拟器**双模型分裂暂不消除** | 消除即等于做 P3;先靠不变量 I/II 把应用侧契约对齐 |
| D7 | 同族残留**不在监听生命周期根治范围**,各自单列:U1(CONNECTING 滞留)、U11、U12、U14、U15,以及审计项 B4~B9 | 与监听生命周期不同因,混做会让回归面失控 |
## 11. 监听生命周期的模块化边界(`net_listener.c`)
> **动机**:真机 net 层 `net_socket.c` 一度 4126 行,而模拟器做同样的事只有 807 行(5 倍);
> `relisten` 一词在文件里出现 110 次。诉求 = **高内聚低耦合**。
> 先由 §4.1 把"周期性重建"收敛到唯一决策者,搬移边界才第一次清晰——**先收敛,再搬移**。
### 11.1 可机械检验的判据
| # | 判据 | 收敛前 | 目标 | 结果 |
|---|---|---|---|---|
| 1 | 模块外调用「重建监听」的**入口** | 8 处散点直调 + 2 处直写状态数组 | **单一入口 `net_listener_try`** | 4 个调用点(形态③ / accept CLOSED 分支 / recv EOF / `net_drop_conn`),无散点 |
| 2 | `s_relisten_*` 静态状态被**模块外直接读写** | 2 处外部写 | **0** | 0(外部写改为 `net_listener_settle`) |
| 3 | 依赖方向 | 无 | 新模块 → `net_socket.c` **单向** | 达成(不反向 include) |
⚠ **明确不承诺**:本重构**不消除 fd 别名**(同一 Socket 既当监听器又当连接)。
那是 CH395F 单连接模型 + bit1 多连接模式**不支持多监听**(§1.4)锁死的,属硬件上限。
重构只提升**组织度**,不改变架构上限——**别指望补丁消失,只指望补丁不再重复**。
### 11.2 边界划分
**搬入 `Drivers/BSP/NET/net_listener.c`**(**真机专属**,sim 不编):
| 搬入 | 理由 |
|---|---|
| `net_relisten_locked` / `net_listener_try` / `net_listener_retry_tick` / `net_listener_clear` / `net_relisten_done` | 「维持 fd 处于 LISTEN 态」的全部机制 |
| `net_drain_chip_rx` | 重建前的排空(只被 relisten 调用) |
| 7 个 `s_relisten_*` 数组 + 7 个 `NET_RELISTEN_*` 常量 | 状态与策略私有化 |
| ~~`net_listener_pump`~~ | 原唯一的周期性重建决策者——**阶段 3 已删除**,决策并入 `net_reconcile_tick` 形态③④(§4.1) |
**留在 `net_socket.c`**(并说明理由,避免"看着像就搬"):
| 留下 | 理由 |
|---|---|
| `net_listen_locked` | 是**应用 API 路径**(首次监听,含 BOUND/CREATED 状态门与缓冲)——属"连接层建监听",不是"维持监听" |
| `net_accept_locked` | 产出的是**连接**(LISTENING→ESTABLISHED 迁移),属连接层;它只**调用**新模块 |
| `net_lost_connect_probe` | 做的是"补 accept"(连接建立),不是重建监听 |
| `net_listener_armed` | **双端公开 API**(sim 也实现);纯 RAM 查询、很薄,留在原处避免 sim 缺符号 |
| ~~`net_resync_listen`~~ | **阶段 1 已删除**:其"只改本层状态"语义并入对账⑤(§4.1),手动 API 面退役 |
| `net_reconcile_tick` | ①②⑤ 是**连接**失同步自愈;③④/U11 亦已回归本 tick(阶段 3,§4.1) |
| `net_sock_t` 全部字段 | 数据归 `net_types.h`(双端共享核心),**不随模块走** |
> ⚠ `net_sock_t` 的 `last_peer_port` / `lost_conn_seen` 是**跨层字段**:`net_relisten_locked`
> 要写它们(转登幽灵甄别基准),`net_lost_connect_probe` 与对账②也要读写。
> **这是本次重构无法消除的残余耦合**(单连接模型的固有结果),明确登记而非假装不存在。
### 11.3 对外接口(4 函数 + 1 枚举,全部窄接口)
```c
/* net_listener.h —— 只被 net_socket.c include(内部头,不进公开 net_socket.h,故 sim 无依赖) */
typedef enum {
NET_LISTENER_SETTLE_QUARANTINE = 1, /* 异常释放(TIMEOUT/RST):隔离 400ms + 排空 RX 残渣 */
NET_LISTENER_SETTLE_GRACEFUL = 2, /* 正常释放(DISCONNECT/FIN):沉降 100ms */
} net_listener_settle_t;
void net_listener_settle(int sockfd, net_listener_settle_t level); /* 只置事实,不动作 */
int net_listener_try(int sockfd); /* 尝试重建一次(不阻塞);已在重试队列返 -1 */
void net_listener_clear(int sockfd); /* 清本 fd 的重建/沉降状态(新 fd 或 close 时) */
void net_listener_retry_tick(void); /* D10 重试轮询(netTask 每轮调用,非阻塞) */
```
**反向依赖(新模块需要宿主提供的)**:`net_sock_t` 定义(`net_types.h`,共享头,不算反向依赖)、
`net_set_errno` / `NET_ERR_RETURN_1`、`net_pending_active`(§11.4)、`ch395f_*` 驱动、`HAL_GetTick`。
`net_listener.c` **不访问** `s_net_socks`——一律用 `p_sock->ch395_sock` 或调用方传入的 `sockfd`。
### 11.4 落地结果与三处「非纯搬移」
`net_socket.c` 4126 → 3656 行(-471);新增 `net_listener.c` / `net_listener.h`。
`RW-data` 980 → 980、`ZI-data` 143060 → 143060 **完全不变**(数据总量不变);
`Code` / `RO-data` 各 -8 / -12 字节(相对 471 行搬移可忽略)。
接口由「8 处散点直调 + 2 处直接写数组」收敛为 **4 个窄接口**;`s_net_socks` 依赖**已消除**。
`@test` PASS + 回读 `No difference found`。
执行中暴露三处**必要**的"非纯搬移"(均已在代码注释留痕,是当前代码形状的成因):
1. **`net_set_errno` 由 `static` 提升为模块间可见** —— 否则新模块链接期报 `L6218E: Undefined symbol`;
`NET_ERR_RETURN_1` 宏随之迁到 `net_socket.h`(它依赖前者,不能只移宏)。
2. **新增 `net_pending_active(fd)`** —— `net_listener_retry_tick` 原有「本 fd 有在途挂起 recv 则跳过」
(L3 保险,防重开 Socket 摧毁在途连接)直接读 `s_net_pending[i].active`;新模块不能碰该 static
数组,故加薄访问器。**语义原样保留**。
3. **`net_listener.c` 必须真机专属** —— 它依赖 `ch395f_*` SPI 驱动,而 sim 是 Winsock 后端
(无 `ch395f_*`、无孤儿态、无重监听概念)→ **无法**在 sim 编译。故只注册 Keil 端,
并在 `scripts/check_registration.py` 的 `SIM_EXEMPT` **显式点名**(遵"显式清单 + 每行理由"原则)。
> **判据修订留痕**:原定「纯搬移可证 = Code / RO-data / RW-data / ZI-data 完全不变」**过严**——
> 接口改造必然改变代码生成。修订为:**RW-data / ZI-data 必须完全不变**(数据总量不变),
> `Code` / `RO-data` 允许极小变化。
## 12. 引用与归档约定
- 本文引用格式:`docs/网络/网络自愈架构设计.md` §N.M(紧邻写法);E 编号引 §5.2
(如"§5.2 E12"),F 编号引 §7。
- **勿引用已删除的过程章节**(T1~T4 审计表、实验方法与原始记录、分阶段勾选清单)。
过程考古:`git show 1a35b36^:docs/网络/N34-自愈审计/`(目录合并前 7 份文档)或
`git log --oneline -- docs/网络/N34-自愈审计/`。
- **勿引用已删除的过程章节**:N34 审计表(T1~T4、实验方法与原始记录、分阶段勾选清单),
以及原《网络Socket监听生命周期设计说明》的 P0/P1/P2 勾选清单与台架验收记录。
过程考古:
- N34 自愈审计:`git show 1a35b36^:docs/网络/N34-自愈审计/`(目录合并前 7 份文档)或
`git log --oneline -- docs/网络/N34-自愈审计/`;
- 监听生命周期设计说明(本文 §1.1~§1.4 / §4.9 / §9~§11 的来源):
`git log --oneline -- docs/子系统设计/网络Socket监听生命周期设计说明.md`。
+2 -2
View File
@@ -275,7 +275,7 @@ CH395F 以太网驱动与 NET 层的实证陷阱:现象、定案根因、修
| B7 | 低 | `ch395f_test_task.c` Phase 5 | 单次写 4093B 超 1KB 发送缓冲与 2048 DMA 载荷 → 整块被丢;注释仍写旧上限。写路径金丝雀(A6)会打 `[ERR]` | ⏳ 待修(chunk 改 ≤1024 或分块) |
| B8 | 低 | `ch395f_test_task.c` Phase 4 | 把 sock6 缓冲重排到块 24-27,与 `net_init` 固定布局中 sock4 的 24-27 **交叠**(§5.45 禁止) | ⏳ 待修 |
| B9 | 低 | `ch395f_reuse_bench.c`、`net_socket.c` | bench 用 `get_phy_status()!=0` 判链路(01H=断开也成立,应 `!= CH395F_PHY_DISCONN`);`NET_PHY_CHECK_INTERVAL_MS` 死宏;无芯片版本门控(§5.16 <0x44 不支持 ALL 版、§5.52 bit3/4 需 ≥0x46,实测 0x4A 无影响) | ⏳ 待修(前两项死代码/卫生) |
| B10 | **中** | `net_reconcile_tick` 认领范围 + `handle_disconnect_event` 释放分支 | **孤儿监听 → 端口永久拒连**:释放分支把状态置 CLOSED 等"应用重新监听",但 `ui_net_sink` 只在 `net_recv` 返 0(EOF)时才清 `g_client`,而 EOF 已被事件分支消费 → net 层等应用重建 / 应用等 net 层给 EOF / 对账只裁 ESTABLISHED·LISTENING 直接跳过 / 芯片不会自己回 LISTEN,**四方互等**。对账①③都要求本层处于 ESTABLISHED/LISTENING,**孤儿态恰好落在两类之外** | ✅ 已修:对账新增**形态④**认领孤儿(`in_use`+`state==CLOSED`+`auto_relisten`+`type==STREAM`+`local_port!=0`,芯片侧不在 LISTEN)→ `net_relisten_try`。⚠ `auto_relisten` 是决定性门:少了它出向连接会被误判孤儿 relisten 成监听器。⚠ ④ 的副作用:`net_recv` 的 EOF 是**边沿**——④抢先 relisten 会把慢消费者的 EOF 永久吞掉。**根治出口 = `net_drop_conn` + 生命周期设计**(自愈收敛阶段 3 已落):原理与分阶段方案 → `docs/子系统设计/网络Socket监听生命周期设计说明.md` |
| B10 | **中** | `net_reconcile_tick` 认领范围 + `handle_disconnect_event` 释放分支 | **孤儿监听 → 端口永久拒连**:释放分支把状态置 CLOSED 等"应用重新监听",但 `ui_net_sink` 只在 `net_recv` 返 0(EOF)时才清 `g_client`,而 EOF 已被事件分支消费 → net 层等应用重建 / 应用等 net 层给 EOF / 对账只裁 ESTABLISHED·LISTENING 直接跳过 / 芯片不会自己回 LISTEN,**四方互等**。对账①③都要求本层处于 ESTABLISHED/LISTENING,**孤儿态恰好落在两类之外** | ✅ 已修:对账新增**形态④**认领孤儿(`in_use`+`state==CLOSED`+`auto_relisten`+`type==STREAM`+`local_port!=0`,芯片侧不在 LISTEN)→ `net_relisten_try`。⚠ `auto_relisten` 是决定性门:少了它出向连接会被误判孤儿 relisten 成监听器。⚠ ④ 的副作用:`net_recv` 的 EOF 是**边沿**——④抢先 relisten 会把慢消费者的 EOF 永久吞掉。**根治出口 = `net_drop_conn` + 生命周期设计**(自愈收敛阶段 3 已落):原理与决策记录 → `docs/网络/网络自愈架构设计.md` §1.1~§1.4、§4.6、§10 |
## 台架注入陷阱(方法论,持续补充)
@@ -309,7 +309,7 @@ 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 规格偏离(E12 登记)**:手册 §7.4 要求缓存区命令"长度与数据之间"留 TSL(读 0.4us/写 0.2us),本仓两个缓存区函数把命令头与数据打包进**同一 DMA 突发**(间隔=0)属规格偏离;**实测未发现危害**(460 轮/≈58MB 双向 SHA-256 仅 1 次未复现异常,"两段+TSL"变体与现状无差异)→ 依 TRAP-NET-20 铁律暂不改产品路径,偏离与证据登记于此、变体留作待用补丁(A/B 结论锚点 `docs/网络/网络自愈架构设计.md` §5.2 E12,过程全文见其 §9 归档)。
- **SPI 通信**:每次事务 `ch395f_spi_begin()/end()` 包裹;分频 8(PCB 约束);大数据走 SPI2 DMA(`CH395F_SPI_DMA_BUF_SIZE`=2048+4=2052,`ch395f.h:148`;历史值 1500/4100 已作废)。⚠ **TSL 规格偏离(E12 登记)**:手册 §7.4 要求缓存区命令"长度与数据之间"留 TSL(读 0.4us/写 0.2us),本仓两个缓存区函数把命令头与数据打包进**同一 DMA 突发**(间隔=0)属规格偏离;**实测未发现危害**(460 轮/≈58MB 双向 SHA-256 仅 1 次未复现异常,"两段+TSL"变体与现状无差异)→ 依 TRAP-NET-20 铁律暂不改产品路径,偏离与证据登记于此、变体留作待用补丁(A/B 结论锚点 `docs/网络/网络自愈架构设计.md` §5.2 E12,过程全文见其 §12 归档)。
- **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/网络/网络自愈架构设计.md` §4.7 与 §5.2 E11/E13)。
- **Socket 缓冲**:默认只给 Socket 0~3;Socket 4~7 必须 `open_socket` 前显式分配(TRAP-NET-02);现行固定布局 6 块/Socket。