a275539c17
task-9 让内容页黄金从 47 涨到 53(通讯设置页六态),本笔只做**当前态陈述**的同步;真值全部本轮实测: `simulator/tests/ref/*.bin` = **53**;`git diff 31ca4a1f..HEAD` 对该目录 = ** 6 个新增、零修改** ⇒ 存量 47 张 逐字节未变(这条是关键:新增页不该让旧页重生成,否则回归基线就失去意义)。 **四类命中各走各路,这是本笔的全部难点**(分类比替换重要得多): · 当前态 → 改,25 处页数,落在 16 只文件(AGENTS、钩子显示名、导航器 docstring、`lvgl-simulator` s kill、 门禁索引与 `ui-golden` 详解、UI 自动化流程、内容页架构、页面补齐规划与扩充方案、数据供给、自动化测 试流程、覆盖率口径、测试开发标准流程、模拟器两份 README); · 变更记录历史行 → **一条不动**,另在其下一行插入当日「47 → 53」新行 6 条(含判据与零漂移证据); · **同数不同义 → 不动**:覆盖率口径里的「47 个 .c」「47 = 13+5+29」、ram_report 的表行号 47、`#47-D` 诊断号、LSI 47kHz、47,360 栈字数、像素哈希 3476/3474 —— 一并替换就会毁掉六个不相干的事实; · 带日期戳 / 自declare 冻结的快照 → 不臆改(数据供给篇那句上一笔已加「当时」,页面补齐规划里那张 「2026-10-25 复核…剩 21 叶」的表自带「完成即冻结不回改」声明)。 复扫残留当前态 47 = **0**。⚠ 顺手纠一条**会教错方法**的陈旧:naive 计数 `grep -c '#page='` 上一笔写的是 22,现在真值是 **23**(文件头示例行也算一次)——旧文档里那个 22 **恰好等于新的正确 bound 数**,不改的话, 下一个人用错算法还能碰巧对上,比错更糟。 ⚠ `AGENTS.md` 里本次有两件事同行相邻(本笔的内容页 53 页 + 批次三那句 `TC-*` 停发),**分两句说明**: 前者是计数同步,后者是昨天那条「批次三不坐」裁定的入口文案,二者不是同一件事。 钩子显示名改在 `.pre-commit-config.yaml`(47 → 53)。**这条必须与代码同批入库**:配置脏而未 stage 时 pre-commit 会直接报错挡住全仓任何提交(今天已实测两次),所以本笔先落它,把后面几笔的路清开。 自查:`check_doc_links --all` rc=0(203 篇)、`check_ids --strict` rc=0 且不可判定绝对 数 **979 未变**(新增 文本里的提交号与 `#page=comm` 没推高它)、`gen_task_table --check` 0、`check_bom_drift` 0、18 只文件 noBOM / LF / U+FFFD=0;行数变动 +49/−47 全部可归属(插 6、删 2、并 1),其余为 1:1 等行替换。登记簿销项与叶数 (`UI补齐.md` + `docs/任务规划/README.md`)**不在本笔** —— 那是口径变更不是计数同步。
873 lines
76 KiB
Markdown
873 lines
76 KiB
Markdown
# 网络自愈架构设计
|
||
|
||
> **定位**:CH395F 网络自愈架构的**现状权威描述**——架构与机制、为什么这样设计、关键参数及其
|
||
> 实证锚点、红线与延后项。
|
||
> **不写历史**:审计过程、分阶段实施勾选清单、台架原始记录、重构过程账目一律不入本文;
|
||
> 需要考古见 §12。正文只描述**当前如何组织**与**为什么这样组织**。
|
||
> **编号是冻结标识**:`I*` / `E*` / `F-*` / `形态*` / `U*` / `TRAP-NET-*` / `D*` 被代码注释、
|
||
> `test/host/` 探针与 `docs/问题记录/` 直接引用——**看懂即可,勿重排改名**,对照见下方导读。
|
||
|
||
## 导读:本文用到的编号与术语
|
||
|
||
| 记号 | 是什么 | 在哪查 |
|
||
|---|---|---|
|
||
| `I1`~`I8` | 本架构的**不变量**(Invariant)——违反即有实测后果 | §2 |
|
||
| `E0`~`E19` | **台架实验**编号(E = Experiment):每个实验是「假设 → 方法 → 判据 → 门禁哪个决策」 | §5.2(原始记录已归档,见 §12) |
|
||
| `F-NET-nn` / `F-DRV-nn` / `F-MS-nn` | 审计的**编号发现**:NET 层 / 驱动层 / 状态镜像层(**2026-10-27 停发**,指代一律用 §7 表的「条目名」列) | §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` 审计遗留章节 |
|
||
| `TRAP-NET-nn` | **陷阱记录**条目 | 同上 |
|
||
| `N-nn` | `docs/任务规划/登记簿.md` 的**任务/发现编号**(⚠ 已销项的号不再可查,本文引用处即其最终事实描述) | 该登记簿 |
|
||
| `D1`~`D7` | 本文**决策记录** | §10 |
|
||
| `自愈收敛阶段 N` | 2026-10 的分阶段实施标签,**只存在于历史文档与变更记录**(代码注释已全部改为平实语言);勾选清单已归档(§12)——**勿用它叙述设计,也勿再写回注释** | §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`~`E19`(**无**连字符)= 本文的**自愈实验**编号,逐条见 §5.2;
|
||
> - 带连字符的 `E-<nn>` = **历史审计发现**编号(属主文档已于 2026-10-15 删除,
|
||
> 编号冻结于 `docs/编号规范.md` §4.3)——
|
||
> §5.2 末行引用它是为了记录该发现的**收口台架**,**不是**实验编号
|
||
> (证据:`test/host/e02_probe.py` 的文档串自述它是该审计发现的 timeSync 触发形态探针);
|
||
> - 该审计编号族已冻结、不再新增,引用一律改说人话。
|
||
|
||
## 1. 为什么是这个架构
|
||
|
||
三个芯片事实决定架构形态(全部台架实证,见 `docs/问题记录/CH395F_Trap_Records.md`
|
||
TRAP-NET-09/-17/-18/-20 与 §5.2 锚点):
|
||
|
||
1. **芯片永不自动回 LISTEN**——单连接复用模型(监听与数据通道复用同一 Socket),
|
||
"监听常驻"只能是软件承诺(I4)。
|
||
2. **事件面不是可靠唯一信源**——事件可被其它合法读者消费,且状态面存在事件覆盖不了的幽灵
|
||
(E7b;E15:幽灵 = 芯片如实报告旧连接拆链进行中,非谎报)→ 正确性必须靠回读芯片事实 +
|
||
有界超时兜底(I3)。
|
||
3. **close 有沉降窗、RX 残渣有害**——带非空 RX 关闭会拆链停滞、吞下一会话 SYN 且自持
|
||
(E4 变体 C 0/20);沉降窗不定长(TRAP-NET-09/-20)→ 复用前清理必须内联进事务(I5)。
|
||
|
||
主流 offload 驱动(WIZnet ioLibrary / Zephyr / FreeRTOS+TCP / mbed / lwIP 移植)里**没有任何
|
||
一家维护第二份 socket 状态镜像**,也没有"周期修正镜像"的对账;芯片怪癖一律**内联进事务、
|
||
收在驱动层、以有界超时兜底**。本架构按同一形态收敛(§2~§4),代码级对照见 §1.5。
|
||
|
||
由此确定本层**不做**的三件事:不把中断事件当正确性来源(I3)、不把软件镜像当事实源(I1)、
|
||
不把监听重建散给多个所有者(I2)。
|
||
|
||
### 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,
|
||
定案为"保留排空现场",论证见 §4.9 / D4)⇒ **断开后芯片必然停在 `0x05/0x00`**。
|
||
探针看到的"孤儿芯片态"不是芯片异常,而是这个配置的**直接且合法的后果**。
|
||
|
||
### 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 要消除的形状。
|
||
|
||
fd 别名让这个边沿**必须有人接**,而"谁重建监听"在类型上无法唯一化:同一 fd 既当监听器又当
|
||
连接时,任何持有 fd 的路径都"看起来有资格"重建。本架构的解法是把两件事彻底拆开(§4.1 / §4.2):
|
||
|
||
- **判据唯一**:周期性"该不该重建"只在 `net_reconcile_tick` 形态③④;
|
||
- **动作唯一**:真正下发 open+listen 只在 `net_listener_tick` 一处,其余路径一律
|
||
`net_listener_request()` **只登记意图**;
|
||
- **消费者驱动即时登记 3 处**(accept CLOSED 分支 / recv EOF / 挂起 recv 断连)与周期兜底
|
||
构成双层,**不可删**(§4.2)。
|
||
|
||
三个已实测的派生失效(保留作**症状判读**:I2/I3 被违反时就是这个样子):
|
||
|
||
| 失效 | 机理 | 现状机制 |
|
||
|---|---|---|
|
||
| 孤儿监听(B10) | 边沿没人接 → net 层等应用重建、应用等 net 层给 EOF、对账只裁 ESTABLISHED/LISTENING 直接跳过、芯片不会自己回 LISTEN → **四方互等** | 对账形态④ 认领孤儿(§4.1)+ 应用侧 `net_drop_conn`(§4.6) |
|
||
| 消费者卡死 | 抢先 relisten 把慢消费者的 EOF **永久**吞掉 → `g_client` 不清、新连接被旧会话静默吸收 | `session_dead` 粘滞**电平**消除(§3.3) |
|
||
| 监听被误关 | `net_can_read` 在非 ESTABLISHED(**含 LISTENING**)返 1 → 消费者 recv 得 `WOULDBLOCK` → 判"真错误"后无护栏 `net_close` 等于**关掉监听 fd** | 应用侧 `g_client != g_listen` 护栏(`Drivers/BSP/UI/ui_net_sink.c`)+ 红线 8(§6) |
|
||
|
||
### 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)。
|
||
|
||
### 1.5 与主流 offload 驱动的代码级对照("是不是过度设计"的正面回答)
|
||
|
||
**对照对象**:WIZnet **ioLibrary** `Ethernet/socket.c`(W5100~W6300 官方驱动,TCP offload
|
||
芯片的事实标准)。其关闭/重开路径原文:
|
||
|
||
```c
|
||
int8_t close(uint8_t sn) {
|
||
setSn_CR(sn, Sn_CR_CLOSE);
|
||
while (getSn_CR(sn)); /* 等命令被芯片处理 */
|
||
setSn_IR(sn, 0xFF); /* ① 复用前清中断 */
|
||
sock_is_sending &= ~(1 << sn); /* ② per-socket 记账清零 */
|
||
sock_remained_size[sn] = 0;
|
||
sock_pack_info[sn] = PACK_NONE;
|
||
while (getSn_SR(sn) != SOCK_CLOSED); /* ③ 等芯片状态真落地才返回 */
|
||
}
|
||
int8_t disconnect(uint8_t sn) {
|
||
...
|
||
while (getSn_SR(sn) != SOCK_CLOSED) { /* ③ 同样等 */
|
||
if (getSn_IR(sn) & Sn_IR_TIMEOUT) { close(sn); return SOCKERR_TIMEOUT; }
|
||
} /* ④ 有界超时兜底 */
|
||
}
|
||
int8_t socket(...) { close(sn); ... while (getSn_SR(sn) == SOCK_CLOSED); } /* 重开前先关干净 */
|
||
int8_t listen(...) { while (getSn_SR(sn) != SOCK_LISTEN) { close(sn); return SOCKERR_SOCKCLOSED; } }
|
||
```
|
||
|
||
⇒ **① 复用前排渣 ② per-socket 记账 ③ 等状态落地 ④ 有界超时——本仓一条不少**:
|
||
分别对应 listen 前排清 SINT(§4.10)、`s_relisten_*[]`、沉降窗(§4.3)、D10 预算(§4.2)。
|
||
**这套形状不是本仓发明,是 offload 芯片驱动的通用形态。**
|
||
|
||
**那本仓多出来的体积从哪来——四条约束的兑换券**:
|
||
|
||
| 约束 | ioLibrary | 本仓 | 复杂度落点 |
|
||
|---|---|---|---|
|
||
| **不能阻塞** | `while (getSn_SR(sn) != SOCK_CLOSED);` 就地死等,`close()` 里连超时都没有 | netTask 绝不能冻结(IWDG + 另外 3 个常驻监听 + 消息队列同任务) | ⚠ **头号来源**:他们一个 `while()` 的事,本仓必须翻译成**可续跑状态机**——`s_relisten_*[]` 8 个数组**全部是"把 while 换成状态"的代价**,无一是额外逻辑 |
|
||
| **监听器归谁** | 8 个独立硬件 socket,关数据 socket 碰不到监听 socket | 单 socket 复用(§1.1) | "监听器需重新武装"这个概念在 ioLibrary 里**根本不存在** |
|
||
| **谁做 TCP 收尾** | 硬件自动:W5500 数据手册明写断开成功 → `SOCK_CLOSED`,否则 **TCP 超时后也变 `SOCK_CLOSED`** | `bit3=1` 下芯片按设计**什么都不做**,等 MCU 关(§1.1) | 沉降被 WIZnet 芯片自己吸收,CH395F 明确甩给软件 |
|
||
| **清中断的代价** | `setSn_IR(sn, 0xFF)` 随便清(每 socket 独立 W1C) | 全局寄存器整体读即清、无按位手段 | 他们一行清干净,本仓必须守 §4.10.5 三条准入判据 |
|
||
|
||
⚠ **ioLibrary 那两条死等不是"更简单",是"把代价挪到运行时"**:官方论坛有"系统挂死、SPI
|
||
驱动在 `close()` 后永远等不到回复"与"`close()` 只轮询 `Sn_CR` 不轮询 `Sn_SR`"的质疑帖。
|
||
本仓没有死等的自由(上表第一行),复杂度只是换了个地方存着。
|
||
|
||
**lwIP / Zephyr / FreeRTOS+TCP 为什么没有这套机器**:完整 TCP 栈跑在 MCU 里,
|
||
TIME_WAIT/2MSL 藏在栈内部,应用永远看不见 FIN_WAIT_2;监听器所有权是显式 API(lwIP raw API
|
||
把"`tcp_close()` 只对 socket 层创建的监听器生效"写进文档)。⇒ **它们把复杂度付在 RAM/CPU
|
||
上,本仓付在驱动代码上**——用 offload 芯片就是为了省那颗栈,**复杂度不会消失,只会搬家**
|
||
(§1.2 那句"需要靠铁律维持的正确性就是架构欠债的凭证"的另一面:欠债要还,但还债的货币
|
||
由芯片选型决定)。
|
||
|
||
**本仓可安全减的部分(诚实清单)**——本节只管"这台机器该不该存在",内部组织度见 §11:
|
||
`net_listener.c` = **7 个函数**(4 公开 + 3 内部)/ 8 个 per-socket 数组。
|
||
可省:`s_relisten_settle_dead[]`(恒 = `settle_start + NET_RELISTEN_SETTLE_MS`,
|
||
可就地算)、`s_relisten_capped[]`(可压成 `quarantine` 的一个 bit)——合计 16 B。
|
||
**不可省**:沉降窗(E4 变体 C 带残渣关闭 0/20)、D10(= ioLibrary 的 TIMEOUT 等价物)、
|
||
listen 前排清(§4.10)、登记/执行分离(§4.2,E17)。
|
||
|
||
**真要大幅减掉这套机器,唯一杠杆是芯片**——而 §1.4 已用三条硬证据否掉多连接模式,且
|
||
`FUN_PARA` 初始化后不可改。WCH 官方博客原话(E15 已引):"客户端主动断连并立即重新发起
|
||
连接请求,此时 CH395 会拒绝该连接请求,**因此需要程序中做一些特殊处理**"——官方口径的
|
||
"特殊处理"就是本文件。
|
||
|
||
**出处**:ioLibrary `Ethernet/socket.c`(github.com/Wiznet/ioLibrary_Driver);
|
||
W5500 数据手册 W5500_ds_v110e.pdf(`Sn_SR` 断开 / TCP 超时条款);WIZnet maker 论坛
|
||
帖 13028(close 后永久等待致挂死)与 10305(轮询口径质疑);lwIP 2.0.x Raw API 文档;
|
||
WCH 官方博客 cnblogs sw111/18451802。
|
||
|
||
## 2. 设计原则(不变量)
|
||
|
||
| # | 不变量 | 违反的后果 |
|
||
|---|---|---|
|
||
| I1 | **单一事实源**:芯片状态的权威副本 = 驱动每 Socket 缓存,唯一刷新点 = `net_poll` 每轮全量刷新;net 层 8 态 `net_sock_state_t` 只是"会话投影"(展示/门控),决策点读缓存或主动回读 | 有损镜像产生"镜像说 A、芯片是 B"的决策窗口(§7「有损镜像决策窗口」) |
|
||
| I2 | **单一重建所有者**:唯一周期性监听重建决策者 = `net_reconcile_tick` 形态③④;事件处理器与沉降登记**只置事实、不动作** | 多所有者竞争——"谁先看到 DISCONNECT 谁重建"→ 抢先 relisten 吞掉慢消费者的 EOF(B10) |
|
||
| I3 | **事件只负责提前唤醒,正确性靠回读 + 有界超时** | 纯事件驱动 → 事件被读清消费竞态吞掉后监听静默死亡(TRAP-NET-17;E14 复核:事件非芯片侧丢失,是被消费) |
|
||
| I4 | **监听常驻 = 软件承诺**(`auto_relisten`),不是芯片属性 | 出向连接被误判孤儿 / 监听永久失效 |
|
||
| I5 | **复用前清理内联进事务**:`sock_reset` = 排空 RX → CLOSE → 排空事件(quiescent)→ 等 byte0=00H | 带非空 RX 关闭 → 拆链停滞、SYN 被吞、级联自持(E4 变体 C 0/20) |
|
||
| I6 | **分层**:驱动持事实与机械;net 层持策略与所有权;应用层零自愈责任 | 应用层重试/手动自愈 = 又一层补丁(原引用号在 §7 无定义行,删号留事实) |
|
||
| I7 | **全局中断寄存器唯一读者 = `net_poll`**(§7「全局读即清搁浅事件」;测试/台架代码同样禁自行读取,等事件一律直读本 Socket SINT) | 吞掉其它 Socket 的 CONNECT/DISCONNECT/RECV_OK 边沿事件(B3 级事故) |
|
||
| I8 | **不留未证明的复杂度**(TRAP-NET-20 铁律) | 正面执行:U15 内化不立项(E6 命中=0);TSL 变体只作待用补丁(E12) |
|
||
|
||
**去抖 + 冷却**:失配连续 N 轮才动手(分级,见 §4.1)+ 动手后 1s 冷却。去抖与冷却本是
|
||
"镜像世界"的自偿成本(§7「去抖冷却自偿成本」)——判据改读驱动缓存后,探针侧去抖已无必要(缓存无瞬态噪声);
|
||
reconciler 保留分级去抖,因为形态① 要让位 M3 窗、形态② 要覆盖芯片 10ms 级可见窗(E2)。
|
||
|
||
## 3. 架构总览
|
||
|
||
### 3.1 分层职责
|
||
|
||
| 层 | 持有什么 | 关键接口 |
|
||
|---|---|---|
|
||
| 驱动层 `Drivers/BSP/CH395F/ch395f.c/.h` | 每 Socket 状态缓存 + 类型化回读/谓词;每 Socket 事件读即清护栏;复合复位状态机;命令完成判据(状态回读);链路态缓存 | `ch395f_sock_state_cached` / `ch395f_sock_events` / `ch395f_sock_reset_begin` / `ch395f_sock_reset_poll` / `ch395f_cmd_done` / `ch395f_glob_event_feed` / `ch395f_link_up_cached` |
|
||
| net 层 `net_socket.c` / `net_events.c` / `net_listener.c` / `net_pending.c`(netTask 单所有者) | fd↔Socket 映射、消息队列串行化、挂起 recv/send;事件分发与回调;会话投影与粘滞电平;单一 reconciler;监听生命周期(armed/D10/settle);同步复位包装 | `net_poll` / `net_reconcile_tick` / `net_listener_request` / `net_listener_settle` / `net_listener_tick` / `net_sock_reset_sync` / `net_drop_conn` |
|
||
| 应用/服务层(`ui_net_sink` / `iec104_serv` / `lftpd` / `time_sync`) | 只管 fd 与数据;放弃会话走 `net_drop_conn`;EOF 不得 `net_close` 监听 fd | — |
|
||
|
||
### 3.2 主循环自愈三 tick
|
||
|
||
| tick | 节奏 | 职责 |
|
||
|---|---|---|
|
||
| `net_poll` | 主循环每轮(≈1ms) | INT# 引脚 → 全局事件汇聚(唯一读者)→ SINT 分派 → **驱动缓存全量刷新** → 链路位喂驱动 → 步进 `ch395f_sock_reset_poll` |
|
||
| `net_process_messages` | 每轮 | 开头按序跑三个非阻塞 tick:`net_listener_tick`(D10 重建**唯一执行者**)→ `net_pending_tick`(结束挂起 recv)→ `net_send_pending_tick`(推进挂起 send),随后排空 RPC 消息队列 |
|
||
| `net_reconcile_tick` | 100ms | 唯一周期**对账**——判据在此,重建动作在 §4.2 唯一执行者(§4.1) |
|
||
|
||
### 3.3 状态模型(会话投影)
|
||
|
||
- `net_sock_state_t` 8 态 = **本层会话投影**,非芯片状态事实源(I1)。
|
||
- `session_dead`:"会话已结束"的粘滞**电平**——消费者不再被抢先 relisten 饿死;
|
||
relisten / 形态④ / listen / RESYNC 刻意不清它(契约注释在 `net_types.h`)。
|
||
- `lost_conn_seen`:M3 可疑电平(§4.1.6)。
|
||
- `last_peer_port` + `released_abnormal`:幽灵甄别与异常释放簿记。
|
||
|
||
### 3.4 全景图
|
||
|
||

|
||
——七个叠加的芯片怪癖与现架构的拆法落点(证据链见陷阱记录与 §5.2)。
|
||
|
||
## 4. 机制详解
|
||
|
||
### 4.1 `net_reconcile_tick`——单一 100ms tick(唯一周期性自愈入口)
|
||
|
||
#### 4.1.1 为什么需要它:状态有两个所有者
|
||
|
||
| 所有者 | 是什么 | 谁更新 |
|
||
|---|---|---|
|
||
| **本层** `p_sock->state` | 软件枚举(`CLOSED=0` / `LISTENING=3` / `ESTABLISHED=5`)= 会话投影,**非芯片事实源**(I1) | **中断事件流**:CONNECT / DISCONNECT / RECV_OK |
|
||
| **芯片** `c.open` + `c.tcp_state` | CH395F 两个真实状态字节,`net_poll` 每轮刷进驱动缓存(新鲜度 ≤1ms) | 芯片自己的 TCP 状态机 |
|
||
|
||
主路径是事件流:芯片状态一变就发中断、本层跟随。**中断没送到 netTask**(被门控丢弃 /
|
||
时序窗口错过 / 芯片压根不发——形态⑤ 台架实证)→ 两侧脱钩,只有两类坏结果:
|
||
|
||
- **连接已死而本层以为活着** → 消费者挂在 recv 上,而对端再也不会发数据;
|
||
- **监听器已没而本层以为在听** → 端口对 SYN 不响应,表现是"连接被拒"。
|
||
|
||
本 tick = 每 100ms 把两侧摆在一起比对的那只手,**5 种失配组合 = 5 个形态**。
|
||
|
||
#### 4.1.2 读判据前必须知道的四个词
|
||
|
||
- `chip_listening` = `(open != CLOSED) && (tcp_state == LISTEN)`,即 `0x05/0x01`。
|
||
⚠ **`0x05/0x00`(Socket 开着、TCP 已关的二段关闭中间态)不算在听**——这个区分很关键。
|
||
- `listener_fd` = `auto_relisten && type==STREAM && local_port!=0`:监听型 fd 门。少了它,
|
||
**出向连接**(本层 CLOSED + 已绑端口)会被误判成监听器而被 relisten。
|
||
- **去抖三档**(连续失配几轮才动手,一轮 = 100ms):② **1 轮**(E2:LISTEN 可见 ≤10ms
|
||
无长尾)|③④⑤ **2 轮**|① **4 轮**(≈400ms,M3 让位窗——① 动作最重,要给正常事件流
|
||
先自己收口的机会)。动手后置 **1s 冷却**防风暴;U11 被动重同步**不去抖、不占冷却**。
|
||
- **判据在对账、动作在别处**(I2):③④ 只 `net_listener_request()` 登记意图,真正执行
|
||
重建的是 `net_listener_tick` 一处(§4.2)。
|
||
|
||
#### 4.1.3 形态表(判据 → 动作)
|
||
|
||
判据读驱动缓存(零 SPI;例外见表注),统一 1s 冷却:
|
||
|
||
| 形态 | 判据(本层 ↔ 芯片) | 动作 | 去抖 | 芯片命令 |
|
||
|---|---|---|---|---|
|
||
| ① | ESTABLISHED ↔ 芯片 CLOSED / TCP_CLOSED | 复用 `handle_disconnect_event`(置事实 + GRACEFUL 沉降) | **4 轮**(≈400ms,M3 让位窗) | 无(置事实) |
|
||
| ② | LISTENING ↔ 芯片已握手(TCP=ESTABLISHED) | `GET_REMOT_IPP` 幽灵甄别 → 补 accept(`handle_connect_event`) | **1 轮**(E2:LISTEN 可见 ≤10ms 无长尾) | 1 次 SPI 读 |
|
||
| ③ | LISTENING ↔ 监听型 fd 而芯片不在 LISTEN | 登记重建请求(动作在 I2 唯一执行者 `net_listener_tick`) | 2 轮 | 无(转交) |
|
||
| ④ | 本层 CLOSED(auto_relisten)而芯片**不在** LISTEN | 登记重建请求(同 ③) | 2 轮 | 无(转交) |
|
||
| ⑤ | ESTABLISHED ↔ 芯片已回 LISTEN(静默回收;§7「静默回收无人认领」) | RESYNC 语义只修本层(转登端口 + 取消挂起 recv/send),零命令 | 2 轮 | **零** |
|
||
| U11 | 本层 CLOSED ↔ 芯片**正在** LISTEN | 只重同步本层为 LISTENING(转登端口) | **不去抖、不占冷却** | **零** |
|
||
|
||
⚠ **U11 不是形态④**。两者判据同族(本层 CLOSED + 监听型 fd)而结论相反:
|
||
**芯片在听 = 本层滞后 → 只改状态;芯片不在听 = 监听真没了 → 登记重建**。搞混的代价就是
|
||
U12 同源事故——对正听着的 Socket 重下发 open+listen 会吃 USE 错、走进关闭分支,
|
||
把健康监听器关掉。
|
||
|
||
表注:形态② 命中时的对端端口回读是本 tick 唯一按需 SPI。
|
||
|
||
#### 4.1.4 每个形态为什么长这样
|
||
|
||
**① 连接已失**:成因 = 连接被拆而 DISCONNECT 没被 netTask 收到,本层停在 ESTABLISHED、
|
||
消费者挂在 recv 上等永远不会来的数据。动作复用断开状态机(含陈旧事件甄别 + 状态迁移 +
|
||
事件派发),**只置事实、不做重建**(重建交 ③④)。判据是**"或"**(`open` 仍可为 `0x05`、
|
||
`tcp_state=0x00` 即成立)。⚠ 判读只认**设备日志**的 `chip closed` 行,**端口可达性不是判据**
|
||
(E18 方法论第一条)。
|
||
|
||
**② 握手已完成、事件没收到**:成因 = 芯片做完三次握手而 CONNECT 没送到,本层以为在听、
|
||
应用不会 accept,数据全堆在芯片 RX,应用侧**完全看不见**这条连接。⚠ **幽灵甄别
|
||
(TRAP-NET-20)是本形态最凶处**:拆链收尾窗口里芯片状态会**如实**显示 ESTABLISHED
|
||
(旧连接未拆净,E15 契约,非谎报),此时补 accept 就把新数据发给已死的旧连接 = 生产数据被
|
||
吞的直接成因;故仅当"对端端口非零且 = `last_peer_port`"时拦截,`peer==0` 不拦截
|
||
(对端端口提交有滞后,不得因此挡掉救援)。去抖 1 轮 = 最激进,因为它是"白捡的救援"、代价小。
|
||
⚠ 本形态**未经负向注入验证**(需吞 CONNECT 事件),依据是幽灵甄别 + E2/E10 台架记录,
|
||
**勿写成"已验证"**。
|
||
|
||
**③ 监听器失位**:成因 = 芯片上的监听器没了(close / 超时 / 回收)且没发事件,本层还以为
|
||
在听 → 端口拒连。最干净的靶 = 空闲监听器(无连接、无消费者)。
|
||
|
||
**④ 孤儿监听**:成因 = 会话结束、本层已收口 CLOSED,但监听没重建成功(或重建事件丢了)。
|
||
与 ③ 的差别**只在本层状态**:③ 是"以为还在听",④ 是"知道会话结束了、但监听器本该在"。
|
||
**它是常态唯一在干活的形态**(归因见 §4.1.5)。
|
||
|
||
**⑤ 芯片静默回收**:成因 = 芯片**自己**把 Socket 收回 LISTEN 且**不发 DISCONNECT 中断**
|
||
(台架实证),本层挂着一个已不存在的"活连接"。动作纪律 = **零芯片命令**:只改本层
|
||
state、转登 `last_peer_port`、清 `lost_conn_seen`、**取消挂起 recv/send**(必通知,防调用方
|
||
挂死)。为什么绝不能顺手 close + 重 open:芯片那个监听器是**健康的**,重下发会摧毁它 →
|
||
端口长期拒绝连接(实测 170 s 拒连;与 U11 同一条纪律)。
|
||
注入验证还顺带量出两个硬事实(§5.2 E18):状态机漏步进会 **SPI 风暴** → `[ERR][WD] task
|
||
stall`;芯片 close 后停在 **FIN_WAIT_2(`0x05/0x06`)≈1 s**(E15 的 2MSL 长尾)——这条时间
|
||
尺度就是 §4.3 沉降上限必须存在的理由。
|
||
|
||
#### 4.1.5 为什么 ①③⑤ 常态 0 命中
|
||
|
||
消费者驱动的挂起 recv / recv EOF 比 100 ms 对账**更快**,多数场景先把本层收口成
|
||
CLOSED → 由形态④ 接手。关键在两条快路径的判据(`net_pending.c` 的挂起 recv 结束判定 +
|
||
`net_socket.c` 的 recv 快路径):**只看本层 `state`、从不读芯片**——所以只要"有人在等",
|
||
事件流就先赢,①③⑤ 永远轮不到。推论就是职责边界:**①③⑤ 管的是"没有人在等"的场景**
|
||
(空闲监听器被静默 close、连接被静默拆掉而无人 recv、芯片静默回收),与 ④ **互补而非
|
||
重复**。→ **三条均不可降级为只留痕**;⑤ 的"取消挂起 recv/send"尤其不可省,否则消费者在
|
||
死连接上干等到 deadline。
|
||
|
||
#### 4.1.6 M3 可疑门
|
||
|
||
`handle_disconnect_event` ESTABLISHED 分支:芯片 `0x05/0x00`(Socket OPEN 但 TCP 槽 0x00——
|
||
活连接应为 0x04,逻辑矛盾态)且 `lost_conn_seen==0` 且 `recv_bytes==0` 且**芯片 RX 有在途
|
||
数据**(不可毁的消费者资产,与 closed-with-data 救回同源)→ 不释放,置可疑电平,
|
||
交形态① 电平二次确认;双锁存 = 强证据直接放行。
|
||
|
||
风暴态免疫(E9 R3:预置残渣后 `n30` 0/50;`:21 @0ms×50` 50/50 零误伤——健康 FIN 芯片为
|
||
0x07、静默回收为 0x00/0x00,均不命中判据)。
|
||
|
||
### 4.2 监听生命周期(单一所有者:登记 / 执行分离)
|
||
|
||
- **`auto_relisten`**:`net_listen` 成功即置位;所有重建路径的门控标志(I4)。
|
||
- **`net_listener_armed`**:唯一监听门控。⚠ **必须放行 ESTABLISHED**——它是单连接模型
|
||
的"待 accept"电平;漏放行时 i104 的 200ms 轮询抢不过形态② 去抖而整场饿死(E10)。
|
||
- **登记侧 `net_listener_request(fd, reason)`**:只置意图(`reason` 仅用于留痕归因),
|
||
**不碰芯片**,因此**不需要状态门**。登记点 5 处:reconciler 形态③④ / accept CLOSED 分支 /
|
||
recv EOF / `net_drop_conn` / 挂起 recv 断连。已在请求中**不重置重试时刻**(防饥饿)。
|
||
- **执行侧 `net_listener_tick()`**(netTask 每轮):**唯一**调用 `net_relisten_locked` 的地方,
|
||
门集集中一处——控制块有效 + `in_use` + 状态 ∈ {LISTENING, CLOSED} + 监听型
|
||
(`auto_relisten` / STREAM / `local_port≠0`)+ **芯片已在 LISTEN 即视为请求已达成、静默了结**
|
||
(U12 的结构性防护,读驱动状态缓存零 SPI)+ **挂起保险**(`net_pending_active` /
|
||
`net_send_pending_active` 命中则跳过,防重开 Socket 摧毁在途连接与在途传输)+ 30ms 节奏 /
|
||
2s 预算(D10)。"预算耗尽"不是终态——reconciler 形态③④ 周期重新登记。
|
||
⚠ **门集只在这一处**:任何"当场动作 + 自带门"的写法都等于把 U12 风险复制到每个调用点
|
||
(漏一处即事故)。
|
||
- **消费者驱动即时登记 ×3**(accept CLOSED 分支 / recv EOF / 挂起 recv 断连):与周期兜底
|
||
构成双层——**不可删**(台架实测:删掉后 CLOSED 窗口拉长到对账周期 + 隔离期,期间调用方
|
||
高频 `net_accept` → `[WD] task stall`)。动作延后一轮(≤1ms)执行,恢复窗与当场动作
|
||
持平(E17)。
|
||
- `net_listener_settle(fd, mode)`:登记"会话已结束,重建前需沉降/隔离"——**只置事实**,
|
||
动作只在 `net_listener_tick` 发生(I2)。
|
||
|
||
### 4.3 沉降与排渣隔离(TRAP-NET-18)
|
||
|
||
- 两档:**GRACEFUL 100ms 底**(正常释放)/ **QUARANTINE 400ms 底**(异常释放:芯片
|
||
TIMEOUT、FTP ABORT);**800ms 上限只留痕、不放行**。
|
||
- **800ms 上限为什么不放行**(E19 反事实 A/B):芯片未回 CLOSED 时强行 `open` 注定吃
|
||
USE 错(TRAP-NET-09),放行与不放行的端口恢复时刻**同一分布**——恢复由"芯片何时回
|
||
CLOSED"决定。**"监听器不会永久失效"的真正兜底 = D10 预算到期 + 对账形态④ 重新登记**,
|
||
不是放行;勿再把本处当兜底。上限到点只打**一行** `keep waiting` 且**隔离标志不清**
|
||
(下一轮继续排空残渣);留痕只留首末(每轮都打会 30ms 刷两行)。
|
||
- **隔离期持续排空 RX**:中止后残渣(实测 rxdrain=2048 = 芯片单 Socket RX 缓冲满容,
|
||
E4)会被下一条连接读走 → 隔离期边等沉降窗边排空。
|
||
- **GRACEFUL 无条件化**(M1;§7「沉降门放大器」):`recv_bytes≠0` 门是背靠背失效循环的放大器
|
||
——被陈旧 DISCONNECT 杀死的零数据会话免沉降 → 残渣杀下一条连接、自持循环;无条件化后
|
||
`n30` 8%→96.7%(E7b)。⚠ M1 必须与 M4-lite(删沉降段全局读即清)**同批**,否则沉降成为
|
||
每次断连热路径后放大「全局读即清」的搁浅面(E7 实测教训)。
|
||
|
||
### 4.4 驱动原语
|
||
|
||
- **类型化回读 + 缓存**:`ch395f_sock_state_cached(sn)` / `ch395f_get_sock_state(sn)` /
|
||
`ch395f_sock_is_listening(sn)`;`open=0` 时 `tcp_state` 返回哨兵 `0xFF`
|
||
(byte0 门控 byte1 的收口);sn>7 边界返回 CLOSED+哨兵。唯一刷新点 = `net_poll`
|
||
每轮全量(单次回读 18.1µs,E2)。
|
||
- **事件护栏**:`ch395f_sock_events(sn)` 只读本 Socket SINT(读即清),永不触碰全局
|
||
寄存器(I7)。测试/台架代码等事件同样直读 `ch395f_get_sock_int_status(sn)`——
|
||
不用全局位图门控(门控观察分不清"芯片未锁存"与"全局位被读清")。
|
||
⚠ **listen/relisten 的排事件读清必须在 LISTEN 命令之前**(E14:post-listen 读清会吞掉
|
||
即时到达的新连接 CONNECT——生产与台架同源根除)。
|
||
- **复合复位状态机**:`ch395f_sock_reset_begin(sn, mode)` / `ch395f_sock_reset_poll(sn)`
|
||
——非阻塞、`net_poll` 每轮步进(同步包装 600ms 放弃后复位机也不搁浅)。
|
||
序列(E4 定案):① 读空 RX(**取读空**——字节数可观测可留痕,保芯片"读出残留数据后再
|
||
关闭"的契约,`docs/芯片手册/CH395F.md` §5.52 bit3=1)② CLOSE_SOCKET ③ 排空本 Socket 事件
|
||
(drain-until-quiescent:连续两轮无新位)④ 轮询 byte0=00H。**LISTEN 态 Socket 的 CLOSE 是
|
||
二段**(E2:一次 CLOSE 只拆监听,byte0 恒留 05H 不自落)——状态机内置二段补发(只补发
|
||
一次)。GRACEFUL 100ms / QUARANTINE 400ms 底,600ms 封顶。
|
||
- **命令完成判据**:`ch395f_cmd_done` = **状态回读**(open 完成→byte0=05H;listen
|
||
完成→TCP=01H;close 完成→byte0=00H)——GET_CMD_STATUS 对这三条命令无判别力且陈旧
|
||
错误码(0x17)跨命令持续存活(E3);GET_CMD_STATUS 仅保留内核前慢命令与"命令级被拒"
|
||
检测(被拒码是新值覆盖残留,方向不受歧义影响)。
|
||
- **链路态缓存**:`net_poll` 读全局后把 bit0~3 喂 `ch395f_glob_event_feed`;PHY_CHANGE
|
||
→ 重读 PHY 更新 `link_up` 缓存 + 变化计数;reconciler 订阅留痕(运行期拔网线不再静默)。
|
||
- net 层同步包装 `net_sock_reset_sync`(listen/connect open 失败重试、close 快路径)
|
||
——忙等统一,close/connect 路径不再读清全局寄存器(I7)。
|
||
- ⚠ **监听重建(relisten)不接进本原语**:现序列 = 一次 CLOSE + CLOSE 前排空 + 等 CLOSED,
|
||
**不等价**于监听隔离段的"隔离窗内持续排空/关闭"(残渣在 CLOSE 之后才落进 RX),也不与
|
||
"同一芯片 Socket 被上层立即复用"共存(复位机会拆掉刚建的监听)。E16 实测回退,
|
||
三条前置语义与回归数据见 §8 延后项 1;在此之前 `net_listener.c` 保留自建序列。
|
||
|
||
### 4.5 recv 语义与挂起(pending)
|
||
|
||
- `net_recv()` 默认**无限阻塞**;`net_recv_nb()` + `net_setblocking(fd, on)`(模式是
|
||
socket 属性);`net_recv_to()` = 显式预算便捷入口(`timeout_ms=0` 仍拒 INVAL)。
|
||
- 唤醒源 = 数据 / EOF / close / 网络重建 CONNRESET;会话死亡兜底 = 形态①(≈400ms)。
|
||
- **挂起 recv / send**:阻塞 recv/send 不独占 netTask(挂起登记 + 主循环节拍完成),
|
||
架构与完成语义 → `docs/网络/Net层阻塞模型架构设计.md`。
|
||
**挂起超时留痕探针**(编译期开关 `NET_PENDING_TRIPWIRE`,默认关):挂起超阈值只
|
||
`[ERR]` 留痕一次(完成逻辑缺位),不伪造 TIMEDOUT。
|
||
- **术语**:"结束挂起 recv" = `net_pending_complete` 把结果(字节数 / EOF(0) / 错误码)
|
||
回传给调用任务并唤醒它,挂起槽随之释放。
|
||
- **文档契约**(嵌入式特有):IWDG `required_mask` 内任务**禁止无限阻塞 recv**——
|
||
必须 `net_select` 超时或 `net_recv_to` 显式预算。
|
||
- 上层 32 轮盲收(`ui_net_sink` / `iec104_serv`)保留现状(E6 裁决,§4.8)。
|
||
|
||
### 4.6 `net_drop_conn`——应用主动拆会话保监听(队头阻塞收口;异步收口 2026-10-25)
|
||
|
||
- 语义(**返回值 = 拆除与重建已登记**,不代表芯片已拆完):`net_pending_settle_close`
|
||
先结束挂起 recv(回传结果 + 唤醒调用方)→ 监听型(`auto_relisten`)fd:
|
||
`net_listener_settle(QUARANTINE)` + `net_listener_request(REQ_DROP)` 登记即返,
|
||
芯片拆除(FIN 关闭 + 排渣 + 排空事件 + 等 byte0=00H)与重建由 `net_listener_tick`
|
||
按 30ms 节奏执行(与被动超时/断连释放同一台机器),失败交 D10 + 形态④;
|
||
非监听 client fd 无监听可保,仍同步 `net_sock_reset_sync`(600ms 封顶)。
|
||
端口恢复窗 ~0.4~1s,与被动断连恢复窗(E1 发现 A)同型。
|
||
- **为什么监听型不得同步等待(探针窗口心跳缺口定位时的论证,随结论一并留档)**:旧形态在
|
||
`NET_MSG_DROP_CONN` 处理器内同步跑 `net_sock_reset_sync(QUARANTINE)`(2ms 节奏、
|
||
400ms 底/600ms 顶,自家注释原记「等待期 netTask 有界占用 ~400-600ms」),netTask
|
||
的 1ms 主循环冻结在同一迭代内:NET 位心跳停摆 + 串行总线整窗停摆(UI 推流/FTP/
|
||
104 在途消息回执顺延、调用方在其分片/无限等待中不见回执)+ 对时调用方自己睡在
|
||
`net_wait_result` 等回执、期间无分片心跳——五位同窗缺席 → 连续 8 个 100ms 抽样
|
||
停喂 → 声明 `[WD] task stall`(V5 时代 req=0x37,缺席位恰为 NET/FTP/UI/对时,
|
||
快照只剩 bit0,即日志中的 `mask=0x00000001`),缺口 800ms~1s,距 IWDG 最坏超时
|
||
1701ms 余量仅 ~0.7s;同场隔离负载(纯 FTP 120s)0 次——因为被动拆链走的本就是
|
||
异步沉降机(D10「重建必须非阻塞」条令),只有主动 drop 一条路径留在同步原语里。
|
||
- **评估否决的方向——「缩短重建重试(5×2s)」**:该退避链在
|
||
`time_sync_open_listener()` 的 bind/listen 失败路径,全程走
|
||
`time_sync_delay_fed()` ≤50ms 分片喂心跳,且**不在 drop→拆链→重监听的心跳缺口
|
||
链上**;缩短它不消除本缺口,反而加大对芯片沉降窗的撞车风险(TRAP-NET-09:关闭
|
||
后需沉降才能重开),故不采。
|
||
- **竞态与信源核对**:① 主路径置 CLOSED 后迟到的 DISCONNECT/TIMEOUT 事件被
|
||
`net_events` 的状态门拦截(只认活会话态),不会把刚登记的 QUARANTINE 降级为
|
||
GRACEFUL;② 零新增读清点——隔离窗内两处本 Socket 读清仍在 `net_listener_tick`
|
||
原位置(§4.10.2),全局寄存器唯一读者仍是 `net_poll`;③ 生产调用者全仓仅
|
||
`time_sync.c`(不消费返回值,accept 轮询天然容忍 WOULDBLOCK 窗)。
|
||
- **幂等**(非 ESTABLISHED/CONNECTING 直接返 0);**EOF 路径不得调**
|
||
(`auto_relisten` 已接管,再调属重复拆)。
|
||
- 芯片物理边界(台架定案,异步收口逐字不变——只移动「等待发生在哪」):
|
||
`close` **无 RST 语义**——对端配合(收到 FIN 后其内核 close):`TIME_WAIT` ~150ms
|
||
→ 形态④ relisten(恢复全程 ~500ms);对端内核存活但不 close:芯片停
|
||
`FIN_WAIT_2` 长尾,**监听恢复受限于对端 close**,兜底 = 芯片 TIMEOUT/KeepAlive 拆
|
||
僵尸 30~90s(E13)——不是应用可加速的路径。
|
||
- 回归锚点:`TC-NETSOCK-240efg / 321 全组 / 460ef`(登记式、netTask 零占用、
|
||
client fd 旧形态保留);台架判据 = V5 复跑(e02_probe + FTP ≥30min)零 stall,
|
||
见 `docs/测试说明/实机测试实施指南.md` §4。
|
||
|
||
### 4.7 KeepAlive(服务器侧)
|
||
|
||
- `handle_connect_event` LISTENING 分支开启,参数 **60s/5s/3**(规定位置见 `docs/芯片手册/CH395F.md` §5.56;
|
||
参数须 500ms 倍数且 IDLE>INTVL,TRAP-NET-06)。
|
||
- 实测判死 **≈30s**(早于手册公式 75s——芯片定时语义与公式不符,以实测为准;短参数
|
||
6s/2s/2 → ≈5.1s 证明参数可按需缩放);静默但**存活**的对端不误杀(对端会 ACK 探测)。
|
||
- 消费链路:`TIM_OUT` → `handle_timeout_event` QUARANTINE → relisten → 端口恢复。
|
||
- **僵尸会话自愈窗**(E13):对端静默死亡(拔线 / PC 崩溃 / 进程被杀,无 FIN/RST)→
|
||
芯片侧 ESTABLISHED 僵尸占住单连接监听——有在途 TX ≈30s(重传耗尽)/ 纯空闲
|
||
≈75-90s(KeepAlive)自动清除;**无需冷启动**(板上 RSTI←NRST,烧录/MCU 复位即芯片
|
||
硬件复位)。台架做连接类实验的控制手段 = 下一组开跑前静置 ≥90s 让僵尸清窗。
|
||
|
||
### 4.8 U15 探针(观察项,宏=0)
|
||
|
||
- `NET_U15_PROBE` 插桩留仓(产品构建编译消失):`net_poll` 每 32 轮对在用 fd 探测
|
||
`GET_RECV_LEN>0 && !readable`(两段确认),armed 日志 + 命中低频日志。
|
||
- **E6 裁决:命中=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 复核)已实证快速复连场景这两个中断会被读清消费掉 → 事件丢了,
|
||
bit3=0 同样停在已关/半关态,孤儿 Socket 照样存在。它只是降低出现概率,代价却是第②条。
|
||
|
||
**④ 所以「这么多问题」不归 bit3=1。** 断开后停 `0x05/0x00` 是 bit3=1 的**合法后果**(D4);
|
||
真正造成永久拒连、消费者卡死、EOF 被吞没的,是软件侧没把「该由 MCU 关 Socket」这条义务
|
||
落到唯一所有者(§1.3),以及 U12 的「relisten 的 LISTEN 失败不 close 芯片 Socket」与 open
|
||
失败路径不对称。bit3=1 只是把义务显式化,把原本被芯片掩盖的架构欠债照了出来。
|
||
|
||
**⑤ 安全处理 = 把 bit3=1 的契约在软件侧收口**(逐条对应上面的缺口):
|
||
|
||
| 契约要求 | 对应缺口 | 收口手段(现状) |
|
||
|---|---|---|
|
||
| 「会话结束」必须**谁都能看见**,不能只在边沿那一刻 | EOF 边沿化:抢先 relisten → recv 永久 `WOULDBLOCK` | `net_sock_t.session_dead` 粘滞电平,`net_recv_locked` 判据 `state==CLOSED \|\| session_dead`(§3.3) |
|
||
| 关闭 Socket 必须**一定有人做**、且只做一次 | 重建所有者无法唯一化 + U12 的 LISTEN 失败不回 close | 重建**判据**唯一化 = `net_reconcile_tick` 形态③④;重建**动作**唯一化 = `net_listener_tick`(§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) |
|
||
|
||
### 4.10 读即清信源审计(谁在读、会不会互吞)
|
||
|
||
> 口径散在多处代码注释里,**没有总表时"想再加一处读清"的人不会先去读那几处**。
|
||
> 本节就是那张挡人的表:现存读清点全量清单 + 新增读清的准入判据。
|
||
|
||
#### 4.10.1 两个读即清寄存器与两级门控
|
||
|
||
| 寄存器 | 命令 | 宽度 | 语义 | 合法读者 |
|
||
|---|---|---|---|---|
|
||
| 全局中断状态 | `GET_GLOB_INT_STATUS_ALL` | 16 位 | 含**全部 8 个 Socket** 的事件位(Socket n = bit(n+4));**整体读即清,芯片无按位清除手段** | **`net_poll` 唯一**(AGENTS 模块约定) |
|
||
| 本 Socket 中断状态 SINT | `GET_INT_STATUS_SN` | 8 位 | 单个 Socket 的事件位;读即清 | `net_poll` 分派 + §4.10.2 的定点排渣 |
|
||
|
||
`net_poll` 是**两级门控**:先读全局位,某 Socket 位为 0 就 `continue`、**不去读它的 SINT**。
|
||
⇒ 任何"顺手全量清一遍"的代码,等于**把别的在用 Socket 的门一并焊死**——它们的
|
||
CONNECT/DISCONNECT 边沿**永久丢失**(RECV 属条件位、下轮可补回,TRAP-NET-08)。
|
||
这就是"全局寄存器只有一个读者"这条约定的全部理由。
|
||
|
||
#### 4.10.2 现存读清点全量清单(产品代码 6 处)
|
||
|
||
| 位置 | 时机 | 会不会吞掉有用事件 |
|
||
|---|---|---|
|
||
| `net_poll` 全局读清(`net_socket.c`) | 事件汇聚入口 | 契约本体——读完立刻按位分派,不吞 |
|
||
| `net_poll` 分派读 SINT(同上) | 受全局位门控 | 不吞 |
|
||
| `net_listen_locked` 预清 | **`tcp_listen` 之前** | 芯片此时还没在听,**产生不了 CONNECT** |
|
||
| `net_relisten_locked` 预清(`net_listener.c`) | **`tcp_listen` 之前**(E14 定案改法) | 同上 |
|
||
| 沉降/隔离窗每 30ms(`net_listener.c`) | `close` 之后、`open` 之前 | 吞的是上一条会话残留 DISCONNECT/TIMEOUT = **本意排渣** |
|
||
| `net_connect_locked` / `net_close_locked` 预清、`sock_reset` 第 3 步(驱动) | 出向连接前 / 关闭前 / 复位排空至 quiescent | "本 Socket 即将被重新使用"的定点排渣(TRAP-NET-12/-18 两级预清) |
|
||
|
||
**共同点:全部只碰本 Socket,且都落在"这个 Socket 即将被重新使用"的边界上。**
|
||
⇒ 当前**没有已知的"事件被读清互吞"活口**(`test/board` 台架里的读清点不算——它们剥离了
|
||
net 层,本就以"我自己是唯一读者"为前提)。
|
||
|
||
#### 4.10.3 三条已根除的互吞形态(勿复活)
|
||
|
||
| 形态 | 危害 | 现状 |
|
||
|---|---|---|
|
||
| 遍历 **8 个 Socket 全清 + 再读清全局**(`net_listen_locked` / `net_relisten_locked`) | 见 §4.10.1——别的在用 Socket 边沿永久丢失;且它在**每次断连的热路径**上 | 已删(多连接时代的调试遗留被原样搬进单连接模型,**不属于任何 Trap 的修复**) |
|
||
| 沉降段的全局读即清 | 每 30ms 一轮就搁浅其它 Socket 的 CONNECT/DISCONNECT/RECV_OK;沉降成为每次断连热路径后放大成 **FTP 控制/数据通道互杀**(E7 实测 lftpd 5s 超时) | 已删(M4-lite,与 M1 同批) |
|
||
| 排事件放在 `tcp_listen` **之后** | listen 后读清把**快速复连新连接的 CONNECT** 当场吞掉 | 已挪到 LISTEN **之前**(生产与台架同源根除,E14) |
|
||
|
||
#### 4.10.4 一处「看着像缺陷、其实是配对消费」
|
||
|
||
`handle_connect_event` 末尾有一处 `(void)ch395f_get_sock_int_status()`,注释只写"清除残留
|
||
TIMEOUT",极易被误读成"随手丢弃整字节"。查调用链才定性:它有**三个调用点**,其中
|
||
**形态② 补 accept 这条路径根本没读过 SINT**(CONNECT 事件本来就没送到),芯片里那个
|
||
CONNECT 位还锁着——软件已补做 accept,**必须清掉**,否则 `net_poll` 下一轮看到全局位会
|
||
**二次分派同一个 CONNECT**。
|
||
|
||
残留风险只剩 net_poll 路径:从分派读 SINT 到此处是**微秒级窗口**,其间锁存进同一字节的
|
||
DISCONNECT 会被 `(void)` 丢掉。但 RECV_OK 是条件位(下轮补回)、DISCONNECT 由形态① 在
|
||
400ms 内兜住 → **有界,不建议改动**。⚠ 真要动它,必须先读懂形态② 那条路径再动手。
|
||
|
||
#### 4.10.5 再加一处读清的三条准入判据
|
||
|
||
1. **只碰本 Socket**——绝不碰全局寄存器(唯一读者 = `net_poll`)。
|
||
2. **必须在 `tcp_listen` 之前**(E14:之后读清会吞掉新连接的 CONNECT)。
|
||
3. **必须说清两件事**:吞掉的是**谁的什么位**(上一条会话残留 = 合法;本 Socket 新事件 =
|
||
违规),以及万一吞错**由哪条兜底**(对账形态 / 条件位自愈)。
|
||
|
||
**结构性残余**:两级门控存在"全局位没置、SINT 却有位"的**理论**可能。E14 的 48 轮里
|
||
2/48 属**门控观察伪象**(SINT 30ms 内其实已捕获),**不是已证实的漏读**。兜底仍是 I3:
|
||
**事件只负责提前唤醒,正确性靠回读 + 有界超时**。
|
||
|
||
## 5. 关键参数与实证锚点
|
||
|
||
### 5.1 参数表
|
||
|
||
| 参数 | 值 | 依据锚点 |
|
||
|---|---|---|
|
||
| `NET_RECONCILE_PERIOD_MS` | 100ms | E2:每轮全量刷新成本 ≈72µs;更密的周期会把 uiTask 最坏单轮推高(`net_listener.c` 注释留痕) |
|
||
| 去抖(分级) | ②=1 轮 / ③④⑤=2 轮 / ①=4 轮 | E2:LISTEN 可见 ≤10ms 无长尾 → ② 可单轮;①=4 轮 ≈400ms = M3 让位窗 |
|
||
| 冷却 | 1s | 防重建风暴(E8 风暴签名) |
|
||
| GRACEFUL / QUARANTINE / 留痕上限 | 100ms / 400ms / 800ms | TRAP-NET-18 二阶段实测;上限必须 < D10 2s(§4.3) |
|
||
| D10 | 30ms / 2s | TRAP-NET-09/-20:沉降窗不定长;2s 是预算不是终态 |
|
||
| `sock_reset_sync` 封顶 | 600ms | E2:LISTEN 态二段 CLOSE ~180ms 落定 + 余量 |
|
||
| KeepAlive | 60s / 5s / 3 | E11:判死 ≈30.0s;不误杀空闲会话 |
|
||
| recv tripwire | 60s | §4.5 挂起留痕探针(不改语义) |
|
||
| 恢复窗(正常断连→监听可用) | 0.3~1s | E1 发现 A:释放只置事实 → 等周期 tick → SYN 静默丢 → 客户端 1s RTO → `:7003` 复连 p50≈1020ms |
|
||
|
||
### 5.2 实验锚点速查(E 编号 = 台架实验序号)
|
||
|
||
> **`E` + 序号 = 台架实验编号**(E = Experiment),不是错误码、也不是小节号。每个实验遵循
|
||
> 「假设 → 方法 → 判据 → 它门禁哪个决策」四段式;本表只给**方法与结论**,原始记录(假设、
|
||
> 原始日志、统计)已归档,取用命令见 §12。
|
||
> ⚠ **这些编号是冻结标识**:`net_socket.c` / `net_events.c` / `net_listener.c` 注释、
|
||
> `test/host/e02_probe.py`、`test/host/e6_soak.py`、`docs/问题记录/CH395F_Trap_Records.md`
|
||
> 都直接引用它们,**重排或改名会断链**。
|
||
> ⚠ 末行 **不是实验编号**——它记录的是**历史审计发现**
|
||
> (带连字符的 `E-<nn>` 族,属主文档已于 2026-10-15 删除并冻结),列在此处只为记录该发现的**收口台架**。
|
||
> 区分规则:**带连字符 = 审计发现,无连字符 = 自愈实验**。
|
||
|
||
| 编号 | 验证了什么(方法) | 结论(现状口径) | 落点 |
|
||
|---|---|---|---|
|
||
| **E0** | 直接读生产常态串口日志(开机后 ~3.5 h,零台架成本) | 常态下对账**零命中**(冷路径);日志噪声主要来自 D10 逐轮留痕 | reconciler 热路径成本可接受;重试日志只留首末 |
|
||
| **E1** | 现网固件 + PC 压测:`n30_repro.py` 背靠背 STOR、`ftp_regression`、`:7003` 快速复连、iec104 连断 | 恢复窗 0.3~1 s(`:7003` 复连 p50≈1020 ms);背靠背 STOR 存在失效循环 | `n30_repro` 纳入常备回归 |
|
||
| **E2** | 裸机 bench(`ch395f_reuse_bench.c` + DWT 计时):`GET_SOCKET_STATUS` 单次耗时;LISTEN 命令后回读看首次可见 TCP=01H 的延迟 | 单次回读 **18.1 µs**(全量刷新成本可忽略);LISTEN 可见延迟 ≤10 ms 无长尾;**LISTEN 态 CLOSE 是二段**(一次只拆监听,byte0 恒留 05H) | 驱动缓存刷新策略;形态② 去抖 1 轮;`sock_reset` 内置二段补发 |
|
||
| **E3** | 同场固件,OPEN / LISTEN / CLOSE / SET_* 五条命令各 50 样本,测 `GET_CMD_STATUS` 判别力 | OPEN / LISTEN / CLOSE **无判别力**,陈旧错误码 0x17 跨命令持续存活;SET 类**完全不更新** | 命令完成判据改用**状态回读**(`ch395f_cmd_done`) |
|
||
| **E4** | 裸机 3 复位变体 ×20 轮:先造污染会话(发 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`;静默回收在累计 ~110 次连接中断里**零复现** | 缺口真实但触发罕见 → 形态⑤ 顺带修,不单独立项 |
|
||
| **E6** | 插桩固件(`NET_U15_PROBE=1` + `test/host/e6_soak.py`):常态 + 每小时 `:7003`×10 + 压测,探"`GET_RECV_LEN>0` 但 `!readable`"失配 | 命中 **0**(~4 h 常态) | U15 观察项**不立项**(I8);上层盲收保留现状(§4.8) |
|
||
| **E7 / E7b** | 候选修改 M1(GRACEFUL 沉降无条件化)+ M2(门控换 `net_listener_armed`),叠加/去掉 M4-lite(删沉降段全局读即清),跑 `n30` | M1+M2+M4-lite 把 `n30` 成功率 **8% → 96.7%**;**M1 单独上会放大全局读即清的搁浅面(§7)** | 三者即现状(§4.2 门控 / §4.3 沉降);纪律:**M1 与 M4-lite 必须同批** |
|
||
| **E8** | 同固件同负载,**只变设备状态**跑 `n30` 矩阵(全新启动 / 累积后 / 仅 ST-Link 复位) | 故障率在 **2%~54%** 间随状态摆动 → 故障率 ∝ 芯片侧残留存量;抓到"风暴签名"三机制(§7「风暴态三机制」) | M3 可疑门 + `sock_reset` quiescent 排空;"沉降让位救回"红线 |
|
||
| **E9** | M3 可疑门判据三轮迭代(`0x05/0x00` + `lost_conn_seen==0` + `recv_bytes==0` + 芯片 RX 有在途数据) | 判据定案为**资产门版**;风暴态免疫 + 零误伤 | §4.1.6 M3 门 |
|
||
| **E10** | 不变量与机制定案后的全量台架回归 | 全绿;暴露 **`net_listener_armed` 必须放行 ESTABLISHED**——漏放行时 i104 的 200 ms 轮询抢不过形态② 去抖而整场饿死 | §4.2 门控定案 |
|
||
| **E11** | 临时固件开启服务器侧 KeepAlive(`CMD_SET_KEEPALIVE_SN`,60s/5s/3),用"禁用 PC 网卡"造"链路在、对端静默" | 真实可用;判死 **≈30.0 s**(早于手册公式 75 s,以实测为准);静默但**存活**的对端不误杀 | §4.7 KeepAlive 定案 |
|
||
| **E12** | SPI 缓存区 TSL 时序 A/B:手册要求"长度与数据之间"留 TSL(读 0.4 µs / 写 0.2 µs),本仓把命令头与数据打包进同一 DMA 突发(间隔=0),对照两臂 | 460 轮 / ≈58 MB 双向 SHA-256 **无可复现危害**、两臂无差异 | §6 红线 6;"两段+TSL"变体留作**待用补丁** |
|
||
| **E13** | 产品固件 + PC 网卡抖动注入,追"残渣风暴态"根因 | 根因 = **僵尸会话**(对端静默死亡、芯片侧 ESTABLISHED 未清、单连接监听被占),而非跨烧录残渣;`RSTI←NRST` 推翻"跨烧录存活";自愈链端到端 30~90 s 有效 | §4.7;PASV 预清缺口由 `sock_reset` 收口 |
|
||
| **E14** | 48 轮直读 SINT 矩阵复核"丢中断"说法(drain-int 现场抓取 + SINT 补读 ×3 + 超时归类) | **芯片侧事件丢失 = 0**;"丢失"实为 drain 读清消费 + 门控竞态 + 主机同步;幽灵态本轮 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 驻留);WCH 官方博客确认"断连即重连被拒"、官方方案 = 关闭完成后重开 | TRAP-NET-20 口径"谎报"→"如实报告";settle 等待 = 官方方案;幽灵甄别 = 护栏;I3 加强 |
|
||
| **E16** | 把 relisten 隔离段收口到驱动 `sock_reset` 原语(删 net 层自建序列与参数副本),同板 A/B 跑 `ftp_abort_probe` + `relisten_stress` + `n30_repro` | **收口失败、已回退**:失效面精确落在"中止后首笔复连";根因两条——残渣在 CLOSE **之后**才落进 RX(一次排空接不住);复位机"逼回 CLOSED"的目标与上层立即复用同一 Socket 冲突 | §8 延后项 1(三条前置语义);§4.4 警告;登记「relisten 沉降段原语化受阻」 |
|
||
| **E17** | **单一所有者收口**:`net_listener_try`(当场动作、**无状态门**,各调用点自带门)改为 `net_listener_request`(只登记意图)+ `net_listener_tick`(唯一执行者,门集含"芯片已在 LISTEN 即了结"),同板全量回归 | 全绿;**延后一轮(≤1 ms)执行不伤即时性**(恢复窗 p50 与当场动作持平);relisten 留痕减半 | §4.2 登记/执行分离;§9.2 门集;§11.1 判据 |
|
||
| **E18** | **负向注入打靶 · 三轮**(注入代码不入库):① 吞掉 DISCONNECT(在 i104 的 200 ms accept 轮询窗内 RST);③ 绕过本层簿记直接 close 芯片空闲监听;⑤ 开盲区吞掉芯片自发的中断 + 等芯片真 CLOSED 再 open+listen | **①③⑤ 全部命中并救回**;①⑤ 的前提会被更快的消费者路径消解 → 常态 0 命中;⑤ 还要先越过芯片 `FIN_WAIT_2`(E15 的 2MSL 长尾,实测 ≈1 s) | §4.1.4 / §4.1.5:**三条均不可降级为只留痕**;⑤ 的**取消挂起 recv/send** 不可省 |
|
||
| **E19** | **`force open` 反事实 A/B**(本层已收口 CLOSED + 芯片停 `FIN_WAIT_2` `0x05/0x06` + **对端挂住不关** = 队头阻塞场景;A = 现状放行,B = 只留痕不放行) | **放行与不放行的端口恢复时刻同一分布** → 动作在全部可产现场景内无可测收益;真正兜住"监听器永久失效"的是 **D10 预算 + 形态④ 重新登记**。两条实测坑:① 本层直接改 `state` **不会**进沉降窗(窗只由 `net_listener_settle` 开启);② `relisten retry budget exhausted` 在此被稳定复现 = 对端不配合拆链的签名,**非回归** | §4.3:800 ms 上限降级为**只留痕**(`keep waiting` 只留首末、隔离标志不清 → 继续排渣) |
|
||
| **队头阻塞审计**(非实验编号) | `test/host/e02_probe.py` 台架定案 `net_drop_conn` 收口后的时序 | 对端配合(收到 FIN 后其内核 close):`TIME_WAIT` ~150 ms → 形态④ relisten,恢复全程 ~500 ms;对端内核存活但不 close:芯片停 `FIN_WAIT_2` 长尾,**监听恢复受限于对端 close** | §4.6 |
|
||
|
||
## 6. 红线(明确不做的事)
|
||
|
||
1. **不改 EXTI/ISR**:全局事件寄存器整体读即清,ISR 读寄存器会把事件从 netTask 抢走
|
||
——正是 B3 事故形状。
|
||
2. **不启用多连接模式(FUN_PARA bit1)**:不支持多监听(A′ 出局);监听常驻只能软件承诺。
|
||
3. **不删消费者驱动即时登记 ×3**(§4.2;删除 → CLOSED 窗口拉长 → `[WD] task stall`)。
|
||
4. **不改单连接复用模型**:硬 TCP 栈原生主流形态(ioLibrary 无 accept、FreeRTOS reuse
|
||
同形),不是对 BSD 的偏离。
|
||
5. **SPI 分频 8、中断经 `net_poll` 集中读清**:PCB 约束 + I7(护栏是收窄读法,不是改路径)。
|
||
6. **不改 TSL 单突发**:E12 实测 460 轮/≈58MB 无危害;B 变体(两段+TSL)留作待用补丁
|
||
(已证明无吞吐/正确性回退)。
|
||
7. **不做盲收内化 / 不加 U15 对账**:E6 命中=0(I8)。
|
||
8. **EOF 时不得 `net_close` 监听 fd**:模块约定铁律(会连监听一起毁掉)。
|
||
9. **不把 DONE_CLOSE 当单次 CLOSE 的充分条件**:LISTEN 态 Socket 需二段(E2)
|
||
——走 `sock_reset`(内置补发)或保留 TRAP-NET-09 式重试。
|
||
10. **不把监听重建接进 `sock_reset` 原语**:三条前置语义未补齐(§8 延后项 1,E16)。
|
||
|
||
## 7. 编号发现速查(12 条,历史对照表)
|
||
|
||
> ⚠ **本节的条目号已于 2026-10-27 停发(编号规范 §9 批次一),正文/注释改用下表「条目名」列指代**;
|
||
> 下表保留编号列仅作历史对照(删表会让历史行的引用报「未登记前缀」假错),
|
||
> 全仓不得再新增对这些号的引用——代码注释与关联文档已改写为「条目名 + §7 落点」的语义形式(2026-10-27 批次一收口:
|
||
> 本文件残留 9 处正文引用(其中孤儿号 `F-DRV-9` 就地删号留事实)+ 1 处主机侧单测注释已全部转语义)。
|
||
|
||
| 编号(历史对照) | 条目名(现行指代) | 一句话结论 | 收口落点 |
|
||
|---|---|---|---|
|
||
| F-NET-2 | `静默回收无人认领` | "本层 ESTABLISHED 而芯片已回 LISTEN"无机制认领 | 对账形态⑤(§4.1) |
|
||
| F-NET-3 | `冗余监听 API 面` | `net_relisten` / `net_resync_listen` / `net_can_accept` 是冗余或过期 API 面 | 全部删除;监听门控统一 `net_listener_armed`(§4.2) |
|
||
| F-NET-4 | `iec104 过期门控` | iec104 过期门控 → 丢 CONNECT 探针对 :2404 从不运行 | 门控统一 `net_listener_armed`(§4.2) |
|
||
| F-NET-7 | `全局读即清搁浅事件` | 全局寄存器读即清残留会搁浅其它 Socket 事件 | I7 + `ch395f_sock_events` 护栏(§4.10) |
|
||
| F-NET-15 | `沉降门放大器` | GRACEFUL 沉降门 `recv_bytes≠0` = 背靠背失效循环放大器 | 沉降无条件化(与删沉降段全局读即清同批,§4.3) |
|
||
| F-NET-17 | `风暴态三机制` | 风暴态三机制(沉降自再生锁存 / 泵与救回互抢 RX / 芯片 OPEN 卡死放大) | M3 可疑门 + `sock_reset` quiescent + 让位红线(§4.1.6) |
|
||
| F-DRV-1 | `七处独立回读` | 同一芯片事实 7 处独立回读、三套去抖参数——单一事实源缺失是补丁膨胀的地基 | 驱动状态缓存 = 单一事实源(I1,§4.4) |
|
||
| F-DRV-4 | `close 序列两套变体` | "close+排空+等 CLOSED"两套同构变体参数漂移 | `ch395f_sock_reset` 统一(§4.4) |
|
||
| F-DRV-8 | `命令完成阻塞原语` | 命令完成等待是阻塞原语(`HAL_Delay` 忙等、4s 上限) | `cmd_done` 状态回读判据 + 非阻塞状态机(§4.4) |
|
||
| F-DRV-13 | `链路事件无人消费` | 链路事件四个全局位读清后无人消费,运行期拔网线完全静默 | `glob_event_feed` + `link_up` 缓存(§4.4) |
|
||
| F-MS-8 | `有损镜像决策窗口` | 芯片 11 态 → net 层 8 态有损投影,有损镜像必生决策窗口 | 镜像降级为会话投影(I1、§3.3) |
|
||
| F-MS-9 | `去抖冷却自偿成本` | 去抖/冷却是镜像世界的自偿成本 | 探针去抖已删(读缓存无瞬态);reconciler 保留分级去抖(让位窗需求) |
|
||
|
||
## 8. 延后项
|
||
|
||
1. **relisten 沉降段统一到驱动 `sock_reset` 原语(受阻)**:原语要能承接监听重建,先补三条
|
||
语义(缺一即回归,E16 实测数据见 §5.2):
|
||
① **窗口内持续清**:残渣在 CLOSE **之后**才落进 RX,只在 CLOSE 前排一次接不住——旧序列
|
||
每个 30 ms 重试轮都 CLOSE + 排空 + 排事件,整个隔离窗持续清,原语的"一次 CLOSE +
|
||
一次排空"与之不等价;
|
||
② **复用让位**:复位机的目标是把本 Socket 逼回 CLOSED,而 FTP 数据通道每笔传输都在同一
|
||
芯片 Socket 上重建监听;复位机未了结时它会去拆刚建的监听(WAIT_CLOSE 的二段 CLOSE
|
||
补发),并让 `net_sock_reset_sync` 600 ms 超时 → `close` 跳过排渣、`listen` 的 open
|
||
重试环退化成无等待空转;
|
||
③ **槽位可加入**:每 Socket 只有一个复位槽位,`ch395f_sock_reset_begin` 返 -1(已在运行)
|
||
不等于失败——同步包装必须**加入**已在跑的复位机,否则 `net_close` 走 "proceed
|
||
anyway"、排渣被跳过。
|
||
补齐 ①②③ 并各自回归后再接;登记 `docs/任务规划/登记簿.md` 条目「relisten 沉降段原语化受阻」。
|
||
2. **部分读→再查 recv_len 定向 TC**:旧"部分读归零"机理未证实(与手册 §5.34 及
|
||
TC-NET-201 相反),注释约束已降级为历史观察——以定向用例结案(陷阱记录「台架纪律」
|
||
节已挂该待办)。
|
||
3. **U15 观察项**:若将来出现数据面失配(RX>0 但 `!readable`),重新武装探针重裁(§4.8)。
|
||
4. **链路事件业务消费**:缓存与留痕已就绪;PHY_CHANGE → 自动重配的触发待产品需求。
|
||
|
||
## 9. 代码级事实清点
|
||
|
||
> 判读本架构或做决策时需要的**源码事实**。⚠ 只给符号名、不固化行号与行数——它们随重构
|
||
> 漂移,是"读代码前的地图"而**不是判据**;要判据请数符号,别数行。
|
||
|
||
### 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_tick`(唯一执行者) | 门集**只在这一处**:状态 ∈ {LISTENING, CLOSED} + 监听型 + **芯片已在 LISTEN 即视为达成、绝不 open**(U12 结构性防护)+ 挂起保险。登记侧 `net_listener_request` 因此**不需要**状态门——任何"当场动作 + 调用点各自补门"的写法都会把 U12 风险复制 N 份(对已 LISTENING 的 fd 再 open+listen 会因 USE 错误走进关闭分支、**关掉正听着新 SYN 的 Socket**) |
|
||
|
||
### 9.3 三个服务的连接结束契约(现状)
|
||
|
||
| 服务 | 连接结束信号 | 监听重建者 | 备注 |
|
||
|---|---|---|---|
|
||
| `lftpd` 控制 :21 | `read_line != 0`;阻塞挂起 recv 保证 CLOSED 时必被结束(FIN→0 / 异常→CONNRESET) | net 层;应用**永不** close 控制 fd | 契约一致,**正面样板** |
|
||
| `time_sync` :8888 | EOF / 预算耗尽 → `net_drop_conn` 拆会话保监听 | net 层(形态④) | 同 fd 即监听 fd,应用侧**不得** `net_close`(红线 8)→ 走 §4.6 |
|
||
| `ui_net_sink` :7003 | `net_recv` 返 0 或真错误;非阻塞收帧 | net 层 | 重建条件**不得**写成 `net_get_sock(g_listen)==NULL`(fd 不释放 → 恒假);靠 `session_dead` 电平 + 形态④ |
|
||
| 模拟器后端 | `recv` 0 恒可得、fd 分离 | 无需 | Winsock 原生;对上述全部失效模式**零覆盖**(D6:双模型分裂暂不消除) |
|
||
|
||
## 10. 决策记录
|
||
|
||
| # | 决定 | 依据 |
|
||
|---|---|---|
|
||
| D1 | **A′(启用芯片多连接模式)出局**,根治必须在单连接复用模型内做 | §1.4:多监听不支持 / 池按源端口匹配会互串 / 实测 TRAP-NET-02、-11 / FUN_PARA 初始化后不可改 |
|
||
| D2 | 采用**电平化 + 单一所有者**路线,**不做监听器一等对象化**(listener 独立对象 + `accept` 返回虚拟句柄) | 后者需控制面重写 15~25 函数、RAM/Flash 上升,只有"同端口并发多连接"需求才值得 |
|
||
| D3 | 周期对账**保留**,其副作用由不变量消除 | TRAP-NET-17 实证事件面不是可靠唯一信源(E14 复核:非芯片侧丢失,是读清消费)→ 周期对账不可删 |
|
||
| D4 | `FUN_PARA` **保持 `0x08`**,不因孤儿态改配置 | bit3=1 是"保留排空现场"契约;bit3=0 会令芯片在 DISCONNECT/TIM_OUT 时**当场清缓冲** →「发完即 FIN」的尾部数据必被硬件擦除、**不可补救**。完整论证见 §4.9 |
|
||
| D6 | 真机/模拟器**双模型分裂暂不消除** | 消除即等于做监听器一等对象化;先靠不变量把应用侧契约对齐 |
|
||
| D7 | 同族残留**不在监听生命周期根治范围**,各自单列:U1(CONNECTING 滞留)、U11、U12、U14、U15,以及审计项 B4~B9 | 与监听生命周期不同因,混做会让回归面失控 |
|
||
|
||
> `D5` 是"分阶段实施顺序"的决策,实施完成后不再构成任何设计含义 → 已删除;**编号不回收**
|
||
> (沿用 `docs/编号规范.md` §5.5 的游标不回退约定)。
|
||
|
||
## 11. 模块化边界(`net_listener.c` / `net_events.c`)
|
||
|
||
> **本节只回答"组织度"**:搬什么、边界在哪、接口多窄。若问的是"这套沉降 / 重试机器本身
|
||
> 是不是我们想多了"——那是**合法性**问题,见 §1.5 与 ioLibrary 的代码级对照。
|
||
> 前提:**先收敛,再搬移**——§4.1 先把周期性重建收敛到唯一决策者,搬移边界才清晰。
|
||
|
||
### 11.1 可机械检验的判据(现状值)
|
||
|
||
| # | 判据 | 现状 |
|
||
|---|---|---|
|
||
| 1 | 模块外调用「重建监听」的**入口** | **单一登记入口 `net_listener_request` + 单一执行者 `net_listener_tick`**;5 个登记点(形态③④ / accept CLOSED 分支 / recv EOF / `net_drop_conn` / 挂起 recv 断连),**0 处模块外执行** |
|
||
| 2 | `s_relisten_*` 静态状态被**模块外直接读写** | **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_request` / `net_listener_tick` / `net_listener_clear` / `net_relisten_done` | 「维持 fd 处于 LISTEN 态」的全部机制 |
|
||
| `net_drain_chip_rx` | 重建前的排空(只被 relisten 调用) |
|
||
| 8 个 `s_relisten_*` 数组 + 7 个 `NET_RELISTEN_*` 常量 | 状态与策略私有化(`s_relisten_capped` 属 §1.5「可省清单」) |
|
||
|
||
**在 `Drivers/BSP/NET/net_events.c`**(**真机专属**,sim 不编):
|
||
|
||
| 内容 | 理由 |
|
||
|---|---|
|
||
| `net_reconcile_tick`(形态①②③④⑤/U11) | 周期面的失同步判据 |
|
||
| 5 个 `handle_*_event` | 边沿面的事件处理 |
|
||
| `net_lost_connect_probe` | 形态② 的补 accept 动作(与对账同职责) |
|
||
|
||
**留在 `net_socket.c`**(并说明理由,避免"看着像就搬"):
|
||
|
||
| 内容 | 理由 |
|
||
|---|---|
|
||
| `net_listen_locked` | 是**应用 API 路径**(首次监听,含 BOUND/CREATED 状态门与缓冲)——属"连接层建监听",不是"维持监听" |
|
||
| `net_accept_locked` | 产出的是**连接**(LISTENING→ESTABLISHED 迁移),属连接层;它只**调用**新模块 |
|
||
| `net_listener_armed` | **双端公开 API**(sim 也实现);纯 RAM 查询、很薄,留在原处避免 sim 缺符号 |
|
||
| `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 函数 + 2 枚举,全部窄接口)
|
||
|
||
```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;
|
||
|
||
typedef enum { /* 登记来源:仅用于留痕归因,不改变动作 */
|
||
NET_LISTENER_REQ_RECON = 0, /* 对账形态③④:芯片侧监听失位 / 孤儿监听 */
|
||
NET_LISTENER_REQ_ACCEPT, /* 消费者 accept 撞上 CLOSED 的监听 fd */
|
||
NET_LISTENER_REQ_EOF, /* 消费者 recv 读到 EOF */
|
||
NET_LISTENER_REQ_DROP, /* net_drop_conn 拆会话保监听 */
|
||
NET_LISTENER_REQ_PENDING, /* 挂起 recv 因断连结束 */
|
||
} net_listener_req_t;
|
||
|
||
void net_listener_settle(int sockfd, net_listener_settle_t level); /* 只置事实,不动作 */
|
||
void net_listener_request(int sockfd, net_listener_req_t reason); /* 只登记意图,不碰芯片 */
|
||
void net_listener_clear(int sockfd); /* 清本 fd 的重建/沉降状态(新 fd 或 close 时) */
|
||
void net_listener_tick(void); /* 唯一执行者(netTask 每轮,非阻塞;30ms/2s) */
|
||
```
|
||
|
||
**反向依赖(新模块需要宿主提供的)**:`net_sock_t` 定义(`net_types.h`,共享头,不算反向
|
||
依赖)、`net_set_errno` / `NET_ERR_RETURN_1`、`net_pending_active`、`net_sock_raw`、
|
||
`ch395f_*` 驱动、`HAL_GetTick`。`net_listener.c` **不访问** `s_net_socks`——一律用
|
||
`p_sock->ch395_sock` 或调用方传入的 `sockfd`。
|
||
|
||
### 11.4 当前代码形状的由来(四条非搬移约束)
|
||
|
||
1. **`net_set_errno` 由 `static` 提升为模块间可见** —— 否则新模块链接期报 `L6218E:
|
||
Undefined symbol`;`NET_ERR_RETURN_1` 宏随之迁到 `net_socket.h`(它依赖前者,不能只移宏)。
|
||
2. **`net_pending_active(fd)` / `net_send_pending_active(fd)` 薄访问器** —— 监听器的
|
||
「本 fd 有在途挂起则跳过」保险(防重开 Socket 摧毁在途连接/在途传输)不得直读挂起 static
|
||
表,**语义原样保留**。
|
||
3. **`net_sock_raw()`** —— 对账必须看到 `in_use==0` / `state==CLOSED` 的槽位(形态④ 认领
|
||
孤儿监听正依赖 CLOSED 态),而 `net_get_sock()` 对未在用槽位返回 NULL 会漏掉这一类。
|
||
声明放**配对头** `net_socket.h` 而非内部头 `net_events.h`——规范 **S17 2.2** 要求非 static
|
||
函数声明在配对头文件。
|
||
4. **两个新模块都真机专属** —— 依赖 `ch395f_*` SPI 驱动与驱动状态缓存,而 sim 是 Winsock
|
||
后端(无 `ch395f_*`、无孤儿态、无"事件未收到/静默回收"概念)→ **无法**在 sim 编译。
|
||
故只注册 Keil 端,并在 `scripts/check_registration.py` 的 `SIM_EXEMPT` **显式点名**
|
||
(遵"显式清单 + 每行理由"原则)。
|
||
|
||
### 11.5 为什么对账与事件处理是**一个**模块(`net_events.c`)
|
||
|
||
判据(`net_reconcile_tick` 形态①②③④⑤/U11)与边沿事件处理(5 个 `handle_*_event`)是同一
|
||
职责的两半——一个周期面、一个事件面,都在做"芯片状态面 ↔ 本层镜像"的收敛。
|
||
|
||
**为什么不是一个模块拆成两个**:剥掉注释后实测**对账区真实调用事件处理器 2 处**(形态② 补
|
||
accept 走 `handle_connect_event`),且 `NET_RECON_STABLE_DEATH` 两边共用。拆成
|
||
`net_events.c` + `net_reconcile.c` 会造出跨模块调用;合起来这 2 处变模块内直调,
|
||
**跨模块导出为零**。
|
||
|
||
**两条可复用的方法论**:
|
||
|
||
- ⚠ **依赖扫描必须先剥注释**——未剥时 `alloc_socket` / `net_close_locked` /
|
||
`net_relisten_locked` 全被误报成依赖(其实只是注释里的提及),足以把人引向错误的边界。
|
||
- **搬移类改动要配机械证明**:剥注释后取**代码行多重集**,新旧之和比对,差异必须**恰好等于**
|
||
声明的改动集(fd 取法替换、`static` 去除、前向声明、新文件自身头部、新增访问器),
|
||
**无一处意外**——否则"注释和代码混在一起改"无法区分意外丢失。
|
||
|
||
## 12. 引用与归档约定
|
||
|
||
- 本文引用格式:`docs/网络/网络自愈架构设计.md` §N.M(紧邻写法);E 编号引 §5.2
|
||
(如"§5.2 E12"),F 编号引 §7。
|
||
- **勿引用已删除的过程章节**。过程考古:
|
||
- N34 自愈审计(三路审计、39 条编号发现全文、E0~E15 原始记录、分阶段实施清单):
|
||
`git show 1a35b36^:docs/网络/N34-自愈审计/`(目录合并前 7 份文档)或
|
||
`git log --oneline -- docs/网络/N34-自愈审计/`;
|
||
- 监听生命周期(本文 §1.1~§1.4 / §4.9 / §9~§11 的来源,含 P0~P3 勾选清单与台架验收记录):
|
||
`git log --oneline -- docs/子系统设计/网络Socket监听生命周期设计说明.md`;
|
||
- `自愈收敛阶段 N` 只出现在历史文档与变更记录里(代码注释已清理为平实语言)——
|
||
**勿再引入**,设计含义一律以本文 §2~§4 为准。
|