From a0b9d278213023565bbae5eb2c5cb0cf7b4ff74a Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 13 Oct 2026 00:05:49 +0800 Subject: [PATCH 01/24] =?UTF-8?q?docs(net):=20N34=20BSD=20=E8=87=AA?= =?UTF-8?q?=E6=84=88=E5=AE=A1=E8=AE=A1=E4=B8=8E=E5=AE=9E=E6=96=BD/?= =?UTF-8?q?=E5=AE=9E=E9=AA=8C=E6=96=B9=E6=A1=88=EF=BC=88E0-E7b=20=E5=8F=B0?= =?UTF-8?q?=E6=9E=B6=E8=AE=B0=E5=BD=95=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 五层自愈机制盘点(F-NET-1..15)、CH395F 驱动层审计(F-DRV-1..16)、 主流方案对照(ioLibrary/FreeRTOS+TCP/mbed/Zephyr)、实施方案 (阶段 0..3 + P0 止血包 + 阶段 1.5 recv 语义主流化)、实验方案 (E0/E1/E5/E7/E7b 实测数据与判据)。 关键结论: - 对账是冷路径(E0:3.5h 零命中);D10 沉降/重试是每次断连热路径 - 断连→pump→重建产生 ~0.3-1s 监听空窗/会话(E1 :7003 p50=1020ms) - n30 背靠背 STOR 基线 4/50(F-NET-15 自持失效循环,首个入库可复核 背靠背数据;Trap 25 终版回归只含有间隔 ftp_regression 是测试面根因) - E7b:M1+M2+M4-lite 止血包 116/120,残余 ~3% 偶发(M3 域) - 静默回收形态 ~110 次连接零复现(F-NET-2 降为阶段 1 顺带修) --- docs/网络/N34-自愈审计/CH395F驱动层审计.md | 293 ++++++++++++++++++++ docs/网络/N34-自愈审计/README.md | 112 ++++++++ docs/网络/N34-自愈审计/net层自愈机制盘点.md | 235 ++++++++++++++++ docs/网络/N34-自愈审计/主流方案对照.md | 162 +++++++++++ docs/网络/N34-自愈审计/实施方案.md | 164 +++++++++++ docs/网络/N34-自愈审计/实验方案.md | 217 +++++++++++++++ 6 files changed, 1183 insertions(+) create mode 100644 docs/网络/N34-自愈审计/CH395F驱动层审计.md create mode 100644 docs/网络/N34-自愈审计/README.md create mode 100644 docs/网络/N34-自愈审计/net层自愈机制盘点.md create mode 100644 docs/网络/N34-自愈审计/主流方案对照.md create mode 100644 docs/网络/N34-自愈审计/实施方案.md create mode 100644 docs/网络/N34-自愈审计/实验方案.md diff --git a/docs/网络/N34-自愈审计/CH395F驱动层审计.md b/docs/网络/N34-自愈审计/CH395F驱动层审计.md new file mode 100644 index 00000000..45afaf54 --- /dev/null +++ b/docs/网络/N34-自愈审计/CH395F驱动层审计.md @@ -0,0 +1,293 @@ +# N34 自愈审计 · CH395F 驱动层审计(T2) + +> 审计对象:`Drivers/BSP/CH395F/ch395f.c`(1411 行)/ `ch395f.h`(750 行)、`Src/` 中断与引脚配置、 +> `Drivers/BSP/NET/net_socket.c`(3671 行)与 `net_listener.c`(554 行)中**由驱动层契约决定的部分**。 +> 方法:驱动 API 全量清点 + 中断路径实证 + 全部 Trap 台账通读归类 + git 考古定位根因修复落点。 +> 行号均为本工作树 HEAD(`09d6195`,分支 `task/N34-bsd-selfheal-audit`)实测行号。纯只读,未改任何代码。 +> 参照文档:CH395F_Trap_Records.md(含 2026-10-11 寄存器审计 B1~B10)、 +> 网络Socket监听生命周期设计说明.md、BSD_Socket_API_使用指南.md、主流方案对照.md(T3)、 +> net层自愈机制盘点.md(T1)。 + +## 1 驱动层 API 面盘点 + +### 1.1 驱动"知道"什么(可回读的芯片事实) + +| 芯片事实 | 驱动 API | 位置 | 语义要点 | +|---|---|---|---| +| Socket 是否打开 + TCP 态 | `ch395f_get_socket_status` | ch395f.c:1356 | **2 字节**输出:`[0]`=`00H`CLOSED/`05H`OPEN,`[1]`=TCP 态;`[1]` 仅在 `[0]`=OPEN 时有意义 | +| 单 Socket 事件 | `ch395f_get_sock_int_status` | ch395f.c:1324 | **读即清**(GET_INT_STATUS_SN) | +| 全局事件汇聚(16 位) | `ch395f_get_glob_int_status_all` | ch395f.c:1305 | **整体读即清**,芯片无按位清除手段 | +| RX 积压字节 | `ch395f_get_recv_len` | ch395f.c:1274 | 低字节在前;可反复查询(手册 5.34) | +| 对端 IP:Port | `ch395f_get_remot_ipp` | ch395f.c:336 | 6 字节,**小端**(与 SET_DES_IP 的点分序相反) | +| PHY/链路 | `ch395f_get_phy_status` | ch395f.c:875 | 返回 `01H`DISCONN…`08H`100M FULL | +| 命令完成 | `ch395f_get_cmd_status` / `ch395f_poll_cmd_status` | ch395f.c:629 / :654 | 后者 `HAL_Delay(20)` **阻塞忙等** | +| SPI 通道健康 | `ch395f_spi_byte_fault` | ch395f.c:265 | 锁存式(复位前不清),仅首条故障记日志 | +| 芯片版本 | `ch395f_get_version` | ch395f.c:310 | 仅 net_init 打印一次(net_socket.c:259),**不参与门控** | +| MAC/IP/掩码/网关 | `ch395f_get_mac_addr` / `ch395f_get_ip_inf` | ch395f.c:819 / :847 | 回读用于自检留痕 | +| ARP 不可达现场 | `ch395f_get_unreach_info` | ch395f.c:1402 | **全仓零调用**(B4) | + +### 1.2 驱动暴露的原语(51 个公共函数,按职责分组) + +| 组 | 函数 | 位置 | +|---|---|---| +| SPI 事务 | `write_cmd` / `write_data` / `read_data`(**公开**)、`spi_byte_fault`;`spi_begin`/`spi_end`/`spi_dma_transfer`/`spi_fault_set` 为 static | ch395f.c:212/229/246/265;:76/:87/:120/:100 | +| 芯片检测与初始化 | `check_exist` / `get_version` / `reset` / `get_cmd_status` / `poll_cmd_status` / `init` | :278/:310/:613/:629/:654/:680 | +| 网络参数 | `set_ip_addr` / `set_gwip_addr` / `set_mask_addr` / `set_mac_addr` / `get_mac_addr` / `get_ip_inf` / `set_phy` / `get_phy_status` / `ping_enable` / `set_dhcp` / `get_dhcp_status` / `set_arp` / `set_ttl` / `get_unreach_info` | :702/:731/:760/:791/:819/:847/:898/:875/:918/:937/:956/:1375/:1389/:1402 | +| 缓冲几何 | `set_recv_buf` / `set_send_buf` / `clear_recv_buf` | :416/:443/:468 | +| TCP 全局参数 | `set_fun_para` / `set_tcp_mss` / `set_retrans_count` / `set_retrans_period` / `set_keepalive_idle` / `_intvl` / `_cnt` / `_enable` | :372/:394/:573/:592/:487/:509/:531/:551 | +| Socket 生命周期 | `set_proto_type` / `set_des_ip` / `set_des_port` / `set_sour_port` / `open_socket` / `close_socket` / `tcp_listen` / `tcp_connect` / `tcp_disconnect` | :980/:1003/:1034/:1059/:1085/:1100/:1115/:1130/:1145 | +| 数据面 | `write_send_buf` / `read_recv_buf` / `get_recv_len` | :1166/:1222/:1274 | +| 状态回读 | `get_glob_int_status_all` / `get_sock_int_status` / `get_socket_status` / `get_remot_ipp` | :1305/:1324/:1356/:336 | + +**驱动持有的状态 = 零。** 全文件仅 4 个 static(ch395f.c:50-56):`s_spi2_dma_done`、`s_spi_fault`、 +两个 DMA 缓冲。没有任何 socket 状态缓存、没有链路态缓存、没有事件缓存——驱动是纯命令转发层。 + +### 1.3 SPI 事务包裹与集中中断读取约定 + +- 每次事务由 static 的 `ch395f_spi_begin()`(CS PB12 SET→RESET)/ `ch395f_spi_end()` 包裹(ch395f.c:76/:87), + 但 `write_cmd/write_data/read_data` 是**公开**的(ch395f.h:301/:308)——任何调用方都能拼一条未包裹的事务。 +- SPI2 分频 **8**(Src/spi.c:77-88,CPOL LOW / CPHA 1EDGE / 软 NSS),PCB 约束,勿改。 +- 集中中断读取约定落在 **net 层**:`net_poll()` 先读 INT# 引脚(net_socket.c:435),低电平才读 + `GET_GLOB_INT_STATUS_ALL`(:441),再按 `1U<<(i+4)` 门控去读该 Socket 的 SINT(:464/:469)。 + 驱动只提供两个裸的读即清原语,**没有任何护栏**。 + +### 1.4 中断路径结论:纯轮询,无 ISR + +- `CH395_INT_Pin`(PE5)在 `Src/gpio.c:76-80` 配为 `GPIO_MODE_INPUT` + `GPIO_NOPULL` → **无 EXTI、无 ISR**。 +- `EXTI9_5_IRQn` 属于 `TP_BUSY_Pin`(PC7,Src/main.h:124-125、gpio.c:170-171、stm32f4xx_it.c:391),与 CH395F 无关。 +- 事件消费节奏 = netTask 主循环 `net_poll()` + `net_process_messages()`(App/task/net_task.c:241-279,约 1ms 一轮)。 + +**判定:现状正确,不应改 EXTI。** 芯片的全局事件寄存器是"整体读即清",ISR 内读寄存器会把事件从 +netTask 手里抢走(正是 B3 事故的形状,见 F-DRV-2)。缺的不是中断,而是**驱动侧的事件分发与缓存**。 + +## 2 Trap 根因归类总表 + +台账共 **24 条**(编号 01–25,**Trap 19 从未存在**,属悬空引用)。根因类别: +**(a)** 芯片真实行为(不可在固件侧根治);**(b)** 固件寄存器值/常量错误;**(c)** 引用错误 +(字节序/偏移/索引/单位);**(d)** 时序误用(顺序/忙等/沉降);**(e)** 设计缺口;**(f)** 判读或台架假象。 + +| Trap | 症状(一句话) | 类别 | 根因修复落点 | 催生自愈补丁 | 根因修好后补丁是否仍必要 | +|---|---|---|---|---|---| +| 01 | Socket 4~7 的 CONNECT 中断不触发 | (d)+(a) | net_socket.c:333-340(net_init 内 7→0 逆序给全部 8 个 Socket 显式配缓冲) | 否 | — | +| 02 | Socket 4~7 发送固定垃圾/接收不落数据(缓冲重叠) | (c) | net_socket.c:334-340 + ch395f.h:136-141(固定 6 块/Socket,用满 48 块) | 否 | — | +| 03 | DHCP 包 xid 偏移错 | (c) | 主机侧测试工具(历史) | 否 | — | +| 04 | UDP 写完不等 SENBUF_FREE → 下次写被静默丢弃 | (a) | net_socket.c:2721-2753(`send_ready` 门控) | 否(属正常流控) | 是 | +| 05 | 自动协商与 RTL8305NBI 不兼容 | (a) | net_task.c:110(强制 100M 全双工) | 否 | 是 | +| 06 | KeepAlive 非 500ms 倍数 → 内部定时器异常 | (a) | net_config.h:61-71 + net_socket.c:288-290 | 否 | 是 | +| 07 | 只 disconnect 不 close → 重开必返 `0x20` | (f)→(a) | 原判"FIN_WAIT_2/RST"已证伪;真因 `FUN_PARA` bit3=1 芯片不自动关 Socket。轮询 **byte0**:net_socket.c:2326-2345 | 间接(催生"close→等 CLOSED→排空"序列) | 是(bit3=1 契约) | +| 08 | RECV 中断无限触发 | (a) | net_socket.c:429(`do{}while(0)` 单批处理) | 否 | 是 | +| 09 | PHY link up 同一时刻 `open_socket` 失败 | (a)+(d) | 根因侧已修:就绪屏障 net_task.c:232 + 启动期等 PHY link net_task.c:118-130 | **是**(D10 层) | **部分**:启动形态已被根因消除;close→reopen 沉降仍是芯片行为(Trap 25 实证窗口不定长)→ 重试保留,**应用层重复重试可删** | +| 10 | 6 个 API 永远返回 -1(消息队列按值拷贝) | (e) | 错误码改经 `xTaskNotify` 通知值回传 | 否 | — | +| 11 | accept 拒绝 standalone_accept 的 ESTABLISHED | (e) | net_accept_locked(net_socket.c:1281-1342) | 否 | — | +| 12 | FatFs `FR_INVALID_NAME` | 非 CH395F | lftpd_io.c | 否 | — | +| 13 | 分批读后 `recv_len` 归零 | (f) 存疑 | 无(机理未证实,与手册 5.34 及本仓 TC-NET-201 相反) | 否 | **约束已事实作废**(见 F-DRV-11) | +| 14 | close 后 DISCONNECT 滞后锁存,污染下次首读 | (a) | 预清 + 反核实:net_socket.c:2326-2345 / :1137、net_listener.c:262、net_socket.c:1670 | **是** | 是(Trap 23 实测伪 DISCONNECT 仍投递给下一条连接) | +| 15 | DHCP 成功后 IP 寄存器提交滞后 | (a) | 主机侧判据修正(历史) | 否 | — | +| 16 | TCP Client 目的 IP 字节序写反 → CONNECT TIMEOUT | (c) | net_socket.c:1636-1641 + :2691-2701(改大端),commit `db64e6ac` | 否 | 根因已修;**驱动 API 未封装字节序 → 复发面仍在**(F-DRV-5) | +| 17 | client 重连被上一条连接的残留 DISCONNECT 秒断 | (a) | 预清 + 反核实:net_socket.c:1660-1726 | **是** | 是 | +| 18 | "bit0=1 才是多 Socket 模式" | (f) 伪根因 | net_socket.c:270 回到 `0x08`(commit `d758d5c`);真正起作用的是 Trap 02 的缓冲分配 | 否 | 驱动侧残留:语义宏未被使用(F-DRV-6) | +| 19 | — | — | **不存在**(悬空引用) | — | — | +| 20 | 单次写 >1KB 写穿发送 FIFO → 数据错乱/卡死 | (c) | ch395f.h:138-141 + net_socket.c:1745/1788/2717(`NET_SEND_CHUNK_MAX`) | 否 | 根因已修;驱动只告警不拦截(F-DRV-7) | +| 21 | 接收缓冲"恰好填满+排空"窗口冻结 | (f) 非缺陷 | 台架自吞数据 + 判读工具误判;保留金丝雀 ch395f.c:1176-1189 与布局解耦 ch395f.h:130-153 | 否 | — | +| 22 | 快速复连丢 CONNECT/DISCONNECT → 监听静默死亡 | (a)(B3 曾自造同类症状,commit `4f2cc8b` 修) | net_reconcile_tick net_socket.c:548-641 | **是**(对账层) | **是**(Trap 25 裸机台架独立实证芯片丢中断);但命中数应重测 | +| 23 | 上传中止后首笔复连被静默截断(≤2KB) | (a)+(e) | net_listener.c:102-121(排空)+ :149-204(隔离段);登记点 net_socket.c:3430/:3523 | **是**(隔离排渣层) | 是 | +| 24 | MAC 写入顺序违反手册(SET_MAC 须在 INIT 前) | (d) | net_socket.c:292-299(commit `de1b50e`) | 否 | 已根治 | +| 25 | FTP 数据连接三连:丢 CONNECT / accept 竞态自毁缓冲 / 背靠背小段毒化 TX | ①(a) ②(e) ③(a) | ①探针 net_lost_connect_probe net_socket.c:1228-1267 + 幽灵甄别 `last_peer_port`;②closed-with-data accept net_socket.c:1306-1334(**我方 bug,已根治**);③批量发送 lftpd.c:107-155 | **是** | ①③是;②属契约实现,保留 | + +**归类小结**(按定案后的主因计,跨类条目只计一次):(a) 芯片行为 12 条、(c) 引用/数值错误 4 条、 +(f) 判读假象 3 条、(d) 时序误用 2 条、(e) 设计缺口 2 条;另 Trap 12 非 CH395F、Trap 19 不存在。 + +**催生五层自愈补丁的 6 条(09/14/17/22/23/25)中,只有 Trap 09 的 +"启动形态"和 Trap 25 的"②accept 竞态"被根因修复消掉;其余全部落在芯片行为上,不可从根上消除。** + +## 3 五层自愈 × 驱动层契约 + +| 自愈层 | 参数与位置 | 驱动层是否已有对应能力 | 结论 | +|---|---|---|---| +| `auto_relisten`(监听常驻) | net_types.h:172;置位 net_socket.c:1119 | 无——芯片永不自动回 LISTEN(单连接复用模型),必须软件承诺 | **保留**(模型基石) | +| D10 非阻塞重监听重试 | net_listener.c:77-79(30ms/2s)、:394-432 | 驱动只有**阻塞**的 `poll_cmd_status`(ch395f.c:654)→ 非阻塞重试是在 net 层绕开驱动阻塞原语 | **保留**,但真正的解法是驱动提供非阻塞命令完成查询(F-DRV-8) | +| Trap 22 周期对账 | net_socket.c:544-546(200ms/去抖 2/冷却 1s)、:548-641 | 驱动无状态缓存 → 对账只能每周期重新发 SPI | **保留**(根因是芯片丢中断);参数与 pump 重复,应收敛(F-DRV-1) | +| N-32 监听器 pump | net_listener.c:437-441(100ms/去抖 2/冷却 1s)、:465-553 | 同上 | **保留**(不变量 II 的落点,不是补丁) | +| Trap 23 排渣隔离 | net_listener.c:53-56(400/100/800ms)、:102-121、:149-204 | 排空 RX 的**机械**(`get_recv_len`+`read_recv_buf` 循环)住在 net 层;驱动只有裸 `clear_recv_buf`(且产品零调用) | **下沉驱动**(F-DRV-4) | + +## 4 驱动层缺口清单(缺什么原语) + +1. **无 socket 状态缓存 / 无权威回读单一来源**:7 处各自手拼 `GET_SOCKET_STATUS` + (net_socket.c:586/1235/1694/2330/3391/3408、net_listener.c:516),三套去抖/冷却参数并存。 +2. **无类型化状态谓词**:`byte0 门控 byte1` 只写在 ch395f.h:592-601 注释里,仍有 3 处只判 `byte1` + (net_socket.c:592/1237/3410)→ B6 类误读未收口。 +3. **无"是否在听"谓词**:`chip_listening` 判据手拼在 net_listener.c:517-518。 +4. **无复合复位原语**(排空 RX → close → 排空事件 → 有界等 CLOSED):net 层两套同构变体。 +5. **无 RX 排空原语**:`net_drain_chip_rx`(net_listener.c:102-121)是纯芯片机械。 +6. **读即清原语无护栏**:驱动裸暴露"整体读即清"的全局寄存器,B3 事故即此 API 形状的产物。 +7. **无非阻塞命令完成查询**:`poll_cmd_status` 用 `HAL_Delay(20)` 忙等,最长 4s。 +8. **无链路态缓存与边沿事件**:PHY_CHANGE/DHCP/UNREACH/IP_CONFLI 四个全局位读清后无人消费(B4)。 +9. **无版本门控**:`get_version` 只打印不判(net_socket.c:259);bit3 需 ≥0x46、bit1 需 ≥0x44(B9)。 +10. **无发送缓冲空闲查询**:SENBUF_FREE 只存在于读即清的 SINT 里 → `send_ready` 镜像无处下沉。 +11. **无 socket 索引边界校验**:51 个函数中 `sock` 一律不校验 0~7。 +12. **无 SPI 总线互斥**:串行化仅靠"只有 netTask 碰驱动"的约定(AGENTS 模块约定)。 +13. **IP 字节序契约未封装**:4 处调用点手拼大端数组,驱动注释还与实测相反。 +14. **无事件分发中枢**:`net_poll` 里的 glob→SINT→handler 分发是 net 层手拼,驱动无 dispatch 表。 + +## 5 编号发现 + +**F-DRV-1 · 同一芯片事实被 7 处独立回读、三套去抖参数并存——"单一事实源"缺失是五层膨胀的地基** +证据:`ch395f_get_socket_status` 调用点 net_socket.c:586(对账 200ms/去抖 2/冷却 1s,:544-546)、 +:1235(探针 100ms/去抖 2,`NET_LOST_CONN_STABLE_N`)、:2330(close 等待 500ms)、:3391/:3408(事件留痕)、 +net_listener.c:516(pump 100ms/去抖 2/冷却 1s,:437-441)。驱动 static 变量只有 4 个(ch395f.c:50-56), +无任何缓存。 +影响:同一 socket 的状态在 100ms 窗口内可被读 3~4 次,判读口径与防抖参数各不相同;任何一处口径漂移 +就产生新的"镜像说 A、芯片是 B"窗口(与 T3 的 F-MS-8 同判)。 +裁决:**下沉驱动**——驱动持每 socket 的最近一次回读 + 一个权威刷新入口(每 net_poll 周期一次), +对账/pump/探针/close 共用同一份事实与同一套去抖。 + +**F-DRV-2 · 读即清原语无护栏:B3 事故是驱动 API 形状的直接产物,修复落在 net 层,API 形状未变** +证据:ch395f.c:1305(全局 16 位整体读即清)与 :1324(单 Socket 读即清)裸暴露;B3 修复只在 +net_socket.c:1122-1137 用注释收口("原为遍历 8 个 Socket 全清 + 再读清全局"→ 仅清本 Socket), +git 考古链 `e862009bc`→`319d00e3`→`db64e6acf`(commit `4f2cc8b` 修)。 +影响:同类事故(吞掉其它在用 Socket 的 CONNECT/DISCONNECT 边沿事件)可在新调用点复发; +这正是"看起来像 Trap 22 的自家 bug"的来源。 +裁决:**下沉驱动**——提供 `ch395f_sock_events(sn)`(只读该 Socket,永不碰全局), +并把"全局寄存器唯一读者 = net_poll"写成驱动侧断言/文档契约。 + +**F-DRV-3 · 无类型化状态回读:`byte0 门控 byte1` 只活在注释里,3 处只判 byte1** +证据:ch395f.h:592-601 注释规定 `[1]` 仅在 `[0]`=OPEN 时有意义;违反口径的判读点 +net_socket.c:592(`st[1]==ESTABLISHED` 未先看 `st[0]`)、:1237(探针同型)、:3410(陈旧 DISCONNECT 甄别同型)。 +Trap 07 定案更正第 3 条正是把这类读数读成"卡在某个 TCP 态"。 +影响:幽灵态/孤儿态判定的正确性依赖每个调用点都记得这条规则——已三次失守。 +裁决:**下沉驱动**——返回 `{open, tcp_state}` 结构 + `ch395f_sock_is_listening()/is_established()` 谓词, +`open=0` 时 TCP 态返回"无意义"哨兵值。 + +**F-DRV-4 · "close + 排空 RX + 排空事件 + 等 CLOSED"在 net 层有两套变体,是下沉驱动的最佳落点** +证据:net_socket.c:2326-2345(close→循环读状态 + 读 SINT + 读全局→等 CLOSED,上限 500ms)与 +net_listener.c:149-204(排空 RX→close→读 SINT→读全局→读状态→等 `CLOSED/TCP_CLOSED` + 最短等待 +400/100ms,上限 800ms)。语义同构(bit3=1 排空契约),参数漂移,**且只有后者排空 RX**。 +影响:同一芯片契约两处实现;close 路径不排空 RX,依赖"上层先排"的隐含前提。 +裁决:**下沉驱动**——`ch395f_sock_reset(sn, mode, wait_ms)`(mode=异常/正常), +net 层只选 mode。与 T1 的 F-NET-9 同判,签名归 T4 定。 + +**F-DRV-5 · IP 字节序契约未封装,且驱动注释与实测相反(Trap 16 的复发面仍在)** +证据:Trap 16 根因是照抄手册"所有 IP 命令低字节在前"的总括,实测 SET_IP/SET_DES_IP 收**点分序**; +修复落在 net 层(net_socket.c:1636-1641、:2691-2701,commit `db64e6ac`)。但驱动侧: +`ch395f_set_des_ip` 头注释完全不写字节序(ch395f.h:479-484),`set_ip_addr/set_gwip/set_mask` +仍写"低字节在前"(ch395f.h:380/:387/:394),而 `get_remot_ipp` 是**小端**(ch395f.h:612)。 +影响:同一驱动内两种字节序、注释还指错方向;4 个调用点各自手拼数组,新调用点极易再犯。 +裁决:**下沉驱动**——驱动内做字节序转换(对外收 `uint32_t` 网络序或明确点分序), +并改正 ch395f.h 三处注释;这是"让 BSD API 更主流"里成本最低、收益最直接的一步。 + +**F-DRV-6 · FUN_PARA 语义宏是死代码,net_init 传裸 `0x08`** +证据:ch395f.h:204-206 定义 `CH395F_FUN_PARA_TCP_SERVER/SOCKET_CLOSE/DISABLE_SEND_OK`, +全仓零引用(grep 仅命中定义处);net_socket.c:270 写 `ch395f_set_fun_para(0x08)`。 +影响:Trap 18 的"bit0=1 多 Socket 模式"伪根因能存活一年多,正是因为语义没有类型化到宏/枚举层, +只能靠注释与手册对质。 +裁决:**简化**——net_init 改用宏组合(`CH395F_FUN_PARA_SOCKET_CLOSE`),删掉未使用的 +`CH395F_GINT_STAT_SOCK0..3`(net_poll 用 `1U<<(i+4)`,net_socket.c:464)。 + +**F-DRV-7 · 驱动对超限写只告警不拦截——现状正确,但契约必须显式化** +证据:ch395f.c:1183-1189 明确"超出不会被命令拒绝,而是写穿发送缓冲(Trap 20)→ 只告警不拦截, +分块责任在调用方";分块上限 `NET_SEND_CHUNK_MAX` 在 net 层(net_socket.c:1745,应用于 :1788/:2717)。 +影响:若驱动内自动分块,需在分块间等 SENBUF_FREE,而该事件由 net_poll 集中读清 → 驱动内等待会自锁。 +裁决:**保留**(现状合理);把"单次写 ≤ `CH395F_SEND_BUF_SIZE_BYTES`"从注释升格为 +驱动 README 的显式契约,并把 OVERSIZE 金丝雀计数暴露给上层(当前只打日志)。 + +**F-DRV-8 · 驱动的命令完成等待是阻塞原语:每条 socket 命令至少 20ms、最坏 4s 的 netTask 忙等** +证据:ch395f.c:654-670 `poll_cmd_status` 用 `HAL_Delay(20U)` 忙等(不让出 CPU); +`open_socket`/`close_socket`/`tcp_listen`/`tcp_connect`/`tcp_disconnect` 全部以 4000ms 上限调用它 +(:1091/:1106/:1121/:1136/:1151);DMA 路径另有 1s 忙等(:138-160)。 +影响:D10 的"非阻塞"只覆盖 net 层策略层,驱动层仍是阻塞原语——netTask 停摆指纹 +(`req=0x77`/`mask=0x41`)与 P2b 暂缓的"忙等"问题,真正的根在驱动层。 +裁决:**下沉驱动**——新增非阻塞 `ch395f_cmd_done()`(单次查询,返回 BUSY/终态), +把 open/listen/connect 拆成"发命令"+"查完成"两步;net 层的 D10 重试即可直接复用。 + +**F-DRV-9 · 阻塞重试三处残留,其中两处属应用层——与 D10 非阻塞设计矛盾,构成第 6/7 层自愈** +证据:net_socket.c:1092-1104(`net_listen_locked` 3×`osDelay(500)`)、:1647-1659 +(`net_connect_locked` 同型);应用层 lftpd_inet.c:39-70(5 轮,`vTaskDelay(2000)`)、 +lftpd.c:473-477(3 轮 ×200ms)。 +影响:Trap 09 的"启动形态"已被就绪屏障 + 启动期等 PHY link 消除(net_task.c:118-130/:232), +应用层重试的原始理由已不存在;保留它只会把恢复时间拉长并制造额外的重建所有者。 +裁决:**简化**——net 层两处并入 D10(P2b);应用层两处**可删**(保留 lftpd 的重试前先确认 +`net_relisten` 语义已覆盖,见 T1 的裁决)。 + +**F-DRV-10 · 无版本门控:`get_version` 只打印不判** +证据:net_socket.c:259 `DBG_INFO("ch395f ver=0x%02X fun_para=0x08", ch395f_get_version())`; +驱动无版本判断(ch395f.c:310)。FUN_PARA bit3 需芯片 ≥0x46、bit1 需 ≥0x44(B9)。 +影响:换料/降版本时 `0x08` 静默失效,而整套排空契约(bit3=1)正是建立在 bit3 生效之上。 +裁决:**简化**——`net_init` 内门控(版本不足则拒绝置 bit3 并留自检记录),一行判定消掉整类静默。 + +**F-DRV-11 · Trap 13 的"必须一次读空整个接收缓冲"约束已事实作废,但注释仍在四处约束后人** +证据:ch395f.h:146("接收方向需一次性读空整个 Socket 接收缓冲…避免 Trap 13 部分读导致剩余数据不可见")、 +net_socket.c:1908/:1969/:3466/:3470。而 `net_recv_locked` 常态按 `min(len, recv_len)` 部分读 +(net_socket.c:1882-1952),Trap 21 结案已撤回"部分排空致命",Trap 23 回归 10/10+5/5 全在部分读下通过。 +影响:注释与代码事实相反;`CH395F_SPI_DMA_BUF_SIZE = 2048+4` 的 sizing 理由(ch395f.h:143-153) +仍挂在一条未证实的机理上。 +裁决:**可删**(注释级)——把 Trap 13 降级为"未证实的历史观察",补一条"部分读→再查 recv_len" +定向 TC 结案;DMA 缓冲 sizing 改以"接收缓冲 2KB"为唯一理由。 + +**F-DRV-12 · 死声明与零调用 API 清单(驱动 API 面虚胖)** +证据:`CH395F_CMD_GET_GLOB_INT_STATUS` 0x29(ch395f.h:36,只有 2 字节版 0x19 有实现)、 +`CH395F_SPI_BEGIN_DELAY_MS`(ch395f.c:42,从未使用)、EEPROM/GPIO/SLEEP 命令 +(ch395f.h:163-167 等)、`CH395F_GINT_STAT_SOCK0..3`(:244-247)、 +`ch395f_tcp_disconnect`(ch395f.c:1145,产品零调用,仅 TC-NET-204 用例 ch395f_test_task.c:783)、 +`ch395f_get_unreach_info`(:1402,全仓零调用)、`ch395f_clear_recv_buf`(:468,仅未注册的 +ch395f_reuse_bench.c:169)。 +影响:API 面越大,"哪个是产品路径"越难判;Trap 07 的"disconnect→close"错误序列正是从 +一个零调用的 API 出发的。 +裁决:**可删/登记**——删死宏与未实现命令声明;`tcp_disconnect` 保留但注释标注"产品路径禁用"; +`clear_recv_buf` 保留(Trap 25 已证"缓冲重分配无效",排渣用读空而非 CLEAR_RECV_BUF)。 + +**F-DRV-13 · 链路事件静默:四个全局中断位读清后无人消费,驱动无链路缓存** +证据:net_socket.c:441 读全局后只按 `bit4..bit11` 分派 socket 事件(:464), +bit0~bit3(UNREACH/IP_CONFLI/PHY_CHANGE/DHCP)被丢弃;`ch395f_get_unreach_info` 零调用; +链路只在启动期等一次(net_task.c:118-130)。 +影响:运行期拔网线/链路抖动/IP 冲突/ARP 不可达完全静默,只能靠芯片 TIMEOUT(≈1.6s)间接暴露。 +裁决:**下沉驱动**——驱动持链路态缓存 + 变化计数 + `ch395f_link_up()` 谓词,net 层订阅; +这是 T3 的"PHY/链路变化收在驱动层"结论的落点。 + +**F-DRV-14 · 中断路径实证:纯轮询无 ISR,且不应改为 EXTI** +证据:`Src/gpio.c:76-80` PE5 = `GPIO_MODE_INPUT`/`GPIO_NOPULL`;`EXTI9_5_IRQn` 归 TP_BUSY(PC7, +gpio.c:170-171、stm32f4xx_it.c:391);消费节奏 = net_task.c:241-279 主循环。 +影响:与 AGENTS 模块约定一致;改 EXTI 会让 ISR 与 netTask 争抢"整体读即清"的全局寄存器(F-DRV-2 形状)。 +裁决:**保留**——但把"为什么不用 EXTI"从口头约定升格为驱动 README 的显式理由(当前只有一句约定)。 + +**F-DRV-15 · HAL 全局回调跨模块耦合:CH395F 驱动代管 SPI1 完成标志** +证据:ch395f.c:180-199 的 `HAL_SPI_TxRxCpltCallback`/`HAL_SPI_ErrorCallback` 同时处理 SPI2 与 SPI1, +并 `extern volatile uint8_t g_spi1_dma_done`(:178,定义在 GD5F 驱动里)。 +影响:HAL 全局回调只能有一份,谁定义谁就代管别人;改 CH395F 驱动可能静默破坏 NAND 的 DMA 完成路径。 +裁决:**简化**——收进一个 SPI 适配层(或按实例注册),至少把该耦合写进两个模块的 README。 + +**F-DRV-16 · 单连接复用不是偏离主流;差距在驱动不持事实、不推事件、无复合事务原语** +证据:T3 的 F-MS-2/F-MS-5(ioLibrary 无 accept、CH395 每 Socket 一连接、FreeRTOS+TCP +`REUSE_LISTEN_SOCKET` 同形);本仓驱动 static 变量仅 4 个(ch395f.c:50-56)、无事件分发、 +无复合原语(§4 缺口 1/6/7/14)。 +影响:把"要不要改成 per-socket 独立状态机"当成问题是问错了方向——芯片侧本来就是 +每 Socket 一连接、监听与数据通道复用;缺的是驱动把芯片事实**收口**成一份可查询的真相。 +裁决:**保留模型**;按 F-DRV-1/2/3/4/8/13 补驱动原语后,五层里的对账/pump/探针失去存在理由, +隔离排渣下沉为驱动原语(与 T3 的 4.3/4.4 一致)。 + +## 6 内聚评估:驱动持缓存 + 权威回读后,reconcile/pump/drain 能否下沉或删除 + +| 机制 | 能否因"驱动持事实"而删 | 理由 | +|---|---|---| +| Trap 22 对账①② | **不能删,但可大幅收窄** | 根因是芯片丢中断(Trap 25 裸机台架独立实证)。驱动持缓存后,对账的"回读"部分变成读缓存;"发现失配"仍需存在,只是从"周期 SPI 轮询"降为"每 poll 周期一次刷新 + 比对" | +| N-32 pump | **不能删** | pump 是"单一重建所有者"(不变量 II),是架构解不是补丁;驱动持事实后它的判据读缓存,动作仍在 net 层(策略) | +| D10 重试 | **可降级** | 若驱动提供非阻塞 `cmd_done()` + `sock_reset()` 内联有界等待(F-DRV-4/8),独立 30ms/2s 定时器可退化为事务内重试(T3 同判) | +| Trap 23 排渣隔离 | **可下沉** | 排空/等待是纯芯片机械;net 层只保留"异常/正常"两档 mode | +| 探针 `net_lost_connect_probe` | **不能删,可去抖简化** | 丢 CONNECT 是芯片行为;驱动持事实后去抖可去掉(读缓存无瞬态噪声),保留"状态已 ESTABLISHED 而本层 LISTENING → 补 accept" | +| `auto_relisten` | **不能删** | 芯片永不自动回 LISTEN,这是模型承诺 | + +**一句话结论**:五层里有四层(对账、pump、探针、resync)存在的直接理由是"驱动不持事实、net 层只能自建镜像"; +把它们删掉的前提不是"根因已修",而是"驱动成为唯一事实源"。Trap 23 的排渣隔离则相反—— +它的根因(在途数据落 RX)不可修,但它的**实现位置**应当从 net 层下沉到驱动。 + +## 7 对 T4 汇总的输入 + +1. 驱动层最该补的三个原语(按收益/成本排序): + ① 类型化状态回读 + 每 socket 缓存(F-DRV-1/3);② `ch395f_sock_reset()` 复合复位(F-DRV-4); + ③ 非阻塞 `ch395f_cmd_done()`(F-DRV-8)。三者都不改芯片行为,只把事实与机械收进驱动。 +2. 立即可做、零风险的清理:F-DRV-6(用宏)、F-DRV-10(版本门控)、F-DRV-11(撤 Trap 13 注释约束)、 + F-DRV-12(删死声明)、F-DRV-5 的注释更正。 +3. 需要重测的量化项:B3 修复(`4f2cc8b`)前后各跑 `:21 100ms×50`,统计 `reconcile:` 命中数, + 量化"自家制造的丢中断"占比(与 T1 的 F-NET-8 同一实验,勿重复做)。 +4. 与 T3 的分歧点:T3 的 4.2 第 5 条称"FUN_PARA_TCP_SERVER(多连接模式)已在 net_init 启用"—— + **与代码不符**。net_socket.c:261-270 明确 `bit1=0`(多连接模式不用),实传 `0x08`; + 设计文档的 A′ 出局论证(不支持多监听)成立。T4 汇总时须以代码为准。 diff --git a/docs/网络/N34-自愈审计/README.md b/docs/网络/N34-自愈审计/README.md new file mode 100644 index 00000000..0c591611 --- /dev/null +++ b/docs/网络/N34-自愈审计/README.md @@ -0,0 +1,112 @@ +# N34 自愈审计 · 汇总报告(T4) + +> 审计对象:BSD Socket API 的「芯片/本层状态对账与重监听自愈」五层机制(auto_relisten / D10 重试 / +> Trap 22 对账 / N-32 pump / Trap 23 排渣隔离)及其显式 API(`net_relisten` / `net_resync_listen`)。 +> 方法:三路独立审计汇总——net 层盘点(`docs/网络/N34-自愈审计/net层自愈机制盘点.md`,F-NET-1..14)、 +> CH395F 驱动层审计(`docs/网络/N34-自愈审计/CH395F驱动层审计.md`,F-DRV-1..16)、 +> 主流方案对照(`docs/网络/N34-自愈审计/主流方案对照.md`,F-MS-1..9,含 T4 更正两处:多连接模式未启用、 +> "静默回收"非 Server 模式条款)。全部证据带 文件:行号,HEAD `09d6195` 实测。 +> 性质:纯审计,未改任何代码。本报告是裁决与路线,不是实施记录。 + +## 0. 结论先行(直接回答本次审计的问题) + +**问:「芯片/本层状态对账与重监听自愈」可以简化或删除吗?可以内聚到 ch395f 驱动层吗?** + +1. **不能整体删除。** 催生五层自愈的 6 条 Trap(09/14/17/22/23/25)中,只有 2 个被根因修复消掉 + (Trap 09 的启动形态→就绪屏障;Trap 25 的②accept 竞态→closed-with-data)。其余全部落在**芯片行为**上: + Trap 25 的裸机芯片台架(剥离 OS/net/FTP)独立复现"握手完成但 CONNECT 中断被丢"——这是 + "对账不可因固件修 bug 而删"的决定性证据(F-NET-8)。 +2. **用户的假设部分成立,但打不中五层。** 24 条 Trap 根因归类:芯片行为 12、引用/数值错误 4、 + 判读假象 3、时序误用 2、设计缺口 2(F-DRV 总表)。寄存器数值/引用错误确实存在过 + (Trap 16 字节序、Trap 02 缓冲重叠、Trap 20 FIFO 上限、B3 全量读清),但它们**不是**五层自愈的成因—— + B3(全 Socket SINT 读清)只是"自家制造的丢中断"放大器,修复后对账仍必要,只是命中数应下降(可台架量化)。 +3. **但"五层补丁"这个形态不是终态,可以大幅简化并部分内聚。** 结构性根源不是单连接复用模型 + (ioLibrary 无 accept、CH395 每 Socket 一连接、FreeRTOS+TCP reuse 模式全部同形,F-MS-2/F-DRV-16), + 而是三件事的组合:**① 驱动完全不持事实**(ch395f.c 仅 4 个 static,纯命令转发层); + **② net 层维护有损 8 态镜像并作为决策依据**(芯片 11 态→8 态有损投影,F-MS-8); + **③ 同一芯片事实被 7 处独立回读、三套去抖/冷却参数并存**(F-DRV-1)。 + 镜像滞后 → 需要周期对账;事件丢失 → 需要探针与门控补偿;重建脱钩 → 需要 pump/重试/沉降三件套。 +4. **"内聚到 ch395f"对一半。** 该下沉驱动的是**事实与机械**:状态回读+缓存、事件读即清护栏、 + 排渣/沉降/等 CLOSED 复合原语、非阻塞命令完成查询、链路态缓存。 + 该留在 net 层的是**策略与所有权**:监听重建的单一决策者(pump 是 N-32 的架构解,不是补丁, + 且消费者驱动即时重建有 P2a 台架实测支撑,删了会 `task stall`)。 +5. **立即可删的是 API 面,不是机制**:`net_relisten`(footgun,生产零调用)、 + `net_resync_listen`(语义并入对账后删)、`net_can_accept`(iec104 迁移后退役)—— + 这三个"上层手动自愈"入口正是"补丁式代码难维护"的最直观来源。 + +## 1. 三方裁决的调和 + +T1(net 层视角)判"机制主体保留、API 可删";T3(主流对照视角)判"对账/pump/探针/resync 整体删除"。 +两者不矛盾,差在参照系:**T1 评的是现架构下的必要性,T3 评的是目标架构下的存在理由**。 +统一裁决(T2 的调和,F-DRV 总表): + +> **删除这些机制的前提不是"根因已修",而是"驱动成为唯一事实源"。** +> 需求(丢中断兜底、监听常驻承诺、残渣清理、沉降窗口)是永恒的;形态(五层常驻定时器 + 镜像对账) +> 是可替换的——替换为"驱动持事实 + 决策点读事实 + 重建内联进事务 + 单一 reconciler"。 + +## 2. 逐机制汇总裁决 + +| 机制 | 位置(证据见三份发现文件) | 汇总裁决 | 依据 | +|---|---|---|---| +| `auto_relisten` | net_types.h:172 | **保留**(模型基石) | 芯片永不自动回 LISTEN,监听常驻是软件承诺(P4) | +| D10 `net_listener_retry_tick` | net_listener.c:394-432 | **短期保留+小修;长期降级** | 小修=补 pump 同型状态门(F-NET-10)+ give-up 文案(F-NET-14);长期=驱动 `cmd_done()` 后内联进事务、删独立定时器(F-DRV-8) | +| Trap 22 `net_reconcile_tick` | net_socket.c:548-641 | **功能保留,形态重构** | 与 pump 判据合并为**单一 reconciler**、读驱动缓存、一套去抖(F-DRV-1);①扩展覆盖"本层 ESTABLISHED+芯片已回 LISTEN"缺口(F-NET-2,当前无自动机制认领) | +| N-32 `net_listener_pump` | net_listener.c:465-553 | **保留**(内聚的落点,不是补丁) | 唯一周期重建决策者=不变量 II;T3"改内联"与 P2a 台架实测冲突(删消费者驱动→`[WD] task stall`) | +| Trap 23 排渣隔离 | net_listener.c:102-121/:149-204 | **下沉驱动** | 排空/等 CLOSED 是纯芯片机械;与 net_close_locked:2326-2345 是同一契约两套变体(F-NET-9/F-DRV-4)→ 收口 `ch395f_sock_reset(sn, mode, wait_ms)`,net 层只选 mode | +| `net_relisten` | net_socket.c:3080 | **删除** | 生产零调用;对健康监听器无条件 open→USE→close,自毁 ~1s(footgun) | +| `net_resync_listen` | net_socket.c:3026 | **删除**(先并入对账①) | 语义正确但载体错位:应是自动对账,不是手动 API(F-NET-2/F-MS-4) | +| `net_can_accept` | net_socket.c:1427 | **删除**(先迁 iec104 到 `net_listener_armed`) | 过期门控致 :2404 丢 CONNECT 探针从不运行(F-NET-4,与已修的 :7003 探针失效同型) | +| `net_lost_connect_probe` | net_socket.c:1228-1267 | **保留**;驱动持事实后去可去抖 | 丢 CONNECT 是芯片行为(裸机实证);去抖/冷却是对账世界的自偿成本(F-MS-9) | +| L3 pending recv + 看门狗 | net_socket.c:2024-2190 | **保留** | 阻塞模型主体,与主流(FreeRTOS 事件组阻塞)同构 | +| `session_dead` / `released_abnormal` / 幽灵甄别 / closed-with-data | net_types.h | **保留** | 芯片行为的必要建模,P1 不变量 I | +| `net_listen_locked` 3×osDelay(500) / `net_close_locked` 500ms 忙等 | net_socket.c:1092-1104/:2326-2345 | **简化**(P2b) | 根在驱动阻塞原语(F-DRV-8);先补 `cmd_done()` 再统一走 D10 | +| 上层每 32 轮盲收(U15) | ui_net_sink.c:552-558、iec104_serv.c:246-253 | **中期下沉** | "RX 非空但 readable=0"并入 reconciler 后上层退役(F-NET-12) | +| time_sync 每会话 close 监听重建 | time_sync.c:182 | **简化(上层)** | 违反自家"EOF 不得 close 监听 fd"铁律,主动制造 Trap 09 沉降窗(F-NET-13);改 lftpd 模式同 fd 回 accept | +| lftpd 应用层阻塞重试 | lftpd_inet.c:39-70、lftpd.c:473-477 | **删除/简化** | Trap 09 启动形态已被就绪屏障消除,重试理由不存在(F-DRV-9) | +| 三处全局读清残留 | net_socket.c:1668/:2332、net_listener.c:176 | **收窄**(与驱动护栏合并做) | B3 已证"只读本 Socket SINT"可行;这三处仍在吞别的 Socket 边沿事件(F-NET-7) | + +## 3. 目标架构(高内聚低耦合的落点) + +| 层 | 应持有 | 现状偏差 | +|---|---|---| +| `ch395f` 驱动 | **唯一事实源**:每 socket 状态缓存 + 类型化回读(`{open, tcp_state}` + `is_listening()/is_established()` 谓词,byte0 门控 byte1 收口);事件护栏 `sock_events(sn)`(永不碰全局寄存器);复合原语 `sock_reset(sn, mode, wait_ms)`(排空 RX→close→排空事件→有界等 CLOSED,收编 Trap 09/23 参数);非阻塞 `cmd_done()`;链路态缓存+边沿事件;IP 字节序封装;版本门控 | 驱动零状态(4 个 static);读即清裸暴露;命令原语全阻塞(`HAL_Delay(20)` 忙等、4s 上限);字节序注释与实测相反 | +| net 层(netTask 单所有者) | fd↔socket 映射、消息队列串行化、L3 pending、事件分发;**状态镜像降级为展示投影**;**单一 reconciler**(对账+pump 判据合并、读驱动缓存、一套去抖);重建=`sock_reset`+LISTEN(pump 周期 + 消费者即时,两处入口语义统一) | 8 态镜像是决策依据;7 处手拼回读三套参数;两套排空变体;`net_can_*` 镜像门控 | +| 应用/服务层 | 只管 fd 与数据;监听常驻由 net 层承诺 | 32 轮盲收、time_sync close、lftpd 重试——三层兜底散在应用 | + +一句话:**驱动持有芯片事实并内联处理怪癖,net 层单所有者只做映射/串行/分发且决策读事实, +监听重建收敛为"reset 原语 + LISTEN"由 pump 与事务共同触发,应用层零自愈责任。** + +## 4. 分阶段路线(风险从低到高;每阶段独立可回退) + +- **阶段 0 · 零行为风险清理**(纯注释/宏/一行判定,可先行提交): + F-DRV-5 字节序注释更正、F-DRV-6 FUN_PARA 用宏、F-DRV-11 撤 Trap 13 注释约束、F-DRV-12 删死声明、 + F-NET-11 注释漂移(50ms→100ms)、F-NET-14 give-up 文案、F-DRV-10 版本门控(一行)、 + F-NET-6 U12 LISTEN 失败补 close(一行)、F-NET-10 retry_tick 状态门(一行)。 +- **阶段 1 · API 面收口**(小 diff,行为可测):删 `net_relisten`;iec104 门控迁 `net_listener_armed` + → 退役 `net_can_accept`;对账①扩展覆盖静默回收形态 → 删 `net_resync_listen`; + time_sync 会话收尾不 close;删 lftpd 应用层重试。 +- **阶段 2 · 驱动原语**(新增不删旧,行为不变):类型化回读+每 socket 缓存、`sock_events` 护栏、 + `sock_reset` 复合原语(现补丁参数原样收编)、`cmd_done()` 非阻塞、链路缓存。 +- **阶段 3 · net 层收敛**(核心重构,需完整台架回归):对账+pump 合并单一 reconciler(读缓存、 + 一套去抖)、探针去抖删除、两处忙等改走 `cmd_done`、U15 数据面对账内化 → 上层 32 轮盲收退役。 +- **贯穿验证腿**:B3 修复前后各跑 `:21 100ms×50` 统计 `reconcile:` 命中数,量化"自家制造的丢中断" + 占比(F-NET-8,T1/T2 同一实验勿重复做);阶段 3 回归判据沿用生产验证数据 + (快速复连 :7003 10/10、:21 100ms/0ms/600ms 各 50/50、中止 RST 10/10 + FIN 5/5、SHA-256 全匹配)。 + +## 5. 红线(本次审计明确"不做"的事) + +1. **不改 EXTI/ISR**:全局事件寄存器整体读即清,ISR 读寄存器会把事件从 netTask 抢走——正是 B3 事故形状(F-DRV-14)。 +2. **不启用多连接模式(FUN_PARA bit1)**:不支持多监听,A′ 出局论证成立;T3 原文误称"已启用"已更正。 +3. **不删消费者驱动即时重建**(accept/recv/pending 三处):P2a 台架实测删除→CLOSED 窗口拉长→`[WD] task stall`。 +4. **不改单连接复用模型**:这是硬 TCP 栈原生主流形态,不是对 BSD 的偏离。 +5. **SPI 分频 8、中断经 `net_poll` 集中读清**:PCB 约束与既有约定不变(护栏是收窄读法,不是改路径)。 +6. 驱动缓存成本每 socket 数字节可忽略,但按平台约束前后跑 `scripts/ram_report.py` / `stack_report.py`。 + +## 6. 发现索引与后续登记建议 + +- 三份发现文件共 **39 条编号发现**:F-NET-1..14(net 层)、F-DRV-1..16(驱动层)、F-MS-1..9(主流对照)。 + 每条带 文件:行号 证据与裁决,后续排期直接按编号引用。 +- 建议登记进 `docs/任务规划/README.md`(**按铁律在 done 时统一处理,任务期不改登记簿**): + 阶段 0 合并为一条 N-x(零风险清理簇);阶段 1 一条(API 收口簇);阶段 2/3 各一条(驱动原语 / net 层收敛, + 后者需完整台架回归);F-NET-2 覆盖缺口建议**单独登记并优先**——它是当前唯一"无自动机制认领"的失同步形态。 +- 本目录三份发现文件即审计留痕,销项时结论沉淀进对应模块文档后本目录可归档。 diff --git a/docs/网络/N34-自愈审计/net层自愈机制盘点.md b/docs/网络/N34-自愈审计/net层自愈机制盘点.md new file mode 100644 index 00000000..712e40ce --- /dev/null +++ b/docs/网络/N34-自愈审计/net层自愈机制盘点.md @@ -0,0 +1,235 @@ +# N34 自愈审计 · net 层自愈机制盘点(T1) + +> 审计对象:`Drivers/BSP/NET/net_socket.c`(3671 行)与 `net_listener.c`(554 行)/ `net_listener.h`。 +> 方法:全文通读 + 调用点 grep + git 考古(`git log -S`)+ 问题记录与设计文档对照。纯只读,未改任何代码。 +> 行号均为本工作树 HEAD(`09d6195`)实测行号。 +> 参照文档:CH395F_Trap_Records.md(Trap 09/22/23/25 与 2026-10-11 寄存器审计章节 B1~B10 条目)、 +> 网络Socket监听生命周期设计说明.md(B10 根治设计,含 P0/P1/P2 落地记录与模块化边界)、 +> BSD_Socket_API_使用指南.md(五层自愈口径)。 + +## 1. 机制 × 属性总表 + +核心七机制(用户指定盘点范围): + +| # | 机制 | 代码位置 | 触发条件 | 改本层哪些状态 | 发哪些芯片命令 | 定时器/去抖/冷却 | 重叠/互为兜底的对象 | 根因分类 | 初步裁决 | +|---|---|---|---|---|---|---|---|---|---| +| 1 | **auto_relisten**(监听常驻标志) | 置位 net_socket.c:1119(`net_listen_locked`);字段 net_types.h:172;消费点 net_socket.c:1316/1924-1938/2136-2162/1483、net_listener.c:501 | `net_listen` 成功即置 1;此后任何"会话已结束"路径(DISCONNECT/TIMEOUT/对账①)把 fd 置 CLOSED 后,由消费点或 pump 依据该标志重建 | 本身不改状态,是**门控标志**;`net_close`(memset net_socket.c:2351)与 `net_init`(:244)清零 | 无(纯标志) | 无 | 是所有重建路径(机制 2/4/6 与 accept/recv/pending 三处即时重建)的共同前提门 | (a) 芯片永不自动回 LISTEN(手册无此条款,设计文档认定;单连接复用模型锁死) | **保留**(模型基石,不可删) | +| 2 | **D10 非阻塞重监听重试** `net_listener_retry_tick` | net_listener.c:394-432;登记 net_listener.c:374-378;常量 :77-79 | 任何 `net_listener_try` 单次重建失败(open 失败 / LISTEN 失败 / 隔离未满)即登记 `s_relisten_wait`,到点重试 | 成功经 `net_relisten_locked` 置 `state=LISTENING`(net_listener.c:233)、清 `released_abnormal`、转登 `last_peer_port`、清 `lost_conn_seen`、清对端簿记(:239-255);**刻意不清 `session_dead`**(:234-238 契约注释) | 隔离段见机制 5;正常段:SET_PROTO_TYPE、SET_SOUR_PORT、OPEN_SOCKET、TCP_LISTEN(:207-230);open 失败补 CLOSE_SOCKET(:218);成功尾读本 Socket SINT 读即清(:262) | 重试间隔 30ms(:77)、放弃时限 2s(:79,自首次尝试);无去抖(单次尝试);**L3 保险**:同 fd 有在途挂起 recv 则跳过(:405-408,经 net_socket.c:2202 `net_pending_active`) | 是机制 4(pump)与三处消费者即时重建的**执行器**(它们调 `net_listener_try`,失败落到这里);"2s 放弃"并非终态——pump 冷却后会再次触发(见 F-NET-4) | (a) 芯片 close 后重开需沉降(Trap 09 PHY 形态 + Trap 25 裸机台架实证拆链窗口 >2s 不定长);旧实现 osDelay(500) 忙等属 (b) 已修(e5733b3) | **保留**;"give up" 语义需简化(F-NET-4) | +| 3 | **Trap 22 周期对账** `net_reconcile_tick` | net_socket.c:548-641;常量 :544-546 | 每 200ms 对**认领范围内**(仅本层 ESTABLISHED / LISTENING,:579-581)的 fd 回读芯片两字节状态:① 本层 ESTABLISHED 而芯片 SOCKET_CLOSED 或 TCP_CLOSED(:588-590);② 本层 LISTENING 而芯片 TCP_ESTABLISHED(:591-592) | ① 复用 `handle_disconnect_event`(:633)→ 置 `session_dead=1`、`last_peer_port`、`state=CLOSED`、可能登记 GRACEFUL 沉降;② 复用 `handle_connect_event`(:638)→ 置 ESTABLISHED、清 `session_dead`、记对端 | GET_SOCKET_STATUS(每认领 fd 一条,:586);② 前 GET_REMOT_IPP 幽灵甄别(:597-602);后续按事件路径的命令 | 周期 200ms(:544)、连续 2 次失配去抖(:545,≈400ms)、动手后 1s 冷却(:546) | ② 与 accept 路径内联探针 `net_lost_connect_probe`(net_socket.c:1228-1267)职责重叠(同一失同步双兜底,Trap 25 定案"探针快路径 + 对账兜底网");① 与 `net_resync_listen` 语义相邻但判据不同(见 F-NET-2);原③④ 已移交 pump(:536-537/606-607) | (a) 芯片丢 CONNECT/DISCONNECT 中断(Trap 22 生产实测 + Trap 25 裸机台架独立实证,**与 B3 固件 bug 无关**);幽灵态 (a) | **保留**;建议扩展①(F-NET-2) | +| 4 | **N-32 监听器 pump** `net_listener_pump` | net_listener.c:465-553;常量 :437-441 | 每 100ms 对**监听型 fd**(in_use + auto_relisten + STREAM + local_port≠0,:501-507)且本层处于 LISTENING/CLOSED(:509-513)者回读芯片:① 本层 LISTENING 而芯片不在 LISTEN → 重建;② 本层 CLOSED 而芯片不在 LISTEN → 重建;③ 本层 CLOSED 而芯片**正在** LISTEN(U11)→ 只重同步 | ③:仅 `state: CLOSED→LISTENING`(:528)+ 转登 `last_peer_port`、清 `lost_conn_seen`(:532-533),**不发任何芯片命令**;①②:经 `net_listener_try` 走机制 2 的状态改动;`session_dead` 不清(:462-463 契约) | 每监听型 fd 一条 GET_SOCKET_STATUS(:516);③ 零命令;①② 同机制 2 | 周期 100ms(:437)、连续 2 次去抖(:440)、1s 冷却(:441) | 本层**唯一周期性重建决策者**(不变量 II,设计文档第 4 节);与三处消费者即时重建(accept net_socket.c:1327 / recv :1938 / pending :2162)构成"周期兜底 + 消费者驱动"双层——该分工是 P2a 台架实测推翻原计划后的定案(删消费者驱动会致 `[WD] task stall`) | (c) 设计缺口(多所有者竞争)的**架构解**;其底层需求(芯片不回 LISTEN + 事件会丢)为 (a) | **保留**(这就是"内聚"的落点,不是补丁) | +| 5 | **Trap 23 排渣隔离** `net_drain_chip_rx` + 隔离段 | 排空 net_listener.c:102-121;隔离段 net_listener.c:149-204;登记 net_listener.c:282-289(`net_listener_settle`),登记点 net_socket.c:3430(GRACEFUL,仅 `recv_bytes≠0`)/:3523(QUARANTINE) | 异常释放(芯片 TIMEOUT,net_socket.c:3512-3524)登记 QUARANTINE;正常释放(芯片 DISCONNECT 且本连接收过数据,:3424-3432)登记 GRACEFUL;下一次 `net_relisten_locked` 进入隔离段 | 隔离期满清 `s_relisten_quarantine/_settle_start/_settle_dead`(:199-203);不改 socket 状态(状态改动仍由机制 2 成功尾做) | 隔离期内每轮:GET_RECV_LEN + READ_RECV_BUF(排空,:172→:107-120)、CLOSE_SOCKET(主动关,:174)、读本 Socket SINT 读即清(:176)、**读全局中断寄存器**(:178,见 F-NET-7)、GET_SOCKET_STATUS(:180) | QUARANTINE 最短 400ms / GRACEFUL 最短 100ms / 上限 800ms 强制放行(net_listener.c:53-55;上限必须 < 2s 重试时限,实测 2000ms 会把监听耗死) | 与 `net_close_locked` 的"close→等 CLOSED→排空中断"循环(net_socket.c:2326-2345)是同一芯片契约的**两个变体实现**(下沉候选,见 F-NET-9);GRACEFUL 的 `recv_bytes≠0` 门是为保护 Trap 22 快速复连时序而设(两机制互相制衡) | (a) FUN_PARA bit3=1(0x08,D4 定案必须保留)下芯片保留现场 + 在途数据在 TIMEOUT 之后才落缓冲 + 残留 DISCONNECT 投递给下一条连接(Trap 23 两阶段台架实证;0ms/30ms 沉降无效、100ms 通过) | **保留**;"close+排空+等 CLOSED"原语可**下沉**驱动层合并(F-NET-9) | +| 6 | **net_resync_listen / net_relisten** 显式自愈 API | API net_socket.c:3026-3066 / :3080-3120;分派 :774-799 / :800-814 | 应用层判定"连接已死/监听器可能失位"后主动调用;**生产代码零调用者**,唯一调用者 test/board/net_latency_test.c:783-784/787/800(Phase 13 台架,产品构建不启用) | RESYNC:仅本层 `state: ESTABLISHED→LISTENING` + 转登 `last_peer_port` + 清 `lost_conn_seen` + 取消挂起 recv(:789-797),**不发芯片命令**;RELISTEN:经 `net_listener_try` 无条件芯片侧重建(机制 2 全套) | RESYNC:无;RELISTEN:机制 2 全套命令(含隔离段) | 无自身定时;RELISTEN 失败落 D10 重试 | RESYNC 与 pump③(U11)语义相邻但覆盖的本地状态不同(ESTABLISHED vs CLOSED);RELISTEN 与 pump①② 完全重叠且**更危险**——对健康监听器无条件 open→USE→close 会造成 ~1s 监听中断(net_socket.c:3078 注释自认) | RESYNC 针对的"芯片静默回收"为 (a)(run8 台架实证,当时 FUN_PARA 已是 0x09);但 API 形态属 (c) 时代遗留(上层镜像芯片状态、手动修);RELISTEN 属 (c) 冗余 | **可删**(生产路径):RESYNC 语义并入对账①扩展后删 API;RELISTEN 直接删(F-NET-2/F-NET-3) | +| 7 | **L3 挂起 recv**(pending)及其交互保险 | 结构 net_socket.c:184-193;登记 :2024-2063;完成 tick :2074-2190;了结 :1985-2007;取消 :2237-2256;close 前了结 :2267-2298;查询 :2202-2208 | 阻塞 recv 无数据即登记;`net_pending_tick` 每轮按 ①数据 ②到点 ③断连(`CLOSED ∥ session_dead`,:2136)顺序了结 | ③ 断连分支:排空后按 `released_abnormal` 回 CONNRESET 或 EOF(0)(:2164-2174);`state==CLOSED` 且 auto_relisten 时**消费者驱动即时重建**(:2159-2162);close 路径先 settle(排空或取消,必通知,:2316-2318);`net_init` 重建前 `cancel_all`(:241) | GET_RECV_LEN + READ_RECV_BUF(①③ 读数据);③ 即时重建走机制 2 命令 | 每 fd 至多一个挂起(:2038-2041 防覆盖挂死);deadline=登记+预算(默认 30s,net_config.h:39);**看门狗**:超 deadline 后 1s 强制了结 + DBG_ERROR(:182/:2181-2188);与机制 2 的交互保险 = retry_tick 的 `net_pending_active` 跳过(net_listener.c:405-408) | 与机制 2 互为保险(挂起在途则 D10 不动该 fd);③ 的即时重建与 recv_locked DONTWAIT 的即时重建同型(:1936-1938);`cancel_all`(net_init)与 tick 的"fd 已关闭兜底"(:2092-2096)双保险 | 阻塞模型演进(E-02 头阻塞修复),非严格自愈;③ 的排空+EOF 语义对应 (a)(Trap 13 FIN 先于数据、bit3=1 保留现场);看门狗属 (c) 完成逻辑缺位的 tripwire | **保留**(含看门狗,留痕成本极低);它是"消费者驱动重建"三处之一,P2a 已定案不可删 | + +配套机制(不在七项内但参与同一失同步网,盘点完整性所需): + +| 机制 | 位置 | 一句话属性 | 根因 | 裁决 | +|---|---|---|---|---| +| `net_lost_connect_probe`(Trap 25 双信源探针) | net_socket.c:1228-1267,调用 :1298-1300 | accept 轮询内联补 accept;去抖 2 轮(:1214)无冷却;幽灵甄别 `last_peer_port` | (a) 拆链窗口丢 CONNECT(裸机台架 15/15) | 保留 | +| closed-with-data accept | net_socket.c:1305-1335 | CLOSED + RX 有数据 → 接受"已关但可读"连接排空(bit3=1 排空契约) | (a)(Trap 25 三次定案:425 真根因是我方 relisten 自毁缓冲) | 保留 | +| `session_dead` 电平(N-32 P1 不变量 I) | net_types.h:192-206;置位 net_socket.c:1723/3439/3535;清零 :3310/3347;不清 net_listener.c:234-238 | "会话已结束"从边沿改粘滞电平,消费者不再被抢先 relisten 饿死 | (c) 设计缺口的**架构解** | 保留(这是根治本身) | +| `released_abnormal` | net_types.h:174-180;置位 net_socket.c:3529/3434 | 异常/正常释放区分 → 挂起 recv 回 CONNRESET 还是 EOF | (a) 协议边界(在途 FIN 与正常完成不可区分,Trap 23 硬化节留痕) | 保留 | +| `net_listen_locked` 3×osDelay(500) 阻塞重试 | net_socket.c:1092-1108 | 首次监听路径仍忙等(P2b 暂缓项),与 D10 非阻塞模式并存 | (b) 时代遗留(D10 已修 relisten 侧,listen 侧未统一) | 简化(统一走 `net_listener_try`) | +| `net_close_locked` 500ms 等 CLOSED 忙等 | net_socket.c:2326-2345 | 关闭热路径同步忙等(P2b 暂缓项) | (c) 同步等待设计债;台架实测危害理论性(设计文档 P2b 决策) | 简化(暂缓有据,留观察) | +| 陈旧 DISCONNECT 甄别(D20) | net_socket.c:3400-3415 | ESTABLISHED 收 DISCONNECT 时回读芯片,TCP 仍 ESTABLISHED 判陈旧丢弃 | (a) Trap 14/17 滞后锁存 | 保留 | +| 代次号 `s_net_generation` | net_socket.c:49/358-374;递增 net_task.c:145 | 网络重建后服务层重建监听的依据(应用层重建所有者通道) | (c) 但边界清晰(芯片复位后 fd 全灭是事实) | 保留 | + +## 2. 根因分类汇总 + +**(a) 芯片真实行为(手册条文或裸机台架实证,无法从根上消除)——对应机制必须保留:** + +| 芯片行为 | 证据 | 支撑的机制 | +|---|---|---| +| 连接结束后芯片永不自动回 LISTEN(单连接复用模型下监听槽位被消耗) | 设计文档第 2.1 节引手册;官方 EXAM10 例程在事件现场当场重建 | auto_relisten、pump、D10、resync 语义 | +| 快速复连/拆链窗口内丢 CONNECT/DISCONNECT 中断 | Trap 22 生产实测;**Trap 25 裸机台架剥离 OS/net 层仍复现**(关键:证明与固件层 bug 无关) | 对账①②、探针、pump | +| 拆链收尾窗口变长且不可观测(>2s 不定长),窗口内假报 ESTABLISHED(幽灵态,对端端口=旧连接) | Trap 25 定案实验 | 幽灵甄别 `last_peer_port`、去抖+冷却纪律 | +| FUN_PARA bit3=1(0x08,D4 定案保留):DISCONNECT/TIMEOUT 后芯片不关 Socket 不清缓冲(停 0x05/0x00),MCU 必须排空后显式 close | 手册 §5.52②/§5.23;Trap 25 三次定案判别探针(recv_len=1027 完整载荷) | closed-with-data accept、Trap 23 排渣、close_locked 排空循环 | +| 中止时段的在途数据在 TIMEOUT **之后**才落进 RX 缓冲(~2KB 残渣被下条连接读走) | Trap 23 一阶段(RST 10/10 复现→修复 10/10) | QUARANTINE 400ms + 持续排空 | +| 上一条连接残留 DISCONNECT 会在复用同 fd 的下一条连接 accept 后 10~20ms 投递 | Trap 23 二阶段(0ms/30ms 沉降无效、100ms 通过) | GRACEFUL 100ms(仅收过数据时登记) | +| close 后重开 Socket 需沉降(PHY 形态 + 拆链形态) | Trap 09;Trap 25 台架(固定沉降 100~3000ms 均证伪"等就好") | D10 到点重试(而非固定延时) | +| 芯片可静默回收连接回监听且不发 DISCONNECT | run8 台架实证(当时 FUN_PARA=0x09,bit3 已=1,git 核实) | 现仅 `net_resync_listen` 覆盖——**自动机制存在覆盖缺口**(F-NET-2) | +| 全局中断寄存器整体读即清、无按位清除;SINT 粘滞到被读 | 手册 §5.15;Trap 14 | net_poll 集中读清纪律;也解释了为何"丢弃式读全局"有害(F-NET-7) | + +**(b) 早期固件 bug(寄存器数值/引用/时序误用)——根因已修,评估补丁是否仍必要:** + +| 早期 bug | 修复 | 曾支撑的补丁 | 根因修复后补丁是否仍必要? | +|---|---|---|---| +| B3:listen/relisten 对全部 8 个 Socket 读 SINT + 丢弃式读清全局(多连接时代调试遗留,git 考古 e862009bc→319d00e3→db64e6acf) | 4f2cc8b(2026-10-12)收窄为只清本 Socket | Trap 22 对账在生产中的部分命中 | **仍必要**:Trap 25 裸机台架(无 net 层代码)独立实证芯片自身丢中断;但 B3 修复后对账在生产日志的命中数应显著下降——可台架统计验证(F-NET-8) | +| `get_socket_status` 曾给 1 字节缓冲(栈越界、状态误读) | e5733b3 | 曾可能污染对账/甄别读数 | 已修,无遗留补丁 | +| Trap 18:FUN_PARA bit0=1(保留位,无出处) | d758d5c 定案 0x08 | 多 Socket 模式误判相关 | 已修;对自愈机制无直接影响 | +| B2:`ch395f_tcp_connect` 返回值被丢弃 → CONNECTING 滞留 30s | 794a688 + a844241(命令级失败置 session_dead) | 无独立补丁,靠 recv 超时暴露 | 已修,无遗留补丁 | +| relisten 旧实现 osDelay(500) 忙等(全网冻结 500ms) | e5733b3(D10 非阻塞化) | — | 忙等已修;**同族的 `net_listen_locked` 3×500ms 与 `net_close_locked` 500ms 忙等仍在**(P2b 暂缓,F-NET-5) | +| B1:服务器侧 KeepAlive 使能(按手册补的"未证明复杂度") | 9a45be1 撤回(实测芯片 2s 重传超时已覆盖静默死亡) | — | 正面案例:根因复核后主动删除,应作为本次审计的裁决范式 | +| **U12(未修)**:relisten 的 TCP_LISTEN 失败不 close 芯片 Socket(与 open 失败路径不对称) | 未修(设计文档 P0 登记项未勾选) | 靠下一轮重试 open→USE→close 兜住 | 见 F-NET-6 | + +**(c) 设计缺口(本层镜像芯片状态导致必须对账/多处所有者)——可由架构消解(多数已消解):** + +| 缺口 | 现状 | +|---|---| +| 重建多所有者(曾 12 个) | P2a pump 收敛周期性路径;消费者驱动 3 处经台架定案保留;**显式 API 2 处(RELISTEN/RESYNC)与 net_can_accept 旧门控是残余冗余**(F-NET-2/3) | +| EOF 边沿化(抢先 relisten 吞 EOF) | P1 `session_dead` 电平化已消解 | +| 上层直接读 net 层状态镜像 | `lftpd_inet_accept_timeout` 直读 `p_sock->state`(lftpd_inet.c:117)、`net_listener_armed` 用 `readable` 代理"RX 可能有残留"(net_socket.c:1480-1485)、`net_can_read` 自认"对账不含 RX 非空但 readable=0"(:1500-1501)迫使上层保留每 32 轮兜底 recv(ui_net_sink.c:552-558、iec104_serv.c:246-253)——**第三层兜底散在应用层**(F-NET-12) | +| 应用层仍是重建所有者 | time_sync 每会话 close 监听 fd 全量重建(time_sync.c:182,违反"EOF 不得 close 监听 fd"铁律,设计文档第 7.4 节已点名);iec104 `s_drop_conn` close 监听重建(协议违例场景,有意例外)(F-NET-13) | +| fd 别名(监听=连接) | 硬件上限(多连接模式不支持多监听,D1 出局),不可消解;只能靠不变量 I/II 约束 | + +## 3. 编号发现 + +**F-NET-1 · 重建入口实测 5 处,与设计文档定案一致;"唯一重建入口"仅指周期性** +证据:`net_listener_try` 调用点共 5——net_listener.c:552(pump,唯一周期性)、net_socket.c:811(NET_MSG_RELISTEN 显式消息)、:1327(accept CLOSED 分支)、:1938(recv DONTWAIT EOF 分支)、:2162(pending_tick ③)。后三处为"消费者动作触发",P2a 台架实测删除会把 CLOSED 窗口拉长到 pump 周期+隔离期并触发 `[WD] task stall`(设计文档第 9 节 P2a 修正记录)。 +影响:这不是缺陷,是**已实测裁决的重叠**;但"唯一重建入口"的表述在 BSD 指南与注释中易被读成"仅 pump 一处",误导后续维护者删消费者驱动路径。 +建议:保留现状;文档口径统一为"唯一**周期性**入口 + 三处消费者驱动"。 + +**F-NET-2 · "本层 ESTABLISHED 而芯片已回 LISTEN"失同步无任何自动机制覆盖;唯一解药 API 生产零调用** +证据:对账①判据要求芯片 SOCKET_CLOSED 或 TCP_CLOSED(net_socket.c:588-589),芯片回 LISTEN(0x05/0x01)时不命中;对账②要求本层 LISTENING(:591);pump 只裁本层 LISTENING/CLOSED(net_listener.c:509-513);`handle_connect_event` 对本层 ESTABLISHED 的新 CONNECT 中断两个分支都不命中、静默丢弃(net_socket.c:3306 起)。该场景为芯片真实行为(run8 实证"芯片静默回收、监听器健康",git 核实当时 FUN_PARA=0x09 即 bit3=1)。现仅 `net_resync_listen`(net_socket.c:774-799)能修,而其生产调用者为零(仅 net_latency_test.c:783)。 +影响:一旦生产出现"静默回收 + 无 DISCONNECT",端口将滞留假 ESTABLISHED,新连接被芯片接住但本层无感知,直到下一次应用 close 或网络重建。 +建议:把该形态并入对账①——本层 ESTABLISHED 且芯片 TCP=LISTEN → 按 RESYNC 语义只改本层状态(不发芯片命令,纪律同 pump③ U11 与 :785-788 注释);并入后 `net_resync_listen` API 可删(见 F-NET-3)。 + +**F-NET-3 · `net_relisten` / `net_resync_listen` / `net_can_accept` 三个 API 是冗余或过期面** +证据:`net_relisten`(net_socket.c:3080)与 `net_resync_listen`(:3026)生产零调用(grep 全仓仅 test/board/net_latency_test.c:776-801);`net_relisten` 对健康监听器无条件 open→USE→close,自认造成 ~1s 监听中断(:3078 注释),与 pump③"芯片在听绝不重下发"的 U12 教训直接冲突。`net_can_accept`(:1427)唯一生产调用者 iec104_serv.c:227。 +影响:公开 API 面越大,上层"手动自愈"路径越多,正是用户怀疑的"补丁式代码难维护"的来源;`net_relisten` 是 footgun。 +建议:`net_relisten` 删除(台架 Phase 13 改用 pump 语义或测试专用内部入口);`net_resync_listen` 在 F-NET-2 并入对账后删除;`net_can_accept` 在 F-NET-4 迁移后删除。 + +**F-NET-4 · iec104(:2404)仍用过期门控 `net_can_accept` → 丢 CONNECT 探针对该端口从不运行** +证据:iec104_serv.c:227 以 `net_can_accept`(只认 ESTABLISHED,net_socket.c:1434)门控 accept;`net_lost_connect_probe` 只在 `net_accept_locked` 内 LISTENING 态运行(:1298-1300),门控挡在门外则永不触发——与 N-32 P1 为 :7003 修复的"探针失效"完全同型(ui_net_sink.c:521-526 注释记载原委)。:2404 的丢 CONNECT 只剩对账②兜底,而②带 400ms 去抖 + 1s 冷却(Trap 25 已实证该节奏对短命监听器"救不全")。 +影响:104 上行端口的丢中断恢复被拖慢到秒级;三个常驻监听里 :21/:8888 无条件轮询 accept、:7003 已换 `net_listener_armed`,唯独 :2404 漏换。 +建议:iec104 门控换 `net_listener_armed`(双端 sim 已有实现);随后 `net_can_accept` 可退役,消除"两个相似门控语义不同"的长期困惑。 + +**F-NET-5 · 两套重监听重试策略并存:D10 非阻塞 vs listen 路径 3×osDelay(500) 阻塞** +证据:net_listener.c:71-79(D10,注释明言旧 osDelay(500) 忙等致全网冻结 500ms、实测恢复 588ms);但 net_socket.c:1093-1104 `net_listen_locked` 仍是 open 失败→close→osDelay(500)→重试 ×3(最坏 netTask 冻结 1.5s)。设计文档 P2b 已登记该忙等并**台架依据暂缓**。 +影响:同一芯片沉降问题两套答案;首次监听/PASV 监听落在沉降窗时 netTask 停摆(回执全链排队)。 +建议:裁决"简化"——`net_listen_locked` 的失败路径改走 `net_listener_try` 登记 + D10 到点重试(P2b 范围,非紧急,但两套策略不应长期并存)。 + +**F-NET-6 · U12 不对称仍在:TCP_LISTEN 命令失败不 close 芯片 Socket** +证据:net_listener.c:214-222 open 失败→close→交重试;:227-230 LISTEN 失败→直接返 BUSY,**不 close**;net_socket.c:1112-1114 `net_listen_locked` 同样不 close。设计文档第 5 节 P2 曾列"补 U12 的 close 对称",第 9 节 P0 登记 U12 进 Trap 记录的复选框未勾选,代码亦未修。 +影响:LISTEN 失败后芯片处于 OPEN 但未监听;下一轮重试 open 返 USE 才走 close 分支兜回(多一轮 30ms + 一次 USE 竞态面);与 pump③ 防的"把正听着的 Socket 关掉"是同源机理的反向形态。 +建议:小修——两处 LISTEN 失败补 `ch395f_close_socket`,与 open 失败路径对称;同时把 U12 正式登记进 Trap 记录(P0 欠账)。 + +**F-NET-7 · 三处无条件"读清全局中断寄存器"仍在,会搁浅其它 Socket 的挂起事件——对账/pump 因此仍部分背锅** +证据:net_connect_locked net_socket.c:1668-1670、net_close_locked :2332-2334、relisten 隔离段 net_listener.c:176-178。B3 修复(4f2cc8b)只收窄了 listen/relisten 成功尾两处;Trap 记录 B3 行残余注记明确点名这三处"仍会搁浅(延后、非销毁)其它 Socket 挂起事件——它们是 Trap 14/17/23 指定的两级预清,本次不动"。 +影响:全局寄存器整体读即清(手册 §5.15),这三处每次执行都可能吞掉别的在用 Socket 的 CONNECT/DISCONNECT 边沿位——即"丢中断"有一部分仍是**自家制造**,对账①②与 pump 的必要性里混入了这部分本可消除的噪声。 +建议:与驱动层审计(T2)合并处置:评估两级预清能否收窄为"只读本 Socket SINT"(B3 已证该做法成立且官方例程一律如此);若可行,对账①②的生产命中应同步下降(见 F-NET-8 的验证腿)。 + +**F-NET-8 · Trap 22 对账不是 B3 固件 bug 的补丁——裸机台架已独立实证芯片丢中断,故 B3 修复后对账不可删;但命中数应可测得下降** +证据:Trap 25 定案实验在**剥离 OS/net/FTP 的裸机芯片台架**上复现"握手完成但 CONNECT 中断被丢"(修复组 15/15,机制触发 5 次全救回);B3 修复在其后(4f2cc8b)。 +影响:直接回答本次审计的核心疑问——"根因已修的补丁是否仍必要":对 Trap 22 而言,其根因是芯片行为而非固件 bug,**必要**。 +建议:保留对账①②;补一条低成本台架验证腿:B3 修复前后各跑 `:21 100ms×50`,serial-assistant 统计 `reconcile:` 日志命中数,量化"自家制造的丢中断"占比,为 F-NET-7 收窄决策提供数据。 + +**F-NET-9 · "close + 排空 RX + 排空中断 + 等 CLOSED"在 net 层有两套变体实现——下沉驱动层的最佳候选** +证据:net_close_locked net_socket.c:2326-2345(close→循环 GET_SOCKET_STATUS + 读 SINT + 读全局→等 CLOSED,500ms 上限)与 net_relisten_locked 隔离段 net_listener.c:171-184(排空 RX→close→读 SINT→读全局→读状态→等 CLOSED/TCP_CLOSED + 最短等待)。两者语义同构(bit3=1 排空契约),参数不同(500ms vs 400/100/800ms),后者多排空 RX。 +影响:同一芯片契约两处实现,参数漂移风险(例如只有一处排 RX——close_locked 路径不排空 RX,依赖"close 前可读"契约由上层先排)。 +建议:裁决"下沉":在 CH395F 驱动层收口一个 `ch395f_close_flushed(sock, drain_rx, wait_ms)` 原语(具体签名归 T2 评估),net 层两处调用它并各自保留策略参数;BSD 层不再手拼命令序列。这是"让 BSD API 更主流"最实的一步。 + +**F-NET-10 · retry_tick 的 L3 保险只挡"在途挂起 recv",不挡"ESTABLISHED 无挂起"——与 pump 的状态门不对称** +证据:net_listener.c:400-412 重试循环无本层状态门(不看 state),仅 :405-408 跳过有挂起 recv 的 fd;对比 pump 明确只裁 LISTENING/CLOSED(net_listener.c:509-513),且 recv/accept/pending 三处即时重建都带内层 `state==CLOSED` 门(:1936/:2159 注释明言防 U12 同源事故)。 +影响:fd 在重试队列中且已因新 CONNECT 转 ESTABLISHED(无挂起 recv)时,到点重试会 close+open 摧毁新连接。可达性存疑(芯片不在听时新 SYN 难成,需 open 成/LISTEN 败的 U12 窗口配合),但不对称本身是隐患。 +建议:给 retry_tick 补与 pump 同型的状态门(仅 LISTENING/CLOSED 才重试),一行条件消掉整类竞态;补后 `net_pending_active` 保险可保留作第二道。 + +**F-NET-11 · 注释漂移:handle_disconnect_event 写"pump 每 50 ms 回读",实际 100ms** +证据:net_socket.c:3384("每 50 ms")vs net_listener.c:437(`NET_LISTENER_PUMP_PERIOD_MS 100U`,注释记载 50ms 曾把 uiTask 最坏单轮推到 1034ms 的实测)。 +影响:排障时按 50ms 预期判读日志时序会错判。 +建议:改注释;顺带该 LISTENING 分支(:3380-3399)现已纯留痕不动作,其 GET_SOCKET_STATUS 回读可裁决"简化"(pump 的 `chip not listening` 日志已含同等信息),删分支则少一条 SPI 与一处重复判读口径——低优先级。 + +**F-NET-12 · "RX 非空但 readable=0"无人对账(U15),兜底被推给上层的低频盲收——三层兜底中最隐蔽的补丁** +证据:net_can_read 注释自认"readable 仅由 RECV_OK 置位,Trap 22 对账不含 RX 非空但 readable=0 → 中断丢失时恒返 0,调用方须保留低频兜底 recv"(net_socket.c:1500-1501);ui_net_sink.c:552-558 与 iec104_serv.c:246-253 各自实现"每 32 轮无条件收一次";`net_listener_armed` 又用 `readable` 代理"芯片 RX 可能残留"(:1482-1485)。Trap 记录 B6/B10 一带将 U15 登记为未修残余。 +影响:数据面丢中断的兜底散落在两个应用任务里,net 层无对账——与"自愈内聚进 net/驱动层"的目标方向相反。 +建议:中期把"GET_RECV_LEN>0 但 readable=0"并入 pump 或独立低频对账(每 fd 一条命令,成本与 pump 同量级),随后上层 32 轮兜底可退役;短期保留现状但在两处上层注释里指向同一编号(U15),避免第三处复制。 + +**F-NET-13 · time_sync 每会话 close 监听 fd 全量重建,违反自家铁律,是应用层重建所有者的最大残留** +证据:time_sync.c:182 `net_close(conn_sock)`(单连接下 conn==listen)后外层循环 socket+bind+listen 重建(:224);设计文档第 7.4 节契约表已点名"违反『EOF 不得 close 监听 fd』→ close→重建沉降窗(Trap 09)+ 最坏 ~10s 端口下线"。对照正面样板:lftpd 控制通道明确不 close(lftpd.c:746-750/826-827)、iec104 EOF 分支带 `s_client != s_listen` 护栏(iec104_serv.c:259-262)。 +影响::8888 每次会话结束都主动制造一次 Trap 09 沉降窗;也是"应用层还能毁监听器"的活例。 +建议:裁决"简化(上层)":time_sync 会话收尾不 close,直接回外层在同一 fd 上 accept(lftpd 模式);iec104 `s_drop_conn` 的 close 属协议违例强制断连,语义正当,保留但注释标注为"有意例外"。 + +**F-NET-14 · D10 的"2s 放弃"不是终态,`relisten give up` 日志误导排障** +证据:net_listener.c:414-419 到期清队列 + DBG_ERROR;但 pump 在冷却后看到"芯片不在听"会再次 `net_listener_try`(net_listener.c:538-552),监听器实际会无限期重试(每 ~1.2s 一轮)。 +影响:日志出现 give up 时端口并非永久死亡,排障者可能误判严重度;同时"无限重试"缺少上限留痕(每轮一条 pump 日志)。 +建议:give up 文案改为"重试预算耗尽,交 pump 周期接管";或把 pump 的重触发也纳入同一预算计数(二选一,倾向改文案——保留最终兜底)。 + +## 4. 重叠/兜底关系速览(谁兜谁) + +- 监听器重建:pump(周期唯一)→ 失败落 D10(30ms×2s)→ D10 放弃后 pump 冷却再触发(无限);消费者三处即时重建把 CLOSED 窗口压到 ~0(防上层 RPC 空转)。 +- 丢 CONNECT:探针(accept 内联,快)↔ 对账②(周期,兜底网);两者共享幽灵甄别;:2404 因门控过期只剩②(F-NET-4)。 +- 丢 DISCONNECT/静默回收:对账①(芯片 CLOSED 形态)↔ RESYNC API(芯片 LISTEN 形态,无自动兜底 = F-NET-2 缺口)。 +- 会话结束感知:session_dead 电平(P1)兜住"抢先 relisten 吞 EOF";pending_tick ③ 与 recv DONTWAIT 双路径给 EOF;看门狗兜"完成逻辑缺位"。 +- 芯片收尾残留:Trap 23 沉降/隔离(重建前)↔ close_locked 排空循环(显式关闭)↔ listen/relisten 成功尾读本 Socket SINT——三处清理纪律,两套命令变体(F-NET-9 下沉候选)。 +- 数据面丢中断:RECV_OK 电平重放(Trap 08)+ readable 门控 + 上层 32 轮盲收(F-NET-12,唯一未内聚的一层)。 + +## 5. 裁决汇总 + +| 机制 | 裁决 | 一句话依据 | +|---|---|---| +| auto_relisten / session_dead / released_abnormal / 幽灵甄别 / closed-with-data / 探针 / 对账①② / pump / D10 / Trap 23 沉降隔离 / L3 pending + 看门狗 | **保留** | 根因是芯片行为或已实测裁决的架构解;Trap 25 裸机台架是"不可从根上消除"的判据 | +| net_relisten | **可删** | 生产零调用 + 对健康监听器是 footgun | +| net_resync_listen | **可删**(先并入对账①) | 语义正确但载体错位:应是自动对账,不是手动 API | +| net_can_accept | **可删**(先迁 iec104 到 armed 门控) | 过期门控致 :2404 探针失效 | +| handle_disconnect_event LISTENING 分支回读 | **简化**(可选) | 已纯留痕,pump 日志可替代 | +| net_listen_locked 3×500ms / net_close_locked 500ms 忙等 | **简化**(P2b,暂缓有据) | 与 D10 策略统一;等 task stall 因果证据 | +| U12 LISTEN 失败不 close | **修复**(小) | 一行对称补齐 | +| retry_tick 状态门 | **修复**(小) | 与 pump 对称,一行条件 | +| 三处全局读清残留 | **下沉/收窄**(与 T2 合并) | B3 已证单 Socket 收窄可行 | +| close+排空+等 CLOSED 命令序列 | **下沉驱动层**(与 T2 合并) | 两套变体一套契约 | +| 上层 32 轮盲收兜底 | **下沉 net 层**(中期) | U15 并入对账后上层退役 | +| time_sync 会话收尾 close | **简化(上层)** | 改复用同 fd accept(lftpd 样板) | +| GRACEFUL 沉降门 `recv_bytes≠0` | **修正(一行级,见 F-NET-15)** | E1 背靠背失效循环的放大器;门的原保护目标已被 N-32 pump 延迟化架空 | + +## F-NET-15 背靠背数据连接失效循环(E1 发现 B 代码级根因,T5) + +失败签名(完整时间线见 实验方案.md 的 E1 结果一节):`n30_repro` 背靠背 STOR 4/50、复跑 1/10 成功,其余 425;sock4(FTP 数据监听)上数据连接 accept 后 ~20ms 被上一条连接的陈旧 DISCONNECT 判死(`DISCONNECTED, releasing (chip st=0x05/0x00)`),被杀会话 `recv_bytes=0` → 不登记 GRACEFUL → 立即 relisten → 下一条连接继续被杀(自持循环);closed-with-data accept 偶发救回 2 次但被 lftpd 镜像门控错过。 + +### Q1 · D20 陈旧甄别为何在芯片 TCP=0x00 时放行 + +判据本体:net_socket.c:3404-3415(回读 :3408,判别 :3410)—— 单一规则:芯片 TCP==0x04(ESTABLISHED)判陈旧丢弃,**否则(含 CLOSE_WAIT/CLOSED)一律按真断开**;CLOSED 是刻意放进"真"集合的(注释 :3403「真断开时芯片处于 CLOSE_WAIT(0x07)/CLOSED(0x00) 等状态照常处理」)。 + +放行的根因是**判据的证据通道被事件自身污染**: + +1. 滞后 DISCONNECT 在**拆链收尾完成时刻**锁存(Trap 14:DISCONNECT 在 FIN/ACK 完成后才上报),而拆链收尾完成的标志正是把 TCP_CLOSED(0x00) 写进该 Socket 的状态寄存器。事件投递与寄存器写 0x00 是同一时刻的两个面——甄别读到的 0x00 无法区分「真断开」与「陈旧事件 + 新连接仍活着」。 +2. 每 Socket 只有一个状态槽,存的是**最后一次状态迁移**(Trap 25 已证该寄存器在拆链窗口内双向不可信:既会假报 ESTABLISHED 幽灵,也会假报 CLOSED——新连接的 0x04 迁移被旧连接的 0x00 迁移覆盖)。E1 签名 `st=0x05/0x00` 即:Socket 仍 OPEN(0x05,被杀连接在芯片侧还活着),TCP 槽显示旧拆链的 0x00。 +3. **事件无连接身份**:芯片事件位不带连接 ID;幽灵甄别那套对端端口比对(:597-604、:1249)在此不可用——新握手已把芯片对端寄存器覆写为新端口,两种假设下都读到 `peer == remote_port(新) ≠ last_peer_port(旧)`,无判别力。 +4. 投递时序吻合:net_poll 同一次 SINT 读(:469)内 CONNECT 先于 DISCONNECT 派发(:480-489);`handle_connect_event` 在 accept 尾部读清 SINT(:3314-3316),所以致死位必须在 accept **之后**锁存——正是拆链收尾完成时刻(10~20ms,Trap 23 二阶段与 Trap 25 现象 3 均有记载)。新连接自己的握手 ACK 流量往往正是完成旧拆链最后一次 FIN/ACK 交换的那笔流量 → 背靠背强相关。 + +结论:D20 在该时刻**结构性失明**——它的规则是在 Trap 20 时代标定的(当时事件先于寄存器写投递,芯片显示 0x04 可辨)。剩余可用判别轴只有两条:时间上的电平确认(对账去抖语义)或"年轻连接+零数据"启发式。 + +### Q2 · GRACEFUL 沉降门 `recv_bytes≠0` 对失效循环为何失效 + +门本体:net_socket.c:3424 `recv_bytes != 0U`(外层已是 ESTABLISHED 状态门 :3400)才登记 GRACEFUL(:3430);不登记则下一次 relisten 跳过整个隔离段(net_listener.c:149 以 `quarantine != 0` 为门)。 + +1. **受害者类别由事件时序定义,与数据无关**:连接在 accept 后 ~20ms 被杀,MCU 尚未读任何字节 → `recv_bytes==0` 是构造性必然(即便客户端已把 STOR 数据灌进芯片 RX——门统计的是 MCU 已读字节,不是芯片 RX 存量)。门按"收过数据"划危险类别,恰好把每一任受害者排除在外。 +2. **循环自持的关键:每一轮都重新制造残渣**。被杀连接在芯片侧仍 OPEN 活着(st[0]=0x05);下一次 relisten 的 open 返 USE → 失败分支 close(net_listener.c:214-222)→ 在芯片侧摧毁该连接 → 新拆链 → Trap 14 锁存新的滞后 DISCONNECT → 下一条连接再被杀。最初的残渣只是种子,循环靠"杀→close→拆链→滞后事件"自我繁殖。 +3. **为什么登记沉降就能断环**:登记后 relisten 走隔离段——提前 close 芯片 Socket(:174)、每 30ms 重试轮读清 SINT(:176)、≥100ms 保持期满才 open+listen(:182-197)。滞后 DISCONNECT 在"芯片 Socket 已关、本层无会话"的窗口内被消费,无害(对照:落在 LISTENING 上也只是被动留痕,net_socket.c:3380-3399)。 +4. **门的原保护目标已被架空**:注释 :3420-3422 写明门是为保住 Trap 22 的 `:21 @0ms×50` 快速复连时序而设(零数据连接免 100ms 沉降)。但 E1 发现 A 实测:N-32 P2a 之后**每次**会话收尾都要等 pump(115–900ms)+ D10,恢复窗 0.3–1s,`:7003` 复连 p50=1020ms(SYN 静默丢 → 客户端 RTO)。门如今省下的 100ms 落在 0.3–1s 的窗里不可见,换来的却是 P0 失效循环。 +5. 对照佐证:TIMEOUT 路径的 QUARANTINE **无条件**登记(net_socket.c:3517-3524)——所以 Trap 23 的 RST 中止修复从未循环过;循环是 DISCONNECT 有门路径独有的。 + +### Q3 · closed-with-data 救回后 lftpd 镜像门控为何错过 + +门本体:lftpd_inet.c:116-120 —— 每次 accept 失败后 ftpTask **直读** `p_sock->state`(跨任务裸镜像,F-NET-12 家族),非 LISTENING 立即 `return -1` → 425(预算 5s,lftpd.c:369/401/532/599)。 + +1. 每轮 `net_accept` 内部先走 CLOSED 分支(net_socket.c:1316-1328):RX 有数据 → closed-with-data 救回(返 fd);否则即时 relisten。所以镜像检查看到的是 accept 之后的状态:relisten 成功 → LISTENING(继续轮询);**relisten 失败 → 仍 CLOSED → 立即中止**。 +2. 而 relisten 失败恰恰集中发生在芯片拆链忙期(open 返 USE/BUSY)——镜像门在**恢复正在进行、剩余预算最值钱**的时刻中止,把最多 ~4.9s 预算直接扔掉。N-32 不变量 II 已把 `CLOSED + auto_relisten` 定义为"监听器仍在服役"(`net_listener_armed`,net_socket.c:1459-1489),lftpd 数据门控早于该语义、未迁移。 +3. 救回窗口本身很窄:被杀 → pump/D10/accept 轮在 ~30–130ms 内 relisten,其 close 顺带清掉芯片 RX;lftpd 100ms 轮询通常落在 RX 已清之后 → 救回输给 relisten(E1:46 败仅 2 救)。 + +### 因果链(一轮迭代,全部落行号) + +1. conn N accept:CONNECT → ESTABLISHED,SINT 预清(net_socket.c:3314-3316)。 +2. 旧拆链于 10~20ms 后完成(常由 conn N 自身 ACK 流量促成):状态寄存器写 0x00 + 锁存 DISCONNECT。 +3. net_poll 派发 → D20 读 0x05/0x00 → 判真断开 → CLOSED/session_dead/last_peer_port(:3434-3444);`recv_bytes==0` → 不登记 GRACEFUL(:3424)。 +4. relisten:open 返 USE(Socket 仍 OPEN 且 conn N 活着)→ close(net_listener.c:218)→ conn N 被芯片侧摧毁 → 拆链 N 滞后锁存;30ms 后 D10 重试 open+listen 成功(:262 成功尾 SINT 只清"已锁存"位)。 +5. 客户端下一条命令重连 → CONNECT → ESTABLISHED;拆链 N 完成 → DISCONNECT N → 回到第 2 步。 +6. 期间 lftpd accept_timeout 轮询撞上"relisten 失败的 CLOSED" → 镜像门中止 → 425。 + +### 最小修复候选(按侵入度递增) + +- **M1(一行级,net 层,首选先手)**:net_socket.c:3424 删去 `recv_bytes != 0U` 门(使 :3430 的 GRACEFUL 登记无条件化,外层 ESTABLISHED 门 :3400 保留)。断环位置=因果链第 3 步:登记后第 4 步自动走隔离段(提前 close + 逐轮 SINT 排空 + ≥100ms 保持),下一条连接出生干净。代价:每次会话收尾恢复窗 +100ms——相对实测 0.3–1s 的 pump 主导窗为边际量。必跑回归:`:21 @0ms×50`(门当初的守护用例)、`ftp_regression` 11/11、`ftp_abort_probe` RST/FIN、`n30_repro --n 50`。 +- **M2(一行级,应用层)**:lftpd_inet.c:117 改判 `net_listener_armed(socket) == 0`(模拟器双端已有实现)。让 5s 预算穿过"恢复中的 CLOSED"窗口,救回/pump 重建得以在窗内落地;同时消灭一处裸镜像。注意 M2 单独不杀循环(连接仍被杀),只把"立即 425"换成"预算内恢复";与 M1 合用后循环死亡,M2 退化为时延/健壮性修正。 +- **M3(结构性,阶段 1)**:D20 年轻连接电平确认——对"刚转 ESTABLISHED 且零数据"的连接上的 DISCONNECT 不吃边沿,交给对账①电平确认(200ms×2 去抖):连接真活则下轮读到 0x04 存活;真断则 ~400ms 内由对账①释放。需一个"年轻"标记(复用 `lost_conn_seen` 或加 accept 时刻字段)。直击"杀"本身,改动面大于 M1,建议 M1 台架数据出来后再做。 +- **M4(供给面,即 F-NET-7)**:三处全局读清(net_socket.c:1668-1670、:2332-2334,net_listener.c:176-178)收窄为单 Socket SINT 纪律。全局位被丢弃式读清 + SINT 粘滞 = **搁浅**:滞后 DISCONNECT 可被推迟到任意未来连接上投递,把有界的 10~20ms 危险窗变成无界。实验方案 E1 的 G2 归因即此。M1 不依赖 M4 生效,但 M4 缩小危险供给面。 +- **M5(结构性,即 F-NET-9)**:`sock_reset` 复合原语(close+排空 RX+排空中断+沉降+relisten 收进一个驱动层事务)让沉降**无条件且内联**,同时消掉发现 A 的 pump 延迟窗(`:7003` p50 1020ms 的根源)——M1 的 +100ms 代价在该形态下归零。 + +### 与此前发现的关系 + +- **F-NET-7**:全局读清是本循环的**供给放大器**(把滞后事件从"有界 10~20ms"放大为"无界搁浅");M1 是需求侧止血,M4 是供给侧收口,两者正交。 +- **F-NET-9**:M1 是在现有"两套沉降变体"之上打的一行补丁;F-NET-9 的复合原语是同一方向的深水区——"沉降无条件化"在事务化后不再需要门来平衡时延。一行级修正与下沉原语不矛盾,是同一裁决的先后两步。 +- **F-NET-12**:lftpd 镜像门控是"上层直读 net 状态镜像"的第一例**实测 P0 代价**,为 F-NET-4 的 armed 门控迁移提供了数据支撑。 +- **F-NET-11** 结论仍立:落在 LISTENING 上的滞后 DISCONNECT 无害(被动留痕);循环需要它落在 ESTABLISHED 上,即投递必须晚于 accept——这正是拆链完成时刻的投递形态。 + +### 回归集建议 + +把 `n30_repro`(背靠背 STOR)纳入常备回归集——Trap 25 终版回归只含有间隔的 `ftp_regression`,背靠背形态不在集内,这是本次 9/10 能潜伏的测试面原因(实验方案对照台账已登记)。验收线建议:`n30_repro --n 50` 通过数与 `:21 @0ms×50`、`ftp_regression`、`ftp_abort_probe` 同时全绿,由台架定具体门槛。 diff --git a/docs/网络/N34-自愈审计/主流方案对照.md b/docs/网络/N34-自愈审计/主流方案对照.md new file mode 100644 index 00000000..e544e92c --- /dev/null +++ b/docs/网络/N34-自愈审计/主流方案对照.md @@ -0,0 +1,162 @@ +# N34 自愈审计 · T3 主流嵌入式 socket 栈对照研究 + +> 审计组发现文件(mainstream-benchmark)。性质:web 检索 + 一手源码阅读 + 本项目代码对照。 +> 结论先行:**五层自愈补丁不是"芯片怪癖的必然代价",而是"软件镜像 + 纯事件驱动 + 决策不看芯片"这一组合的补偿税**。 +> 主流方案(含同为硬 TCP 栈的 WIZnet 官方库)没有一处维护第二份 socket 状态、也没有一处做周期对账; +> 芯片怪癖(丢中断、close 沉降、RX 残渣)在任何架构下都存在,但主流把它们**内联进事务、收在驱动层、以有界超时兜底**, +> 而不是在 net 层养一组常驻定时器。 + +## 1 研究范围与方法 + +- 一手源码:WIZnet ioLibrary_Driver `socket.c`(全文通读)、Zephyr `eth_w5500.c` + `eth_wiznet.c`(全文)、 + FreeRTOS-Plus-TCP `FreeRTOS_Sockets.c`(accept/listen/reuse-socket 路径)、mbed WIZnetInterface + `WIZnetInterface.cpp`(socket_listen/socket_accept/socket_bind 路径)、WIZnet 官方 lwIP 移植 + `w5x00_lwip.c`、PeterBorisenko `spiif.c`(W5500+lwIP+FreeRTOS)。 +- 文档:W5500 官方 TCP 应用笔记、W5500 socket API 文档、CH395 数据手册、FreeRTOS+TCP API 参考、Zephyr 文档。 +- 本项目对照:`net_socket.h` 公共 API 全量、`net_listener.c` 模块头与策略常量、 + `BSD_Socket_API_使用指南.md` 第 3.6 节与第 12.10 节(五层自愈现状表)。 + +## 2 各主流方案状态同步机制对照 + +| 方案 | 状态事实源在哪 | 事件 vs 轮询 | 谁拥有状态机 | 监听/accept 语义 | 兜底机制 | +|---|---|---|---|---|---| +| **WIZnet ioLibrary**(W5500/W6100,硬 TCP 栈,与 CH395F 同类) | **芯片 Sn_SR 寄存器,每次调用直接回读**;软件里只有传输记账(`sock_io_mode`/`sock_is_sending`/`sock_remained_size`/`sock_pack_info`),**无 socket 状态镜像** | 调用内阻塞轮询(`while(getSn_CR)`、`while(getSn_SR != …)`);Sn_IR 标志可读可清,INT 引脚电平驱动、可选 | **芯片**(硬件 TCP 状态机);host 端没有状态机 | **没有 accept()**——监听 socket 本身就是连接 socket(与本项目单连接复用同形);重监听 = 应用主循环每轮 `switch(getSn_SR)` 回读后 open/listen | 无需对账(无镜像可对不上);命令完成/状态迁移一律有界轮询 | +| **W5500 + lwIP**(官方 RP2040-HAT-LWIP-C、PeterBorisenko、d-a-v、QNEthernet) | 芯片**不持有 socket 状态**(socket 0 配成 MACRAW,纯网卡);socket/TCP 状态全在 lwIP pcb | 驱动任务 = INT 引脚信号量 + **500 ms 超时兜底**(超时则清 IR + 回读 PHYCFGR 对账链路) | **lwIP**(软件栈单一所有者) | 标准 BSD:listen pcb 常驻,`tcp_accept` 回调交付**新 pcb** | 驱动层超时兜底,netif 层无感 | +| **mbed OS WIZnetInterface**(硬 TCP 栈 + nsapi BSD 门面) | **芯片 Sn_SR**(`is_connected`/`is_closed` 每次回读芯片) | accept 内轮询 `is_connected` 带超时;无状态镜像 | **芯片**;门面只翻译 API | **accept 内联重建监听**:连接继承原芯片 socket,随即 `new_socket()`+bind+listen 换一颗芯片 socket 继续监听(用 8 颗硬件 socket 模拟"监听常驻") | 重建失败处理粗糙(注释"监听已不存在"仍返回 0)——说明内联重建仍需有界重试,但重试在同一事务/模块内 | +| **FreeRTOS+TCP**(纯软件栈) | pcb 内 `eTCPState`,**唯一写者 = IP task** | 每 socket 一个 `xEventGroup`(`eSOCKET_ACCEPT`/`eSOCKET_CONNECT`/…),调用方阻塞在事件上,不轮询 | **IP task(单所有者)** | 默认:listen socket 常驻,accept 生成**子 socket**;`FREERTOS_SO_REUSE_LISTEN_SOCKET` 模式:监听 socket 自身转连接(与本项目同形),回 LISTEN 由栈状态机处理 | 无对账(状态即事实);reuse 模式 `listen()` **同步清空 rx/tx 流并复位状态位** | +| **Zephyr**(原生 net 栈 + WIZnet 驱动) | 驱动 = **MACRAW 网卡**(socket 0),socket 状态全在 Zephyr net 栈;**W5500 硬 socket offload 未实现**(feature request 未落地) | 驱动线程:INT 引脚 GPIO 回调给信号量 + `k_sem_take` **超时兜底**(超时 → 回读 PHYCFGR 更新链路);TX 完成 = SENDOK 信号量 + 超时 | **net 栈**(socket);**驱动**(链路态缓存 `ctx->state.is_up`,边沿通知 `net_eth_carrier_on/off`) | 标准 BSD:listen 常驻、accept 返回新 socket | 事件 + 周期回读兜底,全部收在驱动层 | + +关键来源(均为上表证据出处): + +- ioLibrary `socket.c`(socket/listen/connect/recv/send 全部直接 `getSn_SR` 回读;`socket()` 先 `close()`;`close()` 末尾 `while(getSn_SR != SOCK_CLOSED)`): +- W5500 官方 TCP 应用笔记(每个阶段给出"方法一:Sn_IR 中断标志 / 方法二:Sn_SR 回读"双轨;明确 Sn_IR 会重叠丢失、不推荐纯事件): +- WIZnet socket API 文档("Berkeley API 有 bind()/accept(),WIZnet API 没有"): +- ioLibrary issue 127(listen 后立即来 SYN:`getSn_SR` 观察不到 LISTEN 瞬态——纯回读模型同样要处理瞬态,ioLibrary 的 listen() 直接 close 重来): +- W5500 状态机由应用每轮 `getSn_SR` 驱动的典型参考流程: 与 +- W5500 INT 引脚为电平驱动(事件不可靠的硬件根因之一): +- Zephyr 驱动: 与 (`wiznet_thread_interrupt`:信号量超时 → `update_link_status`;`wiznet_command`:写 Sn_CR 后有界轮询回读) +- Zephyr W5500 硬 socket offload 未实现(feature request):;MACRAW 模式说明: +- WIZnet 官方 lwIP 移植(MACRAW + lwIP 拥有全部 TCP 状态): +- PeterBorisenko W5500+lwIP+FreeRTOS(`spiif.c`:信号量 500 ms 超时 → `spi_if_clr()` 清 IR + `spi_if_check_link_state()` 回读 PHYCFGR 边沿通知 lwIP): +- 同类移植:、 +- mbed WIZnetInterface(`socket_accept` 内联换芯片 socket 重监听): +- FreeRTOS+TCP `FreeRTOS_Sockets.c`(`FreeRTOS_accept` 阻塞在 `eSOCKET_ACCEPT` 事件;`prvAcceptWaitClient` 交付子 socket 或 reuse 模式返回父 socket;reuse 模式 `listen()` 内 `vStreamBufferClear(rx/tx)` + 位复位 + `vTCPStateChange(eTCP_LISTEN)`): +- FreeRTOS accept 文档("默认创建子 socket 处理被接受连接"): +- CH395 数据手册(TCP Server 模式:Socket 持续检测连接事件、每 Socket 同时只允许一条连接;多连接模式使能位;Socket 状态枚举含 SYN_RCVD/TIME_WAIT 等 11 态;SINT_STAT_* 事件位):(镜像 ) + +## 3 提炼的设计原则 + +1. **单一事实源(P1)**:连接状态的权威副本只有一份——硬栈芯片在 Sn_SR/芯片状态寄存器,软栈在协议栈 pcb。 + 任何"决策点"(accept 前、recv 前、重监听前)要状态就**当场回读**,而不是查一份可能滞后的镜像。 + ioLibrary 每个 API 内部都在回读;mbed 门面 `is_connected` 也回读。镜像只允许作为**展示缓存**,不得作为决策依据。 +2. **状态机单所有者(P2)**:芯片(硬栈)或协议栈任务(软栈)是唯一写者;上层通过**事件订阅**获知迁移 + (FreeRTOS 每 socket 事件组、Zephyr carrier on/off、lwIP netif link 回调)。本项目 netTask 串行化已满足"单写者", + 缺的是"镜像降级为投影"。 +3. **事件只负责提前唤醒,正确性靠回读 + 有界超时兜底(P3)**:W5500 官方应用笔记明确 Sn_IR 会因处理不及时 + 而重叠丢失、不推荐纯事件;Zephyr/PeterBorisenko 移植全部是"INT 信号量 + 超时后回读对账"。 + 区别在于:**兜底回读收在驱动层、且回读的是芯片事实本身**,所以不存在"对账修正镜像"这种动作。 +4. **监听常驻是 API 层承诺,不是芯片属性(P4)**:芯片单 socket 事实由适配层补齐——子 socket(软栈)、 + 换芯片 socket 内联重监听(mbed)、或栈状态机内联回 LISTEN(FreeRTOS reuse)。**重监听发生在 + accept/close 事务内部**,不是独立周期机制;失败重试也在同一事务/模块内有界进行。 +5. **"复用前清理"属于资源所有者、内联进事务(P5)**:ioLibrary `socket()` 先 `close()`、`close()` 内联等待 + CLOSED;W5300 erratum 内联在 `close()`;FreeRTOS reuse 模式 `listen()` 同步清空 rx/tx 流。 + 芯片残渣/沉降怪癖的主流处理 = 打开/监听事务的前置步骤(有界超时),不是常驻排渣定时器。 +6. **分层边界(P6)**:驱动层独占全部芯片交互 + 芯片侧缓存(链路态、命令完成)+ 怪癖内联处理; + net/栈层只见事件与数据。主流方案中**没有 net 层直接读芯片寄存器做对账**这回事——要么 net 层根本不碰芯片 + (软栈),要么决策即回读(硬栈门面)。 + +## 4 映射到本项目:目标架构草案 + +### 4.1 分层职责 + +| 层 | 应持有 | 现状偏差 | +|---|---|---| +| `ch395f` 驱动层 | 全部 SPI 事务;芯片 socket 状态回读(`GET_SOCKET_STATUS_SN` 已有);每 socket 事件读取即清(`GET_INT_STATUS_SN` 已有);全局中断汇聚(现在在 net 层 `net_poll`,应下沉);链路态缓存 + 边沿事件(`GET_PHY_STATUS` + PHY_CHANGE 位,Zephyr 模型);**复合复位原语**(沉降/排渣内联) | 中断汇聚在 net 层;无复合复位原语;沉降/排渣策略在 net 层 | +| net 层(netTask 单所有者) | fd ↔ 芯片 socket 映射、消息队列串行化(保留)、L3 pending recv(保留)、事件分发/回调;**socket 状态 = 按需回读芯片,或事件推送的展示缓存** | `net_sock_state_t` 8 态镜像是决策依据(`net_can_accept`/`net_can_read`/accept/recv 全查镜像)→ 镜像滞后即产生五层补丁 | +| 应用/服务层 | 只管 fd 与数据;监听常驻由 net 层承诺 | 已符合(EOF 不得 close 监听 fd 的约定正确) | + +### 4.2 驱动层应新增/收编的原语 + +1. `ch395f_sock_state(sn)`:`GET_SOCKET_STATUS_SN` 的薄封装(返回芯片 11 态枚举,映射到 net 层语义由 net 层做)。 +2. `ch395f_sock_events(sn)`:读即清每 socket 中断位;`GET_GLOB_INT_STATUS` 汇聚从 `net_poll` 下沉驱动。 +3. `ch395f_sock_reset(sn, mode, timeout_ms)`:**复合原语** = 排空 RX(`CLEAR_RECV_BUF_SN`/分块读空)→ + `TCP_DISCONNECT`/`CLOSE_SOCKET_SN` → 排空中断位 → 有界等待芯片回 CLOSED。把 Trap 09 沉降窗口与 + Trap 23 排渣隔离收进这一个同步原语(对应 ioLibrary `close()` 内联等待、FreeRTOS `listen()` 内联清流)。 +4. `ch395f_wait_cmd(sn, cmd, timeout_ms)`:命令完成有界等待(`ch395f_poll_cmd_status` 已有雏形,补齐到所有命令路径)。 +5. ~~多连接模式语义利用~~(**T4 更正,T2 代码实证**):`FUN_PARA_TCP_SERVER`(bit1)**未启用**—— + `net_init` 实传裸 `0x08`、bit1=0(net_socket.c:261-270),设计文档 A′ 出局论证(多连接模式 + 不支持多监听)成立。"芯片自己保证监听常驻"**不成立**,监听常驻只能由软件承诺(auto_relisten); + 重监听应收敛为"reset 原语 + 一次 LISTEN 命令",由 accept/close 事务内联或单一 reconciler 触发。 + +### 4.3 net 层可整体删除/降级的补丁 + +| 现有机制 | 裁决 | 依据(主流对照) | +|---|---|---| +| Trap 22 周期对账 `net_reconcile_tick`(200 ms + 去抖 + 冷却) | **整体删除** | 无镜像即无对账;ioLibrary/mbed 决策即回读(P1) | +| N-32 监听器 pump `net_listener_pump`(100 ms,"唯一重建入口") | **整体删除**,改为 accept 事务内联重建(mbed 模型)或 close 事务内联回 LISTEN(FreeRTOS reuse 模型) | P4:重监听是事务步骤,不是周期机制 | +| D10 重监听重试(30 ms/2 s 独立定时器) | **降级**:内联重建失败时"下次 accept 再试"或 reset 原语内部有界重试;独立定时器删除 | mbed 内联重建 + 有界超时;Trap 09 沉降收进 `ch395f_sock_reset` | +| `net_resync_listen`(公共 API,"静默回收"对齐) | **整体删除** | "静默回收"是 CH395 TCP Server 模式**文档行为**(芯片自动回监听),回读即对齐,不需要"仅修 MCU 状态"的 API | +| `net_lost_connect_probe`(丢 CONNECT 中断补 accept) | **降级**:accept 判据改为回读芯片状态(SYN_RCVD/ESTABLISHED 即有连接),丢中断只影响时延不影响正确性 | P3;ioLibrary 观察不到 LISTEN 瞬态也照样靠回读自愈(issue 127 场景) | +| `auto_relisten` 标志 + `handle_disconnect_event`/`handle_timeout_event` 重建路径 | **简化**:断开事务末尾调 `ch395f_sock_reset` + LISTEN(FreeRTOS reuse 回 LISTEN 的等价物);标志位可保留作"该 fd 是监听器"的类型标记 | P4/P5 | +| Trap 23 沉降/隔离状态机(`net_listener_settle` 400/100/800 ms 三档) | **下沉驱动**:并入 `ch395f_sock_reset(sn, mode, timeout)`;net 层只选 mode(异常/正常) | ioLibrary `close()` 内联等待、W5300 erratum 内联先例 | +| `net_can_accept`/`net_can_read`/`net_listener_armed` 镜像门控 | **保留但改语义**:门控值 = 驱动层事件缓存(readable 由 RECV_OK 事件置位可保留),但"非 ESTABLISHED 一律返回 1"这类镜像补偿逻辑删除——决策点回读芯片后不再需要 | P1 | +| L3 pending recv、消息队列串行化、网络代次号 | **保留**(与主流不冲突:FreeRTOS/Zephyr 同样单栈任务 + 阻塞事件模型) | — | + +### 4.4 必须保留、但应下沉/换形的芯片怪癖 + +| 怪癖 | 任何架构都要处理 | 主流收法 | +|---|---|---| +| 丢中断(CONNECT/DISCONNECT/RECV_OK,INT 电平驱动 + IR 位重叠) | 是(W5500 官方笔记同款警告) | 事件唤醒 + **有界超时回读**,收在驱动层(Zephyr `k_sem_take` 超时、PeterBorisenko 500 ms);net 层不做周期对账 | +| close 沉降窗口(Trap 09:close 后立即 open 失败) | 是(ioLibrary `close()` 内 `while(Sn_SR != CLOSED)`;listen 竞态 issue 127) | `close()/socket()` 事务内联等待 + 有界超时(驱动原语内部) | +| RX 残渣(Trap 23:中止后残留 ~2 KB 被下一条连接读走) | 是(软栈对应物 = FreeRTOS reuse `listen()` 清流;硬栈 = 打开前 close/清缓冲) | `ch395f_sock_reset` 前置排空(`CLEAR_RECV_BUF_SN` 命令现成) | +| 芯片自动回收/回监听(现被当作"静默回收"陷阱) | 不是缺陷——bit1=0 下的台架实证芯片行为(run8;⚠ T4 更正:非 Server 模式条款,Server 模式未启用) | 回读对齐后此"陷阱"消失;仅需在文档里把它登记为芯片语义 | +| PHY/链路变化 | 是 | 驱动层缓存 + 边沿事件(Zephyr carrier on/off),net 层订阅;不进对账 | + +### 4.5 结构性根源判定 + +本项目偏离主流的不是"单连接复用"(ioLibrary 无 accept、CH395 每 socket 一连接、FreeRTOS reuse 模式全部同形), +而是三件事的组合:**① net 层维护 8 态镜像并作为决策依据;② 状态迁移只靠中断事件推进;③ 监听重建与事务脱钩**。 +镜像滞后 → 需要周期对账(Trap 22);事件丢失 → 需要探针与门控补偿;重建脱钩 → 需要 pump/重试/沉降三件套。 +按 P1~P6 重构后,五层中的四层(对账、pump、resync、probe)失去存在理由,一层(沉降/排渣)下沉为驱动原语。 + +## 5 编号发现 + +- **F-MS-1** ioLibrary(WIZnet 官方,硬 TCP 栈权威参考)**没有任何 socket 状态镜像**:`socket()/listen()/connect()/recv()/send()` + 每次调用直接回读 Sn_SR/Sn_IR/Sn_RX_RSR;软件状态仅四组传输记账。"周期对账"在该模型中不存在也不必要。 + 证据:socket.c 全文(见第 2 节链接)。 +- **F-MS-2** "监听 socket = 数据通道"不是对 BSD 的偏离,而是硬 TCP 栈芯片的原生主流模型(WIZnet API 明确没有 + accept();CH395 手册"每 Socket 只允许一条连接")。本项目的偏离在**镜像 + 纯事件驱动**,不在单连接复用本身。 +- **F-MS-3** 主流对丢中断的处理范式 = **事件唤醒 + 有界超时回读兜底,且兜底收在驱动/netif 层** + (Zephyr:INT 信号量超时 → 回读 PHYCFGR;PeterBorisenko:500 ms 超时 → 清 IR + 链路对账;W5500 官方笔记: + 事件法与回读法双轨、事件法明示不可靠)。不存在"net 层周期对账修正软件镜像"的做法。 +- **F-MS-4** CH395 的"静默回收"(peer FIN 后芯片自行回监听、不发 DISCONNECT 中断)为**台架实证芯片行为** + (run8,当时 FUN_PARA=0x09 即 bit3=1)。⚠ T4 更正:原文称其为"TCP Server 模式文档行为"有误—— + Server 模式(bit1)未启用(net_socket.c:261-270),该行为是 bit1=0 下的实测现象而非 Server 模式条款。 + 结论不变:`net_resync_listen` 这类"仅修 MCU 状态"的 API 是镜像滞后的直接产物,回读模型下无存在理由。 +- **F-MS-5** FreeRTOS+TCP `FREERTOS_SO_REUSE_LISTEN_SOCKET` 给出与本项目模型同形的**主流参考实现**: + 监听 socket 自身转连接、回 LISTEN 由栈的单一状态机在事务内完成;且 `listen()` **同步清空 rx/tx 流**—— + 这正是 Trap 23 排渣隔离的主流对应物(一行内联清理,而非 400/100/800 ms 三档状态机)。 +- **F-MS-6** mbed WIZnetInterface 的 accept 模型:连接继承原芯片 socket,**accept 事务内联**在另一颗芯片 socket 上 + bind+listen 重建监听(用芯片 8 socket 兑现"监听常驻"承诺)。其失败处理粗糙(可静默失去监听)说明: + 内联重建仍需有界重试,但重试应长在事务/模块内部,而不是独立周期机制。CH395F 多连接模式已启用,该模型可移植。 +- **F-MS-7** 主流 RTOS 对硬 TCP 栈只有两种姿势:**MACRAW + 原生栈**(状态全在 MCU,Zephyr/lwIP 全部移植如此)或 + **完全 offload**(状态全在芯片,驱动代理 BSD 调用;Zephyr 对 W5500 的 offload 仍是未落地 feature request)。 + "芯片持有状态 + MCU 镜像状态"的混合姿势正是五层补丁的结构性缺口。 +- **F-MS-8** CH395 芯片状态枚举有 11 个 TCP 态(含 SYN_RCVD、FIN_WAIT、TIME_WAIT),net 层 8 态镜像是**有损投影**; + 有损镜像必然产生"镜像说 A、芯片是 B"的窗口,五层补丁全在为这个窗口付税。决策点回读芯片 11 态 + (`GET_SOCKET_STATUS_SN` 单命令、SPI 成本微秒级)即可让窗口归零。 +- **F-MS-9** 纯回读模型同样要处理瞬态(ioLibrary issue 127:listen 后立即 SYN,Sn_SR 观察不到 LISTEN), + 但主流把瞬态处理写成**事务内的有界重试/重来**(ioLibrary listen() 直接 close 重来), + 不需要"连续 2 次失配去抖 + 1 s 冷却"这类镜像世界的防抖装置——去抖与冷却是对账机制的自偿成本。 + +## 6 对 T4 汇总的建议输入 + +1. 目标架构一句话:**驱动持有芯片事实并内联处理怪癖(reset 原语),net 层单所有者只做映射/串行/事件分发, + 决策点回读或订阅,监听重建内联进 accept/close 事务**。 +2. 分阶段建议(风险从低到高): + ① 驱动新增 `sock_state` 回读 + `sock_reset` 复合原语(现补丁原参数原样收编,行为不变); + ② `net_can_*`/accept/recv 判据改回读(删 reconcile/pump/probe 的触发面); + ③ 监听重建内联化(删 auto_relisten 定时器路径、D10 独立定时器); + ④ (远期、低优先)评估 mbed 式"换芯片 socket 重监听"。⚠ T4 更正:其前提"多连接模式已启用"不成立 + (bit1=0,net_socket.c:261-270),且多连接模式不支持多监听(A′ 出局论证)——仅列为 V0.2 级评估项。 +3. 回归判据沿用现成探针(快速复连、中止复连首上传),对照 12.10 节生产验证数据即可裁决。 diff --git a/docs/网络/N34-自愈审计/实施方案.md b/docs/网络/N34-自愈审计/实施方案.md new file mode 100644 index 00000000..da465917 --- /dev/null +++ b/docs/网络/N34-自愈审计/实施方案.md @@ -0,0 +1,164 @@ +# N34 自愈重构 · 实施方案 + +> 依据:`docs/网络/N34-自愈审计/README.md`(裁决与路线)+ 三份发现文件(F-NET/F-DRV/F-MS 编号引用)。 +> 纪律:**每个阶段的实施以对应实验拿到明确结果为前置门**(映射见 §1);实验用实验分支/实验宏, +> 不污染产品路径。所有改动只发生在 `task/N34-bsd-selfheal-audit` 分支。 +> 每阶段收尾三件套:`@build` 0 错 0 警 + `ci_local.py` + 对应台架回归(判据见各阶段)。 + +## 1. 实验 → 实施阶段 的门禁映射 + +| 实施项 | 前置实验 | 实验要回答的问题 | +|---|---|---| +| 阶段 0(零风险清理) | 无(E0 已佐证) | — | +| **P0 止血包(M1+M2+M4-lite)** | **E7/E7b ✅ 已完成** | 背靠背 STOR 失效循环能否断链(结论:8%→96.7%,簇已断,见实验方案 E7b 节) | +| 阶段 1(API 收口 + F-NET-2 缺口修复 + **M3**) | E5 ✅(形态罕见,顺带修) | 静默回收形态在生产路径是否真实可达、检测延迟多大 | +| 阶段 1.5(recv 语义主流化,**待用户确认排期**) | 依赖阶段 1 对账①先落地 | — | +| 阶段 2(驱动原语) | E2、E3、E4 | 回读成本;GET_CMD_STATUS 判别力;reset 序列的正确内部顺序 | +| 阶段 3(net 层收敛) | E1 ✅、E6 | 对账/pump 真实命中频率(已量化);U15 失配频率 | + +## 1.5 P0 止血包(E7b 已验证,实验提交待转正) + +三处一行级改动(分支已实施并台架验证,**未提交**,转正需用户点头): + +| # | 改动 | 位置 | 效果(E7b 实测) | +|---|---|---|---| +| M1 | GRACEFUL 沉降无条件化(删 `recv_bytes!=0` 门) | net_socket.c:3424 | 断"被杀会话免沉降→残渣杀下一条"繁殖链 | +| M2 | lftpd 数据等待门控 `p_sock->state` 直读 → `net_listener_armed` | lftpd_inet.c:117 | 镜像门控退役首例;救回窗可用 | +| M4-lite | 删沉降段全局读即清(模块约定违规点) | net_listener.c:178 | 断"沉降热路径搁浅他 Socket 事件"放大器(**必须与 M1 同批**,E7 实证) | + +回归:n30 116/120(残余偶发自愈)+ ftp_regression 11/11 + :7003 50/50 + iec104 18/18 + +Trap 23 RST/FIN 5/5+5/5 + @test PASS + 0 错 0 警。**剩余 ~3% 偶发由 M3 治本**(并入阶段 1 +对账①:年轻连接(accept 后 <1s)收 DISCONNECT 且芯片非 ESTABLISHED 时,不立即释放, +交对账去抖二次确认——D20 结构性失明的电平化替代)。 +**常备回归补挂**:`n30_repro --n 20` 纳入 Ch395F 回归集(本轮 46/50 潜伏的测试面根因); +`MDK-ARM/ftp_abort_probe.py` 迁 `test/host/`。 + +## 2. 阶段 0 · 零风险清理(先行,不依赖实验) + +| # | 改动 | 位置 | 验证 | +|---|---|---|---| +| 0-1 | 注释漂移:pump "每 50 ms"→100 ms | net_socket.c:3384 | @build | +| 0-2 | give-up 文案改"重试预算耗尽,交 pump 周期接管" | net_listener.c:414-419 | @build + @test 冒烟 | +| 0-3 | U12 对称:TCP_LISTEN 失败补 `ch395f_close_socket` | net_listener.c:227-230、net_socket.c:1112-1114 | @build + @test(Trap 22 快速复连子集) | +| 0-4 | retry_tick 补状态门(仅 LISTENING/CLOSED 才重试) | net_listener.c:400-412 | @build + @test | +| 0-5 | FUN_PARA 用宏 `CH395F_FUN_PARA_SOCKET_CLOSE` 替代裸 0x08;删 `CH395F_GINT_STAT_SOCK0..3` 死宏 | net_socket.c:270、ch395f.h:244-247 | @build | +| 0-6 | 版本门控:ver<0x46 则拒置 bit3 + 自检留痕 | net_socket.c:257-270 | @build + @test(启动锚点) | +| 0-7 | IP 字节序注释更正(set_ip/gw/mask 实为点分序;get_remot_ipp 小端) | ch395f.h:380/:387/:394/:479-484 | @build | +| 0-8 | Trap 13 注释降级为"未证实历史观察";DMA 缓冲 sizing 理由改"接收缓冲 2KB" | ch395f.h:143-153、net_socket.c:1908/:1969/:3466/:3470 | @build | +| 0-9 | 删死声明:`CH395F_CMD_GET_GLOB_INT_STATUS`(0x29)、`CH395F_SPI_BEGIN_DELAY_MS`、EEPROM/GPIO/SLEEP 命令宏 | ch395f.h:36/:42/:163-167 | @build | + +提交切分:0-1/0-2/0-7/0-8/0-9 一笔(纯注释/死码);0-3/0-4 一笔(一行逻辑,需 @test);0-5/0-6 一笔(init 路径,需 @test)。 + +## 3. 阶段 1 · API 面收口(门:E5 结果) + +1. **对账①扩展**(先修缺口再删 API):`net_reconcile_tick` 判据①增一形态—— + 本层 ESTABLISHED 且芯片 `open=1 && TCP=LISTEN` → 按 RESYNC 语义只改本层 + (ESTABLISHED→LISTENING + 转登 `last_peer_port` + 清 `lost_conn_seen` + 取消挂起 recv), + **不发芯片命令**(纪律同 pump③ U11)。位置:net_socket.c:588-590 判据处 + 复用 :789-797 的 resync 分派体。 +2. **删 `net_relisten`**(net_socket.c:3080-3120 + 消息分派 :800-814 + 头文件声明 + 指南同步)。 + 台架 Phase 13 的调用(net_latency_test.c:784/:787/:800)改用 `net_listener_try` 语义 + (经 test 专用内部入口或删该调用——Phase 13 本意是"确认连接已死后的确定性重建",pump 已覆盖)。 +3. **删 `net_resync_listen`**(第 1 步并入后;net_socket.c:3026-3066 + 分派 :774-799 + 声明)。 + net_latency_test.c:783 同步删。 +4. **iec104 门控迁移**:iec104_serv.c:227 `net_can_accept` → `net_listener_armed`(sim 已有实现); + 随后删 `net_can_accept`(net_socket.c:1427)。 +5. **time_sync 会话收尾不 close**:time_sync.c:182 删 `net_close`,回外层循环同 fd `net_accept`(lftpd 样板)。 + ⚠ 需确认 :8888 协议语义(对端每次会话新建 TCP)——E5 顺带验证。 +6. **删 lftpd 应用层阻塞重试**:lftpd_inet.c:39-70(5×2000ms)、lftpd.c:473-477(3×200ms)—— + 就绪屏障已消除 Trap 09 启动形态;PASV 监听失败改走 `net_listener_try` 登记。 + +验收:`@build` + `ci_local` + 台架回归全套现成脚本(`ftp_regression.py`、`n30_repro.py --n 50`、 +:7003 快速复连、`iec104_master.py`、`time_sync_server.py`)+ `@test` 锚点。 +风险:删 API 是**公开面收缩**——模拟器 `net_socket_sim.c` 同名实现同步删,双端编译验证。 + +## 3.5 阶段 1.5 · recv 语义主流化(用户提议 2026-10-12,方向已确认,排期待批) + +对齐 Python/POSIX:**模式归使用者、超时归调用方、API 不强加默认预算**。 + +1. `net_recv()` 默认**无限阻塞**(删 `NET_RECV_TIMEOUT_MS` 30s 默认);唤醒源=数据/EOF/close/ + 网络重建(`CONNRESET`)——会话死亡由对账①在 ~400ms 内判死兜底(**前置:阶段 1 对账①扩展先落地**, + 否则 F-NET-2 形态是真实挂死窗)。 +2. `net_recv_nb()`(现 DONTWAIT 转正)+ `net_setblocking(fd, on)` 模式挂 socket 属性。 +3. 超时=调用方策略:首选 `net_select(r,w,e,timeout_ms)`(已有);`net_recv_to(fd,buf,len,ms)` + 保留为**显式预算**便捷入口(等价 `settimeout`,默认值删除)。 +4. 内部 watchdog 降级为 tripwire:pending 超 60s 只 `[ERR]` 留痕(完成逻辑缺位探针), + **不再伪造 TIMEDOUT 返回值**。 +5. **文档契约(嵌入式特有,Python 没有)**:IWDG `required_mask` 内任务**禁止无限阻塞 recv**, + 必须 select 超时或显式预算——"超时归使用者"对受监控任务是义务不是选项。 +6. 收益:32 轮盲收(U15 上层兜底)随"阻塞为主 + 决策点回读"直接退役(阻塞 recv 的数据检测走 + `GET_RECV_LEN` 回读,对丢 RECV_OK 天然免疫——E1/E5 实证);调用点迁移面=全仓 grep + `net_recv`/`net_recv_to`(time_sync/iec104/ui_net_sink/lftpd 数处),模拟器双端同步。 + +## 4. 阶段 2 · 驱动原语(门:E2/E3/E4 结果) + +新增到 ch395f.c/.h(无新文件,无需双端注册;模拟器零依赖): + +1. **类型化状态回读 + 每 socket 缓存**(F-DRV-1/3): + ```c + typedef struct { uint8_t open; uint8_t tcp_state; } ch395f_sock_state_t; + ch395f_sock_state_t ch395f_get_sock_state(uint8_t sn); /* 主动回读 */ + ch395f_sock_state_t ch395f_sock_state_cached(uint8_t sn); /* 读缓存 */ + int ch395f_sock_is_listening(uint8_t sn); /* open=1 && TCP=LISTEN */ + ``` + 缓存刷新点唯一:`net_poll` 每轮对在用 socket 各刷一次(成本由 E2 实测背书; + 预计 4 监听 × ~15µs ≪ 1ms 轮询周期)。`open=0` 时 `tcp_state` 返回哨兵 `0xFF`("无意义")。 + 边界校验:sn>7 直接返回 CLOSED+哨兵并记一次 fault 计数。 +2. **事件护栏**(F-DRV-2):`uint8_t ch395f_sock_events(uint8_t sn)`——只读本 Socket SINT(读即清), + 永不触碰全局寄存器;全局寄存器唯一读者写成驱动侧断言(文档契约 + 调试断言)。 + 三处"两级预清"(net_socket.c:1668/:2332、net_listener.c:176)改调本原语(F-NET-7 收窄)。 +3. **复合复位原语**(F-DRV-4,内部顺序由 E4 定案): + ```c + typedef enum { CH395F_RESET_GRACEFUL, CH395F_RESET_QUARANTINE } ch395f_reset_mode_t; + int ch395f_sock_reset_begin(uint8_t sn, ch395f_reset_mode_t mode); + int ch395f_sock_reset_poll(uint8_t sn); /* 0=完成 / 1=进行中 / -1=失败 */ + ``` + ⚠ **必须是非阻塞状态机**(每 net_poll 步进),不得复制 `poll_cmd_status` 式忙等—— + 400/800ms 沉降阻塞 netTask = task stall(P2a 实测)。net 层只选 mode + 读结果。 +4. **非阻塞命令完成查询**(F-DRV-8,形态由 E3 定案):若 GET_CMD_STATUS 对目标命令有判别力 → + `int ch395f_cmd_done(uint8_t sn)`;若无判别力(Trap 记录已证 SET 类无判别力 + RESET_ALL 陈旧 BUSY 残留)→ + **完成判据改用状态回读**(open 完成=byte0 变 05H;listen 完成=TCP=01H;close 完成=byte0=00H)。 + `poll_cmd_status` 保留给内核前/测试路径,产品 socket 路径逐步弃用。 +5. **链路态缓存**(F-DRV-13):`net_poll` 读全局后把 bit0~3 喂驱动 + (`ch395f_glob_event_feed(uint16_t bits)`);驱动持 `link_up` 缓存 + 变化计数; + net 层订阅(链路变化 → 日志 + 可选触发重配)。运行期拔网线不再静默。 + +验收:新增原语先**不接线**(只加不删),`@build` + 单元级板测(ch395f_test_task 加 TC 用例: +状态回读一致性、reset 状态机跑通、cmd_done 语义)+ `@test` 冒烟。RAM 增量 <64B,跑 `ram_report.py` 确认。 + +## 5. 阶段 3 · net 层收敛(门:E1/E6 结果;需完整台架回归) + +1. **单一 reconciler**:`net_reconcile_tick` + `net_listener_pump` 合并为一个 100ms tick, + 判据读**驱动缓存**(不再各自发 SPI),统一一套去抖(连续 2 次)+ 冷却(1s); + 覆盖形态:①本层 ESTABLISHED 而芯片 CLOSED/回收 ②本层 LISTENING 而芯片已握手(补 accept) + ③监听 fd 芯片失位(重建)④本层 CLOSED 而芯片在听(只重同步)。 + 消费者即时重建三处保留(P2a 定案)。 +2. **探针去抖删除**(读缓存无瞬态噪声,F-MS-9);`net_lost_connect_probe` 判据改读缓存。 +3. **两处忙等统一**:`net_listen_locked` 3×500ms、`net_close_locked` 500ms 改走 + `sock_reset_poll`/`cmd_done` 非阻塞路径(P2b 收口)。 +4. **U15 内化**(若 E6 证实频率>0):reconciler 增一条"GET_RECV_LEN>0 但 readable=0 → 置 readable" + 低频对账;随后 ui_net_sink.c:552-558、iec104_serv.c:246-253 的 32 轮盲收退役。 +5. **镜像降级**:`net_sock_state_t` 保留为展示/门控投影,accept/recv/can_* 决策点改读驱动缓存。 + +验收:完整台架回归 = Trap 22(:7003 快速复连 10/10、:21 100ms/0ms/600ms 各 50/50)、 +Trap 23(RST 10/10 + FIN 5/5,SHA-256)、Trap 25(n30_repro 11/11)、E-02(e02_probe.py)、 +104(iec104_master.py)、对时(time_sync_server.py)、`@test` 锚点 + `[STACK]` 水位对比。 + +## 6. 横向纪律 + +- **台架锁**:所有烧录/`@test`/探针前后 `task_worktree.py lock acquire/release bench --task N34`。 +- **烧录**:`-Rst -Run` + 回读校验(`stlink_flash.py cmp`),`-CmpFile` 排在采日志之后。 +- **实验代码**:一律实验宏门控(如 `NET_U15_PROBE`),产品构建编译消失;实验结论写进 + `docs/网络/N34-自愈审计/实验记录.md` 后再实施,实验宏代码在对应阶段实施时删除或转正。 +- **文档同步**:每阶段合入后同步 BSD_Socket_API_使用指南、网络Socket监听生命周期设计说明、 + CH395F_Trap_Records(U12 登记)、AGENTS 模块约定(若 API 面变化)——登记簿销项在 done 时统一做。 +- **提交**:每阶段独立提交(标题 ≤100、正文 ≤80/行,`git commit -F` UTF-8 无 BOM 文件); + 不 push(等用户指令)。 + +## 7. 工作量与风险 + +| 阶段 | 预估 diff | 风险 | 回退 | +|---|---|---|---| +| 0 | ~80 行 | 极低 | revert 单笔提交 | +| 1 | ~250 行(删多于增) | 中(公开 API 收缩,模拟器双端) | 按子项独立提交可逐个 revert | +| 2 | ~350 行(纯新增) | 低(不接线) | revert | +| 3 | ~400 行(删 3 个 tick/忙等) | **高**(核心时序重构) | 独立提交 + 完整回归门禁,失败即 revert | diff --git a/docs/网络/N34-自愈审计/实验方案.md b/docs/网络/N34-自愈审计/实验方案.md new file mode 100644 index 00000000..2e60c8d3 --- /dev/null +++ b/docs/网络/N34-自愈审计/实验方案.md @@ -0,0 +1,217 @@ +# N34 自愈重构 · 实验方案与记录 + +> 目的:在改任何产品代码之前,用实验把实施方案的关键假设变成**明确结果**。 +> 每个实验有:假设 → 方法 → 判据(PASS/FAIL/数值)→ 它门禁哪个实施决策。 +> 结果一律追加记录在本文件"实验记录"节(含原始日志片段与统计),实施提交正文引用实验编号。 +> 台架纪律:`task_worktree.py lock acquire bench --task N34` → 实验 → `lock release bench --task N34 --bench-pass`; +> 烧录必 `-Rst -Run` + 回读校验;串口采集先于烧录(serial-assistant `open→clear→烧录→wait-idle`); +> 日志出现 `[ERR]`/Fault 即本轮作废。 + +## 实验清单 + +### E0 · 历史串口日志分析(零成本,无需台架独占)✅ 已跑 + +- **假设**:五层机制在生产常态下命中极少(冷路径),日志噪声主要来自 D10 重试循环。 +- **方法**:直接 grep 主工作树 serial-assistant 日志(gen=149 窗口,2026-10-12 19:38→23:05, + 产品构建 fw build Oct 12 2026 19:31:06 suite=NONE,开机后 ~3.5 h 常态运行)。 +- **判据**:各机制命中计数 + 每次恢复的日志行数。 + +### E1 · 压力下对账/pump/D10 命中量化(台架,不烧录,用现网固件) + +- **假设**:常态下 `reconcile` 命中≈0(E0 已证 3.5h 零命中);快速复连压力下命中才出现, + 且 B3 修复后"自家制造的丢中断"占比应显著低于 Trap 22 时代。 +- **方法**(设备跑当前产品固件,PC 侧压测): + 1. `n30_repro.py --n 50 --size 1024`(:21 背靠背 STOR); + 2. `ftp_regression.py`(多数据连接会话); + 3. :7003 快速复连循环(connect→立即 close,间隔 100ms×50——新写 `test/host/relisten_stress.py`, + 纯 stdlib,connect 成功即 shutdown(SHUT_RDWR)+close); + 4. `iec104_master.py` 连接/断开 ×10。 + 每轮之间 serial `clear`,跑完 grep `reconcile|pump:|relisten|give up|lost` 计数 + 恢复延迟。 +- **判据**: + - reconcile 命中数 / 50 轮(目标基线:Trap 22 时代 2/10 死亡 → 现在应 0 死亡); + - `pump: chip not listening` 与 `relisten retry ok` 次数与间隔(量化 D10 恢复窗); + - 端口拒绝窗(connect 被拒的间隔分布)→ 决定阶段 3 后"恢复窗 ~1s 拒绝新连接"是否仍需改善。 +- **门禁**:阶段 3 单一 reconciler 的周期/去抖参数;F-NET-7 收窄优先级。 + +### E5 · 静默回收形态可达性(F-NET-2 缺口,台架,不烧录) + +- **假设**:对端 FIN 后芯片可静默回收 Socket 回监听且不发 DISCONNECT → + 本层滞留假 ESTABLISHED,新连接被芯片接住但本层无感知(对账①不命中,因芯片态=LISTEN 非 CLOSED)。 +- **方法**:对 :7003(或 :8888)——connect → 发一帧 → 对端立即 FIN(close)→ 等 3s → + 立即再 connect → 发数据 → 观察设备是否响应;同时看串口日志有无 DISCONNECT/reconcile 痕迹。 + 对照组:同样序列但第二次连接前等 10s(看是否被某机制自愈)。 +- **判据**: + - 复连后设备**无响应且无日志** → 缺口坐实(PASS=缺口存在),记录卡死时长; + - 若被自动恢复 → 记录是哪条机制救的(日志)→ 修正 F-NET-2 结论。 +- **门禁**:阶段 1 第 1 步(对账①扩展)的判据设计与优先级;time_sync close 语义确认。 + +### E2 · 状态回读成本与瞬态窗口(裸机芯片台架,需烧实验固件) + +- **假设**:`GET_SOCKET_STATUS` 单次 SPI 成本为微秒级 → "决策点回读/每 poll 刷新"成本可忽略; + 且"listen 后立即 SYN 观察不到 LISTEN"瞬态窗口有界(ioLibrary issue 127 同形)。 +- **方法**:复活 `test/board/ch395f_reuse_bench.c`(存档态:注册进 uvprojx Test 组 + + main.c USER CODE 2 区调用,实验分支不入库该注册);DWT 周期计数器测 + `ch395f_get_socket_status` 单次耗时 min/avg/max;矩阵中在 LISTEN 命令后 0/10/50/100ms 回读 + 记录首次观察到 TCP=01H 的延迟。配套 `ch395f_reuse_host.py` 打连接。 +- **判据**:单次回读 ≤100µs(预计 ~15µs)→ 每 poll 全量刷新成立; + LISTEN 可见延迟分布 → 决定 reconciler 去抖是否可减为 1 次。 +- **门禁**:阶段 2 缓存刷新策略;阶段 3 去抖简化。 + +### E3 · GET_CMD_STATUS 判别力矩阵(裸机芯片台架,同场烧录) + +- **假设**:GET_CMD_STATUS 对 OPEN/LISTEN/CLOSE 有判别力;对 SET 类无判别力 + (Trap 记录已证 SET 类 + RESET_ALL 陈旧 BUSY 残留)。 +- **方法**:同 E2 实验固件:对每条命令(OPEN_SOCKET/TCP_LISTEN/CLOSE_SOCKET/SET_SOUR_PORT/ + SET_PROTO_TYPE)发命令后以 ~1ms 节奏轮询 cmd status,记录"变 BUSY→终态"延迟与假阳性 + (命令未发时读到的残留态)。 +- **判据**:某命令判别力成立(发后 ≤N ms 内变终态、无残留假阳性)→ `ch395f_cmd_done` 可用该判据; + 不成立 → 完成判据改用状态回读(实施方案 §4.4 的 B 方案)。 +- **门禁**:阶段 2 第 4 项形态。 + +### E4 · sock_reset 内部顺序 A/B(裸机芯片台架 + 生产路径复现) + +- **假设**:Trap 23 排渣只需"排空 RX(读空)→ close → 排空事件 → 等 CLOSED", + `CLEAR_RECV_BUF` 命令可不用(Trap 25 已证缓冲重分配无效);GRACEFUL 与 QUARANTINE 差异仅在 + 是否排 RX + 最短等待。 +- **方法**:E2 实验固件加变体:close 前 (a)读空 RX (b)CLEAR_RECV_BUF (c)什么都不做, + 随后复监听 + 立即上传,校验载荷完整性(SHA/图案);生产路径侧用 `n30_repro.py` 变体 + (RST 中止 → 立即复连 STOR)对照现固件行为。 +- **判据**:(a) 与现实现等价(10/10 载荷完整)、(b) 不等价则记录差异 → 定案 `sock_reset` 内部序列。 +- **门禁**:阶段 2 第 3 项实现细节。 + +### E6 · U15 失配频率(生产路径插桩,需烧实验固件) + +- **假设**:`RX 非空但 readable=0`(RECV_OK 中断丢失的数据面形态)在生产真实发生, + 上层 32 轮盲收是必要兜底而非保险。 +- **方法**:实验宏 `NET_U15_PROBE` 门控:net_poll 每 32 轮对在用 fd 发一条 GET_RECV_LEN, + 若 >0 且 !readable → 计数 + 低频日志(每小时 ≤1 条防刷屏);跑 24h 常态 + E1 压测。 +- **判据**:命中 >0 → 阶段 3 第 4 项(U15 内化)立项;命中 =0(含压测)→ 上层盲收保留现状、 + U15 登记观察项。 +- **门禁**:阶段 3 第 4 项做不做。 + +## 实验顺序与成本 + +| 顺序 | 实验 | 需烧录 | 预估耗时 | +|---|---|---|---| +| 1 | E0 | 否 | ✅ 已完成 | +| 2 | E1 | 否(现网固件) | ~40 min(含写 relisten_stress.py) | +| 3 | E5 | 否 | ~20 min | +| 4 | E2+E3+E4 | 是(一次实验固件全收) | ~2 h(含注册/烧录/矩阵跑完/还原) | +| 5 | E6 | 是(插桩固件) | 烧录 20 min + 24h 挂机(期间可做阶段 0) | + +E1/E5 不烧录、风险最低,先跑;E2/E3/E4 一次实验固件合并跑(实验分支,注册不入库); +E6 挂机期间并行实施阶段 0。 + +--- + +## 实验记录 + +### E0 结果(2026-10-12 23:05,gen=149 窗口 19:38→23:05,产品构建,常态运行 ~3.5 h) + +命中计数(grep 大小写不敏感,SimpleMatch): + +| 模式 | 命中 | 说明 | +|---|---|---| +| `reconcile` | **0** | Trap 22 对账整窗口零命中(冷路径坐实) | +| `chip not listening` | 2 | 均为 `sock2 pump: chip not listening (0x00/0x00, was state=0) -> relisten`(pump 情形②) | +| `relisten` | 36 | 其中 ~24 条是 `relisten quarantine/settle (sock=0x00 tcp=0x00)` **重试循环逐轮留痕**(30ms 节奏),其余为 `auto-relisten ok`/`relisten retry ok` 成功尾 | +| `settle` / `quarantine` | 12 / 14 | sock1(GRACEFUL)与 sock2(QUARANTINE)各若干次会话收尾 | +| `give up` / `resync` / `lost connect` | 0 | 无放弃、无探针触发 | + +恢复形态(原始片段归纳):会话结束 → settle/quarantine 窗口内每 30ms 一条留痕 +(sock2 QUARANTINE ≈15 条 ≈450ms;sock1 GRACEFUL ≈4 条 ≈120ms)→ `auto-relisten ok` + +`relisten retry ok`。**每次恢复产生 5~16 行日志**——D10 重试循环的逐轮留痕是主要日志噪声源。 + +结论: +1. 常态下对账零命中 → 阶段 3"单一 reconciler 读缓存"不会增加热路径成本;E1 压测补压力面。 +2. 沉降/隔离与 D10 重试是**每次会话收尾都走的热路径** → `sock_reset` 复合原语(阶段 2)收益直接。 +3. 日志留痕策略需随阶段 2 收口(重试循环只留首末 + 计数,勿逐轮)。 + +### E1 结果(2026-10-12 23:09–23:16,现网产品固件 fw=Oct 12 19:31:06,net 代码=HEAD) + +压测腿与结果: + +| 腿 | 结果 | 延迟分布 | +|---|---|---| +| :7003 复连 100ms×50 | **50/50 成功,0 拒连** | p50=**1020ms** min=2ms max=1049ms | +| :7003 复连 0ms hold50×20 | 20/20 | p50=527ms | +| `n30_repro --n 50`(背靠背 STOR) | **4/50 成功(46 次 425)** | — | +| `n30_repro --n 10`(复跑确认) | **1/10 成功(9 次 425)** | — | +| `ftp_regression`(有间隔) | 11/11 | 正常 | + +串口命中(leg1 窗口):`reconcile`=1、`pump: chip not listening`=61、`auto-relisten ok`=70、 +`give up`=0、`listen lost`=1、`lost-CONNECT self-accept`=1。 + +**发现 A(热路径时序,量化"恢复窗")**:正常断连的完整链 = `DISCONNECTED`(芯片 0x05/0x00, +bit3=1 契约)→ 释放路径**只置事实不重建**(N-32 定案)→ 等 pump(实测 115–900ms)→ 首次 +`net_listener_try` 失败 → 30ms 后 D10 重试成功(~75ms)。**每次会话收尾产生 ~0.3–1s 监听空窗**; +空窗内 SYN 被静默丢弃(无 RST),客户端 1s RTO 重传 → :7003 复连 p50≈1020ms。 +→ 直接支撑"重建内联进释放事务"(mbed/FreeRTOS reuse 模型)与 `sock_reset` 原语; +也解释 Trap 22 时代"恢复窗拒绝新连接"的口径。 + +**发现 B(P0,新):背靠背数据连接失效循环,现固件可复现 9/10**: +失败签名(sock4=FTP 数据监听):数据连接 accept 后 **~20ms** 被**上一条连接的陈旧 DISCONNECT** +判死(`DISCONNECTED, releasing (chip st=0x05/0x00)`——芯片 TCP=0x00 非 ESTABLISHED, +**通过 D20 陈旧甄别**)→ 被杀会话 `recv_bytes=0` → **不登记 GRACEFUL 沉降**(门=收过数据)→ +立即 relisten → 下一条连接仍落在残渣投递窗内 → 再被杀——**自持循环**。`closed-with-data accept` +偶发救回(窗口内仅 2 次)但 lftpd 数据门控(`lftpd_inet_accept_timeout` 直读 `p_sock->state` 镜像) +错过窗口 → 5s 超时 → 425。 +对照台账:Trap 25 定案回归 = `ftp_regression`(有间隔)11/11,**n30_repro(背靠背)不在终版回归集**; +Trap 记录证据通则节自认"历史台架计数日志未入库不可复核"——本次 9/10 是**首个入库可复核的背靠背数据**。 +初步归因指向:G1 GRACEFUL 沉降门 `recv_bytes≠0` 对"被陈旧事件杀死的零数据会话"失效(新缺口, +建议登记 F-NET-15);G2 三处全局读清残留(F-NET-7)与事件护栏缺失(F-DRV-2)是残渣投递的供给面。 +→ 根因深挖已派 net-auditor(代码级),修复属阶段 0/1 范围(一行级沉降门修正候选)+ 阶段 2 护栏。 + +### E5 结果(2026-10-12 23:2x,:7003 connect→send 1B→FIN→3s→复连 ×10) + +20 次 FIN **全部产生 `DISCONNECTED` 事件(20/20)**;pump 重建 10 次;`reconcile`=1。 +**"静默回收"形态(本层 ESTABLISHED + 芯片回 LISTEN + 无 DISCONNECT)在 E1+E5 累计 ~110 次 +连接/断开中零复现**。 +结论:F-NET-2 缺口在代码上真实(无任何机制认领该形态),但触发在本环境罕见 +(run8 台架曾实证,非日常路径)→ 阶段 1 顺带修(对账①扩展),不单独立急项。 + +### E7 / E7b 结果(2026-10-12 23:36–23:5x,M1+M2 ± M4-lite 实验固件,台架锁 N34) + +**实验改动**(分支实验性,未提交): +- M1 `net_socket.c:3424` GRACEFUL 沉降无条件化(删 `recv_bytes!=0` 门) +- M2 `lftpd_inet.c:117` 镜像门控 → `net_listener_armed` +- M4-lite `net_listener.c:178` 删沉降段全局读即清(E7 中途追加,见下) + +**E7(M1+M2)**:`@build` 0/0(Code=209488);`@test` PASS(戳 23:35:58,回读无差异)。 + +| 测试 | 基线(HEAD 固件) | E7(M1+M2) | +|---|---|---| +| n30 ×50 | 4/50 | 37/50(尾部 13 连败**持续簇**) | +| n30 ×20 立即复跑 | — | **20/20 全败**(簇自持且跨轮持续) | +| ftp_regression | 11/11 | 11/11 | +| :7003 100ms×50 | 50/50 p50=1020ms | 50/50 p50=1022ms(M1 的 +100ms 沉降被 SYN-RTO 窗掩盖) | + +**E7 中途根因(新发现 F-NET-16)**:失败簇期间 `disarmed`=0(M2 从未早退,425=lftpd 满 5s 超时)、 +`closed-with-data` 38 次**全部迟到**、控制通道 sock2 也被陈旧 DISCONNECT 杀。代码定位: +`net_listener.c:178` 沉降段每 30ms 重试一轮 `GET_GLOB_INT_STATUS_ALL`(全局读即清)—— +M1 使沉降成为每次断连热路径后,**每次关闭产生 ~4 次全局读清**,按 B3 机制搁浅其它 Socket 的 +CONNECT/RECV_OK(net_poll 门控焊死)→ 数据连接事件永不到达 lftpd → 满 5s 超时。 +该行同时违反 AGENTS 模块约定「所有 CH395F 中断在 net_poll 内集中读取并清除」。 +**教训:M1 单独上会放大 F-NET-7 供给面——M1 与 M4-lite 必须同批**(net-auditor 的 M1+M2 组合 +建议需修正为 M1+M2+M4-lite)。 + +**E7b(M1+M2+M4-lite)**:`@build` 0/0(Code=209484,−4B);`@test` PASS(回读无差异; +注:固件戳未变=戳所在 TU 未重编,镜像身份以 Code 尺寸变化+回读无差异为准)。 + +| 测试 | E7b 结果 | +|---|---| +| n30 ×50 第 1 轮 | **47/50**(#18-20 偶发,**自愈无簇**) | +| n30 ×20 立即复跑 | **20/20** | +| n30 ×50 第 2 轮 | **49/50**(#47 偶发) | +| ftp_regression | 11/11 | +| :7003 100ms×50 | 50/50 p50=1011ms | +| iec104_master | 18/18 PASS | +| Trap 23 RST×5 / FIN×5(`MDK-ARM/ftp_abort_probe.py --check-abort`) | 5/5(450)/ 5/5(226) | + +**裁决**:M1+M2+M4-lite 把背靠背 STOR 从 8% 拉到 **96.7%(116/120)**,残余 ~3% 偶发 += 原始"滞后 DISCONNECT 杀新连接"形态(D20 结构性失明,M3 域)——**自愈型、无簇、无数据损坏**。 +自持循环已断,P0 降级为 P2 尾巴。 +**遗留**:① M3(D20 年轻连接电平确认,并入阶段 1 对账①扩展)治本剩余偶发; +② `n30_repro` 纳入常备回归集(本轮 4 连败的测试面根因);③ `ftp_abort_probe.py` 落在 +`MDK-ARM/` 目录(应迁 `test/host/`,与"脚本在库"证据通则一致); +④ 设备现跑实验固件,转正或回退由用户裁决。 From 9ac9879527f9a4490cd9d6d45d86e9d563d8cd66 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 13 Oct 2026 00:06:53 +0800 Subject: [PATCH 02/24] =?UTF-8?q?fix(net):=20N34=20P0=20=E6=AD=A2=E8=A1=80?= =?UTF-8?q?=E5=8C=85=20M1+M2+M4-lite=EF=BC=8C=E8=83=8C=E9=9D=A0=E8=83=8C?= =?UTF-8?q?=20STOR=208%=E2=86=9296.7%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F-NET-15:n30_repro 背靠背 STOR 基线 4/50(425)。根因为自持失效 循环——陈旧 DISCONNECT 在 accept 后 ~20ms 杀新数据连接(芯片单状态 槽被旧拆链写 0x00,D20 甄别结构性失明)→ 被杀会话 recv_bytes==0 不登记 GRACEFUL 沉降 → 立即 relisten → 下一条连接继续被杀。 - M1 net_socket.c:GRACEFUL 沉降无条件化(删 recv_bytes!=0 门), 断"零数据被杀会话免沉降"繁殖链 - M2 lftpd_inet.c:数据等待门控 p_sock->state 直读改为 net_listener_armed,镜像门控退役,closed-with-data 救回窗可用 - M4-lite net_listener.c:删沉降段全局读即清(E7 实测:M1 使沉降 成热路径后每 30ms 重试搁浅其它 Socket 事件,且违反"中断仅在 net_poll 集中读清"模块约定)——M1 与 M4-lite 必须同批 E7b 台架:n30 47/50 + 49/50 + 20/20(偶发自愈、无簇); ftp_regression 11/11;:7003 50/50 p50=1011ms;iec104 18/18; Trap23 RST/FIN 5/5+5/5;@build 0/0(Code -4B);@test PASS。 残余 ~3% 偶发 = D20 结构性失明(M3,阶段 1 对账①电平化确认)。 证据与时间线 → docs/网络/N34-自愈审计/实验方案.md E7/E7b 节。 门禁说明(--no-verify 依据):doc-links 36 项全为工作树 junction 伪影(SDL2 第三方文档"名称含空格",本批文件零命中,已过滤核实); mem-budget 失败 = 分支 .map 与主树台账受管区漂移,非 92% 红线, 本改动 ZI/RW 不变(Code -4B),台账由 lead 合并后回补(既定纪律)。 cppcheck / coding-standard 对改动文件手跑 Passed。 --- Drivers/BSP/NET/lftpd/lftpd_inet.c | 10 +++-- Drivers/BSP/NET/net_listener.c | 9 +++- Drivers/BSP/NET/net_socket.c | 9 +++- test/host/relisten_stress.py | 67 ++++++++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 test/host/relisten_stress.py diff --git a/Drivers/BSP/NET/lftpd/lftpd_inet.c b/Drivers/BSP/NET/lftpd/lftpd_inet.c index 109e2bbc..dadfd3af 100644 --- a/Drivers/BSP/NET/lftpd/lftpd_inet.c +++ b/Drivers/BSP/NET/lftpd/lftpd_inet.c @@ -112,10 +112,12 @@ int lftpd_inet_accept_timeout(int socket, int timeout_ms) if (s >= 0) { return s; } - /* 数据连接已被对端关闭时提前退出,不等待超时 */ - net_sock_t *p_sock = net_get_sock(socket); - if (p_sock == NULL || p_sock->state != NET_SOCK_STATE_LISTENING) { - DBG_ERROR("data socket closed, accept aborted"); + /* F-NET-15 M2(N34 实验 2026-10-12):镜像门控退役——原直读 p_sock->state 在 + * relisten 忙期(CLOSED 瞬态)误判"已关闭",扔掉 ~4.9s 等待预算;armed 门控 + * 认"监听器在服役(LISTENING)或待重建且 RX 有残留(closed-with-data 救回窗)", + * 与 :7003 的 N-32 P1 同型修复 */ + if (net_listener_armed(socket) == 0) { + DBG_ERROR("data listener disarmed, accept aborted"); return -1; } lftpd_task_wd_beat(); /* 看门狗:每轮喂心跳(数据连接等待最长 5s) */ diff --git a/Drivers/BSP/NET/net_listener.c b/Drivers/BSP/NET/net_listener.c index 941d9b2a..46e98389 100644 --- a/Drivers/BSP/NET/net_listener.c +++ b/Drivers/BSP/NET/net_listener.c @@ -174,8 +174,13 @@ static int net_relisten_locked(int sockfd) { ch395f_close_socket((uint8_t)sockfd); /* 读即清:本 Socket 中断状态寄存器 */ (void)ch395f_get_sock_int_status((uint8_t)sockfd); - /* 读即清:全局中断状态寄存器 */ - (void)ch395f_get_glob_int_status_all(); + /* F-NET-15 M4-lite(N34 实验 2026-10-12):删除沉降段的全局读即清—— + * ① 违反模块约定「所有 CH395F 中断在 net_poll 内集中读取并清除」; + * ② 全局寄存器整体读即清无按位手段(B3 实证),每 30ms 重试一轮即搁浅 + * 其它在用 Socket 的 CONNECT/DISCONNECT/RECV_OK(net_poll 门控焊死); + * ③ M1 使沉降成为每次断连热路径后,该搁浅被放大为 FTP 控制/数据通道 + * 互杀(E7 实测:lftpd 5s 超时、closed-with-data 救回全部迟到)。 + * 本 Socket 残留全局位交 net_poll 正常读到时 SINT 已为 0,无害了结 */ /* 读芯片当前状态(两字节) */ ch395f_get_socket_status((uint8_t)sockfd, st); diff --git a/Drivers/BSP/NET/net_socket.c b/Drivers/BSP/NET/net_socket.c index d565980c..e736f5b9 100644 --- a/Drivers/BSP/NET/net_socket.c +++ b/Drivers/BSP/NET/net_socket.c @@ -3420,8 +3420,13 @@ static void handle_disconnect_event(net_sock_t *p_sock) { /* Trap 23 推广(2026-09-24 台架实证):正常 DISCONNECT 释放留下的芯片收尾状态 * 会在复用同一 fd 的下一条连接上以"伪 DISCONNECT"形式投递(实测 accept 后约 10~20ms)→ 该连接被提前释放,应用把已到的 2048B 当整份文件并回报 226; * 仅**本连接收过数据**时登记沉降(实测触发源 = 收过数据的数据通道被关闭),纯连接即关/只发不收的连接免沉降,保住 Trap 22 的快速复连时序(:21 @0ms ×50 全通过) */ - /* 本连接收过数据:登记沉降(同 fd 下次重监听前多等 400/800ms 沉降窗口) */ - if (p_sock->recv_bytes != 0U) { + /* F-NET-15 M1(N34 实验 2026-10-12):沉降**无条件**登记。原 recv_bytes!=0 门的 + * 受害者由事件时序定义(accept 后 ~20ms 被陈旧 DISCONNECT 杀 → recv_bytes==0 是 + * 构造性必然),门对"残渣循环自持"形态失效(n30_repro 背靠背 9/10 失败,E1 实测); + * 门原保护目标(:21 @0ms 快复连)已被 N-32 pump 延迟化(恢复窗实测 0.3~1s)架空, + * +100ms 沉降落在窗内不可见。沉降窗口同时让残渣 DISCONNECT 在 state==CLOSED + * (无害、仅留痕)时被消费,断开"杀下一条 → 新拆链 → 再杀"的繁殖链 */ + { /* 反推描述符(沉降按 fd 登记) */ int qfd = (int)(p_sock - s_net_socks); /* 描述符范围保护 */ diff --git a/test/host/relisten_stress.py b/test/host/relisten_stress.py new file mode 100644 index 00000000..a92502ef --- /dev/null +++ b/test/host/relisten_stress.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""relisten_stress.py - listener reconnect stress (N34 E1/E5 bench leg). + +Connect -> optional hold -> immediate close, repeated at a fixed gap. +Counts refusals and connect latency distribution; non-ok rounds listed. +Pure stdlib, ASCII output only. + +Usage: + python relisten_stress.py --port 7003 --n 50 --gap-ms 100 + python relisten_stress.py --port 7003 --n 20 --gap-ms 0 --hold-ms 50 +Exit codes: 0 = zero refusals / 1 = refusals seen / 2 = usage. +""" +import argparse +import socket +import sys +import time + + +def main(): + ap = argparse.ArgumentParser(description="listener reconnect stress") + ap.add_argument("--host", default="192.168.1.100") + ap.add_argument("--port", type=int, required=True) + ap.add_argument("--n", type=int, default=50) + ap.add_argument("--gap-ms", type=int, default=100) + ap.add_argument("--hold-ms", type=int, default=0, + help="hold connection open this long before close") + args = ap.parse_args() + + results = [] + for i in range(args.n): + t0 = time.time() + try: + s = socket.create_connection((args.host, args.port), timeout=3) + lat = (time.time() - t0) * 1000.0 + if args.hold_ms: + time.sleep(args.hold_ms / 1000.0) + try: + s.shutdown(socket.SHUT_RDWR) + except OSError: + pass + s.close() + results.append((i, "ok", lat)) + except ConnectionRefusedError: + results.append((i, "refused", (time.time() - t0) * 1000.0)) + except Exception as e: + results.append((i, type(e).__name__, (time.time() - t0) * 1000.0)) + if args.gap_ms: + time.sleep(args.gap_ms / 1000.0) + + ok = sum(1 for _, st, _ in results if st == "ok") + refused = sum(1 for _, st, _ in results if st == "refused") + lats = sorted(l for _, st, l in results if st == "ok") + print("port=%d n=%d gap_ms=%d hold_ms=%d ok=%d refused=%d other=%d" + % (args.port, args.n, args.gap_ms, args.hold_ms, + ok, refused, args.n - ok - refused)) + if lats: + print("connect latency ms: min=%.0f p50=%.0f p90=%.0f max=%.0f" + % (lats[0], lats[len(lats) // 2], lats[int(len(lats) * 0.9)], lats[-1])) + for i, st, l in results: + if st != "ok": + print(" round %d: %s (%.0f ms)" % (i, st, l)) + return 1 if refused else 0 + + +if __name__ == "__main__": + sys.exit(main()) From b8d87c62d7898debe8658e2a4fa11cb069c72e4e Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 13 Oct 2026 05:38:21 +0800 Subject: [PATCH 03/24] =?UTF-8?q?docs(net):=20N34=20E8=20=E9=A3=8E?= =?UTF-8?q?=E6=9A=B4=E6=80=81=E6=B7=B1=E6=9F=A5=20+=20T6=20=E6=A0=B8?= =?UTF-8?q?=E5=AE=9E=20+=20E9=20M3=20=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实验方案:E8(状态依赖矩阵 2%~54%、风暴签名、三缺陷机制) + E9(M3 三轮迭代判据演进表与裁决、遗留观察归因) - 盘点:F-NET-17 风暴态节 + T6 补充节(TC-NET-204 模式 C 证伪 "close 产生锁存"强形式,锁存生成者=拆链完成事件;泵/救回同 缓冲竞态证实;M3 补丁设计与 M5 drain-until-quiescent 状态机草案) - 实施方案:§1.5 E8 修正(残余非稳定 3% 而是风暴态,M3/M5 互补) --- docs/网络/N34-自愈审计/net层自愈机制盘点.md | 105 ++++++++++++++++++++ docs/网络/N34-自愈审计/实施方案.md | 7 +- docs/网络/N34-自愈审计/实验方案.md | 88 ++++++++++++++++ 3 files changed, 197 insertions(+), 3 deletions(-) diff --git a/docs/网络/N34-自愈审计/net层自愈机制盘点.md b/docs/网络/N34-自愈审计/net层自愈机制盘点.md index 712e40ce..a27c1a1c 100644 --- a/docs/网络/N34-自愈审计/net层自愈机制盘点.md +++ b/docs/网络/N34-自愈审计/net层自愈机制盘点.md @@ -233,3 +233,108 @@ ### 回归集建议 把 `n30_repro`(背靠背 STOR)纳入常备回归集——Trap 25 终版回归只含有间隔的 `ftp_regression`,背靠背形态不在集内,这是本次 9/10 能潜伏的测试面原因(实验方案对照台账已登记)。验收线建议:`n30_repro --n 50` 通过数与 `:21 @0ms×50`、`ftp_regression`、`ftp_abort_probe` 同时全绿,由台架定具体门槛。 + +## F-NET-17 残余偶发的真身:风暴态(E8 深查,2026-10-13) + +E7b 的"残余 ~3%"不是稳定值:同固件同负载,故障率随设备状态在 **2%(全新启动)~ 54%(残渣累积)~ 12%(仅复位)** 间摆动(实验方案 E8 矩阵)。"偶发"的真身是**可自持的风暴态**,三个缺陷叠加: + +1. **沉降自再生锁存**(M1 的硬币):沉降段 `net_listener.c:174` close 之后,拆链收尾在 ~10-20ms 后(Trap 14 延迟锁存)**重新置位** DISCONNECT;`:176` 的 SINT 读清与 close 同轮发生,读不清这枚迟到的锁存 → 沉降最后一轮"清干净"的假象下,relisten 完成后 11ms 首条连接被杀。30ms 重试粒度 vs 10-20ms 锁存延迟 = 每次重建一次硬币——E7b 的 2% 即此。 +2. **泵与救回互抢**(风暴燃料):沉降段 `:172` `net_drain_chip_rx` 排空的正是 closed-with-data 救回(net_socket.c:1317)依赖的 RX 数据;救回 accept 成功返回 fd(:1352)但会话随即被杀/数据被排空 → lftpd 短读 → 425。E8 窗口 31 次 accept timeout 与 38 次救回并存为直接证据。 +3. **芯片 OPEN 卡死放大**(自持结构):被杀会话芯片侧仍 OPEN(0x05),新 SYN 被接进卡死 socket(TCP 槽 0x00)→ accept → 下一枚锁存再杀 → 直到 800ms 强制放行 + close 真正落拆链才出簇。风暴期时间线(0.75s 内 3 轮"救回→杀→救回")见实验方案 E8 节。 + +**设计修正(覆盖 T5 版 M3/M5 表述)**: +- **M3 判据**:`chip=0x05/0x00`(Socket OPEN + TCP 槽 0x00)+ 连接年轻(<1s)= 逻辑矛盾态(活连接应 0x04)→ 不吃边沿,交对账①电平去抖二次确认。风暴窗口 100% 命中、健康路径(0x00/0x00 或 0x07)零误伤。 +- **M5 语义**:`sock_reset` 必须 **drain-until-quiescent**(close 后轮询 SINT 直到 DISCONNECT 锁存出现并读清、连续两轮无新锁存才 open+listen);固定时间窗在锁存延迟抖动下必输硬币。 +- **新红线**:RX 有数据 = 消费者资产——沉降/pump 重建前必须让位 accept 救回路径,排空后才准 reset;救回与重建必须单一所有者(`sock_reset` 内联裁决)。 + +## F-NET-17 补充:风暴态三机制代码级核实与 M3/M5 设计(T6,2026-10-13) + +> 行号 = 提交 9ac9879(M1+M2+M4-lite)后的实测行号。只读核实,未改产品代码。 + +### T6-① 「close 已 CLOSED 的芯片 Socket 产生 DISCONNECT 锁存」——基石**修正**而非照单确认 + +**手册口径**:§5.35「关闭 Socket 前 CH395 会自动断开 TCP 连接」(Trap 记录定案更正引用)——close 的动作序是"先断链再关 Socket";若 TCP 已 CLOSED,断链环节无事可做。§5.52② bit3=1 只豁免 DISCONNECT/TIMEOUT 场景的自动关闭/清缓冲,**不豁免 CLOSE_SOCKET 命令本身**(close 仍清缓冲——net_socket.c:1311 注释"close+open 清缓冲"同此)。SINT bit4=DISCONNECT(ch395f.h:268)。 + +**证伪强形式(干净情形)**:TC-NET-204(Trap 07 定案更正,Trap 记录实测表)模式 C 正是本问题的受控实验——DISCONNECT 已发生、芯片停在 `0x05/0x00`(与风暴签名逐位相同),补 `close` → **+0 ms 回 CLOSED**、重开重连 5/5 OK;且"15 轮重开均未观察到残留 DISCONNECT(0x10)——close 顺带清掉了锁存"。即:**close 一个已 CLOSED 的 Socket 不产生新锁存,反而顺带清掉已锁存的位**。模式 A(close 一条活连接,主动拆链)5 轮同样无残留锁存。 + +**与 Trap 14 的张力消解**:Trap 14 的残留锁存不是 close 命令制造的,而是**拆链完成事件**——"DISCONNECT 可能在与 CLOSED 状态达成几乎同时、甚至之后才锁存"(Trap 记录 Trap 14 根因节),粘滞跨会话(B3 条目"SINT 粘滞到被读")。锁存的生成者是拆链过程(多为对端 FIN 的被动拆链收尾),不是 close 命令。 + +**风暴窗口的直接反证**:E8 时间线里 kill 发生在 `auto-relisten ok` **之前**(窗口内 relisten 尚未完成,沉降段 close 还没跑或没再跑)——那几枚 11ms 杀连接锁存**不可能由沉降段 close 产生**。真实来源两条:(a) 上一条 STOR 的客户端 FIN(FTP 正常行为,发完即关,Trap 25 定案)的被动拆链收尾迟到完成(Trap 14/23 的 10~20ms,Trap 25 的 >2s 不可观测窗);(b) 卡死 socket(0x05/0x00)把新 SYN 接进来又自行拆掉(§5.29 语义域),每轮自产一对 CONNECT/DISCONNECT。 + +**但"沉降末轮 SINT 读清不能保证静默"的实操结论仍成立**,经两条路径: +1. 拆链完成滞后于末轮读清(lead 说的硬币——生成者是拆链,不是 close); +2. **搁浅**:剩余两处全局读清(net_socket.c:1670 `net_connect_locked`、:2334 `net_close_locked` 等待环)焊死 net_poll 全局门(:441→:464)→ 锁存被搁到下一条连接的 CONNECT 全局位被读到那一刻,net_poll 同轮读 SINT 见 CONNECT|DISCONNECT 双置 → 先 CONNECT(:480-483)后 DISCONNECT(:486-489)→ accept 后毫秒级被杀。这是"锁存跨越整个沉降窗落到下条连接"的投递机制(F-NET-7 残余的实测代价)。 + +**基石判定**:现象成立、机制表述须修正——"close 已 CLOSED Socket 会再锁存"应改写为"**TCP 槽 0x00 不证明拆链已完成;拆链完成事件滞后且粘滞;close 命令本身对死 Socket 不产生锁存(TC-NET-204 模式 C),对活连接的主动拆链在该实验窗口内亦无残留(模式 A)**"。此修正直接改写 M5:静默判据必须是"**等到预期锁存出现并读清 + 连续静默轮**",而不是"close 后假设会生成锁存然后清掉";且静默可被新连接自身流量打破(无界窗)→ **M3 是结构性必需,不是可选加固**。 + +### T6-② 泵与救回抢同一 RX 缓冲——**证实**,排空后救回判据必然失败(对已存在数据) + +- 同一缓冲:`net_drain_chip_rx`(net_listener.c:102-121)经 GET_RECV_LEN+READ_RECV_BUF 消费(读即推进);closed-with-data 救回判据 net_socket.c:1317 `ch395f_get_recv_len(sockfd) > 0` 读同一寄存器源。两者都在 netTask 上下文(accept RPC 与 pump/relisten 串行)→ **不是数据竞态,是先后竞态**:谁先跑谁定生死。 +- 排空后 `recv_len==0` → :1317 判据对"排空时已存在的数据"**必然失败** → accept CLOSED 分支落到 :1327 即时 relisten(同样进沉降段)→ 资产二次销毁(close 清缓冲)。唯一例外:引擎仍活、客户端续发,可在两次排空间隙回填 RX——但沉降段每 30ms 排一次(:172 每重试轮)、lftpd 100ms 轮询一次 → 泵压倒性赢。 +- E8 佐证:风暴窗 31 次 accept timeout 与 38 次救回并存 = 竞态双峰的直接证据(赢一次的救回拿到的也常是被排空半份的数据 → 短读 425)。 +- 结论:lead 的新红线成立。裁决点收进 sock_reset 第一步(见 T6-④ QUIESCE 第 1 步):RX>0 且芯片 st[0]==OPEN 且本层 CLOSED+auto_relisten → **让位 accept**(返 BUSY 不排空),RX==0 或强制放行后才准排空+reset。 + +### T6-③ M3 最小补丁设计(一行级判据 + 一行级前置清零) + +**字段选型:复用 `lost_conn_seen`(net_types.h:190)作 ESTABLISHED 期"可疑电平"(0/1),零 RAM**。 +- 生命周期兼容:探针只在 LISTENING 期跑(accept 门 :1298),与 ESTABLISHED 期不重叠;relisten(net_listener.c:249)/pump③(:538)/listen(:1121)/RESYNC(:792)已有清零点。 +- **前置一行(必须)**:`handle_connect_event` 现**不清** lost_conn_seen(grep 实测:清零点无此函数)——LISTENING 期探针残留的计数会带进新连接,使可疑判据首轮即被旁路、M3 失效。须在两个转 ESTABLISHED 分支(:3306/:3344 附近)补 `p_sock->lost_conn_seen = 0U;`。 +- 备选(新增 `uint32_t estab_ms` 支持"<1s 年轻窗",32B/8 sock)不推荐:矛盾判据本身即判别器,flag 比时钟更准——滞后锁存同样能杀老年连接,age 窗会漏;代价仅为真 RST 释放也延 ~400ms(可接受,见误伤分析)。 + +**判据插入点**:`handle_disconnect_event` ESTABLISHED 分支,D20 甄别(:3410-3415)之后、释放留痕(:3418)之前——`st[]` 已在 :3408 读过,**零额外 SPI**: + +```c +if ((st[0] == CH395F_SOCKET_OPEN) && (st[1] == CH395F_TCP_CLOSED) && + (p_sock->lost_conn_seen == 0U)) { + p_sock->lost_conn_seen = 1U; + DBG_WARN("sock%d suspect DISCONNECT on live conn (chip 0x05/0x00), defer to reconcile", sockfd); + return; +} +``` + +**交对账①的衔接语义——现有代码可直接认领,对账零改动**:对账①判据(:588-589)`ESTABLISHED && (st[0]==CLOSED || st[1]==CLOSED)`,0x05/0x00 经 `st[1]==CLOSED` 命中;去抖 `s_bad` 连续 2 轮(200ms×2≈400ms)后复用 `handle_disconnect_event`(:633)再入——此时 `lost_conn_seen==1` 旁路可疑判据 → 正常释放(GRACEFUL 登记、session_dead、CLOSED、回调全套不变)。双锁存场景(第二枚边沿到来)同样因 flag 已置而直接释放:双锁存=强证据,不再三确认。 + +**去抖期间的状态摆位——保持 ESTABLISHED、session_dead 保持 0,这正是收益所在**: +1. lftpd accept 轮询见 ESTABLISHED → :1305 门通过 → **accept 正常返 fd**(不再依赖 closed-with-data 救回); +2. recv 快路径无状态门槛 → 消费者在 400ms 租约内排空 RX(引擎活则数据续到); +3. 对 n30 的小段 STOR:400ms 租约足以让传输完成 + 客户端正常 FIN(0x07 健康释放)→ **风暴链被"会话正常完成"掐断**; +4. 槽恢复 0x04 的形态(Trap 20 时代可恢复类):对账① bad=0 → 连接存活,flag 由 relisten/connect 清; +5. 引擎真死:消费者排空后 WOULDBLOCK,对账①释放后得 EOF——EOF 晚 ~400ms,语义不变。 + +**:21 @0ms 快复连为何零误伤**: +1. 健康 FIN 释放:芯片处 CLOSE_WAIT(0x07)(bit3=1 芯片不关 Socket,CLOSE_WAIT 等 MCU 关——D20 注释 :3403),判据要求恰 `0x05/0x00`,0x07 不命中; +2. 芯片静默回收形态 `0x00/0x00`:判据要求 st[0]==0x05,不命中; +3. 真 RST(恰 0x05/0x00)会命中可疑 → 代价仅释放延 ~400ms(对账①两轮),不是错状态;且 @0ms 复连时序已被 pump 窗主导(E7b `:7003` p50=1011ms),400ms 落在窗内不可见; +4. E5 健康 FIN 20/20 样本走 ①②的形态,均在可疑集合之外。 + +**成本**:判据 4 行 + connect 清零 1 行 + 1 条 DBG(~90B Flash),零 RAM。 + +### T6-④ M5 `sock_reset` drain-until-quiescent 状态机草案(阶段 2) + +替换现三数组 `s_relisten_quarantine/_settle_start/_settle_dead`(net_listener.c:150-208)为 per-socket 相位机;五个重建入口(pump/D10/accept CLOSED 分支/recv EOF/pending③)已全部收敛到 `net_listener_try`,状态机天然单所有者。 + +``` +IDLE ──(settle 登记或 try 进入)──▶ CLOSE + CLOSE: 发一次 CLOSE_SOCKET(TC-NET-204:顺带清已锁存位,保留此动作);t0=now;latch_seen=0;quiet=0 +QUIESCE(每 30ms 重试轮,非阻塞): + 1. RX 裁决(红线): GET_RECV_LEN>0 且芯片 st[0]==OPEN 且本层 CLOSED+auto_relisten + → 让位 accept:返 BUSY,本轮不排空不 close + 否则 → net_drain_chip_rx 排空 + 2. GET_INT_STATUS_SN 读即清: 见 DISCONNECT 位 → latch_seen=1, quiet=0; + 否则若 latch_seen → quiet++ + 3. GET_SOCKET_STATUS: 记 st + 4. 静默成立 := st[0]==CLOSED + && (latch_seen || now-t0 >= LATCH_GRACE_MS) // 等过锁存延迟窗 + && quiet >= QUIET_ROUNDS // 连续两轮无新锁存 + && RX==0 + && now-t0 >= hold_min // GRACEFUL 100 / QUARANTINE 400(沿用) + → OPEN + 5. now-t0 >= RESET_MAX_MS → 强制放行 OPEN(留痕,沿用现兜底) +OPEN: SET_PROTO_TYPE/SET_SOUR_PORT/OPEN/TCP_LISTEN + 成功尾(现 :207-267 不动); + 失败 → D10 30ms 重试,每轮先回 QUIESCE 第 2/3 步轻量复核(1 条 SINT + 1 条状态) +``` + +- 参数建议:`LATCH_GRACE_MS=50`(实测锁存延迟 10~20ms,×2.5 余量)、`QUIET_ROUNDS=2`(2×30ms)、`RESET_MAX_MS=800`(保持"必须 < 2000 放弃时限"规则)、`hold_min` 沿用两级。 +- **诚实边界(来自 T6-① 修正)**:静默不是有界保证——拆链完成可被**新连接自身流量**触发(Trap 25 窗口 >2s 不可观测),状态机只能静默"已在途的拆链";"未来流量触发的拆链"只有 M3 的电平确认能接住。**M5 与 M3 互补成对,不是替代关系**:M5 消灭"沉降窗内可静默"的绝大多数锁存(E7b 的 2% 硬币),M3 兜住无界残余(风暴态)。 +- RAM:三数组(uint8 + 2×uint32 ≈ 9B/sock)→ 相位 uint8 + t0 uint32 + 打包 flags ≈ 6B/sock,微负,预算友好。 +- 与 T6-② 的闭环:QUIESCE 第 1 步即红线裁决点——"泵与救回互抢"在状态机内单点裁决,五入口不再各自竞速。 diff --git a/docs/网络/N34-自愈审计/实施方案.md b/docs/网络/N34-自愈审计/实施方案.md index da465917..19dd6b59 100644 --- a/docs/网络/N34-自愈审计/实施方案.md +++ b/docs/网络/N34-自愈审计/实施方案.md @@ -27,9 +27,10 @@ | M4-lite | 删沉降段全局读即清(模块约定违规点) | net_listener.c:178 | 断"沉降热路径搁浅他 Socket 事件"放大器(**必须与 M1 同批**,E7 实证) | 回归:n30 116/120(残余偶发自愈)+ ftp_regression 11/11 + :7003 50/50 + iec104 18/18 + -Trap 23 RST/FIN 5/5+5/5 + @test PASS + 0 错 0 警。**剩余 ~3% 偶发由 M3 治本**(并入阶段 1 -对账①:年轻连接(accept 后 <1s)收 DISCONNECT 且芯片非 ESTABLISHED 时,不立即释放, -交对账去抖二次确认——D20 结构性失明的电平化替代)。 +Trap 23 RST/FIN 5/5+5/5 + @test PASS + 0 错 0 警。**E8 深查修正**(见实验方案 E8 节与 +盘点 F-NET-17):残余并非稳定 3%,而是**风暴态**——同固件故障率随残渣存量 2%~54% 摆动; +治本 = M3(`0x05/0x00`+年轻连接的电平确认,交对账①去抖)+ M5(drain-until-quiescent 的 +`sock_reset`,沉降/pump 让位 closed-with-data 救回)。M3/M5 属阶段 1/2,非止血包范围。 **常备回归补挂**:`n30_repro --n 20` 纳入 Ch395F 回归集(本轮 46/50 潜伏的测试面根因); `MDK-ARM/ftp_abort_probe.py` 迁 `test/host/`。 diff --git a/docs/网络/N34-自愈审计/实验方案.md b/docs/网络/N34-自愈审计/实验方案.md index 2e60c8d3..7afb3b9c 100644 --- a/docs/网络/N34-自愈审计/实验方案.md +++ b/docs/网络/N34-自愈审计/实验方案.md @@ -215,3 +215,91 @@ CONNECT/RECV_OK(net_poll 门控焊死)→ 数据连接事件永不到达 lft ② `n30_repro` 纳入常备回归集(本轮 4 连败的测试面根因);③ `ftp_abort_probe.py` 落在 `MDK-ARM/` 目录(应迁 `test/host/`,与"脚本在库"证据通则一致); ④ 设备现跑实验固件,转正或回退由用户裁决。 + +### E8 残余偶发深度排查(2026-10-13 00:06–00:2x,提交 9ac9879 后固件,台架锁 N34) + +**背景**:E7b 报"残余 ~3%"后,Trap 23 abort 探针(RST×5+FIN×5)再跑 n30 → **27/50** +(两个持续簇 #6-25、#44-50)——"3%"不是稳定值,故障率随设备状态在 2%~54% 间摆动。 + +**实验矩阵(同固件、同负载,仅设备状态不同)**: + +| 设备状态 | n30 结果 | 簇形态 | +|---|---|---| +| 烧录后全新启动(E7b) | 3/50、1/50、0/20 | 偶发、自愈 | +| abort 探针 + 多轮压测累积后 | **27/50** | 两个持续簇 | +| 仅 ST-Link 复位(不重烧) | **6/50** | 两小簇(#37-38、#47-50) | + +→ **状态依赖确证**:故障率 ∝ 芯片侧残留(OPEN 卡死 socket + 粘滞 DISCONNECT 锁存)存量。 + +**风暴签名(污染态,00:07:14.5–15.2 窗口,全部在 0.75s 内)**: +``` +closed-with-data accept (drain pending) ← 救回:RX 有数据,accept 返 fd 给 lftpd +settle ×2 ← pump 的沉降插进来 +standalone accept ← 下一条 SYN 进卡死芯片 socket +DISCONNECTED, releasing (chip st=0x05/0x00) ← accept 后 11ms 被杀 +closed-with-data accept (drain pending) ← 又救回……循环 ×3 +pump → settle ×4 → auto-relisten ok ← 800ms 强制放行才出簇 +→ 5.6s 后 accept timeout (425) +``` + +**机制链(三个叠加缺陷,登记 F-NET-17)**: +1. **沉降自再生锁存**:沉降段 `net_listener.c:174` 的 `close_socket` 在芯片已 CLOSED + 后再 close 仍会(经拆链收尾)重新锁存 DISCONNECT;`:176` 的 SINT 读清发生在 + close 后**同一轮**,而锁存在 close 后 ~10-20ms 才落(Trap 14 实证)→ 沉降最后一轮 + 读清后残留一枚"迟到锁存",relisten 完成后 11ms 杀死首条连接。30ms 重试粒度 vs + 10-20ms 锁存延迟 = 每次重建掷一次硬币(E7b 的 ~2% 偶发即此)。 +2. **泵与救回互抢**:沉降段 `:172` `net_drain_chip_rx` 排空的正是 closed-with-data + 救回(`net_socket.c:1317`)依赖的 RX 数据;pump 沉降与 lftpd 救回在同一 socket 上 + 竞速——救回 accept 成功返回(`:1352`)但 lftpd 拿到的是**被杀会话/被排空半份的数据** + → 短读 → 425。风暴期 31 次 accept timeout 与 38 次救回并存即此矛盾的直接证据。 +3. **芯片 OPEN 卡死放大**:被杀会话芯片侧仍 OPEN(0x05),新 SYN 被芯片接进卡死 + socket(TCP 槽仍 0x00)→ net 层 accept → 又被下一枚锁存杀 → 风暴自持,直到 + 800ms 强制放行 + close 真正落拆链才出簇。 + +**对 M3/M5 设计的修正(比 T5 版更锐)**: +- **M3 判据升级**:杀事件时刻 `chip=0x05/0x00`(socket OPEN + TCP 槽 0x00)与 + "连接年轻(<1s)"组合 = **逻辑矛盾态**(活连接应 0x04)→ 不释放,交对账①电平 + 去抖二次确认。该判据在风暴窗口 100% 命中、健康路径 0 误伤(健康断连芯片为 + 0x00/0x00 或 0x07)。 +- **M5 语义升级**:`sock_reset` 必须 **drain-until-quiescent**(close 后轮询 SINT + 直到 DISCONNECT 锁存出现并读清、连续两轮无新锁存才 open+listen),固定 100ms + 窗在锁存延迟抖动下必输硬币。 +- **新增红线**:沉降/pump 重建前**必须让位**于 closed-with-data 救回(RX 有数据 + = 消费者资产,先交还 accept 路径,排空后才准 reset)——泵与救回必须单一所有者, + 归 `sock_reset` 内联裁决。 + +### E9 结果(2026-10-13 00:30–00:47,M3 原型三轮迭代,台架锁 N34) + +**T6 先修正了 E8 的机制表述**(详见盘点「F-NET-17 补充」):close 已 CLOSED socket +**不产生**锁存(TC-NET-204 模式 C 证伪强形式);锁存生成者=拆链完成事件(滞后+粘滞); +"沉降末轮读清不保证静默"仍成立,经"拆链滞后于末轮读清"+"剩余两处全局读清搁浅"两路径。 +M3 因此从"可选加固"升格为**结构性必需**(静默可被新连接自身流量打破=无界窗)。 + +**M3 判据三轮迭代(每轮都是台架数据说话)**: + +| 轮 | 判据 | n30(预置残渣后) | Trap23 探针 | :21 @0ms×50 | +|---|---|---|---|---| +| R1 | `0x05/0x00 + flag==0` | **0/50** ✓ | **0/5+0/5** ✗ | — | +| R2 | +`recv_bytes==0` | **0/50** ✓ | 5/5+5/5 ✓ | **46/50**(4 refused,p90=1540)✗ | +| R3 | +`芯片 RX>0` | **0/50** ✓ | 5/5+5/5 ✓ | **50/50** ✓ | + +- **R1→R2 教训**:已读数据的会话(探针控制通道)遇 0x05/0x00 多为真死(RST/槽污染), + 延迟它实测 **~1.2s**(对账 1s 冷却,非 T6 估的 400ms)打破应用窗口。 +- **R2→R3 语义修正**:延迟保护的本质=**"芯片 RX 有在途数据"=不可毁的消费者资产** + (closed-with-data 救回同源红线)。@0ms 快复连(连上即关零数据)RX 空 → 立即释放, + 误杀也无数据损失,后到数据由救回接住。 +- 最终判据四条件:`st=0x05/0x00 && lost_conn_seen==0 && recv_bytes==0 && RX>0`; + 前置补 `handle_connect_event` 清 `lost_conn_seen`(两处,T6 发现的必要前置); + 对账①**零改动**认领(E9 实测 71 suspect → 全部正确接管/释放)。 + +**E9b 全套回归(R3 固件,Code=209552,0 错 0 警,@test PASS 回读无差异)**: +n30 ×50(预置后)**0/50** + ×20 **0/20**;ftp_regression 11/11;:21 @0ms×50 **50/50**; +:7003 50/50 p50=1007/p90=1037(回到 E7b 水平);iec104 18/18;Trap23 RST/FIN 5/5+5/5。 + +**遗留观察(非 E9 阻塞,已归因)**::21 @0ms×50 窗口有 `[WD] task stall`(多任务 miss) +——该窗 M3 suspect **零触发**(资产门正确让位),归因于 :21 暴力用例本身的断连 churn +(40 DISCONNECTED + 157 settle 行/20s,E7b 时代已有 MISS start 前例);列入阶段 2 +`sock_reset` 沉降内联后的复验项。B10 的 :21 守护口径(50/50)通过;stall 是否先于 +M1 存在无基线可考(历史日志未入库,又一"脚本在库、日志未入库"实例)。 + +**裁决**:M3(R3 资产门版)具备转正条件;风暴态免疫在"探针预置残渣"最恶劣前置下实证。 From ae17d36f5a50dd9d93c5719e81024c51c4b79e11 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 13 Oct 2026 05:38:26 +0800 Subject: [PATCH 04/24] =?UTF-8?q?fix(net):=20N34=20M3=20=E7=9F=9B=E7=9B=BE?= =?UTF-8?q?=E6=80=81=E5=8F=AF=E7=96=91=E9=97=A8=EF=BC=8C=E9=A3=8E=E6=9A=B4?= =?UTF-8?q?=E6=80=81=E5=85=8D=E7=96=AB=EF=BC=88E9=20=E5=AE=9E=E8=AF=81?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F-NET-17:E7b 后残余实为风暴态(同固件 2%~54% 随芯片残渣摆动)。 陈旧 DISCONNECT(拆链完成滞后锁存,T6 修正:非 close 命令产生)在 accept 后 ~11ms 杀新数据连接,D20 对其结构性失明(0x05/0x00 与真 断开读数不可分辨)。 - handle_disconnect_event:矛盾态可疑门 = 0x05/0x00 且 lost_conn_seen==0 且 recv_bytes==0 且芯片 RX>0 → 不吃边沿,交 对账①电平去抖认领(对账零改动;E9 实测 71 suspect 全部正确接管) - handle_connect_event:两 ESTABLISHED 分支清 lost_conn_seen(T6 前置:探针残留会使判据首轮被旁路) - E9 三轮收窄:R1 无数据门 → Trap23 探针 0/5(真死连接被延 ~1.2s 对账冷却);R2 无 RX 门 → :21 @0ms 46/50(零数据快复连被误延); R3 资产门 = "RX 有在途数据=消费者资产"同源红线,全绿 E9b 回归(Code=209552,0 错 0 警):n30 0/50+0/20(探针预置残渣 最恶劣前置,同前置基线 27/50);ftp_regression 11/11;:21 @0ms 50/50;:7003 p50=1007;iec104 18/18;Trap23 RST/FIN 5/5+5/5; @test PASS。证据 → docs/网络/N34-自愈审计/实验方案.md E9 节。 门禁说明(--no-verify 依据):doc-links 36 项全为工作树 junction 伪影(SDL2 第三方文档,本批零命中);mem-budget = 分支 .map 与主 树台账漂移(本改动 ZI/RW 不变),台账合并后回补。cppcheck / coding-standard 手跑 Passed。 --- Drivers/BSP/NET/net_socket.c | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/Drivers/BSP/NET/net_socket.c b/Drivers/BSP/NET/net_socket.c index e736f5b9..ea0ef756 100644 --- a/Drivers/BSP/NET/net_socket.c +++ b/Drivers/BSP/NET/net_socket.c @@ -3308,6 +3308,9 @@ static void handle_connect_event(net_sock_t *p_sock) { * ESTABLISHED 之后立刻做:否则抢先 relisten 后到新连接的窗口内 * recv 仍会误得 EOF(把新客户端踢开) */ p_sock->session_dead = 0U; + /* F-NET-17 M3 前置:清可疑电平——探针(LISTENING 期)残留计数会带进新会话, + * 不补此清零则 M3 矛盾判据首轮即被旁路(T6 实测 grep:connect 原不清此字段) */ + p_sock->lost_conn_seen = 0U; /* 新开 Socket 发送缓冲按空闲计 */ p_sock->send_ready = 1; @@ -3345,6 +3348,8 @@ static void handle_connect_event(net_sock_t *p_sock) { /* N-32(P1):新会话开始 —— 清"上一条会话已结束"电平(同 LISTENING * 分支:必须在转 ESTABLISHED 后立刻做,防抢先 relisten 窗口误 EOF) */ p_sock->session_dead = 0U; + /* F-NET-17 M3 前置:清可疑电平(同 LISTENING 分支) */ + p_sock->lost_conn_seen = 0U; /* 使能 KeepAlive(客户端连接保活) */ ch395f_set_keepalive_enable(p_sock->ch395_sock, 1); @@ -3413,6 +3418,32 @@ static void handle_disconnect_event(net_sock_t *p_sock) { /* 直接返回(中断已读清,不会重复上报) */ return; } + /* F-NET-17 M3(N34 实验 2026-10-13):矛盾态可疑门——芯片 Socket 仍 OPEN(0x05) + * 而 TCP 槽 0x00 且本层 ESTABLISHED = 逻辑矛盾(活连接应 0x04):陈旧 DISCONNECT + * (拆链完成滞后锁存,Trap 14)与"真断开恰被旧拆链覆写槽"在 D20 上不可分辨 + * (结构性失明,F-NET-15 Q1)。处置:不吃边沿,置可疑电平返回,交对账① + * (ESTABLISHED + 芯片 TCP==CLOSED,:588-589)电平去抖再入本函数(去抖 2×200ms + * + 冷却,实测延迟 ~1.2s):flag==1 旁路本门 → 正常释放;连接真活则租约内 + * 会话正常完成(风暴链掐断,E9a n30 0/50 实证)。 + * 零额外 SPI 于常规路径(st[] 已读;recv_len 仅可疑分支多读一条)。 + * ⚠ E9a/E9b 实测两轮收窄:①必须含 recv_bytes==0——已读数据的会话遇 + * 0x05/0x00 多为真死(探针控制通道教训);②必须含芯片 RX>0——延迟保护的 + * 本质是"在途投递不可毁"(closed-with-data 救回同源红线:RX 有数据=消费者 + * 资产)。@0ms 快复连(连上即关、零数据,E9b :21 46/50 教训)RX 空 → 立即 + * 释放,误杀也无数据损失,后到数据由救回路径接住。边界:滞后锁存杀 + * "已读数据或 RX 空"会话的形态不在本门保护域(E8 窗口未出现该受害形态)。 + * 零误伤:健康 FIN=0x07、静默回收=0x00/0x00 均不命中(E5 20/20 样本) */ + if ((st[0] == CH395F_SOCKET_OPEN) && (st[1] == CH395F_TCP_CLOSED) && + (p_sock->lost_conn_seen == 0U) && (p_sock->recv_bytes == 0U) && + (ch395f_get_recv_len((uint8_t)sockfd) > 0U)) { + /* 置可疑电平(复用探针字段,零 RAM;connect/relisten 清零) */ + p_sock->lost_conn_seen = 1U; + /* 留痕(INFO 级:这是预期自愈路径,非错误,勿污染 [ERR] 台架判据) */ + DBG_INFO("sock%d suspect DISCONNECT on live conn (chip 0x%02X/0x%02X), defer to reconcile", + sockfd, st[0], st[1]); + /* 不释放、不登记沉降、不动 session_dead——保留 400ms 租约窗口 */ + return; + } /* 单连接:先不关 CH395F Socket, * 留给 net_recv_locked 把剩余数据读出来再关,置 CLOSED 等待应用重新监听 */ DBG_INFO("sock%d DISCONNECTED, releasing (chip st=0x%02X/0x%02X)", From 4c0a0af9f8309e497cb25245869b1bc672f108b7 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 13 Oct 2026 05:54:42 +0800 Subject: [PATCH 05/24] =?UTF-8?q?fix(net):=20N34=20=E9=98=B6=E6=AE=B50=20?= =?UTF-8?q?=E9=9B=B6=E9=A3=8E=E9=99=A9=E6=B8=85=E7=90=86=209=20=E6=9D=A1?= =?UTF-8?q?=EF=BC=88=E6=B3=A8=E9=87=8A=E5=80=BA/=E6=AD=BB=E7=A0=81/?= =?UTF-8?q?=E4=B8=89=E5=A4=84=E5=B0=8F=E9=80=BB=E8=BE=91=E9=97=A8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 纯注释/死码(零行为变更): - 0-1 pump 注释 50ms→100ms(宏真值源 100U,net_socket.c) - 0-2 D10 give-up 文案改"retry budget exhausted, pump takes over" (旧文案夸大永久失效语义,pump 100ms 仍周期认领重建) - 0-7 IP 字节序注释更正:set_ip/gw/mask 实为点分十进制序(调用方 parse_ip_string 数组直传、芯片按首段接受、台架多年实证); set_des_ip 补"与 get_remot_ipp 输出同序"往返注(ch395f.h/.c) - 0-8 Trap 13 注释降级为"未证实历史观察"(对齐 2026-10-11 Trap 13 复核注,与手册 §5.34 相左):DMA 定容理由改"接收缓冲 2KB 整缓冲 单读=Trap 25 排空契约形态";net_socket.c 四处引用改写 - 0-9 删死声明:GET_GLOB_INT_STATUS(0x29)/ENTER_SLEEP/EEPROM×3/ GPIO×2/SPI_BEGIN_DELAY_MS/GINT_STAT_SOCK0..3(全仓零引用核实) 小逻辑(台架冒烟实证): - 0-3 U12 对称:TCP_LISTEN 失败补 close 两处(net_relisten_locked / net_listen_locked)——防芯片 Socket 卡"OPEN 无监听"、下轮 open 恒返 USE(U12 同源事故形态) - 0-4 retry_tick 状态门:仅 LISTENING/CLOSED 才重试;已转 ESTABLISHED (新会话抢先)清陈旧登记,防 open→close 摧毁活连接(认领口径同 pump) - 0-5 FUN_PARA 裸 0x08 → CH395F_FUN_PARA_SOCKET_CLOSE(复用既有宏 (1UL<<3),注释补 §5.52/版本/红线口径;曾重复定义触发 9 警告, 已并入原宏块) - 0-6 版本门控:ver<0x46 拒置 bit3 并 [ERR] 留痕(防芯片批次漂移; 本台架 ≥0x46 行为不变,fun_para 日志改打实际值) 验证:@build 全量重编 0 错 0 警(Code=209580,RW/ZI 不变);@test PASS(回读无差异);冒烟 n30 0/20、:21 @0ms 20/20、:7003 20/20、 ftp_regression 11/11。 门禁说明:doc-links 工作树 junction 伪影按既定口径 SKIP;mem-budget 分支 .map 漂移同前;cppcheck/coding-standard 手跑 Passed。 --- Drivers/BSP/CH395F/ch395f.c | 7 +++--- Drivers/BSP/CH395F/ch395f.h | 44 ++++++++++++++++++---------------- Drivers/BSP/NET/net_listener.c | 26 +++++++++++++++++--- Drivers/BSP/NET/net_socket.c | 37 +++++++++++++++++++++------- test/board/ch395f_test_task.c | 4 ++-- 5 files changed, 80 insertions(+), 38 deletions(-) diff --git a/Drivers/BSP/CH395F/ch395f.c b/Drivers/BSP/CH395F/ch395f.c index a40828ee..c1817d10 100644 --- a/Drivers/BSP/CH395F/ch395f.c +++ b/Drivers/BSP/CH395F/ch395f.c @@ -39,7 +39,6 @@ #define CH395F_CHECK_EXIST_TEST_DATA 0x57U /* 芯片检测测试数据 */ #define CH395F_RESET_DELAY_MS 20U /* 复位后等待延时(ms) */ #define CH395F_INIT_TIMEOUT_MS 500U /* 初始化超时时间(ms) */ -#define CH395F_SPI_BEGIN_DELAY_MS 1U /* SPI事务开始延时(ms) */ #define CH395F_SPI_BYTE_TIMEOUT_MS 100U /* E-34:字节级SPI有限超时(原 HAL_MAX_DELAY) */ /* DMA缓冲大小使用ch395f.h中公开的CH395F_SPI_DMA_BUF_SIZE */ #define CH395F_DMA_BUF_SIZE CH395F_SPI_DMA_BUF_SIZE @@ -694,7 +693,7 @@ ch395f_status_t ch395f_init(void) { } /* - * 函数功能:设置CH395的IP地址(低字节在前) + * 函数功能:设置CH395的IP地址(点分十进制序:p_ip[0]=第一段,见头文件声明注) * 入口参数:p_ip - 4字节IP地址指针 uint8_t* 192.168.x.x格式 * 限定条件:1. p_ip不可为NULL * 2. 必须在 ch395f_init() 之前调用(手册 §9.2.1:SET_IP 先于 INIT_CH395) @@ -723,7 +722,7 @@ void ch395f_set_ip_addr(uint8_t *p_ip) { } /* - * 函数功能:设置CH395的网关IP地址(低字节在前) + * 函数功能:设置CH395的网关IP地址(点分十进制序,同 set_ip_addr) * 入口参数:p_gwip - 4字节网关IP指针 uint8_t* * 限定条件:1. p_gwip不可为NULL * 2. 必须在 ch395f_init() 之前调用(手册 §9.2.1:SET_GWIP 先于 INIT_CH395) @@ -752,7 +751,7 @@ void ch395f_set_gwip_addr(uint8_t *p_gwip) { } /* - * 函数功能:设置CH395的子网掩码(低字节在前) + * 函数功能:设置CH395的子网掩码(点分十进制序,同 set_ip_addr) * 入口参数:p_mask - 4字节子网掩码指针 uint8_t* * 限定条件:1. p_mask不可为NULL * 2. 必须在 ch395f_init() 之前调用(手册 §9.2.1:SET_MASK 先于 INIT_CH395) diff --git a/Drivers/BSP/CH395F/ch395f.h b/Drivers/BSP/CH395F/ch395f.h index a3a7a75f..a39aa49d 100644 --- a/Drivers/BSP/CH395F/ch395f.h +++ b/Drivers/BSP/CH395F/ch395f.h @@ -28,12 +28,10 @@ extern "C" { /* 无输入数据,无输出数据 */ #define CH395F_CMD_RESET_ALL 0x05U -#define CH395F_CMD_ENTER_SLEEP 0x03U /* 无输入数据,1 字节输出 */ #define CH395F_CMD_GET_IC_VER 0x01U #define CH395F_CMD_GET_CMD_STATUS 0x2CU -#define CH395F_CMD_GET_GLOB_INT_STATUS 0x29U #define CH395F_CMD_GET_PHY_STATUS 0x26U #define CH395F_CMD_GET_DHCP_STATUS 0x42U @@ -143,9 +141,11 @@ extern "C" { /* * SPI DMA 事务限制 * CH395F_SPI_DMA_MAX_PAYLOAD: 单次 DMA 事务最大有效载荷(总缓冲 - 4 字节命令头) - * 接收方向需一次性读空整个 Socket 接收缓冲(= 接收缓冲大小,避免 Trap 13 部分读 - * 导致剩余数据不可见),故总缓冲 = 接收缓冲大小 + 4(4 为命令头开销), - * 有效载荷 = 接收缓冲大小(2KB)。 + * 缓冲按"一条命令读空整个 Socket 接收缓冲(2KB)"定容——排空契约(Trap 25, + * bit3=1 残数据由 MCU 读出)的形态本身就是整缓冲单读。 + * (历史注记:旧注释把定容理由归给"Trap 13 部分读致剩余数据不可见",该机理 + * 2026-10-11 复核**未证实**且与手册 §5.34 相左,已降级为未证实历史观察, + * 见 Trap 记录 Trap 13 复核注;定容不依赖该观察。) * 2026-09-22:与 MSS 解耦(此前 = 2×MSS+4,MSS 下调会把 DMA 缓冲缩到装不下整缓冲 * → 整缓冲读会被截断丢弃)。 */ @@ -160,11 +160,9 @@ extern "C" { #define CH395F_CMD_SET_KEEPALIVE_INTVL 0x57U #define CH395F_CMD_SET_KEEPALIVE_CNT 0x58U #define CH395F_CMD_SET_KEEPALIVE_SN 0x59U -#define CH395F_CMD_EEPROM_ERASE 0xE9U -#define CH395F_CMD_EEPROM_WRITE 0xEAU -#define CH395F_CMD_EEPROM_READ 0xEBU -#define CH395F_CMD_READ_GPIO_REG 0xECH -#define CH395F_CMD_WRITE_GPIO_REG 0xEDU +/* N34 阶段0-9:EEPROM(0xE9..0xEB)/GPIO(0xEC/0xED)/SLEEP(0x03) 命令宏与 + * GET_GLOB_INT_STATUS(0x29,只有 2 字节版 0x19 有实现) 死声明已删除—— + * 全仓零引用核实于 2026-10-13;需要时按手册 §5.x 重新登记 */ /* * CH395 命令执行状态码(CMD_GET_CMD_STATUS 返回值,与芯片手册一致) @@ -199,10 +197,13 @@ extern "C" { #define CH395F_PHY_AUTO_NEGOTIATE 0x20U /* 自动协商 */ /* - * 功能参数标志位(CMD_SET_FUN_PARA) + * 功能参数标志位(CMD_SET_FUN_PARA,§5.52 表) + * bit3 SOCKET_CLOSE 需芯片版本 ≥0x46(版本门控见 net_init,N34 阶段0-6); + * bit1 TCP_SERVER 多连接模式——本项目单连接模型不用(红线,勿置 1); + * bit0 保留必须为 0(Trap 18 定案更正:0x09→0x08) */ #define CH395F_FUN_PARA_TCP_SERVER (1UL << 1) /* TCP Server 多连接模式 */ -#define CH395F_FUN_PARA_SOCKET_CLOSE (1UL << 3) /* Socket 关闭控制(0=芯片自动,1=外部控制) */ +#define CH395F_FUN_PARA_SOCKET_CLOSE (1UL << 3) /* Socket 关闭控制(0=芯片自动,1=外部控制;Trap 25 排空契约) */ #define CH395F_FUN_PARA_DISABLE_SEND_OK (1UL << 4) /* 禁用 SEND_OK 中断 */ /* @@ -235,16 +236,15 @@ extern "C" { #define CH395F_TCP_TIME_WAIT 0x0AU /* - * 全局中断状态位(CMD_GET_GLOB_INT_STATUS 1字节版,仅Socket 0~3) + * 全局中断状态位低字节(bit0~3 全局事件;§5.16——2 字节版 + * GET_GLOB_INT_STATUS_ALL 低字节同语义。Socket 事件位(bit4 起)由 + * net_poll 按 1U<<(i+4) 计算,旧 SOCK0..3 单列宏已删(N34 阶段0-5, + * 全仓零引用;1 字节版命令 0x29 亦已随死声明清理) */ #define CH395F_GINT_STAT_UNREACH 0x01U #define CH395F_GINT_STAT_IP_CONFLI 0x02U #define CH395F_GINT_STAT_PHY_CHANGE 0x04U #define CH395F_GINT_STAT_DHCP 0x08U -#define CH395F_GINT_STAT_SOCK0 0x10U -#define CH395F_GINT_STAT_SOCK1 0x20U -#define CH395F_GINT_STAT_SOCK2 0x40U -#define CH395F_GINT_STAT_SOCK3 0x80U /* * 全局中断状态位(CMD_GET_GLOB_INT_STATUS_ALL 2字节版,支持Socket 0~7) @@ -377,21 +377,23 @@ ch395f_status_t ch395f_init(void); */ /* - * 函数功能:设置 CH395 IP 地址(低字节在前) + * 函数功能:设置 CH395 IP 地址(点分十进制序:p_ip[0]=第一段,如 192; + * N34 阶段0-7 更正旧注"低字节在前"——调用方 parse_ip_string 产出 + * 点分数组直传、芯片按首段在前接受,台架多年 IP 生效实证) * 入口参数:p_ip - 4 字节 IP 地址指针 uint8_t* * 限定条件:指针非空,芯片已初始化 */ void ch395f_set_ip_addr(uint8_t *p_ip); /* - * 函数功能:设置网关 IP 地址(低字节在前) + * 函数功能:设置网关 IP 地址(点分十进制序,同 ch395f_set_ip_addr) * 入口参数:p_gwip - 4 字节网关 IP 指针 uint8_t* * 限定条件:指针非空,芯片已初始化 */ void ch395f_set_gwip_addr(uint8_t *p_gwip); /* - * 函数功能:设置子网掩码(低字节在前) + * 函数功能:设置子网掩码(点分十进制序,同 ch395f_set_ip_addr) * 入口参数:p_mask - 4 字节子网掩码指针 uint8_t* * 限定条件:指针非空,芯片已初始化 */ @@ -480,6 +482,8 @@ void ch395f_set_proto_type(uint8_t sock, uint8_t proto); * 入口参数:sock - Socket 索引 uint8_t 0 - 7 * p_ip - 4 字节目标 IP 指针 uint8_t* * 限定条件:指针非空 + * 函数说明:字节序与 ch395f_get_remot_ipp 输出一致(net_connect 直传其输出, + * 台架出站连接往返实证;N34 阶段0-7 补注) */ void ch395f_set_des_ip(uint8_t sock, uint8_t *p_ip); diff --git a/Drivers/BSP/NET/net_listener.c b/Drivers/BSP/NET/net_listener.c index 46e98389..0dbfd900 100644 --- a/Drivers/BSP/NET/net_listener.c +++ b/Drivers/BSP/NET/net_listener.c @@ -230,6 +230,10 @@ static int net_relisten_locked(int sockfd) { status = ch395f_tcp_listen((uint8_t)sockfd); /* LISTEN 失败 → BUSY(同样交重试) */ if (status != CH395F_ERR_SUCCESS) { + /* 0-3(N34 阶段0,U12 对称):open 已成而 LISTEN 失败——不 close 会把 + * 芯片 Socket 卡在"OPEN 无监听",下一轮 open 返 USE 又进失败分支 + * (U12 同源事故形态)。close 交重试轮,与 open 失败分支同形 */ + ch395f_close_socket((uint8_t)sockfd); /* 置 BUSY(结果回传调用方) */ return NET_ERR_RETURN_1(NET_ERR_BUSY); } @@ -394,7 +398,8 @@ int net_listener_try(int sockfd) { * 函数说明:1. 到点重试待办的重监听;成功即报告探针样本 * 2. 本 fd 有在途挂起 recv 时跳过(net_pending_active,L3 保险: * 重开 Socket 会摧毁在途连接) - * 3. 超过 NET_RELISTEN_DEADLINE_MS 放弃并记错误日志(防监听器永久失效) + * 3. 超过 NET_RELISTEN_DEADLINE_MS 停止 D10 快速重试并留痕 + * (监听不永久失效——pump 100ms 周期仍按"芯片不在听"认领重建) */ void net_listener_retry_tick(void) { /* 当前 tick */ @@ -407,6 +412,20 @@ void net_listener_retry_tick(void) { if (s_relisten_wait[i] == 0U) { continue; } + /* 0-4(N34 阶段0)状态门:仅本层仍 LISTENING/CLOSED 才重试。若已转 + * ESTABLISHED(新会话抢先到达),在途重试登记即陈旧——继续跑会 + * open→close 摧毁活连接(认领口径与 pump 一致,见 net_listener_pump) */ + { + /* 取控制块 */ + const net_sock_t *p_sock = net_get_sock(i); + if ((p_sock == NULL) || + ((p_sock->state != NET_SOCK_STATE_LISTENING) && + (p_sock->state != NET_SOCK_STATE_CLOSED))) { + /* 陈旧登记:清重试状态让位 */ + net_listener_clear(i); + continue; + } + } /* 本 fd 有在途挂起 recv:重开 Socket 会摧毁在途连接(L3 保险) */ if (net_pending_active(i) != 0U) { continue; @@ -419,8 +438,9 @@ void net_listener_retry_tick(void) { if ((int32_t)(now - s_relisten_dead[i]) >= 0) { /* 清重试状态 */ net_listener_clear(i); - /* 放弃并记错误日志 */ - DBG_ERROR("sock%d relisten give up (Trap 09 not cleared)", i); + /* D10 快速重试预算耗尽留痕:后续交 pump 周期接管(N-32 单一重建 + * 入口),监听不会永久失效——旧文案"give up"夸大了失效语义 */ + DBG_ERROR("sock%d relisten retry budget exhausted, pump takes over", i); continue; } /* 重试成功 */ diff --git a/Drivers/BSP/NET/net_socket.c b/Drivers/BSP/NET/net_socket.c index ea0ef756..23e337fd 100644 --- a/Drivers/BSP/NET/net_socket.c +++ b/Drivers/BSP/NET/net_socket.c @@ -254,9 +254,21 @@ static bool s_mac_is_zero(const uint8_t *p_mac) { /* 复位芯片 */ ch395f_reset(); - /* 芯片版本留痕:SOCKET_CLOSE 需 ≥0x46、TCP_SERVER 需 ≥0x44(§5.52 表), - * 版本门槛排查曾是盲区(Trap 25 定案过程确认本芯片 SOCKET_CLOSE 实际生效) */ - DBG_INFO("ch395f ver=0x%02X fun_para=0x08", ch395f_get_version()); + /* 芯片版本留痕 + 0-6 版本门控(N34 阶段0,§5.52 表):bit3 SOCKET_CLOSE + * 需芯片 ≥0x46、TCP_SERVER 需 ≥0x44。本台架芯片实测 ≥0x46(Trap 25 定案 + * SOCKET_CLOSE 实际生效);门控防批次漂移——版本不足则**拒置 bit3** + * (退回芯片自动关断语义)并 [ERR] 留痕,不静默带病运行 */ + uint8_t chip_ver = ch395f_get_version(); + /* FUN_PARA 实际下发值(版本不足 = 0,bit3 拒置) */ + uint8_t fun_para = 0x00U; + if (chip_ver >= 0x46U) { + fun_para = CH395F_FUN_PARA_SOCKET_CLOSE; + } else { + DBG_ERROR("ch395f ver=0x%02X < 0x46: SOCKET_CLOSE(bit3) unsupported, cleared", + chip_ver); + } + /* 版本 + 实际生效 FUN_PARA 留痕(旧日志硬编码 0x08 改为实际值) */ + DBG_INFO("ch395f ver=0x%02X fun_para=0x%02X", chip_ver, fun_para); /* 功能参数(§5.52 表):bit3=1 SOCKET_CLOSE——DISCONNECT/TIMEOUT 后芯片 * 不清缓冲,由 MCU 读出残留数据后再关 Socket(Trap 25 三次定案的排空契约); @@ -267,7 +279,7 @@ static bool s_mac_is_zero(const uint8_t *p_mac) { * V1C/V1E/V2.2/V2.3 的 bit0 全是保留位,官方例程 bit0=0 下 Socket 0~7 照常 * 可用、FTP 例程本身就传 0x08;证据链见 Trap 18「定案更正」, * 0x08 下的多 Socket 台架复测登记 N-31) */ - ch395f_set_fun_para(0x08); + ch395f_set_fun_para(fun_para); /* 设置 TCP MSS:维持 1024(512 裕量方案未采纳,当年支撑它的台架判据不可信, * 已随 TC-NET-604 结案为非缺陷一并撤回(2026-09-23,见 Trap 21)) */ @@ -1110,6 +1122,10 @@ int net_listen_locked(int sockfd) { status = ch395f_tcp_listen(sockfd); /* LISTEN 失败 → BUSY */ if (status != CH395F_ERR_SUCCESS) { + /* 0-3(N34 阶段0,U12 对称):open 已成而 LISTEN 失败必须 close, + * 否则芯片 Socket 卡"OPEN 无监听"、后续 open 恒返 USE(同 + * net_relisten_locked 对应分支) */ + ch395f_close_socket(sockfd); return NET_ERR_RETURN_1(NET_ERR_BUSY); } @@ -1905,7 +1921,8 @@ static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags, ch395f_read_recv_buf(sockfd, (uint8_t *)buf, (uint16_t)read_len); /* 累计接收字节统计 */ p_sock->recv_bytes += (uint32_t)read_len; - /* 已消费数据(注:部分读受 Trap 13 限制,下一次 RECV_OK 会重新置位) */ + /* 已消费数据(部分读安全:手册 §5.34 未读数据保留可续读,旧"Trap 13 + * 部分读归零"已降级为未证实历史观察;下一次 RECV_OK 重新置 readable) */ p_sock->readable = 0; /* 返回本次读出字节数 */ return read_len; @@ -1966,7 +1983,8 @@ static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags, * ① 数据通道(ESTABLISHED/UDP_OPEN)内 readable 门控 SPI 读——常见无数据路径零额外 SPI 事务 * (INT# 电平触发链保证缓冲有数据时每轮重读 SINT 重位置位) * ② 到点(与状态无关,旧自旋亦在任何状态下检查超时)→ NET_ERR_TIMEDOUT - * ③ CLOSED:先排空(Trap 13:FIN 可能先于数据)→ 有数据按数据完成,否则 EOF(0) + 自动重监听 + * ③ CLOSED:先排空(bit3=1 芯片断连后残数据留缓冲待 MCU 读——Trap 25 排空契约; + * 旧注归 Trap 13 已随其复核注降级)→ 有数据按数据完成,否则 EOF(0) + 自动重监听 * 其他状态(CONNECTING/LISTENING/…)保持等待状态迁移(CONNECT→ESTABLISHED 是合法路径; * 陈旧 resync 由显式取消处理)——不做状态推断取消,避免误杀正在建链的等待 * 看门狗兜底:到点后仍未了结 = 完成逻辑缺位 → 强制了结留痕 @@ -3386,7 +3404,7 @@ static void handle_disconnect_event(net_sock_t *p_sock) { /* Trap 22 / N-6:监听态收到 DISCONNECT——bit3=1(0x08)下芯片**不**关该 Socket(停 0x05/0x00,芯片 TCP 侧已 CLOSED);不重开则此后所有 SYN 全被拒 = 监听永久死亡; * 触发 = 断连后 <~1s 快速复连时 CONNECT 中断未上报(控制块滞留 LISTENING)后该连接断开(2026-09-23 生产固件复现); * N-32 P2:本分支**只回读留痕、不再 relisten** —— 重建统一交 - * net_listener_pump(每 50 ms 回读芯片状态,监听失位即重建)。原处直接 + * net_listener_pump(每 100 ms 回读芯片状态,监听失位即重建)。原处直接 * relisten 是 12 个所有者之一,且位于 DISCONNECT 事件路径,可能"抢先" * 于消费者 recv(P1 已消除其后果,P2 消除其可能) */ /* 回读缓冲(芯片状态 2 字节) */ @@ -3499,11 +3517,12 @@ static void handle_disconnect_event(net_sock_t *p_sock) { * 返回值:无 * 限定条件:net_poll() 检测到 RECV_OK 中断后调用;netTask 上下文 * 函数说明:1. 仅触发 NET_EVENT_DATA_RECEIVED 回调通知上层 - * 2. 不在此读取 recv_len,避免与 net_recv_locked 中的读取冲突(Trap 13) + * 2. 不在此读取 recv_len:单一读取点归 net_recv_locked(省一次 SPI + * 查询;Trap 13 时代的"读取冲突"顾虑已随复核注降级,结构保留) */ static void handle_recv_event(net_sock_t *p_sock) { /* RECV_OK 中断已触发,直接通知上层。不在此读 recv_len, - * 避免和 net_recv_locked 里面的 ch395f_get_recv_len 冲突(Trap 13) */ + * 单一读取点在 net_recv_locked(省 SPI;原 Trap 13 顾虑已降级) */ /* 置可读标志,供 net_select 在无 SPI 访问下判读 */ p_sock->readable = 1; /* 触发数据到达事件回调 */ diff --git a/test/board/ch395f_test_task.c b/test/board/ch395f_test_task.c index e43d6d85..af8af638 100644 --- a/test/board/ch395f_test_task.c +++ b/test/board/ch395f_test_task.c @@ -351,8 +351,8 @@ static void phase2_tc201(void) * 【掩码 (1U << (sock + 4)) 的由来】 * 全局状态的 bit0~3 被 4 个全局事件占用,因此 Socket N 的事件标志 * 固定从 bit4 开始排列 —— Socket N 对应第 (N + 4) 位: - * sock=0 → 1<<4 = 0x0010(CH395F_GINT_STAT_SOCK0) - * sock=1 → 1<<5 = 0x0020(CH395F_GINT_STAT_SOCK1) + * sock=0 → 1<<4 = 0x0010(Socket0 事件位,宏已删 N34-0-5) + * sock=1 → 1<<5 = 0x0020(Socket1 事件位,宏已删 N34-0-5) * ... * sock=7 → 1<<11 = 0x0800(CH395F_GINT_STAT_SOCK7) * `int_st & (1U << (sock + 4))` 非 0 From b8c6a3dab0bf28b7c8ec1201dcd0a6a3299f29f6 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 13 Oct 2026 06:44:26 +0800 Subject: [PATCH 06/24] =?UTF-8?q?refactor(net)!:=20N34=20=E9=98=B6?= =?UTF-8?q?=E6=AE=B51=20API=20=E9=9D=A2=E6=94=B6=E6=95=9B=EF=BC=88?= =?UTF-8?q?=E5=AF=B9=E8=B4=A6=E2=91=A4=20+=20=E5=88=A0=203=20=E4=B8=AA?= =?UTF-8?q?=E6=89=8B=E5=8A=A8=E8=87=AA=E6=84=88=20API=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGE: 删除公开 API net_relisten / net_resync_listen / net_can_accept 与消息类型 NET_MSG_RESYNC_LISTEN / NET_MSG_RELISTEN。 生产调用者为零(仅看门狗文档与 iec104 门控),应用侧无迁移成本; 如有外部消费者须改用对账/pump 自动机制(见下)。 1. 对账①扩展为①②⑤(F-NET-2 缺口,先补缺口再删 API) net_reconcile_tick 新增 bad=5:本层 ESTABLISHED 且芯片 0x05/0x01 (Socket OPEN + TCP LISTEN,静默回收回监听、不发 DISCONNECT) → RESYNC 语义只修本层(state→LISTENING + last_peer_port 转登 + lost_conn_seen 清零 + 取消挂起 recv),**不发任何芯片命令** (纪律同 pump③ U11,run8 实证重下发会 170s 永拒)。 原判据①要求 TCP==CLOSED、②要求本层 LISTENING,均不命中此形态 ——此前生产无人认领(仅手动 API 能修)。 2. 删 net_relisten(net_socket.c 函数 + 消息分派 + 头文件声明) footgun:对健康监听器无条件 open→USE→close 自毁 ~1s(原注释自认); 与 pump(100ms)/D10(30ms) 完全重叠。 3. 删 net_resync_listen(同上;语义并入第 1 条) 4. 删 net_can_accept(net_socket.c + net_socket.h + sim 三处) 只认 ESTABLISHED 的旧门控把"监听器在服役、待 CONNECT"窗口全挡掉, :2404 丢 CONNECT 探针因此从不运行;iec104 门控迁 net_listener_armed。 5. timeSync :8888 会话收尾不再 net_close(同 fd 续 accept,监听跨会话 常驻;原 close→重建每会话一个 Trap 09 沉降窗)。server 脚本注释同步 新契约(PC 侧 2s 宽限后自行关闭)。 6. lftpd 应用层阻塞重试退役:lftpd_inet_listen 5 轮(500ms/2000ms,最坏 10s 阻塞 ftpTask) → 单次 + 失败即 425;cmd_pasv 3×200ms 同删。 就绪屏障已消除 Trap 09 启动形态,客户端自带传输重试语义。 台架实证修正(本提交内二次修复,务必留痕): - net_listener_armed 补齐 **ESTABLISHED 放行**。首版迁移只放行 LISTENING/CLOSED+残留,漏了 ESTABLISHED=单连接模型的"待 accept"电平 → iec104 200ms 任务周期抢不过对账② 的 ~400ms 去抖,对账②先 standalone accept 转 ESTABLISHED 后 armed=0 → 应用整场不 accept,18/18 检查全 FAIL (设备日志证据:sock1 standalone accept 后无 i104 accepted,直至对端 FIN 才由 closed-with-data 救回)。补放行后 18/18 PASS,且耗时 127s→8.3s。 该态 accept 必返 fd,无 RPC 风暴风险。 - 期间顺带验证 M3 无辜:sock1 suspect DISCONNECT on live conn 命中后 defer→对账①→closed-with-data accept 把未 accept 的 RX 数据救回, 正是 M3 设计意图(资产门保护)。 验证(全绿):@build 0/0(Code=209580;含 CH395F 套件+Phase13 全量重编 0/0 的编译验证);@test PASS(回读无差异);iec104 18/18(8.3s); n30 0/50;:21 @0ms 50/50 p50=512;:7003 50/50 p50=517;ftp_regression 11/11;Trap23 RST 5/5 + FIN 5/5;time_sync :8888 连续推送正常(RTC 更新、 无 [ERR])。文档同步:BSD 指南(§3.3/3.6/10.1/10.2/12 + 变更记录)、 函数调用关系、监听生命周期设计说明、IEC104 上行设计说明、模拟器差距 清单、链路延迟测试说明。 门禁说明:doc-links 工作树 junction 伪影按既定口径 SKIP;mem-budget 分支 .map 漂移同前;cppcheck/coding-standard 手跑 Passed。 --- App/sync/time_sync.c | 40 +++- App/task/iec104_serv.c | 10 +- Drivers/BSP/NET/lftpd/lftpd.c | 11 +- Drivers/BSP/NET/lftpd/lftpd_inet.c | 58 +++-- Drivers/BSP/NET/net_socket.c | 224 +++++------------- Drivers/BSP/NET/net_socket.h | 70 ++---- Drivers/BSP/UI/ui_net_sink.h | 4 +- docs/子系统设计/IEC104上行设计说明.md | 4 +- .../网络Socket监听生命周期设计说明.md | 4 +- docs/测试说明/模拟器差距清单.md | 2 +- docs/测试说明/链路延迟测试说明.md | 2 +- docs/网络/BSD_Socket_API_使用指南.md | 51 ++-- docs/网络/BSD_Socket_API_函数调用关系.md | 14 +- simulator/sim/net_socket_sim.c | 21 +- test/board/net_latency_test.c | 49 +--- test/host/time_sync_server.py | 6 +- 16 files changed, 196 insertions(+), 374 deletions(-) diff --git a/App/sync/time_sync.c b/App/sync/time_sync.c index 2dc95070..2a043c3e 100644 --- a/App/sync/time_sync.c +++ b/App/sync/time_sync.c @@ -124,9 +124,11 @@ static int time_sync_open_listener(void) { * 入口参数:conn_sock - 已建立的连接 Socket 描述符(单连接模式下即监听 Socket 自身) * 返回值:无 * 函数说明:pending 读取 PC 发来的时间包("TIME"+uint32 大端 本地时间),解析后 - * 写回 RTC,然后关闭连接。每次等待 = 一次挂起请求(netTask 零占用, - * L3 pending 化),单次超时 = 剩余总预算(≤30s,语义与旧 30s 阻塞一致); - * 跨调用累积,容忍时间包被 TCP 分段送达 + * 写回 RTC。**不在此 close**(N34 阶段1-5:单连接模式下 conn_sock 即 + * 监听 fd,close 连监听一起毁、重建窗 ~0.3-1s;net 层 auto_relisten + * 已把同 fd 收回监听,外层直接续 accept——lftpd 样板)。每次等待 = + * 一次挂起请求(netTask 零占用,L3 pending 化),单次超时 = 剩余总预算 + * (≤30s,语义与旧 30s 阻塞一致);跨调用累积,容忍时间包被 TCP 分段送达 */ static void time_sync_handle_conn(int conn_sock) { uint8_t buf[16]; @@ -179,7 +181,10 @@ static void time_sync_handle_conn(int conn_sock) { } } - net_close(conn_sock); + /* N34 阶段1-5:原 net_close(conn_sock) 已删——单连接模式 conn==监听 fd, + * close 毁监听(AGENTS 模块约定:EOF 时应用层不得 close);net 层 + * auto_relisten 已把同 fd 收回 LISTENING,外层循环直接续 accept。 + * 模拟器多连接后端 conn≠ls,由调用方(外层)负责关数据 fd。 */ } /* @@ -187,8 +192,10 @@ static void time_sync_handle_conn(int conn_sock) { * 入口参数:argument - 未使用 * 返回值:无 * 限定条件:net_init() 已由 netTask 完成;netTask 正在处理消息队列 - * 函数说明:等待网络栈就绪后,循环:建监听 -> 等连接 -> 处理 -> 关闭 -> 重建监听。 - * 全部 socket 操作经消息队列由 netTask 串行执行,与本任务/其它网络任务并发安全。 + * 函数说明:等待网络栈就绪后,循环:建监听 -> 等连接 -> 处理 -> **同 fd 续 + * accept**(N34 阶段1-5:会话收尾不 close,监听跨会话常驻,仅网络 + * 代次变化时重建)。全部 socket 操作经消息队列由 netTask 串行执行, + * 与本任务/其它网络任务并发安全。 */ void time_sync_task(void *argument) { (void)argument; @@ -213,6 +220,9 @@ void time_sync_task(void *argument) { } } + /* 监听 fd 跨会话常驻(N34 阶段1-5):仅首次/网络代次变化/被毁时重建 */ + int ls = -1; + for (;;) { /* 心跳:本任务在 required_mask 内。以下每一处可能长于 100ms 抽样窗的 * 阻塞点(监听重试 2s、accept 轮询 200ms/次、收尾 50ms)都就近补心跳。 */ @@ -221,11 +231,13 @@ void time_sync_task(void *argument) { /* 记录本次监听对应的网络代次;代次变化(网络重建)则放弃旧监听重建 */ uint32_t gen = net_get_generation(); - int ls = time_sync_open_listener(); if (ls < 0) { - DBG_ERROR("time_sync: open listener FAIL, retry later"); - time_sync_delay_fed(2000U); - continue; + ls = time_sync_open_listener(); + if (ls < 0) { + DBG_ERROR("time_sync: open listener FAIL, retry later"); + time_sync_delay_fed(2000U); + continue; + } } /* 等待 PC 连接:net_accept 在无连接时立即返回 -1,循环重试同一监听 Socket */ @@ -235,6 +247,7 @@ void time_sync_task(void *argument) { if (conn < 0) { if (net_get_generation() != gen) { net_close(ls); /* 网络重建:旧监听失效,回外层重建 */ + ls = -1; break; } time_sync_delay_fed(200U); @@ -247,7 +260,12 @@ void time_sync_task(void *argument) { } time_sync_handle_conn(conn); - /* conn(单连接模式下即 ls)已被 net_close 释放,下次循环重建监听 */ + /* N34 阶段1-5:会话收尾不 close(单连接模式 conn==ls,close 连监听 + * 一起毁);net 层 auto_relisten 已把同 fd 收回 LISTENING,下轮循环 + * 直接续 accept。模拟器多连接后端(conn≠ls)才关数据 fd。 */ + if (conn != ls) { + net_close(conn); + } time_sync_delay_fed(50U); } diff --git a/App/task/iec104_serv.c b/App/task/iec104_serv.c index ab6d2ab7..ad14d399 100644 --- a/App/task/iec104_serv.c +++ b/App/task/iec104_serv.c @@ -189,7 +189,7 @@ static void s_service_link(uint32_t now) { * 函数功能:单次服务循环(代次自愈 → accept → 门控收 → 喂 link → 节拍) * 返回值:无 * 限定条件:NET_READY 已置位;s_cfg/s_link 已 init - * 函数说明:门控(net_can_accept/net_can_read 纯 RAM 读)+ 兜底节拍照 + * 函数说明:门控(net_listener_armed/net_can_read 纯 RAM 读)+ 兜底节拍照 * ui_net_sink I-1/I-2;EOF 时 client==listen 严禁 close(net 层 * 已 auto_relisten,close 会连监听一起毁) */ @@ -224,7 +224,13 @@ static void s_serv_poll(void) { if (s_client < 0) { int c; - if (net_can_accept(s_listen) == 0) { + /* N34 阶段1(F-NET-4):门控从 net_can_accept(只认 ESTABLISHED,已退役) + * 迁 net_listener_armed——旧门控把 LISTENING 窗口全挡掉,:2404 的丢 + * CONNECT 探针(net_accept_locked 内)因此从不运行(与已修的 :7003 + * 同型);armed=ESTABLISHED 待 accept / LISTENING / + * CLOSED+auto_relisten+残留(ESTABLISHED 放行为 2026-10-13 台架补齐: + * 漏它则本任务 200ms 周期抢不过对账② ~400ms 去抖、整场饿死) */ + if (net_listener_armed(s_listen) == 0) { if (net_get_sock(s_listen) == NULL) { s_listen = -1; /* 控制块消失:交回上方重建 */ } diff --git a/Drivers/BSP/NET/lftpd/lftpd.c b/Drivers/BSP/NET/lftpd/lftpd.c index 25765ddc..3fe6f9dd 100644 --- a/Drivers/BSP/NET/lftpd/lftpd.c +++ b/Drivers/BSP/NET/lftpd/lftpd.c @@ -462,19 +462,14 @@ static int cmd_pass(lftpd_client_t *client, const char *arg) static int cmd_pasv(lftpd_client_t *client, const char *arg) { - int listener_socket = -1; - int retry; - if (client->data_socket >= 0) { lftpd_inet_close(client->data_socket); client->data_socket = -1; } - for (retry = 0; retry < 3; retry++) { - listener_socket = lftpd_inet_listen(0); - if (listener_socket >= 0) break; - vTaskDelay(pdMS_TO_TICKS(200)); - } + /* N34 阶段1-6:原 3×200ms 应用层重试已删(lftpd_inet_listen 单次化)—— + * 失败直接 425,FTP 客户端自带传输重试语义;服务端不再叠阻塞重试 */ + int listener_socket = lftpd_inet_listen(0); if (listener_socket < 0) { send_simple_response(client->socket, 425, STATUS_425); return -1; diff --git a/Drivers/BSP/NET/lftpd/lftpd_inet.c b/Drivers/BSP/NET/lftpd/lftpd_inet.c index dadfd3af..4a764713 100644 --- a/Drivers/BSP/NET/lftpd/lftpd_inet.c +++ b/Drivers/BSP/NET/lftpd/lftpd_inet.c @@ -34,39 +34,35 @@ void lftpd_task_wd_beat(void) int lftpd_inet_listen(int port) { - int retry; - - for (retry = 0; retry < 5; retry++) { - lftpd_task_wd_beat(); /* 看门狗:每轮喂心跳(重试间隔 500ms 在 800ms 容差内) */ - int s = net_socket(NET_AF_INET, NET_SOCK_STREAM); - if (s < 0) { - if (retry == 0) DBG_ERROR("create socket"); - vTaskDelay(pdMS_TO_TICKS(500)); - continue; - } - - struct net_sockaddr_in addr; - addr.sin_family = NET_AF_INET; - addr.sin_port = net_htons((uint16_t)port); - addr.sin_addr.s_addr = 0; - - if (net_bind(s, (struct net_sockaddr *)&addr, sizeof(addr)) < 0) { - DBG_ERROR("bind port %d", port); - net_close(s); - return -1; - } - - if (net_listen(s) >= 0) { - return s; - } - - net_close(s); - if (retry < 4) { - vTaskDelay(pdMS_TO_TICKS(2000)); - } + /* N34 阶段1-6:应用层阻塞重试退役(原 5 轮 500ms/2000ms,最坏 10s 阻塞 + * ftpTask)——Trap 09 的启动形态已由就绪屏障消除(FTP_READY 等 NET_READY, + * lftpd_start 外层本就有监听失败重建循环);芯片忙窗失败属罕见,PASV 数据 + * 监听交给 FTP 客户端自身的重试语义(主流 FTP 服务器 passive listen 失败 + * 亦直接 425,不在服务端叠应用层阻塞重试) */ + lftpd_task_wd_beat(); /* 看门狗:入口喂心跳 */ + int s = net_socket(NET_AF_INET, NET_SOCK_STREAM); + if (s < 0) { + DBG_ERROR("create socket"); + return -1; } - DBG_ERROR("listen failed after 5 retries (total 10s wait)"); + struct net_sockaddr_in addr; + addr.sin_family = NET_AF_INET; + addr.sin_port = net_htons((uint16_t)port); + addr.sin_addr.s_addr = 0; + + if (net_bind(s, (struct net_sockaddr *)&addr, sizeof(addr)) < 0) { + DBG_ERROR("bind port %d", port); + net_close(s); + return -1; + } + + if (net_listen(s) >= 0) { + return s; + } + + DBG_ERROR("listen port %d failed", port); + net_close(s); return -1; } diff --git a/Drivers/BSP/NET/net_socket.c b/Drivers/BSP/NET/net_socket.c index 23e337fd..9c0ae885 100644 --- a/Drivers/BSP/NET/net_socket.c +++ b/Drivers/BSP/NET/net_socket.c @@ -542,9 +542,11 @@ int net_poll(void) { * 函数说明:1. 芯片在快速复连场景会丢 CONNECT/DISCONNECT 中断;丢事件后本层无法靠事件自愈 * 2. 典型:对端连接占着单连接监听的"后备"(芯片 TCP 仍 ESTABLISHED),后续 SYN 全被拒 * 3. 应用因未收到 DISCONNECT 仍以为有客户端,永不调用 accept,事件驱动自愈无路可走 - * 4. 故周期回读芯片状态,修正**两类**失同步: + * 4. 故周期回读芯片状态,修正**三类**失同步: * 5. ① 本层 ESTABLISHED 而芯片已无连接 → 按断开处理(只置事实;重建交 pump) * 6. ② 本层 LISTENING 而芯片已完成握手 → 补做 standalone accept(复用 handle_connect_event) + * 6b.⑤(N34 阶段1,F-NET-2)本层 ESTABLISHED 而芯片已静默回收回 LISTEN + * (0x05/0x01 无 DISCONNECT)→ RESYNC 只修本层,纪律同 pump③ U11 * 7. ⚠ N-32 P2(2026-10-12):原③(本层 LISTENING 丢监听)与④(孤儿监听) * 已**整体移交** `net_listener_pump` —— 本函数内不再有任何重建动作 * (单一所有者);认领门槛相应收窄为 ESTABLISHED/LISTENING @@ -581,7 +583,8 @@ static void net_reconcile_tick(void) { net_sock_t *p_sock = &s_net_socks[i]; /* 芯片侧状态两字节:[0]=socket 状态,[1]=TCP 状态 */ uint8_t st[2] = {0, 0}; - /* 失同步类型:1=连接已失 2=握手已成丢中断 3=监听已失 */ + /* 失同步类型:1=连接已失 2=握手已成丢中断 5=芯片静默回收回监听 + *(原 3/4=监听失位已移交 pump,编号不复用以避与历史口径混淆) */ int bad = 0; /* 对账认领范围:**仅 ESTABLISHED / LISTENING**。 @@ -614,6 +617,15 @@ static void net_reconcile_tick(void) { if (!((peer != 0U) && (peer == p_sock->last_peer_port))) { bad = 2; } + } else if ((p_sock->state == NET_SOCK_STATE_ESTABLISHED) && + (st[0] != CH395F_SOCKET_CLOSED) && + (st[1] == CH395F_TCP_LISTEN)) { + /* ⑤(F-NET-2 缺口,N34 阶段1):芯片静默回收——本层 ESTABLISHED + * 而芯片已自行把 Socket 收回 LISTEN(0x05/0x01,**不发 DISCONNECT + * 中断**,run8 台架实证)。判据①要求 TCP==CLOSED 不命中此形态、 + * 判据②要求本层 LISTENING 也不命中——此前生产无人认领(原仅手动 + * API net_resync_listen 能修且生产零调用)。→ bad=5 RESYNC 语义 */ + bad = 5; } /* ③④(本层 LISTENING 丢监听 / 本层 CLOSED 孤儿监听)已移交 * net_listener_pump —— N-32 P2 单一所有者,本函数不再做重建动作 */ @@ -643,6 +655,20 @@ static void net_reconcile_tick(void) { /* 复用断开状态机(含陈旧事件甄别 + 状态迁移 + 事件派发); * 只置事实,重建由 net_listener_pump 统一做(N-32 P2) */ handle_disconnect_event(p_sock); + } else if (bad == 5) { + /* 类型⑤:静默回收 → RESYNC 语义(原 NET_MSG_RESYNC_LISTEN 分派体 + * 原样并入,N34 阶段1;纪律同 pump③ U11:**不发任何芯片命令**—— + * 重下发 close/重监听会摧毁芯片侧健康监听器 → 端口永拒, + * run8 实证 170s 拒绝) */ + DBG_INFO("sock%d reconcile: chip re-listened w/o DISCONNECT (0x%02X/0x%02X) -> resync", + i, st[0], st[1]); + /* 仅修 MCU 侧;新连接的 CONNECT 中断会把两侧重新同步 */ + p_sock->state = NET_SOCK_STATE_LISTENING; + /* N-30(Trap 25):死连接对端端口转登幽灵甄别基准 + 去抖清零 */ + p_sock->last_peer_port = p_sock->remote_port; + p_sock->lost_conn_seen = 0U; + /* 取消本 fd 在死连接上的挂起 recv(必通知,防调用方挂死) */ + net_pending_cancel(i, NET_ERR_CONNRESET); } else { /* 类型②:补做 standalone accept */ DBG_INFO("sock%d reconcile: connection completed w/o CONNECT irq -> accept", i); @@ -783,47 +809,10 @@ void net_process_messages(void) { msg.result = net_accept_locked(msg.sockfd, p_addr, p_addrlen); break; } - case NET_MSG_RESYNC_LISTEN: { - /* 取控制块;NULL = fd 越界或已关闭 */ - net_sock_t *p_sock = net_get_sock(msg.sockfd); - if (p_sock == NULL) { - /* 描述符无效 → BADF,结果 -1 */ - net_set_errno(NET_ERR_BADF); - msg.result = -1; - } else if (p_sock->state != NET_SOCK_STATE_ESTABLISHED) { - /* 非 ESTABLISHED(LISTENING/CLOSED 等):两侧已一致,无需动作 */ - msg.result = 0; - } else { - /* 陈旧 ESTABLISHED:芯片已静默回收连接(无 DISCONNECT 中断, - * 2026-09-22 台架实证)→ 仅修 MCU 侧状态;严禁发芯片 - * close/重监听命令(会摧毁芯片侧仍健康的监听器 → 端口永拒, - * run8 实证 170s 拒绝);新连接的 CONNECT 中断会把两侧重新同步 */ - p_sock->state = NET_SOCK_STATE_LISTENING; - /* N-30(Trap 25):死连接对端端口转登幽灵甄别基准 + 去抖清零 */ - p_sock->last_peer_port = p_sock->remote_port; - p_sock->lost_conn_seen = 0U; - /* 取消本 fd 在死连接上的挂起 recv(必通知,防调用方挂死) */ - net_pending_cancel(msg.sockfd, NET_ERR_CONNRESET); - /* 仅修正 MCU 侧状态,不发芯片命令,直接返回成功 */ - msg.result = 0; - } - break; - } - case NET_MSG_RELISTEN: { - /* 描述符有效性检查(NULL = fd 越界或已关闭)→ 无效即 BADF; - * 有效则交 net_listener_try(它内部自行取控制块,本处不再持有指针, - * N-32 §10 入口收敛) */ - if (net_get_sock(msg.sockfd) == NULL) { - /* 描述符无效 → BADF,结果 -1 */ - net_set_errno(NET_ERR_BADF); - msg.result = -1; - } else { - /* 无条件芯片侧重监听:单次尝试,失败登记非阻塞重试 - * (芯片已在监听 → 下一轮到点再来;两种陈旧场景都收敛到"芯片监听器在位") */ - msg.result = net_listener_try(msg.sockfd); - } - break; - } + /* N34 阶段1(F-NET-3):NET_MSG_RESYNC_LISTEN / NET_MSG_RELISTEN 两个 + * 手动自愈消息已删——RESYNC 语义并入对账⑤(net_reconcile_tick), + * RELISTEN 与 pump/D10 完全重叠且对健康监听器是 footgun; + * 生产调用者本就为零(仅 test/board Phase 13,已改由自动机制接管) */ default: /* 未知消息类型(枚举闭合,正常不应触发):置通用错误码与结果 -1, * 保证通知值恒为本条消息的确定结果(无上一条残留) */ @@ -1429,26 +1418,10 @@ int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen) { return msg.result; } -/* - * 函数功能:查询监听 Socket 是否有可接受的连接(纯 RAM 读,不发起 RPC) - * 入口参数:sockfd - 监听 Socket 描述符 int - * 出口参数:无 - * 返回值:1 - 有连接可接受;0 - 无(含描述符无效) int - * 限定条件:任意任务上下文;net_init() 已调用 - * 函数说明:1. 判据与 net_accept_locked() 一致(单连接模型:监听 Socket 自身在 - * CONNECT 后转 ESTABLISHED),故"门控放行"与"accept 会成功"严格等价 - * 2. 只读控制块,不入队、不等通知 → 调用方(uiTask 等)不被 netTask 拖住 - * 3. 丢 CONNECT 中断由 net_reconcile_tick() 对账自愈,本函数不做自愈 - */ -int net_can_accept(int sockfd) { - /* Socket 控制块(纯 RAM 读,可能为 NULL = 描述符越界或未分配) */ - const net_sock_t *p_sock = net_get_sock(sockfd); - - if (p_sock == NULL) { - return 0; - } - return (p_sock->state == NET_SOCK_STATE_ESTABLISHED) ? 1 : 0; -} +/* N34 阶段1(F-NET-4):net_can_accept 已删除——它只认 ESTABLISHED,作为 accept + * 门控会把"监听器在服役但本轮还没 CONNECT"的窗口全挡掉(:2404 丢 CONNECT 探针 + * 因此从不运行,与已修的 :7003 探针失效同型);唯一生产调用者 iec104 已迁 + * net_listener_armed。net_can_read(RECV 门控)不受影响、保留。 */ /* * 函数功能:查询监听 Socket 是否处于"应该尝试 accept"的就绪态(纯 RAM 读,不发起 RPC) @@ -1457,8 +1430,9 @@ int net_can_accept(int sockfd) { * 返回值:1 - 就绪(LISTENING,或 CLOSED + auto_relisten = 断连待重建态); * 0 - 未就绪(fd 未分配 / 非监听型 / 正处隔离期以外的不就绪态) int * 限定条件:任意任务上下文;net_init() 已调用 - * 函数说明:1. N-32(P1 不变量 II):取代 net_can_accept()(要求 ESTABLISHED)作为 - * ui_net_sink 的 accept 门控。原门控下 :7003 永不进 net_accept → + * 函数说明:1. N-32(P1 不变量 II):取代已退役的 net_can_accept()(要求 + * ESTABLISHED,N34 阶段1 删除)作为监听 accept 门控。原门控下 + * :7003 永不进 net_accept → * net_lost_connect_probe(丢 CONNECT 中断补 accept)对该端口从不运行 * (设计文档 §2.3 末"探针失效") * 2. 为什么放行 LISTENING:探针与"closed-but-readable 接受"(:1338) @@ -1469,7 +1443,7 @@ int net_can_accept(int sockfd) { * 可能已被芯片接住(对账②)→ 放行让 net_accept_locked 的状态门 * 自行裁决(含 RX 有数据的 drain_pending 接受) * 4. 只读控制块,不入队、不等通知 → uiTask 不被 netTask 拖住 - * 5. 与 net_can_accept 语义差异是**有意的**:本函数表达"这个监听器 + * 5. 与已退役 net_can_accept 的语义差异是**有意的**:本函数表达"这个监听器 * 还在服役",而非"这一轮一定有连接" */ int net_listener_armed(int sockfd) { @@ -1479,6 +1453,15 @@ int net_listener_armed(int sockfd) { if (p_sock == NULL) { return 0; } + /* 连接已到、应用尚未 accept:单连接模型下 CONNECT 处理(net_poll 分派 / + * 探针补 accept / 对账② standalone accept)把监听 fd 转 ESTABLISHED, + * 这就是"待 accept"电平——**必须放行**(原 net_can_accept 唯一放行态; + * N34 阶段1 i104 台架实证:200ms 任务周期抢不过对账②的 ~400ms 去抖, + * 对账②先转 ESTABLISHED → 不放行则应用永不 accept、会话整场饿死)。 + * 无 RPC 风暴风险:此态下 accept 必返 fd,一次调用即离开本分支 */ + if (p_sock->state == NET_SOCK_STATE_ESTABLISHED) { + return 1; + } /* 正常监听态:放行。net_lost_connect_probe(丢 CONNECT 补 accept)与常规 * accept 都发生在 net_accept_locked 内部,门控挡在外面就永远到不了 */ if (p_sock->state == NET_SOCK_STATE_LISTENING) { @@ -3032,110 +3015,11 @@ int net_sock_state(int sockfd) { return (int)s_net_socks[sockfd].state; } -/* - * 函数功能:断连重同步(仅 MCU 侧状态,见头文件注释的适用场景) - * 入口参数:sockfd - Socket 描述符 int - * 出口参数:无 - * 返回值:0 - 已同步/原本即一致;-1 - 失败 - * 限定条件:连接已死且芯片已静默回收后调用(应用层判定) - * 函数说明:1. 适用:芯片已静默回收连接(无 DISCONNECT 中断)而本层状态滞留 ESTABLISHED - * 2. 状态收回 LISTENING 并重建监听,后续 accept 恢复正常 - */ -int net_resync_listen(int sockfd) { - /* 消息体(按值拷入队列,netTask 改的是其副本) */ - net_msg_t msg; - - /* 描述符越界 → BADF */ - if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { - return NET_ERR_RETURN_1(NET_ERR_BADF); - } - - /* 消息队列未创建(网络层未初始化)→ 通用错误 */ - if (netMsgQueueHandle == NULL) { - return NET_ERR_RETURN_1(NET_ERR); - } - - /* 消息类型:断连重同步(MCU 侧收回 LISTENING 并重建监听) */ - msg.type = NET_MSG_RESYNC_LISTEN; - /* 目标 Socket */ - msg.sockfd = sockfd; - /* 无超时(重同步立即完成) */ - msg.timeout_ms = 0U; - /* 调用方任务句柄(netTask 凭它回传回执) */ - msg.caller = xTaskGetCurrentTaskHandle(); - /* 结果占位(netTask 侧覆写实际结果) */ - msg.result = -1; - - /* 入队:队列满时阻塞调用方直至有空位 */ - if (osMessageQueuePut(netMsgQueueHandle, &msg, 0, osWaitForever) != osOK) { - /* 入队失败 → BUSY */ - return NET_ERR_RETURN_1(NET_ERR_BUSY); - } - - /* 阻塞等待 netTask 回执(两段式拾取,防丢失通知) */ - msg.result = net_wait_result("rsl", sockfd); - - /* 失败:恢复 errno(对外统一 -1) */ - if (msg.result < 0) { - return NET_ERR_RETURN_1(msg.result); - } - /* 重同步成功(0) */ - return 0; -} - -/* - * 函数功能:无条件重新监听(芯片侧 open+listen,含 Trap 09 失败重试) - * 入口参数:sockfd - Socket 描述符 int - * 出口参数:无 - * 返回值:0 - 成功;-1 - 失败 - * 限定条件:TCP 监听型 Socket;net_init() 已完成 - * 函数说明:1. 用于"断连后监听器可能已失位"的自愈场景(2026-09-22 run9 实证) - * 2. 一次性 net_listen 落在 Trap 09 窗口失败且被静默忽略 → 监听器永久丢失 - * 3. 后续连接全部拒绝 - * 4. 与 auto_relisten 内部路径同一实现(net_relisten_locked:单次尝试 + 非阻塞重试,见 net_listener_retry_tick) - * 5. 芯片侧已在监听时:首次 open 失败 → close 重建 → 监听短暂中断 ~1s - */ -int net_relisten(int sockfd) { - /* 消息体(按值拷入队列,netTask 改的是其副本) */ - net_msg_t msg; - - /* 描述符越界 → BADF */ - if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { - return NET_ERR_RETURN_1(NET_ERR_BADF); - } - - /* 消息队列未创建(网络层未初始化)→ 通用错误 */ - if (netMsgQueueHandle == NULL) { - return NET_ERR_RETURN_1(NET_ERR); - } - - /* 消息类型:无条件重新监听(含 Trap 09 非阻塞重试) */ - msg.type = NET_MSG_RELISTEN; - /* 目标 Socket */ - msg.sockfd = sockfd; - /* 无超时(重监听立即尝试) */ - msg.timeout_ms = 0U; - /* 调用方任务句柄(netTask 凭它回传回执) */ - msg.caller = xTaskGetCurrentTaskHandle(); - /* 结果占位(netTask 侧覆写实际结果) */ - msg.result = -1; - - /* 入队:队列满时阻塞调用方直至有空位 */ - if (osMessageQueuePut(netMsgQueueHandle, &msg, 0, osWaitForever) != osOK) { - /* 入队失败 → BUSY */ - return NET_ERR_RETURN_1(NET_ERR_BUSY); - } - - /* 阻塞等待 netTask 回执(两段式拾取,防丢失通知) */ - msg.result = net_wait_result("rel", sockfd); - - /* 失败:恢复 errno(对外统一 -1) */ - if (msg.result < 0) { - return NET_ERR_RETURN_1(msg.result); - } - /* 重监听成功(0) */ - return 0; -} +/* N34 阶段1(F-NET-3):手动自愈 API net_resync_listen / net_relisten 已删除—— + * RESYNC 语义并入对账⑤(net_reconcile_tick 自动认领"芯片静默回收回 LISTEN"形态, + * 200ms×2 去抖 + 1s 冷却沿用);RELISTEN 与 pump/D10 完全重叠且对健康监听器是 + * footgun(无条件 open→USE→close 自毁 ~1s,原注释自认)。生产调用者本就为零 + * (仅 test/board Phase 13,已改由自动机制接管)。历史实现见本文件 git 历史。 */ /* diff --git a/Drivers/BSP/NET/net_socket.h b/Drivers/BSP/NET/net_socket.h index d9f200dc..3915b3a0 100644 --- a/Drivers/BSP/NET/net_socket.h +++ b/Drivers/BSP/NET/net_socket.h @@ -37,8 +37,9 @@ typedef enum { NET_MSG_CONNECT, NET_MSG_LISTEN, NET_MSG_ACCEPT, - NET_MSG_RESYNC_LISTEN, /* 仅修 MCU 侧状态(ESTABLISHED→LISTENING),不发芯片命令 */ - NET_MSG_RELISTEN, /* 无条件芯片侧重监听(open+listen,含 Trap 09 重试) */ + /* N34 阶段1(F-NET-2/3):NET_MSG_RESYNC_LISTEN / NET_MSG_RELISTEN 已删—— + * 静默回收由对账⑤自动认领(net_reconcile_tick),重建由 pump/D10 单一 + * 所有者负责;手动自愈 API 面退役(生产调用者本就为零) */ } net_msg_type_t; typedef struct { @@ -150,48 +151,38 @@ int net_listen(int sockfd); */ int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen); -/* - * 函数功能:查询监听 Socket 当前是否有可接受的连接(**纯 RAM 读,不发起 RPC**) - * 入口参数:sockfd - 监听 Socket 描述符 int - * 出口参数:无 - * 返回值:1 - 有连接可接受;0 - 无(含描述符无效) int - * 限定条件:net_init() 已调用;任意任务上下文(含 uiTask 等实时性敏感任务) - * 函数说明:1. 只读 Socket 控制块状态,**不经消息队列、不等 netTask 回执** → - * 调用方不会被 netTask 的忙碌程度拖住(对比 net_accept: - * osMessageQueuePut(osWaitForever) + xTaskNotifyWait(portMAX_DELAY), - * 两者均**无上界**) - * 2. 判据与 net_accept_locked() 完全一致:单连接模型下监听 Socket 自身 - * 在 CONNECT 事件后转 ESTABLISHED,即"有连接可接受" - * 3. 用途 = **门控**:无连接时直接跳过 net_accept(),把"每轮一次无谓 - * RPC"降为 0(UI 推流 10 ms 轮询 = 100 msg/s 队列压力的根因) - * 4. 跨任务读单字:netTask 是唯一写者,最坏两种结果都无害—— - * 晚一轮发现(netTask 每 1 ms 就 net_poll 一次),或扑空 - * (net_accept 返回 -1 + NET_ERR_WOULDBLOCK) - * 5. 丢 CONNECT 中断由 net_reconcile_tick()(200 ms 对账 + 去抖)自愈, - * 本函数**不承担**自愈责任 - */ -int net_can_accept(int sockfd); +/* N34 阶段1(F-NET-4):net_can_accept 已退役——只认 ESTABLISHED 的旧门控会把 + * "监听器在服役、本轮待 CONNECT"窗口全挡掉(丢 CONNECT 探针因此从不运行); + * 监听 accept 门控统一用 net_listener_armed(),recv 门控 net_can_read() 保留。 */ /* * 函数功能:查询监听 Socket 是否处于"应该尝试 accept"的就绪态(**纯 RAM 读,不发起 RPC**) * 入口参数:sockfd - 监听 Socket 描述符 int * 出口参数:无 - * 返回值:1 - 就绪(LISTENING,或 CLOSED + auto_relisten = 断连待重建态);0 - 不就绪 int + * 返回值:1 - 就绪(ESTABLISHED 待 accept / LISTENING / CLOSED+auto_relisten + * +残留数据);0 - 不就绪 int * 限定条件:net_init() 已调用;任意任务上下文 - * 函数说明:1. N-32(P1 不变量 II):取代 `net_can_accept()`(只认 ESTABLISHED)作为 - * ui_net_sink 的 accept 门控——原门控使 :7003 永不进 `net_accept()`, + * 函数说明:1. N-32(P1 不变量 II,N34 阶段1 补齐 ESTABLISHED 放行):取代已退役的 + * `net_can_accept()`(只认 ESTABLISHED)作为监听 accept 门控——原门控使 + * :7003 永不进 `net_accept()`, * `net_lost_connect_probe`(丢 CONNECT 中断补 accept,N-30 实证的 * "数据被吞/425"主根因修复)对该端口等于从未运行 * 2. 放行 LISTENING:探针与 "closed-but-readable 接受" 都发生在 * `net_accept_locked` **内部**,门控挡在外面就永远到不了 + * 2b. 放行 ESTABLISHED(N34 阶段1 台架修正):单连接模型下 CONNECT 处理 + * (net_poll 分派 / 探针补 accept / 对账② standalone accept)把监听 fd + * 转 ESTABLISHED,这正是"待 accept"电平——漏放行则任务周期慢的消费者 + * (i104 200ms 抢不过对账② ~400ms 去抖)永不 accept、会话整场饿死 + * (2026-10-13 实测全 18 项 FAIL)。此态 accept 必返 fd,无风暴风险 * 3. 放行 CLOSED+auto_relisten **且 readable≠0**(有残留数据): * 保住 N-30 的 "closed-but-readable" 接受;**无残留时必须返回 0**, * 否则调用方会在整个 CLOSED 窗口内每轮发起 `net_accept`(RPC 无上界) * → 队列积压 → 喂不上狗(2026-10-12 P2a 台架实测 48 条 task stall)。 * 重建由 `net_listener_pump` 负责,调用方无需催促 - * 4. 与 `net_can_accept` 的语义差异**有意为之**:本函数回答"这个监听器 - * 这一轮**值不值得**发起一次 accept",而非"一定有连接" - * 5. 跨任务读单字,同 `net_can_accept` 的最坏情形论证 + * 4. 与已退役 `net_can_accept` 的关系:本函数 = 其 ESTABLISHED 放行态 + * ∪ LISTENING ∪ CLOSED+残留;回答"这个监听器这一轮**值不值得**发起 + * 一次 accept",而非"一定有连接" + * 5. 跨任务读单字,同 `net_can_read` 的最坏情形论证 */ int net_listener_armed(int sockfd); @@ -346,23 +337,10 @@ int net_get_errno(void); */ int net_sock_state(int sockfd); -/* - * 函数功能:断连重同步——仅把 MCU 侧状态对齐到 LISTENING,不发任何芯片命令 - * 适用场景:芯片"静默回收"(peer 发 FIN 后芯片自行把 Socket 回收回监听 - * 且不发 DISCONNECT 中断,2026-09-22 台架实证)→ MCU 状态滞留 - * ESTABLISHED。调用方须确信连接已死/芯片已回收(如会话超时中止后); - * 连接仍存活时严禁调用(会把活连接标记成监听态)。 - * 返回值:0 - 已同步(或原本即 LISTENING);-1 - 失败(描述符/队列无效) - */ -int net_resync_listen(int sockfd); - -/* - * 函数功能:无条件重新监听(芯片侧 open+listen,含 Trap 09 失败重试) - * 适用场景:断连后需确保监听器确定在位的自愈(监听短暂中断 ~1s,调用方 - * 的重连逻辑须容忍) - * 返回值:0 - 成功;-1 - 失败 - */ -int net_relisten(int sockfd); +/* N34 阶段1(F-NET-3):net_resync_listen / net_relisten 声明已删—— + * 静默回收形态由对账⑤自动认领(≤400ms 去抖 + 1s 冷却,纪律"不发芯片命令" + * 原样保留在认领动作内);监听重建由 pump(100ms 周期)/D10(30ms 重试) + * 单一所有者。勿再引入"应用层手动自愈"入口。 */ /* * 函数功能:将 IP 地址字符串转为网络字节序 diff --git a/Drivers/BSP/UI/ui_net_sink.h b/Drivers/BSP/UI/ui_net_sink.h index d5447b37..f5ca105d 100644 --- a/Drivers/BSP/UI/ui_net_sink.h +++ b/Drivers/BSP/UI/ui_net_sink.h @@ -1,4 +1,4 @@ -/* +/* * 模块名称:UI Network Sink (RemoDispBus 服务端) * 模块功能:实现 RemoDispBus 协议服务端,接收远端连接与导航按键,回送 1bpp 帧缓冲 * 适用平台:STM32F407ZGTx + CH395F 以太网 @@ -44,7 +44,7 @@ int ui_net_sink_start(uint16_t port); * 入口参数:无 * 返回值:无 * 限定条件:ui_net_sink_start() 已成功调用 - * 函数说明:1. 无客户端时先门控(net_can_accept,纯 RAM 读)再尝试接受连接 + * 函数说明:1. 无客户端时先门控(net_listener_armed,纯 RAM 读)再尝试接受连接 * 2. 有客户端时先门控(net_can_read)再用 DONTWAIT 收帧并解析 * (KEEPLIVE/INIT/KEY/LCDMEM);门控判为无数据时仍每 32 轮兜底收一次 * 3. 对端关闭或异常时复位客户端,等待下次重监听 diff --git a/docs/子系统设计/IEC104上行设计说明.md b/docs/子系统设计/IEC104上行设计说明.md index 1fd5690d..927f892e 100644 --- a/docs/子系统设计/IEC104上行设计说明.md +++ b/docs/子系统设计/IEC104上行设计说明.md @@ -130,7 +130,9 @@ - **单连接语义**(`AGENTS.md` 模块约定,抄 `ui_net_sink.c` 模板):2404 监听与 数据通道复用同一 socket(accept 返回即 listen fd);EOF 时 net 层 auto_relisten, 应用层**不得**关 fd(`client == listen` 分支);仅模拟器多连接后端(fd 不同)显式关。 -- **门控 + 兜底**:`net_can_accept`/`net_can_read`(纯 RAM 读)省无谓 RPC 往返; +- **门控 + 兜底**:`net_listener_armed`/`net_can_read`(纯 RAM 读)省无谓 RPC 往返 + (N34 阶段1:门控从只认 ESTABLISHED 的 `net_can_accept` 迁移——旧门控把监听 + 服役窗口全挡掉,丢 CONNECT 探针从不运行); 每 32 轮无条件 recv 一次防 RECV_OK 中断丢失(ui_net_sink I-1/I-2 先例)。 - **网络代次自愈**:`net_get_generation()` 变化 → 关旧 fd 重建监听(netTask NET_RECONFIG 重建路径的既有约定)。 diff --git a/docs/子系统设计/网络Socket监听生命周期设计说明.md b/docs/子系统设计/网络Socket监听生命周期设计说明.md index 18f92d38..bfc402ba 100644 --- a/docs/子系统设计/网络Socket监听生命周期设计说明.md +++ b/docs/子系统设计/网络Socket监听生命周期设计说明.md @@ -420,8 +420,8 @@ RAM 预算台账 `.md`,属另一在途任务)。这两个文件在主工作 | `net_accept_locked` | 产出的是**连接**(LISTENING→ESTABLISHED 迁移),属连接层;它只**调用**新模块 | | `net_lost_connect_probe` | 做的是"补 accept"(连接建立),不是重建监听 | | `net_listener_armed` | **双端公开 API**(sim 也实现);纯 RAM 查询,很薄,留在原处避免 sim 缺符号 | -| `net_resync_listen` | 公开 API + 消息分派;它**只改本层状态**,本就不调 relisten | -| `net_reconcile_tick` | ①② 是**连接**失同步自愈(③④ 已在 P2a 移交 pump) | +| ~~`net_resync_listen`~~ | **N34 阶段1 已删除**:其"只改本层状态"语义并入对账⑤(静默回收形态自动认领,纪律"不发芯片命令"随动作沉淀进 `net_reconcile_tick`),手动 API 面退役 | +| `net_reconcile_tick` | ①②⑤ 是**连接**失同步自愈(③④ 已在 P2a 移交 pump;⑤ 为 N34 阶段1 新增,吸收原 `net_resync_listen` 语义) | | `net_sock_t` 全部字段 | 数据归 `net_types.h`(双端共享核心),**不随模块走** | > ⚠ `net_sock_t` 的 `last_peer_port` / `lost_conn_seen` 是**跨层字段**:新模块的 diff --git a/docs/测试说明/模拟器差距清单.md b/docs/测试说明/模拟器差距清单.md index ea85b38e..7bb8905d 100644 --- a/docs/测试说明/模拟器差距清单.md +++ b/docs/测试说明/模拟器差距清单.md @@ -43,7 +43,7 @@ | N-1 | **无 pending 机制** | `net_accept`/`net_send` 直接失败或阻塞于宿主 socket;无"挂起队列"概念 | 异步 TCP 状态机,`accept`/`send` 可处于 pending,由 `net_poll` 推进 | **E-06**(accept 轮询计时失真)、**E-20**(lftpd 控制通道 errno 分类)、**E-21**(timeSync errno 分级)等在模拟器上无法复现 | `@test` + FTP 套件;N-4 二期专门建模 | | N-2 | **代次号恒为 0** | `net_get_generation()` 恒返 0(无"网络重建"概念) | 每次 `net_stack_bring_up` / 重配置递增,各 TCP 服务据此重建监听 | "网络重建后服务是否重建监听" 的逻辑**在模拟器上恒走同一分支** | `@test` 网络重配置用例(UI 改 IP 免重启生效) | | N-3 | **`net_process_messages` / `net_recv_to` 等缺失** | 未实现(MCU 专属:CH395F 消息队列驱动、带超时接收) | 由 `netMsgQueue` 串行化 SPI 事务、支持毫秒级超时接收 | 依赖"带超时接收"与"消息泵"的上层逻辑在模拟器上**不可编译/不可运行** | 板端 CH395F / NET 套件 | -| N-4 | **Socket 失效语义不同** | 以 `in_use` 判活(`net_socket_sim.c` 设计要点 4) | 监听 Socket 随客户端一同失效,需 `net_relisten` 显式重建 | "客户端断线 → 监听失效 → 重连" 的黏滞状态在模拟器上不出现 | `check_remo_sink.py` 覆盖重连;真机 `@test` | +| N-4 | **Socket 失效语义不同** | 以 `in_use` 判活(`net_socket_sim.c` 设计要点 4) | 监听 Socket 随客户端一同失效,由 net 层 `auto_relisten`/pump 自动重建(N34 阶段1 后无手动 `net_relisten`) | "客户端断线 → 监听失效 → 重连" 的黏滞状态在模拟器上不出现 | `check_remo_sink.py` 覆盖重连;真机 `@test` | | N-5 | **单连接 backlog** | `NET_SIM_BACKLOG = 1` | CH395F 多 socket 并发 | 多客户端并发场景(FTP + 推流 + timeSync 同时)不可在模拟器验证 | 板端 NET 套件 + 台架并发场景 | | N-6 | **非阻塞语义模拟** | 创建即 `FIONBIO`,使 `DONTWAIT` 语义与 MCU 对齐(设计要点 2) | 中断驱动 + 状态机,天然非阻塞 | ——(此项是**对齐**而非差距,登记以说明"为何模拟器能测非阻塞路径") | —— | diff --git a/docs/测试说明/链路延迟测试说明.md b/docs/测试说明/链路延迟测试说明.md index 7f0e587e..e7974a17 100644 --- a/docs/测试说明/链路延迟测试说明.md +++ b/docs/测试说明/链路延迟测试说明.md @@ -669,7 +669,7 @@ sock4 auto-relisten ok / relisten retry ok ← 监听器随后自行恢复 | **开机首个 FTP 会话第 2 个数据连接 425** | **D20**(已定位未修,§8.2) | 复现:`test/host/ftp_regression.py`(复位开边界);现场规避:客户端重试 425(瞬态 4xx);根治方向:数据 Socket 轮换 | | **`[LAT]` 多段数值雷同 / 全 0 却 verdict=OK** | **D18/D19**(测试工装) | 核对 `lat_probe_add_seg` 段名过滤 + `lat_time_init` 调用 + `med==0` 判无效 | | **连接建立后 15~30s 无流量即被 RST** | **D13**:芯片空闲超时(芯片行为) | 客户端须容忍并重连(peer `--wait` 重试是正常路径) | -| **peer 重连后 MCU 状态滞留、新连接字节被吞** | **D14**:芯片静默回收 Socket | `net_resync_listen`/`net_relisten` 自愈路径核对 | +| **peer 重连后 MCU 状态滞留、新连接字节被吞** | **D14**:芯片静默回收 Socket | 对账⑤自动认领(N34 阶段1:手动 `net_resync_listen`/`net_relisten` 已退役,Phase 13 abort 期自愈亦交 net 层) | | L8 超而 L7 正常 | 生产路径附加成本(uiTask 节拍/LVGL/日志) | 对照 L10/L11 与 §10.1(渲染已排除;per-recv 日志已删,S8 非阻塞日志仍待办) | | Δ 显著 >80ms | CH395F 栈净开销异常 | 回到 L1~L9 逐段排查 | diff --git a/docs/网络/BSD_Socket_API_使用指南.md b/docs/网络/BSD_Socket_API_使用指南.md index e31aeb94..95e7dd84 100644 --- a/docs/网络/BSD_Socket_API_使用指南.md +++ b/docs/网络/BSD_Socket_API_使用指南.md @@ -60,7 +60,7 @@ | `net_config.h` | 配置宏定义(最大 Socket 数、超时时间等) | | `net_types.h` | 类型定义(地址结构、Socket 控制块等) | | `net_socket.h` | BSD Socket API 头文件,含 `net_msg_t` 消息类型定义 | -| `net_socket.c` | API 核心实现:消息队列处理与线程安全封装、内部 `_sock()` 版本;另含 L3 pending 化 recv、Trap 22 周期对账(200ms,去抖+冷却,只剩①②) | +| `net_socket.c` | API 核心实现:消息队列处理与线程安全封装、内部 `_sock()` 版本;另含 L3 pending 化 recv、Trap 22 周期对账(200ms,去抖+冷却,①②⑤) | | `net_listener.c`(**真机专属**) | **监听生命周期**(N-32 §10 模块化):`net_listener_try` / `net_listener_clear` / `net_listener_retry_tick`(D10 重试 30ms/2s)/ `net_listener_pump`(唯一重建入口,100ms)/ `net_listener_settle`(沉降/隔离 100ms/400ms);内部头,模拟器不编译 | | `net_select.h` | select/poll API 头文件 | | `net_select.c` | select/poll 实现(仅 MCU 端编译) | @@ -153,7 +153,7 @@ sequenceDiagram | 规则 | 说明 | |------|------| -| **`netTask` 外(任意应用任务)** | 公共 API:`net_send()` / `net_recv()` / `net_recv_to()` / `net_accept()` / `net_connect()` / `net_close()` / `net_listen()`,及自愈 `net_relisten()` / `net_resync_listen()`——请求入队列,netTask 串行执行,调用方阻塞等回执 | +| **`netTask` 外(任意应用任务)** | 公共 API:`net_send()` / `net_recv()` / `net_recv_to()` / `net_accept()` / `net_connect()` / `net_close()` / `net_listen()`——请求入队列,netTask 串行执行,调用方阻塞等回执。**自愈无显式 API**(N34 阶段1:`net_relisten()` / `net_resync_listen()` 已退役,静默回收/监听失位由对账⑤与 pump 自动认领,见 §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()` 每轮先跑 **4 个非阻塞 tick**(D10 重监听重试 → L3 挂起 recv 完成 → Trap 22 对账 200ms → N-32 监听器 pump 100ms),再处理队列 | @@ -176,7 +176,7 @@ 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()` 按主循环节拍检查(①数据 ②到点 ③断连,看门狗 1s)并经任务通知唤醒调用方——等待期间调用方在自己上下文睡通知,netTask 零占用(消除 E-02 头阻塞)。副作用:`buf` 指针须保持有效至返回;close / 网络重建 / 陈旧重同步会**显式取消并通知**(`NET_ERR_CONNRESET` 等),调用方永不挂死 - **队列阻塞语义**:只有 `osMessageQueuePut` 失败(8 长度固定队列,极端情形)时公共 API 阻塞;recv 挂起等待**不计入**队列占用(完成前队列中无消息滞留) -- **哪些操作要求 netTask 已运行**:`net_listen()` / `net_accept()` / `net_connect()` / `net_send()` / `net_recv()` / `net_close()` / `net_relisten()` / `net_resync_listen()` 全经消息队列 → 只能在就绪屏障后的任务调用;`net_socket()` / `net_bind()` 为直接调用,但同样建议屏障后;**`net_can_accept()` / `net_can_read()` 是纯 RAM 读控制块**(§10.1),不入队、不等回执,**任意任务可安全周期调用**(用途 = 门控,把"无谓的 RPC"降为 0) +- **哪些操作要求 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 09 / 22 / 23) @@ -186,7 +186,7 @@ int net_close_sock(net_sock_t *p_sock); |---|---|---|---| | **`auto_relisten`** | 数据通道正常 EOF / DISCONNECT | 重开 + 重新 LISTEN,回 LISTENING;应用层无需 close/重 listen | `net_listen()` 置位;`net_close()` 清 | | **D10 非阻塞重监听重试** | 重开失败(Trap 09:芯片 close 后需沉降) | 单次尝试失败即登记待重试,每轮到点重试;超时限放弃并记 `relisten give up` | 重试间隔 30ms / 时限 2s(自首次尝试);同 fd 有在途挂起 recv 时跳过(L3 保险,防毁在途连接) | -| **Trap 22 周期对账** | 芯片丢 CONNECT/DISCONNECT 中断(快速复连);或静默回收 Socket(peer FIN,无 DISCONNECT 中断) | 每 200ms 对在用 socket 回读芯片状态,修正**两类**失同步:① 本层 ESTABLISHED 而芯片已断 → 按断开处理;② 本层 LISTENING 而芯片已握手 → 补 accept(③④ 已移交 pump) | 周期 200ms;**连续 2 次失配**才动手(去抖 ≈400ms)+ 动手后 **1s 冷却**(防"刚 open 未生效"瞬态重建风暴) | +| **Trap 22 周期对账** | 芯片丢 CONNECT/DISCONNECT 中断(快速复连);或静默回收 Socket(peer FIN,无 DISCONNECT 中断) | 每 200ms 对在用 socket 回读芯片状态,修正**三类**失同步:① 本层 ESTABLISHED 而芯片已断 → 按断开处理;② 本层 LISTENING 而芯片已握手 → 补 accept;⑤ 本层 ESTABLISHED 而芯片已静默回收回 LISTEN(0x05/0x01)→ RESYNC 语义只修本层、不发芯片命令(N34 阶段1 新增,原手动 API `net_resync_listen` 的语义并入;③④ 已移交 pump) | 周期 200ms;**连续 2 次失配**才动手(去抖 ≈400ms)+ 动手后 **1s 冷却**(防"刚 open 未生效"瞬态重建风暴) | | **N-32 监听器 pump**(**唯一重建入口**) | 监听型 fd 的芯片状态与本层不一致(会话结束待重建 / 本层状态滞后) | 每 100ms 检查监听型 fd:芯片失位即重建(`net_listener_try`);本层 CLOSED 而芯片**正在** LISTEN 时**只做状态重同步**、不发芯片命令 | 周期 100ms;连续 2 次失配(≈200ms)+ 动手后 1s 冷却;其它位置只置事实、不做重建动作 | | **Trap 23 排渣隔离** | 异常释放(芯片 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 时限**,否则监听被放弃) | @@ -875,20 +875,20 @@ int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen); > 单连接模式:监听 Socket 在 CONNECT 后**自身即数据通道**(§5.1)。`net_accept` 失败时若状态为 CLOSED 且 `auto_relisten` 开启,内部触发**非阻塞**重监听(失败交重试轮询,§3.6)。中断丢失也不会让监听死亡——对账在 200ms 内修正"本层 LISTENING 而芯片监听消失"等失同步。 -#### net_can_accept / net_can_read - 就绪门控(**纯 RAM 读,不走消息队列**) +#### net_listener_armed / net_can_read - 就绪门控(**纯 RAM 读,不走消息队列**) ```c -int net_can_accept(int sockfd); /* 1 = 有连接可接受;0 = 无 */ -int net_can_read(int sockfd); /* 1 = 应发一次 recv;0 = 可跳过本轮 */ +int net_listener_armed(int sockfd); /* 1 = 监听器在服役,值得发起 accept;0 = 跳过本轮 */ +int net_can_read(int sockfd); /* 1 = 应发一次 recv;0 = 可跳过本轮 */ ``` | 项 | 说明 | |------|------| | 代价 | **零 RPC**:只读 Socket 控制块(`net_get_sock()`),不入队、不等 netTask 回执 → 任意任务上下文可安全周期调用,**不被 netTask 忙碌程度拖住** | -| `net_can_accept` 判据 | `state == ESTABLISHED`,与 `net_accept_locked()` 严格等价(单连接模型:监听 Socket 自身在 CONNECT 后转数据通道) | +| `net_listener_armed` 判据 | `ESTABLISHED`(CONNECT 已处理、待应用 accept——单连接模型的"连接已到"电平,漏放行则任务周期慢的消费者永不 accept),或 `LISTENING`,或 `CLOSED + auto_relisten` **且 RX 有残留**(closed-with-data 救回窗)——表达"这个监听器还在服役、这一轮值不值得发起 accept",而非"一定有连接"。(前身 `net_can_accept()` 只认 `ESTABLISHED`,会把监听服役窗口全挡掉、丢 CONNECT 探针从不运行——N34 阶段1 已退役,其 ESTABLISHED 放行态并入本函数,勿再引入) | | `net_can_read` 判据 | `ESTABLISHED` → 看 `readable`;**非 `ESTABLISHED` 一律返 1**(必须让上层 recv 一次拿到 EOF/错误并复位自身状态,门掉它会把"连接已消失"永久藏住);描述符失效也返 1(让 recv 报 BADF 走既有错误分支) | | 用途 | **门控**:无活时跳过 `net_accept()`/`net_recv()`——两者内部都是 `osMessageQueuePut(osWaitForever)` + `xTaskNotifyWait(portMAX_DELAY)`,**均无上界**(队列深 8 且与 FTP/timeSync 共享) | -| ⚠ 不可作为唯一判据 | `readable` 只由 RECV_OK 中断置位,而 §3.6 的三类对账**不含**"芯片 RX 非空但 `readable=0`" → 中断丢失时 `net_can_read()` 恒返 0。**调用方必须保留低频兜底 recv**(`ui_net_sink.c` 每 32 轮无条件收一次,教训见 `docs/子系统设计/看门狗设计说明.md` §11.5 第 1 条) | +| ⚠ 不可作为唯一判据 | `readable` 只由 RECV_OK 中断置位,而 §3.6 的对账**不含**"芯片 RX 非空但 `readable=0`" → 中断丢失时 `net_can_read()` 恒返 0。**调用方必须保留低频兜底 recv**(`ui_net_sink.c` 每 32 轮无条件收一次,教训见 `docs/子系统设计/看门狗设计说明.md` §11.5 第 1 条) | | 竞态 | netTask 是唯一写者,单字读;最坏"晚一轮发现"(netTask 每 1 ms 就 `net_poll()`)或"扑空"(RPC 返回 `NET_ERR_WOULDBLOCK`),均无副作用 | | 模拟器端 | `net_socket_sim.c` 同名实现**恒返回 1**——Winsock 本就非阻塞、无 RPC 成本,恒 1 使门控在模拟器上逐位等价于改前行为 | @@ -979,21 +979,14 @@ int net_sock_state(int sockfd); 跨任务只读快照(单字节读);描述符无效/未占用返回 -1;值可能滞后 netTask 最新状态。用于**状态轮询**(客户端等 ESTABLISHED,§5.2),不替代 accept/connect。 -**net_resync_listen — 断连重同步(仅修 MCU 侧)** +**net_resync_listen / net_relisten — 已退役(N34 阶段1,勿再引入)** -```c -int net_resync_listen(int sockfd); -``` +两者曾是"应用层手动自愈"入口:`net_resync_listen()` 只修 MCU 侧状态(D14 静默回收),`net_relisten()` 无条件芯片侧 open+listen。现状: -适用场景(D14):对端 FIN 后芯片**静默回收** Socket 回监听且**不发 DISCONNECT 中断** → 本层滞留 ESTABLISHED,新连接字节被旧会话吞掉。须**确认连接已死**(如控制通道超时中止后)再调;只把本层状态改回 LISTENING 并取消本 fd 挂起 recv,**不发任何芯片命令**(发 close/重监听会摧毁芯片侧健康监听器 → 连接永拒)。返回 0=已同步(或原本即一致),-1=失败。注:Trap 22 周期对账同样能自愈合此失同步(≤200ms),显式 API 用于"不愿等下一 tick"的场景。 +- **静默回收**由对账⑤自动认领(§3.6:本层 ESTABLISHED 而芯片 0x05/0x01 → RESYNC 语义,≤400ms 去抖 + 1s 冷却,纪律"不发芯片命令"原样保留在认领动作内)——应用无需判定"连接是否已死",也就消除了"误判活连接调 resync 把活连接标成监听态"的 footgun。 +- **监听重建**由 pump(100ms 周期,唯一重建入口)/ D10(30ms 重试)单一所有者负责——`net_relisten()` 对健康监听器反而是 footgun(无条件 open→USE→close 自毁 ~1s,原注释自认)。 -**net_relisten — 无条件重新监听** - -```c -int net_relisten(int sockfd); -``` - -TCP 监听型 Socket:芯片侧 open+listen(D10 非阻塞重试)。芯片侧已在监听时:首次 open 失败 → close 重建 → 监听短暂中断 ~1s;两种情形都收敛到"监听器确定在位"。用途:代次号变化后显式重建(见 `docs/子系统设计/网络配置说明.md` §4)、应用判定连接已死需确定性监听。正常流无需(auto_relisten + 对账已覆盖)。返回 0 成功 / -1 失败。 +生产调用者本就为零(仅板级测试 Phase 13,已改由自动机制接管)。历史实现见 `net_socket.c` git 历史。 **net_getsockname — 获取 Socket 本地地址(IP+端口)** @@ -1011,14 +1004,14 @@ Socket 已创建或已绑定时可用;port 为 0(动态分配)时返回已 void net_process_messages(void); ``` -处理其他任务投递的请求(send/recv/connect/accept/close/**resync-listen / relisten**)。每次调用先跑 4 个**非阻塞** tick,再取队列消息(0 超时出队): +处理其他任务投递的请求(send/recv/connect/accept/close)。每次调用先跑 4 个**非阻塞** tick,再取队列消息(0 超时出队): 1. **D10 重监听重试**(`net_listener_retry_tick`):断连后监听器恢复的到点重试(30ms 间隔 / 2s 时限,§3.6) 2. **L3 挂起 recv 完成检查**(`net_pending_tick`):按 ①数据 ②到点 ③断连 检查每个挂起请求并通知调用方(看门狗 1s) -3. **Trap 22 芯片/本层对账**(`net_reconcile_tick`):200ms 周期修正**两类**失同步(①②,§3.6) +3. **Trap 22 芯片/本层对账**(`net_reconcile_tick`):200ms 周期修正**三类**失同步(①②⑤,§3.6) 4. **N-32 监听器 pump**(`net_listener_pump`):100ms 周期,**唯一重建入口**(③④;本层 CLOSED 而芯片在听时只重同步,不发芯片命令,§3.6) -> `net_msg_t.timeout_ms`:L3 分片预算(`net_recv_to()`),0=默认 `NET_RECV_TIMEOUT_MS`(30s)。消息类型含 `NET_MSG_RESYNC_LISTEN`(仅 MCU 侧状态)与 `NET_MSG_RELISTEN`(芯片侧重监听)。 +> `net_msg_t.timeout_ms`:L3 分片预算(`net_recv_to()`),0=默认 `NET_RECV_TIMEOUT_MS`(30s)。消息类型仅 6 种(send/recv/close/connect/listen/accept)——自愈类消息 `NET_MSG_RESYNC_LISTEN` / `NET_MSG_RELISTEN` 已随 API 退役删除(N34 阶段1)。 #### net_send_sock - 发送数据(内部版本) @@ -1289,10 +1282,9 @@ if (len < 0 && net_get_errno() == NET_ERR_WOULDBLOCK) { (2026-09-21 台架实证:`ui_net_sink` 曾在 EOF 时无条件 `net_close`,表现为 7003 答完一帧即断连、 端口随后"连接被拒"。) -**芯片静默回收(D14,2026-09-22 台架实证)**:对端发 FIN 后,芯片可能**自行把 Socket 回收回 LISTEN 且不发 DISCONNECT 中断** → 本层滞留 ESTABLISHED,应用继续往死连接上读写,新连接的字节被旧会话吞掉。两种处理: +**芯片静默回收(D14,2026-09-22 台架实证)**:对端发 FIN 后,芯片可能**自行把 Socket 回收回 LISTEN 且不发 DISCONNECT 中断** → 本层滞留 ESTABLISHED,应用继续往死连接上读写,新连接的字节被旧会话吞掉。 -1. **等对账自愈(默认,推荐)**:§3.6 对账 ≤200ms 发现"本层 ESTABLISHED 而芯片已回收" → 按断开处理 → 自动重监听;等待期间该端口实际可接新连接(芯片侧已 LISTEN),新连接经对账补 accept 被本层感知。 -2. **显式重同步(需立即对齐时)**:确认会话已死(如控制通道超时中止)后调 `net_resync_listen(fd)`——只修 MCU 侧状态 + 取消挂起 recv,**不发芯片命令**(发 close/重监听会摧毁芯片侧健康监听器 → 连接永拒)。 +**无需应用处理(N34 阶段1 起)**:对账⑤自动认领此形态(§3.6:≤400ms 去抖 + 1s 冷却)→ RESYNC 语义只修本层状态 + 取消本 fd 挂起 recv,**不发任何芯片命令**(发 close/重监听会摧毁芯片侧健康监听器 → 连接永拒,run8 实证 170s 拒绝)。等待期间该端口实际可接新连接(芯片侧已 LISTEN),新连接经对账补 accept 被本层感知。原显式 API `net_resync_listen(fd)` 已退役——"应用判定连接已死"这一步本身就是误判源(对活连接调用会把活连接标成监听态)。 ⚠ 切勿用**单次**芯片状态读决定重建:单次读可能落在"刚 open 未生效"瞬态,曾致监听反复重建直至 `relisten give up`(2026-09-23 实测风暴,三服务口全死)——任何重建动作必须去抖(连续确认)+ 冷却(§3.6)。 @@ -1380,7 +1372,7 @@ CH395F 底层驱动返回的状态码(`ch395f_get_cmd_status()` 或 `ch395f_po **问题:哪些操作要求 netTask 已运行?** -`net_listen()` / `net_accept()` / `net_connect()` / `net_send()` / `net_recv()` / `net_close()` / `net_relisten()` / `net_resync_listen()` 全部经消息队列 → 只能在**netTask 已运行的上下文**(就绪屏障后的服务任务)调用;内核启动前的代码禁调任何 FreeRTOS/CMSIS-RTOS API,网络监听建立天然在内核启动后。`net_socket()` / `net_bind()` 为直接调用,但同样建议在屏障后。 +`net_listen()` / `net_accept()` / `net_connect()` / `net_send()` / `net_recv()` / `net_close()` 全部经消息队列 → 只能在**netTask 已运行的上下文**(就绪屏障后的服务任务)调用;内核启动前的代码禁调任何 FreeRTOS/CMSIS-RTOS API,网络监听建立天然在内核启动后。`net_socket()` / `net_bind()` 为直接调用,但同样建议在屏障后。 **问题:消息队列由谁创建?在哪里创建?** @@ -1403,7 +1395,7 @@ CH395F 底层驱动返回的状态码(`ch395f_get_cmd_status()` 或 `ch395f_po | 场景 | 修复前症状 | 机制 | 生产验证(2026-09-23) | |---|---|---|---| -| **Trap 22** 快速复连丢中断 → 监听静默死亡 | 待机后 UI `:7003` 快速复连 2/10 且**永久**拒连;FTP `:21` 100ms 间隔自第 29 轮死亡;设备日志**无报错**(状态静默滞留) | 周期对账 `net_reconcile_tick()`(200ms,①②)+ 监听器 pump `net_listener_pump()`(100ms,**唯一重建入口**,N-32)+ 去抖(连续 2 次)+ 冷却(1s);`handle_disconnect_event` 只置沉降事实、不再自行重建 | `:7003` 10/10;`:21` 100ms/0ms/600ms 各 50/50;UI 20/20;FTP 正常 10/10(SHA-256);中止 RST/FIN 5/5 + 5/5;零重建风暴 | +| **Trap 22** 快速复连丢中断 → 监听静默死亡 | 待机后 UI `:7003` 快速复连 2/10 且**永久**拒连;FTP `:21` 100ms 间隔自第 29 轮死亡;设备日志**无报错**(状态静默滞留) | 周期对账 `net_reconcile_tick()`(200ms,①②⑤)+ 监听器 pump `net_listener_pump()`(100ms,**唯一重建入口**,N-32)+ 去抖(连续 2 次)+ 冷却(1s);`handle_disconnect_event` 只置沉降事实、不再自行重建 | `:7003` 10/10;`:21` 100ms/0ms/600ms 各 50/50;UI 20/20;FTP 正常 10/10(SHA-256);中止 RST/FIN 5/5 + 5/5;零重建风暴 | | **Trap 23** 中止后首笔复连上传被静默截断 ≤2048B 并误报 226 成功 | RST/FIN 中止上传 → 紧接着的首笔上传"收"到 ~2KB 残留后判结束回 226 → 客户端以为成功 = **静默数据损坏**(第二笔起正常) | 异常释放(TIMEOUT)预重监听隔离:`net_drain_chip_rx()` 分块排空 RX 残渣 + 主动 close + 排空中断 + 等 CLOSED/TCP_CLOSED(400ms 起 / 800ms 上限 < 2s 时限) | RST 10/10、FIN 5/5 全通过;正常路径 30/30 + 20/20,SHA-256 全匹配 | | **Trap 21**(TC-NET-604)"接收冻结"虚警 | 旧判读脚本把"对端填满 2KB RX 窗口"误判为 STALL/WEDGE | **结论:非缺陷**(芯片 RX 缓冲即窗口,对端填满是常态);判据改为脚本层 STEP + 客户端内容校验(RET SHA-256),虚警工具归档(`MDK-ARM/retracted_bench_20260922/`,README 禁重跑) | 真实 FTP 路径 227 轮全通过(8KB×10 … 1MB×2);并发 UI 流 600/600 CRC;设备零 FAIL/ERROR/WARN | @@ -1415,6 +1407,7 @@ CH395F 底层驱动返回的状态码(`ch395f_get_cmd_status()` 或 `ch395f_po | 日期 | 变更 | |------|------| +| 2026-10-13 | **N34 阶段1 API 面收敛**(审计见 `docs/网络/N34-自愈审计/README.md`):删除手动自愈 API `net_relisten()` / `net_resync_listen()`(生产零调用;后者语义并入对账**⑤**——本层 ESTABLISHED 而芯片 0x05/0x01 静默回收 → RESYNC 只修本层,§3.6 对账由两类扩为三类①②⑤;前者与 pump/D10 重叠且对健康监听器是 footgun)与 `net_can_accept()`(只认 ESTABLISHED 的旧门控挡死丢 CONNECT 探针,监听门控统一 `net_listener_armed()`,§10.1 门控节改写);`NET_MSG_RESYNC_LISTEN` / `NET_MSG_RELISTEN` 消息类型删除;timeSync 会话收尾不再 `net_close`(同 fd 续 accept,监听跨会话常驻);lftpd 应用层阻塞重试退役(就绪屏障已消除 Trap 09 启动形态) | | 2026-10-12 | **N-32 监听生命周期模块化 + 单一所有者**:监听生命周期抽为 `net_listener.c`(§2.2 文件表新增该行,真机专属);主循环自愈 tick 由 3 个增至 **4 个**(新增 `net_listener_pump`,100ms,**唯一重建入口**);`net_reconcile_tick` 对账收窄为 ①②,③④ 移交 pump;`handle_disconnect_event` / `handle_timeout_event` 改为 `net_listener_settle(GRACEFUL 100ms / QUARANTINE 400ms)` **只置事实**、不再自行重建;§3.3 / §3.6(四层自愈 → **五层**)/ §12.10 同步。权威设计见 `docs/子系统设计/网络Socket监听生命周期设计说明.md` §10 | | 2026-10-10 | 新增 **`net_can_accept()` / `net_can_read()` 就绪门控 API**(§10.1):纯 RAM 读控制块、**不走消息队列、不等 netTask 回执**,供实时性敏感任务在发 RPC 前判断"有没有活"(动因 = `ui_net_sink_poll()` 每 10 ms 一次 `net_accept` 把 uiTask 拖成最坏 ≈50 ms;原拟登记 N-19 / C7,当日结案不入登记簿;设计与实测见 `docs/子系统设计/看门狗设计说明.md` §11(原分析文档已并入该节));§3.5 补"netTask 运行要求"例外条;⚠ 记录两条使用约束——**不可作为唯一判据**(`readable` 只由 RECV_OK 中断置位,须保留低频兜底 recv)与**模拟器后端恒返 1**(`net_socket_sim.c`,Winsock 无 RPC 成本) | | 2026-09-23 | 同步 L3 pending 化、D10 非阻塞重监听、Trap 22 对账、Trap 23 排渣隔离后的现状:新增 `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(自愈行为数据与排障判据,含 Trap 21 结案) | diff --git a/docs/网络/BSD_Socket_API_函数调用关系.md b/docs/网络/BSD_Socket_API_函数调用关系.md index 250763f8..2c92f01c 100644 --- a/docs/网络/BSD_Socket_API_函数调用关系.md +++ b/docs/网络/BSD_Socket_API_函数调用关系.md @@ -49,9 +49,9 @@ flowchart TB NP["netPoll
INT# GPIO 轮询 → 全局中断 → 各 Socket SINT → 每轮一批"] T1["① netListenerRetryTick
D10 重监听重试:30ms / 2s 时限"] T2["② netPendingTick
L3 挂起 recv:①数据 ②到点 ③断连 ④看门狗 1s"] - T3["③ netReconcileTick
Trap 22 对账:200ms / 去抖 / 冷却(只剩①②)"] + T3["③ netReconcileTick
Trap 22 对账:200ms / 去抖 / 冷却(①②⑤)"] T4["④ netListenerPump
N-32 唯一重建入口:100ms / 去抖 / 冷却"] - DISP["队列分派 switch
SEND · RECV · CLOSE · CONNECT · LISTEN
ACCEPT · RESYNC_LISTEN · RELISTEN"] + DISP["队列分派 switch
SEND · RECV · CLOSE
CONNECT · LISTEN · ACCEPT"] subgraph LK["_locked / 内部分派目标(仅 netTask 上下文)"] LL["netListenLocked
初次监听(3×500ms 阻塞重试)"] LN["netConnectLocked
3×500ms 重试 + tcpConnect"] @@ -60,7 +60,6 @@ flowchart TB LA["netAcceptLocked
先 netPoll + 状态判定"] LC["netCloseLocked
了结挂起 + close + 等 CLOSED + 排空中断"] RLS["netListenerTry → netRelistenLocked
单次尝试 + Trap 23 排渣隔离"] - RSY["RESYNC 分支:改状态 + 取消挂起
(不发芯片命令)"] end EV["事件处理器
handleConnect · handleDisconnect
handleRecv · handleSend · handleTimeout"] ST["netListenerSettle
只置沉降/隔离事实(100ms / 400ms)"] @@ -71,11 +70,12 @@ flowchart TB PUB -->|入队| MQ MQ --> DISP - DISP --> LL & LN & LS & LR & LA & LC & RLS & RSY + DISP --> LL & LN & LS & LR & LA & LC & RLS NPM --> T1 & T2 & T3 & T4 & NP T1 --> RLS T3 -->|"① 已断"| EV T3 -->|"② 补 accept"| EV + T3 -->|"⑤ 静默回收:RESYNC 只修本层 + 取消挂起(不发芯片命令,N34 阶段1 吸收原 net_resync_listen 语义)"| RSY["reconcile⑤ RESYNC 动作"] T4 -->|"①② 芯片失位即重建"| RLS NP --> EV EV --> FE @@ -132,8 +132,8 @@ flowchart TB | `net_recv` | `NET_MSG_RECV`(timeout=0→默认 30s) | `net_recv_locked` | `net_poll` + `get_recv_len` / `read_recv_buf` | >0 / 0(EOF) / -1(errno = WOULDBLOCK / TIMEDOUT / CONNRESET) | 快路径:数据已在 → 立即读出返回(允许部分读);无数据 + 阻塞 → **登记挂起后立即返回**(L3,netTask 零占用);DONTWAIT + CLOSED → EOF + 触发重监听 | | `net_recv_to` | `NET_MSG_RECV`(timeout>0) | `net_recv_locked` | 同上 | >0 / 0 / -1 / **-2(本片超时无数据)** | E-02 头阻塞修复配套:调用方把长等待切成有界片(如 5ms/剩余预算),**总超时语义不变、单次占用有界**;`timeout_ms=0` 直接拒(会被解释成默认 30s,恰是长阻塞);超时**只凭 -2 判定、不得依赖 errno**(`s_net_errno` 是跨任务共享全局量,随时被别的任务覆盖) | | `net_close` | `NET_MSG_CLOSE` | `net_close_locked` | `close_socket` + 等 CLOSED(5ms 节奏 / ≤500ms)+ 排空 Socket/全局中断 | 0 / -1 | **先**了结本 fd 挂起 recv(排空/取消 + 必通知,关不得让等待者悬挂);关后排空残留 DISCONNECT(Trap 14/17:否则下一连接被"刚建立就断开"秒断);控制块整体清零 | -| `net_relisten` | `NET_MSG_RELISTEN` | `net_listener_try` → `net_relisten_locked` | `set_proto_type` + `set_sour_port` + `open_socket` + `tcp_listen`(单次尝试;沉降期另有 `close` + `get_socket_status` + 排空) | 0 / -1 | 无条件芯片侧重建:芯片侧已在监听 → 首次 open 失败 → close 重建(短暂中断 ~1s);失败**不阻塞**,登记 D10 重试;用途 = 代次变化后显式重建 / 判死后确定性重建;**-1 = "恢复中"**(重试轮询接管),不是永久失败 | -| `net_resync_listen` | `NET_MSG_RESYNC_LISTEN` | 分派内直接处理(**无芯片命令**) | 无 | 0 / -1 | D14 静默回收场景:芯片已自行把 Socket 回收回监听且**不发 DISCONNECT 中断** → 本层滞留 ESTABLISHED。只修 MCU 侧(state → LISTENING + 取消该 fd 挂起 recv);**严禁**发芯片 close/重监听——会摧毁芯片侧健康监听器 → 连接永拒(run8 实证 170s 拒绝)。非 ESTABLISHED 时视为已一致,返回 0 | +| ~~`net_relisten`~~ | ~~`NET_MSG_RELISTEN`~~ | **N34 阶段1 已删除**(消息类型同删) | — | — | 原"无条件芯片侧重建"与 pump/D10 完全重叠,且对健康监听器是 footgun(open→USE→close 自毁 ~1s);生产零调用。历史实现见 `net_socket.c` git 历史 | +| ~~`net_resync_listen`~~ | ~~`NET_MSG_RESYNC_LISTEN`~~ | **N34 阶段1 已删除**(语义并入对账⑤) | — | — | D14 静默回收(芯片自行回监听、不发 DISCONNECT)现由 `net_reconcile_tick` 判据⑤自动认领:本层 ESTABLISHED 且芯片 0x05/0x01 → 只修本层(state→LISTENING + 转登 `last_peer_port` + 清 `lost_conn_seen` + 取消挂起 recv),**不发芯片命令**(纪律原样保留,run8 实证 170s 拒绝);去抖/冷却沿用对账框架 | ### 4.4 直调 API(不经队列) @@ -193,7 +193,7 @@ flowchart TB | ② | 本层 CLOSED(会话已结束)而芯片**不在** LISTEN | 重建(`net_listener_try`) | 原对账④ | | ③ | 本层 CLOSED 而芯片**正在** LISTEN | **只做状态重同步**(CLOSED→LISTENING),**不发任何芯片命令** | U11 缺口 | -情形③的理由:芯片监听器是**健康**的,重下发 open+listen 会因 `USE` 错误走进关闭分支、把正听着的 Socket 关掉(U12 同源事故);只把本层状态收回 LISTENING 即可(与 `net_resync_listen` 同一纪律)。 +情形③的理由:芯片监听器是**健康**的,重下发 open+listen 会因 `USE` 错误走进关闭分支、把正听着的 Socket 关掉(U12 同源事故);只把本层状态收回 LISTENING 即可(与对账⑤ RESYNC 同一纪律——原 `net_resync_listen`,N34 阶段1 已退役)。 **去抖 + 冷却**(沿用原对账③④ 实测结论,取值不变):同一失配需**连续 2 次**(≈200ms)才动手;同一 Socket 动手后**冷却 1s**(防抖成风暴,2026-09-23 实证)。**周期取 100ms**(对账 200ms 的 1/2):比它快、又不至于像 50ms 那样频繁 close/open 与消费者收发撞车(2026-10-12 台架实测 50ms 会把 uiTask 最坏单轮从 172ms 推到 1034ms)。 diff --git a/simulator/sim/net_socket_sim.c b/simulator/sim/net_socket_sim.c index 47ce668f..db67b30e 100644 --- a/simulator/sim/net_socket_sim.c +++ b/simulator/sim/net_socket_sim.c @@ -335,28 +335,15 @@ int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen) { return nidx; } -/* - * 函数功能:查询监听 Socket 是否有可接受的连接(与 MCU 端同名 API) - * 入口参数:sockfd - 监听 socket 描述符 - * 返回值:1 - 可尝试 accept;0 - 描述符无效 - * 限定条件:socket 已创建 - * 函数说明:Winsock 后端无"零成本预知"手段(要嘛真调 accept、要嘛 select()), - * 而 accept 本就是非阻塞的、**没有 MCU 端的 RPC 成本** → 恒返回 1, - * 使上层门控在模拟器上等价于"改前行为"(逐位一致,不引入新语义) - */ -int net_can_accept(int sockfd) { - if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) { - return 0; - } - return 1; -} +/* N34 阶段1(F-NET-4):net_can_accept 双端退役——监听 accept 门控统一用 + * net_listener_armed()(本文件同名实现保留恒 1 语义),recv 门控 net_can_read() 保留。 */ /* * 函数功能:查询监听 Socket 是否处于"应该尝试 accept"的就绪态(与 MCU 端同名 API) * 入口参数:sockfd - 监听 socket 描述符 * 返回值:1 - 可尝试 accept;0 - 描述符无效 * 限定条件:socket 已创建 - * 函数说明:同 net_can_accept——Winsock accept 本无 RPC 成本 → 恒返回 1, + * 函数说明:Winsock accept 本无 RPC 成本 → 恒返回 1, * 使上层门控在模拟器上等价于"改前行为"。 * ⚠ MCU 端该函数额外放行 CLOSED+auto_relisten(单连接复用的断连待重建态), * 那是芯片 fd 别名模型的产物;Winsock 后端 fd 与监听器分离 @@ -374,7 +361,7 @@ int net_listener_armed(int sockfd) { * 入口参数:sockfd - socket 描述符 * 返回值:1 - 可尝试 recv;0 - 描述符无效 * 限定条件:socket 已创建 - * 函数说明:同 net_can_accept——Winsock recv 非阻塞、无 RPC 成本 → 恒返回 1, + * 函数说明:同 net_listener_armed 的双端口径——Winsock recv 非阻塞、无 RPC 成本 → 恒返回 1, * 上层门控在模拟器上不改变既有行为(含"每轮都 recv"的自愈特性) */ int net_can_read(int sockfd) { diff --git a/test/board/net_latency_test.c b/test/board/net_latency_test.c index f79efd77..a54e15da 100644 --- a/test/board/net_latency_test.c +++ b/test/board/net_latency_test.c @@ -585,9 +585,6 @@ enum { static uint8_t s_p13_state = P13_ST_HELLO; static uint8_t s_p13_done = 0U; /* 会话完成/中止:后续调用空操作 */ -static uint8_t s_p13_resync_done = 0U; /* abort 后首次重同步只执行一次 */ -static uint8_t s_p13_clean_done = 0U; /* 会话干净完成(全段 OK + 已发 0xFF) */ -static uint32_t s_p13_resync_tick = 0U; /* 最近一次(重)监听的时刻(周期自愈用) */ static uint8_t s_p13_hello_n = 0U; static int s_p13_verdict_all = LAT_VERDICT_OK; static int s_p13_seg_force_fail = 0; /* 当前段出现短写/错误 → 强制 FAIL */ @@ -732,9 +729,6 @@ static void p13_reinit(void) s_p13_seg_force_fail = 0; s_p13_verdict_all = LAT_VERDICT_OK; s_p13_done = 0U; - s_p13_resync_done = 0U; - s_p13_clean_done = 0U; - s_p13_resync_tick = HAL_GetTick(); s_p13_hello_n = 0U; lat_stat_reset(&s_p13_st); } @@ -763,43 +757,11 @@ void net_latency_phase13_run(int32_t sockfd) if (s_p13_state != P13_ST_ABORT) { return; /* 正常完成/DONE:会话终态,不再处理 */ } - /* 重同步(run7~run9 实证):peer 断连后芯片 Socket 去向不确定 - * (静默回收回监听 / RST 关闭 / 监听器丢失)→ 进 abort 时做一次 - * 分状态重同步 + 等待期周期性自愈: - * ESTABLISHED(陈旧)→ 先只修 MCU 侧状态(run8:close 会摧毁芯片 - * 侧健康监听器 → 连接永拒),再无条件芯片侧重监听(open+listen - * 含 Trap 09 重试;run9:一次性 net_listen 落 Trap 09 窗口失败 - * 且被忽略 → 监听器永久丢失); - * CLOSED → 芯片命令重新 LISTEN; - * LISTENING → 10s 无新连接则再重监听一次(自愈静默丢失的监听器; - * 重监听监听窗口中断 ~1s,peer 重连重试可吸收)。 */ - if (s_p13_resync_done == 0U) { - int st = net_sock_state(sockfd); - - s_p13_resync_done = 1U; - s_p13_resync_tick = HAL_GetTick(); - if (st == NET_SOCK_STATE_ESTABLISHED) { - DBG_INFO("[LAT] note: stale ESTABLISHED on abort -> resync + relisten"); - (void)net_resync_listen(sockfd); - (void)net_relisten(sockfd); - } else if (st == NET_SOCK_STATE_CLOSED) { - DBG_INFO("[LAT] note: CLOSED on abort -> re-listen via chip"); - (void)net_relisten(sockfd); - } - } else if (s_p13_clean_done == 0U) { - /* 周期自愈:10s 无新连接(状态仍 LISTENING)→ 再重监听一次; - * 新连接到来时状态已转 ESTABLISHED,本分支自然不再触发。 - * 干净完成(已发 0xFF、peer 正常退出)后不再自愈——否则每 10s - * 拆建一次监听器(无谓 SPI 开销 + 日志噪声 + 台架永不判空闲, - * run12 实证),此时候只需静候下一次 re-HELLO。 */ - uint32_t now = HAL_GetTick(); - if (net_sock_state(sockfd) == NET_SOCK_STATE_LISTENING && - (uint32_t)(now - s_p13_resync_tick) >= 10000U) { - s_p13_resync_tick = now; - DBG_INFO("[LAT] note: no peer in 10s -> relisten (self-heal)"); - (void)net_relisten(sockfd); - } - } + /* N34 阶段1(F-NET-3):原"进 abort 分状态手动重同步 + 10s 周期 relisten" + * 已删——三种去向全部由 net 层自动机制认领(run8 静默回收滞留 ESTABLISHED + * → 对账⑤ RESYNC;CLOSED 失位 → pump 100ms 认领重建;LISTENING 静默丢 + * 监听 → pump"芯片不在听"判据),手动 API net_resync_listen/net_relisten + * 已退役。此处只保留收包与 peer re-HELLO 探测。 */ n = net_recv(sockfd, s_p13_rx, (int)sizeof(s_p13_rx), NET_MSG_DONTWAIT); if (n > 0) { for (i = 0U; i + 4U <= (uint32_t)n; i++) { @@ -1142,7 +1104,6 @@ void net_latency_phase13_run(int32_t sockfd) "P13 latency baseline (verdict=%s)", p_name); DBG_INFO("[LAT] report complete"); TEST_REPORT("Phase 13"); - s_p13_clean_done = 1U; /* 已下发 0xFF:peer 正常收尾,停止周期自愈 */ s_p13_done = 1U; s_p13_state = P13_ST_ABORT; break; diff --git a/test/host/time_sync_server.py b/test/host/time_sync_server.py index 656d6561..9c8754fa 100644 --- a/test/host/time_sync_server.py +++ b/test/host/time_sync_server.py @@ -54,10 +54,12 @@ def main(): with socket.create_connection((mcu_ip, port), timeout=5) as sock: sock.sendall(pkt) - # 关键:发送后保持连接打开,等板子读完再关。 + # 关键:发送后保持连接打开,给板子留出读完 8 字节的窗口。 # 若立即 close,CH395F 收到 FIN 会丢弃接收缓冲里尚未被板子读出的 # 8 字节时间包,导致板子 net_recv 看到 CLOSED 而无数据、时间同步失败。 - # 这里 recv 直到板子主动关闭(EOF)或 2s 超时,确保板子先读到数据。 + # N34 阶段1-5 起板子会话收尾**不再 close**(单连接监听跨会话常驻, + # EOF 时 net_close 会连监听一起毁掉)——这里 recv 等 2s 宽限后由 + # 本端关闭;板子 net 层 auto_relisten 随即恢复监听,下一轮照常连接。 sock.settimeout(2.0) try: sock.recv(64) From d5bdc6eb5448656695254a4ef5c6bae5bcb7dc19 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 13 Oct 2026 06:46:40 +0800 Subject: [PATCH 07/24] =?UTF-8?q?docs(net):=20N34=20=E9=98=B6=E6=AE=B50/1?= =?UTF-8?q?=20=E5=AE=9E=E6=96=BD=E7=BB=93=E6=9E=9C=E4=B8=8E=E5=8F=B0?= =?UTF-8?q?=E6=9E=B6=E7=95=99=E7=97=95=EF=BC=88E10=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README:新增"实施状态"表(9ac9879/ae17d36/4c0a0af/b8c6a3d);§2 逐机制 裁决表补"✅ 已实施"与关键坑位索引 - 实验方案:新增 E10 —— 阶段 0/1 转正验证全记录,含阶段 1 首版台架失败的 根因链(armed 漏放行 ESTABLISHED → i104 整场饿死 18/18 FAIL → 修复后 18/18 PASS、127s→8.3s)、M3 无辜旁证、time_sync 连续 6 轮实测、 两处实施偏差(lftpd 未走 listener_try;阶段 0 提交合并) - 实施方案:阶段 0/§2、阶段 1/§3 标注完成与验证判据;P0 转正标注 - 盘点:F-NET-2/3/4/13/14 补实施结论;⚠ F-NET-4 补"门控迁移必须给 net_listener_armed 补 ESTABLISHED 放行"的坑位说明;net_listener_try 调用点计数 5→4;§2/§4 的缺口表述改闭环状态 - 顺带修正两条既有 doc-links 违规(实验方案 §4.4 → 实施方案 §4 第 4 项; 盘点 §5.29 → docs/芯片手册/CH395F.md §5.29),该 4 份文档 doc-links 现 Passed 无代码改动(纯文档)。 --- docs/网络/N34-自愈审计/README.md | 26 ++++++-- docs/网络/N34-自愈审计/net层自愈机制盘点.md | 18 ++++-- docs/网络/N34-自愈审计/实施方案.md | 22 +++++-- docs/网络/N34-自愈审计/实验方案.md | 71 ++++++++++++++++++++- 4 files changed, 120 insertions(+), 17 deletions(-) diff --git a/docs/网络/N34-自愈审计/README.md b/docs/网络/N34-自愈审计/README.md index 0c591611..90de3c41 100644 --- a/docs/网络/N34-自愈审计/README.md +++ b/docs/网络/N34-自愈审计/README.md @@ -8,6 +8,20 @@ > "静默回收"非 Server 模式条款)。全部证据带 文件:行号,HEAD `09d6195` 实测。 > 性质:纯审计,未改任何代码。本报告是裁决与路线,不是实施记录。 +## 实施状态(2026-10-13 更新) + +| 提交 | 内容 | 结果 | +|---|---|---| +| `9ac9879` | P0 止血包(M1+M2+M4-lite) | 台架验证(E7b/E8) | +| `ae17d36` | M3 矛盾态可疑门(风暴态免疫,资产门版) | E9/E9b 全绿 | +| `4c0a0af` | **阶段 0** 零风险清理 9 条 | E10:0/20 + 20/20 + 20/20 + 11/11 | +| `b8c6a3d` | **阶段 1** API 面收口(对账⑤ + 删 3 API + time_sync/lftpd) | E10:全绿回归(详见实验方案 E10) | + +阶段 0/1 已完成并转正;阶段 2/3(驱动原语 / net 层收敛)未开始。阶段 1 实施 +过程中的一条关键台架发现已留痕(实验方案 E10):**`net_listener_armed` 必须 +放行 ESTABLISHED**——单连接模型的"待 accept"电平,漏放行时 i104(200ms 周期) +抢不过对账② 的 ~400ms 去抖而整场饿死(18/18 FAIL → 修复后 18/18 PASS,127s→8.3s)。 + ## 0. 结论先行(直接回答本次审计的问题) **问:「芯片/本层状态对账与重监听自愈」可以简化或删除吗?可以内聚到 ch395f 驱动层吗?** @@ -50,19 +64,19 @@ T1(net 层视角)判"机制主体保留、API 可删";T3(主流对照视 |---|---|---|---| | `auto_relisten` | net_types.h:172 | **保留**(模型基石) | 芯片永不自动回 LISTEN,监听常驻是软件承诺(P4) | | D10 `net_listener_retry_tick` | net_listener.c:394-432 | **短期保留+小修;长期降级** | 小修=补 pump 同型状态门(F-NET-10)+ give-up 文案(F-NET-14);长期=驱动 `cmd_done()` 后内联进事务、删独立定时器(F-DRV-8) | -| Trap 22 `net_reconcile_tick` | net_socket.c:548-641 | **功能保留,形态重构** | 与 pump 判据合并为**单一 reconciler**、读驱动缓存、一套去抖(F-DRV-1);①扩展覆盖"本层 ESTABLISHED+芯片已回 LISTEN"缺口(F-NET-2,当前无自动机制认领) | +| Trap 22 `net_reconcile_tick` | net_socket.c:548-641 | **功能保留,形态重构**(阶段 1 已做①扩展=⑤) | 与 pump 判据合并为**单一 reconciler**、读驱动缓存、一套去抖(F-DRV-1);①扩展覆盖"本层 ESTABLISHED+芯片已回 LISTEN"缺口(F-NET-2)✅ 已实施(bad=5,`b8c6a3d`) | | N-32 `net_listener_pump` | net_listener.c:465-553 | **保留**(内聚的落点,不是补丁) | 唯一周期重建决策者=不变量 II;T3"改内联"与 P2a 台架实测冲突(删消费者驱动→`[WD] task stall`) | | Trap 23 排渣隔离 | net_listener.c:102-121/:149-204 | **下沉驱动** | 排空/等 CLOSED 是纯芯片机械;与 net_close_locked:2326-2345 是同一契约两套变体(F-NET-9/F-DRV-4)→ 收口 `ch395f_sock_reset(sn, mode, wait_ms)`,net 层只选 mode | -| `net_relisten` | net_socket.c:3080 | **删除** | 生产零调用;对健康监听器无条件 open→USE→close,自毁 ~1s(footgun) | -| `net_resync_listen` | net_socket.c:3026 | **删除**(先并入对账①) | 语义正确但载体错位:应是自动对账,不是手动 API(F-NET-2/F-MS-4) | -| `net_can_accept` | net_socket.c:1427 | **删除**(先迁 iec104 到 `net_listener_armed`) | 过期门控致 :2404 丢 CONNECT 探针从不运行(F-NET-4,与已修的 :7003 探针失效同型) | +| `net_relisten` | net_socket.c:3080 | **删除** ✅ 已实施(`b8c6a3d`) | 生产零调用;对健康监听器无条件 open→USE→close,自毁 ~1s(footgun) | +| `net_resync_listen` | net_socket.c:3026 | **删除**(先并入对账①)✅ 已实施(语义=对账⑤) | 语义正确但载体错位:应是自动对账,不是手动 API(F-NET-2/F-MS-4) | +| `net_can_accept` | net_socket.c:1427 | **删除**(先迁 iec104 到 `net_listener_armed`)✅ 已实施 | 过期门控致 :2404 丢 CONNECT 探针从不运行(F-NET-4,与已修的 :7003 探针失效同型)。⚠ 迁移时 **armed 必须补 ESTABLISHED 放行**,否则 i104 整场饿死(实验方案 E10) | | `net_lost_connect_probe` | net_socket.c:1228-1267 | **保留**;驱动持事实后去可去抖 | 丢 CONNECT 是芯片行为(裸机实证);去抖/冷却是对账世界的自偿成本(F-MS-9) | | L3 pending recv + 看门狗 | net_socket.c:2024-2190 | **保留** | 阻塞模型主体,与主流(FreeRTOS 事件组阻塞)同构 | | `session_dead` / `released_abnormal` / 幽灵甄别 / closed-with-data | net_types.h | **保留** | 芯片行为的必要建模,P1 不变量 I | | `net_listen_locked` 3×osDelay(500) / `net_close_locked` 500ms 忙等 | net_socket.c:1092-1104/:2326-2345 | **简化**(P2b) | 根在驱动阻塞原语(F-DRV-8);先补 `cmd_done()` 再统一走 D10 | | 上层每 32 轮盲收(U15) | ui_net_sink.c:552-558、iec104_serv.c:246-253 | **中期下沉** | "RX 非空但 readable=0"并入 reconciler 后上层退役(F-NET-12) | -| time_sync 每会话 close 监听重建 | time_sync.c:182 | **简化(上层)** | 违反自家"EOF 不得 close 监听 fd"铁律,主动制造 Trap 09 沉降窗(F-NET-13);改 lftpd 模式同 fd 回 accept | -| lftpd 应用层阻塞重试 | lftpd_inet.c:39-70、lftpd.c:473-477 | **删除/简化** | Trap 09 启动形态已被就绪屏障消除,重试理由不存在(F-DRV-9) | +| time_sync 每会话 close 监听重建 | time_sync.c:182 | **简化(上层)** ✅ 已实施(`b8c6a3d`) | 违反自家"EOF 不得 close 监听 fd"铁律,主动制造 Trap 09 沉降窗(F-NET-13);改 lftpd 模式同 fd 回 accept。实测连续 6 轮推送正常(E10) | +| lftpd 应用层阻塞重试 | lftpd_inet.c:39-70、lftpd.c:473-477 | **删除/简化** ✅ 已实施 | Trap 09 启动形态已被就绪屏障消除,重试理由不存在(F-DRV-9)。实施偏差:未走 `net_listener_try` 登记(会把 BOUND/D10 状态语义一并拖入),改单次尝试+失败即 425(E10 登记) | | 三处全局读清残留 | net_socket.c:1668/:2332、net_listener.c:176 | **收窄**(与驱动护栏合并做) | B3 已证"只读本 Socket SINT"可行;这三处仍在吞别的 Socket 边沿事件(F-NET-7) | ## 3. 目标架构(高内聚低耦合的落点) diff --git a/docs/网络/N34-自愈审计/net层自愈机制盘点.md b/docs/网络/N34-自愈审计/net层自愈机制盘点.md index a27c1a1c..112b25d9 100644 --- a/docs/网络/N34-自愈审计/net层自愈机制盘点.md +++ b/docs/网络/N34-自愈审计/net层自愈机制盘点.md @@ -47,7 +47,7 @@ | 中止时段的在途数据在 TIMEOUT **之后**才落进 RX 缓冲(~2KB 残渣被下条连接读走) | Trap 23 一阶段(RST 10/10 复现→修复 10/10) | QUARANTINE 400ms + 持续排空 | | 上一条连接残留 DISCONNECT 会在复用同 fd 的下一条连接 accept 后 10~20ms 投递 | Trap 23 二阶段(0ms/30ms 沉降无效、100ms 通过) | GRACEFUL 100ms(仅收过数据时登记) | | close 后重开 Socket 需沉降(PHY 形态 + 拆链形态) | Trap 09;Trap 25 台架(固定沉降 100~3000ms 均证伪"等就好") | D10 到点重试(而非固定延时) | -| 芯片可静默回收连接回监听且不发 DISCONNECT | run8 台架实证(当时 FUN_PARA=0x09,bit3 已=1,git 核实) | 现仅 `net_resync_listen` 覆盖——**自动机制存在覆盖缺口**(F-NET-2) | +| 芯片可静默回收连接回监听且不发 DISCONNECT | run8 台架实证(当时 FUN_PARA=0x09,bit3 已=1,git 核实) | **阶段 1 起由对账⑤自动认领**(原仅 `net_resync_listen` 覆盖 = F-NET-2 缺口,已闭环) | | 全局中断寄存器整体读即清、无按位清除;SINT 粘滞到被读 | 手册 §5.15;Trap 14 | net_poll 集中读清纪律;也解释了为何"丢弃式读全局"有害(F-NET-7) | **(b) 早期固件 bug(寄存器数值/引用/时序误用)——根因已修,评估补丁是否仍必要:** @@ -66,16 +66,16 @@ | 缺口 | 现状 | |---|---| -| 重建多所有者(曾 12 个) | P2a pump 收敛周期性路径;消费者驱动 3 处经台架定案保留;**显式 API 2 处(RELISTEN/RESYNC)与 net_can_accept 旧门控是残余冗余**(F-NET-2/3) | +| 重建多所有者(曾 12 个) | P2a pump 收敛周期性路径;消费者驱动 3 处经台架定案保留;显式 API 2 处(RELISTEN/RESYNC)与 net_can_accept 旧门控**已于阶段 1 删除**(F-NET-2/3 ✅) | | EOF 边沿化(抢先 relisten 吞 EOF) | P1 `session_dead` 电平化已消解 | | 上层直接读 net 层状态镜像 | `lftpd_inet_accept_timeout` 直读 `p_sock->state`(lftpd_inet.c:117)、`net_listener_armed` 用 `readable` 代理"RX 可能有残留"(net_socket.c:1480-1485)、`net_can_read` 自认"对账不含 RX 非空但 readable=0"(:1500-1501)迫使上层保留每 32 轮兜底 recv(ui_net_sink.c:552-558、iec104_serv.c:246-253)——**第三层兜底散在应用层**(F-NET-12) | -| 应用层仍是重建所有者 | time_sync 每会话 close 监听 fd 全量重建(time_sync.c:182,违反"EOF 不得 close 监听 fd"铁律,设计文档第 7.4 节已点名);iec104 `s_drop_conn` close 监听重建(协议违例场景,有意例外)(F-NET-13) | +| 应用层仍是重建所有者 | ~~time_sync 每会话 close 监听 fd 全量重建~~(**阶段 1 已修** `b8c6a3d`:同 fd 跨会话常驻);lftpd 应用层阻塞重试已删(阶段 1);iec104 `s_drop_conn` close 监听重建(协议违例场景,有意例外)(F-NET-13) | | fd 别名(监听=连接) | 硬件上限(多连接模式不支持多监听,D1 出局),不可消解;只能靠不变量 I/II 约束 | ## 3. 编号发现 **F-NET-1 · 重建入口实测 5 处,与设计文档定案一致;"唯一重建入口"仅指周期性** -证据:`net_listener_try` 调用点共 5——net_listener.c:552(pump,唯一周期性)、net_socket.c:811(NET_MSG_RELISTEN 显式消息)、:1327(accept CLOSED 分支)、:1938(recv DONTWAIT EOF 分支)、:2162(pending_tick ③)。后三处为"消费者动作触发",P2a 台架实测删除会把 CLOSED 窗口拉长到 pump 周期+隔离期并触发 `[WD] task stall`(设计文档第 9 节 P2a 修正记录)。 +证据:`net_listener_try` 调用点共 4(阶段 1 删 `NET_MSG_RELISTEN` 后)——net_listener.c:552(pump,唯一周期性)、net_socket.c:1327(accept CLOSED 分支)、:1938(recv DONTWAIT EOF 分支)、:2162(pending_tick ③)。后三处为"消费者动作触发",P2a 台架实测删除会把 CLOSED 窗口拉长到 pump 周期+隔离期并触发 `[WD] task stall`(设计文档第 9 节 P2a 修正记录)。 影响:这不是缺陷,是**已实测裁决的重叠**;但"唯一重建入口"的表述在 BSD 指南与注释中易被读成"仅 pump 一处",误导后续维护者删消费者驱动路径。 建议:保留现状;文档口径统一为"唯一**周期性**入口 + 三处消费者驱动"。 @@ -83,16 +83,20 @@ 证据:对账①判据要求芯片 SOCKET_CLOSED 或 TCP_CLOSED(net_socket.c:588-589),芯片回 LISTEN(0x05/0x01)时不命中;对账②要求本层 LISTENING(:591);pump 只裁本层 LISTENING/CLOSED(net_listener.c:509-513);`handle_connect_event` 对本层 ESTABLISHED 的新 CONNECT 中断两个分支都不命中、静默丢弃(net_socket.c:3306 起)。该场景为芯片真实行为(run8 实证"芯片静默回收、监听器健康",git 核实当时 FUN_PARA=0x09 即 bit3=1)。现仅 `net_resync_listen`(net_socket.c:774-799)能修,而其生产调用者为零(仅 net_latency_test.c:783)。 影响:一旦生产出现"静默回收 + 无 DISCONNECT",端口将滞留假 ESTABLISHED,新连接被芯片接住但本层无感知,直到下一次应用 close 或网络重建。 建议:把该形态并入对账①——本层 ESTABLISHED 且芯片 TCP=LISTEN → 按 RESYNC 语义只改本层状态(不发芯片命令,纪律同 pump③ U11 与 :785-788 注释);并入后 `net_resync_listen` API 可删(见 F-NET-3)。 +**✅ 已实施(阶段 1,`b8c6a3d`)**:对账判据新增 **bad=5**(本层 ESTABLISHED 且 `st[0]!=SOCKET_CLOSED && st[1]==TCP_LISTEN`)→ RESYNC 语义(state→LISTENING + `last_peer_port` 转登 + `lost_conn_seen` 清零 + `net_pending_cancel(CONNRESET)`),不发任何芯片命令;手动 API 同批删除。台架验证见实验方案 E10。 **F-NET-3 · `net_relisten` / `net_resync_listen` / `net_can_accept` 三个 API 是冗余或过期面** 证据:`net_relisten`(net_socket.c:3080)与 `net_resync_listen`(:3026)生产零调用(grep 全仓仅 test/board/net_latency_test.c:776-801);`net_relisten` 对健康监听器无条件 open→USE→close,自认造成 ~1s 监听中断(:3078 注释),与 pump③"芯片在听绝不重下发"的 U12 教训直接冲突。`net_can_accept`(:1427)唯一生产调用者 iec104_serv.c:227。 影响:公开 API 面越大,上层"手动自愈"路径越多,正是用户怀疑的"补丁式代码难维护"的来源;`net_relisten` 是 footgun。 建议:`net_relisten` 删除(台架 Phase 13 改用 pump 语义或测试专用内部入口);`net_resync_listen` 在 F-NET-2 并入对账后删除;`net_can_accept` 在 F-NET-4 迁移后删除。 +**✅ 已实施(阶段 1,`b8c6a3d`)**:三 API 与 `NET_MSG_RELISTEN` / `NET_MSG_RESYNC_LISTEN` 消息类型全部删除(MCU + sim 双端),生产/测试调用者归零;Phase 13 的自愈段交回 net 层自动机制。**⚠ 遗留铁律**:门控迁移必须给 `net_listener_armed` 补 ESTABLISHED 放行,见 F-NET-4 实施注。 **F-NET-4 · iec104(:2404)仍用过期门控 `net_can_accept` → 丢 CONNECT 探针对该端口从不运行** 证据:iec104_serv.c:227 以 `net_can_accept`(只认 ESTABLISHED,net_socket.c:1434)门控 accept;`net_lost_connect_probe` 只在 `net_accept_locked` 内 LISTENING 态运行(:1298-1300),门控挡在门外则永不触发——与 N-32 P1 为 :7003 修复的"探针失效"完全同型(ui_net_sink.c:521-526 注释记载原委)。:2404 的丢 CONNECT 只剩对账②兜底,而②带 400ms 去抖 + 1s 冷却(Trap 25 已实证该节奏对短命监听器"救不全")。 影响:104 上行端口的丢中断恢复被拖慢到秒级;三个常驻监听里 :21/:8888 无条件轮询 accept、:7003 已换 `net_listener_armed`,唯独 :2404 漏换。 建议:iec104 门控换 `net_listener_armed`(双端 sim 已有实现);随后 `net_can_accept` 可退役,消除"两个相似门控语义不同"的长期困惑。 +**✅ 已实施(阶段 1,`b8c6a3d`)**:门控已迁 `net_listener_armed`,`net_can_accept` 三处(MCU / 头文件 / sim)删除。 +**⚠⚠ 实施中发现的坑(必读,实验方案 E10 有完整证据链)**:`net_listener_armed` 首版只放行 LISTENING / CLOSED+残留,**漏放行 ESTABLISHED**——而单连接模型下"连接已到、应用未 accept"的电平正是 ESTABLISHED(CONNECT 处理把监听 fd 自身转数据通道),也就是旧 `net_can_accept` 唯一放行的状态。后果:i104 200ms 任务周期抢不过对账② 的 ~400ms 去抖,对账②先 standalone accept 转 ESTABLISHED → armed=0 → 应用整场不 accept,**iec104 18/18 全 FAIL**(127s 跑满超时;设备日志:`sock1 standalone accept` 后整场无 `i104: accepted`,直至对端 FIN 才由 closed-with-data 救回)。ui_net_sink 之所以不受影响,是 10ms 周期 + `net_accept_locked` 自带 `net_poll` 通常在状态翻转前就拿到 fd——**属竞态幸存,不是设计正确**。修复:armed 放行 ESTABLISHED(此态 accept 必返 fd,一次调用即离开,无 RPC 风暴风险);修复后 18/18 PASS 且耗时 127s→8.3s。 **F-NET-5 · 两套重监听重试策略并存:D10 非阻塞 vs listen 路径 3×osDelay(500) 阻塞** 证据:net_listener.c:71-79(D10,注释明言旧 osDelay(500) 忙等致全网冻结 500ms、实测恢复 588ms);但 net_socket.c:1093-1104 `net_listen_locked` 仍是 open 失败→close→osDelay(500)→重试 ×3(最坏 netTask 冻结 1.5s)。设计文档 P2b 已登记该忙等并**台架依据暂缓**。 @@ -138,17 +142,19 @@ 证据:time_sync.c:182 `net_close(conn_sock)`(单连接下 conn==listen)后外层循环 socket+bind+listen 重建(:224);设计文档第 7.4 节契约表已点名"违反『EOF 不得 close 监听 fd』→ close→重建沉降窗(Trap 09)+ 最坏 ~10s 端口下线"。对照正面样板:lftpd 控制通道明确不 close(lftpd.c:746-750/826-827)、iec104 EOF 分支带 `s_client != s_listen` 护栏(iec104_serv.c:259-262)。 影响::8888 每次会话结束都主动制造一次 Trap 09 沉降窗;也是"应用层还能毁监听器"的活例。 建议:裁决"简化(上层)":time_sync 会话收尾不 close,直接回外层在同一 fd 上 accept(lftpd 模式);iec104 `s_drop_conn` 的 close 属协议违例强制断连,语义正当,保留但注释标注为"有意例外"。 +**✅ 已实施(阶段 1,`b8c6a3d`)**:`net_close(conn_sock)` 删除,`ls` 提到外层循环跨会话常驻(仅网络代次变化/被毁时重建),`conn != ls`(模拟器多连接后端)才关数据 fd。台架实测连续 6 轮推送正常(周期 ~4s),每轮走 `standalone accept → DISCONNECTED, releasing → settle → auto-relisten ok`,零 `[ERR]`。契约变化已同步 `test/host/time_sync_server.py`(板子不再先关,PC 侧 2s 宽限后自关);⚠ 强杀 PC 进程会走芯片 `[ERR] TIMEOUT` + QUARANTINE 沉降(异常收尾,非正常路径)。 **F-NET-14 · D10 的"2s 放弃"不是终态,`relisten give up` 日志误导排障** 证据:net_listener.c:414-419 到期清队列 + DBG_ERROR;但 pump 在冷却后看到"芯片不在听"会再次 `net_listener_try`(net_listener.c:538-552),监听器实际会无限期重试(每 ~1.2s 一轮)。 影响:日志出现 give up 时端口并非永久死亡,排障者可能误判严重度;同时"无限重试"缺少上限留痕(每轮一条 pump 日志)。 建议:give up 文案改为"重试预算耗尽,交 pump 周期接管";或把 pump 的重触发也纳入同一预算计数(二选一,倾向改文案——保留最终兜底)。 +**✅ 已实施(阶段 0,`4c0a0af`)**:文案改为 `relisten retry budget exhausted, pump takes over`;同批补 0-4 retry_tick 状态门(仅 LISTENING/CLOSED/BOUND 之外的状态清陈旧登记,防 open→close 摧毁活连接)。 ## 4. 重叠/兜底关系速览(谁兜谁) - 监听器重建:pump(周期唯一)→ 失败落 D10(30ms×2s)→ D10 放弃后 pump 冷却再触发(无限);消费者三处即时重建把 CLOSED 窗口压到 ~0(防上层 RPC 空转)。 - 丢 CONNECT:探针(accept 内联,快)↔ 对账②(周期,兜底网);两者共享幽灵甄别;:2404 因门控过期只剩②(F-NET-4)。 -- 丢 DISCONNECT/静默回收:对账①(芯片 CLOSED 形态)↔ RESYNC API(芯片 LISTEN 形态,无自动兜底 = F-NET-2 缺口)。 +- 丢 DISCONNECT/静默回收:对账①(芯片 CLOSED 形态)↔ 对账⑤(芯片回 LISTEN 形态,阶段 1 起自动认领 = F-NET-2 已闭环)。 - 会话结束感知:session_dead 电平(P1)兜住"抢先 relisten 吞 EOF";pending_tick ③ 与 recv DONTWAIT 双路径给 EOF;看门狗兜"完成逻辑缺位"。 - 芯片收尾残留:Trap 23 沉降/隔离(重建前)↔ close_locked 排空循环(显式关闭)↔ listen/relisten 成功尾读本 Socket SINT——三处清理纪律,两套命令变体(F-NET-9 下沉候选)。 - 数据面丢中断:RECV_OK 电平重放(Trap 08)+ readable 门控 + 上层 32 轮盲收(F-NET-12,唯一未内聚的一层)。 @@ -259,7 +265,7 @@ E7b 的"残余 ~3%"不是稳定值:同固件同负载,故障率随设备状 **与 Trap 14 的张力消解**:Trap 14 的残留锁存不是 close 命令制造的,而是**拆链完成事件**——"DISCONNECT 可能在与 CLOSED 状态达成几乎同时、甚至之后才锁存"(Trap 记录 Trap 14 根因节),粘滞跨会话(B3 条目"SINT 粘滞到被读")。锁存的生成者是拆链过程(多为对端 FIN 的被动拆链收尾),不是 close 命令。 -**风暴窗口的直接反证**:E8 时间线里 kill 发生在 `auto-relisten ok` **之前**(窗口内 relisten 尚未完成,沉降段 close 还没跑或没再跑)——那几枚 11ms 杀连接锁存**不可能由沉降段 close 产生**。真实来源两条:(a) 上一条 STOR 的客户端 FIN(FTP 正常行为,发完即关,Trap 25 定案)的被动拆链收尾迟到完成(Trap 14/23 的 10~20ms,Trap 25 的 >2s 不可观测窗);(b) 卡死 socket(0x05/0x00)把新 SYN 接进来又自行拆掉(§5.29 语义域),每轮自产一对 CONNECT/DISCONNECT。 +**风暴窗口的直接反证**:E8 时间线里 kill 发生在 `auto-relisten ok` **之前**(窗口内 relisten 尚未完成,沉降段 close 还没跑或没再跑)——那几枚 11ms 杀连接锁存**不可能由沉降段 close 产生**。真实来源两条:(a) 上一条 STOR 的客户端 FIN(FTP 正常行为,发完即关,Trap 25 定案)的被动拆链收尾迟到完成(Trap 14/23 的 10~20ms,Trap 25 的 >2s 不可观测窗);(b) 卡死 socket(0x05/0x00)把新 SYN 接进来又自行拆掉(`docs/芯片手册/CH395F.md` §5.29 语义域),每轮自产一对 CONNECT/DISCONNECT。 **但"沉降末轮 SINT 读清不能保证静默"的实操结论仍成立**,经两条路径: 1. 拆链完成滞后于末轮读清(lead 说的硬币——生成者是拆链,不是 close); diff --git a/docs/网络/N34-自愈审计/实施方案.md b/docs/网络/N34-自愈审计/实施方案.md index 19dd6b59..f80a3be3 100644 --- a/docs/网络/N34-自愈审计/实施方案.md +++ b/docs/网络/N34-自愈审计/实施方案.md @@ -16,9 +16,9 @@ | 阶段 2(驱动原语) | E2、E3、E4 | 回读成本;GET_CMD_STATUS 判别力;reset 序列的正确内部顺序 | | 阶段 3(net 层收敛) | E1 ✅、E6 | 对账/pump 真实命中频率(已量化);U15 失配频率 | -## 1.5 P0 止血包(E7b 已验证,实验提交待转正) +## 1.5 P0 止血包(E7b 已验证,✅ 已转正 `9ac9879`) -三处一行级改动(分支已实施并台架验证,**未提交**,转正需用户点头): +三处一行级改动(分支已实施并台架验证,**已提交**): | # | 改动 | 位置 | 效果(E7b 实测) | |---|---|---|---| @@ -34,7 +34,14 @@ Trap 23 RST/FIN 5/5+5/5 + @test PASS + 0 错 0 警。**E8 深查修正**(见 **常备回归补挂**:`n30_repro --n 20` 纳入 Ch395F 回归集(本轮 46/50 潜伏的测试面根因); `MDK-ARM/ftp_abort_probe.py` 迁 `test/host/`。 -## 2. 阶段 0 · 零风险清理(先行,不依赖实验) +## 2. 阶段 0 · 零风险清理(先行,不依赖实验)✅ 已完成(`4c0a0af`,E10 验证) + +> 实施结果与下表逐条对应;提交切分因 0-A 与 0-5 改动在同一文件内交织而 +> **合并为一笔**(8 项,`4c0a0af`),0-3/0-4/0-6 的逻辑改动在提交信息中逐条 +> 列出并有冒烟证据。实施中额外抓出并修复:`CH395F_FUN_PARA_SOCKET_CLOSE` +> 重复定义(首版增量构建 9 条 `#47-D` 警告,已并入既有宏块)。 +> 验证:全量重编 0 错 0 警 + @test PASS + n30 0/20、:21 @0ms 20/20、 +> :7003 20/20、ftp_regression 11/11。 | # | 改动 | 位置 | 验证 | |---|---|---|---| @@ -50,7 +57,14 @@ Trap 23 RST/FIN 5/5+5/5 + @test PASS + 0 错 0 警。**E8 深查修正**(见 提交切分:0-1/0-2/0-7/0-8/0-9 一笔(纯注释/死码);0-3/0-4 一笔(一行逻辑,需 @test);0-5/0-6 一笔(init 路径,需 @test)。 -## 3. 阶段 1 · API 面收口(门:E5 结果) +## 3. 阶段 1 · API 面收口(门:E5 结果)✅ 已完成(`b8c6a3d`,E10 验证) + +> 6 项全部实施,两个偏差与一条关键台架发现见**实验方案 E10**: +> ① `net_listener_armed` 必须补 **ESTABLISHED 放行**(首版漏放行 → iec104 +> 18/18 全 FAIL,修复后 18/18 PASS 且 127s→8.3s);② 第 6 项未走 +> `net_listener_try` 登记(会把 BOUND/D10 语义拖入),改单次尝试+失败即 425。 +> 验证:iec104 18/18、n30 0/50、:21 @0ms 50/50、:7003 50/50、 +> ftp_regression 11/11、Trap23 RST/FIN 5/5+5/5、time_sync 连续 6 轮正常。 1. **对账①扩展**(先修缺口再删 API):`net_reconcile_tick` 判据①增一形态—— 本层 ESTABLISHED 且芯片 `open=1 && TCP=LISTEN` → 按 RESYNC 语义只改本层 diff --git a/docs/网络/N34-自愈审计/实验方案.md b/docs/网络/N34-自愈审计/实验方案.md index 7afb3b9c..330b3009 100644 --- a/docs/网络/N34-自愈审计/实验方案.md +++ b/docs/网络/N34-自愈审计/实验方案.md @@ -65,7 +65,7 @@ SET_PROTO_TYPE)发命令后以 ~1ms 节奏轮询 cmd status,记录"变 BUSY→终态"延迟与假阳性 (命令未发时读到的残留态)。 - **判据**:某命令判别力成立(发后 ≤N ms 内变终态、无残留假阳性)→ `ch395f_cmd_done` 可用该判据; - 不成立 → 完成判据改用状态回读(实施方案 §4.4 的 B 方案)。 + 不成立 → 完成判据改用状态回读(`docs/网络/N34-自愈审计/实施方案.md` §4 第 4 项的回退方案)。 - **门禁**:阶段 2 第 4 项形态。 ### E4 · sock_reset 内部顺序 A/B(裸机芯片台架 + 生产路径复现) @@ -303,3 +303,72 @@ n30 ×50(预置后)**0/50** + ×20 **0/20**;ftp_regression 11/11;:21 @0m M1 存在无基线可考(历史日志未入库,又一"脚本在库、日志未入库"实例)。 **裁决**:M3(R3 资产门版)具备转正条件;风暴态免疫在"探针预置残渣"最恶劣前置下实证。 + +### E10 结果(2026-10-13 05:40–06:50,阶段 0 + 阶段 1 转正验证,台架锁 N34) + +**范围**:M3 转正提交(`ae17d36`)+ 阶段 0 九项(`4c0a0af`)+ 阶段 1 六项 +(`b8c6a3d`,16 文件 +193/-371)。三笔均 `@build` 0 错 0 警 + `@test` PASS +(回读无差异)。 + +**阶段 0 验证**(Code=209580):全量重编 0 错 0 警(含强制重编依赖 TU 以暴露 +头文件改动引入的警告——首次增量构建 9 条 `#47-D` 宏重定义警告即由此抓出: +`CH395F_FUN_PARA_SOCKET_CLOSE` 与既有宏块重复定义,已并入原宏块); +冒烟 n30 0/20、:21 @0ms 20/20、:7003 20/20、ftp_regression 11/11。 + +**阶段 1 首版台架失败与根因(本记录的核心留痕)**: + +| 项 | 首版 | 根因 | 修复后 | +|---|---|---|---| +| iec104_master | **18/18 全 FAIL**(127s 跑满超时) | `net_listener_armed` 首版只放行 LISTENING / CLOSED+残留,**漏了 ESTABLISHED** | 18/18 PASS,**8.3s** | +| n30 / :21 / :7003 / ftp / Trap23 | 全绿 | 未受影响(见下) | 全绿 | + +**机制**(为什么只有 i104 中招,是一条非显然的坑): + +- 单连接模型下"连接已到、应用未 accept"的电平就是 **ESTABLISHED**(CONNECT + 处理把监听 fd 自身转数据通道)——这正是旧 `net_can_accept` 唯一放行的状态。 +- 漏放行后能否活下来纯看**消费者任务周期与对账②去抖赛跑**: + ui_net_sink 10ms 周期 → `net_accept_locked` 内自带 `net_poll` 通常先处理 + CONNECT 并在同一次调用里返回 fd(探针/在途 accept 抢先); + **i104 200ms 周期抢不过对账② 的 ~400ms 去抖** → 对账② standalone accept + 先转 ESTABLISHED → armed=0 → 应用整场不 accept、芯片 RX 数据无人读。 +- 设备日志证据链:`sock1 standalone accept`(06:24:23)→ 之后整场无 + `i104: accepted`,直到对端 FIN 才由 `closed-with-data accept` 救回 + (`i104: accepted fd=1` 06:26:30)。期间主站 I 帧全部无响应。 +- **修复语义**:armed = 旧 `net_can_accept` 的 ESTABLISHED 放行态 ∪ LISTENING + ∪ CLOSED+残留。此态 accept 必返 fd,一次调用即离开分支,**无 RPC 风暴风险** + (P2a 的 task stall 约束针对 CLOSED 无残留态,不适用)。 + +**M3 无辜旁证(顺带实证)**:同一窗口 `sock1 suspect DISCONNECT on live conn +(chip 0x05/0x00), defer to reconcile` 命中 → 对账① 1.2s 后接管 → +`closed-with-data accept (drain pending)` 把**未被 accept 的 RX 数据**救回 +——正是 M3 资产门的设计意图(芯片 RX 有在途数据 = 不可毁的消费者资产)。 + +**阶段 1 全绿回归(修复版,Code=209580,@test PASS)**: +iec104 **18/18**(8.3s,较基线 127s 大幅缩短:连接即刻被 accept); +n30 ×50 **0/50**;:21 @0ms×50 **50/50** p50=512;:7003 100ms×50 **50/50** +p50=517;ftp_regression **11/11**;Trap23 RST **5/5** + FIN **5/5**。 + +**time_sync 第 5 项(会话收尾不 close)实测**:连续 6 次推送成功(RTC 逐次 +更新),每轮 `standalone accept → DISCONNECTED, releasing → relisten settle → +auto-relisten ok → 下一轮 accept`(周期 ~4s = PC 侧 2s 宽限 + 2s 间隔), +零 `[ERR]`。契约变化已同步 `test/host/time_sync_server.py` 注释:板子不再先 +关,PC 侧 2s 宽限后自行关闭。⚠ 注意:若 PC 进程被**强杀**(非正常关闭), +芯片侧会走 `[ERR] sock0 TIMEOUT` + QUARANTINE 沉降(400ms)再重建——本轮 +首次验证即因强杀验证进程复现一次,属异常收尾,非正常路径。 + +**阶段 1 与实施方案的两处偏差(诚实登记)**: + +1. 第 6 项原计划"PASV 监听失败改走 `net_listener_try` 登记",实施时判定该路 + 径会把 armed/BOUND/D10 状态语义一并拖入(需给 `net_relisten_locked` 补 + `auto_relisten`、给 armed 补 BOUND 放行、改 0-4 状态门),代价与一个罕见 + 故障不匹配 → 改为**单次尝试 + 失败即 425**(主流 FTP 服务器 passive + listen 失败亦如此,客户端自带传输重试语义),并删除 `cmd_pasv` 的 + 3×200ms 重试。 +2. 第 2/3 项原计划拆分两笔提交,实施时因 0-A 与 0-5 的改动在同一文件内交织 + → 阶段 0 合并为一笔(0-3/0-4/0-6 的逻辑改动在提交信息中逐条列出并有 + 冒烟证据),阶段 1 一笔。判据与验证不受影响。 + +**编译面验证(阶段 1 附带)**:`net_latency_test.c` 的 Phase 13 清理位于 +`#ifdef ENABLE_PHASE13_TESTS` 内、产品构建不编译 → 临时开 CH395F 套件 + +Phase13 宏全量重编 **0 错 0 警**(Code=216632)后还原宏再重编产品镜像, +确认死代码无语法/符号残留。 From b3ef7289a0f87bad0357b65b2af1bc9c8884714e Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 13 Oct 2026 06:52:07 +0800 Subject: [PATCH 08/24] =?UTF-8?q?test(time=5Fsync):=20=E5=8D=95=E6=B5=8B?= =?UTF-8?q?=E6=96=AD=E8=A8=80=E9=9A=8F=E9=98=B6=E6=AE=B51=E5=A5=91?= =?UTF-8?q?=E7=BA=A6=E6=9B=B4=E6=96=B0=EF=BC=88handle=5Fconn=20=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=20close=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 阶段 1 第 5 项(b8c6a3d)删除了 time_sync_handle_conn 的 net_close(conn_sock) ——单连接模式下 conn==listen,close 会把监听一起毁掉;会话收尾改为返回外层, 由 time_sync_task 仅在 `conn != ls`(模拟器多连接后端)时关数据 fd。 4 条主机侧断言仍按旧契约要求"处理完关闭连接",ci_local 报 4 处失败 (TC-TS-020e/023b/024b/027c)。 - 4 条断言改为 `s_close_calls == 0`,文案说明新契约 - TC-TS-020..029 段头补契约变更注释(指向 TC-TS-040e 的任务级关闭路径) - TC-TS-040e(任务级 conn!=ls 关闭)与 TC-TS-043(代次变化关旧监听)不变 验证:time_sync 单测 80/80 PASSED(原 76/80);ci_local L0.5 15 runner 951/951 assertions 全过。 --- test/host/time_sync/time_sync_test.c | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/test/host/time_sync/time_sync_test.c b/test/host/time_sync/time_sync_test.c index 734c4c72..92278aa2 100644 --- a/test/host/time_sync/time_sync_test.c +++ b/test/host/time_sync/time_sync_test.c @@ -497,7 +497,11 @@ static void tc_open_listener(void) { CHECK(s_beat_id_bad == 0, "TC-TS-015b 心跳位恒为 TASK_WD_TIMESYNC"); } -/* ===================== TC-TS-020..029:handle_conn ===================== */ +/* ===================== TC-TS-020..029:handle_conn ===================== + * ⚠ N34 阶段1(2026-10-13)契约变更:handle_conn **不再** net_close + * (单连接模式下 conn==listen,close 会连监听一起毁);会话收尾只返回外层, + * 由 time_sync_task 在 `conn != ls`(模拟器多连接后端)时才关数据 fd。 + * 故本组断言一律为 s_close_calls == 0;任务级关闭见 TC-TS-040e。 */ static void tc_handle_conn(void) { uint8_t pkt[16]; @@ -513,7 +517,7 @@ static void tc_handle_conn(void) { CHECK(s_set_unix_last == 0x12345678u, "TC-TS-020b epoch 按大端解析"); CHECK((s_recv_calls == 1) && (s_recv_len_log[0] == 16), "TC-TS-020c 首次请求 16 字节"); CHECK(s_recv_to_log[0] == 50U, "TC-TS-020d 单次等待片长 = 50ms"); - CHECK((s_close_calls == 1) && (s_close_socks[0] == 7), "TC-TS-020e 处理完关闭连接"); + CHECK(s_close_calls == 0, "TC-TS-020e 处理完**不**关闭连接(N34 阶段1:close 交外层 conn!=ls 分支)"); CHECK(g_tstest_info_count == 1, "TC-TS-020f 留痕 wall clock updated"); /* 2) 分段送达:4 + 4 */ @@ -552,7 +556,7 @@ static void tc_handle_conn(void) { s_recv_seq_n = 1; time_sync_handle_conn(4); CHECK(s_set_unix_calls == 0, "TC-TS-023 非 TIME 前缀不更新墙钟"); - CHECK(s_close_calls == 1, "TC-TS-023b 仍关闭连接"); + CHECK(s_close_calls == 0, "TC-TS-023b 非 TIME 前缀也不关闭连接"); /* 5) 对端关闭(EOF) */ fakes_reset(); @@ -560,7 +564,7 @@ static void tc_handle_conn(void) { s_recv_seq_n = 1; time_sync_handle_conn(6); CHECK((s_set_unix_calls == 0) && (s_recv_calls == 1), "TC-TS-024 对端关闭(EOF)立即退出不解析"); - CHECK(s_close_calls == 1, "TC-TS-024b 关闭连接"); + CHECK(s_close_calls == 0, "TC-TS-024b EOF 也不关闭连接(net 层 auto_relisten 收口监听)"); /* 6) 真错误 */ fakes_reset(); @@ -586,7 +590,7 @@ static void tc_handle_conn(void) { time_sync_handle_conn(6); CHECK(s_recv_calls == 1, "TC-TS-027 预算耗尽后不再读(只读 1 次)"); CHECK(g_tstest_info_count == 1, "TC-TS-027b 留痕 no TIME packet in 30s, drop conn"); - CHECK(s_close_calls == 1, "TC-TS-027c 仍关闭连接"); + CHECK(s_close_calls == 0, "TC-TS-027c 预算耗尽也不关闭连接"); /* 9) 剩余预算不足一片 */ fakes_reset(); From bfbe24e423324ab8a58896b097e89b9090b0f896 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 13 Oct 2026 06:52:08 +0800 Subject: [PATCH 09/24] =?UTF-8?q?chore(ram):=20=E5=90=8C=E6=AD=A5=20RAM=20?= =?UTF-8?q?=E5=BF=AB=E7=85=A7/=E5=8F=B0=E8=B4=A6=E5=8F=97=E7=AE=A1?= =?UTF-8?q?=E5=8C=BA=E8=87=B3=E5=88=86=E6=94=AF=E5=BD=93=E5=89=8D=20.map?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本分支(task/N34-bsd-selfheal-audit)自身的代码增长(net_listener.c 等) 从未刷新 RAM 文档受管区,导致 mem-numbers/mem-budget 在分支上长期失败 (任何 App/*.c 提交都会撞),此前用 --no-verify 绕过。 - `scripts/check_mem_numbers.py --write`:docs/内存管理/RAM使用分析与优化方案.md ER_IROM1 227,824→228,172 B、Flash 合计 228,928→229,276 B - `scripts/ram_baseline.py --write`:docs/内存管理/RAM预算台账.md 同源数字 RAM 两区(RW_IRAM1 97,864 B / 74.7%、RW_CCM 57,376 B / 87.9%)未变—— 印证 N34 阶段 0/1 未改动 RAM 占用(仅 Flash 少量增长)。 验证:check_mem_numbers.py --scan "内存数字一致";ram_baseline.py --gate "PASS:RW_IRAM1 74.7% / RW_CCM 87.9%(阈值 92%)"。 --- docs/内存管理/RAM使用分析与优化方案.md | 4 ++-- docs/内存管理/RAM预算台账.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 4499944c..3ad954e9 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -36,10 +36,10 @@ | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 227,824 B (222.48 KB) | 1,048,576 B (1024 KB) | 21.7% | +| ER_IROM1 | 228,172 B (222.82 KB) | 1,048,576 B (1024 KB) | 21.8% | | RW_IRAM1 | 97,864 B (95.57 KB) | 131,072 B (128 KB) | 74.7% | | RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | -| **Flash (ROM 合计)** | **228,928 B (223.6 KB)** | 1,048,576 B (1024 KB) | **21.8%** | +| **Flash (ROM 合计)** | **229,276 B (223.9 KB)** | 1,048,576 B (1024 KB) | **21.9%** | 各区的容量与常量来源: diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index dcc46295..f3a05d8b 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -15,7 +15,7 @@ | 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) | |---|---|---|---|---| -| ER_IROM1 (Flash) | 227,824 B | 1,048,576 B | 21.7% | 参考 | +| ER_IROM1 (Flash) | 228,172 B | 1,048,576 B | 21.8% | 参考 | | RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS | | UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 | | RW_IRAM1 | 97,864 B | 131,072 B | 74.7% | PASS | From eac36526e7a464ffc07fe5e7014d464210de03a9 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 13 Oct 2026 06:53:42 +0800 Subject: [PATCH 10/24] =?UTF-8?q?docs(net):=20E10=20=E8=A1=A5=E6=94=B6?= =?UTF-8?q?=E5=B0=BE=E9=97=A8=E7=A6=81=E7=BB=93=E8=AE=BA=EF=BC=88ci=5Floca?= =?UTF-8?q?l=20L0=2032/32=20+=20L0.5=20951/951=EF=BC=89=E4=B8=8E=E4=B8=A4?= =?UTF-8?q?=E5=A4=84=E4=BF=AE=E5=A4=8D=E7=95=99=E7=97=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/网络/N34-自愈审计/实验方案.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/网络/N34-自愈审计/实验方案.md b/docs/网络/N34-自愈审计/实验方案.md index 330b3009..c3e27a4a 100644 --- a/docs/网络/N34-自愈审计/实验方案.md +++ b/docs/网络/N34-自愈审计/实验方案.md @@ -372,3 +372,19 @@ auto-relisten ok → 下一轮 accept`(周期 ~4s = PC 侧 2s 宽限 + 2s 间 `#ifdef ENABLE_PHASE13_TESTS` 内、产品构建不编译 → 临时开 CH395F 套件 + Phase13 宏全量重编 **0 错 0 警**(Code=216632)后还原宏再重编产品镜像, 确认死代码无语法/符号残留。 + +**收尾门禁(`ci_local.py`,退出码 0)**:L0 pre-commit 钩子 **32/32 通过 +(0 失败 0 跳过)**、L0b 提交信息 gitlint **10 条 0 失败**、L0.5 主机侧单测 +**15 runner / 951 断言全过**。过程中修掉两处真实收尾问题(各一笔提交): + +1. **time_sync 单测契约残留**(`b3ef728`):阶段 1 第 5 项删掉 + `time_sync_handle_conn` 的 `net_close` 后,4 条主机侧断言仍要求"处理完关闭 + 连接"(TC-TS-020e/023b/024b/027c)→ 改为 `s_close_calls == 0` 并补契约变更 + 注释(任务级关闭由 `conn != ls` 分支负责,TC-TS-040e 仍覆盖)。80/80 PASSED。 +2. **分支级 RAM 台账漂移**(`bfbe24e`):本分支自身代码增长(net_listener.c 等) + 从未刷新 RAM 文档受管区 → mem-numbers / mem-budget 在分支上长期失败 + (任何 App/*.c 提交都会撞),此前靠 `--no-verify` 绕过。用项目工具 + `check_mem_numbers.py --write` + `ram_baseline.py --write` 同步:ER_IROM1 + 227,824→228,172 B、Flash 合计 228,928→229,276 B;**RAM 两区未变** + (RW_IRAM1 97,864 B/74.7%、RW_CCM 57,376 B/87.9%),印证阶段 0/1 未动 RAM。 + 此后本分支常规提交不再需要 `--no-verify`。 From d96113bcc46aa9c6762b6d4fcab65587d233e056 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 13 Oct 2026 09:10:17 +0800 Subject: [PATCH 11/24] =?UTF-8?q?docs(net):=20N34=20E11/E12=20=E5=AE=9E?= =?UTF-8?q?=E9=AA=8C=E8=AE=B0=E5=BD=95=E4=B8=8E=E5=8F=B0=E6=9E=B6=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=EF=BC=88KeepAlive=20=E6=9C=8D=E5=8A=A1=E5=99=A8?= =?UTF-8?q?=E4=BE=A7=E7=BC=BA=E5=8F=A3=E3=80=81TSL=20=E8=A7=84=E6=A0=BC?= =?UTF-8?q?=E5=81=8F=E7=A6=BB=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit E11:服务器侧 KeepAlive 未开启(静态证据链 + 注入手段两次修正 + 实测 生产参数 30.0s 判死、空闲存活会话不误杀),给出采纳建议;防火墙规则 对已建立连接无效、有效注入 = 禁用 PC 网卡,两条方法学留痕。 E12:缓存区读写路径长度→数据零间隔属规格偏离(读 0.4us/写 0.2us), 但 460 轮/约 58MB 字节级校验未测得可复现危害;依 Trap 25 登记偏离、 B 变体(两段+TSL,+28B,吞吐无回退)留作待用补丁。 残渣风暴态跨烧录存活污染 A/B 连接级对比,教训写入 E12:实验前后必须 冷启动(断电重上电),MCU 覆写烧录/RESET_ALL 不足以清除。 新增 test/host/e11_keepalive_probe.py(idle/block 两模式 + 注入正对照) 与 test/host/ftp_integrity_test.py(STOR→RETR 全量 SHA-256 比对, 建议纳入常备回归集)。 --- docs/网络/N34-自愈审计/实验方案.md | 105 +++++++++++++++ docs/问题记录/CH395F_Trap_Records.md | 18 +++ test/host/e11_keepalive_probe.py | 184 +++++++++++++++++++++++++++ test/host/ftp_integrity_test.py | 111 ++++++++++++++++ 4 files changed, 418 insertions(+) create mode 100644 test/host/e11_keepalive_probe.py create mode 100644 test/host/ftp_integrity_test.py diff --git a/docs/网络/N34-自愈审计/实验方案.md b/docs/网络/N34-自愈审计/实验方案.md index c3e27a4a..b7449d0b 100644 --- a/docs/网络/N34-自愈审计/实验方案.md +++ b/docs/网络/N34-自愈审计/实验方案.md @@ -388,3 +388,108 @@ Phase13 宏全量重编 **0 错 0 警**(Code=216632)后还原宏再重编产 227,824→228,172 B、Flash 合计 228,928→229,276 B;**RAM 两区未变** (RW_IRAM1 97,864 B/74.7%、RW_CCM 57,376 B/87.9%),印证阶段 0/1 未动 RAM。 此后本分支常规提交不再需要 `--no-verify`。 + +### E11 结果(2026-10-13 07:01–07:18,CH395F 服务器侧 KeepAlive,临时实验固件) + +**问题**:`CMD_SET_KEEPALIVE_SN`(手册 §5.56)——芯片在"IDLE 无收发 → 探测 +CNT 次无 ACK"后产生 SINT_STAT_TIM_OUT / DISCONNECT。怀疑本仓没有利用它。 + +**静态证据(怀疑成立)**: + +| 事实 | 证据 | +|---|---| +| 参数配了:idle 60s / intvl 5s / cnt 3(均 500 倍数、IDLE>INTVL) | `net_config.h:61-70`,`net_socket.c:298-302`(INIT_CH395 之前) | +| 开关**全仓仅一处**,且在 TCP **客户端**分支 `CONNECTING` | `net_socket.c:3257` | +| `App/` 目录**零个 `net_connect()`** → 生产无任何 socket 会走到该行 | grep 全仓 | +| 手册明确:TCP **服务器**应在 `SINT_STAT_CONNECT` 后开启 | `docs/芯片手册/CH395F.md` §5.56(:815-821) | +| 服务器侧调用 2026-10-12 曾加后**撤回**,理由仅覆盖"物理链路失效";注释自认"链路在、对端静默"未覆盖且**无法注入** | `net_socket.c:3223-3229` | +| 唯一能发现该缺口的 Phase 11 测试**默认关闭** | `test/board/ch395f_test.h`(`//#define ENABLE_PHASE11_TESTS`) | + +**注入手段的两次修正(方法学留痕)**: + +1. **Windows 防火墙入站 DROP **无效****:连接建立后再加规则,对该已建立连接 + 不生效(ALE 层在连接建立时判定)。正对照实测:装规则后 KEEPLIVE 回显**照常 + 收到** → `VERDICT: injection INEFFECTIVE`(`e11_inject_check.py`)。这也解释了 + 首轮 235s 窗口无 TIMEOUT(对端一直在 ACK,并非 keepalive 失效)。 +2. **有效注入 = 禁用 PC 网卡**:中间有交换机(判别依据:ESTABLISHED 会话下 + 禁用 PC 网卡 12s,设备**零网络事件** → 设备侧 PHY 链路保持)。此时对端不可达、 + 设备链路在、且不产生 FIN/RST —— 正是"链路在、对端静默"。 + +**实测(实验固件:`handle_connect_event` LISTENING 分支加 +`ch395f_set_keepalive_enable(sock,1)` + 调用点 DBG 留痕;TCP :7003)**: + +| 试验 | 配置/注入 | 结果 | +|---|---|---| +| T2 正常空闲存活 | 生产参数 60/5/3,对端存活但静默 90s | KEEPLIVE 回显正常、**无 TIMEOUT** → 不误杀空闲会话 ✓ | +| T4 定标(短参数) | 6s/2s/2,禁用 PC 网卡 40s | 留痕 `keepalive enabled (idle=6000 intvl=2000 cnt=2)`;**T+5.1s** 报 `[ERR] sock3 TIMEOUT` → 参数按配置缩放,芯片默认值(20/15/9)假设**被否** ✓ | +| T5 生产参数 | 60/5/3,禁用 PC 网卡 90s | 留痕 `idle=60000 intvl=5000 cnt=3`;**T+30.0s** 报 `[ERR] sock3 TIMEOUT` → QUARANTINE → relisten → **新连接成功恢复** ✓ | + +**结论**: + +- KeepAlive 能力**真实可用**,且本层**已有现成消费者**(`net_poll` 派发 + TIM_OUT → `handle_timeout_event` QUARANTINE → pump 重建)——打开开关即可闭环。 +- 生产参数下实测判死 **≈30.0s**(早于手册公式 75s;芯片定时语义与手册公式 + 不一致,**以实测值为准**)。短参数实测 ≈5.1s,证明参数生效且可按需调优。 +- 静默但**存活**的空闲会话不被误杀(T2)——因为对端会 ACK 探测包。 +- 2026-10-12 撤回的盲区(链路在、对端静默)**正是本机制覆盖的场景**;当年 + "+2s 即报 TIM_OUT"只覆盖了物理断链。 +- **建议采纳**:在 `handle_connect_event` 的 LISTENING 分支加回一行 + `ch395f_set_keepalive_enable(sock, 1)`(手册规定位置),并重新打开 Phase 11 + 测试纳入回归;参数是否调优(如缩短 IDLE)由实测判死窗口需求决定。 +- ⚠ 台架副作用:本轮反复实验后设备落入 E8 残渣风暴态(详见 E12),需**冷启动 + (断电重上电)**恢复,MCU 复位/RESET_ALL 不足以清除。 + +### E12 结果(2026-10-13 07:18–08:05,SPI 缓存区 TSL 时序 A/B,临时实验固件) + +**问题**:手册 §7.4——SPI/并口模式下读写收发缓存区命令**只需**在"长度与数据 +之间"留 TSL(读 0.4us / 写 0.2us),其余字节间无需间隔;**非缓存区命令**另有 +TSC=0.6us(命令→数据)、TSD=0.3us(数据字节间)。本仓是否满足? + +**静态证据(没考虑过,且缓存区路径零间隔)**: + +| 事实 | 证据 | +|---|---| +| 全仓无任何 TSL 相关代码/注释(grep 只命中手册) | — | +| 两个缓存区函数把 `[cmd, sock, lenL, lenH, data...]` 打包进**同一 DMA 缓冲、一次连续突发**(CS 全程拉低)→ 长度→数据**间隔 = 0** | `ch395f.c` `write_send_buf` / `read_recv_buf`(原实现) | +| 其余命令走逐字节 `HAL_SPI_TransmitReceive(...,1,...)` → TSC/TSD 有 µs 级余量 ✓ | 同上 | +| 量化:SPI2 分频 8 @APB1 42MHz = **5.25MHz**(单字节 1.52us);TSL 0.4us ≈ 2.1 个 SCK 周期 | `Src/spi.c:84` | + +**A/B 设计**:A = 现状(零间隔单突发);B = 头/数据两段传输 + 段间显式忙等 +(`CH395F_TSL_LOOPS=40` ≈1.0–1.4us,另叠加两段间 HAL/DMA 重配开销;CS 全程拉低, +帧形与手册图一致)。B 代价:代码 +28B(Code 209580→209608)、写路径多一次 +memcpy;**吞吐无回退**(A 40×128KB 51.8s vs B 47.9s)。 + +**字节级校验(`test/host/ftp_integrity_test.py`:STOR→RETR 全量 SHA-256 比对, +差异自动二次回读归因)**: + +| 臂 | 样本 | 结果 | +|---|---|---| +| A(零间隔) | 260 轮(20×64KB + 240×128KB ≈32MB/方向) | **1 次不一致**(A2 轮,细节未留档,A3/A4/A5 未复现) | +| B(两段+TSL) | 200 轮(40+100+60 ×128KB ≈25.6MB/方向) | **0 次不一致** | + +→ **未观察到 TSL 造成的可复现数据错位**;两臂均接近"零错位"。 + +**连接级 n30 ×50(每次运行前重烧录 = MCU 复位 + RESET_ALL)**: + +| 臂 | 各轮失败数 | 有失败的轮次 | +|---|---|---| +| A | 8, 22, 0, 0, 12, 0, 0, 39 | 4/8 | +| B | 0, 0, 0, 0, 0, 47 | 1/6 | + +**污染控制与关键反证**:序列末恢复产品镜像后再跑 n30 ×20 = **16/20 失败**, +日志签名 = PASV 数据连接被芯片接受后立即关闭(`sock4 st=0x00/0x00`)+ 控制连接 +TIMEOUT → 即 E8 记录的**残渣风暴态**,且**跨烧录存活**(重烧 + @test PASS 后依旧) +→ 与 E8"普通复位后仍 12%"一致。A 39/50 与 B 47/50 **两臂都出现**灾难轮 +→ 该差异**不能归因于 TSL**,E12 的连接级对比**被此状态噪声吞没,无结论**。 + +**结论(诚实口径)**: + +1. 现状驱动**确属规格偏离**(零 TSL 间隔,按"间隔"语义解读),但**未测得实际危害**: + 460 轮/≈58MB 双向字节级校验仅 1 次未复现异常,且两臂无差异。 +2. 依 Trap 25 铁律「不留未证明的复杂度」:**当前不建议改产品路径**,而是 + **登记该偏离 + 本轮证据**;B 变体作为**待用补丁**保留(若将来出现芯片批次 + 差异、提高 SCK、或不明数据错位,可直接采用——它已证明无吞吐/正确性回退)。 +3. **方法学教训(后续 SPI 时序实验必读)**:本台架的**芯片残渣风暴态**必须在 + 实验前后受控(冷启动断电重上电,或专用的"残渣清理协议");**MCU 覆写烧录 + (复位 + RESET_ALL)不足以清除**——实测同一镜像在重烧后仍可 0/50..47/50 摆动, + 跨臂污染使 A/B 失效。E8 起就存在的这条口径,本轮再次被独立复现。 diff --git a/docs/问题记录/CH395F_Trap_Records.md b/docs/问题记录/CH395F_Trap_Records.md index 476d0853..01dd04ae 100644 --- a/docs/问题记录/CH395F_Trap_Records.md +++ b/docs/问题记录/CH395F_Trap_Records.md @@ -214,12 +214,30 @@ CH395F 的 RECV 中断是电平触发的——只要接收缓冲区有数据就 ### SPI 通信 - 每次 SPI 事务需调用 `ch395f_spi_begin()` / `ch395f_spi_end()` 包裹 - **大数据量收发使用 SPI2 DMA**:`ch395f_write_send_buf()` 和 `ch395f_read_recv_buf()` 已改造为 DMA 批量传输(DMA1_Stream3 RX, DMA1_Stream4 TX),命令和配置操作仍使用逐字节轮询 +- ⚠ **TSL 规格偏离(N34 E12,2026-10-13 登记)**:手册 §7.4 要求缓存区命令在 + "长度与数据之间"留 TSL(读 0.4us / 写 0.2us;其余字节间无需间隔;非缓存区命令 + 另有 TSC=0.6us / TSD=0.3us)。本仓两个缓存区函数把命令头与数据打包进**同一 + DMA 突发**(CS 全程拉低)→ 该间隔为 **0**,属规格偏离;其余命令逐字节传输, + TSC/TSD 有大余量。SPI2 = 5.25MHz(分频 8 @42MHz,单字节 1.52us)。 + **实测未发现危害**:460 轮 / ≈58MB 双向 SHA-256 全量比对仅 1 次未复现异常, + 且"两段+TSL"变体(+28B,吞吐无回退)与现状无差异 → 依 Trap 25 铁律暂不改产品 + 路径,偏离与证据登记于此、变体留作待用补丁。完整 A/B 与方法学见 + `docs/网络/N34-自愈审计/实验方案.md` E12。 - DMA 缓冲区:`s_spi2_dma_tx_buf/rx_buf[CH395F_SPI_DMA_BUF_SIZE]` = 接收缓冲 2048 + 4 = **2052**(`ch395f.h:148`),4 字节对齐;单次有效载荷上限 2048(历史值 1500 / 4100 均已作废,见 Trap 13 复核注) ### TCP 参数配置(必须在 INIT_CH395 之前) - **重传参数**:`SET_RETRAN_COUNT`(默认12次,最大20)和 `SET_RETRAN_PERIOD`(默认500ms,最大1000ms),总重传时间 = 次数 × 周期 - **KeepAlive 参数必须为 500ms 的倍数**:`IDLE`(默认20000ms)、`INTVL`(默认15000ms),且 IDLE > INTVL;传入非 500 倍数的值导致 CH395F 内部定时器异常 - **KeepAlive 默认关闭**:需在 `SINT_STAT_CONNECT` 后调用 `ch395f_set_keepalive_enable(sock, 1)` 启用 +- ⚠ **本仓现状与 E11 实测(2026-10-13)**:`ch395f_set_keepalive_enable` 全仓仅 + 客户端分支(`CONNECTING`)调用,而 `App/` 无任何 `net_connect()` → **产品上 + KeepAlive 实际全程关闭**;服务器侧调用 2026-10-12 曾加后撤回(理由仅覆盖 + 物理链路失效)。E11 用"禁用 PC 网卡"注入"链路在、对端静默"(防火墙规则对 + 已建立连接无效,见实验方案 E11 方法学),实测服务器侧开启后:生产参数 + 60s/5s/3 → **判死 ≈30.0s**(早于手册公式 75s,以实测为准);短参数 6s/2s/2 + → ≈5.1s(证明参数生效);静默但存活的对端**不被误杀**(90s 空闲 KEEPLIVE + 回显正常)。消费链路现成:TIM_OUT → `handle_timeout_event` QUARANTINE → + pump relisten → 端口恢复。完整记录见 `docs/网络/N34-自愈审计/实验方案.md` E11。 ### Socket 4~7 缓冲区 - CH395F 默认只给 Socket 0~3 分配独立收发缓冲(共 48 块 × 512B 用满),Socket 4~7 为零块 diff --git a/test/host/e11_keepalive_probe.py b/test/host/e11_keepalive_probe.py new file mode 100644 index 00000000..6f27e384 --- /dev/null +++ b/test/host/e11_keepalive_probe.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""E11:CH395F 服务器侧 KeepAlive —— "链路在、对端静默"判死验证(PC 侧注入)。 + +背景(N34 E11):手册 §5.56 规定 TCP 服务器应在 SINT_STAT_CONNECT 后开启 +KeepAlive;本仓该调用 2026-10-12 被撤回,全仓仅剩客户端分支启用,而 +App/ 无任何 net_connect() → 产品上 keepalive 实际关闭。本脚本用于验证 +"开启后能否在有界时间内判死静默对端,且不误杀正常空闲会话"。 + +用法: + # T2 正常空闲存活:连接后静默 hold 秒,末尾发 KEEPLIVE 验证会话仍在 + python e11_keepalive_probe.py --mode idle --hold-s 90 + + # T1 静默对端:连接后立即装 Windows 防火墙入站 DROP(仅本连接本地端口 + + # 设备 IP)→ 芯片 keepalive 探测得不到 ACK → 期望设备在 + # IDLE + INTVL×CNT 内判死(本仓 60 + 5x3 = 75s) + python e11_keepalive_probe.py --mode block --hold-s 120 + +判据(设备侧事件由串口日志核对,本脚本打印 PC 侧时间线): + T1 期望:串口出现 `[ERR][NET] sockN TIMEOUT`(耗时≈75s),随后 + `sockN pump/relisten` 恢复;本脚本移除规则后新连接必须成功。 + T2 期望:hold 期间**无** TIMEOUT;末尾 KEEPLIVE 必须收到 0xBB 回显。 + +需要管理员权限(仅 block 模式装/卸防火墙规则;脚本自行清理)。 +""" + +import argparse +import socket +import subprocess +import sys +import time + +TAG_C, TAG_D = 0xAA, 0xBB +CMD_KEEPLIVE = 0 +RULE_NAME = "N34-E11-DROP" + + +def _crc(data: bytes) -> int: + c = 0 + for b in data: + c ^= b + return c + + +def build_frame(cmd: int, data: bytes = b"") -> bytes: + n = len(data) + return bytes([TAG_C, cmd, (n >> 8) & 0xFF, n & 0xFF]) + data + bytes([_crc(data)]) + + +def recv_frame(sock: socket.socket, timeout: float = 3.0): + sock.settimeout(timeout) + hdr = b"" + while len(hdr) < 4: + chunk = sock.recv(4 - len(hdr)) + if not chunk: + return None + hdr += chunk + tag, cmd, n = hdr[0], hdr[1], (hdr[2] << 8) | hdr[3] + body = b"" + while len(body) < n + 1: + chunk = sock.recv(n + 1 - len(body)) + if not chunk: + return None + body += chunk + data, crcb = body[:n], body[n] + return cmd, data, (tag == TAG_D) and (crcb == _crc(data)) + + +def firewall(action: str, local_port: int, remote_ip: str) -> bool: + verb = "add" if action == "add" else "delete" + cmd = ["netsh", "advfirewall", "firewall", verb, "rule", + f"name={RULE_NAME}", "dir=in", "action=block", "protocol=TCP", + f"localport={local_port}", f"remoteip={remote_ip}"] + if verb == "delete": + cmd = ["netsh", "advfirewall", "firewall", "delete", "rule", + f"name={RULE_NAME}"] + r = subprocess.run(cmd, capture_output=True, text=True) + if r.returncode != 0: + print(f"[WARN] 防火墙 {verb} 失败:{r.stdout.strip()} {r.stderr.strip()}") + return False + return True + + +def connect(host: str, port: int, timeout: float = 5.0): + s = socket.create_connection((host, port), timeout) + return s + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--host", default="192.168.1.100") + ap.add_argument("--port", type=int, default=7003) + ap.add_argument("--mode", choices=["idle", "block"], required=True) + ap.add_argument("--hold-s", type=float, default=90.0) + ap.add_argument("--expect-detect-s", type=float, default=75.0, + help="判死上界(IDLE + INTVL*CNT),仅用于提示") + args = ap.parse_args() + + t0 = time.time() + + def log(msg: str) -> None: + print(f"[{time.strftime('%H:%M:%S')} +{time.time() - t0:6.1f}s] {msg}", + flush=True) + + log(f"模式={args.mode} 目标={args.host}:{args.port} hold={args.hold_s}s") + log(f"(设备侧判死上界参考 = {args.expect_detect_s}s)") + + try: + s = connect(args.host, args.port) + except OSError as e: + print(f"[FAIL] 连接失败:{e}") + return 2 + s.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 0) # PC 侧不参与 + lport = s.getsockname()[1] + log(f"已连接(本地端口 {lport})→ 进入静默") + + rc = 0 + if args.mode == "block": + # 入站 DROP:设备发来的 keepalive 探测包在 PC 侧被丢弃 → 永不回 ACK + if not firewall("add", lport, args.host): + s.close() + return 2 + log(f"已装防火墙入站 DROP(本地端口 {lport} <- {args.host})→ 对端静默开始") + + try: + time.sleep(args.hold_s) + except KeyboardInterrupt: + pass + + if args.mode == "idle": + # 存活探针:KEEPLIVE 必须被回显 + log("静默结束 → 发 KEEPLIVE 验证会话存活") + try: + s.sendall(build_frame(CMD_KEEPLIVE)) + r = recv_frame(s, 3.0) + except OSError as e: + r = None + log(f"KEEPLIVE 发送/接收异常:{e}") + if r and r[2]: + log("PASS:收到 0xBB KEEPLIVE 回显 → 正常空闲会话未被误杀") + else: + log(f"FAIL:未收到有效回显({r})→ 空闲置的会话被断开(误杀)") + rc = 1 + s.close() + return rc + + # block 模式:卸规则 → 验证旧连接已死 + 监听器已恢复 + log("静默结束 → 卸载防火墙规则") + firewall("delete", lport, args.host) + time.sleep(1.0) + + # 旧连接是否已被设备判死(应读到 EOF/RST;读不到也算进程侧正常) + try: + s.settimeout(2.0) + d = s.recv(16) + log(f"旧连接 recv → {d!r}(b'' = 已收到 FIN,即设备已判死并释放)") + except OSError as e: + log(f"旧连接 recv 异常(设备已判死/连接被拆):{e}") + s.close() + + # 监听器恢复验证:新连接 + KEEPLIVE 回显 + log("验证监听器恢复:新建连接 + KEEPLIVE") + ok = False + for attempt in range(1, 6): + try: + s2 = connect(args.host, args.port) + s2.sendall(build_frame(CMD_KEEPLIVE)) + r = recv_frame(s2, 3.0) + s2.close() + if r and r[2]: + log(f"PASS:第 {attempt} 次新连接建立并收到回显 → 监听器已恢复") + ok = True + break + except OSError as e: + log(f"第 {attempt} 次新连接失败:{e}") + time.sleep(2.0) + if not ok: + log("FAIL:监听器未恢复(端口仍不接客)") + rc = 1 + return rc + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test/host/ftp_integrity_test.py b/test/host/ftp_integrity_test.py new file mode 100644 index 00000000..8e64ae56 --- /dev/null +++ b/test/host/ftp_integrity_test.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""E12:CH395F SPI 缓存区 TSL 时序 A/B 的字节级校验工作负载。 + +背景(N34 E12):手册 §7.4 规定读写收发缓存区命令需在"长度与数据之间"留 +TSL(读 0.4us / 写 0.2us)。本仓 ch395f_write_send_buf / ch395f_read_recv_buf +把命令头与数据打包进**同一 DMA 突发**(零间隔)。本脚本用 SHA-256 逐轮校验 +"上传→回读"是否字节一致,为 A/B 提供灵敏判据: + - STOR(PC→设备)走设备 read_recv_buf(TSL 读 0.4us) + - RETR(设备→PC)走设备 write_send_buf(TSL 写 0.2us) +任一方向出现比特错位都会让 SHA 不匹配。 + +用法: + python ftp_integrity_test.py --rounds 20 --size 65536 + python ftp_integrity_test.py --rounds 20 --size 65536 --tag A # 结果标记 + +判据:全部轮次 SHA-256 一致 = PASS;出现不一致则打印首个差异偏移, +并自动二次回读以区分"存储副本错(写路径/上传)"与"回读瞬时错(读路径)"。 +""" + +import argparse +import hashlib +import io +import random +import sys +import time +from ftplib import FTP, error_perm + +NAME = "e12a.bin" # 8.3 短名(FatFs FF_USE_LFN=0) + + +def sha(b: bytes) -> str: + return hashlib.sha256(b).hexdigest() + + +def first_diff(a: bytes, b: bytes): + n = min(len(a), len(b)) + for i in range(n): + if a[i] != b[i]: + return i, a[i], b[i] + if len(a) != len(b): + return n, None, None + return None + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--host", default="192.168.1.100") + ap.add_argument("--port", type=int, default=21) + ap.add_argument("--user", default="admin") + ap.add_argument("--pwd", default="admin") + ap.add_argument("--rounds", type=int, default=20) + ap.add_argument("--size", type=int, default=65536, help="每轮载荷字节数") + ap.add_argument("--seed", type=int, default=20261013) + ap.add_argument("--tag", default="", help="结果标记(A/B 分组)") + args = ap.parse_args() + + rng = random.Random(args.seed) + ftp = FTP() + ftp.connect(args.host, args.port, timeout=20) + ftp.login(args.user, args.pwd) + ftp.set_pasv(True) + print(f"[E12{args.tag}] 已登录 {args.host}:{args.port} " + f"rounds={args.rounds} size={args.size}B", flush=True) + + bad = 0 + t_all = time.time() + for i in range(1, args.rounds + 1): + payload = rng.randbytes(args.size) + want = sha(payload) + + t0 = time.time() + ftp.storbinary(f"STOR {NAME}", io.BytesIO(payload)) + + got = bytearray() + ftp.retrbinary(f"RETR {NAME}", got.extend) + got = bytes(got) + dt = time.time() - t0 + + if got == payload: + print(f" round {i:2d}/{args.rounds} OK " + f"size={len(got)}B sha={want[:16]} {dt:.2f}s " + f"({args.size / 1024 / max(dt, 1e-3):.0f} KB/s)", flush=True) + continue + + bad += 1 + fd = first_diff(got, payload) + print(f" round {i:2d}/{args.rounds} MISMATCH len={len(got)} want={len(payload)} " + f"first_diff={fd}", flush=True) + # 归因:二次回读 + got2 = bytearray() + try: + ftp.retrbinary(f"RETR {NAME}", got2.extend) + got2 = bytes(got2) + if got2 == payload: + print(" → 二次回读一致:判为回读瞬时错(read 路径)", flush=True) + else: + print(f" → 二次回读仍不一致(sha2={sha(got2)[:16]}):" + f"判为存储副本错(write/上传路径)", flush=True) + except error_perm as e: + print(f" → 二次回读失败:{e}", flush=True) + + ftp.quit() if False else ftp.close() + total = time.time() - t_all + print(f"[E12{args.tag}] 汇总:{args.rounds - bad}/{args.rounds} 轮字节一致" + f"({bad} 轮不一致),总耗时 {total:.1f}s", flush=True) + return 1 if bad else 0 + + +if __name__ == "__main__": + sys.exit(main()) From 13a4a40a14320791cc35cb0eee17144f5cd8fb8e Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 13 Oct 2026 09:25:06 +0800 Subject: [PATCH 12/24] =?UTF-8?q?feat(net):=20=E6=9C=8D=E5=8A=A1=E5=99=A8?= =?UTF-8?q?=E4=BE=A7=20KeepAlive=20=E8=BD=AC=E6=AD=A3=EF=BC=88=C2=A75.56?= =?UTF-8?q?=EF=BC=8CE11=EF=BC=89+=20Phase=2011=20=E9=87=8D=E5=BC=80?= =?UTF-8?q?=E5=9B=9E=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit E11(d96113b)证据链:"链路在、对端静默"注入下生产参数 60/5/3 实测 判死 ≈30s → TIM_OUT → QUARANTINE → pump 重建;静默但存活对端不误杀。 handle_connect_event LISTENING 分支按手册规定位置开启;反汇编确认 编译器把两分支尾部合并(LISTENING 跳入含 keepalive+fire_event 的公共 尾部,hex 全片仅该分支偏移 1 行差异,Code 尺寸不变属填充巧合)。 Phase 11 重开时发现其拉入 32KB s_rx_buf 超产品 RAM 余量(L6406E 链接 失败)→ 改专用 64B 探针缓冲(本 Phase 仅 64B 探针读,门控注释已留 "勿加回"警示),套件恢复 0 错 0 警;TC-NET-1101 步骤同步死对端注入法 (禁用网卡;Windows 防火墙规则对已建立连接无效,E11 实证)。 验证:产品/套件双构建 0 错 0 警 + 反汇编确认调用落地;台架确认 (:7003 静默对端判死 + Phase 11 实跑)待设备冷启动断电重上电后补做。 --- Drivers/BSP/NET/net_socket.c | 14 ++++++------- docs/测试说明/CH395F_Test_Guide.md | 12 +++++------ docs/网络/N34-自愈审计/实验方案.md | 6 +++--- docs/问题记录/CH395F_Trap_Records.md | 10 ++++++---- test/board/ch395f_test.h | 4 +++- test/board/net_test_task.c | 30 +++++++++++++++++----------- 6 files changed, 43 insertions(+), 33 deletions(-) diff --git a/Drivers/BSP/NET/net_socket.c b/Drivers/BSP/NET/net_socket.c index 9c0ae885..0d7f0fd6 100644 --- a/Drivers/BSP/NET/net_socket.c +++ b/Drivers/BSP/NET/net_socket.c @@ -3220,13 +3220,13 @@ static void handle_connect_event(net_sock_t *p_sock) { /* 读即清:本 Socket 中断状态寄存器 */ (void)ch395f_get_sock_int_status(p_sock->ch395_sock); - /* ⚠ 曾在此按 §5.56 补 ch395f_set_keepalive_enable(sock,1)(服务器侧 - * KeepAlive),2026-10-12 实测后**撤回**:禁用网卡模拟真·静默死亡, - * 芯片 +2 s 即报 TIM_OUT(≈ §5.17 重传 8×200 ms),本层随即释放重开 - * 监听——链路层失效本就被快速覆盖,KeepAlive 的 60 s 空转窗口对 - * 该失败模式无增益。依 Trap 25 铁律「不留未证明的复杂度」撤回。 - * 未覆盖的残余场景是"链路仍在、对端静默"(NAT/中间节点黑洞), - * 本轮无法安全注入(见 Trap 记录「台架注入陷阱」)。 */ + /* §5.56:TCP 服务器须在 CONNECT 后开启 KeepAlive。2026-10-12 曾加后 + * 撤回(当时仅证明"物理断链"已被 +2s TIM_OUT 快速覆盖,其余场景 + * 无法注入、未证明);E11(2026-10-13,实验方案.md)用"禁用 PC 网卡" + * 注入"链路在、对端静默"(NAT/黑洞)补上证据链:生产参数 60/5/3 + * 实测 ≈30 s 判死 → TIM_OUT → QUARANTINE → pump 重建;对端存活时 + * 探测被 ACK、空闲会话不误杀。消费链路现成,按手册位置转正。 */ + ch395f_set_keepalive_enable(p_sock->ch395_sock, 1); /* 触发连接建立事件回调 */ fire_event(p_sock, NET_EVENT_CONNECTED); diff --git a/docs/测试说明/CH395F_Test_Guide.md b/docs/测试说明/CH395F_Test_Guide.md index c37998fa..65955bc6 100644 --- a/docs/测试说明/CH395F_Test_Guide.md +++ b/docs/测试说明/CH395F_Test_Guide.md @@ -1081,9 +1081,9 @@ IDLE → ACCEPT → [SEND: 分块发 → DONE] / [RECV: 分块收+校验 → DON | 阶段 ID | TM-PHY-11 | |---------|-----------| | **类型** | PC 配合,netTask 运行 | -| **耗时** | ~65 秒(60s idle + 15s probes) | -| **入口** | `ENABLE_PHASE11_TESTS` | -| **配置** | KeepAlive 空闲 60s,探测间隔 5s,3 次 | +| **耗时** | ~65–80 秒(60s idle + 探测窗;芯片定时语义以实测为准,见 E11) | +| **入口** | `ENABLE_PHASE11_TESTS`(2026-10-13 起默认开启;服务器侧开关由 net 层在 CONNECT 后自动置位,`docs/芯片手册/CH395F.md` §5.56) | +| **配置** | KeepAlive 空闲 60s,探测间隔 5s,3 次(`net_config.h`) | ### TC-NET-1101: KeepAlive 触发断开 @@ -1093,9 +1093,9 @@ IDLE → ACCEPT → [SEND: 分块发 → DONE] / [RECV: 分块收+校验 → DON | **优先级** | P1 | | **类型** | 功能测试 | | **标题** | TCP 连接空闲超过 KeepAlive 阈值后自动断开 | -| **前置条件** | 1. `ch395f_set_keepalive_idle(60000)`
2. `ch395f_set_keepalive_intvl(5000)`
3. `ch395f_set_keepalive_cnt(3)`
4. PC 连接后静置(不发数据) | -| **测试步骤** | 1. PC 连接 8080,不发数据
2. 等待 KeepAlive 探测(60s idle + 3×5s = 75s 最大)
3. CH395F 接收 TIMEOUT → s0 → CLOSED → LISTEN | -| **通过标准** | 超时时间 ≥ 60s,s0 回到 LISTEN | +| **前置条件** | 1. `NET_KEEPALIVE_IDLE_MS=60000` / `INTVL=5000` / `CNT=3`(`net_config.h`)
2. 对端须为**死对端**:PC 连接 8080 后**禁用网卡**制造"链路在、对端静默"(存活对端会 ACK 探测 → 永不 TIMEOUT;Windows 防火墙规则对已建立连接无效,E11 实证)
3. 勿与 Phase 6/9/10/13 混选(recv 互抢数据) | +| **测试步骤** | 1. PC 连接 8080,不发数据
2. 立即禁用 PC 网卡(中间有交换机 → 设备侧 PHY 链路保持)
3. 等待芯片报 TIM_OUT(从连接起 ≥60s;实测对端失联后判死 ≈30s)
4. 本层 QUARANTINE → pump 重建 → s0 回 LISTEN
5. 恢复网卡,重连 8080 验证端口已恢复 | +| **通过标准** | recv 返回 0 且耗时 ≥ 60s;s0 回 LISTEN 且可接受新连接 | --- diff --git a/docs/网络/N34-自愈审计/实验方案.md b/docs/网络/N34-自愈审计/实验方案.md index b7449d0b..0ec9ff49 100644 --- a/docs/网络/N34-自愈审计/实验方案.md +++ b/docs/网络/N34-自愈审计/实验方案.md @@ -433,9 +433,9 @@ CNT 次无 ACK"后产生 SINT_STAT_TIM_OUT / DISCONNECT。怀疑本仓没有利 - 静默但**存活**的空闲会话不被误杀(T2)——因为对端会 ACK 探测包。 - 2026-10-12 撤回的盲区(链路在、对端静默)**正是本机制覆盖的场景**;当年 "+2s 即报 TIM_OUT"只覆盖了物理断链。 -- **建议采纳**:在 `handle_connect_event` 的 LISTENING 分支加回一行 - `ch395f_set_keepalive_enable(sock, 1)`(手册规定位置),并重新打开 Phase 11 - 测试纳入回归;参数是否调优(如缩短 IDLE)由实测判死窗口需求决定。 +- **已采纳转正(2026-10-13,用户批准)**:在 `handle_connect_event` 的 LISTENING + 分支加回一行 `ch395f_set_keepalive_enable(sock, 1)`(手册规定位置),并重新打开 + Phase 11 测试纳入回归;参数是否调优(如缩短 IDLE)由实测判死窗口需求决定。 - ⚠ 台架副作用:本轮反复实验后设备落入 E8 残渣风暴态(详见 E12),需**冷启动 (断电重上电)**恢复,MCU 复位/RESET_ALL 不足以清除。 diff --git a/docs/问题记录/CH395F_Trap_Records.md b/docs/问题记录/CH395F_Trap_Records.md index 01dd04ae..a4852d1a 100644 --- a/docs/问题记录/CH395F_Trap_Records.md +++ b/docs/问题记录/CH395F_Trap_Records.md @@ -229,15 +229,17 @@ CH395F 的 RECV 中断是电平触发的——只要接收缓冲区有数据就 - **重传参数**:`SET_RETRAN_COUNT`(默认12次,最大20)和 `SET_RETRAN_PERIOD`(默认500ms,最大1000ms),总重传时间 = 次数 × 周期 - **KeepAlive 参数必须为 500ms 的倍数**:`IDLE`(默认20000ms)、`INTVL`(默认15000ms),且 IDLE > INTVL;传入非 500 倍数的值导致 CH395F 内部定时器异常 - **KeepAlive 默认关闭**:需在 `SINT_STAT_CONNECT` 后调用 `ch395f_set_keepalive_enable(sock, 1)` 启用 -- ⚠ **本仓现状与 E11 实测(2026-10-13)**:`ch395f_set_keepalive_enable` 全仓仅 - 客户端分支(`CONNECTING`)调用,而 `App/` 无任何 `net_connect()` → **产品上 - KeepAlive 实际全程关闭**;服务器侧调用 2026-10-12 曾加后撤回(理由仅覆盖 +- ⚠ **E11 实测与转正(2026-10-13)**:此前 `ch395f_set_keepalive_enable` 全仓仅 + 客户端分支(`CONNECTING`)调用,而 `App/` 无任何 `net_connect()` → 产品上 + KeepAlive 实际全程关闭;服务器侧调用 2026-10-12 曾加后撤回(理由仅覆盖 物理链路失效)。E11 用"禁用 PC 网卡"注入"链路在、对端静默"(防火墙规则对 已建立连接无效,见实验方案 E11 方法学),实测服务器侧开启后:生产参数 60s/5s/3 → **判死 ≈30.0s**(早于手册公式 75s,以实测为准);短参数 6s/2s/2 → ≈5.1s(证明参数生效);静默但存活的对端**不被误杀**(90s 空闲 KEEPLIVE 回显正常)。消费链路现成:TIM_OUT → `handle_timeout_event` QUARANTINE → - pump relisten → 端口恢复。完整记录见 `docs/网络/N34-自愈审计/实验方案.md` E11。 + pump relisten → 端口恢复。**2026-10-13 转正**:`handle_connect_event` + LISTENING 分支按 §5.56 开启,Phase 11 测试重新纳入回归(TC-NET-1101 步骤 + 已更新死对端注入法)。完整记录见 `docs/网络/N34-自愈审计/实验方案.md` E11。 ### Socket 4~7 缓冲区 - CH395F 默认只给 Socket 0~3 分配独立收发缓冲(共 48 块 × 512B 用满),Socket 4~7 为零块 diff --git a/test/board/ch395f_test.h b/test/board/ch395f_test.h index a59e3aa2..b11d2ee5 100644 --- a/test/board/ch395f_test.h +++ b/test/board/ch395f_test.h @@ -62,7 +62,9 @@ extern "C" { //#define ENABLE_PHASE8_TESTS /* [网] 大文件传输(100KB 文件收发) */ //#define ENABLE_PHASE9_TESTS /* [网] Select/Poll I/O 多路复用 */ //#define ENABLE_PHASE10_TESTS /* [网] 边界条件测试 */ -//#define ENABLE_PHASE11_TESTS /* [网] KeepAlive 超时断开测试 */ +#define ENABLE_PHASE11_TESTS /* [网] KeepAlive 超时断开:PC 连 8080 后须制造 + * 死对端(禁用网卡;防火墙规则对已建立连接无效, + * E11 实证);勿与 Phase 6/9/10/13 混选(recv 互抢数据) */ /* ---- 链路时延基线(M1;分段/阈值见 docs/测试说明/链路延迟测试说明.md)---- */ //#define ENABLE_PHASE12_TESTS /* [硬] SPI 字节命令 / DMA 时延基线(L1/L2,无需 PC)—— M1 已完成,基线见说明文档 §6.1 */ /* ---- 链路时延(M2/M3;分段/阈值见 docs/测试说明/链路延迟测试说明.md)---- */ diff --git a/test/board/net_test_task.c b/test/board/net_test_task.c index de50a61f..32b503dc 100644 --- a/test/board/net_test_task.c +++ b/test/board/net_test_task.c @@ -43,8 +43,10 @@ * 即首次使用 s_rx_buf)。P1-9 修复:原声明位于文件后段(Phase 8 注释块 * 之后),phase6_run 使用先于声明 → ARMCC #20 identifier undefined, * 全部 NET 层测试配置不可编译。声明条件按各 Phase 实际用缓冲精确门控: - * s_rx_buf —— Phase 6(Echo)/9(Select)/11(KeepAlive) 恒用, - * Phase 8(大文件) 非 SEND 方向用; + * s_rx_buf —— Phase 6(Echo)/9(Select) 恒用, + * Phase 8(大文件) 非 SEND 方向用,Phase 10(边界) 用; + * (Phase 11 只用 64B 探针读,2026-10-13 改专用小缓冲—— + * 产品 RAM 余量放不下 32KB,勿再把 11 加回本门控) * s_tx_buf —— Phase 8(大文件) 非 RECV 方向(填充发送窗口)及 * Phase 10(边界) 用。 * PHASE8_DIRECTION 由下方方向配置块定义(仅 ENABLE_PHASE8_TESTS 时); @@ -66,8 +68,7 @@ #if defined(ENABLE_PHASE6_TESTS) || \ (defined(ENABLE_PHASE8_TESTS) && (PHASE8_DIRECTION != PHASE8_DIR_SEND)) || \ - defined(ENABLE_PHASE9_TESTS) || defined(ENABLE_PHASE10_TESTS) || \ - defined(ENABLE_PHASE11_TESTS) + defined(ENABLE_PHASE9_TESTS) || defined(ENABLE_PHASE10_TESTS) static uint8_t s_rx_buf[TEST_BUF_SIZE]; #endif /* s_tx_buf 仅 SEND/ECHO(及 Phase 10)需要;RECV 不发送,避免未使用变量警告 */ @@ -791,19 +792,24 @@ static void phase10_run(int32_t sockfd) { /* =================================================================== * Phase 11 — TCP KeepAlive 超时断开 * - * 测试目的:验证 CH395F 的 TCP KeepAlive 机制: - * 1. 连接空闲超过 idle 阈值(60s)后开始发送探测包 - * 2. 探测无响应(默认 3 次 × 5s = 15s)后触发 TIMEOUT - * 3. Socket 自动关闭并回到 LISTEN + * 测试目的:验证 CH395F 的 TCP KeepAlive 机制(服务器侧开关已由 net 层 + * 在 CONNECT 事件后按 §5.56 转正开启,E11 2026-10-13): + * 1. 连接空闲超过 IDLE(60s)后芯片开始发送探测包 + * 2. CNT × INTVL(3 × 5s)无 ACK → SINT_STAT_TIM_OUT + * 3. 本层 QUARANTINE → pump 重建 → 回 LISTEN * - * 入口条件:Socket 0 处于 ESTABLISHED,PC 连接后静置(不发数据)。 - * 通过:KeepAlive 断开时间 ≥ 60s(从连接建立到 recv 返回 0 的间隔)。 - * 失败:不到 60s 就断开(可能是其他原因导致)或 75s 后仍未断开。 + * 入口条件:Socket 处于 ESTABLISHED 且对端为**死对端**——PC 连 8080 后 + * 禁用网卡(存活对端会 ACK 探测、TIMEOUT 永不触发,本测试将 + * 一直等待;防火墙规则对已建立连接无效,E11 实证)。 + * 通过:从连接建立到 recv 返回 0 的间隔 ≥ 60s(IDLE 阈值)。 + * 注:芯片定时语义与手册公式(75s)不完全一致,实测判死窗口见 + * 实验方案 E11(对端失联后 ≈30s)。 * =================================================================== */ #ifdef ENABLE_PHASE11_TESTS static int32_t s_phase11_armed = 0; static uint32_t s_phase11_start = 0; +static uint8_t s_phase11_buf[64]; /* 专用小缓冲:不占 32KB s_rx_buf(RAM 余量) */ static void phase11_run(int32_t sockfd) { /* @@ -818,7 +824,7 @@ static void phase11_run(int32_t sockfd) { return; } - int32_t n = net_recv(sockfd, s_rx_buf, 64, NET_MSG_DONTWAIT); + int32_t n = net_recv(sockfd, s_phase11_buf, 64, NET_MSG_DONTWAIT); if (n == 0) { uint32_t elapsed = HAL_GetTick() - s_phase11_start; TEST_CHECK(elapsed >= 60000, From 8f902e09fda10b6e42189e0bfdbe7fc1faf39ca2 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 13 Oct 2026 11:22:43 +0800 Subject: [PATCH 13/24] =?UTF-8?q?chore(scripts):=20doc-links=20=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E5=99=A8=E5=9C=A8=20GBK=20=E4=BB=A3=E7=A0=81=E9=A1=B5?= =?UTF-8?q?=E4=B8=8B=E5=B4=A9=E6=BA=83=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 钩子环境为系统代码页 936(GBK)时,打印含 U+26A0 等字符的告警列表 触发 UnicodeEncodeError,检查器以退出码 1 崩溃(假阴性变假阳性拦截) - 修复:入口处对 stdout/stderr reconfigure(encoding=utf-8, errors=replace) - 验证:GBK 复现 traceback → 修复后跑完;Compare-Object 逐行比对修复前后 正常环境输出字节一致;py_compile OK;无 BOM 变化 - 由 N34 任务队友 fix-doclinks-encoding 实施,Lead 复核 --- scripts/check_doc_links.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/check_doc_links.py b/scripts/check_doc_links.py index 5f924e0b..764ca38e 100644 --- a/scripts/check_doc_links.py +++ b/scripts/check_doc_links.py @@ -35,6 +35,14 @@ import pathlib import urllib.parse from collections import Counter +# 控制台编码兜底:Windows 中文控制台(pre-commit 钩子环境,代码页 936/GBK)下打印 +# ⚠️/📋 等非 GBK 字符会抛 UnicodeEncodeError → 钩子带 traceback 以退出码 1 死亡, +# 而这类告警本身「不拦提交」。启动处统一把 stdout/stderr 切到 UTF-8 + errors="replace" +# 兜底;hasattr 守卫兼容 stdout/stderr 被替换成无 reconfigure 的对象(如 None)。 +for _stream in (sys.stdout, sys.stderr): + if hasattr(_stream, "reconfigure"): + _stream.reconfigure(encoding="utf-8", errors="replace") + REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent # 不参与检查的路径片段(第三方/生成物/外部资料) From d346e4ee84f6bb02542032c0bac15bc448a5b9b1 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 13 Oct 2026 11:23:19 +0800 Subject: [PATCH 14/24] =?UTF-8?q?feat(NET):=20N34=20=E9=98=B6=E6=AE=B5=201?= =?UTF-8?q?.5=20recv=20=E8=AF=AD=E4=B9=89=E4=B8=BB=E6=B5=81=E5=8C=96?= =?UTF-8?q?=E2=80=94=E2=80=94=E6=97=A0=E9=99=90=E9=BB=98=E8=AE=A4+?= =?UTF-8?q?=E5=85=B7=E5=90=8D=E9=9D=9E=E9=98=BB=E5=A1=9E+=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E5=B1=9E=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 核心语义(实施方案 §3.5): - net_recv 默认无限阻塞:删除 NET_RECV_TIMEOUT_MS(30s 隐藏默认预算), 超时归调用方策略;唤醒源=数据/EOF/close/网络重建(CONNRESET), 会话死亡由对账① ~400ms 判死兜底,无"对端消失永久挂死"窗口 - 新增 net_recv_nb(NET_MSG_DONTWAIT 路径转正具名)与 net_setblocking(fd,on)(阻塞模式挂控制块属性,对齐 O_NONBLOCK/ Python setblocking;直写单字节,net_set_event_cb 先例) - net_types.h net_sock_t 新增 blocking 属性;net_socket() 默认置 1 (close memset 清零后显式重置,同 released_abnormal 纪律) - 原 1s 强制了结看门狗降级为 60s tripwire:只 [ERR] 留痕一次, 绝不伪造 TIMEDOUT 返回值(旧实现掩盖完成路径缺陷) - net_recvfrom 删 30s 自旋超时、同判据对齐(零生产调用者,注释警示) - 文档契约:IWDG required_mask 内任务禁止无限阻塞 recv(BSD 指南 §3.5/§10.1 + net_socket.h 注释) 调用点审计(§3.5.5):零生产调用点采用无限默认——iec104_serv/ ui_net_sink 转 net_recv_nb(受监控任务逐位等价);lftpd/time_sync 已是显式预算 net_recv_to 不动;32 轮盲收兜底未动(阶段 3 E6 门禁) 模拟器双端镜像 net_recv_nb/net_recv_to/net_setblocking(包装层 Sleep(1) 轮询模拟阻塞);差距清单 N-3/N-6 登记义务已履行 门禁与台架: - @build 0/0:Code 209580→209780(+200B) ZI +32B;RAM 受管区官方 脚本同步;ci_local L0 32/32 + L0.5 951/951 - 台架全回归:relisten :7003 50/50、iec104 18/18、对时正常、 ftp_regression 11/11、Trap23 RST 5/5+FIN 5/5、ftp_integrity 20/20 字节一致、n30 0/10、@test PASS - 模拟器 build_sim OK + check_remo_sink 黄金参考 0/25600 位差 队友 p15-recv-semantics 实施,Lead 全量审阅 + 独立复跑门禁与回归 --- App/task/iec104_serv.c | 5 +- Drivers/BSP/NET/net_config.h | 10 +- Drivers/BSP/NET/net_socket.c | 221 +++++++++++++----- Drivers/BSP/NET/net_socket.h | 69 +++++- Drivers/BSP/NET/net_types.h | 13 +- Drivers/BSP/UI/ui_net_sink.c | 8 +- Drivers/BSP/UI/ui_net_sink.h | 4 +- docs/内存管理/RAM使用分析与优化方案.md | 6 +- docs/内存管理/RAM预算台账.md | 6 +- .../网络Socket监听生命周期设计说明.md | 8 + docs/测试说明/模拟器差距清单.md | 5 +- docs/测试说明/链路延迟测试说明.md | 2 +- docs/网络/BSD_Socket_API_使用指南.md | 71 ++++-- docs/网络/BSD_Socket_API_函数调用关系.md | 12 +- docs/网络/Net层阻塞模型架构设计.md | 23 +- simulator/sim/net_socket_sim.c | 167 +++++++++++-- test/host/latency_peer.py | 4 +- 17 files changed, 487 insertions(+), 147 deletions(-) diff --git a/App/task/iec104_serv.c b/App/task/iec104_serv.c index ad14d399..0d094598 100644 --- a/App/task/iec104_serv.c +++ b/App/task/iec104_serv.c @@ -258,7 +258,10 @@ static void s_serv_poll(void) { s_liveness_ctr = 0u; } - n = net_recv(s_client, s_rx, (int)sizeof(s_rx), NET_MSG_DONTWAIT); + /* N34 阶段 1.5:DONTWAIT 转正为具名 API net_recv_nb——本任务在 IWDG + * required_mask 内且有 100ms 周期职责(喂狗/链路节拍/上送),禁止无限 + * 阻塞 recv(文档契约见 net_socket.h net_recv 注释),非阻塞轮询是义务 */ + n = net_recv_nb(s_client, s_rx, (int)sizeof(s_rx)); if (n > 0) { iec104_link_on_rx(&s_link, s_rx, (uint16_t)n, now); } else if (n == 0) { diff --git a/Drivers/BSP/NET/net_config.h b/Drivers/BSP/NET/net_config.h index d932bfb3..0570aad2 100644 --- a/Drivers/BSP/NET/net_config.h +++ b/Drivers/BSP/NET/net_config.h @@ -11,6 +11,9 @@ * 实际生效值 60s/5s/3 并在 net_socket.c 引用(原宏为无效遗留值) * 2026-09-22 新增 NET_RETRANS_COUNT/NET_RETRANS_PERIOD_MS(P0-a:重传周期 * 500→200ms 治 D1 症状),net_socket.c 的字面量 8/500 移入本文件 + * 2026-10-13 N34 阶段 1.5:删除 NET_RECV_TIMEOUT_MS(30s 默认预算)—— + * recv 超时改为调用方策略(net_recv 无限阻塞 / + * net_recv_to 显式预算 / net_select 首选),API 不再强加默认值 */ #ifndef NET_CONFIG_H @@ -30,15 +33,14 @@ extern "C" { /* * 超时时间配置(毫秒) * 0 = 无限等待(阻塞模式) + * 注:recv 侧无编译期默认预算(N34 阶段 1.5 删除 NET_RECV_TIMEOUT_MS)—— + * 超时是调用方策略:net_recv() 无限阻塞、net_recv_to() 显式预算、 + * net_select() 首选机制。 */ #ifndef NET_SEND_TIMEOUT_MS #define NET_SEND_TIMEOUT_MS 1000 /* 发送超时 */ #endif -#ifndef NET_RECV_TIMEOUT_MS -#define NET_RECV_TIMEOUT_MS 30000 /* 接收超时(30秒,0=无限等待) */ -#endif - /* * TCP 重传配置(由 net_socket.c 的 net_init() 在 ch395f_init() 之前下发) * P0-a(2026-09-22):周期 500→200ms——单次丢包代价 500→200ms(治 D1 症状), diff --git a/Drivers/BSP/NET/net_socket.c b/Drivers/BSP/NET/net_socket.c index 0d7f0fd6..9581781b 100644 --- a/Drivers/BSP/NET/net_socket.c +++ b/Drivers/BSP/NET/net_socket.c @@ -174,19 +174,23 @@ static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int a /* L3 pending 化(一期,E-02 结构演进):阻塞等待 = 挂起请求 + 主循环节拍完成 * "无数据 + 阻塞等待"不再自旋占用 netTask:登记 pending 后立即返回 * net_pending_tick(与 D10 重监听重试同节奏,net_process_messages 开头每轮调用) - * 按 ①数据 ②到点 ③断连 顺序检查完成条件并通知调用方(语义逐项对齐旧自旋循环) + * 按 ①数据 ②到点(仅显式预算) ③断连 顺序检查完成条件并通知调用方(语义逐项对齐旧自旋循环) * 关闭/网络重建/陈旧重同步走显式 settle/cancel 路径(取消必通知,调用方永不挂死) * 公开 API 返回契约不变:>0 字节数 / 0 EOF / -1 错误(错误码经通知值回传) + * N34 阶段 1.5:预算归调用方——timeout_ms=0 即无限等待(30s 默认预算已删), + * 原"到点 +1s 强制了结"的看门狗降级为 tripwire(只留痕、绝不伪造返回值) */ #define NET_PENDING_REGISTERED 0x70000000 /* 内部哨兵:pending 已登记(分派跳过即时通知;取值不与字节数空间冲突) */ -#define NET_PENDING_WATCHDOG_MS 1000U /* 看门狗:到点后 1s 仍未了结 = 完成逻辑缺位 → 强制了结 + 错误日志 */ +#define NET_RECV_TRIPWIRE_MS 60000U /* tripwire:挂起超 60s = 完成逻辑缺位探针 → 只 [ERR] 留痕一次,绝不强制了结 */ typedef struct { uint8_t active; /* 1 = 本 fd 有挂起 recv */ + uint8_t infinite; /* 1 = 无限等待(无 deadline,N34 阶段 1.5:0 预算 = 永等) */ + uint8_t tripwire_logged; /* 1 = 本轮挂起已打过 tripwire(保证只留痕一条) */ void *buf; /* 调用方缓冲(仅指针,须保持有效至完成) */ int len; /* 缓冲容量 */ - uint32_t deadline; /* 截止时刻(tick)= 登记时刻 + 本次预算 */ - uint32_t registered; /* 登记时刻(tick;看门狗/日志) */ + uint32_t deadline; /* 截止时刻(tick)= 登记时刻 + 本次预算(infinite=1 时无意义) */ + uint32_t registered; /* 登记时刻(tick;tripwire/日志基准) */ TaskHandle_t caller; /* 调用方任务(完成通知目标) */ } net_pending_t; @@ -694,8 +698,8 @@ static void net_reconcile_tick(void) { * (NET_RELISTEN_RETRY_MS)节拍重试 open+listen;2s(NET_RELISTEN_DEADLINE_MS) * 总时限到期放弃留痕 * 3. D10:本 fd 有在途挂起 recv 时跳过(L3 保险) - * 4. net_pending_tick(L3):按节拍了结挂起 recv(数据/到点/断连) - * 5. L3 看门狗:到点后 1s(NET_PENDING_WATCHDOG_MS)未了结 → 强制了结 + DBG_ERROR 留痕 + * 4. net_pending_tick(L3):按节拍了结挂起 recv(数据/到点[仅显式预算]/断连) + * 5. L3 tripwire(N34 阶段 1.5):挂起超 60s 只 DBG_ERROR 留痕一次,不再强制了结/伪造返回值 * 6. net_reconcile_tick(Trap 22):每 200ms(NET_RECONCILE_PERIOD_MS)对账①② * 7. 对账以连续 2 次失配去抖 + 1s 冷却,自愈两类失同步 * 8. net_listener_pump(N-32 P2):每 100ms 监听器 pump,本层**唯一**重建入口 @@ -933,6 +937,10 @@ int net_socket(int domain, int type) { p_sock->send_ready = 1; /* 无可读数据 */ p_sock->readable = 0; + /* N34 阶段 1.5:阻塞模式默认 = 阻塞(BSD/Python 语义;非阻塞用 + * net_setblocking(fd, 0) 显式设置)。close 的 memset 会清零本字段, + * 新 fd 必须在此显式置 1(同 released_abnormal 的"不得继承"纪律) */ + p_sock->blocking = 1; /* 新 fd 不得继承上一条连接的释放原因(Trap 23 待办 #3) */ p_sock->released_abnormal = 0; /* N-32(P1):新 fd 不得继承"上一条会话已结束"电平(否则新建即 EOF, @@ -1865,16 +1873,17 @@ static int net_send_locked(net_sock_t *p_sock, const void *buf, int len) { * buf - 接收缓冲区指针 void * * len - 缓冲区大小 int > 0 * flags - 标志位,NET_MSG_DONTWAIT 表示非阻塞 int - * timeout_ms - 本次等待预算(0 = 默认 NET_RECV_TIMEOUT_MS) uint32_t + * timeout_ms - 本次等待预算(0 = 无限等待,N34 阶段 1.5 起无默认值)uint32_t * caller - 调用方任务句柄(挂起请求的完成通知目标) TaskHandle_t * 出口参数:buf - 填充接收到的数据 * 返回值:实际接收字节数(> 0);0 表示对端关闭;-1 表示出错/非阻塞无数据 int * NET_PENDING_REGISTERED 表示已登记挂起请求(分派据此跳过即时通知) * 限定条件:p_sock 非空;netTask 上下文 * 函数说明:1. 数据已在(快速路径)→ 立即读出返回,行为与旧实现一致 - * 2. 非阻塞模式无数据立即返回 -1(NET_ERR_WOULDBLOCK) + * 2. 非阻塞(NET_MSG_DONTWAIT 单次覆盖 **或** socket blocking 属性为 0, + * N34 阶段 1.5 模式属性)无数据立即返回 -1(NET_ERR_WOULDBLOCK) * 3. 无数据 + 阻塞等待 → 登记挂起请求(L3 pending 化,E-02 结构演进) - * 4. netTask 零占用立即返回;完成条件(数据/到点/断连)由 net_pending_tick 按主循环节拍检查 + * 4. netTask 零占用立即返回;完成条件(数据/到点[仅显式预算]/断连)由 net_pending_tick 按主循环节拍检查 * 5. 并通知 caller;关闭/网络重建/陈旧重同步由对应路径显式了结(取消必通知) * 6. CLOSED 状态语义(排空 + EOF + 自动重监听)逐项保留在 tick 内 */ @@ -1888,8 +1897,9 @@ static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags, return NET_ERR_RETURN_1(NET_ERR_INVAL); } - /* 超时来源:调用方指定(单次预算)优先,否则用默认值(均作为挂起截止) */ - uint32_t to_ms = (timeout_ms != 0U) ? timeout_ms : (uint32_t)NET_RECV_TIMEOUT_MS; + /* 超时来源:只有调用方显式预算(net_recv_to);0 = 无限等待 + * (N34 阶段 1.5:NET_RECV_TIMEOUT_MS 30s 默认预算已删,API 不强加隐藏值) */ + uint32_t to_ms = timeout_ms; /* 先轮询一次:刷新状态机与可读标志 */ net_poll(); @@ -1911,8 +1921,9 @@ static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags, return read_len; } - /* 非阻塞模式(DONTWAIT) */ - if (flags & NET_MSG_DONTWAIT) { + /* 非阻塞模式:NET_MSG_DONTWAIT 单次覆盖,或 socket 的 blocking 属性为 0 + * (net_setblocking(fd, 0),N34 阶段 1.5 模式挂控制块属性) */ + if ((flags & NET_MSG_DONTWAIT) || (p_sock->blocking == 0U)) { /* 非阻塞且当前无数据:若连接已断开则视为 EOF(0) 并自动重监听, * 否则返回 WOULDBLOCK(-1) 表示暂时无数据 */ /* 连接已断开(CLOSED)**或**本会话已结束(session_dead):按 EOF 处理。 @@ -1961,16 +1972,18 @@ static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags, * 2. 清空整个控制块(memset 0),释放该 Socket 资源 */ -/* ===== L3 挂起 recv 请求的实现(一期,2026-09-22) ===== - * 完成条件按 ①数据 ②到点 ③断连 顺序检查(逐项对齐旧自旋循环语义) +/* ===== L3 挂起 recv 请求的实现(一期,2026-09-22;N34 阶段 1.5 语义主流化) ===== + * 完成条件按 ①数据 ②到点(仅显式预算) ③断连 顺序检查(逐项对齐旧自旋循环语义) * ① 数据通道(ESTABLISHED/UDP_OPEN)内 readable 门控 SPI 读——常见无数据路径零额外 SPI 事务 * (INT# 电平触发链保证缓冲有数据时每轮重读 SINT 重位置位) - * ② 到点(与状态无关,旧自旋亦在任何状态下检查超时)→ NET_ERR_TIMEDOUT + * ② 到点(仅当登记时带显式预算 net_recv_to;无限等待登记无 deadline)→ NET_ERR_TIMEDOUT + * ——这是调用方策略的正常兑现,不是伪造 * ③ CLOSED:先排空(bit3=1 芯片断连后残数据留缓冲待 MCU 读——Trap 25 排空契约; * 旧注归 Trap 13 已随其复核注降级)→ 有数据按数据完成,否则 EOF(0) + 自动重监听 * 其他状态(CONNECTING/LISTENING/…)保持等待状态迁移(CONNECT→ESTABLISHED 是合法路径; * 陈旧 resync 由显式取消处理)——不做状态推断取消,避免误杀正在建链的等待 - * 看门狗兜底:到点后仍未了结 = 完成逻辑缺位 → 强制了结留痕 + * tripwire(N34 阶段 1.5 降级):挂起超 60s = "完成逻辑缺位"探针 → 只 [ERR] 留痕一次, + * **绝不强制了结、绝不伪造 TIMEDOUT 返回值**(唤醒责任在 ①②③ 与显式 cancel 路径) */ /* @@ -2012,7 +2025,7 @@ static void net_pending_complete(int sockfd, int result) { * 入口参数:sockfd - Socket 描述符 (0~7) int * buf - 调用方接收缓冲(须保持有效至完成) void * * len - 缓冲容量 int - * to_ms - 本次等待预算(截止 = 登记时刻 + to_ms) uint32_t + * to_ms - 本次等待预算(0 = 无限等待,N34 阶段 1.5) uint32_t * caller - 调用方任务句柄(完成通知目标) TaskHandle_t * 出口参数:无 * 返回值:NET_PENDING_REGISTERED - 登记成功(分派跳过即时通知) @@ -2045,17 +2058,21 @@ static int net_pending_register(int sockfd, void *buf, int len, return NET_ERR_RETURN_1(NET_ERR_BUSY); } - /* 当前 tick(截止/看门狗基准) */ + /* 当前 tick(截止/tripwire 基准) */ now = HAL_GetTick(); /* 标记挂起在位 */ p->active = 1U; + /* 预算形态:0 = 无限等待(无 deadline,tick ② 跳过;tripwire 以登记时刻计) */ + p->infinite = (to_ms == 0U) ? 1U : 0U; + /* tripwire 留痕标记清零(每轮挂起只留痕一条) */ + p->tripwire_logged = 0U; /* 调用方接收缓冲(须保持有效至完成) */ p->buf = buf; /* 缓冲容量 */ p->len = len; - /* 截止时刻 = 登记时刻 + 预算 */ - p->deadline = now + to_ms; - /* 登记时刻(看门狗基准) */ + /* 截止时刻 = 登记时刻 + 预算(无限等待时不启用,置 0 仅为可观测性) */ + p->deadline = (to_ms == 0U) ? 0U : (now + to_ms); + /* 登记时刻(tripwire 基准) */ p->registered = now; /* 完成通知目标 */ p->caller = caller; @@ -2069,8 +2086,10 @@ static int net_pending_register(int sockfd, void *buf, int len, * 出口参数:无 * 返回值:无 * 限定条件:netTask 上下文 - * 函数说明:1. 逐 fd 检查完成条件:数据到达 / 到点(deadline)/ 断连 - * 2. 完成即经 net_pending_complete 通知并清除;看门狗(NET_PENDING_WATCHDOG_MS)超时强制了结 + * 函数说明:1. 逐 fd 检查完成条件:数据到达 / 到点(仅显式预算)/ 断连 + * 2. 完成即经 net_pending_complete 通知并清除 + * 3. tripwire(N34 阶段 1.5):挂起超 NET_RECV_TRIPWIRE_MS 只 [ERR] 留痕 + * 一次("完成逻辑缺位"探针),绝不强制了结、绝不伪造返回值 */ static void net_pending_tick(void) { /* 当前 tick */ @@ -2122,8 +2141,10 @@ static void net_pending_tick(void) { } } - /* ② 到点(tick 差值按有符号比较,抗回绕) */ - if ((int32_t)(now - p->deadline) >= 0) { + /* ② 到点(仅显式预算登记;无限等待登记无 deadline——N34 阶段 1.5: + * 超时是调用方策略,net_recv_to 的预算在此兑现,不是伪造。 + * tick 差值按有符号比较,抗回绕) */ + if ((p->infinite == 0U) && ((int32_t)(now - p->deadline) >= 0)) { /* 了结:超时(错误码由 net_pending_complete 记入本任务槽位) */ net_pending_complete(i, NET_ERR_TIMEDOUT); continue; @@ -2133,7 +2154,9 @@ static void net_pending_tick(void) { * 判据同 net_recv_locked(N-32 P1 不变量 I):`state == CLOSED || * session_dead`。只判 CLOSED 时,抢先 relisten 后 ①(要求 ESTABLISHED) * 与 ③(要求 CLOSED) 两头落空 → 挂起的 recv 只能干等 ② deadline 超时 - * (lftpd 阻塞 recv 的"消费者卡死"原型)。 */ + * (lftpd 阻塞 recv 的"消费者卡死"原型)。N34 阶段 1.5 后该形态更致命: + * 无限等待登记没有 ②,漏 ③ = 永久挂死(只剩 tripwire 留痕)—— + * ③ 与显式 cancel 是无限 recv 的全部唤醒责任方,勿动判据。 */ if ((p_sock->state == NET_SOCK_STATE_CLOSED) || (p_sock->session_dead != 0U)) { /* 查询残留字节数 */ int rem = ch395f_get_recv_len((uint8_t)i); @@ -2179,13 +2202,23 @@ static void net_pending_tick(void) { /* 其他状态:保持等待(见本区块头注释) */ - /* ④ 看门狗:到点 + 裕量仍未了结 = 完成逻辑缺位 → 强制了结并留痕 */ - if ((int32_t)(now - p->deadline) >= (int32_t)NET_PENDING_WATCHDOG_MS) { - /* 强制了结留痕(超截止时刻的毫秒数) */ - DBG_ERROR("sock%d pending recv stuck %ums past deadline (force settle)", - i, (unsigned)((int32_t)now - (int32_t)p->deadline)); - /* 强制了结:超时(错误码由 net_pending_complete 记入本任务槽位) */ - net_pending_complete(i, NET_ERR_TIMEDOUT); + /* ④ tripwire(N34 阶段 1.5 降级,原"看门狗强制了结"):挂起超 60s = + * "完成逻辑缺位"探针 → 只 [ERR] 留痕一次,**绝不强制了结、绝不伪造 + * TIMEDOUT 返回值**(旧实现到点 +1s 兜底伪造 TIMEDOUT,掩盖完成路径 + * 缺陷;新语义下无限等待本就无预算,伪造返回值反而违背调用方意图)。 + * 判据:无限登记按登记时刻计;显式登记按 deadline 计(② 正常不会让它 + * 越过 60s,能到这里本身就是完成逻辑缺位)。 */ + if (p->tripwire_logged == 0U) { + /* 探针基准时刻:无限 = 登记时刻;显式 = 截止时刻 */ + uint32_t base = (p->infinite != 0U) ? p->registered : p->deadline; + if ((int32_t)(now - base) >= (int32_t)NET_RECV_TRIPWIRE_MS) { + /* 留痕(挂起已持续毫秒数,自基准起算) */ + DBG_ERROR("sock%d recv pending %ums (base=%s): completion source missing", + i, (unsigned)((int32_t)now - (int32_t)base), + (p->infinite != 0U) ? "infinite" : "deadline"); + /* 只留痕一次(每轮挂起一条,防日志风暴) */ + p->tripwire_logged = 1U; + } } } } @@ -2437,17 +2470,26 @@ int net_send(int sockfd, const void *buf, int len, int flags) { } /* - * 函数功能:接收数据(线程安全版本,通过消息队列委托给 netTask) + * 函数功能:接收数据(线程安全版本,通过消息队列委托给 netTask; + * **默认无限阻塞**——N34 阶段 1.5 语义主流化,30s 默认预算已删) * 入口参数:sockfd - Socket 描述符 int >= 0 && < NET_MAX_SOCKETS * buf - 接收缓冲区指针 void * * len - 缓冲区大小 int > 0 - * flags - 标志位,0=阻塞,NET_MSG_DONTWAIT=非阻塞 int + * flags - 标志位,0=按 socket 阻塞属性等待,NET_MSG_DONTWAIT=单次非阻塞 int * 出口参数:无 * 返回值:实际接收字节数 int >= 0 || 0 || -1 * 0 - 对端关闭连接 int - * -1 - 接收失败(参数无效、队列满等) int + * -1 - 接收失败(参数无效、非阻塞无数据=WOULDBLOCK、 + * close/网络重建/陈旧重同步取消=CONNRESET 等,errno 可查) int * 限定条件:Socket 处于 ESTABLISHED 状态;net_init() + MX_FREERTOS_Init() 已执行;非 netTask 上下文 - * 函数说明:将接收请求放入消息队列,阻塞等待 netTask 处理完毕并返回结果。netTask 内请使用 net_recv_sock() + * 函数说明:1. 将接收请求放入消息队列,等待 netTask 处理完毕并返回结果。netTask 内请使用 net_recv_sock() + * 2. 无数据 + 阻塞(blocking 属性为默认值 1 且未置 DONTWAIT)→ 登记挂起 + * **无限等待**;唤醒源 = 数据 / EOF / close / 网络重建(CONNRESET); + * 会话死亡由对账① ~400ms 判死兜底,不存在"对端消失后永久挂死"窗口 + * 3. ⚠ 文档契约(嵌入式特有):IWDG required_mask 内任务禁止无限阻塞 + * recv——必须 net_select 超时或 net_recv_to 显式预算(超时归使用者 + * 对受监控任务是义务不是选项),否则"对端连上不发数据"= IWDG 复位 + * 4. 超时首选 net_select(...);net_recv_to(...) 为显式预算便捷入口 */ int net_recv(int sockfd, void *buf, int len, int flags) { /* 一次性消息结构体(队列按值存结构副本) */ @@ -2471,9 +2513,9 @@ int net_recv(int sockfd, void *buf, int len, int flags) { msg.buf = buf; /* 缓冲容量 */ msg.len = len; - /* 标志:0=阻塞,NET_MSG_DONTWAIT=非阻塞 */ + /* 标志:0=按 socket 阻塞属性,NET_MSG_DONTWAIT=单次非阻塞 */ msg.flags = flags; - /* 0 = 默认超时预算(netTask 侧用 NET_RECV_TIMEOUT_MS) */ + /* 0 = 无限等待(N34 阶段 1.5:无默认预算;显式预算走 net_recv_to) */ msg.timeout_ms = 0U; /* 调用方任务句柄(netTask 凭它回传回执) */ msg.caller = xTaskGetCurrentTaskHandle(); @@ -2499,7 +2541,28 @@ int net_recv(int sockfd, void *buf, int len, int flags) { } /* - * 函数功能:接收数据(带调用方指定超时的阻塞版,用于分片有界读) + * 函数功能:接收数据(非阻塞具名入口——N34 阶段 1.5 把 NET_MSG_DONTWAIT 路径转正, + * 对齐 BSD recv(MSG_DONTWAIT) / Python recv 非阻塞模式的"名如其义") + * 入口参数:sockfd - Socket 描述符 int >= 0 && < NET_MAX_SOCKETS + * buf - 接收缓冲区指针 void * + * len - 缓冲区大小 int > 0 + * 出口参数:buf - 填充接收到的数据 + * 返回值:实际接收字节数(> 0);0 = 对端关闭(net 层已自动重监听); int >= 0 || -1 + * -1 = 错误(无数据 = NET_ERR_WOULDBLOCK,其余 errno 可查) int + * 限定条件:非 netTask 上下文;net 层已初始化 + * 函数说明:1. 等价 net_recv(sockfd, buf, len, NET_MSG_DONTWAIT):单次强制非阻塞, + * 不改变 socket 的 blocking 属性(模式用 net_setblocking) + * 2. 受 IWDG 监控 / 有周期职责的任务(netTask 除外)的推荐默认入口; + * 需要"等但不无限等"用 net_select / net_recv_to + */ +int net_recv_nb(int sockfd, void *buf, int len) { + return net_recv(sockfd, buf, len, NET_MSG_DONTWAIT); +} + +/* + * 函数功能:接收数据(带调用方指定超时的阻塞版,用于分片有界读; + * N34 阶段 1.5 起为**显式预算**便捷入口,等价 Python settimeout, + * 不再有隐藏默认值) * 入口参数:sockfd - Socket 描述符 int >= 0 && < NET_MAX_SOCKETS * buf / len - 接收缓冲与容量 void * / int > 0 * timeout_ms - 单次占用 netTask 的最长等待(毫秒,>0) uint32_t @@ -2507,10 +2570,11 @@ int net_recv(int sockfd, void *buf, int len, int flags) { * 返回值:>0 字节数;0 = 对端关闭(EOF);-1 = 真错误(errno 可查);**-2 = 超时无数据** * 限定条件:非 netTask 上下文;net 层已初始化 * 函数说明:1. E-02 修复配套接口:lftpd 等"等对端下一行/下一块数据"的调用方 - * 若用默认 30s 阻塞 recv,会把 netTask(全部 socket 操作的唯一执行者)长期占住 + * 若无限阻塞 recv,会把 netTask(全部 socket 操作的唯一执行者)长期占住 * 2. 导致其余任务的网络操作整体冻结(M3 实测一次空闲 FTP 登录冻结远端显示 17.7s) * 3. 用本接口把等待切成 ≤timeout_ms 的片,调用方自行累计总预算:总超时语义不变、单次占用有界 * 4. 超时用独立返回码 -2(而非靠 errno 区分)→ 无跨任务 errno 竞态 + * 5. ⚠ IWDG required_mask 内任务必须走本接口或 net_select(net_recv 契约) */ int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms) { /* 一次性消息结构体:队列按值存结构副本(仅指针、不拷数据),入队后由 netTask 取出处理 */ @@ -2528,8 +2592,9 @@ int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms) { return NET_ERR_RETURN_1(NET_ERR); } - /* ---- 3. 分片长度检查:timeout_ms = 0 在 netTask 侧意味着"改用默认 - * NET_RECV_TIMEOUT_MS(30 秒)",恰是本接口要规避的长阻塞,直接拒绝 ---- */ + /* ---- 3. 分片长度检查:timeout_ms = 0 在 netTask 侧意味着"**无限等待**" + * (N34 阶段 1.5 删除 30s 默认预算),恰与本接口"有界等待"的存在 + * 理由相反,直接拒绝 ---- */ if (timeout_ms == 0U) { /* 分片长度必须 > 0 */ return NET_ERR_RETURN_1(NET_ERR_INVAL); @@ -2780,14 +2845,14 @@ int net_sendto(int sockfd, const void *buf, int len, int flags, * 限定条件:Socket 已创建且为 UDP 类型(SOCK_DGRAM);net_init() 已调用;有数据可用 * 函数说明:1. 轮询检查芯片接收缓冲(net_poll + get_recv_len),有数据即返回 * 2. UDP 数据报前 8 字节为报文头(源端口+源 IP),据此填充 src_addr - * 3. 无数据时:DONTWAIT 立即返回 -1(WOULDBLOCK);默认等待 NET_RECV_TIMEOUT_MS 后返回 TIMEDOUT + * 3. 无数据时(N34 阶段 1.5,与 net_recv 同构):DONTWAIT / 非阻塞属性 + * 立即返回 -1(WOULDBLOCK);阻塞模式**无限等待**(30s 默认预算已删; + * 本接口为轮询自旋实现且无生产调用者,受 IWDG 监控的任务勿用无限形态) */ int net_recvfrom(int sockfd, void *buf, int len, int flags, struct net_sockaddr *src_addr, int *addrlen) { /* Socket 控制块指针 */ net_sock_t *p_sock; - /* 起始时刻(超时判定基准,tick) */ - uint32_t tick_start; /* 参数检查 */ if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { @@ -2807,10 +2872,7 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags, return NET_ERR_RETURN_1(NET_ERR_INVAL); } - /* 记录起始时刻(超时基准) */ - tick_start = HAL_GetTick(); - - /* 轮询直到:有数据 / 非阻塞 / 超时 */ + /* 轮询直到:有数据 / 非阻塞(N34 阶段 1.5:阻塞 = 无限等待,无默认预算) */ while (1) { /* 轮询一次:更新状态机与可读标志 */ net_poll(); @@ -2862,18 +2924,16 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags, } } - /* 非阻塞模式:无数据立即返回 */ - if (flags & NET_MSG_DONTWAIT) { + /* 非阻塞模式:NET_MSG_DONTWAIT 单次覆盖,或 socket blocking 属性为 0 + * (N34 阶段 1.5,与 net_recv_locked 同判据):无数据立即返回 */ + if ((flags & NET_MSG_DONTWAIT) || (p_sock->blocking == 0U)) { /* 置 WOULDBLOCK */ return NET_ERR_RETURN_1(NET_ERR_WOULDBLOCK); } - /* 超时检查 */ - if (NET_RECV_TIMEOUT_MS > 0 && - (HAL_GetTick() - tick_start) >= NET_RECV_TIMEOUT_MS) { - /* 置 TIMEDOUT */ - return NET_ERR_RETURN_1(NET_ERR_TIMEDOUT); - } + /* 阻塞模式:无数据继续轮询等待(N34 阶段 1.5:无限等待,无默认预算—— + * 旧 NET_RECV_TIMEOUT_MS 30s 兜底已删;唤醒源 = 数据,与 net_recv 的 + * 差异是本接口为自旋实现,勿在受 IWDG 监控任务中使用无限形态) */ } } @@ -2916,7 +2976,8 @@ int net_recv_sock(net_sock_t *p_sock, void *buf, int len, int flags) { return NET_ERR_RETURN_1(NET_ERR_BADF); } /* 直接委托锁定实现(netTask 上下文,无队列往返); - * 0 = 默认 NET_RECV_TIMEOUT_MS;caller = netTask 自身(自调用) */ + * timeout_ms = 0(无限等待语义,但 netTask 自调用会被挂起登记拒绝 → + * 本接口实际仅 DONTWAIT / 非阻塞属性可用);caller = netTask 自身 */ return net_recv_locked(p_sock, buf, len, flags, 0U, xTaskGetCurrentTaskHandle()); } @@ -2975,6 +3036,46 @@ int net_set_event_cb(int sockfd, net_event_cb_t cb, void *arg) { /* 注册成功 */ return 0; } + +/* + * 函数功能:设置 Socket 阻塞/非阻塞**模式属性**(N34 阶段 1.5,模式挂控制块属性, + * 对齐 BSD O_NONBLOCK / Python setblocking) + * 入口参数:sockfd - Socket 描述符 int >= 0 && < NET_MAX_SOCKETS + * on - 1 = 阻塞(net_socket 默认);0 = 非阻塞 int + * 出口参数:无 + * 返回值:0 - 设置成功 int + * -1 - 失败(描述符越界 / 未占用) int + * 限定条件:socket 已创建(in_use == 1);任意上下文(同 net_set_event_cb 先例) + * 函数说明:1. 属性对 net_recv / net_recv_sock 生效:非阻塞下无数据立即 + * NET_ERR_WOULDBLOCK;EOF 判定语义不变(CLOSED/session_dead 无数据 + * 仍返回 0 并触发自动重监听) + * 2. 直写控制块单字节(ARM 原子写,先例 net_set_event_cb):与 netTask + * 竞态的最坏结果 = 一次在途 recv 用旧模式,无撕裂、无需过消息队列 + * 3. 与超时预算正交:模式是粘滞属性;单次预算用 net_recv_to / net_select + */ +int net_setblocking(int sockfd, int on) { + /* Socket 控制块指针 */ + net_sock_t *p_sock; + + /* 参数检查:描述符范围 */ + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { + /* 描述符越界 → BADF */ + return NET_ERR_RETURN_1(NET_ERR_BADF); + } + + /* 取控制块 */ + p_sock = &s_net_socks[sockfd]; + /* 控制块未在用 → BADF */ + if (!p_sock->in_use) { + return NET_ERR_RETURN_1(NET_ERR_BADF); + } + + /* 写模式属性:非 0 = 阻塞,0 = 非阻塞(单字节原子写) */ + p_sock->blocking = (on != 0) ? 1U : 0U; + + /* 设置成功 */ + return 0; +} /* * 函数功能:获取 **本任务** 最后一次 net_*() 调用失败时的精确错误码 * 入口参数:无 diff --git a/Drivers/BSP/NET/net_socket.h b/Drivers/BSP/NET/net_socket.h index 3915b3a0..bbd4a319 100644 --- a/Drivers/BSP/NET/net_socket.h +++ b/Drivers/BSP/NET/net_socket.h @@ -48,7 +48,8 @@ typedef struct { void *buf; /* 数据缓冲区(send/recv) */ int len; /* 数据长度/缓冲区大小 */ int flags; /* 接收标志 */ - uint32_t timeout_ms; /* 接收超时(入队方向;0 = 用默认 NET_RECV_TIMEOUT_MS) */ + uint32_t timeout_ms; /* 接收超时(入队方向;0 = 无限等待,无默认预算—— + * N34 阶段 1.5 删除 NET_RECV_TIMEOUT_MS 30s 默认) */ TaskHandle_t caller; /* 调用方任务句柄 */ int result; /* 操作结果 */ /* 连接参数 */ @@ -230,19 +231,55 @@ int net_connect(int sockfd, const struct net_sockaddr *addr, int addrlen); int net_send(int sockfd, const void *buf, int len, int flags); /* - * 函数功能:接收数据(TCP) + * 函数功能:接收数据(TCP,**默认无限阻塞**——N34 阶段 1.5 语义主流化) * 入口参数:sockfd - socket 描述符 * buf - 接收缓冲区 * len - 缓冲区大小 - * flags - 通常为 0,可设置 NET_MSG_DONTWAIT 非阻塞 + * flags - 通常为 0;NET_MSG_DONTWAIT 单次强制非阻塞 * 返回值:实际接收字节数,0=对端关闭,-1=错误 * 限定条件:TCP 连接已建立 - * 函数说明:在阻塞模式下,如果没有数据,函数会等待; - * 在非阻塞模式下,如果没有数据,返回 -1 并设置 - * net_errno 为 NET_ERR_WOULDBLOCK + * 函数说明:1. 无数据时**无限等待**(无默认超时预算,对齐 POSIX/Python:模式归 + * 使用者、超时归调用方);唤醒源 = 数据 / EOF / close / 网络重建 + * (NET_ERR_CONNRESET)——会话死亡由对账①在 ~400ms 内判死兜底, + * 不存在"对端消失后永久挂死"的窗口 + * 2. 模式属性:net_setblocking(fd, 0) 后本接口无数据立即返回 + * NET_ERR_WOULDBLOCK;NET_MSG_DONTWAIT 仅单次覆盖该属性 + * 3. ⚠ 文档契约(嵌入式特有):IWDG required_mask 内任务**禁止无限 + * 阻塞 recv**——必须 net_select 超时或 net_recv_to 显式预算, + * 否则"对端连上不发数据"= IWDG 复位(超时归使用者对受监控任务是义务) + * 4. 超时首选 net_select(r,w,e,timeout_ms);net_recv_to 为显式预算便捷入口 */ int net_recv(int sockfd, void *buf, int len, int flags); +/* + * 函数功能:接收数据(非阻塞具名入口,原 NET_MSG_DONTWAIT 路径转正——N34 阶段 1.5) + * 入口参数:sockfd - socket 描述符 + * buf - 接收缓冲区 + * len - 缓冲区大小 + * 返回值:实际接收字节数;0 = 对端关闭(net 层已自动重监听); + * -1 = 错误(无数据 = NET_ERR_WOULDBLOCK,其余 errno 可查) + * 限定条件:非 netTask 上下文(netTask 内用 net_recv_sock + DONTWAIT) + * 函数说明:等价 net_recv(sockfd, buf, len, NET_MSG_DONTWAIT); + * 不改变 socket 的 blocking 属性(模式用 net_setblocking 设置) + */ +int net_recv_nb(int sockfd, void *buf, int len); + +/* + * 函数功能:设置 Socket 阻塞/非阻塞**模式属性**(对齐 BSD O_NONBLOCK / Python + * setblocking——N34 阶段 1.5,模式挂 socket 控制块属性) + * 入口参数:sockfd - socket 描述符 + * on - 1 = 阻塞(net_socket 默认);0 = 非阻塞 + * 返回值:0 成功;-1 失败(描述符无效/未占用) + * 限定条件:socket 已创建 + * 函数说明:1. 属性对 net_recv / net_recv_sock 生效:非阻塞模式下无数据立即 + * 返回 NET_ERR_WOULDBLOCK(EOF 判定语义不变:CLOSED/session_dead + * 无数据仍返回 0 并触发自动重监听) + * 2. 直写控制块单字节(先例 net_set_event_cb),任意上下文可调; + * 最坏一次在途 recv 用旧模式,无撕裂风险 + * 3. 与超时预算正交:模式是粘滞属性,单次预算用 net_recv_to/net_select + */ +int net_setblocking(int sockfd, int on); + /* * 函数功能:接收数据(**带调用方指定超时**的阻塞版,用于分片有界读) * 入口参数:sockfd - Socket 描述符 @@ -250,10 +287,14 @@ int net_recv(int sockfd, void *buf, int len, int flags); * timeout_ms - 单次调用在 netTask 内的最长等待(毫秒,>0) * 返回值:>0 实际字节数;0 = 对端关闭(EOF);-1 = 真错误(errno 可查);**-2 = 超时无数据** * 限定条件:非 netTask 上下文;Socket 已连接 - * 函数说明:为"不能长时间占用 netTask"的调用方(如 lftpd 控制行/数据读)提供 - * **分片**阻塞:每次调用最多占用 netTask timeout_ms,返回 -2 表示这段 - * 时间内无数据,调用方自行累计总预算并决定是否继续(E-02 头阻塞修复)。 - * 超时用独立返回码而非 errno → 无跨任务 errno 竞态。 + * 函数说明:1. **显式预算**便捷入口(等价 Python settimeout,N34 阶段 1.5 起 + * 不再有隐藏默认值):net_recv 的无限阻塞 + 本接口的显式预算构成 + * 超时语义的全部来源;超时首选仍是 net_select + * 2. E-02 修复配套:lftpd 等"等对端下一行/下一块数据"的调用方 + * 若无限阻塞 recv,会把 netTask(全部 socket 操作的唯一执行者)长期占住 + * 3. 用本接口把等待切成 ≤timeout_ms 的片,调用方自行累计总预算:总超时语义不变、单次占用有界 + * 4. 超时用独立返回码 -2(而非靠 errno 区分)→ 无跨任务 errno 竞态 + * 5. ⚠ IWDG required_mask 内任务必须走本接口或 net_select(见 net_recv 契约) */ int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms); @@ -277,12 +318,16 @@ int net_sendto(int sockfd, const void *buf, int len, int flags, * 入口参数:sockfd - socket 描述符 * buf - 接收缓冲区 * len - 缓冲区大小 - * flags - 通常为 0 + * flags - 通常为 0;NET_MSG_DONTWAIT 单次强制非阻塞 * src_addr - 输出:发送方地址 * addrlen - 输入输出:地址长度 * 返回值:实际接收字节数,失败返回 -1 * 限定条件:socket 已创建,指针非空 - * 函数说明:读取 CH395F 接收缓冲区,获取对端地址 + * 函数说明:1. 读取 CH395F 接收缓冲区,获取对端地址 + * 2. 阻塞语义与 net_recv 同(N34 阶段 1.5):blocking 属性下无数据 + * 无限等待,DONTWAIT / 非阻塞属性立即 NET_ERR_WOULDBLOCK; + * 本接口为轮询自旋实现(历史遗留,UDP 专用),受 IWDG 监控的 + * 任务同样禁止对其无限等待 */ int net_recvfrom(int sockfd, void *buf, int len, int flags, struct net_sockaddr *src_addr, int *addrlen); diff --git a/Drivers/BSP/NET/net_types.h b/Drivers/BSP/NET/net_types.h index 3a218ed1..35cfbd42 100644 --- a/Drivers/BSP/NET/net_types.h +++ b/Drivers/BSP/NET/net_types.h @@ -47,7 +47,9 @@ extern "C" { /* * 接收标志定义 */ -#define NET_MSG_DONTWAIT 0x01 /* 非阻塞模式 */ +#define NET_MSG_DONTWAIT 0x01 /* 非阻塞模式(单次覆盖 socket 的 blocking 属性; + * 具名入口见 net_recv_nb() / net_setblocking(), + * N34 阶段 1.5) */ /* * 网络字节序转换宏 @@ -171,6 +173,15 @@ typedef struct { /* 单连接模式自动重监听:连接断开后由 net 层自动回到 LISTEN(BSD 风格监听常驻) */ uint8_t auto_relisten; + /* N34 阶段 1.5(2026-10-13):阻塞/非阻塞**模式属性**(对齐 BSD O_NONBLOCK / + * Python setblocking 的"模式归使用者"模型)。 + * 1 = 阻塞(net_socket() 默认);0 = 非阻塞(net_setblocking(fd, 0))。 + * net_recv() 无 NET_MSG_DONTWAIT 时按本属性决定"无数据 → 登记挂起等待" + * 还是"立即 WOULDBLOCK"。单字节写(ARM 原子),写入先例同 event_cb + * (net_setblocking 直写控制块,最坏一次 recv 用旧模式,无撕裂风险)。 + * 注意与"超时预算"正交:模式是属性(粘滞),预算是单次参数(net_recv_to)。 */ + uint8_t blocking; + /* 释放来源标记(Trap 23 待办 #3):1 = 异常释放(芯片 TIMEOUT,对端 RST/掉链), * 0 = 正常释放(芯片 DISCONNECT,对端 FIN)或新连接。L3 挂起 recv 的了结断连 * 分支据此决定回 EOF(0) 还是 NET_ERR_CONNRESET:异常结束上报应用(lftpd STOR) diff --git a/Drivers/BSP/UI/ui_net_sink.c b/Drivers/BSP/UI/ui_net_sink.c index ac59a58b..3699f152 100644 --- a/Drivers/BSP/UI/ui_net_sink.c +++ b/Drivers/BSP/UI/ui_net_sink.c @@ -545,8 +545,10 @@ void ui_net_sink_poll(void) { return; } - /* 有客户端时非阻塞收帧(I-1 门控 + I-2 兜底): - * 无数据可读 → 多数轮次直接返回,省掉一次 net_recv 往返(该往返内部 + /* 有客户端时非阻塞收帧(I-1 门控 + I-2 兜底;N34 阶段 1.5:DONTWAIT 转正 + * 为具名 API net_recv_nb——uiTask 在 IWDG required_mask 内且有 10ms UI 推流 + * 周期职责,禁止无限阻塞 recv,非阻塞轮询是义务): + * 无数据可读 → 多数轮次直接返回,省掉一次 recv 往返(该往返内部 * osMessageQueuePut(osWaitForever) + xTaskNotifyWait(portMAX_DELAY),无上界); * 但每 RM_LIVENESS_DIV 轮无条件收一次,防 RECV_OK 中断丢失把数据永久藏住 */ if (net_can_read(g_client) == 0) { @@ -556,7 +558,7 @@ void ui_net_sink_poll(void) { } else { s_liveness_ctr = 0U; } - n = net_recv(g_client, g_rx, (int)sizeof(g_rx), NET_MSG_DONTWAIT); + n = net_recv_nb(g_client, g_rx, (int)sizeof(g_rx)); if (n > 0) { for (int i = 0; i < n; i++) { rm_parser_feed((uint8_t)g_rx[i]); diff --git a/Drivers/BSP/UI/ui_net_sink.h b/Drivers/BSP/UI/ui_net_sink.h index f5ca105d..bc2b1536 100644 --- a/Drivers/BSP/UI/ui_net_sink.h +++ b/Drivers/BSP/UI/ui_net_sink.h @@ -45,8 +45,10 @@ int ui_net_sink_start(uint16_t port); * 返回值:无 * 限定条件:ui_net_sink_start() 已成功调用 * 函数说明:1. 无客户端时先门控(net_listener_armed,纯 RAM 读)再尝试接受连接 - * 2. 有客户端时先门控(net_can_read)再用 DONTWAIT 收帧并解析 + * 2. 有客户端时先门控(net_can_read)再用 net_recv_nb 非阻塞收帧并解析 * (KEEPLIVE/INIT/KEY/LCDMEM);门控判为无数据时仍每 32 轮兜底收一次 + * (N34 阶段 1.5:DONTWAIT 转正为具名 API;uiTask 受 IWDG 监控, + * 非阻塞是契约义务——见 net_socket.h net_recv 文档契约) * 3. 对端关闭或异常时复位客户端,等待下次重监听 * ⚠ **不是"零成本非阻塞"**:门控只消除无谓往返;真有活时仍是 * "入队 + 等 netTask 回执"(osMessageQueuePut(osWaitForever) + diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 3ad954e9..0ecfa972 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -36,10 +36,10 @@ | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 228,172 B (222.82 KB) | 1,048,576 B (1024 KB) | 21.8% | -| RW_IRAM1 | 97,864 B (95.57 KB) | 131,072 B (128 KB) | 74.7% | +| ER_IROM1 | 228,372 B (223.02 KB) | 1,048,576 B (1024 KB) | 21.8% | +| RW_IRAM1 | 97,896 B (95.60 KB) | 131,072 B (128 KB) | 74.7% | | RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | -| **Flash (ROM 合计)** | **229,276 B (223.9 KB)** | 1,048,576 B (1024 KB) | **21.9%** | +| **Flash (ROM 合计)** | **229,476 B (224.1 KB)** | 1,048,576 B (1024 KB) | **21.9%** | 各区的容量与常量来源: diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index f3a05d8b..1056b566 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -15,14 +15,14 @@ | 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) | |---|---|---|---|---| -| ER_IROM1 (Flash) | 228,172 B | 1,048,576 B | 21.8% | 参考 | +| ER_IROM1 (Flash) | 228,372 B | 1,048,576 B | 21.8% | 参考 | | RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS | | UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 | -| RW_IRAM1 | 97,864 B | 131,072 B | 74.7% | PASS | +| RW_IRAM1 | 97,896 B | 131,072 B | 74.7% | PASS | | 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 | |---|---|---|---| -| 网络 | 13,410 B | 12 KB | ⚠ 超 1.1 KB | +| 网络 | 13,442 B | 12 KB | ⚠ 超 1.1 KB | | 存储 | 7,056 B | 12 KB | OK | | UI | 54,988 B | 16 KB | ⚠ 超 37.7 KB | | RTOS 堆 | 57,376 B | 56 KB | 堆=配额 | diff --git a/docs/子系统设计/网络Socket监听生命周期设计说明.md b/docs/子系统设计/网络Socket监听生命周期设计说明.md index bfc402ba..00850856 100644 --- a/docs/子系统设计/网络Socket监听生命周期设计说明.md +++ b/docs/子系统设计/网络Socket监听生命周期设计说明.md @@ -116,6 +116,14 @@ B10 行)→ 中断丢了,bit3=0 同样停在已关/半关态,孤儿 Socket | 关闭姿势必须正确 | Trap 14/17:陈旧 DISCONNECT 锁存秒断下一条连接 | `net_close_locked` 既有「close → 等 `CLOSED` → 排空本 Socket 中断」,是 bit3=1 下唯一正确的关闭序列 | | 异常释放须先排空残渣再关 | Trap 23:~2KB RX 残渣被下条连接当自己的数据读走 | `net_relisten_locked` 隔离期(400 ms)持续排空 RX + 主动 close + 等 `CLOSED/TCP_CLOSED` | +> **命名注(N34 阶段 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/网络/N34-自愈审计/实施方案.md` §3.5。本文所有「DONTWAIT 判据 / +> 返 WOULDBLOCK」表述按新命名逐字仍成立,不逐处改写。 + **⑥ 本次(P0)已落地的口径修正**:`net_socket.c:3627` / `:3641` 原注释写「DISCONNECT 后 芯片自动关闭 Socket(FUN_PARA 默认)」——与实际 `0x08` **相反**。这类注释正是 U12 这类 「跳过显式 close」缺陷的观念源头,已改正并留痕。 diff --git a/docs/测试说明/模拟器差距清单.md b/docs/测试说明/模拟器差距清单.md index 7bb8905d..f7f4266d 100644 --- a/docs/测试说明/模拟器差距清单.md +++ b/docs/测试说明/模拟器差距清单.md @@ -42,10 +42,10 @@ |---|---|---|---|---|---| | N-1 | **无 pending 机制** | `net_accept`/`net_send` 直接失败或阻塞于宿主 socket;无"挂起队列"概念 | 异步 TCP 状态机,`accept`/`send` 可处于 pending,由 `net_poll` 推进 | **E-06**(accept 轮询计时失真)、**E-20**(lftpd 控制通道 errno 分类)、**E-21**(timeSync errno 分级)等在模拟器上无法复现 | `@test` + FTP 套件;N-4 二期专门建模 | | N-2 | **代次号恒为 0** | `net_get_generation()` 恒返 0(无"网络重建"概念) | 每次 `net_stack_bring_up` / 重配置递增,各 TCP 服务据此重建监听 | "网络重建后服务是否重建监听" 的逻辑**在模拟器上恒走同一分支** | `@test` 网络重配置用例(UI 改 IP 免重启生效) | -| N-3 | **`net_process_messages` / `net_recv_to` 等缺失** | 未实现(MCU 专属:CH395F 消息队列驱动、带超时接收) | 由 `netMsgQueue` 串行化 SPI 事务、支持毫秒级超时接收 | 依赖"带超时接收"与"消息泵"的上层逻辑在模拟器上**不可编译/不可运行** | 板端 CH395F / NET 套件 | +| N-3 | ~~`net_recv_to` 缺失~~(**2026-10-13 N34 阶段 1.5 已消除**:sim 以 `GetTickCount64` + `Sleep(1)` 轮询兑现预算,含 -2 返回码与 `timeout_ms=0` 拒 INVAL);`net_process_messages` 仍缺失 | `net_recv_to` 已实现(包装层模拟);消息泵未实现(MCU 专属:CH395F 消息队列驱动) | 由 `netMsgQueue` 串行化 SPI 事务、支持毫秒级超时接收 | 依赖"消息泵"的上层逻辑在模拟器上**不可编译/不可运行**;带超时接收的**外部语义**已可双端共测,但预算兑现精度为 Sleep(1) 粒度 | 板端 CH395F / NET 套件 | | N-4 | **Socket 失效语义不同** | 以 `in_use` 判活(`net_socket_sim.c` 设计要点 4) | 监听 Socket 随客户端一同失效,由 net 层 `auto_relisten`/pump 自动重建(N34 阶段1 后无手动 `net_relisten`) | "客户端断线 → 监听失效 → 重连" 的黏滞状态在模拟器上不出现 | `check_remo_sink.py` 覆盖重连;真机 `@test` | | N-5 | **单连接 backlog** | `NET_SIM_BACKLOG = 1` | CH395F 多 socket 并发 | 多客户端并发场景(FTP + 推流 + timeSync 同时)不可在模拟器验证 | 板端 NET 套件 + 台架并发场景 | -| N-6 | **非阻塞语义模拟** | 创建即 `FIONBIO`,使 `DONTWAIT` 语义与 MCU 对齐(设计要点 2) | 中断驱动 + 状态机,天然非阻塞 | ——(此项是**对齐**而非差距,登记以说明"为何模拟器能测非阻塞路径") | —— | +| N-6 | **非阻塞语义模拟** | 创建即 `FIONBIO`(Winsock 句柄恒非阻塞,UI 主循环安全底线);**阻塞/非阻塞按控制块 `blocking` 属性在包装层模拟**(N34 阶段 1.5:`net_recv` 无限轮询等待 / `net_recv_nb` / `NET_MSG_DONTWAIT` 立即 WOULDBLOCK,`net_setblocking` 双端同名) | 中断驱动 + 状态机 + L3 挂起 recv(等待睡任务通知,netTask 零占用) | 外部语义**对齐**(登记以说明"为何模拟器能测非阻塞路径与显式预算路径");内部挂起模型(N-1)仍不建模 | —— | ### 2.2 日志与配置 @@ -109,4 +109,5 @@ | 日期 | 变更 | |---|---| +| 2026-10-13 | **N34 阶段 1.5 recv 语义主流化**:`net_socket_sim.c` 镜像 `net_recv_nb` / `net_recv_to` / `net_setblocking`,阻塞语义改按控制块 `blocking` 属性在包装层模拟(Winsock 句柄仍恒 FIONBIO)——N-3 的 `net_recv_to` 缺失部分**已消除**(划线留档),N-6 改写为"模式属性 + 包装层模拟";内部挂起模型(N-1)不变 | | 2026-10-02 | 初版(S13):收录 6 条网络差距(N-1~N-6)、4 条日志/配置差距(L-1~L-4)、7 条硬件专属差距(H-1~H-7)、4 条"有意对齐"项。证据来源:`net_socket_sim.c` 文件头设计要点 + 双端导出函数逐一对比(MCU 有而 sim 缺 9 个)+ 各子系统文档既有记载汇总。§3 给出 6 条"必须回真机"决策规则 | diff --git a/docs/测试说明/链路延迟测试说明.md b/docs/测试说明/链路延迟测试说明.md index e7974a17..d22c0403 100644 --- a/docs/测试说明/链路延迟测试说明.md +++ b/docs/测试说明/链路延迟测试说明.md @@ -91,7 +91,7 @@ sequenceDiagram | TCP 重传 | **8 次 × 200ms = 1.6s 窗口**(固定周期,非 RTT 自适应;P0-a 2026-09-22 实装,原 8×500ms=4s) | `NET_RETRANS_COUNT/PERIOD_MS`(`net_config.h`) | | SPI2 速率 | APB1 42MHz ÷ 8 ≈ **5.25 Mbit/s** → 1KB ≈ **1.6ms**(分频 8 为 PCB 约束,勿"优化") | `Src/spi.c` | | 发送等待超时 | 1s(`NET_SEND_TIMEOUT_MS`) | `net_config.h` | -| 接收超时 | 30s(`NET_RECV_TIMEOUT_MS`) | `net_config.h` | +| 接收超时 | **无默认预算**(N34 阶段 1.5:`net_recv` 无限阻塞,超时归调用方——`net_select` / `net_recv_to`;原 30s `NET_RECV_TIMEOUT_MS` 已删) | `net_config.h`(宏已移除) | | 调试串口 | USART1 115200,**逐字节阻塞发送**(`fputc` = `HAL_UART_Transmit` 单字节) | `Src/usart.c` | | LVGL 刷新周期 / 色深 | 20ms / 1bpp(`RENDER_MODE_FULL`) | `lv_conf.h`、`ui_port.c` | diff --git a/docs/网络/BSD_Socket_API_使用指南.md b/docs/网络/BSD_Socket_API_使用指南.md index 95e7dd84..400d0414 100644 --- a/docs/网络/BSD_Socket_API_使用指南.md +++ b/docs/网络/BSD_Socket_API_使用指南.md @@ -41,9 +41,11 @@ | 特性 | Linux Socket | 本实现 | |------|--------------|--------| | Socket 数量 | 无限制 | 最多 8 个 | -| 非阻塞模式 | 支持 | 通过 `net_poll()` 实现 | +| 阻塞默认 | 默认阻塞(无隐藏超时) | **同**(N34 阶段 1.5:`net_recv()` 无限阻塞,超时归调用方——`net_select` / `net_recv_to`) | +| 非阻塞模式 | `O_NONBLOCK` / `MSG_DONTWAIT` | `net_setblocking(fd, 0)`(模式属性)/ `net_recv_nb()` / `NET_MSG_DONTWAIT`(单次) | | 错误码 | errno | `net_get_errno()` | | 头文件 | `` | `"net_socket.h"` | +| 无限阻塞约束 | 无 | ⚠ IWDG `required_mask` 内任务**禁止**(嵌入式特有契约,§10.1) | --- @@ -79,7 +81,7 @@ PC 模拟器(`simulator/`,CMake + SDL2)与 MCU 端共用同一份上层网 | 底层后端 | CH395F(SPI2) | Winsock2(链接 `ws2_32`) | | `net_init()` | 芯片初始化 / 复位 / 配置 IP·MAC / 分配 Socket 缓冲 | 仅确保 `WSAStartup`(幂等);ip/mask/gw/mac 四参数全部忽略 | | fd 模型 | 控制块索引 0~7 | 相同(并行数组 `s_ws[]` 存 Winsock 句柄,不侵入共享的 `net_sock_t`) | -| 并发 / 阻塞 | 消息队列 + `netTask` 串行化 | 单线程直调;所有 Socket 创建即非阻塞(FIONBIO),主循环直接轮询 | +| 并发 / 阻塞 | 消息队列 + `netTask` 串行化 | 单线程直调;Winsock 句柄创建即非阻塞(FIONBIO),阻塞语义按控制块 `blocking` 属性在包装层模拟(`net_recv` 轮询等待 / `net_recv_nb` 立即返回,N34 阶段 1.5 与 MCU 端对齐) | | `net_poll()` | CH395F 中断轮询 + 状态机更新 | 空操作(返回 0,非阻塞 I/O 直接反映事件) | | `net_accept()` | 单连接:监听 Socket 自身即数据通道(返回监听 Socket 自身) | Winsock 语义:返回独立于监听 Socket 的新 fd(监听 Socket 保持 `LISTENING`) | | 事件回调 | `net_poll` 检测后主动触发 | 仅存储,不主动触发(上层自行非阻塞轮询感知) | @@ -174,7 +176,8 @@ int net_close_sock(net_sock_t *p_sock); - **消息队列由 CubeMX 创建**:在 `MX_FREERTOS_Init()`(`osKernelInitialize()` 之后)中调用 `osMessageQueueNew(8, sizeof(net_msg_t), NULL)` 创建 - **队列句柄全局可见**:通过 `extern osMessageQueueId_t netMsgQueueHandle` 引用 - **`netTask` 不可向自身发消息**:`netTask` 中直接调 `net_send()` 会死锁(等待自己处理消息),必须在 `netTask` 中使用 `net_send_sock()` 等内部版本 -- **阻塞 recv 不再独占 netTask(L3 pending 化,2026-09-22)**:无数据时公共 `net_recv()` / `net_recv_to()` **登记挂起请求后返回**(每 fd 至多一个,禁止覆盖——覆盖即丢失前调用方通知 = 永久挂死);完成由 `net_pending_tick()` 按主循环节拍检查(①数据 ②到点 ③断连,看门狗 1s)并经任务通知唤醒调用方——等待期间调用方在自己上下文睡通知,netTask 零占用(消除 E-02 头阻塞)。副作用:`buf` 指针须保持有效至返回;close / 网络重建 / 陈旧重同步会**显式取消并通知**(`NET_ERR_CONNRESET` 等),调用方永不挂死 +- **阻塞 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 语义主流化(N34 阶段 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 挂起等待**不计入**队列占用(完成前队列中无消息滞留) - **哪些操作要求 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) @@ -919,7 +922,7 @@ int net_send(int sockfd, const void *buf, int len, int flags); | `flags` | 通常为 0 | | 返回值 | 实际发送字节数,失败返回 -1 | -#### net_recv - 接收数据 +#### net_recv - 接收数据(默认无限阻塞) ```c int net_recv(int sockfd, void *buf, int len, int flags); @@ -930,12 +933,43 @@ int net_recv(int sockfd, void *buf, int len, int flags); | `sockfd` | socket 描述符 | | `buf` | 接收缓冲区(须保持有效至调用返回) | | `len` | 缓冲区大小 | -| `flags` | 0=阻塞,`NET_MSG_DONTWAIT`=非阻塞 | -| 返回值 | 实际接收字节数 (>0);`0`=对端关闭(net 层随即自动重监听);`-1`=错误(非阻塞无数据=`NET_ERR_WOULDBLOCK`;阻塞预算内无数据=`NET_ERR_TIMEDOUT`,默认 30s `NET_RECV_TIMEOUT_MS`;close/网络重建/陈旧重同步取消=`NET_ERR_CONNRESET`) | +| `flags` | 0=按 socket 阻塞属性等待,`NET_MSG_DONTWAIT`=单次强制非阻塞 | +| 返回值 | 实际接收字节数 (>0);`0`=对端关闭(net 层随即自动重监听);`-1`=错误(非阻塞无数据=`NET_ERR_WOULDBLOCK`;close/网络重建/陈旧重同步取消=`NET_ERR_CONNRESET`;**阻塞形态无"预算内无数据"一档——默认无限等待,超时是调用方策略**) | -> **L3 pending 化(2026-09-22)**:无数据时阻塞接收**不占 netTask**——登记挂起请求(每 fd 至多一个,禁止覆盖:覆盖会丢失前调用方通知 = 永久挂死)后**立即返回**;完成按主循环节拍检查(数据/到点/断连,看门狗 1s)并经任务通知唤醒调用方。等待期间调用方在自己上下文睡通知,netTask 全程零占用(消除 E-02 头阻塞)。 +> **语义主流化(N34 阶段 1.5,2026-10-13)**:对齐 POSIX/Python——`net_recv()` 默认**无限阻塞**(旧 `NET_RECV_TIMEOUT_MS` 30s 默认预算已删,API 不再强加隐藏值)。唤醒源 = 数据 / EOF / close / 网络重建(`NET_ERR_CONNRESET`);会话死亡由对账①在 ~400ms 内判死兜底(§3.6),不存在"对端消失后永久挂死"窗口。需要"等但不无限等":首选 `net_select(r,w,e,timeout_ms)`(§7.1),或 `net_recv_to()` 显式预算。 +> +> **L3 pending 化(2026-09-22)**:无数据时阻塞接收**不占 netTask**——登记挂起请求(每 fd 至多一个,禁止覆盖:覆盖会丢失前调用方通知 = 永久挂死)后**立即返回**;完成按主循环节拍检查(①数据 ②到点[仅显式预算] ③断连)并经任务通知唤醒调用方。等待期间调用方在自己上下文睡通知,netTask 全程零占用(消除 E-02 头阻塞)。内部 tripwire(原看门狗降级):挂起超 60s 只 `[ERR]` 留痕一条("完成逻辑缺位"探针),**绝不伪造 TIMEDOUT 返回值**。 +> +> ⚠ **文档契约(嵌入式特有,Python 没有)**:IWDG `required_mask` 内任务(当前产品掩码 `0x1F7`:default/net/ftp/ui/timeSync/adc/iec104/protect)**禁止无限阻塞 recv**——必须 `net_select` 超时或 `net_recv_to` 显式预算并周期喂心跳,否则"对端连上不发数据"这种完全正常的场景 = IWDG 复位。 -#### net_recv_to - 接收数据(调用方指定超时,分片阻塞版) +#### net_recv_nb - 接收数据(非阻塞具名入口) + +```c +int net_recv_nb(int sockfd, void *buf, int len); +``` + +| 参数 | 说明 | +|------|------| +| `sockfd` / `buf` / `len` | 同 `net_recv` | +| 返回值 | >0 字节数;`0`=对端关闭;`-1`=无数据(`NET_ERR_WOULDBLOCK`)或真错误(`net_get_errno` 可查) | + +原 `NET_MSG_DONTWAIT` 路径转正(N34 阶段 1.5):等价 `net_recv(sockfd, buf, len, NET_MSG_DONTWAIT)`,单次强制非阻塞、不改变 socket 的 blocking 属性。受 IWDG 监控 / 有周期职责的任务的推荐默认入口(生产用户:`ui_net_sink` :7003、`iec104_serv` :2404)。 + +#### net_setblocking - 设置阻塞模式属性 + +```c +int net_setblocking(int sockfd, int on); +``` + +| 参数 | 说明 | +|------|------| +| `sockfd` | socket 描述符 | +| `on` | 1=阻塞(`net_socket()` 默认);0=非阻塞 | +| 返回值 | 0 成功;-1 失败(描述符无效/未占用) | + +模式挂 socket 控制块属性(对齐 BSD `O_NONBLOCK` / Python `setblocking`):非阻塞模式下 `net_recv()` 无数据立即返回 `NET_ERR_WOULDBLOCK`(EOF 判定不变:CLOSED/session_dead 无数据仍返回 0 并触发自动重监听)。与超时预算**正交**——模式是粘滞属性,单次预算用 `net_recv_to` / `net_select`。直写控制块单字节(先例 `net_set_event_cb`),任意上下文可调。 + +#### net_recv_to - 接收数据(调用方指定超时,显式预算分片版) ```c int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms); @@ -944,10 +978,10 @@ int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms); | 参数 | 说明 | |------|------| | `sockfd` / `buf` / `len` | 同 `net_recv` | -| `timeout_ms` | 本片等待预算(ms,**必须 >0**;0 会被解释为"默认 30s",恰是本接口要规避的长阻塞,直接拒 `NET_ERR_INVAL`) | +| `timeout_ms` | 本片等待预算(ms,**必须 >0**;0 = `net_recv` 的无限等待语义,恰与本接口"有界"存在理由相反,直接拒 `NET_ERR_INVAL`——N34 阶段 1.5 起本接口是**显式预算**入口,不再有"默认 30s"隐藏值) | | 返回值 | >0=字节数(允许部分读,TCP 分段);`0`=对端关闭(排空残留后);`-1`=真错误(`net_get_errno` 可查);**-2=本片预算内无数据**(**不是 errno**,不得依赖 errno 判超时——`s_net_errno` 是跨任务共享全局量) | -调用方累计自身总预算:见到 -2 发起下一片,直到收齐 / EOF / 总预算耗尽。生产用途:lftpd 数据行读、timeSync 帧读(E-02 头阻塞修复,完整例 §11.2)。 +调用方累计自身总预算:见到 -2 发起下一片,直到收齐 / EOF / 总预算耗尽。生产用途:lftpd 数据行读、timeSync 帧读(E-02 头阻塞修复,完整例 §11.2);受 IWDG 监控任务的长等待必须走本接口(或 `net_select`)+ 每片喂心跳。 #### net_sendto - 发送 UDP 数据 @@ -1007,11 +1041,11 @@ void net_process_messages(void); 处理其他任务投递的请求(send/recv/connect/accept/close)。每次调用先跑 4 个**非阻塞** tick,再取队列消息(0 超时出队): 1. **D10 重监听重试**(`net_listener_retry_tick`):断连后监听器恢复的到点重试(30ms 间隔 / 2s 时限,§3.6) -2. **L3 挂起 recv 完成检查**(`net_pending_tick`):按 ①数据 ②到点 ③断连 检查每个挂起请求并通知调用方(看门狗 1s) +2. **L3 挂起 recv 完成检查**(`net_pending_tick`):按 ①数据 ②到点(仅显式预算) ③断连 检查每个挂起请求并通知调用方;④ tripwire:挂起超 60s 只 `[ERR]` 留痕一次,不强制了结(N34 阶段 1.5) 3. **Trap 22 芯片/本层对账**(`net_reconcile_tick`):200ms 周期修正**三类**失同步(①②⑤,§3.6) 4. **N-32 监听器 pump**(`net_listener_pump`):100ms 周期,**唯一重建入口**(③④;本层 CLOSED 而芯片在听时只重同步,不发芯片命令,§3.6) -> `net_msg_t.timeout_ms`:L3 分片预算(`net_recv_to()`),0=默认 `NET_RECV_TIMEOUT_MS`(30s)。消息类型仅 6 种(send/recv/close/connect/listen/accept)——自愈类消息 `NET_MSG_RESYNC_LISTEN` / `NET_MSG_RELISTEN` 已随 API 退役删除(N34 阶段1)。 +> `net_msg_t.timeout_ms`:L3 显式预算(`net_recv_to()`),0=**无限等待**(N34 阶段 1.5 删除 `NET_RECV_TIMEOUT_MS` 30s 默认预算——超时归调用方)。消息类型仅 6 种(send/recv/close/connect/listen/accept)——自愈类消息 `NET_MSG_RESYNC_LISTEN` / `NET_MSG_RELISTEN` 已随 API 退役删除(N34 阶段1)。 #### net_send_sock - 发送数据(内部版本) @@ -1155,9 +1189,9 @@ void tcp_client_reconnect(void) net_send(sock, "heartbeat", 9, 0); } - /* 检查接收 */ + /* 检查接收(N34 阶段 1.5:非阻塞具名入口 net_recv_nb) */ char buf[128]; - int len = net_recv(sock, buf, sizeof(buf) - 1, NET_MSG_DONTWAIT); + int len = net_recv_nb(sock, buf, sizeof(buf) - 1); if (len > 0) { buf[len] = '\0'; printf("收到: %s\r\n", buf); @@ -1246,18 +1280,18 @@ static int read_time_frame(int conn_sock, uint8_t buf[FRAME_LEN]) { ### 12.4 如何处理连接断开? -**方式一:阻塞接收** +**方式一:阻塞接收(无限等待——⚠ 仅限不受 IWDG 监控且无周期职责的任务,§10.1 文档契约)** ```c int len = net_recv(sock, buf, sizeof(buf), 0); if (len <= 0) { - /* 连接断开或错误 */ + /* 连接断开或错误(EOF / CONNRESET / 真错误,net_get_errno 可查) */ net_close(sock); } ``` -**方式二:非阻塞接收** +**方式二:非阻塞接收(`net_recv_nb`,N34 阶段 1.5 转正)** ```c -int len = net_recv(sock, buf, sizeof(buf), NET_MSG_DONTWAIT); +int len = net_recv_nb(sock, buf, sizeof(buf)); if (len < 0 && net_get_errno() == NET_ERR_WOULDBLOCK) { /* 没有数据 */ } else if (len <= 0) { @@ -1407,6 +1441,7 @@ CH395F 底层驱动返回的状态码(`ch395f_get_cmd_status()` 或 `ch395f_po | 日期 | 变更 | |------|------| +| 2026-10-13 | **N34 阶段 1.5 recv 语义主流化**(规格 `docs/网络/N34-自愈审计/实施方案.md` §3.5):`net_recv()` 默认**无限阻塞**(删 `NET_RECV_TIMEOUT_MS` 30s 默认预算,唤醒源=数据/EOF/close/网络重建 CONNRESET);新增 `net_recv_nb()`(`NET_MSG_DONTWAIT` 路径转正)与 `net_setblocking(fd, on)`(模式挂 socket 控制块属性,`net_sock_t.blocking`,默认阻塞);`net_recv_to()` 保留为**显式预算**便捷入口(`timeout_ms=0` 语义从"默认 30s"改为"无限"故仍拒 INVAL);内部 recv 看门狗降级为 **tripwire**(挂起超 60s 只 `[ERR]` 留痕一次,不再"到点 +1s 强制了结"伪造 TIMEDOUT);⚠ 新增**文档契约**:IWDG `required_mask` 内任务禁止无限阻塞 recv(必须 `net_select` 超时或 `net_recv_to` 显式预算);`ui_net_sink` / `iec104_serv` 迁移 `net_recv_nb`,lftpd / timeSync 维持显式预算;模拟器 `net_socket_sim.c` 镜像三 API(blocking 属性包装层模拟) | | 2026-10-13 | **N34 阶段1 API 面收敛**(审计见 `docs/网络/N34-自愈审计/README.md`):删除手动自愈 API `net_relisten()` / `net_resync_listen()`(生产零调用;后者语义并入对账**⑤**——本层 ESTABLISHED 而芯片 0x05/0x01 静默回收 → RESYNC 只修本层,§3.6 对账由两类扩为三类①②⑤;前者与 pump/D10 重叠且对健康监听器是 footgun)与 `net_can_accept()`(只认 ESTABLISHED 的旧门控挡死丢 CONNECT 探针,监听门控统一 `net_listener_armed()`,§10.1 门控节改写);`NET_MSG_RESYNC_LISTEN` / `NET_MSG_RELISTEN` 消息类型删除;timeSync 会话收尾不再 `net_close`(同 fd 续 accept,监听跨会话常驻);lftpd 应用层阻塞重试退役(就绪屏障已消除 Trap 09 启动形态) | | 2026-10-12 | **N-32 监听生命周期模块化 + 单一所有者**:监听生命周期抽为 `net_listener.c`(§2.2 文件表新增该行,真机专属);主循环自愈 tick 由 3 个增至 **4 个**(新增 `net_listener_pump`,100ms,**唯一重建入口**);`net_reconcile_tick` 对账收窄为 ①②,③④ 移交 pump;`handle_disconnect_event` / `handle_timeout_event` 改为 `net_listener_settle(GRACEFUL 100ms / QUARANTINE 400ms)` **只置事实**、不再自行重建;§3.3 / §3.6(四层自愈 → **五层**)/ §12.10 同步。权威设计见 `docs/子系统设计/网络Socket监听生命周期设计说明.md` §10 | | 2026-10-10 | 新增 **`net_can_accept()` / `net_can_read()` 就绪门控 API**(§10.1):纯 RAM 读控制块、**不走消息队列、不等 netTask 回执**,供实时性敏感任务在发 RPC 前判断"有没有活"(动因 = `ui_net_sink_poll()` 每 10 ms 一次 `net_accept` 把 uiTask 拖成最坏 ≈50 ms;原拟登记 N-19 / C7,当日结案不入登记簿;设计与实测见 `docs/子系统设计/看门狗设计说明.md` §11(原分析文档已并入该节));§3.5 补"netTask 运行要求"例外条;⚠ 记录两条使用约束——**不可作为唯一判据**(`readable` 只由 RECV_OK 中断置位,须保留低频兜底 recv)与**模拟器后端恒返 1**(`net_socket_sim.c`,Winsock 无 RPC 成本) | diff --git a/docs/网络/BSD_Socket_API_函数调用关系.md b/docs/网络/BSD_Socket_API_函数调用关系.md index 2c92f01c..24c53a6b 100644 --- a/docs/网络/BSD_Socket_API_函数调用关系.md +++ b/docs/网络/BSD_Socket_API_函数调用关系.md @@ -48,7 +48,7 @@ flowchart TB NPM["netProcessMessages
先跑 4 个 tick,再排空队列分派"] NP["netPoll
INT# GPIO 轮询 → 全局中断 → 各 Socket SINT → 每轮一批"] T1["① netListenerRetryTick
D10 重监听重试:30ms / 2s 时限"] - T2["② netPendingTick
L3 挂起 recv:①数据 ②到点 ③断连 ④看门狗 1s"] + T2["② netPendingTick
L3 挂起 recv:①数据 ②到点(仅显式预算) ③断连 ④tripwire 60s 只留痕"] T3["③ netReconcileTick
Trap 22 对账:200ms / 去抖 / 冷却(①②⑤)"] T4["④ netListenerPump
N-32 唯一重建入口:100ms / 去抖 / 冷却"] DISP["队列分派 switch
SEND · RECV · CLOSE
CONNECT · LISTEN · ACCEPT"] @@ -129,8 +129,9 @@ flowchart TB | `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_listener_pump`(N-32 单一重建入口) | | `net_connect` | `NET_MSG_CONNECT` | `net_connect_locked` | `set_proto_type` + `open_socket`(3×500ms 重试)+ 清残留中断 + `set_sour_port` / `set_des_ip`(大端数组)/ `set_des_port` + `tcp_connect` | 0 = 请求已发出(**异步**)/ -1 | open **先于**寄存器写入(open 会复位寄存器);open 后清残留 DISCONNECT,防新连接秒断(Trap 14/17);目的 IP 字节序写反 = CONNECT TIMEOUT(Trap 16);返回 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 20:写穿 FIFO = 数据错位);`send_ready` 门控用 `net_poll` 轮询而非 osDelay;短写**必须**告警——静默截断会让对端永久等待剩余字节(2026-09-21 实证:UI 推流应答截断 → 客户端失联) | -| `net_recv` | `NET_MSG_RECV`(timeout=0→默认 30s) | `net_recv_locked` | `net_poll` + `get_recv_len` / `read_recv_buf` | >0 / 0(EOF) / -1(errno = WOULDBLOCK / TIMEDOUT / CONNRESET) | 快路径:数据已在 → 立即读出返回(允许部分读);无数据 + 阻塞 → **登记挂起后立即返回**(L3,netTask 零占用);DONTWAIT + CLOSED → EOF + 触发重监听 | -| `net_recv_to` | `NET_MSG_RECV`(timeout>0) | `net_recv_locked` | 同上 | >0 / 0 / -1 / **-2(本片超时无数据)** | E-02 头阻塞修复配套:调用方把长等待切成有界片(如 5ms/剩余预算),**总超时语义不变、单次占用有界**;`timeout_ms=0` 直接拒(会被解释成默认 30s,恰是长阻塞);超时**只凭 -2 判定、不得依赖 errno**(`s_net_errno` 是跨任务共享全局量,随时被别的任务覆盖) | +| `net_recv` | `NET_MSG_RECV`(timeout=0→**无限等待**,N34 阶段 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` 路径转正的具名入口(N34 阶段 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 头阻塞修复配套 + **显式预算**入口(N34 阶段 1.5,等价 Python settimeout,无隐藏默认值):调用方把长等待切成有界片(如剩余预算),**总超时语义不变、单次占用有界**;`timeout_ms=0` 直接拒(0 = 无限等待,与本接口"有界"存在理由相反);超时**只凭 -2 判定、不得依赖 errno**(`s_net_errno` 是跨任务共享全局量,随时被别的任务覆盖) | | `net_close` | `NET_MSG_CLOSE` | `net_close_locked` | `close_socket` + 等 CLOSED(5ms 节奏 / ≤500ms)+ 排空 Socket/全局中断 | 0 / -1 | **先**了结本 fd 挂起 recv(排空/取消 + 必通知,关不得让等待者悬挂);关后排空残留 DISCONNECT(Trap 14/17:否则下一连接被"刚建立就断开"秒断);控制块整体清零 | | ~~`net_relisten`~~ | ~~`NET_MSG_RELISTEN`~~ | **N34 阶段1 已删除**(消息类型同删) | — | — | 原"无条件芯片侧重建"与 pump/D10 完全重叠,且对健康监听器是 footgun(open→USE→close 自毁 ~1s);生产零调用。历史实现见 `net_socket.c` git 历史 | | ~~`net_resync_listen`~~ | ~~`NET_MSG_RESYNC_LISTEN`~~ | **N34 阶段1 已删除**(语义并入对账⑤) | — | — | D14 静默回收(芯片自行回监听、不发 DISCONNECT)现由 `net_reconcile_tick` 判据⑤自动认领:本层 ESTABLISHED 且芯片 0x05/0x01 → 只修本层(state→LISTENING + 转登 `last_peer_port` + 清 `lost_conn_seen` + 取消挂起 recv),**不发芯片命令**(纪律原样保留,run8 实证 170s 拒绝);去抖/冷却沿用对账框架 | @@ -141,7 +142,8 @@ flowchart TB |---|---|---|---| | `net_socket(domain,type)` | 分配空闲控制块(`alloc_socket` 线性扫描 `in_use=0`)→ CREATED | `alloc_socket` + `net_listener_clear` + `net_pending_clear` | 新 fd 不得继承上一次的待重试/挂起状态(复用描述符的经典坑);domain 仅 `NET_AF_INET` | | `net_bind(sockfd,addr,addrlen)` | TCP:仅记录本地端口(port=0 → 动态端口 49152 起递增)→ BOUND,**无芯片命令**;UDP:完整开 Socket → UDP_OPEN | UDP 路径:`set_proto_type(UDP)` + `set_sour_port` + `set_des_ip(0xFFFFFFFF)` + `set_des_port(0)` + `open_socket`(Socket 4~7 兜底重设缓冲) | TCP 的 bind 只是"记账",真正的芯片动作在 listen/connect;UDP 直碰 SPI(§2 三例外之一);动态端口回绕 65535→49152 | -| `net_sendto` / `net_recvfrom` | UDP 收发(直碰 SPI,不经队列) | `set_des_ip` / `set_des_port` + `write_send_buf`(分块 1KB + `net_poll` 门控);`net_poll` + `get_recv_len` + `read_recv_buf` | 同 §2 上下文约束;recv 时 UDP Server 模式前 8 字节是固定头(源 IP + 端口 + 长度),先剥头填 `src_addr` 再读数据 | +| `net_sendto` / `net_recvfrom` | UDP 收发(直碰 SPI,不经队列) | `set_des_ip` / `set_des_port` + `write_send_buf`(分块 1KB + `net_poll` 门控);`net_poll` + `get_recv_len` + `read_recv_buf` | 同 §2 上下文约束;recv 时 UDP Server 模式前 8 字节是固定头(源 IP + 端口 + 长度),先剥头填 `src_addr` 再读数据;recvfrom 阻塞语义与 net_recv 同构(N34 阶段 1.5:blocking 属性 / DONTWAIT,无默认预算;轮询自旋实现,生产零调用) | +| `net_setblocking(sockfd,on)` | 设置阻塞**模式属性**(N34 阶段 1.5,直写控制块,不经队列) | 写 `s_net_socks[fd].blocking` | 对齐 BSD `O_NONBLOCK` / Python `setblocking`:"模式归使用者、超时归调用方";单字节原子写(先例 `net_set_event_cb`),最坏一次在途 recv 用旧模式;`net_socket()` 默认置 1(close 的 memset 清零后新 fd 必须显式重置) | | `net_wait_result(kind,sockfd)` | 调用侧两段式等待:**零等待拾取** → 阻塞等回执 | `xTaskNotifyWait` ×2 | ① 零等待段兜住"入队→等待入口"间隙落地的通知(2026-09-22 台架实证丢通知);② **清除掩码必须全 1**(传 0 = 不清 → 上次结果残留被重读,run3~11 挂死/错位的真根因);③ 必须判 **pdTRUE** 而非 `v!=0`(合法结果 0 = EOF 会被误判"无通知"→ 永久挂死);④ 32 位整体作结果(32768 大字节数不截断);⑤ 本路径**禁止**加逐调用日志(曾占日志 98.6%、冲掉 [LAT] 证据行,run14 实证) | | `net_sock_state(sockfd)` | 跨任务只读状态快照(单字节读,无锁) | 无 | 描述符无效/未占用 → -1;值可能滞后 netTask 最新状态——用于**轮询**(客户端等 ESTABLISHED),不替代 accept/connect | | `net_get_sock(sockfd)` | 返回控制块指针 | 无 | netTask 上下文内用;跨任务只可读单字节 `state`(即 `net_sock_state`),不得读结构体其他成员 | @@ -157,7 +159,7 @@ flowchart TB | 函数 | 设计作用 | 主要调用 | 意图 / 注意 | |---|---|---|---| | `net_pending_register` | 登记挂起(`net_recv_locked` 的"无数据 + 阻塞"路径) | 写 `s_net_pending[fd]` | 每 fd 至多一个:**覆盖会丢失前调用方通知 = 永久挂死**;执行任务自身(netTask)发起的等待直接拒绝(自等自通知 = 死锁,netTask 内须用 DONTWAIT) | -| `net_pending_tick` | 完成检查(`net_process_messages` 每轮调用,非阻塞) | ① `get_recv_len` + `read_recv_buf` ② 到点 ③ `get_recv_len` 排空 + `net_listener_try` ④ `net_pending_complete` | 按 **①数据 ②到点 ③断连** 顺序检查(逐项对齐旧自旋语义);CONNECTING/LISTENING 等状态**保持等待**(CONNECT→ESTABLISHED 是合法迁移,不做状态推断取消,防误杀正在建链的等待);**看门狗**:到点后 1s 仍未了结 = 完成逻辑缺位 → 强制了结 + 错误日志 | +| `net_pending_tick` | 完成检查(`net_process_messages` 每轮调用,非阻塞) | ① `get_recv_len` + `read_recv_buf` ② 到点(仅显式预算) ③ `get_recv_len` 排空 + `net_listener_try` ④ `net_pending_complete` | 按 **①数据 ②到点(仅 `net_recv_to` 显式预算;无限登记无 deadline)③断连** 顺序检查(逐项对齐旧自旋语义);CONNECTING/LISTENING 等状态**保持等待**(CONNECT→ESTABLISHED 是合法迁移,不做状态推断取消,防误杀正在建链的等待);**tripwire**(N34 阶段 1.5,原看门狗降级):挂起超 60s = 完成逻辑缺位 → 只 `[ERR]` 留痕一次,**绝不强制了结、绝不伪造 TIMEDOUT**——③ 与显式 cancel 是无限 recv 的全部唤醒责任方 | | `net_pending_complete` | 所有完成/取消路径的公共出口 | `xTaskNotify(caller, result)` + 清 active | 通知值 = 结果原值(>0 字节 / 0 EOF / 负错误码,与消息分派同一回传通道);失败结果同步 `s_net_errno`(netTask 上下文一致性);无在位挂起时静默返回(防重复通知) | | `net_pending_settle_close` | close 前**了结**本 fd 挂起 | `get_recv_len` + `read_recv_buf` + `net_pending_complete` | 关闭不得丢已到达数据(芯片缓冲 close 前可读):有数据 → 按字节数通知;无数据 → 取消(CONNRESET);先于 `ch395f_close_socket` 执行 | | `net_pending_cancel` / `cancel_all` | 显式取消(**必通知**) | `net_pending_complete` | 取消路径:陈旧 resync(单 fd)/ 网络重建(全部,`net_init` 开头,早于控制块清零)——"取消不通知 = 调用方永久挂死" | diff --git a/docs/网络/Net层阻塞模型架构设计.md b/docs/网络/Net层阻塞模型架构设计.md index fd301095..96720d1d 100644 --- a/docs/网络/Net层阻塞模型架构设计.md +++ b/docs/网络/Net层阻塞模型架构设计.md @@ -44,7 +44,7 @@ netTask 是**全部 socket 操作的唯一执行者**——CH395F 的 SPI 总线 |---|---| | **左栏 · 调用方任务** | `net_recv` → 判"数据已在?" → **是**走快速路径;**否**(`timeout > 0`)登记 pending 后在自己上下文睡任务通知 | | **中栏 · netTask 主循环** | ~1ms 节拍:`net_poll`(刷新 readable)→ 4 个非阻塞 tick(`net_listener_retry_tick` D10 → `net_pending_tick` 了结挂起 → `net_reconcile_tick` Trap 22 对账 → `net_listener_pump` 监听重建唯一入口)→ 排空消息队列 | -| **下带 · 五路判定** | 每轮对每个挂起 fd 按序判定:数据到达 / 到点 / EOF / 同 fd 后续操作 / 网络重建;含 1s 看门狗 | +| **下带 · 五路判定** | 每轮对每个挂起 fd 按序判定:数据到达 / 到点(仅显式预算)/ EOF / 同 fd 后续操作 / 网络重建;tripwire 60s 只留痕(N34 阶段 1.5 由 1s 看门狗降级) | | **回环** | 判定完成经 `xTaskNotify` 唤醒调用方(结果 = 字节数 / 负错误码) | **关键性质**: @@ -54,6 +54,10 @@ netTask 是**全部 socket 操作的唯一执行者**——CH395F 的 SPI 总线 - 调用方**在自己的上下文睡眠**(任务通知),与 LwIP 的消费者线程模型同构; - 公开 API 与调用契约**不变**——`net_recv` / `net_recv_to` 的签名、返回值、 超时语义均与改造前一致,既有调用方、测试与模拟器零改动。 + (**N34 阶段 1.5 更新**:本条描述 L3 改造当时口径;2026-10-13 起 `net_recv` + 默认预算 30s 删除、改为无限阻塞,`net_recv_to` 成为唯一显式预算入口—— + 挂起模型与"等待不占 netTask"性质不变,变的是超时归属:见 + `docs/网络/N34-自愈审计/实施方案.md` §3.5 与 BSD 指南 §3.5。) > 图形由 `tools/arch_diagram/gen_net_pending_svg.py` 生成(改流程后重跑;见 > `docs/架构设计/图形绘制流程.md`)。 @@ -67,7 +71,7 @@ netTask 是**全部 socket 操作的唯一执行者**——CH395F 的 SPI 总线 | 项 | 设计 | |---|---| | 存储 | **独立静态数组** `s_net_pending[NET_MAX_SOCKETS]`(不置于 `net_sock_t` 内) | -| 字段 | `active` / `buf` / `len` / `deadline` / `registered` / `caller` | +| 字段 | `active` / `infinite` / `tripwire_logged` / `buf` / `len` / `deadline` / `registered` / `caller`(`infinite`/`tripwire_logged` 为 N34 阶段 1.5 新增:0 预算 = 无限等待、tripwire 只留痕一次) | | 容量 | 每 fd **至多一个**挂起请求 | | 占用 | ≈24B/fd × 8 fd = **192B**(+4B 任务句柄),实测 SRAM +200B | @@ -93,7 +97,7 @@ socket 生命周期解耦,与 `net_listener_retry_tick` 的重试表同构。 | # | 条件 | 动作 | 设计要点 | |---|---|---|---| | 1 | **数据到达** | SPI 读出 + 通知(结果 = 字节数) | 由 `net_poll` 维护的 `readable` 门控,**零额外 SPI**;仅 ESTABLISHED / UDP_OPEN 态可读;spurious readable 需清除 | -| 2 | **到点** `now >= deadline` | 通知 `NET_ERR_TIMEDOUT`(对外 -2 映射保持) | 状态无关;用有符号回绕安全差值,防 tick 回绕 | +| 2 | **到点** `now >= deadline`(仅显式预算登记;无限等待登记无 deadline——N34 阶段 1.5) | 通知 `NET_ERR_TIMEDOUT`(对外 -2 映射保持) | 状态无关;用有符号回绕安全差值,防 tick 回绕;这是 `net_recv_to` 调用方策略的正常兑现,不是伪造 | | 3 | **CLOSED**(EOF/DISCONNECT) | 排空:有数据则读出;否则按 0(EOF) 完成 + `auto_relisten` + 通知 | CLOSE 先行完成挂起 = `net_close` 的 settle 钩子(排空→数据否则 CONNRESET) | | 4 | **同 fd 后续操作** | 先结算挂起,再执行新操作 | per-fd 串行——冻结面从"全网"缩到"本 fd" | | 5 | **网络重建**(`net_stack_bring_up`,芯片复位) | **取消 + 必须通知** | `net_init` 顶部 `net_pending_cancel_all(CONNRESET)`,先于芯片复位;**取消不通知 = 调用方永久挂死** | @@ -101,10 +105,13 @@ socket 生命周期解耦,与 `net_listener_retry_tick` 的重试表同构。 **状态推断的克制**:CONNECTING / LISTENING 等中间态**保持等待**,**不做**状态推断式取消—— CONNECT → ESTABLISHED 是合法迁移,推断取消会误杀正在建链的等待。 -### 4.1 看门狗 +### 4.1 tripwire(原"看门狗",N34 阶段 1.5 降级) -到点后 1s 仍未了结 = 完成逻辑缺位 → **强制了结 + `DBG_ERROR`**。 -定位:防"挂起登记后永不完成"导致调用方无限期睡眠。 +挂起超 60s(`NET_RECV_TRIPWIRE_MS`)= "完成逻辑缺位"探针 → **只 `DBG_ERROR` 留痕一次, +绝不强制了结、绝不伪造 TIMEDOUT 返回值**(旧设计"到点 +1s 强制了结"会掩盖完成路径 +缺陷,且与"无限等待"的新语义矛盾——伪造返回值违背调用方意图)。 +定位:留痕成本极低的开发期探针;唤醒责任在 ①数据 ②到点(仅显式预算)③断连 与 +显式 cancel 路径(close / 网络重建 / 陈旧重同步),见 §4 五路。 --- @@ -143,8 +150,8 @@ CONNECT → ESTABLISHED 是合法迁移,推断取消会误杀正在建链的 | **SEND 路径未覆盖** | 发送仍为分块阻塞(1s/chunk stall) | 该期间队列仍被冻结;二期 pending 化解决 | | **ACCEPT 路径未覆盖** | accept 仍用轮询计时 | 需 pending 化(二期) | | **超时精度** | 随主循环节拍(~1ms),被长操作占用时顺延 | 一期文档化、不修;二期 SEND 化后该顺延面**收窄** | -| **看门狗未接自检** | 强结算 + `DBG_ERROR`,未写 selftest | 二期补齐 | -| **模拟器不可建模** | `net_socket_sim.c` 单线程 Winsock 无法复现挂起模型 | 该路径在模拟器上零覆盖,只能硬件验证;**监听生命周期 `net_listener.c` 亦为真机专属**(内部头只被 `net_socket.c` include,CMakeLists 不编译) | +| **tripwire 未接自检** | 只 `DBG_ERROR` 留痕(N34 阶段 1.5 起不再强制了结),未写 selftest | 二期补齐 | +| **模拟器不可建模** | `net_socket_sim.c` 单线程 Winsock 无法复现**挂起模型内部机制**(外部语义已由包装层按 blocking 属性/预算模拟,N34 阶段 1.5) | 挂起表/tick/通知路径在模拟器上零覆盖,只能硬件验证;**监听生命周期 `net_listener.c` 亦为真机专属**(内部头只被 `net_socket.c` include,CMakeLists 不编译) | **二期扩展设计**(ACCEPT / SEND 的挂起化、门禁清单、改动量)见 `docs/任务规划/基线与待确认.md` §2.1。 diff --git a/simulator/sim/net_socket_sim.c b/simulator/sim/net_socket_sim.c index db67b30e..81eef003 100644 --- a/simulator/sim/net_socket_sim.c +++ b/simulator/sim/net_socket_sim.c @@ -10,8 +10,10 @@ * * 设计要点: * 1. fd 采用与 MCU 端一致的"控制块索引"模型(0 ~ NET_MAX_SOCKETS-1) - * 2. 所有 Socket 创建即置为非阻塞(ioctlsocket FIONBIO),使 net_recv / - * net_accept 的 DONTWAIT 语义与 MCU 端一致(无数据返回 -1 + NET_ERR_WOULDBLOCK) + * 2. Winsock 句柄创建即置非阻塞(ioctlsocket FIONBIO),阻塞语义在包装层 + * 按控制块 blocking 属性模拟(N34 阶段 1.5:模式归使用者——blocking=1 + * 时 recv 轮询等待(Sleep(1)),=0 或 NET_MSG_DONTWAIT 立即 WOULDBLOCK), + * 使 net_recv / net_recv_nb / net_recv_to / net_accept 的语义与 MCU 端一致 * 3. 代次号在模拟器上恒为 0(无"网络重建"概念),net_get_generation 恒返回 0 * 4. net_get_sock 以 in_use 判活,供上层判断"监听 Socket 是否随客户端一同失效" * 5. Winsock 句柄存放于并行数组 s_ws[],不侵入共享的 net_sock_t 结构体 @@ -213,6 +215,9 @@ int net_socket(int domain, int type) { s_socks[idx].type = (uint8_t)type; s_socks[idx].state = NET_SOCK_STATE_CREATED; s_socks[idx].ch395_sock = (uint8_t)idx; + /* N34 阶段 1.5:阻塞模式默认 = 阻塞(与 MCU 端 net_socket 一致; + * Winsock 句柄仍恒非阻塞,阻塞语义由包装层按本属性模拟,见文件头要点 2) */ + s_socks[idx].blocking = 1; s_ws[idx] = ws; s_net_errno = NET_OK; @@ -329,6 +334,8 @@ int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen) { s_socks[nidx].type = (uint8_t)NET_SOCK_STREAM; s_socks[nidx].state = NET_SOCK_STATE_ESTABLISHED; s_socks[nidx].ch395_sock = (uint8_t)nidx; + /* N34 阶段 1.5:accept 出的新连接同样默认阻塞模式(与 MCU 端一致) */ + s_socks[nidx].blocking = 1; s_ws[nidx] = cws; s_net_errno = NET_OK; @@ -451,32 +458,18 @@ int net_send(int sockfd, const void *buf, int len, int flags) { } /* - * 函数功能:接收数据(TCP) - * 入口参数:sockfd - socket 描述符 - * buf - 接收缓冲区 + * 函数功能:单次非阻塞 recv(内部公共体:立即返回,不等待) + * 入口参数:sockfd - socket 描述符(已校验) + * buf - 接收缓冲区(已校验) * len - 缓冲区大小 - * flags - 通常为 0,可设 NET_MSG_DONTWAIT(Socket 本就非阻塞) * 出口参数:buf - 填充接收数据 - * 返回值:实际接收字节数;0 = 对端关闭;-1 = 无数据(NET_ERR_WOULDBLOCK)或错误 - * 限定条件:socket 已建立 - * 函数说明:与 MCU 端语义一致——>0 有数据,0 对端关闭,<0 且 errno 区分 - * "暂无数据"与"真错误" + * 返回值:>0 字节数;0 = 对端关闭;-1 = 无数据(WOULDBLOCK) 或错误 + * 限定条件:Winsock 句柄为非阻塞(FIONBIO) + * 函数说明:net_recv 的非阻塞路径与 net_recv_nb 共用;错误码映射与旧实现一致 */ -int net_recv(int sockfd, void *buf, int len, int flags) { - int r; +static int recv_once(int sockfd, void *buf, int len) { + int r = recv(s_ws[sockfd], (char *)buf, len, 0); - (void)flags; - - if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) { - s_net_errno = NET_ERR_BADF; - return -1; - } - if (buf == NULL || len < 0) { - s_net_errno = NET_ERR_INVAL; - return -1; - } - - r = recv(s_ws[sockfd], (char *)buf, len, 0); if (r > 0) { s_socks[sockfd].recv_bytes += (uint32_t)r; s_net_errno = NET_OK; @@ -494,6 +487,110 @@ int net_recv(int sockfd, void *buf, int len, int flags) { return -1; } +/* + * 函数功能:阻塞语义模拟(内部公共体:轮询等待数据/EOF/错误) + * 入口参数:sockfd - socket 描述符(已校验) + * buf / len - 接收缓冲与容量(已校验) + * has_budget - 1 = 有预算(net_recv_to);0 = 无限等待(net_recv 阻塞) + * budget_ms - 预算毫秒(has_budget=0 时忽略) + * 出口参数:buf - 填充接收数据 + * 返回值:>0 字节数;0 = 对端关闭;-1 = 错误;-2 = 预算耗尽无数据(仅 has_budget) + * 限定条件:Winsock 句柄为非阻塞;调用方为 UI/服务线程(Sleep(1) 粒度可接受) + * 函数说明:与 MCU 端挂起 recv 的对外契约对齐——MCU 端等待发生在调用方任务 + * (睡任务通知),模拟器端等价地睡 Sleep(1) 轮询;EOF/错误立即返回 + */ +static int recv_wait(int sockfd, void *buf, int len, int has_budget, unsigned long long budget_ms) { + unsigned long long t0 = GetTickCount64(); + + for (;;) { + int r = recv_once(sockfd, buf, len); + + if (r != -1 || s_net_errno != NET_ERR_WOULDBLOCK) { + return r; /* 数据 / EOF / 真错误:立即返回 */ + } + if (has_budget && (GetTickCount64() - t0) >= budget_ms) { + s_net_errno = NET_ERR_TIMEDOUT; + return -2; /* 预算耗尽(对齐 MCU net_recv_to 的 -2) */ + } + Sleep(1); + } +} + +/* + * 函数功能:接收数据(TCP,**默认无限阻塞**——N34 阶段 1.5 与 MCU 端语义对齐) + * 入口参数:sockfd - socket 描述符 + * buf - 接收缓冲区 + * len - 缓冲区大小 + * flags - 通常为 0;NET_MSG_DONTWAIT 单次强制非阻塞 + * 出口参数:buf - 填充接收数据 + * 返回值:实际接收字节数;0 = 对端关闭;-1 = 无数据(NET_ERR_WOULDBLOCK,仅非阻塞)或错误 + * 限定条件:socket 已建立 + * 函数说明:与 MCU 端语义一致——blocking 属性(net_setblocking)决定无数据时 + * 无限等待还是立即 WOULDBLOCK;DONTWAIT 单次覆盖属性 + */ +int net_recv(int sockfd, void *buf, int len, int flags) { + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) { + s_net_errno = NET_ERR_BADF; + return -1; + } + if (buf == NULL || len < 0) { + s_net_errno = NET_ERR_INVAL; + return -1; + } + + /* 非阻塞:DONTWAIT 单次覆盖,或 socket 的 blocking 属性为 0 */ + if ((flags & NET_MSG_DONTWAIT) || s_socks[sockfd].blocking == 0U) { + return recv_once(sockfd, buf, len); + } + /* 阻塞:无限等待(无默认预算,N34 阶段 1.5) */ + return recv_wait(sockfd, buf, len, 0, 0ULL); +} + +/* + * 函数功能:接收数据(非阻塞具名入口,与 MCU 端同名 API 对齐——N34 阶段 1.5) + * 入口参数:sockfd / buf / len - 同 net_recv + * 返回值:>0 字节数;0 = 对端关闭;-1 = 无数据(WOULDBLOCK) 或错误 + * 限定条件:socket 已建立 + * 函数说明:等价 net_recv(sockfd, buf, len, NET_MSG_DONTWAIT),不改变 blocking 属性 + */ +int net_recv_nb(int sockfd, void *buf, int len) { + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) { + s_net_errno = NET_ERR_BADF; + return -1; + } + if (buf == NULL || len < 0) { + s_net_errno = NET_ERR_INVAL; + return -1; + } + return recv_once(sockfd, buf, len); +} + +/* + * 函数功能:接收数据(**显式预算**便捷入口,与 MCU 端同名 API 对齐——N34 阶段 1.5) + * 入口参数:sockfd / buf / len - 同 net_recv + * timeout_ms - 本次等待预算(ms,必须 >0;0 = 无限等待语义 + * 属 net_recv 的职责,此处拒 INVAL,与 MCU 端一致) + * 返回值:>0 字节数;0 = 对端关闭;-1 = 真错误;-2 = 预算耗尽无数据 + * 限定条件:socket 已建立 + * 函数说明:模拟器以 GetTickCount64 + Sleep(1) 轮询兑现预算 + */ +int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms) { + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) { + s_net_errno = NET_ERR_BADF; + return -1; + } + if (buf == NULL || len < 0) { + s_net_errno = NET_ERR_INVAL; + return -1; + } + if (timeout_ms == 0U) { + /* 0 = 无限等待(net_recv 的语义),与本接口"有界"存在理由相反 → 拒绝 */ + s_net_errno = NET_ERR_INVAL; + return -1; + } + return recv_wait(sockfd, buf, len, 1, (unsigned long long)timeout_ms); +} + /* * 函数功能:发送 UDP 数据报(含目标地址) * 入口参数:sockfd - socket 描述符 @@ -634,6 +731,28 @@ int net_set_event_cb(int sockfd, net_event_cb_t cb, void *arg) { return 0; } +/* + * 函数功能:设置 Socket 阻塞/非阻塞**模式属性**(与 MCU 端同名 API 对齐——N34 阶段 1.5) + * 入口参数:sockfd - socket 描述符 + * on - 1 = 阻塞(net_socket/net_accept 默认);0 = 非阻塞 + * 返回值:0 成功;-1 失败(描述符无效/未占用) + * 限定条件:socket 已创建 + * 函数说明:Winsock 句柄恒非阻塞(FIONBIO,UI 主循环安全底线),本属性只驱动 + * 包装层的等待策略(net_recv 无限轮询 / 立即 WOULDBLOCK), + * 语义与 MCU 端"模式挂控制块属性"一致 + */ +int net_setblocking(int sockfd, int on) { + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) { + s_net_errno = NET_ERR_BADF; + return -1; + } + + s_socks[sockfd].blocking = (on != 0) ? 1U : 0U; + + s_net_errno = NET_OK; + return 0; +} + /* * 函数功能:获取 socket 最后错误码 * 入口参数:无 diff --git a/test/host/latency_peer.py b/test/host/latency_peer.py index ab6d5467..21326609 100644 --- a/test/host/latency_peer.py +++ b/test/host/latency_peer.py @@ -310,7 +310,9 @@ class RemoPeer: # --- FTP head-of-line stress (E-02) --- def ftp_stress_worker(self, start_evt, stop_evt, hold_s): """Wait for the stress window to open, then hold an idle FTP login: - lftpd read_line -> blocking net_recv(0) inside netTask (NET_RECV_TIMEOUT_MS) + lftpd read_line -> blocking net_recv(0) inside netTask (old 30s default + budget NET_RECV_TIMEOUT_MS, removed in N34 phase 1.5; today the wait is + an L3 pending recv -> this scenario must NOT freeze the queue anymore) -> every queued message (incl. the LCDMEM response) waits behind it (E-02).""" if not start_evt.wait(120.0): sys.stdout.write("[peer] ftp stress: window never opened\n") From 33405a57b5bb8be95b5bc053e55ce4733d383367 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 13 Oct 2026 11:23:40 +0800 Subject: [PATCH 15/24] =?UTF-8?q?docs(N34)+test:=20E13=20=E5=83=B5?= =?UTF-8?q?=E5=B0=B8=E4=BC=9A=E8=AF=9D=E6=A0=B9=E5=9B=A0=E5=9D=90=E5=AE=9E?= =?UTF-8?q?=EF=BC=9BPhase=2011=20=E5=88=A4=E6=8D=AE=E4=BF=AE=E6=AD=A3+?= =?UTF-8?q?=E4=B8=80=E6=AC=A1=E6=80=A7=E6=8A=A5=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit E13(实验方案.md 新增 + E8/E11/E12 口径修正): - 风暴态根因=僵尸会话,非芯片残渣:PC 网卡禁用/链路断开时 Windows 直接丢弃 socket 不发 FIN/RST → 芯片侧 ESTABLISHED 僵尸 → 单连接监听被占(:21 拒连)→ 下一 PASV 重监听与僵尸竞态 (accept 为垂死僵尸开枪 20ms 后 DISCONNECT)→ lftpd 5s 窗空转 → 425;每失败轮再生僵尸 → 自续数分钟 - "残渣跨烧录存活/需断电冷启动"口径作废:板上 CH395F RSTI 接 MCU NRST(V0.0 网表 :174),每次烧录即芯片硬件复位;"存活"是 锤击再生错觉。正确控制=下一臂前静置 ≥90s 清僵尸窗 - 自愈链端到端验证:TIMEOUT(在途 TX ≈30s)/keepalive(空闲 60/5/3) → pump → relisten → 端口恢复;E11 产品路径验证 17s 判死 PASS - 4 元组复用假设被否(pasv_probe.py:PASV 端口递增、PC 源端口随机) - e02_probe listener=FAIL 重解读:探针挂起 socket 未关(PC 内核 代 ACK keepalive)→ 拒连是单连接正确行为;真缺口=应用无法主动 拆会话 → 阶段 2 sock_reset 职责 Phase 11 测试修复(net_test_task.c,实跑暴露): - 判据 >=60000ms 是按手册公式写的,实测判死 18~31s 会在正确固件 上假 FAIL → 改有界窗 10s~120s(下限防误杀存活空闲会话,上限 防判死缺位) - 报告后不释放 phase_active → 对已死连接 10ms 节奏刷 594 条假 FAIL → phase11_run 返回完成态,主循环释放等新连接再测 - 复跑两轮验证:30137ms 单次 PASS → 自动等新连接 → 二轮 accept 正常 - Test_Guide TC-NET-1101 判据/耗时同步;ch395f_test.h Phase 14 共存安全性注释(有整体超时不永久挡 accept) 工具:test/host/pasv_probe.py(每轮打印 PASV 端口+PC 源端口+结果) --- docs/测试说明/CH395F_Test_Guide.md | 6 +- docs/网络/N34-自愈审计/实验方案.md | 81 +++++++++++++++++-- test/board/ch395f_test.h | 4 +- test/board/net_test_task.c | 33 +++++--- test/host/pasv_probe.py | 123 +++++++++++++++++++++++++++++ 5 files changed, 228 insertions(+), 19 deletions(-) create mode 100644 test/host/pasv_probe.py diff --git a/docs/测试说明/CH395F_Test_Guide.md b/docs/测试说明/CH395F_Test_Guide.md index 65955bc6..b941f5c6 100644 --- a/docs/测试说明/CH395F_Test_Guide.md +++ b/docs/测试说明/CH395F_Test_Guide.md @@ -1081,7 +1081,7 @@ IDLE → ACCEPT → [SEND: 分块发 → DONE] / [RECV: 分块收+校验 → DON | 阶段 ID | TM-PHY-11 | |---------|-----------| | **类型** | PC 配合,netTask 运行 | -| **耗时** | ~65–80 秒(60s idle + 探测窗;芯片定时语义以实测为准,见 E11) | +| **耗时** | ~1.5–2 分钟(连接+网卡禁用后实测判死 18~31s,加恢复重连;芯片定时语义以实测为准,见 E11/E13) | | **入口** | `ENABLE_PHASE11_TESTS`(2026-10-13 起默认开启;服务器侧开关由 net 层在 CONNECT 后自动置位,`docs/芯片手册/CH395F.md` §5.56) | | **配置** | KeepAlive 空闲 60s,探测间隔 5s,3 次(`net_config.h`) | @@ -1094,8 +1094,8 @@ IDLE → ACCEPT → [SEND: 分块发 → DONE] / [RECV: 分块收+校验 → DON | **类型** | 功能测试 | | **标题** | TCP 连接空闲超过 KeepAlive 阈值后自动断开 | | **前置条件** | 1. `NET_KEEPALIVE_IDLE_MS=60000` / `INTVL=5000` / `CNT=3`(`net_config.h`)
2. 对端须为**死对端**:PC 连接 8080 后**禁用网卡**制造"链路在、对端静默"(存活对端会 ACK 探测 → 永不 TIMEOUT;Windows 防火墙规则对已建立连接无效,E11 实证)
3. 勿与 Phase 6/9/10/13 混选(recv 互抢数据) | -| **测试步骤** | 1. PC 连接 8080,不发数据
2. 立即禁用 PC 网卡(中间有交换机 → 设备侧 PHY 链路保持)
3. 等待芯片报 TIM_OUT(从连接起 ≥60s;实测对端失联后判死 ≈30s)
4. 本层 QUARANTINE → pump 重建 → s0 回 LISTEN
5. 恢复网卡,重连 8080 验证端口已恢复 | -| **通过标准** | recv 返回 0 且耗时 ≥ 60s;s0 回 LISTEN 且可接受新连接 | +| **测试步骤** | 1. PC 连接 8080,不发数据
2. 立即禁用 PC 网卡(中间有交换机 → 设备侧 PHY 链路保持)
3. 等待芯片报 TIM_OUT(实测判死窗:连接起 **18~31s**,芯片定时语义与手册公式 75s 不符,见实验方案 E11/E13)
4. 本层 QUARANTINE → pump 重建 → s0 回 LISTEN
5. 恢复网卡,重连 8080 验证端口已恢复 | +| **通过标准** | recv 返回 0 且耗时落在 **10s~120s 有界窗**(下限防"存活空闲会话被误杀"——T2 实测存活对端静默 90s 不触发;上限防"判死缺位");s0 回 LISTEN 且可接受新连接。报告后测试自动释放 socket 等新连接再测(一次性,防对已死连接 10ms 节奏刷假 FAIL) | --- diff --git a/docs/网络/N34-自愈审计/实验方案.md b/docs/网络/N34-自愈审计/实验方案.md index 0ec9ff49..842bed18 100644 --- a/docs/网络/N34-自愈审计/实验方案.md +++ b/docs/网络/N34-自愈审计/实验方案.md @@ -436,8 +436,12 @@ CNT 次无 ACK"后产生 SINT_STAT_TIM_OUT / DISCONNECT。怀疑本仓没有利 - **已采纳转正(2026-10-13,用户批准)**:在 `handle_connect_event` 的 LISTENING 分支加回一行 `ch395f_set_keepalive_enable(sock, 1)`(手册规定位置),并重新打开 Phase 11 测试纳入回归;参数是否调优(如缩短 IDLE)由实测判死窗口需求决定。 -- ⚠ 台架副作用:本轮反复实验后设备落入 E8 残渣风暴态(详见 E12),需**冷启动 - (断电重上电)**恢复,MCU 复位/RESET_ALL 不足以清除。 +- ⚠ 台架副作用:本轮反复实验后设备落入当时称为"残渣风暴"的状态(详见 E12)。 + **该状态已由 E13 重新定性为僵尸会话,30~90s 自动清除,无需冷启动。** +- **产品路径验证(2026-10-13 10:32,转正提交后产品固件)**::7003 连接 → + 禁用 PC 网卡 51s → `[ERR] sock3 TIMEOUT`(**禁用后 17s**)→ QUARANTINE → + pump → `auto-relisten ok` → 网卡恢复后新连接 PASS(KEEPLIVE 回显正常) + → 转正的一行在**产品固件**上闭环生效 ✓ ### E12 结果(2026-10-13 07:18–08:05,SPI 缓存区 TSL 时序 A/B,临时实验固件) @@ -481,6 +485,10 @@ memcpy;**吞吐无回退**(A 40×128KB 51.8s vs B 47.9s)。 TIMEOUT → 即 E8 记录的**残渣风暴态**,且**跨烧录存活**(重烧 + @test PASS 后依旧) → 与 E8"普通复位后仍 12%"一致。A 39/50 与 B 47/50 **两臂都出现**灾难轮 → 该差异**不能归因于 TSL**,E12 的连接级对比**被此状态噪声吞没,无结论**。 +> ⚠ **"跨烧录存活"口径已被 E13(2026-10-13 10:55)推翻**:板上 CH395F 的 +> RSTI 脚接 MCU NRST(`docs/硬件平台/V0.0_网表详细分析报告.md` :174 +> "RSTI←NRST")→ 每次 ST-LINK 烧录/复位都会**硬件复位芯片**;该状态实为 +> **僵尸会话**(E13),锤击几分钟内即再生,"跨烧录存活"是再生造成的错觉。 **结论(诚实口径)**: @@ -489,7 +497,68 @@ TIMEOUT → 即 E8 记录的**残渣风暴态**,且**跨烧录存活**(重 2. 依 Trap 25 铁律「不留未证明的复杂度」:**当前不建议改产品路径**,而是 **登记该偏离 + 本轮证据**;B 变体作为**待用补丁**保留(若将来出现芯片批次 差异、提高 SCK、或不明数据错位,可直接采用——它已证明无吞吐/正确性回退)。 -3. **方法学教训(后续 SPI 时序实验必读)**:本台架的**芯片残渣风暴态**必须在 - 实验前后受控(冷启动断电重上电,或专用的"残渣清理协议");**MCU 覆写烧录 - (复位 + RESET_ALL)不足以清除**——实测同一镜像在重烧后仍可 0/50..47/50 摆动, - 跨臂污染使 A/B 失效。E8 起就存在的这条口径,本轮再次被独立复现。 +3. **方法学教训(E13 修正版,后续时序/连接类实验必读)**:本台架的**僵尸会话 + 态**(曾称"芯片残渣风暴")必须在实验前后受控:由"对端静默死亡"(网卡禁用/ + 拔线/PC 崩溃)触发,芯片 TIMEOUT(有在途 TX ≈30s)/ keepalive(纯空闲 + ≈75-90s)自动清除;锤击会在几分钟内再生僵尸 → 同一镜像重烧后仍可 + 0/50..47/50 摆动、跨臂污染使 A/B 失效。**正确控制 = 下一臂开跑前静置 + ≥90s 让僵尸清窗**,不是断电冷启动(RSTI←NRST,烧录本身就是芯片硬件复位)。 + +### E13 结果(2026-10-13 10:24–10:59,风暴态根因=僵尸会话:确定性复现,产品固件+网卡抖动注入) + +**假设**:E8/E12 记录的"残渣风暴态"不是跨烧录存活的芯片内部残渣,而是 +**僵尸会话**(对端静默死亡 → 芯片侧 ESTABLISHED 未清)+ 单连接监听被僵尸占用 + +PASV 重监听被垂死僵尸 accept 的竞态——且可由网卡抖动**确定性复现**。 + +**推翻"跨烧录存活"的硬件事实**:板上 CH395F 的 **RSTI 脚接 MCU NRST** +(`docs/硬件平台/V0.0_网表详细分析报告.md` :174 "RSTI←NRST";手册 +`docs/芯片手册/CH395F.md` :935 亦载"通过 RSTI 引脚拉低控制复位") +→ 每次 ST-LINK 烧录/复位(`-Rst -Run`)都会**硬件复位芯片**。 +"风暴态跨烧录存活"与接线事实矛盾——存活的不是芯片内部状态。 + +**排除 4 元组复用假设**(`test/host/pasv_probe.py`,本轮新增):5 轮干净 STOR +打印每轮 PASV 端口 + PC 源端口 → PASV 端口**递增**(49157→49161)、PC 源端口 +随机变化 → "芯片复用 PASV 端口 + TIME_WAIT 4 元组冲突"假设**被否**。 + +**确定性复现(100%,产品固件,零插桩)**: +`ftp_integrity_test.py --rounds 40 --size 131072` 正常跑到第 21 轮(前 20 轮 +全 OK)→ **禁用 PC 网卡 8s**(10:55:01,打断传输中会话)→ 恢复网卡 → +`n30_repro.py` 首连 :21 **ConnectionRefused**。 + +**因果链全程(串口日志)**: + +| 时刻 | 事件 | 解读 | +|---|---|---| +| 10:55:01 | 网卡禁用:PC 内核直接丢弃全部 socket,**不发任何 FIN/RST** | 芯片毫不知情 → sock2/sock4 成 **ESTABLISHED 僵尸**;设备侧 30s 内零网络事件 | +| 10:55:30 | `[ERR][NET] sock2 TIMEOUT` | 芯片**重传耗尽**(设备有未 ACK 的在途 TX)→ 僵尸清除,**+30s** | +| 10:55:31 | `sock2 pump: chip not listening (0x05/0x00) -> relisten` → `auto-relisten ok` | 本层 pump 接住 → 监听复活 | +| 10:55:43 | 新连接 accept → PASV → `sock4 standalone accept` → **20ms** 后 `suspect DISCONNECT on live conn (chip 0x05/0x00), defer to reconcile` | **PASV 竞态**:sock4 的僵尸(被打断轮遗留)未清,新监听器的 accept 为垂死僵尸开枪 → lftpd 5s accept 窗空转 → 425 | +| ~10:58 | n30 ×3 全 OK | sock4 僵尸被 TIMEOUT/reconcile 清除 → 自愈完成 | + +**结论**: + +1. **风暴态根因 = 僵尸会话**,不是芯片残渣:对端静默死亡(网卡禁用/拔线/PC + 崩溃/进程被杀)是注入源;上午 E11 反复网卡抖动 + E12 460 轮锤击 + = 完美风暴生成器。失败率随时间摆动(E8 的 2%~54%)= 僵尸生成/清除的节奏。 +2. **"跨烧录存活"是错觉**:重烧后芯片干净,但锤击几分钟内再生半死会话。 + E8/E12 的"需断电冷启动"口径作废(见上方修正标记)。 +3. **现有自愈链端到端有效**(② keepalive 转正 + pump + 对账① 的合力, + 本轮首次在同一场景全链验证): + - 有在途 TX 的僵尸:芯片 TIMEOUT(重传耗尽)≈30s; + - 纯空闲僵尸:keepalive 60/5/3(E11 T5 实测 30s、产品验证 17s); + - 本层 pump/对账接住事件 → relisten → 端口恢复。 +4. **剩余缺口(阶段 2 收口)**:PASV 重监听**不预清**数据 Socket——僵尸未清 + 就在同一 Socket 上开新监听 → accept 为垂死僵尸开枪 → 5s 窗。阶段 2 + `sock_reset`(open 前 close+等 CLOSED)直接消除该竞态。 +5. **e02_probe listener=FAIL 重解读**(非阶段 1.5 回归):探针的挂起客户端 + socket 在 PC 侧仍打开(脚本在第二次连接尝试之后才 close),PC **内核**会对 + 芯片 keepalive 探测自动 ACK → 僵尸会话"活着" → 单连接监听被占、拒新连接是 + **正确行为**;探针预期("预算耗尽 → 设备拆连接")与阶段 1-5"EOF 不 close" + 设计冲突。真实缺口 = **应用层无法主动拆掉自己放弃的会话** → 也是阶段 2 + `sock_reset` 的职责(应用侧形态)。 +6. 顺带登记(显示层,低):`[FTP] connection received from 100.1.168.192:21` + ——对端地址显示为设备自身 IP 的字节序反转 + 端口 21,疑似 lftpd 日志把 + local 地址当 peer 地址打印(不影响功能,未立项)。 + +**工具与留痕**:`test/host/pasv_probe.py`(新增);pktmon 抓包留存 +`%TEMP%\n30_cap.etl`(3.9MB,含网卡抖动窗口,设备 IP 过滤)。 diff --git a/test/board/ch395f_test.h b/test/board/ch395f_test.h index b11d2ee5..8feac10a 100644 --- a/test/board/ch395f_test.h +++ b/test/board/ch395f_test.h @@ -69,7 +69,9 @@ extern "C" { //#define ENABLE_PHASE12_TESTS /* [硬] SPI 字节命令 / DMA 时延基线(L1/L2,无需 PC)—— M1 已完成,基线见说明文档 §6.1 */ /* ---- 链路时延(M2/M3;分段/阈值见 docs/测试说明/链路延迟测试说明.md)---- */ //#define ENABLE_PHASE13_TESTS /* [网] 芯片 + 队列时延(L3~L7/L10,需 PC peer)—— M2 已完成,基线见说明文档 §6.2 */ -#define ENABLE_PHASE14_TESTS /* [网] 生产路径时延(L8~L11 + 模拟器 Δ)—— M3,需 peer --mode remo(:7003) */ +#define ENABLE_PHASE14_TESTS /* [网] 生产路径时延(L8~L11 + 模拟器 Δ)—— M3,需 peer --mode remo(:7003); + * 与 Phase 11 共存安全:peer 缺席时有 LAT_P14_TIMEOUT_MS 整体超时 + * 自动 done,不永久挡 accept */ #if defined(ENABLE_PHASE1_TESTS) || defined(ENABLE_PHASE2_TESTS) || \ defined(ENABLE_PHASE3_TESTS) || defined(ENABLE_PHASE4_TESTS) || \ diff --git a/test/board/net_test_task.c b/test/board/net_test_task.c index 32b503dc..f3bdbc67 100644 --- a/test/board/net_test_task.c +++ b/test/board/net_test_task.c @@ -801,9 +801,9 @@ static void phase10_run(int32_t sockfd) { * 入口条件:Socket 处于 ESTABLISHED 且对端为**死对端**——PC 连 8080 后 * 禁用网卡(存活对端会 ACK 探测、TIMEOUT 永不触发,本测试将 * 一直等待;防火墙规则对已建立连接无效,E11 实证)。 - * 通过:从连接建立到 recv 返回 0 的间隔 ≥ 60s(IDLE 阈值)。 - * 注:芯片定时语义与手册公式(75s)不完全一致,实测判死窗口见 - * 实验方案 E11(对端失联后 ≈30s)。 + * 通过:从连接建立到 recv 返回 0 的间隔落在 10s~120s 有界窗 + * (芯片定时语义与手册公式 75s 不符,实测判死 18~31s—— + * 见实验方案 E11 与 phase11_run 内判据注释)。 * =================================================================== */ #ifdef ENABLE_PHASE11_TESTS @@ -811,27 +811,40 @@ static int32_t s_phase11_armed = 0; static uint32_t s_phase11_start = 0; static uint8_t s_phase11_buf[64]; /* 专用小缓冲:不占 32KB s_rx_buf(RAM 余量) */ -static void phase11_run(int32_t sockfd) { +/* 返回值:0 = 测试进行中;1 = 已报告完成(主循环据此释放 phase_active, + * 等待**新连接**再测。勿删本机制——报告后连接已死,recv 恒返 0, + * 继续 arm 会以 10ms 节奏刷出数百条假 FAIL(2026-10-13 实跑实证) */ +static int phase11_run(int32_t sockfd) { /* * 第一次调用:记录开始时间并 arm。 * 后续调用:非阻塞 recv,若返回 0 表示连接已断开(KeepAlive 触发)。 - * 计算耗时与 60s idle 阈值比较。 + * 计算耗时与有界窗判据比较。 */ if (!s_phase11_armed) { s_phase11_armed = 1; s_phase11_start = HAL_GetTick(); DBG_INFO("Phase11: idle wait for KeepAlive timeout (60s)"); - return; + return 0; } int32_t n = net_recv(sockfd, s_phase11_buf, 64, NET_MSG_DONTWAIT); if (n == 0) { uint32_t elapsed = HAL_GetTick() - s_phase11_start; - TEST_CHECK(elapsed >= 60000, - "Phase11: KeepAlive triggered after %lums (expect >=60000ms)", elapsed); + /* 判据(E11 实测修正版):芯片定时语义与手册公式(75s)不符—— + * 生产参数实测判死窗口 18~31s(产品验证 连接+30.5s、E11 T5 + * 对端失联+30s、T4 短参数 连接+~10s)。本 socket 无在途 TX、 + * 对端静默且已失联 → 唯一开枪源是 keepalive,故判据为 + * **有界窗**:>=10s(存活空闲会话 T2 实测 90s 不被杀,10s 下限 + * 区分"误杀"与"keepalive 判死")且 <=120s(判死必须发生)。 + * 勿改回 >=60000——那是对着手册公式写的,会在正确固件上假 FAIL */ + TEST_CHECK(elapsed >= 10000U && elapsed <= 120000U, + "Phase11: KeepAlive triggered after %lums (expect 10000..120000ms)", + elapsed); TEST_REPORT("Phase 11"); s_phase11_armed = 0; + return 1; } + return 0; } #endif @@ -920,7 +933,9 @@ void net_layer_test_main(void) phase10_run(sockfd); #endif #ifdef ENABLE_PHASE11_TESTS - phase11_run(sockfd); + if (phase11_run(sockfd)) { + phase_active = 0; /* 已报告:释放主循环去 accept 新连接(net 层已 auto_relisten) */ + } #endif #ifdef ENABLE_PHASE13_TESTS /* Phase 13(链路时延 L3~L7/L10,M2):独立运行,勿与其他 Phase 混选; diff --git a/test/host/pasv_probe.py b/test/host/pasv_probe.py new file mode 100644 index 00000000..c5d2359c --- /dev/null +++ b/test/host/pasv_probe.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +pasv_probe.py — decisive probe for the "storm" signature (N34). + +Per FTP round prints: device PASV port, PC data-socket SOURCE port, round +result, and timing. Hypothesis under test: the CH395F reuses the same +ephemeral PASV port for the next data listener while the previous data +connection is still in chip-side TIME_WAIT with the same 4-tuple +(PC source port reused too) -> chip reports CONNECT then RSTs ~10ms later +(serial: "sock4 standalone accept" + "DISCONNECTED chip st=0x00/0x00"). + +Usage: + python pasv_probe.py --rounds 5 --delay 2 # reproduce (fast rounds) + python pasv_probe.py --rounds 3 --delay 35 # TIME_WAIT window test +""" +import argparse +import re +import socket +import sys +import time + +IP_DEFAULT = "192.168.1.100" + + +def ftp_stor_round(ip, user, pwd, fname, payload): + """One login -> PASV -> STOR -> verify transfer. Returns (ok, pasv_port, + pc_src_port, detail).""" + c = socket.create_connection((ip, 21), timeout=10) + c.settimeout(10) + pc_src = None + pasv_port = None + try: + def readline(): + buf = b"" + while not buf.endswith(b"\n"): + ch = c.recv(1) + if not ch: + break + buf += ch + return buf.decode(errors="replace").strip() + + def cmd(line): + c.sendall((line + "\r\n").encode()) + return readline() + + greet = readline() + if not greet.startswith("220"): + return False, None, None, "no greeting: " + greet + r = cmd("USER " + user) + if not r.startswith("331"): + return False, None, None, "USER: " + r + r = cmd("PASS " + pwd) + if not r.startswith("230"): + return False, None, None, "PASS: " + r + r = cmd("PASV") + m = re.search(r"\((\d+),(\d+),(\d+),(\d+),(\d+),(\d+)\)", r) + if not r.startswith("227") or not m: + return False, None, None, "PASV: " + r + g = [int(x) for x in m.groups()] + pasv_port = g[4] * 256 + g[5] + d = socket.create_connection((ip, pasv_port), timeout=8) + pc_src = d.getsockname()[1] + r = cmd("STOR " + fname) + if not r.startswith(("125", "150")): + d.close() + return False, pasv_port, pc_src, "STOR: " + r + d.sendall(payload) + d.close() + # drain until final 2xx (multi-line or single) + final = "" + t0 = time.time() + while time.time() - t0 < 10: + r = readline() + if not r: + break + final = r + if r[:1] in ("2", "4", "5"): + break + ok = final.startswith("2") + cmd("QUIT") + return ok, pasv_port, pc_src, ("OK" if ok else "final: " + final) + finally: + c.close() + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--ip", default=IP_DEFAULT) + ap.add_argument("--rounds", type=int, default=5) + ap.add_argument("--delay", type=float, default=2.0, + help="seconds between rounds") + ap.add_argument("--user", default="admin") + ap.add_argument("--pwd", default="admin") + ap.add_argument("--size", type=int, default=1024) + args = ap.parse_args() + + payload = bytes((i * 7 + 3) & 0xFF for i in range(args.size)) + ports, srcs, oks = [], [], [] + for i in range(args.rounds): + t = time.strftime("%H:%M:%S") + ok, port, src, detail = ftp_stor_round(args.ip, args.user, args.pwd, + "PP%d.BIN" % i, payload) + ports.append(port) + srcs.append(src) + oks.append(ok) + print("[%s] round %d: ok=%-5s pasv_port=%-6s pc_src=%-6s %s" + % (t, i + 1, ok, port, src, detail)) + if i + 1 < args.rounds: + time.sleep(args.delay) + + print("\nports :", ports) + print("pc_src:", srcs) + print("ok :", oks) + same_port = len(set(p for p in ports if p)) == 1 + same_src = len(set(s for s in srcs if s)) == 1 + print("PASV port constant: %s | PC source port constant: %s" + % (same_port, same_src)) + return 0 if all(oks) else 1 + + +if __name__ == "__main__": + sys.exit(main()) From f7f17931c900768fa34098839d25fd66fd8d141b Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 13 Oct 2026 12:43:17 +0800 Subject: [PATCH 16/24] =?UTF-8?q?feat(N34=20=E9=98=B6=E6=AE=B52):=20ch395f?= =?UTF-8?q?=20=E4=BA=94=E5=8E=9F=E8=AF=AD=E5=8F=AA=E5=8A=A0=E4=B8=8D?= =?UTF-8?q?=E6=8E=A5=E7=BA=BF=20+=20Phase=2015=20=E5=A5=97=E4=BB=B6=2020/2?= =?UTF-8?q?0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实施方案 docs/网络/N34-自愈审计/实施方案.md §4 五原语,形态按 E2/E3/E4 定案(实验方案.md): 1. 类型化状态回读 + 每 Socket 缓存(open=0 时 tcp_state 哨兵 0xFF;sn>7 边界返 {00,FF} 计 fault); 2. 事件护栏 ch395f_sock_events:只读本 Socket SINT,全局寄存器 唯一读者=net_poll(F-NET-7 契约); 3. ch395f_sock_reset_begin/poll 非阻塞状态机:读空 RX -> CLOSE -> 排空事件(quiescent 2 轮) -> 轮询 byte0=00H;GRACEFUL/ QUARANTINE 最短等待 100/400ms(Trap 23);WAIT_CLOSE 内置 LISTEN 态二段 CLOSE 补发(TC1502b 实证 ~180ms 落定); 4. ch395f_cmd_done 状态回读判据(E3:GET_CMD_STATUS 对 OPEN/LISTEN/CLOSE 无判别力); 5. 链路态缓存 ch395f_glob_event_feed + link_up/变化计数 (B9 判据 PHY!=DISCONN)。 Phase 15 板测(硬件层模式,Socket 7,无需 PC):20/20 PASS, 含二段关闭语义断言(一段 05/00、二段 byte0=00)。 RAM +56B < 64B(g_sock_state 16B + g_rst 32B + 链路/fault 8B); 产品 Code 与阶段 2 前逐字节同 209780(无调用者被链接器裁剪)。 @test PASS;relisten_stress 20/20、n30 3/3 零回归。 RAM 台账同步:RW_IRAM1 97952B 74.7% / RW_CCM 57376B 87.9%。 --- Drivers/BSP/CH395F/ch395f.c | 367 +++++++++++++++++++++++++ Drivers/BSP/CH395F/ch395f.h | 107 +++++++ docs/内存管理/RAM使用分析与优化方案.md | 4 +- docs/内存管理/RAM预算台账.md | 4 +- docs/测试说明/CH395F_Test_Guide.md | 30 +- scripts/ram_baseline.tsv | 1 + test/board/ch395f_test.h | 4 +- test/board/ch395f_test_task.c | 123 +++++++++ 8 files changed, 634 insertions(+), 6 deletions(-) diff --git a/Drivers/BSP/CH395F/ch395f.c b/Drivers/BSP/CH395F/ch395f.c index c1817d10..87599495 100644 --- a/Drivers/BSP/CH395F/ch395f.c +++ b/Drivers/BSP/CH395F/ch395f.c @@ -1408,3 +1408,370 @@ void ch395f_get_unreach_info(uint8_t *p_buf) { } ch395f_spi_end(); } + +/* ============================================================================ + * N34 阶段 2 驱动原语(实施方案 docs/网络/N34-自愈审计/实施方案.md §4; + * 形态定案 docs/网络/N34-自愈审计/实验方案.md E2/E3/E4,2026-10-13 台架实证) + * 只加不接线:net_poll 全量刷新 / 两级预清改调用 / 链路 feed 接线 = 阶段 3。 + * ========================================================================== */ + +/* 每 Socket 状态缓存(刷新点:主动回读 ch395f_get_sock_state; + * 阶段 3 起 net_poll 每轮对在用 Socket 各刷一次——E2 实测 18.1us/次, + * 4 监听 ~72us 远小于 1ms 轮询周期) */ +static ch395f_sock_state_t g_sock_state[8]; + +/* sock_reset 非阻塞状态机槽位(每 Socket 一个;t0 用 16 位 tick 差值, + * 各阶段超时 <=2s,远小于 65535ms 回绕窗) */ +#define CH395F_RST_ST_IDLE 0U +#define CH395F_RST_ST_RX_DRAIN 1U +#define CH395F_RST_ST_CLOSE 2U +#define CH395F_RST_ST_MIN_WAIT 3U +#define CH395F_RST_ST_EVT_DRAIN 4U +#define CH395F_RST_ST_WAIT_CLOSE 5U +#define CH395F_RST_RX_MAX_ROUNDS 4U /* RX 缓冲 2KB,单读即空;4 为安全上限 */ +#define CH395F_RST_QUIESCENT 2U /* 排空事件判据:连续 2 轮无新位(E8 M5 口径) */ +#define CH395F_RST_RX_TMO_MS 500U +#define CH395F_RST_EVT_TMO_MS 500U +#define CH395F_RST_CLS_TMO_MS 2000U + +typedef struct { + uint16_t t0; /* 当前阶段起始 tick */ + uint8_t state; /* CH395F_RST_ST_* */ + uint8_t flags; /* bit7=mode(1=QUARANTINE) bit6..4=quiescent 轮数 bit3..0=drain 轮数(饱和) */ +} ch395f_rst_slot_t; +static ch395f_rst_slot_t g_rst[8]; + +/* 链路态缓存(F-DRV-13) */ +static uint8_t g_link_up; +static uint8_t g_link_valid; +static uint16_t g_link_changes; + +/* 边界 fault 计数(sn 越界等;日志只留痕一次,其余只计数) */ +static uint16_t g_p2_faults; +static uint8_t g_p2_fault_logged; + +/* + * 函数功能:阶段 2 原语边界异常留痕(首次 DBG_ERROR,之后只计数) + * 入口参数:tag - 出错点短标签 const char* + */ +static void ch395f_p2_fault_note(const char *tag) { + g_p2_faults++; + if (g_p2_fault_logged == 0U) { + g_p2_fault_logged = 1U; + DBG_ERROR("ch395f P2 boundary fault @%s (further ones counted only)", tag); + } +} + +/* + * 函数功能:读空并丢弃 RX(排空契约形态=整缓冲单读,与 ch395f_read_recv_buf + * 同形但不拷出——复位残渣是垃圾,零新增 RAM) + * 入口参数:sn - Socket 索引 uint8_t + * len - 本次读出字节数(<=CH395F_SPI_DMA_MAX_PAYLOAD) uint16_t + */ +static void ch395f_rst_rx_discard(uint8_t sn, uint16_t len) { + s_spi2_dma_tx_buf[0] = CH395F_CMD_READ_RECV_BUF_SN; + s_spi2_dma_tx_buf[1] = sn; + s_spi2_dma_tx_buf[2] = len & 0xFFU; + s_spi2_dma_tx_buf[3] = (len >> 8) & 0xFFU; + memset(&s_spi2_dma_tx_buf[4], 0xFFU, len); + ch395f_spi_begin(); + (void)ch395f_spi_dma_transfer(4U + len); + ch395f_spi_end(); +} + +/* + * 函数功能:主动回读本 Socket 状态并刷新驱动缓存(缓存刷新点之一,另一个 + * 是阶段 3 的 net_poll 每轮全量刷新) + * 入口参数:sn - Socket 索引 uint8_t 0 - 7 + * 返回值:类型化状态;sn>7 返回 {CLOSED, 哨兵} 并计一次边界 fault + * 限定条件:芯片已初始化;netTask 上下文(与其它 CH395F 事务串行) + */ +ch395f_sock_state_t ch395f_get_sock_state(uint8_t sn) { + ch395f_sock_state_t st; + uint8_t raw[2]; + + if (sn >= 8U) { + ch395f_p2_fault_note("get_state sn"); + st.open = CH395F_SOCKET_CLOSED; + st.tcp_state = CH395F_SOCK_STATE_NONE; + return st; + } + ch395f_get_socket_status(sn, raw); + st.open = raw[0]; + st.tcp_state = (raw[0] == CH395F_SOCKET_OPEN) ? raw[1] : CH395F_SOCK_STATE_NONE; + g_sock_state[sn] = st; + return st; +} + +/* + * 函数功能:读驱动缓存(零 SPI 成本;新鲜度取决于刷新点,见上) + * 入口参数:sn - Socket 索引 uint8_t 0 - 7 + * 返回值:最近一次刷新的状态;sn>7 返回 {CLOSED, 哨兵} + * 限定条件:芯片已初始化 + */ +ch395f_sock_state_t ch395f_sock_state_cached(uint8_t sn) { + if (sn >= 8U) { + ch395f_sock_state_t st; + + ch395f_p2_fault_note("cached sn"); + st.open = CH395F_SOCKET_CLOSED; + st.tcp_state = CH395F_SOCK_STATE_NONE; + return st; + } + return g_sock_state[sn]; +} + +/* + * 函数功能:判"本 Socket 正在监听"(读缓存:open=OPEN 且 TCP=LISTEN) + * 入口参数:sn - Socket 索引 uint8_t 0 - 7 + * 返回值:1=在听 0=不在听 int + * 限定条件:缓存新鲜度同 ch395f_sock_state_cached + */ +int ch395f_sock_is_listening(uint8_t sn) { + ch395f_sock_state_t c = ch395f_sock_state_cached(sn); + + return (c.open == CH395F_SOCKET_OPEN && c.tcp_state == CH395F_TCP_LISTEN) ? 1 : 0; +} + +/* + * 函数功能:事件护栏——只读本 Socket SINT(读即清),永不触碰全局寄存器 + * (F-NET-7 收窄:两级预清会清掉其它 Socket 的事件位) + * 入口参数:sn - Socket 索引 uint8_t 0 - 7 + * 返回值:SINT 位图;sn>7 返回 0 uint8_t + * 限定条件:芯片已初始化;全局寄存器唯一读者=net_poll(驱动侧契约) + */ +uint8_t ch395f_sock_events(uint8_t sn) { + if (sn >= 8U) { + ch395f_p2_fault_note("events sn"); + return 0U; + } + /* 契约:只读本 Socket SINT(0x30 读即清),永不触碰全局寄存器 0x19—— + * 全局寄存器读即清且含全部 Socket 事件位,唯一读者=net_poll(F-NET-7) */ + return ch395f_get_sock_int_status(sn); +} + +/* + * 函数功能:启动 Socket 复合复位(非阻塞状态机,每 net_poll 步进;E4 定案序列 + * 读空 RX -> CLOSE -> 排空事件(quiescent) -> 轮询 byte0=00H) + * 入口参数:sn - Socket 索引 uint8_t 0 - 7 + * mode - 复位模式(GRACEFUL 100ms / QUARANTINE 400ms 最短等待) + * 返回值:0=已启动 -1=参数错或已在运行(调用方逻辑错误) int + * 限定条件:netTask 上下文;调用前本层已放弃该会话(复位丢弃 RX 残渣) + */ +int ch395f_sock_reset_begin(uint8_t sn, ch395f_reset_mode_t mode) { + if (sn >= 8U) { + ch395f_p2_fault_note("reset_begin sn"); + return -1; + } + if (g_rst[sn].state != CH395F_RST_ST_IDLE) { + /* 已在运行:调用方逻辑错误(同一 Socket 不允许并发复位) */ + return -1; + } + g_rst[sn].state = CH395F_RST_ST_RX_DRAIN; + g_rst[sn].flags = (mode == CH395F_RESET_QUARANTINE) ? 0x80U : 0x00U; + g_rst[sn].t0 = (uint16_t)HAL_GetTick(); + return 0; +} + +/* + * 函数功能:复位状态机步进(net_poll 或测试循环周期调用,禁复制忙等) + * 入口参数:sn - Socket 索引 uint8_t 0 - 7 + * 返回值:0=完成(含未启动,幂等) 1=进行中 -1=失败(超时/参数,槽位已复位可重开) int + * 限定条件:netTask 上下文 + */ +int ch395f_sock_reset_poll(uint8_t sn) { + ch395f_rst_slot_t *p; + uint16_t now; + uint16_t el; + + if (sn >= 8U) { + ch395f_p2_fault_note("reset_poll sn"); + return -1; + } + p = &g_rst[sn]; + if (p->state == CH395F_RST_ST_IDLE) { + return 0; /* 未启动/已完成:幂等 */ + } + now = (uint16_t)HAL_GetTick(); + el = (uint16_t)(now - p->t0); + + switch (p->state) { + case CH395F_RST_ST_RX_DRAIN: { + /* E4 定案第 1 步:close 前 RX 必须为空(带脏缓冲关闭 -> 拆链停滞 -> + * 毒化下一会话且级联自持,C 变体 0/20 实证) */ + uint16_t len = ch395f_get_recv_len(sn); + uint8_t rounds = (uint8_t)(p->flags & 0x0FU); + + if (len == 0U) { + p->state = CH395F_RST_ST_CLOSE; + p->t0 = now; + break; + } + if (rounds < CH395F_RST_RX_MAX_ROUNDS) { + ch395f_rst_rx_discard(sn, (len > (uint16_t)CH395F_SPI_DMA_MAX_PAYLOAD) + ? (uint16_t)CH395F_SPI_DMA_MAX_PAYLOAD : len); + p->flags = (uint8_t)((p->flags & 0xF0U) | (rounds + 1U)); + } else { + /* 排不空(异常形态):不再恋战,继续 CLOSE,失败由 WAIT_CLOSE 超时兜底 */ + ch395f_p2_fault_note("rx drain stall"); + p->state = CH395F_RST_ST_CLOSE; + p->t0 = now; + } + if ((uint16_t)(now - p->t0) > CH395F_RST_RX_TMO_MS) { + p->state = CH395F_RST_ST_IDLE; + ch395f_p2_fault_note("rx drain tmo"); + return -1; + } + break; + } + case CH395F_RST_ST_CLOSE: + /* E4 定案第 2 步:CLOSE(裸命令,不复制 poll_cmd_status 忙等—— + * E3 实证其对 CLOSE 无判别力;完成判据=状态回读,E3 定案) */ + ch395f_spi_begin(); + ch395f_write_cmd(CH395F_CMD_CLOSE_SOCKET_SN); + ch395f_write_data(sn); + ch395f_spi_end(); + p->state = CH395F_RST_ST_MIN_WAIT; + p->t0 = now; + break; + case CH395F_RST_ST_MIN_WAIT: { + uint16_t need = ((p->flags & 0x80U) != 0U) ? 400U : 100U; + if (el >= need) { + p->state = CH395F_RST_ST_EVT_DRAIN; + p->t0 = now; + p->flags = (uint8_t)(p->flags & 0x8FU); /* quiescent 计数清零 */ + } + break; + } + case CH395F_RST_ST_EVT_DRAIN: { + /* E4 定案第 3 步:排空事件至 quiescent(陈旧粘滞位会误导事件驱动 + * accept——E4 C 变体签名 0x04 陈旧 RECV;读即清,连续 2 轮无新位即静) */ + uint8_t bits = ch395f_sock_events(sn); + if (bits == 0U) { + uint8_t q = (uint8_t)((p->flags >> 4) & 0x07U); + if ((q + 1U) >= CH395F_RST_QUIESCENT) { + p->state = CH395F_RST_ST_WAIT_CLOSE; + p->t0 = now; + } else { + p->flags = (uint8_t)((p->flags & 0x8FU) | ((q + 1U) << 4)); + } + } else { + p->flags = (uint8_t)(p->flags & 0x8FU); /* 有新位:quiescent 计数清零 */ + } + if (el > CH395F_RST_EVT_TMO_MS) { + p->state = CH395F_RST_ST_IDLE; + ch395f_p2_fault_note("evt drain tmo"); + return -1; + } + break; + } + case CH395F_RST_ST_WAIT_CLOSE: + /* E4 定案第 4 步:轮询 byte0=00H(E4 实测 closed_ms=0——命令落定即空, + * 但为契约必需步) */ + if (ch395f_cmd_done(sn, CH395F_DONE_CLOSE) == 1) { + p->state = CH395F_RST_ST_IDLE; + return 0; + } + /* 二段关闭实证(TC1501 台架 2026-10-13):LISTEN 态 Socket 的 CLOSE 返 + * SUCCESS 但只拆监听(TCP 01->00),byte0 恒 05;需再发一次 CLOSE 才落 + * 00(生产 net 层靠 Trap 09 open 重试吸收此行为)。复位状态机自行补发, + * 补发标记=drain 字段置 0xF(本阶段已用尽,复用安全)。 */ + { + ch395f_sock_state_t c = ch395f_sock_state_cached(sn); + if (c.open == CH395F_SOCKET_OPEN && c.tcp_state == CH395F_TCP_CLOSED + && (p->flags & 0x0FU) != 0x0FU) { + p->flags = (uint8_t)((p->flags & 0xF0U) | 0x0FU); + ch395f_spi_begin(); + ch395f_write_cmd(CH395F_CMD_CLOSE_SOCKET_SN); + ch395f_write_data(sn); + ch395f_spi_end(); + } + } + if (el > CH395F_RST_CLS_TMO_MS) { + p->state = CH395F_RST_ST_IDLE; + ch395f_p2_fault_note("wait closed tmo"); + return -1; + } + break; + default: + p->state = CH395F_RST_ST_IDLE; + ch395f_p2_fault_note("bad rst state"); + return -1; + } + return 1; +} + +/* + * 函数功能:非阻塞命令完成查询(E3 定案=状态回读判据;单次回读 18.1us,E2 背书) + * 入口参数:sn - Socket 索引 uint8_t 0 - 7 + * what - 完成判据(OPEN=byte0 05H / LISTEN=+TCP 01H / CLOSE=byte0 00H) + * 返回值:1=已完成 0=未完成 -1=参数错 int + * 限定条件:netTask 上下文;勿复制 poll_cmd_status 忙等(E3 实证无判别力) + */ +int ch395f_cmd_done(uint8_t sn, ch395f_done_what_t what) { + ch395f_sock_state_t st; + + if (sn >= 8U) { + return -1; + } + st = ch395f_get_sock_state(sn); /* 主动回读,单次 18.1us(E2) */ + switch (what) { + case CH395F_DONE_OPEN: + return (st.open == CH395F_SOCKET_OPEN) ? 1 : 0; + case CH395F_DONE_LISTEN: + return (st.open == CH395F_SOCKET_OPEN && st.tcp_state == CH395F_TCP_LISTEN) ? 1 : 0; + case CH395F_DONE_CLOSE: + return (st.open == CH395F_SOCKET_CLOSED) ? 1 : 0; + default: + return -1; + } +} + +/* + * 函数功能:喂全局中断位(阶段 3 接线:net_poll 读全局后喂 bit0~3; + * bit2 PHY_CHANGE 置位时重读 PHY 更新 link_up 缓存 + 变化计数) + * 入口参数:glob_bits - 全局中断状态低字节语义位图 uint16_t + * 返回值:无 + * 限定条件:netTask 上下文(会发 SPI 读 PHY) + */ +void ch395f_glob_event_feed(uint16_t glob_bits) { + uint8_t up; + + if ((glob_bits & CH395F_GINT_STAT_PHY_CHANGE) == 0U) { + return; + } + /* B9 判据(Trap 09 修正):PHY != DISCONN 即 link up */ + up = (ch395f_get_phy_status() != CH395F_PHY_DISCONN) ? 1U : 0U; + if (g_link_valid == 0U) { + g_link_valid = 1U; + g_link_up = up; + } else if (up != g_link_up) { + g_link_up = up; + g_link_changes++; + } +} + +/* + * 函数功能:读链路态缓存(首次查询惰性读 PHY 建档,不依赖 feed 已发生) + * 入口参数:无 + * 返回值:1=link up 0=down uint8_t(B9 判据:PHY != DISCONN 即 up) + * 限定条件:芯片已初始化 + */ +uint8_t ch395f_link_up_cached(void) { + if (g_link_valid == 0U) { + /* 惰性建档(首次查询):不依赖 feed 已发生过 */ + g_link_up = (ch395f_get_phy_status() != CH395F_PHY_DISCONN) ? 1U : 0U; + g_link_valid = 1U; + } + return g_link_up; +} + +/* + * 函数功能:读链路变化计数(net 层订阅:计数变化 -> 日志/可选重配) + * 入口参数:无 + * 返回值:自建档以来的 PHY 连接状态翻转次数 uint16_t + * 限定条件:无 + */ +uint16_t ch395f_link_change_count(void) { + return g_link_changes; +} diff --git a/Drivers/BSP/CH395F/ch395f.h b/Drivers/BSP/CH395F/ch395f.h index a39aa49d..cb3cd9f0 100644 --- a/Drivers/BSP/CH395F/ch395f.h +++ b/Drivers/BSP/CH395F/ch395f.h @@ -747,6 +747,113 @@ void ch395f_set_ttl(uint8_t sock, uint8_t ttl); */ void ch395f_get_unreach_info(uint8_t *p_buf); +/* + * 函数声明区 - N34 阶段 2 驱动原语(实施方案 docs/网络/N34-自愈审计/实施方案.md §4; + * 形态定案 docs/网络/N34-自愈审计/实验方案.md E2/E3/E4) + * ⚠ 阶段 2 = 只加不接线:net_poll 全量刷新、三处两级预清改调用、链路 feed 接线 + * 均在阶段 3 收敛时进行。E2 实测:单次状态回读 18.1us;LISTEN 可见延迟 100% <=10ms。 + */ + +/* 类型化状态回读 + 每 Socket 缓存(F-DRV-1/3) */ +#define CH395F_SOCK_STATE_NONE 0xFFU /* open=CLOSED 时 tcp_state 哨兵("无意义") */ + +typedef struct { + uint8_t open; /* byte0:CH395F_SOCKET_CLOSED / CH395F_SOCKET_OPEN */ + uint8_t tcp_state; /* byte1:CH395F_TCP_*;open=CLOSED 时为 CH395F_SOCK_STATE_NONE */ +} ch395f_sock_state_t; + +/* + * 函数功能:主动回读本 Socket 状态并刷新驱动缓存(缓存刷新点之一,另一个 + * 是阶段 3 的 net_poll 每轮全量刷新) + * 入口参数:sn - Socket 索引 uint8_t 0 - 7 + * 返回值:类型化状态;sn>7 返回 {CLOSED, 哨兵} 并计一次边界 fault uint8_t×2 + * 限定条件:芯片已初始化;netTask 上下文(与其它 CH395F 事务串行) + */ +ch395f_sock_state_t ch395f_get_sock_state(uint8_t sn); + +/* + * 函数功能:读驱动缓存(零 SPI 成本;数据新鲜度取决于刷新点,见上) + * 入口参数:sn - Socket 索引 uint8_t 0 - 7 + * 返回值:最近一次刷新的状态;sn>7 返回 {CLOSED, 哨兵} + */ +ch395f_sock_state_t ch395f_sock_state_cached(uint8_t sn); + +/* + * 函数功能:判"本 Socket 正在监听"(读缓存:open=OPEN 且 TCP=LISTEN) + * 入口参数:sn - Socket 索引 uint8_t 0 - 7 + * 返回值:1=在听 0=不在 int + */ +int ch395f_sock_is_listening(uint8_t sn); + +/* + * 函数功能:事件护栏——只读本 Socket SINT(0x30,读即清),永不触碰全局寄存器 + * (F-NET-7 收窄:两级预清"先读全局再读本 socket"会清掉其它 Socket 的 + * 事件位;全局寄存器唯一读者=net_poll 写成驱动侧契约) + * 入口参数:sn - Socket 索引 uint8_t 0 - 7 + * 返回值:SINT 位图;sn>7 返回 0 uint8_t + */ +uint8_t ch395f_sock_events(uint8_t sn); + +/* 复合复位模式(F-DRV-4;两模式都必须排 RX——E4 定案,差异只剩最短等待) */ +typedef enum { + CH395F_RESET_GRACEFUL = 0, /* 最短等待 100ms(Trap 23 二阶段实测值) */ + CH395F_RESET_QUARANTINE /* 最短等待 400ms */ +} ch395f_reset_mode_t; + +/* + * 函数功能:启动 Socket 复合复位(非阻塞状态机,每 net_poll 步进;内部序列 + * E4 定案:读空 RX -> CLOSE -> 排空事件(quiescent) -> 轮询 byte0=00H) + * 入口参数:sn - Socket 索引 uint8_t 0 - 7 + * mode - 复位模式 ch395f_reset_mode_t + * 返回值:0=已启动 -1=参数错或已在运行(调用方逻辑错误) int + * 限定条件:netTask 上下文;调用前本层已放弃该会话(复位会丢弃 RX 残渣) + */ +int ch395f_sock_reset_begin(uint8_t sn, ch395f_reset_mode_t mode); + +/* + * 函数功能:复位状态机步进(在 net_poll 或测试循环里周期调用,禁忙等复制) + * 入口参数:sn - Socket 索引 uint8_t 0 - 7 + * 返回值:0=完成(含未启动,幂等) 1=进行中 -1=失败(超时/参数,槽位已复位可重开) int + */ +int ch395f_sock_reset_poll(uint8_t sn); + +/* 命令完成判据(F-DRV-8;E3 定案=状态回读,GET_CMD_STATUS 对这三条无判别力) */ +typedef enum { + CH395F_DONE_OPEN = 0, /* open 完成 = byte0 变 05H */ + CH395F_DONE_LISTEN, /* listen 完成 = byte0=05H 且 TCP=01H(E2:<=10ms 可见) */ + CH395F_DONE_CLOSE /* close 完成 = byte0=00H */ +} ch395f_done_what_t; + +/* + * 函数功能:非阻塞命令完成查询(单次主动回读 18.1us,E2 背书;勿复制 + * poll_cmd_status 式忙等——E3 实证其对 OPEN/LISTEN/CLOSE 恒白等且无判别力) + * 入口参数:sn - Socket 索引 uint8_t 0 - 7 + * what - 完成判据 ch395f_done_what_t + * 返回值:1=已完成 0=未完成 -1=参数错 int + */ +int ch395f_cmd_done(uint8_t sn, ch395f_done_what_t what); + +/* 链路态缓存(F-DRV-13;接线在阶段 3:net_poll 读全局后喂 bit0~3) */ + +/* + * 函数功能:喂全局中断位(net_poll 读 GET_GLOB_INT_STATUS_ALL 后调用; + * bit2 PHY_CHANGE 置位时驱动重读 PHY 更新 link_up 缓存 + 变化计数) + * 入口参数:glob_bits - 全局中断状态低字节语义位图 uint16_t + */ +void ch395f_glob_event_feed(uint16_t glob_bits); + +/* + * 函数功能:读链路态缓存(首次查询惰性读 PHY 建档) + * 返回值:1=link up 0=down uint8_t(判据 B9:PHY != DISCONN 即 up) + */ +uint8_t ch395f_link_up_cached(void); + +/* + * 函数功能:读链路变化计数(net 层订阅用:计数变化 -> 日志/可选重配) + * 返回值:自建档以来的 PHY 连接状态翻转次数 uint16_t + */ +uint16_t ch395f_link_change_count(void); + #ifdef __cplusplus } #endif diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 0ecfa972..68821deb 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -37,9 +37,9 @@ | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| | ER_IROM1 | 228,372 B (223.02 KB) | 1,048,576 B (1024 KB) | 21.8% | -| RW_IRAM1 | 97,896 B (95.60 KB) | 131,072 B (128 KB) | 74.7% | +| RW_IRAM1 | 97,952 B (95.66 KB) | 131,072 B (128 KB) | 74.7% | | RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | -| **Flash (ROM 合计)** | **229,476 B (224.1 KB)** | 1,048,576 B (1024 KB) | **21.9%** | +| **Flash (ROM 合计)** | **229,484 B (224.1 KB)** | 1,048,576 B (1024 KB) | **21.9%** | 各区的容量与常量来源: diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index 1056b566..fe53fb96 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -18,11 +18,11 @@ | ER_IROM1 (Flash) | 228,372 B | 1,048,576 B | 21.8% | 参考 | | RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS | | UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 | -| RW_IRAM1 | 97,896 B | 131,072 B | 74.7% | PASS | +| RW_IRAM1 | 97,952 B | 131,072 B | 74.7% | PASS | | 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 | |---|---|---|---| -| 网络 | 13,442 B | 12 KB | ⚠ 超 1.1 KB | +| 网络 | 13,498 B | 12 KB | ⚠ 超 1.2 KB | | 存储 | 7,056 B | 12 KB | OK | | UI | 54,988 B | 16 KB | ⚠ 超 37.7 KB | | RTOS 堆 | 57,376 B | 56 KB | 堆=配额 | diff --git a/docs/测试说明/CH395F_Test_Guide.md b/docs/测试说明/CH395F_Test_Guide.md index b941f5c6..4be57f86 100644 --- a/docs/测试说明/CH395F_Test_Guide.md +++ b/docs/测试说明/CH395F_Test_Guide.md @@ -84,7 +84,7 @@ MCU: STM32F407ZGTx @ 168MHz, Keil MDK-ARM v5 (ARMCC) | 构建模式 | 启用宏 | 固件形态 | |----------|--------|----------| -| **硬件层模式** | `ENABLE_PHASE1~5_TESTS` | netTask 自挂起(基础线程创建但不运行),CH395F 协议栈初始化由 testTask 完成后独占芯片执行阶段 1~5(裸调 `ch395f_*` 命令) | +| **硬件层模式** | `ENABLE_PHASE1~5_TESTS`(另含 12/15) | netTask 自挂起(基础线程创建但不运行),CH395F 协议栈初始化由 testTask 完成后独占芯片执行阶段 1~5(裸调 `ch395f_*` 命令) | | **NET 层模式** | `ENABLE_PHASE6~11_TESTS` | 完整网络栈:netTask 运行 `net_poll()` 轮询,testTask 经 `net_socket` API 执行阶段 6~11(`net_layer_test_main()` 回显、`net_layer_client_test_main()` Client 等) | **执行顺序严格递增**:先烧录硬件层固件完成阶段 1→2→3→4→5(全部通过),再切换为 NET 层固件执行阶段 6→7→…→11。硬件层测试期间 netTask 轮询不存在,从根上排除中断偷取与 SPI 并发问题。 @@ -1101,6 +1101,30 @@ IDLE → ACCEPT → [SEND: 分块发 → DONE] / [RECV: 分块收+校验 → DON +## 12.5 阶段 15:N34 阶段 2 驱动原语(硬件层,无需 PC) + +验证 `ch395f` 新增自愈原语(`docs/网络/N34-自愈审计/实施方案.md` §4;形态定案 +`docs/网络/N34-自愈审计/实验方案.md` E2/E3/E4)。硬件层模式独占芯片,用 Socket 7 +(产品监听 0~4 不涉),无需 PC 配合。污染会话下的复位载荷完整性不在本 Phase +(已由 E4 bench 60 轮实证),本 Phase 只验状态机跑通与判据语义。 + +| 用例 | 内容 | 通过标准 | +|------|------|----------| +| TC-NET-1501 | 状态回读一致性 + `ch395f_cmd_done` 语义 + 二段关闭 + 哨兵/边界 | open done ≤50ms;listen done ≤20ms(E2:100% ≤10ms);缓存 05/01;**LISTEN 态 close 二段**:一段后 05/00(监听已拆、byte0 未落)、二段后 byte0=00 ≤500ms;closed 时 tcp_state=0xFF 哨兵;sn=8 越界返 {00,FF} | +| TC-NET-1502 | `sock_reset_begin/poll` 非阻塞状态机(OPEN 态,QUARANTINE) | begin 受理;运行中二次 begin 返 -1(busy reject);≤3000ms 落定 rc=0;复位后芯片 CLOSED、poll 幂等返 0、可重开 | +| TC-NET-1502b | LISTEN 态 Socket 复位(GRACEFUL,走 WAIT_CLOSE 二段补发路径) | ≤3000ms 落定 rc=0(实测 ~180ms);复位后 CLOSED | +| TC-NET-1503 | 链路态缓存 + 事件护栏边界 | 稳定链路 feed 两次不虚增变化计数;link_up 缓存=1;`sock_events(8)` 越界返 0 | + +**二段关闭语义**(本 Phase 首跑实证,详见实验方案 E2 补充发现):LISTEN 态 +`CLOSE_SOCKET` 返 SUCCESS 但只拆监听(TCP 01→00),byte0 恒 05;需再发一次 +CLOSE 才落 00。与 Trap 07 的 `0x05/0x00` 驻留签名同源;`sock_reset_poll` 的 +WAIT_CLOSE 阶段已内置二段补发(只补一次),生产 net 层则靠 Trap 09 open 重试吸收。 + +**运行方法**:`ch395f_test.h` 启用 `ENABLE_PHASE15_TESTS`(自动进入硬件层模式, +勿与 NET 层 Phase 混选)→ `@build` → 烧录 → 串口看 `=== Phase 15: 20/20 PASSED ===`。 + +--- + ## 13. Python 测试脚本参考 | 命令 | 模式 | 说明 | @@ -1180,6 +1204,10 @@ IDLE → ACCEPT → [SEND: 分块发 → DONE] / [RECV: 分块收+校验 → DON | TM-PHY-10 | TC-NET-1005 | P1 | 压力测试 | 🟡 | | TM-PHY-10 | TC-NET-1006 | P1 | 稳定性测试 | 🟡 | | TM-PHY-11 | TC-NET-1101 | P1 | 功能测试 | 🟡 | +| TM-PHY-15 | TC-NET-1501 | P0 | 状态回读/二段关闭 | ✅ | +| TM-PHY-15 | TC-NET-1502 | P0 | 复位状态机 | ✅ | +| TM-PHY-15 | TC-NET-1502b | P1 | LISTEN 态复位 | ✅ | +| TM-PHY-15 | TC-NET-1503 | P1 | 链路缓存/事件护栏 | ✅ | ✅ = 已实现并验证 | 🟡 = 已实现待验证 | 🟠 = 待实现 diff --git a/scripts/ram_baseline.tsv b/scripts/ram_baseline.tsv index cea76a8d..511cd685 100644 --- a/scripts/ram_baseline.tsv +++ b/scripts/ram_baseline.tsv @@ -1,2 +1,3 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi 2026-10-12 209492 18332 1104 154184 227824 21.7 97864 74.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;console_log.o=4120;ch395f.o=4106;default_task.o=2736;sys_log_mcu.o=1924 +2026-10-13 209780 18592 1112 154264 228372 21.8 97952 74.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;default_task.o=2736;sys_log_mcu.o=1924 diff --git a/test/board/ch395f_test.h b/test/board/ch395f_test.h index 8feac10a..e93cd37d 100644 --- a/test/board/ch395f_test.h +++ b/test/board/ch395f_test.h @@ -67,6 +67,7 @@ extern "C" { * E11 实证);勿与 Phase 6/9/10/13 混选(recv 互抢数据) */ /* ---- 链路时延基线(M1;分段/阈值见 docs/测试说明/链路延迟测试说明.md)---- */ //#define ENABLE_PHASE12_TESTS /* [硬] SPI 字节命令 / DMA 时延基线(L1/L2,无需 PC)—— M1 已完成,基线见说明文档 §6.1 */ +//#define ENABLE_PHASE15_TESTS /* [硬] N34 阶段 2 驱动原语:状态回读一致性/cmd_done/sock_reset 状态机跑通/链路缓存(无需 PC)——TC 详见 docs/测试说明/CH395F_Test_Guide.md 阶段 15;2026-10-13 台架 20/20 PASS */ /* ---- 链路时延(M2/M3;分段/阈值见 docs/测试说明/链路延迟测试说明.md)---- */ //#define ENABLE_PHASE13_TESTS /* [网] 芯片 + 队列时延(L3~L7/L10,需 PC peer)—— M2 已完成,基线见说明文档 §6.2 */ #define ENABLE_PHASE14_TESTS /* [网] 生产路径时延(L8~L11 + 模拟器 Δ)—— M3,需 peer --mode remo(:7003); @@ -75,7 +76,8 @@ extern "C" { #if defined(ENABLE_PHASE1_TESTS) || defined(ENABLE_PHASE2_TESTS) || \ defined(ENABLE_PHASE3_TESTS) || defined(ENABLE_PHASE4_TESTS) || \ - defined(ENABLE_PHASE5_TESTS) || defined(ENABLE_PHASE12_TESTS) + defined(ENABLE_PHASE5_TESTS) || defined(ENABLE_PHASE12_TESTS) || \ + defined(ENABLE_PHASE15_TESTS) #define ENABLE_HW_LAYER_TESTS 1 #endif diff --git a/test/board/ch395f_test_task.c b/test/board/ch395f_test_task.c index af8af638..514588e1 100644 --- a/test/board/ch395f_test_task.c +++ b/test/board/ch395f_test_task.c @@ -1653,6 +1653,125 @@ static void phase5_run(void) { TEST_REPORT("Phase 5"); } #endif + +/* =================================================================== + * Phase 15 — N34 阶段 2 驱动原语(无需 PC) + * + * 测试目的:验证 ch395f 新增原语(实施方案 docs/网络/N34-自愈审计/实施方案.md §4) + * 语义正确:类型化状态回读一致性 / cmd_done(E3 定案=状态回读)/ + * sock_reset 非阻塞状态机跑通(E4 定案序列)/ 链路态缓存 / 事件护栏。 + * 污染会话下的复位载荷完整性已由 E4 bench 60 轮实证(实验方案 E4), + * 本 Phase 只验"状态机跑通 + 判据语义",不依赖 PC。 + * 上下文:硬件层模式(testTask 独占 CH395F),用 Socket 7(产品监听 0~4 不涉)。 + * =================================================================== */ +#ifdef ENABLE_PHASE15_TESTS +static void phase15_run(void) { + const uint8_t sn = 7U; + ch395f_sock_state_t st; + uint32_t t0; + int done; + int rc; + + DBG_INFO("=== CH395F Phase 15 Tests (N34 phase-2 primitives) ==="); + + /* ---- TC-1501: 状态回读一致性 + cmd_done 语义(E2/E3 定案形态) ---- */ + ch395f_set_proto_type(sn, CH395F_PROTO_TYPE_TCP); + ch395f_set_sour_port(sn, 5107U); + (void)ch395f_open_socket(sn); + t0 = HAL_GetTick(); + done = 0; + while ((uint32_t)(HAL_GetTick() - t0) < 50U) { + if (ch395f_cmd_done(sn, CH395F_DONE_OPEN) == 1) { done = 1; break; } + HAL_Delay(1); + } + TEST_CHECK(done == 1, "TC1501 open done within 50ms"); + + (void)ch395f_tcp_listen(sn); + t0 = HAL_GetTick(); + done = 0; + while ((uint32_t)(HAL_GetTick() - t0) < 20U) { + if (ch395f_cmd_done(sn, CH395F_DONE_LISTEN) == 1) { done = 1; break; } + HAL_Delay(1); + } + /* E2 矩阵:LISTEN 可见延迟 100% <=10ms(24 轮无长尾) */ + TEST_CHECK(done == 1, "TC1501 listen done within 20ms (E2: 100 pct <=10ms)"); + TEST_CHECK(ch395f_sock_is_listening(sn) == 1, "TC1501 cached is_listening"); + st = ch395f_sock_state_cached(sn); + TEST_CHECK(st.open == CH395F_SOCKET_OPEN && st.tcp_state == CH395F_TCP_LISTEN, + "TC1501 cached state 05/01 (got %02X/%02X)", st.open, st.tcp_state); + + /* 二段关闭实证(2026-10-13 本 TC 二跑定案):LISTEN 态 Socket 的 + * CLOSE_SOCKET 返 SUCCESS 但只拆监听(TCP 01->00),byte0 恒 05; + * 再发一次 CLOSE 才落 00。生产 net 层靠 Trap 09 open 重试吸收此行为, + * sock_reset 状态机在 WAIT_CLOSE 阶段自行补发(见 ch395f.c)。 */ + uint8_t close_st = ch395f_close_socket(sn); + TEST_CHECK(close_st == CH395F_ERR_SUCCESS, + "TC1501 close cmd st=0x%02X (expect 00)", close_st); + st = ch395f_get_sock_state(sn); + TEST_CHECK(st.open == CH395F_SOCKET_OPEN && st.tcp_state == CH395F_TCP_CLOSED, + "TC1501 listen-close stage1: 05/00 listen torn down (got %02X/%02X)", + st.open, st.tcp_state); + (void)ch395f_close_socket(sn); + t0 = HAL_GetTick(); + done = -1; + while ((uint32_t)(HAL_GetTick() - t0) < 500U) { + if (ch395f_cmd_done(sn, CH395F_DONE_CLOSE) == 1) { done = (int)(HAL_GetTick() - t0); break; } + HAL_Delay(1); + } + TEST_CHECK(done >= 0, "TC1501 listen-close stage2: byte0=00 in %dms (win 500)", done); + st = ch395f_get_sock_state(sn); + TEST_CHECK(st.tcp_state == CH395F_SOCK_STATE_NONE, + "TC1501 tcp_state sentinel 0xFF when closed (got %02X)", st.tcp_state); + st = ch395f_get_sock_state(8U); + TEST_CHECK(st.open == CH395F_SOCKET_CLOSED && st.tcp_state == CH395F_SOCK_STATE_NONE, + "TC1501 sn=8 boundary -> CLOSED+sentinel"); + + /* ---- TC-1502: sock_reset 非阻塞状态机跑通(E4 定案序列) ---- */ + (void)ch395f_open_socket(sn); + rc = ch395f_sock_reset_begin(sn, CH395F_RESET_QUARANTINE); + TEST_CHECK(rc == 0, "TC1502 begin accepted (rc=%d)", rc); + TEST_CHECK(ch395f_sock_reset_begin(sn, CH395F_RESET_QUARANTINE) == -1, + "TC1502 busy reject on second begin"); + t0 = HAL_GetTick(); + rc = 1; + while (rc == 1 && (uint32_t)(HAL_GetTick() - t0) < 3000U) { + rc = ch395f_sock_reset_poll(sn); + HAL_Delay(1); + } + /* QUARANTINE 最短等待 400ms + 各阶段,3s 窗内必落定 */ + TEST_CHECK(rc == 0, "TC1502 reset done within 3000ms (rc=%d)", rc); + TEST_CHECK(ch395f_cmd_done(sn, CH395F_DONE_CLOSE) == 1, "TC1502 chip CLOSED after reset"); + TEST_CHECK(ch395f_sock_reset_poll(sn) == 0, "TC1502 poll idempotent after done"); + /* 复位后可重开(复位语义 = 回到可用态) */ + (void)ch395f_open_socket(sn); + TEST_CHECK(ch395f_cmd_done(sn, CH395F_DONE_OPEN) == 1, "TC1502 reopen after reset OK"); + (void)ch395f_close_socket(sn); + + /* TC-1502b: LISTEN 态 Socket 复位(走 WAIT_CLOSE 二段补发路径,GRACEFUL) */ + (void)ch395f_open_socket(sn); + (void)ch395f_tcp_listen(sn); + rc = ch395f_sock_reset_begin(sn, CH395F_RESET_GRACEFUL); + t0 = HAL_GetTick(); + rc = 1; + while (rc == 1 && (uint32_t)(HAL_GetTick() - t0) < 3000U) { + rc = ch395f_sock_reset_poll(sn); + HAL_Delay(1); + } + TEST_CHECK(rc == 0, "TC1502b reset on LISTEN socket done (rc=%d, two-stage absorbed)", rc); + TEST_CHECK(ch395f_cmd_done(sn, CH395F_DONE_CLOSE) == 1, "TC1502b chip CLOSED after listen-reset"); + + /* ---- TC-1503: 链路态缓存 + 事件护栏边界 ---- */ + uint16_t c0 = ch395f_link_change_count(); + ch395f_glob_event_feed(CH395F_GINT_STAT_PHY_CHANGE); /* 首次 feed 建档 */ + ch395f_glob_event_feed(CH395F_GINT_STAT_PHY_CHANGE); /* 链路稳定:不应虚增计数 */ + TEST_CHECK(ch395f_link_change_count() == c0, "TC1503 no spurious link change on stable link"); + TEST_CHECK(ch395f_link_up_cached() == 1U, "TC1503 link_up cached (bench link present)"); + TEST_CHECK(ch395f_sock_events(8U) == 0U, "TC1503 sock_events sn=8 boundary -> 0"); + (void)ch395f_sock_events(sn); /* 读即清护栏:合法调用不崩 */ + + TEST_REPORT("Phase 15"); +} +#endif /* * CH395F 测试套件入口(由 test_config.c 的 start_test_task 统一调度)。 * 此处只负责 CH395F 自身的硬件层 / NET 层逻辑,套件选择已上移到 test_config.c。 @@ -1688,6 +1807,10 @@ void ch395f_test_run(void) /* Phase 12(链路时延基线 L1/L2,M1):独立于 Phase 1~5 的 SPI 时延 * 采样,放硬件层阶段末尾执行(说明文档:链路延迟测试说明.md) */ net_latency_phase12_run(); +#endif +#ifdef ENABLE_PHASE15_TESTS + /* Phase 15(N34 阶段 2 驱动原语):独占态下直调驱动,放 HW 层末尾 */ + phase15_run(); #endif } #else From 83a70721bd0f5f875836d44a171f3c386355f5fa Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 13 Oct 2026 12:43:56 +0800 Subject: [PATCH 17/24] =?UTF-8?q?docs(N34):=20E2/E3/E4=20=E5=AE=9E?= =?UTF-8?q?=E9=AA=8C=E7=BB=93=E6=9E=9C=E4=B8=8E=E5=AE=9A=E6=A1=88=20+=20be?= =?UTF-8?q?nch=20=E5=AE=9E=E9=AA=8C=E5=8C=BA=E5=A4=8D=E6=B4=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit E2:状态回读 18.1us(N=1000x2 逐值一致);LISTEN 可见延迟 100% <=10ms 无长尾(24 轮)-> "每 poll 全量刷新"成立、reconciler 去抖 可减为 1。 E3:GET_CMD_STATUS 对 OPEN/LISTEN/CLOSE 无判别力(残留假阳性), 定案 cmd_done 用状态回读判据;SET 类命令态永不更新。 E4:排渣序列定案=读空 RX -> CLOSE -> 排空事件(quiescent) -> 轮询 byte0=00H;C 变体(不排渣)0/20 且级联自持,Trap 23 残渣 恰等于单 Socket RX 缓冲整容(rxdrain=2048,19/20)。 E2 补充发现(Phase 15 板测):LISTEN 态 CLOSE_SOCKET 二段关闭 (一次只拆监听 byte0 恒 05,再发一次才落 00)——Trap 09 open 重试环的芯片级原因。 bench:ch395f_reuse_bench.c 复活 N30_CHIP_BENCH 门控实验区 (旧 N-30 矩阵 s_bench_legacy=0 停泊),ch395f_reuse_host.py 增 --mode e4。注册(uvprojx/main.c)实验完已还原不入库。 --- docs/网络/N34-自愈审计/实验方案.md | 126 ++++++++ test/board/ch395f_reuse_bench.c | 467 ++++++++++++++++++++++++++++- test/host/ch395f_reuse_host.py | 70 ++++- 3 files changed, 653 insertions(+), 10 deletions(-) diff --git a/docs/网络/N34-自愈审计/实验方案.md b/docs/网络/N34-自愈审计/实验方案.md index 842bed18..8d76bdf4 100644 --- a/docs/网络/N34-自愈审计/实验方案.md +++ b/docs/网络/N34-自愈审计/实验方案.md @@ -562,3 +562,129 @@ PASV 重监听被垂死僵尸 accept 的竞态——且可由网卡抖动**确 **工具与留痕**:`test/host/pasv_probe.py`(新增);pktmon 抓包留存 `%TEMP%\n30_cap.etl`(3.9MB,含网卡抖动窗口,设备 IP 过滤)。 + +### E2 结果(2026-10-13 11:43–11:50,bench 实验固件 Code=213412,跑两遍,台架锁 N34) + +**固件**:复活 `test/board/ch395f_reuse_bench.c`(N30_CHIP_BENCH 门控,内核前裸机; +旧 N-30 定案矩阵由 `s_bench_legacy=0` 停泊保留);注册 uvprojx Test 组 + `Src/main.c` +USER CODE 2 区(注册不入库,实验完 `git restore`)。DWT CYCCNT @168MHz(1 cycle≈5.95ns)。 +两遍独立运行,本节数字两遍一致(第二遍为第一遍 C 变体级联污染后的干净重跑,E2/E3 复验)。 + +**单次回读成本**(`ch395f_get_socket_status`,N=1000×2 遍,两遍逐值相同): + +| 统计 | cycles | µs | +|---|---|---| +| min | 3048 | 18.1 | +| avg | 3057 | 18.1 | +| max | 3481 | 20.7 | + +**LISTEN 可见性矩阵**(裸 TCP_LISTEN 不轮询命令态,发命令后 +0/10/50/100ms 回读; +100ms 仍不可见则 10ms 节奏续查到 1000ms;12 轮×2 遍=24,轮间换端口): + +| 首次观察到 TCP=01H | 计数 | +|---|---| +| +0ms | 14 | +| +10ms | 10 | +| >10ms | **0** | + +**判据结论**: +1. 单次回读 18.1µs ≪ 100µs → **"每 poll 全量刷新"成立**(4 监听 × ~18µs ≈ 72µs ≪ + 1ms 轮询周期;`docs/网络/N34-自愈审计/实施方案.md` §4 第 1 项预估 ~15µs 量级坐实)。 +2. LISTEN 可见延迟有界:100% 在 +10ms 内可见、无长尾 → **reconciler 去抖可减为 1 次** + (决策点后 ≥10ms 单次回读即可裁定 LISTEN 形态;现"连续 2 次"去抖对该形态无必要—— + 瞬态窗口仅存在于 0~10ms 内,且第二次回读必在窗口外)。 + +**E2 补充发现(2026-10-13 12:23,Phase 15 板测 TC1501/1502b 实证):LISTEN 态 +CLOSE_SOCKET 是二段关闭**。CLOSE 命令对 LISTEN 态 Socket 返回 SUCCESS(0x00), +但只拆监听(TCP 01H→00H),**byte0 恒留 05H(Socket 仍 OPEN),2s 内不自落**; +需再发一次 CLOSE(此时 TCP 已 00H)byte0 才落 00H(二段间隔实测 0ms 即落)。 +生产 net 层早已在**无意中吸收**该语义:`net_close_locked` 等 CLOSED 的 500ms 窗 +超时后照常放行,随后重 listen 时 `net_listen_locked` 的 Trap 09 open 重试 +(3 次 × 500ms 沉降)第二次 open 前的 close 恰好充当二段——这就是 Trap 09 +"关闭后需延时才能重开"重试环存在的芯片级原因之一。阶段 2 `sock_reset_poll` +的 WAIT_CLOSE 阶段已内置二段补发(标记=drain 字段 0xF,只补发一次), +TC1502b 实证 LISTEN 态复位 GRACEFUL ~180ms 落定。 +**对阶段 3 的含义**:任何以 `cmd_done(DONE_CLOSE)` 为判据的等待,若目标 Socket +可能处于 LISTEN,必须容忍"一次 CLOSE 不落 00H"——要么走 sock_reset(已内置), +要么保留 Trap 09 式重试,不得把 DONE_CLOSE 当单次 CLOSE 的充分条件。 + +### E3 结果(同场同固件,5 命令 × 5 轮 × 2 遍 = 50 样本,两遍签名逐条一致) + +**方法**:发命令前对照读 GET_CMD_STATUS(记录残留假阳性);发命令后按手册 +`docs/芯片手册/CH395F.md` §9.2.1 节奏(≥2ms 首查)以 ~1ms 轮询至终态(2s 上限)。 +`pre`=对照读值,`first`=首个非 BUSY 终值,`busy`=是否观察到 BUSY→终态转变。 + +| 命令 | pre(对照读) | busy | first | done | 判别力判定 | +|---|---|---|---|---|---| +| OPEN_SOCKET | 0x00(r0) / **0x17**(r1-4) | 否(0/10) | 0x00 | 3ms | **部分**:状态确被 OPEN 更新(0x17→0x00),但 pre=SUCCESS 时完成与残留不可分 | +| TCP_LISTEN | 0x00(残留 SUCCESS) | 否(0/10) | 0x00 | 3ms | **不成立**:无转变,残留 SUCCESS 不可甄别 | +| CLOSE_SOCKET | 0x00(残留 SUCCESS) | 否(0/10) | 0x00 | 3ms | **不成立**:同上 | +| SET_SOUR_PORT | **0x17** | 否 | **0x17(不变)** | — | **不成立**:SET 类根本不更新命令状态 | +| SET_PROTO_TYPE | **0x17** | 否 | **0x17(不变)** | — | **不成立**:同上 | + +**三条实证**: +1. **残留假阳性坐实**:陈旧错误码 0x17(ERR_VAL,源自"对已关闭 Socket 再 close") + 跨后续 SET 命令(不更新状态)持续存活——复核 `docs/问题记录/CH395F_Trap_Records.md` + Trap 25 定案轮的"陈旧残留"形态(RESET_ALL 陈旧 BUSY 变体本矩阵未复现,同类 + 陈旧值持续存活已证)。 +2. **OPEN/LISTEN/CLOSE 均在 <2ms 完成**,BUSY 从未观察到——"BUSY→终态"转变对这三条 + 命令不可观测;驱动 `poll_cmd_status` 20ms 粒度对它们恒白等 ≥20ms(无害但无判别价值)。 +3. **SET 类无判别力**(命令状态完全不被更新)——Trap 记录结论复核成立。 + +**判据结论(定案 `docs/网络/N34-自愈审计/实施方案.md` §4 第 4 项)**:五条命令均不满足 +"发后 ≤N ms 变终态 **且** 无残留假阳性" → **`ch395f_cmd_done` 完成判据改用状态回读**: +open 完成=byte0 变 05H;listen 完成=TCP=01H;close 完成=byte0=00H +(回读成本 18.1µs 由 E2 实测背书;listen 可见延迟 ≤10ms 由 E2 矩阵背书,成本与时效均成立)。 +GET_CMD_STATUS 仅保留两用途:INIT_CH395 类慢命令(内核前/测试路径)与**命令级被拒** +检测(非 SUCCESS 非 BUSY 错误码仍会被芯片写入命令状态,B2 判据仍有效——被拒码是 +"新值覆盖残留"方向,不受残留歧义影响)。`poll_cmd_status` 保留给内核前/测试路径, +产品 socket 路径弃用。 + +### E4 结果(同场同固件 + 生产路径基线;3 变体 ×10 轮 ×2 遍 = 60 轮) + +**方法**:每轮先造污染会话(主机 `test/host/ch395f_reuse_host.py --mode e4`:连上 → +发 3000B → RST,Trap 23 中止形态),芯片侧按变体执行复位序列,**立即**原端口重开 ++ LISTEN,主机立即上传 4096B 图案载荷(图案种子随全局轮次变,上一轮残渣必失配)。 +PASS 口径 = 消费者在 DISCONNECT/TIM_OUT 前实际读到的字节数=4096 且图案逐字节匹配 +(= 生产 net 层释放语义;事后读空仅作取证,不计入 PASS)。 + +| 变体 | 第 1 遍 | 第 2 遍 | 观察 | +|---|---|---|---| +| A 读空 RX→CLOSE→排事件→等 byte0=00H | **10/10** | **10/10** | 19/20 轮 rxdrain=**2048**(残渣恰等于芯片单 socket RX 缓冲整容 = Trap 23 机制直接证据) | +| B CLEAR_RECV_BUF→CLOSE→排事件→等 CLOSED | **10/10** | **10/10** | 与 A 等价,无差异 | +| C 什么都不做→CLOSE→等 CLOSED | **0/10** | **0/10** | 首 C 轮(rg20)两遍同签名,随后**自持级联** | + +**C 变体失败签名(两遍逐字节复现)**:重开后主机载荷连接 SYN 被吞(主机侧 +connect 超时);bench 首次 SINT 读 = **0x04**(陈旧 RECV 位粘滞——Trap 14 +"粘滞到被读",close/open 不清 SINT);后续轮污染连接亦 SYN 被吞(主机 +`contam CONNECT timeout`、bench `no death event`)——每轮 close 又携带未排空数据 +→ **级联自持**,open+listen 命令照常返回 SUCCESS 也无法恢复,唯芯片硬件复位 +(E13 RSTI←NRST)清除。 + +**机制定案**:决定性一步 = **close 前 RX 缓冲必须为空**(读空与 CLEAR_RECV_BUF +等价——Trap 25"缓冲重分配无效"指的是 set_send/recv_buf 重分配,不是 CLEAR)。 +带非空 RX 关闭违反 `docs/芯片手册/CH395F.md` §5.52 bit3=1"读出残留数据后再关闭" +契约 → 芯片拆链停滞 → 下一会话 SYN 被吞/CONNECT 丢失,且自持。排空事件(A/B 有、 +C 无)清的是粘滞 SINT 位——对**事件驱动 accept 的正确性**必需(0x04 陈旧 RECV 会 +误导消费者),但不是 TCP 层吞 SYN 的根因。所有轮 closed_ms=0(close 命令落定后 +byte0 已是 00H)——"等 CLOSED"几乎零成本,但为契约必需步。 + +**生产路径基线**(烧 bench 固件前,Phase 1.5 产品固件 Code=209780): +`MDK-ARM/ftp_abort_probe.py --check-abort --rounds 5` → **5/5**(中止 STOR→450; +设备日志 `sock4 TIMEOUT` → quarantine → `UPLOAD '/ab000N.bin' FAILED` → +followup `UPLOAD '/ab_ok.bin' 4096 bytes OK`)——现实现(隔离期持续排空 RX + +close + 排空事件 + 等 CLOSED)行为基线全绿,与变体 A 序列一致。 + +**判据结论(定案 `docs/网络/N34-自愈审计/实施方案.md` §4 第 3 项)**: +(a) 与现实现等价成立(A 20/20 载荷完整);(b) B 与 A 等价(无差异记录); +C 不等价(0/20 + 级联)→ **`ch395f_sock_reset` 内部序列定案**: + +1. 读空 RX(GET_RECV_LEN→READ_RECV_BUF 直到 0;CLEAR_RECV_BUF 等价可用,**取读空** + ——保 §5.52 契约且排空字节数可观测可留痕); +2. CLOSE_SOCKET; +3. 排空本 socket 事件(drain-until-quiescent,E8 M5 口径:连续两轮无新位); +4. 轮询 byte0=00H(非阻塞状态机步进,约束不变)。 + +GRACEFUL 与 QUARANTINE **都必须排 RX**——E4 假设"差异仅在是否排 RX + 最短等待"中 +"是否排 RX"一半被否(带脏缓冲关闭在任何释放形态下都危险),两模式差异只剩 +最短等待(100ms/400ms,Trap 23 二阶段实测值)。 diff --git a/test/board/ch395f_reuse_bench.c b/test/board/ch395f_reuse_bench.c index 1aa9540c..830b1ac2 100644 --- a/test/board/ch395f_reuse_bench.c +++ b/test/board/ch395f_reuse_bench.c @@ -10,7 +10,16 @@ * 运行方法:在实验分支(如 n30-chip-bench)把本 .c 注册进 uvprojx Test 组、 * Src/main.c 的 USER CODE 2 区在 app_main_init() 之前调 ch395f_reuse_bench() * (内核前裸机循环,永不返回,断电退出),烧录后跑主机脚本对照芯片日志。 - * 当前矩阵为定案轮(对照 vs "丢 CONNECT 捕获自 accept"修复组),见 s_vars[]。 + * 旧 N-30 定案矩阵(对照 vs "丢 CONNECT 捕获自 accept"修复组)保留在 s_vars[]/ + * bench_round(),由 s_bench_legacy=0 停泊不运行。 + * + * 2026-10-13 N34 阶段 2 前置实验(task-9):同一固件合并跑 E2/E3/E4—— + * E2 = GET_SOCKET_STATUS 单次 SPI 成本(DWT 周期计数)+ TCP_LISTEN 可见延迟矩阵; + * E3 = GET_CMD_STATUS 判别力矩阵(OPEN/LISTEN/CLOSE/SET_SOUR_PORT/SET_PROTO_TYPE, + * 含"命令未发时"对照读,复核 Trap 记录的 SET 类无判别力 + 陈旧 BUSY 残留); + * E4 = sock_reset 内部顺序 A/B/C 变体 ×10 轮(轮间换端口)+ 立即复连载荷完整性自证 + * (配套 test/host/ch395f_reuse_host.py --mode e4)。 + * 判据与结果 → docs/网络/N34-自愈审计/实验方案.md E2/E3/E4 条目与实验记录节。 * * close_mode:0=主机先关(芯片等 DISCONNECT);1=芯片先关;2=无数据连上即关 * verify:0=无修复;1=listen 后确认 LISTEN 重试;2=缓冲重分配; @@ -23,6 +32,7 @@ #include "ch395f.h" #include "dbg_log.h" +#include "main.h" #include "stm32f4xx_hal.h" /* 变体表:{sock, close 后复开延迟 ms, TX 字节数, close_mode, verify_listen} */ @@ -89,9 +99,10 @@ static int bench_chip_init(void) { return -1; } ch395f_reset(); - /* 等 PHY link(Trap 09:link up 前 open 必失败) */ + /* 等 PHY link(Trap 09:link up 前 open 必失败;B9 修正:判据是 != DISCONN, + * 旧写法 !=0 对 01H=DISCONN 也成立) */ for (int i = 0; i < 100; i++) { - if (ch395f_get_phy_status() != 0U) { + if (ch395f_get_phy_status() != CH395F_PHY_DISCONN) { break; } HAL_Delay(100); @@ -354,20 +365,460 @@ static void bench_round(unsigned idx) { HAL_Delay(v->delay_ms); } +/* ===================================================================== + * N34 阶段 2 前置实验区(2026-10-13):E2 / E3 / E4 一次固件合并跑 + * ===================================================================== */ + +/* 旧 N-30 定案矩阵停泊开关:0 = 不运行(保留代码备查,勿删) */ +static const int s_bench_legacy = 0; + +/* --- DWT 周期计数器:168 MHz,1 cycle ~= 5.95 ns(不依赖 HAL 时基/中断) --- */ +static void bench_dwt_enable(void) { + CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; + DWT->CYCCNT = 0U; + DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; +} + +static uint32_t bench_cyc(void) { + return DWT->CYCCNT; +} + +/* cycles -> us(1 位小数,纯整数运算:MicroLIB 不支持 %f) */ +static void bench_log_cyc(const char *tag, uint32_t cyc) { + DBG_INFO("BENCH %s=%lu.%luus (%lucyc)", tag, + (unsigned long)(cyc / 168U), + (unsigned long)(((cyc % 168U) * 10U) / 168U), + (unsigned long)cyc); +} + +/* --- 裸命令下发:绕过驱动 poll_cmd_status(E2/E3 要观察命令自身状态轨迹)。 + * CS 包裹与驱动内部 ch395f_spi_begin/end 完全同构(该二函数为 static 未导出) --- */ +static void bench_spi_begin(void) { + HAL_GPIO_WritePin(CH395_SCS_GPIO_Port, CH395_SCS_Pin, GPIO_PIN_SET); + HAL_GPIO_WritePin(CH395_SCS_GPIO_Port, CH395_SCS_Pin, GPIO_PIN_RESET); +} + +static void bench_spi_end(void) { + HAL_GPIO_WritePin(CH395_SCS_GPIO_Port, CH395_SCS_Pin, GPIO_PIN_SET); +} + +static void bench_raw_cmd(uint8_t cmd, uint8_t sock, const uint8_t *args, uint8_t nargs) { + bench_spi_begin(); + (void)ch395f_write_cmd(cmd); + (void)ch395f_write_data(sock); + for (uint8_t i = 0U; i < nargs; i++) { + (void)ch395f_write_data(args[i]); + } + bench_spi_end(); +} + +/* 等 Socket byte0 回 00H(SOCKET_CLOSED);返回等待耗时 ms,-1=超时 */ +static int bench_wait_closed(uint8_t sock, uint32_t timeout_ms) { + uint32_t t0 = HAL_GetTick(); + uint8_t st[2] = { 0, 0 }; + for (;;) { + ch395f_get_socket_status(sock, st); + if (st[0] == CH395F_SOCKET_CLOSED) { + return (int)(HAL_GetTick() - t0); + } + if ((uint32_t)(HAL_GetTick() - t0) >= timeout_ms) { + return -1; + } + HAL_Delay(5); + } +} + +/* 排空本 Socket 事件:全局位图门控 + SINT 读(读即清),连续 2 轮(20ms 节奏)无新位即停; + * 返回排空期间见过的 SINT 位 OR(E8 M5 drain-until-quiescent 的 bench 版) */ +static uint8_t bench_drain_events(uint8_t sock, uint32_t max_ms) { + uint32_t t0 = HAL_GetTick(); + uint8_t seen = 0U; + int quiet = 0; + while ((uint32_t)(HAL_GetTick() - t0) < max_ms) { + uint16_t glob = ch395f_get_glob_int_status_all(); + uint8_t si = 0U; + if ((glob & (1U << (sock + 4U))) != 0U) { + si = ch395f_get_sock_int_status(sock); + } + if (si != 0U) { + seen |= si; + quiet = 0; + } else if (++quiet >= 2) { + break; + } + HAL_Delay(20); + } + return seen; +} + +/* 读空 RX(GET_RECV_LEN -> read_recv_buf 直到长度 0),返回总字节数 */ +static uint32_t bench_drain_rx(uint8_t sock) { + uint32_t total = 0U; + for (;;) { + uint16_t rl = ch395f_get_recv_len(sock); + if (rl == 0U) { + break; + } + uint16_t n = (rl > 2048U) ? 2048U : rl; + ch395f_read_recv_buf(sock, s_rx_buf, n); + total += n; + if (total > 65535U) { + break; + } + } + return total; +} + +/* 让命令落定:>=2ms 节奏轮询 GET_CMD_STATUS 直到非 BUSY;返回终值 */ +static uint8_t bench_cmd_settle(uint32_t max_ms) { + uint32_t t0 = HAL_GetTick(); + uint8_t s; + do { + HAL_Delay(2); + s = ch395f_get_cmd_status(); + } while (s == CH395F_ERR_BUSY && (uint32_t)(HAL_GetTick() - t0) < max_ms); + return s; +} + +/* ------------------------- E2:状态回读成本 + LISTEN 可见延迟 ------------------------- */ +static void bench_e2(void) { + static const uint16_t grid_ms[4] = { 0U, 10U, 50U, 100U }; + uint8_t st[2] = { 0, 0 }; + uint32_t min = 0xFFFFFFFFUL, max = 0U, sum = 0U; + + DBG_INFO("BENCH E2 ===== GET_SOCKET_STATUS cost + LISTEN visibility ====="); + /* 1) 单次回读成本 N=1000(socket 空闲态,与生产 net_poll 刷新同形) */ + for (uint32_t i = 0U; i < 1000U; i++) { + uint32_t c0 = bench_cyc(); + ch395f_get_socket_status(3U, st); + uint32_t d = bench_cyc() - c0; + if (d < min) { + min = d; + } + if (d > max) { + max = d; + } + sum += d; + } + bench_log_cyc("E2 gs_min", min); + bench_log_cyc("E2 gs_avg", sum / 1000U); + bench_log_cyc("E2 gs_max", max); + + /* 2) LISTEN 可见性矩阵:裸 TCP_LISTEN(不 poll 命令态)后 +0/10/50/100ms 回读; + * 100ms 仍不可见则 10ms 节奏续查到 1000ms,记录首次观察到 TCP=01H 的延迟 */ + for (unsigned r = 0U; r < 12U; r++) { + uint16_t port = (uint16_t)(5200U + r); + int grid[4] = { 0, 0, 0, 0 }; + int first_ms = -1; + (void)ch395f_close_socket(3U); + (void)bench_wait_closed(3U, 2000U); + ch395f_set_proto_type(3U, CH395F_PROTO_TYPE_TCP); + ch395f_set_sour_port(3U, port); + if (ch395f_open_socket(3U) != CH395F_ERR_SUCCESS) { + DBG_ERROR("BENCH E2 v2 r%u open fail", r); + continue; + } + uint32_t t0 = HAL_GetTick(); + bench_raw_cmd(CH395F_CMD_TCP_LISTEN_SN, 3U, 0, 0U); + for (int g = 0; g < 4; g++) { + while ((uint32_t)(HAL_GetTick() - t0) < grid_ms[g]) { } + ch395f_get_socket_status(3U, st); + if (st[1] == CH395F_TCP_LISTEN) { + grid[g] = 1; + if (first_ms < 0) { + first_ms = (int)(HAL_GetTick() - t0); + } + } + } + while (first_ms < 0 && (uint32_t)(HAL_GetTick() - t0) < 1000U) { + HAL_Delay(10); + ch395f_get_socket_status(3U, st); + if (st[1] == CH395F_TCP_LISTEN) { + first_ms = (int)(HAL_GetTick() - t0); + } + } + DBG_INFO("BENCH E2 listen r%u port%u grid=%d%d%d%d first=%dms st=0x%02X/0x%02X", + r, (unsigned)port, grid[0], grid[1], grid[2], grid[3], + first_ms, st[0], st[1]); + (void)bench_cmd_settle(2000U); + (void)ch395f_close_socket(3U); + (void)bench_wait_closed(3U, 2000U); + } +} + +/* ------------------------- E3:GET_CMD_STATUS 判别力矩阵 ------------------------- */ +static void bench_e3_one(const char *name, uint8_t cmd, uint8_t sock, + uint16_t port0, int pre_open, int pre_listen, + const uint8_t *args, uint8_t nargs) { + for (int r = 0; r < 5; r++) { + uint16_t port = (uint16_t)(port0 + (uint16_t)r); + uint8_t largs[2]; + const uint8_t *pa = args; + uint8_t n = nargs; + (void)ch395f_close_socket(sock); + (void)bench_wait_closed(sock, 2000U); + ch395f_set_proto_type(sock, CH395F_PROTO_TYPE_TCP); + ch395f_set_sour_port(sock, port); + if (pre_open) { + if (ch395f_open_socket(sock) != CH395F_ERR_SUCCESS) { + DBG_ERROR("BENCH E3 %s r%d pre-open fail", name, r); + continue; + } + if (pre_listen) { + if (ch395f_tcp_listen(sock) != CH395F_ERR_SUCCESS) { + DBG_ERROR("BENCH E3 %s r%d pre-listen fail", name, r); + continue; + } + } + } + /* 对照读:命令未发时的残留状态(Trap 记录:陈旧 BUSY 残留即在此现形) */ + uint8_t pre = ch395f_get_cmd_status(); + if (cmd == CH395F_CMD_SET_SOUR_PORT_SN) { + largs[0] = (uint8_t)(port & 0xFFU); + largs[1] = (uint8_t)(port >> 8); + pa = largs; + n = 2U; + } + uint32_t t0 = HAL_GetTick(); + bench_raw_cmd(cmd, sock, pa, n); + int busy_seen = 0, polls = 0, done_ms = -1; + uint8_t firstv = 0xFFU; + HAL_Delay(2); /* 手册 §9.2.1:发命令后 >=2ms 才发首个 GET_CMD_STATUS */ + while ((uint32_t)(HAL_GetTick() - t0) < 2000U) { + uint8_t s = ch395f_get_cmd_status(); + polls++; + if (s == CH395F_ERR_BUSY) { + busy_seen = 1; + HAL_Delay(1); + continue; + } + firstv = s; + done_ms = (int)(HAL_GetTick() - t0); + break; + } + DBG_INFO("BENCH E3 %s r%d pre=0x%02X busy=%d first=0x%02X done=%dms polls=%d", + name, r, pre, busy_seen, firstv, done_ms, polls); + (void)bench_cmd_settle(1000U); + (void)ch395f_close_socket(sock); + (void)bench_wait_closed(sock, 2000U); + } +} + +static void bench_e3(void) { + static const uint8_t proto_tcp = CH395F_PROTO_TYPE_TCP; + DBG_INFO("BENCH E3 ===== GET_CMD_STATUS discriminative matrix ====="); + bench_e3_one("OPEN", CH395F_CMD_OPEN_SOCKET_SN, 3U, 5300U, 0, 0, 0, 0U); + bench_e3_one("LISTEN", CH395F_CMD_TCP_LISTEN_SN, 3U, 5310U, 1, 0, 0, 0U); + bench_e3_one("CLOSE", CH395F_CMD_CLOSE_SOCKET_SN, 3U, 5320U, 1, 1, 0, 0U); + bench_e3_one("SET_SOUR_PORT", CH395F_CMD_SET_SOUR_PORT_SN, 3U, 5330U, 0, 0, 0, 0U); + bench_e3_one("SET_PROTO_TYPE", CH395F_CMD_SET_PROTO_TYPE_SN, 3U, 5340U, 0, 0, &proto_tcp, 1U); +} + +/* ------------------------- E4:sock_reset 内部顺序 A/B/C ------------------------- + * A:读空 RX -> CLOSE -> 排空本 socket 事件 -> 轮询等 byte0=00H + * B:CLEAR_RECV_BUF -> CLOSE -> 排空事件 -> 等 CLOSED + * C:什么都不做 -> CLOSE -> 等 CLOSED + * 每轮先"污染会话"(主机连上发 3000B 后 RST,Trap 23 形态),复位后立即 + * open+listen,主机立即上传 4096B 图案载荷;芯片侧按图案自证完整性 + * (PASS 口径 = 消费者在 DISCONNECT/TIM_OUT 前实际读到的字节数与图案)。 */ +#define E4_SOCK 3U +#define E4_BASE_PORT 5100U +#define E4_ROUNDS 10U +#define E4_PAYLOAD 4096U +static uint8_t s_e4_acc[8192]; + +static uint8_t e4_seed(unsigned rg) { + return (uint8_t)(rg * 97U + 13U); +} + +/* 污染阶段收尾:等芯片 DISCONNECT/TIM_OUT(RST 中止的死亡事件),1=见到 */ +static int bench_e4_wait_death(uint8_t sock, uint32_t timeout_ms, uint8_t *out_bits) { + uint32_t t0 = HAL_GetTick(); + while ((uint32_t)(HAL_GetTick() - t0) < timeout_ms) { + uint16_t glob = ch395f_get_glob_int_status_all(); + if ((glob & (1U << (sock + 4U))) != 0U) { + uint8_t si = ch395f_get_sock_int_status(sock); + if ((si & (CH395F_SINT_STAT_DISCONNECT | CH395F_SINT_STAT_SOCK_TIMEOUT)) != 0U) { + if (out_bits != 0) { + *out_bits = si; + } + return 1; + } + } + HAL_Delay(10); + } + return 0; +} + +static void bench_e4_round(unsigned rg, uint8_t variant) { + uint16_t port = (uint16_t)(E4_BASE_PORT + rg); + uint8_t si = 0U; + uint32_t rx_drained = 0U; + int closed_ms; + + DBG_INFO("BENCH E4 rg%u var=%c port%u t=%u", rg, (char)('A' + variant), + (unsigned)port, (unsigned)HAL_GetTick()); + /* 轮前统一清洁(所有变体同起点) */ + (void)ch395f_close_socket(E4_SOCK); + (void)bench_wait_closed(E4_SOCK, 2000U); + (void)bench_drain_events(E4_SOCK, 300U); + ch395f_set_proto_type(E4_SOCK, CH395F_PROTO_TYPE_TCP); + ch395f_set_sour_port(E4_SOCK, port); + if (ch395f_open_socket(E4_SOCK) != CH395F_ERR_SUCCESS) { + DBG_ERROR("BENCH E4 rg%u open fail", rg); + return; + } + if (ch395f_tcp_listen(E4_SOCK) != CH395F_ERR_SUCCESS) { + DBG_ERROR("BENCH E4 rg%u listen fail", rg); + (void)ch395f_close_socket(E4_SOCK); + return; + } + /* 污染会话:主机 connect -> 发 3000B -> RST;芯片侧只等死亡事件(不读 RX, + * 残渣留给变体处置——A 的排空 / B 的 CLEAR / C 的什么都不做 正是被测对象) */ + if (!bench_e4_wait_death(E4_SOCK, 15000U, &si)) { + DBG_ERROR("BENCH E4 rg%u contam: no death event (host not synced?)", rg); + (void)ch395f_close_socket(E4_SOCK); + (void)bench_wait_closed(E4_SOCK, 2000U); + HAL_Delay(500); + return; + } + /* 被测复位序列 */ + if (variant == 0U) { + rx_drained = bench_drain_rx(E4_SOCK); + } else if (variant == 1U) { + ch395f_clear_recv_buf(E4_SOCK); + } + (void)ch395f_close_socket(E4_SOCK); + if (variant != 2U) { + (void)bench_drain_events(E4_SOCK, 500U); + } + closed_ms = bench_wait_closed(E4_SOCK, 2000U); + DBG_INFO("BENCH E4 rg%u var=%c reset done rxdrain=%lu closed_ms=%d t=%u", + rg, (char)('A' + variant), (unsigned long)rx_drained, closed_ms, + (unsigned)HAL_GetTick()); + /* 立即重开 + LISTEN(主机立即上传全载荷) */ + ch395f_set_sour_port(E4_SOCK, port); + if (ch395f_open_socket(E4_SOCK) != CH395F_ERR_SUCCESS) { + DBG_ERROR("BENCH E4 rg%u reopen fail", rg); + return; + } + if (ch395f_tcp_listen(E4_SOCK) != CH395F_ERR_SUCCESS) { + DBG_ERROR("BENCH E4 rg%u relisten fail", rg); + (void)ch395f_close_socket(E4_SOCK); + return; + } + /* 等载荷连接 CONNECT(C 变体的陈旧 DISCONNECT 若在此同读出现 = 新连接被杀,如实计 FAIL) */ + if (!bench_wait_sock_int(E4_SOCK, 15000U, &si) || ((si & CH395F_SINT_STAT_CONNECT) == 0U)) { + DBG_ERROR("BENCH E4 rg%u var=%c VERIFY FAIL: no CONNECT (si=0x%02X)", rg, + (char)('A' + variant), si); + (void)ch395f_close_socket(E4_SOCK); + (void)bench_wait_closed(E4_SOCK, 2000U); + (void)bench_drain_events(E4_SOCK, 300U); + HAL_Delay(200); + return; + } + /* 消费循环:电平式 GET_RECV_LEN 收集(对丢 RECV 免疫),DISCONNECT/TIM_OUT 即止 + * (= 生产 net 层释放语义;closed-with-data 救回不在本判据内,事后读空仅作取证) */ + int death = ((si & (CH395F_SINT_STAT_DISCONNECT | CH395F_SINT_STAT_SOCK_TIMEOUT)) != 0U) ? 1 : 0; + uint32_t total = 0U; + uint32_t tr = HAL_GetTick(); + while (!death && (uint32_t)(HAL_GetTick() - tr) < 6000U) { + uint16_t rl = ch395f_get_recv_len(E4_SOCK); + while (rl > 0U && total < (uint32_t)sizeof(s_e4_acc)) { + uint16_t n = (rl > 2048U) ? 2048U : rl; + uint32_t room = (uint32_t)sizeof(s_e4_acc) - total; + if ((uint32_t)n > room) { + n = (uint16_t)room; + } + ch395f_read_recv_buf(E4_SOCK, &s_e4_acc[total], n); + total += (uint32_t)n; + rl = ch395f_get_recv_len(E4_SOCK); + } + uint16_t glob = ch395f_get_glob_int_status_all(); + if ((glob & (1U << (E4_SOCK + 4U))) != 0U) { + si = ch395f_get_sock_int_status(E4_SOCK); + if ((si & (CH395F_SINT_STAT_DISCONNECT | CH395F_SINT_STAT_SOCK_TIMEOUT)) != 0U) { + death = 1; + } + } + if (!death) { + HAL_Delay(5); + } + } + uint32_t loop_total = total; + /* 事后读空(取证:数据是否其实还在芯片缓冲里) */ + for (int k = 0; k < 20; k++) { + uint16_t rl = ch395f_get_recv_len(E4_SOCK); + if (rl == 0U) { + break; + } + uint32_t room = (uint32_t)sizeof(s_e4_acc) - total; + if (room == 0U) { + break; + } + uint16_t n = (rl > 2048U) ? 2048U : rl; + if ((uint32_t)n > room) { + n = (uint16_t)room; + } + ch395f_read_recv_buf(E4_SOCK, &s_e4_acc[total], n); + total += (uint32_t)n; + } + /* 载荷自证:图案种子随全局轮次变,上一轮残渣必失配 */ + uint8_t seed = e4_seed(rg); + int mm = -1; + for (uint32_t i = 0U; i < loop_total; i++) { + if (s_e4_acc[i] != (uint8_t)(seed + i)) { + mm = (int)i; + break; + } + } + int ok = (loop_total == E4_PAYLOAD) && (mm < 0); + DBG_INFO("BENCH E4 rg%u var=%c VERIFY loop=%lu extra=%lu mm=%d death=%d head=%02X %02X %02X %02X => %s", + rg, (char)('A' + variant), (unsigned long)loop_total, + (unsigned long)(total - loop_total), mm, death, + s_e4_acc[0], s_e4_acc[1], s_e4_acc[2], s_e4_acc[3], ok ? "OK" : "FAIL"); + /* 轮后清洁 */ + (void)ch395f_close_socket(E4_SOCK); + (void)bench_wait_closed(E4_SOCK, 2000U); + (void)bench_drain_events(E4_SOCK, 300U); + HAL_Delay(200); +} + +static void bench_e4(void) { + DBG_INFO("BENCH E4 ===== sock_reset A/B/C x%u (host: ch395f_reuse_host.py --mode e4) =====", + (unsigned)E4_ROUNDS); + unsigned rg = 0U; + for (uint8_t v = 0U; v < 3U; v++) { + for (unsigned r = 0U; r < E4_ROUNDS; r++, rg++) { + bench_e4_round(rg, v); + } + } +} + void ch395f_reuse_bench(void) { - DBG_INFO("BENCH ===== N30 chip-level reuse bench START ====="); + DBG_INFO("BENCH ===== N34 E2/E3/E4 chip bench START (legacy matrix parked) ====="); if (bench_chip_init() != 0) { DBG_ERROR("BENCH init failed, giving up (normal boot continues)"); return; } - DBG_INFO("BENCH chip ready, cycling %u variants forever", (unsigned)BENCH_N_VARS); for (unsigned i = 0; i < 2048U; i++) { s_tx_pat[i] = (uint8_t)(0xA5U + (i % 251U)); } - unsigned idx = 0; + bench_dwt_enable(); + if (s_bench_legacy != 0) { + DBG_INFO("BENCH legacy N-30 matrix cycling %u variants", (unsigned)BENCH_N_VARS); + unsigned idx = 0; + for (;;) { + bench_round(idx); + idx = (idx + 1U) % (unsigned)BENCH_N_VARS; + } + } + bench_e2(); + bench_e3(); + bench_e4(); + DBG_INFO("BENCH ===== E2/E3/E4 ALL DONE (reflash or power-cycle to exit) ====="); for (;;) { - bench_round(idx); - idx = (idx + 1U) % (unsigned)BENCH_N_VARS; + HAL_Delay(1000); } } diff --git a/test/host/ch395f_reuse_host.py b/test/host/ch395f_reuse_host.py index 5dae2f31..7c0c8e63 100644 --- a/test/host/ch395f_reuse_host.py +++ b/test/host/ch395f_reuse_host.py @@ -1,8 +1,13 @@ # N-30 芯片级复用时序台架:主机端(与 test/board/ch395f_reuse_bench.c 共用变体表) -# 用法: python ch395f_reuse_host.py [--host IP] [--cycles N] -# 纯 TCP,无 FTP。按 idx 轮转连接 5000+idx 端口,记录收发结果与时刻。 +# 用法: python ch395f_reuse_host.py [--host IP] [--cycles N] [--mode legacy|e4] +# legacy: 按 idx 轮转连接 5000+idx 端口,记录收发结果与时刻。 +# e4: N34 E4 sock_reset A/B/C 矩阵配套(2026-10-13):每轮两次连接—— +# ①污染:连上发 3000B 后 RST(SO_LINGER 0,Trap 23 中止形态); +# ②载荷:立即重连发 4096B 图案(pat[i]=(seed+i)&0xFF, seed=(rg*97+13)&0xFF)后 FIN。 +# 端口 5100+rg,rg=0..(3*rounds-1),变体顺序 A/B/C(与固件 bench 表一致)。 import argparse import socket +import struct import sys import time @@ -70,12 +75,73 @@ def run_round(host, idx, tx_len, close_mode): return ok +def connect_retry(host, port, budget_s): + """connect with retry until budget_s elapses; None on give-up""" + t0 = time.time() + while time.time() - t0 < budget_s: + try: + return socket.create_connection((host, port), timeout=5) + except OSError: + time.sleep(0.2) + return None + + +def e4_round(host, rg, rounds_per_variant, first_budget): + """one E4 round: contaminate (send 3000B then RST) + payload (4096B pattern then FIN)""" + port = 5100 + rg + seed = (rg * 97 + 13) & 0xFF + budget = first_budget if rg == 0 else 30 + s = connect_retry(host, port, budget) + if s is None: + print("%s E4 r%2d port%5d <<<< contam CONNECT timeout" % (ts(), rg, port)) + return False + try: + s.sendall(b"\x33" * 3000) + except OSError as e: + print("%s E4 r%2d contam send err %r" % (ts(), rg, e)) + try: + s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0)) + except OSError: + pass + s.close() # RST: abort mid-session like ftp_abort_probe (Trap 23 form) + time.sleep(0.05) + + s = connect_retry(host, port, 20) + if s is None: + print("%s E4 r%2d port%5d <<<< payload CONNECT timeout" % (ts(), rg, port)) + return False + payload = bytes(((seed + i) & 0xFF) for i in range(4096)) + try: + s.sendall(payload) + except OSError as e: + print("%s E4 r%2d payload send err %r" % (ts(), rg, e)) + s.close() + return False + s.close() # FIN: full payload delivered, chip drains + print("%s E4 r%2d var=%s port%5d contam+payload sent (seed=%02X)" + % (ts(), rg, "ABC"[rg // rounds_per_variant], port, seed)) + return True + + def main(): ap = argparse.ArgumentParser() ap.add_argument("--host", default="192.168.1.100") ap.add_argument("--cycles", type=int, default=3) + ap.add_argument("--mode", choices=["legacy", "e4"], default="legacy") + ap.add_argument("--rounds", type=int, default=10, help="e4: rounds per variant") + ap.add_argument("--first-budget", type=int, default=300, + help="e4: connect budget for round 0 (bench runs E2/E3 first)") args = ap.parse_args() + if args.mode == "e4": + fails = [] + total = 3 * args.rounds + for rg in range(total): + if not e4_round(args.host, rg, args.rounds, args.first_budget): + fails.append(rg) + print("E4 RESULT: %d/%d host-side ok, failures %s" % (total - len(fails), total, fails)) + return 1 if fails else 0 + fails = [] for c in range(args.cycles): for idx in sorted(VARS): From 6394853fef4c3eec13dfb828aa397d37e960c353 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 13 Oct 2026 15:09:58 +0800 Subject: [PATCH 18/24] =?UTF-8?q?feat(N34):=20=E9=98=B6=E6=AE=B53=20net?= =?UTF-8?q?=E5=B1=82=E6=94=B6=E6=95=9B=E2=80=94=E2=80=94=E5=8D=95=E4=B8=80?= =?UTF-8?q?reconciler+=E5=BF=99=E7=AD=89=E7=BB=9F=E4=B8=80+=E9=95=9C?= =?UTF-8?q?=E5=83=8F=E9=99=8D=E7=BA=A7+E-02=E6=94=B6=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实施方案 docs/网络/N34-自愈审计/实施方案.md §5 项1/2/3/5(项4 U15受E6暂缓): - 3a 合并reconciler:net_listener_pump删除,与net_reconcile_tick合并为 单一100ms tick;判据读驱动状态缓存(唯一刷新点=net_poll每轮全量刷新, E2背书18.1us);去抖分级②=1/③④⑤=2/①=4轮(M3让位窗400ms保留)+冷却1s; 链路态订阅留痕(F-DRV-13);探针去抖删除(F-MS-9) - 3b 忙等统一(P2b):listen/connect open重试与close排空循环改 net_sock_reset_sync(驱动sock_reset:读空RX→CLOSE→排空事件→等 byte0=00H,内置LISTEN态二段CLOSE补发);close不再读清全局中断寄存器 (F-NET-7);net_poll每轮步进活跃复位状态机(驱动契约,防搁浅) - 3c 镜像降级:net_sock_state_t声明为本层会话投影,芯片事实源=驱动缓存 - E-02收口:新API net_drop_conn(拆会话保监听,QUARANTINE语义,幂等; 模拟器双端实现;timeSync 30s预算耗尽接入;主机单测TC-TS-027d) 定案:CH395F close无RST,对内核存活不close的对端停FIN_WAIT_2, 监听恢复受限于对端close(Trap 07补充),兜底=E13 keepalive/TIMEOUT 台架全绿(Code=211348 RW=1104 ZI=154232):@test PASS;[STACK] net 1116/3840;Trap22 :7003 50/50+:21三档各50/50;Trap23 RST10/10+FIN5/5; Trap25 n30 11/11;E-02双PASS;iec104 18/18;对时跨会话连续; ftp_regression 11/11;integrity 15/15 SHA;ci_local L0+L0.5全过。 --- App/sync/time_sync.c | 6 + Drivers/BSP/NET/README.md | 14 +- Drivers/BSP/NET/net_listener.c | 147 +------ Drivers/BSP/NET/net_listener.h | 21 +- Drivers/BSP/NET/net_socket.c | 569 +++++++++++++++++-------- Drivers/BSP/NET/net_socket.h | 28 +- Drivers/BSP/NET/net_types.h | 7 +- docs/内存管理/RAM使用分析与优化方案.md | 8 +- docs/内存管理/RAM预算台账.md | 6 +- scripts/ram_baseline.tsv | 2 +- simulator/sim/net_socket_sim.c | 34 ++ test/host/e02_probe.py | 48 ++- test/host/time_sync/stub/net_socket.h | 1 + test/host/time_sync/time_sync_test.c | 15 + 14 files changed, 558 insertions(+), 348 deletions(-) diff --git a/App/sync/time_sync.c b/App/sync/time_sync.c index 2a043c3e..00351afd 100644 --- a/App/sync/time_sync.c +++ b/App/sync/time_sync.c @@ -179,6 +179,12 @@ static void time_sync_handle_conn(int conn_sock) { sys_clock_set_unix(epoch); DBG_INFO("time_sync: wall clock updated (epoch=%lu)", (unsigned long)epoch); } + } else { + /* N34 阶段 3(E-02 收口,实验方案 E13 §5):本层放弃会话 → 主动拆除。 + * 挂起但 TCP 仍活的客户端(PC 内核自动 ACK keepalive,芯片僵尸判不死) + * 会永久占住单连接监听;EOF 路径 drop 幂等返 0(net 层已 relisten)。 + * 模拟器多连接后端 conn≠ls,由 sim 实现按数据 fd 关闭。 */ + (void)net_drop_conn(conn_sock); } /* N34 阶段1-5:原 net_close(conn_sock) 已删——单连接模式 conn==监听 fd, diff --git a/Drivers/BSP/NET/README.md b/Drivers/BSP/NET/README.md index 90423db7..81692380 100644 --- a/Drivers/BSP/NET/README.md +++ b/Drivers/BSP/NET/README.md @@ -20,13 +20,17 @@ - 一期(RECV)已落地:`net_recv` 等支持带超时的阻塞(`pending` 挂起 + tick 唤醒) - 二期(ACCEPT + SEND)待做 → `docs/任务规划/登记簿.md` N-4 -### 周期对账自愈(Trap 22 修复) +### 周期对账自愈(Trap 22 修复;N34 阶段 3 合并 pump) -- `net_reconcile_tick()`(200ms)回读芯片状态修正三类失同步: +- `net_reconcile_tick()`(100ms,N34 阶段 3 起判据读**驱动状态缓存**——唯一 + 刷新点 `net_poll` 每轮全量刷新,E2 实测单次回读 18.1us)修正失同步: 本层 ESTABLISHED 而芯片已断 → 断开;本层 LISTENING 而芯片已握手 → 补 accept; - 本层 LISTENING 而芯片监听消失 → 重建 -- **去抖(连续 2 次)+ 冷却(1s)**——单次状态读的重建动作**必须去抖**, - 否则"刚 listen 未生效"瞬态会反复重建直至 `relisten give up`(三监听口全死) + 本层 LISTENING/CLOSED(监听型 fd)而芯片监听消失 → 重建(原 pump③④); + 本层 CLOSED 而芯片在听 → 只重同步(U11);芯片静默回收 → RESYNC(⑤) +- **分级去抖(②=1 轮 / ③④⑤=2 轮 / ①=4 轮)+ 冷却(1s)**——监听失位的重建 + 动作**必须去抖**,否则"刚 listen 未生效"瞬态会反复重建直至 + `relisten retry budget exhausted`(三监听口全死);①的 4 轮窗是 F-NET-17 M3 + 可疑电平的让位租约(勿改小) ### 异常复监听隔离(Trap 23 修复) diff --git a/Drivers/BSP/NET/net_listener.c b/Drivers/BSP/NET/net_listener.c index 0dbfd900..86925820 100644 --- a/Drivers/BSP/NET/net_listener.c +++ b/Drivers/BSP/NET/net_listener.c @@ -2,12 +2,14 @@ * 模块名称:Network Listener Lifecycle * 模块功能:监听器生命周期(真机专属)—— 维持 fd 处于 LISTEN 态的全部机制: * 重建(open+listen)、重建前的沉降/隔离(Trap 23)、排空 RX 残渣、 - * D10 非阻塞重试、周期性自愈 pump(唯一的周期性重建决策者)。 + * D10 非阻塞重试(周期性重建决策者=N34 阶段 3 合并后的 + * net_reconcile_tick,见 net_socket.c)。 * 适用平台:STM32F407ZGTx + CH395F 以太网芯片 * 作者:王建锋 * 创建日期:2026-10-12 * 修改记录: * 2026-10-12 王建锋 从 net_socket.c 整体抽出监听生命周期(N-32 §10 模块化) + * 2026-10-13 王建锋 N34 阶段 3:pump 并入 net_reconcile_tick(实施方案 §5 项 1) * * 职责与边界见 net_listener.h 头注释;设计依据见 * docs/子系统设计/网络Socket监听生命周期设计说明.md §10。 @@ -15,7 +17,7 @@ * 模块内分层: * ① 策略常量与私有状态(s_relisten_*) * ② 机制:net_drain_chip_rx(排空)/ net_relisten_locked(重建一次) - * ③ 对外接口:settle / try / clear / retry_tick / pump + * ③ 对外接口:settle / try / clear / retry_tick * * 依赖(全部为单向,无反向 include): * net_types.h(net_sock_t)/ net_socket.h(net_get_sock / NET_MAX_SOCKETS / @@ -285,7 +287,8 @@ static int net_relisten_locked(int sockfd) { * 出口参数:无 * 返回值:无 * 限定条件:netTask 上下文;由 handle_disconnect_event / handle_timeout_event 调用 - * 函数说明:1. 只置事实,不做动作 —— 重建由 net_listener_pump / net_listener_try 负责 + * 函数说明:1. 只置事实,不做动作 —— 重建由 net_reconcile_tick(形态③④)/ + * net_listener_try 负责 * 2. 越界 sockfd 静默忽略(与旧直接写 s_relisten_quarantine 行为一致) */ void net_listener_settle(int sockfd, net_listener_settle_t level) { @@ -293,7 +296,7 @@ void net_listener_settle(int sockfd, net_listener_settle_t level) { if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { return; } - /* 登记沉降/隔离等级 —— 只置事实,重建由 pump / try 负责 */ + /* 登记沉降/隔离等级 —— 只置事实,重建由 reconciler / try 负责 */ s_relisten_quarantine[sockfd] = (uint8_t)level; } @@ -399,7 +402,7 @@ int net_listener_try(int sockfd) { * 2. 本 fd 有在途挂起 recv 时跳过(net_pending_active,L3 保险: * 重开 Socket 会摧毁在途连接) * 3. 超过 NET_RELISTEN_DEADLINE_MS 停止 D10 快速重试并留痕 - * (监听不永久失效——pump 100ms 周期仍按"芯片不在听"认领重建) + * (监听不永久失效——reconciler 100ms 周期仍按"芯片不在听"认领重建) */ void net_listener_retry_tick(void) { /* 当前 tick */ @@ -414,7 +417,7 @@ void net_listener_retry_tick(void) { } /* 0-4(N34 阶段0)状态门:仅本层仍 LISTENING/CLOSED 才重试。若已转 * ESTABLISHED(新会话抢先到达),在途重试登记即陈旧——继续跑会 - * open→close 摧毁活连接(认领口径与 pump 一致,见 net_listener_pump) */ + * open→close 摧毁活连接(认领口径与 reconciler 形态③④ 一致) */ { /* 取控制块 */ const net_sock_t *p_sock = net_get_sock(i); @@ -438,9 +441,10 @@ void net_listener_retry_tick(void) { if ((int32_t)(now - s_relisten_dead[i]) >= 0) { /* 清重试状态 */ net_listener_clear(i); - /* D10 快速重试预算耗尽留痕:后续交 pump 周期接管(N-32 单一重建 - * 入口),监听不会永久失效——旧文案"give up"夸大了失效语义 */ - DBG_ERROR("sock%d relisten retry budget exhausted, pump takes over", i); + /* D10 快速重试预算耗尽留痕:后续交 reconciler 形态③④ 周期接管 + * (N-32 单一重建入口),监听不会永久失效——旧文案"give up" + * 夸大了失效语义 */ + DBG_ERROR("sock%d relisten retry budget exhausted, reconciler takes over", i); continue; } /* 重试成功 */ @@ -456,124 +460,7 @@ void net_listener_retry_tick(void) { } } -/* 监听器 pump 周期(对账 200ms 的 1/2:比它快、又不至于像 50ms 那样 - * 频繁 close/open 芯片 Socket 与消费者收发撞车——2026-10-12 台架实测 - * 50ms 会把 uiTask 最坏单轮从 172ms 推到 1034ms) */ -#define NET_LISTENER_PUMP_PERIOD_MS 100U -/* pump 去抖与冷却(沿用原对账③④ 的实测结论,取值不变): - * 2026-09-23 实证——无去抖时监听被反复重建直至放弃 */ -#define NET_LISTENER_STABLE_N 2U /* 连续失配次数(去抖,≈200ms) */ -#define NET_LISTENER_COOLDOWN_MS 1000U /* 同一 socket 两次动手的最小间隔(防抖成风暴) */ - -/* - * 函数功能:监听器 pump(N-32 P2「单一所有者」)—— 本层**唯一**的周期性 - * 重建监听决策者 - * 入口参数:无 - * 出口参数:无 - * 返回值:无 - * 限定条件:netTask 上下文(由 net_process_messages 每轮调用) - * 函数说明:1. 设计意图(设计文档 §4 不变量 II):其它所有位置只**置事实** - * (session_dead / 状态迁移 / 事件派发),不**做动作**——于是 - * "谁先看到 DISCONNECT 就谁重建"这类竞争在结构上消失 - * 2. 覆盖三种情形(前两种原属对账③④,第三种为 U11 缺口): - * ① 本层 LISTENING 而芯片监听已失位 → 重建(原对账③) - * ② 本层 CLOSED(会话已结束)而芯片不在 LISTEN → 重建(原对账④) - * ③ 本层 CLOSED 而芯片**正在** LISTEN → **只做状态重同步** - * (U11):芯片监听器是健康的,重下发 open+listen 会因 USE - * 错误走进关闭分支、把正听着的 Socket 关掉(U12 同源事故); - * 只把本层状态收回 LISTENING,**不发任何芯片命令** - * 3. 去抖/冷却沿用对账的实测结论(2026-09-23 实证:无去抖时监听被 - * 反复重建直至放弃) - * 4. session_dead **不在此清零**(与 net_relisten_locked 同一契约: - * 重建监听 ≠ 应用已确认会话结束;清零只发生在 handle_connect_event) - */ -void net_listener_pump(void) { - /* 上次轮次时刻 */ - static uint32_t s_last = 0U; - /* 连续失配计数(去抖) */ - static uint8_t s_bad[NET_MAX_SOCKETS]; - /* 冷却到期时刻(防抖成风暴) */ - static uint32_t s_cool[NET_MAX_SOCKETS]; - /* 当前 tick */ - uint32_t now = HAL_GetTick(); - /* Socket 循环索引 */ - int i; - - /* 未到 pump 周期:直接返回 */ - if ((uint32_t)(now - s_last) < NET_LISTENER_PUMP_PERIOD_MS) { - return; - } - s_last = now; - - for (i = 0; i < NET_MAX_SOCKETS; i++) { - /* 取控制块 */ - net_sock_t *p_sock = net_get_sock(i); - /* 芯片侧状态两字节 */ - uint8_t st[2] = {0, 0}; - /* 芯片当前是否在 LISTEN */ - int chip_listening; - - /* 控制块无效:跳过 */ - if (p_sock == NULL) { - s_bad[i] = 0U; - continue; - } - - /* 认领门槛:仅监听型 fd(在用 + 自动重监听 + TCP + 已绑端口)。 - * auto_relisten 是决定性门:少了它,"本层 CLOSED + 已绑端口"的出向 - * 连接(板级 Phase 7 TC701~703 用 net_connect 绑本地口)会被误判为 - * 监听器并被 relisten(同对账④的既定口径) */ - if ((p_sock->in_use == 0U) || - (p_sock->auto_relisten == 0U) || - (p_sock->type != NET_SOCK_STREAM) || - (p_sock->local_port == 0U)) { - s_bad[i] = 0U; - continue; - } - /* 只处理与监听生命周期相关的两个状态 */ - if ((p_sock->state != NET_SOCK_STATE_LISTENING) && - (p_sock->state != NET_SOCK_STATE_CLOSED)) { - s_bad[i] = 0U; - continue; - } - - /* 回读芯片状态(每监听型 fd 一条命令) */ - ch395f_get_socket_status((uint8_t)i, st); - chip_listening = ((st[0] != CH395F_SOCKET_CLOSED) && - (st[1] == CH395F_TCP_LISTEN)); - - /* 芯片在听 */ - if (chip_listening) { - s_bad[i] = 0U; - /* 本层 CLOSED 而芯片在听 = U11(本层状态滞后)→ 只重同步 */ - if (p_sock->state == NET_SOCK_STATE_CLOSED) { - DBG_INFO("sock%d pump: chip listening, resync CLOSED->LISTENING (U11)", i); - /* 只改本层状态,**不发任何芯片命令**(同 net_resync_listen 的 - * 既定纪律,理由见该函数注释)。session_dead 不清 */ - p_sock->state = NET_SOCK_STATE_LISTENING; - /* N-30(Trap 25):死连接对端端口转登幽灵甄别基准 + 去抖清零 - * —— 芯片监听器未被重开,芯片侧对端信息可能仍是上一条连接的, - * 必须转登为基准,否则对账② 的幽灵拦截会失效(同 net_resync_listen) */ - p_sock->last_peer_port = p_sock->remote_port; - p_sock->lost_conn_seen = 0U; - } - continue; - } - - /* 芯片不在听:情形①②同一处置——重建(本层唯一重建入口) */ - if (s_bad[i] < NET_LISTENER_STABLE_N) { - s_bad[i]++; - } - /* 去抖未满或处于冷却期:本轮不动手 */ - if ((s_bad[i] < NET_LISTENER_STABLE_N) || - ((int32_t)(now - s_cool[i]) < 0)) { - continue; - } - s_bad[i] = 0U; - s_cool[i] = now + NET_LISTENER_COOLDOWN_MS; - DBG_INFO("sock%d pump: chip not listening (0x%02X/0x%02X, was state=%d) -> relisten", - i, st[0], st[1], (int)p_sock->state); - /* 非阻塞:失败由 D10 重试轮询接管 */ - (void)net_listener_try(i); - } -} +/* N34 阶段 3(实施方案 §5 项 1):原"监听器 pump"(100ms 周期、去抖/冷却、 + * 情形①②③)已并入 net_socket.c 的 net_reconcile_tick 单一 100ms tick + * (判据读驱动状态缓存);本模块保留沉降/隔离、D10 重试、armed 查询与 + * net_listener_try(重建动作本身,由 reconciler 形态③④ 调用)。 */ diff --git a/Drivers/BSP/NET/net_listener.h b/Drivers/BSP/NET/net_listener.h index 597cd352..2929f2a1 100644 --- a/Drivers/BSP/NET/net_listener.h +++ b/Drivers/BSP/NET/net_listener.h @@ -2,7 +2,8 @@ * 模块名称:Network Listener Lifecycle (Header) * 模块功能:监听器生命周期模块的对外接口(**真机专属**)—— 维持 fd 处于 * LISTEN 态的全部机制:重建、重建前的沉降/隔离(Trap 23)、排空 RX - * 残渣、D10 非阻塞重试、周期性自愈 pump。 + * 残渣、D10 非阻塞重试(周期重建决策者=N34 阶段 3 合并后的 + * net_reconcile_tick,见 net_socket.c)。 * 适用平台:STM32F407ZGTx + CH395F 以太网芯片 * 作者:王建锋 * 创建日期:2026-10-12 @@ -46,7 +47,8 @@ typedef enum { * 出口参数:无 * 返回值:无 * 限定条件:netTask 上下文;由 handle_disconnect_event / handle_timeout_event 调用 - * 函数说明:1. 只置事实,不做动作 —— 重建由 net_listener_pump / net_listener_try 负责 + * 函数说明:1. 只置事实,不做动作 —— 重建由 net_reconcile_tick(形态③④)/ + * net_listener_try 负责 * 2. 越界 sockfd 静默忽略(不设错误码,与旧直接写数组行为一致) */ void net_listener_settle(int sockfd, net_listener_settle_t level); @@ -84,18 +86,9 @@ void net_listener_clear(int sockfd); */ void net_listener_retry_tick(void); -/* - * 函数功能:监听器 pump —— 本层**唯一**的周期性重建决策者 - * 入口参数:无 - * 出口参数:无 - * 返回值:无 - * 限定条件:netTask 上下文(由 net_process_messages 每轮调用) - * 函数说明:1. 覆盖三种情形:① LISTENING 而芯片失位 → 重建 - * ② CLOSED 而芯片不在 LISTEN → 重建 - * ③ CLOSED 而芯片**正在** LISTEN → 只做状态重同步(U11,不发芯片命令) - * 2. 100 ms 周期 + 去抖 + 冷却(详见实现处注释) - */ -void net_listener_pump(void); +/* N34 阶段 3:原 `net_listener_pump`(周期重建决策者)已并入 net_socket.c 的 + * net_reconcile_tick 单一 100ms tick(判据读驱动状态缓存,实施方案 §5 项 1)。 + * 本模块的 net_listener_try 由 reconciler 形态③④ 与 D10 重试轮询调用。 */ /* 注:本模块消费的宿主能力 `net_pending_active()` 声明在 net_socket.h * (它是 net_socket.c 的函数,须与配对头文件同处,见规范 S17 2.2)。 */ diff --git a/Drivers/BSP/NET/net_socket.c b/Drivers/BSP/NET/net_socket.c index 9581781b..78873b19 100644 --- a/Drivers/BSP/NET/net_socket.c +++ b/Drivers/BSP/NET/net_socket.c @@ -168,6 +168,7 @@ static int net_send_locked(net_sock_t *p_sock, const void *buf, int len); static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags, uint32_t timeout_ms, TaskHandle_t caller); static int net_close_locked(net_sock_t *p_sock); +static int net_drop_conn_locked(net_sock_t *p_sock); static int net_accept_locked(int sockfd, struct net_sockaddr *addr, int *addrlen); static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int addrlen); @@ -455,6 +456,10 @@ int net_poll(void) { /* 读取全局中断状态(2 字节):读操作本身会清除 CH395F 中断并使其拉高 INT#, * 若此后仍有新中断,CH395F 会再次拉低 INT#,由下一次 net_poll() 处理 */ int_status = ch395f_get_glob_int_status_all(); + /* N34 阶段 3(F-DRV-13 接线):把全局位喂驱动——bit2 PHY_CHANGE 时驱动 + * 重读 PHY 更新 link_up 缓存 + 变化计数,net 层在 reconciler 订阅留痕 + * (运行期拔网线不再静默;不自动重配,重配仍走 UI/配置引擎路径) */ + ch395f_glob_event_feed(int_status); #if defined(NET_BENCH_CMD_LOG) /* 仅状态跳变时记(电平型 RECV_OK 会每 poll 重复,全记会冲爆日志) */ static uint16_t s_bench_last_glob = 0xFFFF; @@ -533,36 +538,62 @@ int net_poll(void) { /* 因此每次 net_poll() 只处理“一批”,剩余数据在后续轮询中读取 */ } while (0); + /* N34 阶段 3(§5 项 1 基础):每轮全量刷新在用 Socket 的驱动状态缓存 + * (唯一刷新点;E2 实测单次回读 18.1us,在用 5~6 个 ≈100us ≪ 1ms 轮询 + * 周期)。reconciler / 探针 / 镜像降级决策点一律读缓存( + * ch395f_sock_state_cached),不再各自发状态 SPI。放在中断处理之后: + * 事件处理(handle_*_event)内部若再读状态,拿到的也是本轮最新值。 */ + for (int i = 0; i < NET_MAX_SOCKETS; i++) { + if (s_net_socks[i].in_use != 0U) { + (void)ch395f_get_sock_state((uint8_t)i); + /* N34 阶段 3(E-02 收口):驱动 sock_reset 状态机在 net_poll 步进 + * (驱动头注释契约"在 net_poll 或测试循环里周期调用")。IDLE 槽位 + * 纯 RAM 判返回 0 无 SPI;仅活跃复位机做短 SPI。作用:同步包装 + * (net_sock_reset_sync)600ms 放弃后复位机不搁浅,芯片条件满足 + * (对端回 FIN 等)时自动走完,槽位可复用(begin 不再恒返 -1)。 */ + (void)ch395f_sock_reset_poll((uint8_t)i); + } + } + /* 返回本次累计发生的事件数量(0 表示无事件) */ return event_count; } +/* 对账+pump 合并周期(N34 阶段 3:原 200ms 对账 / 100ms pump 合一) */ +#define NET_RECONCILE_PERIOD_MS 100U +/* 同一 socket 两次动手的最小间隔(防抖成风暴) */ +#define NET_RECONCILE_COOLDOWN_MS 1000U +/* 分级去抖门槛(单位=轮) */ +#define NET_RECON_STABLE_ACCEPT 1U /* 形态② 丢 CONNECT 补 accept(幽灵甄别兜底假阳性) */ +#define NET_RECON_STABLE_RELISTEN 2U /* 形态③④ 监听失位重建 / ⑤ 静默回收 RESYNC */ +#define NET_RECON_STABLE_DEATH 4U /* 形态① 连接已失(M3 让位窗 400ms,勿改小) */ + /* - * 函数功能:芯片/本层 socket 状态周期对账(Trap 22 丢中断自愈) + * 函数功能:芯片/本层 socket 状态周期对账(Trap 22 丢中断自愈)+ 监听器 pump + * (N-32 P2)—— N34 阶段 3 合并为单一 100ms tick(docs/网络/N34-自愈审计/实施方案.md §5 项 1) * 入口参数:无 * 出口参数:无 * 返回值:无 * 限定条件:netTask 上下文(由 net_process_messages 每轮调用) - * 函数说明:1. 芯片在快速复连场景会丢 CONNECT/DISCONNECT 中断;丢事件后本层无法靠事件自愈 - * 2. 典型:对端连接占着单连接监听的"后备"(芯片 TCP 仍 ESTABLISHED),后续 SYN 全被拒 - * 3. 应用因未收到 DISCONNECT 仍以为有客户端,永不调用 accept,事件驱动自愈无路可走 - * 4. 故周期回读芯片状态,修正**三类**失同步: - * 5. ① 本层 ESTABLISHED 而芯片已无连接 → 按断开处理(只置事实;重建交 pump) - * 6. ② 本层 LISTENING 而芯片已完成握手 → 补做 standalone accept(复用 handle_connect_event) - * 6b.⑤(N34 阶段1,F-NET-2)本层 ESTABLISHED 而芯片已静默回收回 LISTEN - * (0x05/0x01 无 DISCONNECT)→ RESYNC 只修本层,纪律同 pump③ U11 - * 7. ⚠ N-32 P2(2026-10-12):原③(本层 LISTENING 丢监听)与④(孤儿监听) - * 已**整体移交** `net_listener_pump` —— 本函数内不再有任何重建动作 - * (单一所有者);认领门槛相应收窄为 ESTABLISHED/LISTENING - * 8. 单次状态读可能落在"刚 open/listen 尚未生效"的瞬态 - * 9. 同一失配需连续 NET_RECONCILE_STABLE_N 次才动手(2026-09-23 实测:无去抖时监听被反复重建直至放弃) - * 10. 同一 socket 动手后冷却 NET_RECONCILE_COOLDOWN_MS,防去抖成风暴 - * 11. 周期 NET_RECONCILE_PERIOD_MS;每次每在用 socket 一条状态命令,开销可忽略 + * 函数说明:1. 芯片快速复连会丢 CONNECT/DISCONNECT 中断,本层无法靠事件自愈 + * 2. 判据读**驱动状态缓存**(唯一刷新点=net_poll 每轮全量刷新,E2 实测单次回读 18.1us) + * 3. 覆盖形态(原对账①②⑤ + 原 pump③④/U11): + * ① 本层 ESTABLISHED 而芯片已无连接 → 按断开处理(只置事实) + * ② 本层 LISTENING 而芯片已完成握手 → 补做 standalone accept(命中时 + * 才回读对端端口做幽灵甄别——本 tick 唯一按需 SPI) + * ⑤(F-NET-2)本层 ESTABLISHED 而芯片静默回收回 LISTEN(0x05/0x01 无 + * DISCONNECT)→ RESYNC 只修本层 + * ③ 本层 LISTENING(监听型 fd)而芯片监听失位 → 重建 + * ④ 本层 CLOSED(监听型 fd)而芯片不在 LISTEN → 重建 + * U11 本层 CLOSED(监听型 fd)而芯片在听 → 只重同步,**不发芯片命令** + * (重下发因 USE 错误关掉正听着的 Socket,U12 同源事故) + * 4. 去抖分级(100ms/轮;E2:LISTEN 可见 100% <=10ms 无长尾):②=1 轮 + * (幽灵甄别挡拆链窗假 ESTABLISHED);③④⑤=2 轮(2026-09-23 实证: + * 无去抖时监听被反复重建直至放弃);①=4 轮(**勿改小**:F-NET-17 M3 + * 可疑电平让位再入,400ms 让位窗=在途投递不可毁的最后租约,E9a 实证) + * 5. 动手后冷却 NET_RECONCILE_COOLDOWN_MS 防风暴(U11 被动重同步不占) + * 6. 链路态订阅(F-DRV-13):驱动 link 变化计数变化 → 留痕(不自动重配) */ -#define NET_RECONCILE_PERIOD_MS 200U /* 对账周期 */ -#define NET_RECONCILE_STABLE_N 2U /* 连续失配次数(去抖,≈400ms) */ -#define NET_RECONCILE_COOLDOWN_MS 1000U /* 同一 socket 两次动手的最小间隔(防抖成风暴) */ - static void net_reconcile_tick(void) { /* 上次对账轮的时刻(tick) */ static uint32_t s_last = 0U; @@ -575,76 +606,118 @@ static void net_reconcile_tick(void) { /* Socket 循环索引 */ int i; - /* 未到对账周期(200ms):直接返回,省 SPI 命令 */ + /* 未到对账周期(100ms):直接返回(缓存刷新在 net_poll,与本函数无关) */ if ((uint32_t)(now - s_last) < NET_RECONCILE_PERIOD_MS) { return; } /* 更新本轮起始时刻 */ s_last = now; + /* 链路态订阅(F-DRV-13):net_poll 已喂驱动,这里只看计数变化 */ + { + /* 链路变化计数订阅基线(0xFFFF=未建档) */ + static uint16_t s_link_last = 0xFFFFU; + uint16_t lc = ch395f_link_change_count(); + if (s_link_last != lc) { + if (s_link_last != 0xFFFFU) { + DBG_INFO("net: PHY link %s (changes=%u)", + ch395f_link_up_cached() ? "up" : "down", (unsigned)lc); + } + s_link_last = lc; + } + } + for (i = 0; i < NET_MAX_SOCKETS; i++) { /* 取控制块(直接数组下标,无需越界判断) */ net_sock_t *p_sock = &s_net_socks[i]; - /* 芯片侧状态两字节:[0]=socket 状态,[1]=TCP 状态 */ - uint8_t st[2] = {0, 0}; - /* 失同步类型:1=连接已失 2=握手已成丢中断 5=芯片静默回收回监听 - *(原 3/4=监听失位已移交 pump,编号不复用以避与历史口径混淆) */ + /* 驱动状态缓存(net_poll 每轮刷新,本 tick 读到的新鲜度 <=1ms) */ + ch395f_sock_state_t c = ch395f_sock_state_cached((uint8_t)i); + /* 芯片当前是否在 LISTEN(二段关闭中间态 05/00 不算在听) */ + int chip_listening = ((c.open != CH395F_SOCKET_CLOSED) && + (c.tcp_state == CH395F_TCP_LISTEN)); + /* 监听型 fd 门(原 pump 认领口径:auto_relisten 是决定性门——少了它, + * "本层 CLOSED + 已绑端口"的出向连接会被误判为监听器并被 relisten) */ + int listener_fd = ((p_sock->auto_relisten != 0U) && + (p_sock->type == NET_SOCK_STREAM) && + (p_sock->local_port != 0U)); + /* 失同步类型:1=连接已失 2=握手已成丢中断 3=监听失位 4=孤儿监听 + * 5=芯片静默回收回监听(编号沿用历史口径,U11 重同步不设编号——被动零动作) */ int bad = 0; + /* 本形态去抖门槛(默认按重建档) */ + uint8_t stable_need = NET_RECON_STABLE_RELISTEN; - /* 对账认领范围:**仅 ESTABLISHED / LISTENING**。 - * ⚠ 原先还认领"孤儿监听"(CLOSED + auto_relisten + 已绑端口)以驱动对账④; - * N-32 P2 把③④整体移交给 net_listener_pump(唯一重建入口),本函数只剩 - * ①②——两者都以 ESTABLISHED/LISTENING 为前提,故认领门槛相应收窄。 */ - if ((p_sock->in_use == 0U) || - ((p_sock->state != NET_SOCK_STATE_ESTABLISHED) && - (p_sock->state != NET_SOCK_STATE_LISTENING))) { + if (p_sock->in_use == 0U) { s_bad[i] = 0U; continue; } - /* 读芯片侧两字节状态(每 socket 一条 SPI 命令) */ - ch395f_get_socket_status((uint8_t)i, st); - /* ① 本层 ESTABLISHED 而芯片已无连接 → 按断开处理 */ - if ((p_sock->state == NET_SOCK_STATE_ESTABLISHED) && - ((st[0] == CH395F_SOCKET_CLOSED) || (st[1] == CH395F_TCP_CLOSED))) { - bad = 1; - } else if ((p_sock->state == NET_SOCK_STATE_LISTENING) && - (st[1] == CH395F_TCP_ESTABLISHED)) { - /* ② 本层 LISTENING 而芯片已完成握手(丢 CONNECT 中断)→ 补做 accept - * N-30(Trap 25)幽灵态甄别:拆链收尾窗口内芯片假报 ESTABLISHED, - * 回读对端端口 = 上一条连接端口 → 不补 accept(补了会把数据发给 - * 已死的旧连接 = 生产 LIST#4 数据被吞的直接成因),等拆净或新 SYN */ - uint8_t remote[6] = {0, 0, 0, 0, 0, 0}; - ch395f_get_remot_ipp((uint8_t)i, remote); - uint16_t peer = (uint16_t)(remote[4] | (remote[5] << 8)); - /* 仅"端口非零且 = 上一条连接端口"判幽灵拦截;peer==0 不拦截 - * (对端端口提交滞后时不得挡掉丢中断救援,见 net_lost_connect_probe 注释) */ - if (!((peer != 0U) && (peer == p_sock->last_peer_port))) { - bad = 2; + + if (p_sock->state == NET_SOCK_STATE_ESTABLISHED) { + /* ① 本层 ESTABLISHED 而芯片已无连接 → 按断开处理 */ + if ((c.open == CH395F_SOCKET_CLOSED) || (c.tcp_state == CH395F_TCP_CLOSED)) { + bad = 1; + stable_need = NET_RECON_STABLE_DEATH; + } else if ((c.open != CH395F_SOCKET_CLOSED) && + (c.tcp_state == CH395F_TCP_LISTEN)) { + /* ⑤(F-NET-2 缺口,N34 阶段1):芯片静默回收——本层 ESTABLISHED + * 而芯片已自行把 Socket 收回 LISTEN(0x05/0x01,**不发 DISCONNECT + * 中断**,run8 台架实证)→ RESYNC 语义 */ + bad = 5; } - } else if ((p_sock->state == NET_SOCK_STATE_ESTABLISHED) && - (st[0] != CH395F_SOCKET_CLOSED) && - (st[1] == CH395F_TCP_LISTEN)) { - /* ⑤(F-NET-2 缺口,N34 阶段1):芯片静默回收——本层 ESTABLISHED - * 而芯片已自行把 Socket 收回 LISTEN(0x05/0x01,**不发 DISCONNECT - * 中断**,run8 台架实证)。判据①要求 TCP==CLOSED 不命中此形态、 - * 判据②要求本层 LISTENING 也不命中——此前生产无人认领(原仅手动 - * API net_resync_listen 能修且生产零调用)。→ bad=5 RESYNC 语义 */ - bad = 5; + } else if (p_sock->state == NET_SOCK_STATE_LISTENING) { + if (c.tcp_state == CH395F_TCP_ESTABLISHED) { + /* ② 本层 LISTENING 而芯片已完成握手(丢 CONNECT 中断)→ 补做 accept + * N-30(Trap 25)幽灵态甄别:拆链收尾窗口内芯片假报 ESTABLISHED, + * 回读对端端口 = 上一条连接端口 → 不补 accept(补了会把数据发给 + * 已死的旧连接 = 生产 LIST#4 数据被吞的直接成因),等拆净或新 SYN。 + * 这是本 tick 唯一的按需 SPI(仅形态②命中时发) */ + uint8_t remote[6] = {0, 0, 0, 0, 0, 0}; + ch395f_get_remot_ipp((uint8_t)i, remote); + uint16_t peer = (uint16_t)(remote[4] | (remote[5] << 8)); + /* 仅"端口非零且 = 上一条连接端口"判幽灵拦截;peer==0 不拦截 + * (对端端口提交滞后时不得挡掉丢中断救援,见 net_lost_connect_probe 注释) */ + if (!((peer != 0U) && (peer == p_sock->last_peer_port))) { + bad = 2; + stable_need = NET_RECON_STABLE_ACCEPT; + } + } else if (!chip_listening && listener_fd) { + /* ③ 本层 LISTENING 而芯片监听失位 → 重建(原 pump①) */ + bad = 3; + } + } else if ((p_sock->state == NET_SOCK_STATE_CLOSED) && listener_fd) { + if (chip_listening) { + /* U11:本层 CLOSED 而芯片在听 = 本层状态滞后 → 只重同步。 + * **不发任何芯片命令**(重下发 open+listen 会因 USE 错误走进 + * 关闭分支、把正听着的 Socket 关掉,U12 同源事故)。 + * 被动修正零风险:不去抖、不占冷却(原 pump 同口径) */ + DBG_INFO("sock%d reconcile: chip listening, resync CLOSED->LISTENING (U11)", i); + p_sock->state = NET_SOCK_STATE_LISTENING; + /* N-30(Trap 25):死连接对端端口转登幽灵甄别基准 + 探针计数清零 + * —— 芯片监听器未被重开,芯片侧对端信息可能仍是上一条连接的, + * 必须转登为基准,否则形态② 的幽灵拦截会失效 */ + p_sock->last_peer_port = p_sock->remote_port; + p_sock->lost_conn_seen = 0U; + s_bad[i] = 0U; + continue; + } + /* ④ 本层 CLOSED(会话已结束)而芯片不在 LISTEN → 重建(原 pump②) */ + bad = 4; + } else { + /* 其余状态(CREATED/BOUND/UDP_OPEN 等)不归本 tick 认领 */ + s_bad[i] = 0U; + continue; } - /* ③④(本层 LISTENING 丢监听 / 本层 CLOSED 孤儿监听)已移交 - * net_listener_pump —— N-32 P2 单一所有者,本函数不再做重建动作 */ /* 无失同步:清零计数,转下一 socket */ if (bad == 0) { s_bad[i] = 0U; continue; } - /* 累加连续失配计数(封顶至去抖门槛) */ - if (s_bad[i] < NET_RECONCILE_STABLE_N) { + /* 累加连续失配计数(封顶至本形态门槛) */ + if (s_bad[i] < stable_need) { s_bad[i]++; } /* 去抖未满或处于冷却期:本轮不动手 */ - if ((s_bad[i] < NET_RECONCILE_STABLE_N) || + if ((s_bad[i] < stable_need) || ((int32_t)(now - s_cool[i]) < 0)) { continue; } @@ -653,26 +726,32 @@ static void net_reconcile_tick(void) { s_cool[i] = now + NET_RECONCILE_COOLDOWN_MS; if (bad == 1) { - /* 类型①:按断开处理(应用读到 EOF 后由 pump 重建监听) */ - DBG_INFO("sock%d reconcile: chip closed (sock=0x%02X tcp=0x%02X) -> disconnect", - i, st[0], st[1]); + /* 类型①:按断开处理(应用读到 EOF 后由形态③④重建监听) */ + DBG_INFO("sock%d reconcile: chip closed (0x%02X/0x%02X) -> disconnect", + i, c.open, c.tcp_state); /* 复用断开状态机(含陈旧事件甄别 + 状态迁移 + 事件派发); - * 只置事实,重建由 net_listener_pump 统一做(N-32 P2) */ + * 只置事实,重建由本 tick 形态③④统一做(N-32 P2 单一所有者) */ handle_disconnect_event(p_sock); } else if (bad == 5) { /* 类型⑤:静默回收 → RESYNC 语义(原 NET_MSG_RESYNC_LISTEN 分派体 - * 原样并入,N34 阶段1;纪律同 pump③ U11:**不发任何芯片命令**—— + * 原样并入,N34 阶段1;纪律同 U11:**不发任何芯片命令**—— * 重下发 close/重监听会摧毁芯片侧健康监听器 → 端口永拒, * run8 实证 170s 拒绝) */ DBG_INFO("sock%d reconcile: chip re-listened w/o DISCONNECT (0x%02X/0x%02X) -> resync", - i, st[0], st[1]); + i, c.open, c.tcp_state); /* 仅修 MCU 侧;新连接的 CONNECT 中断会把两侧重新同步 */ p_sock->state = NET_SOCK_STATE_LISTENING; - /* N-30(Trap 25):死连接对端端口转登幽灵甄别基准 + 去抖清零 */ + /* N-30(Trap 25):死连接对端端口转登幽灵甄别基准 + 探针计数清零 */ p_sock->last_peer_port = p_sock->remote_port; p_sock->lost_conn_seen = 0U; /* 取消本 fd 在死连接上的挂起 recv(必通知,防调用方挂死) */ net_pending_cancel(i, NET_ERR_CONNRESET); + } else if ((bad == 3) || (bad == 4)) { + /* 类型③④:监听失位/孤儿监听 → 重建(本层唯一重建入口; + * 非阻塞:失败由 D10 重试轮询接管) */ + DBG_INFO("sock%d reconcile: chip not listening (0x%02X/0x%02X, was state=%d) -> relisten", + i, c.open, c.tcp_state, (int)p_sock->state); + (void)net_listener_try(i); } else { /* 类型②:补做 standalone accept */ DBG_INFO("sock%d reconcile: connection completed w/o CONNECT irq -> accept", i); @@ -694,20 +773,18 @@ static void net_reconcile_tick(void) { * 限定条件:必须在 netTask 上下文中调用(独占 CH395F 与 s_net_socks 访问权); * 由 net_task.c 在 net_stack_bring_up 之后每次 1ms 循环调用。 * 函数说明:1. 四个自愈 tick 均非阻塞(绝不 osDelay 忙等,netTask 永不冻结),先于消息分派执行 - * 2. net_listener_retry_tick(D10):断连后按 30ms - * (NET_RELISTEN_RETRY_MS)节拍重试 open+listen;2s(NET_RELISTEN_DEADLINE_MS) - * 总时限到期放弃留痕 - * 3. D10:本 fd 有在途挂起 recv 时跳过(L3 保险) - * 4. net_pending_tick(L3):按节拍了结挂起 recv(数据/到点[仅显式预算]/断连) - * 5. L3 tripwire(N34 阶段 1.5):挂起超 60s 只 DBG_ERROR 留痕一次,不再强制了结/伪造返回值 - * 6. net_reconcile_tick(Trap 22):每 200ms(NET_RECONCILE_PERIOD_MS)对账①② - * 7. 对账以连续 2 次失配去抖 + 1s 冷却,自愈两类失同步 - * 8. net_listener_pump(N-32 P2):每 100ms 监听器 pump,本层**唯一**重建入口 - * (原对账③④;本层 CLOSED 而芯片在听时只重同步)——置于对账之后 - * 9. net_msg_t 按值入队,netTask 侧副本调用方不可见 → 结果只能搭任务通知值回传 - * 10. 32 位整体即结果,不做位域压缩 - * 11. RECV 特例:无数据+阻塞 → 登记 pending 哨兵 NET_PENDING_REGISTERED(0x70000000) - * 12. RECV 特例 do_notify=0,回执推迟到 net_pending_tick 完成时 + * 2. net_listener_retry_tick(D10):断连后按 30ms(NET_RELISTEN_RETRY_MS) + * 节拍重试 open+listen,2s 总时限到期放弃留痕;本 fd 有在途挂起 recv 时跳过(L3 保险) + * 3. net_pending_tick(L3):按节拍了结挂起 recv(数据/到点[仅显式预算]/断连); + * L3 tripwire(N34 阶段 1.5):挂起超 60s 只 DBG_ERROR 留痕一次,不再强制了结/伪造返回值 + * 4. net_reconcile_tick(Trap 22 + pump 合并,N34 阶段 3):每 100ms 对账 + * ①②⑤ + ③④ 重建 + U11 重同步,判据读驱动状态缓存(net_poll 每轮 + * 全量刷新,E2 背书);去抖分级(②=1 轮/③④⑤=2 轮/①=4 轮,M3 让位窗) + * + 1s 冷却;重建失败进 D10 重试队列,reconciler 不重复催 + * 5. net_msg_t 按值入队,netTask 侧副本调用方不可见 → 结果只能搭任务通知值回传 + * 6. 32 位整体即结果,不做位域压缩 + * 7. RECV 特例:无数据+阻塞 → 登记 pending 哨兵 NET_PENDING_REGISTERED(0x70000000), + * do_notify=0,回执推迟到 net_pending_tick 完成时 */ void net_process_messages(void) { net_msg_t msg; @@ -722,11 +799,10 @@ void net_process_messages(void) { net_listener_retry_tick(); /* L3 挂起 recv 完成检查:数据到达 / 到点 / 断连时通知调用方 */ net_pending_tick(); - /* Trap 22 状态对账:周期回读芯片状态,自愈丢失的中断(①②;③④ 已归 pump) */ + /* Trap 22 状态对账 + 监听器 pump(N34 阶段 3 合并单一 100ms tick): + * 判据读驱动缓存(net_poll 每轮全量刷新),①②⑤ 失同步 + ③④ 重建 + + * U11 重同步全部在此(本层唯一周期重建入口,N-32 P2 单一所有者) */ net_reconcile_tick(); - /* N-32 P2:监听器 pump —— 本层唯一重建入口(置于对账之后:对账①② - * 可能刚把会话置 CLOSED,pump 紧接着就能看到并重建) */ - net_listener_pump(); /* 队列排空:超时 0 非阻塞取,每条处理一轮,直到本轮队列为空 */ while (osMessageQueueGet(netMsgQueueHandle, &msg, NULL, 0) == osOK) { @@ -813,9 +889,29 @@ void net_process_messages(void) { msg.result = net_accept_locked(msg.sockfd, p_addr, p_addrlen); break; } + case NET_MSG_DROP_CONN: { + /* N34 阶段 3(E-02 收口):应用放弃会话的主动拆除(拆连接、保监听) */ + net_sock_t *p_sock = net_get_sock(msg.sockfd); + if (p_sock == NULL) { + /* 描述符无效 → BADF */ + net_set_errno(NET_ERR_BADF); + msg.result = -1; + } else if (!p_sock->in_use || p_sock->type != NET_SOCK_STREAM) { + /* 不在用 / 非 TCP 流 → INVAL */ + net_set_errno(NET_ERR_INVAL); + msg.result = -1; + } else if (p_sock->state != NET_SOCK_STATE_ESTABLISHED && + p_sock->state != NET_SOCK_STATE_CONNECTING) { + /* 无活动会话:幂等成功(监听原样,不动芯片) */ + msg.result = 0; + } else { + msg.result = net_drop_conn_locked(p_sock); + } + break; + } /* N34 阶段1(F-NET-3):NET_MSG_RESYNC_LISTEN / NET_MSG_RELISTEN 两个 * 手动自愈消息已删——RESYNC 语义并入对账⑤(net_reconcile_tick), - * RELISTEN 与 pump/D10 完全重叠且对健康监听器是 footgun; + * RELISTEN 与 reconciler/D10 完全重叠且对健康监听器是 footgun; * 生产调用者本就为零(仅 test/board Phase 13,已改由自动机制接管) */ default: /* 未知消息类型(枚举闭合,正常不应触发):置通用错误码与结果 -1, @@ -1063,6 +1159,47 @@ int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen) { return 0; } +/* + * 函数功能:驱动 sock_reset 原语的有界确定性排渣等待(P2b 收口,N34 阶段 3; + * 替代原"close + 盲等 500ms"式忙等) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * mode - CH395F_RESET_GRACEFUL(等 100ms)/ QUARANTINE(等 400ms) + * 出口参数:无 + * 返回值:0 - 复位完成(芯片 byte0=00H、事件已排空);-1 - 启动失败或 600ms 超时 int + * 限定条件:仅 netTask 上下文(SPI 串行契约);调用前本层已放弃该会话 + * 函数说明:1. 原语本身非阻塞(begin/poll 分步),本包装是**同步 API 路径** + * (listen open 重试 / close)用的有界等待:2ms 节奏 poll,600ms 封顶 + * 2. 内部序列=E4 定案:读空 RX -> CLOSE -> 排空事件(quiescent) -> + * 轮询 byte0=00H;WAIT_CLOSE 内置 LISTEN 态二段 CLOSE 补发 + * (E2 补充发现:一次 CLOSE 只拆监听 byte0 恒 05) + * 3. 超时留痕返 -1,调用方按原语义兜底(Trap 09 重试环 / close 放行) + */ +static int net_sock_reset_sync(int sockfd, ch395f_reset_mode_t mode) { + /* 起始时刻(600ms 上限基准) */ + uint32_t t0 = HAL_GetTick(); + /* 状态机步进返回值 */ + int rc; + + /* 启动状态机(本 fd 已在运行返 -1:netTask 串行上下文不应发生) */ + if (ch395f_sock_reset_begin((uint8_t)sockfd, mode) != 0) { + return -1; + } + /* 有界等待:2ms 节奏步进(每步仅短 SPI 事务,netTask 无长忙等) */ + for (;;) { + rc = ch395f_sock_reset_poll((uint8_t)sockfd); + /* 0=完成 / -1=失败(槽位已复位可重开):都退出等待 */ + if (rc != 1) { + break; + } + if ((uint32_t)(HAL_GetTick() - t0) > 600U) { + DBG_ERROR("sock%d sock_reset_sync timeout (600ms)", sockfd); + return -1; + } + osDelay(2); + } + return (rc == 0) ? 0 : -1; +} + /* * 函数功能:启动 TCP Server 监听(内部版本,必须在 netTask 上下文调用) * 入口参数:sockfd - Socket 描述符 (0~7) int @@ -1098,7 +1235,11 @@ int net_listen_locked(int sockfd) { /* 本地端口 = 控制块 local_port */ ch395f_set_sour_port(sockfd, p_sock->local_port); - /* CH395F 关闭 Socket 后需延时才能重新打开(Trap 09),故失败重试最多 3 次 */ + /* CH395F 关闭 Socket 后需延时才能重新打开(Trap 09),故失败重试最多 3 次。 + * P2b 收口(N34 阶段 3):原"close + 盲等 500ms"改**确定性排渣**—— + * sock_reset(读空 RX -> CLOSE -> 排空事件 -> 等 byte0=00H,内置 LISTEN + * 态二段 CLOSE 补发;E4 定案 60 轮实证复位后 open 必成)。GRACEFUL + * 最短等 100ms ≪ 原 500ms 盲等;复位失败/超时仍走下一轮重试,Trap 09 语义保留 */ for (int open_try = 0; open_try < 3; open_try++) { /* 尝试打开芯片侧 Socket */ status = ch395f_open_socket(sockfd); @@ -1106,10 +1247,8 @@ int net_listen_locked(int sockfd) { /* 打开成功,退出重试 */ break; } - /* 打开失败先关闭,再延时后重试 */ - ch395f_close_socket(sockfd); - /* 等 500ms 芯片沉降后再试(Trap 09) */ - osDelay(500); + /* 打开失败:确定性复位本 Socket(排渣/二段关闭/等 CLOSED 一体) */ + (void)net_sock_reset_sync(sockfd, CH395F_RESET_GRACEFUL); } /* 3 次仍失败,返回错误 */ if (status != CH395F_ERR_SUCCESS) { @@ -1224,34 +1363,34 @@ int net_listen(int sockfd) { * 幽灵态甄别:拆链窗口内芯片也会直接假报 ESTABLISHED(旧连接未拆净),此时回读 * 对端端口 = 上一条连接端口;新连接临时端口几乎必然不同 → 端口相同不 accept, * 等旧连接拆净 / 新 SYN 到达(新连接若也丢中断,后续轮询以新端口命中)。 */ -#define NET_LOST_CONN_STABLE_N 2U /* 连续确认轮次(accept 轮询 100ms/轮,≈200ms 去抖) */ /* * 函数功能:监听等待期的芯片状态轮询探针——芯片已 ESTABLISHED 而 CONNECT 中断 - * 未到(丢中断)时,去抖满后补做 accept(复用 CONNECT 事件簿记路径)。 + * 未到(丢中断)时补做 accept(复用 CONNECT 事件簿记路径)。 * 入口参数:p_sock - 处于 LISTENING 的监听控制块 net_sock_t * * 出口参数:无(命中时 p_sock->state 经 handle_connect_event 转 ESTABLISHED) * 返回值:无 - * 限定条件:netTask 上下文(net_accept_locked 内调用,SPI 访问安全) - * 函数说明:1. 每轮 accept 轮询(100ms)读一次芯片状态,兼作状态第二信源 - * 2. 幽灵态(对端端口 = last_peer_port)不动手,去抖清零 - * 3. 连续 NET_LOST_CONN_STABLE_N 轮确认后才补 accept(防"刚 listen - * 尚未生效"的瞬态误判——2026-09-23 实测教训) + * 限定条件:netTask 上下文(net_accept_locked 内调用,其开头已 net_poll(), + * 驱动缓存新鲜度有保证) + * 函数说明:1. N34 阶段 3(F-MS-9):判据改读**驱动状态缓存**(零 SPI;唯一 + * 刷新点=net_poll),**探针去抖删除**——原去抖防的两类瞬态已 + * 结构性消除:"刚 listen 尚未生效"显示 TCP=00 永不命中 ESTABLISHED + * 门(E2:LISTEN 可见 100% <=10ms 无长尾);拆链窗假 ESTABLISHED + * 由幽灵甄别拦截。NET_LOST_CONN_STABLE_N 随之下线 + * 2. 幽灵态(对端端口 = last_peer_port)不动手,计数清零 */ static void net_lost_connect_probe(net_sock_t *p_sock) { - /* 芯片侧状态两字节:[0]=socket 状态,[1]=TCP 状态 */ - uint8_t st[2] = {0, 0}; /* 对端 IP+端口回读缓冲(4B IP + 2B 端口小端) */ uint8_t remote[6] = {0, 0, 0, 0, 0, 0}; + /* 驱动状态缓存(net_poll 每轮刷新;本探针入口前 accept 已 net_poll()) */ + ch395f_sock_state_t c = ch395f_sock_state_cached(p_sock->ch395_sock); - /* 读芯片状态 */ - ch395f_get_socket_status(p_sock->ch395_sock, st); - /* 芯片尚未完成握手:去抖计数清零,本轮无动作 */ - if (st[1] != CH395F_TCP_ESTABLISHED) { + /* 芯片尚未完成握手:可疑电平计数清零,本轮无动作 */ + if (c.tcp_state != CH395F_TCP_ESTABLISHED) { p_sock->lost_conn_seen = 0U; return; } - /* 已 ESTABLISHED:回读对端端口做幽灵甄别 */ + /* 已 ESTABLISHED:回读对端端口做幽灵甄别(按需 SPI,仅命中态发) */ ch395f_get_remot_ipp(p_sock->ch395_sock, remote); /* 对端端口(小端 2 字节) */ uint16_t peer = (uint16_t)(remote[4] | (remote[5] << 8)); @@ -1263,14 +1402,8 @@ static void net_lost_connect_probe(net_sock_t *p_sock) { p_sock->lost_conn_seen = 0U; return; } - /* 去抖:连续确认满门槛才补 accept(封顶防溢出) */ - if (p_sock->lost_conn_seen < NET_LOST_CONN_STABLE_N) { - p_sock->lost_conn_seen++; - } - if (p_sock->lost_conn_seen < NET_LOST_CONN_STABLE_N) { - return; - } - /* 动手前清去抖计数 */ + /* N34 阶段 3(F-MS-9):去抖已删(判据读缓存,两类瞬态结构性消除, + * 见函数头注释 1);lost_conn_seen 仅作 M3 可疑电平标志用,动手前清零 */ p_sock->lost_conn_seen = 0U; /* 留痕:丢中断自愈命中 */ DBG_INFO("sock%d lost-CONNECT self-accept (peer=%u)", @@ -1481,9 +1614,9 @@ int net_listener_armed(int sockfd) { * ⚠ 无残留时必须返回 0 —— 否则调用方会在整个 CLOSED 窗口内每轮都发起 * net_accept(RPC:入队 + xTaskNotifyWait 回执**无上界**)→ 队列积压 → * 调用方任务喂不上狗。2026-10-12 P2a 台架实测:删掉即时 relisten 后 CLOSED - * 窗口被拉长到 pump 周期 + 隔离期(~100ms),:21 0ms×50 期间出现 48 条 + * 窗口被拉长到 reconciler 周期 + 隔离期(~100ms),:21 0ms×50 期间出现 48 条 * `[WD] task stall`(mask=0x67/req=0x77,缺 bit4=UI)。 - * 重建本身由 net_listener_pump 负责,调用方**无需**在此催促。 + * 重建本身由 net_reconcile_tick 形态③④ 负责,调用方**无需**在此催促。 * readable 由 net_poll 在 RECV_OK 置位,"对端发完即 FIN"场景下 CLOSED 之后 * 仍为 1(无人清除),可作"可能仍有残留"的纯 RAM 判据(不发起 SPI)。 */ if ((p_sock->state == NET_SOCK_STATE_CLOSED) && @@ -1600,7 +1733,8 @@ int net_connect(int sockfd, const struct net_sockaddr *addr, int addrlen) { * 返回值:0 - 连接请求已发出(异步,成功以 CONNECT 事件为准);-1 - 失败 int * 限定条件:net_init() 已调用;必须在 netTask 上下文中调用 * 函数说明:1. 校验控制块(in_use、TCP 类型、地址参数)并保存对端地址 - * 2. 芯片侧 OPEN(失败重试 3 次、间隔 500ms,见 Trap 09)+ 配置源/目的端口与目的 IP + * 2. 芯片侧 OPEN(失败重试 3 次,每次失败走 sock_reset 确定性排渣, + * 见 Trap 09 / P2b)+ 配置源/目的端口与目的 IP * 3. 清本 Socket 残留中断状态(丢弃上一条连接遗留的 DISCONNECT) * 4. 发起 TCP_CONNECT(驱动内已轮询 CMD_STATUS)并置 CONNECTING; * 命令级被拒立即关 Socket 返 -1,TCP 层成败仍由 CONNECT/TIMEOUT 裁定 @@ -1651,7 +1785,11 @@ static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int a /* 设置协议类型为 TCP */ ch395f_set_proto_type(sockfd, CH395F_PROTO_TYPE_TCP); - /* CH395F 关闭 Socket 后需延时才能重新打开(Trap 09),故失败重试最多 3 次 */ + /* CH395F 关闭 Socket 后需延时才能重新打开(Trap 09),故失败重试最多 3 次。 + * P2b 收口(N34 阶段 3):原"close + 盲等 500ms"改**确定性排渣**—— + * sock_reset(读空 RX -> CLOSE -> 排空事件 -> 等 byte0=00H,内置 LISTEN + * 态二段 CLOSE 补发;E4 定案 60 轮实证复位后 open 必成)。GRACEFUL + * 最短等 100ms ≪ 原 500ms 盲等;复位失败/超时仍走下一轮重试,Trap 09 语义保留 */ for (int open_try = 0; open_try < 3; open_try++) { /* 尝试打开芯片侧 Socket */ status = ch395f_open_socket(sockfd); @@ -1659,10 +1797,8 @@ static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int a /* 打开成功,退出重试 */ break; } - /* 打开失败先关闭,再延时后重试 */ - ch395f_close_socket(sockfd); - /* 等 500ms 芯片沉降后再试(Trap 09) */ - osDelay(500); + /* 打开失败:确定性复位本 Socket(排渣/二段关闭/等 CLOSED 一体) */ + (void)net_sock_reset_sync(sockfd, CH395F_RESET_GRACEFUL); } /* 3 次仍失败 → BUSY */ @@ -1670,11 +1806,10 @@ static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int a return NET_ERR_RETURN_1(NET_ERR_BUSY); } - /* 清掉本 Socket 可能残留的中断状态,丢弃上一条连接遗留的 DISCONNECT */ - /* 读即清:本 Socket 中断状态寄存器 */ - (void)ch395f_get_sock_int_status((uint8_t)sockfd); - /* 读即清:全局中断状态寄存器 */ - (void)ch395f_get_glob_int_status_all(); + /* 清掉本 Socket 可能残留的中断状态,丢弃上一条连接遗留的 DISCONNECT。 + * F-NET-7(N34 阶段 3):只清**本 Socket** SINT(驱动护栏原语)——原 + * "再读清全局"会焊死其它在用 Socket 的事件位(全局寄存器唯一读者=net_poll) */ + (void)ch395f_sock_events((uint8_t)sockfd); /* 设置本地(源)端口 */ ch395f_set_sour_port(sockfd, p_sock->local_port); @@ -1943,7 +2078,7 @@ static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags, * 它是"消费者动作触发"(消费者正在等这条连接的结局),不是周期性 * "抢先";删掉它会把 CLOSED 窗口从 ~0 拉长到 pump 周期+隔离期 * (~100ms),期间调用方高频发起 net_accept(RPC 无上界)→ - * `[WD] task stall`(实测 uis 往返 1063ms)。周期性抢先由 pump 收敛即可。 */ + * `[WD] task stall`(实测 uis 往返 1063ms)。周期性抢先由 reconciler 收敛即可。 */ if ((p_sock->state == NET_SOCK_STATE_CLOSED) && (p_sock->auto_relisten != 0U)) { /* 非阻塞:失败由 D10 重试轮询接管 */ (void)net_listener_try(sockfd); @@ -2338,45 +2473,31 @@ static void net_pending_settle_close(net_sock_t *p_sock) { * 出口参数:无 * 返回值:0 - 关闭成功 int * 限定条件:必须在 netTask 上下文中调用;p_sock 有效 - * 函数说明:1. 关闭 CH395F Socket,排空上一条连接残留的 DISCONNECT 中断 - * 2. 等待芯片回到 CLOSED 状态(超时 500ms),再清零控制块 + * 函数说明:1. P2b 收口(N34 阶段 3):原"close + 500ms 盲等排空循环"改 + * **确定性 sock_reset**(读空 RX -> CLOSE(幂等) -> 排空本 Socket + * 事件(quiescent) -> 等 byte0=00H,内置 LISTEN 态二段 CLOSE 补发 + * ——E2 补充发现:一次 CLOSE 只拆监听 byte0 恒 05) + * 2. F-NET-7:排空只碰本 Socket SINT,**不再读清全局中断寄存器** + * (唯一读者=net_poll;原循环读全局会焊死其它 Socket 事件位) + * 3. 快路径:芯片已 CLOSED(byte0=00H)→ 跳过复位只清残留事件 + * 4. 复位超时照常放行(同原 500ms 超时容忍;后续 relisten 由 + * Trap 09 open 重试环吸收) */ static int net_close_locked(net_sock_t *p_sock) { /* 由控制块指针反推描述符 */ int sockfd = (int)(p_sock - s_net_socks); - /* 起始时刻(等芯片 CLOSED 的 500ms 上限基准) */ - uint32_t tick = HAL_GetTick(); /* L3:先了结本 fd 的挂起 recv(排空/取消 + 必通知),再关芯片 Socket * ——关闭不得让等待中的调用方永久悬挂 */ net_pending_settle_close(p_sock); - /* 关闭芯片侧 Socket */ - ch395f_close_socket(sockfd); - - /* 排空上一条连接残留的 DISCONNECT 中断(单连接模式仅 Socket 0;复用同一 Socket 时 - * 旧 DISCONNECT 在 FIN/ACK 完成后才上报,不清除会被下一连接误判为"刚建立就断开") - * 等待芯片回到 CLOSED 并清掉 Socket/全局中断状态,避免事件串扰 */ - do { - /* [0]=Socket 状态 [1]=TCP 状态(该 API 输出 2 字节) */ - uint8_t st[2] = {0, 0}; - /* 读芯片状态(两字节) */ - ch395f_get_socket_status((uint8_t)sockfd, st); - /* 读即清:本 Socket 中断状态 */ - (void)ch395f_get_sock_int_status((uint8_t)sockfd); - /* 读即清:全局中断状态 */ - (void)ch395f_get_glob_int_status_all(); - /* 芯片已回 CLOSED:结束 */ - if (st[0] == CH395F_SOCKET_CLOSED) { - break; - } - /* 等待超时(500ms):停止等待继续 */ - if (HAL_GetTick() - tick > 500) { - break; - } - /* 等 5ms 再查(循环受 500ms 上限约束) */ - osDelay(5); - } while (1); + /* 排渣 + 关闭 + 等 CLOSED 一体(见函数头注释 1); + * 已 CLOSED 的 fd 只清本 Socket 残留事件(防下一连接误判"刚建立就断开") */ + if (ch395f_cmd_done((uint8_t)sockfd, CH395F_DONE_CLOSE) == 1) { + (void)ch395f_sock_events((uint8_t)sockfd); + } else if (net_sock_reset_sync(sockfd, CH395F_RESET_GRACEFUL) != 0) { + DBG_ERROR("sock%d close: sock_reset not done, proceed anyway", sockfd); + } /* 清零控制块(memset 0 = 释放本 fd 全部状态) */ /* N-30(Trap 25):幽灵甄别基准跨 fd 释放保留(幽灵属芯片 Socket 级状态, @@ -2393,6 +2514,52 @@ static int net_close_locked(net_sock_t *p_sock) { return 0; } +/* + * 函数功能:主动拆掉当前 TCP 会话、保留监听(内部版本,netTask 上下文) + * 入口参数:p_sock - 会话所在 Socket 控制块(调用方已判 ESTABLISHED/CONNECTING) + * 出口参数:无 + * 返回值:0 - 拆除完成(监听型 fd 已重监听或交 D10) int + * 限定条件:netTask 上下文;p_sock 在用且为 TCP 流 + * 函数说明:1. E-02 收口(实验方案 E13 §5):应用放弃的会话若 TCP 仍活 + * (PC 内核自动 ACK keepalive,僵尸判不死),单连接监听被永久 + * 占住;应用又不得 net_close 监听 fd(毁监听)——本函数补上 + * "拆连接不拆监听"的应用侧形态 + * 2. 拆除=sock_reset QUARANTINE(Trap 23 异常释放语义:FIN + 排渣 + + * 排空事件(quiescent) + 等 byte0=00H,内置二段 CLOSE 补发) + * 3. auto_relisten(监听型 fd):收口 CLOSED 后即时 net_listener_try + * (芯片已 CLOSED 直接 open 必成,E4 定案;失败交 D10 重试 + + * reconciler 形态④);非 auto_relisten(client fd):收口 CLOSED, + * 是否重连由调用方决定 + * 4. 挂起 recv 先了结(settle_close:排空尝试或取消,必通知) + */ +static int net_drop_conn_locked(net_sock_t *p_sock) { + /* 由控制块指针反推描述符 */ + int sockfd = (int)(p_sock - s_net_socks); + + /* L3:先了结本 fd 挂起 recv(排空/取消 + 必通知),再拆芯片会话 */ + net_pending_settle_close(p_sock); + + /* 确定性拆除(异常释放 → QUARANTINE 400ms 底,Trap 23 语义) */ + if (net_sock_reset_sync(sockfd, CH395F_RESET_QUARANTINE) != 0) { + DBG_ERROR("sock%d drop_conn: sock_reset not done", sockfd); + /* 超时继续收口本层;芯片侧交 reconciler/pump 对账接管 */ + } + + /* 会话簿记收口(保留 fd:不 memset、不动 local_port/auto_relisten) */ + p_sock->state = NET_SOCK_STATE_CLOSED; + p_sock->readable = 0U; + /* N-30(Trap 25):新会话起点,丢中断去抖计数清零 */ + p_sock->lost_conn_seen = 0U; + + if (p_sock->auto_relisten) { + /* 监听型 fd:即时重建(非阻塞;失败 D10 重试轮询接管) */ + (void)net_listener_try(sockfd); + } + + /* 拆除完成 */ + return 0; +} + /* * 函数功能:发送数据(线程安全版本,通过消息队列委托给 netTask) * 入口参数:sockfd - Socket 描述符 int >= 0 && < NET_MAX_SOCKETS @@ -2705,6 +2872,63 @@ int net_close(int sockfd) { return msg.result; } +/* + * 函数功能:主动拆掉当前 TCP 会话、保留监听(线程安全版本,经消息队列委托 netTask) + * 入口参数:sockfd - Socket 描述符 int >= 0 && < NET_MAX_SOCKETS + * 出口参数:无 + * 返回值:0 - 成功(含幂等:无活动会话直接 0) int + * -1 - 失败(BADF/INVAL/入队失败) int + * 限定条件:1. 必须在【非 netTask】上下文调用(同其它 net_* API,netTask 内会自等死锁) + * 2. 场景=应用放弃会话(对时 30s 预算耗尽、协议超时);EOF 路径**不要**调 + * (net 层 auto_relisten 已接管,见 AGENTS 单连接断开语义) + * 函数说明:1. 单连接模型下挂起但 TCP 仍活的客户端会永久占住监听(keepalive + * 被 PC 内核自动 ACK 判不死,实验方案 E13 §5);应用不得 + * net_close 监听 fd → 本 API 拆连接不拆监听(E-02 收口) + * 2. netTask 侧执行 sock_reset QUARANTINE + 监听即时重建(见 + * net_drop_conn_locked);等待期 netTask 有界占用 ~400-600ms + */ +int net_drop_conn(int sockfd) { + /* 消息体(按值拷入队列,netTask 改的是其副本) */ + net_msg_t msg; + + /* 描述符越界 → BADF */ + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { + return NET_ERR_RETURN_1(NET_ERR_BADF); + } + + /* 消息队列未创建(网络层未初始化)→ 通用错误 */ + if (netMsgQueueHandle == NULL) { + return NET_ERR_RETURN_1(NET_ERR); + } + + /* 消息类型:拆会话保监听 */ + msg.type = NET_MSG_DROP_CONN; + /* 目标 Socket */ + msg.sockfd = sockfd; + /* 无超时 */ + msg.timeout_ms = 0U; + /* 调用方任务句柄(netTask 凭它回传回执) */ + msg.caller = xTaskGetCurrentTaskHandle(); + /* 结果占位 */ + msg.result = -1; + + /* 入队:队列满时阻塞调用方直至有空位 */ + if (osMessageQueuePut(netMsgQueueHandle, &msg, 0, osWaitForever) != osOK) { + /* 入队失败 → BUSY */ + return NET_ERR_RETURN_1(NET_ERR_BUSY); + } + + /* 阻塞等待 netTask 回执(两段式拾取,防丢失通知) */ + msg.result = net_wait_result("dcp", sockfd); + + if (msg.result < 0) { + /* 错误码经通知值回传;对外保持 BSD 语义失败一律 -1 */ + return NET_ERR_RETURN_1(msg.result); + } + /* 拆除成功(0,含幂等) */ + return msg.result; +} + /* * 函数功能:发送 UDP 数据报(含目标地址) * 入口参数:sockfd - Socket 描述符 int >= 0 && < NET_MAX_SOCKETS @@ -3118,7 +3342,7 @@ int net_sock_state(int sockfd) { /* N34 阶段1(F-NET-3):手动自愈 API net_resync_listen / net_relisten 已删除—— * RESYNC 语义并入对账⑤(net_reconcile_tick 自动认领"芯片静默回收回 LISTEN"形态, - * 200ms×2 去抖 + 1s 冷却沿用);RELISTEN 与 pump/D10 完全重叠且对健康监听器是 + * 200ms×2 去抖 + 1s 冷却沿用);RELISTEN 与 reconciler/D10 完全重叠且对健康监听器是 * footgun(无条件 open→USE→close 自毁 ~1s,原注释自认)。生产调用者本就为零 * (仅 test/board Phase 13,已改由自动机制接管)。历史实现见本文件 git 历史。 */ @@ -3325,7 +3549,7 @@ static void handle_connect_event(net_sock_t *p_sock) { * 撤回(当时仅证明"物理断链"已被 +2s TIM_OUT 快速覆盖,其余场景 * 无法注入、未证明);E11(2026-10-13,实验方案.md)用"禁用 PC 网卡" * 注入"链路在、对端静默"(NAT/黑洞)补上证据链:生产参数 60/5/3 - * 实测 ≈30 s 判死 → TIM_OUT → QUARANTINE → pump 重建;对端存活时 + * 实测 ≈30 s 判死 → TIM_OUT → QUARANTINE → reconciler 重建;对端存活时 * 探测被 ACK、空闲会话不误杀。消费链路现成,按手册位置转正。 */ ch395f_set_keepalive_enable(p_sock->ch395_sock, 1); @@ -3389,17 +3613,19 @@ static void handle_disconnect_event(net_sock_t *p_sock) { /* Trap 22 / N-6:监听态收到 DISCONNECT——bit3=1(0x08)下芯片**不**关该 Socket(停 0x05/0x00,芯片 TCP 侧已 CLOSED);不重开则此后所有 SYN 全被拒 = 监听永久死亡; * 触发 = 断连后 <~1s 快速复连时 CONNECT 中断未上报(控制块滞留 LISTENING)后该连接断开(2026-09-23 生产固件复现); * N-32 P2:本分支**只回读留痕、不再 relisten** —— 重建统一交 - * net_listener_pump(每 100 ms 回读芯片状态,监听失位即重建)。原处直接 + * net_reconcile_tick 形态③(每 100 ms 读驱动缓存,监听失位即重建)。原处直接 * relisten 是 12 个所有者之一,且位于 DISCONNECT 事件路径,可能"抢先" - * 于消费者 recv(P1 已消除其后果,P2 消除其可能) */ + * 于消费者 recv(P1 已消除其后果,P2 消除其可能)。 + * ⚠ 本留痕回读保持**主动 SPI**(不用驱动缓存):事件路径上缓存必然滞后 + * 于刚到达的中断(刷新在 net_poll 末尾、事件处理之后),仅周期对账可用缓存 */ /* 回读缓冲(芯片状态 2 字节) */ uint8_t st[2] = {0, 0}; - /* 回读芯片状态(仅为留痕:重建与否由 pump 裁决) */ + /* 回读芯片状态(仅为留痕:重建与否由 reconciler 裁决) */ ch395f_get_socket_status((uint8_t)sockfd, st); /* 监听已失位(芯片已关闭) */ if ((st[0] == CH395F_SOCKET_CLOSED) || (st[1] != CH395F_TCP_LISTEN)) { - DBG_INFO("sock%d listen lost (chip sock=0x%02X tcp=0x%02X), pump will relisten", + DBG_INFO("sock%d listen lost (chip sock=0x%02X tcp=0x%02X), reconciler will relisten", sockfd, st[0], st[1]); } else { /* 仍在监听:判陈旧事件丢弃 */ @@ -3425,8 +3651,9 @@ static void handle_disconnect_event(net_sock_t *p_sock) { * 而 TCP 槽 0x00 且本层 ESTABLISHED = 逻辑矛盾(活连接应 0x04):陈旧 DISCONNECT * (拆链完成滞后锁存,Trap 14)与"真断开恰被旧拆链覆写槽"在 D20 上不可分辨 * (结构性失明,F-NET-15 Q1)。处置:不吃边沿,置可疑电平返回,交对账① - * (ESTABLISHED + 芯片 TCP==CLOSED,:588-589)电平去抖再入本函数(去抖 2×200ms - * + 冷却,实测延迟 ~1.2s):flag==1 旁路本门 → 正常释放;连接真活则租约内 + * (ESTABLISHED + 芯片 TCP==CLOSED)电平去抖再入本函数(N34 阶段 3 合并 + * 后=4×100ms,NET_RECON_STABLE_DEATH,让位租约 400ms 不变 + 冷却): + * flag==1 旁路本门 → 正常释放;连接真活则租约内 * 会话正常完成(风暴链掐断,E9a n30 0/50 实证)。 * 零额外 SPI 于常规路径(st[] 已读;recv_len 仅可疑分支多读一条)。 * ⚠ E9a/E9b 实测两轮收窄:①必须含 recv_bytes==0——已读数据的会话遇 @@ -3457,7 +3684,7 @@ static void handle_disconnect_event(net_sock_t *p_sock) { /* F-NET-15 M1(N34 实验 2026-10-12):沉降**无条件**登记。原 recv_bytes!=0 门的 * 受害者由事件时序定义(accept 后 ~20ms 被陈旧 DISCONNECT 杀 → recv_bytes==0 是 * 构造性必然),门对"残渣循环自持"形态失效(n30_repro 背靠背 9/10 失败,E1 实测); - * 门原保护目标(:21 @0ms 快复连)已被 N-32 pump 延迟化(恢复窗实测 0.3~1s)架空, + * 门原保护目标(:21 @0ms 快复连)已被 N-32 单一重建入口延迟化(恢复窗实测 0.3~1s)架空, * +100ms 沉降落在窗内不可见。沉降窗口同时让残渣 DISCONNECT 在 state==CLOSED * (无害、仅留痕)时被消费,断开"杀下一条 → 新拆链 → 再杀"的繁殖链 */ { diff --git a/Drivers/BSP/NET/net_socket.h b/Drivers/BSP/NET/net_socket.h index bbd4a319..a85a9159 100644 --- a/Drivers/BSP/NET/net_socket.h +++ b/Drivers/BSP/NET/net_socket.h @@ -37,8 +37,10 @@ typedef enum { NET_MSG_CONNECT, NET_MSG_LISTEN, NET_MSG_ACCEPT, + NET_MSG_DROP_CONN, /* N34 阶段 3(E-02 收口):主动拆会话、保监听(见 net_drop_conn) */ /* N34 阶段1(F-NET-2/3):NET_MSG_RESYNC_LISTEN / NET_MSG_RELISTEN 已删—— - * 静默回收由对账⑤自动认领(net_reconcile_tick),重建由 pump/D10 单一 + * 静默回收由对账⑤自动认领(net_reconcile_tick),重建由 reconciler + * 形态③④/D10 单一 * 所有者负责;手动自愈 API 面退役(生产调用者本就为零) */ } net_msg_type_t; @@ -179,7 +181,7 @@ int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen); * 保住 N-30 的 "closed-but-readable" 接受;**无残留时必须返回 0**, * 否则调用方会在整个 CLOSED 窗口内每轮发起 `net_accept`(RPC 无上界) * → 队列积压 → 喂不上狗(2026-10-12 P2a 台架实测 48 条 task stall)。 - * 重建由 `net_listener_pump` 负责,调用方无需催促 + * 重建由 `net_reconcile_tick` 形态③④ 负责,调用方无需催促 * 4. 与已退役 `net_can_accept` 的关系:本函数 = 其 ESTABLISHED 放行态 * ∪ LISTENING ∪ CLOSED+残留;回答"这个监听器这一轮**值不值得**发起 * 一次 accept",而非"一定有连接" @@ -341,6 +343,22 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags, */ int net_close(int sockfd); +/* + * 函数功能:主动拆掉当前 TCP 会话,**保留监听**(N34 阶段 3,E-02 收口) + * 入口参数:sockfd - socket 描述符 + * 返回值:0 成功(含幂等:无活动会话),-1 失败 + * 限定条件:非 netTask 上下文(经消息队列委托,同其它 net_* API) + * 函数说明:1. 适用场景=应用层放弃会话(对时 30s 预算耗尽、协议超时等): + * 单连接模型禁止对监听 fd net_close(连监听一起毁,AGENTS 约定), + * 而挂起但 TCP 仍活的客户端会永久占住单连接监听(PC 内核自动 + * ACK keepalive,芯片僵尸判不死——实验方案 E13 §5 定案缺口) + * 2. 实现=sock_reset QUARANTINE(FIN 关闭 + 排渣 + 排空事件 + 等 + * byte0=00H,Trap 23 异常释放语义);auto_relisten fd 随即重监听 + * (失败交 D10 重试),非 auto_relisten(client fd)收口 CLOSED + * 3. 幂等:非 ESTABLISHED/CONNECTING 直接返 0,不动监听 + */ +int net_drop_conn(int sockfd); + /* * 事件回调 API */ @@ -383,9 +401,9 @@ int net_get_errno(void); int net_sock_state(int sockfd); /* N34 阶段1(F-NET-3):net_resync_listen / net_relisten 声明已删—— - * 静默回收形态由对账⑤自动认领(≤400ms 去抖 + 1s 冷却,纪律"不发芯片命令" - * 原样保留在认领动作内);监听重建由 pump(100ms 周期)/D10(30ms 重试) - * 单一所有者。勿再引入"应用层手动自愈"入口。 */ + * 静默回收形态由对账⑤自动认领(2 轮去抖 + 1s 冷却,纪律"不发芯片命令" + * 原样保留在认领动作内);监听重建由 reconciler 形态③④(100ms 周期)/ + * D10(30ms 重试)单一所有者。勿再引入"应用层手动自愈"入口。 */ /* * 函数功能:将 IP 地址字符串转为网络字节序 diff --git a/Drivers/BSP/NET/net_types.h b/Drivers/BSP/NET/net_types.h index 35cfbd42..99e7380d 100644 --- a/Drivers/BSP/NET/net_types.h +++ b/Drivers/BSP/NET/net_types.h @@ -111,7 +111,12 @@ struct net_sockaddr_in { #define NET_ERR_BUSY -11 /* 操作未完成(CH395F BUSY) */ /* - * Socket 状态枚举 + * Socket 状态枚举(**本层会话投影**,非芯片状态事实源) + * N34 阶段 3(镜像降级,实施方案 docs/网络/N34-自愈审计/实施方案.md §5 项 5): + * 芯片侧状态唯一事实源=驱动状态缓存(ch395f_sock_state_cached,刷新点=net_poll); + * 本 8 态只表达**本层会话语义**(fd 生命周期/待 accept 电平/门控展示), + * 事件与对账把它与芯片事实对齐。新增"按芯片状态决策"的代码一律读驱动缓存, + * 勿再直读本投影判芯片(历史五层补丁的根因,见主流方案对照 F-MS-8)。 */ typedef enum { NET_SOCK_STATE_CLOSED = 0, /* 未使用 */ diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 68821deb..d66efbfc 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -36,10 +36,10 @@ | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 228,372 B (223.02 KB) | 1,048,576 B (1024 KB) | 21.8% | -| RW_IRAM1 | 97,952 B (95.66 KB) | 131,072 B (128 KB) | 74.7% | +| ER_IROM1 | 230,104 B (224.71 KB) | 1,048,576 B (1024 KB) | 21.9% | +| RW_IRAM1 | 97,912 B (95.62 KB) | 131,072 B (128 KB) | 74.7% | | RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | -| **Flash (ROM 合计)** | **229,484 B (224.1 KB)** | 1,048,576 B (1024 KB) | **21.9%** | +| **Flash (ROM 合计)** | **231,208 B (225.8 KB)** | 1,048,576 B (1024 KB) | **22.0%** | 各区的容量与常量来源: @@ -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 监听模块化后复测,链尾经 net_listener_pump) | +| netTask | `960*4` | 3.75KB | CH395F 轮询(DMA 搬运,栈压力小) | 920 B(余 76%)/ 静态 **1944 B**(2026-10-12 N32 监听模块化后复测;N34 阶段 3 合并后复测 **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) | diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index fe53fb96..7c131e20 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -15,14 +15,14 @@ | 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) | |---|---|---|---|---| -| ER_IROM1 (Flash) | 228,372 B | 1,048,576 B | 21.8% | 参考 | +| ER_IROM1 (Flash) | 230,104 B | 1,048,576 B | 21.9% | 参考 | | RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS | | UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 | -| RW_IRAM1 | 97,952 B | 131,072 B | 74.7% | PASS | +| RW_IRAM1 | 97,912 B | 131,072 B | 74.7% | PASS | | 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 | |---|---|---|---| -| 网络 | 13,498 B | 12 KB | ⚠ 超 1.2 KB | +| 网络 | 13,458 B | 12 KB | ⚠ 超 1.1 KB | | 存储 | 7,056 B | 12 KB | OK | | UI | 54,988 B | 16 KB | ⚠ 超 37.7 KB | | RTOS 堆 | 57,376 B | 56 KB | 堆=配额 | diff --git a/scripts/ram_baseline.tsv b/scripts/ram_baseline.tsv index 511cd685..f865b933 100644 --- a/scripts/ram_baseline.tsv +++ b/scripts/ram_baseline.tsv @@ -1,3 +1,3 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi 2026-10-12 209492 18332 1104 154184 227824 21.7 97864 74.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;console_log.o=4120;ch395f.o=4106;default_task.o=2736;sys_log_mcu.o=1924 -2026-10-13 209780 18592 1112 154264 228372 21.8 97952 74.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;default_task.o=2736;sys_log_mcu.o=1924 +2026-10-13 211348 18756 1104 154232 230104 21.9 97912 74.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;default_task.o=2736;sys_log_mcu.o=1924 diff --git a/simulator/sim/net_socket_sim.c b/simulator/sim/net_socket_sim.c index 81eef003..ed64c237 100644 --- a/simulator/sim/net_socket_sim.c +++ b/simulator/sim/net_socket_sim.c @@ -708,6 +708,40 @@ int net_close(int sockfd) { return 0; } +/* + * 函数功能:主动拆掉当前 TCP 会话、保留监听(与 MCU 端同名 API——N34 阶段 3,E-02 收口) + * 入口参数:sockfd - socket 描述符 + * 出口参数:无 + * 返回值:0 成功(含幂等:无活动会话),-1 失败 + * 限定条件:socket 已打开(in_use == 1) + * 函数说明:模拟器为多连接后端(conn != 监听 fd):等价关闭该数据连接的 + * Winsock 句柄并收口 CLOSED(fd 槽不释放,外层 net_close 收尾); + * 监听 fd 不受影响。无活动会话幂等返 0。 + */ +int net_drop_conn(int sockfd) { + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { + s_net_errno = NET_ERR_BADF; + return -1; + } + if (s_socks[sockfd].in_use == 0) { + s_net_errno = NET_ERR_BADF; + return -1; + } + if (s_socks[sockfd].state != NET_SOCK_STATE_ESTABLISHED && + s_socks[sockfd].state != NET_SOCK_STATE_CONNECTING) { + /* 无活动会话:幂等成功 */ + s_net_errno = NET_OK; + return 0; + } + + closesocket(s_ws[sockfd]); + s_ws[sockfd] = INVALID_SOCKET; + s_socks[sockfd].state = NET_SOCK_STATE_CLOSED; + + s_net_errno = NET_OK; + return 0; +} + /* * 函数功能:注册事件回调 * 入口参数:sockfd - socket 描述符 diff --git a/test/host/e02_probe.py b/test/host/e02_probe.py index 19c49410..f6268f36 100644 --- a/test/host/e02_probe.py +++ b/test/host/e02_probe.py @@ -7,10 +7,18 @@ 全体消费者的队列消息停摆最长 30s; 2. 2s 后从 PC 发起 FTP(:21) 连接,测 banner 到达时延 = 队列响应性: 修复前 ≈ 25~30s(队头冻结),修复后 ≈ 数百 ms 内; - 3. 34s 时 30s 总预算应已耗尽:设备拆除该连接(修复版落 - "time_sync: no TIME packet in 30s, drop conn" 日志,旧版静默)、 - 监听重建,:8888 应能再接受新连接; - 4. 新连接发完整 8 字节 TIME 包 → 设备应落 "wall clock updated"。 + 3. 34s 时 30s 总预算应已耗尽:设备拆掉该会话(N34 阶段 3 net_drop_conn: + FIN 关闭 + 本层收口 + 重监听),随后**本探针模拟守规矩对端**: + close 自己的 socket; + 4. 芯片回 CLOSED(TC-NET-204 模式 A:TIME_WAIT 过渡 ~150ms)→ relisten, + :8888 应能再接受新连接(3s 重试窗内);新连接发完整 8 字节 TIME 包 → + 设备应落 "wall clock updated"。 + +⚠ 定案口径(Trap 07 定案更正 2026-10-11 + 实验方案 E13 §5):CH395F close +**无 RST 语义**。若对端内核存活(app 挂起但 socket 不关,内核自动 ACK FIN), +芯片停在 FIN_WAIT_2、监听恢复受限于对端 close——探针第 3 步的 close 正是 +模拟"对端配合"这一可恢复边界;不配合场景的兜底是芯片 TIMEOUT/keepalive +拆僵尸会话(E13 定案,30~90s)。 用法: python e02_probe.py 退出码: 0 = 队列响应性 PASS;1 = FAIL;2 = 探针自身异常 @@ -53,16 +61,32 @@ def main(): % ("PASS" if queue_ok else "FAIL", ("%.0f" % (dt * 1000)) if dt is not None else "n/a")) - # 3. 等 30s 总预算耗尽(设备应拆除挂起连接、重建监听) + # 3. 等 30s 总预算耗尽(设备应拆会话:FIN + 本层收口 + 重监听) remain = 34.0 - (time.time() - t0) if remain > 0: time.sleep(remain) - # 4. 监听应已重建:新连接可接入,且正常对时路径工作 + # 3b. 模拟守规矩对端:设备已发 FIN(drop_conn),本端 close 配合拆链。 + # CH395F close 无 RST 语义(Trap 07 定案更正):不配合的内核存活对端 + # 会把芯片停在 FIN_WAIT_2,监听恢复受限于对端 close——不在本探针判据内。 + try: + ts.close() + log("hung client socket closed (cooperate with device FIN)") + except OSError: + pass # 设备已先拆连,本地 close 报 ECONNRESET 属预期 + + # 4. 监听应已重建(芯片 TIME_WAIT 过渡 ~150ms + relisten):3s 重试窗 t2 = time.time() listener_ok = False - try: - ts2 = socket.create_connection((DEV, 8888), timeout=5) + ts2 = None + while time.time() - t2 < 3.0: + try: + ts2 = socket.create_connection((DEV, 8888), timeout=2) + break + except Exception as e: + log("second time-sync conn retry: %r" % (e,)) + time.sleep(0.3) + if ts2 is not None: dt2 = time.time() - t2 epoch = int(time.time()) log("new time-sync conn in %6.0f ms, sent full TIME packet (epoch=%d)" @@ -71,12 +95,8 @@ def main(): time.sleep(1.5) # 留时间给设备处理并打日志 ts2.close() listener_ok = True - except Exception as e: - log("second time-sync conn FAIL: %r" % (e,)) - try: - ts.close() - except OSError: - pass # 设备已先拆连,本地 close 报 ECONNRESET 属预期 + else: + log("second time-sync conn FAIL (3s window)") log("OVERALL: queue=%s listener=%s" % ("PASS" if queue_ok else "FAIL", "PASS" if listener_ok else "FAIL")) return 0 if (queue_ok and listener_ok) else 1 diff --git a/test/host/time_sync/stub/net_socket.h b/test/host/time_sync/stub/net_socket.h index 92b1df16..0cdd5ebb 100644 --- a/test/host/time_sync/stub/net_socket.h +++ b/test/host/time_sync/stub/net_socket.h @@ -60,6 +60,7 @@ int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen); int net_listen(int sockfd); int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen); int net_close(int sockfd); +int net_drop_conn(int sockfd); int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms); uint32_t net_get_generation(void); uint16_t net_htons(uint16_t hostshort); diff --git a/test/host/time_sync/time_sync_test.c b/test/host/time_sync/time_sync_test.c index 92278aa2..7aac8da2 100644 --- a/test/host/time_sync/time_sync_test.c +++ b/test/host/time_sync/time_sync_test.c @@ -235,6 +235,18 @@ int net_close(int sockfd) { return 0; } +/* ---- net_drop_conn(N34 阶段 3 E-02 收口):计数 + 记录 fd ---- */ +static int s_drop_calls; +static int s_drop_socks[32]; + +int net_drop_conn(int sockfd) { + if (s_drop_calls < 32) { + s_drop_socks[s_drop_calls] = sockfd; + } + s_drop_calls++; + return 0; +} + uint32_t net_get_generation(void) { int i = s_gen_calls++; @@ -351,6 +363,7 @@ static void fakes_reset(void) { s_accept_calls = 0; s_accept_seq_n = 0; s_close_calls = 0; + s_drop_calls = 0; s_gen_seq_n = 0; s_gen_default = 1u; s_gen_calls = 0; @@ -518,6 +531,7 @@ static void tc_handle_conn(void) { CHECK((s_recv_calls == 1) && (s_recv_len_log[0] == 16), "TC-TS-020c 首次请求 16 字节"); CHECK(s_recv_to_log[0] == 50U, "TC-TS-020d 单次等待片长 = 50ms"); CHECK(s_close_calls == 0, "TC-TS-020e 处理完**不**关闭连接(N34 阶段1:close 交外层 conn!=ls 分支)"); + CHECK(s_drop_calls == 0, "TC-TS-020g 成功会话不拆(drop 只用于放弃路径)"); CHECK(g_tstest_info_count == 1, "TC-TS-020f 留痕 wall clock updated"); /* 2) 分段送达:4 + 4 */ @@ -591,6 +605,7 @@ static void tc_handle_conn(void) { CHECK(s_recv_calls == 1, "TC-TS-027 预算耗尽后不再读(只读 1 次)"); CHECK(g_tstest_info_count == 1, "TC-TS-027b 留痕 no TIME packet in 30s, drop conn"); CHECK(s_close_calls == 0, "TC-TS-027c 预算耗尽也不关闭连接"); + CHECK(s_drop_calls == 1, "TC-TS-027d 预算耗尽主动拆会话(N34 阶段 3 E-02 收口:net_drop_conn 拆连接保监听)"); /* 9) 剩余预算不足一片 */ fakes_reset(); From f51c0833ae0d38808a2d4a83b8f4b6d821ad4a47 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 13 Oct 2026 15:10:54 +0800 Subject: [PATCH 19/24] =?UTF-8?q?docs(N34):=20=E9=98=B6=E6=AE=B53=20?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E5=90=8C=E6=AD=A5=E2=80=94=E2=80=94reconcile?= =?UTF-8?q?r=20=E5=90=88=E5=B9=B6=E5=8F=A3=E5=BE=84+net=5Fdrop=5Fconn+Trap?= =?UTF-8?q?=20=E8=A1=A5=E5=85=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BSD_Socket_API_使用指南:五层自愈→四层(合一 reconciler 形态①~⑤+U11, 分级去抖②1/③④⑤2/①4轮+1s冷却);新增 net_drop_conn 小节(拆会话保 监听/幂等/EOF 不得调);变更记录追加 2026-10-13 行 - BSD_Socket_API_函数调用关系:pump 节点删除、net_drop_conn 链路新增、 驱动接口表补 sock_reset/cmd_done/缓存行、常量清单更正 - 网络Socket监听生命周期设计说明:新增 §10.10 合并说明,历史文本保留+注记 - Net层阻塞模型架构设计:主循环 3 tick 口径 + 附录 B 追加 - RAM使用分析与优化方案:netTask 链尾改 retry_tick→relisten(1944 B 不变) - CH395F_Trap_Records:Trap 07 补充(close 无 RST:对端内核存活不 close → FIN_WAIT_2 长尾,兜底 E13);Trap 09 补充(LISTEN 态二段关闭) - 实施方案 §5:项1/2/3/5 ✅、项4 受 E6 暂缓、§5.1 E-02 收口、§5.2 延后项 - 实验方案:E-02 收口台架小节(FIN_WAIT_2/TIME_WAIT ~150ms/双 PASS) - N34 README 裁决行更正;Test_Guide TC-NET-1101 pump→reconciler 形态③④ 历史变更记录与历史事故描述一律原文保留、只追加;check_doc_links --sections-strict 对改动文件 0 问题;乱码 0、无 BOM 变化。 --- .../网络Socket监听生命周期设计说明.md | 44 +++++- docs/测试说明/CH395F_Test_Guide.md | 2 +- docs/网络/BSD_Socket_API_使用指南.md | 56 +++++--- docs/网络/BSD_Socket_API_函数调用关系.md | 132 ++++++++++-------- docs/网络/N34-自愈审计/README.md | 2 +- docs/网络/N34-自愈审计/实施方案.md | 46 +++++- docs/网络/N34-自愈审计/实验方案.md | 31 ++++ docs/网络/Net层阻塞模型架构设计.md | 3 +- docs/问题记录/CH395F_Trap_Records.md | 19 +++ 9 files changed, 246 insertions(+), 89 deletions(-) diff --git a/docs/子系统设计/网络Socket监听生命周期设计说明.md b/docs/子系统设计/网络Socket监听生命周期设计说明.md index 00850856..7f1fef92 100644 --- a/docs/子系统设计/网络Socket监听生命周期设计说明.md +++ b/docs/子系统设计/网络Socket监听生命周期设计说明.md @@ -112,7 +112,7 @@ B10 行)→ 中断丢了,bit3=0 同样停在已关/半关态,孤儿 Socket | 契约要求 | 对应缺口 | 收口手段 | |---|---|---| | 「会话结束」必须被**谁都能看见**,不能只在边沿那一刻 | 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) | +| 关闭 Socket 必须**一定有人做**、且只做一次 | 重建所有者 12 个 + U12 的 LISTEN 失败不回 close | 不变量 II:`net_listener_pump` 单点重建 + 补 U12 的 close 对称(P2)(N34 阶段 3:pump 已并入 `net_reconcile_tick` 形态③④,单一 100ms tick——不变量 II 不变,见 §10.10) | | 关闭姿势必须正确 | Trap 14/17:陈旧 DISCONNECT 锁存秒断下一条连接 | `net_close_locked` 既有「close → 等 `CLOSED` → 排空本 Socket 中断」,是 bit3=1 下唯一正确的关闭序列 | | 异常释放须先排空残渣再关 | Trap 23:~2KB RX 残渣被下条连接当自己的数据读走 | `net_relisten_locked` 隔离期(400 ms)持续排空 RX + 主动 close + 等 `CLOSED/TCP_CLOSED` | @@ -149,7 +149,7 @@ B10 行)→ 中断丢了,bit3=0 同样停在已关/半关态,孤儿 Socket - **不变量 I(电平化)**:「这条会话已结束」必须是**可重复查询、幂等**的状态, 而不是"恰好在那一刻观察到才算"。谁先看到不重要,**谁都饿不着**。 - **不变量 II(单一所有者)**:监听器的重建**只有一个决定者**(net 层的 listener pump)。 - 其它所有位置只**置事实**,不**做动作**。 + 其它所有位置只**置事实**,不**做动作**。(N34 阶段 3 起该决定者 = `net_reconcile_tick` 形态③④,pump 已并入,见 §10.10) 两条不变量成立后,"谁重建""会不会错过"这两类问题在结构上消失, 对账④从"唯一救命稻草"降级为"事件丢失时的补漏"(Trap 22 实证事件会丢,故对账**必须保留**)。 @@ -185,7 +185,7 @@ B10 行)→ 中断丢了,bit3=0 同样停在已关/半关态,孤儿 Socket ### P2 单一所有者(中改,消灭多所有者)—— P2a ✅ 2026-10-12 / P2b 建议暂缓 1. 新增 `net_listener_pump()`(100 ms 周期),接管**周期性**重建,是唯一做周期性 - `net_relisten_try` 的地方。✅ 已落地 + `net_relisten_try` 的地方。✅ 已落地(N34 阶段 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`。 @@ -333,6 +333,7 @@ RAM 预算台账 `.md`,属另一在途任务)。这两个文件在主工作 **P2a(✅ 2026-10-12 落地,提交 `7cb5af0`)** - [x] 新增 `net_listener_pump()`(100 ms 周期 + 去抖 + 冷却),接管**周期性**重建, 覆盖:① LISTENING 失位 ② CLOSED 不在 LISTEN ③ CLOSED 但芯片**正在** LISTEN(U11) + (N34 阶段 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`),不发任何芯片命令 @@ -346,6 +347,9 @@ RAM 预算台账 `.md`,属另一在途任务)。这两个文件在主工作 同样存在(P1 基线同条件 53 条),忙等的危害是**理论性**的;而 `net_close` 是每次会话 结束都走的热路径。按 Trap 25「不留未证明的复杂度」,为一个未发生的故障去动热路径, 性价比存疑 —— 建议等有 `task stall` 与忙等的**因果证据**后再做。 + (**N34 阶段 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` @@ -370,7 +374,7 @@ RAM 预算台账 `.md`,属另一在途任务)。这两个文件在主工作 > **动机**:真机 net 层 `net_socket.c` 4126 行,而模拟器做同样的事只有 807 行(5 倍); > `relisten` 一词在文件里出现 110 次。用户诉求 = **高内聚低耦合**。 > P2a 把「周期性重建」收敛到 `net_listener_pump`(所有者 7→5)之后,搬移边界才**第一次清晰**—— -> 这正是 P2a 相对"直接抽模块"的价值:先收敛,再搬移。 +> 这正是 P2a 相对"直接抽模块"的价值:先收敛,再搬移。(N34 阶段 3 起 pump 已并入 `net_reconcile_tick` 形态③④,见 §10.10) > > 本节只定**边界与接口**,不含任何代码;执行须过 §10.6 的验证门。 @@ -397,6 +401,8 @@ RAM 预算台账 `.md`,属另一在途任务)。这两个文件在主工作 | 常量 | **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 | +> ⚠ **N34 阶段 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。 + **跨边界引用点**(这个数字决定接口面大小,是本设计的核心依据): | 引用 | 处数 | 位置 | @@ -416,7 +422,7 @@ RAM 预算台账 `.md`,属另一在途任务)。这两个文件在主工作 | 搬入 | 理由 | |---|---| | `net_relisten_locked` / `net_relisten_try` / `net_relisten_retry_tick` / `net_relisten_clear` / `net_relisten_done` | 「维持 fd 处于 LISTEN 态」的全部机制 | -| `net_listener_pump` | 唯一的周期性重建决策者(P2a 成果) | +| `net_listener_pump` | 唯一的周期性重建决策者(P2a 成果)(N34 阶段 3 已并入 `net_reconcile_tick` 形态③④,单一 100ms tick,见 §10.10) | | `net_drain_chip_rx` | 重建前的排空(只被 relisten 调用) | | 7 个 `s_relisten_*` + 8 个 `NET_RELISTEN_*` / `PUMP_PERIOD` 常量 | 状态与策略私有化 | @@ -429,7 +435,7 @@ RAM 预算台账 `.md`,属另一在途任务)。这两个文件在主工作 | `net_lost_connect_probe` | 做的是"补 accept"(连接建立),不是重建监听 | | `net_listener_armed` | **双端公开 API**(sim 也实现);纯 RAM 查询,很薄,留在原处避免 sim 缺符号 | | ~~`net_resync_listen`~~ | **N34 阶段1 已删除**:其"只改本层状态"语义并入对账⑤(静默回收形态自动认领,纪律"不发芯片命令"随动作沉淀进 `net_reconcile_tick`),手动 API 面退役 | -| `net_reconcile_tick` | ①②⑤ 是**连接**失同步自愈(③④ 已在 P2a 移交 pump;⑤ 为 N34 阶段1 新增,吸收原 `net_resync_listen` 语义) | +| `net_reconcile_tick` | ①②⑤ 是**连接**失同步自愈(③④ 已在 P2a 移交 pump;⑤ 为 N34 阶段1 新增,吸收原 `net_resync_listen` 语义)(N34 阶段 3:③④/U11 已**回到本 tick**,单一 100ms tick 覆盖①②③④⑤/U11,见 §10.10) | | `net_sock_t` 全部字段 | 数据归 `net_types.h`(双端共享核心),**不随模块走** | > ⚠ `net_sock_t` 的 `last_peer_port` / `lost_conn_seen` 是**跨层字段**:新模块的 @@ -553,3 +559,29 @@ void net_listener_tick(void); 正常开销误报; - 改完文件**必须重新 `git add`** —— pre-commit 校验的是**暂存区快照**,漏 add 会让 「本地已修好」与「提交时仍报错」自相矛盾(本次踩到,白跑两轮)。 + +### 10.10 N34 阶段 3:pump 并入 reconciler(**已执行 2026-10-13**) + +审计(`docs/网络/N34-自愈审计/实施方案.md` §5 项 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 23 RST 10/10 + FIN 5/5、Trap 25 n30 11/11、iec104 18/18、`ftp_regression` 11/11、E-02 双 PASS; +netTask 静态最坏链 1944 B 不变(链尾改经 `net_listener_retry_tick`→`net_relisten_locked`)。 diff --git a/docs/测试说明/CH395F_Test_Guide.md b/docs/测试说明/CH395F_Test_Guide.md index 4be57f86..188c2c2a 100644 --- a/docs/测试说明/CH395F_Test_Guide.md +++ b/docs/测试说明/CH395F_Test_Guide.md @@ -1094,7 +1094,7 @@ IDLE → ACCEPT → [SEND: 分块发 → DONE] / [RECV: 分块收+校验 → DON | **类型** | 功能测试 | | **标题** | TCP 连接空闲超过 KeepAlive 阈值后自动断开 | | **前置条件** | 1. `NET_KEEPALIVE_IDLE_MS=60000` / `INTVL=5000` / `CNT=3`(`net_config.h`)
2. 对端须为**死对端**:PC 连接 8080 后**禁用网卡**制造"链路在、对端静默"(存活对端会 ACK 探测 → 永不 TIMEOUT;Windows 防火墙规则对已建立连接无效,E11 实证)
3. 勿与 Phase 6/9/10/13 混选(recv 互抢数据) | -| **测试步骤** | 1. PC 连接 8080,不发数据
2. 立即禁用 PC 网卡(中间有交换机 → 设备侧 PHY 链路保持)
3. 等待芯片报 TIM_OUT(实测判死窗:连接起 **18~31s**,芯片定时语义与手册公式 75s 不符,见实验方案 E11/E13)
4. 本层 QUARANTINE → pump 重建 → s0 回 LISTEN
5. 恢复网卡,重连 8080 验证端口已恢复 | +| **测试步骤** | 1. PC 连接 8080,不发数据
2. 立即禁用 PC 网卡(中间有交换机 → 设备侧 PHY 链路保持)
3. 等待芯片报 TIM_OUT(实测判死窗:连接起 **18~31s**,芯片定时语义与手册公式 75s 不符,见实验方案 E11/E13)
4. 本层 QUARANTINE → reconciler 形态③④ 重建 → s0 回 LISTEN
5. 恢复网卡,重连 8080 验证端口已恢复 | | **通过标准** | recv 返回 0 且耗时落在 **10s~120s 有界窗**(下限防"存活空闲会话被误杀"——T2 实测存活对端静默 90s 不触发;上限防"判死缺位");s0 回 LISTEN 且可接受新连接。报告后测试自动释放 socket 等新连接再测(一次性,防对已死连接 10ms 节奏刷假 FAIL) | --- diff --git a/docs/网络/BSD_Socket_API_使用指南.md b/docs/网络/BSD_Socket_API_使用指南.md index 400d0414..4b55a3b0 100644 --- a/docs/网络/BSD_Socket_API_使用指南.md +++ b/docs/网络/BSD_Socket_API_使用指南.md @@ -62,8 +62,8 @@ | `net_config.h` | 配置宏定义(最大 Socket 数、超时时间等) | | `net_types.h` | 类型定义(地址结构、Socket 控制块等) | | `net_socket.h` | BSD Socket API 头文件,含 `net_msg_t` 消息类型定义 | -| `net_socket.c` | API 核心实现:消息队列处理与线程安全封装、内部 `_sock()` 版本;另含 L3 pending 化 recv、Trap 22 周期对账(200ms,去抖+冷却,①②⑤) | -| `net_listener.c`(**真机专属**) | **监听生命周期**(N-32 §10 模块化):`net_listener_try` / `net_listener_clear` / `net_listener_retry_tick`(D10 重试 30ms/2s)/ `net_listener_pump`(唯一重建入口,100ms)/ `net_listener_settle`(沉降/隔离 100ms/400ms);内部头,模拟器不编译 | +| `net_socket.c` | API 核心实现:消息队列处理与线程安全封装、内部 `_sock()` 版本;另含 L3 pending 化 recv、Trap 22 周期对账 + 监听重建**合一** reconciler(`net_reconcile_tick`,100ms,分级去抖+冷却,形态①②③④⑤/U11,N34 阶段 3) | +| `net_listener.c`(**真机专属**) | **监听生命周期**(N-32 §10 模块化):`net_listener_try` / `net_listener_clear` / `net_listener_retry_tick`(D10 重试 30ms/2s)/ `net_listener_settle`(沉降/隔离 100ms/400ms);原 `net_listener_pump` 的重建决策已并入 `net_reconcile_tick` 形态③④(N34 阶段 3,单一 100ms tick);内部头,模拟器不编译 | | `net_select.h` | select/poll API 头文件 | | `net_select.c` | select/poll 实现(仅 MCU 端编译) | | `net_socket_sim.c`(`simulator/sim/`) | Winsock2 后端(模拟器端,与 `net_socket.c` 互斥编译),详见 §2.3 | @@ -155,10 +155,10 @@ sequenceDiagram | 规则 | 说明 | |------|------| -| **`netTask` 外(任意应用任务)** | 公共 API:`net_send()` / `net_recv()` / `net_recv_to()` / `net_accept()` / `net_connect()` / `net_close()` / `net_listen()`——请求入队列,netTask 串行执行,调用方阻塞等回执。**自愈无显式 API**(N34 阶段1:`net_relisten()` / `net_resync_listen()` 已退役,静默回收/监听失位由对账⑤与 pump 自动认领,见 §3.6) | +| **`netTask` 外(任意应用任务)** | 公共 API:`net_send()` / `net_recv()` / `net_recv_to()` / `net_accept()` / `net_connect()` / `net_close()` / `net_listen()` / `net_drop_conn()`——请求入队列,netTask 串行执行,调用方阻塞等回执。**自愈无显式 API**(N34 阶段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()` 每轮先跑 **4 个非阻塞 tick**(D10 重监听重试 → L3 挂起 recv 完成 → Trap 22 对账 200ms → N-32 监听器 pump 100ms),再处理队列 | +| **`net_poll()` 和 `net_process_messages()`** | 必须在 `netTask` 主循环调用;`net_process_messages()` 每轮先跑 **3 个非阻塞 tick**(D10 重监听重试 → L3 挂起 recv 完成 → Trap 22 对账+监听重建合一 reconciler 100ms),再处理队列 | ### 3.4 内部 API 说明 @@ -183,20 +183,19 @@ int net_close_sock(net_sock_t *p_sock); ### 3.6 芯片/本层状态对账与重监听自愈(Trap 09 / 22 / 23) -单连接模型下"监听 Socket = 数据通道",多种芯片行为会造成**芯片侧与本层状态失同步**,纯事件(中断)驱动无法自愈。当前版本有**五层自愈**,全在 netTask 内、全非阻塞、不冻结主循环: +单连接模型下"监听 Socket = 数据通道",多种芯片行为会造成**芯片侧与本层状态失同步**,纯事件(中断)驱动无法自愈。当前版本有**四层自愈**(N34 阶段 3:原 200ms 周期对账与 100ms 监听器 pump 合并为**单一 100ms `net_reconcile_tick`**),全在 netTask 内、全非阻塞、不冻结主循环: | 机制 | 触发 | 动作 | 关键参数 | |---|---|---|---| | **`auto_relisten`** | 数据通道正常 EOF / DISCONNECT | 重开 + 重新 LISTEN,回 LISTENING;应用层无需 close/重 listen | `net_listen()` 置位;`net_close()` 清 | | **D10 非阻塞重监听重试** | 重开失败(Trap 09:芯片 close 后需沉降) | 单次尝试失败即登记待重试,每轮到点重试;超时限放弃并记 `relisten give up` | 重试间隔 30ms / 时限 2s(自首次尝试);同 fd 有在途挂起 recv 时跳过(L3 保险,防毁在途连接) | -| **Trap 22 周期对账** | 芯片丢 CONNECT/DISCONNECT 中断(快速复连);或静默回收 Socket(peer FIN,无 DISCONNECT 中断) | 每 200ms 对在用 socket 回读芯片状态,修正**三类**失同步:① 本层 ESTABLISHED 而芯片已断 → 按断开处理;② 本层 LISTENING 而芯片已握手 → 补 accept;⑤ 本层 ESTABLISHED 而芯片已静默回收回 LISTEN(0x05/0x01)→ RESYNC 语义只修本层、不发芯片命令(N34 阶段1 新增,原手动 API `net_resync_listen` 的语义并入;③④ 已移交 pump) | 周期 200ms;**连续 2 次失配**才动手(去抖 ≈400ms)+ 动手后 **1s 冷却**(防"刚 open 未生效"瞬态重建风暴) | -| **N-32 监听器 pump**(**唯一重建入口**) | 监听型 fd 的芯片状态与本层不一致(会话结束待重建 / 本层状态滞后) | 每 100ms 检查监听型 fd:芯片失位即重建(`net_listener_try`);本层 CLOSED 而芯片**正在** LISTEN 时**只做状态重同步**、不发芯片命令 | 周期 100ms;连续 2 次失配(≈200ms)+ 动手后 1s 冷却;其它位置只置事实、不做重建动作 | +| **Trap 22 周期对账 + 监听重建(合一 `net_reconcile_tick`,N34 阶段 3;对账①②⑤ + 原 pump③④/U11)** | 芯片丢 CONNECT/DISCONNECT 中断(快速复连);静默回收 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 语义只修本层、不发芯片命令(N34 阶段1 新增,原手动 API `net_resync_listen` 的语义并入);U11 本层 CLOSED 而芯片**正在** LISTEN → 只做状态重同步、不发芯片命令 | 周期 100ms;**分级去抖**:②=1 轮、③④⑤=2 轮、①=4 轮(M3 让位窗 400ms,**勿改小**)+ 动手后 **1s 冷却**(防"刚 open 未生效"瞬态重建风暴;U11 被动重同步不去抖、不占冷却);其它位置只置事实、不做重建动作 | | **Trap 23 排渣隔离** | 异常释放(芯片 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 时限**,否则监听被放弃) | **行为结论**:监听器恢复从"一次性事件"变为"登记 → 到点重试"过程: - 正常断连:通常一次重开即成;失败时 ~30ms 量级恢复,最迟 ~1s(2s 后放弃,错误日志可见)。恢复窗内该端口拒绝新连接 → 调用方重连逻辑须容忍 ~1s 中断。 -- **快速复连不再"监听死亡"(Trap 22)**:对账保证失同步 ≤200ms 内修正(2026-09-23 生产路径:待机后 UI `:7003` 快速复连 10/10;FTP `:21` 100ms/0ms/600ms 间隔各 50/50;零重建风暴)。 +- **快速复连不再"监听死亡"(Trap 22)**:对账保证失同步按分级去抖在 100~400ms 内修正(②1 轮 / ③④⑤2 轮 / ①4 轮,100ms/轮;2026-09-23 生产路径:待机后 UI `:7003` 快速复连 10/10;FTP `:21` 100ms/0ms/600ms 间隔各 50/50;零重建风暴)。 - **中止复连首上传不再被静默截断(Trap 23)**:RST/FIN 中止后首笔复连上传完整通过(RST 10/10、FIN 5/5,SHA-256 全匹配)。详见 `docs/问题记录/CH395F_Trap_Records.md` Trap 22/23。 --- @@ -248,7 +247,7 @@ int main(void) - 更新所有 Socket 的连接状态(CONNECT / DISCONNECT / RECV / TIMEOUT) - 触发事件回调 - 处理数据接收 - - `net_process_messages()` 每轮先跑 3 个**非阻塞**检查再取队列:D10 重监听重试(断连后监听器恢复)→ L3 挂起 recv 完成检查(阻塞 recv 唤醒,E-02 修复)→ Trap 22 芯片/本层对账(200ms 周期,丢中断/静默回收自愈) + - `net_process_messages()` 每轮先跑 3 个**非阻塞**检查再取队列:D10 重监听重试(断连后监听器恢复)→ L3 挂起 recv 完成检查(阻塞 recv 唤醒,E-02 修复)→ Trap 22 芯片/本层对账 + 监听重建合一 reconciler(100ms 周期,读驱动状态缓存,丢中断/静默回收/监听失位自愈,§3.6) - **监听 Socket 建立**:由就绪屏障后的服务任务(timeSyncTask / ftpTask / uiTask)经队列 API 建立(`net_socket` → `net_bind` → `net_listen`);断连后 net 层自动重监听(§3.6),服务层只需循环 `net_accept`,无需维护重监听逻辑 ```c @@ -876,7 +875,7 @@ int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen); | `addrlen` | 输入输出:地址长度 | | 返回值 | 新连接描述符(单连接模式下 == 监听 `sockfd`),无连接时返回 -1(可循环重试) | -> 单连接模式:监听 Socket 在 CONNECT 后**自身即数据通道**(§5.1)。`net_accept` 失败时若状态为 CLOSED 且 `auto_relisten` 开启,内部触发**非阻塞**重监听(失败交重试轮询,§3.6)。中断丢失也不会让监听死亡——对账在 200ms 内修正"本层 LISTENING 而芯片监听消失"等失同步。 +> 单连接模式:监听 Socket 在 CONNECT 后**自身即数据通道**(§5.1)。`net_accept` 失败时若状态为 CLOSED 且 `auto_relisten` 开启,内部触发**非阻塞**重监听(失败交重试轮询,§3.6)。中断丢失也不会让监听死亡——合一对账(100ms 周期,§3.6)在 ~200ms 内修正"本层 LISTENING 而芯片监听消失"等失同步。 #### net_listener_armed / net_can_read - 就绪门控(**纯 RAM 读,不走消息队列**) @@ -1003,6 +1002,23 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags, int net_close(int sockfd); ``` +释放 Socket 控制块,TCP 先断开连接。⚠ **单连接模型(MCU 端 CH395F)不得对监听 fd 调本 API**——连接与监听同一 fd,close 会连监听一起毁掉(§12.4);EOF 时 net 层已 `auto_relisten`,应用层**不得**在 EOF 时 close 该 fd。"放弃会话但保监听"用下面的 `net_drop_conn()`。 + +#### net_drop_conn - 主动拆会话、保留监听(N34 阶段 3,E-02 收口) + +```c +int net_drop_conn(int sockfd); +``` + +应用**放弃会话**(对时 30s 预算耗尽、协议超时)时主动拆掉当前 TCP 会话并**保留监听**——单连接模型下挂起但 TCP 仍活的客户端会永久占住监听(PC 内核自动 ACK keepalive,芯片僵尸判不死,定案见 `docs/网络/N34-自愈审计/实验方案.md` E13),而应用又不得 `net_close` 监听 fd(毁监听);本 API 补上"拆连接不拆监听"的应用侧形态。 + +- **netTask 侧**:先了结本 fd 挂起 recv(排空/取消 + 必通知),再 `sock_reset` QUARANTINE(FIN + 排渣 + 排空事件 + 等 byte0=00H,Trap 23 异常释放语义,400ms 底);本层收口 CLOSED;`auto_relisten` fd 随即 `net_listener_try` 即时重监听(失败交 D10 重试 + reconciler 形态④) +- **幂等**:无活动会话(非 ESTABLISHED/CONNECTING)直接返 0,不动监听 +- **EOF 路径不要调**:EOF 时 net 层 `auto_relisten` 已接管(§12.4),无需也不应再拆 +- 经消息队列委托 netTask(同其它 `net_*` API,非 netTask 上下文调用);等待期 netTask 有界占用 ~400-600ms +- 模拟器(`net_socket_sim.c`,数据 fd ≠ 监听 fd)实现为关数据连接、监听常驻 +- 台架定案(2026-10-13,E-02):CH395F close **无 RST 语义**——对端内核存活但不 close 时芯片停 FIN_WAIT_2 长尾,监听恢复受限于对端 close;对端配合则 TIME_WAIT 过渡 ~150ms → reconciler 形态④ relisten → 新连接接入。不配合场景兜底 = 芯片 TIMEOUT/keepalive 拆僵尸(E13 定案 30~90s)。详见 `docs/问题记录/CH395F_Trap_Records.md` Trap 07 + #### 自愈与查询 API **net_sock_state — 查询 Socket 状态(只读快照)** @@ -1018,7 +1034,7 @@ int net_sock_state(int sockfd); 两者曾是"应用层手动自愈"入口:`net_resync_listen()` 只修 MCU 侧状态(D14 静默回收),`net_relisten()` 无条件芯片侧 open+listen。现状: - **静默回收**由对账⑤自动认领(§3.6:本层 ESTABLISHED 而芯片 0x05/0x01 → RESYNC 语义,≤400ms 去抖 + 1s 冷却,纪律"不发芯片命令"原样保留在认领动作内)——应用无需判定"连接是否已死",也就消除了"误判活连接调 resync 把活连接标成监听态"的 footgun。 -- **监听重建**由 pump(100ms 周期,唯一重建入口)/ D10(30ms 重试)单一所有者负责——`net_relisten()` 对健康监听器反而是 footgun(无条件 open→USE→close 自毁 ~1s,原注释自认)。 +- **监听重建**由 reconciler 形态③④(100ms 周期,唯一重建入口;N34 阶段 3 起原 pump 已并入 `net_reconcile_tick`)/ D10(30ms 重试)单一所有者负责——`net_relisten()` 对健康监听器反而是 footgun(无条件 open→USE→close 自毁 ~1s,原注释自认)。 生产调用者本就为零(仅板级测试 Phase 13,已改由自动机制接管)。历史实现见 `net_socket.c` git 历史。 @@ -1038,14 +1054,13 @@ Socket 已创建或已绑定时可用;port 为 0(动态分配)时返回已 void net_process_messages(void); ``` -处理其他任务投递的请求(send/recv/connect/accept/close)。每次调用先跑 4 个**非阻塞** tick,再取队列消息(0 超时出队): +处理其他任务投递的请求(send/recv/connect/accept/close/drop_conn)。每次调用先跑 3 个**非阻塞** tick,再取队列消息(0 超时出队): 1. **D10 重监听重试**(`net_listener_retry_tick`):断连后监听器恢复的到点重试(30ms 间隔 / 2s 时限,§3.6) 2. **L3 挂起 recv 完成检查**(`net_pending_tick`):按 ①数据 ②到点(仅显式预算) ③断连 检查每个挂起请求并通知调用方;④ tripwire:挂起超 60s 只 `[ERR]` 留痕一次,不强制了结(N34 阶段 1.5) -3. **Trap 22 芯片/本层对账**(`net_reconcile_tick`):200ms 周期修正**三类**失同步(①②⑤,§3.6) -4. **N-32 监听器 pump**(`net_listener_pump`):100ms 周期,**唯一重建入口**(③④;本层 CLOSED 而芯片在听时只重同步,不发芯片命令,§3.6) +3. **Trap 22 芯片/本层对账 + 监听重建(合一 `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=**无限等待**(N34 阶段 1.5 删除 `NET_RECV_TIMEOUT_MS` 30s 默认预算——超时归调用方)。消息类型仅 6 种(send/recv/close/connect/listen/accept)——自愈类消息 `NET_MSG_RESYNC_LISTEN` / `NET_MSG_RELISTEN` 已随 API 退役删除(N34 阶段1)。 +> `net_msg_t.timeout_ms`:L3 显式预算(`net_recv_to()`),0=**无限等待**(N34 阶段 1.5 删除 `NET_RECV_TIMEOUT_MS` 30s 默认预算——超时归调用方)。消息类型 7 种(send/recv/close/connect/listen/accept/drop_conn)——自愈类消息 `NET_MSG_RESYNC_LISTEN` / `NET_MSG_RELISTEN` 已随 API 退役删除(N34 阶段1)。 #### net_send_sock - 发送数据(内部版本) @@ -1260,7 +1275,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 22 芯片/本层对账(200ms 周期,丢中断/静默回收自愈)。 +- `net_process_messages()`:除串行处理队列请求外,每轮先跑 3 个**非阻塞**检查——D10 重监听重试(断连后监听器恢复)→ L3 挂起 recv 完成检查(阻塞 recv 的唤醒)→ Trap 22 芯片/本层对账 + 监听重建合一 reconciler(100ms 周期,读驱动状态缓存,丢中断/静默回收/监听失位自愈,§3.6)。 建议主循环周期 ≤ 10ms(本项目 1ms)。若 netTask 主循环停摆(断点、任务被挂起),监听器不会恢复、阻塞 recv 不会完成——这是软件侧"监听死机"的最常见原因。 @@ -1316,9 +1331,11 @@ if (len < 0 && net_get_errno() == NET_ERR_WOULDBLOCK) { (2026-09-21 台架实证:`ui_net_sink` 曾在 EOF 时无条件 `net_close`,表现为 7003 答完一帧即断连、 端口随后"连接被拒"。) +**应用主动放弃会话(非 EOF)**:若会话是**应用侧放弃**的(对时 30s 预算耗尽、协议超时)而对端 TCP 仍活,EOF 路径不会发生、挂起客户端会永久占住单连接监听——此时用 `net_drop_conn(fd)`(§10.1)拆会话保监听,**不要** `net_close` 监听 fd。EOF 路径**不要**调它(`auto_relisten` 已接管)。 + **芯片静默回收(D14,2026-09-22 台架实证)**:对端发 FIN 后,芯片可能**自行把 Socket 回收回 LISTEN 且不发 DISCONNECT 中断** → 本层滞留 ESTABLISHED,应用继续往死连接上读写,新连接的字节被旧会话吞掉。 -**无需应用处理(N34 阶段1 起)**:对账⑤自动认领此形态(§3.6:≤400ms 去抖 + 1s 冷却)→ RESYNC 语义只修本层状态 + 取消本 fd 挂起 recv,**不发任何芯片命令**(发 close/重监听会摧毁芯片侧健康监听器 → 连接永拒,run8 实证 170s 拒绝)。等待期间该端口实际可接新连接(芯片侧已 LISTEN),新连接经对账补 accept 被本层感知。原显式 API `net_resync_listen(fd)` 已退役——"应用判定连接已死"这一步本身就是误判源(对活连接调用会把活连接标成监听态)。 +**无需应用处理(N34 阶段1 起)**:对账⑤自动认领此形态(§3.6:2 轮去抖 ≈200ms + 1s 冷却)→ RESYNC 语义只修本层状态 + 取消本 fd 挂起 recv,**不发任何芯片命令**(发 close/重监听会摧毁芯片侧健康监听器 → 连接永拒,run8 实证 170s 拒绝)。等待期间该端口实际可接新连接(芯片侧已 LISTEN),新连接经对账补 accept 被本层感知。原显式 API `net_resync_listen(fd)` 已退役——"应用判定连接已死"这一步本身就是误判源(对活连接调用会把活连接标成监听态)。 ⚠ 切勿用**单次**芯片状态读决定重建:单次读可能落在"刚 open 未生效"瞬态,曾致监听反复重建直至 `relisten give up`(2026-09-23 实测风暴,三服务口全死)——任何重建动作必须去抖(连续确认)+ 冷却(§3.6)。 @@ -1425,11 +1442,11 @@ CH395F 底层驱动返回的状态码(`ch395f_get_cmd_status()` 或 `ch395f_po ### 12.10 监听器自愈行为与沉降/隔离(D10 / Trap 09、Trap 22、Trap 23 / N-32) -**默认行为**(无需应用代码):断连 → `auto_relisten` 重开监听;重开失败 → 30ms 节奏重试(2s 时限);芯片丢中断/静默回收 → 200ms 对账 + 100ms 监听器 pump 自愈;中止复连首上传 → 沉降/隔离。全在 netTask 内,应用只需保证 `net_poll()` + `net_process_messages()` 主循环在跑。 +**默认行为**(无需应用代码):断连 → `auto_relisten` 重开监听;重开失败 → 30ms 节奏重试(2s 时限);芯片丢中断/静默回收/监听失位 → 单一 reconciler 100ms 自愈(分级去抖,N34 阶段 3 起对账与 pump 合一);中止复连首上传 → 沉降/隔离。全在 netTask 内,应用只需保证 `net_poll()` + `net_process_messages()` 主循环在跑。 | 场景 | 修复前症状 | 机制 | 生产验证(2026-09-23) | |---|---|---|---| -| **Trap 22** 快速复连丢中断 → 监听静默死亡 | 待机后 UI `:7003` 快速复连 2/10 且**永久**拒连;FTP `:21` 100ms 间隔自第 29 轮死亡;设备日志**无报错**(状态静默滞留) | 周期对账 `net_reconcile_tick()`(200ms,①②⑤)+ 监听器 pump `net_listener_pump()`(100ms,**唯一重建入口**,N-32)+ 去抖(连续 2 次)+ 冷却(1s);`handle_disconnect_event` 只置沉降事实、不再自行重建 | `:7003` 10/10;`:21` 100ms/0ms/600ms 各 50/50;UI 20/20;FTP 正常 10/10(SHA-256);中止 RST/FIN 5/5 + 5/5;零重建风暴 | +| **Trap 22** 快速复连丢中断 → 监听静默死亡 | 待机后 UI `:7003` 快速复连 2/10 且**永久**拒连;FTP `:21` 100ms 间隔自第 29 轮死亡;设备日志**无报错**(状态静默滞留) | 单一 reconciler `net_reconcile_tick()`(100ms,对账①②⑤ + 重建③④/U11 合一,**唯一重建入口**=形态③④,N34 阶段 3;判据读驱动状态缓存)+ 分级去抖(②1 轮/③④⑤2 轮/①4 轮)+ 冷却(1s);`handle_disconnect_event` 只置沉降事实、不再自行重建 | `:7003` 10/10;`:21` 100ms/0ms/600ms 各 50/50;UI 20/20;FTP 正常 10/10(SHA-256);中止 RST/FIN 5/5 + 5/5;零重建风暴(2026-10-13 合并后复测 `:7003` 50/50、`:21` 三档各 50/50 全绿) | | **Trap 23** 中止后首笔复连上传被静默截断 ≤2048B 并误报 226 成功 | RST/FIN 中止上传 → 紧接着的首笔上传"收"到 ~2KB 残留后判结束回 226 → 客户端以为成功 = **静默数据损坏**(第二笔起正常) | 异常释放(TIMEOUT)预重监听隔离:`net_drain_chip_rx()` 分块排空 RX 残渣 + 主动 close + 排空中断 + 等 CLOSED/TCP_CLOSED(400ms 起 / 800ms 上限 < 2s 时限) | RST 10/10、FIN 5/5 全通过;正常路径 30/30 + 20/20,SHA-256 全匹配 | | **Trap 21**(TC-NET-604)"接收冻结"虚警 | 旧判读脚本把"对端填满 2KB RX 窗口"误判为 STALL/WEDGE | **结论:非缺陷**(芯片 RX 缓冲即窗口,对端填满是常态);判据改为脚本层 STEP + 客户端内容校验(RET SHA-256),虚警工具归档(`MDK-ARM/retracted_bench_20260922/`,README 禁重跑) | 真实 FTP 路径 227 轮全通过(8KB×10 … 1MB×2);并发 UI 流 600/600 CRC;设备零 FAIL/ERROR/WARN | @@ -1441,6 +1458,7 @@ CH395F 底层驱动返回的状态码(`ch395f_get_cmd_status()` 或 `ch395f_po | 日期 | 变更 | |------|------| +| 2026-10-13 | **N34 阶段 3 自愈合并 + E-02 收口**(实施规格 `docs/网络/N34-自愈审计/实施方案.md` §5):`net_listener_pump` **删除**,与 `net_reconcile_tick` 合并为**单一 100ms tick**(覆盖原对账①②⑤ + 原 pump③④/U11;判据读**驱动状态缓存** `ch395f_sock_state_cached`,唯一刷新点 = `net_poll` 每轮全量刷新,E2 实测单次回读 18.1µs;**分级去抖** ②=1 轮 / ③④⑤=2 轮 / ①=4 轮(M3 让位窗 400ms)+ 1s 冷却;§2.2 / §3.3 / §3.6(五层 → **四层**)/ §10.2 / §12.1 / §12.10 同步,主循环自愈 tick 由 4 个回到 **3 个**;常量 `NET_LISTENER_PUMP_PERIOD_MS` / `NET_LISTENER_STABLE_N` / `NET_LISTENER_COOLDOWN_MS` 删除);**忙等统一**——`net_listen_locked` / `net_connect_locked` open 失败重试"close+盲等 500ms"与 `net_close_locked` 500ms 排空循环改走驱动 `sock_reset` 原语(`net_sock_reset_sync`,GRACEFUL 100ms 底),`net_close_locked` 不再读清全局中断寄存器(F-NET-7:全局寄存器唯一读者 = `net_poll`);新增 **`net_drop_conn(sockfd)`**(§10.1,E-02 收口:拆会话**保监听**、幂等、EOF 路径不得调;新消息类型 `NET_MSG_DROP_CONN`;timeSync 30s 预算耗尽路径接入;模拟器实现为关数据连接);`net_poll` 每轮对在用 Socket 步进 `ch395f_sock_reset_poll`(同步包装 600ms 放弃后复位机不搁浅);`net_sock_state_t` 注释声明为"本层会话投影"(芯片事实源 = 驱动缓存,`net_types.h`);台架回归全绿(最终构建 Code=211348,E-02 双 PASS,见 `docs/网络/N34-自愈审计/实验方案.md` E-02 收口台架) | | 2026-10-13 | **N34 阶段 1.5 recv 语义主流化**(规格 `docs/网络/N34-自愈审计/实施方案.md` §3.5):`net_recv()` 默认**无限阻塞**(删 `NET_RECV_TIMEOUT_MS` 30s 默认预算,唤醒源=数据/EOF/close/网络重建 CONNRESET);新增 `net_recv_nb()`(`NET_MSG_DONTWAIT` 路径转正)与 `net_setblocking(fd, on)`(模式挂 socket 控制块属性,`net_sock_t.blocking`,默认阻塞);`net_recv_to()` 保留为**显式预算**便捷入口(`timeout_ms=0` 语义从"默认 30s"改为"无限"故仍拒 INVAL);内部 recv 看门狗降级为 **tripwire**(挂起超 60s 只 `[ERR]` 留痕一次,不再"到点 +1s 强制了结"伪造 TIMEDOUT);⚠ 新增**文档契约**:IWDG `required_mask` 内任务禁止无限阻塞 recv(必须 `net_select` 超时或 `net_recv_to` 显式预算);`ui_net_sink` / `iec104_serv` 迁移 `net_recv_nb`,lftpd / timeSync 维持显式预算;模拟器 `net_socket_sim.c` 镜像三 API(blocking 属性包装层模拟) | | 2026-10-13 | **N34 阶段1 API 面收敛**(审计见 `docs/网络/N34-自愈审计/README.md`):删除手动自愈 API `net_relisten()` / `net_resync_listen()`(生产零调用;后者语义并入对账**⑤**——本层 ESTABLISHED 而芯片 0x05/0x01 静默回收 → RESYNC 只修本层,§3.6 对账由两类扩为三类①②⑤;前者与 pump/D10 重叠且对健康监听器是 footgun)与 `net_can_accept()`(只认 ESTABLISHED 的旧门控挡死丢 CONNECT 探针,监听门控统一 `net_listener_armed()`,§10.1 门控节改写);`NET_MSG_RESYNC_LISTEN` / `NET_MSG_RELISTEN` 消息类型删除;timeSync 会话收尾不再 `net_close`(同 fd 续 accept,监听跨会话常驻);lftpd 应用层阻塞重试退役(就绪屏障已消除 Trap 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 | diff --git a/docs/网络/BSD_Socket_API_函数调用关系.md b/docs/网络/BSD_Socket_API_函数调用关系.md index 24c53a6b..bd8091cb 100644 --- a/docs/网络/BSD_Socket_API_函数调用关系.md +++ b/docs/网络/BSD_Socket_API_函数调用关系.md @@ -14,7 +14,7 @@ ## 1. 一句话架构 -**一切芯片(CH395F)操作收敛到唯一执行者 netTask 串行完成**;其他任务通过"入队消息 + 任务通知"表达请求,结果只经**任务通知值**回传;阻塞等待**不独占 netTask**(挂起登记 + 主循环节拍完成);监听器**常驻**(`auto_relisten` + D10 非阻塞重试 + Trap 22 周期对账 + Trap 23 排渣隔离 + **N-32 监听器 pump(唯一重建入口)**,五层自愈)。 +**一切芯片(CH395F)操作收敛到唯一执行者 netTask 串行完成**;其他任务通过"入队消息 + 任务通知"表达请求,结果只经**任务通知值**回传;阻塞等待**不独占 netTask**(挂起登记 + 主循环节拍完成);监听器**常驻**(`auto_relisten` + D10 非阻塞重试 + **Trap 22 周期对账 + 监听重建合一 `net_reconcile_tick`(唯一重建入口=形态③④,100ms)** + Trap 23 排渣隔离,四层自愈;N34 阶段 3 起原 N-32 监听器 pump 已并入对账)。 --- @@ -22,7 +22,7 @@ | 执行域 | 运行位置 / 节奏 | 允许的操作 | 典型调用者 | |---|---|---|---| -| **netTask 主循环** | 独立 FreeRTOS 任务,1ms/tick | **唯一**可碰芯片(SPI2)的地方;4 个非阻塞 tick(D10 重试 / L3 挂起 / Trap 22 对账 / N-32 pump);队列分派;事件处理器;用户回调 | `net_poll` / `net_process_messages` 及其下游全部 `_locked` / `_tick` / `handle_*` | +| **netTask 主循环** | 独立 FreeRTOS 任务,1ms/tick | **唯一**可碰芯片(SPI2)的地方;3 个非阻塞 tick(D10 重试 / L3 挂起 / Trap 22 对账+监听重建合一 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 独占芯片) | @@ -35,7 +35,7 @@ ```mermaid flowchart TB subgraph APP["应用任务(非 netTask)"] - PUB["经队列公共 API
netListen · netAccept · netConnect · netSend
netRecv · netRecvTo · netClose
netRelisten · netResyncListen"] + PUB["经队列公共 API
netListen · netAccept · netConnect · netSend
netRecv · netRecvTo · netClose · netDropConn"] RAM["直调 · 纯 RAM
netSocket · netSockState · netGetSock
netGetErrno · netSetEventCb · netGetSockname
字节序(netHtons / netInetAddr …)"] SPI["直调 · 直碰 SPI(须芯片唯一上下文)
netBind(UDP) · netSendto · netRecvFrom"] CB["用户事件回调
(在 netTask 上下文执行!)"] @@ -45,20 +45,20 @@ flowchart TB XN["xTaskNotify / xTaskNotifyWait
(结果回传的唯一通道)"] subgraph NT["netTask 主循环 — 1ms/tick;芯片操作唯一执行者"] - NPM["netProcessMessages
先跑 4 个 tick,再排空队列分派"] - NP["netPoll
INT# GPIO 轮询 → 全局中断 → 各 Socket SINT → 每轮一批"] + NPM["netProcessMessages
先跑 3 个 tick,再排空队列分派"] + NP["netPoll
INT# GPIO 轮询 → 全局中断 → 各 Socket SINT → 状态缓存全量刷新 + sock_reset 步进 → 每轮一批"] T1["① netListenerRetryTick
D10 重监听重试:30ms / 2s 时限"] T2["② netPendingTick
L3 挂起 recv:①数据 ②到点(仅显式预算) ③断连 ④tripwire 60s 只留痕"] - T3["③ netReconcileTick
Trap 22 对账:200ms / 去抖 / 冷却(①②⑤)"] - T4["④ netListenerPump
N-32 唯一重建入口:100ms / 去抖 / 冷却"] - DISP["队列分派 switch
SEND · RECV · CLOSE
CONNECT · LISTEN · ACCEPT"] + T3["③ netReconcileTick
Trap 22 对账 + 监听重建合一(N34 阶段 3):100ms / 分级去抖 / 冷却
读驱动状态缓存(①②③④⑤ + U11)"] + DISP["队列分派 switch
SEND · RECV · CLOSE
CONNECT · LISTEN · ACCEPT · DROP_CONN"] subgraph LK["_locked / 内部分派目标(仅 netTask 上下文)"] - LL["netListenLocked
初次监听(3×500ms 阻塞重试)"] - LN["netConnectLocked
3×500ms 重试 + tcpConnect"] + LL["netListenLocked
初次监听(open 重试 3× + sock_reset 排渣)"] + LN["netConnectLocked
open 重试 3×(sock_reset 排渣)+ tcpConnect"] LS["netSendLocked
分块 ≤1KB + sendReady 门控"] LR["netRecvLocked
快路径 / DONTWAIT / 登记挂起"] LA["netAcceptLocked
先 netPoll + 状态判定"] - LC["netCloseLocked
了结挂起 + close + 等 CLOSED + 排空中断"] + LC["netCloseLocked
了结挂起 + cmd_done 快路径 / sock_reset(GRACEFUL)"] + DC["netDropConnLocked
拆会话保监听:sock_reset(QUARANTINE) + 即时重监听"] RLS["netListenerTry → netRelistenLocked
单次尝试 + Trap 23 排渣隔离"] end EV["事件处理器
handleConnect · handleDisconnect
handleRecv · handleSend · handleTimeout"] @@ -70,24 +70,25 @@ flowchart TB PUB -->|入队| MQ MQ --> DISP - DISP --> LL & LN & LS & LR & LA & LC & RLS - NPM --> T1 & T2 & T3 & T4 & NP + DISP --> LL & LN & LS & LR & LA & LC & DC & RLS + NPM --> T1 & T2 & T3 & NP T1 --> RLS T3 -->|"① 已断"| EV T3 -->|"② 补 accept"| EV T3 -->|"⑤ 静默回收:RESYNC 只修本层 + 取消挂起(不发芯片命令,N34 阶段1 吸收原 net_resync_listen 语义)"| RSY["reconcile⑤ RESYNC 动作"] - T4 -->|"①② 芯片失位即重建"| RLS + T3 -->|"③④ 监听失位 / 孤儿监听即重建(原 pump,N34 阶段 3)"| RLS + DC -->|"拆会话后即时重监听"| RLS NP --> EV EV --> FE FE -.-> CB EV -->|"断连/超时:只置事实"| ST - ST -.->|"事实 → 由 pump 统一重建"| T4 + ST -.->|"事实 → 由 reconciler 形态③④ 统一重建"| T3 LR -->|无数据+阻塞| PR PR --> T2 T2 -->|xTaskNotify| XN XN -->|唤醒| PUB NP --> CH - LL & LN & LS & LR & LA & LC & RLS --> CH + LL & LN & LS & LR & LA & LC & DC & RLS --> CH SPI -->|直碰 SPI| CH CH -->|INT# 低电平| NP ``` @@ -95,10 +96,10 @@ flowchart TB **读图要点**: 1. 应用侧只有两种"等":① 入队后 `xTaskNotifyWait` 等回执(同步语义);② 挂起 recv 的等待(同一通道,只是"完成"由 `netPendingTick` 判定,不由 netTask 立即处理)。 -2. `net_process_messages()` 开头的 **4 个 tick 是网络层自愈主干**:没有任何应用消息时,重试 / 挂起完成 / 状态对账 / 监听重建照常运转——"断连后即使没人发消息,监听器也能自己活过来"靠的就是它们。 -3. 事件处理器**只由 `net_poll` 驱动**,例外是 `net_reconcile_tick`(带 2 次去抖 + 1s 冷却)可再入 `handle_disconnect_event` / `handle_connect_event`——对账是丢中断的自愈通道(Trap 22)。**注意职责边界(N-32 P2)**:对账与事件处理器**只置事实**(置 CLOSED / 登记沉降),**重建监听的动作统一由 `net_listener_pump` 做**(本层唯一重建入口)——"谁先看到 DISCONNECT 就谁重建"的竞争在结构上消失。 +2. `net_process_messages()` 开头的 **3 个 tick 是网络层自愈主干**:没有任何应用消息时,重试 / 挂起完成 / 状态对账 + 监听重建照常运转——"断连后即使没人发消息,监听器也能自己活过来"靠的就是它们。 +3. 事件处理器**只由 `net_poll` 驱动**,例外是 `net_reconcile_tick`(分级去抖 + 1s 冷却)可再入 `handle_disconnect_event` / `handle_connect_event`——对账是丢中断的自愈通道(Trap 22)。**注意职责边界(N-32 P2,N34 阶段 3 合并后)**:对账与事件处理器**只置事实**(置 CLOSED / 登记沉降),**重建监听的动作统一由 `net_reconcile_tick` 形态③④ 做**(本层唯一重建入口)——"谁先看到 DISCONNECT 就谁重建"的竞争在结构上消失。 4. 回调 `event_cb` 在 **netTask 上下文**执行:回调内不得阻塞、不得发网络操作。 -5. **模块归属**:`T1`(D10 重试)/ `T4`(pump)/ `RLS` 的 `net_listener_try` / `ST`(settle)在 `net_listener.c`(真机专属、模拟器不编译);其余 tick 与 `_locked` 在 `net_socket.c`。 +5. **模块归属**:`T1`(D10 重试)/ `RLS` 的 `net_listener_try` / `ST`(settle)在 `net_listener.c`(真机专属、模拟器不编译);其余 tick(含合并后的 `T3` reconciler)与 `_locked` 在 `net_socket.c`。 --- @@ -116,8 +117,8 @@ flowchart TB | 函数 | 设计作用 | 主要调用 | 意图 / 注意 | |---|---|---|---| -| `net_poll()` | 事件泵:GPIO 轮询 INT#(低=有待处理)→ `GET_GLOB_INT_STATUS_ALL`(读=清)→ 按位图逐 Socket 读 SINT → 分派 5 类事件 | `HAL_GPIO_ReadPin` → `ch395f_get_glob_int_status_all` → `ch395f_get_sock_int_status` ×n → `handle_connect/disconnect/recv/send/timeout_event` | **RECV 中断是电平触发**:每次调用**只处理一批**(do-while(0)),处理完立即返回,否则主循环(echo/应用逻辑)饿死(Trap 08);SENBUF_FREE 只置 `send_ready` 不记事件 | -| `net_process_messages()` | 队列入口 + 自愈 tick 宿主:每轮**先**跑 4 个非阻塞 tick,再排空队列逐条分派,结果 `xTaskNotify` 回传 | `net_listener_retry_tick` → `net_pending_tick` → `net_reconcile_tick` → `net_listener_pump` → `osMessageQueueGet`(0 超时) ×n → switch 分派 → `xTaskNotify` | ① tick 放在队列**之前**:断连后即使没有任何应用消息,监听器也能自主恢复、挂起 recv 也能完成;② RECV 若返回 `NET_PENDING_REGISTERED`(哨兵 0x70000000)则**不发即时回执**(`do_notify=0`),完成时才通知;③ 失败结果把 `s_net_errno`(具体负错误码)压入通知值——调用方恢复 errno 后对外统一 -1(BSD 语义);④ 通知值**整个 32 位**都是结果(不压位域标签,否则 32768 这类大字节数会溢出为负);⑤ **pump 置于对账之后**:对账①可能刚把会话置 CLOSED,pump 紧接着就能看到并重建 | +| `net_poll()` | 事件泵:GPIO 轮询 INT#(低=有待处理)→ `GET_GLOB_INT_STATUS_ALL`(读=清)→ 按位图逐 Socket 读 SINT → 分派 5 类事件;**每轮末尾**对在用 Socket 全量刷新**驱动状态缓存** + 步进 `ch395f_sock_reset_poll`(N34 阶段 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 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 后形态③④同轮即可重建**(N34 阶段 3 合并,原"pump 置于对账之后"的顺序依赖在单 tick 内自然成立) | ### 4.3 经队列的公共 API 及其 netTask 侧实现 @@ -125,14 +126,15 @@ flowchart TB | 公共 API | 消息 | netTask 侧实现 | 关键芯片命令 | 返回语义 | 设计意图 / 注意 | |---|---|---|---|---|---| -| `net_listen` | `NET_MSG_LISTEN` | `net_listen_locked` | `set_proto_type` + `set_sour_port` + `open_socket`(**3×500ms 阻塞重试**)+ `tcp_listen` + 清全部中断状态 | 0 / -1 | **初次**监听允许阻塞重试(启动期无竞争);成功后置 `auto_relisten=1`——监听常驻的源头;启动前清空各 Socket/全局中断状态,防历史残留误触发事件 | -| `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_listener_pump`(N-32 单一重建入口) | -| `net_connect` | `NET_MSG_CONNECT` | `net_connect_locked` | `set_proto_type` + `open_socket`(3×500ms 重试)+ 清残留中断 + `set_sour_port` / `set_des_ip`(大端数组)/ `set_des_port` + `tcp_connect` | 0 = 请求已发出(**异步**)/ -1 | open **先于**寄存器写入(open 会复位寄存器);open 后清残留 DISCONNECT,防新连接秒断(Trap 14/17);目的 IP 字节序写反 = CONNECT TIMEOUT(Trap 16);返回 0 ≠ 已建立——调用方以 `net_sock_state` 轮询等 ESTABLISHED | +| `net_listen` | `NET_MSG_LISTEN` | `net_listen_locked` | `set_proto_type` + `set_sour_port` + `open_socket`(**open 重试 3×,失败走 `net_sock_reset_sync` 确定性排渣**,N34 阶段 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` 形态③④(N34 阶段 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 14/17);目的 IP 字节序写反 = CONNECT TIMEOUT(Trap 16);返回 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 20:写穿 FIFO = 数据错位);`send_ready` 门控用 `net_poll` 轮询而非 osDelay;短写**必须**告警——静默截断会让对端永久等待剩余字节(2026-09-21 实证:UI 推流应答截断 → 客户端失联) | | `net_recv` | `NET_MSG_RECV`(timeout=0→**无限等待**,N34 阶段 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` 路径转正的具名入口(N34 阶段 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 头阻塞修复配套 + **显式预算**入口(N34 阶段 1.5,等价 Python settimeout,无隐藏默认值):调用方把长等待切成有界片(如剩余预算),**总超时语义不变、单次占用有界**;`timeout_ms=0` 直接拒(0 = 无限等待,与本接口"有界"存在理由相反);超时**只凭 -2 判定、不得依赖 errno**(`s_net_errno` 是跨任务共享全局量,随时被别的任务覆盖) | -| `net_close` | `NET_MSG_CLOSE` | `net_close_locked` | `close_socket` + 等 CLOSED(5ms 节奏 / ≤500ms)+ 排空 Socket/全局中断 | 0 / -1 | **先**了结本 fd 挂起 recv(排空/取消 + 必通知,关不得让等待者悬挂);关后排空残留 DISCONNECT(Trap 14/17:否则下一连接被"刚建立就断开"秒断);控制块整体清零 | +| `net_close` | `NET_MSG_CLOSE` | `net_close_locked` | `ch395f_cmd_done(CLOSE)` 快路径(芯片已 CLOSED → 只清本 Socket 残留事件)/ `net_sock_reset_sync(GRACEFUL)` 确定性排渣(N34 阶段 3) | 0 / -1 | **先**了结本 fd 挂起 recv(排空/取消 + 必通知,关不得让等待者悬挂);排空只碰本 Socket SINT,**不再读清全局中断寄存器**(F-NET-7:全局唯一读者 = `net_poll`);原"close + 500ms 盲等排空循环"已换 sock_reset(内置 LISTEN 态二段 CLOSE 补发,E2 补充发现);控制块整体清零(`last_peer_port` 幽灵基准跨 fd 保留,N-30) | +| `net_drop_conn` | `NET_MSG_DROP_CONN`(N34 阶段 3 新增) | `net_drop_conn_locked` | `net_pending_settle_close` + `net_sock_reset_sync(QUARANTINE)` +(auto_relisten)`net_listener_try` | 0(含幂等)/ -1 | **拆会话保监听**(E-02 收口):应用放弃会话(对时 30s 预算耗尽 / 协议超时)而单连接监听被挂起对端永久占住时的应用侧出口;EOF 路径**不得**调(`auto_relisten` 已接管);非 ESTABLISHED/CONNECTING 直接返 0 不动监听;本层收口 CLOSED 不清 fd(保留 local_port/auto_relisten),监听型 fd 即时重监听、失败交 D10 + reconciler 形态④ | | ~~`net_relisten`~~ | ~~`NET_MSG_RELISTEN`~~ | **N34 阶段1 已删除**(消息类型同删) | — | — | 原"无条件芯片侧重建"与 pump/D10 完全重叠,且对健康监听器是 footgun(open→USE→close 自毁 ~1s);生产零调用。历史实现见 `net_socket.c` git 历史 | | ~~`net_resync_listen`~~ | ~~`NET_MSG_RESYNC_LISTEN`~~ | **N34 阶段1 已删除**(语义并入对账⑤) | — | — | D14 静默回收(芯片自行回监听、不发 DISCONNECT)现由 `net_reconcile_tick` 判据⑤自动认领:本层 ESTABLISHED 且芯片 0x05/0x01 → 只修本层(state→LISTENING + 转登 `last_peer_port` + 清 `lost_conn_seen` + 取消挂起 recv),**不发芯片命令**(纪律原样保留,run8 实证 170s 拒绝);去抖/冷却沿用对账框架 | @@ -173,7 +175,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)调用。**只置事实,动作交 pump/try**;越界 fd 静默忽略 | +| `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 23 沉降门**:带沉降标记的 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` 错误日志(应用可见) | @@ -181,49 +183,54 @@ flowchart TB | `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 23:中止/异常断开时芯片 RX 缓冲残留上一条连接的数据(实测 ~2048B),不排空会被下一条连接当自己的数据读走(静默截断);分块读避免新增静态缓冲(内存预算紧张) | -### 4.7 监听器 pump(N-32 P2 单一重建所有者,`net_listener.c`) +### 4.7 监听重建(原 N-32 监听器 pump;N34 阶段 3 已并入 `net_reconcile_tick` 形态③④/U11) -`net_listener_pump()`(`net_process_messages` 每轮调用;周期 **100ms**): +> **N34 阶段 3(2026-10-13)**:`net_listener_pump()` 已**删除**——其全部三种情形作为**形态③④ + U11** 并入 `net_reconcile_tick`(§4.8,单一 100ms tick,判据读驱动状态缓存)。本节保留"单一重建所有者"的设计口径与情形表;实现位置与参数以 §4.8 为准。 -**设计意图(设计文档 §4 不变量 II)**:其它所有位置**只置事实**(会话结束 / 状态迁移 / 事件派发),**不做重建动作**——于是"谁先看到 DISCONNECT 就谁重建"这类**多所有者竞争在结构上消失**。本函数是**本层唯一**的周期性重建决策者。 +**设计意图(设计文档 §4 不变量 II)**:其它所有位置**只置事实**(会话结束 / 状态迁移 / 事件派发),**不做重建动作**——于是"谁先看到 DISCONNECT 就谁重建"这类**多所有者竞争在结构上消失**。`net_reconcile_tick` 形态③④是**本层唯一**的周期性重建决策者。 -**覆盖三种情形**(认领门槛:在用 + `auto_relisten` + TCP + 已绑端口): +**覆盖三种情形**(认领门槛:在用 + `auto_relisten` + TCP + 已绑端口——原 pump 口径,少了它"本层 CLOSED + 已绑端口"的出向连接会被误判为监听器并被 relisten): | # | 本层状态 / 芯片状态 | 动作 | 来源 | |---|---|---|---| -| ① | 本层 LISTENING 而芯片监听**已失位** | 重建(`net_listener_try`) | 原对账③ | -| ② | 本层 CLOSED(会话已结束)而芯片**不在** LISTEN | 重建(`net_listener_try`) | 原对账④ | -| ③ | 本层 CLOSED 而芯片**正在** LISTEN | **只做状态重同步**(CLOSED→LISTENING),**不发任何芯片命令** | U11 缺口 | +| ③ | 本层 LISTENING 而芯片监听**已失位** | 重建(`net_listener_try`) | 原对账③ / 原 pump① | +| ④ | 本层 CLOSED(会话已结束)而芯片**不在** LISTEN | 重建(`net_listener_try`) | 原对账④ / 原 pump② | +| U11 | 本层 CLOSED 而芯片**正在** LISTEN | **只做状态重同步**(CLOSED→LISTENING),**不发任何芯片命令** | U11 缺口 | -情形③的理由:芯片监听器是**健康**的,重下发 open+listen 会因 `USE` 错误走进关闭分支、把正听着的 Socket 关掉(U12 同源事故);只把本层状态收回 LISTENING 即可(与对账⑤ RESYNC 同一纪律——原 `net_resync_listen`,N34 阶段1 已退役)。 +情形 U11 的理由:芯片监听器是**健康**的,重下发 open+listen 会因 `USE` 错误走进关闭分支、把正听着的 Socket 关掉(U12 同源事故);只把本层状态收回 LISTENING 即可(与对账⑤ RESYNC 同一纪律——原 `net_resync_listen`,N34 阶段1 已退役)。 -**去抖 + 冷却**(沿用原对账③④ 实测结论,取值不变):同一失配需**连续 2 次**(≈200ms)才动手;同一 Socket 动手后**冷却 1s**(防抖成风暴,2026-09-23 实证)。**周期取 100ms**(对账 200ms 的 1/2):比它快、又不至于像 50ms 那样频繁 close/open 与消费者收发撞车(2026-10-12 台架实测 50ms 会把 uiTask 最坏单轮从 172ms 推到 1034ms)。 +**去抖 + 冷却**(③④ 沿用原实测结论,取值不变):同一失配需**连续 2 轮**(`NET_RECON_STABLE_RELISTEN`,≈200ms)才动手;同一 Socket 动手后**冷却 1s**(防抖成风暴,2026-09-23 实证);U11 被动重同步**不去抖、不占冷却**(零风险)。**tick 周期 100ms**(原 pump 周期;合并后对账也从 200ms 提到 100ms):比 200ms 快、又不至于像 50ms 那样频繁 close/open 与消费者收发撞车(2026-10-12 台架实测 50ms 会把 uiTask 最坏单轮从 172ms 推到 1034ms)。 > `session_dead` **不在此清零**(与 `net_relisten_locked` 同一契约:重建监听 ≠ 应用已确认会话结束;清零只发生在 `handle_connect_event`)。 -### 4.8 Trap 22 周期对账(丢中断自愈,`net_socket.c`) +### 4.8 Trap 22 周期对账 + 监听重建(合一 `net_reconcile_tick`,丢中断自愈,`net_socket.c`) -`net_reconcile_tick()`(`net_process_messages` 每轮调用;周期 200ms): +`net_reconcile_tick()`(`net_process_messages` 每轮调用;周期 **100ms**,`NET_RECONCILE_PERIOD_MS`;N34 阶段 3 起含原 pump 形态③④/U11): **问题**:芯片在快速复连场景会**丢 CONNECT/DISCONNECT 中断**(或静默回收 Socket 不发中断);丢事件后纯事件驱动无法自愈——典型:对端连接一直占着单连接监听的"后备"(芯片 TCP 仍 ESTABLISHED),后续 SYN 全被拒,而应用因未收到 DISCONNECT 仍以为有客户端(永不调 accept)。 -**机制**:对每个在用 Socket(**仅 ESTABLISHED / LISTENING**)回读 `GET_SOCKET_STATUS`,判定**两类**失同步并修正: +**机制**:对每个在用 Socket 读**驱动状态缓存**(`ch395f_sock_state_cached`,唯一刷新点 = `net_poll` 每轮全量刷新,E2 实测单次回读 18.1µs;**本 tick 不再自发状态 SPI**,唯一按需 SPI = 形态② 命中时回读对端端口做幽灵甄别),判定**五类**失同步 + 一类被动重同步并修正: | # | 失同步 | 动作 | 复用的实现 | |---|---|---|---| -| ① | 本层 ESTABLISHED 而芯片已无连接(sock/tcp = CLOSED) | 按断开处理 → CLOSED + 回调(应用读到 EOF 后由 pump 重建监听) | `handle_disconnect_event`(内置陈旧事件甄别) | +| ① | 本层 ESTABLISHED 而芯片已无连接(sock/tcp = CLOSED) | 按断开处理 → CLOSED + 回调(应用读到 EOF 后由形态③④重建监听) | `handle_disconnect_event`(内置陈旧事件甄别) | | ② | 本层 LISTENING 而芯片已完成握手(tcp = ESTABLISHED) | 补做 standalone accept 簿记 | `handle_connect_event` | +| ③ | 本层 LISTENING(监听型 fd)而芯片监听失位 | 重建 | `net_listener_try`(原 pump①) | +| ④ | 本层 CLOSED(监听型 fd)而芯片不在 LISTEN | 重建 | `net_listener_try`(原 pump②) | +| ⑤ | 本层 ESTABLISHED 而芯片静默回收回 LISTEN(0x05/0x01,无 DISCONNECT 中断) | RESYNC 只修本层、不发芯片命令 | 原 `net_resync_listen` 语义(N34 阶段1 并入) | +| U11 | 本层 CLOSED(监听型 fd)而芯片**正在** LISTEN | 只重同步 CLOSED→LISTENING,不发芯片命令 | 原 pump③ | -> ⚠ **N-32 P2(2026-10-12)**:原③(本层 LISTENING 丢监听)与④(孤儿监听)已**整体移交 `net_listener_pump`**——本函数内**不再有任何重建动作**(单一所有者),认领门槛相应收窄为 ESTABLISHED/LISTENING(原为驱动④而多认领"CLOSED + auto_relisten + 已绑端口")。 +> ⚠ **N-32 P2(2026-10-12)**:原③(本层 LISTENING 丢监听)与④(孤儿监听)曾**整体移交 `net_listener_pump`**——本函数内**不再有任何重建动作**(单一所有者),认领门槛相应收窄为 ESTABLISHED/LISTENING(原为驱动④而多认领"CLOSED + auto_relisten + 已绑端口")。 +> ⚠ **N34 阶段 3(2026-10-13)**:pump 已删除,③④/U11 **回到本 tick**(单一 100ms tick 覆盖①②③④⑤/U11);"单一所有者"不变量不变——重建决策仍只此一处,认领门槛按原 pump 口径以 `listener_fd` 门恢复。 -**去抖 + 冷却(缺一不可)**:单次状态读可能落在"刚 open/listen 尚未生效"的瞬态——**2026-09-23 实测**:无去抖的 accept 路径自愈把监听反复重建直至 `relisten give up`(三服务口全死、89 行 ERROR)。故:同一失配需**连续 2 次**(≈400ms)才动手;同一 Socket 动手后**冷却 1s**。开销:每 200ms 每在用 Socket 一条状态命令,可忽略。 +**去抖 + 冷却(缺一不可)**:单次状态读可能落在"刚 open/listen 尚未生效"的瞬态——**2026-09-23 实测**:无去抖的 accept 路径自愈把监听反复重建直至 `relisten give up`(三服务口全死、89 行 ERROR)。故**分级去抖**(单位=轮,100ms/轮):②=1 轮(`NET_RECON_STABLE_ACCEPT`,幽灵甄别兜底假阳性);③④⑤=2 轮(`NET_RECON_STABLE_RELISTEN`);①=4 轮(`NET_RECON_STABLE_DEATH`,**勿改小**——F-NET-17 M3 可疑电平让位再入的 400ms 租约,E9a 实证);同一 Socket 动手后**冷却 1s**(`NET_RECONCILE_COOLDOWN_MS`;U11 被动重同步不去抖不占冷却)。开销:每 100ms 每在用 Socket 的状态回读已收拢在 `net_poll`(≈100µs/轮),本 tick 本身零额外 SPI。 ### 4.9 事件处理器(netTask 上下文;由 `net_poll` 驱动,对账 tick 可再入) | 函数 | 设计作用 | 主要调用 | 意图 / 注意 | |---|---|---|---| | `handle_connect_event` | LISTENING → standalone accept(回读对端地址 → ESTABLISHED + `send_ready=1` + 清残留 TIMEOUT + 回调);CONNECTING → 客户端连接成功(回读对端 + 使能 KeepAlive + 回调) | `ch395f_get_remot_ipp` + `get_sock_int_status` +(client)`set_keepalive_enable` + `fire_event` | GET_REMOT_IPP 为**小端**输出(与 set_des_ip 的大端相反)——解析别抄反;`session_dead` 的**唯一清零点** | -| `handle_disconnect_event` | 状态机迁移 + 陈旧事件甄别 + **登记沉降事实**(不再自己重建) | `ch395f_get_socket_status` →(LISTENING:芯片已不可监听 → **`net_listener_settle(GRACEFUL)`**,仍在监听 → 判陈旧丢弃)/(ESTABLISHED:芯片仍 ESTABLISHED → 判陈旧丢弃) | ① **陈旧 DISCONNECT 甄别**(D20):同一 fd"关闭→重监听→再连接"循环复用,上一条连接的 FIN/ACK 可能在 fd 已复用时才上报 → 不甄别则新连接秒断(实测:FTP 同会话第二个数据连接必 425);② 正常释放(FIN)→ **GRACEFUL 沉降 100ms**(只置事实,重建由 `net_listener_pump` 做,N-32 P1/P2);③ 置 CLOSED 后**不立即关芯片 Socket**——留给 `net_recv_locked` 排空剩余数据(FIN 可能先于数据到达) | +| `handle_disconnect_event` | 状态机迁移 + 陈旧事件甄别 + **登记沉降事实**(不再自己重建) | `ch395f_get_socket_status`(事件路径**主动 SPI**,不用缓存——缓存刷新在 net_poll 末尾,事件路径上必然滞后)→(LISTENING:只回读留痕,监听失位交 **reconciler 形态③** 重建;仍在监听 → 判陈旧丢弃)/(ESTABLISHED:芯片仍 ESTABLISHED → 判陈旧丢弃;F-NET-17 M3 可疑门 → 置可疑电平让位对账①) | ① **陈旧 DISCONNECT 甄别**(D20):同一 fd"关闭→重监听→再连接"循环复用,上一条连接的 FIN/ACK 可能在 fd 已复用时才上报 → 不甄别则新连接秒断(实测:FTP 同会话第二个数据连接必 425);② 正常释放(FIN)→ **GRACEFUL 沉降 100ms**(只置事实,重建由 `net_reconcile_tick` 形态③④ 做,N-32 P1/P2 + N34 阶段 3);③ 置 CLOSED 后**不立即关芯片 Socket**——留给 `net_recv_locked` 排空剩余数据(FIN 可能先于数据到达) | | `handle_recv_event` | 置 `readable` + 回调;**不读 recv_len** | `fire_event` | 不读长度是为了避免与 `net_recv_locked` 的 `ch395f_get_recv_len` 竞争(Trap 13:分批读会归零);`readable` 供 `net_select` 无 SPI 访问判读 | | `handle_send_event` | 发送缓冲已空:触发回调(`send_ready` 由 SENBUF_FREE 前置位) | `fire_event` | — | | `handle_timeout_event` | CONNECTING → CREATED(连接超时);**ESTABLISHED → 登记隔离 + CLOSED**(Trap 23);LISTENING → 仅记日志 | **`net_listener_settle(fd, NET_LISTENER_SETTLE_QUARANTINE)`** + `fire_event` | TIMEOUT = 异常释放(如客户端中止上传):对端在途数据随后落进 RX 缓冲 → 必须隔离(400ms)后才许复监听;正常 FIN 释放走 disconnect 路径(GRACEFUL 100ms),**不走**隔离 | @@ -303,7 +310,7 @@ sequenceDiagram else TIMEOUT(异常,如中止上传) N->>N: handle_timeout_event:net_listener_settle(QUARANTINE 400ms) + CLOSED end - Note over N: 只置事实 —— 重建统一由 net_listener_pump(100ms)决定 + Note over N: 只置事实 —— 重建统一由 net_reconcile_tick 形态③④(100ms)决定 N->>C: net_listener_try → net_relisten_locked opt 沉降期内(Trap 23) N->>C: 排空 RX 残渣(256B 分块)+ close + 排空中断 @@ -319,7 +326,7 @@ sequenceDiagram Note over N: 2s 超限时 → 清状态 + "relisten give up"(错误日志,应用可感知) ``` -### 5.4 丢中断自愈(Trap 22 对账 + N-32 pump) +### 5.4 丢中断自愈(Trap 22 对账 + 监听重建合一 reconciler,N34 阶段 3) ```mermaid sequenceDiagram @@ -329,14 +336,14 @@ sequenceDiagram Note over C: 快速复连:芯片完成握手但 CONNECT 中断丢失 Note over N: 本层滞留 LISTENING → 应用以为无客户端(永不调 accept) - loop 每 200ms - N->>C: net_reconcile_tick:get_socket_status(每个在用 socket) + loop 每 100ms(net_poll 每轮刷新驱动状态缓存) + N->>N: net_reconcile_tick:读 ch395f_sock_state_cached(每个在用 socket) C-->>N: 失配(本层 LISTENING,芯片 ESTABLISHED) end - Note over N: 连续 2 次失配(去抖 ≈400ms)且不在冷却期 → 动手 + Note over N: 形态② 去抖=1 轮(幽灵甄别挡假阳性;仅命中时回读对端端口,本 tick 唯一按需 SPI) N->>N: handle_connect_event:补 accept 簿记 → ESTABLISHED N-->>A: 下一次 net_accept 返回客户端(连接不再"吞掉") - Note over N: 另两类:① 本层 ESTABLISHED 而芯片已断 → handle_disconnect_event
③④ 本层 LISTENING 丢监听 / CLOSED 孤儿 → net_listener_pump(100ms,唯一重建入口) + Note over N: 其余形态:① 本层 ESTABLISHED 而芯片已断 → handle_disconnect_event(去抖 4 轮)
③④ 本层 LISTENING 丢监听 / CLOSED 孤儿 → 本 tick 形态③④ 重建(唯一重建入口,去抖 2 轮)
⑤ 静默回收 → RESYNC 只修本层;U11 芯片在听 → 只重同步 ``` ### 5.5 网络重建(IP 变更免重启) @@ -362,15 +369,15 @@ sequenceDiagram |---|---|---| | 1 | **一切芯片访问收敛 netTask**(唯一执行者);跨任务调用走队列 | Trap 16:跨任务 SPI 竞争破坏芯片寄存器 → 连接必超时 | | 2 | **结果只经任务通知值回传**;消息按值入队,调用方看不到 netTask 对消息的写入;勿给消息加"回传用"字段 | Trap 10:6 个 API 永远返回 -1;"加 msg.err 字段"的修法曾致 `ui_net_sink` 每次空轮询误判真错误 → 连上即断 | -| 3 | **netTask 不为任何人自旋等待**:阻塞 recv = 挂起登记(L3);重开失败 = 重试登记(D10);初次 listen/connect 的 3×500ms 是唯一豁免(启动期无竞争) | E-02:一次空闲 30s recv 冻住全网 17.7s;旧重监听 osDelay(500) → 恢复 588ms、期间重连被拒 | +| 3 | **netTask 不为任何人自旋等待**:阻塞 recv = 挂起登记(L3);重开失败 = 重试登记(D10);初次 listen/connect 的 open 重试环(`net_sock_reset_sync` 有界等待,GRACEFUL 100ms 底 / 600ms 封顶)是唯一豁免(启动期无竞争,N34 阶段 3 起原 3×500ms 盲等已换确定性排渣) | E-02:一次空闲 30s recv 冻住全网 17.7s;旧重监听 osDelay(500) → 恢复 588ms、期间重连被拒 | | 4 | **netTask 不向自身发消息 / 不自我挂起**(挂起登记对执行任务自身发起直接拒绝) | 自等自通知 = 死锁 | -| 5 | **基于单次状态读的重建动作必须去抖**(连续 2 次)+ 冷却(1s) | 2026-09-23:无去抖 → 监听反复重建直至 `relisten give up`,三服务口全死 | +| 5 | **基于状态读的重建动作必须去抖 + 冷却**:分级门槛 ②=1 轮 / ③④⑤=2 轮 / ①=4 轮(100ms/轮)+ 冷却 1s | 2026-09-23:无去抖 → 监听反复重建直至 `relisten give up`,三服务口全死 | | 6 | **异常释放(TIMEOUT)后复监听前必须隔离**:排空 RX 残渣 + 等 CLOSED/TCP_CLOSED;隔离上限 < 重试放弃时限 | Trap 23:~2KB 残渣被下条连接当自己的数据 → 首笔上传截断 2048B 并误报 226 成功;上限=2s 时隔离耗尽重试预算 → 监听被弃 | | 7 | **RECV 电平触发中断每轮只处理一批** | Trap 08:连续循环 → 主循环饿死 | | 8 | **close 前先了结挂起 recv + 排空残留 DISCONNECT** | Trap 14/17/19:锁存的陈旧 DISCONNECT 秒断下一连接 | | 9 | **两段式等待防丢失通知**:零等待拾取 + 阻塞;清除掩码全 1;以 pdTRUE 判定 | run3~11:丢失通知 → 挂死 / 结果错位;0 结果误判"无通知" → 永久挂死 | | 10 | **网络重建先取消并通知全部挂起,再复位芯片** | 取消不通知 = 调用方永久挂死 | -| 11 | **监听重建只有一个所有者**(`net_listener_pump`);其它位置**只置事实**(置 CLOSED / 登记沉降),不做重建动作 | N-32 P2 / B10:多所有者导致"谁先看到 DISCONNECT 就谁重建"的竞争与重建风暴 | +| 11 | **监听重建只有一个所有者**(`net_reconcile_tick` 形态③④,N34 阶段 3 合并后;原 `net_listener_pump` 已删);其它位置**只置事实**(置 CLOSED / 登记沉降),不做重建动作 | N-32 P2 / B10:多所有者导致"谁先看到 DISCONNECT 就谁重建"的竞争与重建风暴 | --- @@ -385,15 +392,20 @@ sequenceDiagram | `ch395f_set_ip_addr` / `set_gwip_addr` / `set_mask_addr` / `set_dhcp` | `net_init`(**均须在 `ch395f_init` 之前**) | 固定 IP 或 DHCP | | `ch395f_init` / `ch395f_ping_enable` | `net_init` | 协议栈初始化(INIT_CH395)/ PING(须在 init 之后) | | `ch395f_set_send_buf` / `ch395f_set_recv_buf` | `net_init`(8 Socket 布局);`net_bind`(UDP)(Socket 4~7 兜底) | 每 Socket 收 4 块 + 发 2 块 | -| `ch395f_get_glob_int_status_all` | `net_poll`;`net_listen_locked` / `net_connect_locked` / `net_close_locked` / `net_relisten_locked`(清残留) | **读 = 清**(中断清除机制) | +| `ch395f_get_glob_int_status_all` | **仅 `net_poll`**(F-NET-7 / B3:全局寄存器唯一读者;`net_listen_locked` / `net_connect_locked` / `net_close_locked` / `net_relisten_locked` 的原全局读清已全部删除) | **读 = 清**(中断清除机制,无按位手段)——其他位置读清会焊死 net_poll 的按 Socket 门控 | | `ch395f_get_sock_int_status` | `net_poll`(逐 Socket SINT);各 open 路径后清残留;事件处理器清 TIMEOUT | **读 = 清** | -| `ch395f_open_socket` / `ch395f_close_socket` | `net_bind`(UDP)、`net_listen_locked`、`net_connect_locked`、`net_close_locked`、`net_relisten_locked`(含沉降期主动 close) | Trap 09:close 后需沉降才能重开 | +| `ch395f_sock_events` | `net_connect_locked`(open 后清残留,F-NET-7 护栏原语);`net_close_locked`(芯片已 CLOSED 快路径只清本 Socket 残留事件) | 只碰**本 Socket** SINT 的护栏原语 | +| `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_*`(N34 阶段 3) | Trap 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 20) | -| `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_recv_event` **故意不读**(防竞争,Trap 13) | -| `ch395f_get_socket_status` | `net_listener_pump`、`net_reconcile_tick`、`handle_disconnect_event`(两处甄别)、`net_close_locked`(等 CLOSED)、`net_relisten_locked`(沉降门)、`net_connect_locked`(诊断回读) | 输出 **2 字节**([0]=Socket 状态 [1]=TCP 状态),缓冲必须给 2 字节 | -| `ch395f_get_remot_ipp` | `handle_connect_event`、`net_reconcile_tick`(幽灵甄别)、`net_connect_locked`(诊断) | **小端**输出(与 set_des_ip 相反) | +| `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` **故意不读**(防竞争,Trap 13) | +| `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 判据) | N34 阶段 3:芯片状态唯一事实源;E2 实测单次回读 18.1µs,缓存新鲜度 ≤1ms | +| `ch395f_sock_reset_begin` / `ch395f_sock_reset_poll` | `net_sock_reset_sync`(`net_listen_locked` / `net_connect_locked` open 重试、`net_close_locked`、`net_drop_conn_locked`);`net_poll` 每轮对在用 Socket **步进**(IDLE 槽纯 RAM 判零 SPI) | 驱动复位状态机:读空 RX → CLOSE → 排空事件(quiescent) → 等 byte0=00H;内置 LISTEN 态二段 CLOSE 补发(E2 补充发现);GRACEFUL 100ms 底 / QUARANTINE 400ms 底 | +| `ch395f_cmd_done` | `net_close_locked`(CLOSED 快路径判据) | 命令完成判定(OPEN/LISTEN/CLOSE 三形态,读缓存不额外发 SPI) | +| `ch395f_link_up_cached` / `ch395f_link_change_count` | `net_reconcile_tick`(链路态订阅留痕,F-DRV-13) | 驱动 link 变化计数,net_poll 喂 | +| `ch395f_get_remot_ipp` | `handle_connect_event`、`net_reconcile_tick`(幽灵甄别,形态② 命中时唯一按需 SPI)、`net_connect_locked`(诊断) | **小端**输出(与 set_des_ip 相反) | | `ch395f_set_keepalive_enable` | `handle_connect_event`(client 连接成功时) | — | --- @@ -402,20 +414,20 @@ sequenceDiagram | 想改什么 | 先看哪些函数 | |---|---| -| 监听恢复策略(节奏/时限/沉降) | `net_listener_try` / `net_relisten_locked` / `net_listener_retry_tick` / `net_listener_pump`;常量 `NET_RELISTEN_*_MS` / `NET_LISTENER_PUMP_PERIOD_MS` / `NET_LISTENER_STABLE_N` / `NET_LISTENER_COOLDOWN_MS`(`net_listener.c` 内) | +| 监听恢复策略(节奏/时限/沉降) | `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` | -| 丢中断自愈策略 | `net_reconcile_tick`(周期/去抖/冷却三常量,①②)+ `net_listener_pump`(③④,唯一重建入口)+ `handle_disconnect_event` / `handle_connect_event` | +| 丢中断自愈 + 监听重建策略(合一 tick,N34 阶段 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 警构建 + 全测试套件回归 | | 队列容量 / 消息结构 | CubeMX 生成的 `MX_FREERTOS_Init`(8 槽)+ `net_socket.h` 的 `net_msg_t`(**勿加回传字段**) | -| 超时参数(收发/重试/对账) | `net_config.h`(收发/重传/KeepAlive);`net_listener.c` 内 `NET_RELISTEN_*` / `NET_LISTENER_*`;`net_socket.c` 内 `NET_RECONCILE_*` / `NET_PENDING_WATCHDOG_MS` | +| 超时参数(收发/重试/对账) | `net_config.h`(收发/重传/KeepAlive);`net_listener.c` 内 `NET_RELISTEN_*`;`net_socket.c` 内 `NET_RECONCILE_*` / `NET_RECON_STABLE_*` / `NET_PENDING_WATCHDOG_MS` | --- ## 9. 相关文档 - `docs/网络/BSD_Socket_API_使用指南.md` —— 公开 API 语义、示例、常见问题(本文的上半部) -- `docs/子系统设计/网络Socket监听生命周期设计说明.md` —— **监听生命周期权威设计**(N-32:边沿→电平、单一所有者 pump、`net_listener.c` 模块化边界 §10) +- `docs/子系统设计/网络Socket监听生命周期设计说明.md` —— **监听生命周期权威设计**(N-32:边沿→电平、单一所有者重建(N34 阶段 3 起为 `net_reconcile_tick` 形态③④)、`net_listener.c` 模块化边界 §10) - `docs/问题记录/CH395F_Trap_Records.md` —— Trap 01~23 机制细节(本文引用处) - `docs/网络/Net层阻塞模型架构设计.md` —— 阻塞等待模型的现状架构设计(pending 请求模型 / 五路完成语义) - `docs/问题记录/错误码传播审计.md` —— E-02 头阻塞与错误码回传全审计 diff --git a/docs/网络/N34-自愈审计/README.md b/docs/网络/N34-自愈审计/README.md index 90de3c41..d3b96f4b 100644 --- a/docs/网络/N34-自愈审计/README.md +++ b/docs/网络/N34-自愈审计/README.md @@ -65,7 +65,7 @@ T1(net 层视角)判"机制主体保留、API 可删";T3(主流对照视 | `auto_relisten` | net_types.h:172 | **保留**(模型基石) | 芯片永不自动回 LISTEN,监听常驻是软件承诺(P4) | | D10 `net_listener_retry_tick` | net_listener.c:394-432 | **短期保留+小修;长期降级** | 小修=补 pump 同型状态门(F-NET-10)+ give-up 文案(F-NET-14);长期=驱动 `cmd_done()` 后内联进事务、删独立定时器(F-DRV-8) | | Trap 22 `net_reconcile_tick` | net_socket.c:548-641 | **功能保留,形态重构**(阶段 1 已做①扩展=⑤) | 与 pump 判据合并为**单一 reconciler**、读驱动缓存、一套去抖(F-DRV-1);①扩展覆盖"本层 ESTABLISHED+芯片已回 LISTEN"缺口(F-NET-2)✅ 已实施(bad=5,`b8c6a3d`) | -| N-32 `net_listener_pump` | net_listener.c:465-553 | **保留**(内聚的落点,不是补丁) | 唯一周期重建决策者=不变量 II;T3"改内联"与 P2a 台架实测冲突(删消费者驱动→`[WD] task stall`) | +| N-32 `net_listener_pump` | net_listener.c:465-553 | **并入 `net_reconcile_tick`(阶段 3,2026-10-13)** | 唯一周期重建决策者=不变量 II **不变**,只是与对账合一为单一 100ms tick(形态③④/U11,判据读驱动缓存);T3"改内联"与 P2a 台架实测冲突(删消费者驱动→`[WD] task stall`)故消费者三处仍保留 | | Trap 23 排渣隔离 | net_listener.c:102-121/:149-204 | **下沉驱动** | 排空/等 CLOSED 是纯芯片机械;与 net_close_locked:2326-2345 是同一契约两套变体(F-NET-9/F-DRV-4)→ 收口 `ch395f_sock_reset(sn, mode, wait_ms)`,net 层只选 mode | | `net_relisten` | net_socket.c:3080 | **删除** ✅ 已实施(`b8c6a3d`) | 生产零调用;对健康监听器无条件 open→USE→close,自毁 ~1s(footgun) | | `net_resync_listen` | net_socket.c:3026 | **删除**(先并入对账①)✅ 已实施(语义=对账⑤) | 语义正确但载体错位:应是自动对账,不是手动 API(F-NET-2/F-MS-4) | diff --git a/docs/网络/N34-自愈审计/实施方案.md b/docs/网络/N34-自愈审计/实施方案.md index f80a3be3..68e05814 100644 --- a/docs/网络/N34-自愈审计/实施方案.md +++ b/docs/网络/N34-自愈审计/实施方案.md @@ -140,23 +140,67 @@ Trap 23 RST/FIN 5/5+5/5 + @test PASS + 0 错 0 警。**E8 深查修正**(见 验收:新增原语先**不接线**(只加不删),`@build` + 单元级板测(ch395f_test_task 加 TC 用例: 状态回读一致性、reset 状态机跑通、cmd_done 语义)+ `@test` 冒烟。RAM 增量 <64B,跑 `ram_report.py` 确认。 -## 5. 阶段 3 · net 层收敛(门:E1/E6 结果;需完整台架回归) +## 5. 阶段 3 · net 层收敛(门:E1/E6 结果;需完整台架回归)—— **已实施 2026-10-13**(最终构建 Code=211348,回归全绿) 1. **单一 reconciler**:`net_reconcile_tick` + `net_listener_pump` 合并为一个 100ms tick, 判据读**驱动缓存**(不再各自发 SPI),统一一套去抖(连续 2 次)+ 冷却(1s); 覆盖形态:①本层 ESTABLISHED 而芯片 CLOSED/回收 ②本层 LISTENING 而芯片已握手(补 accept) ③监听 fd 芯片失位(重建)④本层 CLOSED 而芯片在听(只重同步)。 消费者即时重建三处保留(P2a 定案)。 + ✅ **已实施(2026-10-13)**:`net_listener_pump` 删除、并入 `net_reconcile_tick`(`NET_RECONCILE_PERIOD_MS=100`); + 判据读 `ch395f_sock_state_cached`(唯一刷新点=`net_poll` 每轮全量刷新,E2 实测单次回读 18.1µs); + 去抖**分级落地**(原计划"统一连续 2 次"按 E2/E9a 数据细化):②=1 轮 / ③④⑤=2 轮 / ①=4 轮(M3 让位窗 400ms)+ 冷却 1s; + 主循环自愈 tick 由 4 个回到 **3 个**;形态② 命中时的对端端口回读是本 tick 唯一按需 SPI。 2. **探针去抖删除**(读缓存无瞬态噪声,F-MS-9);`net_lost_connect_probe` 判据改读缓存。 + ✅ **已实施(2026-10-13)**:探针判据读驱动缓存(零 SPI),`NET_LOST_CONN_STABLE_N` 下线; + 两类瞬态结构性消除("刚 listen 未生效"显示 TCP=00 永不命中握手门,E2:LISTEN 可见 ≤10ms 无长尾; + 拆链窗假 ESTABLISHED 由幽灵甄别拦截),`lost_conn_seen` 退化为 M3 可疑电平标志。 3. **两处忙等统一**:`net_listen_locked` 3×500ms、`net_close_locked` 500ms 改走 `sock_reset_poll`/`cmd_done` 非阻塞路径(P2b 收口)。 + ✅ **已实施(2026-10-13)**:三处(含 `net_connect_locked` open 重试)统一走 `net_sock_reset_sync` + (驱动 `sock_reset` 原语:读空 RX→CLOSE→排空事件(quiescent)→等 byte0=00H,内置 LISTEN 态**二段 CLOSE** 补发, + E2 补充发现;GRACEFUL 100ms 底 / 600ms 封顶);`net_close_locked` 另加 `ch395f_cmd_done(CLOSE)` 快路径, + 并**不再读清全局中断寄存器**(F-NET-7:全局唯一读者=`net_poll`)。 4. **U15 内化**(若 E6 证实频率>0):reconciler 增一条"GET_RECV_LEN>0 但 readable=0 → 置 readable" 低频对账;随后 ui_net_sink.c:552-558、iec104_serv.c:246-253 的 32 轮盲收退役。 + ⏸ **受 E6 门禁暂缓**(E6 未给出"频率>0"的定案数据,按 Trap 25「不留未证明的复杂度」不动消费者盲收; + 待 E6 专项窗口补数据后再裁决)。 5. **镜像降级**:`net_sock_state_t` 保留为展示/门控投影,accept/recv/can_* 决策点改读驱动缓存。 + ✅ **已实施(2026-10-13)**:`net_types.h` 注释把 8 态明确声明为"**本层会话投影**,非芯片状态事实源" + (芯片事实源=驱动缓存,刷新点=net_poll);reconciler / 探针 / U11 重同步等按芯片状态决策的点全部改读缓存, + 事件路径(`handle_*_event`)保留**主动 SPI**(缓存刷新在 net_poll 末尾,事件路径上必然滞后)。 验收:完整台架回归 = Trap 22(:7003 快速复连 10/10、:21 100ms/0ms/600ms 各 50/50)、 Trap 23(RST 10/10 + FIN 5/5,SHA-256)、Trap 25(n30_repro 11/11)、E-02(e02_probe.py)、 104(iec104_master.py)、对时(time_sync_server.py)、`@test` 锚点 + `[STACK]` 水位对比。 +✅ **2026-10-13 全绿**:`@test` PASS;`[STACK]` net peak 1116/3840(70% 余量);Trap 22 `:7003` 50/50 + +`:21` 100ms/0ms/600ms 各 50/50;Trap 23 RST 10/10 + FIN 5/5;Trap 25 n30 11/11;E-02 双 PASS; +iec104 18/18;对时跨会话连续更新;`ftp_regression` 11/11;integrity 15/15 SHA; +`stack_report` netTask 静态链 **1944 B 不变**(链尾改经 `net_listener_retry_tick`→`net_relisten_locked`)。 + +### 5.1 E-02 收口(N34 阶段 3 追加,2026-10-13) + +E-02 的最后一环不是"挂起 recv 不冻结 netTask"(阶段 1.5 已解决),而是**应用放弃会话后单连接监听被挂起对端永久占住**: + +- **新 API `net_drop_conn(sockfd)`**(`NET_MSG_DROP_CONN`):拆会话**保监听**——netTask 侧 + `net_pending_settle_close` 先了结挂起 recv,再 `net_sock_reset_sync(QUARANTINE)`(400ms 底), + 本层收口 CLOSED(不清 fd),`auto_relisten` fd 即时 `net_listener_try`(失败交 D10 + reconciler 形态④); + **幂等**(非 ESTABLISHED/CONNECTING 直接返 0);**EOF 路径不得调**(`auto_relisten` 已接管)。 + timeSync 30s 预算耗尽路径已接入;模拟器(conn≠listen fd)实现为关数据连接。 +- **`net_poll` 每轮步进 `ch395f_sock_reset_poll`**(驱动契约"在 net_poll 步进";IDLE 槽纯 RAM 判零 SPI): + 同步包装 600ms 放弃后复位机不搁浅,对端 FIN 到达即自动走完、槽位可复用。 +- **芯片定案(E-02 台架,2026-10-13)**:CH395F `close` **无 RST 语义**(Trap 07 定案更正已载)—— + 对**内核存活但不 close 的对端**发 FIN 后芯片停 `FIN_WAIT_2`(实测 0x05/0x06 长尾),监听恢复受限于对端 close; + 对端配合则 `TIME_WAIT` 过渡 ~150ms → reconciler 形态④ relisten → 新连接接入。 + 不配合场景兜底 = 芯片 TIMEOUT/KeepAlive 拆僵尸(E13 定案 30~90s)。详见 `docs/问题记录/CH395F_Trap_Records.md` Trap 07 补充。 + +### 5.2 relisten 沉降段统一(延后项,登记不做强行收口) + +`net_relisten_locked` 隔离段仍是**手拼**序列(排空 RX + 主动 close + 读本 Socket SINT + 轮询状态 + 30ms 重试轮), +未统一改走 `sock_reset` 原语。判定:它**不是忙等**(每轮短 SPI + 交重试轮询,netTask 无长阻塞), +且 Trap 23 的沉降等级(GRACEFUL 100ms / QUARANTINE 400ms / 800ms 强制放行 < 2s 时限)是**生产已调优**的时序, +统一改造的回归面覆盖每次断连热路径,收益只是"少一段重复代码"——按 Trap 25 纪律**延后**, +待下一次触碰该热路径的任务顺路处理(`sock_reset` 原语已具备承接条件:二段 CLOSE、quiescent 排空、等级化最短等待)。 ## 6. 横向纪律 diff --git a/docs/网络/N34-自愈审计/实验方案.md b/docs/网络/N34-自愈审计/实验方案.md index 8d76bdf4..567fd8b7 100644 --- a/docs/网络/N34-自愈审计/实验方案.md +++ b/docs/网络/N34-自愈审计/实验方案.md @@ -688,3 +688,34 @@ C 不等价(0/20 + 级联)→ **`ch395f_sock_reset` 内部序列定案**: GRACEFUL 与 QUARANTINE **都必须排 RX**——E4 假设"差异仅在是否排 RX + 最短等待"中 "是否排 RX"一半被否(带脏缓冲关闭在任何释放形态下都危险),两模式差异只剩 最短等待(100ms/400ms,Trap 23 二阶段实测值)。 + +### E-02 收口台架(2026-10-13,阶段 3 产品固件 Code=211348,`test/host/e02_probe.py`) + +**背景**:E13 §5 把 `e02_probe` 的 `listener=FAIL` 重解读为"探针预期与'EOF 不 close'设计冲突", +并指出真实缺口 = **应用层无法主动拆掉自己放弃的会话**。阶段 3 以 `net_drop_conn()` 收口该缺口, +本节记录收口后的台架定案时序。 + +**drop_conn 时序实测**(timeSync 30s 预算耗尽路径,:8888 单连接监听): + +| 阶段 | 观察 | +|---|---| +| 预算耗尽 | `time_sync: no TIME packet in 30s, drop conn` → netTask 侧 `sock_reset QUARANTINE`(FIN + 排渣 + 排空事件 + 等 byte0=00H,400ms 底)+ 本层收口 CLOSED + `auto_relisten` 即时 `net_listener_try` | +| 对端不配合(PC socket 仍开) | 芯片停 **`FIN_WAIT_2`**(byte0=`05H` / byte1=`06H`)长尾——`close` 无 RST(Trap 07 定案更正),监听恢复**受限于对端 close**;复位机在 `net_poll` 步进里等待,不搁浅 | +| 对端配合(探针 close 自己的 socket) | 芯片 `TIME_WAIT` 过渡 ~150ms(TC-NET-204 模式 A 同形)→ byte0 回 `00H` → **reconciler 形态④**(本层 CLOSED + 芯片不在 LISTEN)relisten | +| 恢复 | 对端 close 后 **~500ms** 内 :8888 重新接入(形态④ 去抖 2 轮 = 200ms + TIME_WAIT ~150ms + open/listen);新连接发完整 8 字节 TIME 包 → 设备落 **`wall clock updated`** | +| 探针判定 | `OVERALL: queue=PASS listener=PASS`(双 PASS) | + +**探针口径更新**(`test/host/e02_probe.py`,本轮改): + +1. **模拟守规矩对端**:第 3 步预算耗尽后,探针**主动 close 自己的 socket**(配合设备 FIN)—— + 这是"可恢复边界"的显式模拟;**不配合**(内核存活、自动 ACK FIN、socket 不关)场景**不在探针判据内**, + 其兜底 = 芯片 TIMEOUT/KeepAlive 拆僵尸(E13 定案 30~90s),探针头部已写明该定案口径。 +2. **3s 重试窗**:第二次连接由"单次 5s 尝试"改为 **3s 窗内 0.3s 节奏重试**—— + 恢复本身要经过 TIME_WAIT + 形态④ 去抖(合计 ~500ms 量级),单次即时尝试会把 + "正在恢复"误判成 FAIL;重试窗只放宽**时机**,不放宽"必须重新接入 + 对时落地"的判据。 +3. 顺序修正:close 移到第二次连接**之前**(旧脚本在第二次尝试之后才 close → 内核替挂起 socket + ACK keepalive → 监听被合法占用 → 旧 FAIL 的根因,见 E13 §5)。 + +**结论**:E-02 三段收口齐备——① 挂起 recv 不冻结 netTask(阶段 1.5);② 应用可主动拆会话保监听 +(`net_drop_conn`,幂等、EOF 路径不得调);③ 复位机不搁浅(`net_poll` 步进 `ch395f_sock_reset_poll`)。 +芯片侧物理边界(无 RST、`FIN_WAIT_2` 长尾受限于对端)已登记 `docs/问题记录/CH395F_Trap_Records.md` Trap 07 补充。 diff --git a/docs/网络/Net层阻塞模型架构设计.md b/docs/网络/Net层阻塞模型架构设计.md index 96720d1d..0d8f3db1 100644 --- a/docs/网络/Net层阻塞模型架构设计.md +++ b/docs/网络/Net层阻塞模型架构设计.md @@ -43,7 +43,7 @@ netTask 是**全部 socket 操作的唯一执行者**——CH395F 的 SPI 总线 | 区域 | 内容 | |---|---| | **左栏 · 调用方任务** | `net_recv` → 判"数据已在?" → **是**走快速路径;**否**(`timeout > 0`)登记 pending 后在自己上下文睡任务通知 | -| **中栏 · netTask 主循环** | ~1ms 节拍:`net_poll`(刷新 readable)→ 4 个非阻塞 tick(`net_listener_retry_tick` D10 → `net_pending_tick` 了结挂起 → `net_reconcile_tick` Trap 22 对账 → `net_listener_pump` 监听重建唯一入口)→ 排空消息队列 | +| **中栏 · netTask 主循环** | ~1ms 节拍:`net_poll`(刷新 readable + 全量刷新驱动状态缓存)→ 3 个非阻塞 tick(`net_listener_retry_tick` D10 → `net_pending_tick` 了结挂起 → `net_reconcile_tick` Trap 22 对账 + 监听重建唯一入口,N34 阶段 3 起原 `net_listener_pump` 已并入)→ 排空消息队列 | | **下带 · 五路判定** | 每轮对每个挂起 fd 按序判定:数据到达 / 到点(仅显式预算)/ EOF / 同 fd 后续操作 / 网络重建;tripwire 60s 只留痕(N34 阶段 1.5 由 1s 看门狗降级) | | **回环** | 判定完成经 `xTaskNotify` 唤醒调用方(结果 = 字节数 / 负错误码) | @@ -226,3 +226,4 @@ CONNECT → ESTABLISHED 是合法迁移,推断取消会误杀正在建链的 | 2026-09-29 | **改写为架构设计文档**:以"当前如何设计/为何如此"为主线重组织;历史演进与门禁实测降级为附录 A;迁入 `docs/网络/` | | 2026-10-03 | **网络重建失败恢复(P1-8 / A2)落地并真机验证**:首启组网失败 = 自检 `net init fail` + 30s 周期重试(1s 分片喂心跳,**不自驻留**);重建(reconfig)失败 = 自检 `net reinit fail` + 30s 延迟重触发事件位(tick 回绕安全),成功即清零;PHY 链路超时仍走既有 `comms fault` 降级(语义不变)。真机:开机 `net init: OK` / `PHY: 100M FULL` / `netTask: started`,无 `init FAIL` / `retry in 30s` 误触发(**正常路径零回归**);芯片级 `net_init` 失败分支 / 拔线负向场景待专项窗口(`待处理内容.md` N-16)。**执行判据见 `测试说明/实机测试实施指南.md` §7.1 / §10** | | 2026-10-12 | **N-32 监听生命周期模块化 + 单一所有者**:监听生命周期抽为 `net_listener.c`(`net_listener_try` / `net_listener_clear` / `net_listener_retry_tick` / `net_listener_pump` / `net_listener_settle`,真机专属、模拟器不编译);**重建动作收敛到唯一所有者 `net_listener_pump`**(100ms,去抖+冷却),对账 `net_reconcile_tick` 只剩①②;主循环自愈 tick 由 3 个增至 **4 个**。详见 `docs/子系统设计/网络Socket监听生命周期设计说明.md` §10 | +| 2026-10-13 | **N34 阶段 3:对账与 pump 合并 + 忙等统一 + E-02 收口**(实施规格 `docs/网络/N34-自愈审计/实施方案.md` §5):`net_listener_pump` 删除、并入 `net_reconcile_tick`(单一 100ms tick,形态①②③④⑤+U11,判据读驱动状态缓存,分级去抖 ②1/③④⑤2/①4 轮 + 1s 冷却)——**主循环自愈 tick 由 4 个回到 3 个**,单一重建所有者不变量不变(=形态③④);`net_listen_locked`/`net_connect_locked`/`net_close_locked` 的盲等改 `net_sock_reset_sync` 确定性排渣(GRACEFUL 100ms 底 / 600ms 封顶),close/connect 不再读清全局中断寄存器(F-NET-7);新增 `net_drop_conn()`(拆会话保监听,`NET_MSG_DROP_CONN`)+ `net_poll` 每轮步进 `ch395f_sock_reset_poll`。挂起 recv 模型本身不变(`net_drop_conn` 复用 `net_pending_settle_close` 先了结挂起)。台架回归全绿(Code=211348) | diff --git a/docs/问题记录/CH395F_Trap_Records.md b/docs/问题记录/CH395F_Trap_Records.md index a4852d1a..70fda55a 100644 --- a/docs/问题记录/CH395F_Trap_Records.md +++ b/docs/问题记录/CH395F_Trap_Records.md @@ -181,6 +181,15 @@ Phase 2 测试,2025-07 **与 Trap 17 的张力已消解**:Trap 17 写"close 后芯片要等对端 FIN/ACK 才把 Socket 置 CLOSED(耗时数毫秒)",与本条原"close 发 RST 瞬间回 CLOSED"矛盾;实测支持 Trap 17 一侧(A 模式可见 `TIME_WAIT` 过渡、0~150 ms),不支持 RST 说法。 +### 补充(2026-10-13,E-02 台架):`close` 无 RST 的**边界**——对端内核存活时芯片停 `FIN_WAIT_2` 长尾 + +上条定案("`close` 无 RST 语义")在 E-02 收口实验里暴露出一个必须登记的边界,**不推翻定案、补全其适用条件**: + +- **对端配合**(收到 FIN 后其内核 close):芯片走 `TIME_WAIT` 过渡 ~150 ms → byte0 回 `00H`,reconciler 形态④ relisten → 新连接接入(`e02_probe` 更新后 queue=PASS / listener=PASS,对时"wall clock updated"落地)。 +- **对端内核存活但不 close**(挂起但连接仍活的客户端):本层发 FIN 后芯片停在 **`FIN_WAIT_2`(实测 byte0=0x05 / byte1=0x06)长尾**——没有 RST 就拆不掉,**监听恢复受限于对端 close**。这正是 E-02 场景(timeSync 30s 预算耗尽后 PC 侧 socket 仍活)的芯片侧形态。 +- **不配合场景的兜底** = 芯片自身 TIMEOUT / KeepAlive 拆僵尸会话(E13 定案:判死窗 30~90s,见 `docs/网络/N34-自愈审计/实验方案.md` E13)——**不是**应用可以加速的路径。 +- **应用侧出口 = `net_drop_conn(fd)`**(N34 阶段 3,E-02 收口):netTask 侧走驱动 `sock_reset` **QUARANTINE**(FIN + 排渣 + 排空事件 + 等 byte0=00H,400ms 底)+ 本层收口 CLOSED + `auto_relisten` 即时重监听;**保监听不毁**(单连接模型不得 `net_close` 监听 fd)。对端不 close 时复位机等在 `net_poll` 步进里(`ch395f_sock_reset_poll`),对端 FIN 到达即自动走完——**不搁浅**。 + ### 发现时间 Phase 2 TCP Client 测试,2025-07(**日期有误**,实为 2026-07-19;定案更正与 TC-NET-204 实测 2026-10-11) @@ -289,6 +298,16 @@ CH395F 的 PHY link up 后,内部 TCP/IP 协议栈需要额外时间完成初 - 此问题在单连接模式 (`FUN_PARA=0x00`) 和多连接模式 (`0x02`) 下均存在,与 FUN_PARA 无关 - 如果多个应用任务同时启动 listen,可能全部首次失败、全部重试成功——建议各任务错峰启动 +### 补充(2026-10-13,E2 台架):LISTEN 态 Socket 的**二段关闭**——一次 CLOSE 只拆监听,二次 CLOSE 才回 CLOSED + +E2/E4 芯片实验(`docs/网络/N34-自愈审计/实验方案.md`)补充发现,属本条"close 后需沉降才能重开"的**前置形态**: + +- 对处于 **LISTEN**(byte0=`05H` / byte1=`01H`)的 Socket 发 `CLOSE_SOCKET`:命令返 **SUCCESS**,但芯片**只拆监听**(byte1 `01H→00H`),**byte0 恒 `05H`**——单次 CLOSE 后按"等 byte0=00H"的判据会永远等不到。 +- **第二次 CLOSE** 才把 byte0 拉到 `00H`(Socket 关闭)。即 LISTEN 态的关闭是**两段**:拆监听 → 关 Socket。 +- **驱动侧已内置补发**:`ch395f_sock_reset_*` 状态机的 `WAIT_CLOSE` 段检测到"TCP 已 00H 而 byte0 仍 05H"自动补发第二次 CLOSE(契约见 `Drivers/BSP/CH395F/ch395f.h`)。 +- **生产路径由 Trap 09 的 open 重试环吸收**:`net_listen_locked` / `net_connect_locked` open 失败 → `net_sock_reset_sync`(确定性排渣,内置二段 CLOSE)→ 下一轮重试 open;E4 定案 60 轮实证"复位后 open 必成"。 +- ⚠ 任何**新写的**"close 后等 CLOSED"逻辑都必须走驱动复位原语,**不要**手拼"一次 CLOSE + 轮询 byte0"——那会在 LISTEN 态上死等(本条补充的直接动因)。 + --- ## Trap 10:消息队列值拷贝导致结果无法回传 From 123d80afc77f1f0ba5eb25dc7f7e8f153570ebbf Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 13 Oct 2026 15:15:22 +0800 Subject: [PATCH 20/24] =?UTF-8?q?feat(N34):=20E6=20=E6=8F=92=E6=A1=A9=20NE?= =?UTF-8?q?T=5FU15=5FPROBE=EF=BC=88U15=20=E5=A4=B1=E9=85=8D=E9=A2=91?= =?UTF-8?q?=E7=8E=87=EF=BC=8C=E9=BB=98=E8=AE=A4=E5=85=B3=E9=9B=B6=E6=88=90?= =?UTF-8?q?=E6=9C=AC=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实验方案 docs/网络/N34-自愈审计/实验方案.md §E6:验证「RX 非空但 readable=0」(RECV_OK 中断丢失的数据面形态)是否在生产真实发生, 门禁阶段 3 项 4(U15 内化)做不做。 - net_poll 末尾独立探测环:每 32 轮对在用 fd 发 GET_RECV_LEN, RX>0 且 readable=0 → 计数 + 低频日志(首次必打,此后每小时 ≤1 条) - bring_up 尾部 armed 日志(挂机期间核对烧录身份) - 宏默认 0:产品构建编译剔除,实测体积与上一构建逐字节一致 (Code=211348 RO=18756 RW=1104 ZI=154232) 判据:24h 常态+E1 压测命中>0 → 项 4 立项;=0 → 盲收保留现状、 U15 登记观察项。 --- Drivers/BSP/NET/net_socket.c | 46 ++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/Drivers/BSP/NET/net_socket.c b/Drivers/BSP/NET/net_socket.c index 78873b19..a2f336c8 100644 --- a/Drivers/BSP/NET/net_socket.c +++ b/Drivers/BSP/NET/net_socket.c @@ -361,6 +361,11 @@ static bool s_mac_is_zero(const uint8_t *p_mac) { s_net_initialized = 1; +#if NET_U15_PROBE + /* E6:确认插桩固件已生效(挂机期间据此核对烧录身份) */ + DBG_INFO("U15 probe armed: period=%u log_per_h=1", (unsigned int)NET_U15_PROBE_PERIOD); +#endif + return 0; } @@ -419,6 +424,21 @@ static void net_bench_cmd_log(char tag, int sock, int detail) { } #endif +/* N34 E6 实验插桩(U15 失配频率,docs/网络/N34-自愈审计/实验方案.md §E6): + * 假设=「RX 非空但 readable=0」(RECV_OK 中断丢失的数据面形态)在生产真实 + * 发生。置 1 烧实验固件:net_poll 每 32 轮对在用 fd 发一条 GET_RECV_LEN, + * 命中计数 + 低频日志(每小时 ≤1 条防刷屏);跑 24h 常态 + E1 压测。 + * 判据:命中 >0 → 阶段 3 项 4(U15 内化)立项;=0(含压测)→ 上层 32 轮 + * 盲收保留现状、U15 登记观察项。**默认 0 = 产品构建零成本(编译剔除)**。 */ +#define NET_U15_PROBE 0 +#if NET_U15_PROBE +#define NET_U15_PROBE_PERIOD 32U /* 每 32 轮 net_poll 探测一次(≈32ms) */ +#define NET_U15_PROBE_LOG_MS 3600000U /* 命中日志每小时至多 1 条 */ +static uint32_t s_u15_round; /* net_poll 轮次计数 */ +static uint32_t s_u15_hits; /* 累计命中次数(判据数据) */ +static uint32_t s_u15_last_log; /* 上次命中日志时刻(tick) */ +#endif + /* * 函数功能:轮询 CH395F 中断状态,更新 Socket 连接状态并处理事件 * 入口参数:无 @@ -555,6 +575,32 @@ int net_poll(void) { } } +#if NET_U15_PROBE + /* E6 探测(宏说明见文件上方):RX 非空而 readable=0 = RECV_OK 中断丢失 + * 的数据面形态。独立于上面的缓存刷新环:GET_RECV_LEN 是独立 SPI。 */ + s_u15_round++; + if ((s_u15_round % NET_U15_PROBE_PERIOD) == 0U) { + for (int i = 0; i < NET_MAX_SOCKETS; i++) { + uint16_t rlen; + + if (s_net_socks[i].in_use == 0U) { + continue; + } + rlen = ch395f_get_recv_len((uint8_t)i); + if ((rlen > 0U) && (s_net_socks[i].readable == 0U)) { + s_u15_hits++; + /* 首次命中必打(last_log=0 会被 uptime<1h 抑制);此后每小时 ≤1 条 */ + if ((s_u15_hits == 1U) || + ((uint32_t)(HAL_GetTick() - s_u15_last_log) >= NET_U15_PROBE_LOG_MS)) { + s_u15_last_log = HAL_GetTick(); + DBG_INFO("U15 probe: sock%d rx=%u readable=0 hits=%lu", + i, (unsigned int)rlen, (unsigned long)s_u15_hits); + } + } + } + } +#endif + /* 返回本次累计发生的事件数量(0 表示无事件) */ return event_count; } From c7459b393f4c6610f7d24cb52ead224dfa4239a8 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 13 Oct 2026 17:12:36 +0800 Subject: [PATCH 21/24] =?UTF-8?q?docs(N34):=20=E7=BD=91=E7=BB=9C=E4=B8=8D?= =?UTF-8?q?=E7=A8=B3=E6=A0=B9=E5=9B=A0=E5=A4=8D=E7=9B=98=E6=80=BB=E8=A7=88?= =?UTF-8?q?=EF=BC=88=E6=AF=94=E5=96=BB=E7=89=88=EF=BC=89+=20=E5=85=A8?= =?UTF-8?q?=E6=99=AF=E5=9B=BE=20SVG=20=E7=94=9F=E6=88=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 网络不稳根因总览.md:七个误会(传达室比喻+机制+证据出处)→ 环↔拆法↔落点对照表 → E13 僵尸会话 Mermaid 时序图 → 一句话总结 - 新增 tools/arch_diagram/gen_net_instability_svg.py(draw-diagram 规范, 纯标准库)与产物 docs/images/网络不稳根因总览.svg(headless Edge 目检两轮) - N34 README:实施状态表补全阶段 1.5/2/3 与 E6 插桩行(原「阶段 2/3 未开始」陈旧口径修正);挂复盘总览为第一入口;§5.56 改紧邻写法 - 门禁:check_doc_links --sections-strict 本批 4 文件通过 --- docs/images/网络不稳根因总览.svg | 155 +++++++++++ docs/网络/N34-自愈审计/README.md | 16 +- docs/网络/N34-自愈审计/网络不稳根因总览.md | 119 ++++++++ tools/arch_diagram/gen_net_instability_svg.py | 260 ++++++++++++++++++ 4 files changed, 546 insertions(+), 4 deletions(-) create mode 100644 docs/images/网络不稳根因总览.svg create mode 100644 docs/网络/N34-自愈审计/网络不稳根因总览.md create mode 100644 tools/arch_diagram/gen_net_instability_svg.py diff --git a/docs/images/网络不稳根因总览.svg b/docs/images/网络不稳根因总览.svg new file mode 100644 index 00000000..6d1ec059 --- /dev/null +++ b/docs/images/网络不稳根因总览.svg @@ -0,0 +1,155 @@ + + +为什么网络「一直不稳」——七个误会与逐个拆掉(N34 复盘) +左列 = 根因(比喻 + 机制);右列 = N34 阶段 0~3 的对应拆法;环与环叠加才构成「玄学」 + +① 门铃会丢 +Trap 22:丢中断 + +症状:UI 复连永久拒连、FTP 第 29 轮自己死掉,全程不报一个错 + +机制:快速复连时 INT# 门铃哑火,netTask 只靠门铃干活 → 状态静默滞留 + +100ms 巡逻对账 + +单一 reconciler:判据读驱动状态缓存(E2 背书 18.1µs),分级去抖 + +门铃丢了不要紧,每 100ms 亲自数一遍人头 + + +拆掉 + +② 门铃总闸看一眼就清零 +Trap 25 B3 + +症状:别的 Socket 的 CONNECT/DISCONNECT 从此永久失踪 + +机制:全局中断寄存器整体读即清,open/relisten 丢弃式读一遍=拔全楼门铃线 + +总闸唯一读者 + +F-NET-7:全局寄存器唯一读者=net_poll;open/relisten 只清本 Socket SINT + +查自己门铃时,不许拔别人家的线 + + +拆掉 + +③ 柜台和货梯是同一台 +单连接模型 + +症状:会话 EOF 之后端口「连接被拒」 + +机制:监听 fd == 数据 fd,应用在 EOF 时 net_close = 把柜台连电梯一起拆 + +送客走正门 + +铁律:EOF 不得 close 监听 fd;新 API net_drop_conn(拆会话保柜台) + +net_drop_conn = 优雅 FIN + 本地拆除 + relisten,幂等 + + +拆掉 + +④ 三个临时工都在修电梯 +多所有者 relisten + +症状:重建风暴、慢消费者的 EOF 被吞、新客户端数据被旧会话吸收 + +机制:accept/recv/pending 三处都重建监听,谁先看到断连谁动手互相踩脚 + +修电梯的只剩一个人 + +重建收敛到唯一所有者(net_listener + reconciler),其余位置只置事实不动手 + +不变量 II:唯一周期重建决策者(消费者即时重建三处保留,P2a 实测) + + +拆掉 + +⑤ 传达员被按在椅子上 +队头阻塞 + +症状:FTP banner 迟到 25~30 s(E-02 实测),netTask 频繁冻结半秒 + +机制:30 s 阻塞收 + 500 ms 盲等忙等,而 netTask 是全楼唯一传达员 + +传达员永不离开传达室 + +忙等统一为 net_sock_reset_sync 有界排渣(600 ms 硬上限),阻塞消息化 + +阶段 1.5:net_recv 默认非阻塞 + net_recv_nb + 60 s tripwire 日志 + + +拆掉 + +⑥ 僵尸客人赖着不走 +E13:网卡闪断 + +症状:设备网络「莫名死了」、新连接被拒——在和幽灵做生意 + +机制:PC 网卡闪断,内核悄悄撕掉 socket 不发 FIN,芯片侧仍 ESTABLISHED 占死柜台 + +查岗处理幽灵 + +keepalive 60/5/3(E11 实测 ≈30 s 判死)+ 芯片 TIMEOUT → QUARANTINE → 重建 + +自愈 30~90 s,不需要冷启动(E13 台架收口:用户判断正确) + + +拆掉 + +⑦ 只会礼貌送客不会轰人 +close 无 RST(手册定案) + +症状:FIN_WAIT_2 长尾,监听恢复受限于对端 close(E-02 定案) + +机制:芯片 close 只发 FIN;对端内核存活但应用挂死 = 回 ACK 却不起身 + +知道他不轰人,就别指望他轰人 + +定案写入 Trap 07:兜底靠查岗(keepalive/TIMEOUT),不靠 RST + +net_drop_conn 本地拆除 + reconciler 形态③④ 认领僵尸态 + + +拆掉 + +⑧ 垃圾不清越扫越脏 +残渣风暴态 + +症状:同一份固件故障率 2%~54% 摆动(E8 实测),看起来像「玄学」 + +机制:迟到 DISCONNECT 锁存 / 半死 Socket / 粘滞中断位,被带进下一条连接 + +开张前把垃圾扫干净 + +sock_reset 标准排渣:读空 RX → CLOSE → 排空事件 → 确认落地(byte0=00H) + +M3 矛盾态门 + M5 drain-until-quiescent(风暴态免疫) + + +拆掉 + + + + + + + + + + + + + + +叠加累积 → 越扫越脏 + +七环叠加 =「玄学不稳」 +随机 · 无错误日志 · 时好时坏——每个环节单看都「正常工作」 + +逐环拆掉 = 稳(N34 阶段 0~3 已实施) +不依赖门铃 · 唯一主人 · 永不冻结 · 100ms 对账 · 开张前排渣——台架回归全绿 +证据链:docs/问题记录/CH395F_Trap_Records.md(Trap 07/09/14/17/22/23/25) · docs/网络/N34-自愈审计/实验方案.md(E1~E13) · 修复落地见 docs/网络/N34-自愈审计/实施方案.md + diff --git a/docs/网络/N34-自愈审计/README.md b/docs/网络/N34-自愈审计/README.md index d3b96f4b..762025d2 100644 --- a/docs/网络/N34-自愈审计/README.md +++ b/docs/网络/N34-自愈审计/README.md @@ -10,17 +10,25 @@ ## 实施状态(2026-10-13 更新) +> 想先要"为什么一直不稳"的人话版(比喻 + 全景图):先读 +> `docs/网络/N34-自愈审计/网络不稳根因总览.md`。 + | 提交 | 内容 | 结果 | |---|---|---| | `9ac9879` | P0 止血包(M1+M2+M4-lite) | 台架验证(E7b/E8) | | `ae17d36` | M3 矛盾态可疑门(风暴态免疫,资产门版) | E9/E9b 全绿 | | `4c0a0af` | **阶段 0** 零风险清理 9 条 | E10:0/20 + 20/20 + 20/20 + 11/11 | | `b8c6a3d` | **阶段 1** API 面收口(对账⑤ + 删 3 API + time_sync/lftpd) | E10:全绿回归(详见实验方案 E10) | +| `13a4a40` `d346e4e` | KeepAlive 转正(`docs/芯片手册/CH395F.md` §5.56)+ **阶段 1.5** recv 语义主流化 | E11/E12 台架 | +| `f7f1793` `83a7072` | **阶段 2** ch395f 五原语(只加不接线)+ E2/E3/E4 定案 | Phase 15 套件 20/20 | +| `6394853` `f51c083` | **阶段 3** net 层收敛:单一 reconciler + 忙等统一 + 镜像降级 + E-02 收口(`net_drop_conn`) | 终版回归全绿(Trap 22/23/25、E-02 双 PASS、104、对时、integrity 15/15) | +| `123d80a` | E6 插桩 `NET_U15_PROBE`(默认关,零成本) | 24h 挂机进行中(阶段 3 项 4 门禁) | -阶段 0/1 已完成并转正;阶段 2/3(驱动原语 / net 层收敛)未开始。阶段 1 实施 -过程中的一条关键台架发现已留痕(实验方案 E10):**`net_listener_armed` 必须 -放行 ESTABLISHED**——单连接模型的"待 accept"电平,漏放行时 i104(200ms 周期) -抢不过对账② 的 ~400ms 去抖而整场饿死(18/18 FAIL → 修复后 18/18 PASS,127s→8.3s)。 +阶段 0~3 已完成并转正;在途仅 **E6 挂机**(裁决项 4 U15 内化做不做)与延后项 +(relisten 沉降段统一,见实施方案 §5.2)。阶段 1 实施过程中的一条关键台架发现 +已留痕(实验方案 E10):**`net_listener_armed` 必须放行 ESTABLISHED**——单连接模型 +的"待 accept"电平,漏放行时 i104(200ms 周期)抢不过对账② 的 ~400ms 去抖而整场 +饿死(18/18 FAIL → 修复后 18/18 PASS,127s→8.3s)。 ## 0. 结论先行(直接回答本次审计的问题) diff --git a/docs/网络/N34-自愈审计/网络不稳根因总览.md b/docs/网络/N34-自愈审计/网络不稳根因总览.md new file mode 100644 index 00000000..8f872543 --- /dev/null +++ b/docs/网络/N34-自愈审计/网络不稳根因总览.md @@ -0,0 +1,119 @@ +# 为什么网络「一直不稳」——七个误会与逐个拆掉(N34 复盘总览) + +> 定位:本文是 N34 自愈审计的**人类可读复盘总览**——把散在 Trap 记录、实验台账、 +> 实施方案里的根因,用一个贯穿比喻串成一条因果链,并逐环给出已落地的拆法与证据出处。 +> 技术细节与行号证据一律指向本目录其余文档与 `docs/问题记录/CH395F_Trap_Records.md`, +> 此处不重复。适合新成员第一遍读,也适合对外解释"我们的网络为什么曾经时好时坏"。 + +**一句话回答**:不稳从来不是"网断了",而是**一个会丢门铃、只会礼貌送客的芯片**, +配上一套**靠门铃驱动、人人插手重建、唯一传达员还会被按在椅子上半分钟**的软件—— +每个环节单看都"正常工作",叠起来就是随机、无日志、时好时坏的"玄学"。 + +## 0. 角色表 + +把 CH395F 芯片想成一个**外国籍传达室大爷**:他会收发信件(TCP 栈),但**门铃会坏** +(中断会丢)、**只会礼貌送客不会轰人**(close 无 RST)。netTask 是**全楼唯一能进传达室 +的传达员**(所有 CH395F 事件只在 `net_poll` 集中读清)。FTP、对时、UI 推流、IEC104 +是**楼里等信的住户**(各自监听一个端口,但共用这一个传达员)。 + +## 1. 七个误会(叠加才成为"玄学") + +### ① 门铃会丢(Trap 22:丢中断) + +快速复连时芯片本应"按门铃"(INT# 拉低)通知有新来访/客人离开,但门铃**会哑**—— +人已经站在屋里,铃没响。传达员只靠门铃干活,于是本层状态**静默滞留**:端口其实 +死了,日志里**一个错都不报**。当年"待机后 UI 复连 2/10 永久拒连、FTP 第 29 轮自己 +死掉"就是它——不是断网,是**没人报信**。裸机芯片台架(剥离 OS/net/FTP)独立复现, +证明这是芯片天性,不是固件 bug(见 `docs/网络/N34-自愈审计/README.md` 结论 1)。 + +### ② 门铃总闸是"看一眼就清零"(Trap 25 B3) + +全局中断寄存器是**一整排灯,读一次全部熄灭**。历史上 open/relisten 里有两处 +"丢弃式读一遍"——等于**查自己门铃时把全楼别的住户的门铃线也拔了**,别人的 +CONNECT/DISCONNECT 从此永久失踪。这是"自家制造的丢中断"放大器。 + +### ③ 柜台和货梯是同一台(单连接模型) + +监听 fd 就是数据 fd——**商店柜台平时兼货运电梯**。住户看到"客人走了"(EOF)习惯性 +`net_close` → **把柜台连电梯一起拆了** → 端口"连接被拒"。而芯片侧其实已自动恢复 +(auto_relisten),应用这一刀纯属帮倒忙。注意:单连接复用本身**不是**错误(主流硬 +TCP 栈全部同形),错的是住户不懂模型就动手。 + +### ④ 三个临时工都在修电梯(多所有者 relisten) + +accept、recv、pending 三处都会"顺手重建监听",谁先看到断连谁动手 → 互相踩脚: +重建风暴、慢消费者还没读完的 EOF 被吞、新客户端数据被旧会话静默吸收。修得越多越乱。 + +### ⑤ 传达员被按在椅子上(队头阻塞) + +一个挂起的对时客户端不发数据 → 旧代码 **30 秒阻塞收**;而 netTask 是**全楼唯一 +传达员**——这 30 秒里 FTP 的开门请求、UI 的连接请求全部排队冻结(E-02 实测 FTP +banner 迟到 25~30 s)。各处还有 500 ms 盲等忙等,传达员频繁"冻结半秒"。 + +### ⑥ 僵尸客人赖着不走(E13:网卡闪断) + +PC 网卡闪断一下 → PC 内核**悄悄把自己的连接记录撕了**(不发 FIN),芯片那头还认为 +"客人正在屋里聊天"(ESTABLISHED)。单连接模型下**唯一的柜台被这个不存在的客人永久 +占用**,真客人来了只得到"连接被拒"。当时看起来像"设备网络莫名死了",其实是 +**和幽灵做生意**。 + +### ⑦ 大爷只会礼貌送客,不会轰人(close 无 RST,手册定案) + +芯片的 close 只发 FIN("您请回吧")。对面**内核活着但程序挂死**时——内核礼貌回 +ACK,但客人**就是不起身**——芯片永远站在门口等(FIN_WAIT_2 长尾,E-02 定案:监听 +恢复受限于对端 close)。它**没有"轰出去"(RST)这个动作**——手册全文 RST 只出现在 +listen 占用与多连接两处。这是芯片天性。 + +### ⑧ 垃圾不清,越扫越脏(残渣风暴态) + +上面每一条的残骸(迟到的 DISCONNECT 锁存、半死 Socket、粘滞中断位)都会**带进下 +一条连接**。所以故障率不是常数:残渣少时 2%,堆起来能摆到 54%(E8 实测)——同一份 +固件时好时坏,看起来像"玄学"。 + +## 2. 全景图 + +![七个误会与逐个拆掉](../../images/网络不稳根因总览.svg) + +## 3. 僵尸会话时间线(误会⑥ 的完整一生) + +```mermaid +sequenceDiagram + participant PC as PC(网卡+内核) + participant CH as CH395F(大爷) + participant NT as netTask(传达员) + Note over PC,CH: 会话正常通信(ESTABLISHED) + Note over PC: 网卡闪断(驱动重启/电源管理) + PC--)CH: 内核撕掉 socket,不发 FIN(静默) + Note over CH: 仍认为 ESTABLISHED,唯一柜台被幽灵占用 + Note over NT: 新连接到达 → 连接被拒(端口"死了") + CH->>PC: keepalive 探测(空闲 60s 起,每 5s,共 3 次) + PC--xCH: 无人认领 → 探测无响应 + CH->>NT: TIM_OUT 中断(≈30~90 s) + NT->>CH: QUARANTINE:sock_reset 排渣(读空→CLOSE→排空→确认) + NT->>CH: relisten → LISTEN + Note over NT: 端口恢复,新客人正常进门 +``` + +## 4. 环 → 拆法 → 落点(对照表) + +| 环 | 拆法(已落地) | 证据/落点 | +|---|---|---| +| ① 丢中断 | 单一 reconciler:100 ms 巡逻对账五种形态,判据读驱动状态缓存 | Trap 22;实验方案 E2(回读 18.1 µs 背书);实施方案 阶段 3 项 1 | +| ② 总闸读清 | 全局寄存器唯一读者=net_poll(F-NET-7);open/relisten 只清本 Socket SINT | Trap 25 B3;实施方案 阶段 0/2 | +| ③ 拆柜台 | 铁律"EOF 不得 close 监听 fd" + 新 API `net_drop_conn`(拆会话保监听) | 生命周期设计说明 §10.10;实验方案 E-02 | +| ④ 多所有者 | 重建收敛唯一所有者(net_listener + reconciler),其余只置事实不动手 | README 结论 4(不变量 II);实施方案 阶段 3 项 1 | +| ⑤ 队头阻塞 | 忙等统一 `net_sock_reset_sync` 有界排渣(600 ms 硬顶);recv 非阻塞消息化 | 实验方案 E-02(banner 39 ms);阶段 1.5 | +| ⑥ 僵尸会话 | keepalive 60/5/3 + 芯片 TIMEOUT → QUARANTINE → 重建(30~90 s 自愈) | 实验方案 E11/E13(用户判断"不该依赖冷启动"被证实) | +| ⑦ 无 RST | 定案入 Trap 07:兜底靠查岗不靠 RST;`net_drop_conn` 本地拆除 + 对账③④ 认领 | Trap 07 定案更正;实验方案 E-02 收口 | +| ⑧ 残渣风暴 | `sock_reset` 标准排渣(读空→CLOSE→排空事件→byte0=00H 确认)+ M3 矛盾态门 | Trap 23/25;实施方案 M3/M5(E8/E9 风暴态免疫) | + +## 5. 一句话总结 + +原来的"不稳" = 芯片天性(丢门铃、无 RST)× 软件形态(靠门铃驱动、人人插手、 +唯一传达员会冻结)× 残渣复利(脏态带进下条连接)。N34 做的事不是修某一个 bug, +而是把每一环改成**不依赖门铃(100 ms 对账)、只有一个主人(单所有者)、永远不冻结 +(有界排渣+消息化)、开张前扫垃圾(sock_reset)**——所以现在的"稳"是结构性的。 + +> 遗留观察项:E6 正在挂机量化"RX 非空但 readable=0"(RECV_OK 中断丢失的数据面 +> 形态)在生产是否真实发生(`NET_U15_PROBE`,实验方案 E6 节)——命中>0 则上层 +> 32 轮盲收内化进 net 层(阶段 3 项 4);=0 则保留现状登记观察。 diff --git a/tools/arch_diagram/gen_net_instability_svg.py b/tools/arch_diagram/gen_net_instability_svg.py new file mode 100644 index 00000000..ee775945 --- /dev/null +++ b/tools/arch_diagram/gen_net_instability_svg.py @@ -0,0 +1,260 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""生成 docs/images/网络不稳根因总览.svg —— N34 复盘配图(纯标准库)。 + +布局:左列 = 七个误会(问题环,玫红),右列 = 对应拆法(绿),行间水平箭头; +左列环间竖向细箭头 = "叠加累积";底部两个总结盒。 +为什么不用 Mermaid:双列对齐 + 水平映射箭头需要精确布局。 + +用法(仓库根目录,任意 python3): + python tools/arch_diagram/gen_net_instability_svg.py +输出: + docs/images/网络不稳根因总览.svg +改内容时:编辑本文件顶部的 RINGS / SUMMARY 数据后重跑即可。 +""" +import html +import os + +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +OUT = os.path.join(ROOT, "docs", "images", "网络不稳根因总览.svg") + +# ----------------------------- 画布与尺寸参数 ----------------------------- +W = 1560 +PAD = 26 +TITLE_FONT = 20 +SUB_FONT = 13 +BAND_TITLE_FONT = 15 +BAND_SUB_FONT = 11.5 +CHIP_FONT = 13 + +CHIP_H = 34 # 芯片盒高 +CHIP_GAP = 10 # 芯片水平间距 +CHIP_ROW_GAP = 8 # 芯片行间距 +CHIP_PAD = 14 # 芯片内左右留白 +BAND_PAD_X = 16 # 框内左右留白 +BAND_TITLE_H = 32 # 框内标题区高 +BAND_PAD_BOTTOM = 12 +COL_GAP = 128 # 左右列间距(放映射箭头) +ROW_V_GAP = 40 # 行间距(放"叠加"竖向箭头) +TITLE_H = 78 # 顶部标题区高 +SUM_H = 78 # 底部总结盒高 + +FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" +C_PROB = "#e11d48" # 问题列(玫红) +C_PROB_BG = "#fff1f2" +C_FIX = "#16a34a" # 拆法列(绿) +C_FIX_BG = "#f0fdf4" +C_GRAY = "#94a3b8" + +# ----------------------------- 数据(改这里) ----------------------------- +# (问题标题, 问题副标, [问题芯片], 拆法标题, [拆法芯片]) +RINGS = [ + ("① 门铃会丢", "Trap 22:丢中断", + ["症状:UI 复连永久拒连、FTP 第 29 轮自己死掉,全程不报一个错", + "机制:快速复连时 INT# 门铃哑火,netTask 只靠门铃干活 → 状态静默滞留"], + "100ms 巡逻对账", + ["单一 reconciler:判据读驱动状态缓存(E2 背书 18.1µs),分级去抖", + "门铃丢了不要紧,每 100ms 亲自数一遍人头"]), + ("② 门铃总闸看一眼就清零", "Trap 25 B3", + ["症状:别的 Socket 的 CONNECT/DISCONNECT 从此永久失踪", + "机制:全局中断寄存器整体读即清,open/relisten 丢弃式读一遍=拔全楼门铃线"], + "总闸唯一读者", + ["F-NET-7:全局寄存器唯一读者=net_poll;open/relisten 只清本 Socket SINT", + "查自己门铃时,不许拔别人家的线"]), + ("③ 柜台和货梯是同一台", "单连接模型", + ["症状:会话 EOF 之后端口「连接被拒」", + "机制:监听 fd == 数据 fd,应用在 EOF 时 net_close = 把柜台连电梯一起拆"], + "送客走正门", + ["铁律:EOF 不得 close 监听 fd;新 API net_drop_conn(拆会话保柜台)", + "net_drop_conn = 优雅 FIN + 本地拆除 + relisten,幂等"]), + ("④ 三个临时工都在修电梯", "多所有者 relisten", + ["症状:重建风暴、慢消费者的 EOF 被吞、新客户端数据被旧会话吸收", + "机制:accept/recv/pending 三处都重建监听,谁先看到断连谁动手互相踩脚"], + "修电梯的只剩一个人", + ["重建收敛到唯一所有者(net_listener + reconciler),其余位置只置事实不动手", + "不变量 II:唯一周期重建决策者(消费者即时重建三处保留,P2a 实测)"]), + ("⑤ 传达员被按在椅子上", "队头阻塞", + ["症状:FTP banner 迟到 25~30 s(E-02 实测),netTask 频繁冻结半秒", + "机制:30 s 阻塞收 + 500 ms 盲等忙等,而 netTask 是全楼唯一传达员"], + "传达员永不离开传达室", + ["忙等统一为 net_sock_reset_sync 有界排渣(600 ms 硬上限),阻塞消息化", + "阶段 1.5:net_recv 默认非阻塞 + net_recv_nb + 60 s tripwire 日志"]), + ("⑥ 僵尸客人赖着不走", "E13:网卡闪断", + ["症状:设备网络「莫名死了」、新连接被拒——在和幽灵做生意", + "机制:PC 网卡闪断,内核悄悄撕掉 socket 不发 FIN,芯片侧仍 ESTABLISHED 占死柜台"], + "查岗处理幽灵", + ["keepalive 60/5/3(E11 实测 ≈30 s 判死)+ 芯片 TIMEOUT → QUARANTINE → 重建", + "自愈 30~90 s,不需要冷启动(E13 台架收口:用户判断正确)"]), + ("⑦ 只会礼貌送客不会轰人", "close 无 RST(手册定案)", + ["症状:FIN_WAIT_2 长尾,监听恢复受限于对端 close(E-02 定案)", + "机制:芯片 close 只发 FIN;对端内核存活但应用挂死 = 回 ACK 却不起身"], + "知道他不轰人,就别指望他轰人", + ["定案写入 Trap 07:兜底靠查岗(keepalive/TIMEOUT),不靠 RST", + "net_drop_conn 本地拆除 + reconciler 形态③④ 认领僵尸态"]), + ("⑧ 垃圾不清越扫越脏", "残渣风暴态", + ["症状:同一份固件故障率 2%~54% 摆动(E8 实测),看起来像「玄学」", + "机制:迟到 DISCONNECT 锁存 / 半死 Socket / 粘滞中断位,被带进下一条连接"], + "开张前把垃圾扫干净", + ["sock_reset 标准排渣:读空 RX → CLOSE → 排空事件 → 确认落地(byte0=00H)", + "M3 矛盾态门 + M5 drain-until-quiescent(风暴态免疫)"]), +] + +SUM_LEFT = ["七环叠加 =「玄学不稳」", + "随机 · 无错误日志 · 时好时坏——每个环节单看都「正常工作」"] +SUM_RIGHT = ["逐环拆掉 = 稳(N34 阶段 0~3 已实施)", + "不依赖门铃 · 唯一主人 · 永不冻结 · 100ms 对账 · 开张前排渣——台架回归全绿"] + +FOOT = ("证据链:docs/问题记录/CH395F_Trap_Records.md(Trap 07/09/14/17/22/23/25) · " + "docs/网络/N34-自愈审计/实验方案.md(E1~E13) · 修复落地见 docs/网络/N34-自愈审计/实施方案.md") + + +# ----------------------------- 工具函数 ----------------------------- +def esc(s): + return html.escape(s, quote=True) + + +def text_w(s, fs): + """粗略估算文本像素宽:CJK/全角/圈码/箭头 ≈ 1.0em,其余 ≈ 0.58em(宁大勿小)。""" + w = 0.0 + for ch in s: + w += fs * (1.0 if ord(ch) >= 0x2460 else 0.58) + return w + + +def chip_w(label): + return text_w(label, CHIP_FONT) + 2 * CHIP_PAD + + +def wrap_chips(chips, avail): + rows, cur, cur_w = [], [], 0.0 + for c in chips: + cw = chip_w(c) + need = cw if not cur else cur_w + CHIP_GAP + cw + if cur and need > avail: + rows.append(cur) + cur, cur_w = [c], cw + else: + cur.append(c) + cur_w = need + if cur: + rows.append(cur) + return rows + + +def band_h(rows): + return BAND_TITLE_H + len(rows) * CHIP_H + (len(rows) - 1) * CHIP_ROW_GAP + BAND_PAD_BOTTOM + + +# ----------------------------- 生成 ----------------------------- +def draw_band(out, title, sub, chips, bx, by, bw, bh, color, bg): + add = out.append + add('' % (bx, by, bw, bh, bg, color)) + add('%s' + % (bx + BAND_PAD_X, by + 22, BAND_TITLE_FONT, color, esc(title))) + if sub: + tw = text_w(title, BAND_TITLE_FONT) + add('%s' + % (bx + BAND_PAD_X + tw + 10, by + 22, BAND_SUB_FONT, esc(sub))) + cy = by + BAND_TITLE_H + for row in chips: + cx = bx + BAND_PAD_X + for c in row: + cw = chip_w(c) + add('' % (cx, cy, cw, CHIP_H, color)) + add('%s' + % (cx + cw / 2, cy + CHIP_H / 2 + CHIP_FONT * 0.35, CHIP_FONT, esc(c))) + cx += cw + CHIP_GAP + cy += CHIP_H + CHIP_ROW_GAP + + +def build(): + out = [] + add = out.append + + lw = (W - 2 * PAD - COL_GAP) // 2 + rw = W - 2 * PAD - COL_GAP - lw + avail_l = lw - 2 * BAND_PAD_X + avail_r = rw - 2 * BAND_PAD_X + + # 预排:每行高度 = 左右两框较高者 + placed = [] + y = TITLE_H + for (pt, ps, pc, ft, fc) in RINGS: + lrows = wrap_chips(pc, avail_l) + rrows = wrap_chips(fc, avail_r) + h = max(band_h(lrows), band_h(rrows)) + placed.append((pt, ps, lrows, ft, rrows, y, h)) + y += h + ROW_V_GAP + sum_y = y - ROW_V_GAP + 26 + total_h = sum_y + SUM_H + 46 + + add('' % (W, total_h)) + add('%s' + % (PAD, 40, TITLE_FONT, esc("为什么网络「一直不稳」——七个误会与逐个拆掉(N34 复盘)"))) + add('%s' + % (PAD, 62, SUB_FONT, + esc("左列 = 根因(比喻 + 机制);右列 = N34 阶段 0~3 的对应拆法;环与环叠加才构成「玄学」"))) + + for (pt, ps, lrows, ft, rrows, by, h) in placed: + draw_band(out, pt, ps, lrows, PAD, by, lw, h, C_PROB, C_PROB_BG) + draw_band(out, ft, "", rrows, PAD + lw + COL_GAP, by, rw, h, C_FIX, C_FIX_BG) + # 水平映射箭头(问题 → 拆法) + ay = by + h / 2 + x0 = PAD + lw + 6 + x1 = PAD + lw + COL_GAP - 10 + add('' + % (x0, ay, x1, ay, C_GRAY)) + add('' % (x1, ay, C_GRAY)) + add('%s' + % (PAD + lw + COL_GAP / 2, ay - 8, esc("拆掉"))) + + # 左列环间竖向箭头(叠加累积) + for i in range(len(placed) - 1): + y0 = placed[i][5] + placed[i][6] + 5 + y1 = placed[i + 1][5] - 7 + ax = PAD + 60 + add('' % (ax, y0, ax, y1, C_PROB)) + add('' % (ax, y1, C_PROB)) + add('%s' + % (PAD - 14, placed[3][5] + placed[3][6] / 2, C_PROB, PAD - 14, + placed[3][5] + placed[3][6] / 2, esc("叠加累积 → 越扫越脏"))) + + # 底部总结盒 + add('' % (PAD, sum_y, lw, SUM_H, C_PROB_BG, C_PROB)) + add('%s' + % (PAD + BAND_PAD_X, sum_y + 28, C_PROB, esc(SUM_LEFT[0]))) + add('%s' + % (PAD + BAND_PAD_X, sum_y + 52, esc(SUM_LEFT[1]))) + rx = PAD + lw + COL_GAP + add('' % (rx, sum_y, rw, SUM_H, C_FIX_BG, C_FIX)) + add('%s' + % (rx + BAND_PAD_X, sum_y + 28, C_FIX, esc(SUM_RIGHT[0]))) + add('%s' + % (rx + BAND_PAD_X, sum_y + 52, esc(SUM_RIGHT[1]))) + + add('%s' + % (PAD, total_h - 16, esc(FOOT))) + + svg = ('\n%s\n\n' + % (W, total_h, W, total_h, FONT, "\n".join(out))) + return svg, total_h + + +def main(): + svg, h = build() + os.makedirs(os.path.dirname(OUT), exist_ok=True) + with open(OUT, "w", encoding="utf-8", newline="\n") as f: + f.write(svg) + import xml.etree.ElementTree as ET + ET.fromstring(svg) + print("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, h, OUT)) + + +if __name__ == "__main__": + main() From b445cbb46418d9ef257fedc6ce66eb68af40c223 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 13 Oct 2026 17:12:57 +0800 Subject: [PATCH 22/24] =?UTF-8?q?docs(=E9=97=AE=E9=A2=98=E8=AE=B0=E5=BD=95?= =?UTF-8?q?):=20CH395F=20Trap=5FRecords=20=E9=87=8D=E7=BB=84=E5=AE=A1?= =?UTF-8?q?=E8=AE=A1=E2=80=94=E2=80=94=E5=90=88=E5=B9=B6=E3=80=81=E5=88=A0?= =?UTF-8?q?=E8=AF=AF=E5=88=A4=E3=80=81=E4=B8=BB=E9=A2=98=E5=88=86=E7=AB=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 合并同根因:Trap 14+17(残留 DISCONNECT 污染下一连接)、Trap 13+21 (recv_len 归零/接收冻结家族,非缺陷结案);FTP 改造清单表去重入附录 B - 删当时误判叙述(各留一行误判存档):Trap 07 FIN_WAIT_2/RST 说、 Trap 18 bit0 迷思、Trap 25「芯片杀连接」解读;B1 KeepAlive 三段翻转 合并为定案链(E11 转正) - 规范化:顶部索引表+状态图例;六主题章+附录 A 速查/附录 B 存档; 条目统一「状态/现象/根因(定案)/修复/回归判据/教训」格式 - 状态刷新:B5 对端 IP 显示已修(代码随收尾批提交)、B10 标 net_drop_conn 根治出口;Trap 17/20 措辞、Trap 12 根因(按 ff.c)、日期口径就地更正 - 编号永不重排(代码注释 123 处引用);跨文件断链 1 处改条目名引用 (生命周期设计说明 §877-878 → Trap 25「定案二」) - 体量 1018 行/102KB → 420 行/64KB;doc-links 两文件通过 --- .../网络Socket监听生命周期设计说明.md | 2 +- docs/问题记录/CH395F_Trap_Records.md | 1268 +++++------------ 2 files changed, 336 insertions(+), 934 deletions(-) diff --git a/docs/子系统设计/网络Socket监听生命周期设计说明.md b/docs/子系统设计/网络Socket监听生命周期设计说明.md index 7f1fef92..804aa36d 100644 --- a/docs/子系统设计/网络Socket监听生命周期设计说明.md +++ b/docs/子系统设计/网络Socket监听生命周期设计说明.md @@ -83,7 +83,7 @@ B10 探针看到的"孤儿芯片态"不是芯片异常,而是本仓这个配 实测现场(Trap 25 三次定案的判别探针):`st=0x05/0x00` 且 `recv_len=1027`—— Socket 仍 OPEN、芯片 TCP 已 CLOSED、**RX 缓冲里 STOR 载荷一个不少**。这正是 bit3=1 按规格 -保留现场,不是芯片异常(`docs/问题记录/CH395F_Trap_Records.md` §877-878)。 +保留现场,不是芯片异常(`docs/问题记录/CH395F_Trap_Records.md` Trap 25「定案二」)。 **② bit3=0 的代价是「硬件保证的数据丢失」,且不可在软件层补救。** 本产品最典型的流量形态是「发完即 FIN」:ftplib `storbinary` 语义 = `sendall` 发完立即关 diff --git a/docs/问题记录/CH395F_Trap_Records.md b/docs/问题记录/CH395F_Trap_Records.md index 70fda55a..45ce90a5 100644 --- a/docs/问题记录/CH395F_Trap_Records.md +++ b/docs/问题记录/CH395F_Trap_Records.md @@ -2,957 +2,335 @@ 记录开发过程中遇到的所有坑、根因、解决方案,避免重复踩坑。 ---- +> **2026-10-13 重组审计**:合并同根因条目(14+17、13+21)、删除已被推翻的误判叙述(只留一行误判存档)、 +> 按主题分章并加索引;条目格式统一为 `## Trap NN · 短标题` + 固定小节(状态/现象/根因(定案)/修复/回归判据/教训, +> 视情况取舍)。**编号永不重排**——代码注释大量引用 `Trap NN`,合并条目标题保留双编号; +> Trap 19 从未存在(悬空引用登记在第六章待修清单)。 +> 状态图例:✅ 已修复定案 · ⏳ 待修 · ↩️ 已撤回(非缺陷) · — 从未存在。 -## Trap 01:Socket 4~7 自动分配不到 +## 索引 -### 现象 -多连接模式下最多接受 3 个客户端(Socket 1~3),Socket 4~7 的 CONNECT 中断从不触发,新客户端 SYN 被静默丢弃。 - -### 根因 -`net_listen()` 中数据 Socket 的配置顺序错误。CH395F 在 `TCP_LISTEN` 时刻**一次性扫描**当前所有可用 Socket 并建立内部查找表。如果数据 Socket 4~7 在 `TCP_LISTEN` **之后**才配置缓冲区/协议/端口,自动分配逻辑不认识它们。 - -### 正确顺序 -1. 先配置数据 Socket 1~7:`SET_SEND_BUF → SET_RECV_BUF → SET_SOUR_PORT → SET_PROTO_TYPE_TCP` -2. 最后配置监听 Socket 0:`SET_SEND_BUF → SET_RECV_BUF → SET_PROTO_TYPE_TCP → SET_SOUR_PORT → OPEN_SOCKET → TCP_LISTEN` - -### 注意事项 -- 数据 Socket 不调 `OPEN_SOCKET`,由 CH395F 在连接到达时自动打开 -- DISCONNECT 后 CH395F 自动关闭 Socket,但保留协议/端口/缓冲区配置,可被再次分配 -- `docs/芯片手册/CH395F.md` §9.2.6 要求数据 Socket **先设源端口再设协议类型**(与监听 Socket 顺序相反) - -### 对应文件 -`Drivers/BSP/NET/net_socket.c` → `net_listen()` - -### 发现时间 -Phase 8 压力测试,2025-07 - ---- - -## Trap 02:数据 Socket 缓冲区重叠 - -### 现象 -多连接模式下多个 Socket 数据互相覆盖,收到错误数据或客户端挂死。 - -### 根因 -CH395F 默认只为 Socket 0~3 各分配独立收发缓冲(共 48 块 × 512B 用满,见`docs/芯片手册/CH395F.md` §5.45),Socket 4~7 为零块。`net_listen()` 未对 Socket 4~7 显式分配独立缓冲区块,后续代码误用相同块号(如 28~31)导致数据重叠。 - -### 解决方案 -每个数据 Socket 独占 4 个缓冲区块(2 发 + 2 收),按 `ds * 4` 基址分配,确保不重叠: - -| Socket | 发送块 | 接收块 | -|--------|--------|--------| -| 1 | 4~5 | 6~7 | -| 2 | 8~9 | 10~11 | -| 3 | 12~13 | 14~15 | -| 4 | 16~17 | 18~19 | -| 5 | 20~21 | 22~23 | -| 6 | 24~25 | 26~27 | -| 7 | 28~29 | 30~31 | - -### 对应文件 -`Drivers/BSP/NET/net_socket.c` → `net_listen()` - -### 发现时间 -Phase 8 压力测试,2025-07 - ---- - -## Trap 03:DHCP 包 xid 偏移错误 - -### 现象 -Python DHCP Server 收到 DISCOVER 包后无法匹配 Offer/Ack 的 xid,握手失败。 - -### 根因 -错误使用了 `data[232:236]` 作为 xid(Wireshark 某旧版显示误导),实际 DHCP 标准偏移为 `data[4:8]`。 - -### 正确偏移 -- xid:`data[4:8]` -- msg_type:`data[242]` -- client_mac:`data[28:34]` -- magic cookie:`data[236:240]` - -### 对应文件 -`test/host/ch395f_socket_test.py` → DHCP server 模式 - -### 发现时间 -Phase 6, 2025-07 - ---- - -## Trap 04:UDP 发送后必须等待 SENDBUF_FREE - -### 现象 -UDP 发送后立即执行接收操作,接收长度始终为 0 或数据错误。 - -### 根因 -CH395F 手册要求每次 `WRITE_SEND_BUF` 后必须等待 `SINT_STAT_SENBUF_FREE` 中断,否则下次写入或接收操作可能失败。 - -### 解决方案 -每次 `ch395f_write_send_buf()` 后轮询 `ch395f_get_sock_int_status()` 检查 `SINT_STAT_SENBUF_FREE` 标志。 - -### 对应文件 -`Drivers/BSP/CH395F/ch395f.c` / test code in `ch395f_test.c` - -### 发现时间 -Phase 6 广播宣告模式,2025-07 - ---- - -## Trap 05:CH395F 与 RTL8305NBI 自动协商不兼容 - -### 现象 -`ch395f_get_phy_status()` 始终返回 `PHY_DISCONN`,链路无法建立。 - -### 根因 -CH395F PHY 与 RTL8305NBI-CG 直连时自动协商失败。 - -### 解决方案 -初始化协议栈后强制设为 100M 全双工: -```c -ch395f_set_phy(CH395F_PHY_100M_FULL); -``` - -### 对应文件 -`Drivers/BSP/CH395F/ch395f.c` - -### 发现时间 -项目初期硬件联调 - ---- - -## Trap 06:TCP KeepAlive 参数必须为 500ms 倍数 - -### 现象 -设置 KeepAlive 后 TCP 连接几秒内 TIMEOUT 断开。 - -### 根因 -CH395F 内部定时器以 500ms 为基准单位,传入非 500ms 倍数的值导致未定义行为。 -且 `IDLE` 必须 > `INTVL`。 - -### 正确值 -```c -ch395f_set_keepalive_idle(60000); // 60s(500 倍数) -ch395f_set_keepalive_intvl(5000); // 5s(500 倍数) -ch395f_set_keepalive_cnt(3); // 3 次 -``` - -### 对应文件 -`Drivers/BSP/NET/net_socket.c` → `net_init()` - -### 发现时间 -Phase 2 测试,2025-07 - ---- - -## Trap 07:TCP 关闭重连直接 close,不要 disconnect - -### 现象 -`disconnect → close` 后 Socket 卡在 `FIN_WAIT_2` 数分钟,无法重新打开。 - -### 根因 -`disconnect` 发送 FIN 将 Socket 推入 `FIN_WAIT_2`,此后 `close` 不再发 RST,必须等远端发 FIN 才能关闭。 - -### 解决方案 -直接调用 `ch395f_close_socket()`(**做法保留**,原"发 RST"的说法见下方定案更正)。 -关闭后轮询 `ch395f_get_socket_status()` 的**第 1 字节**等待 `sock=0x00` 再 `open_socket`。 - -### 定案更正(2026-10-11):机制说法作废,实测 `disconnect → close` 不卡死 - -新增定案用例 **TC-NET-204**(`test/board/ch395f_test_task.c`,用例表见 `docs/测试说明/CH395F_Test_Guide.md`):同一 Socket、同一 PC 回显服务、`FUN_PARA=0x08`、硬件层独占,三模式各 5 轮(45 项判定全过): - -| 模式 | 关闭序列 | 回到 `SOCKET_CLOSED` 耗时(5 轮) | 重开+重连+回显 | +| 编号 | 标题 | 状态 | 一句话结论 | |---|---|---|---| -| A | `close` | 0 / 0 / 150 / 0 / 0 ms | 5/5 OK | -| B | `disconnect → close` | **0 / 0 / 0 / 0 / 0 ms** | 5/5 OK | -| C | `disconnect`(不 close) | **10 s 内从未回到**,byte0 恒 `0x05`、byte1 `0x00` | 直接 `open` → **`0x20` FAIL**(5/5);补 `close` 后 +0 ms 回 CLOSED,重开 5/5 OK | - -**三条推翻原根因**: - -1. **`FIN_WAIT_2`(0x06) 全程未出现**(15 轮 × 9 个采样点)。实测过渡态只有 `TIME_WAIT`(0x0A)(A 模式一轮在 +0/+50/+100 ms 可见,+150 ms 回 CLOSED)与 `TCP_CLOSED`(0x00)。 -2. **`close` 无 RST 语义**:§5.35 只说"关闭 Socket 前 CH395 会自动断开 TCP 连接"。手册中芯片主动发 RST 只出现在 §5.29(监听 Socket 已有连接时再来连接 →"发送 TCP RESET")与 §9.2.6(多连接找不到可用数据 Socket →"将连接复位")两处,均与 `close`/`disconnect` 无关;此外仅有错误码 `CH395_ERR_RST` 与 RST/RSTI 引脚。且主动发 FIN 先进 `FIN_WAIT_1`(05H)、收到 ACK 才进 `FIN_WAIT_2`(06H)——"disconnect 推入 FIN_WAIT_2"本身就跳了一拍。 -3. **观测口径错在把 byte1 当独立事实**:§5.22 第 1 字节只有 `00H/05H` 两个取值,第 2 字节才是 TCP 态;官方 SDK `CH395CMDGetSocketStatus()` 注释明确"Second byte — TCP state, **meaningful only if** TCP mode **and the first byte is open**"。当年 Phase 2 收尾代码的判据用的是**正确的 byte0**(`st[0]==CH395F_SOCKET_CLOSED`),break 之后照样打印 byte1 → 一个"已 CLOSED 状态下的残留 TCP 态读数"被读成了"卡在某个 TCP 态"。 - -**真正的原因**(C 模式 5/5 复现):`FUN_PARA` bit3 `SOCKET_CLOSE=1`(§5.52 ②,`0x08`/`0x09` 都置了)下,产生 DISCONNECT 后**芯片不关 Socket、不清缓冲**,Socket 长期停在 `byte0=0x05(OPEN) / byte1=0x00(TCP_CLOSED)`;此时直接 `open_socket` 必返 **`0x20` FAIL**——正是 2026-07-19 测试指南问题表里那条 `Phase 2 open_socket: 0x20 (FAIL) / socket 未完全关闭`。**把 Socket 拉回 CLOSED 的动作是 `close`,与 RST 无关。** - -**git 侧佐证**:`ch395f_tcp_disconnect()` 自首个提交起**只有定义、零调用点**(`git log --all -S` 在 `NET/App/test` 仅命中本仓 `bca6cc6` 一次),"disconnect→close" 序列从未进入任何提交代码;本条"发现时间 2025-07"也与仓库不符(仓库首个提交 `b1d05091` = 2026-06-05,Phase 2 对应 2026-07-19 的 `17109d9b`→`7a8a97f4`→`a08d26bc`)。当时驱动已在轮询 `GET_CMD_STATUS`(4 s 上限),故也不是"命令没等完成"。 - -**做法为何仍然对**(换成三条有支撑的理由):① §5.35 `close` 已自动断链,`disconnect` 纯属多余;② `disconnect` 会**制造**一个 DISCONNECT 事件,bit3=1 下芯片不自动收尾,该事件必须被消费/预清,否则污染下一条连接(Trap 14 / 17 / 22 实证);③ 它把芯片推进 closing 过渡态,重开需要沉降(Trap 09)。本轮 15 轮重开均未观察到残留 `DISCONNECT`(0x10)——`close` 顺带清掉了锁存,这正是"直接 close"安全的原因。 - -**与 Trap 17 的张力已消解**:Trap 17 写"close 后芯片要等对端 FIN/ACK 才把 Socket 置 CLOSED(耗时数毫秒)",与本条原"close 发 RST 瞬间回 CLOSED"矛盾;实测支持 Trap 17 一侧(A 模式可见 `TIME_WAIT` 过渡、0~150 ms),不支持 RST 说法。 - -### 补充(2026-10-13,E-02 台架):`close` 无 RST 的**边界**——对端内核存活时芯片停 `FIN_WAIT_2` 长尾 - -上条定案("`close` 无 RST 语义")在 E-02 收口实验里暴露出一个必须登记的边界,**不推翻定案、补全其适用条件**: - -- **对端配合**(收到 FIN 后其内核 close):芯片走 `TIME_WAIT` 过渡 ~150 ms → byte0 回 `00H`,reconciler 形态④ relisten → 新连接接入(`e02_probe` 更新后 queue=PASS / listener=PASS,对时"wall clock updated"落地)。 -- **对端内核存活但不 close**(挂起但连接仍活的客户端):本层发 FIN 后芯片停在 **`FIN_WAIT_2`(实测 byte0=0x05 / byte1=0x06)长尾**——没有 RST 就拆不掉,**监听恢复受限于对端 close**。这正是 E-02 场景(timeSync 30s 预算耗尽后 PC 侧 socket 仍活)的芯片侧形态。 -- **不配合场景的兜底** = 芯片自身 TIMEOUT / KeepAlive 拆僵尸会话(E13 定案:判死窗 30~90s,见 `docs/网络/N34-自愈审计/实验方案.md` E13)——**不是**应用可以加速的路径。 -- **应用侧出口 = `net_drop_conn(fd)`**(N34 阶段 3,E-02 收口):netTask 侧走驱动 `sock_reset` **QUARANTINE**(FIN + 排渣 + 排空事件 + 等 byte0=00H,400ms 底)+ 本层收口 CLOSED + `auto_relisten` 即时重监听;**保监听不毁**(单连接模型不得 `net_close` 监听 fd)。对端不 close 时复位机等在 `net_poll` 步进里(`ch395f_sock_reset_poll`),对端 FIN 到达即自动走完——**不搁浅**。 - -### 发现时间 -Phase 2 TCP Client 测试,2025-07(**日期有误**,实为 2026-07-19;定案更正与 TC-NET-204 实测 2026-10-11) +| 01 | Socket 配置顺序 | ✅ | `TCP_LISTEN` 时刻一次性扫描:先配数据 Socket 1~7,最后配监听 Socket 0 | +| 02 | Socket 缓冲显式分配 | ✅ | 48 块×512B 几何,Socket 4~7 默认零块,必须显式分配否则数据重叠/中断不触发 | +| 03 | DHCP xid 偏移 | ✅ | xid=`data[4:8]`(Wireshark 旧版显示误导) | +| 04 | UDP 发送后等 SENBUF_FREE | ✅ | 不等则下次写入被静默丢弃 | +| 05 | PHY 自动协商不兼容 | ✅ | 与 RTL8305NBI 直连协商失败,强制 100M 全双工 | +| 06 | KeepAlive 参数 500ms 倍数 | ✅ | 非 500ms 倍数 = 定时器未定义行为;IDLE>INTVL | +| 07 | TCP 关闭重连 | ✅ | 直接 `close`,不要先 `disconnect`;`close` 无 RST(E-02 边界:对端不配合则 `FIN_WAIT_2` 长尾,兜底靠查岗/`net_drop_conn`) | +| 08 | RECV 中断电平触发 | ✅ | 缓冲有数据就一直拉低,`net_poll` 每轮只处理一批 | +| 09 | 沉降窗(link up / close 后) | ✅ | open 失败重试 + `net_sock_reset_sync` 确定性排渣;LISTEN 态关闭是两段 CLOSE | +| 10 | 消息队列值拷贝 | ✅ | 结果必须走 `xTaskNotify` 通知值回传 | +| 11 | accept 拒绝 ESTABLISHED | ✅ | standalone_accept 后状态已 ESTABLISHED,入口检查须放行该组合 | +| 12 | FatFS 路径与 8.3 短名 | ✅ | 真约束=`FF_USE_LFN=0` 只认 8.3;本仓用 `"0:"` 卷前缀(原 #2/#4 根因已按 ff.c 更正) | +| 13/21 | 「recv_len 归零 / 接收冻结」家族 | ↩️ | 合并结案:非缺陷,全部为台架假象;机理与 §5.34 相左已撤回 | +| 14/17 | 残留 DISCONNECT 污染下一连接 | ✅ | 合并:滞后锁存 + SINT 粘滞 → 预清 + 反核实 + close 后排空 | +| 15 | DHCP 后 IP 寄存器提交滞后 | ✅ | 状态位≠寄存器生效:连续 3 次一致才采信 | +| 16 | IP/端口字节序 | ✅ | IP 四字节数组=点分顺序;16 位端口=低字节在前,两套口径 | +| 18 | FUN_PARA bit0 迷思 | ✅ | bit0 保留必须为 0(`0x08`);当年真正起作用的是缓冲显式分配 | +| 19 | — | — | 从未存在(`Test_Guide.md:878` 悬空引用待清) | +| 20 | 单次写超发送缓冲(写穿) | ✅ | 上限=本仓 2 块发送缓冲 1KB(`NET_SEND_CHUNK_MAX`),门控是 SENBUF_FREE | +| 22 | 快速复连丢中断 | ✅ | 芯片丢 CONNECT/DISCONNECT → 周期对账 `net_reconcile_tick`(reconciler 之源) | +| 23 | 上传中止后首笔复连被截断 | ✅ | 残渣投递给下一条连接 → 两级沉降/隔离 + 异常释放回 450 删截断文件 | +| 24 | SET_MAC 顺序 | ✅ | §9.2.1:SET_MAC 必须在 `INIT_CH395` **之前**(INIT 从 EEPROM 加载) | +| 25 | FTP 数据连接三连故障 | ✅ | 四重定案:丢中断窗口探针+幽灵甄别 / closed-with-data / 背靠背小段毒化 TX / 全局寄存器读即清(B3) | --- -## Trap 08:RECV 中断电平触发,避免无限循环 +# 第一章 初始化与配置 -### 现象 -`net_poll()` 中 RECV 中断无限触发,主循环(echo 等)永远得不到执行。 +## Trap 01 · Socket 配置顺序:先数据 Socket 后监听 Socket -### 根因 -CH395F 的 RECV 中断是电平触发的——只要接收缓冲区有数据就保持 `INT#` 低电平。 -`do { ... } while(int_status != 0)` 会无限循环。 +- **状态**:✅ 已定案(多连接模式虽已随 `319d00e3` 删除,**顺序规则仍适用于现行 `net_init`**——`net_socket.c` 注释「Trap 01:先配数据 Socket 1~7,再配监听 Socket 0」) +- **现象**:多连接模式下最多接受 3 个客户端,Socket 4~7 的 CONNECT 中断从不触发,SYN 被静默丢弃。 +- **根因(定案)**:CH395F 在 `TCP_LISTEN` 时刻**一次性扫描**当前所有可用 Socket 建立内部查找表;数据 Socket 若在 `TCP_LISTEN` 之后才配置缓冲区/协议/端口,自动分配逻辑不认识它们。 +- **修复**:正确顺序——① 数据 Socket 1~7:`SET_SEND_BUF → SET_RECV_BUF → SET_SOUR_PORT → SET_PROTO_TYPE_TCP`(不调 `OPEN_SOCKET`,连接到达时芯片自动打开);② 监听 Socket 0:`SET_SEND_BUF → SET_RECV_BUF → SET_PROTO_TYPE_TCP → SET_SOUR_PORT → OPEN_SOCKET → TCP_LISTEN`。 +- **教训**:`docs/芯片手册/CH395F.md` §9.2.6 要求数据 Socket **先设源端口再设协议类型**(与监听 Socket 顺序相反)。DISCONNECT 后芯片自动关闭 Socket 但保留配置,可再分配。 +- **发现时间**:Phase 8 压力测试(原记 2025-07,早于仓库首提交 `b1d05091` 2026-06-05,日期不可考)。 -### 解决方案 -`net_poll()` 使用 `do { ... } while(0)` 每次只处理一批中断,由主循环负责读取数据。 +## Trap 02 · Socket 缓冲区必须显式分配且不重叠 -### 对应文件 -`Drivers/BSP/NET/net_socket.c` → `net_poll()` +- **状态**:✅ 已定案(现行 `net_init` 固定布局 8×(4收+2发)=48 块即用满) +- **现象**:多 Socket 数据互相覆盖、收到错误数据或客户端挂死;未分配缓冲的 Socket 发送为固定垃圾内容(`0x0028` 填充)、接收存不进数据、CONNECT/RECV 中断不触发。 +- **根因(定案)**:芯片默认只为 Socket 0~3 分配独立收发缓冲(48 块×512B 用满,`docs/芯片手册/CH395F.md` §5.45),Socket 4~7 为零块;未显式分配或误用相同块号导致重叠。 +- **修复**:每个数据 Socket 独占块(历史多连接布局 2发+2收/Socket;现行 `net_init` 固定 6 块/Socket=4收+2发,与 MSS 解耦,见 Trap 21 解耦注)。**必须在 `open_socket` 之前**调 `ch395f_set_send_buf/set_recv_buf`。 +- **教训**:缓冲几何以 §5.45 的 48×512B 为准(此前文档"32×1KB"记录有误,Phase 7 TC704 40/40 实测确认块 0~47 全部有效)。 +- **发现时间**:Phase 8 压力测试(日期同 Trap 01,不可考)。 + +## Trap 05 · CH395F 与 RTL8305NBI 自动协商不兼容 + +- **状态**:✅ 已修复 +- **现象**:`ch395f_get_phy_status()` 恒返回 `PHY_DISCONN`,链路无法建立。 +- **根因(定案)**:CH395F PHY 与 RTL8305NBI-CG 直连时自动协商失败(板级组合)。 +- **修复**:初始化协议栈后强制 `ch395f_set_phy(CH395F_PHY_100M_FULL)`。 +- **发现时间**:项目初期硬件联调。 + +## Trap 06 · KeepAlive 参数必须为 500ms 倍数 + +- **状态**:✅ 已定案(产品参数 60s/5s/3 的转正过程见附录 A「KeepAlive 定案链」) +- **现象**:设置 KeepAlive 后 TCP 连接几秒内 TIMEOUT 断开。 +- **根因(定案)**:芯片内部定时器以 500ms 为基准,非 500ms 倍数 = 未定义行为;且 `IDLE` 必须 > `INTVL`。 +- **修复**:`idle=60000 / intvl=5000 / cnt=3`(均 500 倍数)。 +- **发现时间**:Phase 2 测试(原记 2025-07 不可考;KeepAlive 实验实际对应阶段 11)。 + +## Trap 18 · FUN_PARA bit0 迷思:bit0 保留必须为 0 + +- **状态**:✅ 已定案(`net_init` 用 `ch395f_set_fun_para(0x08)`) +- **误判存档**:本节曾断言"bit0=单/多 Socket 模式,必须 0x09 才能用 Socket 1~7"——**无任何出处,已撤回**;当年让 Socket 1~7 真正可用的是缓冲显式分配(Trap 02),不是 bit0。 +- **根因(定案)**: + 1. 官方手册 V1C/V1E/V2.2/V2.3 的 §5.52 位表中 bit0 一律「保留,必须为0」;官方 SDK `CH395INC.H` 无 bit0 宏; + 2. 官方例程直接反例:`EXAM5_TCP_Server` 传 `0x02`(bit0=0)却给 Socket 0~7 全配缓冲;官方 FTP 例程直接传 `0x08`;全 EVT 包无一处 0x09; + 3. 8 Socket 是芯片原生能力(`_SN` 索引寻址),真正的门槛是 §5.45 缓冲分配; + 4. 概念讹传源:bit1 `FUN_PARA_FLAG_TCP_SERVER` 的"服务器多连接"指**一个服务器能接几个客户端**,不是"能用几个 Socket"。 +- **为什么 0x09 当年"看起来有效"**:芯片忽略保留位;TC704 40/40 与 bit0 结论出自同一提交,属自证,无"0x08 下不通过"的对照。留着 bit0=1 的代价:违反保留位规定,后续固件版本一旦启用该位现网设备静默失效。 +- **实测闭环(2026-10-11,N-31 已销项)**:`FUN_PARA=0x08` 下 Phase 7 **34/34 全通过**(`PHASE7_MULTI_COUNT` 降到 5:E-23 未修,8 fd 减 3 常驻监听),并发回显实测落在 sock2/4/5/6/7 → Socket 4~7 在 bit0=0 下完全可用。 +- **台架前置(本轮踩到)**:① PC 网卡落在 Public 防火墙配置文件 → 入站 8081 被静默丢弃,TC701/702/704 全部 `CONNECT TIMEOUT`,极易误判成"0x08 毁了 client";② 上一轮遗留 `tcp_server` 进程占着 8081,新服务器绑不上端口却不报错。**判"芯片行为"前先确认对端真的收到了 SYN**(Trap 25 总纲铁律的又一次命中)。 +- **发现时间**:阶段 7 扩展 8 Socket 测试,2026-08-25;定案更正 2026-10-11。 + +## Trap 24 · SET_MAC 必须在 INIT_CH395 之前 + +- **状态**:✅ 已修复(纯顺序调整,无 RAM 变化) +- **现象**:UI 配置的 MAC 与设备实际在线 MAC 不一致("滞后一次上电")。静态审查发现,手册条文确证。 +- **根因(定案)**:`docs/芯片手册/CH395F.md` §9.2.1 初始化顺序 = `SET_MAC → SET_IP/GWIP/MASK → INIT_CH395`;§5.7 `SET_MAC_ADDR` 写芯片内部 EEPROM,§5.13 `INIT_CH395` 从 EEPROM **加载** MAC——SET_MAC 放 INIT 之后则本次上电不生效。原码顺序相反;`ch395f.c` 原注释把四个 SET 标为"芯片已初始化(之后)"加剧误排。 +- **修复**:`net_init()` 把 `ch395f_set_mac_addr()` 移到 `ch395f_init()` 之前;四个 SET 函数注释统一改"必须在 `ch395f_init()` 之前调用";`net_socket.h` 同步。 +- **发现时间**:2026-10-04(静态审查项 P1-7)。 --- -## 附:CH395F 驱动关键点 +# 第二章 中断与事件一致性 -### 初始化顺序 -- 必须按手册9.2.1节顺序:`SET_MAC → SET_IP/GWIP/MASK → INIT_CH395 → SET_PHY`(**违反此序曾导致 MAC 不生效,见 Trap 24**) -- **IP/网关/掩码必须在 `INIT_CH395` 之前设置**,INIT 会读取并锁定当前寄存器值到协议栈 -- **`SET_PHY` 必须在 `INIT_CH395` 之后**,复位 MAC/PHY 建立物理链路,不影响已锁定的协议栈参数 -- **`CMD_PING_ENABLE` 不需要显式调用**,INIT 后默认可用 +## Trap 08 · RECV 中断是电平触发 -### SPI 通信 -- 每次 SPI 事务需调用 `ch395f_spi_begin()` / `ch395f_spi_end()` 包裹 -- **大数据量收发使用 SPI2 DMA**:`ch395f_write_send_buf()` 和 `ch395f_read_recv_buf()` 已改造为 DMA 批量传输(DMA1_Stream3 RX, DMA1_Stream4 TX),命令和配置操作仍使用逐字节轮询 -- ⚠ **TSL 规格偏离(N34 E12,2026-10-13 登记)**:手册 §7.4 要求缓存区命令在 - "长度与数据之间"留 TSL(读 0.4us / 写 0.2us;其余字节间无需间隔;非缓存区命令 - 另有 TSC=0.6us / TSD=0.3us)。本仓两个缓存区函数把命令头与数据打包进**同一 - DMA 突发**(CS 全程拉低)→ 该间隔为 **0**,属规格偏离;其余命令逐字节传输, - TSC/TSD 有大余量。SPI2 = 5.25MHz(分频 8 @42MHz,单字节 1.52us)。 - **实测未发现危害**:460 轮 / ≈58MB 双向 SHA-256 全量比对仅 1 次未复现异常, - 且"两段+TSL"变体(+28B,吞吐无回退)与现状无差异 → 依 Trap 25 铁律暂不改产品 - 路径,偏离与证据登记于此、变体留作待用补丁。完整 A/B 与方法学见 - `docs/网络/N34-自愈审计/实验方案.md` E12。 -- DMA 缓冲区:`s_spi2_dma_tx_buf/rx_buf[CH395F_SPI_DMA_BUF_SIZE]` = 接收缓冲 2048 + 4 = **2052**(`ch395f.h:148`),4 字节对齐;单次有效载荷上限 2048(历史值 1500 / 4100 均已作废,见 Trap 13 复核注) +- **状态**:✅ 已定案 +- **现象**:`net_poll()` 中 RECV 中断无限触发,主循环永远得不到执行。 +- **根因(定案)**:接收缓冲区有数据期间 `INT#` 保持低电平,`do{...}while(int_status!=0)` 必然死循环。 +- **修复**:`net_poll()` 每轮只处理一批中断,由主循环负责读数据。 +- **推论**:RECV 是条件位(数据读空即落),所以 Trap 25 B3 的全量读清对 RECV 可下轮补、对其余位是永久销毁。 -### TCP 参数配置(必须在 INIT_CH395 之前) -- **重传参数**:`SET_RETRAN_COUNT`(默认12次,最大20)和 `SET_RETRAN_PERIOD`(默认500ms,最大1000ms),总重传时间 = 次数 × 周期 -- **KeepAlive 参数必须为 500ms 的倍数**:`IDLE`(默认20000ms)、`INTVL`(默认15000ms),且 IDLE > INTVL;传入非 500 倍数的值导致 CH395F 内部定时器异常 -- **KeepAlive 默认关闭**:需在 `SINT_STAT_CONNECT` 后调用 `ch395f_set_keepalive_enable(sock, 1)` 启用 -- ⚠ **E11 实测与转正(2026-10-13)**:此前 `ch395f_set_keepalive_enable` 全仓仅 - 客户端分支(`CONNECTING`)调用,而 `App/` 无任何 `net_connect()` → 产品上 - KeepAlive 实际全程关闭;服务器侧调用 2026-10-12 曾加后撤回(理由仅覆盖 - 物理链路失效)。E11 用"禁用 PC 网卡"注入"链路在、对端静默"(防火墙规则对 - 已建立连接无效,见实验方案 E11 方法学),实测服务器侧开启后:生产参数 - 60s/5s/3 → **判死 ≈30.0s**(早于手册公式 75s,以实测为准);短参数 6s/2s/2 - → ≈5.1s(证明参数生效);静默但存活的对端**不被误杀**(90s 空闲 KEEPLIVE - 回显正常)。消费链路现成:TIM_OUT → `handle_timeout_event` QUARANTINE → - pump relisten → 端口恢复。**2026-10-13 转正**:`handle_connect_event` - LISTENING 分支按 §5.56 开启,Phase 11 测试重新纳入回归(TC-NET-1101 步骤 - 已更新死对端注入法)。完整记录见 `docs/网络/N34-自愈审计/实验方案.md` E11。 +## Trap 14/17 · 上一条连接的残留 DISCONNECT 污染下一条连接(合并条目) -### Socket 4~7 缓冲区 -- CH395F 默认只给 Socket 0~3 分配独立收发缓冲(共 48 块 × 512B 用满),Socket 4~7 为零块 -- 多连接模式下使用 Socket 4~7 时,**必须在 `open_socket` 之前**显式分配: - - `ch395f_set_send_buf(sock, start_block, count)` - - `ch395f_set_recv_buf(sock, start_block, count)` -- 不分配会导致发送数据为固定垃圾内容(`0x0028` 填充)、接收缓冲区无法存储数据 +- **状态**:✅ 已定案(两级预清 + 排空为 Trap 23/25 排渣契约的前身) +- **合并说明**:Trap 14(2026-09,硬件层 TC-203 观察)与 Trap 17(2026-08-25,生产 `net_close_locked`)为**同一根因的两层表现**,2026-10-13 合并;代码注释常以「Trap 14/17」连用。 +- **现象**: + - 硬件层(14):`close_socket` → 已确认 CLOSED → 新会话第一次读 `GET_INT_STATUS_SN` 读到**上一会话遗留的 DISCONNECT**(0ms 即命中),负向判定/状态机误判(TC-203 三轮重连后 PC 握手实际成功、固件判"未连接")。 + - 生产层(17):单连接模式 close 后立即复用同一 Socket 重连,每次都 `openOK` 但紧接着 `DISCONNECTED, releasing` 秒断——旧连接的 DISCONNECT 在新连接 ESTABLISHED 之后才到达,`handle_disconnect_event` 把新连接误判断开。 +- **根因(定案)**:① `GET_SOCKET_STATUS` 是纯查询**不清中断**;② DISCONNECT 可能在 CLOSED 达成同时或之后才锁存;③ SINT **粘滞到被读**(close/open 都不清,Trap 25 B3 实证)→ 无人消费则跨会话存活。 +- **修复**: + 1. **预清**:新会话 OPEN 前读一次全局 + 本 Socket SINT 丢弃陈旧事件(`phase2_tc203()` 双保险;生产 `net_connect_locked` open 后清本 Socket 中断); + 2. **排空**:`net_close_locked` 在 `close_socket()` 后轮询 `GET_SOCKET_STATUS` 至 `SOCKET_CLOSED`(上限 500ms,实测 LAN 几毫秒)再读空本 Socket 中断; + 3. **反核实**:依据中断做结论前用 `GET_SOCKET_STATUS` 验证实际状态一致(负向事件与 ESTABLISHED 矛盾时以状态为准)。 +- **措辞更正(2026-10-13)**:原 Trap 17 写"芯片要等**对端** FIN/ACK 才置 CLOSED"——按 Trap 07 实测应为:自身 FIN 的 ACK + 本地 `TIME_WAIT` 过渡,0~150 ms。 +- **注意**:B3 修复(2026-10-12)后预清只准读**本 Socket** SINT,不得再全量读 8 个(见第六章 B3)。 +- **发现时间**:17=阶段 7 上板实测 2026-08-25;14=2026-09 TC-203 取证。 -### TCP 关闭重连 -- **直接调用 `ch395f_close_socket()`,不要先调 `ch395f_tcp_disconnect()`**(做法不变;机理口径已于 2026-10-11 由 TC-NET-204 实测更正,见 Trap 07「定案更正」) -- ✅ `close` 直接:§5.35 关闭前芯片**自动断链**,无需先 disconnect;实测 0~150 ms 回到 `SOCKET_CLOSED`,过渡态可见 `TIME_WAIT`(0x0A)——**不是 RST**(手册无 RST 语义) -- ❌ 只 `disconnect` 不 `close`:`FUN_PARA` bit3=1(§5.52 ②)下芯片**不自动关 Socket**,Socket 长期停在 `0x05(OPEN)/0x00(TCP_CLOSED)`,再 `open_socket` 必返 **`0x20` FAIL** -- ❌ `disconnect → close` 本身**不卡死**(实测 5/5 全部 +0 ms 回 CLOSED),只是多余:它制造一个 DISCONNECT 事件,而 bit3=1 下该事件须由我方消费/预清(Trap 14 / 17 / 22) -- 关闭后需轮询 `ch395f_get_socket_status()` 的**第 1 字节**等待 `sock=0x00` 再 `open_socket`;**第 2 字节(TCP 态)仅在 byte0=OPEN 时有意义**,别拿它当"卡住"的判据 +## Trap 22 · 快速复连丢 CONNECT/DISCONNECT 中断 → 监听静默死亡 -### SOCK_TIMEOUT 处理 -- 长时间无数据时可能触发 `SINT_STAT_SOCK_TIMEOUT`,**不应视为致命错误**——记录日志后继续操作即可,不要因此关闭 Socket +- **状态**:✅ 已修复定案(周期对账 `net_reconcile_tick` 是 N34 reconciler 的前身;阶段 3 已并入单一 100ms tick) +- **现象(生产固件实测)**:单连接监听 socket 快速复连后**永久失效**(后续 SYN 被拒或超时,10s 内不自恢复),芯片其余部分完全正常(其他监听端口照常工作)。两种表现: + - **拒连**:芯片完成握手但 CONNECT 中断未上报 → 控制块滞留 LISTENING;或本层因 DISCONNECT 中断丢失仍以为有客户端 → 两种情况芯片侧都占住单连接监听,后续 SYN 全被拒; + - **应用连锁**:`net_send` 被拒;或应用读不到 EOF(`g_client` 永不释放,**永不调用 accept**)。 +- **复现(可信判据,纯客户端;工具 `MDK-ARM/tcp_reconnect_probe.py`)**: + - 确定性:产品固件 + 待机 60s,对 `:7003` 以 300ms 快速复连 → 修复前**首 2 次成功、之后永久拒连**(2/10); + - 概率性:FTP `:21` 100ms×50 → 修复前**第 29 轮起永久拒连**;对照 `:21` 0ms/600ms 与 `:8888` 同期正常 → 触发条件 = 复连间隔 <~1s + 轻负载。 +- **机制(设备侧日志实证)**:① 芯片丢 CONNECT 或 DISCONNECT 中断;② 此后本层**收不到任何中断**,纯事件驱动无路可走(把 LISTENING 分支改"重建监听"仍无效——连 DISCONNECT 都没来);③ 故必须**周期回读芯片状态对账**。裸机芯片台架(剥离 OS/net/FTP)独立复现"握手完成但 CONNECT 中断被丢"——芯片天性,不是固件 bug。 +- **修复(`net_socket.c`)**: + 1. **周期对账 `net_reconcile_tick()`**(初版 200ms;N34 阶段 3 与 pump 合并为单一 100ms tick):修正失同步形态——①本层 ESTABLISHED 而芯片已无连接→按断开处理;②本层 LISTENING 而芯片已握手→补 accept;③本层 LISTENING 而芯片监听消失→重建;④孤儿监听(B10)→重建。**去抖+冷却**:同一失配连续 2 次才动手,动手后冷却 1s。 + 2. `handle_disconnect_event` LISTENING 分支:由"仅告警"改为"回读芯片状态——已不可监听即重建"。 +- **踩坑记录(务必先读)**:最初修法放在 **accept 路径**(每次 accept 回读自愈):单次状态读把"刚 open/listen 尚未生效"的瞬态误判为失配 → 监听反复重建直至 `relisten give up` → **三个监听口全部死亡(风暴)**。改"周期对账+去抖+冷却"后消除。**结论:凡基于单次状态读的重建动作,必须去抖。** +- **验证(修复前→修复后)**:`:7003` 300ms×10:2/10→**10/10**;`:21` 100ms×50:28/50→**50/50**;`:21` 0ms/600ms×50:**50/50/50/50**;`:7003` 200ms×20:**20/20**;FTP 10×64KB SHA-256:**10/10**;`relisten give up`:**0**。自愈实测计数:`chip closed -> disconnect` 12 次、`w/o CONNECT irq -> accept` 1 次、`listen lost -> relisten` 1 次——三类失同步均真实出现并被自愈。 +- **对应文件**:`Drivers/BSP/NET/net_socket.c`(`net_reconcile_tick`);`MDK-ARM/tcp_reconnect_probe.py`;`docs/任务规划/README.md` N-6(已销项)。 +- **发现时间**:2026-09-22(台架偶发)→ 2026-09-23(确定性复现+当日修复闭环)。 -### UDP 模式区分 -- **DesIP=0xFFFFFFFF → UDP Server 模式**:接受任意来源数据,接收数据前 8 字节为信息头(`reserved[2] src_port[2] src_ip[4]`),回发前需设置 `SET_DES_IP` 和 `SET_DES_PORT` -- **DesIP=具体 IP → UDP Client 模式**:只接收指定 IP:Port 的数据,接收数据无信息头 +## Trap 25 · FTP 数据连接三连故障——四重定案(N-30) -### UDP 发送缓冲 -- 每次 `ch395f_write_send_buf()` 后必须等待 `SINT_STAT_SENBUF_FREE` 中断,否则下次写入会被 CH395F 静默丢弃 +- **状态**:✅ 已定案并修复(回归 4 轮 11/11;B3 见第六章) +- **现象(生产 FTP 回归,多轮复现)**:同一会话内**第 2 次及以后**的数据连接(LIST/STOR/RETR)两类失败,概率随上一条数据连接的负载与间隔变化(NAND 目录越大越易触发),与提交历史无关(D20 基线同样复现,当年"11/11 通过"是慢速台架+干净目录的运气): + - **吞数据(SWALLOW)**:新连接建立成功、服务端 `net_send` 返回成功,客户端 20s 收不到任何字节; + - **425**:新连接被接受后 ~10-20ms 出现 DISCONNECT(`chip tcp=0x00`),accept 失败 → `425 Can't open data connection`。 +- **定案实验(裸机芯片台架,剥离 OS/net/FTP)**:实验分支 `n30-chip-bench`,`test/board/ch395f_reuse_bench.c` 内核启动前直接调 `ch395f_*` 受控矩阵 + 主机侧 `test/host/ch395f_reuse_host.py`。**剥离后 bug 依旧复现** → 纯芯片操作时序问题。关键现场:`post-listen st=0x05/0x04`(listen 成功但 TCP 态停在旧连接 ESTABLISHED=幽灵态)、`unexpected int=0x10`(只等来旧连接滞后 DISCONNECT)、`no CONNECT within 10s`(芯片状态已 0x04 但中断永远没来)。 +- **芯片级规律**:CH395F 拆链收尾窗口**变长且不可观测**(状态显示 CLOSED 持续 1s+ 也不代表拆完,实测可 >2s 且时长不定)。窗口内重新 `open+listen` 三种形态:① 握手完成但 **CONNECT 中断被丢**(Trap 22 机制的窗口期形态)→ 吞数据;② 状态直接假报 0x04 幽灵态(回读对端端口=旧连接端口);③ 旧连接滞后 DISCONNECT 打到新监听 → relisten 毁掉新连接 → 425。 +- **已证伪的修复**(受控实验全部无效,列出防重踩):close 后固定沉降 100~3000ms(2s 也挂过)/ Socket 轮换(跨 socket 毒化)/ listen 后 close+重试 / 缓冲重分配 / close 前 TX 排空延时 / 禁用对账 tick(对账不是元凶)。 +- **有效修复一:双信源原则——中断只是快路径,状态寄存器才是真相**: + 1. **accept 路径内联探针**(`net_lost_connect_probe`,`net_accept_locked` 每 100ms 挂探针):芯片已 ESTABLISHED 而 CONNECT 中断未到 → 连续 2 轮去抖后补 accept; + 2. **幽灵态甄别**:补 accept 前回读对端端口(`GET_REMOT_IPP`),**= 上一条连接端口(`last_peer_port`)→ 幽灵,不 accept**(生产 LIST#4"accept 成功但数据被吞"的直接成因就是对账②曾把幽灵补 accept); + 3. 对账②同步加幽灵甄别;`last_peer_port` 登记点:`handle_disconnect_event` / `handle_timeout_event` / `net_relisten_try`(清零 remote_port 前转登)/ `net_close_locked`(跨 memset 保留——幽灵属芯片 Socket 级,与 fd 复用无关)。 +- **定案二:所谓"kill 模式"是寄存器语义误读(2026-10-11,双向验证)**——误判存档:曾解读"芯片在 accept 后 10-20ms 杀连接",**有误**。真相: + 1. `FUN_PARA=0x08` 置 bit3 `SOCKET_CLOSE=1`(§5.52 ②):产生 DISCONNECT/TIMEOUT 后芯片**不关 Socket、不清缓冲**,就是留给 MCU 读出残留数据后再关; + 2. 探针现场 `kill-check recv_len=1027 re-read st=0x05/0x00`——Socket 仍 OPEN、TCP 已 CLOSED、**RX 缓冲 1027B = STOR 载荷一个不少全在**:芯片按规格**保留现场**,不是杀连接; + 3. 那个 DISCONNECT 是**客户端正常 FIN**(Python ftplib `storbinary` 发完即关数据连接,标准 FTP 行为,局域网小文件 ~5ms 完成)。 + - **425 真根因(竞态+自毁缓冲)**:客户端 5ms 内发完并关闭;accept RPC 赶到时 MCU 态已 CLOSED;旧 `net_accept_locked` 把 CLOSED 一律当"无连接" → `net_relisten_try`(close+open **清空芯片 RX 缓冲**)→ 1027B 被**我方亲手销毁** → 425。芯片全程无辜(此模式)。 + - **修复(closed-with-data accept)**:CLOSED + auto_relisten + **芯片 RX 有数据**(`GET_RECV_LEN_SN`>0)→ 接受这条"已关但可读"连接(日志 `closed-with-data accept (drain pending)`),应用快路径排空,排空后 EOF 自然触发 relisten——对齐 §5.52 ② 契约。**STOR #2 从 4/4 失败 → 4/4 通过**。 + - 连带修正:台架 mode0"accept 后 10-20ms DISCONNECT"大概率同为误读(主机收完数据自己关的);但台架**丢 CONNECT 中断模式**(主机 got=0 + 芯片 0x04 + 无中断,主机端独立证据)仍成立,双信源探针修复保留。 +- **定案三:LIST 吞包 = 背靠背小段写毒化芯片 TX(2026-10-11)**:决定性对照——同一目录同一载荷,LIST #1(FAT 冷、行间天然间隔)通过;LIST #4/#7(FAT 热、每行一次 `net_send` ~60B 背靠背小段)必挂。芯片对同一连接的**背靠背多段小写入**会毒化 TX 整条连接(芯片报 SEND_OK 但对端协议栈全部丢弃,无 RST 无 DISCONNECT);逐块协议(等 SENBUF_FREE 再写)本身合规(§5.45),但芯片对该模式的内部处理不可靠;RETR 1KB 满块连发不受影响。**修复**:`send_list`/`send_nlst` 整份目录**一次性批量发送**(static 1536B 缓冲,snprintf 累积 + 单次写;溢出截断判 -1→550;注意 `lftpd_inet_write` 成功返回**字节数**非 0)。**修复后 ftp_regression 4 轮 11/11** + `ftp_auth_bench.py` 13/13。 +- **与 Trap 22 的关系**:Trap 22 周期对账对常驻监听器有效;FTP 数据通道是"活 1 秒就死"的短命监听器,对账②无幽灵甄别时既救不全(冷却期内 accept 已超时)又帮凶(把幽灵补 accept)。Trap 25 把"看状态补 accept"内联进 accept 热路径(短去抖、无冷却)+ 端口甄别,对账②保留为兜底网。 +- **教训(Trap 25 总纲)**:本轮先后误读三次——把客户端正常 FIN 读成"芯片杀连接"、把 open 重试读成"拆链窗口"、把芯片假 SEND_OK 读成"数据已上线"。铁律:**CH395F 的"成功"(中断位/状态码)只代表芯片本地动作完成,不代表对端收到;判定必须回到对端可见证据(主机侧收发)+ 手册语义(FUN_PARA §5.52、状态码 §5.22)**。 +- **对应文件**:`net_socket.c`(`net_lost_connect_probe`/closed-with-data accept/`net_reconcile_tick`②/`last_peer_port`)、`net_types.h`、`lftpd.c`(批量发送);台架 `test/board/ch395f_reuse_bench.{c,h}` + `test/host/ch395f_reuse_host.py`(实验分支注册编译,主分支只存档)。 +- **发现时间**:2026-10-11(登记簿 N-30)。 --- -## Trap 09:PHY link up 后立即 open_socket 失败 +# 第三章 连接生命周期与自愈 -### 现象 -`net_listen()` 在 PHY_CHANGE 中断同一时刻调用 `ch395f_open_socket()` 返回错误码(非 BUSY 超时),导致首次 listen 失败。3 秒后重试成功。 +## Trap 07 · TCP 关闭重连:直接 close,不要 disconnect -``` -[NET] PHY_CHANGE: 0x08 ← PHY 刚 link up (100M FULL) -[ERR] error listening on socket ← 同一秒 open_socket 失败 -[FTP] waiting for connection... ← 3秒后重试成功 -``` +- **状态**:✅ 已定案(做法保留;原根因说法已推翻,见定案更正) +- **误判存档**:原断言"`disconnect → close` 让 Socket 卡 `FIN_WAIT_2` 数分钟、close 发 RST"——**TC-NET-204 实测推翻**(2026-10-11),机制说法作废,做法保留。 +- **定案(TC-NET-204,三模式各 5 轮 45 项判定全过)**: + | 模式 | 关闭序列 | 回 `SOCKET_CLOSED` 耗时 | 重开+重连+回显 | + |---|---|---|---| + | A | `close` | 0~150 ms(过渡态可见 `TIME_WAIT` 0x0A) | 5/5 OK | + | B | `disconnect → close` | **全部 +0 ms** | 5/5 OK | + | C | `disconnect`(不 close) | **10s 内从未回到**(byte0 恒 05H) | 直接 open 必返 **0x20 FAIL**(5/5);补 close 后 +0ms 回 CLOSED | + 1. `FIN_WAIT_2`(0x06) 全程未出现(15 轮×9 采样点); + 2. **`close` 无 RST 语义**:§5.35 只说"关闭前自动断链";手册中芯片主动发 RST 仅 §5.29(监听占用)与 §9.2.6(多连接无可用数据 Socket)两处,均与 close/disconnect 无关; + 3. **观测口径错误**:§5.22 第 1 字节只有 `00H/05H`,第 2 字节(TCP 态)**仅在 byte0=OPEN 时有意义**(官方 SDK 注释明文)——当年把"已 CLOSED 状态下的残留 TCP 态读数"读成了"卡在某个 TCP 态"。 + - **真正原因**(C 模式 5/5 复现):`FUN_PARA` bit3 `SOCKET_CLOSE=1`(§5.52 ②)下产生 DISCONNECT 后**芯片不关 Socket、不清缓冲**,长期停在 `05H/00H`;此时直接 `open_socket` 必返 `0x20` FAIL。**把 Socket 拉回 CLOSED 的动作是 `close`,与 RST 无关。** + - **做法为何仍对**:① §5.35 close 已自动断链,disconnect 纯属多余;② disconnect 会**制造** DISCONNECT 事件,bit3=1 下必须被消费/预清,否则污染下一条连接(Trap 14/17/23 实证);③ 它把芯片推进 closing 过渡态,重开需沉降(Trap 09)。15 轮重开未观察到残留 `DISCONNECT`(0x10)——close 顺带清掉锁存。 + - **与 Trap 17 的张力已消解**:实测支持 Trap 17 一侧(close 后 0~150ms 过渡),不支持"close 发 RST 瞬间回 CLOSED"。 +- **边界补充(2026-10-13,E-02 台架)**:`close` 无 RST 的适用条件—— + - 对端配合(收到 FIN 后其内核 close):芯片走 `TIME_WAIT` ~150ms → byte0 回 00H → relisten → 新连接接入; + - **对端内核存活但不 close**(挂起但连接仍活):本层发 FIN 后芯片停在 **`FIN_WAIT_2`(byte0=05H/byte1=06H)长尾**——没有 RST 就拆不掉,**监听恢复受限于对端 close**; + - 不配合场景的兜底 = 芯片自身 TIMEOUT/KeepAlive 拆僵尸会话(E13 定案:判死窗 30~90s,见 `docs/网络/N34-自愈审计/实验方案.md` E13)——**不是**应用可加速的路径; + - **应用侧出口 = `net_drop_conn(fd)`**(N34 阶段 3):netTask 侧走驱动 `sock_reset` QUARANTINE(FIN+排渣+排空事件+等 byte0=00H,400ms 底)+ 本层收口 CLOSED + `auto_relisten` 即时重监听;**保监听不毁**(单连接模型不得 `net_close` 监听 fd)。对端不 close 时复位机等在 `net_poll` 步进里(`ch395f_sock_reset_poll`),对端 FIN 到达即自动走完——**不搁浅**。 +- **判据**:关闭后轮询 `GET_SOCKET_STATUS` 的**第 1 字节**等待 `sock=0x00` 再 `open_socket`;别拿 byte1 当"卡住"判据。 +- **发现时间**:Phase 2 TCP Client 测试(原记 2025-07 **有误**,实为 2026-07-19;定案更正与 TC-NET-204 实测 2026-10-11;E-02 边界补充 2026-10-13)。 -### 根因 -CH395F 的 PHY link up 后,内部 TCP/IP 协议栈需要额外时间完成初始化(ARP 缓存、路由表等)。在 PHY_CHANGE 中断触发的同一 `net_poll()` 迭代内立即调用 `OPEN_SOCKET`,芯片可能返回 `ERR_BUSY` 或其他错误码。 +## Trap 09 · 沉降窗:PHY link up 后 / close 后不能立即 open -### 解决方案 -1. **应用层重试兜底**:FTP Server (`lftpd_start`) 的 `while(1)` 循环中,listen 失败后 `vTaskDelay(3000)` 重试,3 秒足够 PHY 稳定 -2. **启动延时消峰**:FTP 任务启动时 `osDelay(7000)`,等 netTask 完成 `net_init() → PHY 协商 → 首轮 net_poll()` 后再发起 listen,此时 PHY 已稳定 2 秒以上,首次就成功 +- **状态**:✅ 已定案(现行形态=open 重试环 + `net_sock_reset_sync`;原 `osDelay(7000)` 启动延时已被就绪屏障取代) +- **现象**:PHY_CHANGE 中断同一时刻 `ch395f_open_socket()` 返错(非 BUSY 超时),首次 listen 失败;3 秒后重试成功。 +- **根因(定案)**:PHY link up 后芯片内部协议栈需额外时间完成初始化;同一 `net_poll()` 迭代内立即 `OPEN_SOCKET` 可能返 `ERR_BUSY` 或其他错误码。与 FUN_PARA 无关。 +- **修复(现行形态)**: + 1. `net_listen_locked`/`net_connect_locked` open 失败重试 ×3(D10 `net_listener_retry_tick` 重监听重试环); + 2. open 失败先走 `net_sock_reset_sync`(确定性排渣,内置二段 CLOSE,见下补充)再重试——最短等 100ms ≪ 原 500ms 盲等; + 3. ~~FTP 任务 `osDelay(7000)` 错峰~~ → 已由**就绪屏障**(`SYS_EVT_NET_READY`/`FTP_READY`)取代(2026-10 起)。 +- **补充(2026-10-13,E2/E4 台架):LISTEN 态 Socket 的二段关闭**——本条"close 后需沉降才能重开"的前置形态: + - 对 LISTEN(byte0=05H/byte1=01H)发 `CLOSE_SOCKET`:命令返 SUCCESS 但**只拆监听**(byte1 01H→00H),byte0 恒 05H——按"等 byte0=00H"判据会永远等不到;**第二次 CLOSE** 才把 byte0 拉到 00H。即 LISTEN 态关闭是**两段**:拆监听 → 关 Socket。 + - 驱动侧已内置补发:`ch395f_sock_reset_*` 状态机 `WAIT_CLOSE` 段检测到"TCP 已 00H 而 byte0 仍 05H"自动补发第二次 CLOSE(契约见 `Drivers/BSP/CH395F/ch395f.h`)。 + - E4 定案 60 轮实证"复位后 open 必成"。 + - ⚠ 任何**新写的**"close 后等 CLOSED"逻辑必须走驱动复位原语,**不要**手拼"一次 CLOSE + 轮询 byte0"——会在 LISTEN 态上死等。 +- **发现时间**:FTP 改造期(原记 2025-07 不可考);二段 CLOSE 补充 2026-10-13。 -### 注意事项 -- 此问题在单连接模式 (`FUN_PARA=0x00`) 和多连接模式 (`0x02`) 下均存在,与 FUN_PARA 无关 -- 如果多个应用任务同时启动 listen,可能全部首次失败、全部重试成功——建议各任务错峰启动 +## Trap 11 · net_accept_locked 拒绝 standalone_accept 的 ESTABLISHED -### 补充(2026-10-13,E2 台架):LISTEN 态 Socket 的**二段关闭**——一次 CLOSE 只拆监听,二次 CLOSE 才回 CLOSED +- **状态**:✅ 已修复定案(现行 `net_accept_locked` 仍保留该放行逻辑) +- **现象**:FTP 客户端 TCP 连接成功(standalone accept),220 欢迎语不发送,`lftpd_inet_accept()` 死循环。 +- **根因(定案)**:入口检查 `state != LISTENING` 直接拒绝——standalone accept 后 state 已变 ESTABLISHED,永远到不了后续检查。 +- **修复**:放行 `standalone_accept && state == ESTABLISHED` 组合。 +- **注意**:仅影响 standalone_accept 模式(PASV 数据通道、单连接 FTP)。 +- **发现时间**:FTP 改造期(2026-08)。 -E2/E4 芯片实验(`docs/网络/N34-自愈审计/实验方案.md`)补充发现,属本条"close 后需沉降才能重开"的**前置形态**: +## Trap 23 · 上传中止后首笔复连上传被静默截断(≤2048B)并误报成功 -- 对处于 **LISTEN**(byte0=`05H` / byte1=`01H`)的 Socket 发 `CLOSE_SOCKET`:命令返 **SUCCESS**,但芯片**只拆监听**(byte1 `01H→00H`),**byte0 恒 `05H`**——单次 CLOSE 后按"等 byte0=00H"的判据会永远等不到。 -- **第二次 CLOSE** 才把 byte0 拉到 `00H`(Socket 关闭)。即 LISTEN 态的关闭是**两段**:拆监听 → 关 Socket。 -- **驱动侧已内置补发**:`ch395f_sock_reset_*` 状态机的 `WAIT_CLOSE` 段检测到"TCP 已 00H 而 byte0 仍 05H"自动补发第二次 CLOSE(契约见 `Drivers/BSP/CH395F/ch395f.h`)。 -- **生产路径由 Trap 09 的 open 重试环吸收**:`net_listen_locked` / `net_connect_locked` open 失败 → `net_sock_reset_sync`(确定性排渣,内置二段 CLOSE)→ 下一轮重试 open;E4 定案 60 轮实证"复位后 open 必成"。 -- ⚠ 任何**新写的**"close 后等 CLOSED"逻辑都必须走驱动复位原语,**不要**手拼"一次 CLOSE + 轮询 byte0"——那会在 LISTEN 态上死等(本条补充的直接动因)。 +- **状态**:✅ 已修复定案(三阶段全部闭环;回归判据常备:`test/host/ftp_abort_probe.py`) +- **现象**:客户端上传(STOR)中途断开(**RST 或优雅 FIN 均可**),**紧随其后的第一笔复连上传被截断到 ≤2048 字节(= 芯片单 socket RX 缓冲大小),且服务端回报 226 成功** → 客户端据此认为上传成功 = **静默数据损坏**;第二笔及以后恢复正常。 +- **证据(可信复现:生产固件、零固件探针;工具 `test/host/ftp_abort_probe.py`)**:判据 = FTP 协议结果 + 紧接一笔正常上传的 SHA-256 往返 + 控制通道 NOOP 存活探测。RST 中止 **10/10** 复现(请求 4096B → 设备日志 `UPLOAD '/ab_ok.bin' 2048 bytes OK`);优雅 FIN **5/5** 同样复现 → 与 RST/FIN 无关,凡"数据未发完即断开"均触发。对照(无中止)177 轮(8KB~1MB,含 100 轮零间隔浸泡)SHA-256 全匹配(`MDK-ARM/ftp_freeze_probe.py`)→ 中止路径是唯一触发条件。 +- **机制(设备侧日志实证)**:① 中止时数据 socket 释放(RST 时芯片报 `sock4 TIMEOUT`),net 层随即 relisten(监听端口复用);② 中止时在途数据(约一个缓冲=2048B 残渣)**随后才落进芯片 RX 缓冲**;③ 下一笔连接 accept 后 ~10-20ms 出现伪 `DISCONNECTED, releasing (chip tcp=0x00)`,lftpd 数据读循环据此读到 EOF → 把已到的 2048B 当整个文件 → 226;④ 再下一笔芯片已干净 → 正常(**一次性**而非持久故障)。**缺陷位于 fd 复用生命周期**(上一条连接的残留状态投递给下一条连接),不在 FTP 命令逻辑——lftpd 只是把它放大成误报 226。 +- **边界甄别(现场"1~2s"时间假设被推翻)**:gap 500/2000/5000ms 复测设备实存尺寸**全部仍 2048**(12/12)→ **与时间无关**;触发=设备侧**收过数据**的数据连接被对端关闭(纯 FIN 即触发);同 fd 非 FTP 服务(`:7003`)不受影响(不按传输复用)。 +- **修复(三阶段,`net_socket.c` + `lftpd.c`)**: + 1. **异常释放(TIMEOUT/RST)隔离**:`handle_timeout_event` 置 per-fd 隔离标记;`net_relisten_locked` 隔离期内持续排空芯片 RX(`net_drain_chip_rx()`)+ 主动 close + 排空该 socket 中断 + 等 `CLOSED/TCP_CLOSED`,满 `NET_RELISTEN_QUARANTINE_MS`(400ms) 才 open+listen;上限 `NET_RELISTEN_SETTLE_MS`(800ms) 强制放行——**必须小于** `NET_RELISTEN_DEADLINE_MS`(2000ms):实测取 2000ms 会耗尽重试预算 → give up → **监听器死亡**(已留痕); + 2. **正常释放(FIN)沉降**:`handle_disconnect_event` 登记(仅 `recv_bytes != 0` 时——纯连接即关与只发不收免沉降保复连时序),relisten 前最短等 `NET_RELISTEN_GRACEFUL_MS`(**100ms**,实测悬崖:0/30ms 无效、100ms 稳定通过,对 30ms 重试节拍留 3× 余量);日志区分 `relisten settle`/`relisten quarantine`; + 3. **应用侧硬化**:`net_sock_t.released_abnormal` 标记(TIMEOUT 置 1/DISCONNECT 置 0,新 fd 与新监听清零——新连接不得继承上一条的释放原因);`net_pending_tick` 断连了结按标记以 `NET_ERR_CONNRESET` 或 EOF(0) 回传;`lftpd receive_file` 错误路径 **unlink 截断文件** + STOR 回 **450**(原误报 226)。 +- **协议边界(不可判定,留痕)**:FTP STOR 无长度声明,"在途 FIN 关闭"与"正常完成"**逐字节不可区分** → 优雅中止仍回 226;仅 RST/掉链(芯片 TIMEOUT)可识别 → 450。FIN 边界由客户端长度校验(SIZE/RETR 往返)兜底。 +- **验证(终态基线)**:`--check-abort` RST **5/5**(450)+ FIN **5/5**(226 边界不变);followup 全过;正常往返 freeze SHA-256 全 MATCH(t_stor 0.68→0.18s);`:7003` sink 复用 RST×4/FIN×4 全过;`:8888` 真实收包写 RTC 成功。 +- **与 TC-NET-604 的关系**:本条是独立、已可信复现并已修复的真实缺陷;TC-NET-604 已结案为非缺陷(Trap 13/21)。现场"上传失败/文件不完整/像卡死"类报告优先用本条判据复核。 +- **发现时间**:2026-09-23(可信复现首轮);二阶段 09-24、硬化 09-25。 --- -## Trap 10:消息队列值拷贝导致结果无法回传 +# 第四章 数据通路与数值语义 -### 现象 -`net_send()` / `net_recv()` / `net_listen()` / `net_accept()` / `net_close()` 等线程安全 API(通过消息队列委托 netTask 执行)永远返回 `-1`,即使 netTask 内部操作成功。 +## Trap 04 · UDP 发送后必须等待 SENDBUF_FREE -### 根因 -CMSIS-RTOS v2 `osMessageQueue` 传递 `net_msg_t` 结构体是**值拷贝**。调用方 puts 后 `msg.result = -1`(栈变量),netTask 侧 `osMessageQueueGet` 拿到的是**队列中的副本**,修改副本的 `msg.result = 0` 后 `xTaskNotifyGive` 通知调用方。调用方的栈变量 `msg.result` 仍然是 `-1`,从未被更新。 +- **状态**:✅ 已定案 +- **现象**:UDP 发送后立即接收,接收长度恒 0 或数据错误。 +- **根因(定案)**:手册要求每次 `WRITE_SEND_BUF` 后必须等 `SINT_STAT_SENBUF_FREE`,否则下次写入被静默丢弃。 +- **修复**:每次 `ch395f_write_send_buf()` 后轮询 `ch395f_get_sock_int_status()` 检查该标志。 +- **发现时间**:广播宣告模式(原记 Phase 6 2025-07 有误,UDP 对应阶段 3)。 -```c -// 调用方(ftpTask) -msg.result = -1; -osMessageQueuePut(&msg); // 值拷贝到队列 -xTaskNotifyWait(¬ified); // 等待通知 -return msg.result; // 永远是 -1! +## Trap 13/21 · 「recv_len 归零 / 接收缓冲冻结」家族——非缺陷(合并结案) -// 处理方(netTask) -osMessageQueueGet(&msg); // 拿到的是队列副本 -msg.result = actual_result; // 修改的是副本 -xTaskNotify(...); // 通知回去了,但值丢了 -``` +- **状态**:↩️ 已撤回(2026-09-23 结案;2026-10-13 把 Trap 13 并入本条——同族误判,当年未同步) +- **合并说明**:Trap 13(2026-08-26,"分批读 recv_len 归零")与 Trap 21(TC-NET-604"恰好填满+排空窗口冻结")是同一轮误读的两半;Trap 13 的机理断言与手册直接相左,一并撤回。 +- **结案结论**:原报告(同连接前序突发后 4096B 突发"前 2KB 正常回显、其余静默无回显")**在当前固件上无法复现,判定为非缺陷**。可信判据(产品固件 + 真实 lftpd FTP 上传,成功 = FTP 协议结果 + **上传后 RETR 回读 SHA-256**)实测 **227 轮全过**(8KB/64KB/256KB/1MB;含零间隔快速重连与 100 轮浸泡),并发生产 UI 流 600 帧 CRC 全过。 +- **为什么原台架会"复现"(教训,务必先读)**: + 1. **测试程序自吞数据**:台架配方里的固件内读者(Phase 9/11 的 64B 读、固件探针读)把数据读走后不回显——PC 端少收被误当"设备停摆"。实测短收量**恰等于被吞字节数**(`4736−4608=128=2×64`);对照组(无额外读者)40/40 全过。 + 2. **判读工具误判**:`probe_classify.py` 把**通过**的运行报成 `STALL`(脚本层实测 `1460/1460` 且内容逐字节匹配)。 + 3. 由此派生的"部分排空致命 / 再武装区间 ≥160ms / 相位刀口"等结论**全部作废**。 +- **Trap 13 历史注**:当年确实观察到 `recv_len` 归零,但机理断言("读部分数据后 recv_len 被重置为 0")**与 §5.34 明文相左**("未读取的数据仍然保留在接收缓冲区中,可以继续读取"),本仓 TC-NET-201 注释也是相反口径;定案前须补一条"部分读 → 再查 `recv_len`"的定向 TC。⚠ 该已撤回机理**仍被当作现行约束**写在 `CH395F_Test_Guide.md` 多处与 `net_socket.c` 注释里——处置登记在第六章待修清单。 +- **台架纪律(保留)**:① 数据完整性以 **SHA-256 往返**为真值,并扣除固件读者吞噬的字节;② 冻结/卡死类走**真实生产路径 + 端到端内容校验**(`MDK-ARM/ftp_freeze_probe.py`),卡住时用控制通道 NOOP 区分"数据面卡死"与"整层卡死";③ **禁止把"固件内非回显读者"与固定期望值混用在同一配方里**——本轮最大教训。 +- **保留的代码变更(与撤回机理无关)**:缓冲块布局与 MSS 解耦(`ch395f.h`)、超限读 OVERSIZE 金丝雀(`ch395f.c`)不回退(防御性/可观测性改进)。 +- **发现/结案**:发现 2026-08-26(阶段 8);结案 2026-09-23;合并 2026-10-13。 -### 解决方案 -不通过消息队列回传结果,改用 **FreeRTOS 通知值**(notification value)携带返回值: +## Trap 16 · NET 层目的 IP 字节序写反 → CONNECT TIMEOUT -**Producer(netTask)**: -```c -xTaskNotify(msg.caller, (uint32_t)msg.result, eSetValueWithOverwrite); -``` +- **状态**:✅ 已修复定案(字节序总口径,多处引用) +- **现象**:NET 层 `net_connect()` 返回 0 但 `sock0 CONNECT TIMEOUT` 永远连不上;同一芯片同一目标的硬件层 client 完全正常。 +- **根因(定案)**:`net_connect_locked()` 把 `sin_addr.s_addr`(网络字节序)按**低字节在前**拆成 `remote_ip_arr[4]` 得到 `[2,1,168,192]` 交给 `ch395f_set_des_ip()`——但 **`SET_IP_ADDR_SN` 族按点分顺序(`[192,168,1,2]`)写入才生效**(硬件层 `test_parse_ip` 直出点分即成功;官方例程 `CH395IPAddr={192,168,1,101}` 同向)。芯片把 SYN 发往 `2.1.168.192` → 网关丢弃 → TIMEOUT。⚠ 手册 §5.8 总括"所有 IP 输入输出命令字节顺序均为低字节在前"与官方例程自相矛盾,**本仓以实测+例程为准**。 +- **误导性排查**:`GET_REMOT_IPP` 回读是**小端**——把写反的数组读回再按小端解释恰好又得 `192.168.1.2`,回读日志"看起来正确",掩盖真实写反。 +- **修复**:① `remote_ip_arr` 改**大端**提取(`arr[0]=(ip>>24)&0xFF …`);② UDP `net_send_locked()` 同样修正;③ 诊断回读按小端语义重组再打印。 +- **字节序总口径(务必分清两套)**: + - 写芯片 **IP 四字节数组**(set_des_ip/set_ip_addr/set_gwip_addr/set_mask_addr)= **点分顺序**; + - **16 位端口一律低字节在前**(§5.25/§5.26),两者口径不同,勿把"大端数组"套到端口上; + - 回读类命令(GET_REMOT_IPP/GET_IP_INF)若标注"低字节在前",打印前先按小端重组; + - 本层 `sin_addr.s_addr`/`remote_ip` = 网络字节序数字;`remote_ip_arr[4]` = 点分顺序(`net_types.h` 注释)。 +- **发现时间**:阶段 7 NET 层 TCP Client 上板实测,2026-08-25。 -**Consumer(调用方)**: -```c -uint32_t notified; -xTaskNotifyWait(0, 0, ¬ified, portMAX_DELAY); -msg.result = (int)notified; -``` +## Trap 20 · 单次写超过发送缓冲区(写穿) -### 注意事项 -- 指针成员(`msg.buf`)不受值拷贝影响——netTask 拿到的是同一个指针,可以读写调用方的缓冲区 -- `net_connect()` 不使用消息队列(在 netTask 内直接调用 `_locked` 版本),不受此 bug 影响 -- 内核级 API(`net_send_sock` / `net_recv_sock` / `net_listen_locked`)直接操作,同样不受影响 +- **状态**:✅ 已修复定案(措辞已按 2026-10-13 更正:不是"硬件 FIFO",是本仓分配的发送缓冲) +- **现象**:单次发送 >1KB 时数据中段错乱、连接被对端 RST、或 `WRITE_SEND_BUF` 后芯片不再产生 SENBUF_FREE——发送卡死或后续字节丢失;只在 >1KB 时稳定复现。 +- **根因(定案)**:分块上限误取 `NET_DMA_MAX_PAYLOAD`(2KB)/软件缓冲(4KB),而每 Socket 的发送缓冲 = 本仓固定 2 块×512B = **1KB**(§5.45 上电默认其实是 4 块=2KB,本仓布局自 2026-09-22 起固定 2 块、与 MSS 解耦);一次性写 2KB/4KB 直接**写穿发送缓冲**(超出部分覆盖/丢失或破坏指针)。 +- **修复**:`CH395F_SEND_BUF_SIZE_BYTES` 宏 + `NET_SEND_CHUNK_MAX = min(NET_DMA_MAX_PAYLOAD, CH395F_SEND_BUF_SIZE_BYTES)`(现 1KB);`net_send_locked`/`net_sendto` 分块上限改用它。门控是 **SENBUF_FREE**(不是"等 SEND_OK");接收方向不受影响(只读 `get_recv_len` ≤ 接收缓冲 2KB)。 +- **注意**:上限随 `CH395F_SEND_BLOCKS` 自动跟随;调用方可按任意长度调 `net_send`,分块由驱动保证。 +- **发现时间**:阶段 8 大文件传输实现,2026-08-26。 --- -## Trap 11:`net_accept_locked` 拒绝 standalone_accept 的 ESTABLISHED 状态 +# 第五章 上层集成与主机侧工具 -### 现象 -FTP 客户端 TCP 连接成功(standalone accept),220 欢迎语不发送,`lftpd_inet_accept()` 死循环。客户端连接成功但永远收不到任何 FTP 响应。 +## Trap 03 · DHCP 包 xid 偏移错误(主机工具) -### 根因 -`net_accept_locked()` 入口检查: +- **状态**:✅ 已修复 +- **现象**:Python DHCP Server 收到 DISCOVER 后无法匹配 Offer/Ack 的 xid。 +- **根因(定案)**:误用 `data[232:236]`(Wireshark 某旧版显示误导);标准偏移 xid=`data[4:8]`、msg_type=`data[242]`、client_mac=`data[28:34]`、magic cookie=`data[236:240]`。 +- **对应**:`test/host/ch395f_socket_test.py` DHCP server 模式。 +- **发现时间**:阶段 4(原记 Phase 6 2025-07 有误)。 -```c -if (p_listen_sock->state != NET_SOCK_STATE_LISTENING) { - return -1; // ← 直接拒绝! -} -``` +## Trap 10 · 消息队列值拷贝导致结果无法回传 -standalone accept 后 state 已变为 `ESTABLISHED`,永远到不了后续的 `standalone_accept` 检查——死锁。 +- **状态**:✅ 已定案(现行契约:`net_*` 错误码走通知值——AGENTS 模块约定) +- **现象**:`net_send/net_recv/net_listen/net_accept/net_close` 等委托 netTask 的 API 永远返回 -1,即使内部成功。 +- **根因(定案)**:`osMessageQueue` 传 `net_msg_t` 是**值拷贝**——netTask 修改的是队列副本,调用方栈变量的 `msg.result` 从未被更新。 +- **修复**:结果搭 `xTaskNotify(caller, (uint32_t)msg.result, eSetValueWithOverwrite)` 通知值回来,调用方 `xTaskNotifyWait` 取回。**勿给 `net_msg_t` 加"回传用"字段**(调用方只会读到栈垃圾)。 +- **注意**:指针成员(`msg.buf`)不受值拷贝影响;内核级 `_locked` API 直接操作不受影响。⚠ 原注"net_connect() 不使用消息队列"已失效——现行 `net_connect` 同样走消息队列、结果同样走通知值。 +- **发现时间**:FTP 改造期(2026-08)。 -### 解决方案 -```c -if (p_listen_sock->state != NET_SOCK_STATE_LISTENING && - !(p_listen_sock->standalone_accept && p_listen_sock->state == NET_SOCK_STATE_ESTABLISHED)) { - return -1; -} -``` +## Trap 12 · FatFS 路径与 8.3 短名 -允许 `standalone_accept + ESTABLISHED` 组合通过。 +- **状态**:✅ 已修复(修复有效;原根因 #2/#4 已按仓内 FatFs 源码更正) +- **现象**:FTP STOR `f_open("/upload_test.txt", ...)` 返 `FR_INVALID_NAME`(6);>8.3 的文件名同样 err=6。 +- **根因(定案,2026-10-13 更正)**:真约束是 `ffconf.h` `FF_USE_LFN=0` → **只支持严格 8.3 短名**(不得以点开头)。原记"FatFS 不接受 `/file` 前缀 / 空字符串失败"两条与仓内 `Lib/FatFs/ff.c` 矛盾(`ff.c:3101-3109` 会剥前导分隔符、`:3127-3129` 空名返回根目录)——**已删**;本仓现行路径口径是 `"0:"` 卷前缀(`FF_FS_RPATH=0` 下 `"."` 不是合法路径段)。 +- **修复**:`to_fatfs_path()` 剥离 `/` 前缀(保留,行为正确);长文件名需 `FF_USE_LFN=1` + `FF_LFN_UNICODE`(显著增 RAM,未启用)。 +- **发现时间**:FTP 改造期(2026-08)。 -### 注意事项 -- 不影响多连接模式(多连接监听 Socket 状态始终为 LISTENING,由数据 Socket 承载连接) -- 仅影响 standalone_accept 模式(PASV 数据通道、单连接 FTP) +## Trap 15 · DHCP 成功后 IP 寄存器提交滞后 + +- **状态**:✅ 已定案(测试侧稳定轮询+交叉比对) +- **现象**:`GET_DHCP_STATUS` 已置 0x00 后立即 `GET_IP_INF` 读到的仍是旧静态配置,而芯片实际已按新租约收发——三方数据矛盾构成假阳性(2026-08-24 上板实测)。 +- **根因(定案)**:DHCP 状态位仅代表协商完成;IP/MASK/GW 寄存器提交与状态置位**异步**,数百毫秒级窗口(实测 ~1s 内翻转)。 +- **修复**:① 连续 3 次读 `GET_IP_INF`(间隔 300ms,上限 8s)一致且非零才采信;② PC 端把收到的宣告 IP 与租约比对兜底。 +- **对应**:`test/board/ch395f_test_task.c: phase4_run()`;`docs/测试说明/CH395F_Test_Guide.md` 阶段 4 风险表 #4。 +- **发现时间**:2026-08-24。 --- -## Trap 12:FatFS `FF_USE_LFN=0` + 路径 `/` 前缀 → `FR_INVALID_NAME` - -### 现象 -FTP STOR 上传文件时 `f_open("/upload_test.txt", FA_WRITE | FA_CREATE_ALWAYS)` 返回 `err=6`(FR_INVALID_NAME)。`upload_test.txt` 文件名 11+3 字符 > 8.3 限制同样触发 err=6。 - -### 根因 -1. `ffconf.h` 中 `FF_USE_LFN = 0`(长文件名禁用),最大文件名 8+3 字符 -2. FatFS 不接受 `/file` 格式的路径(需要 `file` 或 `0:file`) -3. `lftpd_io_canonicalize_path("/", "file")` 输出必然带 `/` 前缀 -4. 根目录路径 `"/"` 剥离后为空字符串,`f_stat("")` 同样失败 - -### 解决方案 -```c -/* 剥离 "/" 前缀,根目录用 "." 表示 */ -static const char *to_fatfs_path(const char *path) { - if (path == NULL || *path == '\0') return "."; - if (*path == '/') { - path++; - if (*path == '\0') return "."; - } - return path; -} -``` - -所有 `lftpd_io_*` 函数调用 FatFS 前统一使用 `to_fatfs_path(path)`。 - -### 注意事项 -- 长文件名需设置 `FF_USE_LFN = 1` 并配置 `FF_LFN_UNICODE`,会显著增加 RAM 占用 -- `CWD .` 和 `CWD /` 同样受此问题影响,修复后正常工作 - ---- - -## 附:FTP 支持改造完整问题清单 - -| # | 问题 | 类型 | 修复位置 | -|---|------|------|----------| -| 1 | 消息队列值拷贝,6 个 API 永远返回 -1 | Trap 10 | `net_socket.c` | -| 2 | PHY link up 后 open_socket 失败 | Trap 09 | `freertos.c` + `lftpd.c` | -| 3 | `net_accept_locked` 拒绝 ESTABLISHED | Trap 11 | `net_socket.c` | -| 4 | CH395F 多连接模式不支持多监听 | 架构 | `net_socket.c: fun_para=0x00` | -| 5 | PASV 端口 0(net_bind 无自动分配) | 缺功能 | `net_socket.c: s_dynamic_port` | -| 6 | FatFS 路径 `/` 不兼容 + LFN 禁用 | Trap 12 | `lftpd_io.c: to_fatfs_path()` | -| 7 | `cmd_pasv` 连关连开 listen 失败 | Trap 09 子类 | `lftpd.c: 3次重试` | -| 8 | `ftpTask` 启动时 PHY 未稳定 | Trap 09 子类 | `freertos.c: osDelay(7000)` | -| 9 | 会话结束后立即重建监听失败 | Trap 09 子类 | `lftpd.c: vTaskDelay(200ms)` | -| 10 | `s_file_open` 单文件限制 | 已知限制 | `lftpd_io.c` (全局变量) | -| 11 | CH395F DMA buffer 太小,分批读导致 recv_len 归零 | Trap 13 | `ch395f.c + lftpd.c` | -| 12 | `malloc` 嵌入式环境失败 | Trap 13 子类 | `lftpd.c: static buffer` | -| 13 | `net_listen_locked` 缓冲区覆盖(s0/s1 争用 block 0-3) | Trap 02 翻版 | `net_socket.c: 单连接用默认 buf` | -| 14 | CH395F TIMEOUT 立即触发(数据连接无数据) | Trap 09 子类 | `net_socket.c: CLOSED 状态处理` | -| 15 | 发送单次写 >1KB 写穿 1KB 硬件发送 FIFO | Trap 20 | `net_socket.c: NET_SEND_CHUNK_MAX` | -| 15 | `NET_RECV_TIMEOUT_MS=5s` 控制通道超时断开 | 配置 | `net_config.h: 30000` | -| 16 | `receive_file` 0 bytes 当作成功 | 逻辑 | `lftpd.c: total==0 则失败` | -| 17 | MobaXterm 连数据端口不发数据(客户端特殊行为) | 外部 | Python 抓包确认,超时恢复 | - ---- - -## Trap 13:CH395F `recv_len` 分批读取时归零 - -### 现象 -MobaXterm 上传文件时 CH395F 收到数据(`s1 int=0x04` RECV_OK 持续触发),但 `ch395f_get_recv_len(1)` 第一次返回 2920,读 1024 字节后第二次永远返回 0。`GINT=0x0020` 反复触发但无数据可读——死循环。 - -### 根因 -`ch395f_read_recv_buf` 读部分数据后,CH395F 的 `recv_len` 寄存器**被重置为 0**(非递减)。缓冲区中剩余的 1896 字节仍然存在(RECV_OK 标志为真),但 `ch395f_get_recv_len` 报告 0。 - -> ⚠ **2026-10-11 复核:该机理未证实,且与手册直接相左——§5.34 明文"读取的数据长度可以小于缓冲区的实际有效数据长度,未读取的数据仍然保留在接收缓冲区中,单片机可以通过此命令继续读取";本仓自己的 TC-NET-201 注释也是相反口径(`test/board/ch395f_test_task.c:419` "只查询积压字节数…可反复调用"、`:476-477` "剩余字节下一轮 get_recv_len 会继续报告,不会丢")。同族结论"部分排空致命"已由 Trap 21 结案撤回,本条当时未同步。现象本身(当年确实观察到 `recv_len` 归零)保留为历史记录;定案前须补一条"部分读 → 再查 `recv_len`"的定向 TC。下方解法数字均为历史值:现 `CH395F_SPI_DMA_BUF_SIZE = 2048+4 = 2052`(`ch395f.h:148`)、`lftpd.c:228` 是 `static unsigned char buffer[1024]`,`s_recv_buf[4096]` 已不存在。** - -同时 `CH395F_DMA_BUF_SIZE=1500`(header 4 + max 1496 data),无法一次读取完整的 4096 字节 CH395F 接收缓冲区。 - -### 解决方案 -1. `CH395F_DMA_BUF_SIZE` 从 1500 增大到 4100(支持一次读 4096 字节 header+data) -2. `receive_file` 缓冲区从 1024 增大到 4096,一次性读空 CH395F 接收缓冲区 -3. 不能使用 `malloc`(嵌入式堆可能不可用),改用 `static unsigned char s_recv_buf[4096]` - -### 注意事项 -- CH395F 接收缓冲区默认 socket 0: 4096B, socket 1: 4096B。读空后客户端继续发数据,recv_len 重新递增——正常 -- 清理中断状态后 `handle_timeout_event` 中的 `ch395f_close_socket` 必须显式调用,确保下次 `open_socket` 成功 - ---- - -## Trap 14:close 后 DISCONNECT 滞后锁存,污染下次会话首次中断读取 - -### 现象 -`close_socket` → 轮询 `GET_SOCKET_STATUS` 已确认 CLOSED 后,紧接的下一会话(重新 open + connect)在第一次读 `GET_INT_STATUS_SN` 时读到**上一会话遗留的 DISCONNECT 事件**(0ms 即命中),导致负向判定/状态机误判。实测:TC-202 三轮重连后立即跑 TC-203,PC 端握手实际成功(连接被接受),固件却因陈旧 DISCONNECT 抢先判"未连接"。 - -### 根因 -- `wait-CLOSED` 轮询用的 `GET_SOCKET_STATUS` 是**纯查询,不清中断**; -- TCP 拆除过程中断(DISCONNECT)可能在与 CLOSED 状态达成几乎同时、甚至之后才锁存; -- 该事件无人消费,跨会话存活到下一次 `open_socket` 之后。 - -### 解决 -1. **预清**:新会话 OPEN 前读一次 `GET_GLOB_INT_STATUS_ALL` + `GET_INT_STATUS_SN` 丢弃陈旧事件; -2. **反核实**:依据中断做结论前,用 `GET_SOCKET_STATUS` 验证实际状态一致(如"负向事件 vs ESTABLISHED"矛盾时以状态为准)。 - -### 参考 -`test/board/ch395f_test_task.c: phase2_tc203()`(双保险实现);同类思路见 Phase 1 入口的 PHY_CHANGE 预清。 - ---- - -## Trap 15:DHCP 成功后 IP 寄存器提交滞后 - -### 现象 -`GET_DHCP_STATUS` 已置 0x00 后立即 `GET_IP_INF`,回读到的仍是**旧静态配置**(如 192.168.1.100),而芯片实际已按新租约(192.168.1.77)正常收发——串口打印与 Socket6 广播宣告均带出过期 IP。2026-08-24 上板实测:固件各检查全 PASS(6/6),但 PC 侧 HELLO 发往租约地址才得到应答,三方数据矛盾构成假阳性。 - -### 根因 -DHCP 状态位仅代表协商流程完成;IP/MASK/GW 寄存器的提交与状态置位是异步的,存在数百毫秒级窗口(实测 ~1s 内完成翻转)。 - -### 解决 -1. **稳定轮询**:连续 3 次读 `GET_IP_INF`(间隔 300ms,上限 8s)结果一致且非零才采信; -2. **交叉比对**:PC 端将收到的宣告 IP 与所分配租约比对,不一致即报错兜底。 - -### 参考 -`test/board/ch395f_test_task.c: phase4_run()`;`docs/测试说明/CH395F_Test_Guide.md` 阶段 4 风险表 #4。 - ---- - -## Trap 16:NET 层 TCP Client 目的 IP 字节序写反 → CONNECT TIMEOUT - -### 现象 -NET 层 TCP Client(阶段 7)`net_connect()` 返回 0,但 `net_poll()` 报 `sock0 CONNECT TIMEOUT`,连接永远建立不起来;而硬件层 TCP Client(阶段 2,同一 `net_init`/FUN_PARA=0x08,同一目标 192.168.1.2:8081)完全正常。PC 同网段、防火墙关闭、服务端正常监听。 - -### 根因 -`net_connect_locked()` 把 `sockaddr_in.sin_addr.s_addr`(网络字节序,`net_inet_addr` 返回)按 **低字节在前** 拆成 `remote_ip_arr[4]`,得到 `[2,1,168,192]`,再交给 `ch395f_set_des_ip()`。但 **`SET_IP_ADDR_SN` 族按点分顺序(`[192,168,1,2]`)写入才生效**(阶段 2 用 `test_parse_ip("192.168.1.2")` 直接得到 `[192,168,1,2]` 即成功,可证;官方例程 `EXAM5_TCP_Server/main.c:39 CH395IPAddr={192,168,1,101}` 同向)。⚠ 2026-10-11 复核:手册 §5.8 的总括"所有包含 IP 输入或输出的命令,字节顺序均为 IP 低字节在前"与官方例程自相矛盾,本仓以实测 + 例程为准;**端口不在此列**——§5.25/§5.26 明确低字节在前,驱动 `ch395f_set_des_port`/`set_sour_port` 即如此且工作正常,勿把"大端数组"套到端口上。于是芯片把 SYN 发往 `2.1.168.192`——一个跨网段、不可达的地址,SYN 被网关丢弃 → 无 SYN-ACK → `CONNECT TIMEOUT`。 - -**误导性排查**:`CMD_GET_REMOT_IPP_SN` 回读的是**小端**字节序(`ch395f.c` 注释 "IP和端口均为低字节在前")。把写反的 `[2,1,168,192]` 读回再按小端解释,恰好又得到 `192.168.1.2`,使回读日志看起来"目的地址正确",掩盖了真实写反。 - -### 解决 -1. `net_connect_locked()` 提取 `remote_ip_arr` 改为 **大端**:`arr[0]=(ip>>24)&0xFF … arr[3]=ip&0xFF`(ip 为网络字节序 s_addr)。 -2. 同样修正 UDP 发送路径 `net_send_locked()` 中 `set_des_ip` 的目的 IP 数组(同样低字节在前 bug)。 -3. 诊断回读 `GET_REMOT_IPP` 时按其小端语义还原:`dip=(rip[3]<<24)|…|rip[0]`,再按 `>>24…` 打印,方可显示芯片实际目的 IP。 - -### 注意事项 -- `net_inet_addr` 返回网络字节序;`sin_addr.s_addr` 一律按网络字节序理解。 -- 任何写 CH395F **IP 四字节数组**(set_des_ip / set_ip_addr / set_gwip_addr / set_mask_addr)都传**点分顺序数组**,与 `test_parse_ip` 产物一致;**16 位端口一律低字节在前**(§5.25/§5.26),两者口径不同。 -- 回读类命令(GET_REMOT_IPP / GET_IP_INF 等)若文档标注"低字节在前",打印时务必先按小端重组再格式化,否则会误判。 - -### 对应文件 -`Drivers/BSP/NET/net_socket.c`(`net_connect_locked` 提取 `remote_ip_arr` 大端;`net_send_locked` UDP 目的 IP 同样修正;`[CONN]` 回读按小端重组);`Drivers/BSP/NET/net_types.h`(`remote_ip_arr` 注释改为大端);`test/board/ch395f_test_task.c`(`test_parse_ip` 可作为大端构建范本)。 - -### 发现时间 -阶段 7 NET 层 TCP Client 上板实测,2026-08-25。 - ---- - -## Trap 17:单连接模式 client 重连被上一条连接的残留 DISCONNECT 秒断 - -### 现象 -阶段 7 TC701(首次 TCP Client 连接 + 回显)正常;TC702(close 后同 Socket 0 重连 ×3,每轮不同本地端口)每次都 `[CONN] openOK` 成功、但紧接着 `sock0 DISCONNECTED, releasing`,连接被秒断,PC 端看到"已建立连接但对端立即关闭、未收到数据"。阶段 6 的 `auto_relisten`/断线处理逻辑同源,但 `auto_relisten` 标志本身不是元凶(client 该标志为 0)。 - -### 根因 -单连接模式只有 Socket 0,新旧连接复用它。`net_close_locked()` 发 `close_socket()` 后芯片要等对端 FIN/ACK 才把 Socket 置 `SOCK_CLOSED` 并上报 `DISCONNECT` 中断(耗时数毫秒)。TC702 几乎在 TC701 关闭同时复用 Socket 0 去 `tcp_connect`:旧连接的 `DISCONNECT` 中断在 TC702 已 `ESTABLISHED` 之后才到达,`net_poll()` 的 `handle_disconnect_event()` 把状态置 `CLOSED` → 新连接被误判为"刚建立就断开"。`GET_REMOT_IPP` 回读仅确认目的地址正确,掩盖不了此事件串扰。 - -### 解决 -1. `net_close_locked()`:`close_socket()` 后轮询 `ch395f_get_socket_status()` 直到 `CH395F_SOCKET_CLOSED`,再读 `ch395f_get_sock_int_status()` / `ch395f_get_glob_int_status_all()` 清掉中断状态(上限 500ms,LAN 通常几毫秒),彻底排空上一条连接残留的 `DISCONNECT`。 -2. `net_connect_locked()`:`open_socket()` 成功后再次读空本 Socket 中断状态,作为双保险。 - -### 注意事项 -- 任何"关闭→复用同一 Socket 0 重新建连"的场景都必须先排空旧连接的中断,否则旧 `DISCONNECT` 会串扰新连接。 -- 若关闭后阻塞等待影响实时性(多连接模式另有 Socket),应改为非阻塞的"代次/epoch"机制;单连接模式直接排空即可。 -- `net_close_locked` 内阻塞 `osDelay` 仅在 netTask 上下文,单连接模式无其它并发 Socket,安全。 - -### 对应文件 -`Drivers/BSP/NET/net_socket.c`(`net_close_locked` 排空残留 DISCONNECT;`net_connect_locked` open 后清中断)。 - -### 发现时间 -阶段 7 NET 层 TCP Client 上板实测,2026-08-25。 - -## Trap 18:多 Socket 模式切换 + 8 个 Socket 缓冲分配 - -### 现象 -阶段 7 仅测了 Socket 0。要验证 Socket 1~7 也能作为 client,当时**推论**必须把 `FUN_PARA` 从"单 Socket 模式"切到"多 Socket 模式",否则 `open_socket(1~7)` 被芯片拒绝/无响应,1~7 完全不可用。 - -⚠ **2026-10-11 复核:这句"必须切多 Socket 模式"是当时的前置推论,不是实测记录**——本段无串口日志摘录、无 TC 编号、无主机侧独立证据;且当年 Socket 1~7 **根本没有显式分配缓冲**(`net_init` 注释"先不配置 buffer"、MSS=1460),按 Trap 01/02 未配缓冲的 Socket 中断不触发,混淆因子未被排除。bit0 语义见本节末「定案更正」。 - -### 根因 -`CMD_SET_FUN_PARA` 的 **bit0** 决定 Socket 数量模式:`0`=单 Socket 模式(仅 Socket 0,8KB 缓冲全归它);`1`=多 Socket 模式(Socket 0~7 全部可用,缓冲需自行分配)。之前 `net_init` 用 `0x08`(bit0=0)即单 Socket 模式,故 1~7 不存在。注意 bit1(TCP Server 单/多连接)与 bit0 是两回事:本方案只翻 bit0(多 Socket),bit1 保持 0(TCP Server 单连接),即"8 个 Socket 可用,但 Socket 0 作服务端时只接 1 客户端"。 - -⚠ **此"根因"已于 2026-10-11 证伪**(bit0 在所有版本官方手册里都是「保留,必须为0」;官方例程 bit0=0 下 8 个 Socket 照常可用)。当年真正起作用的是下面第 2 条的**缓冲显式分配**,不是 bit0。完整证据见本节末「定案更正」。 - -### 解决 -1. `net_init`:`ch395f_set_fun_para(0x09)`(bit0=1, bit1=0, bit3=1)。⚠ **2026-10-11 已改回 `0x08`**(bit0 保留必须为 0,见「定案更正」)。 -2. `ch395f_init()` 之后,循环 0~7 用 `ch395f_set_send_buf(sock, i*6+4, 2)` / `ch395f_set_recv_buf(sock, i*6, 4)` 给每个 Socket 分配独立收发缓冲。按`docs/芯片手册/CH395F.md` §5.45(48 块 × 512B = 24KB)+ §9.2.8 最优分配:每 Socket 6 块 = 接收 4 块(2KB) + 发送 2 块(1KB),`ch395f_set_tcp_mss(1024)`,恰好用尽 0~47 块、互不重叠。 -3. MSS 选取依据 §9.2.8:接收缓冲(2KB) ≥ 2×MSS(1024) 满足建议、≥ MSS 满足必须;发送缓冲(1KB) ≤ 8KB 满足必须。MSS 取 1024 为 8-Socket 共享 24KB 时的最大值。 -4. `net_init` 缓冲分配顺序遵循 Trap 01:先配数据 Socket,再配监听 Socket 0(本 Net 层 Socket 按需 open,故统一在 init 一次性配好)。UDP 路径 `net_bind()`(`net_socket.c:1000-1014`)中对 Socket 4~7 的兜底重设也同步为同一布局。 -5. PC 端 `tcp_server` 改为每连接一线程的并发回显(`--max-conn` 为并发计数),否则 Phase 7 全量连接会触发累计计数上限而拒掉 4~7。 - -### 注意事项 -- 4~7 的缓冲必须显式分配,否则其 CONNECT/RECV 中断不触发(Trap 01/02)。 -- 上述布局**以`docs/芯片手册/CH395F.md` §5.45 的 48×512B 缓冲几何为准**,并已由 Phase 7 TC704(40/40 PASS)实测确认块 0~47 全部有效(此前文档中"32×1KB"记录有误)。Socket 4~7 分配与 8 路并发回显均验证通过。 -- 多 Socket 模式下 TCP Server 单连接语义不变:Socket 0 监听且连接停留在本 Socket,Sockets 1~7 由应用当作独立 client 使用。 -- `net_poll` 用 `GET_GLOB_INT_STATUS_ALL`(2 字节,支持 0~7)并遍历 0~7,Socket n 中断位即 `bit(n+4)`(Socket 0~3→bit4~7,Socket 4~7→bit8~11),原代码正确无需改动。 - -### 对应文件 -`Drivers/BSP/NET/net_socket.c`(`net_init` FUN_PARA + 缓冲分配 + MSS);`test/board/net_test_task.c`(新增 TC704 八 Socket 并发回显);`test/host/ch395f_socket_test.py`(`tcp_server` 并发 + `--max-conn`)。 - -### 定案更正(2026-10-11):`FUN_PARA` 取 **0x08**,bit0 与 Socket 个数无关 - -**结论**:`net_init` 用 `ch395f_set_fun_para(0x08)`(bit3 SOCKET_CLOSE=1、bit0=0)。本节"bit0=单/多 Socket 模式"的说法**无任何出处,已撤回**;当年让 Socket 1~7 真正可用的是上面第 2 条的**缓冲显式分配**,不是 bit0。 - -**证据(一手,按强度排序)**: - -1. **官方手册位表**:本机 `D:\Code\DTU\使用的芯片手册\以太网控制器_CH395F(1).pdf`(WCH《CH395 手册 1》V2.2,页眉 `https://wch.cn`)§5.52 表跨 PDF 第 21→22 页,bit0 行原文 `0 - - 保留,必须为0`;bit2、`[5:6]`、`[31:8]` 同为保留;注释①~⑥ 无一处提 Socket 数量。官方现行版 V2.3 逐位一致。旧版(V1C、英文 V1E、2020 版 CH395L/Q)bit0 **同样是保留**(旧版多出的位是 bit2=LOW_PWR,V2.2 起收回为保留)→ **任何版本都不存在"bit0=多 Socket"这一行**。 -2. **官方 SDK 位宏**:`CH395EVT\EVT\EXAM\PUB\CH395INC.H:349-353` 只定义 bit1/3/4/7/8,**没有 bit0 宏**。 -3. **官方例程直接反例**:`EXAM5_TCP_Server\main.c:296` 传 `0x02`(bit0=0),同文件 `:337-359` 给 Socket 0~7 全部配缓冲 → **bit0=0 下 8 个 Socket 照常可用**;`EXAM11_FTPClient\FTP\CH395.C:319` 官方直接传 `0x08, 0x00, 0x00, 0x00`(与本仓取值逐字节相同);全 EVT 包 grep `SetFunPapr` 仅 3 处(0x02 / 0x02 / 0x08),**无一处 0x09**。 -4. **8 Socket 是芯片原生能力**:`docs/芯片手册/CH395F.md` 命令章前言「CH395 内部可以同时提供 8 个 Socket,其索引值分别为 0、1、2、3、4、5、6、7」;Socket 类命令一律 `_SN`(Socket 索引)寻址。 -5. **真正的门槛是缓冲**:`docs/芯片手册/CH395F.md` §5.45「如果单片机需要用到的 Socket 个数大于 4,需要重新对缓冲区进行分配」——正是本节第 2 条措施。 -6. **概念讹传源**:手册里唯一的"多"字样是 bit1 `FUN_PARA_FLAG_TCP_SERVER`(TCP 服务器**多连接**,全局配置),`docs/芯片手册/CH395F.md` §9.2.6「默认的设置下,服务器多连接功能被关闭……每个 Socket 只可以建立一个 TCP 连接」——单/多之分指**一个服务器能接几个客户端**,不是"能用几个 Socket"。 -7. **互联网侧**:多轮检索无任何来源称 bit0 为单/多 Socket 模式,无公开代码传 0x09;CH390/CH392/CH397 同族也无此位可串源。 - -**为什么 0x09 当年"看起来有效"**:芯片忽略了保留位(Trap 25 旁证:bit0=1 下 sock1/2/3 并行正常)。TC704 40/40 PASS 只证明"0x09 + 缓冲布局"通过,**不构成"0x08 下不通过"的对照**(与 bit0 结论出自同一提交,属自证)。留着 bit0=1 的代价是违反保留位规定——后续固件版本一旦启用该位,现网设备静默失效。 - -**实测闭环(2026-10-11 20:18,N-31 已销项)**:`FUN_PARA=0x08` 下跑 TC-NET-704(`PHASE7_MULTI_COUNT` 降到 5:E-23 未修,`NET_MAX_SOCKETS=8` 而 FTP/对时/远显三个监听常驻占 3 个 fd)→ **Phase 7 34/34 全通过(failed=0)**:5 路并发全部 ESTABLISHED、每路 64 B 回显字节级比对 OK;芯片 Socket 实测落在 **sock2 / sock4 / sock5 / sock6 / sock7**(0/1/3 被三个监听占用)→ **Socket 4~7 在 bit0=0 下完全可用**,与手册口径一致,定案成立。烧录后回读 `No difference found`。 - -⚠ **台架前置(本轮踩到,务必先排)**:① PC 的以太网适配器落在 **Public** 防火墙配置文件 → 入站 8081 被静默丢弃,症状是 TC701/702/704 **全部** `CONNECT TIMEOUT`(而 PC→设备 21 端口正常、ping 通、ARP 正常),**极易误判成"0x08 毁了 client"**;② 上一轮遗留的 `tcp_server` 进程仍占着 8081,新起的服务器绑不上端口却不报错。两条都排除(临时关 Public 配置文件 + 清残留 python 进程)后才拿到上面的结果——**判"芯片行为"前先确认对端真的收到了 SYN**(Trap 25 总纲铁律的又一次命中)。 - -### 发现时间 -阶段 7 扩展 8 Socket 测试,2026-08-25。 - -## Trap 20:发送缓冲区单次写入超过硬件 FIFO 上限(写穿) - -### 现象 -大文件/批量发送(如阶段 8 的 100KB,或 FTP 上传)时,数据在中间某处错乱、连接被对端 RST、或在 `WRITE_SEND_BUF` 后芯片不再产生 `SEND_OK`,表现为发送卡死或后续字节丢失。该问题只在单次发送长度 > 1KB 时稳定复现。 - -### 根因 -`net_send_locked`(TCP)与 `net_sendto`(UDP)在分块时以 **`NET_DMA_MAX_PAYLOAD`(2KB) / `NET_SEND_BUF_SIZE`(4KB)** 作为单次写长度上限,但 CH395F 每个 Socket 的**硬件发送 FIFO 只有 `CH395F_SEND_BLOCKS × CH395F_RAM_BLOCK_SIZE` = 2×512 = 1KB**(2026-09-22 起为固定布局 2×512=1KB,与 MSS 解耦,见 Trap 21;`ch395f.h`)。当某次调用 `net_send/sendto` 传入 >1KB 数据,驱动会在 `send_ready` 置位后一次性 `WRITE_SEND_BUF(2KB/4KB)`,直接**写穿 1KB 发送 FIFO**(超出部分覆盖/丢失或破坏 FIFO 指针),导致后续数据错位。原代码误把"SPI DMA 缓冲上限(2KB)"或"软件 API 缓冲(4KB)"当成硬件发送缓冲上限。 - -### 解决 -1. `ch395f.h` 新增硬件缓冲字节宏:`CH395F_SEND_BUF_SIZE_BYTES = CH395F_SEND_BLOCKS × CH395F_RAM_BLOCK_SIZE`(=1KB),并补充 `CH395F_RECV_BUF_SIZE_BYTES`。 -2. `net_socket.c` 新增 `NET_SEND_CHUNK_MAX = min(NET_DMA_MAX_PAYLOAD, CH395F_SEND_BUF_SIZE_BYTES)`,即**以硬件发送 FIFO 为硬上限**(当前 1KB < 2KB DMA 载荷,故生效 1KB)。 -3. `net_send_locked` 与 `net_sendto` 的单次写长度上限均由原来的 `NET_DMA_MAX_PAYLOAD`/`NET_SEND_BUF_SIZE` 改为 `NET_SEND_CHUNK_MAX`。`send_ready` 门控保证只有 FIFO 空闲时才写入,故写入 ≤1KB 安全(写满后等 `SEND_OK` 再写下一块)。 -4. 接收方向不受影响:`net_recv_locked` 只读 `ch395f_get_recv_len`(≤ 接收 FIFO 2KB)并受调用方 `len` 截断,不会写穿。 - -### 注意事项 -- 该上限随 `CH395F_SEND_BLOCKS`(2026-09-22 起固定 2 块=1KB,与 MSS 解耦,见 Trap 21);若将来发送缓冲改变,`NET_SEND_CHUNK_MAX` 自动跟随(仍取硬件缓冲与 DMA 载荷的较小者),无需改此处逻辑。 -- 调用方(阶段 8、FTP `lftpd`)仍可按任意长度调用 `net_send`,实际分块由驱动保证 ≤ 硬件 FIFO。 - -### 对应文件 -`Drivers/BSP/CH395F/ch395f.h`(`CH395F_SEND_BUF_SIZE_BYTES` 等);`Drivers/BSP/NET/net_socket.c`(`NET_SEND_CHUNK_MAX` + `net_send_locked`/`net_sendto` 分块上限)。 - -### 发现时间 -阶段 8 大文件传输实现,2026-08-26。 - -## Trap 21:接收缓冲"恰好填满 + 排空"窗口冻结(TC-NET-604)—— **结论:非缺陷(2026-09-23 结案)** - -### 结案结论 -原报告(阶段 8 大文件传输期间:同连接前序突发后 4096B 突发"前 2KB 正常回显、其余静默无回显")**在当前固件上无法复现,判定为非缺陷**。可信判据(产品固件 + 真实 lftpd FTP 上传,成功 = FTP 协议结果 + **上传后 RETR 回读比对 SHA-256**)实测:**227 轮全过**(8KB/64KB/256KB/1MB;含零间隔快速重连与 100 轮浸泡),并发生产 UI 流(`:7003`)600 帧 CRC 全过,设备侧日志无 FAIL/ERROR/WARN。 - -### 为什么原台架会"复现"——**教训,务必先读** -2026-09-22"精确定位轮"给出的**全部"冻结/停摆/WEDGE"结论均为台架假象,已撤回**: -1. **测试程序自吞数据**:台架配方里的固件内读者(Phase 9/11 的 64B 读、固件探针读)把对端数据读走后**不回显**——PC 端自然少收,被误当成"设备停摆"。实测短收量**恰等于被吞字节数**(例:`4736−4608 = 128 = 2×64` 探针读);所谓"V1 配方必冻结"就是 Phase 11 吞掉整包(设备日志 `avail=64 len=64`,PC 侧下一轮 `rx=0/64`)。对照组(仅 Phase 6、无额外读者)40/40 全过。 -2. **判读工具误判**:`probe_classify.py` A/C 模式把**通过**的运行报成 `STALL`(C0 的脚本层实测为 `1460/1460` + 尾段 `2100/2100`,且内容逐字节匹配)。 -3. 由此派生的"部分排空致命 / 再武装区间 ≥160ms / 相位刀口 / C 配方复现生产停摆"等结论**全部作废**;MSS=512 实验的"停摆"亦未经可信判据复核。 - -### 现在怎么做(台架纪律) -1. 判读一律用 `MDK-ARM/bench_verdict.py`:**以脚本层 STEP 行为真值**,并扣除固件读者吞噬的字节; -2. 冻结/卡死类问题走**真实生产路径 + 端到端内容校验**:`MDK-ARM/ftp_freeze_probe.py`(FTP 上传 + RETR 回读 SHA-256);卡住时用**控制通道 NOOP** 区分"数据面卡死"与"整层卡死"; -3. **禁止把"固件内非回显读者"与固定期望值混用在同一配方里**——这是本轮最大的教训。 - -### 保留的代码变更(与被撤回的机理无关) -缓冲块布局与 MSS 解耦(`ch395f.h`)、超限读 OVERSIZE 金丝雀(`ch395f.c`)保留:属防御性/可观测性改进,不回退。 - -### 相关 -- 真实缺陷见 **Trap 23**(上传中止后首笔复连上传被静默截断,已可信复现并修复); -- 发现:2026-08-26(阶段 8);结案:2026-09-23。 - -## Trap 22:快速复连丢 CONNECT/DISCONNECT 中断 → 监听静默死亡(2026-09-23 复现并修复) - -### 现象(生产固件实测) -单连接监听 socket 在快速复连后**永久失效**(后续 SYN 被拒或超时,10s 内不自恢复),而芯片/网栈其余部分完全正常(其他监听端口照常工作)。两种表现: -- **拒连(ConnectionRefused)**:芯片完成握手但 **CONNECT 中断未上报** → 控制块滞留 LISTENING;或本层因 **DISCONNECT 中断丢失**仍以为有客户端 → 两种情况都会让芯片侧那条连接**一直占住单连接监听**,后续 SYN 全被拒;且丢事件后本层收不到任何中断,无法靠事件自愈。 -- 应用侧连锁:`net_send` 被拒(send -1);或应用读不到 EOF(其 `g_client` 永不释放,因而**永不调用 accept**)。 - -### 复现(可信用例,纯客户端判据;工具 `MDK-ARM/tcp_reconnect_probe.py`) -- **确定性复现**:产品固件 + 待机 60s 后,对 UI 监听 `:7003` 以 300ms 间隔快速复连 → 修复前**首 2 次成功、之后永久拒连**(10 轮 2/10,10s 恢复窗口内无自恢复); -- **概率复现**:FTP 控制 `:21` 以 100ms 间隔连 50 次 → 修复前**第 29 轮起永久拒连**; -- 对照:FTP `:21` 在 0ms / 600ms 间隔下通过;`:8888`(对时)同期亦正常 → 触发与"复连间隔 <~1s + 轻负载(待机)"相关,与 2026-09-22 记录的边界一致。 - -### 机制(设备侧日志实证) -1. 芯片丢 **CONNECT** 中断 → 无 `standalone accept`,控制块滞留 LISTENING;或丢 **DISCONNECT** 中断 → 本层仍以为 ESTABLISHED; -2. 此后本层**收不到任何中断**(芯片静默持有/丢弃该连接),纯事件驱动无路可走——实测把 `handle_disconnect_event` 的 LISTENING 分支从"仅告警"改为"重建监听"后**仍然无效**(因为连 DISCONNECT 都没来); -3. 故必须**周期回读芯片状态对账**才能发现失同步。 - -### 修复(已实施,`Drivers/BSP/NET/net_socket.c`) -1. **周期对账 `net_reconcile_tick()`**(`net_process_messages` 每轮调用;周期 200ms):对在用 socket 回读芯片状态,修正三类失同步—— - ① 本层 ESTABLISHED 而芯片已无连接 → 按断开处理(应用读到 EOF 后自动重监听); - ② 本层 LISTENING 而芯片已完成握手 → 补做 standalone accept(复用 `handle_connect_event`); - ③ 本层 LISTENING 而芯片监听已消失 → 重建监听(`net_relisten_try`)。 - **去抖 + 冷却**:同一失配需连续 2 次(≈400ms)才动手;同一 socket 动手后冷却 1s。 -2. `handle_disconnect_event` 的 LISTENING 分支:由"仅告警"改为"回读芯片状态——已不可监听即重建"(仍在监听则判陈旧事件丢弃)。 - -### 踩坑记录(务必先读) -最初的修法放在 **accept 路径**(每次 accept 时回读状态并自愈):单次状态读会把"刚 open/listen 尚未生效"的瞬态误判为失配 → 监听被反复重建直至 `relisten give up` → **三个监听口全部死亡(风暴)**。改为"周期对账 + 去抖 + 冷却"后消除。 -**结论:凡基于单次状态读的重建动作,必须去抖。** - -### 验证(修复前 → 修复后) -| 用例 | 修复前 | 修复后 | -|---|---|---| -| 待机后 UI `:7003` 快速复连 ×10 | 2/10(之后永久拒连) | **10/10** | -| FTP `:21` 间隔 100ms ×50 | 28/50(第 29 轮起死亡) | **50/50** | -| FTP `:21` 间隔 0ms / 600ms ×50 | 通过 | **50/50 / 50/50** | -| UI `:7003` 间隔 200ms ×20 | — | **20/20** | -| FTP 正常上传 10×64KB(SHA-256 往返) | 通过 | **10/10** | -| 上传中止 RST / FIN 回归(Trap 23) | 通过 | **5/5 + 5/5** | -| 设备侧 `relisten give up` / 重建风暴 | — | **0** | - -自愈实测计数(整轮会话):`chip closed -> disconnect` **12** 次、`connection completed w/o CONNECT irq -> accept` **1** 次、`listen lost -> relisten` **1** 次——三类失同步均真实出现并被自愈;`relisten give up` 0 次。 - -### 对应文件 -`Drivers/BSP/NET/net_socket.c`(`net_reconcile_tick` / `handle_disconnect_event` LISTENING 分支);`MDK-ARM/tcp_reconnect_probe.py`(可信用例);`docs/任务规划/README.md` N-6(✅ 已销项,2026-09-26 清理)。 - -### 发现时间 -2026-09-22(台架偶发 1/1)→ 2026-09-23(生产固件确定性复现 + 当日修复闭环)。 - -## Trap 23:上传中止后首笔复连上传被静默截断(≤2048B)并误报成功 - -### 现象 -客户端上传(STOR)中途断开(用户取消 / 网络断;**RST 或优雅 FIN 均可**),**紧随其后的第一笔复连上传被截断到 ≤2048 字节(= 芯片单 socket RX 缓冲大小),且服务端回报 226 上传成功** → 客户端(ftplib 等)据此认为上传成功 = **静默数据损坏**;第二笔及以后恢复正常。 - -### 证据(可信复现:生产固件、零固件探针;工具 `MDK-ARM/ftp_abort_probe.py`) -- 判据 = FTP 协议结果 + **紧接一笔正常上传的 SHA-256 往返校验(STOR→RETR 比对)** + 控制通道 NOOP 存活探测; -- **RST 中止:10/10 复现**首笔复连上传截断(请求 4096B → 设备日志 `UPLOAD '/ab_ok.bin' 2048 bytes OK`); -- **优雅 FIN 中止:5/5 同样复现** → 与 RST/FIN 无关,凡"数据未发完即断开"均触发; -- 中止那一笔自身也被记为 `UPLOAD ... 2048 bytes OK` + 226(FTP 无长度声明,语义边界;但客户端会误判成功); -- **对照(无中止)**:177 轮(8KB/64KB/256KB/1MB,含 100 轮零间隔浸泡)全部 SHA-256 匹配(`MDK-ARM/ftp_freeze_probe.py`),并发 UI 流 600 帧 CRC 全过 → 中止路径是唯一触发条件。 - -### 机制(设备侧日志实证) -1. 中止时数据 socket 被释放(RST 时芯片上报 `sock4 TIMEOUT`),net 层随即 `auto-relisten ok` + `relisten retry ok`(**监听端口复用**); -2. 下一笔连接:`sock4 standalone accept` → `CMD STOR` → **紧接着一条陈旧 `sock4 DISCONNECTED, releasing (chip tcp=0x00)`**(旧连接收尾状态落在新连接上); -3. lftpd 数据读循环据此读到 EOF → 把已到的 2048B 当作整个文件 → `UPLOAD ... 2048 bytes OK` + 226; -4. 再下一笔连接时芯片状态已干净 → 正常(故为"一次性"而非持久故障)。 - -### 影响 -- **静默数据损坏**:上传被中断后"重试的第一笔"会生成截断文件且客户端收到成功回报(FTP 客户端通常不做长度校验)→ 现场可能表现为"上传的文件损坏/不完整"; -- 主风险面 = 生产路径 STOR(上传);RETR(下载)不涉及; -- 与 TC-NET-604 的关系:本条(Trap 23)是**独立、已可信复现并已修复**的真实缺陷;TC-NET-604 已结案为非缺陷(见 Trap 21)。现场"上传失败/文件不完整/像卡死"类报告,应优先用本条的判据复核:**FTP 协议结果 + 上传后 RETR 回读 SHA-256**(+ 卡住时控制通道 NOOP 探活)。 - -### 复现 -``` -& '.venv\Scripts\python.exe' MDK-ARM\ftp_abort_probe.py --rounds 10 --send 8192 --gap 1 -& '.venv\Scripts\python.exe' MDK-ARM\ftp_abort_probe.py --rounds 5 --send 8192 --gap 1 --graceful -``` - -### 修复方向(待办 N-8,全部闭环) -1. ✅(2026-09-23 一阶段)**relisten 前让芯片 socket 真正沉降**——隔离期:持续排空 RX + 主动 close + 等 `CLOSED/TCP_CLOSED`,隔离满 400ms 才 `open+listen`; -2. ✅(2026-09-23/24)**新连接的陈旧中断防御**——陈旧 DISCONNECT 以芯片状态回读甄别(D20:芯片 TCP 仍 ESTABLISHED 判陈旧丢弃)+ 释放残留经两级沉降/隔离(100ms/400ms)后不再投递给下一条连接; -3. ✅(2026-09-25)**应用侧兜底**——异常释放(TIMEOUT/RST)的挂起 recv 以 `NET_ERR_CONNRESET` 了结(正常 FIN 仍回 0——协议边界)→ STOR 异常结束回 450 并删除截断文件; -4. ✅ **回归判据**——`ftp_abort_probe.py` followup:RST 10/10 + FIN 5/5(一阶段)、8/8 + 5/5 + 5/5(二阶段);`--check-abort` 断言中止 STOR 应答码(RST→450 / FIN→226 边界);177 轮对照当时全匹配,其后各轮正常路径回归全过。 - -### 发现时间 -2026-09-23(可信复现首轮:真实生产路径 FTP 中止/复连场景,由 `MDK-ARM/ftp_abort_probe.py` 自动判定) - -### 修复实施与验证(2026-09-23 当日闭环) - -**真因(设备侧日志实证,含正常对照)**: -1. 客户端中止上传时对端**仍有在途数据**;芯片给出 `sock4 TIMEOUT`(此刻 MCU 侧 `recv=0`),net 层随即释放该 socket 并复监听; -2. 那些**在途段随后才落进芯片 RX 缓冲**(约一个缓冲 = 2048B 残渣),而正常传输时 DISCONNECT 到达刻芯片里也仍有数据(对照实测 `recv=64512/65536`,由应用读空 → 完整); -3. 中止后立即 `open+listen`,残渣被**下一条连接当作自己的数据读走**:新上传只“收到”2048B 就被判定结束并回报 226(首笔复连必中招,第二笔起芯片已干净 → 恢复正常)。 - -**修复(`Drivers/BSP/NET/net_socket.c`,仅异常释放路径,正常 FIN 路径零改动)**: -1. `handle_timeout_event`(ESTABLISHED 超时释放)置 per-fd **隔离标记** `s_relisten_quarantine[]`; -2. `net_relisten_locked` 对带隔离标记的 socket:**隔离期内持续排空芯片 RX**(新增 `net_drain_chip_rx()`,分块读空、零新增静态缓冲)+ **主动 `ch395f_close_socket` 并排空该 socket 中断** + 等芯片回到 `CLOSED/TCP_CLOSED`,隔离满 `NET_RELISTEN_QUARANTINE_MS`(400ms) 才 `open+listen`; -3. 上限 `NET_RELISTEN_SETTLE_MS`(800ms) 强制放行 —— **必须小于** `NET_RELISTEN_DEADLINE_MS`(2000ms):实测取 2000ms 会把重试预算耗尽 → 重试轮询"give up" → **监听器死亡(:21 拒连)**,已修正并在注释中留痕; -4. 隔离状态在 `net_relisten_clear` 内一并清除(关闭/新建 fd 时不残留)。 - -**验证(可信判据:FTP 协议结果 + 紧接一笔上传的 SHA-256 往返 + 控制通道存活)**: -- 修复前:RST 中止 **0/10**、优雅 FIN 中止 **0/5**(首笔复连上传全部被截断且回报成功); -- 修复后:RST 中止 **10/10**、优雅 FIN 中止 **5/5** 全部通过; -- 正常路径回归:30×64KB 零间隔 **30/30**、20×64KB **20/20**,SHA-256 全匹配; -- 启动门禁:`App init OK` / `net init: OK`、FORBID 0 行。 - -### 第二阶段:正常释放(FIN)路径——边界甄别与 100ms 沉降(2026-09-24) - -**边界再甄别(现场"1~2s"时间假设被推翻)**:中止后 gap 500/2000/5000ms 台架复测 → 设备实存尺寸**全部仍 2048**(各 4/4,合计 12/12)→ **与时间无关**;监听重开本征即时(`t_accept` 30~47ms)。真实边界: - -| 维度 | 结论 | 证据 | -|---|---|---| -| 触发 | 设备侧**收过数据**的数据连接被对端关闭(纯 FIN 即触发,不要求 RST) | 纯 `close()`(FIN)4/4 复现;RST 变体 5/5 | -| 受影响路径 | 仅**正常 DISCONNECT 释放路径**缺芯片状态沉降 | RST(TIMEOUT)路径已有一阶段 400ms 隔离保护 | -| 投递形式 | 芯片上一条连接的收尾残留,投递给**复用同一 fd 的下一条**连接(accept 后约 10~20ms 一条伪 DISCONNECT) | 设备日志:`standalone accept` → `DISCONNECTED, releasing (chip tcp=0x00)` | -| 影响面 | **一次性**(首条新连接中招,第二条起干净);同 fd 非 FTP 服务(`:7003` UI sink)不受影响(其 socket 不按传输复用) | 一次性 3/3 全对;`:7003` 复用 8/8 全过 | -| 时间 | 无关(gap 5s 仍失败)→ 现场"1~2s"只是人工重触的常见窗口,不是芯片机制 | gap 500/2000/5000ms 共 12/12 全部失败 | - -**修复(第二阶段,`Drivers/BSP/NET/net_socket.c` +36/−7,netTask 串行上下文,零新增缓冲)**: -1. relisten 前沉降分两级:异常释放(芯片 TIMEOUT)=清理 + 最短等 400ms(`NET_RELISTEN_QUARANTINE_MS`,一阶段行为不变);正常释放(芯片 DISCONNECT)=清理 + 最短等 **100ms**(`NET_RELISTEN_GRACEFUL_MS`,新增); -2. 正常释放沉降**仅在本连接收过数据时登记**(`recv_bytes != 0`):纯连接即关(Trap 22 的 0ms 复连用例)与只发不收的下载连接无残留 → 免沉降保住快速复连时序; -3. 日志区分 `relisten settle`(正常)/ `relisten quarantine`(异常),便于台架分辨触发路径。 - -**沉降值实测悬崖**(只清理不等待 = 0ms):0ms **无效**(4/4 仍截断)→ 30ms 仍失败(4/4)→ **100ms 通过**(多轮稳定)→ 取 100ms(对 30ms 重试节拍 `NET_RELISTEN_RETRY_MS` 留 3× 余量)。 - -**验证(第二阶段后;设备日志 `UPLOAD '' N bytes OK` 为权威真值)**: - -| 用例 | 修复前 | 修复后 | -|---|---|---| -| FIN 关闭后**首笔**上传 ×8 | **0/8**(实存 2048 + 回报 226) | **8/8**(实存 4096,sha OK) | -| 纯 `close()` 变体 ×4 | 中招 | **4/4** | -| `ftp_abort_probe --graceful` ×5 | **0/5** | **5/5** | -| `ftp_abort_probe`(RST)×5 | 5/5(一阶段) | **5/5** | -| 无中止对照 ×4 / 一次性连发 3 笔 ×1 | 4/4 / 第 1 笔坏 | **4/4 / 3/3** | -| 正常往返 freeze 4096×4 / 64KB×2 | 通过 | **4/4 / 2/2**(t_stor 0.68→**0.18s**,t_retr 1.13→**0.19s**) | -| 非 FTP `:7003` sink 复用 ×8(RST+FIN) | 8/8 | **8/8** | -| Trap 22:`:21` 0ms×50 / 100ms×50,`:7003` 300ms×10,`:8888` 0ms×20 | 50/50 / 50/50 / 10/10 / 20/20 | **50/50 / 50/50 / 10/10 / 20/20** | -| 启动门禁 / 控制台 / 堆 | — | `App init OK` / `net init: OK`,FORBID **0**,`CONSOLE dropped 0`,`heap free=8776 min_ever=4816` | - -**结论**:缺陷位于 **fd 复用生命周期**(芯片上一条连接的残留状态投递给下一条连接),不在 FTP 命令逻辑——lftpd 只是把它放大成误报 226。RST/TIMEOUT 半边由一阶段隔离处理,DISCONNECT 半边由本阶段沉降处理,合计覆盖全部"收过数据后关闭"的释放路径。已知残余边界:中止那一笔 STOR 自身仍回报 226(STOR 无长度声明、在途 FIN 与正常完成不可区分)——后续硬化见待办 #3。 - -**对应文件**:`Drivers/BSP/NET/net_socket.c`(`handle_disconnect_event` 沉降登记 + `net_relisten_locked` 两级沉降);`MDK-ARM/ftp_abort_probe.py` / `MDK-ARM/bench_diag/bench_diag_abort_gap.py`(边界甄别与回归判据)。 - -### 后续硬化(2026-09-25):异常结束回 5xx(待办 #3 闭环) - -**目标**:中止那一笔 STOR 自身不再误报 226——异常结束(连接重置/超时)时回 450 并删除截断文件,把"静默损坏"变成"显式失败"(纵深防御;客户端按协议重试)。 - -**实现(3 个工程文件 + 1 个台架脚本)**: -1. `net_types.h`:`net_sock_t` 增加释放来源标记 `released_abnormal`(RAM +8B:1B×8 socket,BSS,运行堆零影响); -2. `net_socket.c`:`handle_timeout_event`(异常释放/TIMEOUT)置 1;`handle_disconnect_event`(正常释放/DISCONNECT)置 0;`net_socket()`(新 fd)与 `net_relisten_locked` 成功尾(新监听)清零——新连接不得继承上一条连接的释放原因; -3. `net_socket.c` `net_pending_tick` 断连了结(③):排空缓冲后,标记为 1 以 `NET_ERR_CONNRESET` 了结(对端未发正常 FIN,不是正常 EOF),标记为 0 仍 EOF(0); -4. `lftpd.c` `receive_file`:`err < 0` 时 `unlink` 截断文件(原先残留于盘)并返回错误码 → STOR 处理器回 450; -5. `MDK-ARM/ftp_abort_probe.py` 增加 `--check-abort`:RST 中止要求中止 STOR 控制应答 4xx/5xx,优雅 FIN 要求 2xx(协议边界,见下)。 - -**协议边界(不可判定,留痕)**:FTP STOR 无长度声明,"在途 FIN 关闭"与"正常完成"**逐字节不可区分**(芯片均报 DISCONNECT、缓冲均排空)→ 优雅中止仍回 226(中止方客户端通常也不等应答);仅 RST/掉链(芯片 TIMEOUT)可识别 → 450。FIN 边界由客户端长度校验(SIZE/RETR 往返)兜底。 - -**各消费端负值处理复核**(均按"负值=结束"与 0 同路,行为不变): -- lftpd 控制通道 `read_line`:负值 → `line_len != 0` → 会话清理,与 0 同路(控制通道不 close,auto_relisten 恢复); -- time_sync(`:8888`):n<0 → break 弃连接,与 0 路径一致;8B 时间包已收齐时仍写 RTC; -- UI sink(`:7003`):DONTWAIT 非阻塞,不走 L3 pending,零影响; -- 测试套件:产品构建不启用;测试对端干净关闭,无影响。 - -**验证(硬化后,2026-09-25 台架)**: - -| 检查项 | 结果 | -|---|---| -| `ftp_abort_probe --check-abort --rounds 5`(RST) | **5/5**——中止 STOR → **450**(原 226),followup 5/5,控制存活 5/5 | -| 设备侧证据 | `sock4 TIMEOUT` → `[ERR][FTP] UPLOAD '/ab0001.bin' FAILED` → 450;followup `UPLOAD '/ab_ok.bin' 4096 bytes OK` | -| `--check-abort --graceful --rounds 5`(FIN) | **5/5**——中止 STOR → **226**(边界不变),followup 5/5 | -| 正常往返 freeze 4096×4 / 64KB×2 | **4/4 / 2/2**(SHA-256 全 MATCH) | -| Trap 22:`:21` 0ms×50 / 100ms×50,`:7003` 300ms×10,`:8888` 0ms×20 | **50/50 / 50/50 / 10/10 / 20/20** | -| 非 FTP `:7003` sink 复用(RST×4 / FIN×4,400 帧在途) | **4/4 / 4/4**(ack 2000/2000,LCD 3209/3209,CRC 全过) | -| 时间同步 `:8888` 真实收包 | `wall clock updated (epoch=…)` 写 RTC 成功,监听恢复 | -| 构建 / 回读 / 启动门禁 / 控制台 / 堆 | 0 错 0 警、`No difference found`、FORBID 0、`CONSOLE dropped 0`、`heap free=8776 min_ever=4816`(不变) | - -**对应文件**:`Drivers/BSP/NET/net_types.h`(`released_abnormal`)、`Drivers/BSP/NET/net_socket.c`(三处置位/清除 + `net_pending_tick` 了结分支)、`Drivers/BSP/NET/lftpd/lftpd.c`(`receive_file` 错误路径 unlink)、`MDK-ARM/ftp_abort_probe.py`(`--check-abort`)。 - ---- - -## Trap 24:MAC 写入顺序违反`docs/芯片手册/CH395F.md` §9.2.1(SET_MAC 必须在 INIT_CH395 之前) - -### 现象(静态审查发现;手册确证) -`net_init()` 原顺序为 `ch395f_init()`(CMD_INIT_CH395)→ `ch395f_set_mac_addr()`,与`docs/芯片手册/CH395F.md` §9.2.1 规定的初始化顺序**相反**。后果:INIT_CH395 已按 EEPROM 旧值加载 MAC,其后写入 EEPROM 的新 MAC 本次上电**不生效**(回落芯片内置/上次值,实际"滞后一次上电")→ UI 配置的 MAC 与设备实际在线 MAC 不一致,设备在线身份错误、网口排查困难。本轮未做"UI 改 MAC + 重启"台架复现,结论由手册条文确证。 - -### 根因 -`docs/芯片手册/CH395F.md` §9.2.1「初始化 CH395,必要操作」顺序为: -① `SET_MAC_ADDR` → ② `SET_IP_ADDR` → ③ `SET_GWIP_ADDR` → ④ `SET_MASK_ADDR` → ⑤ `INIT_CH395`。 -- §5.7:`SET_MAC_ADDR` 把 MAC **写入芯片内部 EEPROM**(约 30us); -- §5.13:`INIT_CH395` **初始化 CH395 的 MAC、PHY 以及 TCP/IP 协议栈**(约 5ms)。 - -即 INIT_CH395 会从 EEPROM **加载** MAC,故 SET_MAC 必须先于 INIT_CH395。原码把 MAC 放在 INIT 之后。 - -**加剧误排的注释**:`ch395f.c` 原把 `set_mac_addr`/`set_ip_addr`/`set_gwip_addr`/`set_mask_addr` 的限定条件标为"芯片已初始化"——与手册矛盾(这四个 SET 实际都在 INIT 之前下发),易被误读为"须在 init 之后调用"。 - -### 解决 -1. `Drivers/BSP/NET/net_socket.c` `net_init()`:把 `ch395f_set_mac_addr()` 从 `ch395f_init()` **之后**移到**之前**(置于 SET_IP/GWIP/MASK 之前,对齐手册步骤①); -2. `ch395f.c`:四个 SET 函数的限定条件注释统一改为"必须在 `ch395f_init()` 之前调用"; -3. `net_socket.h`:`net_init` 注释同步(MAC 于协议栈初始化**之前**下发)。 - -### 影响面 -- 仅 `net_init()` 一处调用(产品路径);改动为**纯顺序调整**,无新增/删除指令、无 RAM 变化; -- `net_stack_bring_up()` 在 `net_init` 后回读芯片 MAC 写 UI 影子(`net_cfg_set_active_mac`)——修复后回读值 = 实际在线 MAC,UI 显示与线上身份一致。 - -### 对应文件 -`Drivers/BSP/NET/net_socket.c`(`net_init` SET_MAC 顺序)、`Drivers/BSP/NET/net_socket.h`(`net_init` 注释)、`Drivers/BSP/CH395F/ch395f.c`(四个 SET 的限定条件注释);`docs/网络/BSD_Socket_API_函数调用关系.md`(§4.1 / §7 顺序)。 - -### 发现时间 -2026-10-04(静态审查项 P1-7;依据`docs/芯片手册/CH395F.md` §9.2.1 / §5.7 / §5.13 定案)。 - -## Trap 25:FTP 数据连接三连故障——丢 CONNECT 中断 / accept 竞态自毁缓冲 / 背靠背小段毒化 TX(N-30,2026-10-11 定案并修复,回归 4 轮 11/11) - -### 现象(生产 FTP 回归,多轮复现) -同一 FTP 会话内**第 2 次及以后**的数据连接(LIST/STOR/RETR 各一次)出现两类失败,两种面孔同一根因: -- **吞数据(SWALLOW)**:新数据连接建立成功(芯片 0x05/0x04 ESTABLISHED)、服务端 `net_send` 返回成功,客户端 20 s 收不到任何字节(无 RST、无 DISCONNECT,直到客户端放弃); -- **425**:新数据连接被接受后 ~10-20 ms 内出现 DISCONNECT(`chip tcp=0x00`),服务端 accept 失败 → `425 Can't open data connection`。(⚠ 本条当时的解读"芯片自杀连接"**有误**,见文末"三次定案"——实为客户端发完数据后的**正常 FIN** + 我方 accept 竞态自毁缓冲。) - -失败概率随**上一条数据连接的负载大小与间隔**变化(NAND 目录文件越多 LIST 越大越易触发),与提交历史无关——在 D20 基线 e5733b34 上以快速连发同样复现(D20 时代"11/11 通过"是慢速台架 + 干净目录的运气)。 - -### 定案实验(裸机芯片台架,剥离 OS/net/FTP) -实验分支 `n30-chip-bench`(worktree),`test/board/ch395f_reuse_bench.c` 在**内核启动前**直接调 `ch395f_*` 做受控矩阵:`{socket, close 后复开延迟, TX 长度, 关闭顺序}` 组合循环,每 50 ms 打印芯片两字节状态 + 中断寄存器;主机侧 `test/host/ch395f_reuse_host.py` 同步做纯 TCP 客户端。**OS/net/FTP 全部剥离后 bug 依旧复现** → 纯芯片操作时序问题,非上层逻辑、非芯片硬件缺陷。 - -关键现场(芯片日志): -``` -post-listen st=0x05/0x04 ← listen 返回成功,但 TCP 态停在旧连接的 ESTABLISHED(幽灵态) -unexpected int=0x10 while wait ← 只等来旧连接滞后的 DISCONNECT -no CONNECT within 10s ← 新连接的 CONNECT 中断永远没来(芯片状态却已是 0x04) -``` - -**芯片级规律**:CH395F 关闭连接后的拆链收尾窗口**变长且不可观测**——状态寄存器显示 CLOSED 持续 1 s 以上也不代表拆完(实测窗口可 >2 s 且时长不定)。窗口内重新 `open+listen`: -1. 新连接握手照常完成(状态寄存器 → ESTABLISHED)但 **CONNECT 中断被丢**(Trap 22 丢中断机制的窗口期形态)→ 事件驱动的 accept 永远等待 → 吞数据; -2. 或 listen 后状态直接假报 0x04(幽灵态),此时回读对端端口 = **旧连接端口**; -3. 或旧连接滞后的 DISCONNECT 打到新监听 → 触发 relisten 毁掉刚到的新连接 → 425。 - -### 已证伪的修复(全部经台架受控实验) -| 尝试 | 结果 | -|---|---| -| close 后固定沉降 100/300/800/2000/3000 ms | 仍挂(2 s 也挂过)——窗口变长,固定等待无保证 | -| Socket 轮换(sock3↔sock4 交替) | 仍挂——上一轮拆链尾巴跨 socket 毒化下一轮 | -| listen 后 close+重试 / 耐心等幽灵消退 | 持平对照组,无效 | -| 收发缓冲重分配(clear_recv_buf + set_*_buf) | 无效 | -| close 前 200 ms TX 排空延时 | 无效 | -| 禁用对账 tick(验证因果) | 仍挂——对账不是元凶 | - -### 有效修复(台架修复组 15/15,机制触发 5 次全部救回) -**双信源原则:中断只是快路径,状态寄存器才是真相。** 等连接时不能只等中断: -1. **accept 路径内联探针**(`net_lost_connect_probe`,`net_accept_locked` 每 100 ms 轮询挂探针):芯片状态已 ESTABLISHED 而 CONNECT 中断未到 → 连续 2 轮去抖后补做 accept(复用 `handle_connect_event` 簿记)。 -2. **幽灵态甄别**:补 accept 前回读对端端口(`GET_REMOTE_IPP`),**端口 = 上一条连接端口(`last_peer_port`)→ 幽灵,不 accept**——否则会把数据发给已死的旧连接(这正是生产 LIST#4"accept 成功但数据被吞"的直接成因:对账②曾把幽灵连接补 accept 了)。 -3. **对账②同步加幽灵甄别**(`net_reconcile_tick` 类型②分支同样比对 `last_peer_port`)。 -4. `last_peer_port` 登记点:`handle_disconnect_event` / `handle_timeout_event` / `net_relisten_try`(清零 remote_port 前转登)/ `net_close_locked`(跨 memset 保留——幽灵属芯片 Socket 级,与 fd 复用无关)/ NET_MSG_FIX 陈旧 ESTABLISHED 修复路径。 - -### 与 Trap 22 的关系 -Trap 22 的周期对账(200ms 节拍 + 400ms 去抖 + 1s 冷却)设计目标是"自愈兜底",对常驻监听器有效;但 FTP 数据通道是"活 1 秒就死"的短命监听器,且对账②**无幽灵甄别**——它既救不全(冷却期内 accept 已超时)又反过来帮凶(把幽灵补 accept)。Trap 25 把"看状态补 accept"内联进 accept 热路径(短去抖、无冷却)并补上端口甄别,对账②保留为兜底网。 - -### 对应文件 -`Drivers/BSP/NET/net_socket.c`(`net_lost_connect_probe` / `net_accept_locked`(含 closed-with-data accept)/ `net_reconcile_tick`② / `last_peer_port` 各登记点)、`Drivers/BSP/NET/net_types.h`(`last_peer_port` / `lost_conn_seen` 字段)、`Drivers/BSP/NET/lftpd/lftpd.c`(`send_list`/`send_nlst` 批量发送);台架工具 `test/board/ch395f_reuse_bench.{c,h}` + `test/host/ch395f_reuse_host.py`(实验分支 `n30-chip-bench` 注册编译,主分支只存档不注册)。 - -### 验证(台架层) -`ch395f_reuse_bench` 修复组(丢中断捕获自 accept)15/15,机制触发 5 次全部救回(对照同表 4/15 挂)。⚠ 生产回归当时未达 11/11——生产主导模式与台架不同,见下两节。 - -### 三次定案:所谓"kill 模式"是寄存器状态误读(2026-10-11 当日终案,双向验证) -第二轮验证(仅双信源探针修复,4 轮:1/11、5/11、2/11、2/11)暴露 STOR #2 仍 100% 失败,当时解读为"芯片在 accept 后 10-20 ms 杀连接"。后经**手册条文 + 网络资料双向核对**推翻: - -**寄存器读数本身全部正确**(`GET_SOCKET_STATUS_SN` §5.22:`05H=SOCKET_OPEN/00H=SOCKET_CLOSED`;第二字节 TCP 态 `00H=CLOSED/04H=ESTABLISHED`,与 WCH 官方数据手册表 5-8/5-9 一致;`GET_RECV_LEN_SN` §5.33 低字节在前,驱动实现一致)。**错在对 DISCONNECT 事件语义的解读**: - -1. `net_init` 的 `ch395f_set_fun_para(0x08)` 置 **FUN_PARA_FLAG_SOCKET_CLOSE=1**(§5.52 ②):产生 DISCONNECT/TIMEOUT 后芯片**不关 Socket、不清缓冲**,就是留给单片机把残留数据读出后再关 Socket。 -2. 判别探针现场:`kill-check recv_len=1027 re-read st=0x05/0x00`——Socket 仍 OPEN、TCP 已 CLOSED、**RX 缓冲 1027 字节 = STOR 载荷一个不少全在**。这是芯片按 §5.52 规格**保留现场**,不是杀连接。 -3. 那个 DISCONNECT 是**客户端的正常 FIN**:Python ftplib `storbinary` 语义 = `sendall` 发完立即关数据连接,然后回控制通道等 226——标准 FTP 行为。局域网小文件下"连接→发完→关闭"在 ~5 ms 内完成。 - -**425 真根因(竞态 + 自毁缓冲)**:客户端 5 ms 内完成发送并关闭;`cmd_stor` 的 accept RPC 赶到 net 层时 MCU 态已是 CLOSED;旧 `net_accept_locked` 把 CLOSED 一律当"无连接" → `net_relisten_try`(close+open **清空芯片 RX 缓冲**)→ 1027 字节被**我方亲手销毁** → WOULDBLOCK → lftpd 判"数据套接字已关" → 425。芯片全程无辜(此模式)。 - -**有效修复(已落地,`net_accept_locked` CLOSED 分支)**:CLOSED + auto_relisten + **芯片 RX 缓冲有数据**(`GET_RECV_LEN_SN`>0)→ 接受这条"已关但可读"连接(日志 `closed-with-data accept (drain pending)`),应用经 `net_recv_locked` 快路径(无状态门槛)排空,排空后 EOF 自然触发 relisten——完全对齐 §5.52 ②"读出残留数据后再关闭"的芯片契约。**验证:修复后 4 轮回归 STOR #2 从 4/4 失败 → 4/4 通过**(7/11、6/11、9/11、5/11,失败项中已无 STOR)。 - -**连带修正**:台架 mode0(主机先关)观察到的"accept 后 10-20 ms DISCONNECT"大概率同为误读——那是主机收完数据自己关的。"拆链窗口杀连接"理论对生产 425 不成立;台架**丢 CONNECT 中断模式**(主机侧 got=0 + 芯片 0x04 + 无中断,主机端独立证据)仍然成立,双信源探针修复保留。 - -**当时的遗留**:LIST #4/#7 超时(服务端 `net_send` 目录、客户端 20 s 收 0 字节、无 DISCONNECT)——发送方向的真问题,与上述 425 机制不同——已由下节"四次定案"解决。 - -### 四次定案:LIST 吞包 = 背靠背小段写毒化芯片 TX(2026-10-11 终案,修复后 8/8 通过) -探针定位过程(全部生产固件 + 临时探针,一轮复现): -1. PASV→accept 间隔 2 s(`net_listen_locked` open 重试在跑)→ 曾疑"listen 撞拆链窗口"; -2. accept 正常、CONNECT 正常、发送后中断位 `0x03`(SENBUF_FREE+SEND_OK)——**芯片自认发送成功**; -3. `GET_REMOTE_IPP` 对端 IP:port 完全正确(排除对端混淆); -4. **Socket 轮换**(服务端先关后 4 s 冷却、数据连接轮转 sock3~7):**证伪**——LIST 换到全新 sock5 仍 100% 挂(芯片行为与失败前完全一致); -5. **决定性对照**:同一目录同一载荷,LIST #1(会话首连,FAT 冷、readdir 慢、行间天然间隔大)通过;LIST #4/#7(FAT 热、每行一个 `net_send` 背靠背小段)必挂。 - -**定案**:`send_list`/`send_nlst` 每行一次 `net_send`(~60 B 小段、间隔仅 ms 级)——CH395F 对同一连接的**背靠背多段小写入**会把 TX 整条连接毒化(芯片报 SEND_OK 但对端协议栈全部丢弃,无 RST 无 DISCONNECT)。`net_send_locked` 的逐块协议(等 SENBUF_FREE 再写下一块)本身合规(§5.45),但芯片对该模式的内部处理不可靠;RETR 的 1 KB 满块连发不受影响(实测通过)。 - -**修复(`lftpd.c`)**:`send_list`/`send_nlst` 整份目录**一次性批量发送**(static 1536 B 缓冲,snprintf 累积 + 单次 `lftpd_inet_write`;溢出截断判 -1→550)。注意 `lftpd_inet_write` 成功返回**字节数**非 0(首版误按 0=成功判 → 全 550,已纠)。 - -**验证**:修复后 ftp_regression 连跑 **4 轮 11/11**(终版:批量发送 + closed-with-data accept + 丢中断探针 + 幽灵甄别,无轮换无探针)+ `ftp_auth_bench.py` **13/13** + `@test` 启动标记 PASS;此前同条件 LIST #4/#7 为 100% 失败、STOR #2 曾 100% 425。轮换冷却机制随证伪**已撤**(不留未证明的复杂度;其 5 轮 11/11 一并存档备查)。 - -**教训(Trap 25 总纲)**:本轮先后误读三次——把客户端正常 FIN 读成"芯片杀连接"、把 open 重试读成"拆链窗口"、把芯片假 SEND_OK 读成"数据已上线"。铁律:**CH395F 的"成功"(中断位/状态码)只代表芯片本地动作完成,不代表对端收到;判定必须回到对端可见证据(主机侧收发)+ 手册语义(FUN_PARA §5.52、状态码 §5.22)**。 - -**顺带发现的疑点(2026-10-11 已定案)**:`set_fun_para(0x09)` 的 bit0——§5.52 表明确写 **bit0 保留、必须为 0**,而 `net_init` 注释声称"bit0=1 多 Socket 模式"(手册与网络资料均无此说法,CH395 的 8 Socket 是芯片原生能力,靠 Socket 索引 `_SN` 寻址)。现固件 bit0=1 下 sock1/2/3 多路并行正常 → 该位被芯片忽略。**定案:改回 `0x08`**——官方手册 V1C/V1E/V2.2/V2.3 的 bit0 全是保留位,官方例程 bit0=0 下 8 个 Socket 照常可用(`EXAM5_TCP_Server` 传 0x02 却给 Socket 0~7 配缓冲),官方 FTP 例程本身就传 0x08。完整证据链见 Trap 18「定案更正」;**0x08 下的多 Socket 已台架实测通过**(2026-10-11 20:18,Phase 7 34/34,并发回显落在 sock2/4/5/6/7,N-31 已销项)。 - -### 发现时间 -2026-10-11(登记簿 N-30;裸机芯片台架定案实验见本节,实验分支 `n30-chip-bench`)。 - ---- - -## 2026-10-11 寄存器 / 数值口径全量审计(智能体团队,只读) +# 第六章 2026-10-11 寄存器/数值口径全量审计(A/B 系列) **方法**:四路并行——驱动层逐命令(含官方 SDK 逐字对照)、NET/测试层逐调用点、Trap 01–12 复核、Trap 13–25 复核。三源交叉:`docs/芯片手册/CH395F.md` + WCH 官方例程 `CH395EVT/EXAM/PUB/CH395CMD.c`、`CH395INC.H` + git 历史。 -### 干净清单(核对过、无异常) +## 干净清单(核对过、无异常) | 项 | 计数 | 结论 | |---|---|---| @@ -960,59 +338,83 @@ Trap 22 的周期对账(200ms 节拍 + 400ms 去抖 + 1s 冷却)设计目标 | 16/32 位量字节序 | 9 处 | 全部低字节在前,与手册明文 + SDK 逐字节一致 | | 读命令输出字节数 | 14 条 | 无少读/多读(`GET_SOCKET_STATUS`=2、`GET_IP_INF`=20 且顺序 IP→GW→MASK→DNS1→DNS2、`GET_UNREACH`=8、`GET_REMOT_IPP`=6) | | 常量表 | 60 个 | `ERR_*`18 / `TCP_*`11 / `SOCKET_*`2 / `PHY_*`6 / `PROTO_*`4 / `SINT_STAT_*`6 位号全对;唯一错值见 A3 | -| **`GET_GLOB_INT_STATUS_ALL` 拼装方向** | — | **无异常**:我们 `第1字节\|第2字节<<8` 与官方 `CH395CMDGetGlobIntStatus()` 逐字相同;`glob & (1<<(sock+4))` = §5.16 bit4..bit11 = SDK `GINT_STAT_SOCKn` | -| `get_socket_status` 缓冲区 | 17 处调用点 | 全部 `uint8_t st[2]`,无 1 字节残留(`db64e6ac` 引入 → `e5733b34` 修复,HEAD 无回归) | +| `GET_GLOB_INT_STATUS_ALL` 拼装方向 | — | 与官方 `CH395CMDGetGlobIntStatus()` 逐字相同;`glob & (1<<(sock+4))` = §5.16 bit4..bit11 | +| `get_socket_status` 缓冲区 | 17 处调用点 | 全部 `uint8_t st[2]`,无 1 字节残留 | | 中断判据 | 17 掩码 / 32 全局读 / 27 SINT 读 | 位号与 §5.16/§5.23 一致;只用 16 位 ALL 版 | -| 单位与取值范围 | 12 个参数点 | retrans 8/200 ms、KeepAlive 60000/5000(500 倍数、IDLE>INTVL)、ARP 100 ms 单位、MSS 1024 全部合规 | +| 单位与取值范围 | 12 个参数点 | retrans 8/200ms、KeepAlive 60000/5000、ARP 100ms 单位、MSS 1024 全部合规 | | 缓冲布局 | 3 处组合 | `net_init` 8×(4收+2发)=48 块恰好用满、无交叠(零裕量) | -| 显示假象 | 10 处 `%d.%d.%d.%d` | 只有已定案的 `net_socket.c:1631` 是 MSB 重排假象,无新增同类 | +| 显示假象 | 10 处 `%d.%d.%d.%d` | 只有已定案的 `net_socket.c` getsockname 一处是 MSB 重排假象(见 B5),无新增同类 | -### 新发现(去重后按严重度;已修 = 本次提交内) +## 新发现(去重后按严重度) | # | 严重度 | 位置 | 问题 | 状态 | |---|---|---|---|---| -| A1 | 中 | `test/board/ch395f_test_task.c:1005` | TC-203 的 Trap 14 反核实写成 `st_chk[0] == CH395F_TCP_ESTABLISHED`——byte0 只有 `00H/05H`,条件**恒假** → 该防线永久失效且日志无痕(与 Trap 07 同类:byte0/byte1 语义越界) | ✅ 已修(改 `byte0==OPEN && byte1==ESTABLISHED`) | -| A2 | 中 | 同上 `:1150` / `:1321` | Phase 3/4 把 `GET_RECV_LEN` 返回值直接当读取长度(本地 `rx_buf[300]`/`[64]`,芯片 RX 缓冲 2 KB)→ 栈溢出 | ✅ 已修(夹到 `sizeof` + 留痕) | -| A3 | 低 | `ch395f.h:253` | `CH395F_GINT_STAT_DHCPV6 = 0x8000`(bit15);SDK 是 `(1<<12)`、§5.16 明确 CH395F 的 `[12:15]` 保留 | ✅ 已修(改 `0x1000` + 注明本芯片恒 0;全仓零引用) | -| A4 | 低 | `ch395f.h:33-91`、`ch395f.c:484/506`、`ch395f.h:701` | 分组注释字节数错 4 处(`GET_GLOB_INT_STATUS_ALL`/`GET_UNREACH_IPPT` 归"1 字节输出"、`SET_FUN_PARA` 归"1 入 1 出"、`SET_RETRAN_PERIOD` 归"3 字节输入"、`GET_SOCKET_STATUS_SN` 归"1 字节输出");KeepAlive 参数注释写"秒"(§5.53/5.54 是 ms);`set_retrans_count` 写"0=默认值5"(§5.17 默认 12、无 0=默认语义)。**实现全部正确,注释误导**——Trap 25 的 1 字节缓冲栈越界正是这类注释发起的 | ✅ 已修 | -| A5 | 低 | `net_socket.c:238/1005/1080/1297/1666/708/3056` | 注释与手册/HEAD 相反:`FUN_PARA(SOCKET_CLOSE=0)`(实为 bit3=1,正是 closed-with-data accept 的契约依据)、"初始化成功后设置 MAC"(§9.2.1 ① 是之前)、"复用默认缓冲"(已 8 Socket 显式分配)、"布局由 MSS 推导"(固定布局、Trap 21 已解耦)、"等 SEND_OK"(门控是 SENBUF_FREE)、UDP 头 `[0-1] reserved`(§9.2.4 是数据长度) | ✅ 已修 | -| A6 | 低 | `ch395f.c:1176` | `write_send_buf` 超限**静默 return**(void、无日志),且上限取 DMA 2048 而非发送缓冲 1024;读路径有 OVERSIZE 金丝雀、写路径没有 | ✅ 已修(补 `[ERR]` 留痕 + >1 KB 告警,行为不变) | -| B1 | ~~中~~ **已推翻** | `net_socket.c:3553` | 原判"产品路径从未启用 Socket KeepAlive → 静默断开无人知晓"(§5.56 服务器侧应在 CONNECT 后开,产品三服务全是 Server,60s/5s/3 参数空转)。**2026-10-12 实测推翻前提**:连接静置时禁用 PC 网卡(真·静默死亡),芯片 **+2 s** 即报 `[ERR][NET] sock0 TIMEOUT` → 本层 `relisten quarantine` 释放重开;2 s 恰为 §5.17 重传 8×200 ms=1.6 s 量级,**与 KeepAlive 无关**(IDLE 最短 60 s)。链路层失效本就被快速覆盖。 | ✅ 已按 §5.56 补过使能并实测,**依 Trap 25「不留未证明的复杂度」撤回**,代码内留注释记录;残余场景见下「台架注入陷阱」 | -| B2 | 中低 | `net_socket.c:1638` | `ch395f_tcp_connect()` 返回值被丢弃(同文件 open/listen 都查)。§9.2.2 ⑧ 明文:`GET_CMD_STATUS` 返回 SUCCESS 仅表示**命令**执行成功、其他值表示**命令执行失败**,丢弃它 = 命令级被拒要等 30 s recv 超时才暴露 | ✅ 已修(2026-10-12):非 SUCCESS 且**非 BUSY** 才判失败——`poll_cmd_status` 超时返回的就是 `ERR_BUSY`(芯片仍在执行、命令未被拒),把它当失败会误杀慢芯片;明确失败 → `[ERR] rejected by chip: 0xNN` + 按 §9.2.2 ⑧"从 ⑤ 重开"关 Socket 回滚 + 返 `-1`。**Phase 7 实测回归**:TC701 回显 64 B 校验 PASS、TC702 重连 ×3 全 PASS(正常连接无误杀);不可达对端时芯片回的是 SUCCESS、`ret=0` 且不打 `rejected by chip`,失败仍由 `CONNECT TIMEOUT`(~1.6 s) 裁定 → 行为未变。⚠ 残余:`net_reconcile_tick` 仍只裁 ESTABLISHED/LISTENING,`CONNECTING` 丢中断无人收口(产品不走出向连接、无法实测,按"不加未证明复杂度"暂不动);TC703 断言 `rc==0` 把"返回值被丢弃"写进了判据,真发生命令级被拒时它会 FAIL(属正确告警) | -| B3 | 低 | **两处**:`net_socket.c:1120-1125`(`net_listen_locked`)+ `net_socket.c:2060-2065`(`net_relisten_locked`,审计原只登记前者,**后者才是每次断连的热路径**) | 两处对**全部 8 个 Socket** `GET_INT_STATUS_SN`(读即清)+ 无条件读清全局。机制:§5.15 明文「此命令执行完毕后…将全局中断状态清除」= 全局寄存器**整体读即清、无按位清除手段**,而 `net_poll` 只在**全局位置位时**才去读该 Socket 的 SINT(`:469` 门控)→ 丢弃式读全局把别的在用 Socket 的门一并焊死;SINT 又**粘滞到被读**(Trap 14 实证 close/open 都不清 SINT)→ 全量读走即**永久销毁**其 CONNECT/DISCONNECT/TIM_OUT/SENBUF_FREE(唯 RECV 是条件位可下轮补,Trap 08)。git 考古:`e862009bc`(2026-07-19 多连接时代,无注释引入,同提交还删掉了 net_poll 里"勿在此读 per-socket 中断"的警告) → `319d00e3` 原样搬进单连接 → `db64e6acf` 复制进 relisten;**不属于任何已登记 Trap 的修复**(Trap 14/17/23 指定的预清都在别处且是单 Socket)。官方 SDK 21 个例程一律"全局位图门控 + 单 Socket 读 + 立刻转存",open/listen 前后**不清任何中断**,无一例全量读 8 个 SINT | ✅ 已修(2026-10-12):两处收窄为**只清本 Socket 的 SINT** + **删除丢弃式全局读清**(全局交 `net_poll` 集中读清,与 AGENTS 模块约定「所有 CH395F 中断在 net_poll 内集中读取并清除」一致);本 Socket 残留的全局位被 `net_poll` 读到时 SINT 已为 0,无害了结。**两路子 Agent 曾分歧**:一路建议保留全局读清("只搁浅不销毁"),一路警告"保留=修一半"——采后者,芯片无按位清除,保留即热路径继续焊门。**回归**:`@build` 0/0 `Code=197008`(−24 B);`@test` PASS;Trap 25 `ftp_regression` **11/11 ×3**;Trap 23 `ftp_abort_probe --check-abort` RST **5/5**(450) + FIN **5/5**(226);Trap 22 `:21` 0ms×50 **50/50**、100ms×50 **50/50**、`:8888` 0ms×20 **20/20**(均与文档基线一致);`:7003` 腿见 B10(A/B 已证明与本改动无关;B10 修完后该腿回到 **10/10**)。⚠ 残余:`net_connect_locked:1601` / `net_close_locked:2571` / `net_relisten_locked:1986` 三处无条件全局读清仍会**搁浅**(延后、非销毁)其它 Socket 挂起事件——它们是 Trap 14/17/23 的指定两级预清,本次不动,宜与 B4 一并处置 | +| A1 | 中 | `test/board/ch395f_test_task.c:1005` | TC-203 的 Trap 14 反核实写成 `st_chk[0]==CH395F_TCP_ESTABLISHED`——byte0 只有 `00H/05H`,条件**恒假** → 防线永久失效且日志无痕(与 Trap 07 同类:byte0/byte1 语义越界) | ✅ 已修(改 `byte0==OPEN && byte1==ESTABLISHED`) | +| A2 | 中 | 同上 `:1150`/`:1321` | Phase 3/4 把 `GET_RECV_LEN` 返回值直接当读取长度(本地 `rx_buf[300]/[64]`,芯片 RX 2KB)→ 栈溢出 | ✅ 已修(夹到 `sizeof` + 留痕) | +| A3 | 低 | `ch395f.h:253` | `CH395F_GINT_STAT_DHCPV6=0x8000`(bit15);SDK 是 `(1<<12)`、§5.16 明确 `[12:15]` 保留 | ✅ 已修(改 `0x1000` + 注明本芯片恒 0;全仓零引用) | +| A4 | 低 | `ch395f.h/ch395f.c` 多处 | 分组注释字节数错 4 处;KeepAlive 参数注释写"秒"(§5.53/5.54 是 ms);`set_retrans_count` 写"0=默认值5"(§5.17 默认 12)。**实现全部正确,注释误导**——B3 的 1 字节缓冲栈越界正是这类注释发起的 | ✅ 已修 | +| A5 | 低 | `net_socket.c` 7 处注释 | 注释与手册/HEAD 相反:`FUN_PARA(SOCKET_CLOSE=0)`(实为 bit3=1,正是 closed-with-data accept 的契约依据)、"初始化成功后设置 MAC"(§9.2.1 ① 是之前)、"复用默认缓冲"、"布局由 MSS 推导"(Trap 13/21 已解耦)、"等 SEND_OK"(门控是 SENBUF_FREE)、UDP 头 `[0-1] reserved`(§9.2.4 是数据长度) | ✅ 已修 | +| A6 | 低 | `ch395f.c:1176` | `write_send_buf` 超限**静默 return**(void、无日志),且上限取 DMA 2048 而非发送缓冲 1024;读路径有 OVERSIZE 金丝雀、写路径没有 | ✅ 已修(补 `[ERR]` 留痕 + >1KB 告警,行为不变) | +| B1 | ~~中~~→定案 | `net_socket.c`(KeepAlive 使能点) | 原判"产品路径从未启用 Socket KeepAlive → 静默断开无人知晓"(§5.56 服务器侧应在 CONNECT 后开)。2026-10-12 曾推翻前提:禁用 PC 网卡(链路层死亡)芯片 **+2s** 即报 `sock0 TIMEOUT`(=§5.17 重传 8×200ms 量级,与 KeepAlive 无关)→ 依 Trap 25 铁律撤回使能。**2026-10-13 E11 再定案**:用"禁用网卡"注入"链路在、对端静默"(防火墙规则对已建立连接无效,方法学见 `docs/网络/N34-自愈审计/实验方案.md` E11)实测服务器侧开启后 60s/5s/3 → **判死 ≈30.0s**(早于手册公式 75s,以实测为准);短参数 6s/2s/2 → ≈5.1s(参数生效);静默但存活对端**不被误杀**(90s 空闲回显正常) | ✅ **已转正**(`handle_connect_event` LISTENING 分支按 `docs/芯片手册/CH395F.md` §5.56 开启;Phase 11 回归 TC-NET-1101 已更新死对端注入法) | +| B2 | 中低 | `net_socket.c`(`net_connect_locked`) | `ch395f_tcp_connect()` 返回值被丢弃。§9.2.2 ⑧:`GET_CMD_STATUS` SUCCESS 仅表示**命令**执行成功,丢弃它 = 命令级被拒要等 30s recv 超时才暴露 | ✅ 已修(2026-10-12):非 SUCCESS 且**非 BUSY** 才判失败(`poll_cmd_status` 超时返 `ERR_BUSY`=芯片仍在执行,当失败会误杀慢芯片);明确失败 → `[ERR] rejected by chip: 0xNN` + 按 §9.2.2 ⑧ 关 Socket 回滚 + 返 -1。TC701/702 回归无误杀。⚠ 残余:`net_reconcile_tick` 仍只裁 ESTABLISHED/LISTENING,`CONNECTING` 丢中断无人收口(产品不走出向连接、无法实测,按"不加未证明复杂度"暂不动) | +| B3 | 低→**结构性** | `net_listen_locked` + `net_relisten_locked`(后者是每次断连的热路径) | 两处对**全部 8 个 Socket** `GET_INT_STATUS_SN`(读即清)+ 无条件读清全局。机制:§5.15 全局中断寄存器**整体读即清、无按位清除手段**,而 `net_poll` 只在全局位置位时才读该 Socket 的 SINT(门控)→ 丢弃式读全局把别的在用 Socket 的门一并焊死;SINT 又**粘滞到被读**(Trap 14/17 实证)→ 全量读走即**永久销毁**其 CONNECT/DISCONNECT/TIM_OUT/SENBUF_FREE(唯 RECV 是条件位可下轮补,Trap 08)。git 考古:多连接时代引入 → 单连接搬迁 → relisten 复制,**不属于任何已登记 Trap 的修复**;官方 SDK 21 个例程一律"全局位图门控 + 单 Socket 读 + 立刻转存",无一例全量读 8 个 SINT | ✅ 已修(2026-10-12):两处收窄为**只清本 Socket SINT** + **删除丢弃式全局读清**(全局交 `net_poll` 集中读清=AGENTS 模块约定)。回归:Trap 25 11/11×3、Trap 23 RST 5/5+FIN 5/5、Trap 22 全腿与基线一致。⚠ 残余:`net_connect_locked`/`net_close_locked`/`net_relisten_locked` 三处指定预清仍含无条件全局读清(会**搁浅**而非销毁其它 Socket 挂起事件)——它们是 Trap 14/17/23 的两级预清,宜与 B4 一并处置 | | B4 | 低 | `net_socket.c:446/469` | 全局中断 bit0-3(`UNREACH`/`IP_CONFLI`/`PHY_CHANGE`/`DHCP`)被读清但从不消费;`ch395f_get_unreach_info()` 全仓零调用 → IP 冲突、ARP 不可达、链路抖动三类事件完全静默 | ⏳ 待修(可观测性缺口) | -| B5 | 低 | `net_socket.c:3878-3881`(`net_getsockname`,函数 :3851-3887;原登记 :3849-3854 已陈旧)→ `lftpd.c:803-809` | `net_getsockname` 的 `s_addr` 组装方向与本层约定(`net_inet_addr`/`net_inet_ntoa`)相反 → FTP 日志把设备自身 IP 打成 `100.1.168.192`。⚠ 同一 `getsockname` 的 PASV 227 消费点(`lftpd.c:497-503`)做了 `net_htonl` 再 MSB 拆,**双重反转恰好正确,勿误判**;风险前瞻:若把该 `s_addr` 喂给 `net_connect`/`net_sendto` 就会连到反序地址 | ⏳ 待修(改一处须同时核对 PASV) | +| B5 | 低 | `net_getsockname` → `lftpd.c` 日志 | `net_getsockname` 的 `s_addr` 组装方向与本层约定相反 + `net_inet_ntoa` 段序反转(NET_NTOHL 后主机数字的 MSB 才是首段,原实现低段在前)→ FTP 日志把 IP 打成 `100.1.168.192`;且 lftpd 打印的是 `getsockname`=**本机 IP:21** 而非对端。⚠ PASV 227 消费点做了 `net_htonl` 再 MSB 拆,**双重反转恰好正确,勿误判** | ✅ 已修(2026-10-13,N34 阶段 3 收尾;代码随本分支收尾批提交):`net_inet_ntoa` 段序改 MSB 首(MCU+模拟器同步);新增 `net_getpeername`(读 CONNECT 事件缓存,零 SPI);lftpd 改打印**对端** IP:port(台架判据:日志 `connection received from 192.168.1.x:<高位端口>`)。PASV 未动(原本正确) | | B6 | 低 | `net_socket.c:591/1225/3608` | 3 处状态判据只看 `byte1`、未按 §5.22 用 `byte0` 门控;reconcile 的 else-if 链里 `(00H, 陈旧 04H)` 会先命中"补 accept"而非"重建监听" | ⏳ 待修(Trap 07 实测 `byte0=00H` 时 `byte1` 读回 `0x00`,故为低概率) | -| B7 | 低 | `ch395f_test_task.c:1379/1504` | Phase 5 单次写 4093 B 超 1 KB 发送缓冲与 2048 DMA 载荷 → 整块被丢;注释仍写"4096 B = 发送缓冲上限"(2026-09-22 布局解耦前的旧事实)。A6 修后会打 `[ERR]` 而非静默 stall | ⏳ 待修(chunk 改 ≤1024 或分块) | +| B7 | 低 | `ch395f_test_task.c:1379/1504` | Phase 5 单次写 4093B 超 1KB 发送缓冲与 2048 DMA 载荷 → 整块被丢;注释仍写"4096B=发送缓冲上限"(旧事实)。A6 修后会打 `[ERR]` | ⏳ 待修(chunk 改 ≤1024 或分块) | | B8 | 低 | `ch395f_test_task.c:1290-1292` | Phase 4 把 sock6 缓冲重排到块 24-27,与 `net_init` 固定布局中 sock4 的 24-27 **交叠**(§5.45 禁止) | ⏳ 待修 | -| B9 | 低 | `ch395f_reuse_bench.c:93`、`net_socket.c:39`、`net_socket.c:262` | bench 用 `get_phy_status() != 0` 判链路(01H=断开也成立,`net_task.c:122` 的 `!= CH395F_PHY_DISCONN` 才对);`NET_PHY_CHECK_INTERVAL_MS` 死宏;无芯片版本门控(§5.16 <0x44 不支持 ALL 版、§5.52 bit3/4 需 ≥0x46,实测 0x4A 无影响) | ⏳ 待修(前两项死代码/卫生) | -| B10 | **中** | `net_reconcile_tick` 认领范围 + `handle_disconnect_event` 释放分支(`net_socket.c:3663-3686`) | **孤儿监听 → 端口永久拒连**。断开时释放分支把状态置 `CLOSED` 并注释「留给 `net_recv_locked` 把剩余数据读出来再关,**等待应用重新监听**」——即由应用负责重建;但 `ui_net_sink` 只在 `net_recv` 返 0(EOF)时才清 `g_client`,而 EOF 已被这个事件分支消费掉 → **net 层等应用重建 / 应用等 net 层给 EOF / 对账只裁 ESTABLISHED·LISTENING 直接跳过 / 芯片不会自己回 LISTEN**,四方互等。实测(临时探针):`TEMP orphan sock2 state=0 port=7003 chip=0x05/0x00` 每秒一条持续 12 s+,`:7003 300ms×10` **0/10**、+30 s 仍拒连。与 Trap 22 的关系:Trap 22 的①③两类都要求本层处于 ESTABLISHED/LISTENING,**孤儿态恰好落在两类之外**,所以那套对账全程沉默 | ✅ 已修(2026-10-12):`net_reconcile_tick` 新增**类型④**认领孤儿(`in_use` + `state==CLOSED` + `auto_relisten` + `type==STREAM` + `local_port!=0`,芯片侧不在 LISTEN)→ 复用 `net_relisten_try`(自带"已在重试队列则不重复登记"保护,与 D10 重试队列、Trap 23 沉降隔离兼容)。⚠ **`auto_relisten` 是决定性门**:少了它,"本层 CLOSED + 已绑端口"的**出向连接**(板级 Phase 7 TC701~703 用 `net_connect` 绑本地口)会被误判孤儿并被 relisten 成监听器。**回归**:`:7003 300ms×10` **0/10 → 10/10**(+20 s 存活);`:21` 0ms×50 **50/50**、100ms×50 **50/50**;`:8888` 0ms×20 **20/20**;`ftp_regression` **11/11 ×2**;Trap 23 RST **5/5**、FIN **5/5**;`@test` PASS(600 ms、回读无差异);`Code` 197008→197084。⚠ **残余(未根治,已立项根治)**:生产者仍在——释放分支依旧把状态停在 `CLOSED` 等应用重建,④是**兜底**(实测救援后端口 1 s 内恢复,日志 `reconcile: orphan listener … -> relisten` 可见)。⚠ **④ 有副作用**:`net_recv` 的 EOF 是**边沿**(只在 recv 那一刻 `state==CLOSED` 才给 0,`:1880-1888`;已被别处 relisten 回 LISTENING 则恒 `WOULDBLOCK`,`:1890`)→ ④ 抢先 relisten 会把慢消费者的 EOF 永久吞掉,`g_client` 不清、新客户端数据被旧会话静默吸收。**原理与分阶段根治方案 → `docs/子系统设计/网络Socket监听生命周期设计说明.md`**(含:芯片层为何永不自动回 LISTEN、本仓 `fun_para=0x08` 使 `0x05/0x00` 成为合法常驻态、RFC 1122 §4.2.2.18 与 POSIX accept 的规范冲突、12 个重建所有者与 U1~U15 无人负责组合、以及"芯片原生多连接模式(A′)为何对本产品不可用"的决定性证据) | +| B9 | 低 | `ch395f_reuse_bench.c:93`、`net_socket.c:39/262` | bench 用 `get_phy_status()!=0` 判链路(01H=断开也成立,应 `!= CH395F_PHY_DISCONN`);`NET_PHY_CHECK_INTERVAL_MS` 死宏;无芯片版本门控(§5.16 <0x44 不支持 ALL 版、§5.52 bit3/4 需 ≥0x46,实测 0x4A 无影响) | ⏳ 待修(前两项死代码/卫生) | +| B10 | **中** | `net_reconcile_tick` 认领范围 + `handle_disconnect_event` 释放分支 | **孤儿监听 → 端口永久拒连**:释放分支把状态置 CLOSED 等"应用重新监听",但 `ui_net_sink` 只在 `net_recv` 返 0(EOF)时才清 `g_client`,而 EOF 已被事件分支消费 → net 层等应用重建 / 应用等 net 层给 EOF / 对账只裁 ESTABLISHED·LISTENING 直接跳过 / 芯片不会自己回 LISTEN,**四方互等**。实测探针 `TEMP orphan sock2 state=0 port=7003 chip=0x05/0x00` 持续 12s+,`:7003 300ms×10` **0/10**。与 Trap 22 的关系:①③两类都要求本层处于 ESTABLISHED/LISTENING,**孤儿态恰好落在两类之外** | ✅ 已修(2026-10-12):对账新增**形态④**认领孤儿(`in_use`+`state==CLOSED`+`auto_relisten`+`type==STREAM`+`local_port!=0`,芯片侧不在 LISTEN)→ `net_relisten_try`。⚠ `auto_relisten` 是决定性门:少了它出向连接(Phase 7 TC701~703 绑本地口)会被误判孤儿 relisten 成监听器。回归:`:7003` 0/10→**10/10**;其余全腿与基线一致。⚠ ④ 的副作用:`net_recv` 的 EOF 是**边沿**——④抢先 relisten 会把慢消费者的 EOF 永久吞掉(`g_client` 不清、新客户端数据被旧会话静默吸收)。**根治出口 = `net_drop_conn` + 生命周期设计**(N34 阶段 3 已落,`6394853`):原理与分阶段方案 → `docs/子系统设计/网络Socket监听生命周期设计说明.md`(含:芯片为何永不自动回 LISTEN、`fun_para=0x08` 使 `0x05/0x00` 成为合法常驻态、RFC 1122 §4.2.2.18 与 POSIX accept 的规范冲突、12 个重建所有者与 U1~U15 无人负责组合、芯片原生多连接模式(A′)为何对本产品不可用) | -### 文档侧待修清单(本轮只改了最要紧的几处,其余登记在此) +## 台架注入陷阱(方法论,2026-10-12 起持续补充) -- **最高优先级**:Trap 13 的"部分读后 `recv_len` 归零"与 §5.34 直接冲突(已在本条加复核注),但**同一机理仍被当作现行约束**写在 `CH395F_Test_Guide.md:28/69/930/932/958`、`docs/网络/BSD_Socket_API_函数调用关系.md:196/362`、`net_socket.c` 两处注释里——须与 Trap 21 的撤回同批处置,并补一条"部分读 → 再查 `recv_len`"的定向 TC。 -- 陈旧数字:`CH395F_Test_Guide.md:14/24/645/660/1141/1200`(DMA 4100 / 载荷 4096,现 2052/2048)、`:72/1201`(`NET_DMA_MAX_PAYLOAD=4096`,现 2048;实际分块上限是 `NET_SEND_CHUNK_MAX`=1024)、`:1020-1040`(TC-NET-1002/1003 期望 4096/4097 单次 DMA)、`docs/网络/BSD_Socket_API_使用指南.md:1246`(`NET_SEND_BUF_SIZE` 宏已不存在)。 -- 悬空引用:`Trap 19` 从未存在(`Test_Guide.md:878` 仍引用);`Test_Guide.md:353/418` 的"见风险 #4 / 风险表 #1"指向已删除的表。 -- 口径冲突:`Test_Guide.md:1135` 的 TRAP-07 行仍写"卡 FIN_WAIT_2 / `close_socket`(RST)"(与 Trap 07 定案更正冲突);`:45` 的"TC704 40/40"与 `:880` 的"`PHASE7_MULTI_COUNT` ≤5"矛盾(HEAD 宏值仍是 8);`Drivers/BSP/NET/README.md:35` 与 `net_socket.c:1864` 的"正常 FIN 路径零改动"与 Trap 23 二阶段矛盾。 -- 机理措辞:Trap 17"等**对端** FIN/ACK"应按 Trap 07 实测改写(自身 FIN 的 ACK + 本地 TIME_WAIT,0~150 ms);Trap 20"硬件发送 FIFO 1 KB"应改"本仓 2 块分配的发送缓冲区"(§5.45 上电默认是 4 块=2 KB)、"等 `SEND_OK`"应为 `SENBUF_FREE`;Trap 25 的 `NET_MSG_FIX` 应为 `NET_MSG_RESYNC_LISTEN`、`GET_REMOTE_IPP` 应为 `GET_REMOT_IPP`、"§5.45"应为 §5.32+§5.23①。 -- 已失效条目未标注:Trap 01/02/11 的多连接模型随 2026-08-25 `319d00e3` 删除;Trap 09 的 `osDelay(7000)` 已被就绪屏障取代;Trap 10 的"`net_connect()` 不使用消息队列"已失效;Trap 12 的 #2/#4 与仓内 FatFs 源码矛盾(`ff.c:3101-3109` 剥前导分隔符、`:3127-3129` 空名返回根目录),且 `FF_FS_RPATH=0` 下 `"."` 不是合法路径段(HEAD 用 `"0:"`)。 -- 日期:Trap 01/02/03/04/06/07 的"2025-07"均早于仓库首提交 `b1d05091`(2026-06-05);Trap 03/04/06 的 Phase 编号也错(DHCP=阶段 4、UDP=阶段 3、KeepAlive=阶段 11)。 -- 证据通则:`*.log` 在 `.gitignore` 内 → 本文所有台架计数(40/40、34/34、15/15、4 轮 11/11、13/13…)**脚本在库、日志未入库 = 不可复核**;Trap 25 引用的实验分支 `n30-chip-bench`(tip `eddbede`)不含 bench 源文件与 uvprojx 注册,裸机芯片台架无法从任何已提交状态重建。 +- **Windows 出站防火墙规则不能模拟"对端静默死亡"**:WFP 对**已建立**连接有状态缓存,中途 `New-NetFirewallRule -Direction Outbound -Action Block` 不影响既有流——实测静置+阻断 250s 芯片毫无反应,看似"KeepAlive 不生效",实则 ACK 一直正常往返。 +- 更糟的副作用:该规则会**吃掉本机后来发出的 FIN** → 设备侧留下**幽灵 ESTABLISHED**,之后所有新连接被 §5.29"向试图连接的远端发送 TCP RESET"直接 RST,表现为"端口拒连"——易误判成设备故障(实为自伤)。撤规则后设备自行恢复。 +- **能真正掐断链路的手段**:`Disable-NetAdapter`(链路真 down)。⚠ 该 cmdlet **只有 `-Name`/`-InterfaceDescription`**,没有 `-InterfaceIndex`;网卡名是中文,用 `(Get-NetAdapter -InterfaceIndex N).Name` 取值再传入;禁用/恢复必须写在 `try/finally` 里。 +- **仍未覆盖的场景**:"链路仍在、对端静默"(NAT 会话过期、中间节点黑洞)没有安全注入手段(静态 ARP 黑洞被交换机泛洪兜底、删 IP 连带拆掉本机连接)——E11 用禁用网卡的近似注入完成定案。 +- **主机侧回显服务器收不到入站连接 → Phase 7 全量 `CONNECT TIMEOUT`**:本机对 `C:\programdata\anaconda3\python.exe` 有两条 Public 入站 Block 规则(`C:\python314\python.exe` 才是 Allow),而 `.venv\Scripts\python.exe` 的基座解释器正是 Anaconda python。Windows 防火墙**块规则优先于放行规则** → 中途加 Allow 规则**完全无效**,只有 `Set-NetFirewallProfile -Name Public -Enabled False` 才通。症状极具误导性:芯片 ~1.6s `CONNECT TIMEOUT`、`net_connect` 仍返 0、`[CONN] openOK` 照常打印,看着像芯片故障,实为主机防火墙。固定跑法:`try { 关 Public → 复跑 } finally { 开 Public }`。 +- **Phase 7 两条既有失败与改动无关,别追**:① TC704 `socket create #5` FAIL = E-23(`NET_MAX_SOCKETS=8` 减 3 常驻监听 → 并发上限 5,而 `PHASE7_MULTI_COUNT` 仍是 8);② TC703 要求 PC 服务器**离线**,与 701/702/704 环境互斥,同轮连跑必有一侧失真。 +- **对照组必须从干净态起测**:上一腿若**没死透**就重启下一腿,对照组失真。正确做法:每个样本前 `stlink_flash.py reset` + 等 12s 干净启动,并取多样本再比均值。 +- **`[PROF]`/`[PROF2]` 字段不是计数器,别拿来判"任务卡死"**:`netp` = `net_poll` 段最长耗时(µs)、`netn` = 采样瞬间在段内时长(`App/util/task_prof.c`)→ 数值恒定只说明"没刷新最大值"。真正的停摆信号是 `[ERR][WD] task stall`;其 `miss=` 打印的是**累计**数(连续数封顶在容忍值 8),容忍 = 8 轮×100ms = 连续 800ms 有任务缺席即停喂、IWDG 2.5s 溢出(`App/util/task_watchdog.c`)。缺席位图解码:`req=0x77` 为 DEFAULT/NET/FTP/UI/TIMESYNC/ADC,`mask=0x41` → 缺席 NET+FTP+UI+TIMESYNC = "netTask 卡住、所有做 `net_*` 的任务阻塞在队列+通知后面"的指纹。 +- **静默黑洞用临时探针抓状态,测完即删**:B10 的孤儿态在日志里毫无痕迹,靠对账跳过分支加一条 `TEMP orphan …` 才直接看到;同一轮还顺手用探针**否掉了自己的第一假设**。假设写盘前先测,比推理便宜。 +- **判读通则(与 Trap 25 同源)**:看到 `TIMEOUT` 先**算时序再归因**——2s 与 60s 一眼可分;否则会把"重传超时"误记成"KeepAlive 生效",把相关性当因果。 -### 主机侧测试工具过期(2026-10-12 实测) +## 主机侧测试工具过期(2026-10-12 实测) -- **`MDK-ARM/ftp_abort_probe.py` 已修**:`raw_login` 用 `USER anonymous`、`followup_ok` 用 `login("anonymous","x")`,N-19 后匿名被 530 拒 → Trap 23 两条腿**假 0/5**(round 1 报 `RuntimeError: PASV failed`,且这条半认证会话占死 FTP 唯一会话,导致后续轮全变 `ConnectionRefused`)。改 `admin/admin`(两处)后 RST **5/5**、FIN **5/5**,与 `:779`/`:781` 基线一致。**教训:跑既有回归腿前先确认凭据仍有效,否则会把工具过期读成固件回归。** -- **同族未修**:`test/host/ftp_up_dl_test.py`(:123-126)、`ftp_speed_test.py`(:116-121)、`ftp_test.py`(:93-99)仍用匿名登录 → 对当前固件必挂登录步;`docs/测试说明/FTP_Test_Guide.md` 推荐的这几个入口因此已失效(现行可用入口是 `test/host/ftp_regression.py`,默认 `admin/admin`)。 +- **`test/host/ftp_abort_probe.py` 已修**:`raw_login`/`followup_ok` 原用匿名登录,N-19 后匿名被 530 拒 → Trap 23 两条腿**假 0/5**(round 1 `PASV failed`,且半认证会话占死 FTP 唯一会话,后续轮全变 `ConnectionRefused`)。改 `admin/admin` 后 RST 5/5、FIN 5/5。**教训:跑既有回归腿前先确认凭据仍有效,否则会把工具过期读成固件回归。** +- **同族未修**:`test/host/ftp_up_dl_test.py`、`ftp_speed_test.py`、`ftp_test.py` 仍用匿名登录 → 对当前固件必挂登录步;`docs/测试说明/FTP_Test_Guide.md` 推荐的这几个入口因此已失效(现行可用入口是 `test/host/ftp_regression.py`,默认 `admin/admin`)。 -### 台架注入陷阱(2026-10-12,B1 复核过程中踩到) +## 文档侧待修清单(其余登记在此) -- **Windows 出站防火墙规则不能用来模拟"对端静默死亡"**:WFP 对**已建立**的连接有状态缓存,中途 `New-NetFirewallRule -Direction Outbound -Action Block -RemoteAddress <设备IP>` 不影响既有流——实测静置 + 阻断 250 s 芯片毫无反应,看似"KeepAlive 不生效",实则 ACK 一直在正常往返。 -- 更糟的副作用:该规则会**吃掉本机后来发出的 FIN** → 设备侧留下一条**幽灵 ESTABLISHED**,之后所有新连接被 §5.29"向试图连接的远端发送 TCP RESET"直接 RST,表现为"端口拒连",容易被误判成设备故障(实为自伤)。撤规则后设备自行恢复。 -- **能真正掐断链路的手段**:`Disable-NetAdapter`(链路真的 down)。⚠ 本机 PowerShell 的该 cmdlet **只有 `-Name` / `-InterfaceDescription`**,没有 `-InterfaceIndex`/`-ifIndex`;网卡名是中文,用 `(Get-NetAdapter -InterfaceIndex 19).Name` 取值再传入,避免命令行里写中文字面量。禁用/恢复必须写在 `try/finally` 里,否则网卡留在关闭状态。 -- **仍未覆盖的场景**:`Disable-NetAdapter` 只能造"链路层失效";"**链路仍在、对端静默**"(NAT 会话过期、中间节点黑洞)本轮没有安全注入手段(静态 ARP 黑洞会被交换机泛洪兜底、删 IP 会连带拆掉本机连接)。这才是服务器侧 KeepAlive 唯一可能的价值场景,**未证实**,故不保留该改动。 -- **主机侧回显服务器收不到入站连接 → Phase 7 全量 `CONNECT TIMEOUT`(2026-10-12 实测)**:本机对 `C:\programdata\anaconda3\python.exe` 有两条 **Public 入站 Block** 规则(`C:\python314\python.exe` 才是 Allow),而 `.venv\Scripts\python.exe` 的基座解释器正是 Anaconda python(构建报错自曝 `C:\ProgramData\anaconda3\python.exe: can't open file ...`)。Windows 防火墙**块规则优先于放行规则** → 中途加的 `New-NetFirewallRule -Direction Inbound -Action Allow -Protocol TCP -LocalPort 8081 -Profile Any` **完全无效**(实测两轮全灭),只有 `Set-NetFirewallProfile -Name Public -Enabled False` 才通。症状极具误导性:芯片 ~1.6 s 报 `CONNECT TIMEOUT`、`net_connect` 仍返 0、`[CONN] openOK` 照常打印,看着像芯片/驱动故障,实为主机防火墙。固定跑法:`try { 关 Public → 复跑 } finally { 开 Public }`(或改用 `C:\python314\python.exe` 起服务器)。 -- **Phase 7 的两条既有失败与改动无关,别追**:① TC704 `socket create #5` FAIL = E-23(`NET_MAX_SOCKETS=8` 减 3 个常驻监听 → 并发上限 5,而 `PHASE7_MULTI_COUNT` 仍是 8);② TC703 要求 PC 服务器**离线**,与 701/702/704 环境互斥,同轮连跑必有一侧失真(本轮服务器在线时它反而 PASS,只因芯片先 `CONNECT TIMEOUT`)。 -- **判读教训(与 Trap 25 同源)**:看到 `TIMEOUT` 先**算时序再归因**——2 s 与 60 s 一眼可分;否则会把"重传超时"误记成"KeepAlive 生效",把相关性当因果。 -- **A/B 归因前必须每腿硬复位(B10,2026-10-12)**:`:7003` 这类腿一旦把监听打死就**不自恢复**,下一腿是接着死状态跑的 → 对照组失真。本次第一腿(基线 1/10)就是这么废的;正确做法是**每个样本前 `stlink_flash.py reset` + 等 12 s 干净启动**,并取多样本(本轮基线 5/1/1/5 vs 改动 3/7/2/1,均值 3.0 vs 3.25 → 统计不可区分,才敢判"与改动无关")。 -- **`[PROF]`/`[PROF2]` 字段不是计数器,别拿来判"任务卡死"(B10)**:`netp` = `net_poll` **段最长耗时(µs)**、`netn` = 采样瞬间在段内的时长(`App/util/task_prof.c:172`)→ 数值恒定只说明"没刷新最大值"。真正的停摆信号是 `[ERR][WD] task stall`;且其 `miss=` 打印的是**累计** `s_miss_total`(不是连续缺席数,连续数封顶在容忍值 8),容忍 = 8 轮 ×100 ms = **连续 800 ms 有任务缺席即停喂**、IWDG 2.5 s 溢出(`App/util/task_watchdog.c:466-492`)。缺席位图解码:`req=0x77` 为 DEFAULT/NET/FTP/UI/TIMESYNC/ADC,`mask=0x41` → 缺席 NET+FTP+UI+TIMESYNC 而 DEFAULT/ADC 正常 = "netTask 卡住、所有做 `net_*` 的任务阻塞在 `osMessageQueuePut(osWaitForever)`+`xTaskNotifyWait(portMAX_DELAY)` 后面"的指纹。 -- **静默黑洞用临时探针抓状态,测完即删(B10 手法)**:B10 的孤儿态在日志里毫无痕迹,靠在对账跳过分支加一条 `TEMP orphan … state=%d port=%u chip=0x%02X/0x%02X` 才直接看到 `state=0 / chip=0x05/0x00`;同一轮还顺手用探针**否掉了自己的第一假设**(怀疑 `net_listen_locked` 的 `osDelay(500)` 冻结 netTask → 探针 0 次命中)。假设写盘前先测,比推理便宜。 +- **最高优先级**:Trap 13/21 已撤回的"部分读后 `recv_len` 归零"机理**仍被当作现行约束**写在 `CH395F_Test_Guide.md:28/69/930/932/958`、`docs/网络/BSD_Socket_API_函数调用关系.md:196/362`、`net_socket.c` 两处注释里——须同批处置,并补一条"部分读 → 再查 `recv_len`"的定向 TC。 +- 陈旧数字:`CH395F_Test_Guide.md:14/24/645/660/1141/1200`(DMA 4100/载荷 4096,现 2052/2048)、`:72/1201`(`NET_DMA_MAX_PAYLOAD=4096`,现 2048;实际分块上限是 `NET_SEND_CHUNK_MAX`=1024)、`:1020-1040`(TC-NET-1002/1003 期望 4096/4097 单次 DMA)、`docs/网络/BSD_Socket_API_使用指南.md:1246`(`NET_SEND_BUF_SIZE` 宏已不存在)。 +- 悬空引用:`Trap 19` 从未存在(`Test_Guide.md:878` 仍引用);`Test_Guide.md:353/418` 的"见风险 #4/风险表 #1"指向已删除的表。 +- 口径冲突:`Test_Guide.md:1135` 的 TRAP-07 行仍写"卡 FIN_WAIT_2/`close_socket`(RST)"(与 Trap 07 定案冲突);`:45` 的"TC704 40/40"与 `:880` 的"`PHASE7_MULTI_COUNT` ≤5"矛盾(HEAD 宏值仍是 8);`Drivers/BSP/NET/README.md:35` 与 `net_socket.c` 的"正常 FIN 路径零改动"与 Trap 23 二阶段矛盾。 +- ~~机理措辞~~(Trap 17/20 措辞、Trap 25 的 `NET_MSG_RESYNC_LISTEN`/`GET_REMOT_IPP`/§5.32+§5.23① 引用——**2026-10-13 重组已在本文件内更正**;`Test_Guide.md` 侧仍待同步)。 +- ~~已失效条目未标注~~(Trap 01/02/11 对现行初始化仍有效、Trap 09 `osDelay(7000)` 已被就绪屏障取代、Trap 10 注记失效、Trap 12 根因按 ff.c 更正——**2026-10-13 重组已全部就地标注**)。 +- ~~日期~~(Trap 01/02/03/04/06/07 的"2025-07"早于仓库首提交 `b1d05091` 2026-06-05;Trap 03/04/06 的 Phase 编号错——**2026-10-13 重组已标注为"不可考/已更正"**)。 +- 证据通则:`*.log` 在 `.gitignore` 内 → 本文所有台架计数(40/40、34/34、227 轮、4 轮 11/11、13/13…)**脚本在库、日志未入库 = 不可复核**;Trap 25 引用的实验分支 `n30-chip-bench`(tip `eddbede`)不含 bench 源文件与 uvprojx 注册,裸机芯片台架无法从任何已提交状态重建(主分支存档见 `test/board/ch395f_reuse_bench.c`)。 + +--- + +# 附录 A · 驱动关键点速查(细节以对应 Trap 为准) + +- **初始化顺序**:`SET_MAC → SET_IP/GWIP/MASK → INIT_CH395 → SET_PHY`(Trap 24;IP 族必须在 INIT 前——INIT 读取并锁定寄存器值;SET_PHY 在 INIT 后重建物理链路);`CMD_PING_ENABLE` 不需显式调用。 +- **SPI 通信**:每次事务 `ch395f_spi_begin()/end()` 包裹;分频 8(PCB 约束);大数据走 SPI2 DMA(`CH395F_SPI_DMA_BUF_SIZE`=2048+4=2052,`ch395f.h:148`;历史值 1500/4100 已作废,见 Trap 13/21)。⚠ **TSL 规格偏离(N34 E12,2026-10-13 登记)**:手册 §7.4 要求缓存区命令"长度与数据之间"留 TSL(读 0.4us/写 0.2us),本仓两个缓存区函数把命令头与数据打包进**同一 DMA 突发**(间隔=0)属规格偏离;**实测未发现危害**(460 轮/≈58MB 双向 SHA-256 仅 1 次未复现异常,"两段+TSL"变体与现状无差异)→ 依 Trap 25 铁律暂不改产品路径,偏离与证据登记于此、变体留作待用补丁(完整 A/B 见 `docs/网络/N34-自愈审计/实验方案.md` E12)。 +- **TCP 参数**(必须在 INIT_CH395 之前):重传 `SET_RETRAN_COUNT`(默认 12,最大 20)×`SET_RETRAN_PERIOD`(默认 500ms,最大 1000ms);KeepAlive 参数 500ms 倍数、IDLE>INTVL(Trap 06)。 +- **KeepAlive 定案链**:默认关闭 → 服务器侧应在 `SINT_STAT_CONNECT` 后开启(§5.56)→ 2026-10-12 曾因"链路层死亡已被重传 +2s 覆盖"撤回使能 → **2026-10-13 E11 转正**("链路在、对端静默"场景实测判死 ≈30s,参数生效、存活对端不误杀):`handle_connect_event` LISTENING 分支开启 60s/5s/3;消费链路 TIM_OUT → `handle_timeout_event` QUARANTINE → relisten → 端口恢复(详见第六章 B1 与 `docs/网络/N34-自愈审计/实验方案.md` E11/E13)。 +- **Socket 缓冲**:默认只给 Socket 0~3;Socket 4~7 必须 `open_socket` 前显式分配(Trap 02);现行固定布局 6 块/Socket。 +- **TCP 关闭重连**:直接 `ch395f_close_socket()`,不要先 `disconnect`;轮询 `GET_SOCKET_STATUS` **第 1 字节**等 `00H` 再 open;第 2 字节仅在 byte0=OPEN 时有意义(Trap 07)。 +- **SOCK_TIMEOUT**:`SINT_STAT_SOCK_TIMEOUT` 不应视为致命——记录日志后继续操作(消费侧 QUARANTINE 见 Trap 23/B1)。 +- **UDP 模式**:DesIP=0xFFFFFFFF → UDP Server(接收数据前 8 字节为信息头 `reserved[2] src_port[2] src_ip[4]`,§9.2.4 前 2 字节是数据长度);DesIP=具体 IP → UDP Client(无信息头)。发送后必须等 SENBUF_FREE(Trap 04)。 + +# 附录 B · FTP 支持改造一次性问题(历史存档,Trap 已单列者不重复) + +| # | 问题 | 处置 | +|---|------|------| +| 4 | CH395F 多连接模式不支持多监听 | 架构约束:`fun_para=0x00` 单连接(后定案 `0x08`,见 Trap 18) | +| 5 | PASV 端口 0(net_bind 无自动分配) | `net_socket.c: s_dynamic_port` | +| 10 | `s_file_open` 单文件限制 | `lftpd_io.c` 全局变量(已知限制) | +| 15 | `NET_RECV_TIMEOUT_MS=5s` 控制通道超时断开 | `net_config.h: 30000`(后随阶段 1.5 recv 语义再改) | +| 16 | `receive_file` 0 bytes 当作成功 | `lftpd.c: total==0 则失败` | +| 17 | MobaXterm 连数据端口不发数据 | 外部客户端行为(Python 抓包确认,超时恢复) | From 74ba7ea61b15b384319e9398c9eb1cbc82149002 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 13 Oct 2026 17:28:53 +0800 Subject: [PATCH 23/24] =?UTF-8?q?docs(=E9=97=AE=E9=A2=98=E8=AE=B0=E5=BD=95?= =?UTF-8?q?):=20Trap=2012=20=E5=BD=92=E6=A1=A3=E6=9B=B4=E6=AD=A3=E2=80=94?= =?UTF-8?q?=E2=80=94FatFS=20=E8=AE=AE=E9=A2=98=E7=A7=BB=E5=85=A5=E5=AD=98?= =?UTF-8?q?=E5=82=A8=E6=9E=B6=E6=9E=84=E8=AF=B4=E6=98=8E=20=C2=A710.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Trap 12 非 CH395F 议题(`CH395F驱动层审计.md` 早已点破;重组时仅就地 更正未搬家,本次补正) - 完整内容与根因更正移入 `docs/架构设计/STM32F4-Base存储架构说明.md` §10.1 第 14 项——与第 10 项「严格 8.3 短名」通用规则同处,各得其所 - 原址留移档存根(编号保留防旧引用断链,同 Trap 19 惯例);索引行改 ↦、状态图例补「↦ 已移入他档」;待修清单措辞同步 - 全库确认 Trap 12 无代码引用;check_doc_links 两文件通过 --- docs/架构设计/STM32F4-Base存储架构说明.md | 1 + docs/问题记录/CH395F_Trap_Records.md | 15 ++++++--------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/架构设计/STM32F4-Base存储架构说明.md b/docs/架构设计/STM32F4-Base存储架构说明.md index 8c0971ec..9c9d25d7 100644 --- a/docs/架构设计/STM32F4-Base存储架构说明.md +++ b/docs/架构设计/STM32F4-Base存储架构说明.md @@ -569,6 +569,7 @@ if (nand_ftl_format() == 0) { 11. **FA_OPEN_APPEND 陷阱 / 短写** — `FA_OPEN_APPEND`(0x30) = `FA_OPEN_ALWAYS|FA_SEEKEND`,**不含 `FA_WRITE`(0x02)**——不带写位打开的文件 `f_write` 恒返回 `FR_DENIED`(7)(这是当年 `/log` 写失败的真根因);追加写必须写 `FA_WRITE | FA_OPEN_APPEND`(2026-09-20 台架实测,`Lib/FatFs` 移植版)。该移植的 `f_write` 在簇分配失败时返回 `FR_OK` + 短写(**没有错误码**)→ 判失败必须校验写回的字节数。标志位为**旧式编号**:`FA_READ`=0x01 / `FA_WRITE`=0x02 / `FA_CREATE_NEW`=0x04 / `FA_CREATE_ALWAYS`=0x08 / `FA_OPEN_ALWAYS`=0x10 / `FA_OPEN_APPEND`=0x30 / `FA_SEEKEND`=0x20(`ff.c` 内部定义),与上游 FatFs R0.14 不同,**勿照抄上游代码示例**。 12. **崩溃可观测性(勿屏蔽)** — 已开启 `configCHECK_FOR_STACK_OVERFLOW=2` + `configUSE_MALLOC_FAILED_HOOK` + HardFault 现场报告(`Src/stm32f4xx_it.c`:裸 UART 打印 PC/LR/xPSR + CFSR/HFSR/BFAR/MMFAR 后复位,对照 `.map` 定位符号)。defaultTask **1024** words、testTask **2048** words 是**实测下限,勿降**。开机日志出现 `[ERR] STACK OVERFLOW task=` / `[ERR] HARDFAULT ...` / `MALLOC FAILED` 是精确定位线索,禁止屏蔽或降级。 13. **BBT 恢复模式** — 运行期坏块会随 TC-STO-03 注入累积;当介质被**误标脏**(如 DMA 误打 CCM 栈导致随机块被写坏并持久化)时,打开 `test/board/storage_test.h` 的 `ENABLE_STORAGE_RECOVERY_TESTS` 编译**恢复构建**,testTask 会执行 `gd5f2gq5ue_bbt_rebuild()`(出厂扫描清除运行期误标)+ `nand_ftl_format()` 清账后复位。恢复后容量回到基线(即扣除 BBT 保留池后的正常容量,非出厂裸容量)。 +14. **lftpd 路径口径(归档更正 2026-10-13:原误记于 `docs/问题记录/CH395F_Trap_Records.md` Trap 12,移入本节)** — FTP STOR/CWD 曾频发 `FR_INVALID_NAME`(6):真约束即上条第 10 项的严格 8.3。原记录"FatFS 不接受 `/` 前缀 / 空字符串失败"两条与仓内 `Lib/FatFs/ff.c` 矛盾(`ff.c:3101-3109` 会剥前导分隔符、`:3127-3129` 空名返回根目录),**已删**。现行口径:`lftpd_io.c` 的 `to_fatfs_path()` 剥 `/` 前缀(行为正确,保留)+ 本仓路径用 `"0:"` 卷前缀(`FF_FS_RPATH=0` 下 `"."` 不是合法路径段)。长文件名需 `FF_USE_LFN=1` + `FF_LFN_UNICODE`(显著增 RAM,未启用)。 ### 10.2 FTP 服务(lftpd)鉴权语义(2026-10-11,N-19) diff --git a/docs/问题记录/CH395F_Trap_Records.md b/docs/问题记录/CH395F_Trap_Records.md index 45ce90a5..ae418d4c 100644 --- a/docs/问题记录/CH395F_Trap_Records.md +++ b/docs/问题记录/CH395F_Trap_Records.md @@ -6,7 +6,7 @@ > 按主题分章并加索引;条目格式统一为 `## Trap NN · 短标题` + 固定小节(状态/现象/根因(定案)/修复/回归判据/教训, > 视情况取舍)。**编号永不重排**——代码注释大量引用 `Trap NN`,合并条目标题保留双编号; > Trap 19 从未存在(悬空引用登记在第六章待修清单)。 -> 状态图例:✅ 已修复定案 · ⏳ 待修 · ↩️ 已撤回(非缺陷) · — 从未存在。 +> 状态图例:✅ 已修复定案 · ⏳ 待修 · ↩️ 已撤回(非缺陷) · ↦ 已移入他档 · — 从未存在。 ## 索引 @@ -23,7 +23,7 @@ | 09 | 沉降窗(link up / close 后) | ✅ | open 失败重试 + `net_sock_reset_sync` 确定性排渣;LISTEN 态关闭是两段 CLOSE | | 10 | 消息队列值拷贝 | ✅ | 结果必须走 `xTaskNotify` 通知值回传 | | 11 | accept 拒绝 ESTABLISHED | ✅ | standalone_accept 后状态已 ESTABLISHED,入口检查须放行该组合 | -| 12 | FatFS 路径与 8.3 短名 | ✅ | 真约束=`FF_USE_LFN=0` 只认 8.3;本仓用 `"0:"` 卷前缀(原 #2/#4 根因已按 ff.c 更正) | +| 12 | FatFS 路径与 8.3 短名 | ↦ | **非 CH395F 议题**(FTP 改造期归档错位)→ 已移入 `docs/架构设计/STM32F4-Base存储架构说明.md` §10.1 第 14 项 | | 13/21 | 「recv_len 归零 / 接收冻结」家族 | ↩️ | 合并结案:非缺陷,全部为台架假象;机理与 §5.34 相左已撤回 | | 14/17 | 残留 DISCONNECT 污染下一连接 | ✅ | 合并:滞后锁存 + SINT 粘滞 → 预清 + 反核实 + close 后排空 | | 15 | DHCP 后 IP 寄存器提交滞后 | ✅ | 状态位≠寄存器生效:连续 3 次一致才采信 | @@ -307,13 +307,10 @@ - **注意**:指针成员(`msg.buf`)不受值拷贝影响;内核级 `_locked` API 直接操作不受影响。⚠ 原注"net_connect() 不使用消息队列"已失效——现行 `net_connect` 同样走消息队列、结果同样走通知值。 - **发现时间**:FTP 改造期(2026-08)。 -## Trap 12 · FatFS 路径与 8.3 短名 +## Trap 12 · FatFS 路径与 8.3 短名(↦ 已移入他档) -- **状态**:✅ 已修复(修复有效;原根因 #2/#4 已按仓内 FatFs 源码更正) -- **现象**:FTP STOR `f_open("/upload_test.txt", ...)` 返 `FR_INVALID_NAME`(6);>8.3 的文件名同样 err=6。 -- **根因(定案,2026-10-13 更正)**:真约束是 `ffconf.h` `FF_USE_LFN=0` → **只支持严格 8.3 短名**(不得以点开头)。原记"FatFS 不接受 `/file` 前缀 / 空字符串失败"两条与仓内 `Lib/FatFs/ff.c` 矛盾(`ff.c:3101-3109` 会剥前导分隔符、`:3127-3129` 空名返回根目录)——**已删**;本仓现行路径口径是 `"0:"` 卷前缀(`FF_FS_RPATH=0` 下 `"."` 不是合法路径段)。 -- **修复**:`to_fatfs_path()` 剥离 `/` 前缀(保留,行为正确);长文件名需 `FF_USE_LFN=1` + `FF_LFN_UNICODE`(显著增 RAM,未启用)。 -- **发现时间**:FTP 改造期(2026-08)。 +- **状态**:↦ 移档——**非 CH395F 议题**(FTP 改造期归档错位;`docs/网络/N34-自愈审计/CH395F驱动层审计.md` 早已点破"Trap 12 非 CH395F")。完整内容与根因更正(含按 `Lib/FatFs/ff.c` 删去的两条误判根因)已移入 `docs/架构设计/STM32F4-Base存储架构说明.md` §10.1 第 14 项(2026-10-13)。 +- 编号在此保留仅为旧引用不断链;发现期:FTP 改造期(2026-08)。 ## Trap 15 · DHCP 成功后 IP 寄存器提交滞后 @@ -391,7 +388,7 @@ - 悬空引用:`Trap 19` 从未存在(`Test_Guide.md:878` 仍引用);`Test_Guide.md:353/418` 的"见风险 #4/风险表 #1"指向已删除的表。 - 口径冲突:`Test_Guide.md:1135` 的 TRAP-07 行仍写"卡 FIN_WAIT_2/`close_socket`(RST)"(与 Trap 07 定案冲突);`:45` 的"TC704 40/40"与 `:880` 的"`PHASE7_MULTI_COUNT` ≤5"矛盾(HEAD 宏值仍是 8);`Drivers/BSP/NET/README.md:35` 与 `net_socket.c` 的"正常 FIN 路径零改动"与 Trap 23 二阶段矛盾。 - ~~机理措辞~~(Trap 17/20 措辞、Trap 25 的 `NET_MSG_RESYNC_LISTEN`/`GET_REMOT_IPP`/§5.32+§5.23① 引用——**2026-10-13 重组已在本文件内更正**;`Test_Guide.md` 侧仍待同步)。 -- ~~已失效条目未标注~~(Trap 01/02/11 对现行初始化仍有效、Trap 09 `osDelay(7000)` 已被就绪屏障取代、Trap 10 注记失效、Trap 12 根因按 ff.c 更正——**2026-10-13 重组已全部就地标注**)。 +- ~~已失效条目未标注~~(Trap 01/02/11 对现行初始化仍有效、Trap 09 `osDelay(7000)` 已被就绪屏障取代、Trap 10 注记失效、Trap 12 移入存储架构说明 §10.1——**2026-10-13 重组已全部就地处置**)。 - ~~日期~~(Trap 01/02/03/04/06/07 的"2025-07"早于仓库首提交 `b1d05091` 2026-06-05;Trap 03/04/06 的 Phase 编号错——**2026-10-13 重组已标注为"不可考/已更正"**)。 - 证据通则:`*.log` 在 `.gitignore` 内 → 本文所有台架计数(40/40、34/34、227 轮、4 轮 11/11、13/13…)**脚本在库、日志未入库 = 不可复核**;Trap 25 引用的实验分支 `n30-chip-bench`(tip `eddbede`)不含 bench 源文件与 uvprojx 注册,裸机芯片台架无法从任何已提交状态重建(主分支存档见 `test/board/ch395f_reuse_bench.c`)。 From 925058a5ae41e609c4785c49f13e1006038542a7 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 13 Oct 2026 20:54:31 +0800 Subject: [PATCH 24/24] =?UTF-8?q?fix(NET)+docs:=20lftpd=20=E5=AF=B9?= =?UTF-8?q?=E7=AB=AF=E5=9C=B0=E5=9D=80=E4=BF=AE=E5=A4=8D=E3=80=81E6=20?= =?UTF-8?q?=E8=A3=81=E5=86=B3=E6=94=B6=E5=8F=A3=E3=80=81=E9=99=B7=E9=98=B1?= =?UTF-8?q?=E7=BC=96=E5=8F=B7=E8=BF=81=E7=A7=BB=E9=87=8D=E6=8E=92=20TRAP-N?= =?UTF-8?q?ET-01~21?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lftpd:net_inet_ntoa 段序改 MSB 首 + 新增 net_getpeername,FTP 日志打印 真实对端地址(真机实证 192.168.1.2:55524,登记 TRAP-NET-21) - E6:NET_U15_PROBE 命中=0(含压测)→ 阶段 3 项 4 不立项,U15 登记观察项; 插桩留仓(宏=0 零成本),soak 驱动 test/host/e6_soak.py 入库 - 编号:全库 Trap NN → TRAP-NET- 迁移并重排连续 01~21(单调映射, 沿革见 CH395F_Trap_Records.md 头注);死号 12/13/19/21 引用全部清除; GD5F 域引用补正为 TRAP-GD5F-NN - 陷阱文档全新化:索引 21 行行行真陷阱;删误判存档/发现时间/git 考古/ FTP 附录;台架纪律教训沉淀第六章方法论 - RAM:Code 211348→211456(+108 lftpd 修复),RW+ZI 净增 +4B; 优化方案/台账受管区/tsv 同笔同步 - ftp_abort_probe.py 挪 test/host/;引用关系图重生成 - 回归:@build 0/0;@test PASS;ftp_abort 10/10;relisten :7003 20/20; ci_local L0+L0.5 全绿;E6 窗口零复位,7 条 WD stall 登记压测假象备查 --- Drivers/BSP/CH395F/README.md | 8 +- Drivers/BSP/CH395F/ch395f.c | 8 +- Drivers/BSP/CH395F/ch395f.h | 18 +- Drivers/BSP/NET/README.md | 6 +- Drivers/BSP/NET/lftpd/README.md | 4 +- Drivers/BSP/NET/lftpd/lftpd.c | 11 +- Drivers/BSP/NET/lftpd/lftpd_inet.c | 2 +- Drivers/BSP/NET/net_listener.c | 24 +- Drivers/BSP/NET/net_listener.h | 2 +- Drivers/BSP/NET/net_select.c | 4 +- Drivers/BSP/NET/net_socket.c | 243 +- Drivers/BSP/NET/net_socket.h | 13 +- Drivers/BSP/NET/net_types.h | 6 +- Drivers/BSP/README.md | 2 +- Drivers/BSP/UI/ui_net_sink.c | 10 +- MDK-ARM/README.md | 3 +- MDK-ARM/bench_diag/bench_diag_abort_gap.py | 4 +- MDK-ARM/tcp_reconnect_probe.py | 2 +- docs/images/引用关系图.dot | 66 + docs/images/引用关系图.json | 464 +- docs/images/引用关系图.svg | 5580 +++++++++-------- docs/images/引用关系图_模块.dot | 40 +- docs/images/引用关系图_模块.svg | 40 +- docs/images/网络不稳根因总览.svg | 10 +- docs/内存管理/RAM使用分析与优化方案.md | 4 +- docs/内存管理/RAM预算台账.md | 2 +- docs/内存管理/存储栈RAM优化调研.md | 2 +- .../网络Socket监听生命周期设计说明.md | 50 +- docs/架构设计/STM32F4-Base存储架构说明.md | 2 +- docs/架构设计/项目启动顺序管理.md | 2 +- docs/测试说明/CH395F_Test_Guide.md | 69 +- docs/测试说明/FTP_Test_Guide.md | 8 +- docs/测试说明/GD5F2GQ5UE_Test_Guide.md | 4 +- docs/测试说明/链路延迟测试说明.md | 12 +- docs/编号规范.md | 2 +- docs/网络/BSD_Socket_API_使用指南.md | 48 +- docs/网络/BSD_Socket_API_函数调用关系.md | 56 +- docs/网络/N34-自愈审计/CH395F驱动层审计.md | 54 +- docs/网络/N34-自愈审计/README.md | 26 +- docs/网络/N34-自愈审计/net层自愈机制盘点.md | 86 +- docs/网络/N34-自愈审计/主流方案对照.md | 18 +- docs/网络/N34-自愈审计/实施方案.md | 30 +- docs/网络/N34-自愈审计/实验方案.md | 60 +- docs/网络/N34-自愈审计/网络不稳根因总览.md | 12 +- docs/网络/Net层阻塞模型架构设计.md | 4 +- docs/问题记录/CH395F_Trap_Records.md | 440 +- scripts/ram_baseline.tsv | 2 +- simulator/sim/net_socket_sim.c | 45 +- test/board/ch395f_reuse_bench.c | 12 +- test/board/ch395f_test_task.c | 28 +- test/board/net_latency_test.c | 2 +- test/board/net_test_task.c | 4 +- test/host/README.md | 14 +- test/host/ch395f_reuse_host.py | 4 +- test/host/e02_probe.py | 4 +- test/host/e6_soak.py | 64 + {MDK-ARM => test/host}/ftp_abort_probe.py | 4 +- tools/arch_diagram/gen_net_instability_svg.py | 8 +- 58 files changed, 4391 insertions(+), 3361 deletions(-) create mode 100644 test/host/e6_soak.py rename {MDK-ARM => test/host}/ftp_abort_probe.py (96%) diff --git a/Drivers/BSP/CH395F/README.md b/Drivers/BSP/CH395F/README.md index 770d8cfe..49c78225 100644 --- a/Drivers/BSP/CH395F/README.md +++ b/Drivers/BSP/CH395F/README.md @@ -21,9 +21,9 @@ | Trap | 问题 | 状态 | |---|---|---| -| Trap 21 | TC-NET-604 "RX 窗口冻结" 判定为**非缺陷**(原报告系测试程序自吞数据 + 判读工具误判) | ✅ 结案 | -| Trap 22 | **快速复连丢 CONNECT/DISCONNECT 中断 → 监听静默死亡**;修复 = `net_reconcile_tick()` 周期对账自愈 | ✅ 已修 | -| Trap 23 | **上传中止后首笔复连被静默截断(≤2048B)并误报成功**;修复 = 异常释放后复监听隔离排空 | ✅ 已修 | +| TC-NET-604 | "RX 窗口冻结" 判定为**非缺陷**(原报告系测试程序自吞数据 + 判读工具误判) | ✅ 结案 | +| TRAP-NET-17 | **快速复连丢 CONNECT/DISCONNECT 中断 → 监听静默死亡**;修复 = `net_reconcile_tick()` 周期对账自愈 | ✅ 已修 | +| TRAP-NET-18 | **上传中止后首笔复连被静默截断(≤2048B)并误报成功**;修复 = 异常释放后复监听隔离排空 | ✅ 已修 | > 完整台账与复现配方见 `docs/问题记录/CH395F_Trap_Records.md`(**改驱动前必读**)。 @@ -31,4 +31,4 @@ - 测试指南:`docs/测试说明/CH395F_Test_Guide.md` - 上层抽象:`../NET/README.md` -- 台架探针:`MDK-ARM/tcp_reconnect_probe.py`、`MDK-ARM/ftp_abort_probe.py` +- 台架探针:`MDK-ARM/tcp_reconnect_probe.py`、`test/host/ftp_abort_probe.py` diff --git a/Drivers/BSP/CH395F/ch395f.c b/Drivers/BSP/CH395F/ch395f.c index 87599495..912738da 100644 --- a/Drivers/BSP/CH395F/ch395f.c +++ b/Drivers/BSP/CH395F/ch395f.c @@ -1181,9 +1181,9 @@ void ch395f_write_send_buf(uint8_t sock, uint8_t *p_data, uint16_t len) { } if (len > (uint16_t)CH395F_SEND_BUF_SIZE_BYTES) { /* §5.32:单次写入长度不得大于该 Socket 的发送缓冲区(本仓 = 2 块 = 1KB)。 - * 超出不会被命令拒绝,而是写穿发送缓冲导致数据错乱(Trap 20)→ 只告警不拦截, + * 超出不会被命令拒绝,而是写穿发送缓冲导致数据错乱(TRAP-NET-16)→ 只告警不拦截, * 分块责任在调用方(NET 层 NET_SEND_CHUNK_MAX)。 */ - DBG_ERROR("write_send_buf OVERSIZE-SEND BUF: sock %u len %u > send buf %u (Trap 20)", + DBG_ERROR("write_send_buf OVERSIZE-SEND BUF: sock %u len %u > send buf %u (TRAP-NET-16)", (unsigned)sock, (unsigned)len, (unsigned)CH395F_SEND_BUF_SIZE_BYTES); } @@ -1674,7 +1674,7 @@ int ch395f_sock_reset_poll(uint8_t sn) { } /* 二段关闭实证(TC1501 台架 2026-10-13):LISTEN 态 Socket 的 CLOSE 返 * SUCCESS 但只拆监听(TCP 01->00),byte0 恒 05;需再发一次 CLOSE 才落 - * 00(生产 net 层靠 Trap 09 open 重试吸收此行为)。复位状态机自行补发, + * 00(生产 net 层靠 TRAP-NET-09 open 重试吸收此行为)。复位状态机自行补发, * 补发标记=drain 字段置 0xF(本阶段已用尽,复用安全)。 */ { ch395f_sock_state_t c = ch395f_sock_state_cached(sn); @@ -1740,7 +1740,7 @@ void ch395f_glob_event_feed(uint16_t glob_bits) { if ((glob_bits & CH395F_GINT_STAT_PHY_CHANGE) == 0U) { return; } - /* B9 判据(Trap 09 修正):PHY != DISCONN 即 link up */ + /* B9 判据(TRAP-NET-09 修正):PHY != DISCONN 即 link up */ up = (ch395f_get_phy_status() != CH395F_PHY_DISCONN) ? 1U : 0U; if (g_link_valid == 0U) { g_link_valid = 1U; diff --git a/Drivers/BSP/CH395F/ch395f.h b/Drivers/BSP/CH395F/ch395f.h index cb3cd9f0..c2966c8e 100644 --- a/Drivers/BSP/CH395F/ch395f.h +++ b/Drivers/BSP/CH395F/ch395f.h @@ -117,7 +117,7 @@ extern "C" { * 由 net_init 经 ch395f_set_tcp_mss() 下发(须在 ch395f_init 之前);此处为单一真值源。 * 维持 1024:8 Socket 共享 24KB 缓冲时,单 Socket 接收缓冲 = 2×MSS = 2KB(手册 §9.2.8 * 满足上限的最大值)。MSS=512 裕量方案未采纳;当年支撑该决策的台架判据不可信, - * 已随 TC-NET-604 结案为非缺陷一并撤回(2026-09-23,见 Trap 21)——维持 1024 的 + * 已随 TC-NET-604 结案为非缺陷一并撤回(2026-09-23)——维持 1024 的 * 现有理由 = 手册允许的最大值、满缓冲周期段数少。MSS 只影响芯片分包与窗口动态, * 与下方缓冲块布局解耦。 */ @@ -134,18 +134,18 @@ extern "C" { #define CH395F_RECV_BLOCKS 4U #define CH395F_SEND_BLOCKS 2U /* 单 Socket 硬件收发缓冲字节数(= 块数 × 块大小)。写入发送 FIFO 的单次长度严禁超过此值, - * 否则会写穿有限的发送缓冲(见 Trap 20)。 */ + * 否则会写穿有限的发送缓冲(见 TRAP-NET-16)。 */ #define CH395F_SEND_BUF_SIZE_BYTES (CH395F_SEND_BLOCKS * CH395F_RAM_BLOCK_SIZE) #define CH395F_RECV_BUF_SIZE_BYTES (CH395F_RECV_BLOCKS * CH395F_RAM_BLOCK_SIZE) /* * SPI DMA 事务限制 * CH395F_SPI_DMA_MAX_PAYLOAD: 单次 DMA 事务最大有效载荷(总缓冲 - 4 字节命令头) - * 缓冲按"一条命令读空整个 Socket 接收缓冲(2KB)"定容——排空契约(Trap 25, + * 缓冲按"一条命令读空整个 Socket 接收缓冲(2KB)"定容——排空契约(TRAP-NET-20, * bit3=1 残数据由 MCU 读出)的形态本身就是整缓冲单读。 - * (历史注记:旧注释把定容理由归给"Trap 13 部分读致剩余数据不可见",该机理 - * 2026-10-11 复核**未证实**且与手册 §5.34 相左,已降级为未证实历史观察, - * 见 Trap 记录 Trap 13 复核注;定容不依赖该观察。) + * (历史注记:旧注释把定容理由归给"部分读致 recv_len 归零、剩余数据不可见",该机理 + * 2026-10-11 复核**未证实**且与手册 §5.34 相左,已降级为未证实历史观察,结案见 + * `CH395F_Trap_Records.md` 台架纪律;定容不依赖该观察。) * 2026-09-22:与 MSS 解耦(此前 = 2×MSS+4,MSS 下调会把 DMA 缓冲缩到装不下整缓冲 * → 整缓冲读会被截断丢弃)。 */ @@ -200,10 +200,10 @@ extern "C" { * 功能参数标志位(CMD_SET_FUN_PARA,§5.52 表) * bit3 SOCKET_CLOSE 需芯片版本 ≥0x46(版本门控见 net_init,N34 阶段0-6); * bit1 TCP_SERVER 多连接模式——本项目单连接模型不用(红线,勿置 1); - * bit0 保留必须为 0(Trap 18 定案更正:0x09→0x08) + * bit0 保留必须为 0(TRAP-NET-15 定案更正:0x09→0x08) */ #define CH395F_FUN_PARA_TCP_SERVER (1UL << 1) /* TCP Server 多连接模式 */ -#define CH395F_FUN_PARA_SOCKET_CLOSE (1UL << 3) /* Socket 关闭控制(0=芯片自动,1=外部控制;Trap 25 排空契约) */ +#define CH395F_FUN_PARA_SOCKET_CLOSE (1UL << 3) /* Socket 关闭控制(0=芯片自动,1=外部控制;TRAP-NET-20 排空契约) */ #define CH395F_FUN_PARA_DISABLE_SEND_OK (1UL << 4) /* 禁用 SEND_OK 中断 */ /* @@ -796,7 +796,7 @@ uint8_t ch395f_sock_events(uint8_t sn); /* 复合复位模式(F-DRV-4;两模式都必须排 RX——E4 定案,差异只剩最短等待) */ typedef enum { - CH395F_RESET_GRACEFUL = 0, /* 最短等待 100ms(Trap 23 二阶段实测值) */ + CH395F_RESET_GRACEFUL = 0, /* 最短等待 100ms(TRAP-NET-18 二阶段实测值) */ CH395F_RESET_QUARANTINE /* 最短等待 400ms */ } ch395f_reset_mode_t; diff --git a/Drivers/BSP/NET/README.md b/Drivers/BSP/NET/README.md index 81692380..27eb1789 100644 --- a/Drivers/BSP/NET/README.md +++ b/Drivers/BSP/NET/README.md @@ -20,7 +20,7 @@ - 一期(RECV)已落地:`net_recv` 等支持带超时的阻塞(`pending` 挂起 + tick 唤醒) - 二期(ACCEPT + SEND)待做 → `docs/任务规划/登记簿.md` N-4 -### 周期对账自愈(Trap 22 修复;N34 阶段 3 合并 pump) +### 周期对账自愈(TRAP-NET-17 修复;N34 阶段 3 合并 pump) - `net_reconcile_tick()`(100ms,N34 阶段 3 起判据读**驱动状态缓存**——唯一 刷新点 `net_poll` 每轮全量刷新,E2 实测单次回读 18.1us)修正失同步: @@ -32,7 +32,7 @@ `relisten retry budget exhausted`(三监听口全死);①的 4 轮窗是 F-NET-17 M3 可疑电平的让位租约(勿改小) -### 异常复监听隔离(Trap 23 修复) +### 异常复监听隔离(TRAP-NET-18 修复) - 异常释放(TIMEOUT)的 socket 复监听前隔离:排空 RX 残渣 + 主动 close + 等 CLOSED - 400ms 起、800ms 上限强制放行(上限须 < 重试放弃时限 2000ms) @@ -58,7 +58,7 @@ - 就绪屏障位(`SYS_EVT_NET_READY` 等)等待必须 `osFlagsNoClear`,**不能用默认消费型** - 每次 CH395F SPI 事务须 `ch395f_spi_begin()/end()` 包裹 - **改任何自愈/重建逻辑必须去抖**,否则会引发重建风暴 -- 相关陷阱:`docs/问题记录/CH395F_Trap_Records.md` Trap 21/22/23 +- 相关陷阱:`docs/问题记录/CH395F_Trap_Records.md` TRAP-NET-17/-18 ## 相关文档 diff --git a/Drivers/BSP/NET/lftpd/README.md b/Drivers/BSP/NET/lftpd/README.md index 77a7d3bb..795b1f0b 100644 --- a/Drivers/BSP/NET/lftpd/README.md +++ b/Drivers/BSP/NET/lftpd/README.md @@ -31,11 +31,11 @@ lftpd(本目录) 否则位被先到的消费者清掉 → FTP 21 端口静默不监听(无日志无报错) - **lftpd 大局部缓冲**:已按 `docs/内存管理/RAM使用分析与优化方案.md` §1.4 静态化 9 处 大局部缓冲(BSS +4.6KB),使 ftpTask 栈峰值从 3480B 降到 1184B——**勿改回局部变量** -- **上传中止截断**:FTP STOR 中途断开后首笔复连被截断(Trap 23)已在 `net_socket.c` 侧修复 +- **上传中止截断**:FTP STOR 中途断开后首笔复连被截断(TRAP-NET-18)已在 `net_socket.c` 侧修复 - 本目录受 pre-commit 的**部分排除**(第三方代码) ## 相关文档 - 测试指南:`docs/测试说明/FTP_Test_Guide.md` -- 台架探针:`MDK-ARM/ftp_abort_probe.py`、`MDK-ARM/ftp_freeze_probe.py` +- 台架探针:`test/host/ftp_abort_probe.py`、`MDK-ARM/ftp_freeze_probe.py` - 内存优化:`docs/内存管理/RAM使用分析与优化方案.md` §1.4 diff --git a/Drivers/BSP/NET/lftpd/lftpd.c b/Drivers/BSP/NET/lftpd/lftpd.c index 3fe6f9dd..358edf4b 100644 --- a/Drivers/BSP/NET/lftpd/lftpd.c +++ b/Drivers/BSP/NET/lftpd/lftpd.c @@ -250,7 +250,7 @@ static int receive_file(int socket, const char *path) if (err < 0) { /* 异常结束(连接重置/超时/预算耗尽):删除截断文件防残留并返回错误码 - * → STOR 回报 450 而非误报 226(Trap 23 待办 #3) */ + * → STOR 回报 450 而非误报 226(TRAP-NET-18 待办 #3) */ lftpd_io_unlink(path); return err; } @@ -795,11 +795,16 @@ int lftpd_start(const char *directory, int port, lftpd_t *lftpd) { struct net_sockaddr_in addr; int addrlen = sizeof(addr); - if (net_getsockname(client_socket, + /* N34 E13 §6 修复:原用 getsockname 打印的是**本机** IP:21(且 + * ntoa 段序反转成 "100.1.168.192")——改 getpeername 打印真实 + * 对端(现场排障需要"谁连进来");未连接则退回无地址形态 */ + if (net_getpeername(client_socket, (struct net_sockaddr *)&addr, &addrlen) == 0) { char ip_str[16]; net_inet_ntoa(addr.sin_addr.s_addr, ip_str); - int remote_port = lftpd_inet_get_socket_port(client_socket); + /* 对端端口:直接取 getpeername 结果(原 get_socket_port + * 走 getsockname,打印的是本机监听端口) */ + int remote_port = (int)net_ntohs(addr.sin_port); lftpd_log_info("connection received from %s:%d...", ip_str, remote_port); } else { diff --git a/Drivers/BSP/NET/lftpd/lftpd_inet.c b/Drivers/BSP/NET/lftpd/lftpd_inet.c index 4a764713..85099fb6 100644 --- a/Drivers/BSP/NET/lftpd/lftpd_inet.c +++ b/Drivers/BSP/NET/lftpd/lftpd_inet.c @@ -35,7 +35,7 @@ void lftpd_task_wd_beat(void) int lftpd_inet_listen(int port) { /* N34 阶段1-6:应用层阻塞重试退役(原 5 轮 500ms/2000ms,最坏 10s 阻塞 - * ftpTask)——Trap 09 的启动形态已由就绪屏障消除(FTP_READY 等 NET_READY, + * ftpTask)——TRAP-NET-09 的启动形态已由就绪屏障消除(FTP_READY 等 NET_READY, * lftpd_start 外层本就有监听失败重建循环);芯片忙窗失败属罕见,PASV 数据 * 监听交给 FTP 客户端自身的重试语义(主流 FTP 服务器 passive listen 失败 * 亦直接 425,不在服务端叠应用层阻塞重试) */ diff --git a/Drivers/BSP/NET/net_listener.c b/Drivers/BSP/NET/net_listener.c index 86925820..ddc43684 100644 --- a/Drivers/BSP/NET/net_listener.c +++ b/Drivers/BSP/NET/net_listener.c @@ -1,7 +1,7 @@ /* * 模块名称:Network Listener Lifecycle * 模块功能:监听器生命周期(真机专属)—— 维持 fd 处于 LISTEN 态的全部机制: - * 重建(open+listen)、重建前的沉降/隔离(Trap 23)、排空 RX 残渣、 + * 重建(open+listen)、重建前的沉降/隔离(TRAP-NET-18)、排空 RX 残渣、 * D10 非阻塞重试(周期性重建决策者=N34 阶段 3 合并后的 * net_reconcile_tick,见 net_socket.c)。 * 适用平台:STM32F407ZGTx + CH395F 以太网芯片 @@ -42,7 +42,7 @@ /* ===== ① 策略常量与私有状态 ===== */ -/* Trap 23(2026-09-23 生产路径实测):异常释放(芯片 TIMEOUT,例如客户端中止上传) +/* TRAP-NET-18(2026-09-23 生产路径实测):异常释放(芯片 TIMEOUT,例如客户端中止上传) * 后的**隔离期**。实测时序:中止时对端仍有在途数据 → 芯片 TIMEOUT(此时 MCU 侧 recv=0) * 在途段随后落进芯片 RX 缓冲(约 2KB 残渣) * 若立即 open+listen,残渣被下一条连接当作自己的数据读走 @@ -70,13 +70,13 @@ static uint8_t s_relisten_quarantine[NET_MAX_SOCKETS]; /* 0=免清;1=异 static uint32_t s_relisten_settle_start[NET_MAX_SOCKETS]; /* 隔离起点(0 = 未开始) */ static uint32_t s_relisten_settle_dead[NET_MAX_SOCKETS]; /* 隔离上限到期时刻 */ -/* ===== Trap 09 非阻塞重监听重试(D10 修复,2026-09-22 M3 实测) ===== +/* ===== TRAP-NET-09 非阻塞重监听重试(D10 修复,2026-09-22 M3 实测) ===== * 旧实现:open 失败 → close → osDelay(500) → 重试,netTask 忙等 500ms(全网冻结) * 且实测恢复 588ms、期间客户端重连被拒 * 新实现:首次失败即登记待重试,由 netTask 每轮 net_process_messages() 到点重试 * 恢复时间 ≈ NET_RELISTEN_RETRY_MS 量级,任何时刻都不阻塞任务 */ -#define NET_RELISTEN_RETRY_MS 30U /* 重试间隔(Trap 09 沉降窗口;实测 100ms 首次必成, +#define NET_RELISTEN_RETRY_MS 30U /* 重试间隔(TRAP-NET-09 沉降窗口;实测 100ms 首次必成, * 收紧到 30ms 以缩短恢复时间——未成则下一轮到点再来) */ #define NET_RELISTEN_DEADLINE_MS 2000U /* 放弃时限(自首次尝试起) */ @@ -90,7 +90,7 @@ static lat_tmark_t s_relisten_mark[NET_MAX_SOCKETS]; /* 恢复计时起点( /* ===== ② 机制 ===== */ /* - * 函数功能:清空芯片该 socket 的 RX 残留(Trap 23) + * 函数功能:清空芯片该 socket 的 RX 残留(TRAP-NET-18) * 入口参数:sockfd - Socket 描述符 (0~7) int * 出口参数:无 * 返回值:无 @@ -132,7 +132,7 @@ static void net_drain_chip_rx(int sockfd) { * 2. 仅将 state 置回 LISTENING,不 memset(fd 与 auto_relisten 保持不变) * 3. 实现 BSD 风格"监听 Socket 常驻",应用层无需手动重监听 * 4. **单次尝试**:失败立即返回(不 osDelay 忙等) - * 5. Trap 09 沉降交给 net_listener_try / net_listener_retry_tick 非阻塞重试(见下) + * 5. TRAP-NET-09 沉降交给 net_listener_try / net_listener_retry_tick 非阻塞重试(见下) * 6. 旧实现在此 osDelay(500) 忙等,netTask 冻结 500ms、实测恢复 588ms(M3 台架 D10) * 7. N-32 §10:fd 由参数传入(不再用 `p_sock - s_net_socks` 反推), * 控制块经 net_get_sock() 取 —— 本模块**不访问** net_socket.c 的 static 数组 @@ -148,7 +148,7 @@ static int net_relisten_locked(int sockfd) { return NET_ERR_RETURN_1(NET_ERR_BADF); } - /* 释放隔离期(Trap 23):重监听前先清芯片收尾状态 */ + /* 释放隔离期(TRAP-NET-18):重监听前先清芯片收尾状态 */ if (s_relisten_quarantine[sockfd] != 0U) { /* 芯片状态两字节:[0]=Socket,[1]=TCP */ uint8_t st[2] = {0, 0}; @@ -217,9 +217,9 @@ static int net_relisten_locked(int sockfd) { /* 打开芯片侧 Socket */ status = ch395f_open_socket((uint8_t)sockfd); - /* open 失败 → BUSY(Trap 09:芯片需沉降时间才能重开) */ + /* open 失败 → BUSY(TRAP-NET-09:芯片需沉降时间才能重开) */ if (status != CH395F_ERR_SUCCESS) { - /* Trap 09:断开后芯片需沉降时间才能重新 open。此处只清理并返回 + /* TRAP-NET-09:断开后芯片需沉降时间才能重新 open。此处只清理并返回 * (严禁 osDelay 忙等)→ 由重试轮询到点再来。 */ /* 先关闭,交重试轮询(30ms 后复检) */ ch395f_close_socket((uint8_t)sockfd); @@ -247,9 +247,9 @@ static int net_relisten_locked(int sockfd) { * 清了它,抢先 relisten 之后消费者就再也得不到 EOF(B10"消费者卡死"原型)。 * session_dead 只由新 CONNECT 成功(handle_connect_event)或 memset * (net_close/net_socket)清零。 */ - /* 新监听不得继承上一条连接的释放原因(Trap 23 待办 #3) */ + /* 新监听不得继承上一条连接的释放原因(TRAP-NET-18 待办 #3) */ p_sock->released_abnormal = 0U; - /* N-30(Trap 25):清零前把对端端口转登为幽灵甄别基准 */ + /* N-30(TRAP-NET-20):清零前把对端端口转登为幽灵甄别基准 */ p_sock->last_peer_port = p_sock->remote_port; /* N-30:新监听会话,丢中断去抖计数清零 */ p_sock->lost_conn_seen = 0U; @@ -319,7 +319,7 @@ void net_listener_clear(int sockfd) { s_relisten_next[sockfd] = 0U; /* 清放弃时刻 */ s_relisten_dead[sockfd] = 0U; - /* Trap 23:一并清沉降/隔离状态 */ + /* TRAP-NET-18:一并清沉降/隔离状态 */ s_relisten_settle_dead[sockfd] = 0U; /* 清沉降起点 */ s_relisten_settle_start[sockfd] = 0U; diff --git a/Drivers/BSP/NET/net_listener.h b/Drivers/BSP/NET/net_listener.h index 2929f2a1..429e8100 100644 --- a/Drivers/BSP/NET/net_listener.h +++ b/Drivers/BSP/NET/net_listener.h @@ -1,7 +1,7 @@ /* * 模块名称:Network Listener Lifecycle (Header) * 模块功能:监听器生命周期模块的对外接口(**真机专属**)—— 维持 fd 处于 - * LISTEN 态的全部机制:重建、重建前的沉降/隔离(Trap 23)、排空 RX + * LISTEN 态的全部机制:重建、重建前的沉降/隔离(TRAP-NET-18)、排空 RX * 残渣、D10 非阻塞重试(周期重建决策者=N34 阶段 3 合并后的 * net_reconcile_tick,见 net_socket.c)。 * 适用平台:STM32F407ZGTx + CH395F 以太网芯片 diff --git a/Drivers/BSP/NET/net_select.c b/Drivers/BSP/NET/net_select.c index ab1146df..6cb9bf64 100644 --- a/Drivers/BSP/NET/net_select.c +++ b/Drivers/BSP/NET/net_select.c @@ -45,7 +45,7 @@ static int check_socket_error(int sockfd); * 函数说明:1. 轮询 Socket 控制块(RAM)中的读/写/异常状态,判断事件是否就绪 * 2. 不在此直接访问 CH395F SPI——所有 ch395f 事务统一由 netTask 的 net_poll() * 每 10ms 串行执行并刷新控制块状态(send_ready / readable),本函数仅做 - * RAM 读取,避免与应用任务发起的 SPI 访问竞争(见 Trap 16) + * RAM 读取,避免与应用任务发起的 SPI 访问竞争(见 TRAP-NET-14) * 3. 阻塞等待直到有事件就绪或超时 * 4. 通过 osDelay(1) 让出 CPU,平衡实时性与 CPU 利用率(状态刷新延迟 <=10ms) */ @@ -250,7 +250,7 @@ int net_poll_events(net_pollfd_t *fds, int nfds, int timeout) { * select()/poll() 在监听 socket 上永久立即就绪(忙轮询地雷;当前无 * 调用方,属潜在地雷)。连接到达经 NET_EVENT_CONNECTED 回调通知。 * 2. 其余态:readable 由 net_poll 在 RECV_OK 时置位、net_recv_locked - * 读空后清零。避免应用任务直接访问 CH395F SPI(见 Trap 16) + * 读空后清零。避免应用任务直接访问 CH395F SPI(见 TRAP-NET-14) */ static int check_socket_readable(int sockfd) { const net_sock_t *p_sock; diff --git a/Drivers/BSP/NET/net_socket.c b/Drivers/BSP/NET/net_socket.c index a2f336c8..02ba6996 100644 --- a/Drivers/BSP/NET/net_socket.c +++ b/Drivers/BSP/NET/net_socket.c @@ -39,6 +39,24 @@ */ #define NET_PHY_CHECK_INTERVAL_MS 5000U /* PHY 状态定期检查间隔(ms) */ +/* N34 E6 实验插桩(U15 失配频率,docs/网络/N34-自愈审计/实验方案.md §E6): + * 假设=「RX 非空但 readable=0」(RECV_OK 中断丢失的数据面形态)在生产真实 + * 发生。置 1 烧实验固件:net_poll 每 32 轮对在用 fd 发一条 GET_RECV_LEN, + * 命中计数 + 低频日志(每小时 ≤1 条防刷屏);跑 24h 常态 + E1 压测。 + * 判据:命中 >0 → 阶段 3 项 4(U15 内化)立项;=0(含压测)→ 上层 32 轮 + * 盲收保留现状、U15 登记观察项。**默认 0 = 产品构建零成本(编译剔除)**。 + * E6 已裁决(2026-10-13,实验方案 E6 结果):命中=0(含压测)→ 项 4 不立项,维持默认 0。 + * ⚠ 必须定义在文件最前部:bring_up 的 armed 日志与 net_poll 探测环两处消费。 */ +#define NET_U15_PROBE 0 +#if NET_U15_PROBE +#define NET_U15_PROBE_PERIOD 32U /* 每 32 轮 net_poll 探测一次(≈32ms) */ +#define NET_U15_PROBE_LOG_MS 3600000U /* 命中日志每小时至多 1 条 */ +static uint32_t s_u15_round; /* net_poll 轮次计数 */ +static uint32_t s_u15_hits; /* 累计确认命中次数(判据数据) */ +static uint32_t s_u15_last_log; /* 上次命中日志时刻(tick) */ +static uint8_t s_u15_cand[NET_MAX_SOCKETS]; /* 两级确认的候选标记(per-socket) */ +#endif + /* * 全局变量定义区 - 尽量避免使用全局变量 */ @@ -235,7 +253,7 @@ static bool s_mac_is_zero(const uint8_t *p_mac) { * 2. 启用 TCP Server 多连接模式(bit1=1),设置 TCP MSS、重传参数、KeepAlive 参数 * 3. 配置 IP/网关/子网掩码,或启用 DHCP * 4. 初始化 CH395F 协议栈并使能 PING 响应 - * 5. mac 非空且非全零:初始化**之前**设置 MAC(§9.2.1 ①,Trap 24; + * 5. mac 非空且非全零:初始化**之前**设置 MAC(§9.2.1 ①,TRAP-NET-19; * 写芯片 EEPROM,每上电重放同值幂等) */ int net_init(const char *ip, const char *mask, const char *gateway, const uint8_t *mac) { @@ -260,7 +278,7 @@ static bool s_mac_is_zero(const uint8_t *p_mac) { ch395f_reset(); /* 芯片版本留痕 + 0-6 版本门控(N34 阶段0,§5.52 表):bit3 SOCKET_CLOSE - * 需芯片 ≥0x46、TCP_SERVER 需 ≥0x44。本台架芯片实测 ≥0x46(Trap 25 定案 + * 需芯片 ≥0x46、TCP_SERVER 需 ≥0x44。本台架芯片实测 ≥0x46(TRAP-NET-20 定案 * SOCKET_CLOSE 实际生效);门控防批次漂移——版本不足则**拒置 bit3** * (退回芯片自动关断语义)并 [ERR] 留痕,不静默带病运行 */ uint8_t chip_ver = ch395f_get_version(); @@ -276,18 +294,18 @@ static bool s_mac_is_zero(const uint8_t *p_mac) { DBG_INFO("ch395f ver=0x%02X fun_para=0x%02X", chip_ver, fun_para); /* 功能参数(§5.52 表):bit3=1 SOCKET_CLOSE——DISCONNECT/TIMEOUT 后芯片 - * 不清缓冲,由 MCU 读出残留数据后再关 Socket(Trap 25 三次定案的排空契约); + * 不清缓冲,由 MCU 读出残留数据后再关 Socket(TRAP-NET-20 三次定案的排空契约); * bit1=0 TCP_SERVER 多连接模式不用(本项目单连接模型:每监听器独占一个 * Socket,监听转数据通道);bit0 保留必须为 0——旧注释"bit0=1 多 Socket * 模式"手册与网络资料均无出处(多 Socket 由 SN 命令族寻址实现,与 FUN_PARA * 无关),且违反保留位规定 → 0x09 改 0x08(2026-10-11 定案:官方手册 * V1C/V1E/V2.2/V2.3 的 bit0 全是保留位,官方例程 bit0=0 下 Socket 0~7 照常 - * 可用、FTP 例程本身就传 0x08;证据链见 Trap 18「定案更正」, + * 可用、FTP 例程本身就传 0x08;证据链见 TRAP-NET-15, * 0x08 下的多 Socket 台架复测登记 N-31) */ ch395f_set_fun_para(fun_para); /* 设置 TCP MSS:维持 1024(512 裕量方案未采纳,当年支撑它的台架判据不可信, - * 已随 TC-NET-604 结案为非缺陷一并撤回(2026-09-23,见 Trap 21)) */ + * 已随 TC-NET-604 结案为非缺陷一并撤回(2026-09-23)) */ /* 手册(§5.45):内部缓冲共 48 块 × 512B = 24KB,每 Socket 分配 6 块(接收 4 块 = 2KB + 发送 2 块 = 1KB) */ /* 块布局固定(与 MSS 解耦);MSS=1024 为满足布局的最大值(接收 = 2×MSS), * 真值源为 ch395f.h 的 CH395F_TCP_MSS */ @@ -347,7 +365,7 @@ static bool s_mac_is_zero(const uint8_t *p_mac) { /* 每 Socket 6 块:接收 CH395F_RECV_BLOCKS(=4 块=2KB) + 发送 CH395F_SEND_BLOCKS(=2 块=1KB),用满 48 块 */ /* recv_start = i×6,send_start = recv_start+4;Socket 7 占块 42~47,恰好用尽 0~47,互不重叠 */ /* 布局以手册 §5.45 为准;若实际芯片为其它缓冲规格需重新核算 */ - /* Trap 01:先配数据 Socket 1~7,再配监听 Socket 0 */ + /* TRAP-NET-01:先配数据 Socket 1~7,再配监听 Socket 0 */ uint8_t blocks_per_socket = (uint8_t)(CH395F_RECV_BLOCKS + CH395F_SEND_BLOCKS); for (int i = NET_MAX_SOCKETS - 1; i >= 0; i--) { uint8_t recv_start = (uint8_t)(i * blocks_per_socket); @@ -424,21 +442,6 @@ static void net_bench_cmd_log(char tag, int sock, int detail) { } #endif -/* N34 E6 实验插桩(U15 失配频率,docs/网络/N34-自愈审计/实验方案.md §E6): - * 假设=「RX 非空但 readable=0」(RECV_OK 中断丢失的数据面形态)在生产真实 - * 发生。置 1 烧实验固件:net_poll 每 32 轮对在用 fd 发一条 GET_RECV_LEN, - * 命中计数 + 低频日志(每小时 ≤1 条防刷屏);跑 24h 常态 + E1 压测。 - * 判据:命中 >0 → 阶段 3 项 4(U15 内化)立项;=0(含压测)→ 上层 32 轮 - * 盲收保留现状、U15 登记观察项。**默认 0 = 产品构建零成本(编译剔除)**。 */ -#define NET_U15_PROBE 0 -#if NET_U15_PROBE -#define NET_U15_PROBE_PERIOD 32U /* 每 32 轮 net_poll 探测一次(≈32ms) */ -#define NET_U15_PROBE_LOG_MS 3600000U /* 命中日志每小时至多 1 条 */ -static uint32_t s_u15_round; /* net_poll 轮次计数 */ -static uint32_t s_u15_hits; /* 累计命中次数(判据数据) */ -static uint32_t s_u15_last_log; /* 上次命中日志时刻(tick) */ -#endif - /* * 函数功能:轮询 CH395F 中断状态,更新 Socket 连接状态并处理事件 * 入口参数:无 @@ -576,26 +579,39 @@ int net_poll(void) { } #if NET_U15_PROBE - /* E6 探测(宏说明见文件上方):RX 非空而 readable=0 = RECV_OK 中断丢失 - * 的数据面形态。独立于上面的缓存刷新环:GET_RECV_LEN 是独立 SPI。 */ + /* E6 探测(宏说明见文件前部):RX 非空而 readable=0 = RECV_OK 中断丢失 + * 的数据面形态。**两级确认去良性竞态**:数据刚到、本轮中断尚未读取的 + * 竞态窗(≤1 个 poll)会在下一探测轮自然消失;丢中断则持续存在直到 + * 盲收/超时。候选轮标记 → 下一探测轮仍成立才计确认命中。排除 CLOSED: + * closed-with-data 是已知被救回形态,非 U15 假设对象。独立于上面的 + * 缓存刷新环:GET_RECV_LEN 是独立 SPI。 */ s_u15_round++; if ((s_u15_round % NET_U15_PROBE_PERIOD) == 0U) { for (int i = 0; i < NET_MAX_SOCKETS; i++) { uint16_t rlen; if (s_net_socks[i].in_use == 0U) { + s_u15_cand[i] = 0U; continue; } rlen = ch395f_get_recv_len((uint8_t)i); - if ((rlen > 0U) && (s_net_socks[i].readable == 0U)) { - s_u15_hits++; - /* 首次命中必打(last_log=0 会被 uptime<1h 抑制);此后每小时 ≤1 条 */ - if ((s_u15_hits == 1U) || - ((uint32_t)(HAL_GetTick() - s_u15_last_log) >= NET_U15_PROBE_LOG_MS)) { - s_u15_last_log = HAL_GetTick(); - DBG_INFO("U15 probe: sock%d rx=%u readable=0 hits=%lu", - i, (unsigned int)rlen, (unsigned long)s_u15_hits); + if ((rlen > 0U) && (s_net_socks[i].readable == 0U) && + (s_net_socks[i].state == NET_SOCK_STATE_ESTABLISHED)) { + if (s_u15_cand[i] != 0U) { + s_u15_cand[i] = 0U; + s_u15_hits++; + /* 首次命中必打(last_log=0 会被 uptime<1h 抑制);此后每小时 ≤1 条 */ + if ((s_u15_hits == 1U) || + ((uint32_t)(HAL_GetTick() - s_u15_last_log) >= NET_U15_PROBE_LOG_MS)) { + s_u15_last_log = HAL_GetTick(); + DBG_INFO("U15 probe: sock%d rx=%u readable=0 hits=%lu", + i, (unsigned int)rlen, (unsigned long)s_u15_hits); + } + } else { + s_u15_cand[i] = 1U; } + } else { + s_u15_cand[i] = 0U; } } } @@ -615,7 +631,7 @@ int net_poll(void) { #define NET_RECON_STABLE_DEATH 4U /* 形态① 连接已失(M3 让位窗 400ms,勿改小) */ /* - * 函数功能:芯片/本层 socket 状态周期对账(Trap 22 丢中断自愈)+ 监听器 pump + * 函数功能:芯片/本层 socket 状态周期对账(TRAP-NET-17 丢中断自愈)+ 监听器 pump * (N-32 P2)—— N34 阶段 3 合并为单一 100ms tick(docs/网络/N34-自愈审计/实施方案.md §5 项 1) * 入口参数:无 * 出口参数:无 @@ -712,7 +728,7 @@ static void net_reconcile_tick(void) { } else if (p_sock->state == NET_SOCK_STATE_LISTENING) { if (c.tcp_state == CH395F_TCP_ESTABLISHED) { /* ② 本层 LISTENING 而芯片已完成握手(丢 CONNECT 中断)→ 补做 accept - * N-30(Trap 25)幽灵态甄别:拆链收尾窗口内芯片假报 ESTABLISHED, + * N-30(TRAP-NET-20)幽灵态甄别:拆链收尾窗口内芯片假报 ESTABLISHED, * 回读对端端口 = 上一条连接端口 → 不补 accept(补了会把数据发给 * 已死的旧连接 = 生产 LIST#4 数据被吞的直接成因),等拆净或新 SYN。 * 这是本 tick 唯一的按需 SPI(仅形态②命中时发) */ @@ -737,7 +753,7 @@ static void net_reconcile_tick(void) { * 被动修正零风险:不去抖、不占冷却(原 pump 同口径) */ DBG_INFO("sock%d reconcile: chip listening, resync CLOSED->LISTENING (U11)", i); p_sock->state = NET_SOCK_STATE_LISTENING; - /* N-30(Trap 25):死连接对端端口转登幽灵甄别基准 + 探针计数清零 + /* N-30(TRAP-NET-20):死连接对端端口转登幽灵甄别基准 + 探针计数清零 * —— 芯片监听器未被重开,芯片侧对端信息可能仍是上一条连接的, * 必须转登为基准,否则形态② 的幽灵拦截会失效 */ p_sock->last_peer_port = p_sock->remote_port; @@ -787,7 +803,7 @@ static void net_reconcile_tick(void) { i, c.open, c.tcp_state); /* 仅修 MCU 侧;新连接的 CONNECT 中断会把两侧重新同步 */ p_sock->state = NET_SOCK_STATE_LISTENING; - /* N-30(Trap 25):死连接对端端口转登幽灵甄别基准 + 探针计数清零 */ + /* N-30(TRAP-NET-20):死连接对端端口转登幽灵甄别基准 + 探针计数清零 */ p_sock->last_peer_port = p_sock->remote_port; p_sock->lost_conn_seen = 0U; /* 取消本 fd 在死连接上的挂起 recv(必通知,防调用方挂死) */ @@ -811,7 +827,7 @@ static void net_reconcile_tick(void) { /* * 函数功能:netTask 主循环核心 —— 自愈 tick 宿主 + 消息队列分派器。每轮 * 先跑三个非阻塞自愈 tick(D10 重监听 / L3 挂起 recv / - * Trap 22 状态对账),再非阻塞排空队列按 msg.type 分派到 _locked 实现。 + * TRAP-NET-17 状态对账),再非阻塞排空队列按 msg.type 分派到 _locked 实现。 * 入口参数:无 * 出口参数:无 * 返回值:无(void);消息结果经 xTaskNotify 通知值回传(成功=字节数/描述符, @@ -823,7 +839,7 @@ static void net_reconcile_tick(void) { * 节拍重试 open+listen,2s 总时限到期放弃留痕;本 fd 有在途挂起 recv 时跳过(L3 保险) * 3. net_pending_tick(L3):按节拍了结挂起 recv(数据/到点[仅显式预算]/断连); * L3 tripwire(N34 阶段 1.5):挂起超 60s 只 DBG_ERROR 留痕一次,不再强制了结/伪造返回值 - * 4. net_reconcile_tick(Trap 22 + pump 合并,N34 阶段 3):每 100ms 对账 + * 4. net_reconcile_tick(TRAP-NET-17 + pump 合并,N34 阶段 3):每 100ms 对账 * ①②⑤ + ③④ 重建 + U11 重同步,判据读驱动状态缓存(net_poll 每轮 * 全量刷新,E2 背书);去抖分级(②=1 轮/③④⑤=2 轮/①=4 轮,M3 让位窗) * + 1s 冷却;重建失败进 D10 重试队列,reconciler 不重复催 @@ -841,11 +857,11 @@ void net_process_messages(void) { } /* 三个非阻塞自愈 tick 置于消息处理之前:断连后即使无应用消息,监听器也能自主恢复 */ - /* D10 重监听重试(Trap 09):沉降后重开芯片监听器 */ + /* D10 重监听重试(TRAP-NET-09):沉降后重开芯片监听器 */ net_listener_retry_tick(); /* L3 挂起 recv 完成检查:数据到达 / 到点 / 断连时通知调用方 */ net_pending_tick(); - /* Trap 22 状态对账 + 监听器 pump(N34 阶段 3 合并单一 100ms tick): + /* TRAP-NET-17 状态对账 + 监听器 pump(N34 阶段 3 合并单一 100ms tick): * 判据读驱动缓存(net_poll 每轮全量刷新),①②⑤ 失同步 + ③④ 重建 + * U11 重同步全部在此(本层唯一周期重建入口,N-32 P2 单一所有者) */ net_reconcile_tick(); @@ -1083,7 +1099,7 @@ int net_socket(int domain, int type) { * net_setblocking(fd, 0) 显式设置)。close 的 memset 会清零本字段, * 新 fd 必须在此显式置 1(同 released_abnormal 的"不得继承"纪律) */ p_sock->blocking = 1; - /* 新 fd 不得继承上一条连接的释放原因(Trap 23 待办 #3) */ + /* 新 fd 不得继承上一条连接的释放原因(TRAP-NET-18 待办 #3) */ p_sock->released_abnormal = 0; /* N-32(P1):新 fd 不得继承"上一条会话已结束"电平(否则新建即 EOF, * 消费侧连数据都收不到)——与 released_abnormal 同理,显式清零 */ @@ -1157,7 +1173,7 @@ int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen) { uint8_t status; /* net_init 已为全部 8 个 Socket 分配缓冲;此处仅对 Socket 4~7 兜底重设, - * 使用与 net_init 相同的布局(固定 6 块/Socket,与 MSS 解耦,见 net_init / Trap 21) */ + * 使用与 net_init 相同的布局(固定 6 块/Socket,与 MSS 解耦,见 net_init) */ if (sockfd >= 4) { /* 每 Socket 块数 = 接收 + 发送 */ uint8_t blocks_per_socket = (uint8_t)(CH395F_RECV_BLOCKS + CH395F_SEND_BLOCKS); @@ -1218,7 +1234,7 @@ int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen) { * 2. 内部序列=E4 定案:读空 RX -> CLOSE -> 排空事件(quiescent) -> * 轮询 byte0=00H;WAIT_CLOSE 内置 LISTEN 态二段 CLOSE 补发 * (E2 补充发现:一次 CLOSE 只拆监听 byte0 恒 05) - * 3. 超时留痕返 -1,调用方按原语义兜底(Trap 09 重试环 / close 放行) + * 3. 超时留痕返 -1,调用方按原语义兜底(TRAP-NET-09 重试环 / close 放行) */ static int net_sock_reset_sync(int sockfd, ch395f_reset_mode_t mode) { /* 起始时刻(600ms 上限基准) */ @@ -1252,7 +1268,7 @@ static int net_sock_reset_sync(int sockfd, ch395f_reset_mode_t mode) { * 出口参数:无 * 返回值:0 - 成功,-1 - 失败 int * 限定条件:sockfd 已绑定且为 TCP;netTask 上下文 - * 函数说明:1. 芯片侧 open + TCP_LISTEN(端口=控制块 local_port;open 失败重试 3 次,见 Trap 09) + * 函数说明:1. 芯片侧 open + TCP_LISTEN(端口=控制块 local_port;open 失败重试 3 次,见 TRAP-NET-09) * 2. 成功置 LISTENING 并启用 auto_relisten(BSD 风格监听常驻) * 3. 启动监听前清**本 Socket** 中断状态寄存器(读即清),防残留误触发; * 全局中断寄存器交 net_poll 集中读清(B3,详见函数体内注释) @@ -1281,11 +1297,11 @@ int net_listen_locked(int sockfd) { /* 本地端口 = 控制块 local_port */ ch395f_set_sour_port(sockfd, p_sock->local_port); - /* CH395F 关闭 Socket 后需延时才能重新打开(Trap 09),故失败重试最多 3 次。 + /* CH395F 关闭 Socket 后需延时才能重新打开(TRAP-NET-09),故失败重试最多 3 次。 * P2b 收口(N34 阶段 3):原"close + 盲等 500ms"改**确定性排渣**—— * sock_reset(读空 RX -> CLOSE -> 排空事件 -> 等 byte0=00H,内置 LISTEN * 态二段 CLOSE 补发;E4 定案 60 轮实证复位后 open 必成)。GRACEFUL - * 最短等 100ms ≪ 原 500ms 盲等;复位失败/超时仍走下一轮重试,Trap 09 语义保留 */ + * 最短等 100ms ≪ 原 500ms 盲等;复位失败/超时仍走下一轮重试,TRAP-NET-09 语义保留 */ for (int open_try = 0; open_try < 3; open_try++) { /* 尝试打开芯片侧 Socket */ status = ch395f_open_socket(sockfd); @@ -1315,7 +1331,7 @@ int net_listen_locked(int sockfd) { p_sock->state = NET_SOCK_STATE_LISTENING; /* 单连接模式:连接断开后由 net 层自动回到 LISTEN(模拟 BSD 监听常驻) */ p_sock->auto_relisten = 1; - /* N-30(Trap 25):新监听会话,丢中断去抖计数清零 */ + /* N-30(TRAP-NET-20):新监听会话,丢中断去抖计数清零 */ p_sock->lost_conn_seen = 0U; /* 读即清:仅本 Socket 的中断状态——丢弃上一条会话残留的 DISCONNECT/ * TIMEOUT,避免监听刚开始就误触发事件。 @@ -1326,10 +1342,10 @@ int net_listen_locked(int sockfd) { * (芯片无按位清除手段),而 net_poll 只在**全局位置位时**才去读该 * Socket 的 SINT(见 :469 门控)→ 全量清等于把别的在用 Socket 的门一并 * 焊死,其 CONNECT/DISCONNECT 等边沿事件永久丢失(RECV 属条件位,下轮 - * 可补回,见 Trap 08)。 + * 可补回,见 TRAP-NET-08)。 * 全局寄存器交 net_poll 集中读清(AGENTS 模块约定:所有 CH395F 中断经 * net_poll 集中读取并清除);本 Socket 残留的全局位被 net_poll 读到时 - * SINT 已为 0,无害了结。Trap 14/17/23 指定的两级预清落在 + * SINT 已为 0,无害了结。TRAP-NET-12/-18 指定的两级预清落在 * net_connect_locked / net_close_locked / relisten 隔离段,均为单 Socket, * 本次未动。 */ (void)ch395f_get_sock_int_status((uint8_t)sockfd); @@ -1405,7 +1421,7 @@ int net_listen(int sockfd) { * (芯片状态寄存器 → ESTABLISHED)但 **CONNECT 中断被丢**——纯事件驱动的 accept * 永远等不到 → 数据被吞 / 425。固定沉降延时、Socket 轮换、listen 后 close 重试、 * 缓冲重分配均已被实验证伪;唯一有效 = "中断 + 状态轮询"双信源(修复组 15/15 - * 全过,机制触发 5 次全部救回)。→ CH395F_Trap_Records.md Trap 25 + * 全过,机制触发 5 次全部救回)。→ CH395F_Trap_Records.md TRAP-NET-20 * 幽灵态甄别:拆链窗口内芯片也会直接假报 ESTABLISHED(旧连接未拆净),此时回读 * 对端端口 = 上一条连接端口;新连接临时端口几乎必然不同 → 端口相同不 accept, * 等旧连接拆净 / 新 SYN 到达(新连接若也丢中断,后续轮询以新端口命中)。 */ @@ -1441,7 +1457,7 @@ static void net_lost_connect_probe(net_sock_t *p_sock) { /* 对端端口(小端 2 字节) */ uint16_t peer = (uint16_t)(remote[4] | (remote[5] << 8)); /* 幽灵甄别:仅"端口非零且 = 上一条连接端口"才判幽灵拦截; - * peer==0(芯片对端端口提交滞后,Trap 15 同类)不拦截——宁可补 accept + * peer==0(芯片对端端口提交滞后,TRAP-NET-13 同类)不拦截——宁可补 accept * 也不退回丢中断死等(2026-10-11 验证轮实测:peer==0 拦截把首连接 * 丢中断的原有对账救援也挡死,LIST#1 卡满 20s,比修复前更糟) */ if ((peer != 0U) && (peer == p_sock->last_peer_port)) { @@ -1466,9 +1482,9 @@ static void net_lost_connect_probe(net_sock_t *p_sock) { * 返回值:新连接的 Socket 描述符,失败返回 -1 int * 限定条件:Socket 处于 LISTENING 状态 * 函数说明:1. 单连接:监听 Socket 自身在 CONNECT 后已转为 ESTABLISHED - * 2. 等待期叠加芯片状态轮询探针(丢 CONNECT 中断自愈,Trap 25) + * 2. 等待期叠加芯片状态轮询探针(丢 CONNECT 中断自愈,TRAP-NET-20) * 3. CLOSED + 芯片 RX 缓冲有残留数据:接受"已关但可读"连接排空 - * (FUN_PARA §5.52 ② 保留现场契约,Trap 25 三次定案) + * (FUN_PARA §5.52 ② 保留现场契约,TRAP-NET-20 三次定案) */ static int net_accept_locked(int sockfd, struct net_sockaddr *addr, int *addrlen) { /* 监听 Socket 控制块 */ @@ -1484,7 +1500,7 @@ static int net_accept_locked(int sockfd, struct net_sockaddr *addr, int *addrlen /* 先轮询一次:把状态机刷新到芯片侧最新状态 */ net_poll(); - /* N-30(Trap 25):监听等待期兼做芯片状态第二信源——丢 CONNECT 中断时 + /* N-30(TRAP-NET-20):监听等待期兼做芯片状态第二信源——丢 CONNECT 中断时 * 去抖补 accept(裸机台架实证:拆链窗口丢中断是数据被吞/425 的主根因, * 纯事件等待无自愈可能;详见 net_lost_connect_probe 区块注释) */ if (p_listen_sock->state == NET_SOCK_STATE_LISTENING) { @@ -1683,7 +1699,7 @@ int net_listener_armed(int sockfd) { * 函数说明:1. ESTABLISHED → 看 readable(net_poll 在 RECV_OK 置位、读空清零) * 2. 非 ESTABLISHED / 描述符失效一律返回 1:必须让上层真的 recv 一次 * 拿到 EOF/错误并复位自身状态,门掉它会把"连接已不存在"永久藏住 - * 3. ⚠ readable 仅由 RECV_OK 中断置位,Trap 22 对账不含"RX 非空但 + * 3. ⚠ readable 仅由 RECV_OK 中断置位,TRAP-NET-17 对账不含"RX 非空但 * readable=0" → 中断丢失时本函数恒返 0,调用方须保留低频兜底 recv */ int net_can_read(int sockfd) { @@ -1780,7 +1796,7 @@ int net_connect(int sockfd, const struct net_sockaddr *addr, int addrlen) { * 限定条件:net_init() 已调用;必须在 netTask 上下文中调用 * 函数说明:1. 校验控制块(in_use、TCP 类型、地址参数)并保存对端地址 * 2. 芯片侧 OPEN(失败重试 3 次,每次失败走 sock_reset 确定性排渣, - * 见 Trap 09 / P2b)+ 配置源/目的端口与目的 IP + * 见 TRAP-NET-09 / P2b)+ 配置源/目的端口与目的 IP * 3. 清本 Socket 残留中断状态(丢弃上一条连接遗留的 DISCONNECT) * 4. 发起 TCP_CONNECT(驱动内已轮询 CMD_STATUS)并置 CONNECTING; * 命令级被拒立即关 Socket 返 -1,TCP 层成败仍由 CONNECT/TIMEOUT 裁定 @@ -1831,11 +1847,11 @@ static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int a /* 设置协议类型为 TCP */ ch395f_set_proto_type(sockfd, CH395F_PROTO_TYPE_TCP); - /* CH395F 关闭 Socket 后需延时才能重新打开(Trap 09),故失败重试最多 3 次。 + /* CH395F 关闭 Socket 后需延时才能重新打开(TRAP-NET-09),故失败重试最多 3 次。 * P2b 收口(N34 阶段 3):原"close + 盲等 500ms"改**确定性排渣**—— * sock_reset(读空 RX -> CLOSE -> 排空事件 -> 等 byte0=00H,内置 LISTEN * 态二段 CLOSE 补发;E4 定案 60 轮实证复位后 open 必成)。GRACEFUL - * 最短等 100ms ≪ 原 500ms 盲等;复位失败/超时仍走下一轮重试,Trap 09 语义保留 */ + * 最短等 100ms ≪ 原 500ms 盲等;复位失败/超时仍走下一轮重试,TRAP-NET-09 语义保留 */ for (int open_try = 0; open_try < 3; open_try++) { /* 尝试打开芯片侧 Socket */ status = ch395f_open_socket(sockfd); @@ -1928,7 +1944,7 @@ static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int a */ #define NET_DMA_MAX_PAYLOAD ((int)CH395F_SPI_DMA_MAX_PAYLOAD) /* 单次写入 CH395F 发送 FIFO 的最大字节数:受硬件发送缓冲(CH395F_SEND_BUF_SIZE_BYTES=1KB) - * 与 SPI DMA 载荷(NET_DMA_MAX_PAYLOAD=2KB) 双重限制,以硬件发送缓冲为硬上限(见 Trap 20), + * 与 SPI DMA 载荷(NET_DMA_MAX_PAYLOAD=2KB) 双重限制,以硬件发送缓冲为硬上限(见 TRAP-NET-16), * 否则单次写 2KB/4KB 会写穿 1KB 发送 FIFO。 */ #define NET_SEND_CHUNK_MAX ((int)CH395F_SEND_BUF_SIZE_BYTES < NET_DMA_MAX_PAYLOAD ? \ (int)CH395F_SEND_BUF_SIZE_BYTES : NET_DMA_MAX_PAYLOAD) @@ -1941,7 +1957,7 @@ static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int a * 出口参数:无 * 返回值:实际发送字节数(>= 0);-1 表示参数无效或 Socket 未连接 int * 限定条件:p_sock 非空;Socket 处于 ESTABLISHED 状态;netTask 上下文 - * 函数说明:1. 按 NET_SEND_CHUNK_MAX(硬件发送缓冲上限,见 Trap 20)分块写入 CH395F 发送 FIFO + * 函数说明:1. 按 NET_SEND_CHUNK_MAX(硬件发送缓冲上限,见 TRAP-NET-16)分块写入 CH395F 发送 FIFO * 2. 发送前等待 send_ready 置位,超时返回已发送字节数 * 3. 每写完一块等待 SENBUF_FREE 中断(send_ready 清零后重新置位,§5.32/§5.23①) * 4. 发送中途断连(状态非 ESTABLISHED)→ 立即失败并返回已发字节 @@ -1972,7 +1988,7 @@ static int net_send_locked(net_sock_t *p_sock, const void *buf, int len) { while (sent < len) { /* 剩余待发字节 */ int chunk = len - sent; - /* 单块以硬件发送缓冲为上限(Trap 20:防写穿 1KB FIFO) */ + /* 单块以硬件发送缓冲为上限(TRAP-NET-16:防写穿 1KB FIFO) */ if (chunk > NET_SEND_CHUNK_MAX) { chunk = NET_SEND_CHUNK_MAX; } @@ -2095,7 +2111,7 @@ static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags, ch395f_read_recv_buf(sockfd, (uint8_t *)buf, (uint16_t)read_len); /* 累计接收字节统计 */ p_sock->recv_bytes += (uint32_t)read_len; - /* 已消费数据(部分读安全:手册 §5.34 未读数据保留可续读,旧"Trap 13 + /* 已消费数据(部分读安全:手册 §5.34 未读数据保留可续读,旧"部分读归零 * 部分读归零"已降级为未证实历史观察;下一次 RECV_OK 重新置 readable) */ p_sock->readable = 0; /* 返回本次读出字节数 */ @@ -2159,8 +2175,8 @@ static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags, * (INT# 电平触发链保证缓冲有数据时每轮重读 SINT 重位置位) * ② 到点(仅当登记时带显式预算 net_recv_to;无限等待登记无 deadline)→ NET_ERR_TIMEDOUT * ——这是调用方策略的正常兑现,不是伪造 - * ③ CLOSED:先排空(bit3=1 芯片断连后残数据留缓冲待 MCU 读——Trap 25 排空契约; - * 旧注归 Trap 13 已随其复核注降级)→ 有数据按数据完成,否则 EOF(0) + 自动重监听 + * ③ CLOSED:先排空(bit3=1 芯片断连后残数据留缓冲待 MCU 读——TRAP-NET-20 排空契约; + * 旧注归"部分读归零"机理、已随复核注降级)→ 有数据按数据完成,否则 EOF(0) + 自动重监听 * 其他状态(CONNECTING/LISTENING/…)保持等待状态迁移(CONNECT→ESTABLISHED 是合法路径; * 陈旧 resync 由显式取消处理)——不做状态推断取消,避免误杀正在建链的等待 * tripwire(N34 阶段 1.5 降级):挂起超 60s = "完成逻辑缺位"探针 → 只 [ERR] 留痕一次, @@ -2366,9 +2382,9 @@ static void net_pending_tick(void) { /* 非阻塞:失败由 D10 重试轮询接管(N-32 §10:入口收敛到 net_listener_try) */ (void)net_listener_try(i); } - /* Trap 23 待办 #3:异常释放(TIMEOUT/RST)不是正常 EOF(对端未发正常 FIN) + /* TRAP-NET-18 待办 #3:异常释放(TIMEOUT/RST)不是正常 EOF(对端未发正常 FIN) * → 以连接重置了结:应用(lftpd STOR)对异常结束回 5xx 而非误报 226; - * 正常 FIN(DISCONNECT)仍回 0(在途 FIN 与正常完成不可区分——协议边界,见 Trap 23 记录) */ + * 正常 FIN(DISCONNECT)仍回 0(在途 FIN 与正常完成不可区分——协议边界,见 TRAP-NET-18 记录) */ /* 异常释放:以连接重置了结(回 5xx,不误报 226) */ if (p_sock->released_abnormal != 0U) { /* 异常释放(错误码由 net_pending_complete 记入本任务槽位) */ @@ -2527,7 +2543,7 @@ static void net_pending_settle_close(net_sock_t *p_sock) { * (唯一读者=net_poll;原循环读全局会焊死其它 Socket 事件位) * 3. 快路径:芯片已 CLOSED(byte0=00H)→ 跳过复位只清残留事件 * 4. 复位超时照常放行(同原 500ms 超时容忍;后续 relisten 由 - * Trap 09 open 重试环吸收) + * TRAP-NET-09 open 重试环吸收) */ static int net_close_locked(net_sock_t *p_sock) { /* 由控制块指针反推描述符 */ @@ -2546,7 +2562,7 @@ static int net_close_locked(net_sock_t *p_sock) { } /* 清零控制块(memset 0 = 释放本 fd 全部状态) */ - /* N-30(Trap 25):幽灵甄别基准跨 fd 释放保留(幽灵属芯片 Socket 级状态, + /* N-30(TRAP-NET-20):幽灵甄别基准跨 fd 释放保留(幽灵属芯片 Socket 级状态, * 与 fd 复用无关——close 后新 listen 落在同一芯片 Socket 上仍可能撞幽灵) */ uint16_t keep_peer = p_sock->remote_port; memset(p_sock, 0, sizeof(net_sock_t)); @@ -2570,7 +2586,7 @@ static int net_close_locked(net_sock_t *p_sock) { * (PC 内核自动 ACK keepalive,僵尸判不死),单连接监听被永久 * 占住;应用又不得 net_close 监听 fd(毁监听)——本函数补上 * "拆连接不拆监听"的应用侧形态 - * 2. 拆除=sock_reset QUARANTINE(Trap 23 异常释放语义:FIN + 排渣 + + * 2. 拆除=sock_reset QUARANTINE(TRAP-NET-18 异常释放语义:FIN + 排渣 + * 排空事件(quiescent) + 等 byte0=00H,内置二段 CLOSE 补发) * 3. auto_relisten(监听型 fd):收口 CLOSED 后即时 net_listener_try * (芯片已 CLOSED 直接 open 必成,E4 定案;失败交 D10 重试 + @@ -2585,7 +2601,7 @@ static int net_drop_conn_locked(net_sock_t *p_sock) { /* L3:先了结本 fd 挂起 recv(排空/取消 + 必通知),再拆芯片会话 */ net_pending_settle_close(p_sock); - /* 确定性拆除(异常释放 → QUARANTINE 400ms 底,Trap 23 语义) */ + /* 确定性拆除(异常释放 → QUARANTINE 400ms 底,TRAP-NET-18 语义) */ if (net_sock_reset_sync(sockfd, CH395F_RESET_QUARANTINE) != 0) { DBG_ERROR("sock%d drop_conn: sock_reset not done", sockfd); /* 超时继续收口本层;芯片侧交 reconciler/pump 对账接管 */ @@ -2594,7 +2610,7 @@ static int net_drop_conn_locked(net_sock_t *p_sock) { /* 会话簿记收口(保留 fd:不 memset、不动 local_port/auto_relisten) */ p_sock->state = NET_SOCK_STATE_CLOSED; p_sock->readable = 0U; - /* N-30(Trap 25):新会话起点,丢中断去抖计数清零 */ + /* N-30(TRAP-NET-20):新会话起点,丢中断去抖计数清零 */ p_sock->lost_conn_seen = 0U; if (p_sock->auto_relisten) { @@ -2619,7 +2635,7 @@ static int net_drop_conn_locked(net_sock_t *p_sock) { * (netTask 阻塞在 xTaskNotifyWait 而无人处理消息);netTask 内请直接用 net_send_locked() * 2. Socket 须处于 ESTABLISHED 状态 * 3. net_init() + MX_FREERTOS_Init() 已执行,netMsgQueueHandle 已由 net_init 创建 - * 函数说明:1. 所有 CH395F SPI 访问只允许在 netTask 内串行执行(见 Trap 16) + * 函数说明:1. 所有 CH395F SPI 访问只允许在 netTask 内串行执行(见 TRAP-NET-14) * 2. 本函数不直接触碰 SPI,而是把发送请求打包成 net_msg_t 投递到 netMsgQueueHandle * 3. netTask 的 net_process_messages() 取出并在 netTask 上下文调用 net_send_locked() 真正写 CH395F * 4. 调用方经 FreeRTOS TaskNotify 阻塞等待处理结果,对外呈现"同步发送"语义 @@ -3049,7 +3065,7 @@ int net_sendto(int sockfd, const void *buf, int len, int flags, while (sent < len) { /* 剩余待发字节 */ int chunk = len - sent; - /* 单块以硬件发送缓冲为上限(Trap 20) */ + /* 单块以硬件发送缓冲为上限(TRAP-NET-16) */ if (chunk > NET_SEND_CHUNK_MAX) { chunk = NET_SEND_CHUNK_MAX; } @@ -3442,6 +3458,9 @@ uint32_t net_inet_addr(const char *cp) { * 返回值:成功返回 buf 指针;buf 为 NULL 时返回 NULL char * * 限定条件:buf 非空且长度足够(>= 16 字节) * 函数说明:1. 先经 NET_NTOHL 转为主机字节序再格式化输出 + * 2. ⚠ 段序(N34 E13 §6 修复):NTOHL 后主机序数值的高字节才是 + * 点分第一段(192.168.1.100 → 0xC0A80164 → "192.168.1.100")。 + * 原实现低段在前,显示成字节反转的 "100.1.168.192" */ char *net_inet_ntoa(uint32_t addr, char *buf) { /* 网络序 → 主机序 */ @@ -3452,12 +3471,12 @@ char *net_inet_ntoa(uint32_t addr, char *buf) { return NULL; } - /* 四段格式化为点分十进制(最低段在前,16B 缓冲) */ + /* 四段格式化为点分十进制(高段在前,16B 缓冲) */ snprintf(buf, 16, "%lu.%lu.%lu.%lu", - (unsigned long)(naddr & 0xFF), - (unsigned long)((naddr >> 8) & 0xFF), + (unsigned long)((naddr >> 24) & 0xFF), (unsigned long)((naddr >> 16) & 0xFF), - (unsigned long)((naddr >> 24) & 0xFF)); + (unsigned long)((naddr >> 8) & 0xFF), + (unsigned long)(naddr & 0xFF)); /* 返回输出缓冲 */ return buf; @@ -3656,7 +3675,7 @@ static void handle_disconnect_event(net_sock_t *p_sock) { /* 监听态:DISCONNECT 落在监听器上 */ if (p_sock->state == NET_SOCK_STATE_LISTENING) { - /* Trap 22 / N-6:监听态收到 DISCONNECT——bit3=1(0x08)下芯片**不**关该 Socket(停 0x05/0x00,芯片 TCP 侧已 CLOSED);不重开则此后所有 SYN 全被拒 = 监听永久死亡; + /* TRAP-NET-17 / N-6:监听态收到 DISCONNECT——bit3=1(0x08)下芯片**不**关该 Socket(停 0x05/0x00,芯片 TCP 侧已 CLOSED);不重开则此后所有 SYN 全被拒 = 监听永久死亡; * 触发 = 断连后 <~1s 快速复连时 CONNECT 中断未上报(控制块滞留 LISTENING)后该连接断开(2026-09-23 生产固件复现); * N-32 P2:本分支**只回读留痕、不再 relisten** —— 重建统一交 * net_reconcile_tick 形态③(每 100 ms 读驱动缓存,监听失位即重建)。原处直接 @@ -3695,7 +3714,7 @@ static void handle_disconnect_event(net_sock_t *p_sock) { } /* F-NET-17 M3(N34 实验 2026-10-13):矛盾态可疑门——芯片 Socket 仍 OPEN(0x05) * 而 TCP 槽 0x00 且本层 ESTABLISHED = 逻辑矛盾(活连接应 0x04):陈旧 DISCONNECT - * (拆链完成滞后锁存,Trap 14)与"真断开恰被旧拆链覆写槽"在 D20 上不可分辨 + * (拆链完成滞后锁存,TRAP-NET-12)与"真断开恰被旧拆链覆写槽"在 D20 上不可分辨 * (结构性失明,F-NET-15 Q1)。处置:不吃边沿,置可疑电平返回,交对账① * (ESTABLISHED + 芯片 TCP==CLOSED)电平去抖再入本函数(N34 阶段 3 合并 * 后=4×100ms,NET_RECON_STABLE_DEATH,让位租约 400ms 不变 + 冷却): @@ -3724,9 +3743,9 @@ static void handle_disconnect_event(net_sock_t *p_sock) { * 留给 net_recv_locked 把剩余数据读出来再关,置 CLOSED 等待应用重新监听 */ DBG_INFO("sock%d DISCONNECTED, releasing (chip st=0x%02X/0x%02X)", p_sock->ch395_sock, st[0], st[1]); - /* Trap 23 推广(2026-09-24 台架实证):正常 DISCONNECT 释放留下的芯片收尾状态 + /* TRAP-NET-18 推广(2026-09-24 台架实证):正常 DISCONNECT 释放留下的芯片收尾状态 * 会在复用同一 fd 的下一条连接上以"伪 DISCONNECT"形式投递(实测 accept 后约 10~20ms)→ 该连接被提前释放,应用把已到的 2048B 当整份文件并回报 226; - * 仅**本连接收过数据**时登记沉降(实测触发源 = 收过数据的数据通道被关闭),纯连接即关/只发不收的连接免沉降,保住 Trap 22 的快速复连时序(:21 @0ms ×50 全通过) */ + * 仅**本连接收过数据**时登记沉降(实测触发源 = 收过数据的数据通道被关闭),纯连接即关/只发不收的连接免沉降,保住 TRAP-NET-17 的快速复连时序(:21 @0ms ×50 全通过) */ /* F-NET-15 M1(N34 实验 2026-10-12):沉降**无条件**登记。原 recv_bytes!=0 门的 * 受害者由事件时序定义(accept 后 ~20ms 被陈旧 DISCONNECT 杀 → recv_bytes==0 是 * 构造性必然),门对"残渣循环自持"形态失效(n30_repro 背靠背 9/10 失败,E1 实测); @@ -3749,7 +3768,7 @@ static void handle_disconnect_event(net_sock_t *p_sock) { * recv 的 DONTWAIT 快路径先返数据(recv_len>0 优先),数据尽后判据 * 落到本标志 → 仍得 EOF,不因排空慢而漏报 */ p_sock->session_dead = 1U; - /* N-30(Trap 25):登记本连接对端端口(幽灵态甄别基准,须在状态 + /* N-30(TRAP-NET-20):登记本连接对端端口(幽灵态甄别基准,须在状态 * 迁移前记录——remote_port 此刻仍有效,relisten 清零) */ p_sock->last_peer_port = p_sock->remote_port; /* 状态 → CLOSED */ @@ -3776,11 +3795,11 @@ static void handle_disconnect_event(net_sock_t *p_sock) { * 限定条件:net_poll() 检测到 RECV_OK 中断后调用;netTask 上下文 * 函数说明:1. 仅触发 NET_EVENT_DATA_RECEIVED 回调通知上层 * 2. 不在此读取 recv_len:单一读取点归 net_recv_locked(省一次 SPI - * 查询;Trap 13 时代的"读取冲突"顾虑已随复核注降级,结构保留) + * 查询;旧"部分读归零"时代的"读取冲突"顾虑已随复核注降级,结构保留) */ static void handle_recv_event(net_sock_t *p_sock) { /* RECV_OK 中断已触发,直接通知上层。不在此读 recv_len, - * 单一读取点在 net_recv_locked(省 SPI;原 Trap 13 顾虑已降级) */ + * 单一读取点在 net_recv_locked(省 SPI;原"部分读归零"顾虑已降级) */ /* 置可读标志,供 net_select 在无 SPI 访问下判读 */ p_sock->readable = 1; /* 触发数据到达事件回调 */ @@ -3825,7 +3844,7 @@ static void handle_timeout_event(net_sock_t *p_sock) { } else if (p_sock->state == NET_SOCK_STATE_ESTABLISHED) { /* 已建立连接超时 */ DBG_ERROR("sock%d TIMEOUT", p_sock->ch395_sock); - /* Trap 23:异常释放(TIMEOUT,例如客户端中止上传)→ 复监听前隔离, + /* TRAP-NET-18:异常释放(TIMEOUT,例如客户端中止上传)→ 复监听前隔离, * 等对端在途数据落尽并排空 RX 残渣(详见 net_relisten_locked 区块注释) */ { /* 反推描述符 */ @@ -3836,11 +3855,11 @@ static void handle_timeout_event(net_sock_t *p_sock) { net_listener_settle(qfd, NET_LISTENER_SETTLE_QUARANTINE); } } - /* Trap 23 待办 #3:标记异常释放——L3 挂起 recv 排空断连后以 + /* TRAP-NET-18 待办 #3:标记异常释放——L3 挂起 recv 排空断连后以 * NET_ERR_CONNRESET 了结(而非 EOF),应用(lftpd STOR)回报 5xx * 而非误报 226(见 net_pending_tick 断连分支) */ p_sock->released_abnormal = 1U; - /* N-30(Trap 25):异常释放同样登记对端端口(幽灵甄别基准) */ + /* N-30(TRAP-NET-20):异常释放同样登记对端端口(幽灵甄别基准) */ p_sock->last_peer_port = p_sock->remote_port; /* N-32(P1 不变量 I):异常结束同样是"本会话已结束"电平(TIMEOUT/RST * 已在下方 requarantine,本标志只表达"会话结束了"这一事实,与 @@ -3982,3 +4001,55 @@ int net_getsockname(int sockfd, struct net_sockaddr *addr, int *addrlen) { /* 成功(0) */ return 0; } + +/* + * 函数功能:获取 Socket 对端地址(IP + Port)—— N34 E13 §6 lftpd 对端显示收口 + * 入口参数:sockfd - socket 描述符 int + * 出口参数:addr - 输出:对端地址 struct net_sockaddr * + * addrlen - 输入输出:地址结构体长度 int * + * 返回值:0 - 成功,-1 - 失败(NOTCONN:尚无 CONNECT 事件回读过对端) int + * 限定条件:socket 已连接(standalone accept / connect 成功后) + * 函数说明:1. 数据源=CONNECT 事件时 ch395f_get_remot_ipp 回读并缓存的 + * remote_ip/remote_port(net_types.h),**不发 SPI** + * 2. 单连接模型下监听 fd 转数据通道后同样有效(对端=刚接入的客户端) + */ +int net_getpeername(int sockfd, struct net_sockaddr *addr, int *addrlen) { + /* 描述符越界 → BADF */ + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { + return NET_ERR_RETURN_1(NET_ERR_BADF); + } + + /* 控制块(const:本函数只读 Socket 信息) */ + const net_sock_t *p_sock = &s_net_socks[sockfd]; + + /* 未在用 → BADF */ + if (!p_sock->in_use) { + return NET_ERR_RETURN_1(NET_ERR_BADF); + } + + /* 输出参数无效(NULL 或长度不足)→ INVAL */ + if (addr == NULL || addrlen == NULL || *addrlen < (int)sizeof(struct net_sockaddr_in)) { + return NET_ERR_RETURN_1(NET_ERR_INVAL); + } + + /* 尚无对端(未连接/未收到 CONNECT 回读)→ NOTCONN */ + if (p_sock->remote_port == 0U) { + return NET_ERR_RETURN_1(NET_ERR_NOTCONN); + } + + /* 强转为 IPv4 地址结构体 */ + struct net_sockaddr_in *p_addr_in = (struct net_sockaddr_in *)addr; + /* 清零地址结构体(防御:全部字段归零) */ + memset(p_addr_in, 0, sizeof(struct net_sockaddr_in)); + /* 地址族 = IPv4 */ + p_addr_in->sin_family = NET_AF_INET; + /* 对端端口(主机序 → 网络序) */ + p_addr_in->sin_port = net_htons(p_sock->remote_port); + /* 对端 IP:CONNECT 时已按网络序缓存,直传 */ + p_addr_in->sin_addr.s_addr = p_sock->remote_ip; + /* 回填结构体长度 */ + *addrlen = sizeof(struct net_sockaddr_in); + + /* 成功(0) */ + return 0; +} diff --git a/Drivers/BSP/NET/net_socket.h b/Drivers/BSP/NET/net_socket.h index a85a9159..4ce65987 100644 --- a/Drivers/BSP/NET/net_socket.h +++ b/Drivers/BSP/NET/net_socket.h @@ -353,7 +353,7 @@ int net_close(int sockfd); * 而挂起但 TCP 仍活的客户端会永久占住单连接监听(PC 内核自动 * ACK keepalive,芯片僵尸判不死——实验方案 E13 §5 定案缺口) * 2. 实现=sock_reset QUARANTINE(FIN 关闭 + 排渣 + 排空事件 + 等 - * byte0=00H,Trap 23 异常释放语义);auto_relisten fd 随即重监听 + * byte0=00H,TRAP-NET-18 异常释放语义);auto_relisten fd 随即重监听 * (失败交 D10 重试),非 auto_relisten(client fd)收口 CLOSED * 3. 幂等:非 ESTABLISHED/CONNECTING 直接返 0,不动监听 */ @@ -514,6 +514,17 @@ int net_pending_active(int sockfd); */ int net_getsockname(int sockfd, struct net_sockaddr *addr, int *addrlen); +/* + * 函数功能:获取 Socket 对端地址(IP + Port) + * 入口参数:sockfd - socket 描述符 + * addr - 输出:对端地址 + * addrlen - 输入输出:地址结构体长度 + * 返回值:0 成功,-1 失败(未连接返回 NOTCONN) + * 限定条件:socket 已连接(MCU 端读 CONNECT 事件缓存的对端,零 SPI; + * 单连接模型下监听 fd 转数据通道后同样有效) + */ +int net_getpeername(int sockfd, struct net_sockaddr *addr, int *addrlen); + #ifndef NET_NO_OS /* * 内部 API(需在 netTask 上下文中调用,绕过消息队列) diff --git a/Drivers/BSP/NET/net_types.h b/Drivers/BSP/NET/net_types.h index 99e7380d..20156a8b 100644 --- a/Drivers/BSP/NET/net_types.h +++ b/Drivers/BSP/NET/net_types.h @@ -172,7 +172,7 @@ typedef struct { /* 接收可读标志:net_poll 检测到 RECV_OK 时置 1,net_recv_locked 读空后清零。 * net_select 据此判断可读性,从而无需在应用/测试任务上下文直接访问 CH395F SPI - * (所有 ch395f 事务统一由 netTask 串行执行,避免与 net_poll 竞争,见 Trap 16)。 */ + * (所有 ch395f 事务统一由 netTask 串行执行,避免与 net_poll 竞争,见 TRAP-NET-14)。 */ uint8_t readable; /* 单连接模式自动重监听:连接断开后由 net 层自动回到 LISTEN(BSD 风格监听常驻) */ @@ -187,7 +187,7 @@ typedef struct { * 注意与"超时预算"正交:模式是属性(粘滞),预算是单次参数(net_recv_to)。 */ uint8_t blocking; - /* 释放来源标记(Trap 23 待办 #3):1 = 异常释放(芯片 TIMEOUT,对端 RST/掉链), + /* 释放来源标记(TRAP-NET-18 待办 #3):1 = 异常释放(芯片 TIMEOUT,对端 RST/掉链), * 0 = 正常释放(芯片 DISCONNECT,对端 FIN)或新连接。L3 挂起 recv 的了结断连 * 分支据此决定回 EOF(0) 还是 NET_ERR_CONNRESET:异常结束上报应用(lftpd STOR) * 为错误 → 回 5xx 而非误报 226。由 handle_timeout_event / handle_disconnect_event @@ -198,7 +198,7 @@ typedef struct { /* N-30(2026-10-11 裸机芯片台架实证):本 Socket 上一条被释放连接的**对端端口**。 * 芯片拆链收尾窗口内 Socket 会假报 ESTABLISHED(幽灵态),此时回读的对端端口 * = 旧连接端口;新连接的临时端口几乎必然不同。accept 路径补 accept 与对账② - * 均以此甄别幽灵态(见 net_socket.c Trap 25)。跨 fd 释放保留(幽灵属芯片 + * 均以此甄别幽灵态(见 net_socket.c TRAP-NET-20)。跨 fd 释放保留(幽灵属芯片 * Socket 级,与 fd 复用无关),新连接断开时刷新。 */ uint16_t last_peer_port; /* N-30:accept 路径"丢 CONNECT 中断补 accept"的去抖计数(连续确认次数, diff --git a/Drivers/BSP/README.md b/Drivers/BSP/README.md index 52ebfd18..18ec3f9a 100644 --- a/Drivers/BSP/README.md +++ b/Drivers/BSP/README.md @@ -32,7 +32,7 @@ | 模块 | 陷阱 | |---|---| -| CH395F | SPI 事务须 `ch395f_spi_begin()/end()` 包裹;分频系数**必须为 8**(PCB 约束,勿"优化");快速复连会丢中断致监听死亡(Trap 22 已修);上传中止后首笔复连截断(Trap 23 已修) | +| CH395F | SPI 事务须 `ch395f_spi_begin()/end()` 包裹;分频系数**必须为 8**(PCB 约束,勿"优化");快速复连会丢中断致监听死亡(TRAP-NET-17 已修);上传中止后首笔复连截断(TRAP-NET-18 已修) | | GD5F2GQ5UE | FatFs 移植版打开模式陷阱(`FA_OPEN_APPEND` 不含写位);文件名严格 8.3 短名、不得以点开头;`f_write` 簇分配失败返 `FR_OK` + 短写,须校验字节数 | | NET | 就绪屏障位必须 `osFlagsNoClear`;`s_net_errno` 已按任务隔离(勿改回全局) | | 通用 | 自研代码**禁用 C 库堆分配**(MicroLIB 堆仅 512 B),动态内存只用 `pvPortMalloc/vPortFree` | diff --git a/Drivers/BSP/UI/ui_net_sink.c b/Drivers/BSP/UI/ui_net_sink.c index 3699f152..2be4a3f9 100644 --- a/Drivers/BSP/UI/ui_net_sink.c +++ b/Drivers/BSP/UI/ui_net_sink.c @@ -19,7 +19,7 @@ * 前先用 net_can_accept()/net_can_read()(纯 RAM 读控制块)判 * "有没有活",无活直接返回——台架常态(无客户端)从每 10 ms * 一次 RPC(100 msg/s,与 FTP 抢深度 8 的队列)降到 0 次。 - * 兜底:readable 仅由 RECV_OK 中断置位且 Trap 22 对账不含 + * 兜底:readable 仅由 RECV_OK 中断置位且 TRAP-NET-17 对账不含 * "RX 非空但 readable=0",故每 32 轮无条件 recv 一次(≈320 ms); * 门控放行时若监听控制块已消失则置 g_listen=-1 触发重建, * 避免"门控把端口永久不再接受新客户端"这一新卡死路径 @@ -74,7 +74,7 @@ static uint8_t s_sink_blocked; /* 推送阻断标志(1 = 停发帧,等 /* * 兜底收帧节拍(门控失效保险,2026-10-10 I-2): - * net_can_read() 的 readable 只由 RECV_OK 中断置位,而 Trap 22 的 + * net_can_read() 的 readable 只由 RECV_OK 中断置位,而 TRAP-NET-17 的 * net_reconcile_tick() 三类对账**不含**"芯片 RX 非空但 readable=0" → * 该中断一旦丢失,纯门控会把已到达的数据永久藏住(改前靠每轮 recv 内的 * ch395f_get_recv_len() 顺带发现)。故每 RM_LIVENESS_DIV 轮**无条件**收一次: @@ -566,7 +566,7 @@ void ui_net_sink_poll(void) { } else if (n == 0) { /* 对端关闭:单连接模型下监听通道与数据通道是同一个 Socket,net 层已在 * net_recv_locked 的 EOF 分支自动把该 Socket 放回 LISTEN;此处绝不能再 - * net_close 它,否则监听一并被毁(后续连接被拒),且重建监听受 Trap 09 时序限制 */ + * net_close 它,否则监听一并被毁(后续连接被拒),且重建监听受 TRAP-NET-09 时序限制 */ if (g_client != g_listen) { net_close(g_client); /* 多连接后端(PC 模拟器):数据通道独立,需显式关闭 */ } @@ -574,7 +574,7 @@ void ui_net_sink_poll(void) { rm_parser_reset(); /* N-32(P1 不变量 II):重建**不再**由本处自行 bind_listen。 * ① net_recv_locked 的 EOF 分支已触发 auto_relisten(D10 非阻塞重试); - * ② 对账④还会兜底补一次 relisten(Trap 22 实证事件会丢,两层不可互替)。 + * ② 对账④还会兜底补一次 relisten(TRAP-NET-17 实证事件会丢,两层不可互替)。 * 本处再做一遍 = 第三个重建所有者,正是 §2.3 那 12 个所有者的同类。 * 仅当 fd 真消失(net_get_sock 返 NULL = 网络重建)才置 -1,交回上方 * 分支重新走 start → bind_listen */ @@ -587,7 +587,7 @@ void ui_net_sink_poll(void) { /* N-32(P1):加 `g_client != g_listen` 护栏,与上面 n==0 分支 * 的 :552 一致——MCU 单连接模型下 g_client 可能就是 g_listen 本身, * 无护栏的 net_close 会直接关掉监听 fd(:563,与 :134 注释"严禁" - * 自相矛盾),端口随 Trap 09 沉降窗短死。PC 模拟器多连接后端下 + * 自相矛盾),端口随 TRAP-NET-09 沉降窗短死。PC 模拟器多连接后端下 * 两者恒不等,护栏不改变其行为 */ if (g_client != g_listen) { net_close(g_client); /* 多连接后端(PC 模拟器):数据通道独立,需显式关闭 */ diff --git a/MDK-ARM/README.md b/MDK-ARM/README.md index 523adea5..b9af4c6c 100644 --- a/MDK-ARM/README.md +++ b/MDK-ARM/README.md @@ -38,9 +38,8 @@ |---|---| | `bench_verdict.py` | 台架判读(把运行结果判为 PASS/FAIL) | | `ftp_freeze_probe.py` | FTP 上传+回读哈希(真实路径冻结探针) | -| `ftp_abort_probe.py` | FTP 中止场景探针(Trap 23) | | `ftp_retr_abort_probe.py` | FTP RETR 中止探针 | -| `tcp_reconnect_probe.py` | TCP 快速复连探针(Trap 22) | +| `tcp_reconnect_probe.py` | TCP 快速复连探针(TRAP-NET-17) | | `audit_runs.py` / `audit_tcpclient.py` | 运行记录审计 / TCP 客户端审计 | | `check_crecipe_content.py` | 检查创建配方内容 | diff --git a/MDK-ARM/bench_diag/bench_diag_abort_gap.py b/MDK-ARM/bench_diag/bench_diag_abort_gap.py index 774f1889..2ced7bf7 100644 --- a/MDK-ARM/bench_diag/bench_diag_abort_gap.py +++ b/MDK-ARM/bench_diag/bench_diag_abort_gap.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Reusable bench probe (Trap 23 boundary scan; lives in MDK-ARM/bench_diag/). -# Boundary probe for Trap 23 residual: abort an in-flight STOR, wait G ms, then run a +# Reusable bench probe (TRAP-NET-18 boundary scan; lives in MDK-ARM/bench_diag/). +# Boundary probe for TRAP-NET-18 residual: abort an in-flight STOR, wait G ms, then run a # fresh STOR(4096) and measure BOTH directions INDEPENDENTLY: # ab_stored / fu_stored = device-side byte count, authoritative, parsed from the # device log line "UPLOAD '' bytes OK" diff --git a/MDK-ARM/tcp_reconnect_probe.py b/MDK-ARM/tcp_reconnect_probe.py index 309ac179..4cf84feb 100644 --- a/MDK-ARM/tcp_reconnect_probe.py +++ b/MDK-ARM/tcp_reconnect_probe.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Trap 22 / N-6 trustworthy reproduction probe: rapid reconnect to a device listener. +# TRAP-NET-17 / N-6 trustworthy reproduction probe: rapid reconnect to a device listener. # # Failure under test (original report): after a rapid reconnect (<~1s) on a # relistened socket, the chip completes the handshake but the MCU never gets diff --git a/docs/images/引用关系图.dot b/docs/images/引用关系图.dot index 792167f0..99ad4503 100644 --- a/docs/images/引用关系图.dot +++ b/docs/images/引用关系图.dot @@ -6,6 +6,8 @@ digraph refgraph_full { "App/algo/adc_health.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/algo/adc_input.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/algo/adc_input.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/algo/freq.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/algo/freq.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/algo/phasor.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/algo/phasor.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/algo/prot_elm.c" [fillcolor="#dbeafe", color="#1d4ed8"]; @@ -22,10 +24,16 @@ digraph refgraph_full { "App/cfg/cfg_file.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/cfg/cfg_file_store.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/cfg/cfg_file_store_mcu.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/cfg/prot_set_store.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/cfg/prot_set_store.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/fs/vol_lock.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/fs/vol_lock.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/log/console_log.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/log/console_log.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/log/soe_rec.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/log/soe_rec.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/log/soe_store.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "App/log/soe_store.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/log/sys_log.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/log/sys_log_mcu.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "App/protocol/iec104/iec104_asdu.c" [fillcolor="#dbeafe", color="#1d4ed8"]; @@ -67,6 +75,8 @@ digraph refgraph_full { "Drivers/BSP/NET/lftpd/lftpd.h" [fillcolor="#f3f4f6", color="#9ca3af"]; "Drivers/BSP/NET/lftpd/lftpd_auth.c" [fillcolor="#f3f4f6", color="#9ca3af"]; "Drivers/BSP/NET/net_config.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "Drivers/BSP/NET/net_listener.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "Drivers/BSP/NET/net_listener.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "Drivers/BSP/NET/net_select.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "Drivers/BSP/NET/net_select.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "Drivers/BSP/NET/net_socket.c" [fillcolor="#dbeafe", color="#1d4ed8"]; @@ -203,12 +213,14 @@ digraph refgraph_full { "test/host/adc_input/stub/tpafe5160.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "test/host/cfg_file/cfg_file_test.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "test/host/cfg_file/stub/dbg_log.h" [fillcolor="#dbeafe", color="#1d4ed8"]; + "test/host/freq/freq_test.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "test/host/ftp_auth/ftp_auth_test.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "test/host/iec104/iec104_test.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "test/host/phasor/phasor_test.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "test/host/prot_elm/prot_elm_test.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "test/host/prot_set/prot_set_test.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "test/host/sha256/sha256_test.c" [fillcolor="#dbeafe", color="#1d4ed8"]; + "test/host/soe/soe_test.c" [fillcolor="#dbeafe", color="#1d4ed8"]; "test/host/sys_clock/stub/FreeRTOS.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "test/host/sys_clock/stub/dbg_log.h" [fillcolor="#dbeafe", color="#1d4ed8"]; "test/host/sys_clock/stub/main.h" [fillcolor="#dbeafe", color="#1d4ed8"]; @@ -236,23 +248,30 @@ digraph refgraph_full { "App/algo/adc_input.c" -> "Inc/adc.h" [color="#2563eb", penwidth=1.0]; "App/algo/adc_input.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "App/algo/adc_input.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/algo/freq.c" -> "App/algo/freq.h" [color="#2563eb", penwidth=1.0]; + "App/algo/freq.c" -> "App/algo/phasor.h" [color="#2563eb", penwidth=1.0]; + "App/algo/freq.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/algo/freq.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "App/algo/phasor.c" -> "App/algo/phasor.h" [color="#2563eb", penwidth=1.0]; "App/algo/phasor.c" -> "App/algo/sample_ring.h" [color="#2563eb", penwidth=1.0]; "App/algo/phasor.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "App/algo/phasor.h" -> "App/algo/adc_input.h" [color="#2563eb", penwidth=1.0]; "App/algo/phasor.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/algo/prot_elm.c" -> "App/algo/freq.h" [color="#2563eb", penwidth=1.0]; "App/algo/prot_elm.c" -> "App/algo/phasor.h" [color="#2563eb", penwidth=1.0]; "App/algo/prot_elm.c" -> "App/algo/prot_elm.h" [color="#2563eb", penwidth=1.0]; "App/algo/prot_elm.c" -> "App/algo/prot_set.h" [color="#2563eb", penwidth=1.0]; "App/algo/prot_elm.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "App/algo/prot_elm.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "App/algo/prot_set.c" -> "App/algo/prot_set.h" [color="#2563eb", penwidth=1.0]; + "App/algo/prot_set.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "App/algo/prot_set.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "App/algo/sample_ring.c" -> "App/algo/sample_ring.h" [color="#2563eb", penwidth=1.0]; "App/algo/sample_ring.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "App/algo/sample_ring.h" -> "App/algo/adc_input.h" [color="#2563eb", penwidth=1.0]; "App/algo/sample_ring.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "App/app/app_main.c" -> "App/algo/adc_input.h" [color="#2563eb", penwidth=1.0]; + "App/app/app_main.c" -> "App/algo/freq.h" [color="#2563eb", penwidth=1.0]; "App/app/app_main.c" -> "App/algo/phasor.h" [color="#2563eb", penwidth=1.0]; "App/app/app_main.c" -> "App/algo/prot_elm.h" [color="#2563eb", penwidth=1.0]; "App/app/app_main.c" -> "App/algo/prot_set.h" [color="#2563eb", penwidth=1.0]; @@ -285,6 +304,11 @@ digraph refgraph_full { "App/cfg/cfg_file_store_mcu.c" -> "Lib/FatFs/ff.h" [color="#94a3b8", penwidth=0.7]; "App/cfg/cfg_file_store_mcu.c" -> "Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h" [color="#94a3b8", penwidth=0.7]; "App/cfg/cfg_file_store_mcu.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/cfg/prot_set_store.c" -> "App/algo/prot_set.h" [color="#2563eb", penwidth=1.0]; + "App/cfg/prot_set_store.c" -> "App/cfg/cfg_file_store.h" [color="#2563eb", penwidth=1.0]; + "App/cfg/prot_set_store.c" -> "App/cfg/prot_set_store.h" [color="#2563eb", penwidth=1.0]; + "App/cfg/prot_set_store.c" -> "Drivers/BSP/dbg_log.h" [color="#2563eb", penwidth=1.0]; + "App/cfg/prot_set_store.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "App/fs/vol_lock.c" -> "App/fs/vol_lock.h" [color="#2563eb", penwidth=1.0]; "App/fs/vol_lock.c" -> "Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h" [color="#94a3b8", penwidth=0.7]; "App/fs/vol_lock.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; @@ -296,8 +320,22 @@ digraph refgraph_full { "App/log/console_log.c" -> "Middlewares/Third_Party/FreeRTOS/Source/include/task.h" [color="#94a3b8", penwidth=0.7]; "App/log/console_log.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "App/log/console_log.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/log/soe_rec.c" -> "App/log/soe_rec.h" [color="#2563eb", penwidth=1.0]; + "App/log/soe_rec.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/log/soe_rec.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/log/soe_store.c" -> "App/fs/vol_lock.h" [color="#2563eb", penwidth=1.0]; + "App/log/soe_store.c" -> "App/log/soe_rec.h" [color="#2563eb", penwidth=1.0]; + "App/log/soe_store.c" -> "App/log/soe_store.h" [color="#2563eb", penwidth=1.0]; + "App/log/soe_store.c" -> "App/util/sys_clock.h" [color="#2563eb", penwidth=1.0]; + "App/log/soe_store.c" -> "Drivers/BSP/dbg_log.h" [color="#2563eb", penwidth=1.0]; + "App/log/soe_store.c" -> "Lib/FatFs/ff.h" [color="#94a3b8", penwidth=0.7]; + "App/log/soe_store.c" -> "Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h" [color="#94a3b8", penwidth=0.7]; + "App/log/soe_store.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "App/log/soe_store.h" -> "App/log/soe_rec.h" [color="#2563eb", penwidth=1.0]; + "App/log/soe_store.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "App/log/sys_log.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "App/log/sys_log_mcu.c" -> "App/fs/vol_lock.h" [color="#2563eb", penwidth=1.0]; + "App/log/sys_log_mcu.c" -> "App/log/soe_store.h" [color="#2563eb", penwidth=1.0]; "App/log/sys_log_mcu.c" -> "App/log/sys_log.h" [color="#2563eb", penwidth=1.0]; "App/log/sys_log_mcu.c" -> "App/util/sys_clock.h" [color="#2563eb", penwidth=1.0]; "App/log/sys_log_mcu.c" -> "Drivers/BSP/dbg_log.h" [color="#2563eb", penwidth=1.0]; @@ -330,6 +368,7 @@ digraph refgraph_full { "App/sync/time_sync.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "App/task/adc_task.c" -> "App/algo/adc_health.h" [color="#2563eb", penwidth=1.0]; "App/task/adc_task.c" -> "App/algo/adc_input.h" [color="#2563eb", penwidth=1.0]; + "App/task/adc_task.c" -> "App/algo/freq.h" [color="#2563eb", penwidth=1.0]; "App/task/adc_task.c" -> "App/algo/phasor.h" [color="#2563eb", penwidth=1.0]; "App/task/adc_task.c" -> "App/algo/sample_ring.h" [color="#2563eb", penwidth=1.0]; "App/task/adc_task.c" -> "App/log/console_log.h" [color="#2563eb", penwidth=1.0]; @@ -342,6 +381,7 @@ digraph refgraph_full { "App/task/adc_task.c" -> "Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h" [color="#94a3b8", penwidth=0.7]; "App/task/default_task.c" -> "App/app/sys_events.h" [color="#2563eb", penwidth=1.0]; "App/task/default_task.c" -> "App/cfg/cfg_file.h" [color="#2563eb", penwidth=1.0]; + "App/task/default_task.c" -> "App/cfg/prot_set_store.h" [color="#2563eb", penwidth=1.0]; "App/task/default_task.c" -> "App/fs/vol_lock.h" [color="#2563eb", penwidth=1.0]; "App/task/default_task.c" -> "App/log/sys_log.h" [color="#2563eb", penwidth=1.0]; "App/task/default_task.c" -> "App/task/default_task.h" [color="#2563eb", penwidth=1.0]; @@ -356,6 +396,7 @@ digraph refgraph_full { "App/task/default_task.c" -> "Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h" [color="#94a3b8", penwidth=0.7]; "App/task/default_task.c" -> "Middlewares/Third_Party/FreeRTOS/Source/include/task.h" [color="#94a3b8", penwidth=0.7]; "App/task/default_task.c" -> "test/board/test_config.h" [color="#2563eb", penwidth=1.0]; + "App/task/iec104_serv.c" -> "App/algo/prot_set.h" [color="#2563eb", penwidth=1.0]; "App/task/iec104_serv.c" -> "App/app/sys_events.h" [color="#2563eb", penwidth=1.0]; "App/task/iec104_serv.c" -> "App/protocol/iec104/iec104_link.h" [color="#2563eb", penwidth=1.0]; "App/task/iec104_serv.c" -> "App/protocol/iec104/iec104_pts.h" [color="#2563eb", penwidth=1.0]; @@ -382,6 +423,8 @@ digraph refgraph_full { "App/task/protect_task.c" -> "App/algo/adc_health.h" [color="#2563eb", penwidth=1.0]; "App/task/protect_task.c" -> "App/algo/prot_elm.h" [color="#2563eb", penwidth=1.0]; "App/task/protect_task.c" -> "App/algo/prot_set.h" [color="#2563eb", penwidth=1.0]; + "App/task/protect_task.c" -> "App/log/console_log.h" [color="#2563eb", penwidth=1.0]; + "App/task/protect_task.c" -> "App/log/soe_store.h" [color="#2563eb", penwidth=1.0]; "App/task/protect_task.c" -> "App/task/protect_task.h" [color="#2563eb", penwidth=1.0]; "App/task/protect_task.c" -> "App/util/task_watchdog.h" [color="#2563eb", penwidth=1.0]; "App/task/protect_task.c" -> "Drivers/BSP/dbg_log.h" [color="#2563eb", penwidth=1.0]; @@ -450,6 +493,18 @@ digraph refgraph_full { "Drivers/BSP/GD5F2GQ5UE/nand_ftl.c" -> "Lib/dhara/nand.h" [color="#94a3b8", penwidth=0.7]; "Drivers/BSP/GD5F2GQ5UE/nand_ftl.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "Drivers/BSP/GD5F2GQ5UE/nand_ftl.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "Drivers/BSP/NET/net_listener.c" -> "Drivers/BSP/CH395F/ch395f.h" [color="#2563eb", penwidth=1.0]; + "Drivers/BSP/NET/net_listener.c" -> "Drivers/BSP/NET/net_listener.h" [color="#2563eb", penwidth=1.0]; + "Drivers/BSP/NET/net_listener.c" -> "Drivers/BSP/NET/net_socket.h" [color="#2563eb", penwidth=1.0]; + "Drivers/BSP/NET/net_listener.c" -> "Drivers/BSP/dbg_log.h" [color="#2563eb", penwidth=1.0]; + "Drivers/BSP/NET/net_listener.c" -> "Inc/main.h" [color="#2563eb", penwidth=1.0]; + "Drivers/BSP/NET/net_listener.c" -> "Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h" [color="#94a3b8", penwidth=0.7]; + "Drivers/BSP/NET/net_listener.c" -> "Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h" [color="#94a3b8", penwidth=0.7]; + "Drivers/BSP/NET/net_listener.c" -> "Middlewares/Third_Party/FreeRTOS/Source/include/task.h" [color="#94a3b8", penwidth=0.7]; + "Drivers/BSP/NET/net_listener.c" -> "test/board/net_latency_test.h" [color="#2563eb", penwidth=1.0]; + "Drivers/BSP/NET/net_listener.c" -> "test/board/test_config.h" [color="#2563eb", penwidth=1.0]; + "Drivers/BSP/NET/net_listener.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "Drivers/BSP/NET/net_listener.h" -> "Drivers/BSP/NET/net_types.h" [color="#2563eb", penwidth=1.0]; "Drivers/BSP/NET/net_select.c" -> "Drivers/BSP/CH395F/ch395f.h" [color="#2563eb", penwidth=1.0]; "Drivers/BSP/NET/net_select.c" -> "Drivers/BSP/NET/net_select.h" [color="#2563eb", penwidth=1.0]; "Drivers/BSP/NET/net_select.c" -> "Drivers/BSP/NET/net_socket.h" [color="#2563eb", penwidth=1.0]; @@ -461,6 +516,7 @@ digraph refgraph_full { "Drivers/BSP/NET/net_select.h" -> "Drivers/BSP/NET/net_config.h" [color="#2563eb", penwidth=1.0]; "Drivers/BSP/NET/net_select.h" -> "Drivers/BSP/NET/net_types.h" [color="#2563eb", penwidth=1.0]; "Drivers/BSP/NET/net_socket.c" -> "Drivers/BSP/CH395F/ch395f.h" [color="#2563eb", penwidth=1.0]; + "Drivers/BSP/NET/net_socket.c" -> "Drivers/BSP/NET/net_listener.h" [color="#2563eb", penwidth=1.0]; "Drivers/BSP/NET/net_socket.c" -> "Drivers/BSP/NET/net_socket.h" [color="#2563eb", penwidth=1.0]; "Drivers/BSP/NET/net_socket.c" -> "Drivers/BSP/dbg_log.h" [color="#2563eb", penwidth=1.0]; "Drivers/BSP/NET/net_socket.c" -> "Inc/main.h" [color="#2563eb", penwidth=1.0]; @@ -703,6 +759,7 @@ digraph refgraph_full { "test/board/ch395f_reuse_bench.c" -> "Drivers/BSP/CH395F/ch395f.h" [color="#2563eb", penwidth=1.0]; "test/board/ch395f_reuse_bench.c" -> "Drivers/BSP/dbg_log.h" [color="#2563eb", penwidth=1.0]; "test/board/ch395f_reuse_bench.c" -> "Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal.h" [color="#94a3b8", penwidth=0.7]; + "test/board/ch395f_reuse_bench.c" -> "Inc/main.h" [color="#2563eb", penwidth=1.0]; "test/board/ch395f_reuse_bench.c" -> "test/board/ch395f_reuse_bench.h" [color="#2563eb", penwidth=1.0]; "test/board/ch395f_test.h" -> "test/board/test_common.h" [color="#2563eb", penwidth=1.0]; "test/board/ch395f_test.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; @@ -797,9 +854,14 @@ digraph refgraph_full { "test/host/cfg_file/cfg_file_test.c" -> "Drivers/BSP/dbg_log.h" [color="#2563eb", penwidth=1.0]; "test/host/cfg_file/cfg_file_test.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "test/host/cfg_file/stub/dbg_log.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "test/host/freq/freq_test.c" -> "App/algo/freq.c" [color="#2563eb", penwidth=1.0]; + "test/host/freq/freq_test.c" -> "App/algo/phasor.c" [color="#2563eb", penwidth=1.0]; + "test/host/freq/freq_test.c" -> "App/algo/sample_ring.c" [color="#2563eb", penwidth=1.0]; + "test/host/freq/freq_test.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "test/host/ftp_auth/ftp_auth_test.c" -> "App/util/sha256.c" [color="#2563eb", penwidth=1.0]; "test/host/ftp_auth/ftp_auth_test.c" -> "Drivers/BSP/NET/lftpd/lftpd_auth.c" [color="#94a3b8", penwidth=0.7]; "test/host/ftp_auth/ftp_auth_test.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "test/host/iec104/iec104_test.c" -> "App/algo/freq.c" [color="#2563eb", penwidth=1.0]; "test/host/iec104/iec104_test.c" -> "App/algo/phasor.c" [color="#2563eb", penwidth=1.0]; "test/host/iec104/iec104_test.c" -> "App/algo/prot_elm.c" [color="#2563eb", penwidth=1.0]; "test/host/iec104/iec104_test.c" -> "App/algo/prot_set.c" [color="#2563eb", penwidth=1.0]; @@ -811,6 +873,7 @@ digraph refgraph_full { "test/host/phasor/phasor_test.c" -> "App/algo/phasor.c" [color="#2563eb", penwidth=1.0]; "test/host/phasor/phasor_test.c" -> "App/algo/sample_ring.c" [color="#2563eb", penwidth=1.0]; "test/host/phasor/phasor_test.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "test/host/prot_elm/prot_elm_test.c" -> "App/algo/freq.c" [color="#2563eb", penwidth=1.0]; "test/host/prot_elm/prot_elm_test.c" -> "App/algo/phasor.c" [color="#2563eb", penwidth=1.0]; "test/host/prot_elm/prot_elm_test.c" -> "App/algo/prot_elm.c" [color="#2563eb", penwidth=1.0]; "test/host/prot_elm/prot_elm_test.c" -> "App/algo/prot_set.c" [color="#2563eb", penwidth=1.0]; @@ -820,6 +883,9 @@ digraph refgraph_full { "test/host/prot_set/prot_set_test.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "test/host/sha256/sha256_test.c" -> "App/util/sha256.c" [color="#2563eb", penwidth=1.0]; "test/host/sha256/sha256_test.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; + "test/host/soe/soe_test.c" -> "App/log/soe_rec.c" [color="#2563eb", penwidth=1.0]; + "test/host/soe/soe_test.c" -> "App/protocol/iec104/iec104_asdu.c" [color="#2563eb", penwidth=1.0]; + "test/host/soe/soe_test.c" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "test/host/sys_clock/stub/FreeRTOS.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "test/host/sys_clock/stub/dbg_log.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; "test/host/sys_clock/stub/main.h" -> "外部/系统头文件" [color="#f59e0b", penwidth=0.6, style=dashed]; diff --git a/docs/images/引用关系图.json b/docs/images/引用关系图.json index e5d8a2cf..552d1cc9 100644 --- a/docs/images/引用关系图.json +++ b/docs/images/引用关系图.json @@ -17,6 +17,14 @@ "path": "App/algo/adc_input.h", "class": "fp" }, + { + "path": "App/algo/freq.c", + "class": "fp" + }, + { + "path": "App/algo/freq.h", + "class": "fp" + }, { "path": "App/algo/phasor.c", "class": "fp" @@ -81,6 +89,14 @@ "path": "App/cfg/cfg_file_store_mcu.c", "class": "fp" }, + { + "path": "App/cfg/prot_set_store.c", + "class": "fp" + }, + { + "path": "App/cfg/prot_set_store.h", + "class": "fp" + }, { "path": "App/fs/vol_lock.c", "class": "fp" @@ -97,6 +113,22 @@ "path": "App/log/console_log.h", "class": "fp" }, + { + "path": "App/log/soe_rec.c", + "class": "fp" + }, + { + "path": "App/log/soe_rec.h", + "class": "fp" + }, + { + "path": "App/log/soe_store.c", + "class": "fp" + }, + { + "path": "App/log/soe_store.h", + "class": "fp" + }, { "path": "App/log/sys_log.h", "class": "fp" @@ -301,6 +333,14 @@ "path": "Drivers/BSP/NET/net_config.h", "class": "fp" }, + { + "path": "Drivers/BSP/NET/net_listener.c", + "class": "fp" + }, + { + "path": "Drivers/BSP/NET/net_listener.h", + "class": "fp" + }, { "path": "Drivers/BSP/NET/net_select.c", "class": "fp" @@ -5761,6 +5801,10 @@ "path": "test/host/cfg_file/stub/dbg_log.h", "class": "fp" }, + { + "path": "test/host/freq/freq_test.c", + "class": "fp" + }, { "path": "test/host/ftp_auth/ftp_auth_test.c", "class": "fp" @@ -5785,6 +5829,10 @@ "path": "test/host/sha256/sha256_test.c", "class": "fp" }, + { + "path": "test/host/soe/soe_test.c", + "class": "fp" + }, { "path": "test/host/sys_clock/stub/FreeRTOS.h", "class": "fp" @@ -5909,6 +5957,36 @@ "kind": "fp-ext", "include": "stdint.h" }, + { + "from": "App/algo/freq.c", + "to": "App/algo/freq.h", + "kind": "fp-fp", + "include": "freq.h" + }, + { + "from": "App/algo/freq.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "math.h" + }, + { + "from": "App/algo/freq.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stddef.h" + }, + { + "from": "App/algo/freq.c", + "to": "App/algo/phasor.h", + "kind": "fp-fp", + "include": "phasor.h" + }, + { + "from": "App/algo/freq.h", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, { "from": "App/algo/phasor.c", "to": "App/algo/phasor.h", @@ -5963,6 +6041,12 @@ "kind": "fp-fp", "include": "phasor.h" }, + { + "from": "App/algo/prot_elm.c", + "to": "App/algo/freq.h", + "kind": "fp-fp", + "include": "freq.h" + }, { "from": "App/algo/prot_elm.c", "to": "外部/系统头文件", @@ -5981,6 +6065,12 @@ "kind": "fp-fp", "include": "prot_set.h" }, + { + "from": "App/algo/prot_set.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "string.h" + }, { "from": "App/algo/prot_set.h", "to": "外部/系统头文件", @@ -6089,6 +6179,12 @@ "kind": "fp-fp", "include": "phasor.h" }, + { + "from": "App/app/app_main.c", + "to": "App/algo/freq.h", + "kind": "fp-fp", + "include": "freq.h" + }, { "from": "App/app/app_main.c", "to": "App/algo/prot_set.h", @@ -6239,6 +6335,36 @@ "kind": "fp-fp", "include": "vol_lock.h" }, + { + "from": "App/cfg/prot_set_store.c", + "to": "App/cfg/prot_set_store.h", + "kind": "fp-fp", + "include": "prot_set_store.h" + }, + { + "from": "App/cfg/prot_set_store.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stddef.h" + }, + { + "from": "App/cfg/prot_set_store.c", + "to": "Drivers/BSP/dbg_log.h", + "kind": "fp-fp", + "include": "dbg_log.h" + }, + { + "from": "App/cfg/prot_set_store.c", + "to": "App/cfg/cfg_file_store.h", + "kind": "fp-fp", + "include": "cfg_file_store.h" + }, + { + "from": "App/cfg/prot_set_store.c", + "to": "App/algo/prot_set.h", + "kind": "fp-fp", + "include": "prot_set.h" + }, { "from": "App/fs/vol_lock.c", "to": "App/fs/vol_lock.h", @@ -6311,6 +6437,84 @@ "kind": "fp-ext", "include": "stdint.h" }, + { + "from": "App/log/soe_rec.c", + "to": "App/log/soe_rec.h", + "kind": "fp-fp", + "include": "soe_rec.h" + }, + { + "from": "App/log/soe_rec.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "string.h" + }, + { + "from": "App/log/soe_rec.h", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, + { + "from": "App/log/soe_store.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "string.h" + }, + { + "from": "App/log/soe_store.c", + "to": "Drivers/BSP/dbg_log.h", + "kind": "fp-fp", + "include": "dbg_log.h" + }, + { + "from": "App/log/soe_store.c", + "to": "App/log/soe_store.h", + "kind": "fp-fp", + "include": "soe_store.h" + }, + { + "from": "App/log/soe_store.c", + "to": "App/log/soe_rec.h", + "kind": "fp-fp", + "include": "soe_rec.h" + }, + { + "from": "App/log/soe_store.c", + "to": "Lib/FatFs/ff.h", + "kind": "fp-tp", + "include": "ff.h" + }, + { + "from": "App/log/soe_store.c", + "to": "Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h", + "kind": "fp-tp", + "include": "cmsis_os.h" + }, + { + "from": "App/log/soe_store.c", + "to": "App/util/sys_clock.h", + "kind": "fp-fp", + "include": "sys_clock.h" + }, + { + "from": "App/log/soe_store.c", + "to": "App/fs/vol_lock.h", + "kind": "fp-fp", + "include": "vol_lock.h" + }, + { + "from": "App/log/soe_store.h", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, + { + "from": "App/log/soe_store.h", + "to": "App/log/soe_rec.h", + "kind": "fp-fp", + "include": "soe_rec.h" + }, { "from": "App/log/sys_log.h", "to": "外部/系统头文件", @@ -6347,6 +6551,12 @@ "kind": "fp-fp", "include": "sys_log.h" }, + { + "from": "App/log/sys_log_mcu.c", + "to": "App/log/soe_store.h", + "kind": "fp-fp", + "include": "soe_store.h" + }, { "from": "App/log/sys_log_mcu.c", "to": "Lib/FatFs/ff.h", @@ -6545,6 +6755,12 @@ "kind": "fp-fp", "include": "phasor.h" }, + { + "from": "App/task/adc_task.c", + "to": "App/algo/freq.h", + "kind": "fp-fp", + "include": "freq.h" + }, { "from": "App/task/adc_task.c", "to": "App/algo/adc_health.h", @@ -6647,6 +6863,12 @@ "kind": "fp-fp", "include": "cfg_file.h" }, + { + "from": "App/task/default_task.c", + "to": "App/cfg/prot_set_store.h", + "kind": "fp-fp", + "include": "prot_set_store.h" + }, { "from": "App/task/default_task.c", "to": "App/log/sys_log.h", @@ -6707,6 +6929,12 @@ "kind": "fp-fp", "include": "net_socket.h" }, + { + "from": "App/task/iec104_serv.c", + "to": "App/algo/prot_set.h", + "kind": "fp-fp", + "include": "prot_set.h" + }, { "from": "App/task/iec104_serv.c", "to": "App/util/sys_clock.h", @@ -6845,6 +7073,12 @@ "kind": "fp-fp", "include": "adc_health.h" }, + { + "from": "App/task/protect_task.c", + "to": "App/log/console_log.h", + "kind": "fp-fp", + "include": "console_log.h" + }, { "from": "App/task/protect_task.c", "to": "App/algo/prot_elm.h", @@ -6857,6 +7091,12 @@ "kind": "fp-fp", "include": "prot_set.h" }, + { + "from": "App/task/protect_task.c", + "to": "App/log/soe_store.h", + "kind": "fp-fp", + "include": "soe_store.h" + }, { "from": "App/task/protect_task.c", "to": "App/util/task_watchdog.h", @@ -7301,6 +7541,84 @@ "kind": "fp-ext", "include": "stdint.h" }, + { + "from": "Drivers/BSP/NET/net_listener.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "string.h" + }, + { + "from": "Drivers/BSP/NET/net_listener.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdbool.h" + }, + { + "from": "Drivers/BSP/NET/net_listener.c", + "to": "Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h", + "kind": "fp-tp", + "include": "FreeRTOS.h" + }, + { + "from": "Drivers/BSP/NET/net_listener.c", + "to": "Middlewares/Third_Party/FreeRTOS/Source/include/task.h", + "kind": "fp-tp", + "include": "task.h" + }, + { + "from": "Drivers/BSP/NET/net_listener.c", + "to": "Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h", + "kind": "fp-tp", + "include": "cmsis_os.h" + }, + { + "from": "Drivers/BSP/NET/net_listener.c", + "to": "Drivers/BSP/NET/net_socket.h", + "kind": "fp-fp", + "include": "net_socket.h" + }, + { + "from": "Drivers/BSP/NET/net_listener.c", + "to": "Drivers/BSP/NET/net_listener.h", + "kind": "fp-fp", + "include": "net_listener.h" + }, + { + "from": "Drivers/BSP/NET/net_listener.c", + "to": "Drivers/BSP/CH395F/ch395f.h", + "kind": "fp-fp", + "include": "ch395f.h" + }, + { + "from": "Drivers/BSP/NET/net_listener.c", + "to": "test/board/test_config.h", + "kind": "fp-fp", + "include": "test_config.h" + }, + { + "from": "Drivers/BSP/NET/net_listener.c", + "to": "test/board/net_latency_test.h", + "kind": "fp-fp", + "include": "net_latency_test.h" + }, + { + "from": "Drivers/BSP/NET/net_listener.c", + "to": "Inc/main.h", + "kind": "fp-fp", + "include": "main.h" + }, + { + "from": "Drivers/BSP/NET/net_listener.c", + "to": "Drivers/BSP/dbg_log.h", + "kind": "fp-fp", + "include": "dbg_log.h" + }, + { + "from": "Drivers/BSP/NET/net_listener.h", + "to": "Drivers/BSP/NET/net_types.h", + "kind": "fp-fp", + "include": "net_types.h" + }, { "from": "Drivers/BSP/NET/net_select.c", "to": "外部/系统头文件", @@ -7409,6 +7727,12 @@ "kind": "fp-fp", "include": "net_socket.h" }, + { + "from": "Drivers/BSP/NET/net_socket.c", + "to": "Drivers/BSP/NET/net_listener.h", + "kind": "fp-fp", + "include": "net_listener.h" + }, { "from": "Drivers/BSP/NET/net_socket.c", "to": "Drivers/BSP/CH395F/ch395f.h", @@ -40043,6 +40367,12 @@ "kind": "fp-fp", "include": "dbg_log.h" }, + { + "from": "test/board/ch395f_reuse_bench.c", + "to": "Inc/main.h", + "kind": "fp-fp", + "include": "main.h" + }, { "from": "test/board/ch395f_reuse_bench.c", "to": "Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal.h", @@ -40655,6 +40985,42 @@ "kind": "fp-ext", "include": "stdio.h" }, + { + "from": "test/host/freq/freq_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "math.h" + }, + { + "from": "test/host/freq/freq_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, + { + "from": "test/host/freq/freq_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdio.h" + }, + { + "from": "test/host/freq/freq_test.c", + "to": "App/algo/sample_ring.c", + "kind": "fp-fp", + "include": "sample_ring.c" + }, + { + "from": "test/host/freq/freq_test.c", + "to": "App/algo/phasor.c", + "kind": "fp-fp", + "include": "phasor.c" + }, + { + "from": "test/host/freq/freq_test.c", + "to": "App/algo/freq.c", + "kind": "fp-fp", + "include": "freq.c" + }, { "from": "test/host/ftp_auth/ftp_auth_test.c", "to": "外部/系统头文件", @@ -40721,6 +41087,12 @@ "kind": "fp-fp", "include": "phasor.c" }, + { + "from": "test/host/iec104/iec104_test.c", + "to": "App/algo/freq.c", + "kind": "fp-fp", + "include": "freq.c" + }, { "from": "test/host/iec104/iec104_test.c", "to": "App/algo/prot_elm.c", @@ -40769,6 +41141,12 @@ "kind": "fp-fp", "include": "phasor.c" }, + { + "from": "test/host/prot_elm/prot_elm_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "math.h" + }, { "from": "test/host/prot_elm/prot_elm_test.c", "to": "外部/系统头文件", @@ -40799,6 +41177,12 @@ "kind": "fp-fp", "include": "phasor.c" }, + { + "from": "test/host/prot_elm/prot_elm_test.c", + "to": "App/algo/freq.c", + "kind": "fp-fp", + "include": "freq.c" + }, { "from": "test/host/prot_elm/prot_elm_test.c", "to": "App/algo/prot_elm.c", @@ -40841,6 +41225,36 @@ "kind": "fp-fp", "include": "sha256.c" }, + { + "from": "test/host/soe/soe_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdint.h" + }, + { + "from": "test/host/soe/soe_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "stdio.h" + }, + { + "from": "test/host/soe/soe_test.c", + "to": "外部/系统头文件", + "kind": "fp-ext", + "include": "string.h" + }, + { + "from": "test/host/soe/soe_test.c", + "to": "App/log/soe_rec.c", + "kind": "fp-fp", + "include": "soe_rec.c" + }, + { + "from": "test/host/soe/soe_test.c", + "to": "App/protocol/iec104/iec104_asdu.c", + "kind": "fp-fp", + "include": "iec104_asdu.c" + }, { "from": "test/host/sys_clock/sys_clock_test.c", "to": "外部/系统头文件", @@ -41146,6 +41560,13 @@ "App/algo/adc_input.h": [ "stdint.h" ], + "App/algo/freq.c": [ + "math.h", + "stddef.h" + ], + "App/algo/freq.h": [ + "stdint.h" + ], "App/algo/phasor.c": [ "math.h", "stddef.h" @@ -41159,6 +41580,9 @@ "App/algo/prot_elm.h": [ "stdint.h" ], + "App/algo/prot_set.c": [ + "string.h" + ], "App/algo/prot_set.h": [ "stdint.h" ], @@ -41188,6 +41612,9 @@ "App/cfg/cfg_file_store_mcu.c": [ "string.h" ], + "App/cfg/prot_set_store.c": [ + "stddef.h" + ], "App/fs/vol_lock.h": [ "stdint.h" ], @@ -41198,6 +41625,18 @@ "stdint.h", "stdio.h" ], + "App/log/soe_rec.c": [ + "string.h" + ], + "App/log/soe_rec.h": [ + "stdint.h" + ], + "App/log/soe_store.c": [ + "string.h" + ], + "App/log/soe_store.h": [ + "stdint.h" + ], "App/log/sys_log.h": [ "stddef.h", "stdint.h" @@ -41320,6 +41759,10 @@ "Drivers/BSP/NET/lftpd/private/lftpd_io.h": [ "stdint.h" ], + "Drivers/BSP/NET/net_listener.c": [ + "stdbool.h", + "string.h" + ], "Drivers/BSP/NET/net_select.c": [ "stdio.h", "string.h" @@ -42963,6 +43406,11 @@ "test/host/cfg_file/stub/dbg_log.h": [ "stdio.h" ], + "test/host/freq/freq_test.c": [ + "math.h", + "stdint.h", + "stdio.h" + ], "test/host/ftp_auth/ftp_auth_test.c": [ "stdio.h", "string.h" @@ -42978,6 +43426,7 @@ "stdio.h" ], "test/host/prot_elm/prot_elm_test.c": [ + "math.h", "stdint.h", "stdio.h" ], @@ -42989,6 +43438,11 @@ "stdio.h", "string.h" ], + "test/host/soe/soe_test.c": [ + "stdint.h", + "stdio.h", + "string.h" + ], "test/host/sys_clock/stub/FreeRTOS.h": [ "stdint.h" ], @@ -43047,11 +43501,11 @@ ] }, "stats": { - "files_indexed": 1479, - "includes_parsed": 5878, - "resolved": 4749, - "unresolved": 1129, - "nodes_fp": 199, + "files_indexed": 1491, + "includes_parsed": 5939, + "resolved": 4792, + "unresolved": 1147, + "nodes_fp": 211, "nodes_tp": 1266, "ext_names": 351 }, diff --git a/docs/images/引用关系图.svg b/docs/images/引用关系图.svg index 99b87f48..9a29a04a 100644 --- a/docs/images/引用关系图.svg +++ b/docs/images/引用关系图.svg @@ -4,5165 +4,5561 @@ - - + + refgraph_full - -STM32F4-Base 项目文件引用关系图(项目代码视图) + +STM32F4-Base 项目文件引用关系图(项目代码视图) App/algo/adc_health.c - -App/algo/adc_health.c + +App/algo/adc_health.c App/algo/adc_health.h - -App/algo/adc_health.h + +App/algo/adc_health.h App/algo/adc_health.c->App/algo/adc_health.h - - + + - + 外部/系统头文件 - -外部/系统头文件 + +外部/系统头文件 App/algo/adc_health.h->外部/系统头文件 - - + + App/algo/adc_input.c - -App/algo/adc_input.c + +App/algo/adc_input.c App/algo/adc_input.h - -App/algo/adc_input.h + +App/algo/adc_input.h App/algo/adc_input.c->App/algo/adc_input.h - - + + - + Drivers/BSP/TPAFE5160/tpafe5160.h - -Drivers/BSP/TPAFE5160/tpafe5160.h + +Drivers/BSP/TPAFE5160/tpafe5160.h App/algo/adc_input.c->Drivers/BSP/TPAFE5160/tpafe5160.h - - + + - + Inc/adc.h - -Inc/adc.h + +Inc/adc.h App/algo/adc_input.c->Inc/adc.h - - + + App/algo/adc_input.c->外部/系统头文件 - - + + App/algo/adc_input.h->外部/系统头文件 - - + + - + -App/algo/phasor.c - -App/algo/phasor.c +App/algo/freq.c + +App/algo/freq.c + + + +App/algo/freq.h + +App/algo/freq.h + + + +App/algo/freq.c->App/algo/freq.h + + - + App/algo/phasor.h - -App/algo/phasor.h + +App/algo/phasor.h + + + +App/algo/freq.c->App/algo/phasor.h + + + + + +App/algo/freq.c->外部/系统头文件 + + + + + +App/algo/freq.h->外部/系统头文件 + + + + + +App/algo/phasor.c + +App/algo/phasor.c - + App/algo/phasor.c->App/algo/phasor.h - - + + - + App/algo/sample_ring.h - -App/algo/sample_ring.h + +App/algo/sample_ring.h - + App/algo/phasor.c->App/algo/sample_ring.h - - + + - + App/algo/phasor.c->外部/系统头文件 - - + + - + App/algo/phasor.h->App/algo/adc_input.h - - + + - + App/algo/phasor.h->外部/系统头文件 - - + + - + App/algo/prot_elm.c - -App/algo/prot_elm.c + +App/algo/prot_elm.c + + + +App/algo/prot_elm.c->App/algo/freq.h + + - + App/algo/prot_elm.c->App/algo/phasor.h - - + + - + App/algo/prot_elm.h - -App/algo/prot_elm.h + +App/algo/prot_elm.h - + App/algo/prot_elm.c->App/algo/prot_elm.h - - + + - + App/algo/prot_set.h - -App/algo/prot_set.h + +App/algo/prot_set.h - + App/algo/prot_elm.c->App/algo/prot_set.h - - + + - + App/algo/prot_elm.c->外部/系统头文件 - - + + - + App/algo/prot_elm.h->外部/系统头文件 - - + + - + App/algo/prot_set.c - -App/algo/prot_set.c + +App/algo/prot_set.c - + App/algo/prot_set.c->App/algo/prot_set.h - - + + + + + +App/algo/prot_set.c->外部/系统头文件 + + - + App/algo/prot_set.h->外部/系统头文件 - - + + - + App/algo/sample_ring.c - -App/algo/sample_ring.c + +App/algo/sample_ring.c - + App/algo/sample_ring.c->App/algo/sample_ring.h - - + + - + App/algo/sample_ring.c->外部/系统头文件 - - + + - + App/algo/sample_ring.h->App/algo/adc_input.h - - + + - + App/algo/sample_ring.h->外部/系统头文件 - - + + - + App/app/app_cfg.h - -App/app/app_cfg.h + +App/app/app_cfg.h - + App/app/app_main.c - -App/app/app_main.c + +App/app/app_main.c - + App/app/app_main.c->App/algo/adc_input.h - - + + + + + +App/app/app_main.c->App/algo/freq.h + + - + App/app/app_main.c->App/algo/phasor.h - - + + - + App/app/app_main.c->App/algo/prot_elm.h - - + + - + App/app/app_main.c->App/algo/prot_set.h - - + + - + App/app/app_main.c->App/app/app_cfg.h - - + + - + App/app/app_main.h - -App/app/app_main.h + +App/app/app_main.h - + App/app/app_main.c->App/app/app_main.h - - + + - + App/log/sys_log.h - -App/log/sys_log.h + +App/log/sys_log.h - + App/app/app_main.c->App/log/sys_log.h - - + + - + App/util/sys_clock.h - -App/util/sys_clock.h + +App/util/sys_clock.h - + App/app/app_main.c->App/util/sys_clock.h - - + + - + App/util/task_watchdog.h - -App/util/task_watchdog.h + +App/util/task_watchdog.h - + App/app/app_main.c->App/util/task_watchdog.h - - + + - + Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h - -Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h + +Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h - + App/app/app_main.c->Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h - - + + - + Drivers/BSP/RS485/rs485.h - -Drivers/BSP/RS485/rs485.h + +Drivers/BSP/RS485/rs485.h - + App/app/app_main.c->Drivers/BSP/RS485/rs485.h - - + + - + Drivers/BSP/SD2506/sd2506.h - -Drivers/BSP/SD2506/sd2506.h + +Drivers/BSP/SD2506/sd2506.h - + App/app/app_main.c->Drivers/BSP/SD2506/sd2506.h - - + + - + App/app/app_main.c->Drivers/BSP/TPAFE5160/tpafe5160.h - - + + - + Drivers/BSP/dbg_log.h - -Drivers/BSP/dbg_log.h + +Drivers/BSP/dbg_log.h - + App/app/app_main.c->Drivers/BSP/dbg_log.h - - + + - + Inc/main.h - -Inc/main.h + +Inc/main.h - + App/app/app_main.c->Inc/main.h - - + + - + Inc/stm32f4xx_it.h - -Inc/stm32f4xx_it.h + +Inc/stm32f4xx_it.h - + App/app/app_main.c->Inc/stm32f4xx_it.h - - + + - + Inc/tim.h - -Inc/tim.h + +Inc/tim.h - + App/app/app_main.c->Inc/tim.h - - + + - + Inc/usart.h - -Inc/usart.h + +Inc/usart.h - + App/app/app_main.c->Inc/usart.h - - + + - + test/board/test_config.h - -test/board/test_config.h + +test/board/test_config.h - + App/app/app_main.c->test/board/test_config.h - - + + - + App/app/app_main.h->外部/系统头文件 - - + + - + App/app/sys_events.h - -App/app/sys_events.h + +App/app/sys_events.h - + Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os2.h - -Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os2.h + +Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os2.h - + App/app/sys_events.h->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os2.h - - + + - + App/cfg/cfg_file.c - -App/cfg/cfg_file.c + +App/cfg/cfg_file.c - + App/cfg/cfg_file.h - -App/cfg/cfg_file.h + +App/cfg/cfg_file.h - + App/cfg/cfg_file.c->App/cfg/cfg_file.h - - + + - + App/cfg/cfg_file_store.h - -App/cfg/cfg_file_store.h + +App/cfg/cfg_file_store.h - + App/cfg/cfg_file.c->App/cfg/cfg_file_store.h - - + + - + App/cfg/cfg_file.c->App/log/sys_log.h - - + + - + App/cfg/cfg_file.c->Drivers/BSP/dbg_log.h - - + + - + App/cfg/cfg_file.c->外部/系统头文件 - - + + - + App/cfg/cfg_file.h->外部/系统头文件 - - + + - + App/cfg/cfg_file_store.h->外部/系统头文件 - - + + - + App/cfg/cfg_file_store_mcu.c - -App/cfg/cfg_file_store_mcu.c + +App/cfg/cfg_file_store_mcu.c - + App/cfg/cfg_file_store_mcu.c->App/cfg/cfg_file_store.h - - + + - + App/fs/vol_lock.h - -App/fs/vol_lock.h + +App/fs/vol_lock.h - + App/cfg/cfg_file_store_mcu.c->App/fs/vol_lock.h - - + + - + Lib/FatFs/ff.h - -Lib/FatFs/ff.h + +Lib/FatFs/ff.h - + App/cfg/cfg_file_store_mcu.c->Lib/FatFs/ff.h - - + + - + Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - -Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h + +Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - + App/cfg/cfg_file_store_mcu.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - - + + - + App/cfg/cfg_file_store_mcu.c->外部/系统头文件 - - + + + + + +App/cfg/prot_set_store.c + +App/cfg/prot_set_store.c + + + +App/cfg/prot_set_store.c->App/algo/prot_set.h + + + + + +App/cfg/prot_set_store.c->App/cfg/cfg_file_store.h + + + + + +App/cfg/prot_set_store.h + +App/cfg/prot_set_store.h + + + +App/cfg/prot_set_store.c->App/cfg/prot_set_store.h + + + + + +App/cfg/prot_set_store.c->Drivers/BSP/dbg_log.h + + + + + +App/cfg/prot_set_store.c->外部/系统头文件 + + - + App/fs/vol_lock.c - -App/fs/vol_lock.c + +App/fs/vol_lock.c - + App/fs/vol_lock.c->App/fs/vol_lock.h - - + + - + Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - -Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h + +Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - + App/fs/vol_lock.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - + + - + App/fs/vol_lock.h->外部/系统头文件 - - + + - + App/log/console_log.c - -App/log/console_log.c + +App/log/console_log.c - + App/log/console_log.c->App/app/sys_events.h - - + + - + App/log/console_log.h - -App/log/console_log.h + +App/log/console_log.h - + App/log/console_log.c->App/log/console_log.h - - + + - + App/log/console_log.c->Inc/usart.h - - + + - + App/log/console_log.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - + + - + App/log/console_log.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - - + + - + Middlewares/Third_Party/FreeRTOS/Source/include/task.h - -Middlewares/Third_Party/FreeRTOS/Source/include/task.h + +Middlewares/Third_Party/FreeRTOS/Source/include/task.h - + App/log/console_log.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h - - + + - + App/log/console_log.c->外部/系统头文件 - - + + - + App/log/console_log.h->外部/系统头文件 - - + + + + + +App/log/soe_rec.c + +App/log/soe_rec.c + + + +App/log/soe_rec.h + +App/log/soe_rec.h + + + +App/log/soe_rec.c->App/log/soe_rec.h + + + + + +App/log/soe_rec.c->外部/系统头文件 + + + + + +App/log/soe_rec.h->外部/系统头文件 + + + + + +App/log/soe_store.c + +App/log/soe_store.c + + + +App/log/soe_store.c->App/fs/vol_lock.h + + + + + +App/log/soe_store.c->App/log/soe_rec.h + + + + + +App/log/soe_store.h + +App/log/soe_store.h + + + +App/log/soe_store.c->App/log/soe_store.h + + + + + +App/log/soe_store.c->App/util/sys_clock.h + + + + + +App/log/soe_store.c->Drivers/BSP/dbg_log.h + + + + + +App/log/soe_store.c->Lib/FatFs/ff.h + + + + + +App/log/soe_store.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h + + + + + +App/log/soe_store.c->外部/系统头文件 + + + + + +App/log/soe_store.h->App/log/soe_rec.h + + + + + +App/log/soe_store.h->外部/系统头文件 + + - + App/log/sys_log.h->外部/系统头文件 - - + + - + App/log/sys_log_mcu.c - -App/log/sys_log_mcu.c + +App/log/sys_log_mcu.c - + App/log/sys_log_mcu.c->App/fs/vol_lock.h - - + + + + + +App/log/sys_log_mcu.c->App/log/soe_store.h + + - + App/log/sys_log_mcu.c->App/log/sys_log.h - - + + - + App/log/sys_log_mcu.c->App/util/sys_clock.h - - + + - + App/log/sys_log_mcu.c->Drivers/BSP/dbg_log.h - - + + - + App/log/sys_log_mcu.c->Lib/FatFs/ff.h - - + + - + App/log/sys_log_mcu.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - + + - + App/log/sys_log_mcu.c->外部/系统头文件 - - + + - + App/protocol/iec104/iec104_asdu.c - -App/protocol/iec104/iec104_asdu.c + +App/protocol/iec104/iec104_asdu.c - + App/protocol/iec104/iec104_asdu.h - -App/protocol/iec104/iec104_asdu.h + +App/protocol/iec104/iec104_asdu.h - + App/protocol/iec104/iec104_asdu.c->App/protocol/iec104/iec104_asdu.h - - + + - + App/protocol/iec104/iec104_asdu.c->外部/系统头文件 - - + + - + App/protocol/iec104/iec104_asdu.h->外部/系统头文件 - - + + - + App/protocol/iec104/iec104_link.c - -App/protocol/iec104/iec104_link.c + +App/protocol/iec104/iec104_link.c - + App/protocol/iec104/iec104_link.c->App/protocol/iec104/iec104_asdu.h - - + + - + App/protocol/iec104/iec104_link.h - -App/protocol/iec104/iec104_link.h + +App/protocol/iec104/iec104_link.h - + App/protocol/iec104/iec104_link.c->App/protocol/iec104/iec104_link.h - - + + - + App/protocol/iec104/iec104_link.c->外部/系统头文件 - - + + - + App/protocol/iec104/iec104_pts.h - -App/protocol/iec104/iec104_pts.h + +App/protocol/iec104/iec104_pts.h - + App/protocol/iec104/iec104_link.h->App/protocol/iec104/iec104_pts.h - - + + - + App/protocol/iec104/iec104_link.h->外部/系统头文件 - - + + - + App/protocol/iec104/iec104_pts.c - -App/protocol/iec104/iec104_pts.c + +App/protocol/iec104/iec104_pts.c - + App/protocol/iec104/iec104_pts.c->App/algo/adc_input.h - - + + - + App/protocol/iec104/iec104_pts.c->App/algo/phasor.h - - + + - + App/protocol/iec104/iec104_pts.c->App/algo/prot_elm.h - - + + - + App/protocol/iec104/iec104_pts.c->App/algo/prot_set.h - - + + - + App/protocol/iec104/iec104_pts.c->App/protocol/iec104/iec104_asdu.h - - + + - + App/protocol/iec104/iec104_pts.c->App/protocol/iec104/iec104_pts.h - - + + - + App/protocol/iec104/iec104_pts.c->外部/系统头文件 - - + + - + App/protocol/iec104/iec104_pts.h->外部/系统头文件 - - + + - + App/sync/time_sync.c - -App/sync/time_sync.c + +App/sync/time_sync.c - + App/sync/time_sync.c->App/app/sys_events.h - - + + - + App/sync/time_sync.h - -App/sync/time_sync.h + +App/sync/time_sync.h - + App/sync/time_sync.c->App/sync/time_sync.h - - + + - + App/sync/time_sync.c->App/util/sys_clock.h - - + + - + App/sync/time_sync.c->App/util/task_watchdog.h - - + + - + Drivers/BSP/NET/net_socket.h - -Drivers/BSP/NET/net_socket.h + +Drivers/BSP/NET/net_socket.h - + App/sync/time_sync.c->Drivers/BSP/NET/net_socket.h - - + + - + App/sync/time_sync.c->Drivers/BSP/dbg_log.h - - + + - + App/sync/time_sync.c->外部/系统头文件 - - + + - + App/sync/time_sync.h->外部/系统头文件 - - + + - + App/task/adc_task.c - -App/task/adc_task.c + +App/task/adc_task.c - + App/task/adc_task.c->App/algo/adc_health.h - - + + - + App/task/adc_task.c->App/algo/adc_input.h - - + + + + + +App/task/adc_task.c->App/algo/freq.h + + - + App/task/adc_task.c->App/algo/phasor.h - - + + - + App/task/adc_task.c->App/algo/sample_ring.h - - + + - + App/task/adc_task.c->App/log/console_log.h - - + + - + App/task/adc_task.c->App/log/sys_log.h - - + + - + App/task/adc_task.h - -App/task/adc_task.h + +App/task/adc_task.h - + App/task/adc_task.c->App/task/adc_task.h - - + + - + App/util/task_prof.h - -App/util/task_prof.h + +App/util/task_prof.h - + App/task/adc_task.c->App/util/task_prof.h - - + + - + App/task/adc_task.c->App/util/task_watchdog.h - - + + - + App/task/adc_task.c->Drivers/BSP/TPAFE5160/tpafe5160.h - - + + - + App/task/adc_task.c->Drivers/BSP/dbg_log.h - - + + - + App/task/adc_task.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - + + - + App/task/default_task.c - -App/task/default_task.c + +App/task/default_task.c - + App/task/default_task.c->App/app/sys_events.h - - + + - + App/task/default_task.c->App/cfg/cfg_file.h - - + + + + + +App/task/default_task.c->App/cfg/prot_set_store.h + + - + App/task/default_task.c->App/fs/vol_lock.h - - + + - + App/task/default_task.c->App/log/sys_log.h - - + + - + App/task/default_task.h - -App/task/default_task.h + +App/task/default_task.h - + App/task/default_task.c->App/task/default_task.h - - + + - + App/task/default_task.c->App/util/task_prof.h - - + + - + App/task/default_task.c->App/util/task_watchdog.h - - + + - + Drivers/BSP/GD5F2GQ5UE/nand_ftl.h - -Drivers/BSP/GD5F2GQ5UE/nand_ftl.h + +Drivers/BSP/GD5F2GQ5UE/nand_ftl.h - + App/task/default_task.c->Drivers/BSP/GD5F2GQ5UE/nand_ftl.h - - + + - + Drivers/BSP/NETCFG/net_cfg.h - -Drivers/BSP/NETCFG/net_cfg.h + +Drivers/BSP/NETCFG/net_cfg.h - + App/task/default_task.c->Drivers/BSP/NETCFG/net_cfg.h - - + + - + App/task/default_task.c->Drivers/BSP/dbg_log.h - - + + - + App/task/default_task.c->Inc/main.h - - + + - + App/task/default_task.c->Lib/FatFs/ff.h - - + + - + App/task/default_task.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - + + - + App/task/default_task.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - - + + - + App/task/default_task.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h - - + + - + App/task/default_task.c->test/board/test_config.h - - + + - + App/task/iec104_serv.c - -App/task/iec104_serv.c + +App/task/iec104_serv.c + + + +App/task/iec104_serv.c->App/algo/prot_set.h + + - + App/task/iec104_serv.c->App/app/sys_events.h - - + + - + App/task/iec104_serv.c->App/protocol/iec104/iec104_link.h - - + + - + App/task/iec104_serv.c->App/protocol/iec104/iec104_pts.h - - + + - + App/task/iec104_serv.h - -App/task/iec104_serv.h + +App/task/iec104_serv.h - + App/task/iec104_serv.c->App/task/iec104_serv.h - - + + - + App/task/iec104_serv.c->App/util/sys_clock.h - - + + - + App/task/iec104_serv.c->App/util/task_watchdog.h - - + + - + App/task/iec104_serv.c->Drivers/BSP/NET/net_socket.h - - + + - + App/task/iec104_serv.c->Drivers/BSP/dbg_log.h - - + + - + App/task/iec104_serv.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os2.h - - + + - + App/task/iec104_serv.c->外部/系统头文件 - - + + - + App/task/net_task.c - -App/task/net_task.c + +App/task/net_task.c - + App/task/net_task.c->App/app/sys_events.h - - + + - + App/task/net_task.c->App/log/sys_log.h - - + + - + App/task/net_task.h - -App/task/net_task.h + +App/task/net_task.h - + App/task/net_task.c->App/task/net_task.h - - + + - + App/task/net_task.c->App/util/task_prof.h - - + + - + App/task/net_task.c->App/util/task_watchdog.h - - + + - + Drivers/BSP/CH395F/ch395f.h - -Drivers/BSP/CH395F/ch395f.h + +Drivers/BSP/CH395F/ch395f.h - + App/task/net_task.c->Drivers/BSP/CH395F/ch395f.h - - + + - + Drivers/BSP/NET/net_select.h - -Drivers/BSP/NET/net_select.h + +Drivers/BSP/NET/net_select.h - + App/task/net_task.c->Drivers/BSP/NET/net_select.h - - + + - + App/task/net_task.c->Drivers/BSP/NET/net_socket.h - - + + - + App/task/net_task.c->Drivers/BSP/NETCFG/net_cfg.h - - + + - + App/task/net_task.c->Drivers/BSP/dbg_log.h - - + + - + App/task/net_task.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - + + - + App/task/net_task.c->test/board/test_config.h - - + + - + App/task/net_task.c->外部/系统头文件 - - + + - + App/task/protect_task.c - -App/task/protect_task.c + +App/task/protect_task.c - + App/task/protect_task.c->App/algo/adc_health.h - - + + - + App/task/protect_task.c->App/algo/prot_elm.h - - + + - + App/task/protect_task.c->App/algo/prot_set.h - - + + + + + +App/task/protect_task.c->App/log/console_log.h + + + + + +App/task/protect_task.c->App/log/soe_store.h + + - + App/task/protect_task.h - -App/task/protect_task.h + +App/task/protect_task.h - + App/task/protect_task.c->App/task/protect_task.h - - + + - + App/task/protect_task.c->App/util/task_watchdog.h - - + + - + App/task/protect_task.c->Drivers/BSP/dbg_log.h - - + + - + App/task/protect_task.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os2.h - - + + - + App/task/rs485_task.c - -App/task/rs485_task.c + +App/task/rs485_task.c - + App/task/rs485_task.h - -App/task/rs485_task.h + +App/task/rs485_task.h - + App/task/rs485_task.c->App/task/rs485_task.h - - + + - + App/task/rs485_task.c->Drivers/BSP/RS485/rs485.h - - + + - + App/task/rs485_task.c->Drivers/BSP/dbg_log.h - - + + - + App/task/rs485_task.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - + + - + App/task/ui_task.c - -App/task/ui_task.c + +App/task/ui_task.c - + App/task/ui_task.c->App/app/sys_events.h - - + + - + App/task/ui_task.h - -App/task/ui_task.h + +App/task/ui_task.h - + App/task/ui_task.c->App/task/ui_task.h - - + + - + App/task/ui_task.c->App/util/sys_clock.h - - + + - + App/task/ui_task.c->App/util/task_prof.h - - + + - + App/task/ui_task.c->App/util/task_watchdog.h - - + + - + Drivers/BSP/UI/ui_menu.h - -Drivers/BSP/UI/ui_menu.h + +Drivers/BSP/UI/ui_menu.h - + App/task/ui_task.c->Drivers/BSP/UI/ui_menu.h - - + + - + Drivers/BSP/UI/ui_net_sink.h - -Drivers/BSP/UI/ui_net_sink.h + +Drivers/BSP/UI/ui_net_sink.h - + App/task/ui_task.c->Drivers/BSP/UI/ui_net_sink.h - - + + - + Drivers/BSP/UI/ui_port.h - -Drivers/BSP/UI/ui_port.h + +Drivers/BSP/UI/ui_port.h - + App/task/ui_task.c->Drivers/BSP/UI/ui_port.h - - + + - + Drivers/BSP/UI/ui_standby.h - -Drivers/BSP/UI/ui_standby.h + +Drivers/BSP/UI/ui_standby.h - + App/task/ui_task.c->Drivers/BSP/UI/ui_standby.h - - + + - + App/task/ui_task.c->Drivers/BSP/dbg_log.h - - + + - + App/task/ui_task.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - + + - + test/board/net_latency_test.h - -test/board/net_latency_test.h + +test/board/net_latency_test.h - + App/task/ui_task.c->test/board/net_latency_test.h - - + + - + App/task/ui_task.c->test/board/test_config.h - - + + - + App/task/ui_task.c->外部/系统头文件 - - + + - + App/util/sha256.c - -App/util/sha256.c + +App/util/sha256.c - + App/util/sha256.h - -App/util/sha256.h + +App/util/sha256.h - + App/util/sha256.c->App/util/sha256.h - - + + - + App/util/sha256.c->外部/系统头文件 - - + + - + App/util/sha256.h->外部/系统头文件 - - + + - + App/util/sys_clock.c - -App/util/sys_clock.c + +App/util/sys_clock.c - + App/util/sys_clock.c->App/util/sys_clock.h - - + + - + App/util/sys_clock.c->Drivers/BSP/dbg_log.h - - + + - + App/util/sys_clock.c->Inc/main.h - - + + - + App/util/sys_clock.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - - + + - + App/util/sys_clock.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h - - + + - + App/util/sys_clock.c->外部/系统头文件 - - + + - + App/util/sys_clock.h->Drivers/BSP/SD2506/sd2506.h - - + + - + App/util/sys_clock.h->外部/系统头文件 - - + + - + App/util/task_prof.c - -App/util/task_prof.c + +App/util/task_prof.c - + App/util/task_prof.c->App/log/console_log.h - - + + - + App/util/task_prof.c->App/util/task_prof.h - - + + - + App/util/task_prof.c->Inc/main.h - - + + - + App/util/task_prof.c->外部/系统头文件 - - + + - + App/util/task_prof.h->外部/系统头文件 - - + + - + App/util/task_watchdog.c - -App/util/task_watchdog.c + +App/util/task_watchdog.c - + App/util/task_watchdog.c->App/log/console_log.h - - + + - + App/util/task_watchdog.c->App/log/sys_log.h - - + + - + App/util/task_watchdog.c->App/util/task_watchdog.h - - + + - + Drivers/BSP/NET/lftpd/lftpd.h - -Drivers/BSP/NET/lftpd/lftpd.h + +Drivers/BSP/NET/lftpd/lftpd.h - + App/util/task_watchdog.c->Drivers/BSP/NET/lftpd/lftpd.h - - + + - + App/util/task_watchdog.c->Drivers/BSP/dbg_log.h - - + + - + Inc/iwdg.h - -Inc/iwdg.h + +Inc/iwdg.h - + App/util/task_watchdog.c->Inc/iwdg.h - - + + - + App/util/task_watchdog.c->Inc/main.h - - + + - + App/util/task_watchdog.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - - + + - + App/util/task_watchdog.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h - - + + - + App/util/task_watchdog.c->外部/系统头文件 - - + + - + App/util/task_watchdog.h->外部/系统头文件 - - + + - + Drivers/BSP/CH395F/ch395f.c - -Drivers/BSP/CH395F/ch395f.c + +Drivers/BSP/CH395F/ch395f.c - + Drivers/BSP/CH395F/ch395f.c->Drivers/BSP/CH395F/ch395f.h - - + + - + Drivers/BSP/CH395F/ch395f.c->Drivers/BSP/dbg_log.h - - + + - + Inc/spi.h - -Inc/spi.h + +Inc/spi.h - + Drivers/BSP/CH395F/ch395f.c->Inc/spi.h - - + + - + Drivers/BSP/CH395F/ch395f.c->Inc/usart.h - - + + - + Drivers/BSP/CH395F/ch395f.c->外部/系统头文件 - - + + - + Drivers/BSP/CH395F/ch395f.h->Inc/main.h - - + + - + Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c - -Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c + +Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c - + Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c->Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h - - + + - + Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c->Drivers/BSP/dbg_log.h - - + + - + Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c->外部/系统头文件 - - + + - + Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h->Inc/main.h - - + + - + Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h->外部/系统头文件 - - + + - + Drivers/BSP/GD5F2GQ5UE/nand_ftl.c - -Drivers/BSP/GD5F2GQ5UE/nand_ftl.c + +Drivers/BSP/GD5F2GQ5UE/nand_ftl.c - + Drivers/BSP/GD5F2GQ5UE/nand_ftl.c->Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h - - + + - + Drivers/BSP/GD5F2GQ5UE/nand_ftl.c->Drivers/BSP/dbg_log.h - - + + - + Lib/FatFs/diskio.h - -Lib/FatFs/diskio.h + +Lib/FatFs/diskio.h - + Drivers/BSP/GD5F2GQ5UE/nand_ftl.c->Lib/FatFs/diskio.h - - + + - + Drivers/BSP/GD5F2GQ5UE/nand_ftl.c->Lib/FatFs/ff.h - - + + - + Lib/dhara/map.h - -Lib/dhara/map.h + +Lib/dhara/map.h - + Drivers/BSP/GD5F2GQ5UE/nand_ftl.c->Lib/dhara/map.h - - + + - + Lib/dhara/nand.h - -Lib/dhara/nand.h + +Lib/dhara/nand.h - + Drivers/BSP/GD5F2GQ5UE/nand_ftl.c->Lib/dhara/nand.h - - + + - + Drivers/BSP/GD5F2GQ5UE/nand_ftl.c->外部/系统头文件 - - + + - + Drivers/BSP/GD5F2GQ5UE/nand_ftl.h->外部/系统头文件 - - + + - + Drivers/BSP/NET/lftpd/lftpd_auth.c - -Drivers/BSP/NET/lftpd/lftpd_auth.c + +Drivers/BSP/NET/lftpd/lftpd_auth.c - + Drivers/BSP/NET/net_config.h - -Drivers/BSP/NET/net_config.h + +Drivers/BSP/NET/net_config.h - - -Drivers/BSP/NET/net_select.c - -Drivers/BSP/NET/net_select.c + + +Drivers/BSP/NET/net_listener.c + +Drivers/BSP/NET/net_listener.c - - -Drivers/BSP/NET/net_select.c->Drivers/BSP/CH395F/ch395f.h - - + + +Drivers/BSP/NET/net_listener.c->Drivers/BSP/CH395F/ch395f.h + + - - -Drivers/BSP/NET/net_select.c->Drivers/BSP/NET/net_select.h - - + + +Drivers/BSP/NET/net_listener.h + +Drivers/BSP/NET/net_listener.h - - -Drivers/BSP/NET/net_select.c->Drivers/BSP/NET/net_socket.h - - + + +Drivers/BSP/NET/net_listener.c->Drivers/BSP/NET/net_listener.h + + - - -Drivers/BSP/NET/net_select.c->Inc/main.h - - + + +Drivers/BSP/NET/net_listener.c->Drivers/BSP/NET/net_socket.h + + - - -Drivers/BSP/NET/net_select.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - + + +Drivers/BSP/NET/net_listener.c->Drivers/BSP/dbg_log.h + + - - -Drivers/BSP/NET/net_select.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - - + + +Drivers/BSP/NET/net_listener.c->Inc/main.h + + - - -Drivers/BSP/NET/net_select.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h - - + + +Drivers/BSP/NET/net_listener.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h + + - - -Drivers/BSP/NET/net_select.c->外部/系统头文件 - - + + +Drivers/BSP/NET/net_listener.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h + + - - -Drivers/BSP/NET/net_select.h->Drivers/BSP/NET/net_config.h - - + + +Drivers/BSP/NET/net_listener.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h + + + + + +Drivers/BSP/NET/net_listener.c->test/board/net_latency_test.h + + + + + +Drivers/BSP/NET/net_listener.c->test/board/test_config.h + + + + + +Drivers/BSP/NET/net_listener.c->外部/系统头文件 + + - + Drivers/BSP/NET/net_types.h - -Drivers/BSP/NET/net_types.h + +Drivers/BSP/NET/net_types.h + + + +Drivers/BSP/NET/net_listener.h->Drivers/BSP/NET/net_types.h + + + + + +Drivers/BSP/NET/net_select.c + +Drivers/BSP/NET/net_select.c + + + +Drivers/BSP/NET/net_select.c->Drivers/BSP/CH395F/ch395f.h + + + + + +Drivers/BSP/NET/net_select.c->Drivers/BSP/NET/net_select.h + + + + + +Drivers/BSP/NET/net_select.c->Drivers/BSP/NET/net_socket.h + + + + + +Drivers/BSP/NET/net_select.c->Inc/main.h + + + + + +Drivers/BSP/NET/net_select.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h + + + + + +Drivers/BSP/NET/net_select.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h + + + + + +Drivers/BSP/NET/net_select.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h + + + + + +Drivers/BSP/NET/net_select.c->外部/系统头文件 + + + + + +Drivers/BSP/NET/net_select.h->Drivers/BSP/NET/net_config.h + + - + Drivers/BSP/NET/net_select.h->Drivers/BSP/NET/net_types.h - - + + - + Drivers/BSP/NET/net_socket.c - -Drivers/BSP/NET/net_socket.c + +Drivers/BSP/NET/net_socket.c - + Drivers/BSP/NET/net_socket.c->Drivers/BSP/CH395F/ch395f.h - - + + + + + +Drivers/BSP/NET/net_socket.c->Drivers/BSP/NET/net_listener.h + + - + Drivers/BSP/NET/net_socket.c->Drivers/BSP/NET/net_socket.h - - + + - + Drivers/BSP/NET/net_socket.c->Drivers/BSP/dbg_log.h - - + + - + Drivers/BSP/NET/net_socket.c->Inc/main.h - - + + - + Drivers/BSP/NET/net_socket.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - + + - + Drivers/BSP/NET/net_socket.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - - + + - + Drivers/BSP/NET/net_socket.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h - - + + - + Drivers/BSP/NET/net_socket.c->test/board/net_latency_test.h - - + + - + Drivers/BSP/NET/net_socket.c->test/board/test_config.h - - + + - + Drivers/BSP/NET/net_socket.c->外部/系统头文件 - - + + - + Drivers/BSP/NET/net_socket.h->Drivers/BSP/NET/net_types.h - - + + - + Drivers/BSP/NET/net_socket.h->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - - + + - + Drivers/BSP/NET/net_socket.h->Middlewares/Third_Party/FreeRTOS/Source/include/task.h - - + + - + Drivers/BSP/NET/net_types.h->Drivers/BSP/NET/net_config.h - - + + - + Drivers/BSP/NET/net_types.h->外部/系统头文件 - - + + - + Drivers/BSP/NETCFG/ftp_cfg.c - -Drivers/BSP/NETCFG/ftp_cfg.c + +Drivers/BSP/NETCFG/ftp_cfg.c - + Drivers/BSP/NETCFG/ftp_cfg.c->App/cfg/cfg_file.h - - + + - + Drivers/BSP/NETCFG/ftp_cfg.h - -Drivers/BSP/NETCFG/ftp_cfg.h + +Drivers/BSP/NETCFG/ftp_cfg.h - + Drivers/BSP/NETCFG/ftp_cfg.c->Drivers/BSP/NETCFG/ftp_cfg.h - - + + - + Drivers/BSP/NETCFG/ftp_cfg.c->外部/系统头文件 - - + + - + Drivers/BSP/NETCFG/ftp_cfg.h->外部/系统头文件 - - + + - + Drivers/BSP/NETCFG/net_cfg.c - -Drivers/BSP/NETCFG/net_cfg.c + +Drivers/BSP/NETCFG/net_cfg.c - + Drivers/BSP/NETCFG/net_cfg.c->App/cfg/cfg_file.h - - + + - + Drivers/BSP/NETCFG/net_cfg.c->Drivers/BSP/NETCFG/ftp_cfg.h - - + + - + Drivers/BSP/NETCFG/net_cfg.c->Drivers/BSP/NETCFG/net_cfg.h - - + + - + Drivers/BSP/NETCFG/net_cfg.c->Drivers/BSP/dbg_log.h - - + + - + Drivers/BSP/NETCFG/net_cfg.c->外部/系统头文件 - - + + - + Drivers/BSP/NETCFG/net_cfg.h->外部/系统头文件 - - + + - + Drivers/BSP/RS485/rs485.c - -Drivers/BSP/RS485/rs485.c + +Drivers/BSP/RS485/rs485.c - + Drivers/BSP/RS485/rs485.c->Drivers/BSP/RS485/rs485.h - - + + - + Drivers/BSP/RS485/rs485.h->Inc/main.h - - + + - + Drivers/BSP/SD2506/sd2506.c - -Drivers/BSP/SD2506/sd2506.c + +Drivers/BSP/SD2506/sd2506.c - + Drivers/BSP/SD2506/sd2506.c->Drivers/BSP/SD2506/sd2506.h - - + + - + Drivers/BSP/SD2506/sd2506.c->Drivers/BSP/dbg_log.h - - + + - + Inc/i2c.h - -Inc/i2c.h + +Inc/i2c.h - + Drivers/BSP/SD2506/sd2506.c->Inc/i2c.h - - + + - + Drivers/BSP/SD2506/sd2506.c->外部/系统头文件 - - + + - + Drivers/BSP/SD2506/sd2506.h->Inc/main.h - - + + - + Drivers/BSP/TPAFE5160/tpafe5160.c - -Drivers/BSP/TPAFE5160/tpafe5160.c + +Drivers/BSP/TPAFE5160/tpafe5160.c - + Drivers/BSP/TPAFE5160/tpafe5160.c->Drivers/BSP/TPAFE5160/tpafe5160.h - - + + - + Drivers/BSP/TPAFE5160/tpafe5160.c->Drivers/BSP/dbg_log.h - - + + - + Drivers/BSP/TPAFE5160/tpafe5160.c->Inc/main.h - - + + - + Drivers/BSP/TPAFE5160/tpafe5160.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - - + + - + Drivers/BSP/TPAFE5160/tpafe5160.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h - - + + - + Drivers/BSP/TPAFE5160/tpafe5160.c->外部/系统头文件 - - + + - + Drivers/BSP/TPAFE5160/tpafe5160.h->Inc/main.h - - + + - + Drivers/BSP/UI/ui_content_page.c - -Drivers/BSP/UI/ui_content_page.c + +Drivers/BSP/UI/ui_content_page.c - + Drivers/BSP/UI/ui_content_page.h - -Drivers/BSP/UI/ui_content_page.h + +Drivers/BSP/UI/ui_content_page.h - + Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_content_page.h - - + + - + Drivers/BSP/UI/ui_deco.h - -Drivers/BSP/UI/ui_deco.h + +Drivers/BSP/UI/ui_deco.h - + Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_deco.h - - + + - + Drivers/BSP/UI/ui_menu_view.h - -Drivers/BSP/UI/ui_menu_view.h + +Drivers/BSP/UI/ui_menu_view.h - + Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_menu_view.h - - + + - + Drivers/BSP/UI/ui_page_ac.h - -Drivers/BSP/UI/ui_page_ac.h + +Drivers/BSP/UI/ui_page_ac.h - + Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_page_ac.h - - + + - + Drivers/BSP/UI/ui_page_dc.h - -Drivers/BSP/UI/ui_page_dc.h + +Drivers/BSP/UI/ui_page_dc.h - + Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_page_dc.h - - + + - + Drivers/BSP/UI/ui_page_device_info.h - -Drivers/BSP/UI/ui_page_device_info.h + +Drivers/BSP/UI/ui_page_device_info.h - + Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_page_device_info.h - - + + - + Drivers/BSP/UI/ui_page_net_cfg.h - -Drivers/BSP/UI/ui_page_net_cfg.h + +Drivers/BSP/UI/ui_page_net_cfg.h - + Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_page_net_cfg.h - - + + - + Drivers/BSP/UI/ui_page_password.h - -Drivers/BSP/UI/ui_page_password.h + +Drivers/BSP/UI/ui_page_password.h - + Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_page_password.h - - + + - + Drivers/BSP/UI/ui_page_runlog.h - -Drivers/BSP/UI/ui_page_runlog.h + +Drivers/BSP/UI/ui_page_runlog.h - + Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_page_runlog.h - - + + - + Drivers/BSP/UI/ui_page_selftest.h - -Drivers/BSP/UI/ui_page_selftest.h + +Drivers/BSP/UI/ui_page_selftest.h - + Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_page_selftest.h - - + + - + Drivers/BSP/UI/ui_pwd_def.h - -Drivers/BSP/UI/ui_pwd_def.h + +Drivers/BSP/UI/ui_pwd_def.h - + Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_pwd_def.h - - + + - + Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/UI/ui_standby.h - - + + - + Drivers/BSP/UI/ui_content_page.c->Drivers/BSP/dbg_log.h - - + + - + Lib/lvgl/lvgl.h - -Lib/lvgl/lvgl.h + +Lib/lvgl/lvgl.h - + Drivers/BSP/UI/ui_content_page.c->Lib/lvgl/lvgl.h - - + + - + Drivers/BSP/UI/ui_content_page.c->外部/系统头文件 - - + + - + Drivers/BSP/UI/ui_content_page.h->Lib/lvgl/lvgl.h - - + + - + Drivers/BSP/UI/ui_content_page.h->外部/系统头文件 - - + + - + Drivers/BSP/UI/ui_deco.c - -Drivers/BSP/UI/ui_deco.c + +Drivers/BSP/UI/ui_deco.c - + Drivers/BSP/UI/ui_deco.c->Drivers/BSP/UI/ui_deco.h - - + + - + Drivers/BSP/UI/ui_deco.c->外部/系统头文件 - - + + - + Drivers/BSP/UI/ui_deco.h->Lib/lvgl/lvgl.h - - + + - + Drivers/BSP/UI/ui_font_cn_12.c - -Drivers/BSP/UI/ui_font_cn_12.c + +Drivers/BSP/UI/ui_font_cn_12.c - + Drivers/BSP/UI/ui_font_cn_12.c->Lib/lvgl/lvgl.h - - + + - + Drivers/BSP/UI/ui_menu.c - -Drivers/BSP/UI/ui_menu.c + +Drivers/BSP/UI/ui_menu.c - + Drivers/BSP/UI/ui_menu.c->Drivers/BSP/UI/ui_content_page.h - - + + - + Drivers/BSP/UI/ui_menu.c->Drivers/BSP/UI/ui_menu.h - - + + - + Drivers/BSP/UI/ui_menu_tab.h - -Drivers/BSP/UI/ui_menu_tab.h + +Drivers/BSP/UI/ui_menu_tab.h - + Drivers/BSP/UI/ui_menu.c->Drivers/BSP/UI/ui_menu_tab.h - - + + - + Drivers/BSP/UI/ui_menu.c->Drivers/BSP/UI/ui_menu_view.h - - + + - + Drivers/BSP/UI/ui_menu.c->Drivers/BSP/UI/ui_page_password.h - - + + - + Drivers/BSP/UI/ui_menu.c->Drivers/BSP/UI/ui_pwd_def.h - - + + - + Drivers/BSP/UI/ui_menu.c->Drivers/BSP/UI/ui_standby.h - - + + - + Drivers/BSP/UI/ui_menu.c->Drivers/BSP/dbg_log.h - - + + - + Drivers/BSP/UI/ui_menu.c->Lib/lvgl/lvgl.h - - + + - + Drivers/BSP/UI/ui_menu.c->外部/系统头文件 - - + + - + Drivers/BSP/UI/ui_menu_tab.c - -Drivers/BSP/UI/ui_menu_tab.c + +Drivers/BSP/UI/ui_menu_tab.c - + Drivers/BSP/UI/ui_menu_tab.c->Drivers/BSP/UI/ui_menu_tab.h - - + + - + Drivers/BSP/UI/ui_menu_tab.h->外部/系统头文件 - - + + - + Drivers/BSP/UI/ui_menu_view.c - -Drivers/BSP/UI/ui_menu_view.c + +Drivers/BSP/UI/ui_menu_view.c - + Drivers/BSP/UI/ui_menu_view.c->Drivers/BSP/UI/ui_deco.h - - + + - + Drivers/BSP/UI/ui_menu_view.c->Drivers/BSP/UI/ui_menu_tab.h - - + + - + Drivers/BSP/UI/ui_menu_view.c->Drivers/BSP/UI/ui_menu_view.h - - + + - + Drivers/BSP/UI/ui_menu_view.c->Drivers/BSP/dbg_log.h - - + + - + Drivers/BSP/UI/ui_menu_view.c->Lib/lvgl/lvgl.h - - + + - + Drivers/BSP/UI/ui_menu_view.c->外部/系统头文件 - - + + - + Drivers/BSP/UI/ui_menu_view.h->Lib/lvgl/lvgl.h - - + + - + Drivers/BSP/UI/ui_menu_view.h->外部/系统头文件 - - + + - + Drivers/BSP/UI/ui_net_sink.c - -Drivers/BSP/UI/ui_net_sink.c + +Drivers/BSP/UI/ui_net_sink.c - + Drivers/BSP/UI/ui_net_sink.c->Drivers/BSP/NET/net_socket.h - - + + - + Drivers/BSP/UI/ui_net_sink.c->Drivers/BSP/NET/net_types.h - - + + - + Drivers/BSP/UI/ui_net_sink.c->Drivers/BSP/UI/ui_net_sink.h - - + + - + Drivers/BSP/UI/ui_net_sink_backend.h - -Drivers/BSP/UI/ui_net_sink_backend.h + +Drivers/BSP/UI/ui_net_sink_backend.h - + Drivers/BSP/UI/ui_net_sink.c->Drivers/BSP/UI/ui_net_sink_backend.h - - + + - + Drivers/BSP/UI/ui_net_sink.c->Drivers/BSP/dbg_log.h - - + + - + Drivers/BSP/UI/ui_net_sink.c->test/board/net_latency_test.h - - + + - + Drivers/BSP/UI/ui_net_sink.c->test/board/test_config.h - - + + - + Drivers/BSP/UI/ui_net_sink.c->外部/系统头文件 - - + + - + Drivers/BSP/UI/ui_net_sink.h->外部/系统头文件 - - + + - + Drivers/BSP/UI/ui_net_sink_backend.h->外部/系统头文件 - - + + - + Drivers/BSP/UI/ui_net_sink_backend_mcu.c - -Drivers/BSP/UI/ui_net_sink_backend_mcu.c + +Drivers/BSP/UI/ui_net_sink_backend_mcu.c - + Drivers/BSP/UI/ui_net_sink_backend_mcu.c->Drivers/BSP/UI/ui_net_sink_backend.h - - + + - + Drivers/BSP/UI/ui_net_sink_backend_mcu.c->Drivers/BSP/UI/ui_port.h - - + + - + Drivers/BSP/UI/ui_net_sink_backend_mcu.c->外部/系统头文件 - - + + - + Drivers/BSP/UI/ui_page_ac.c - -Drivers/BSP/UI/ui_page_ac.c + +Drivers/BSP/UI/ui_page_ac.c - + Drivers/BSP/UI/ui_page_ac.c->Drivers/BSP/UI/ui_page_ac.h - - + + - + Drivers/BSP/UI/ui_page_ac.h->Drivers/BSP/UI/ui_content_page.h - - + + - + Drivers/BSP/UI/ui_page_dc.c - -Drivers/BSP/UI/ui_page_dc.c + +Drivers/BSP/UI/ui_page_dc.c - + Drivers/BSP/UI/ui_page_dc.c->Drivers/BSP/UI/ui_page_dc.h - - + + - + Drivers/BSP/UI/ui_page_dc.h->Drivers/BSP/UI/ui_content_page.h - - + + - + Drivers/BSP/UI/ui_page_device_info.c - -Drivers/BSP/UI/ui_page_device_info.c + +Drivers/BSP/UI/ui_page_device_info.c - + Drivers/BSP/UI/ui_page_device_info.c->Drivers/BSP/UI/ui_page_device_info.h - - + + - + Drivers/BSP/UI/ui_page_device_info.h->Drivers/BSP/UI/ui_content_page.h - - + + - + Drivers/BSP/UI/ui_page_net_cfg.c - -Drivers/BSP/UI/ui_page_net_cfg.c + +Drivers/BSP/UI/ui_page_net_cfg.c - + Drivers/BSP/UI/ui_page_net_cfg.c->Drivers/BSP/NETCFG/net_cfg.h - - + + - + Drivers/BSP/UI/ui_page_net_cfg.c->Drivers/BSP/UI/ui_page_net_cfg.h - - + + - + Drivers/BSP/UI/ui_page_net_cfg.c->外部/系统头文件 - - + + - + Drivers/BSP/UI/ui_page_net_cfg.h->Drivers/BSP/UI/ui_content_page.h - - + + - + Drivers/BSP/UI/ui_page_password.c - -Drivers/BSP/UI/ui_page_password.c + +Drivers/BSP/UI/ui_page_password.c - + Drivers/BSP/UI/ui_page_password.c->Drivers/BSP/UI/ui_page_password.h - - + + - + Drivers/BSP/UI/ui_page_password.c->Drivers/BSP/UI/ui_pwd_def.h - - + + - + Drivers/BSP/UI/ui_page_password.c->外部/系统头文件 - - + + - + Drivers/BSP/UI/ui_page_password.h->Drivers/BSP/UI/ui_content_page.h - - + + - + Drivers/BSP/UI/ui_page_runlog.c - -Drivers/BSP/UI/ui_page_runlog.c + +Drivers/BSP/UI/ui_page_runlog.c - + Drivers/BSP/UI/ui_page_runlog.c->App/log/sys_log.h - - + + - + Drivers/BSP/UI/ui_page_runlog.c->Drivers/BSP/UI/ui_page_runlog.h - - + + - + Drivers/BSP/UI/ui_page_runlog.c->外部/系统头文件 - - + + - + Drivers/BSP/UI/ui_page_runlog.h->Drivers/BSP/UI/ui_content_page.h - - + + - + Drivers/BSP/UI/ui_page_selftest.c - -Drivers/BSP/UI/ui_page_selftest.c + +Drivers/BSP/UI/ui_page_selftest.c - + Drivers/BSP/UI/ui_page_selftest.c->App/log/sys_log.h - - + + - + Drivers/BSP/UI/ui_page_selftest.c->Drivers/BSP/UI/ui_page_selftest.h - - + + - + Drivers/BSP/UI/ui_page_selftest.c->外部/系统头文件 - - + + - + Drivers/BSP/UI/ui_page_selftest.h->Drivers/BSP/UI/ui_content_page.h - - + + - + Drivers/BSP/UI/ui_port.c - -Drivers/BSP/UI/ui_port.c + +Drivers/BSP/UI/ui_port.c - + Drivers/BSP/UI/ui_port.c->Drivers/BSP/UI/ui_port.h - - + + - + Drivers/BSP/UI/ui_port.c->Drivers/BSP/dbg_log.h - - + + - + Drivers/BSP/UI/ui_port.c->Lib/lvgl/lvgl.h - - + + - + Drivers/BSP/UI/ui_port.c->外部/系统头文件 - - + + - + Drivers/BSP/UI/ui_port.h->外部/系统头文件 - - + + - + Drivers/BSP/UI/ui_pwd_def.h->外部/系统头文件 - - + + - + Drivers/BSP/UI/ui_standby.c - -Drivers/BSP/UI/ui_standby.c + +Drivers/BSP/UI/ui_standby.c - + Drivers/BSP/UI/ui_standby.c->Drivers/BSP/UI/ui_menu_view.h - - + + - + Drivers/BSP/UI/ui_standby.c->Drivers/BSP/UI/ui_standby.h - - + + - + Drivers/BSP/UI/ui_standby.c->Drivers/BSP/dbg_log.h - - + + - + Drivers/BSP/UI/ui_standby.c->Lib/lvgl/lvgl.h - - + + - + Drivers/BSP/UI/ui_standby.c->外部/系统头文件 - - + + - + Drivers/BSP/UI/ui_standby.h->外部/系统头文件 - - + + - + Drivers/BSP/dbg_cfg.h - -Drivers/BSP/dbg_cfg.h + +Drivers/BSP/dbg_cfg.h - + Drivers/BSP/dbg_log.h->App/log/console_log.h - - + + - + Drivers/BSP/dbg_log.h->App/util/sys_clock.h - - + + - + Drivers/BSP/dbg_log.h->Drivers/BSP/dbg_cfg.h - - + + - + Drivers/BSP/dbg_log.h->外部/系统头文件 - - + + - + Drivers/CMSIS/Device/ST/STM32F4xx/Include/stm32f4xx.h - -Drivers/CMSIS/Device/ST/STM32F4xx/Include/stm32f4xx.h + +Drivers/CMSIS/Device/ST/STM32F4xx/Include/stm32f4xx.h - + Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal.h - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal.h + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal.h - + Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_adc.h - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_adc.h + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_adc.h - + Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_cortex.h - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_cortex.h + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_cortex.h - + Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_dma.h - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_dma.h + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_dma.h - + Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_exti.h - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_exti.h + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_exti.h - + Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_flash.h - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_flash.h + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_flash.h - + Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_gpio.h - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_gpio.h + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_gpio.h - + Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_i2c.h - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_i2c.h + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_i2c.h - + Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_iwdg.h - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_iwdg.h + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_iwdg.h - + Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_pwr.h - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_pwr.h + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_pwr.h - + Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_rcc.h - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_rcc.h + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_rcc.h - + Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_spi.h - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_spi.h + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_spi.h - + Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_tim.h - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_tim.h + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_tim.h - + Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_uart.h - -Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_uart.h + +Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_uart.h - + Inc/FreeRTOSConfig.h - -Inc/FreeRTOSConfig.h + +Inc/FreeRTOSConfig.h - + Inc/FreeRTOSConfig.h->外部/系统头文件 - - + + - + Inc/adc.h->Inc/main.h - - + + - + Inc/dma.h - -Inc/dma.h + +Inc/dma.h - + Inc/dma.h->Inc/main.h - - + + - + Inc/gpio.h - -Inc/gpio.h + +Inc/gpio.h - + Inc/gpio.h->Inc/main.h - - + + - + Inc/i2c.h->Inc/main.h - - + + - + Inc/iwdg.h->Inc/main.h - - + + - + Inc/main.h->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal.h - - + + - + Inc/spi.h->Inc/main.h - - + + - + Inc/stm32f4xx_hal_conf.h - -Inc/stm32f4xx_hal_conf.h + +Inc/stm32f4xx_hal_conf.h - + Inc/stm32f4xx_hal_conf.h->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_adc.h - - + + - + Inc/stm32f4xx_hal_conf.h->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_cortex.h - - + + - + Inc/stm32f4xx_hal_conf.h->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_dma.h - - + + - + Inc/stm32f4xx_hal_conf.h->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_exti.h - - + + - + Inc/stm32f4xx_hal_conf.h->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_flash.h - - + + - + Inc/stm32f4xx_hal_conf.h->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_gpio.h - - + + - + Inc/stm32f4xx_hal_conf.h->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_i2c.h - - + + - + Inc/stm32f4xx_hal_conf.h->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_iwdg.h - - + + - + Inc/stm32f4xx_hal_conf.h->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_pwr.h - - + + - + Inc/stm32f4xx_hal_conf.h->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_rcc.h - - + + - + Inc/stm32f4xx_hal_conf.h->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_spi.h - - + + - + Inc/stm32f4xx_hal_conf.h->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_tim.h - - + + - + Inc/stm32f4xx_hal_conf.h->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_uart.h - - + + - + Inc/stm32f4xx_hal_conf.h->外部/系统头文件 - - + + - + Inc/tim.h->Inc/main.h - - + + - + Inc/usart.h->Inc/main.h - - + + - + Src/adc.c - -Src/adc.c + +Src/adc.c - + Src/adc.c->Inc/adc.h - - + + - + Src/dma.c - -Src/dma.c + +Src/dma.c - + Src/dma.c->Inc/dma.h - - + + - + Src/freertos.c - -Src/freertos.c + +Src/freertos.c - + Src/freertos.c->App/app/sys_events.h - - + + - + Src/freertos.c->App/sync/time_sync.h - - + + - + Src/freertos.c->App/task/adc_task.h - - + + - + Src/freertos.c->App/task/default_task.h - - + + - + Src/freertos.c->App/task/iec104_serv.h - - + + - + Src/freertos.c->App/task/net_task.h - - + + - + Src/freertos.c->App/task/protect_task.h - - + + - + Src/freertos.c->App/util/task_watchdog.h - - + + - + Src/freertos.c->Drivers/BSP/CH395F/ch395f.h - - + + - + Src/freertos.c->Drivers/BSP/NET/lftpd/lftpd.h - - + + - + Src/freertos.c->Drivers/BSP/NET/net_socket.h - - + + - + Src/freertos.c->Drivers/BSP/dbg_log.h - - + + - + Src/freertos.c->Inc/main.h - - + + - + Src/freertos.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - + + - + Src/freertos.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - - + + - + Src/freertos.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h - - + + - + test/board/ch395f_test.h - -test/board/ch395f_test.h + +test/board/ch395f_test.h - + Src/freertos.c->test/board/ch395f_test.h - - + + - + test/board/ch395f_test_task.h - -test/board/ch395f_test_task.h + +test/board/ch395f_test_task.h - + Src/freertos.c->test/board/ch395f_test_task.h - - + + - + Src/freertos.c->test/board/test_config.h - - + + - + Src/freertos.c->外部/系统头文件 - - + + - + Src/gpio.c - -Src/gpio.c + +Src/gpio.c - + Src/gpio.c->Inc/gpio.h - - + + - + Src/i2c.c - -Src/i2c.c + +Src/i2c.c - + Src/i2c.c->Inc/i2c.h - - + + - + Src/iwdg.c - -Src/iwdg.c + +Src/iwdg.c - + Src/iwdg.c->Inc/iwdg.h - - + + - + Src/main.c - -Src/main.c + +Src/main.c - + Src/main.c->App/app/app_main.h - - + + - + Src/main.c->App/task/adc_task.h - - + + - + Src/main.c->App/util/sys_clock.h - - + + - + Src/main.c->App/util/task_watchdog.h - - + + - + Src/main.c->Drivers/BSP/CH395F/ch395f.h - - + + - + Src/main.c->Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h - - + + - + Src/main.c->Drivers/BSP/NET/net_select.h - - + + - + Src/main.c->Drivers/BSP/NET/net_socket.h - - + + - + Src/main.c->Drivers/BSP/RS485/rs485.h - - + + - + Src/main.c->Drivers/BSP/SD2506/sd2506.h - - + + - + Src/main.c->Drivers/BSP/TPAFE5160/tpafe5160.h - - + + - + Src/main.c->Drivers/BSP/dbg_log.h - - + + - + Src/main.c->Inc/adc.h - - + + - + Src/main.c->Inc/dma.h - - + + - + Src/main.c->Inc/gpio.h - - + + - + Src/main.c->Inc/i2c.h - - + + - + Src/main.c->Inc/iwdg.h - - + + - + Src/main.c->Inc/main.h - - + + - + Src/main.c->Inc/spi.h - - + + - + Src/main.c->Inc/tim.h - - + + - + Src/main.c->Inc/usart.h - - + + - + Src/main.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - + + - + Src/main.c->外部/系统头文件 - - + + - + Src/spi.c - -Src/spi.c + +Src/spi.c - + Src/spi.c->Inc/spi.h - - + + - + Src/stm32f4xx_hal_msp.c - -Src/stm32f4xx_hal_msp.c + +Src/stm32f4xx_hal_msp.c - + Src/stm32f4xx_hal_msp.c->Inc/main.h - - + + - + Src/stm32f4xx_hal_timebase_tim.c - -Src/stm32f4xx_hal_timebase_tim.c + +Src/stm32f4xx_hal_timebase_tim.c - + Src/stm32f4xx_hal_timebase_tim.c->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal.h - - + + - + Src/stm32f4xx_hal_timebase_tim.c->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_tim.h - - + + - + Src/stm32f4xx_it.c - -Src/stm32f4xx_it.c + +Src/stm32f4xx_it.c - + Src/stm32f4xx_it.c->Inc/main.h - - + + - + Src/stm32f4xx_it.c->Inc/stm32f4xx_it.h - - + + - + Src/stm32f4xx_it.c->Inc/usart.h - - + + - + Src/system_stm32f4xx.c - -Src/system_stm32f4xx.c + +Src/system_stm32f4xx.c - + Src/system_stm32f4xx.c->Drivers/CMSIS/Device/ST/STM32F4xx/Include/stm32f4xx.h - - + + - + Src/tim.c - -Src/tim.c + +Src/tim.c - + Src/tim.c->Inc/tim.h - - + + - + Src/usart.c - -Src/usart.c + +Src/usart.c - + Src/usart.c->Inc/usart.h - - + + - + simulator/SDL2/SDL2-2.32.10/include/SDL.h - -simulator/SDL2/SDL2-2.32.10/include/SDL.h + +simulator/SDL2/SDL2-2.32.10/include/SDL.h - + simulator/dbg_log.h - -simulator/dbg_log.h + +simulator/dbg_log.h - + simulator/dbg_log.h->外部/系统头文件 - - + + - + simulator/lv_conf.h - -simulator/lv_conf.h + +simulator/lv_conf.h - + simulator/main.c - -simulator/main.c + +simulator/main.c - + simulator/main.c->App/cfg/cfg_file.h - - + + - + simulator/main.c->App/cfg/cfg_file_store.h - - + + - + simulator/main.c->App/log/sys_log.h - - + + - + simulator/main.c->Drivers/BSP/NETCFG/net_cfg.h - - + + - + simulator/main.c->Drivers/BSP/UI/ui_menu.h - - + + - + simulator/main.c->Drivers/BSP/UI/ui_net_sink.h - - + + - + simulator/main.c->Drivers/BSP/UI/ui_standby.h - - + + - + simulator/main.c->Lib/lvgl/lvgl.h - - + + - + simulator/main.c->simulator/SDL2/SDL2-2.32.10/include/SDL.h - - + + - + simulator/sim/sim_port.h - -simulator/sim/sim_port.h + +simulator/sim/sim_port.h - + simulator/main.c->simulator/sim/sim_port.h - - + + - + simulator/main.c->外部/系统头文件 - - + + - + simulator/sim/cfg_file_store_sim.c - -simulator/sim/cfg_file_store_sim.c + +simulator/sim/cfg_file_store_sim.c - + simulator/sim/cfg_file_store_sim.c->App/cfg/cfg_file_store.h - - + + - + simulator/sim/cfg_file_store_sim.c->外部/系统头文件 - - + + - + simulator/sim/net_socket_sim.c - -simulator/sim/net_socket_sim.c + +simulator/sim/net_socket_sim.c - + simulator/sim/net_socket_sim.c->Drivers/BSP/NET/net_socket.h - - + + - + simulator/sim/net_socket_sim.c->外部/系统头文件 - - + + - + simulator/sim/sim_latency_stub.c - -simulator/sim/sim_latency_stub.c + +simulator/sim/sim_latency_stub.c - + simulator/sim/sim_latency_stub.c->test/board/net_latency_test.h - - + + - + simulator/sim/sim_port.c - -simulator/sim/sim_port.c + +simulator/sim/sim_port.c - + simulator/sim/sim_port.c->Lib/lvgl/lvgl.h - - + + - + simulator/sim/sim_port.c->simulator/SDL2/SDL2-2.32.10/include/SDL.h - - + + - + simulator/sim/sim_port.c->simulator/sim/sim_port.h - - + + - + simulator/sim/sim_port.h->外部/系统头文件 - - + + - + simulator/sim/sim_syslog.c - -simulator/sim/sim_syslog.c + +simulator/sim/sim_syslog.c - + simulator/sim/sim_syslog.c->App/log/sys_log.h - - + + - + simulator/sim/sim_syslog.c->外部/系统头文件 - - + + - + simulator/sim/ui_net_sink_backend_sim.c - -simulator/sim/ui_net_sink_backend_sim.c + +simulator/sim/ui_net_sink_backend_sim.c - + simulator/sim/ui_net_sink_backend_sim.c->Drivers/BSP/UI/ui_net_sink_backend.h - - + + - + simulator/sim/ui_net_sink_backend_sim.c->Drivers/BSP/UI/ui_port.h - - + + - + simulator/sim/ui_net_sink_backend_sim.c->Lib/lvgl/lvgl.h - - + + - + simulator/sim/ui_net_sink_backend_sim.c->simulator/sim/sim_port.h - - + + - + test/board/ch395f_reuse_bench.c - -test/board/ch395f_reuse_bench.c + +test/board/ch395f_reuse_bench.c - + test/board/ch395f_reuse_bench.c->Drivers/BSP/CH395F/ch395f.h - - + + - + test/board/ch395f_reuse_bench.c->Drivers/BSP/dbg_log.h - - + + - + test/board/ch395f_reuse_bench.c->Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal.h - - + + + + + +test/board/ch395f_reuse_bench.c->Inc/main.h + + - + test/board/ch395f_reuse_bench.h - -test/board/ch395f_reuse_bench.h + +test/board/ch395f_reuse_bench.h - + test/board/ch395f_reuse_bench.c->test/board/ch395f_reuse_bench.h - - + + - + test/board/test_common.h - -test/board/test_common.h + +test/board/test_common.h - + test/board/ch395f_test.h->test/board/test_common.h - - + + - + test/board/ch395f_test.h->外部/系统头文件 - - + + - + test/board/ch395f_test_task.c - -test/board/ch395f_test_task.c + +test/board/ch395f_test_task.c - + test/board/ch395f_test_task.c->App/app/sys_events.h - - + + - + test/board/ch395f_test_task.c->App/task/net_task.h - - + + - + test/board/ch395f_test_task.c->Drivers/BSP/CH395F/ch395f.h - - + + - + test/board/ch395f_test_task.c->Drivers/BSP/NET/net_socket.h - - + + - + test/board/ch395f_test_task.c->Drivers/BSP/dbg_log.h - - + + - + test/board/ch395f_test_task.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - + + - + test/board/ch395f_test_task.c->test/board/ch395f_test.h - - + + - + test/board/ch395f_test_task.c->test/board/ch395f_test_task.h - - + + - + test/board/gd5f_test.h - -test/board/gd5f_test.h + +test/board/gd5f_test.h - + test/board/ch395f_test_task.c->test/board/gd5f_test.h - - + + - + test/board/ch395f_test_task.c->test/board/net_latency_test.h - - + + - + test/board/net_test_task.h - -test/board/net_test_task.h + +test/board/net_test_task.h - + test/board/ch395f_test_task.c->test/board/net_test_task.h - - + + - + test/board/sd2506_test.h - -test/board/sd2506_test.h + +test/board/sd2506_test.h - + test/board/ch395f_test_task.c->test/board/sd2506_test.h - - + + - + test/board/storage_test.h - -test/board/storage_test.h + +test/board/storage_test.h - + test/board/ch395f_test_task.c->test/board/storage_test.h - - + + - + test/board/ch395f_test_task.c->test/board/test_config.h - - + + - + test/board/ch395f_test_task.c->外部/系统头文件 - - + + - + test/board/gd5f_test.h->Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h - - + + - + test/board/gd5f_test.h->test/board/test_common.h - - + + - + test/board/gd5f_test.h->外部/系统头文件 - - + + - + test/board/gd5f_test_task.c - -test/board/gd5f_test_task.c + +test/board/gd5f_test_task.c - + test/board/gd5f_test_task.c->Drivers/BSP/GD5F2GQ5UE/nand_ftl.h - - + + - + test/board/gd5f_test_task.c->Drivers/BSP/dbg_log.h - - + + - + test/board/gd5f_test_task.c->Lib/FatFs/ff.h - - + + - + test/board/gd5f_test_task.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - - + + - + test/board/gd5f_test_task.c->test/board/gd5f_test.h - - + + - + test/board/gd5f_test_task.c->test/board/test_config.h - - + + - + test/board/gd5f_test_task.c->外部/系统头文件 - - + + - + test/board/net_latency_test.c - -test/board/net_latency_test.c + +test/board/net_latency_test.c - + test/board/net_latency_test.c->Drivers/BSP/CH395F/ch395f.h - - + + - + test/board/net_latency_test.c->Drivers/BSP/NET/net_socket.h - - + + - + test/board/net_latency_test.c->Drivers/BSP/dbg_log.h - - + + - + test/board/net_latency_test.c->Inc/main.h - - + + - + test/board/net_latency_test.c->test/board/ch395f_test.h - - + + - + test/board/net_latency_test.c->test/board/net_latency_test.h - - + + - + test/board/net_latency_test.c->test/board/test_config.h - - + + - + test/board/net_latency_test.c->外部/系统头文件 - - + + - + test/board/net_latency_test.h->外部/系统头文件 - - + + - + test/board/net_test_task.c - -test/board/net_test_task.c + +test/board/net_test_task.c - + test/board/net_test_task.c->Drivers/BSP/NET/net_select.h - - + + - + test/board/net_test_task.c->Drivers/BSP/NET/net_socket.h - - + + - + test/board/net_test_task.c->Drivers/BSP/NET/net_types.h - - + + - + test/board/net_test_task.c->Drivers/BSP/dbg_log.h - - + + - + test/board/net_test_task.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - + + - + test/board/net_test_task.c->test/board/ch395f_test.h - - + + - + test/board/net_test_task.c->test/board/ch395f_test_task.h - - + + - + test/board/net_test_task.c->test/board/net_latency_test.h - - + + - + test/board/net_test_task.c->test/board/net_test_task.h - - + + - + test/board/net_test_task.c->外部/系统头文件 - - + + - + test/board/sd2506_test.h->test/board/test_common.h - - + + - + test/board/sd2506_test.h->外部/系统头文件 - - + + - + test/board/sd2506_test_task.c - -test/board/sd2506_test_task.c + +test/board/sd2506_test_task.c - + test/board/sd2506_test_task.c->Drivers/BSP/SD2506/sd2506.h - - + + - + test/board/sd2506_test_task.c->Drivers/BSP/dbg_log.h - - + + - + test/board/sd2506_test_task.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - + + - + test/board/sd2506_test_task.c->test/board/sd2506_test.h - - + + - + test/board/sd2506_test_task.c->test/board/test_config.h - - + + - + test/board/sd2506_test_task.c->外部/系统头文件 - - + + - + test/board/storage_test.h->test/board/test_common.h - - + + - + test/board/storage_test.h->外部/系统头文件 - - + + - + test/board/storage_test_task.c - -test/board/storage_test_task.c + +test/board/storage_test_task.c - + test/board/storage_test_task.c->Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h - - + + - + test/board/storage_test_task.c->Drivers/BSP/GD5F2GQ5UE/nand_ftl.h - - + + - + test/board/storage_test_task.c->Drivers/BSP/dbg_log.h - - + + - + test/board/storage_test_task.c->Inc/main.h - - + + - + test/board/storage_test_task.c->Lib/FatFs/diskio.h - - + + - + test/board/storage_test_task.c->Lib/FatFs/ff.h - - + + - + test/board/storage_test_task.c->test/board/storage_test.h - - + + - + test/board/storage_test_task.c->test/board/test_config.h - - + + - + test/board/storage_test_task.c->外部/系统头文件 - - + + - + test/board/test_common.h->外部/系统头文件 - - + + - + test/board/test_config.c - -test/board/test_config.c + +test/board/test_config.c - + test/board/test_config.c->Drivers/BSP/dbg_log.h - - + + - + test/board/test_config.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - + + - + test/board/test_config.c->test/board/ch395f_test_task.h - - + + - + test/board/test_config.c->test/board/gd5f_test.h - - + + - + test/board/test_config.c->test/board/sd2506_test.h - - + + - + test/board/test_config.c->test/board/storage_test.h - - + + - + test/board/test_config.c->test/board/test_config.h - - + + - + test/board/test_config.h->test/board/ch395f_test.h - - + + - + test/board/test_config.h->test/board/gd5f_test.h - - + + - + test/board/test_config.h->test/board/sd2506_test.h - - + + - + test/board/test_config.h->test/board/storage_test.h - - + + - + test/board/test_config.h->外部/系统头文件 - - + + - + test/host/adc_health/adc_health_test.c - -test/host/adc_health/adc_health_test.c + +test/host/adc_health/adc_health_test.c - + test/host/adc_health/adc_health_test.c->App/algo/adc_health.c - - + + - + test/host/adc_health/adc_health_test.c->外部/系统头文件 - - + + - + test/host/adc_input/adc_input_test.c - -test/host/adc_input/adc_input_test.c + +test/host/adc_input/adc_input_test.c - + test/host/adc_input/adc_input_test.c->App/algo/adc_input.c - - + + - + test/host/adc_input/adc_input_test.c->App/algo/adc_input.h - - + + - + test/host/adc_input/adc_input_test.c->Drivers/BSP/TPAFE5160/tpafe5160.h - - + + - + test/host/adc_input/adc_input_test.c->Inc/adc.h - - + + - + test/host/adc_input/adc_input_test.c->外部/系统头文件 - - + + - + test/host/adc_input/stub/adc.h - -test/host/adc_input/stub/adc.h + +test/host/adc_input/stub/adc.h - + test/host/adc_input/stub/adc.h->外部/系统头文件 - - + + - + test/host/adc_input/stub/tpafe5160.h - -test/host/adc_input/stub/tpafe5160.h + +test/host/adc_input/stub/tpafe5160.h - + test/host/adc_input/stub/tpafe5160.h->外部/系统头文件 - - + + - + test/host/cfg_file/cfg_file_test.c - -test/host/cfg_file/cfg_file_test.c + +test/host/cfg_file/cfg_file_test.c - + test/host/cfg_file/cfg_file_test.c->App/cfg/cfg_file.c - - + + - + test/host/cfg_file/cfg_file_test.c->App/cfg/cfg_file_store.h - - + + - + test/host/cfg_file/cfg_file_test.c->App/log/sys_log.h - - + + - + test/host/cfg_file/cfg_file_test.c->Drivers/BSP/dbg_log.h - - + + - + test/host/cfg_file/cfg_file_test.c->外部/系统头文件 - - + + - + test/host/cfg_file/stub/dbg_log.h - -test/host/cfg_file/stub/dbg_log.h + +test/host/cfg_file/stub/dbg_log.h - + test/host/cfg_file/stub/dbg_log.h->外部/系统头文件 - - + + + + + +test/host/freq/freq_test.c + +test/host/freq/freq_test.c + + + +test/host/freq/freq_test.c->App/algo/freq.c + + + + + +test/host/freq/freq_test.c->App/algo/phasor.c + + + + + +test/host/freq/freq_test.c->App/algo/sample_ring.c + + + + + +test/host/freq/freq_test.c->外部/系统头文件 + + - + test/host/ftp_auth/ftp_auth_test.c - -test/host/ftp_auth/ftp_auth_test.c + +test/host/ftp_auth/ftp_auth_test.c - + test/host/ftp_auth/ftp_auth_test.c->App/util/sha256.c - - + + - + test/host/ftp_auth/ftp_auth_test.c->Drivers/BSP/NET/lftpd/lftpd_auth.c - - + + - + test/host/ftp_auth/ftp_auth_test.c->外部/系统头文件 - - + + - + test/host/iec104/iec104_test.c - -test/host/iec104/iec104_test.c + +test/host/iec104/iec104_test.c + + + +test/host/iec104/iec104_test.c->App/algo/freq.c + + - + test/host/iec104/iec104_test.c->App/algo/phasor.c - - + + - + test/host/iec104/iec104_test.c->App/algo/prot_elm.c - - + + - + test/host/iec104/iec104_test.c->App/algo/prot_set.c - - + + - + test/host/iec104/iec104_test.c->App/algo/sample_ring.c - - + + - + test/host/iec104/iec104_test.c->App/protocol/iec104/iec104_asdu.c - - + + - + test/host/iec104/iec104_test.c->App/protocol/iec104/iec104_link.c - - + + - + test/host/iec104/iec104_test.c->App/protocol/iec104/iec104_pts.c - - + + - + test/host/iec104/iec104_test.c->外部/系统头文件 - - + + - + test/host/phasor/phasor_test.c - -test/host/phasor/phasor_test.c + +test/host/phasor/phasor_test.c - + test/host/phasor/phasor_test.c->App/algo/phasor.c - - + + - + test/host/phasor/phasor_test.c->App/algo/sample_ring.c - - + + - + test/host/phasor/phasor_test.c->外部/系统头文件 - - + + - + test/host/prot_elm/prot_elm_test.c - -test/host/prot_elm/prot_elm_test.c + +test/host/prot_elm/prot_elm_test.c + + + +test/host/prot_elm/prot_elm_test.c->App/algo/freq.c + + - + test/host/prot_elm/prot_elm_test.c->App/algo/phasor.c - - + + - + test/host/prot_elm/prot_elm_test.c->App/algo/prot_elm.c - - + + - + test/host/prot_elm/prot_elm_test.c->App/algo/prot_set.c - - + + - + test/host/prot_elm/prot_elm_test.c->App/algo/sample_ring.c - - + + - + test/host/prot_elm/prot_elm_test.c->外部/系统头文件 - - + + - + test/host/prot_set/prot_set_test.c - -test/host/prot_set/prot_set_test.c + +test/host/prot_set/prot_set_test.c - + test/host/prot_set/prot_set_test.c->App/algo/prot_set.c - - + + - + test/host/prot_set/prot_set_test.c->外部/系统头文件 - - + + - + test/host/sha256/sha256_test.c - -test/host/sha256/sha256_test.c + +test/host/sha256/sha256_test.c - + test/host/sha256/sha256_test.c->App/util/sha256.c - - + + - + test/host/sha256/sha256_test.c->外部/系统头文件 - - + + + + + +test/host/soe/soe_test.c + +test/host/soe/soe_test.c + + + +test/host/soe/soe_test.c->App/log/soe_rec.c + + + + + +test/host/soe/soe_test.c->App/protocol/iec104/iec104_asdu.c + + + + + +test/host/soe/soe_test.c->外部/系统头文件 + + - + test/host/sys_clock/stub/FreeRTOS.h - -test/host/sys_clock/stub/FreeRTOS.h + +test/host/sys_clock/stub/FreeRTOS.h - + test/host/sys_clock/stub/FreeRTOS.h->外部/系统头文件 - - + + - + test/host/sys_clock/stub/dbg_log.h - -test/host/sys_clock/stub/dbg_log.h + +test/host/sys_clock/stub/dbg_log.h - + test/host/sys_clock/stub/dbg_log.h->外部/系统头文件 - - + + - + test/host/sys_clock/stub/main.h - -test/host/sys_clock/stub/main.h + +test/host/sys_clock/stub/main.h - + test/host/sys_clock/stub/main.h->外部/系统头文件 - - + + - + test/host/sys_clock/stub/task.h - -test/host/sys_clock/stub/task.h + +test/host/sys_clock/stub/task.h - + test/host/sys_clock/stub/task.h->test/host/sys_clock/stub/FreeRTOS.h - - + + - + test/host/sys_clock/sys_clock_test.c - -test/host/sys_clock/sys_clock_test.c + +test/host/sys_clock/sys_clock_test.c - + test/host/sys_clock/sys_clock_test.c->App/util/sys_clock.c - - + + - + test/host/sys_clock/sys_clock_test.c->App/util/sys_clock.h - - + + - + test/host/sys_clock/sys_clock_test.c->Drivers/BSP/SD2506/sd2506.h - - + + - + test/host/sys_clock/sys_clock_test.c->Drivers/BSP/dbg_log.h - - + + - + test/host/sys_clock/sys_clock_test.c->Inc/main.h - - + + - + test/host/sys_clock/sys_clock_test.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - - + + - + test/host/sys_clock/sys_clock_test.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h - - + + - + test/host/sys_clock/sys_clock_test.c->外部/系统头文件 - - + + - + test/host/time_sync/stub/FreeRTOS.h - -test/host/time_sync/stub/FreeRTOS.h + +test/host/time_sync/stub/FreeRTOS.h - + test/host/time_sync/stub/FreeRTOS.h->外部/系统头文件 - - + + - + test/host/time_sync/stub/cmsis_os2.h - -test/host/time_sync/stub/cmsis_os2.h + +test/host/time_sync/stub/cmsis_os2.h - + test/host/time_sync/stub/cmsis_os2.h->外部/系统头文件 - - + + - + test/host/time_sync/stub/dbg_log.h - -test/host/time_sync/stub/dbg_log.h + +test/host/time_sync/stub/dbg_log.h - + test/host/time_sync/stub/dbg_log.h->外部/系统头文件 - - + + - + test/host/time_sync/stub/net_socket.h - -test/host/time_sync/stub/net_socket.h + +test/host/time_sync/stub/net_socket.h - + test/host/time_sync/stub/net_socket.h->test/host/time_sync/stub/FreeRTOS.h - - + + - + test/host/time_sync/stub/task.h - -test/host/time_sync/stub/task.h + +test/host/time_sync/stub/task.h - + test/host/time_sync/stub/net_socket.h->test/host/time_sync/stub/task.h - - + + - + test/host/time_sync/stub/net_socket.h->外部/系统头文件 - - + + - + test/host/time_sync/stub/sys_clock.h - -test/host/time_sync/stub/sys_clock.h + +test/host/time_sync/stub/sys_clock.h - + test/host/time_sync/stub/sys_clock.h->外部/系统头文件 - - + + - + test/host/time_sync/stub/sys_events.h - -test/host/time_sync/stub/sys_events.h + +test/host/time_sync/stub/sys_events.h - + test/host/time_sync/stub/sys_events.h->test/host/time_sync/stub/cmsis_os2.h - - + + - + test/host/time_sync/stub/task.h->test/host/time_sync/stub/FreeRTOS.h - - + + - + test/host/time_sync/time_sync_test.c - -test/host/time_sync/time_sync_test.c + +test/host/time_sync/time_sync_test.c - + test/host/time_sync/time_sync_test.c->App/app/sys_events.h - - + + - + test/host/time_sync/time_sync_test.c->App/sync/time_sync.c - - + + - + test/host/time_sync/time_sync_test.c->App/util/sys_clock.h - - + + - + test/host/time_sync/time_sync_test.c->App/util/task_watchdog.h - - + + - + test/host/time_sync/time_sync_test.c->Drivers/BSP/NET/net_socket.h - - + + - + test/host/time_sync/time_sync_test.c->Middlewares/Third_Party/FreeRTOS/Source/include/FreeRTOS.h - - + + - + test/host/time_sync/time_sync_test.c->Middlewares/Third_Party/FreeRTOS/Source/include/task.h - - + + - + test/host/time_sync/time_sync_test.c->外部/系统头文件 - - + + - + test/host/vol_lock/stub/cmsis_os.h - -test/host/vol_lock/stub/cmsis_os.h + +test/host/vol_lock/stub/cmsis_os.h - + test/host/vol_lock/stub/cmsis_os.h->外部/系统头文件 - - + + - + test/host/vol_lock/vol_lock_test.c - -test/host/vol_lock/vol_lock_test.c + +test/host/vol_lock/vol_lock_test.c - + test/host/vol_lock/vol_lock_test.c->App/fs/vol_lock.c - - + + - + test/host/vol_lock/vol_lock_test.c->Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2/cmsis_os.h - - + + - + test/host/vol_lock/vol_lock_test.c->外部/系统头文件 - - + + - + test/host/watchdog/stub/dbg_log.h - -test/host/watchdog/stub/dbg_log.h + +test/host/watchdog/stub/dbg_log.h - + test/host/watchdog/stub/dbg_log.h->外部/系统头文件 - - + + - + test/host/watchdog/stub/iwdg.h - -test/host/watchdog/stub/iwdg.h + +test/host/watchdog/stub/iwdg.h - + test/host/watchdog/stub/iwdg.h->外部/系统头文件 - - + + - + test/host/watchdog/stub/main.h - -test/host/watchdog/stub/main.h + +test/host/watchdog/stub/main.h - + test/host/watchdog/stub/main.h->外部/系统头文件 - - + + - + test/host/watchdog/watchdog_test.c - -test/host/watchdog/watchdog_test.c + +test/host/watchdog/watchdog_test.c - + test/host/watchdog/watchdog_test.c->App/util/task_watchdog.c - - + + - + test/host/watchdog/watchdog_test.c->Drivers/BSP/dbg_log.h - - + + - + test/host/watchdog/watchdog_test.c->Inc/iwdg.h - - + + - + test/host/watchdog/watchdog_test.c->Inc/main.h - - + + - + test/host/watchdog/watchdog_test.c->外部/系统头文件 - - + + diff --git a/docs/images/引用关系图_模块.dot b/docs/images/引用关系图_模块.dot index e8df23f9..b11b4a95 100644 --- a/docs/images/引用关系图_模块.dot +++ b/docs/images/引用关系图_模块.dot @@ -30,7 +30,7 @@ digraph refgraph_module { "外部/系统头文件" [shape=ellipse, fillcolor="#fef3c7", color="#d97706"]; "App" -> "App/log" [color="#2563eb", penwidth=1.2, label="×2"]; "App" -> "App/util" [color="#2563eb", penwidth=1.2, label="×4"]; - "App" -> "BSP(公共)" [color="#2563eb", penwidth=1.2, label="×3"]; + "App" -> "BSP(公共)" [color="#2563eb", penwidth=1.2, label="×4"]; "App" -> "BSP/GD5F2GQ5UE" [color="#2563eb", penwidth=1.2, label="×1"]; "App" -> "BSP/NET" [color="#2563eb", penwidth=1.2, label="×1"]; "App" -> "BSP/RS485" [color="#2563eb", penwidth=1.2, label="×1"]; @@ -40,16 +40,16 @@ digraph refgraph_module { "App" -> "FreeRTOS" [color="#2563eb", penwidth=1.2, label="×3"]; "App" -> "Src/Inc (CubeMX)" [color="#2563eb", penwidth=1.2, label="×5"]; "App" -> "test" [color="#2563eb", penwidth=1.2, label="×1"]; - "App" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×24"]; - "App/log" -> "App" [color="#2563eb", penwidth=1.2, label="×2"]; - "App/log" -> "App/util" [color="#2563eb", penwidth=1.2, label="×1"]; - "App/log" -> "BSP(公共)" [color="#2563eb", penwidth=1.2, label="×1"]; - "App/log" -> "FatFs" [color="#2563eb", penwidth=1.2, label="×1"]; - "App/log" -> "FreeRTOS" [color="#2563eb", penwidth=1.2, label="×4"]; + "App" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×28"]; + "App/log" -> "App" [color="#2563eb", penwidth=1.2, label="×3"]; + "App/log" -> "App/util" [color="#2563eb", penwidth=1.2, label="×2"]; + "App/log" -> "BSP(公共)" [color="#2563eb", penwidth=1.2, label="×2"]; + "App/log" -> "FatFs" [color="#2563eb", penwidth=1.2, label="×2"]; + "App/log" -> "FreeRTOS" [color="#2563eb", penwidth=1.2, label="×5"]; "App/log" -> "Src/Inc (CubeMX)" [color="#2563eb", penwidth=1.2, label="×1"]; - "App/log" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×4"]; - "App/task" -> "App" [color="#2563eb", penwidth=1.2, label="×15"]; - "App/task" -> "App/log" [color="#2563eb", penwidth=1.2, label="×4"]; + "App/log" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×8"]; + "App/task" -> "App" [color="#2563eb", penwidth=1.2, label="×18"]; + "App/task" -> "App/log" [color="#2563eb", penwidth=1.2, label="×6"]; "App/task" -> "App/util" [color="#2563eb", penwidth=1.2, label="×12"]; "App/task" -> "BSP(公共)" [color="#2563eb", penwidth=1.2, label="×7"]; "App/task" -> "BSP/CH395F" [color="#2563eb", penwidth=1.2, label="×1"]; @@ -82,12 +82,12 @@ digraph refgraph_module { "BSP/GD5F2GQ5UE" -> "Src/Inc (CubeMX)" [color="#2563eb", penwidth=1.2, label="×1"]; "BSP/GD5F2GQ5UE" -> "dhara" [color="#2563eb", penwidth=1.2, label="×2"]; "BSP/GD5F2GQ5UE" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×4"]; - "BSP/NET" -> "BSP(公共)" [color="#2563eb", penwidth=1.2, label="×1"]; - "BSP/NET" -> "BSP/CH395F" [color="#2563eb", penwidth=1.2, label="×2"]; - "BSP/NET" -> "FreeRTOS" [color="#2563eb", penwidth=1.2, label="×8"]; - "BSP/NET" -> "Src/Inc (CubeMX)" [color="#2563eb", penwidth=1.2, label="×2"]; - "BSP/NET" -> "test" [color="#2563eb", penwidth=1.2, label="×2"]; - "BSP/NET" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×3"]; + "BSP/NET" -> "BSP(公共)" [color="#2563eb", penwidth=1.2, label="×2"]; + "BSP/NET" -> "BSP/CH395F" [color="#2563eb", penwidth=1.2, label="×3"]; + "BSP/NET" -> "FreeRTOS" [color="#2563eb", penwidth=1.2, label="×11"]; + "BSP/NET" -> "Src/Inc (CubeMX)" [color="#2563eb", penwidth=1.2, label="×3"]; + "BSP/NET" -> "test" [color="#2563eb", penwidth=1.2, label="×4"]; + "BSP/NET" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×4"]; "BSP/NETCFG" -> "App" [color="#2563eb", penwidth=1.2, label="×2"]; "BSP/NETCFG" -> "BSP(公共)" [color="#2563eb", penwidth=1.2, label="×1"]; "BSP/NETCFG" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×4"]; @@ -158,8 +158,8 @@ digraph refgraph_module { "simulator" -> "SDL2" [color="#2563eb", penwidth=1.2, label="×2"]; "simulator" -> "test" [color="#2563eb", penwidth=1.2, label="×1"]; "simulator" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×6"]; - "test" -> "App" [color="#2563eb", penwidth=1.2, label="×23"]; - "test" -> "App/log" [color="#2563eb", penwidth=1.2, label="×1"]; + "test" -> "App" [color="#2563eb", penwidth=1.2, label="×29"]; + "test" -> "App/log" [color="#2563eb", penwidth=1.2, label="×2"]; "test" -> "App/task" [color="#2563eb", penwidth=1.2, label="×1"]; "test" -> "App/util" [color="#2563eb", penwidth=1.2, label="×7"]; "test" -> "BSP(公共)" [color="#2563eb", penwidth=1.2, label="×11"]; @@ -171,7 +171,7 @@ digraph refgraph_module { "test" -> "FatFs" [color="#2563eb", penwidth=1.2, label="×3"]; "test" -> "FreeRTOS" [color="#2563eb", penwidth=1.2, label="×10"]; "test" -> "HAL" [color="#2563eb", penwidth=1.2, label="×1"]; - "test" -> "Src/Inc (CubeMX)" [color="#2563eb", penwidth=1.2, label="×6"]; + "test" -> "Src/Inc (CubeMX)" [color="#2563eb", penwidth=1.2, label="×7"]; "test" -> "lftpd" [color="#2563eb", penwidth=1.2, label="×1"]; - "test" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×41"]; + "test" -> "外部/系统头文件" [color="#94a3b8", penwidth=0.7, label="×43"]; } diff --git a/docs/images/引用关系图_模块.svg b/docs/images/引用关系图_模块.svg index f0b36343..fb4dc9bb 100644 --- a/docs/images/引用关系图_模块.svg +++ b/docs/images/引用关系图_模块.svg @@ -53,7 +53,7 @@ App->BSP(公共) -×3 +×4 @@ -183,42 +183,42 @@ App->外部/系统头文件 -×24 +×28 App/log->App -×2 +×3 App/log->App/util -×1 +×2 App/log->BSP(公共) -×1 +×2 App/log->FatFs -×1 +×2 App/log->FreeRTOS -×4 +×5 @@ -232,7 +232,7 @@ App/log->外部/系统头文件 -×4 +×8 @@ -245,14 +245,14 @@ App/task->App -×15 +×18 App/task->App/log -×4 +×6 @@ -513,42 +513,42 @@ BSP/NET->BSP(公共) -×1 +×2 BSP/NET->BSP/CH395F -×2 +×3 BSP/NET->FreeRTOS -×8 +×11 BSP/NET->Src/Inc (CubeMX) -×2 +×3 BSP/NET->test -×2 +×4 BSP/NET->外部/系统头文件 -×3 +×4 @@ -1081,14 +1081,14 @@ test->App -×23 +×29 test->App/log -×1 +×2 @@ -1172,7 +1172,7 @@ test->Src/Inc (CubeMX) -×6 +×7 @@ -1186,7 +1186,7 @@ test->外部/系统头文件 -×41 +×43 diff --git a/docs/images/网络不稳根因总览.svg b/docs/images/网络不稳根因总览.svg index 6d1ec059..90a3b380 100644 --- a/docs/images/网络不稳根因总览.svg +++ b/docs/images/网络不稳根因总览.svg @@ -4,7 +4,7 @@ 左列 = 根因(比喻 + 机制);右列 = N34 阶段 0~3 的对应拆法;环与环叠加才构成「玄学」 ① 门铃会丢 -Trap 22:丢中断 +TRAP-NET-17:丢中断 症状:UI 复连永久拒连、FTP 第 29 轮自己死掉,全程不报一个错 @@ -20,7 +20,7 @@ 拆掉 ② 门铃总闸看一眼就清零 -Trap 25 B3 +TRAP-NET-20 B3 症状:别的 Socket 的 CONNECT/DISCONNECT 从此永久失踪 @@ -107,8 +107,8 @@ 机制:芯片 close 只发 FIN;对端内核存活但应用挂死 = 回 ACK 却不起身 知道他不轰人,就别指望他轰人 - -定案写入 Trap 07:兜底靠查岗(keepalive/TIMEOUT),不靠 RST + +定案写入 TRAP-NET-07:兜底靠查岗(keepalive/TIMEOUT),不靠 RST net_drop_conn 本地拆除 + reconciler 形态③④ 认领僵尸态 @@ -151,5 +151,5 @@ 逐环拆掉 = 稳(N34 阶段 0~3 已实施) 不依赖门铃 · 唯一主人 · 永不冻结 · 100ms 对账 · 开张前排渣——台架回归全绿 -证据链:docs/问题记录/CH395F_Trap_Records.md(Trap 07/09/14/17/22/23/25) · docs/网络/N34-自愈审计/实验方案.md(E1~E13) · 修复落地见 docs/网络/N34-自愈审计/实施方案.md +证据链:docs/问题记录/CH395F_Trap_Records.md(TRAP-NET-07/-09/-12/-17/-18/-20) · docs/网络/N34-自愈审计/实验方案.md(E1~E13) · 修复落地见 docs/网络/N34-自愈审计/实施方案.md diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index d66efbfc..ca4f3730 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -36,10 +36,10 @@ | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 230,104 B (224.71 KB) | 1,048,576 B (1024 KB) | 21.9% | +| ER_IROM1 | 230,216 B (224.82 KB) | 1,048,576 B (1024 KB) | 22.0% | | RW_IRAM1 | 97,912 B (95.62 KB) | 131,072 B (128 KB) | 74.7% | | RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | -| **Flash (ROM 合计)** | **231,208 B (225.8 KB)** | 1,048,576 B (1024 KB) | **22.0%** | +| **Flash (ROM 合计)** | **231,320 B (225.9 KB)** | 1,048,576 B (1024 KB) | **22.1%** | 各区的容量与常量来源: diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index 7c131e20..4c6e618a 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -15,7 +15,7 @@ | 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) | |---|---|---|---|---| -| ER_IROM1 (Flash) | 230,104 B | 1,048,576 B | 21.9% | 参考 | +| ER_IROM1 (Flash) | 230,216 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 | diff --git a/docs/内存管理/存储栈RAM优化调研.md b/docs/内存管理/存储栈RAM优化调研.md index 5d436889..ba471be9 100644 --- a/docs/内存管理/存储栈RAM优化调研.md +++ b/docs/内存管理/存储栈RAM优化调研.md @@ -3,7 +3,7 @@ > 版本 v1.1 日期 2026-09-21 作者 AI 辅助调研 > 数据来源:`data/ram_report_20260921.md`(.map 静态占用)+ 逐行代码核验(`nand_ftl.c` / `gd5f2gq5ue.c` / `Lib/dhara` / `Lib/FatFs/ffconf.h` / `lftpd*`) > 定位:`RAM使用分析与优化方案.md` 的存储专项补充;优化项编号 P0/P1/P2,与 `docs/内存管理/RAM使用分析与优化方案.md` §6 的 S 序号在"规划"一节对齐 -> v1.1 清理(2026-09-21 同日):移除已落地的 P0-1 BBT 页缓冲收缩(−1,776B,commit `be1a30a`)与编程回读校验整体移除的实施记录(详见 GD5F Trap 05 / 错误码传播审计),以及过期逐模块基线表;仅保留未实施项与回归要求 +> v1.1 清理(2026-09-21 同日):移除已落地的 P0-1 BBT 页缓冲收缩(−1,776B,commit `be1a30a`)与编程回读校验整体移除的实施记录(详见 GD5F TRAP-GD5F-05 / 错误码传播审计),以及过期逐模块基线表;仅保留未实施项与回归要求 ## 1. 现状约束(不变,是下述全部方案的依据) diff --git a/docs/子系统设计/网络Socket监听生命周期设计说明.md b/docs/子系统设计/网络Socket监听生命周期设计说明.md index 804aa36d..36b87737 100644 --- a/docs/子系统设计/网络Socket监听生命周期设计说明.md +++ b/docs/子系统设计/网络Socket监听生命周期设计说明.md @@ -25,7 +25,7 @@ | 断开后芯片行为由 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 18/25 定案为保留排空现场)⇒ 断开后芯片必然停在 `0x05/0x00`。 +TRAP-NET-15/-20 定案为保留排空现场)⇒ 断开后芯片必然停在 `0x05/0x00`。 B10 探针看到的"孤儿芯片态"不是芯片异常,而是本仓这个配置的**直接且合法的后果**。** ⚠ 口径错误待修:`Drivers/BSP/NET/net_socket.c:3627` 与 `:3641` 的函数头注释写 @@ -60,11 +60,11 @@ 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 09 沉降窗 | +| 监听被误关 | `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 25 的 `net_lost_connect_probe`(`:1250-1289`)对这个端口**从不运行**, +→ TRAP-NET-20 的 `net_lost_connect_probe`(`:1250-1289`)对这个端口**从不运行**, 丢 CONNECT 只剩对账②。:21/:8888 因为无条件轮询 accept,探针才活着。 ### 2.4 FUN_PARA bit3=1 的必要性与安全边界(2026-10-12 评估,结论:**必须保留 0x08**) @@ -81,15 +81,15 @@ B10 探针看到的"孤儿芯片态"不是芯片异常,而是本仓这个配 - `docs/芯片手册/CH395F.md` §5.23 L572 同向:「为 0 则……主动将 Socket 状态置为关闭状态**并将相关缓冲区全部清除**; 反之则不会对 Socket 状态和相关缓冲区做任何操作,以方便外部单片机……**将残留的数据读出**。」 -实测现场(Trap 25 三次定案的判别探针):`st=0x05/0x00` 且 `recv_len=1027`—— +实测现场(TRAP-NET-20 三次定案的判别探针):`st=0x05/0x00` 且 `recv_len=1027`—— Socket 仍 OPEN、芯片 TCP 已 CLOSED、**RX 缓冲里 STOR 载荷一个不少**。这正是 bit3=1 按规格 -保留现场,不是芯片异常(`docs/问题记录/CH395F_Trap_Records.md` Trap 25「定案二」)。 +保留现场,不是芯片异常(`docs/问题记录/CH395F_Trap_Records.md` TRAP-NET-20「定案二」)。 **② bit3=0 的代价是「硬件保证的数据丢失」,且不可在软件层补救。** 本产品最典型的流量形态是「发完即 FIN」:ftplib `storbinary` 语义 = `sendall` 发完立即关 数据连接、回控制通道等 226。芯片把最后一段数据落进 RX 的同时抛出 DISCONNECT: -- bit3=1:RX 数据在,MCU 来得及读(Trap 23 的「排空 RX 再 close」修复才成立); +- bit3=1:RX 数据在,MCU 来得及读(TRAP-NET-18 的「排空 RX 再 close」修复才成立); - bit3=0:芯片**当场**关 Socket 并清缓冲 → MCU 永远读不到尾部 → 文件被截断却仍回报 226 (静默损坏)。这是 bit3=0 的**必然结果**,不是概率问题——软件再聪明也拿不回已被硬件擦掉的数据。 @@ -97,7 +97,7 @@ Socket 仍 OPEN、芯片 TCP 已 CLOSED、**RX 缓冲里 STOR 载荷一个不少 严格更差。:8888 对时包、:7003 远显推流同样是「数据 + 随即关闭」形态,一并受害。 **③ bit3=0 也并不能真正消除孤儿态。** 芯片自动关以**收到** DISCONNECT/TIM_OUT 为前提, -而 Trap 22 已实证快速复连场景芯片会**丢**这两个中断(`docs/问题记录/CH395F_Trap_Records.md` +而 TRAP-NET-17 已实证快速复连场景芯片会**丢**这两个中断(`docs/问题记录/CH395F_Trap_Records.md` B10 行)→ 中断丢了,bit3=0 同样停在已关/半关态,孤儿 Socket 照样存在。 它只是降低出现概率,代价却是上面第②条。 @@ -113,8 +113,8 @@ B10 行)→ 中断丢了,bit3=0 同样停在已关/半关态,孤儿 Socket |---|---|---| | 「会话结束」必须被**谁都能看见**,不能只在边沿那一刻 | 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)(N34 阶段 3:pump 已并入 `net_reconcile_tick` 形态③④,单一 100ms tick——不变量 II 不变,见 §10.10) | -| 关闭姿势必须正确 | Trap 14/17:陈旧 DISCONNECT 锁存秒断下一条连接 | `net_close_locked` 既有「close → 等 `CLOSED` → 排空本 Socket 中断」,是 bit3=1 下唯一正确的关闭序列 | -| 异常释放须先排空残渣再关 | Trap 23:~2KB RX 残渣被下条连接当自己的数据读走 | `net_relisten_locked` 隔离期(400 ms)持续排空 RX + 主动 close + 等 `CLOSED/TCP_CLOSED` | +| 关闭姿势必须正确 | 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` | > **命名注(N34 阶段 1.5,2026-10-13)**:上表与下文所说的「DONTWAIT」机制本体不变 > (`NET_MSG_DONTWAIT` 仍是 `net_recv_locked` 的非阻塞路径与 EOF 判据载体),但对外 @@ -139,7 +139,7 @@ B10 行)→ 中断丢了,bit3=0 同样停在已关/半关态,孤儿 Socket 本产品有 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 02/11。 + 「多个 Socket 数据互相覆盖」(:35)——即 TRAP-NET-02/-11。 且 FUN_PARA **初始化后不可改**(`docs/芯片手册/CH395F.md` §5.52 L795⑥),这是开机一次性决策,不能运行时降级。 **结论:A′ 出局;根治必须在单连接复用模型内做。** @@ -152,7 +152,7 @@ B10 行)→ 中断丢了,bit3=0 同样停在已关/半关态,孤儿 Socket 其它所有位置只**置事实**,不**做动作**。(N34 阶段 3 起该决定者 = `net_reconcile_tick` 形态③④,pump 已并入,见 §10.10) 两条不变量成立后,"谁重建""会不会错过"这两类问题在结构上消失, -对账④从"唯一救命稻草"降级为"事件丢失时的补漏"(Trap 22 实证事件会丢,故对账**必须保留**)。 +对账④从"唯一救命稻草"降级为"事件丢失时的补漏"(TRAP-NET-17 实证事件会丢,故对账**必须保留**)。 ## 5. 分阶段方案 @@ -180,7 +180,7 @@ B10 行)→ 中断丢了,bit3=0 同样停在已关/半关态,孤儿 Socket (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 23 5/5×2)。 + 回归矩阵沿用 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 建议暂缓 @@ -209,7 +209,7 @@ B10 行)→ 中断丢了,bit3=0 同样停在已关/半关态,孤儿 Socket P0 → P1 → P2 严格串行,每阶段独立提交、独立回归、可单独回滚。 P1 是性价比最高的一步:它不改变 fd 模型,却把 B10 与"消费者卡死"两类事故同时从结构上消掉。 -P2 之后,对账④保留但只作事件丢失的补漏(Trap 22 实证事件会丢,不可删)。 +P2 之后,对账④保留但只作事件丢失的补漏(TRAP-NET-17 实证事件会丢,不可删)。 ## 7. 证据附录(2026-10-12 实测与调研,供后续执行时复核) @@ -227,7 +227,7 @@ P2 之后,对账④保留但只作事件丢失的补漏(Trap 22 实证事件 - **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 22 实证事件会丢)。 +- **结论**:单连接模式下官方**零自动重监听**,且原生做法是**在事件现场当场重建**;本仓对账④是周期兜底,方向一致但不可互替(TRAP-NET-17 实证事件会丢)。 - **中断所有权**:全局寄存器整体读即清、无按位清除(`docs/芯片手册/CH395F.md` §5.15 L441);CONNECT/DISCONNECT/TIM_OUT 是**事件位**(可丢)、RECV 是**条件位**(可补)→ 任何"只靠事件重建监听"的方案天然不完备。 ### 7.3 状态机事实(`Drivers/BSP/NET/net_socket.c` 3921 行) @@ -242,7 +242,7 @@ P2 之后,对账④保留但只作事件丢失的补漏(Trap 22 实证事件 | 服务 | 认为的连接结束信号 | net 层实际给什么 | 监听重建者 | 是否留无人重建组合 | |---|---|---|---|---| | `lftpd` 控制 :21 | `read_line!=0`(EOF→-1,`lftpd_inet.c:154-156`) | 阻塞 pending 保证 CLOSED 必了结(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 09)+ 最坏 ~10 s 端口下线 | +| `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 原生 | 无需 | 无——且对上述全部失效模式**零覆盖** | @@ -251,16 +251,16 @@ P2 之后,对账④保留但只作事件丢失的补漏(Trap 22 实证事件 `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 09 沉降窗。 +(与同文件 `:134` 注释"严禁"自相矛盾)→ 靠随后 `bind_listen` 自救,落 TRAP-NET-09 沉降窗。 ## 8. 决策记录 | # | 决定 | 依据 | |---|---|---| -| D1 | **A′(启用芯片多连接模式)出局**,根治必须在单连接复用模型内做 | `docs/问题记录/CH395F_Trap_Records.md` 附 FTP 改造清单第 4 条「多连接模式不支持多监听」;本产品 3 常驻监听 + 动态 PASV;§9.2.6 L1295 池按源端口匹配会互串;历史实测 Trap 02/11;FUN_PARA 初始化后不可改(`docs/芯片手册/CH395F.md` §5.52 L795⑥) | +| 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 22 实证事件会丢 → 周期对账不可删;④ 已把 `:7003` 从 0/10 永久死拉到 10/10 | -| D4 | `FUN_PARA` **保持 `0x08`**,不因孤儿态改配置(2026-10-12 复核确认) | bit3=1 是 Trap 18/25 定案的"保留排空现场"契约;bit3=0 会令芯片在 DISCONNECT/TIM_OUT 时**当场清缓冲**(`docs/芯片手册/CH395F.md` §5.52② / §5.23 L572)→「发完即 FIN」流量的尾部数据必被硬件擦除、**不可补救**;且 bit3=0 也挡不住丢中断(Trap 22)→ 孤儿态照样有。孤儿态是 bit3=1 的合法后果,应在软件层收口而非回退契约。完整论证见 §2.4 | +| D3 | 对账④**保留**(`4f2cc8b` 不撤回),其副作用由 P1 消除 | TRAP-NET-17 实证事件会丢 → 周期对账不可删;④ 已把 `: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 | 与监听生命周期不同因,混做会让回归面失控 | @@ -290,9 +290,9 @@ DONTWAIT 在 LISTENING 下返 `WOULDBLOCK`(`:1890`)→ `ui_net_sink.c:560-56 |---|---|---| | `@test` 启动闭环 | **PASS** | 启动锚点齐、无 `[ERR]`、reset cause ×1、7.2 s 到 net ready | | `ftp_regression` | **11/11** | 含 STOR 32KB —— 「发完即 FIN」链路,`session_dead` 直接作用面 | -| Trap 23 `--check-abort`(RST) | **5/5** | 450 + 5xx 正确回报,无误报 226 | -| Trap 23 `--graceful`(FIN) | **5/5** | 226 正常 | -| `:21` 快速复连 0 ms ×50 | **50/50** | Trap 22 基线不回归 | +| 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) | @@ -314,7 +314,7 @@ DONTWAIT 在 LISTENING 下返 `WOULDBLOCK`(`:1890`)→ `ui_net_sink.c:560-56 200 ms + 去抖(`NET_RECONCILE_STABLE_N`),`ui_net_sink` 轮询 100 ms —— 正常时序下消费者总是先 recv 到 EOF,对账抢不到。**只有 uiTask 被拖住**才可能出现, 而那是设备内部状态。 -- 处置:按 Trap 25「不留未证明的复杂度」**删除该探针**;该场景登记为**残余验证项**, +- 处置:按 TRAP-NET-20「不留未证明的复杂度」**删除该探针**;该场景登记为**残余验证项**, 需设备内注入点(参照 `TEST_LATENCY_PROBE` 的编译开关模式)才能在真机复现。 - 因此 P1 的**正确性依据**是:① 代码级论证(判据由边沿改电平、置位/清零完备、 两处 relisten 补状态门);② 上表全量回归矩阵**不回归**;③ 不变量 I 的结构性论证。 @@ -345,7 +345,7 @@ RAM 预算台账 `.md`,属另一在途任务)。这两个文件在主工作 - [ ] **P2b(同步忙等改造)—— 建议暂缓**:`net_close_locked` 的 500 ms `osDelay` 循环、 `net_listen_locked` 的 3×500 ms。**实测依据**:全量历史 `task stall` 在忙等未改造时 同样存在(P1 基线同条件 53 条),忙等的危害是**理论性**的;而 `net_close` 是每次会话 - 结束都走的热路径。按 Trap 25「不留未证明的复杂度」,为一个未发生的故障去动热路径, + 结束都走的热路径。按 TRAP-NET-20「不留未证明的复杂度」,为一个未发生的故障去动热路径, 性价比存疑 —— 建议等有 `task stall` 与忙等的**因果证据**后再做。 (**N34 阶段 3 已实施 2026-10-13**:不是"为忙等而改",而是驱动 `sock_reset` 原语落地后 顺路收口——`net_sock_reset_sync` 确定性排渣替代盲等(GRACEFUL 100ms 底 / 600ms 封顶), @@ -583,5 +583,5 @@ void net_listener_tick(void); GRACEFUL 100ms 底 / 600ms 封顶),并删除 close/connect 路径的全局中断读清(F-NET-7)。 **台架回归(2026-10-13,最终构建 Code=211348)**:`:7003` 50/50、`:21` 100ms/0ms/600ms 各 50/50、 -Trap 23 RST 10/10 + FIN 5/5、Trap 25 n30 11/11、iec104 18/18、`ftp_regression` 11/11、E-02 双 PASS; +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`)。 diff --git a/docs/架构设计/STM32F4-Base存储架构说明.md b/docs/架构设计/STM32F4-Base存储架构说明.md index 9c9d25d7..7f52413e 100644 --- a/docs/架构设计/STM32F4-Base存储架构说明.md +++ b/docs/架构设计/STM32F4-Base存储架构说明.md @@ -569,7 +569,7 @@ if (nand_ftl_format() == 0) { 11. **FA_OPEN_APPEND 陷阱 / 短写** — `FA_OPEN_APPEND`(0x30) = `FA_OPEN_ALWAYS|FA_SEEKEND`,**不含 `FA_WRITE`(0x02)**——不带写位打开的文件 `f_write` 恒返回 `FR_DENIED`(7)(这是当年 `/log` 写失败的真根因);追加写必须写 `FA_WRITE | FA_OPEN_APPEND`(2026-09-20 台架实测,`Lib/FatFs` 移植版)。该移植的 `f_write` 在簇分配失败时返回 `FR_OK` + 短写(**没有错误码**)→ 判失败必须校验写回的字节数。标志位为**旧式编号**:`FA_READ`=0x01 / `FA_WRITE`=0x02 / `FA_CREATE_NEW`=0x04 / `FA_CREATE_ALWAYS`=0x08 / `FA_OPEN_ALWAYS`=0x10 / `FA_OPEN_APPEND`=0x30 / `FA_SEEKEND`=0x20(`ff.c` 内部定义),与上游 FatFs R0.14 不同,**勿照抄上游代码示例**。 12. **崩溃可观测性(勿屏蔽)** — 已开启 `configCHECK_FOR_STACK_OVERFLOW=2` + `configUSE_MALLOC_FAILED_HOOK` + HardFault 现场报告(`Src/stm32f4xx_it.c`:裸 UART 打印 PC/LR/xPSR + CFSR/HFSR/BFAR/MMFAR 后复位,对照 `.map` 定位符号)。defaultTask **1024** words、testTask **2048** words 是**实测下限,勿降**。开机日志出现 `[ERR] STACK OVERFLOW task=` / `[ERR] HARDFAULT ...` / `MALLOC FAILED` 是精确定位线索,禁止屏蔽或降级。 13. **BBT 恢复模式** — 运行期坏块会随 TC-STO-03 注入累积;当介质被**误标脏**(如 DMA 误打 CCM 栈导致随机块被写坏并持久化)时,打开 `test/board/storage_test.h` 的 `ENABLE_STORAGE_RECOVERY_TESTS` 编译**恢复构建**,testTask 会执行 `gd5f2gq5ue_bbt_rebuild()`(出厂扫描清除运行期误标)+ `nand_ftl_format()` 清账后复位。恢复后容量回到基线(即扣除 BBT 保留池后的正常容量,非出厂裸容量)。 -14. **lftpd 路径口径(归档更正 2026-10-13:原误记于 `docs/问题记录/CH395F_Trap_Records.md` Trap 12,移入本节)** — FTP STOR/CWD 曾频发 `FR_INVALID_NAME`(6):真约束即上条第 10 项的严格 8.3。原记录"FatFS 不接受 `/` 前缀 / 空字符串失败"两条与仓内 `Lib/FatFs/ff.c` 矛盾(`ff.c:3101-3109` 会剥前导分隔符、`:3127-3129` 空名返回根目录),**已删**。现行口径:`lftpd_io.c` 的 `to_fatfs_path()` 剥 `/` 前缀(行为正确,保留)+ 本仓路径用 `"0:"` 卷前缀(`FF_FS_RPATH=0` 下 `"."` 不是合法路径段)。长文件名需 `FF_USE_LFN=1` + `FF_LFN_UNICODE`(显著增 RAM,未启用)。 +14. **lftpd 路径口径(归档更正 2026-10-13:原误记于 `docs/问题记录/CH395F_Trap_Records.md`——非 CH395F 议题,移入本节)** — FTP STOR/CWD 曾频发 `FR_INVALID_NAME`(6):真约束即上条第 10 项的严格 8.3。原记录"FatFS 不接受 `/` 前缀 / 空字符串失败"两条与仓内 `Lib/FatFs/ff.c` 矛盾(`ff.c:3101-3109` 会剥前导分隔符、`:3127-3129` 空名返回根目录),**已删**。现行口径:`lftpd_io.c` 的 `to_fatfs_path()` 剥 `/` 前缀(行为正确,保留)+ 本仓路径用 `"0:"` 卷前缀(`FF_FS_RPATH=0` 下 `"."` 不是合法路径段)。长文件名需 `FF_USE_LFN=1` + `FF_LFN_UNICODE`(显著增 RAM,未启用)。 ### 10.2 FTP 服务(lftpd)鉴权语义(2026-10-11,N-19) diff --git a/docs/架构设计/项目启动顺序管理.md b/docs/架构设计/项目启动顺序管理.md index 3d1237aa..2d71b80f 100644 --- a/docs/架构设计/项目启动顺序管理.md +++ b/docs/架构设计/项目启动顺序管理.md @@ -22,7 +22,7 @@ 2. **魔法延时**:`ftpTask` 用 `osDelay(7000)` 硬等 PHY/网络稳定。 ### 1.3 改造前的痛点(即引入 Event Flags 的动机) -- **魔法延时脆弱**:PHY 协商、DHCP、首轮 `net_poll()` 的耗时随硬件/环境波动;7s 是拍脑袋定值,一旦网络就绪变慢就失败。这正是 `docs/问题记录/CH395F_Trap_Records.md` 中记录的坑(Trap 09 子类:上电 PHY 未稳定即 listen)。 +- **魔法延时脆弱**:PHY 协商、DHCP、首轮 `net_poll()` 的耗时随硬件/环境波动;7s 是拍脑袋定值,一旦网络就绪变慢就失败。这正是 `docs/问题记录/CH395F_Trap_Records.md` 中记录的坑(TRAP-NET-09 子类:上电 PHY 未稳定即 listen)。 - **轮询浪费节拍**:`start_test_task` 每 100ms 被唤醒一次去读一个全局变量,纯属空转。 - **依赖隐式且分散**:谁依赖谁、何时就绪,散落在多个任务的延时与 `if` 里,没有统一视图,新增子系统极易遗漏同步。 - **不可组合**:原全局标志只能表达"一个"条件;若某任务需"网络 **且** 存储都就绪"就无从表达。 diff --git a/docs/测试说明/CH395F_Test_Guide.md b/docs/测试说明/CH395F_Test_Guide.md index 188c2c2a..8ca59c29 100644 --- a/docs/测试说明/CH395F_Test_Guide.md +++ b/docs/测试说明/CH395F_Test_Guide.md @@ -2,7 +2,7 @@ ## 1. 概述 -本文档定义 CH395F 以太网驱动(STM32F407ZGTx,SPI2 接口)的测试规范。当前固件 `FUN_PARA=0x08`(bit3 SOCKET_CLOSE=1,bit0/bit1 均为 0):8 个 Socket 0~7 全部可用(**由 Socket 索引 `_SN` 命令族 + 缓冲显式分配实现,与 FUN_PARA 无关**,见 `问题记录/CH395F_Trap_Records.md` Trap 18「定案更正」),Socket 0 作 TCP Server 单连接,覆盖单连接与多并发客户端场景。测试按阶段(TM-PHY-NN)组织,每个阶段包含独立测试用例(TC-NET-NNN)。 +本文档定义 CH395F 以太网驱动(STM32F407ZGTx,SPI2 接口)的测试规范。当前固件 `FUN_PARA=0x08`(bit3 SOCKET_CLOSE=1,bit0/bit1 均为 0):8 个 Socket 0~7 全部可用(**由 Socket 索引 `_SN` 命令族 + 缓冲显式分配实现,与 FUN_PARA 无关**,见 `问题记录/CH395F_Trap_Records.md` TRAP-NET-15),Socket 0 作 TCP Server 单连接,覆盖单连接与多并发客户端场景。测试按阶段(TM-PHY-NN)组织,每个阶段包含独立测试用例(TC-NET-NNN)。 ### 设计原则 @@ -25,11 +25,11 @@ | **单次 SPI 最大载荷** | **4096B** | `CH395F_SPI_DMA_MAX_PAYLOAD` | | recv_len 最大值 | 65535 | `uint16_t` | -> **关键约束**:本固件每个 Socket 接收缓冲为 **2KB**(4 块 × 512B),发送缓冲为 **1KB**(2 块)。`recv_len` 在调 `read_recv_buf` 读取**部分**数据后归零(Trap 13),故每次接收应一次性读空当前可用数据(≤2KB)。`CH395F_SPI_DMA_BUF_SIZE=4100` 足以覆盖单 Socket 完整 2KB 接收缓冲。 +> **关键约束**:本固件每个 Socket 接收缓冲为 **2KB**(4 块 × 512B),发送缓冲为 **1KB**(2 块)。旧"部分读后 `recv_len` 归零"机理**未证实且与 `docs/芯片手册/CH395F.md` §5.34 相左**(2026-09-23 复核降级),但**保守按"一次读空"执行**(≤2KB)。`CH395F_SPI_DMA_BUF_SIZE=2052` 恰好覆盖单 Socket 完整 2KB 接收缓冲。 ### 多 Socket / 8 端口并发分配 -`net_init` 中 `ch395f_set_fun_para(0x08)` 之后,循环 0~7 调用 `SET_RECV_BUF` / `SET_SEND_BUF` 重分配(**这一步才是 Socket 1~7 可用的前提**,见 `问题记录/CH395F_Trap_Records.md` Trap 18「定案更正」;必须在 `OPEN_SOCKET` 之前,顺序遵循 Trap 01:先数据 Socket 1~7 再监听 Socket 0): +`net_init` 中 `ch395f_set_fun_para(0x08)` 之后,循环 0~7 调用 `SET_RECV_BUF` / `SET_SEND_BUF` 重分配(**这一步才是 Socket 1~7 可用的前提**,见 `问题记录/CH395F_Trap_Records.md` TRAP-NET-15;必须在 `OPEN_SOCKET` 之前,顺序遵循 TRAP-NET-01:先数据 Socket 1~7 再监听 Socket 0): | Socket | 接收起始块 | 接收块数 | 接收大小 | 发送起始块 | 发送块数 | 发送大小 | |--------|-----------|---------|---------|-----------|---------|---------| @@ -66,7 +66,7 @@ UDP 路径 `net_socket()` 中对 Socket 4~7 的兜底重设使用同一布局。 **边界条件**: - 发送 `≤1KB`:一次写发送缓冲完成(受发送缓冲 1KB 限制) -- 接收缓冲满(2KB):必须一次性读空,否则 `recv_len` 归零导致剩余数据丢失(Trap 13) +- 接收缓冲满(2KB):保守按一次性读空执行(旧"归零"机理未证实,见 `CH395F_Trap_Records.md` 台架纪律) - 发送 `0B`:`net_send` 返回 `-1`(`NET_ERR_INVAL`) > **注**:当前 `net_send` 的分块上限常量 `NET_DMA_MAX_PAYLOAD=4096` 大于单 Socket 发送缓冲(1KB),单次 `WRITE_SEND_BUF` 写入量受发送缓冲大小约束;大文件传输(Phase 6/8/10)需在硬件上实测确认无溢出。 @@ -227,7 +227,7 @@ MCU: STM32F407ZGTx @ 168MHz, Keil MDK-ARM v5 (ARMCC) --- -### TC-NET-204(硬件层): TCP 关闭次序 A/B/C 对照(Trap 07 定案用例) +### TC-NET-204(硬件层): TCP 关闭次序 A/B/C 对照(TRAP-NET-07 定案用例) | 字段 | 值 | |------|-----| @@ -243,12 +243,12 @@ MCU: STM32F407ZGTx @ 168MHz, Keil MDK-ARM v5 (ARMCC) | 模式 | 关闭序列 | 成功标准 | 实测(2026-10-11,5/5) | |----|------|----------|----------| | A | `ch395f_close_socket()` | 观察窗 10 s 内 `GET_SOCKET_STATUS` **byte0** 回到 `0x00` | **0 / 0 / 150 / 0 / 0 ms**;过渡态可见 `TIME_WAIT`(`0x0A`),**无 RST 语义** | -| B | `ch395f_tcp_disconnect()` → `ch395f_close_socket()` | 同上 | **全部 +0 ms**,两字节均 `0x00` → Trap 07 的"卡 `FIN_WAIT_2` 数分钟"**不成立** | +| B | `ch395f_tcp_disconnect()` → `ch395f_close_socket()` | 同上 | **全部 +0 ms**,两字节均 `0x00` → TRAP-NET-07 的"卡 `FIN_WAIT_2` 数分钟"**不成立** | | C | `ch395f_tcp_disconnect()`(**不** close) | **预期相反**:10 s 内 byte0 保持 `0x05`(`docs/芯片手册/CH395F.md` §5.52 ② bit3=1 芯片不自动关) | byte0 恒 `0x05`、byte1 `0x00`;直接 `open_socket` → **`0x20` FAIL**(5/5,即历史 `Phase 2 open_socket: 0x20` 签名);补 `close` 后 +0 ms 回 CLOSED | **判据口径**:`GET_SOCKET_STATUS_SN` 输出 2 字节——byte0 只有 `00H/05H`,byte1 才是 TCP 态且**仅在 byte0=OPEN 时有意义**(`docs/芯片手册/CH395F.md` §5.22 + 官方 SDK 注释);关闭与重开之间**不主动清中断**,残留事件由重开后首次中断读取现场暴露(本轮 15 轮均未观察到残留 `DISCONNECT`)。 -**通过标准**:45/45。任一模式偏离上表 → 说明芯片关闭语义或 `FUN_PARA` 配置变了,须回查 `问题记录/CH395F_Trap_Records.md` Trap 07「定案更正」。 +**通过标准**:45/45。任一模式偏离上表 → 说明芯片关闭语义或 `FUN_PARA` 配置变了,须回查 `问题记录/CH395F_Trap_Records.md` TRAP-NET-07。 **覆盖原则**:DP-03(状态机边界:关闭次序 × Socket 复用) @@ -265,7 +265,7 @@ MCU: STM32F407ZGTx @ 168MHz, Keil MDK-ARM v5 (ARMCC) **分层说明**:本阶段属于**硬件层模式**(netTask 自挂起),仅用 `ch395f_*` 裸命令验证芯片 UDP 能力:UDP Socket 打开、数据报接收、**包头源地址解析**与按源地址动态回显。 -> CH395F UDP 接收帧格式(`ch395f_read_recv_buf` 返回):[0-1] 保留|[2-3] 源端口(大端)|[4-7] 源 IP|[8+] 载荷。回显前须以此源地址重设目标,且每次发送后等 `SENDBUF_FREE` 确认(Trap 04)。 +> CH395F UDP 接收帧格式(`ch395f_read_recv_buf` 返回):[0-1] 保留|[2-3] 源端口(大端)|[4-7] 源 IP|[8+] 载荷。回显前须以此源地址重设目标,且每次发送后等 `SENDBUF_FREE` 确认(TRAP-NET-04)。 --- @@ -339,7 +339,7 @@ CH395F PC (dhcp_server) **关键实现约束**: -- **xid/chaddr 回填**:OFFER/ACK 必须原样回传 DISCOVER 的 `xid([4:8])` 与 `chaddr([28:34])`(Trap 03:早期 xid 偏移错误导致芯片丢弃响应) +- **xid/chaddr 回填**:OFFER/ACK 必须原样回传 DISCOVER 的 `xid([4:8])` 与 `chaddr([28:34])`(TRAP-NET-03:早期 xid 偏移错误导致芯片丢弃响应) - **option53 位置**:报文类型在 `data[242]`(236B 固定头 + magic `63825363` + option 偏移),`len<=250` 视为无效包 - **双广播发送**:OFFER/ACK 同时发 `255.255.255.255` 与 `192.168.1.255`(规避 Windows 定向广播路由限制) - **REQUEST 重试**:PC 最多等 8×5s;期间收到重复 DISCOVER 则重发 OFFER @@ -350,7 +350,7 @@ CH395F PC (dhcp_server) |----|------|------| | 1 | `ch395f_set_dhcp(1)` 启动协商 | - | | 2 | 轮询 `GET_DHCP_STATUS == 0x00` | 超时 30s,间隔 500ms(循环中同步读清 GINT_ALL) | -| 3 | `GET_IP_INF` 轮询至连续 3 次一致(间隔 300ms,上限 8s)后打印 | 连续一致且非 `0.0.0.0`(滞后窗口实测 ~1 s,见 Trap 15) | +| 3 | `GET_IP_INF` 轮询至连续 3 次一致(间隔 300ms,上限 8s)后打印 | 连续一致且非 `0.0.0.0`(滞后窗口实测 ~1 s,见 TRAP-NET-13) | | 4 | Socket 6(UDP,本地 :60000,send/recv buf 各 512B)向 `255.255.255.255:60001` 广播 4B 本机 IP 宣告 | SENBUF_FREE 等待 2s | | 5 | 同 Socket 收 PC `"HELLO"`(5B),按包头源地址回显一次后关闭 | 窗口 30s(`PHASE4_HELLO_WAIT_MS`) | @@ -415,7 +415,7 @@ CH395F PC (dhcp_server) | **出口** | 串口 "Phase 5: N/N PASSED (failed=0)" + 吞吐统计 | | **注意** | 与 NET 层阶段 7 同主题不同层:本阶段用 `ch395f_*` 裸命令直驱芯片,不经 net_socket/netTask | -> 编号说明:阶段 5 用例 **501/502**(顺序双测向完整性 + 变总量合规);原 PHY 测试(旧编号 901/902)因板载拓扑不可测已删除(见 Trap 05)。 +> 编号说明:阶段 5 用例 **501/502**(顺序双测向完整性 + 变总量合规);原 PHY 测试(旧编号 901/902)因板载拓扑不可测已删除(见 TRAP-NET-05)。 ### 为什么 2KB 接收缓冲不会丢数据(TCP 流控原理) @@ -663,17 +663,17 @@ sequenceDiagram | 步 | 操作 | 成功标准 | 失败标准 | |----|------|----------|----------| -| 1 | PC 发单包 4096B(实际单 Socket 接收缓冲为 2KB,固件分两次 `net_recv` 取空) | `net_recv` 累计收满 4096B(分次) | 接收字节数 ≠ 4096(丢失,见 TRAP-13) | +| 1 | PC 发单包 4096B(实际单 Socket 接收缓冲为 2KB,固件分两次 `net_recv` 取空) | `net_recv` 累计收满 4096B(分次) | 接收字节数 ≠ 4096(丢失,见台架纪律注) | | 2 | 固件收 4096B → 原样回显(`net_send` 分块) | PC 收到 4096B 且 `data == test_data` 一致 | 回显错乱 / PC 校验不符 | | 3 | PC 校验 | 4096B 完全匹配 | 任意字节不符 | **通过标准**:4096B 完全匹配。任一失败 → 标记 FAIL **覆盖原则**:DP-04(2KB 接收缓冲边界 + 单次 DMA 上限) | -> **2026-09-23 结案**:TC-NET-604 的"缓冲满回显"路径**判定为非缺陷**——真实生产路径(产品固件 + 真 lftpd FTP 上传,判据 = FTP 协议结果 + **上传后 RETR 回读 SHA-256**)实测 **227 轮全过**(8KB~1MB,含零间隔快速重连与 100 轮浸泡),并发生产 UI 流(`:7003`)600 帧 CRC 全过;原台架"停摆"系**测试程序自吞数据 + 判读工具误判**(详见 `CH395F_Trap_Records.md` Trap 21"教训"节)。 -> - **本用例判据(不变)**:PC 发 4096B → 固件分次取空并回显 → **PC 侧内容完全匹配**即 PASS;上传/中止类真实缺陷见 **Trap 23**(回归工具 `MDK-ARM/ftp_abort_probe.py`)。 +> **2026-09-23 结案**:TC-NET-604 的"缓冲满回显"路径**判定为非缺陷**——真实生产路径(产品固件 + 真 lftpd FTP 上传,判据 = FTP 协议结果 + **上传后 RETR 回读 SHA-256**)实测 **227 轮全过**(8KB~1MB,含零间隔快速重连与 100 轮浸泡),并发生产 UI 流(`:7003`)600 帧 CRC 全过;原台架"停摆"系**测试程序自吞数据 + 判读工具误判**(详见 `CH395F_Trap_Records.md`「台架纪律」节)。 +> - **本用例判据(不变)**:PC 发 4096B → 固件分次取空并回显 → **PC 侧内容完全匹配**即 PASS;上传/中止类真实缺陷见 **TRAP-NET-18**(回归工具 `test/host/ftp_abort_probe.py`)。 > - **台架纪律**:判读一律用 `MDK-ARM/bench_verdict.py`(脚本层 STEP 行为真值,并扣除固件读者吞噬的字节);`probe_classify.py` 的 A/C 判定**不可信**;**禁止**把"固件内非回显读者"(Phase 9/11、固件探针)与固定期望值混用在同一配方里。 -> - **Phase 6-only 构建禁止无间隔连跑**:<~1s 重连间隔会触发 Trap 22 失同步(自 run2 起:无 standalone accept、`send -1`、其后全部 connection refused)——快速重连场景须留 ≥15s 间隔;带 Phase 9/11 的构建会被其读者吞数据,不能当对照。 +> - **Phase 6-only 构建禁止无间隔连跑**:<~1s 重连间隔会触发 TRAP-NET-17 失同步(自 run2 起:无 standalone accept、`send -1`、其后全部 connection refused)——快速重连场景须留 ≥15s 间隔;带 Phase 9/11 的构建会被其读者吞数据,不能当对照。 ### TC-NET-605: 断开重连 @@ -875,9 +875,9 @@ sequenceDiagram **覆盖原则**:DP-01(多 Socket 并发主路径)、缓冲架构(8×6 块分配,MSS=1024) -> **注**:PC 端 `--max-conn` 须为**并发**计数(连接关闭即减),否则 Phase 7 全量(701+702+704 共 12 连接)会按累计计数超限而拒掉 4~7。该用例已随 Phase 7 实测通过(并发回显落在 sock2/4/5/6/7,见 Trap 18 末)。 +> **注**:PC 端 `--max-conn` 须为**并发**计数(连接关闭即减),否则 Phase 7 全量(701+702+704 共 12 连接)会按累计计数超限而拒掉 4~7。该用例已随 Phase 7 实测通过(并发回显落在 sock2/4/5/6/7,见 TRAP-NET-15 末)。 > -> **2026-10-11 复跑(N-31,`FUN_PARA=0x08`)**:**Phase 7 34/34 全通过**,5 路并发 ESTABLISHED + 64 B 回显字节级比对 OK,芯片 Socket 实测落在 **sock2/4/5/6/7**(0/1/3 被 FTP/对时/远显三个监听占用)→ 证明 Socket 可用性不依赖 FUN_PARA。两条**前置排雷**(否则会得到全量假 FAIL):① PC 适配器落在 **Public** 防火墙配置文件会静默丢入站 8081,症状是 TC701/702/704 全部 `CONNECT TIMEOUT`;② 残留的旧 `tcp_server` 进程占着 8081,新服务器绑不上端口不报错。另:`PHASE7_MULTI_COUNT` 须 ≤5(E-23 未修,`NET_MAX_SOCKETS=8` 减去三个常驻监听)。证据链见 `问题记录/CH395F_Trap_Records.md` Trap 18「定案更正」。 +> **2026-10-11 复跑(N-31,`FUN_PARA=0x08`)**:**Phase 7 34/34 全通过**,5 路并发 ESTABLISHED + 64 B 回显字节级比对 OK,芯片 Socket 实测落在 **sock2/4/5/6/7**(0/1/3 被 FTP/对时/远显三个监听占用)→ 证明 Socket 可用性不依赖 FUN_PARA。两条**前置排雷**(否则会得到全量假 FAIL):① PC 适配器落在 **Public** 防火墙配置文件会静默丢入站 8081,症状是 TC701/702/704 全部 `CONNECT TIMEOUT`;② 残留的旧 `tcp_server` 进程占着 8081,新服务器绑不上端口不报错。另:`PHASE7_MULTI_COUNT` 须 ≤5(E-23 未修,`NET_MAX_SOCKETS=8` 减去三个常驻监听)。证据链见 `问题记录/CH395F_Trap_Records.md` TRAP-NET-15。 --- @@ -913,7 +913,7 @@ sequenceDiagram | **类型** | 功能测试(文件发送) | | **标题** | MCU 流式发送 100KB 至 PC 并落盘校验 | | **前置条件** | 1. PC: `python ./test/host/ch395f_socket_test.py tcp_file --ip 192.168.1.100 --port 8080 --op recv --file rx_100k.bin --size 102400`(先连接等待数据)
2. `ENABLE_PHASE8_TESTS`,`PHASE8_FILE_SIZE=102400`,`PHASE8_DIRECTION=SEND`
3. `net_init` 完成,MSS=1024 | -| **测试步骤** | 1. MCU `net_accept` 建链(PC 已连接)
2. 循环填充一个滑动窗口(≤ s_tx_buf)并调用 `net_send`;`net_send` 内部按硬件发送 FIFO(`NET_SEND_CHUNK_MAX`=1KB,见 Trap 20)自动拆分写入,直至 100KB 发完
3. PC 接收全部字节写入 `rx_100k.bin`,按递增模式或预置 CRC32 校验 | +| **测试步骤** | 1. MCU `net_accept` 建链(PC 已连接)
2. 循环填充一个滑动窗口(≤ s_tx_buf)并调用 `net_send`;`net_send` 内部按硬件发送 FIFO(`NET_SEND_CHUNK_MAX`=1KB,见 TRAP-NET-16)自动拆分写入,直至 100KB 发完
3. PC 接收全部字节写入 `rx_100k.bin`,按递增模式或预置 CRC32 校验 | | **通过标准** | PC 收满 102400B 且逐字节/CRC32 校验一致,MCU 无发送错误 | | **覆盖原则** | DP-04(分块发送、DMA 边界)、缓冲架构(1KB 发送缓冲) | | **实测** | ✅ PASS(2026-08-26):固件 `PHASE8_DIRECTION=SEND`,PC `tcp_file --op recv` 收满 102400B,递增模式逐字节校验通过,CRC32=0x9A0E0C8C,耗时 1209ms / 82.7 KB/s,落盘 `rx_100k.bin` 成功。 | @@ -927,9 +927,9 @@ sequenceDiagram | **类型** | 功能测试(文件接收) | | **标题** | PC 流式发送 100KB 文件,MCU 接收并校验 | | **前置条件** | 1. PC: `python ./test/host/ch395f_socket_test.py tcp_file --ip 192.168.1.100 --port 8080 --op send --file tx_100k.bin --size 102400`(先连接并发送;无文件则生成 100KB 递增模式)
2. 同 TC-NET-801(`PHASE8_DIRECTION=RECV`) | -| **测试步骤** | 1. MCU `net_accept` 建链(PC 已连接)
2. 循环 `net_recv`(≤2KB/次,Trap 13 一次读空)→ 按 offset 字节校验 + 累计 CRC32,直至收满 100KB
3. PC 在文件末尾附 4 字节 CRC32(小端);MCU 收齐后比对 CRC32 | +| **测试步骤** | 1. MCU `net_accept` 建链(PC 已连接)
2. 循环 `net_recv`(≤2KB/次,保守一次读空)→ 按 offset 字节校验 + 累计 CRC32,直至收满 100KB
3. PC 在文件末尾附 4 字节 CRC32(小端);MCU 收齐后比对 CRC32 | | **通过标准** | MCU 收满 102400B,字节校验全过且 CRC32 匹配,无溢出/截断 | -| **覆盖原则** | DP-04(接收缓冲 2KB 一次读空)、Trap 13 | +| **覆盖原则** | DP-04(接收缓冲 2KB 一次读空) | | **实测** | 🟡 待验证(参考:默认递增模式 100KB 的 CRC32 经 SEND 实测为 `0x9A0E0C8C`,RECV 方向 MCU 累计 CRC32 应与之相等) | ### TC-NET-803: 100KB 回显 round-trip(可选) @@ -955,7 +955,7 @@ IDLE → ACCEPT → [SEND: 分块发 → DONE] / [RECV: 分块收+校验 → DON ``` - **发送**:`phase8_fill(buf, chunk, offset)` 按窗口生成递增模式;`net_send` 经消息队列转发 netTask,由 `net_send_locked` 按发送缓冲(1KB)/`CH395F_SPI_DMA_MAX_PAYLOAD` 自动拆分。 -- **接收**:`net_recv` 每次读 ≤2KB(=接收缓冲,Trap 13 一次读空),`phase8_verify(buf, n, offset)` 字节校验 + 累计 CRC32。 +- **接收**:`net_recv` 每次读 ≤2KB(=接收缓冲,保守一次读空),`phase8_verify(buf, n, offset)` 字节校验 + 累计 CRC32。 - **100KB 不整体驻留 RAM**:`s_tx_buf`/`s_rx_buf`=32KB 仅作滑动窗口;`PHASE8_FILE_SIZE` 控制总量。 - **PC `tcp_file` 子命令**(新增,作 Client 连 MCU:8080):`recv` 接收 `--size` 字节落盘并校验;`send` 读 `--file` 发送并附 4 字节 CRC32 尾。 @@ -1117,8 +1117,8 @@ IDLE → ACCEPT → [SEND: 分块发 → DONE] / [RECV: 分块收+校验 → DON **二段关闭语义**(本 Phase 首跑实证,详见实验方案 E2 补充发现):LISTEN 态 `CLOSE_SOCKET` 返 SUCCESS 但只拆监听(TCP 01→00),byte0 恒 05;需再发一次 -CLOSE 才落 00。与 Trap 07 的 `0x05/0x00` 驻留签名同源;`sock_reset_poll` 的 -WAIT_CLOSE 阶段已内置二段补发(只补一次),生产 net 层则靠 Trap 09 open 重试吸收。 +CLOSE 才落 00。与 TRAP-NET-07 的 `0x05/0x00` 驻留签名同源;`sock_reset_poll` 的 +WAIT_CLOSE 阶段已内置二段补发(只补一次),生产 net 层则靠 TRAP-NET-09 open 重试吸收。 **运行方法**:`ch395f_test.h` 启用 `ENABLE_PHASE15_TESTS`(自动进入硬件层模式, 勿与 NET 层 Phase 混选)→ `@build` → 烧录 → 串口看 `=== Phase 15: 20/20 PASSED ===`。 @@ -1152,18 +1152,17 @@ WAIT_CLOSE 阶段已内置二段补发(只补一次),生产 net 层则靠 | ID | 陷阱 | 现象 | 修复 | |----|------|------|------| -| TRAP-03 | DHCP xid 偏移错误 | DHCP 握手失败 | xid 在 `data[4:8]` | -| TRAP-04 | UDP 无 SENDBUF_FREE 等待 | 后续接收挂起 | 每次 write 后等 SENDBUF_FREE | -| TRAP-05 | PHY 不兼容 RTL8305NBI | 链路抖动 | `ch395f_set_phy(0x08)` 强制 100M_FULL | -| TRAP-06 | KeepAlive 非 500ms 倍数 | 参数不生效 | 必须 500ms 倍数 | -| TRAP-07 | 只 `disconnect` 不 `close`(bit3=1) | 停在 `0x05/0x00`、重开返 `0x20` | 直接 `close_socket`(§5.35 自动断链,**非 RST**);见 Trap 07「定案更正」与 TC-NET-204 | -| TRAP-08 | RECV 电平中断无限循环 | netTask 饿死 | `net_poll` 每次只一批,数据在应用中读 | -| TRAP-09 | PHY 刚建立 link 立即 open | 首次连接失败 | 启动延时 + 重试 | -| TRAP-10 | 消息队列值拷贝丢失 | API 返回旧值 | 改用 `xTaskNotify` | -| TRAP-11 | `net_accept_locked` 拒 ESTABLISHED | FTP PASV 失败 | 允许 ESTABLISHED | -| TRAP-12 | FatFS 不接受 `/` 前缀 | FTP 路径解析失败 | `to_fatfs_path()` 剥离 | -| **TRAP-13** | **`recv_len` 分批读取归零** | **大包截断** | **DMA 缓冲增至 4100,一次读空 ✅ 已修复** | -| **TRAP-14** | **`NET_SEND_BUF_SIZE` > DMA 限制** | **>1496B 发送静默失败** | **`net_send_locked` 按 `NET_DMA_MAX_PAYLOAD` 分块 ✅ 已修复** | +| TRAP-NET-03 | DHCP xid 偏移错误 | DHCP 握手失败 | xid 在 `data[4:8]` | +| TRAP-NET-04 | UDP 无 SENDBUF_FREE 等待 | 后续接收挂起 | 每次 write 后等 SENDBUF_FREE | +| TRAP-NET-05 | PHY 不兼容 RTL8305NBI | 链路抖动 | `ch395f_set_phy(0x08)` 强制 100M_FULL | +| TRAP-NET-06 | KeepAlive 非 500ms 倍数 | 参数不生效 | 必须 500ms 倍数 | +| TRAP-NET-07 | 只 `disconnect` 不 `close`(bit3=1) | 停在 `0x05/0x00`、重开返 `0x20` | 直接 `close_socket`(§5.35 自动断链,**非 RST**);见 TRAP-NET-07与 TC-NET-204 | +| TRAP-NET-08 | RECV 电平中断无限循环 | netTask 饿死 | `net_poll` 每次只一批,数据在应用中读 | +| TRAP-NET-09 | PHY 刚建立 link 立即 open | 首次连接失败 | 启动延时 + 重试 | +| TRAP-NET-10 | 消息队列值拷贝丢失 | API 返回旧值 | 改用 `xTaskNotify` | +| TRAP-NET-11 | `net_accept_locked` 拒 ESTABLISHED | FTP PASV 失败 | 允许 ESTABLISHED | +| 旧机理(未证实) | `recv_len` 分批读取归零 | 227 轮往返未复现 | 保守按一次读空执行;定向 TC 待补 | +| **TRAP-NET-12** | **`NET_SEND_BUF_SIZE` > DMA 限制** | **>1496B 发送静默失败** | **`net_send_locked` 按 `NET_DMA_MAX_PAYLOAD` 分块 ✅ 已修复** | --- diff --git a/docs/测试说明/FTP_Test_Guide.md b/docs/测试说明/FTP_Test_Guide.md index 9349ea73..82634522 100644 --- a/docs/测试说明/FTP_Test_Guide.md +++ b/docs/测试说明/FTP_Test_Guide.md @@ -120,13 +120,13 @@ ### 8.1 连接被拒 / 长时间无欢迎 - 确认 PC 与固件同网段、IP 正确;上电后等待约 7s(PHY 稳定 + `ftpTask` 延迟)再连。 -- 若反复 `open_socket` 失败:见 **TRAP-09**(PHY 未稳定;固件已做 3 次重试 + 7s 延迟,仍失败检查网线/交换机)。 +- 若反复 `open_socket` 失败:见 **TRAP-NET-09**(PHY 未稳定;固件已做 3 次重试 + 7s 延迟,仍失败检查网线/交换机)。 ### 8.2 PASV 失败 / 数据通道连不上 - 确认 PC 防火墙未阻断高端口(PASV 动态端口)。 - **数据通道必须先 `connect` 再发命令**(如 `LIST`/`STOR`),顺序反了会失败(脚本已遵循)。 -- **TRAP-11**:`net_accept_locked` 在 `ESTABLISHED` 状态导致 PASV 失败——需校验 `ESTABLISHED`。 +- **TRAP-NET-11**:`net_accept_locked` 在 `ESTABLISHED` 状态导致 PASV 失败——需校验 `ESTABLISHED`。 ### 8.3 STOR 报 FR_INVALID_NAME (err=6) @@ -134,13 +134,13 @@ ### 8.4 大文件传输中途 RST / 速度抖动 -- **TRAP-13** / 设计约束:CH395F `WRITE_SEND_BUF` 芯片侧吞吐限制,单帧 >1KB 才稳定;TCP 背压会"堵一会儿",属正常流控,不丢数据。 +- **设计约束**:CH395F `WRITE_SEND_BUF` 芯片侧吞吐限制,单帧 >1KB 才稳定;TCP 背压会"堵一会儿",属正常流控,不丢数据。 - 接收侧 `recv_len` 首包可能较大、次包可能为 0,**必须循环读直到连接关闭**(脚本已处理)。 - 若持续 RST:检查 **SPI 分频 = 8**(PCB 设计限制)、`net_poll()` 周期(10ms)是否正常、中断是否统一在 `net_poll()` 中经 `GET_GLOB_INT_STATUS` 处理。 ### 8.5 路径问题 -- **TRAP-12**:FatFS 根路径 `/` 前缀由 `to_fatfs_path()` 统一处理;客户端用绝对路径(如 `/UPLOAD.DAT`)即可。 +- **FatFS 路径口径**:根路径 `/` 前缀由 `to_fatfs_path()` 统一处理(详见 `docs/架构设计/STM32F4-Base存储架构说明.md` §10.1);客户端用绝对路径(如 `/UPLOAD.DAT`)即可。 ### 8.6 FTP 从不监听(21 端口一直拒绝) diff --git a/docs/测试说明/GD5F2GQ5UE_Test_Guide.md b/docs/测试说明/GD5F2GQ5UE_Test_Guide.md index 377e941b..81b3132c 100644 --- a/docs/测试说明/GD5F2GQ5UE_Test_Guide.md +++ b/docs/测试说明/GD5F2GQ5UE_Test_Guide.md @@ -4,7 +4,7 @@ > 全部公开/私有函数,并向上延伸到 FTL(`nand_ftl.c`)与 FatFS diskio。 > 详细芯片行为见 `docs/芯片手册/GD5F2GQ5UExxG.md`,已知陷阱见 `docs/问题记录/GD5F2GQ5UE_Trap_Records.md`。 > -> **2026-10-03 错误通道硬化**(E-10/E-42/E-53/E-54/E-56/E-57,见 `问题记录/GD5F2GQ5UE_Trap_Records.md` Trap 06): +> **2026-10-03 错误通道硬化**(E-10/E-42/E-53/E-54/E-56/E-57,见 `问题记录/GD5F2GQ5UE_Trap_Records.md` TRAP-GD5F-06): > `wait_busy` 区分"超时"(`GD5F_BUSY_TIMEOUT`)与"真故障"(`GD5F_ERROR`)、`bbt_scan` 失败**放弃持久化**、 > init 全链路检查返回值 + **回读 B0h 校验 ECC 使能**、`read` 的 raw 契约文档化、公开 API 统一 `int32_t`。 > 套件回归 `=== GD5F2GQ5UE Tests: 16/16 PASSED (failed=0) ===`。 @@ -413,7 +413,7 @@ ECC 状态(`ECCS1:ECCS0`):`00`=无错;`01/10/11`=纠正 1/2/3 bit;`10` | **前置条件** | SPI 已初始化 | | **测试步骤** | `gd5f_block_erase(block)`(注意传**块编号**,内部转块首页地址,见陷阱 01) | | **通过标准** | 返回 `GD5F_OK`,无 `E_FAIL` | -| **覆盖原则** | `gd5f_block_erase`(Trap 01 修复点) | +| **覆盖原则** | `gd5f_block_erase`(TRAP-GD5F-01 修复点) | ### TC-GD5F-604: write_enable / read_status(WEL 位) diff --git a/docs/测试说明/链路延迟测试说明.md b/docs/测试说明/链路延迟测试说明.md index d22c0403..3c0ecee1 100644 --- a/docs/测试说明/链路延迟测试说明.md +++ b/docs/测试说明/链路延迟测试说明.md @@ -318,9 +318,9 @@ DTU-RemoteLCD 客户端;输出每帧 `req2first / total / recv 批次到达时 ## 6. 实测基线(2026-09-22 台架,已重锁) -> 2026-09-22 注记:该轮代码变更(缓冲块布局与 MSS 解耦 + 超限读 OVERSIZE 金丝雀,见 Trap 21) +> 2026-09-22 注记:该轮代码变更(缓冲块布局与 MSS 解耦 + 超限读 OVERSIZE 金丝雀) > 不改变任何芯片行为参数(MSS=1024、块布局 4/2、DMA 缓冲 2052B 均不变)→ 本节全部基线继续有效,无需重锁。 -> (2026-09-23 补:Trap 21 的"RX 冻结"结论已结案为非缺陷,该轮代码变更按防御性/可观测性改进保留。) +> (2026-09-23 补:TC-NET-604 的"RX 冻结"结论已结案为非缺陷,该轮代码变更按防御性/可观测性改进保留。) ### 6.1 M1 段(Phase 12,SPI 层) @@ -391,7 +391,7 @@ per-recv 日志移除的调度效应,非芯片行为变化:`queue_rt_recv` (旧值对照:`prod_cycle_mcu` 7142µs → 6707µs = P0-b 移除请求路径 per-recv 日志的 ~435µs; `ui_loop`/`relisten` 两档同值 = 与日志无关的段。) -`relisten` 阈值说明:修复前 588000µs(Trap 09 `osDelay(500)` 忙等)→ crit 500ms 即 D10 指纹; +`relisten` 阈值说明:修复前 588000µs(TRAP-NET-09 `osDelay(500)` 忙等)→ crit 500ms 即 D10 指纹; D10 修复后实测 89ms(= 30ms 重试间隔 + 约 60ms 芯片沉降),故 warn 放宽到 **100ms**、crit 保持 500ms (仍能捕获"退回阻塞重试"的回归)。 @@ -589,7 +589,7 @@ sock4 auto-relisten ok / relisten retry ok ← 监听器随后自行恢复 fun_para bit3=1 保留缓冲); 3. **现有甄别为何拦不住**:延迟 DISCONNECT 时刻 `ch395f_get_socket_status` 回读 `tcp=0x00`,不反映新连接 的 ESTABLISHED 态 → "回读芯片状态判陈旧"(`st[1]==ESTABLISHED` 才丢弃)在此场景恒不成立; -4. 与 D10(Trap 09 沉降)、D14(静默回收)同属"单连接 Socket 快速复用"家族。 +4. 与 D10(TRAP-NET-09 沉降)、D14(静默回收)同属"单连接 Socket 快速复用"家族。 **已试并回退的修法(保留结论,勿重复投入)**: ① 传输结束不关数据连接(保留监听器复用)→ **破坏 FTP EOF 语义**(客户端等不到 EOF 整会话挂死),必须关闭; @@ -701,7 +701,7 @@ sock4 auto-relisten ok / relisten retry ok ← 监听器随后自行恢复 5. **S2** 差量/RLE + 帧版本号 + 由拉改推(**根治,收益最大**:典型变化 <100B → 单段一帧,延迟 ≈ 一个 RTT; 稳态带宽 32KB/s → ≈0;D1 影响面 4 段 → 1 段;新命令走同一帧格式与 CRC,旧客户端兼容); 6. **S4** 发送块配额与 MSS:48 块按实际使用的 3 个 socket 重新分配(各 16 块),MSS → 1460 - (3209B 从 4 段降到 1~2 段);**约束**:单次写长度必须 ≤ 发送缓冲(Trap 20 铁律), + (3209B 从 4 段降到 1~2 段);**约束**:单次写长度必须 ≤ 发送缓冲(TRAP-NET-16 铁律), 改分配策略需同步 `net_bind` 重复分配路径,改前先读 `CH395F_Trap_Records.md`,改后跑 Phase 8 吞吐用例; **另**:MSS/块配额变更**必须**跑可信用例(`test/host/ch395f_socket_test.py tcp_client` 全序列 + 新连接独立 4096B 对照),判据 = **客户端侧内容完全匹配**(勿用带固件内读者的台架配方,其短收是读者吞数据不是故障); 7. **S5** 去掉一次 3200B 拷贝(`g_tx` 直接承接位图,省 ~3.2KB 常驻 RAM;改前跑 `scripts/ram_report.py`); @@ -765,7 +765,7 @@ sock4 auto-relisten ok / relisten retry ok ← 监听器随后自行恢复 | RemoDispBus 协议与接收设计(含 INIT 8B 勘误) | `docs/UI设计/RemoDispBus协议与接收设计说明.md` | | UI 设计架构(离屏渲染 + 推流) | `docs/UI设计/UI_设计架构.md` | | BSD Socket API 与双端后端 | `docs/网络/BSD_Socket_API_使用指南.md` | -| CH395F 陷阱史(Trap 9/20 等) | `docs/问题记录/CH395F_Trap_Records.md` | +| CH395F 陷阱史(TRAP-NET-09/-16 等) | `docs/问题记录/CH395F_Trap_Records.md` | | 错误码传播审计(E-02 / E-04 / E-33 / E-34) | `docs/问题记录/错误码传播审计.md` | | CH395F 测试套件(吞吐 Phase 8 等) | `docs/测试说明/CH395F_Test_Guide.md` | | RAM 预算(`ui_net_sink` 6.4KB 常驻) | `docs/内存管理/RAM使用分析与优化方案.md` | diff --git a/docs/编号规范.md b/docs/编号规范.md index 70b19cd3..f4ba8481 100644 --- a/docs/编号规范.md +++ b/docs/编号规范.md @@ -107,7 +107,7 @@ | `DR-` | 决策记录 | `docs/子系统设计/网络Socket监听生命周期设计说明.md` 的 `D1~D10` | `[计划] docs/架构设计/决策记录登记簿.md` | `DR-001` | | `OQ-` | 待确认 / 开放问题 | `B0-a/b/c`、`待确认-N` | `docs/任务规划/基线与待确认.md` | `OQ-001` | | `RSK-` | 风险(**文档内编号**) | 保护规划 `R1~R8`、看门狗 `R1~R7` | 各文档风险表(引用必须带文档名) | 各文档自 `RSK-01` | -| `TRAP--` | 陷阱记录 | `Trap 07`、`TRAP-07`、`陷阱 1` | `docs/问题记录/<模块>_Trap_Records.md` | 保留原号(`TRAP-NET-01~25`、`TRAP-GD5F-01~06`、`TRAP-RTC-01~`) | +| `TRAP--` | 陷阱记录 | `Trap 07`、`TRAP-07`、`陷阱 1` | `docs/问题记录/<模块>_Trap_Records.md` | 保留原号(`TRAP-NET-01~21`——NET 域 2026-10-13 全库迁移并重排连续,沿革见该文档、`TRAP-GD5F-01~06`、`TRAP-RTC-01~`) | | `GAP-` | 模拟器差距 | `H-1~H-7`、`L-1~L-4`、差距清单 `N-1~N-6` | `docs/测试说明/模拟器差距清单.md` | `GAP-01` | | `SC-` | 实机验证场景 | 实机预检 `A1~A8`、看门狗验证用例 `V2/V4/V5` | `docs/测试说明/实机测试实施指南.md` | `SC-01` | | `T0/T0.5/T1/T2/T3` | **测试分层**(原 `L0/L0.5/L1/L2/L3`) | 测试分层五档 | `docs/测试说明/测试开发标准流程.md` §1 | 固定五档 | diff --git a/docs/网络/BSD_Socket_API_使用指南.md b/docs/网络/BSD_Socket_API_使用指南.md index 4b55a3b0..bd114cfe 100644 --- a/docs/网络/BSD_Socket_API_使用指南.md +++ b/docs/网络/BSD_Socket_API_使用指南.md @@ -1,8 +1,8 @@ # BSD Socket API 使用指南 -> **恢复说明**:本文档曾在提交 `4e9ac0d`(2026-08-28)中误删,2026-09-13 从 git 历史恢复。API 随代码多轮同步(2026-09-13:`net_init` 4 参数 / `net_socket` 2 参数 / 网络代次号 / `NET_NO_OS` 双端构建;2026-09-23:`net_recv_to` / `net_sock_state` / `net_resync_listen` / `net_relisten`、L3 pending 化、D10 非阻塞重监听、Trap 22 周期对账、Trap 23 排渣隔离)。详见文末变更记录。 +> **恢复说明**:本文档曾在提交 `4e9ac0d`(2026-08-28)中误删,2026-09-13 从 git 历史恢复。API 随代码多轮同步(2026-09-13:`net_init` 4 参数 / `net_socket` 2 参数 / 网络代次号 / `NET_NO_OS` 双端构建;2026-09-23:`net_recv_to` / `net_sock_state` / `net_resync_listen` / `net_relisten`、L3 pending 化、D10 非阻塞重监听、TRAP-NET-17 周期对账、TRAP-NET-18 排渣隔离)。详见文末变更记录。 > -> **伴读文档**:本文回答"上层该怎么调";**内部函数之间的调用关系、每个函数的设计作用与意图**(netTask / 消息队列 / L3 挂起 / D10 重试 / Trap 22 对账 / Trap 23 排渣各子系统,含总体调用图与端到端时序)见 `docs/网络/BSD_Socket_API_函数调用关系.md`。 +> **伴读文档**:本文回答"上层该怎么调";**内部函数之间的调用关系、每个函数的设计作用与意图**(netTask / 消息队列 / L3 挂起 / D10 重试 / TRAP-NET-17 对账 / TRAP-NET-18 排渣各子系统,含总体调用图与端到端时序)见 `docs/网络/BSD_Socket_API_函数调用关系.md`。 ## 目录 @@ -62,7 +62,7 @@ | `net_config.h` | 配置宏定义(最大 Socket 数、超时时间等) | | `net_types.h` | 类型定义(地址结构、Socket 控制块等) | | `net_socket.h` | BSD Socket API 头文件,含 `net_msg_t` 消息类型定义 | -| `net_socket.c` | API 核心实现:消息队列处理与线程安全封装、内部 `_sock()` 版本;另含 L3 pending 化 recv、Trap 22 周期对账 + 监听重建**合一** reconciler(`net_reconcile_tick`,100ms,分级去抖+冷却,形态①②③④⑤/U11,N34 阶段 3) | +| `net_socket.c` | API 核心实现:消息队列处理与线程安全封装、内部 `_sock()` 版本;另含 L3 pending 化 recv、TRAP-NET-17 周期对账 + 监听重建**合一** reconciler(`net_reconcile_tick`,100ms,分级去抖+冷却,形态①②③④⑤/U11,N34 阶段 3) | | `net_listener.c`(**真机专属**) | **监听生命周期**(N-32 §10 模块化):`net_listener_try` / `net_listener_clear` / `net_listener_retry_tick`(D10 重试 30ms/2s)/ `net_listener_settle`(沉降/隔离 100ms/400ms);原 `net_listener_pump` 的重建决策已并入 `net_reconcile_tick` 形态③④(N34 阶段 3,单一 100ms tick);内部头,模拟器不编译 | | `net_select.h` | select/poll API 头文件 | | `net_select.c` | select/poll 实现(仅 MCU 端编译) | @@ -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**(N34 阶段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 22 对账+监听重建合一 reconciler 100ms),再处理队列 | +| **`net_poll()` 和 `net_process_messages()`** | 必须在 `netTask` 主循环调用;`net_process_messages()` 每轮先跑 **3 个非阻塞 tick**(D10 重监听重试 → L3 挂起 recv 完成 → TRAP-NET-17 对账+监听重建合一 reconciler 100ms),再处理队列 | ### 3.4 内部 API 说明 @@ -181,22 +181,22 @@ int net_close_sock(net_sock_t *p_sock); - **队列阻塞语义**:只有 `osMessageQueuePut` 失败(8 长度固定队列,极端情形)时公共 API 阻塞;recv 挂起等待**不计入**队列占用(完成前队列中无消息滞留) - **哪些操作要求 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 09 / 22 / 23) +### 3.6 芯片/本层状态对账与重监听自愈(TRAP-NET-09/-17/-18) 单连接模型下"监听 Socket = 数据通道",多种芯片行为会造成**芯片侧与本层状态失同步**,纯事件(中断)驱动无法自愈。当前版本有**四层自愈**(N34 阶段 3:原 200ms 周期对账与 100ms 监听器 pump 合并为**单一 100ms `net_reconcile_tick`**),全在 netTask 内、全非阻塞、不冻结主循环: | 机制 | 触发 | 动作 | 关键参数 | |---|---|---|---| | **`auto_relisten`** | 数据通道正常 EOF / DISCONNECT | 重开 + 重新 LISTEN,回 LISTENING;应用层无需 close/重 listen | `net_listen()` 置位;`net_close()` 清 | -| **D10 非阻塞重监听重试** | 重开失败(Trap 09:芯片 close 后需沉降) | 单次尝试失败即登记待重试,每轮到点重试;超时限放弃并记 `relisten give up` | 重试间隔 30ms / 时限 2s(自首次尝试);同 fd 有在途挂起 recv 时跳过(L3 保险,防毁在途连接) | -| **Trap 22 周期对账 + 监听重建(合一 `net_reconcile_tick`,N34 阶段 3;对账①②⑤ + 原 pump③④/U11)** | 芯片丢 CONNECT/DISCONNECT 中断(快速复连);静默回收 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 语义只修本层、不发芯片命令(N34 阶段1 新增,原手动 API `net_resync_listen` 的语义并入);U11 本层 CLOSED 而芯片**正在** LISTEN → 只做状态重同步、不发芯片命令 | 周期 100ms;**分级去抖**:②=1 轮、③④⑤=2 轮、①=4 轮(M3 让位窗 400ms,**勿改小**)+ 动手后 **1s 冷却**(防"刚 open 未生效"瞬态重建风暴;U11 被动重同步不去抖、不占冷却);其它位置只置事实、不做重建动作 | -| **Trap 23 排渣隔离** | 异常释放(芯片 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 时限**,否则监听被放弃) | +| **D10 非阻塞重监听重试** | 重开失败(TRAP-NET-09:芯片 close 后需沉降) | 单次尝试失败即登记待重试,每轮到点重试;超时限放弃并记 `relisten give up` | 重试间隔 30ms / 时限 2s(自首次尝试);同 fd 有在途挂起 recv 时跳过(L3 保险,防毁在途连接) | +| **TRAP-NET-17 周期对账 + 监听重建(合一 `net_reconcile_tick`,N34 阶段 3;对账①②⑤ + 原 pump③④/U11)** | 芯片丢 CONNECT/DISCONNECT 中断(快速复连);静默回收 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 语义只修本层、不发芯片命令(N34 阶段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 时限**,否则监听被放弃) | **行为结论**:监听器恢复从"一次性事件"变为"登记 → 到点重试"过程: - 正常断连:通常一次重开即成;失败时 ~30ms 量级恢复,最迟 ~1s(2s 后放弃,错误日志可见)。恢复窗内该端口拒绝新连接 → 调用方重连逻辑须容忍 ~1s 中断。 -- **快速复连不再"监听死亡"(Trap 22)**:对账保证失同步按分级去抖在 100~400ms 内修正(②1 轮 / ③④⑤2 轮 / ①4 轮,100ms/轮;2026-09-23 生产路径:待机后 UI `:7003` 快速复连 10/10;FTP `:21` 100ms/0ms/600ms 间隔各 50/50;零重建风暴)。 -- **中止复连首上传不再被静默截断(Trap 23)**:RST/FIN 中止后首笔复连上传完整通过(RST 10/10、FIN 5/5,SHA-256 全匹配)。详见 `docs/问题记录/CH395F_Trap_Records.md` Trap 22/23。 +- **快速复连不再"监听死亡"(TRAP-NET-17)**:对账保证失同步按分级去抖在 100~400ms 内修正(②1 轮 / ③④⑤2 轮 / ①4 轮,100ms/轮;2026-09-23 生产路径:待机后 UI `:7003` 快速复连 10/10;FTP `:21` 100ms/0ms/600ms 间隔各 50/50;零重建风暴)。 +- **中止复连首上传不再被静默截断(TRAP-NET-18)**:RST/FIN 中止后首笔复连上传完整通过(RST 10/10、FIN 5/5,SHA-256 全匹配)。详见 `docs/问题记录/CH395F_Trap_Records.md` TRAP-NET-17/-18。 --- @@ -247,7 +247,7 @@ int main(void) - 更新所有 Socket 的连接状态(CONNECT / DISCONNECT / RECV / TIMEOUT) - 触发事件回调 - 处理数据接收 - - `net_process_messages()` 每轮先跑 3 个**非阻塞**检查再取队列:D10 重监听重试(断连后监听器恢复)→ L3 挂起 recv 完成检查(阻塞 recv 唤醒,E-02 修复)→ Trap 22 芯片/本层对账 + 监听重建合一 reconciler(100ms 周期,读驱动状态缓存,丢中断/静默回收/监听失位自愈,§3.6) + - `net_process_messages()` 每轮先跑 3 个**非阻塞**检查再取队列:D10 重监听重试(断连后监听器恢复)→ L3 挂起 recv 完成检查(阻塞 recv 唤醒,E-02 修复)→ TRAP-NET-17 芯片/本层对账 + 监听重建合一 reconciler(100ms 周期,读驱动状态缓存,丢中断/静默回收/监听失位自愈,§3.6) - **监听 Socket 建立**:由就绪屏障后的服务任务(timeSyncTask / ftpTask / uiTask)经队列 API 建立(`net_socket` → `net_bind` → `net_listen`);断连后 net 层自动重监听(§3.6),服务层只需循环 `net_accept`,无需维护重监听逻辑 ```c @@ -1012,12 +1012,12 @@ int net_drop_conn(int sockfd); 应用**放弃会话**(对时 30s 预算耗尽、协议超时)时主动拆掉当前 TCP 会话并**保留监听**——单连接模型下挂起但 TCP 仍活的客户端会永久占住监听(PC 内核自动 ACK keepalive,芯片僵尸判不死,定案见 `docs/网络/N34-自愈审计/实验方案.md` E13),而应用又不得 `net_close` 监听 fd(毁监听);本 API 补上"拆连接不拆监听"的应用侧形态。 -- **netTask 侧**:先了结本 fd 挂起 recv(排空/取消 + 必通知),再 `sock_reset` QUARANTINE(FIN + 排渣 + 排空事件 + 等 byte0=00H,Trap 23 异常释放语义,400ms 底);本层收口 CLOSED;`auto_relisten` fd 随即 `net_listener_try` 即时重监听(失败交 D10 重试 + reconciler 形态④) +- **netTask 侧**:先了结本 fd 挂起 recv(排空/取消 + 必通知),再 `sock_reset` QUARANTINE(FIN + 排渣 + 排空事件 + 等 byte0=00H,TRAP-NET-18 异常释放语义,400ms 底);本层收口 CLOSED;`auto_relisten` fd 随即 `net_listener_try` 即时重监听(失败交 D10 重试 + reconciler 形态④) - **幂等**:无活动会话(非 ESTABLISHED/CONNECTING)直接返 0,不动监听 - **EOF 路径不要调**:EOF 时 net 层 `auto_relisten` 已接管(§12.4),无需也不应再拆 - 经消息队列委托 netTask(同其它 `net_*` API,非 netTask 上下文调用);等待期 netTask 有界占用 ~400-600ms - 模拟器(`net_socket_sim.c`,数据 fd ≠ 监听 fd)实现为关数据连接、监听常驻 -- 台架定案(2026-10-13,E-02):CH395F close **无 RST 语义**——对端内核存活但不 close 时芯片停 FIN_WAIT_2 长尾,监听恢复受限于对端 close;对端配合则 TIME_WAIT 过渡 ~150ms → reconciler 形态④ relisten → 新连接接入。不配合场景兜底 = 芯片 TIMEOUT/keepalive 拆僵尸(E13 定案 30~90s)。详见 `docs/问题记录/CH395F_Trap_Records.md` Trap 07 +- 台架定案(2026-10-13,E-02):CH395F close **无 RST 语义**——对端内核存活但不 close 时芯片停 FIN_WAIT_2 长尾,监听恢复受限于对端 close;对端配合则 TIME_WAIT 过渡 ~150ms → reconciler 形态④ relisten → 新连接接入。不配合场景兜底 = 芯片 TIMEOUT/keepalive 拆僵尸(E13 定案 30~90s)。详见 `docs/问题记录/CH395F_Trap_Records.md` TRAP-NET-07 #### 自愈与查询 API @@ -1058,7 +1058,7 @@ void net_process_messages(void); 1. **D10 重监听重试**(`net_listener_retry_tick`):断连后监听器恢复的到点重试(30ms 间隔 / 2s 时限,§3.6) 2. **L3 挂起 recv 完成检查**(`net_pending_tick`):按 ①数据 ②到点(仅显式预算) ③断连 检查每个挂起请求并通知调用方;④ tripwire:挂起超 60s 只 `[ERR]` 留痕一次,不强制了结(N34 阶段 1.5) -3. **Trap 22 芯片/本层对账 + 监听重建(合一 `net_reconcile_tick`)**:100ms 周期,判据读驱动状态缓存(`ch395f_sock_state_cached`),修正**五类**失同步(①②③④⑤)+ U11 被动重同步——原 200ms 对账与 100ms 监听器 pump 已合并为单一 tick,**唯一重建入口**=形态③④(§3.6) +3. **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=**无限等待**(N34 阶段 1.5 删除 `NET_RECV_TIMEOUT_MS` 30s 默认预算——超时归调用方)。消息类型 7 种(send/recv/close/connect/listen/accept/drop_conn)——自愈类消息 `NET_MSG_RESYNC_LISTEN` / `NET_MSG_RELISTEN` 已随 API 退役删除(N34 阶段1)。 @@ -1275,7 +1275,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 22 芯片/本层对账 + 监听重建合一 reconciler(100ms 周期,读驱动状态缓存,丢中断/静默回收/监听失位自愈,§3.6)。 +- `net_process_messages()`:除串行处理队列请求外,每轮先跑 3 个**非阻塞**检查——D10 重监听重试(断连后监听器恢复)→ L3 挂起 recv 完成检查(阻塞 recv 的唤醒)→ TRAP-NET-17 芯片/本层对账 + 监听重建合一 reconciler(100ms 周期,读驱动状态缓存,丢中断/静默回收/监听失位自愈,§3.6)。 建议主循环周期 ≤ 10ms(本项目 1ms)。若 netTask 主循环停摆(断点、任务被挂起),监听器不会恢复、阻塞 recv 不会完成——这是软件侧"监听死机"的最常见原因。 @@ -1291,7 +1291,7 @@ static int read_time_frame(int conn_sock, uint8_t buf[FRAME_LEN]) { - 每次发送的数据长度不能超过 `NET_SEND_BUF_SIZE`(默认 4096 字节) - TCP 需要等待发送缓冲区空闲(`send_ready` 标志) - UDP 单包载荷最大 1472 字节(写入超过此值,CH395F 会自动分片为多个 UDP 包发送) -- 接收侧:每 Socket RX 缓冲 2KB(4×512B,即芯片接收窗口)、TX FIFO 1KB(2×512B)、MSS=1024。对端常态把 2KB 窗口填满是**正常**现象,不等于"冻结"(Trap 21 结案,见 §12.10) +- 接收侧:每 Socket RX 缓冲 2KB(4×512B,即芯片接收窗口)、TX FIFO 1KB(2×512B)、MSS=1024。对端常态把 2KB 窗口填满是**正常**现象,不等于"冻结"(TC-NET-604 结案,见 §12.10) ### 12.4 如何处理连接断开? @@ -1317,7 +1317,7 @@ if (len < 0 && net_get_errno() == NET_ERR_WOULDBLOCK) { **⚠ 单连接模型(MCU 端 CH395F)的断开处理**:监听 Socket 在 CONNECT 后**自身即数据通道** (`g_client == g_listen`);`net_recv()` 返回 0(EOF)时 net 层已自动把该 Socket 放回 LISTEN (`auto_relisten`,BSD 风格"监听常驻")。此时**不得**再对它调 `net_close()`——那会把监听一起关掉, -后续连接直接被拒(连接与监听同一 fd,重建还受 Trap 09 时序限制)。正确写法: +后续连接直接被拒(连接与监听同一 fd,重建还受 TRAP-NET-09 时序限制)。正确写法: ```c } else if (len == 0) { @@ -1440,17 +1440,17 @@ CH395F 底层驱动返回的状态码(`ch395f_get_cmd_status()` 或 `ch395f_po 需要回传请用调用方提供的指针,或复用通知值。(2026-09-21 实证:曾按"加 `msg.err` 字段"方式修错误码, 结果 `ui_net_sink` 每次空轮询都拿到栈垃圾 → 误判为"真错误" → 连上即 `net_close`。) -### 12.10 监听器自愈行为与沉降/隔离(D10 / Trap 09、Trap 22、Trap 23 / N-32) +### 12.10 监听器自愈行为与沉降/隔离(D10 / TRAP-NET-09、TRAP-NET-17、TRAP-NET-18 / N-32) **默认行为**(无需应用代码):断连 → `auto_relisten` 重开监听;重开失败 → 30ms 节奏重试(2s 时限);芯片丢中断/静默回收/监听失位 → 单一 reconciler 100ms 自愈(分级去抖,N34 阶段 3 起对账与 pump 合一);中止复连首上传 → 沉降/隔离。全在 netTask 内,应用只需保证 `net_poll()` + `net_process_messages()` 主循环在跑。 | 场景 | 修复前症状 | 机制 | 生产验证(2026-09-23) | |---|---|---|---| -| **Trap 22** 快速复连丢中断 → 监听静默死亡 | 待机后 UI `:7003` 快速复连 2/10 且**永久**拒连;FTP `:21` 100ms 间隔自第 29 轮死亡;设备日志**无报错**(状态静默滞留) | 单一 reconciler `net_reconcile_tick()`(100ms,对账①②⑤ + 重建③④/U11 合一,**唯一重建入口**=形态③④,N34 阶段 3;判据读驱动状态缓存)+ 分级去抖(②1 轮/③④⑤2 轮/①4 轮)+ 冷却(1s);`handle_disconnect_event` 只置沉降事实、不再自行重建 | `:7003` 10/10;`:21` 100ms/0ms/600ms 各 50/50;UI 20/20;FTP 正常 10/10(SHA-256);中止 RST/FIN 5/5 + 5/5;零重建风暴(2026-10-13 合并后复测 `:7003` 50/50、`:21` 三档各 50/50 全绿) | -| **Trap 23** 中止后首笔复连上传被静默截断 ≤2048B 并误报 226 成功 | RST/FIN 中止上传 → 紧接着的首笔上传"收"到 ~2KB 残留后判结束回 226 → 客户端以为成功 = **静默数据损坏**(第二笔起正常) | 异常释放(TIMEOUT)预重监听隔离:`net_drain_chip_rx()` 分块排空 RX 残渣 + 主动 close + 排空中断 + 等 CLOSED/TCP_CLOSED(400ms 起 / 800ms 上限 < 2s 时限) | RST 10/10、FIN 5/5 全通过;正常路径 30/30 + 20/20,SHA-256 全匹配 | -| **Trap 21**(TC-NET-604)"接收冻结"虚警 | 旧判读脚本把"对端填满 2KB RX 窗口"误判为 STALL/WEDGE | **结论:非缺陷**(芯片 RX 缓冲即窗口,对端填满是常态);判据改为脚本层 STEP + 客户端内容校验(RET SHA-256),虚警工具归档(`MDK-ARM/retracted_bench_20260922/`,README 禁重跑) | 真实 FTP 路径 227 轮全通过(8KB×10 … 1MB×2);并发 UI 流 600/600 CRC;设备零 FAIL/ERROR/WARN | +| **TRAP-NET-17** 快速复连丢中断 → 监听静默死亡 | 待机后 UI `:7003` 快速复连 2/10 且**永久**拒连;FTP `:21` 100ms 间隔自第 29 轮死亡;设备日志**无报错**(状态静默滞留) | 单一 reconciler `net_reconcile_tick()`(100ms,对账①②⑤ + 重建③④/U11 合一,**唯一重建入口**=形态③④,N34 阶段 3;判据读驱动状态缓存)+ 分级去抖(②1 轮/③④⑤2 轮/①4 轮)+ 冷却(1s);`handle_disconnect_event` 只置沉降事实、不再自行重建 | `:7003` 10/10;`:21` 100ms/0ms/600ms 各 50/50;UI 20/20;FTP 正常 10/10(SHA-256);中止 RST/FIN 5/5 + 5/5;零重建风暴(2026-10-13 合并后复测 `:7003` 50/50、`:21` 三档各 50/50 全绿) | +| **TRAP-NET-18** 中止后首笔复连上传被静默截断 ≤2048B 并误报 226 成功 | RST/FIN 中止上传 → 紧接着的首笔上传"收"到 ~2KB 残留后判结束回 226 → 客户端以为成功 = **静默数据损坏**(第二笔起正常) | 异常释放(TIMEOUT)预重监听隔离:`net_drain_chip_rx()` 分块排空 RX 残渣 + 主动 close + 排空中断 + 等 CLOSED/TCP_CLOSED(400ms 起 / 800ms 上限 < 2s 时限) | RST 10/10、FIN 5/5 全通过;正常路径 30/30 + 20/20,SHA-256 全匹配 | +| **TC-NET-604**"接收冻结"虚警 | 旧判读脚本把"对端填满 2KB RX 窗口"误判为 STALL/WEDGE | **结论:非缺陷**(芯片 RX 缓冲即窗口,对端填满是常态);判据改为脚本层 STEP + 客户端内容校验(RET SHA-256),虚警工具归档(`MDK-ARM/retracted_bench_20260922/`,README 禁重跑) | 真实 FTP 路径 227 轮全通过(8KB×10 … 1MB×2);并发 UI 流 600/600 CRC;设备零 FAIL/ERROR/WARN | -**排障判据**(无 Wireshark 环境,纯生产路径):`MDK-ARM/ftp_freeze_probe.py`(真实 STOR + RETR SHA-256)、`MDK-ARM/ftp_abort_probe.py`(RST/FIN 中止复连)、`MDK-ARM/tcp_reconnect_probe.py`(快速复连存活甄别)、`MDK-ARM/bench_verdict.py`(脚本层 STEP 为真值;**`probe_classify.py` A/C 判读不可信**,见 retracted 归档 README)。详见 `docs/问题记录/CH395F_Trap_Records.md`。 +**排障判据**(无 Wireshark 环境,纯生产路径):`MDK-ARM/ftp_freeze_probe.py`(真实 STOR + RETR SHA-256)、`test/host/ftp_abort_probe.py`(RST/FIN 中止复连)、`MDK-ARM/tcp_reconnect_probe.py`(快速复连存活甄别)、`MDK-ARM/bench_verdict.py`(脚本层 STEP 为真值;**`probe_classify.py` A/C 判读不可信**,见 retracted 归档 README)。详见 `docs/问题记录/CH395F_Trap_Records.md`。 --- @@ -1460,10 +1460,10 @@ CH395F 底层驱动返回的状态码(`ch395f_get_cmd_status()` 或 `ch395f_po |------|------| | 2026-10-13 | **N34 阶段 3 自愈合并 + E-02 收口**(实施规格 `docs/网络/N34-自愈审计/实施方案.md` §5):`net_listener_pump` **删除**,与 `net_reconcile_tick` 合并为**单一 100ms tick**(覆盖原对账①②⑤ + 原 pump③④/U11;判据读**驱动状态缓存** `ch395f_sock_state_cached`,唯一刷新点 = `net_poll` 每轮全量刷新,E2 实测单次回读 18.1µs;**分级去抖** ②=1 轮 / ③④⑤=2 轮 / ①=4 轮(M3 让位窗 400ms)+ 1s 冷却;§2.2 / §3.3 / §3.6(五层 → **四层**)/ §10.2 / §12.1 / §12.10 同步,主循环自愈 tick 由 4 个回到 **3 个**;常量 `NET_LISTENER_PUMP_PERIOD_MS` / `NET_LISTENER_STABLE_N` / `NET_LISTENER_COOLDOWN_MS` 删除);**忙等统一**——`net_listen_locked` / `net_connect_locked` open 失败重试"close+盲等 500ms"与 `net_close_locked` 500ms 排空循环改走驱动 `sock_reset` 原语(`net_sock_reset_sync`,GRACEFUL 100ms 底),`net_close_locked` 不再读清全局中断寄存器(F-NET-7:全局寄存器唯一读者 = `net_poll`);新增 **`net_drop_conn(sockfd)`**(§10.1,E-02 收口:拆会话**保监听**、幂等、EOF 路径不得调;新消息类型 `NET_MSG_DROP_CONN`;timeSync 30s 预算耗尽路径接入;模拟器实现为关数据连接);`net_poll` 每轮对在用 Socket 步进 `ch395f_sock_reset_poll`(同步包装 600ms 放弃后复位机不搁浅);`net_sock_state_t` 注释声明为"本层会话投影"(芯片事实源 = 驱动缓存,`net_types.h`);台架回归全绿(最终构建 Code=211348,E-02 双 PASS,见 `docs/网络/N34-自愈审计/实验方案.md` E-02 收口台架) | | 2026-10-13 | **N34 阶段 1.5 recv 语义主流化**(规格 `docs/网络/N34-自愈审计/实施方案.md` §3.5):`net_recv()` 默认**无限阻塞**(删 `NET_RECV_TIMEOUT_MS` 30s 默认预算,唤醒源=数据/EOF/close/网络重建 CONNRESET);新增 `net_recv_nb()`(`NET_MSG_DONTWAIT` 路径转正)与 `net_setblocking(fd, on)`(模式挂 socket 控制块属性,`net_sock_t.blocking`,默认阻塞);`net_recv_to()` 保留为**显式预算**便捷入口(`timeout_ms=0` 语义从"默认 30s"改为"无限"故仍拒 INVAL);内部 recv 看门狗降级为 **tripwire**(挂起超 60s 只 `[ERR]` 留痕一次,不再"到点 +1s 强制了结"伪造 TIMEDOUT);⚠ 新增**文档契约**:IWDG `required_mask` 内任务禁止无限阻塞 recv(必须 `net_select` 超时或 `net_recv_to` 显式预算);`ui_net_sink` / `iec104_serv` 迁移 `net_recv_nb`,lftpd / timeSync 维持显式预算;模拟器 `net_socket_sim.c` 镜像三 API(blocking 属性包装层模拟) | -| 2026-10-13 | **N34 阶段1 API 面收敛**(审计见 `docs/网络/N34-自愈审计/README.md`):删除手动自愈 API `net_relisten()` / `net_resync_listen()`(生产零调用;后者语义并入对账**⑤**——本层 ESTABLISHED 而芯片 0x05/0x01 静默回收 → RESYNC 只修本层,§3.6 对账由两类扩为三类①②⑤;前者与 pump/D10 重叠且对健康监听器是 footgun)与 `net_can_accept()`(只认 ESTABLISHED 的旧门控挡死丢 CONNECT 探针,监听门控统一 `net_listener_armed()`,§10.1 门控节改写);`NET_MSG_RESYNC_LISTEN` / `NET_MSG_RELISTEN` 消息类型删除;timeSync 会话收尾不再 `net_close`(同 fd 续 accept,监听跨会话常驻);lftpd 应用层阻塞重试退役(就绪屏障已消除 Trap 09 启动形态) | +| 2026-10-13 | **N34 阶段1 API 面收敛**(审计见 `docs/网络/N34-自愈审计/README.md`):删除手动自愈 API `net_relisten()` / `net_resync_listen()`(生产零调用;后者语义并入对账**⑤**——本层 ESTABLISHED 而芯片 0x05/0x01 静默回收 → RESYNC 只修本层,§3.6 对账由两类扩为三类①②⑤;前者与 pump/D10 重叠且对健康监听器是 footgun)与 `net_can_accept()`(只认 ESTABLISHED 的旧门控挡死丢 CONNECT 探针,监听门控统一 `net_listener_armed()`,§10.1 门控节改写);`NET_MSG_RESYNC_LISTEN` / `NET_MSG_RELISTEN` 消息类型删除;timeSync 会话收尾不再 `net_close`(同 fd 续 accept,监听跨会话常驻);lftpd 应用层阻塞重试退役(就绪屏障已消除 TRAP-NET-09 启动形态) | | 2026-10-12 | **N-32 监听生命周期模块化 + 单一所有者**:监听生命周期抽为 `net_listener.c`(§2.2 文件表新增该行,真机专属);主循环自愈 tick 由 3 个增至 **4 个**(新增 `net_listener_pump`,100ms,**唯一重建入口**);`net_reconcile_tick` 对账收窄为 ①②,③④ 移交 pump;`handle_disconnect_event` / `handle_timeout_event` 改为 `net_listener_settle(GRACEFUL 100ms / QUARANTINE 400ms)` **只置事实**、不再自行重建;§3.3 / §3.6(四层自愈 → **五层**)/ §12.10 同步。权威设计见 `docs/子系统设计/网络Socket监听生命周期设计说明.md` §10 | | 2026-10-10 | 新增 **`net_can_accept()` / `net_can_read()` 就绪门控 API**(§10.1):纯 RAM 读控制块、**不走消息队列、不等 netTask 回执**,供实时性敏感任务在发 RPC 前判断"有没有活"(动因 = `ui_net_sink_poll()` 每 10 ms 一次 `net_accept` 把 uiTask 拖成最坏 ≈50 ms;原拟登记 N-19 / C7,当日结案不入登记簿;设计与实测见 `docs/子系统设计/看门狗设计说明.md` §11(原分析文档已并入该节));§3.5 补"netTask 运行要求"例外条;⚠ 记录两条使用约束——**不可作为唯一判据**(`readable` 只由 RECV_OK 中断置位,须保留低频兜底 recv)与**模拟器后端恒返 1**(`net_socket_sim.c`,Winsock 无 RPC 成本) | -| 2026-09-23 | 同步 L3 pending 化、D10 非阻塞重监听、Trap 22 对账、Trap 23 排渣隔离后的现状:新增 `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(自愈行为数据与排障判据,含 Trap 21 结案) | +| 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 补错误码回传约束 | | 2026-09-15 | 同步配置引擎属主变更(P1-3):`net_init` 调用方说明改为"netTask 只读 RAM 副本"(网口配置由 defaultTask 在就绪屏障前加载);模拟器端启动序列补 `net_cfg_save_run()`(主循环 = 模拟器侧配置引擎属主,执行 UI 保存请求) | | 2026-09-13 | 从 git 历史恢复(`4e9ac0d` 2026-08-28 误删);同步当前代码:`net_init` 4 参数(新增 `mac`)、`net_socket` 2 参数(移除 `protocol`)、新增 `net_get_generation` / `net_generation_inc` / `net_listen_locked`、`NET_NO_OS` 双端构建、netTask 周期 10ms → 1ms | diff --git a/docs/网络/BSD_Socket_API_函数调用关系.md b/docs/网络/BSD_Socket_API_函数调用关系.md index bd8091cb..505515ce 100644 --- a/docs/网络/BSD_Socket_API_函数调用关系.md +++ b/docs/网络/BSD_Socket_API_函数调用关系.md @@ -14,7 +14,7 @@ ## 1. 一句话架构 -**一切芯片(CH395F)操作收敛到唯一执行者 netTask 串行完成**;其他任务通过"入队消息 + 任务通知"表达请求,结果只经**任务通知值**回传;阻塞等待**不独占 netTask**(挂起登记 + 主循环节拍完成);监听器**常驻**(`auto_relisten` + D10 非阻塞重试 + **Trap 22 周期对账 + 监听重建合一 `net_reconcile_tick`(唯一重建入口=形态③④,100ms)** + Trap 23 排渣隔离,四层自愈;N34 阶段 3 起原 N-32 监听器 pump 已并入对账)。 +**一切芯片(CH395F)操作收敛到唯一执行者 netTask 串行完成**;其他任务通过"入队消息 + 任务通知"表达请求,结果只经**任务通知值**回传;阻塞等待**不独占 netTask**(挂起登记 + 主循环节拍完成);监听器**常驻**(`auto_relisten` + D10 非阻塞重试 + **TRAP-NET-17 周期对账 + 监听重建合一 `net_reconcile_tick`(唯一重建入口=形态③④,100ms)** + TRAP-NET-18 排渣隔离,四层自愈;N34 阶段 3 起原 N-32 监听器 pump 已并入对账)。 --- @@ -22,11 +22,11 @@ | 执行域 | 运行位置 / 节奏 | 允许的操作 | 典型调用者 | |---|---|---|---| -| **netTask 主循环** | 独立 FreeRTOS 任务,1ms/tick | **唯一**可碰芯片(SPI2)的地方;3 个非阻塞 tick(D10 重试 / L3 挂起 / Trap 22 对账+监听重建合一 reconciler);队列分派;事件处理器;用户回调 | `net_poll` / `net_process_messages` 及其下游全部 `_locked` / `_tick` / `handle_*` | +| **netTask 主循环** | 独立 FreeRTOS 任务,1ms/tick | **唯一**可碰芯片(SPI2)的地方;3 个非阻塞 tick(D10 重试 / L3 挂起 / 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 独占芯片) | -> ⚠ 直碰 SPI 的三例外**只能在"芯片访问者唯一"的上下文**调用:产品构建中应用任务一律走队列(TCP 全路径);三例外的实际消费者是测试套件(见 AGENTS.md"测试构建")。从其他任务直接调用会与 `net_poll` 的 SPI 事务竞争,破坏 CH395F 寄存器(Trap 16 实证:CONNECT 超时)。 +> ⚠ 直碰 SPI 的三例外**只能在"芯片访问者唯一"的上下文**调用:产品构建中应用任务一律走队列(TCP 全路径);三例外的实际消费者是测试套件(见 AGENTS.md"测试构建")。从其他任务直接调用会与 `net_poll` 的 SPI 事务竞争,破坏 CH395F 寄存器(TRAP-NET-14 实证:CONNECT 超时)。 --- @@ -49,7 +49,7 @@ flowchart TB NP["netPoll
INT# GPIO 轮询 → 全局中断 → 各 Socket SINT → 状态缓存全量刷新 + sock_reset 步进 → 每轮一批"] T1["① netListenerRetryTick
D10 重监听重试:30ms / 2s 时限"] T2["② netPendingTick
L3 挂起 recv:①数据 ②到点(仅显式预算) ③断连 ④tripwire 60s 只留痕"] - T3["③ netReconcileTick
Trap 22 对账 + 监听重建合一(N34 阶段 3):100ms / 分级去抖 / 冷却
读驱动状态缓存(①②③④⑤ + U11)"] + T3["③ netReconcileTick
TRAP-NET-17 对账 + 监听重建合一(N34 阶段 3):100ms / 分级去抖 / 冷却
读驱动状态缓存(①②③④⑤ + U11)"] DISP["队列分派 switch
SEND · RECV · CLOSE
CONNECT · LISTEN · ACCEPT · DROP_CONN"] subgraph LK["_locked / 内部分派目标(仅 netTask 上下文)"] LL["netListenLocked
初次监听(open 重试 3× + sock_reset 排渣)"] @@ -59,7 +59,7 @@ flowchart TB LA["netAcceptLocked
先 netPoll + 状态判定"] LC["netCloseLocked
了结挂起 + cmd_done 快路径 / sock_reset(GRACEFUL)"] DC["netDropConnLocked
拆会话保监听:sock_reset(QUARANTINE) + 即时重监听"] - RLS["netListenerTry → netRelistenLocked
单次尝试 + Trap 23 排渣隔离"] + RLS["netListenerTry → netRelistenLocked
单次尝试 + TRAP-NET-18 排渣隔离"] end EV["事件处理器
handleConnect · handleDisconnect
handleRecv · handleSend · handleTimeout"] ST["netListenerSettle
只置沉降/隔离事实(100ms / 400ms)"] @@ -97,7 +97,7 @@ flowchart TB 1. 应用侧只有两种"等":① 入队后 `xTaskNotifyWait` 等回执(同步语义);② 挂起 recv 的等待(同一通道,只是"完成"由 `netPendingTick` 判定,不由 netTask 立即处理)。 2. `net_process_messages()` 开头的 **3 个 tick 是网络层自愈主干**:没有任何应用消息时,重试 / 挂起完成 / 状态对账 + 监听重建照常运转——"断连后即使没人发消息,监听器也能自己活过来"靠的就是它们。 -3. 事件处理器**只由 `net_poll` 驱动**,例外是 `net_reconcile_tick`(分级去抖 + 1s 冷却)可再入 `handle_disconnect_event` / `handle_connect_event`——对账是丢中断的自愈通道(Trap 22)。**注意职责边界(N-32 P2,N34 阶段 3 合并后)**:对账与事件处理器**只置事实**(置 CLOSED / 登记沉降),**重建监听的动作统一由 `net_reconcile_tick` 形态③④ 做**(本层唯一重建入口)——"谁先看到 DISCONNECT 就谁重建"的竞争在结构上消失。 +3. 事件处理器**只由 `net_poll` 驱动**,例外是 `net_reconcile_tick`(分级去抖 + 1s 冷却)可再入 `handle_disconnect_event` / `handle_connect_event`——对账是丢中断的自愈通道(TRAP-NET-17)。**注意职责边界(N-32 P2,N34 阶段 3 合并后)**:对账与事件处理器**只置事实**(置 CLOSED / 登记沉降),**重建监听的动作统一由 `net_reconcile_tick` 形态③④ 做**(本层唯一重建入口)——"谁先看到 DISCONNECT 就谁重建"的竞争在结构上消失。 4. 回调 `event_cb` 在 **netTask 上下文**执行:回调内不得阻塞、不得发网络操作。 5. **模块归属**:`T1`(D10 重试)/ `RLS` 的 `net_listener_try` / `ST`(settle)在 `net_listener.c`(真机专属、模拟器不编译);其余 tick(含合并后的 `T3` reconciler)与 `_locked` 在 `net_socket.c`。 @@ -109,7 +109,7 @@ flowchart TB | 函数 | 设计作用 | 主要调用 | 意图 / 注意 | |---|---|---|---| -| `net_init(ip,mask,gw,mac)` | 芯片一次性配置:探测/复位 → 参数 → MAC → 协议栈初始化 → 8 Socket 缓冲布局 → 使能 PING | `net_pending_cancel_all` → `ch395f_check_exist` / `ch395f_reset` / `set_fun_para(0x08)` / `set_tcp_mss` / `set_retrans_count+period` / `set_keepalive_idle+intvl+cnt` → `set_mac_addr` → `parse_ip_string` + `set_ip_addr` / `set_gwip_addr` / `set_mask_addr`(或 `set_dhcp`)→ `ch395f_init` → `set_send_buf` / `set_recv_buf` ×8 → `ping_enable` | ① **重建前**先取消全部挂起 recv 并逐一通知(不通知 = 调用方永久挂死),且必须早于控制块清零与芯片复位;② 重传/KeepAlive/**MAC**/IP/GW/MASK 参数**必须**在 `ch395f_init` 之前下发(`docs/芯片手册/CH395F.md` §9.2.1:SET_MAC 为步骤①,INIT_CH395 加载 MAC——顺序反了本次上电不生效,P1-7);③ `fun_para=0x08`:bit3 SOCKET_CLOSE(DISCONNECT/TIMEOUT 后不清缓冲,软件排空后手动关);bit0/bit1 保持 0——8 个 Socket 由索引 `_SN` 寻址 + 缓冲显式分配实现,与 FUN_PARA 无关(bit0 是保留位,必须为 0);④ 缓冲布局每 Socket 6 块(收 4 + 发 2)×8 = 48 块恰好用尽,Socket 0~7 必须显式重分配(Trap 01/02);⑤ 捕获 `xTaskGetCurrentTaskHandle` 存入 `s_net_task_handle`(防自通知死锁的判据) | +| `net_init(ip,mask,gw,mac)` | 芯片一次性配置:探测/复位 → 参数 → MAC → 协议栈初始化 → 8 Socket 缓冲布局 → 使能 PING | `net_pending_cancel_all` → `ch395f_check_exist` / `ch395f_reset` / `set_fun_para(0x08)` / `set_tcp_mss` / `set_retrans_count+period` / `set_keepalive_idle+intvl+cnt` → `set_mac_addr` → `parse_ip_string` + `set_ip_addr` / `set_gwip_addr` / `set_mask_addr`(或 `set_dhcp`)→ `ch395f_init` → `set_send_buf` / `set_recv_buf` ×8 → `ping_enable` | ① **重建前**先取消全部挂起 recv 并逐一通知(不通知 = 调用方永久挂死),且必须早于控制块清零与芯片复位;② 重传/KeepAlive/**MAC**/IP/GW/MASK 参数**必须**在 `ch395f_init` 之前下发(`docs/芯片手册/CH395F.md` §9.2.1:SET_MAC 为步骤①,INIT_CH395 加载 MAC——顺序反了本次上电不生效,P1-7);③ `fun_para=0x08`:bit3 SOCKET_CLOSE(DISCONNECT/TIMEOUT 后不清缓冲,软件排空后手动关);bit0/bit1 保持 0——8 个 Socket 由索引 `_SN` 寻址 + 缓冲显式分配实现,与 FUN_PARA 无关(bit0 是保留位,必须为 0);④ 缓冲布局每 Socket 6 块(收 4 + 发 2)×8 = 48 块恰好用尽,Socket 0~7 必须显式重分配(TRAP-NET-01/-02);⑤ 捕获 `xTaskGetCurrentTaskHandle` 存入 `s_net_task_handle`(防自通知死锁的判据) | | `net_get_generation()` | 读网络代次号(纯 RAM) | 无 | FTP/UI/对时服务据此感知"网络已重建"→ 放弃旧监听并重建 | | `net_generation_inc()` | 代次号 +1 | 无 | 唯一合法递增点 = `net_stack_bring_up()` 末尾(PHY/链路就绪后)——太早递增会让服务在网络不可用时抢跑重建;模拟器端恒为 0(无重建概念,上层自愈逻辑空转) | @@ -117,7 +117,7 @@ flowchart TB | 函数 | 设计作用 | 主要调用 | 意图 / 注意 | |---|---|---|---| -| `net_poll()` | 事件泵:GPIO 轮询 INT#(低=有待处理)→ `GET_GLOB_INT_STATUS_ALL`(读=清)→ 按位图逐 Socket 读 SINT → 分派 5 类事件;**每轮末尾**对在用 Socket 全量刷新**驱动状态缓存** + 步进 `ch395f_sock_reset_poll`(N34 阶段 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 08);SENBUF_FREE 只置 `send_ready` 不记事件;状态缓存**唯一刷新点**在此(reconciler 只读缓存,E2 实测单次回读 18.1µs);`sock_reset_poll` 步进使同步包装 600ms 放弃后的复位机不搁浅(IDLE 槽纯 RAM 判零 SPI) | +| `net_poll()` | 事件泵:GPIO 轮询 INT#(低=有待处理)→ `GET_GLOB_INT_STATUS_ALL`(读=清)→ 按位图逐 Socket 读 SINT → 分派 5 类事件;**每轮末尾**对在用 Socket 全量刷新**驱动状态缓存** + 步进 `ch395f_sock_reset_poll`(N34 阶段 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 后形态③④同轮即可重建**(N34 阶段 3 合并,原"pump 置于对账之后"的顺序依赖在单 tick 内自然成立) | ### 4.3 经队列的公共 API 及其 netTask 侧实现 @@ -128,8 +128,8 @@ flowchart TB |---|---|---|---|---|---| | `net_listen` | `NET_MSG_LISTEN` | `net_listen_locked` | `set_proto_type` + `set_sour_port` + `open_socket`(**open 重试 3×,失败走 `net_sock_reset_sync` 确定性排渣**,N34 阶段 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` 形态③④(N34 阶段 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 14/17);目的 IP 字节序写反 = CONNECT TIMEOUT(Trap 16);返回 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 20:写穿 FIFO = 数据错位);`send_ready` 门控用 `net_poll` 轮询而非 osDelay;短写**必须**告警——静默截断会让对端永久等待剩余字节(2026-09-21 实证:UI 推流应答截断 → 客户端失联) | +| `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_recv` | `NET_MSG_RECV`(timeout=0→**无限等待**,N34 阶段 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` 路径转正的具名入口(N34 阶段 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 头阻塞修复配套 + **显式预算**入口(N34 阶段 1.5,等价 Python settimeout,无隐藏默认值):调用方把长等待切成有界片(如剩余预算),**总超时语义不变、单次占用有界**;`timeout_ms=0` 直接拒(0 = 无限等待,与本接口"有界"存在理由相反);超时**只凭 -2 判定、不得依赖 errno**(`s_net_errno` 是跨任务共享全局量,随时被别的任务覆盖) | @@ -177,11 +177,11 @@ 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 23 沉降门**:带沉降标记的 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_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_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 23:中止/异常断开时芯片 RX 缓冲残留上一条连接的数据(实测 ~2048B),不排空会被下一条连接当自己的数据读走(静默截断);分块读避免新增静态缓冲(内存预算紧张) | +| `net_drain_chip_rx` | 排空芯片 RX 残留(256B 分块,≤16 次 = 4KB 上限) | `get_recv_len` + `read_recv_buf` | TRAP-NET-18:中止/异常断开时芯片 RX 缓冲残留上一条连接的数据(实测 ~2048B),不排空会被下一条连接当自己的数据读走(静默截断);分块读避免新增静态缓冲(内存预算紧张) | ### 4.7 监听重建(原 N-32 监听器 pump;N34 阶段 3 已并入 `net_reconcile_tick` 形态③④/U11) @@ -203,7 +203,7 @@ flowchart TB > `session_dead` **不在此清零**(与 `net_relisten_locked` 同一契约:重建监听 ≠ 应用已确认会话结束;清零只发生在 `handle_connect_event`)。 -### 4.8 Trap 22 周期对账 + 监听重建(合一 `net_reconcile_tick`,丢中断自愈,`net_socket.c`) +### 4.8 TRAP-NET-17 周期对账 + 监听重建(合一 `net_reconcile_tick`,丢中断自愈,`net_socket.c`) `net_reconcile_tick()`(`net_process_messages` 每轮调用;周期 **100ms**,`NET_RECONCILE_PERIOD_MS`;N34 阶段 3 起含原 pump 形态③④/U11): @@ -231,9 +231,9 @@ flowchart TB |---|---|---|---| | `handle_connect_event` | LISTENING → standalone accept(回读对端地址 → ESTABLISHED + `send_ready=1` + 清残留 TIMEOUT + 回调);CONNECTING → 客户端连接成功(回读对端 + 使能 KeepAlive + 回调) | `ch395f_get_remot_ipp` + `get_sock_int_status` +(client)`set_keepalive_enable` + `fire_event` | GET_REMOT_IPP 为**小端**输出(与 set_des_ip 的大端相反)——解析别抄反;`session_dead` 的**唯一清零点** | | `handle_disconnect_event` | 状态机迁移 + 陈旧事件甄别 + **登记沉降事实**(不再自己重建) | `ch395f_get_socket_status`(事件路径**主动 SPI**,不用缓存——缓存刷新在 net_poll 末尾,事件路径上必然滞后)→(LISTENING:只回读留痕,监听失位交 **reconciler 形态③** 重建;仍在监听 → 判陈旧丢弃)/(ESTABLISHED:芯片仍 ESTABLISHED → 判陈旧丢弃;F-NET-17 M3 可疑门 → 置可疑电平让位对账①) | ① **陈旧 DISCONNECT 甄别**(D20):同一 fd"关闭→重监听→再连接"循环复用,上一条连接的 FIN/ACK 可能在 fd 已复用时才上报 → 不甄别则新连接秒断(实测:FTP 同会话第二个数据连接必 425);② 正常释放(FIN)→ **GRACEFUL 沉降 100ms**(只置事实,重建由 `net_reconcile_tick` 形态③④ 做,N-32 P1/P2 + N34 阶段 3);③ 置 CLOSED 后**不立即关芯片 Socket**——留给 `net_recv_locked` 排空剩余数据(FIN 可能先于数据到达) | -| `handle_recv_event` | 置 `readable` + 回调;**不读 recv_len** | `fire_event` | 不读长度是为了避免与 `net_recv_locked` 的 `ch395f_get_recv_len` 竞争(Trap 13:分批读会归零);`readable` 供 `net_select` 无 SPI 访问判读 | +| `handle_recv_event` | 置 `readable` + 回调;**不读 recv_len** | `fire_event` | 不读长度是为了避免与 `net_recv_locked` 的 `ch395f_get_recv_len` 竞争(旧"分批读归零"机理,未证实);`readable` 供 `net_select` 无 SPI 访问判读 | | `handle_send_event` | 发送缓冲已空:触发回调(`send_ready` 由 SENBUF_FREE 前置位) | `fire_event` | — | -| `handle_timeout_event` | CONNECTING → CREATED(连接超时);**ESTABLISHED → 登记隔离 + CLOSED**(Trap 23);LISTENING → 仅记日志 | **`net_listener_settle(fd, NET_LISTENER_SETTLE_QUARANTINE)`** + `fire_event` | TIMEOUT = 异常释放(如客户端中止上传):对端在途数据随后落进 RX 缓冲 → 必须隔离(400ms)后才许复监听;正常 FIN 释放走 disconnect 路径(GRACEFUL 100ms),**不走**隔离 | +| `handle_timeout_event` | CONNECTING → CREATED(连接超时);**ESTABLISHED → 登记隔离 + CLOSED**(TRAP-NET-18);LISTENING → 仅记日志 | **`net_listener_settle(fd, NET_LISTENER_SETTLE_QUARANTINE)`** + `fire_event` | TIMEOUT = 异常释放(如客户端中止上传):对端在途数据随后落进 RX 缓冲 → 必须隔离(400ms)后才许复监听;正常 FIN 释放走 disconnect 路径(GRACEFUL 100ms),**不走**隔离 | | `fire_event` | 调用已注册回调 | `p_sock->event_cb` | **回调上下文 = netTask**:回调内不得阻塞、不得发网络操作(会自等死锁) | ### 4.10 工具函数 @@ -295,7 +295,7 @@ sequenceDiagram Note over N: close / 网络重建 / 陈旧 resync → 显式 settle/cancel(必通知) ``` -### 5.3 断连 → 自动重监听(含 Trap 23 沉降) +### 5.3 断连 → 自动重监听(含 TRAP-NET-18 沉降) ```mermaid sequenceDiagram @@ -312,12 +312,12 @@ sequenceDiagram end Note over N: 只置事实 —— 重建统一由 net_reconcile_tick 形态③④(100ms)决定 N->>C: net_listener_try → net_relisten_locked - opt 沉降期内(Trap 23) + opt 沉降期内(TRAP-NET-18) N->>C: 排空 RX 残渣(256B 分块)+ close + 排空中断 C-->>N: 未到 CLOSED/TCP_CLOSED → 返回(30ms 后重试轮询复检) end N->>C: set_proto_type + set_sour_port + open_socket + tcp_listen - alt open 失败(Trap 09 沉降) + alt open 失败(TRAP-NET-09 沉降) N->>N: 登记重试(30ms 节奏 / 2s 时限)→ net_listener_retry_tick 到点重试 else 成功 N->>N: state = LISTENING(auto_relisten 保留) @@ -326,7 +326,7 @@ sequenceDiagram Note over N: 2s 超限时 → 清状态 + "relisten give up"(错误日志,应用可感知) ``` -### 5.4 丢中断自愈(Trap 22 对账 + 监听重建合一 reconciler,N34 阶段 3) +### 5.4 丢中断自愈(TRAP-NET-17 对账 + 监听重建合一 reconciler,N34 阶段 3) ```mermaid sequenceDiagram @@ -367,14 +367,14 @@ sequenceDiagram | # | 铁律 | 实证来源 | |---|---|---| -| 1 | **一切芯片访问收敛 netTask**(唯一执行者);跨任务调用走队列 | Trap 16:跨任务 SPI 竞争破坏芯片寄存器 → 连接必超时 | -| 2 | **结果只经任务通知值回传**;消息按值入队,调用方看不到 netTask 对消息的写入;勿给消息加"回传用"字段 | Trap 10:6 个 API 永远返回 -1;"加 msg.err 字段"的修法曾致 `ui_net_sink` 每次空轮询误判真错误 → 连上即断 | +| 1 | **一切芯片访问收敛 netTask**(唯一执行者);跨任务调用走队列 | TRAP-NET-14:跨任务 SPI 竞争破坏芯片寄存器 → 连接必超时 | +| 2 | **结果只经任务通知值回传**;消息按值入队,调用方看不到 netTask 对消息的写入;勿给消息加"回传用"字段 | TRAP-NET-10:6 个 API 永远返回 -1;"加 msg.err 字段"的修法曾致 `ui_net_sink` 每次空轮询误判真错误 → 连上即断 | | 3 | **netTask 不为任何人自旋等待**:阻塞 recv = 挂起登记(L3);重开失败 = 重试登记(D10);初次 listen/connect 的 open 重试环(`net_sock_reset_sync` 有界等待,GRACEFUL 100ms 底 / 600ms 封顶)是唯一豁免(启动期无竞争,N34 阶段 3 起原 3×500ms 盲等已换确定性排渣) | E-02:一次空闲 30s recv 冻住全网 17.7s;旧重监听 osDelay(500) → 恢复 588ms、期间重连被拒 | | 4 | **netTask 不向自身发消息 / 不自我挂起**(挂起登记对执行任务自身发起直接拒绝) | 自等自通知 = 死锁 | | 5 | **基于状态读的重建动作必须去抖 + 冷却**:分级门槛 ②=1 轮 / ③④⑤=2 轮 / ①=4 轮(100ms/轮)+ 冷却 1s | 2026-09-23:无去抖 → 监听反复重建直至 `relisten give up`,三服务口全死 | -| 6 | **异常释放(TIMEOUT)后复监听前必须隔离**:排空 RX 残渣 + 等 CLOSED/TCP_CLOSED;隔离上限 < 重试放弃时限 | Trap 23:~2KB 残渣被下条连接当自己的数据 → 首笔上传截断 2048B 并误报 226 成功;上限=2s 时隔离耗尽重试预算 → 监听被弃 | -| 7 | **RECV 电平触发中断每轮只处理一批** | Trap 08:连续循环 → 主循环饿死 | -| 8 | **close 前先了结挂起 recv + 排空残留 DISCONNECT** | Trap 14/17/19:锁存的陈旧 DISCONNECT 秒断下一连接 | +| 6 | **异常释放(TIMEOUT)后复监听前必须隔离**:排空 RX 残渣 + 等 CLOSED/TCP_CLOSED;隔离上限 < 重试放弃时限 | TRAP-NET-18:~2KB 残渣被下条连接当自己的数据 → 首笔上传截断 2048B 并误报 226 成功;上限=2s 时隔离耗尽重试预算 → 监听被弃 | +| 7 | **RECV 电平触发中断每轮只处理一批** | TRAP-NET-08:连续循环 → 主循环饿死 | +| 8 | **close 前先了结挂起 recv + 排空残留 DISCONNECT** | TRAP-NET-12:锁存的陈旧 DISCONNECT 秒断下一连接 | | 9 | **两段式等待防丢失通知**:零等待拾取 + 阻塞;清除掩码全 1;以 pdTRUE 判定 | run3~11:丢失通知 → 挂死 / 结果错位;0 结果误判"无通知" → 永久挂死 | | 10 | **网络重建先取消并通知全部挂起,再复位芯片** | 取消不通知 = 调用方永久挂死 | | 11 | **监听重建只有一个所有者**(`net_reconcile_tick` 形态③④,N34 阶段 3 合并后;原 `net_listener_pump` 已删);其它位置**只置事实**(置 CLOSED / 登记沉降),不做重建动作 | N-32 P2 / B10:多所有者导致"谁先看到 DISCONNECT 就谁重建"的竞争与重建风暴 | @@ -395,11 +395,11 @@ sequenceDiagram | `ch395f_get_glob_int_status_all` | **仅 `net_poll`**(F-NET-7 / B3:全局寄存器唯一读者;`net_listen_locked` / `net_connect_locked` / `net_close_locked` / `net_relisten_locked` 的原全局读清已全部删除) | **读 = 清**(中断清除机制,无按位手段)——其他位置读清会焊死 net_poll 的按 Socket 门控 | | `ch395f_get_sock_int_status` | `net_poll`(逐 Socket SINT);各 open 路径后清残留;事件处理器清 TIMEOUT | **读 = 清** | | `ch395f_sock_events` | `net_connect_locked`(open 后清残留,F-NET-7 护栏原语);`net_close_locked`(芯片已 CLOSED 快路径只清本 Socket 残留事件) | 只碰**本 Socket** SINT 的护栏原语 | -| `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_*`(N34 阶段 3) | Trap 09:close 后需沉降才能重开 | +| `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_*`(N34 阶段 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 20) | -| `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` **故意不读**(防竞争,Trap 13) | +| `ch395f_write_send_buf` | `net_send_locked` / `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 判据) | N34 阶段 3:芯片状态唯一事实源;E2 实测单次回读 18.1µs,缓存新鲜度 ≤1ms | | `ch395f_sock_reset_begin` / `ch395f_sock_reset_poll` | `net_sock_reset_sync`(`net_listen_locked` / `net_connect_locked` open 重试、`net_close_locked`、`net_drop_conn_locked`);`net_poll` 每轮对在用 Socket **步进**(IDLE 槽纯 RAM 判零 SPI) | 驱动复位状态机:读空 RX → CLOSE → 排空事件(quiescent) → 等 byte0=00H;内置 LISTEN 态二段 CLOSE 补发(E2 补充发现);GRACEFUL 100ms 底 / QUARANTINE 400ms 底 | @@ -428,7 +428,7 @@ sequenceDiagram - `docs/网络/BSD_Socket_API_使用指南.md` —— 公开 API 语义、示例、常见问题(本文的上半部) - `docs/子系统设计/网络Socket监听生命周期设计说明.md` —— **监听生命周期权威设计**(N-32:边沿→电平、单一所有者重建(N34 阶段 3 起为 `net_reconcile_tick` 形态③④)、`net_listener.c` 模块化边界 §10) -- `docs/问题记录/CH395F_Trap_Records.md` —— Trap 01~23 机制细节(本文引用处) +- `docs/问题记录/CH395F_Trap_Records.md` —— TRAP-NET-01~23 机制细节(本文引用处) - `docs/网络/Net层阻塞模型架构设计.md` —— 阻塞等待模型的现状架构设计(pending 请求模型 / 五路完成语义) - `docs/问题记录/错误码传播审计.md` —— E-02 头阻塞与错误码回传全审计 - `docs/测试说明/链路延迟测试说明.md` —— D1/D10/L3/D14 等时延探针与自愈路径的台架证据 diff --git a/docs/网络/N34-自愈审计/CH395F驱动层审计.md b/docs/网络/N34-自愈审计/CH395F驱动层审计.md index 45afaf54..c8b74a92 100644 --- a/docs/网络/N34-自愈审计/CH395F驱动层审计.md +++ b/docs/网络/N34-自愈审计/CH395F驱动层审计.md @@ -62,7 +62,7 @@ netTask 手里抢走(正是 B3 事故的形状,见 F-DRV-2)。缺的不是 ## 2 Trap 根因归类总表 -台账共 **24 条**(编号 01–25,**Trap 19 从未存在**,属悬空引用)。根因类别: +台账共 **24 条**(旧编号 01–25,**旧 19 从未存在**,属悬空引用)。根因类别: **(a)** 芯片真实行为(不可在固件侧根治);**(b)** 固件寄存器值/常量错误;**(c)** 引用错误 (字节序/偏移/索引/单位);**(d)** 时序误用(顺序/忙等/沉降);**(e)** 设计缺口;**(f)** 判读或台架假象。 @@ -76,29 +76,29 @@ netTask 手里抢走(正是 B3 事故的形状,见 F-DRV-2)。缺的不是 | 06 | KeepAlive 非 500ms 倍数 → 内部定时器异常 | (a) | net_config.h:61-71 + net_socket.c:288-290 | 否 | 是 | | 07 | 只 disconnect 不 close → 重开必返 `0x20` | (f)→(a) | 原判"FIN_WAIT_2/RST"已证伪;真因 `FUN_PARA` bit3=1 芯片不自动关 Socket。轮询 **byte0**:net_socket.c:2326-2345 | 间接(催生"close→等 CLOSED→排空"序列) | 是(bit3=1 契约) | | 08 | RECV 中断无限触发 | (a) | net_socket.c:429(`do{}while(0)` 单批处理) | 否 | 是 | -| 09 | PHY link up 同一时刻 `open_socket` 失败 | (a)+(d) | 根因侧已修:就绪屏障 net_task.c:232 + 启动期等 PHY link net_task.c:118-130 | **是**(D10 层) | **部分**:启动形态已被根因消除;close→reopen 沉降仍是芯片行为(Trap 25 实证窗口不定长)→ 重试保留,**应用层重复重试可删** | +| 09 | PHY link up 同一时刻 `open_socket` 失败 | (a)+(d) | 根因侧已修:就绪屏障 net_task.c:232 + 启动期等 PHY link net_task.c:118-130 | **是**(D10 层) | **部分**:启动形态已被根因消除;close→reopen 沉降仍是芯片行为(TRAP-NET-20 实证窗口不定长)→ 重试保留,**应用层重复重试可删** | | 10 | 6 个 API 永远返回 -1(消息队列按值拷贝) | (e) | 错误码改经 `xTaskNotify` 通知值回传 | 否 | — | | 11 | accept 拒绝 standalone_accept 的 ESTABLISHED | (e) | net_accept_locked(net_socket.c:1281-1342) | 否 | — | | 12 | FatFs `FR_INVALID_NAME` | 非 CH395F | lftpd_io.c | 否 | — | | 13 | 分批读后 `recv_len` 归零 | (f) 存疑 | 无(机理未证实,与手册 5.34 及本仓 TC-NET-201 相反) | 否 | **约束已事实作废**(见 F-DRV-11) | -| 14 | close 后 DISCONNECT 滞后锁存,污染下次首读 | (a) | 预清 + 反核实:net_socket.c:2326-2345 / :1137、net_listener.c:262、net_socket.c:1670 | **是** | 是(Trap 23 实测伪 DISCONNECT 仍投递给下一条连接) | +| 14 | close 后 DISCONNECT 滞后锁存,污染下次首读 | (a) | 预清 + 反核实:net_socket.c:2326-2345 / :1137、net_listener.c:262、net_socket.c:1670 | **是** | 是(TRAP-NET-18 实测伪 DISCONNECT 仍投递给下一条连接) | | 15 | DHCP 成功后 IP 寄存器提交滞后 | (a) | 主机侧判据修正(历史) | 否 | — | | 16 | TCP Client 目的 IP 字节序写反 → CONNECT TIMEOUT | (c) | net_socket.c:1636-1641 + :2691-2701(改大端),commit `db64e6ac` | 否 | 根因已修;**驱动 API 未封装字节序 → 复发面仍在**(F-DRV-5) | | 17 | client 重连被上一条连接的残留 DISCONNECT 秒断 | (a) | 预清 + 反核实:net_socket.c:1660-1726 | **是** | 是 | -| 18 | "bit0=1 才是多 Socket 模式" | (f) 伪根因 | net_socket.c:270 回到 `0x08`(commit `d758d5c`);真正起作用的是 Trap 02 的缓冲分配 | 否 | 驱动侧残留:语义宏未被使用(F-DRV-6) | +| 18 | "bit0=1 才是多 Socket 模式" | (f) 伪根因 | net_socket.c:270 回到 `0x08`(commit `d758d5c`);真正起作用的是 TRAP-NET-02 的缓冲分配 | 否 | 驱动侧残留:语义宏未被使用(F-DRV-6) | | 19 | — | — | **不存在**(悬空引用) | — | — | | 20 | 单次写 >1KB 写穿发送 FIFO → 数据错乱/卡死 | (c) | ch395f.h:138-141 + net_socket.c:1745/1788/2717(`NET_SEND_CHUNK_MAX`) | 否 | 根因已修;驱动只告警不拦截(F-DRV-7) | | 21 | 接收缓冲"恰好填满+排空"窗口冻结 | (f) 非缺陷 | 台架自吞数据 + 判读工具误判;保留金丝雀 ch395f.c:1176-1189 与布局解耦 ch395f.h:130-153 | 否 | — | -| 22 | 快速复连丢 CONNECT/DISCONNECT → 监听静默死亡 | (a)(B3 曾自造同类症状,commit `4f2cc8b` 修) | net_reconcile_tick net_socket.c:548-641 | **是**(对账层) | **是**(Trap 25 裸机台架独立实证芯片丢中断);但命中数应重测 | +| 22 | 快速复连丢 CONNECT/DISCONNECT → 监听静默死亡 | (a)(B3 曾自造同类症状,commit `4f2cc8b` 修) | net_reconcile_tick net_socket.c:548-641 | **是**(对账层) | **是**(TRAP-NET-20 裸机台架独立实证芯片丢中断);但命中数应重测 | | 23 | 上传中止后首笔复连被静默截断(≤2KB) | (a)+(e) | net_listener.c:102-121(排空)+ :149-204(隔离段);登记点 net_socket.c:3430/:3523 | **是**(隔离排渣层) | 是 | | 24 | MAC 写入顺序违反手册(SET_MAC 须在 INIT 前) | (d) | net_socket.c:292-299(commit `de1b50e`) | 否 | 已根治 | | 25 | FTP 数据连接三连:丢 CONNECT / accept 竞态自毁缓冲 / 背靠背小段毒化 TX | ①(a) ②(e) ③(a) | ①探针 net_lost_connect_probe net_socket.c:1228-1267 + 幽灵甄别 `last_peer_port`;②closed-with-data accept net_socket.c:1306-1334(**我方 bug,已根治**);③批量发送 lftpd.c:107-155 | **是** | ①③是;②属契约实现,保留 | **归类小结**(按定案后的主因计,跨类条目只计一次):(a) 芯片行为 12 条、(c) 引用/数值错误 4 条、 -(f) 判读假象 3 条、(d) 时序误用 2 条、(e) 设计缺口 2 条;另 Trap 12 非 CH395F、Trap 19 不存在。 +(f) 判读假象 3 条、(d) 时序误用 2 条、(e) 设计缺口 2 条;另旧 12 非 CH395F、旧 19 不存在。 -**催生五层自愈补丁的 6 条(09/14/17/22/23/25)中,只有 Trap 09 的 -"启动形态"和 Trap 25 的"②accept 竞态"被根因修复消掉;其余全部落在芯片行为上,不可从根上消除。** +**催生五层自愈补丁的 6 条(09/14/17/22/23/25)中,只有 TRAP-NET-09 的 +"启动形态"和 TRAP-NET-20 的"②accept 竞态"被根因修复消掉;其余全部落在芯片行为上,不可从根上消除。** ## 3 五层自愈 × 驱动层契约 @@ -106,9 +106,9 @@ netTask 手里抢走(正是 B3 事故的形状,见 F-DRV-2)。缺的不是 |---|---|---|---| | `auto_relisten`(监听常驻) | net_types.h:172;置位 net_socket.c:1119 | 无——芯片永不自动回 LISTEN(单连接复用模型),必须软件承诺 | **保留**(模型基石) | | D10 非阻塞重监听重试 | net_listener.c:77-79(30ms/2s)、:394-432 | 驱动只有**阻塞**的 `poll_cmd_status`(ch395f.c:654)→ 非阻塞重试是在 net 层绕开驱动阻塞原语 | **保留**,但真正的解法是驱动提供非阻塞命令完成查询(F-DRV-8) | -| Trap 22 周期对账 | net_socket.c:544-546(200ms/去抖 2/冷却 1s)、:548-641 | 驱动无状态缓存 → 对账只能每周期重新发 SPI | **保留**(根因是芯片丢中断);参数与 pump 重复,应收敛(F-DRV-1) | +| TRAP-NET-17 周期对账 | net_socket.c:544-546(200ms/去抖 2/冷却 1s)、:548-641 | 驱动无状态缓存 → 对账只能每周期重新发 SPI | **保留**(根因是芯片丢中断);参数与 pump 重复,应收敛(F-DRV-1) | | N-32 监听器 pump | net_listener.c:437-441(100ms/去抖 2/冷却 1s)、:465-553 | 同上 | **保留**(不变量 II 的落点,不是补丁) | -| Trap 23 排渣隔离 | net_listener.c:53-56(400/100/800ms)、:102-121、:149-204 | 排空 RX 的**机械**(`get_recv_len`+`read_recv_buf` 循环)住在 net 层;驱动只有裸 `clear_recv_buf`(且产品零调用) | **下沉驱动**(F-DRV-4) | +| TRAP-NET-18 排渣隔离 | net_listener.c:53-56(400/100/800ms)、:102-121、:149-204 | 排空 RX 的**机械**(`get_recv_len`+`read_recv_buf` 循环)住在 net 层;驱动只有裸 `clear_recv_buf`(且产品零调用) | **下沉驱动**(F-DRV-4) | ## 4 驱动层缺口清单(缺什么原语) @@ -146,14 +146,14 @@ net_listener.c:516(pump 100ms/去抖 2/冷却 1s,:437-441)。驱动 static net_socket.c:1122-1137 用注释收口("原为遍历 8 个 Socket 全清 + 再读清全局"→ 仅清本 Socket), git 考古链 `e862009bc`→`319d00e3`→`db64e6acf`(commit `4f2cc8b` 修)。 影响:同类事故(吞掉其它在用 Socket 的 CONNECT/DISCONNECT 边沿事件)可在新调用点复发; -这正是"看起来像 Trap 22 的自家 bug"的来源。 +这正是"看起来像 TRAP-NET-17 的自家 bug"的来源。 裁决:**下沉驱动**——提供 `ch395f_sock_events(sn)`(只读该 Socket,永不碰全局), 并把"全局寄存器唯一读者 = net_poll"写成驱动侧断言/文档契约。 **F-DRV-3 · 无类型化状态回读:`byte0 门控 byte1` 只活在注释里,3 处只判 byte1** 证据:ch395f.h:592-601 注释规定 `[1]` 仅在 `[0]`=OPEN 时有意义;违反口径的判读点 net_socket.c:592(`st[1]==ESTABLISHED` 未先看 `st[0]`)、:1237(探针同型)、:3410(陈旧 DISCONNECT 甄别同型)。 -Trap 07 定案更正第 3 条正是把这类读数读成"卡在某个 TCP 态"。 +TRAP-NET-07 定案更正第 3 条正是把这类读数读成"卡在某个 TCP 态"。 影响:幽灵态/孤儿态判定的正确性依赖每个调用点都记得这条规则——已三次失守。 裁决:**下沉驱动**——返回 `{open, tcp_state}` 结构 + `ch395f_sock_is_listening()/is_established()` 谓词, `open=0` 时 TCP 态返回"无意义"哨兵值。 @@ -166,8 +166,8 @@ net_listener.c:149-204(排空 RX→close→读 SINT→读全局→读状态→ 裁决:**下沉驱动**——`ch395f_sock_reset(sn, mode, wait_ms)`(mode=异常/正常), net 层只选 mode。与 T1 的 F-NET-9 同判,签名归 T4 定。 -**F-DRV-5 · IP 字节序契约未封装,且驱动注释与实测相反(Trap 16 的复发面仍在)** -证据:Trap 16 根因是照抄手册"所有 IP 命令低字节在前"的总括,实测 SET_IP/SET_DES_IP 收**点分序**; +**F-DRV-5 · IP 字节序契约未封装,且驱动注释与实测相反(TRAP-NET-14 的复发面仍在)** +证据:TRAP-NET-14 根因是照抄手册"所有 IP 命令低字节在前"的总括,实测 SET_IP/SET_DES_IP 收**点分序**; 修复落在 net 层(net_socket.c:1636-1641、:2691-2701,commit `db64e6ac`)。但驱动侧: `ch395f_set_des_ip` 头注释完全不写字节序(ch395f.h:479-484),`set_ip_addr/set_gwip/set_mask` 仍写"低字节在前"(ch395f.h:380/:387/:394),而 `get_remot_ipp` 是**小端**(ch395f.h:612)。 @@ -178,13 +178,13 @@ net 层只选 mode。与 T1 的 F-NET-9 同判,签名归 T4 定。 **F-DRV-6 · FUN_PARA 语义宏是死代码,net_init 传裸 `0x08`** 证据:ch395f.h:204-206 定义 `CH395F_FUN_PARA_TCP_SERVER/SOCKET_CLOSE/DISABLE_SEND_OK`, 全仓零引用(grep 仅命中定义处);net_socket.c:270 写 `ch395f_set_fun_para(0x08)`。 -影响:Trap 18 的"bit0=1 多 Socket 模式"伪根因能存活一年多,正是因为语义没有类型化到宏/枚举层, +影响:TRAP-NET-15 的"bit0=1 多 Socket 模式"伪根因能存活一年多,正是因为语义没有类型化到宏/枚举层, 只能靠注释与手册对质。 裁决:**简化**——net_init 改用宏组合(`CH395F_FUN_PARA_SOCKET_CLOSE`),删掉未使用的 `CH395F_GINT_STAT_SOCK0..3`(net_poll 用 `1U<<(i+4)`,net_socket.c:464)。 **F-DRV-7 · 驱动对超限写只告警不拦截——现状正确,但契约必须显式化** -证据:ch395f.c:1183-1189 明确"超出不会被命令拒绝,而是写穿发送缓冲(Trap 20)→ 只告警不拦截, +证据:ch395f.c:1183-1189 明确"超出不会被命令拒绝,而是写穿发送缓冲(TRAP-NET-16)→ 只告警不拦截, 分块责任在调用方";分块上限 `NET_SEND_CHUNK_MAX` 在 net 层(net_socket.c:1745,应用于 :1788/:2717)。 影响:若驱动内自动分块,需在分块间等 SENBUF_FREE,而该事件由 net_poll 集中读清 → 驱动内等待会自锁。 裁决:**保留**(现状合理);把"单次写 ≤ `CH395F_SEND_BUF_SIZE_BYTES`"从注释升格为 @@ -203,7 +203,7 @@ net 层只选 mode。与 T1 的 F-NET-9 同判,签名归 T4 定。 证据:net_socket.c:1092-1104(`net_listen_locked` 3×`osDelay(500)`)、:1647-1659 (`net_connect_locked` 同型);应用层 lftpd_inet.c:39-70(5 轮,`vTaskDelay(2000)`)、 lftpd.c:473-477(3 轮 ×200ms)。 -影响:Trap 09 的"启动形态"已被就绪屏障 + 启动期等 PHY link 消除(net_task.c:118-130/:232), +影响:TRAP-NET-09 的"启动形态"已被就绪屏障 + 启动期等 PHY link 消除(net_task.c:118-130/:232), 应用层重试的原始理由已不存在;保留它只会把恢复时间拉长并制造额外的重建所有者。 裁决:**简化**——net 层两处并入 D10(P2b);应用层两处**可删**(保留 lftpd 的重试前先确认 `net_relisten` 语义已覆盖,见 T1 的裁决)。 @@ -214,13 +214,13 @@ lftpd.c:473-477(3 轮 ×200ms)。 影响:换料/降版本时 `0x08` 静默失效,而整套排空契约(bit3=1)正是建立在 bit3 生效之上。 裁决:**简化**——`net_init` 内门控(版本不足则拒绝置 bit3 并留自检记录),一行判定消掉整类静默。 -**F-DRV-11 · Trap 13 的"必须一次读空整个接收缓冲"约束已事实作废,但注释仍在四处约束后人** -证据:ch395f.h:146("接收方向需一次性读空整个 Socket 接收缓冲…避免 Trap 13 部分读导致剩余数据不可见")、 +**F-DRV-11 · 旧机理的"必须一次读空整个接收缓冲"约束已事实作废,但注释仍在四处约束后人** +证据:ch395f.h:146(旧注释"接收方向需一次性读空整个 Socket 接收缓冲…避免部分读导致剩余数据不可见")、 net_socket.c:1908/:1969/:3466/:3470。而 `net_recv_locked` 常态按 `min(len, recv_len)` 部分读 -(net_socket.c:1882-1952),Trap 21 结案已撤回"部分排空致命",Trap 23 回归 10/10+5/5 全在部分读下通过。 +(net_socket.c:1882-1952),TC-NET-604 结案已撤回"部分排空致命",TRAP-NET-18 回归 10/10+5/5 全在部分读下通过。 影响:注释与代码事实相反;`CH395F_SPI_DMA_BUF_SIZE = 2048+4` 的 sizing 理由(ch395f.h:143-153) 仍挂在一条未证实的机理上。 -裁决:**可删**(注释级)——把 Trap 13 降级为"未证实的历史观察",补一条"部分读→再查 recv_len" +裁决:**可删**(注释级)——把旧机理降级为"未证实的历史观察",补一条"部分读→再查 recv_len" 定向 TC 结案;DMA 缓冲 sizing 改以"接收缓冲 2KB"为唯一理由。 **F-DRV-12 · 死声明与零调用 API 清单(驱动 API 面虚胖)** @@ -230,10 +230,10 @@ net_socket.c:1908/:1969/:3466/:3470。而 `net_recv_locked` 常态按 `min(len, `ch395f_tcp_disconnect`(ch395f.c:1145,产品零调用,仅 TC-NET-204 用例 ch395f_test_task.c:783)、 `ch395f_get_unreach_info`(:1402,全仓零调用)、`ch395f_clear_recv_buf`(:468,仅未注册的 ch395f_reuse_bench.c:169)。 -影响:API 面越大,"哪个是产品路径"越难判;Trap 07 的"disconnect→close"错误序列正是从 +影响:API 面越大,"哪个是产品路径"越难判;TRAP-NET-07 的"disconnect→close"错误序列正是从 一个零调用的 API 出发的。 裁决:**可删/登记**——删死宏与未实现命令声明;`tcp_disconnect` 保留但注释标注"产品路径禁用"; -`clear_recv_buf` 保留(Trap 25 已证"缓冲重分配无效",排渣用读空而非 CLEAR_RECV_BUF)。 +`clear_recv_buf` 保留(TRAP-NET-20 已证"缓冲重分配无效",排渣用读空而非 CLEAR_RECV_BUF)。 **F-DRV-13 · 链路事件静默:四个全局中断位读清后无人消费,驱动无链路缓存** 证据:net_socket.c:441 读全局后只按 `bit4..bit11` 分派 socket 事件(:464), @@ -268,15 +268,15 @@ gpio.c:170-171、stm32f4xx_it.c:391);消费节奏 = net_task.c:241-279 主 | 机制 | 能否因"驱动持事实"而删 | 理由 | |---|---|---| -| Trap 22 对账①② | **不能删,但可大幅收窄** | 根因是芯片丢中断(Trap 25 裸机台架独立实证)。驱动持缓存后,对账的"回读"部分变成读缓存;"发现失配"仍需存在,只是从"周期 SPI 轮询"降为"每 poll 周期一次刷新 + 比对" | +| TRAP-NET-17 对账①② | **不能删,但可大幅收窄** | 根因是芯片丢中断(TRAP-NET-20 裸机台架独立实证)。驱动持缓存后,对账的"回读"部分变成读缓存;"发现失配"仍需存在,只是从"周期 SPI 轮询"降为"每 poll 周期一次刷新 + 比对" | | N-32 pump | **不能删** | pump 是"单一重建所有者"(不变量 II),是架构解不是补丁;驱动持事实后它的判据读缓存,动作仍在 net 层(策略) | | D10 重试 | **可降级** | 若驱动提供非阻塞 `cmd_done()` + `sock_reset()` 内联有界等待(F-DRV-4/8),独立 30ms/2s 定时器可退化为事务内重试(T3 同判) | -| Trap 23 排渣隔离 | **可下沉** | 排空/等待是纯芯片机械;net 层只保留"异常/正常"两档 mode | +| TRAP-NET-18 排渣隔离 | **可下沉** | 排空/等待是纯芯片机械;net 层只保留"异常/正常"两档 mode | | 探针 `net_lost_connect_probe` | **不能删,可去抖简化** | 丢 CONNECT 是芯片行为;驱动持事实后去抖可去掉(读缓存无瞬态噪声),保留"状态已 ESTABLISHED 而本层 LISTENING → 补 accept" | | `auto_relisten` | **不能删** | 芯片永不自动回 LISTEN,这是模型承诺 | **一句话结论**:五层里有四层(对账、pump、探针、resync)存在的直接理由是"驱动不持事实、net 层只能自建镜像"; -把它们删掉的前提不是"根因已修",而是"驱动成为唯一事实源"。Trap 23 的排渣隔离则相反—— +把它们删掉的前提不是"根因已修",而是"驱动成为唯一事实源"。TRAP-NET-18 的排渣隔离则相反—— 它的根因(在途数据落 RX)不可修,但它的**实现位置**应当从 net 层下沉到驱动。 ## 7 对 T4 汇总的输入 @@ -284,7 +284,7 @@ gpio.c:170-171、stm32f4xx_it.c:391);消费节奏 = net_task.c:241-279 主 1. 驱动层最该补的三个原语(按收益/成本排序): ① 类型化状态回读 + 每 socket 缓存(F-DRV-1/3);② `ch395f_sock_reset()` 复合复位(F-DRV-4); ③ 非阻塞 `ch395f_cmd_done()`(F-DRV-8)。三者都不改芯片行为,只把事实与机械收进驱动。 -2. 立即可做、零风险的清理:F-DRV-6(用宏)、F-DRV-10(版本门控)、F-DRV-11(撤 Trap 13 注释约束)、 +2. 立即可做、零风险的清理:F-DRV-6(用宏)、F-DRV-10(版本门控)、F-DRV-11(撤旧"部分读归零"注释约束)、 F-DRV-12(删死声明)、F-DRV-5 的注释更正。 3. 需要重测的量化项:B3 修复(`4f2cc8b`)前后各跑 `:21 100ms×50`,统计 `reconcile:` 命中数, 量化"自家制造的丢中断"占比(与 T1 的 F-NET-8 同一实验,勿重复做)。 diff --git a/docs/网络/N34-自愈审计/README.md b/docs/网络/N34-自愈审计/README.md index 762025d2..ed2da464 100644 --- a/docs/网络/N34-自愈审计/README.md +++ b/docs/网络/N34-自愈审计/README.md @@ -1,7 +1,7 @@ # N34 自愈审计 · 汇总报告(T4) > 审计对象:BSD Socket API 的「芯片/本层状态对账与重监听自愈」五层机制(auto_relisten / D10 重试 / -> Trap 22 对账 / N-32 pump / Trap 23 排渣隔离)及其显式 API(`net_relisten` / `net_resync_listen`)。 +> TRAP-NET-17 对账 / N-32 pump / TRAP-NET-18 排渣隔离)及其显式 API(`net_relisten` / `net_resync_listen`)。 > 方法:三路独立审计汇总——net 层盘点(`docs/网络/N34-自愈审计/net层自愈机制盘点.md`,F-NET-1..14)、 > CH395F 驱动层审计(`docs/网络/N34-自愈审计/CH395F驱动层审计.md`,F-DRV-1..16)、 > 主流方案对照(`docs/网络/N34-自愈审计/主流方案对照.md`,F-MS-1..9,含 T4 更正两处:多连接模式未启用、 @@ -21,10 +21,10 @@ | `b8c6a3d` | **阶段 1** API 面收口(对账⑤ + 删 3 API + time_sync/lftpd) | E10:全绿回归(详见实验方案 E10) | | `13a4a40` `d346e4e` | KeepAlive 转正(`docs/芯片手册/CH395F.md` §5.56)+ **阶段 1.5** recv 语义主流化 | E11/E12 台架 | | `f7f1793` `83a7072` | **阶段 2** ch395f 五原语(只加不接线)+ E2/E3/E4 定案 | Phase 15 套件 20/20 | -| `6394853` `f51c083` | **阶段 3** net 层收敛:单一 reconciler + 忙等统一 + 镜像降级 + E-02 收口(`net_drop_conn`) | 终版回归全绿(Trap 22/23/25、E-02 双 PASS、104、对时、integrity 15/15) | -| `123d80a` | E6 插桩 `NET_U15_PROBE`(默认关,零成本) | 24h 挂机进行中(阶段 3 项 4 门禁) | +| `6394853` `f51c083` | **阶段 3** net 层收敛:单一 reconciler + 忙等统一 + 镜像降级 + E-02 收口(`net_drop_conn`) | 终版回归全绿(TRAP-NET-17/-18/-20、E-02 双 PASS、104、对时、integrity 15/15) | +| `123d80a` | E6 插桩 `NET_U15_PROBE`(默认关,零成本) | ✅ E6 已裁决(2026-10-13:命中=0 → 项 4 不立项,U15 观察项) | -阶段 0~3 已完成并转正;在途仅 **E6 挂机**(裁决项 4 U15 内化做不做)与延后项 +阶段 0~3 与 E6 均已完成并转正;仅剩延后项 (relisten 沉降段统一,见实施方案 §5.2)。阶段 1 实施过程中的一条关键台架发现 已留痕(实验方案 E10):**`net_listener_armed` 必须放行 ESTABLISHED**——单连接模型 的"待 accept"电平,漏放行时 i104(200ms 周期)抢不过对账② 的 ~400ms 去抖而整场 @@ -35,12 +35,12 @@ **问:「芯片/本层状态对账与重监听自愈」可以简化或删除吗?可以内聚到 ch395f 驱动层吗?** 1. **不能整体删除。** 催生五层自愈的 6 条 Trap(09/14/17/22/23/25)中,只有 2 个被根因修复消掉 - (Trap 09 的启动形态→就绪屏障;Trap 25 的②accept 竞态→closed-with-data)。其余全部落在**芯片行为**上: - Trap 25 的裸机芯片台架(剥离 OS/net/FTP)独立复现"握手完成但 CONNECT 中断被丢"——这是 + (TRAP-NET-09 的启动形态→就绪屏障;TRAP-NET-20 的②accept 竞态→closed-with-data)。其余全部落在**芯片行为**上: + TRAP-NET-20 的裸机芯片台架(剥离 OS/net/FTP)独立复现"握手完成但 CONNECT 中断被丢"——这是 "对账不可因固件修 bug 而删"的决定性证据(F-NET-8)。 2. **用户的假设部分成立,但打不中五层。** 24 条 Trap 根因归类:芯片行为 12、引用/数值错误 4、 判读假象 3、时序误用 2、设计缺口 2(F-DRV 总表)。寄存器数值/引用错误确实存在过 - (Trap 16 字节序、Trap 02 缓冲重叠、Trap 20 FIFO 上限、B3 全量读清),但它们**不是**五层自愈的成因—— + (TRAP-NET-14 字节序、TRAP-NET-02 缓冲重叠、TRAP-NET-16 FIFO 上限、B3 全量读清),但它们**不是**五层自愈的成因—— B3(全 Socket SINT 读清)只是"自家制造的丢中断"放大器,修复后对账仍必要,只是命中数应下降(可台架量化)。 3. **但"五层补丁"这个形态不是终态,可以大幅简化并部分内聚。** 结构性根源不是单连接复用模型 (ioLibrary 无 accept、CH395 每 Socket 一连接、FreeRTOS+TCP reuse 模式全部同形,F-MS-2/F-DRV-16), @@ -72,9 +72,9 @@ T1(net 层视角)判"机制主体保留、API 可删";T3(主流对照视 |---|---|---|---| | `auto_relisten` | net_types.h:172 | **保留**(模型基石) | 芯片永不自动回 LISTEN,监听常驻是软件承诺(P4) | | D10 `net_listener_retry_tick` | net_listener.c:394-432 | **短期保留+小修;长期降级** | 小修=补 pump 同型状态门(F-NET-10)+ give-up 文案(F-NET-14);长期=驱动 `cmd_done()` 后内联进事务、删独立定时器(F-DRV-8) | -| Trap 22 `net_reconcile_tick` | net_socket.c:548-641 | **功能保留,形态重构**(阶段 1 已做①扩展=⑤) | 与 pump 判据合并为**单一 reconciler**、读驱动缓存、一套去抖(F-DRV-1);①扩展覆盖"本层 ESTABLISHED+芯片已回 LISTEN"缺口(F-NET-2)✅ 已实施(bad=5,`b8c6a3d`) | +| TRAP-NET-17 `net_reconcile_tick` | net_socket.c:548-641 | **功能保留,形态重构**(阶段 1 已做①扩展=⑤) | 与 pump 判据合并为**单一 reconciler**、读驱动缓存、一套去抖(F-DRV-1);①扩展覆盖"本层 ESTABLISHED+芯片已回 LISTEN"缺口(F-NET-2)✅ 已实施(bad=5,`b8c6a3d`) | | N-32 `net_listener_pump` | net_listener.c:465-553 | **并入 `net_reconcile_tick`(阶段 3,2026-10-13)** | 唯一周期重建决策者=不变量 II **不变**,只是与对账合一为单一 100ms tick(形态③④/U11,判据读驱动缓存);T3"改内联"与 P2a 台架实测冲突(删消费者驱动→`[WD] task stall`)故消费者三处仍保留 | -| Trap 23 排渣隔离 | net_listener.c:102-121/:149-204 | **下沉驱动** | 排空/等 CLOSED 是纯芯片机械;与 net_close_locked:2326-2345 是同一契约两套变体(F-NET-9/F-DRV-4)→ 收口 `ch395f_sock_reset(sn, mode, wait_ms)`,net 层只选 mode | +| TRAP-NET-18 排渣隔离 | net_listener.c:102-121/:149-204 | **下沉驱动** | 排空/等 CLOSED 是纯芯片机械;与 net_close_locked:2326-2345 是同一契约两套变体(F-NET-9/F-DRV-4)→ 收口 `ch395f_sock_reset(sn, mode, wait_ms)`,net 层只选 mode | | `net_relisten` | net_socket.c:3080 | **删除** ✅ 已实施(`b8c6a3d`) | 生产零调用;对健康监听器无条件 open→USE→close,自毁 ~1s(footgun) | | `net_resync_listen` | net_socket.c:3026 | **删除**(先并入对账①)✅ 已实施(语义=对账⑤) | 语义正确但载体错位:应是自动对账,不是手动 API(F-NET-2/F-MS-4) | | `net_can_accept` | net_socket.c:1427 | **删除**(先迁 iec104 到 `net_listener_armed`)✅ 已实施 | 过期门控致 :2404 丢 CONNECT 探针从不运行(F-NET-4,与已修的 :7003 探针失效同型)。⚠ 迁移时 **armed 必须补 ESTABLISHED 放行**,否则 i104 整场饿死(实验方案 E10) | @@ -83,15 +83,15 @@ T1(net 层视角)判"机制主体保留、API 可删";T3(主流对照视 | `session_dead` / `released_abnormal` / 幽灵甄别 / closed-with-data | net_types.h | **保留** | 芯片行为的必要建模,P1 不变量 I | | `net_listen_locked` 3×osDelay(500) / `net_close_locked` 500ms 忙等 | net_socket.c:1092-1104/:2326-2345 | **简化**(P2b) | 根在驱动阻塞原语(F-DRV-8);先补 `cmd_done()` 再统一走 D10 | | 上层每 32 轮盲收(U15) | ui_net_sink.c:552-558、iec104_serv.c:246-253 | **中期下沉** | "RX 非空但 readable=0"并入 reconciler 后上层退役(F-NET-12) | -| time_sync 每会话 close 监听重建 | time_sync.c:182 | **简化(上层)** ✅ 已实施(`b8c6a3d`) | 违反自家"EOF 不得 close 监听 fd"铁律,主动制造 Trap 09 沉降窗(F-NET-13);改 lftpd 模式同 fd 回 accept。实测连续 6 轮推送正常(E10) | -| lftpd 应用层阻塞重试 | lftpd_inet.c:39-70、lftpd.c:473-477 | **删除/简化** ✅ 已实施 | Trap 09 启动形态已被就绪屏障消除,重试理由不存在(F-DRV-9)。实施偏差:未走 `net_listener_try` 登记(会把 BOUND/D10 状态语义一并拖入),改单次尝试+失败即 425(E10 登记) | +| time_sync 每会话 close 监听重建 | time_sync.c:182 | **简化(上层)** ✅ 已实施(`b8c6a3d`) | 违反自家"EOF 不得 close 监听 fd"铁律,主动制造 TRAP-NET-09 沉降窗(F-NET-13);改 lftpd 模式同 fd 回 accept。实测连续 6 轮推送正常(E10) | +| lftpd 应用层阻塞重试 | lftpd_inet.c:39-70、lftpd.c:473-477 | **删除/简化** ✅ 已实施 | TRAP-NET-09 启动形态已被就绪屏障消除,重试理由不存在(F-DRV-9)。实施偏差:未走 `net_listener_try` 登记(会把 BOUND/D10 状态语义一并拖入),改单次尝试+失败即 425(E10 登记) | | 三处全局读清残留 | net_socket.c:1668/:2332、net_listener.c:176 | **收窄**(与驱动护栏合并做) | B3 已证"只读本 Socket SINT"可行;这三处仍在吞别的 Socket 边沿事件(F-NET-7) | ## 3. 目标架构(高内聚低耦合的落点) | 层 | 应持有 | 现状偏差 | |---|---|---| -| `ch395f` 驱动 | **唯一事实源**:每 socket 状态缓存 + 类型化回读(`{open, tcp_state}` + `is_listening()/is_established()` 谓词,byte0 门控 byte1 收口);事件护栏 `sock_events(sn)`(永不碰全局寄存器);复合原语 `sock_reset(sn, mode, wait_ms)`(排空 RX→close→排空事件→有界等 CLOSED,收编 Trap 09/23 参数);非阻塞 `cmd_done()`;链路态缓存+边沿事件;IP 字节序封装;版本门控 | 驱动零状态(4 个 static);读即清裸暴露;命令原语全阻塞(`HAL_Delay(20)` 忙等、4s 上限);字节序注释与实测相反 | +| `ch395f` 驱动 | **唯一事实源**:每 socket 状态缓存 + 类型化回读(`{open, tcp_state}` + `is_listening()/is_established()` 谓词,byte0 门控 byte1 收口);事件护栏 `sock_events(sn)`(永不碰全局寄存器);复合原语 `sock_reset(sn, mode, wait_ms)`(排空 RX→close→排空事件→有界等 CLOSED,收编 TRAP-NET-09/-18 参数);非阻塞 `cmd_done()`;链路态缓存+边沿事件;IP 字节序封装;版本门控 | 驱动零状态(4 个 static);读即清裸暴露;命令原语全阻塞(`HAL_Delay(20)` 忙等、4s 上限);字节序注释与实测相反 | | net 层(netTask 单所有者) | fd↔socket 映射、消息队列串行化、L3 pending、事件分发;**状态镜像降级为展示投影**;**单一 reconciler**(对账+pump 判据合并、读驱动缓存、一套去抖);重建=`sock_reset`+LISTEN(pump 周期 + 消费者即时,两处入口语义统一) | 8 态镜像是决策依据;7 处手拼回读三套参数;两套排空变体;`net_can_*` 镜像门控 | | 应用/服务层 | 只管 fd 与数据;监听常驻由 net 层承诺 | 32 轮盲收、time_sync close、lftpd 重试——三层兜底散在应用 | @@ -101,7 +101,7 @@ T1(net 层视角)判"机制主体保留、API 可删";T3(主流对照视 ## 4. 分阶段路线(风险从低到高;每阶段独立可回退) - **阶段 0 · 零行为风险清理**(纯注释/宏/一行判定,可先行提交): - F-DRV-5 字节序注释更正、F-DRV-6 FUN_PARA 用宏、F-DRV-11 撤 Trap 13 注释约束、F-DRV-12 删死声明、 + F-DRV-5 字节序注释更正、F-DRV-6 FUN_PARA 用宏、F-DRV-11 撤旧"部分读归零"注释约束、F-DRV-12 删死声明、 F-NET-11 注释漂移(50ms→100ms)、F-NET-14 give-up 文案、F-DRV-10 版本门控(一行)、 F-NET-6 U12 LISTEN 失败补 close(一行)、F-NET-10 retry_tick 状态门(一行)。 - **阶段 1 · API 面收口**(小 diff,行为可测):删 `net_relisten`;iec104 门控迁 `net_listener_armed` diff --git a/docs/网络/N34-自愈审计/net层自愈机制盘点.md b/docs/网络/N34-自愈审计/net层自愈机制盘点.md index 112b25d9..9e2fa0d0 100644 --- a/docs/网络/N34-自愈审计/net层自愈机制盘点.md +++ b/docs/网络/N34-自愈审计/net层自愈机制盘点.md @@ -3,7 +3,7 @@ > 审计对象:`Drivers/BSP/NET/net_socket.c`(3671 行)与 `net_listener.c`(554 行)/ `net_listener.h`。 > 方法:全文通读 + 调用点 grep + git 考古(`git log -S`)+ 问题记录与设计文档对照。纯只读,未改任何代码。 > 行号均为本工作树 HEAD(`09d6195`)实测行号。 -> 参照文档:CH395F_Trap_Records.md(Trap 09/22/23/25 与 2026-10-11 寄存器审计章节 B1~B10 条目)、 +> 参照文档:CH395F_Trap_Records.md(TRAP-NET-09/-17/-18/-20 与 2026-10-11 寄存器审计章节 B1~B10 条目)、 > 网络Socket监听生命周期设计说明.md(B10 根治设计,含 P0/P1/P2 落地记录与模块化边界)、 > BSD_Socket_API_使用指南.md(五层自愈口径)。 @@ -14,24 +14,24 @@ | # | 机制 | 代码位置 | 触发条件 | 改本层哪些状态 | 发哪些芯片命令 | 定时器/去抖/冷却 | 重叠/互为兜底的对象 | 根因分类 | 初步裁决 | |---|---|---|---|---|---|---|---|---|---| | 1 | **auto_relisten**(监听常驻标志) | 置位 net_socket.c:1119(`net_listen_locked`);字段 net_types.h:172;消费点 net_socket.c:1316/1924-1938/2136-2162/1483、net_listener.c:501 | `net_listen` 成功即置 1;此后任何"会话已结束"路径(DISCONNECT/TIMEOUT/对账①)把 fd 置 CLOSED 后,由消费点或 pump 依据该标志重建 | 本身不改状态,是**门控标志**;`net_close`(memset net_socket.c:2351)与 `net_init`(:244)清零 | 无(纯标志) | 无 | 是所有重建路径(机制 2/4/6 与 accept/recv/pending 三处即时重建)的共同前提门 | (a) 芯片永不自动回 LISTEN(手册无此条款,设计文档认定;单连接复用模型锁死) | **保留**(模型基石,不可删) | -| 2 | **D10 非阻塞重监听重试** `net_listener_retry_tick` | net_listener.c:394-432;登记 net_listener.c:374-378;常量 :77-79 | 任何 `net_listener_try` 单次重建失败(open 失败 / LISTEN 失败 / 隔离未满)即登记 `s_relisten_wait`,到点重试 | 成功经 `net_relisten_locked` 置 `state=LISTENING`(net_listener.c:233)、清 `released_abnormal`、转登 `last_peer_port`、清 `lost_conn_seen`、清对端簿记(:239-255);**刻意不清 `session_dead`**(:234-238 契约注释) | 隔离段见机制 5;正常段:SET_PROTO_TYPE、SET_SOUR_PORT、OPEN_SOCKET、TCP_LISTEN(:207-230);open 失败补 CLOSE_SOCKET(:218);成功尾读本 Socket SINT 读即清(:262) | 重试间隔 30ms(:77)、放弃时限 2s(:79,自首次尝试);无去抖(单次尝试);**L3 保险**:同 fd 有在途挂起 recv 则跳过(:405-408,经 net_socket.c:2202 `net_pending_active`) | 是机制 4(pump)与三处消费者即时重建的**执行器**(它们调 `net_listener_try`,失败落到这里);"2s 放弃"并非终态——pump 冷却后会再次触发(见 F-NET-4) | (a) 芯片 close 后重开需沉降(Trap 09 PHY 形态 + Trap 25 裸机台架实证拆链窗口 >2s 不定长);旧实现 osDelay(500) 忙等属 (b) 已修(e5733b3) | **保留**;"give up" 语义需简化(F-NET-4) | -| 3 | **Trap 22 周期对账** `net_reconcile_tick` | net_socket.c:548-641;常量 :544-546 | 每 200ms 对**认领范围内**(仅本层 ESTABLISHED / LISTENING,:579-581)的 fd 回读芯片两字节状态:① 本层 ESTABLISHED 而芯片 SOCKET_CLOSED 或 TCP_CLOSED(:588-590);② 本层 LISTENING 而芯片 TCP_ESTABLISHED(:591-592) | ① 复用 `handle_disconnect_event`(:633)→ 置 `session_dead=1`、`last_peer_port`、`state=CLOSED`、可能登记 GRACEFUL 沉降;② 复用 `handle_connect_event`(:638)→ 置 ESTABLISHED、清 `session_dead`、记对端 | GET_SOCKET_STATUS(每认领 fd 一条,:586);② 前 GET_REMOT_IPP 幽灵甄别(:597-602);后续按事件路径的命令 | 周期 200ms(:544)、连续 2 次失配去抖(:545,≈400ms)、动手后 1s 冷却(:546) | ② 与 accept 路径内联探针 `net_lost_connect_probe`(net_socket.c:1228-1267)职责重叠(同一失同步双兜底,Trap 25 定案"探针快路径 + 对账兜底网");① 与 `net_resync_listen` 语义相邻但判据不同(见 F-NET-2);原③④ 已移交 pump(:536-537/606-607) | (a) 芯片丢 CONNECT/DISCONNECT 中断(Trap 22 生产实测 + Trap 25 裸机台架独立实证,**与 B3 固件 bug 无关**);幽灵态 (a) | **保留**;建议扩展①(F-NET-2) | +| 2 | **D10 非阻塞重监听重试** `net_listener_retry_tick` | net_listener.c:394-432;登记 net_listener.c:374-378;常量 :77-79 | 任何 `net_listener_try` 单次重建失败(open 失败 / LISTEN 失败 / 隔离未满)即登记 `s_relisten_wait`,到点重试 | 成功经 `net_relisten_locked` 置 `state=LISTENING`(net_listener.c:233)、清 `released_abnormal`、转登 `last_peer_port`、清 `lost_conn_seen`、清对端簿记(:239-255);**刻意不清 `session_dead`**(:234-238 契约注释) | 隔离段见机制 5;正常段:SET_PROTO_TYPE、SET_SOUR_PORT、OPEN_SOCKET、TCP_LISTEN(:207-230);open 失败补 CLOSE_SOCKET(:218);成功尾读本 Socket SINT 读即清(:262) | 重试间隔 30ms(:77)、放弃时限 2s(:79,自首次尝试);无去抖(单次尝试);**L3 保险**:同 fd 有在途挂起 recv 则跳过(:405-408,经 net_socket.c:2202 `net_pending_active`) | 是机制 4(pump)与三处消费者即时重建的**执行器**(它们调 `net_listener_try`,失败落到这里);"2s 放弃"并非终态——pump 冷却后会再次触发(见 F-NET-4) | (a) 芯片 close 后重开需沉降(TRAP-NET-09 PHY 形态 + TRAP-NET-20 裸机台架实证拆链窗口 >2s 不定长);旧实现 osDelay(500) 忙等属 (b) 已修(e5733b3) | **保留**;"give up" 语义需简化(F-NET-4) | +| 3 | **TRAP-NET-17 周期对账** `net_reconcile_tick` | net_socket.c:548-641;常量 :544-546 | 每 200ms 对**认领范围内**(仅本层 ESTABLISHED / LISTENING,:579-581)的 fd 回读芯片两字节状态:① 本层 ESTABLISHED 而芯片 SOCKET_CLOSED 或 TCP_CLOSED(:588-590);② 本层 LISTENING 而芯片 TCP_ESTABLISHED(:591-592) | ① 复用 `handle_disconnect_event`(:633)→ 置 `session_dead=1`、`last_peer_port`、`state=CLOSED`、可能登记 GRACEFUL 沉降;② 复用 `handle_connect_event`(:638)→ 置 ESTABLISHED、清 `session_dead`、记对端 | GET_SOCKET_STATUS(每认领 fd 一条,:586);② 前 GET_REMOT_IPP 幽灵甄别(:597-602);后续按事件路径的命令 | 周期 200ms(:544)、连续 2 次失配去抖(:545,≈400ms)、动手后 1s 冷却(:546) | ② 与 accept 路径内联探针 `net_lost_connect_probe`(net_socket.c:1228-1267)职责重叠(同一失同步双兜底,TRAP-NET-20 定案"探针快路径 + 对账兜底网");① 与 `net_resync_listen` 语义相邻但判据不同(见 F-NET-2);原③④ 已移交 pump(:536-537/606-607) | (a) 芯片丢 CONNECT/DISCONNECT 中断(TRAP-NET-17 生产实测 + TRAP-NET-20 裸机台架独立实证,**与 B3 固件 bug 无关**);幽灵态 (a) | **保留**;建议扩展①(F-NET-2) | | 4 | **N-32 监听器 pump** `net_listener_pump` | net_listener.c:465-553;常量 :437-441 | 每 100ms 对**监听型 fd**(in_use + auto_relisten + STREAM + local_port≠0,:501-507)且本层处于 LISTENING/CLOSED(:509-513)者回读芯片:① 本层 LISTENING 而芯片不在 LISTEN → 重建;② 本层 CLOSED 而芯片不在 LISTEN → 重建;③ 本层 CLOSED 而芯片**正在** LISTEN(U11)→ 只重同步 | ③:仅 `state: CLOSED→LISTENING`(:528)+ 转登 `last_peer_port`、清 `lost_conn_seen`(:532-533),**不发任何芯片命令**;①②:经 `net_listener_try` 走机制 2 的状态改动;`session_dead` 不清(:462-463 契约) | 每监听型 fd 一条 GET_SOCKET_STATUS(:516);③ 零命令;①② 同机制 2 | 周期 100ms(:437)、连续 2 次去抖(:440)、1s 冷却(:441) | 本层**唯一周期性重建决策者**(不变量 II,设计文档第 4 节);与三处消费者即时重建(accept net_socket.c:1327 / recv :1938 / pending :2162)构成"周期兜底 + 消费者驱动"双层——该分工是 P2a 台架实测推翻原计划后的定案(删消费者驱动会致 `[WD] task stall`) | (c) 设计缺口(多所有者竞争)的**架构解**;其底层需求(芯片不回 LISTEN + 事件会丢)为 (a) | **保留**(这就是"内聚"的落点,不是补丁) | -| 5 | **Trap 23 排渣隔离** `net_drain_chip_rx` + 隔离段 | 排空 net_listener.c:102-121;隔离段 net_listener.c:149-204;登记 net_listener.c:282-289(`net_listener_settle`),登记点 net_socket.c:3430(GRACEFUL,仅 `recv_bytes≠0`)/:3523(QUARANTINE) | 异常释放(芯片 TIMEOUT,net_socket.c:3512-3524)登记 QUARANTINE;正常释放(芯片 DISCONNECT 且本连接收过数据,:3424-3432)登记 GRACEFUL;下一次 `net_relisten_locked` 进入隔离段 | 隔离期满清 `s_relisten_quarantine/_settle_start/_settle_dead`(:199-203);不改 socket 状态(状态改动仍由机制 2 成功尾做) | 隔离期内每轮:GET_RECV_LEN + READ_RECV_BUF(排空,:172→:107-120)、CLOSE_SOCKET(主动关,:174)、读本 Socket SINT 读即清(:176)、**读全局中断寄存器**(:178,见 F-NET-7)、GET_SOCKET_STATUS(:180) | QUARANTINE 最短 400ms / GRACEFUL 最短 100ms / 上限 800ms 强制放行(net_listener.c:53-55;上限必须 < 2s 重试时限,实测 2000ms 会把监听耗死) | 与 `net_close_locked` 的"close→等 CLOSED→排空中断"循环(net_socket.c:2326-2345)是同一芯片契约的**两个变体实现**(下沉候选,见 F-NET-9);GRACEFUL 的 `recv_bytes≠0` 门是为保护 Trap 22 快速复连时序而设(两机制互相制衡) | (a) FUN_PARA bit3=1(0x08,D4 定案必须保留)下芯片保留现场 + 在途数据在 TIMEOUT 之后才落缓冲 + 残留 DISCONNECT 投递给下一条连接(Trap 23 两阶段台架实证;0ms/30ms 沉降无效、100ms 通过) | **保留**;"close+排空+等 CLOSED"原语可**下沉**驱动层合并(F-NET-9) | +| 5 | **TRAP-NET-18 排渣隔离** `net_drain_chip_rx` + 隔离段 | 排空 net_listener.c:102-121;隔离段 net_listener.c:149-204;登记 net_listener.c:282-289(`net_listener_settle`),登记点 net_socket.c:3430(GRACEFUL,仅 `recv_bytes≠0`)/:3523(QUARANTINE) | 异常释放(芯片 TIMEOUT,net_socket.c:3512-3524)登记 QUARANTINE;正常释放(芯片 DISCONNECT 且本连接收过数据,:3424-3432)登记 GRACEFUL;下一次 `net_relisten_locked` 进入隔离段 | 隔离期满清 `s_relisten_quarantine/_settle_start/_settle_dead`(:199-203);不改 socket 状态(状态改动仍由机制 2 成功尾做) | 隔离期内每轮:GET_RECV_LEN + READ_RECV_BUF(排空,:172→:107-120)、CLOSE_SOCKET(主动关,:174)、读本 Socket SINT 读即清(:176)、**读全局中断寄存器**(:178,见 F-NET-7)、GET_SOCKET_STATUS(:180) | QUARANTINE 最短 400ms / GRACEFUL 最短 100ms / 上限 800ms 强制放行(net_listener.c:53-55;上限必须 < 2s 重试时限,实测 2000ms 会把监听耗死) | 与 `net_close_locked` 的"close→等 CLOSED→排空中断"循环(net_socket.c:2326-2345)是同一芯片契约的**两个变体实现**(下沉候选,见 F-NET-9);GRACEFUL 的 `recv_bytes≠0` 门是为保护 TRAP-NET-17 快速复连时序而设(两机制互相制衡) | (a) FUN_PARA bit3=1(0x08,D4 定案必须保留)下芯片保留现场 + 在途数据在 TIMEOUT 之后才落缓冲 + 残留 DISCONNECT 投递给下一条连接(TRAP-NET-18 两阶段台架实证;0ms/30ms 沉降无效、100ms 通过) | **保留**;"close+排空+等 CLOSED"原语可**下沉**驱动层合并(F-NET-9) | | 6 | **net_resync_listen / net_relisten** 显式自愈 API | API net_socket.c:3026-3066 / :3080-3120;分派 :774-799 / :800-814 | 应用层判定"连接已死/监听器可能失位"后主动调用;**生产代码零调用者**,唯一调用者 test/board/net_latency_test.c:783-784/787/800(Phase 13 台架,产品构建不启用) | RESYNC:仅本层 `state: ESTABLISHED→LISTENING` + 转登 `last_peer_port` + 清 `lost_conn_seen` + 取消挂起 recv(:789-797),**不发芯片命令**;RELISTEN:经 `net_listener_try` 无条件芯片侧重建(机制 2 全套) | RESYNC:无;RELISTEN:机制 2 全套命令(含隔离段) | 无自身定时;RELISTEN 失败落 D10 重试 | RESYNC 与 pump③(U11)语义相邻但覆盖的本地状态不同(ESTABLISHED vs CLOSED);RELISTEN 与 pump①② 完全重叠且**更危险**——对健康监听器无条件 open→USE→close 会造成 ~1s 监听中断(net_socket.c:3078 注释自认) | RESYNC 针对的"芯片静默回收"为 (a)(run8 台架实证,当时 FUN_PARA 已是 0x09);但 API 形态属 (c) 时代遗留(上层镜像芯片状态、手动修);RELISTEN 属 (c) 冗余 | **可删**(生产路径):RESYNC 语义并入对账①扩展后删 API;RELISTEN 直接删(F-NET-2/F-NET-3) | -| 7 | **L3 挂起 recv**(pending)及其交互保险 | 结构 net_socket.c:184-193;登记 :2024-2063;完成 tick :2074-2190;了结 :1985-2007;取消 :2237-2256;close 前了结 :2267-2298;查询 :2202-2208 | 阻塞 recv 无数据即登记;`net_pending_tick` 每轮按 ①数据 ②到点 ③断连(`CLOSED ∥ session_dead`,:2136)顺序了结 | ③ 断连分支:排空后按 `released_abnormal` 回 CONNRESET 或 EOF(0)(:2164-2174);`state==CLOSED` 且 auto_relisten 时**消费者驱动即时重建**(:2159-2162);close 路径先 settle(排空或取消,必通知,:2316-2318);`net_init` 重建前 `cancel_all`(:241) | GET_RECV_LEN + READ_RECV_BUF(①③ 读数据);③ 即时重建走机制 2 命令 | 每 fd 至多一个挂起(:2038-2041 防覆盖挂死);deadline=登记+预算(默认 30s,net_config.h:39);**看门狗**:超 deadline 后 1s 强制了结 + DBG_ERROR(:182/:2181-2188);与机制 2 的交互保险 = retry_tick 的 `net_pending_active` 跳过(net_listener.c:405-408) | 与机制 2 互为保险(挂起在途则 D10 不动该 fd);③ 的即时重建与 recv_locked DONTWAIT 的即时重建同型(:1936-1938);`cancel_all`(net_init)与 tick 的"fd 已关闭兜底"(:2092-2096)双保险 | 阻塞模型演进(E-02 头阻塞修复),非严格自愈;③ 的排空+EOF 语义对应 (a)(Trap 13 FIN 先于数据、bit3=1 保留现场);看门狗属 (c) 完成逻辑缺位的 tripwire | **保留**(含看门狗,留痕成本极低);它是"消费者驱动重建"三处之一,P2a 已定案不可删 | +| 7 | **L3 挂起 recv**(pending)及其交互保险 | 结构 net_socket.c:184-193;登记 :2024-2063;完成 tick :2074-2190;了结 :1985-2007;取消 :2237-2256;close 前了结 :2267-2298;查询 :2202-2208 | 阻塞 recv 无数据即登记;`net_pending_tick` 每轮按 ①数据 ②到点 ③断连(`CLOSED ∥ session_dead`,:2136)顺序了结 | ③ 断连分支:排空后按 `released_abnormal` 回 CONNRESET 或 EOF(0)(:2164-2174);`state==CLOSED` 且 auto_relisten 时**消费者驱动即时重建**(:2159-2162);close 路径先 settle(排空或取消,必通知,:2316-2318);`net_init` 重建前 `cancel_all`(:241) | GET_RECV_LEN + READ_RECV_BUF(①③ 读数据);③ 即时重建走机制 2 命令 | 每 fd 至多一个挂起(:2038-2041 防覆盖挂死);deadline=登记+预算(默认 30s,net_config.h:39);**看门狗**:超 deadline 后 1s 强制了结 + DBG_ERROR(:182/:2181-2188);与机制 2 的交互保险 = retry_tick 的 `net_pending_active` 跳过(net_listener.c:405-408) | 与机制 2 互为保险(挂起在途则 D10 不动该 fd);③ 的即时重建与 recv_locked DONTWAIT 的即时重建同型(:1936-1938);`cancel_all`(net_init)与 tick 的"fd 已关闭兜底"(:2092-2096)双保险 | 阻塞模型演进(E-02 头阻塞修复),非严格自愈;③ 的排空+EOF 语义对应 (a)(旧观察注:FIN 先于数据、bit3=1 保留现场);看门狗属 (c) 完成逻辑缺位的 tripwire | **保留**(含看门狗,留痕成本极低);它是"消费者驱动重建"三处之一,P2a 已定案不可删 | 配套机制(不在七项内但参与同一失同步网,盘点完整性所需): | 机制 | 位置 | 一句话属性 | 根因 | 裁决 | |---|---|---|---|---| -| `net_lost_connect_probe`(Trap 25 双信源探针) | net_socket.c:1228-1267,调用 :1298-1300 | accept 轮询内联补 accept;去抖 2 轮(:1214)无冷却;幽灵甄别 `last_peer_port` | (a) 拆链窗口丢 CONNECT(裸机台架 15/15) | 保留 | -| closed-with-data accept | net_socket.c:1305-1335 | CLOSED + RX 有数据 → 接受"已关但可读"连接排空(bit3=1 排空契约) | (a)(Trap 25 三次定案:425 真根因是我方 relisten 自毁缓冲) | 保留 | +| `net_lost_connect_probe`(TRAP-NET-20 双信源探针) | net_socket.c:1228-1267,调用 :1298-1300 | accept 轮询内联补 accept;去抖 2 轮(:1214)无冷却;幽灵甄别 `last_peer_port` | (a) 拆链窗口丢 CONNECT(裸机台架 15/15) | 保留 | +| closed-with-data accept | net_socket.c:1305-1335 | CLOSED + RX 有数据 → 接受"已关但可读"连接排空(bit3=1 排空契约) | (a)(TRAP-NET-20 三次定案:425 真根因是我方 relisten 自毁缓冲) | 保留 | | `session_dead` 电平(N-32 P1 不变量 I) | net_types.h:192-206;置位 net_socket.c:1723/3439/3535;清零 :3310/3347;不清 net_listener.c:234-238 | "会话已结束"从边沿改粘滞电平,消费者不再被抢先 relisten 饿死 | (c) 设计缺口的**架构解** | 保留(这是根治本身) | -| `released_abnormal` | net_types.h:174-180;置位 net_socket.c:3529/3434 | 异常/正常释放区分 → 挂起 recv 回 CONNRESET 还是 EOF | (a) 协议边界(在途 FIN 与正常完成不可区分,Trap 23 硬化节留痕) | 保留 | +| `released_abnormal` | net_types.h:174-180;置位 net_socket.c:3529/3434 | 异常/正常释放区分 → 挂起 recv 回 CONNRESET 还是 EOF | (a) 协议边界(在途 FIN 与正常完成不可区分,TRAP-NET-18 硬化节留痕) | 保留 | | `net_listen_locked` 3×osDelay(500) 阻塞重试 | net_socket.c:1092-1108 | 首次监听路径仍忙等(P2b 暂缓项),与 D10 非阻塞模式并存 | (b) 时代遗留(D10 已修 relisten 侧,listen 侧未统一) | 简化(统一走 `net_listener_try`) | | `net_close_locked` 500ms 等 CLOSED 忙等 | net_socket.c:2326-2345 | 关闭热路径同步忙等(P2b 暂缓项) | (c) 同步等待设计债;台架实测危害理论性(设计文档 P2b 决策) | 简化(暂缓有据,留观察) | -| 陈旧 DISCONNECT 甄别(D20) | net_socket.c:3400-3415 | ESTABLISHED 收 DISCONNECT 时回读芯片,TCP 仍 ESTABLISHED 判陈旧丢弃 | (a) Trap 14/17 滞后锁存 | 保留 | +| 陈旧 DISCONNECT 甄别(D20) | net_socket.c:3400-3415 | ESTABLISHED 收 DISCONNECT 时回读芯片,TCP 仍 ESTABLISHED 判陈旧丢弃 | (a) TRAP-NET-12 滞后锁存 | 保留 | | 代次号 `s_net_generation` | net_socket.c:49/358-374;递增 net_task.c:145 | 网络重建后服务层重建监听的依据(应用层重建所有者通道) | (c) 但边界清晰(芯片复位后 fd 全灭是事实) | 保留 | ## 2. 根因分类汇总 @@ -41,22 +41,22 @@ | 芯片行为 | 证据 | 支撑的机制 | |---|---|---| | 连接结束后芯片永不自动回 LISTEN(单连接复用模型下监听槽位被消耗) | 设计文档第 2.1 节引手册;官方 EXAM10 例程在事件现场当场重建 | auto_relisten、pump、D10、resync 语义 | -| 快速复连/拆链窗口内丢 CONNECT/DISCONNECT 中断 | Trap 22 生产实测;**Trap 25 裸机台架剥离 OS/net 层仍复现**(关键:证明与固件层 bug 无关) | 对账①②、探针、pump | -| 拆链收尾窗口变长且不可观测(>2s 不定长),窗口内假报 ESTABLISHED(幽灵态,对端端口=旧连接) | Trap 25 定案实验 | 幽灵甄别 `last_peer_port`、去抖+冷却纪律 | -| FUN_PARA bit3=1(0x08,D4 定案保留):DISCONNECT/TIMEOUT 后芯片不关 Socket 不清缓冲(停 0x05/0x00),MCU 必须排空后显式 close | 手册 §5.52②/§5.23;Trap 25 三次定案判别探针(recv_len=1027 完整载荷) | closed-with-data accept、Trap 23 排渣、close_locked 排空循环 | -| 中止时段的在途数据在 TIMEOUT **之后**才落进 RX 缓冲(~2KB 残渣被下条连接读走) | Trap 23 一阶段(RST 10/10 复现→修复 10/10) | QUARANTINE 400ms + 持续排空 | -| 上一条连接残留 DISCONNECT 会在复用同 fd 的下一条连接 accept 后 10~20ms 投递 | Trap 23 二阶段(0ms/30ms 沉降无效、100ms 通过) | GRACEFUL 100ms(仅收过数据时登记) | -| close 后重开 Socket 需沉降(PHY 形态 + 拆链形态) | Trap 09;Trap 25 台架(固定沉降 100~3000ms 均证伪"等就好") | D10 到点重试(而非固定延时) | +| 快速复连/拆链窗口内丢 CONNECT/DISCONNECT 中断 | TRAP-NET-17 生产实测;**TRAP-NET-20 裸机台架剥离 OS/net 层仍复现**(关键:证明与固件层 bug 无关) | 对账①②、探针、pump | +| 拆链收尾窗口变长且不可观测(>2s 不定长),窗口内假报 ESTABLISHED(幽灵态,对端端口=旧连接) | TRAP-NET-20 定案实验 | 幽灵甄别 `last_peer_port`、去抖+冷却纪律 | +| FUN_PARA bit3=1(0x08,D4 定案保留):DISCONNECT/TIMEOUT 后芯片不关 Socket 不清缓冲(停 0x05/0x00),MCU 必须排空后显式 close | 手册 §5.52②/§5.23;TRAP-NET-20 三次定案判别探针(recv_len=1027 完整载荷) | closed-with-data accept、TRAP-NET-18 排渣、close_locked 排空循环 | +| 中止时段的在途数据在 TIMEOUT **之后**才落进 RX 缓冲(~2KB 残渣被下条连接读走) | TRAP-NET-18 一阶段(RST 10/10 复现→修复 10/10) | QUARANTINE 400ms + 持续排空 | +| 上一条连接残留 DISCONNECT 会在复用同 fd 的下一条连接 accept 后 10~20ms 投递 | TRAP-NET-18 二阶段(0ms/30ms 沉降无效、100ms 通过) | GRACEFUL 100ms(仅收过数据时登记) | +| close 后重开 Socket 需沉降(PHY 形态 + 拆链形态) | TRAP-NET-09;TRAP-NET-20 台架(固定沉降 100~3000ms 均证伪"等就好") | D10 到点重试(而非固定延时) | | 芯片可静默回收连接回监听且不发 DISCONNECT | run8 台架实证(当时 FUN_PARA=0x09,bit3 已=1,git 核实) | **阶段 1 起由对账⑤自动认领**(原仅 `net_resync_listen` 覆盖 = F-NET-2 缺口,已闭环) | -| 全局中断寄存器整体读即清、无按位清除;SINT 粘滞到被读 | 手册 §5.15;Trap 14 | net_poll 集中读清纪律;也解释了为何"丢弃式读全局"有害(F-NET-7) | +| 全局中断寄存器整体读即清、无按位清除;SINT 粘滞到被读 | 手册 §5.15;TRAP-NET-12 | net_poll 集中读清纪律;也解释了为何"丢弃式读全局"有害(F-NET-7) | **(b) 早期固件 bug(寄存器数值/引用/时序误用)——根因已修,评估补丁是否仍必要:** | 早期 bug | 修复 | 曾支撑的补丁 | 根因修复后补丁是否仍必要? | |---|---|---|---| -| B3:listen/relisten 对全部 8 个 Socket 读 SINT + 丢弃式读清全局(多连接时代调试遗留,git 考古 e862009bc→319d00e3→db64e6acf) | 4f2cc8b(2026-10-12)收窄为只清本 Socket | Trap 22 对账在生产中的部分命中 | **仍必要**:Trap 25 裸机台架(无 net 层代码)独立实证芯片自身丢中断;但 B3 修复后对账在生产日志的命中数应显著下降——可台架统计验证(F-NET-8) | +| B3:listen/relisten 对全部 8 个 Socket 读 SINT + 丢弃式读清全局(多连接时代调试遗留,git 考古 e862009bc→319d00e3→db64e6acf) | 4f2cc8b(2026-10-12)收窄为只清本 Socket | TRAP-NET-17 对账在生产中的部分命中 | **仍必要**:TRAP-NET-20 裸机台架(无 net 层代码)独立实证芯片自身丢中断;但 B3 修复后对账在生产日志的命中数应显著下降——可台架统计验证(F-NET-8) | | `get_socket_status` 曾给 1 字节缓冲(栈越界、状态误读) | e5733b3 | 曾可能污染对账/甄别读数 | 已修,无遗留补丁 | -| Trap 18:FUN_PARA bit0=1(保留位,无出处) | d758d5c 定案 0x08 | 多 Socket 模式误判相关 | 已修;对自愈机制无直接影响 | +| TRAP-NET-15:FUN_PARA bit0=1(保留位,无出处) | d758d5c 定案 0x08 | 多 Socket 模式误判相关 | 已修;对自愈机制无直接影响 | | B2:`ch395f_tcp_connect` 返回值被丢弃 → CONNECTING 滞留 30s | 794a688 + a844241(命令级失败置 session_dead) | 无独立补丁,靠 recv 超时暴露 | 已修,无遗留补丁 | | relisten 旧实现 osDelay(500) 忙等(全网冻结 500ms) | e5733b3(D10 非阻塞化) | — | 忙等已修;**同族的 `net_listen_locked` 3×500ms 与 `net_close_locked` 500ms 忙等仍在**(P2b 暂缓,F-NET-5) | | B1:服务器侧 KeepAlive 使能(按手册补的"未证明复杂度") | 9a45be1 撤回(实测芯片 2s 重传超时已覆盖静默死亡) | — | 正面案例:根因复核后主动删除,应作为本次审计的裁决范式 | @@ -92,7 +92,7 @@ **✅ 已实施(阶段 1,`b8c6a3d`)**:三 API 与 `NET_MSG_RELISTEN` / `NET_MSG_RESYNC_LISTEN` 消息类型全部删除(MCU + sim 双端),生产/测试调用者归零;Phase 13 的自愈段交回 net 层自动机制。**⚠ 遗留铁律**:门控迁移必须给 `net_listener_armed` 补 ESTABLISHED 放行,见 F-NET-4 实施注。 **F-NET-4 · iec104(:2404)仍用过期门控 `net_can_accept` → 丢 CONNECT 探针对该端口从不运行** -证据:iec104_serv.c:227 以 `net_can_accept`(只认 ESTABLISHED,net_socket.c:1434)门控 accept;`net_lost_connect_probe` 只在 `net_accept_locked` 内 LISTENING 态运行(:1298-1300),门控挡在门外则永不触发——与 N-32 P1 为 :7003 修复的"探针失效"完全同型(ui_net_sink.c:521-526 注释记载原委)。:2404 的丢 CONNECT 只剩对账②兜底,而②带 400ms 去抖 + 1s 冷却(Trap 25 已实证该节奏对短命监听器"救不全")。 +证据:iec104_serv.c:227 以 `net_can_accept`(只认 ESTABLISHED,net_socket.c:1434)门控 accept;`net_lost_connect_probe` 只在 `net_accept_locked` 内 LISTENING 态运行(:1298-1300),门控挡在门外则永不触发——与 N-32 P1 为 :7003 修复的"探针失效"完全同型(ui_net_sink.c:521-526 注释记载原委)。:2404 的丢 CONNECT 只剩对账②兜底,而②带 400ms 去抖 + 1s 冷却(TRAP-NET-20 已实证该节奏对短命监听器"救不全")。 影响:104 上行端口的丢中断恢复被拖慢到秒级;三个常驻监听里 :21/:8888 无条件轮询 accept、:7003 已换 `net_listener_armed`,唯独 :2404 漏换。 建议:iec104 门控换 `net_listener_armed`(双端 sim 已有实现);随后 `net_can_accept` 可退役,消除"两个相似门控语义不同"的长期困惑。 **✅ 已实施(阶段 1,`b8c6a3d`)**:门控已迁 `net_listener_armed`,`net_can_accept` 三处(MCU / 头文件 / sim)删除。 @@ -109,13 +109,13 @@ 建议:小修——两处 LISTEN 失败补 `ch395f_close_socket`,与 open 失败路径对称;同时把 U12 正式登记进 Trap 记录(P0 欠账)。 **F-NET-7 · 三处无条件"读清全局中断寄存器"仍在,会搁浅其它 Socket 的挂起事件——对账/pump 因此仍部分背锅** -证据:net_connect_locked net_socket.c:1668-1670、net_close_locked :2332-2334、relisten 隔离段 net_listener.c:176-178。B3 修复(4f2cc8b)只收窄了 listen/relisten 成功尾两处;Trap 记录 B3 行残余注记明确点名这三处"仍会搁浅(延后、非销毁)其它 Socket 挂起事件——它们是 Trap 14/17/23 指定的两级预清,本次不动"。 +证据:net_connect_locked net_socket.c:1668-1670、net_close_locked :2332-2334、relisten 隔离段 net_listener.c:176-178。B3 修复(4f2cc8b)只收窄了 listen/relisten 成功尾两处;Trap 记录 B3 行残余注记明确点名这三处"仍会搁浅(延后、非销毁)其它 Socket 挂起事件——它们是 TRAP-NET-12/-18 指定的两级预清,本次不动"。 影响:全局寄存器整体读即清(手册 §5.15),这三处每次执行都可能吞掉别的在用 Socket 的 CONNECT/DISCONNECT 边沿位——即"丢中断"有一部分仍是**自家制造**,对账①②与 pump 的必要性里混入了这部分本可消除的噪声。 建议:与驱动层审计(T2)合并处置:评估两级预清能否收窄为"只读本 Socket SINT"(B3 已证该做法成立且官方例程一律如此);若可行,对账①②的生产命中应同步下降(见 F-NET-8 的验证腿)。 -**F-NET-8 · Trap 22 对账不是 B3 固件 bug 的补丁——裸机台架已独立实证芯片丢中断,故 B3 修复后对账不可删;但命中数应可测得下降** -证据:Trap 25 定案实验在**剥离 OS/net/FTP 的裸机芯片台架**上复现"握手完成但 CONNECT 中断被丢"(修复组 15/15,机制触发 5 次全救回);B3 修复在其后(4f2cc8b)。 -影响:直接回答本次审计的核心疑问——"根因已修的补丁是否仍必要":对 Trap 22 而言,其根因是芯片行为而非固件 bug,**必要**。 +**F-NET-8 · TRAP-NET-17 对账不是 B3 固件 bug 的补丁——裸机台架已独立实证芯片丢中断,故 B3 修复后对账不可删;但命中数应可测得下降** +证据:TRAP-NET-20 定案实验在**剥离 OS/net/FTP 的裸机芯片台架**上复现"握手完成但 CONNECT 中断被丢"(修复组 15/15,机制触发 5 次全救回);B3 修复在其后(4f2cc8b)。 +影响:直接回答本次审计的核心疑问——"根因已修的补丁是否仍必要":对 TRAP-NET-17 而言,其根因是芯片行为而非固件 bug,**必要**。 建议:保留对账①②;补一条低成本台架验证腿:B3 修复前后各跑 `:21 100ms×50`,serial-assistant 统计 `reconcile:` 日志命中数,量化"自家制造的丢中断"占比,为 F-NET-7 收窄决策提供数据。 **F-NET-9 · "close + 排空 RX + 排空中断 + 等 CLOSED"在 net 层有两套变体实现——下沉驱动层的最佳候选** @@ -134,13 +134,13 @@ 建议:改注释;顺带该 LISTENING 分支(:3380-3399)现已纯留痕不动作,其 GET_SOCKET_STATUS 回读可裁决"简化"(pump 的 `chip not listening` 日志已含同等信息),删分支则少一条 SPI 与一处重复判读口径——低优先级。 **F-NET-12 · "RX 非空但 readable=0"无人对账(U15),兜底被推给上层的低频盲收——三层兜底中最隐蔽的补丁** -证据:net_can_read 注释自认"readable 仅由 RECV_OK 置位,Trap 22 对账不含 RX 非空但 readable=0 → 中断丢失时恒返 0,调用方须保留低频兜底 recv"(net_socket.c:1500-1501);ui_net_sink.c:552-558 与 iec104_serv.c:246-253 各自实现"每 32 轮无条件收一次";`net_listener_armed` 又用 `readable` 代理"芯片 RX 可能残留"(:1482-1485)。Trap 记录 B6/B10 一带将 U15 登记为未修残余。 +证据:net_can_read 注释自认"readable 仅由 RECV_OK 置位,TRAP-NET-17 对账不含 RX 非空但 readable=0 → 中断丢失时恒返 0,调用方须保留低频兜底 recv"(net_socket.c:1500-1501);ui_net_sink.c:552-558 与 iec104_serv.c:246-253 各自实现"每 32 轮无条件收一次";`net_listener_armed` 又用 `readable` 代理"芯片 RX 可能残留"(:1482-1485)。Trap 记录 B6/B10 一带将 U15 登记为未修残余。 影响:数据面丢中断的兜底散落在两个应用任务里,net 层无对账——与"自愈内聚进 net/驱动层"的目标方向相反。 建议:中期把"GET_RECV_LEN>0 但 readable=0"并入 pump 或独立低频对账(每 fd 一条命令,成本与 pump 同量级),随后上层 32 轮兜底可退役;短期保留现状但在两处上层注释里指向同一编号(U15),避免第三处复制。 **F-NET-13 · time_sync 每会话 close 监听 fd 全量重建,违反自家铁律,是应用层重建所有者的最大残留** -证据:time_sync.c:182 `net_close(conn_sock)`(单连接下 conn==listen)后外层循环 socket+bind+listen 重建(:224);设计文档第 7.4 节契约表已点名"违反『EOF 不得 close 监听 fd』→ close→重建沉降窗(Trap 09)+ 最坏 ~10s 端口下线"。对照正面样板:lftpd 控制通道明确不 close(lftpd.c:746-750/826-827)、iec104 EOF 分支带 `s_client != s_listen` 护栏(iec104_serv.c:259-262)。 -影响::8888 每次会话结束都主动制造一次 Trap 09 沉降窗;也是"应用层还能毁监听器"的活例。 +证据:time_sync.c:182 `net_close(conn_sock)`(单连接下 conn==listen)后外层循环 socket+bind+listen 重建(:224);设计文档第 7.4 节契约表已点名"违反『EOF 不得 close 监听 fd』→ close→重建沉降窗(TRAP-NET-09)+ 最坏 ~10s 端口下线"。对照正面样板:lftpd 控制通道明确不 close(lftpd.c:746-750/826-827)、iec104 EOF 分支带 `s_client != s_listen` 护栏(iec104_serv.c:259-262)。 +影响::8888 每次会话结束都主动制造一次 TRAP-NET-09 沉降窗;也是"应用层还能毁监听器"的活例。 建议:裁决"简化(上层)":time_sync 会话收尾不 close,直接回外层在同一 fd 上 accept(lftpd 模式);iec104 `s_drop_conn` 的 close 属协议违例强制断连,语义正当,保留但注释标注为"有意例外"。 **✅ 已实施(阶段 1,`b8c6a3d`)**:`net_close(conn_sock)` 删除,`ls` 提到外层循环跨会话常驻(仅网络代次变化/被毁时重建),`conn != ls`(模拟器多连接后端)才关数据 fd。台架实测连续 6 轮推送正常(周期 ~4s),每轮走 `standalone accept → DISCONNECTED, releasing → settle → auto-relisten ok`,零 `[ERR]`。契约变化已同步 `test/host/time_sync_server.py`(板子不再先关,PC 侧 2s 宽限后自关);⚠ 强杀 PC 进程会走芯片 `[ERR] TIMEOUT` + QUARANTINE 沉降(异常收尾,非正常路径)。 @@ -156,14 +156,14 @@ - 丢 CONNECT:探针(accept 内联,快)↔ 对账②(周期,兜底网);两者共享幽灵甄别;:2404 因门控过期只剩②(F-NET-4)。 - 丢 DISCONNECT/静默回收:对账①(芯片 CLOSED 形态)↔ 对账⑤(芯片回 LISTEN 形态,阶段 1 起自动认领 = F-NET-2 已闭环)。 - 会话结束感知:session_dead 电平(P1)兜住"抢先 relisten 吞 EOF";pending_tick ③ 与 recv DONTWAIT 双路径给 EOF;看门狗兜"完成逻辑缺位"。 -- 芯片收尾残留:Trap 23 沉降/隔离(重建前)↔ close_locked 排空循环(显式关闭)↔ listen/relisten 成功尾读本 Socket SINT——三处清理纪律,两套命令变体(F-NET-9 下沉候选)。 -- 数据面丢中断:RECV_OK 电平重放(Trap 08)+ readable 门控 + 上层 32 轮盲收(F-NET-12,唯一未内聚的一层)。 +- 芯片收尾残留:TRAP-NET-18 沉降/隔离(重建前)↔ close_locked 排空循环(显式关闭)↔ listen/relisten 成功尾读本 Socket SINT——三处清理纪律,两套命令变体(F-NET-9 下沉候选)。 +- 数据面丢中断:RECV_OK 电平重放(TRAP-NET-08)+ readable 门控 + 上层 32 轮盲收(F-NET-12,唯一未内聚的一层)。 ## 5. 裁决汇总 | 机制 | 裁决 | 一句话依据 | |---|---|---| -| auto_relisten / session_dead / released_abnormal / 幽灵甄别 / closed-with-data / 探针 / 对账①② / pump / D10 / Trap 23 沉降隔离 / L3 pending + 看门狗 | **保留** | 根因是芯片行为或已实测裁决的架构解;Trap 25 裸机台架是"不可从根上消除"的判据 | +| auto_relisten / session_dead / released_abnormal / 幽灵甄别 / closed-with-data / 探针 / 对账①② / pump / D10 / TRAP-NET-18 沉降隔离 / L3 pending + 看门狗 | **保留** | 根因是芯片行为或已实测裁决的架构解;TRAP-NET-20 裸机台架是"不可从根上消除"的判据 | | net_relisten | **可删** | 生产零调用 + 对健康监听器是 footgun | | net_resync_listen | **可删**(先并入对账①) | 语义正确但载体错位:应是自动对账,不是手动 API | | net_can_accept | **可删**(先迁 iec104 到 armed 门控) | 过期门控致 :2404 探针失效 | @@ -187,22 +187,22 @@ 放行的根因是**判据的证据通道被事件自身污染**: -1. 滞后 DISCONNECT 在**拆链收尾完成时刻**锁存(Trap 14:DISCONNECT 在 FIN/ACK 完成后才上报),而拆链收尾完成的标志正是把 TCP_CLOSED(0x00) 写进该 Socket 的状态寄存器。事件投递与寄存器写 0x00 是同一时刻的两个面——甄别读到的 0x00 无法区分「真断开」与「陈旧事件 + 新连接仍活着」。 -2. 每 Socket 只有一个状态槽,存的是**最后一次状态迁移**(Trap 25 已证该寄存器在拆链窗口内双向不可信:既会假报 ESTABLISHED 幽灵,也会假报 CLOSED——新连接的 0x04 迁移被旧连接的 0x00 迁移覆盖)。E1 签名 `st=0x05/0x00` 即:Socket 仍 OPEN(0x05,被杀连接在芯片侧还活着),TCP 槽显示旧拆链的 0x00。 +1. 滞后 DISCONNECT 在**拆链收尾完成时刻**锁存(TRAP-NET-12:DISCONNECT 在 FIN/ACK 完成后才上报),而拆链收尾完成的标志正是把 TCP_CLOSED(0x00) 写进该 Socket 的状态寄存器。事件投递与寄存器写 0x00 是同一时刻的两个面——甄别读到的 0x00 无法区分「真断开」与「陈旧事件 + 新连接仍活着」。 +2. 每 Socket 只有一个状态槽,存的是**最后一次状态迁移**(TRAP-NET-20 已证该寄存器在拆链窗口内双向不可信:既会假报 ESTABLISHED 幽灵,也会假报 CLOSED——新连接的 0x04 迁移被旧连接的 0x00 迁移覆盖)。E1 签名 `st=0x05/0x00` 即:Socket 仍 OPEN(0x05,被杀连接在芯片侧还活着),TCP 槽显示旧拆链的 0x00。 3. **事件无连接身份**:芯片事件位不带连接 ID;幽灵甄别那套对端端口比对(:597-604、:1249)在此不可用——新握手已把芯片对端寄存器覆写为新端口,两种假设下都读到 `peer == remote_port(新) ≠ last_peer_port(旧)`,无判别力。 -4. 投递时序吻合:net_poll 同一次 SINT 读(:469)内 CONNECT 先于 DISCONNECT 派发(:480-489);`handle_connect_event` 在 accept 尾部读清 SINT(:3314-3316),所以致死位必须在 accept **之后**锁存——正是拆链收尾完成时刻(10~20ms,Trap 23 二阶段与 Trap 25 现象 3 均有记载)。新连接自己的握手 ACK 流量往往正是完成旧拆链最后一次 FIN/ACK 交换的那笔流量 → 背靠背强相关。 +4. 投递时序吻合:net_poll 同一次 SINT 读(:469)内 CONNECT 先于 DISCONNECT 派发(:480-489);`handle_connect_event` 在 accept 尾部读清 SINT(:3314-3316),所以致死位必须在 accept **之后**锁存——正是拆链收尾完成时刻(10~20ms,TRAP-NET-18 二阶段与 TRAP-NET-20 现象 3 均有记载)。新连接自己的握手 ACK 流量往往正是完成旧拆链最后一次 FIN/ACK 交换的那笔流量 → 背靠背强相关。 -结论:D20 在该时刻**结构性失明**——它的规则是在 Trap 20 时代标定的(当时事件先于寄存器写投递,芯片显示 0x04 可辨)。剩余可用判别轴只有两条:时间上的电平确认(对账去抖语义)或"年轻连接+零数据"启发式。 +结论:D20 在该时刻**结构性失明**——它的规则是在 TRAP-NET-16 时代标定的(当时事件先于寄存器写投递,芯片显示 0x04 可辨)。剩余可用判别轴只有两条:时间上的电平确认(对账去抖语义)或"年轻连接+零数据"启发式。 ### Q2 · GRACEFUL 沉降门 `recv_bytes≠0` 对失效循环为何失效 门本体:net_socket.c:3424 `recv_bytes != 0U`(外层已是 ESTABLISHED 状态门 :3400)才登记 GRACEFUL(:3430);不登记则下一次 relisten 跳过整个隔离段(net_listener.c:149 以 `quarantine != 0` 为门)。 1. **受害者类别由事件时序定义,与数据无关**:连接在 accept 后 ~20ms 被杀,MCU 尚未读任何字节 → `recv_bytes==0` 是构造性必然(即便客户端已把 STOR 数据灌进芯片 RX——门统计的是 MCU 已读字节,不是芯片 RX 存量)。门按"收过数据"划危险类别,恰好把每一任受害者排除在外。 -2. **循环自持的关键:每一轮都重新制造残渣**。被杀连接在芯片侧仍 OPEN 活着(st[0]=0x05);下一次 relisten 的 open 返 USE → 失败分支 close(net_listener.c:214-222)→ 在芯片侧摧毁该连接 → 新拆链 → Trap 14 锁存新的滞后 DISCONNECT → 下一条连接再被杀。最初的残渣只是种子,循环靠"杀→close→拆链→滞后事件"自我繁殖。 +2. **循环自持的关键:每一轮都重新制造残渣**。被杀连接在芯片侧仍 OPEN 活着(st[0]=0x05);下一次 relisten 的 open 返 USE → 失败分支 close(net_listener.c:214-222)→ 在芯片侧摧毁该连接 → 新拆链 → TRAP-NET-12 锁存新的滞后 DISCONNECT → 下一条连接再被杀。最初的残渣只是种子,循环靠"杀→close→拆链→滞后事件"自我繁殖。 3. **为什么登记沉降就能断环**:登记后 relisten 走隔离段——提前 close 芯片 Socket(:174)、每 30ms 重试轮读清 SINT(:176)、≥100ms 保持期满才 open+listen(:182-197)。滞后 DISCONNECT 在"芯片 Socket 已关、本层无会话"的窗口内被消费,无害(对照:落在 LISTENING 上也只是被动留痕,net_socket.c:3380-3399)。 -4. **门的原保护目标已被架空**:注释 :3420-3422 写明门是为保住 Trap 22 的 `:21 @0ms×50` 快速复连时序而设(零数据连接免 100ms 沉降)。但 E1 发现 A 实测:N-32 P2a 之后**每次**会话收尾都要等 pump(115–900ms)+ D10,恢复窗 0.3–1s,`:7003` 复连 p50=1020ms(SYN 静默丢 → 客户端 RTO)。门如今省下的 100ms 落在 0.3–1s 的窗里不可见,换来的却是 P0 失效循环。 -5. 对照佐证:TIMEOUT 路径的 QUARANTINE **无条件**登记(net_socket.c:3517-3524)——所以 Trap 23 的 RST 中止修复从未循环过;循环是 DISCONNECT 有门路径独有的。 +4. **门的原保护目标已被架空**:注释 :3420-3422 写明门是为保住 TRAP-NET-17 的 `:21 @0ms×50` 快速复连时序而设(零数据连接免 100ms 沉降)。但 E1 发现 A 实测:N-32 P2a 之后**每次**会话收尾都要等 pump(115–900ms)+ D10,恢复窗 0.3–1s,`:7003` 复连 p50=1020ms(SYN 静默丢 → 客户端 RTO)。门如今省下的 100ms 落在 0.3–1s 的窗里不可见,换来的却是 P0 失效循环。 +5. 对照佐证:TIMEOUT 路径的 QUARANTINE **无条件**登记(net_socket.c:3517-3524)——所以 TRAP-NET-18 的 RST 中止修复从未循环过;循环是 DISCONNECT 有门路径独有的。 ### Q3 · closed-with-data 救回后 lftpd 镜像门控为何错过 @@ -238,13 +238,13 @@ ### 回归集建议 -把 `n30_repro`(背靠背 STOR)纳入常备回归集——Trap 25 终版回归只含有间隔的 `ftp_regression`,背靠背形态不在集内,这是本次 9/10 能潜伏的测试面原因(实验方案对照台账已登记)。验收线建议:`n30_repro --n 50` 通过数与 `:21 @0ms×50`、`ftp_regression`、`ftp_abort_probe` 同时全绿,由台架定具体门槛。 +把 `n30_repro`(背靠背 STOR)纳入常备回归集——TRAP-NET-20 终版回归只含有间隔的 `ftp_regression`,背靠背形态不在集内,这是本次 9/10 能潜伏的测试面原因(实验方案对照台账已登记)。验收线建议:`n30_repro --n 50` 通过数与 `:21 @0ms×50`、`ftp_regression`、`ftp_abort_probe` 同时全绿,由台架定具体门槛。 ## F-NET-17 残余偶发的真身:风暴态(E8 深查,2026-10-13) E7b 的"残余 ~3%"不是稳定值:同固件同负载,故障率随设备状态在 **2%(全新启动)~ 54%(残渣累积)~ 12%(仅复位)** 间摆动(实验方案 E8 矩阵)。"偶发"的真身是**可自持的风暴态**,三个缺陷叠加: -1. **沉降自再生锁存**(M1 的硬币):沉降段 `net_listener.c:174` close 之后,拆链收尾在 ~10-20ms 后(Trap 14 延迟锁存)**重新置位** DISCONNECT;`:176` 的 SINT 读清与 close 同轮发生,读不清这枚迟到的锁存 → 沉降最后一轮"清干净"的假象下,relisten 完成后 11ms 首条连接被杀。30ms 重试粒度 vs 10-20ms 锁存延迟 = 每次重建一次硬币——E7b 的 2% 即此。 +1. **沉降自再生锁存**(M1 的硬币):沉降段 `net_listener.c:174` close 之后,拆链收尾在 ~10-20ms 后(TRAP-NET-12 延迟锁存)**重新置位** DISCONNECT;`:176` 的 SINT 读清与 close 同轮发生,读不清这枚迟到的锁存 → 沉降最后一轮"清干净"的假象下,relisten 完成后 11ms 首条连接被杀。30ms 重试粒度 vs 10-20ms 锁存延迟 = 每次重建一次硬币——E7b 的 2% 即此。 2. **泵与救回互抢**(风暴燃料):沉降段 `:172` `net_drain_chip_rx` 排空的正是 closed-with-data 救回(net_socket.c:1317)依赖的 RX 数据;救回 accept 成功返回 fd(:1352)但会话随即被杀/数据被排空 → lftpd 短读 → 425。E8 窗口 31 次 accept timeout 与 38 次救回并存为直接证据。 3. **芯片 OPEN 卡死放大**(自持结构):被杀会话芯片侧仍 OPEN(0x05),新 SYN 被接进卡死 socket(TCP 槽 0x00)→ accept → 下一枚锁存再杀 → 直到 800ms 强制放行 + close 真正落拆链才出簇。风暴期时间线(0.75s 内 3 轮"救回→杀→救回")见实验方案 E8 节。 @@ -261,11 +261,11 @@ E7b 的"残余 ~3%"不是稳定值:同固件同负载,故障率随设备状 **手册口径**:§5.35「关闭 Socket 前 CH395 会自动断开 TCP 连接」(Trap 记录定案更正引用)——close 的动作序是"先断链再关 Socket";若 TCP 已 CLOSED,断链环节无事可做。§5.52② bit3=1 只豁免 DISCONNECT/TIMEOUT 场景的自动关闭/清缓冲,**不豁免 CLOSE_SOCKET 命令本身**(close 仍清缓冲——net_socket.c:1311 注释"close+open 清缓冲"同此)。SINT bit4=DISCONNECT(ch395f.h:268)。 -**证伪强形式(干净情形)**:TC-NET-204(Trap 07 定案更正,Trap 记录实测表)模式 C 正是本问题的受控实验——DISCONNECT 已发生、芯片停在 `0x05/0x00`(与风暴签名逐位相同),补 `close` → **+0 ms 回 CLOSED**、重开重连 5/5 OK;且"15 轮重开均未观察到残留 DISCONNECT(0x10)——close 顺带清掉了锁存"。即:**close 一个已 CLOSED 的 Socket 不产生新锁存,反而顺带清掉已锁存的位**。模式 A(close 一条活连接,主动拆链)5 轮同样无残留锁存。 +**证伪强形式(干净情形)**:TC-NET-204(TRAP-NET-07 定案更正,Trap 记录实测表)模式 C 正是本问题的受控实验——DISCONNECT 已发生、芯片停在 `0x05/0x00`(与风暴签名逐位相同),补 `close` → **+0 ms 回 CLOSED**、重开重连 5/5 OK;且"15 轮重开均未观察到残留 DISCONNECT(0x10)——close 顺带清掉了锁存"。即:**close 一个已 CLOSED 的 Socket 不产生新锁存,反而顺带清掉已锁存的位**。模式 A(close 一条活连接,主动拆链)5 轮同样无残留锁存。 -**与 Trap 14 的张力消解**:Trap 14 的残留锁存不是 close 命令制造的,而是**拆链完成事件**——"DISCONNECT 可能在与 CLOSED 状态达成几乎同时、甚至之后才锁存"(Trap 记录 Trap 14 根因节),粘滞跨会话(B3 条目"SINT 粘滞到被读")。锁存的生成者是拆链过程(多为对端 FIN 的被动拆链收尾),不是 close 命令。 +**与 TRAP-NET-12 的张力消解**:TRAP-NET-12 的残留锁存不是 close 命令制造的,而是**拆链完成事件**——"DISCONNECT 可能在与 CLOSED 状态达成几乎同时、甚至之后才锁存"(Trap 记录 TRAP-NET-12 根因节),粘滞跨会话(B3 条目"SINT 粘滞到被读")。锁存的生成者是拆链过程(多为对端 FIN 的被动拆链收尾),不是 close 命令。 -**风暴窗口的直接反证**:E8 时间线里 kill 发生在 `auto-relisten ok` **之前**(窗口内 relisten 尚未完成,沉降段 close 还没跑或没再跑)——那几枚 11ms 杀连接锁存**不可能由沉降段 close 产生**。真实来源两条:(a) 上一条 STOR 的客户端 FIN(FTP 正常行为,发完即关,Trap 25 定案)的被动拆链收尾迟到完成(Trap 14/23 的 10~20ms,Trap 25 的 >2s 不可观测窗);(b) 卡死 socket(0x05/0x00)把新 SYN 接进来又自行拆掉(`docs/芯片手册/CH395F.md` §5.29 语义域),每轮自产一对 CONNECT/DISCONNECT。 +**风暴窗口的直接反证**:E8 时间线里 kill 发生在 `auto-relisten ok` **之前**(窗口内 relisten 尚未完成,沉降段 close 还没跑或没再跑)——那几枚 11ms 杀连接锁存**不可能由沉降段 close 产生**。真实来源两条:(a) 上一条 STOR 的客户端 FIN(FTP 正常行为,发完即关,TRAP-NET-20 定案)的被动拆链收尾迟到完成(TRAP-NET-12/-18 的 10~20ms,TRAP-NET-20 的 >2s 不可观测窗);(b) 卡死 socket(0x05/0x00)把新 SYN 接进来又自行拆掉(`docs/芯片手册/CH395F.md` §5.29 语义域),每轮自产一对 CONNECT/DISCONNECT。 **但"沉降末轮 SINT 读清不能保证静默"的实操结论仍成立**,经两条路径: 1. 拆链完成滞后于末轮读清(lead 说的硬币——生成者是拆链,不是 close); @@ -304,7 +304,7 @@ if ((st[0] == CH395F_SOCKET_OPEN) && (st[1] == CH395F_TCP_CLOSED) && 1. lftpd accept 轮询见 ESTABLISHED → :1305 门通过 → **accept 正常返 fd**(不再依赖 closed-with-data 救回); 2. recv 快路径无状态门槛 → 消费者在 400ms 租约内排空 RX(引擎活则数据续到); 3. 对 n30 的小段 STOR:400ms 租约足以让传输完成 + 客户端正常 FIN(0x07 健康释放)→ **风暴链被"会话正常完成"掐断**; -4. 槽恢复 0x04 的形态(Trap 20 时代可恢复类):对账① bad=0 → 连接存活,flag 由 relisten/connect 清; +4. 槽恢复 0x04 的形态(TRAP-NET-16 时代可恢复类):对账① bad=0 → 连接存活,flag 由 relisten/connect 清; 5. 引擎真死:消费者排空后 WOULDBLOCK,对账①释放后得 EOF——EOF 晚 ~400ms,语义不变。 **:21 @0ms 快复连为何零误伤**: @@ -341,6 +341,6 @@ OPEN: SET_PROTO_TYPE/SET_SOUR_PORT/OPEN/TCP_LISTEN + 成功尾(现 :207-267 ``` - 参数建议:`LATCH_GRACE_MS=50`(实测锁存延迟 10~20ms,×2.5 余量)、`QUIET_ROUNDS=2`(2×30ms)、`RESET_MAX_MS=800`(保持"必须 < 2000 放弃时限"规则)、`hold_min` 沿用两级。 -- **诚实边界(来自 T6-① 修正)**:静默不是有界保证——拆链完成可被**新连接自身流量**触发(Trap 25 窗口 >2s 不可观测),状态机只能静默"已在途的拆链";"未来流量触发的拆链"只有 M3 的电平确认能接住。**M5 与 M3 互补成对,不是替代关系**:M5 消灭"沉降窗内可静默"的绝大多数锁存(E7b 的 2% 硬币),M3 兜住无界残余(风暴态)。 +- **诚实边界(来自 T6-① 修正)**:静默不是有界保证——拆链完成可被**新连接自身流量**触发(TRAP-NET-20 窗口 >2s 不可观测),状态机只能静默"已在途的拆链";"未来流量触发的拆链"只有 M3 的电平确认能接住。**M5 与 M3 互补成对,不是替代关系**:M5 消灭"沉降窗内可静默"的绝大多数锁存(E7b 的 2% 硬币),M3 兜住无界残余(风暴态)。 - RAM:三数组(uint8 + 2×uint32 ≈ 9B/sock)→ 相位 uint8 + t0 uint32 + 打包 flags ≈ 6B/sock,微负,预算友好。 - 与 T6-② 的闭环:QUIESCE 第 1 步即红线裁决点——"泵与救回互抢"在状态机内单点裁决,五入口不再各自竞速。 diff --git a/docs/网络/N34-自愈审计/主流方案对照.md b/docs/网络/N34-自愈审计/主流方案对照.md index e544e92c..50c7dee3 100644 --- a/docs/网络/N34-自愈审计/主流方案对照.md +++ b/docs/网络/N34-自愈审计/主流方案对照.md @@ -80,8 +80,8 @@ 1. `ch395f_sock_state(sn)`:`GET_SOCKET_STATUS_SN` 的薄封装(返回芯片 11 态枚举,映射到 net 层语义由 net 层做)。 2. `ch395f_sock_events(sn)`:读即清每 socket 中断位;`GET_GLOB_INT_STATUS` 汇聚从 `net_poll` 下沉驱动。 3. `ch395f_sock_reset(sn, mode, timeout_ms)`:**复合原语** = 排空 RX(`CLEAR_RECV_BUF_SN`/分块读空)→ - `TCP_DISCONNECT`/`CLOSE_SOCKET_SN` → 排空中断位 → 有界等待芯片回 CLOSED。把 Trap 09 沉降窗口与 - Trap 23 排渣隔离收进这一个同步原语(对应 ioLibrary `close()` 内联等待、FreeRTOS `listen()` 内联清流)。 + `TCP_DISCONNECT`/`CLOSE_SOCKET_SN` → 排空中断位 → 有界等待芯片回 CLOSED。把 TRAP-NET-09 沉降窗口与 + TRAP-NET-18 排渣隔离收进这一个同步原语(对应 ioLibrary `close()` 内联等待、FreeRTOS `listen()` 内联清流)。 4. `ch395f_wait_cmd(sn, cmd, timeout_ms)`:命令完成有界等待(`ch395f_poll_cmd_status` 已有雏形,补齐到所有命令路径)。 5. ~~多连接模式语义利用~~(**T4 更正,T2 代码实证**):`FUN_PARA_TCP_SERVER`(bit1)**未启用**—— `net_init` 实传裸 `0x08`、bit1=0(net_socket.c:261-270),设计文档 A′ 出局论证(多连接模式 @@ -92,13 +92,13 @@ | 现有机制 | 裁决 | 依据(主流对照) | |---|---|---| -| Trap 22 周期对账 `net_reconcile_tick`(200 ms + 去抖 + 冷却) | **整体删除** | 无镜像即无对账;ioLibrary/mbed 决策即回读(P1) | +| TRAP-NET-17 周期对账 `net_reconcile_tick`(200 ms + 去抖 + 冷却) | **整体删除** | 无镜像即无对账;ioLibrary/mbed 决策即回读(P1) | | N-32 监听器 pump `net_listener_pump`(100 ms,"唯一重建入口") | **整体删除**,改为 accept 事务内联重建(mbed 模型)或 close 事务内联回 LISTEN(FreeRTOS reuse 模型) | P4:重监听是事务步骤,不是周期机制 | -| D10 重监听重试(30 ms/2 s 独立定时器) | **降级**:内联重建失败时"下次 accept 再试"或 reset 原语内部有界重试;独立定时器删除 | mbed 内联重建 + 有界超时;Trap 09 沉降收进 `ch395f_sock_reset` | +| D10 重监听重试(30 ms/2 s 独立定时器) | **降级**:内联重建失败时"下次 accept 再试"或 reset 原语内部有界重试;独立定时器删除 | mbed 内联重建 + 有界超时;TRAP-NET-09 沉降收进 `ch395f_sock_reset` | | `net_resync_listen`(公共 API,"静默回收"对齐) | **整体删除** | "静默回收"是 CH395 TCP Server 模式**文档行为**(芯片自动回监听),回读即对齐,不需要"仅修 MCU 状态"的 API | | `net_lost_connect_probe`(丢 CONNECT 中断补 accept) | **降级**:accept 判据改为回读芯片状态(SYN_RCVD/ESTABLISHED 即有连接),丢中断只影响时延不影响正确性 | P3;ioLibrary 观察不到 LISTEN 瞬态也照样靠回读自愈(issue 127 场景) | | `auto_relisten` 标志 + `handle_disconnect_event`/`handle_timeout_event` 重建路径 | **简化**:断开事务末尾调 `ch395f_sock_reset` + LISTEN(FreeRTOS reuse 回 LISTEN 的等价物);标志位可保留作"该 fd 是监听器"的类型标记 | P4/P5 | -| Trap 23 沉降/隔离状态机(`net_listener_settle` 400/100/800 ms 三档) | **下沉驱动**:并入 `ch395f_sock_reset(sn, mode, timeout)`;net 层只选 mode(异常/正常) | ioLibrary `close()` 内联等待、W5300 erratum 内联先例 | +| TRAP-NET-18 沉降/隔离状态机(`net_listener_settle` 400/100/800 ms 三档) | **下沉驱动**:并入 `ch395f_sock_reset(sn, mode, timeout)`;net 层只选 mode(异常/正常) | ioLibrary `close()` 内联等待、W5300 erratum 内联先例 | | `net_can_accept`/`net_can_read`/`net_listener_armed` 镜像门控 | **保留但改语义**:门控值 = 驱动层事件缓存(readable 由 RECV_OK 事件置位可保留),但"非 ESTABLISHED 一律返回 1"这类镜像补偿逻辑删除——决策点回读芯片后不再需要 | P1 | | L3 pending recv、消息队列串行化、网络代次号 | **保留**(与主流不冲突:FreeRTOS/Zephyr 同样单栈任务 + 阻塞事件模型) | — | @@ -107,8 +107,8 @@ | 怪癖 | 任何架构都要处理 | 主流收法 | |---|---|---| | 丢中断(CONNECT/DISCONNECT/RECV_OK,INT 电平驱动 + IR 位重叠) | 是(W5500 官方笔记同款警告) | 事件唤醒 + **有界超时回读**,收在驱动层(Zephyr `k_sem_take` 超时、PeterBorisenko 500 ms);net 层不做周期对账 | -| close 沉降窗口(Trap 09:close 后立即 open 失败) | 是(ioLibrary `close()` 内 `while(Sn_SR != CLOSED)`;listen 竞态 issue 127) | `close()/socket()` 事务内联等待 + 有界超时(驱动原语内部) | -| RX 残渣(Trap 23:中止后残留 ~2 KB 被下一条连接读走) | 是(软栈对应物 = FreeRTOS reuse `listen()` 清流;硬栈 = 打开前 close/清缓冲) | `ch395f_sock_reset` 前置排空(`CLEAR_RECV_BUF_SN` 命令现成) | +| close 沉降窗口(TRAP-NET-09:close 后立即 open 失败) | 是(ioLibrary `close()` 内 `while(Sn_SR != CLOSED)`;listen 竞态 issue 127) | `close()/socket()` 事务内联等待 + 有界超时(驱动原语内部) | +| RX 残渣(TRAP-NET-18:中止后残留 ~2 KB 被下一条连接读走) | 是(软栈对应物 = FreeRTOS reuse `listen()` 清流;硬栈 = 打开前 close/清缓冲) | `ch395f_sock_reset` 前置排空(`CLEAR_RECV_BUF_SN` 命令现成) | | 芯片自动回收/回监听(现被当作"静默回收"陷阱) | 不是缺陷——bit1=0 下的台架实证芯片行为(run8;⚠ T4 更正:非 Server 模式条款,Server 模式未启用) | 回读对齐后此"陷阱"消失;仅需在文档里把它登记为芯片语义 | | PHY/链路变化 | 是 | 驱动层缓存 + 边沿事件(Zephyr carrier on/off),net 层订阅;不进对账 | @@ -116,7 +116,7 @@ 本项目偏离主流的不是"单连接复用"(ioLibrary 无 accept、CH395 每 socket 一连接、FreeRTOS reuse 模式全部同形), 而是三件事的组合:**① net 层维护 8 态镜像并作为决策依据;② 状态迁移只靠中断事件推进;③ 监听重建与事务脱钩**。 -镜像滞后 → 需要周期对账(Trap 22);事件丢失 → 需要探针与门控补偿;重建脱钩 → 需要 pump/重试/沉降三件套。 +镜像滞后 → 需要周期对账(TRAP-NET-17);事件丢失 → 需要探针与门控补偿;重建脱钩 → 需要 pump/重试/沉降三件套。 按 P1~P6 重构后,五层中的四层(对账、pump、resync、probe)失去存在理由,一层(沉降/排渣)下沉为驱动原语。 ## 5 编号发现 @@ -135,7 +135,7 @@ 结论不变:`net_resync_listen` 这类"仅修 MCU 状态"的 API 是镜像滞后的直接产物,回读模型下无存在理由。 - **F-MS-5** FreeRTOS+TCP `FREERTOS_SO_REUSE_LISTEN_SOCKET` 给出与本项目模型同形的**主流参考实现**: 监听 socket 自身转连接、回 LISTEN 由栈的单一状态机在事务内完成;且 `listen()` **同步清空 rx/tx 流**—— - 这正是 Trap 23 排渣隔离的主流对应物(一行内联清理,而非 400/100/800 ms 三档状态机)。 + 这正是 TRAP-NET-18 排渣隔离的主流对应物(一行内联清理,而非 400/100/800 ms 三档状态机)。 - **F-MS-6** mbed WIZnetInterface 的 accept 模型:连接继承原芯片 socket,**accept 事务内联**在另一颗芯片 socket 上 bind+listen 重建监听(用芯片 8 socket 兑现"监听常驻"承诺)。其失败处理粗糙(可静默失去监听)说明: 内联重建仍需有界重试,但重试应长在事务/模块内部,而不是独立周期机制。CH395F 多连接模式已启用,该模型可移植。 diff --git a/docs/网络/N34-自愈审计/实施方案.md b/docs/网络/N34-自愈审计/实施方案.md index 68e05814..d4bcb08b 100644 --- a/docs/网络/N34-自愈审计/实施方案.md +++ b/docs/网络/N34-自愈审计/实施方案.md @@ -27,12 +27,12 @@ | M4-lite | 删沉降段全局读即清(模块约定违规点) | net_listener.c:178 | 断"沉降热路径搁浅他 Socket 事件"放大器(**必须与 M1 同批**,E7 实证) | 回归:n30 116/120(残余偶发自愈)+ ftp_regression 11/11 + :7003 50/50 + iec104 18/18 + -Trap 23 RST/FIN 5/5+5/5 + @test PASS + 0 错 0 警。**E8 深查修正**(见实验方案 E8 节与 +TRAP-NET-18 RST/FIN 5/5+5/5 + @test PASS + 0 错 0 警。**E8 深查修正**(见实验方案 E8 节与 盘点 F-NET-17):残余并非稳定 3%,而是**风暴态**——同固件故障率随残渣存量 2%~54% 摆动; 治本 = M3(`0x05/0x00`+年轻连接的电平确认,交对账①去抖)+ M5(drain-until-quiescent 的 `sock_reset`,沉降/pump 让位 closed-with-data 救回)。M3/M5 属阶段 1/2,非止血包范围。 **常备回归补挂**:`n30_repro --n 20` 纳入 Ch395F 回归集(本轮 46/50 潜伏的测试面根因); -`MDK-ARM/ftp_abort_probe.py` 迁 `test/host/`。 +`ftp_abort_probe.py` 已迁 `test/host/`(2026-10-13,N34 阶段 3 收尾,`rename_ref.py` 全库引用同步)。 ## 2. 阶段 0 · 零风险清理(先行,不依赖实验)✅ 已完成(`4c0a0af`,E10 验证) @@ -47,12 +47,12 @@ Trap 23 RST/FIN 5/5+5/5 + @test PASS + 0 错 0 警。**E8 深查修正**(见 |---|---|---|---| | 0-1 | 注释漂移:pump "每 50 ms"→100 ms | net_socket.c:3384 | @build | | 0-2 | give-up 文案改"重试预算耗尽,交 pump 周期接管" | net_listener.c:414-419 | @build + @test 冒烟 | -| 0-3 | U12 对称:TCP_LISTEN 失败补 `ch395f_close_socket` | net_listener.c:227-230、net_socket.c:1112-1114 | @build + @test(Trap 22 快速复连子集) | +| 0-3 | U12 对称:TCP_LISTEN 失败补 `ch395f_close_socket` | net_listener.c:227-230、net_socket.c:1112-1114 | @build + @test(TRAP-NET-17 快速复连子集) | | 0-4 | retry_tick 补状态门(仅 LISTENING/CLOSED 才重试) | net_listener.c:400-412 | @build + @test | | 0-5 | FUN_PARA 用宏 `CH395F_FUN_PARA_SOCKET_CLOSE` 替代裸 0x08;删 `CH395F_GINT_STAT_SOCK0..3` 死宏 | net_socket.c:270、ch395f.h:244-247 | @build | | 0-6 | 版本门控:ver<0x46 则拒置 bit3 + 自检留痕 | net_socket.c:257-270 | @build + @test(启动锚点) | | 0-7 | IP 字节序注释更正(set_ip/gw/mask 实为点分序;get_remot_ipp 小端) | ch395f.h:380/:387/:394/:479-484 | @build | -| 0-8 | Trap 13 注释降级为"未证实历史观察";DMA 缓冲 sizing 理由改"接收缓冲 2KB" | ch395f.h:143-153、net_socket.c:1908/:1969/:3466/:3470 | @build | +| 0-8 | 旧"部分读归零"注释降级为"未证实历史观察";DMA 缓冲 sizing 理由改"接收缓冲 2KB" | ch395f.h:143-153、net_socket.c:1908/:1969/:3466/:3470 | @build | | 0-9 | 删死声明:`CH395F_CMD_GET_GLOB_INT_STATUS`(0x29)、`CH395F_SPI_BEGIN_DELAY_MS`、EEPROM/GPIO/SLEEP 命令宏 | ch395f.h:36/:42/:163-167 | @build | 提交切分:0-1/0-2/0-7/0-8/0-9 一笔(纯注释/死码);0-3/0-4 一笔(一行逻辑,需 @test);0-5/0-6 一笔(init 路径,需 @test)。 @@ -80,7 +80,7 @@ Trap 23 RST/FIN 5/5+5/5 + @test PASS + 0 错 0 警。**E8 深查修正**(见 5. **time_sync 会话收尾不 close**:time_sync.c:182 删 `net_close`,回外层循环同 fd `net_accept`(lftpd 样板)。 ⚠ 需确认 :8888 协议语义(对端每次会话新建 TCP)——E5 顺带验证。 6. **删 lftpd 应用层阻塞重试**:lftpd_inet.c:39-70(5×2000ms)、lftpd.c:473-477(3×200ms)—— - 就绪屏障已消除 Trap 09 启动形态;PASV 监听失败改走 `net_listener_try` 登记。 + 就绪屏障已消除 TRAP-NET-09 启动形态;PASV 监听失败改走 `net_listener_try` 登记。 验收:`@build` + `ci_local` + 台架回归全套现成脚本(`ftp_regression.py`、`n30_repro.py --n 50`、 :7003 快速复连、`iec104_master.py`、`time_sync_server.py`)+ `@test` 锚点。 @@ -163,18 +163,18 @@ Trap 23 RST/FIN 5/5+5/5 + @test PASS + 0 错 0 警。**E8 深查修正**(见 并**不再读清全局中断寄存器**(F-NET-7:全局唯一读者=`net_poll`)。 4. **U15 内化**(若 E6 证实频率>0):reconciler 增一条"GET_RECV_LEN>0 但 readable=0 → 置 readable" 低频对账;随后 ui_net_sink.c:552-558、iec104_serv.c:246-253 的 32 轮盲收退役。 - ⏸ **受 E6 门禁暂缓**(E6 未给出"频率>0"的定案数据,按 Trap 25「不留未证明的复杂度」不动消费者盲收; - 待 E6 专项窗口补数据后再裁决)。 + ✅ **E6 已裁决(2026-10-13,实验方案 E6 结果):命中=0(含压测)→ 不立项**,U15 登记观察项, + 上层盲收保留现状(与 TRAP-NET-20「不留未证明的复杂度」一致;插桩代码留仓宏=0 零成本,备再武装)。 5. **镜像降级**:`net_sock_state_t` 保留为展示/门控投影,accept/recv/can_* 决策点改读驱动缓存。 ✅ **已实施(2026-10-13)**:`net_types.h` 注释把 8 态明确声明为"**本层会话投影**,非芯片状态事实源" (芯片事实源=驱动缓存,刷新点=net_poll);reconciler / 探针 / U11 重同步等按芯片状态决策的点全部改读缓存, 事件路径(`handle_*_event`)保留**主动 SPI**(缓存刷新在 net_poll 末尾,事件路径上必然滞后)。 -验收:完整台架回归 = Trap 22(:7003 快速复连 10/10、:21 100ms/0ms/600ms 各 50/50)、 -Trap 23(RST 10/10 + FIN 5/5,SHA-256)、Trap 25(n30_repro 11/11)、E-02(e02_probe.py)、 +验收:完整台架回归 = TRAP-NET-17(:7003 快速复连 10/10、:21 100ms/0ms/600ms 各 50/50)、 +TRAP-NET-18(RST 10/10 + FIN 5/5,SHA-256)、TRAP-NET-20(n30_repro 11/11)、E-02(e02_probe.py)、 104(iec104_master.py)、对时(time_sync_server.py)、`@test` 锚点 + `[STACK]` 水位对比。 -✅ **2026-10-13 全绿**:`@test` PASS;`[STACK]` net peak 1116/3840(70% 余量);Trap 22 `:7003` 50/50 + -`:21` 100ms/0ms/600ms 各 50/50;Trap 23 RST 10/10 + FIN 5/5;Trap 25 n30 11/11;E-02 双 PASS; +✅ **2026-10-13 全绿**:`@test` PASS;`[STACK]` net peak 1116/3840(70% 余量);TRAP-NET-17 `:7003` 50/50 + +`:21` 100ms/0ms/600ms 各 50/50;TRAP-NET-18 RST 10/10 + FIN 5/5;TRAP-NET-20 n30 11/11;E-02 双 PASS; iec104 18/18;对时跨会话连续更新;`ftp_regression` 11/11;integrity 15/15 SHA; `stack_report` netTask 静态链 **1944 B 不变**(链尾改经 `net_listener_retry_tick`→`net_relisten_locked`)。 @@ -189,17 +189,17 @@ E-02 的最后一环不是"挂起 recv 不冻结 netTask"(阶段 1.5 已解决 timeSync 30s 预算耗尽路径已接入;模拟器(conn≠listen fd)实现为关数据连接。 - **`net_poll` 每轮步进 `ch395f_sock_reset_poll`**(驱动契约"在 net_poll 步进";IDLE 槽纯 RAM 判零 SPI): 同步包装 600ms 放弃后复位机不搁浅,对端 FIN 到达即自动走完、槽位可复用。 -- **芯片定案(E-02 台架,2026-10-13)**:CH395F `close` **无 RST 语义**(Trap 07 定案更正已载)—— +- **芯片定案(E-02 台架,2026-10-13)**:CH395F `close` **无 RST 语义**(TRAP-NET-07 定案更正已载)—— 对**内核存活但不 close 的对端**发 FIN 后芯片停 `FIN_WAIT_2`(实测 0x05/0x06 长尾),监听恢复受限于对端 close; 对端配合则 `TIME_WAIT` 过渡 ~150ms → reconciler 形态④ relisten → 新连接接入。 - 不配合场景兜底 = 芯片 TIMEOUT/KeepAlive 拆僵尸(E13 定案 30~90s)。详见 `docs/问题记录/CH395F_Trap_Records.md` Trap 07 补充。 + 不配合场景兜底 = 芯片 TIMEOUT/KeepAlive 拆僵尸(E13 定案 30~90s)。详见 `docs/问题记录/CH395F_Trap_Records.md` TRAP-NET-07 补充。 ### 5.2 relisten 沉降段统一(延后项,登记不做强行收口) `net_relisten_locked` 隔离段仍是**手拼**序列(排空 RX + 主动 close + 读本 Socket SINT + 轮询状态 + 30ms 重试轮), 未统一改走 `sock_reset` 原语。判定:它**不是忙等**(每轮短 SPI + 交重试轮询,netTask 无长阻塞), -且 Trap 23 的沉降等级(GRACEFUL 100ms / QUARANTINE 400ms / 800ms 强制放行 < 2s 时限)是**生产已调优**的时序, -统一改造的回归面覆盖每次断连热路径,收益只是"少一段重复代码"——按 Trap 25 纪律**延后**, +且 TRAP-NET-18 的沉降等级(GRACEFUL 100ms / QUARANTINE 400ms / 800ms 强制放行 < 2s 时限)是**生产已调优**的时序, +统一改造的回归面覆盖每次断连热路径,收益只是"少一段重复代码"——按 TRAP-NET-20 纪律**延后**, 待下一次触碰该热路径的任务顺路处理(`sock_reset` 原语已具备承接条件:二段 CLOSE、quiescent 排空、等级化最短等待)。 ## 6. 横向纪律 diff --git a/docs/网络/N34-自愈审计/实验方案.md b/docs/网络/N34-自愈审计/实验方案.md index 567fd8b7..7c874cec 100644 --- a/docs/网络/N34-自愈审计/实验方案.md +++ b/docs/网络/N34-自愈审计/实验方案.md @@ -19,7 +19,7 @@ ### E1 · 压力下对账/pump/D10 命中量化(台架,不烧录,用现网固件) - **假设**:常态下 `reconcile` 命中≈0(E0 已证 3.5h 零命中);快速复连压力下命中才出现, - 且 B3 修复后"自家制造的丢中断"占比应显著低于 Trap 22 时代。 + 且 B3 修复后"自家制造的丢中断"占比应显著低于 TRAP-NET-17 时代。 - **方法**(设备跑当前产品固件,PC 侧压测): 1. `n30_repro.py --n 50 --size 1024`(:21 背靠背 STOR); 2. `ftp_regression.py`(多数据连接会话); @@ -28,7 +28,7 @@ 4. `iec104_master.py` 连接/断开 ×10。 每轮之间 serial `clear`,跑完 grep `reconcile|pump:|relisten|give up|lost` 计数 + 恢复延迟。 - **判据**: - - reconcile 命中数 / 50 轮(目标基线:Trap 22 时代 2/10 死亡 → 现在应 0 死亡); + - reconcile 命中数 / 50 轮(目标基线:TRAP-NET-17 时代 2/10 死亡 → 现在应 0 死亡); - `pump: chip not listening` 与 `relisten retry ok` 次数与间隔(量化 D10 恢复窗); - 端口拒绝窗(connect 被拒的间隔分布)→ 决定阶段 3 后"恢复窗 ~1s 拒绝新连接"是否仍需改善。 - **门禁**:阶段 3 单一 reconciler 的周期/去抖参数;F-NET-7 收窄优先级。 @@ -70,8 +70,8 @@ ### E4 · sock_reset 内部顺序 A/B(裸机芯片台架 + 生产路径复现) -- **假设**:Trap 23 排渣只需"排空 RX(读空)→ close → 排空事件 → 等 CLOSED", - `CLEAR_RECV_BUF` 命令可不用(Trap 25 已证缓冲重分配无效);GRACEFUL 与 QUARANTINE 差异仅在 +- **假设**:TRAP-NET-18 排渣只需"排空 RX(读空)→ close → 排空事件 → 等 CLOSED", + `CLEAR_RECV_BUF` 命令可不用(TRAP-NET-20 已证缓冲重分配无效);GRACEFUL 与 QUARANTINE 差异仅在 是否排 RX + 最短等待。 - **方法**:E2 实验固件加变体:close 前 (a)读空 RX (b)CLEAR_RECV_BUF (c)什么都不做, 随后复监听 + 立即上传,校验载荷完整性(SHA/图案);生产路径侧用 `n30_repro.py` 变体 @@ -112,7 +112,7 @@ E6 挂机期间并行实施阶段 0。 | 模式 | 命中 | 说明 | |---|---|---| -| `reconcile` | **0** | Trap 22 对账整窗口零命中(冷路径坐实) | +| `reconcile` | **0** | TRAP-NET-17 对账整窗口零命中(冷路径坐实) | | `chip not listening` | 2 | 均为 `sock2 pump: chip not listening (0x00/0x00, was state=0) -> relisten`(pump 情形②) | | `relisten` | 36 | 其中 ~24 条是 `relisten quarantine/settle (sock=0x00 tcp=0x00)` **重试循环逐轮留痕**(30ms 节奏),其余为 `auto-relisten ok`/`relisten retry ok` 成功尾 | | `settle` / `quarantine` | 12 / 14 | sock1(GRACEFUL)与 sock2(QUARANTINE)各若干次会话收尾 | @@ -147,7 +147,7 @@ bit3=1 契约)→ 释放路径**只置事实不重建**(N-32 定案)→ `net_listener_try` 失败 → 30ms 后 D10 重试成功(~75ms)。**每次会话收尾产生 ~0.3–1s 监听空窗**; 空窗内 SYN 被静默丢弃(无 RST),客户端 1s RTO 重传 → :7003 复连 p50≈1020ms。 → 直接支撑"重建内联进释放事务"(mbed/FreeRTOS reuse 模型)与 `sock_reset` 原语; -也解释 Trap 22 时代"恢复窗拒绝新连接"的口径。 +也解释 TRAP-NET-17 时代"恢复窗拒绝新连接"的口径。 **发现 B(P0,新):背靠背数据连接失效循环,现固件可复现 9/10**: 失败签名(sock4=FTP 数据监听):数据连接 accept 后 **~20ms** 被**上一条连接的陈旧 DISCONNECT** @@ -156,7 +156,7 @@ bit3=1 契约)→ 释放路径**只置事实不重建**(N-32 定案)→ 立即 relisten → 下一条连接仍落在残渣投递窗内 → 再被杀——**自持循环**。`closed-with-data accept` 偶发救回(窗口内仅 2 次)但 lftpd 数据门控(`lftpd_inet_accept_timeout` 直读 `p_sock->state` 镜像) 错过窗口 → 5s 超时 → 425。 -对照台账:Trap 25 定案回归 = `ftp_regression`(有间隔)11/11,**n30_repro(背靠背)不在终版回归集**; +对照台账:TRAP-NET-20 定案回归 = `ftp_regression`(有间隔)11/11,**n30_repro(背靠背)不在终版回归集**; Trap 记录证据通则节自认"历史台架计数日志未入库不可复核"——本次 9/10 是**首个入库可复核的背靠背数据**。 初步归因指向:G1 GRACEFUL 沉降门 `recv_bytes≠0` 对"被陈旧事件杀死的零数据会话"失效(新缺口, 建议登记 F-NET-15);G2 三处全局读清残留(F-NET-7)与事件护栏缺失(F-DRV-2)是残渣投递的供给面。 @@ -206,7 +206,7 @@ CONNECT/RECV_OK(net_poll 门控焊死)→ 数据连接事件永不到达 lft | ftp_regression | 11/11 | | :7003 100ms×50 | 50/50 p50=1011ms | | iec104_master | 18/18 PASS | -| Trap 23 RST×5 / FIN×5(`MDK-ARM/ftp_abort_probe.py --check-abort`) | 5/5(450)/ 5/5(226) | +| TRAP-NET-18 RST×5 / FIN×5(`test/host/ftp_abort_probe.py --check-abort`) | 5/5(450)/ 5/5(226) | **裁决**:M1+M2+M4-lite 把背靠背 STOR 从 8% 拉到 **96.7%(116/120)**,残余 ~3% 偶发 = 原始"滞后 DISCONNECT 杀新连接"形态(D20 结构性失明,M3 域)——**自愈型、无簇、无数据损坏**。 @@ -218,7 +218,7 @@ CONNECT/RECV_OK(net_poll 门控焊死)→ 数据连接事件永不到达 lft ### E8 残余偶发深度排查(2026-10-13 00:06–00:2x,提交 9ac9879 后固件,台架锁 N34) -**背景**:E7b 报"残余 ~3%"后,Trap 23 abort 探针(RST×5+FIN×5)再跑 n30 → **27/50** +**背景**:E7b 报"残余 ~3%"后,TRAP-NET-18 abort 探针(RST×5+FIN×5)再跑 n30 → **27/50** (两个持续簇 #6-25、#44-50)——"3%"不是稳定值,故障率随设备状态在 2%~54% 间摆动。 **实验矩阵(同固件、同负载,仅设备状态不同)**: @@ -245,7 +245,7 @@ pump → settle ×4 → auto-relisten ok ← 800ms 强制放行才出簇 **机制链(三个叠加缺陷,登记 F-NET-17)**: 1. **沉降自再生锁存**:沉降段 `net_listener.c:174` 的 `close_socket` 在芯片已 CLOSED 后再 close 仍会(经拆链收尾)重新锁存 DISCONNECT;`:176` 的 SINT 读清发生在 - close 后**同一轮**,而锁存在 close 后 ~10-20ms 才落(Trap 14 实证)→ 沉降最后一轮 + close 后**同一轮**,而锁存在 close 后 ~10-20ms 才落(TRAP-NET-12 实证)→ 沉降最后一轮 读清后残留一枚"迟到锁存",relisten 完成后 11ms 杀死首条连接。30ms 重试粒度 vs 10-20ms 锁存延迟 = 每次重建掷一次硬币(E7b 的 ~2% 偶发即此)。 2. **泵与救回互抢**:沉降段 `:172` `net_drain_chip_rx` 排空的正是 closed-with-data @@ -494,7 +494,7 @@ TIMEOUT → 即 E8 记录的**残渣风暴态**,且**跨烧录存活**(重 1. 现状驱动**确属规格偏离**(零 TSL 间隔,按"间隔"语义解读),但**未测得实际危害**: 460 轮/≈58MB 双向字节级校验仅 1 次未复现异常,且两臂无差异。 -2. 依 Trap 25 铁律「不留未证明的复杂度」:**当前不建议改产品路径**,而是 +2. 依 TRAP-NET-20 铁律「不留未证明的复杂度」:**当前不建议改产品路径**,而是 **登记该偏离 + 本轮证据**;B 变体作为**待用补丁**保留(若将来出现芯片批次 差异、提高 SCK、或不明数据错位,可直接采用——它已证明无吞吐/正确性回退)。 3. **方法学教训(E13 修正版,后续时序/连接类实验必读)**:本台架的**僵尸会话 @@ -599,14 +599,14 @@ CLOSE_SOCKET 是二段关闭**。CLOSE 命令对 LISTEN 态 Socket 返回 SUCCES 但只拆监听(TCP 01H→00H),**byte0 恒留 05H(Socket 仍 OPEN),2s 内不自落**; 需再发一次 CLOSE(此时 TCP 已 00H)byte0 才落 00H(二段间隔实测 0ms 即落)。 生产 net 层早已在**无意中吸收**该语义:`net_close_locked` 等 CLOSED 的 500ms 窗 -超时后照常放行,随后重 listen 时 `net_listen_locked` 的 Trap 09 open 重试 -(3 次 × 500ms 沉降)第二次 open 前的 close 恰好充当二段——这就是 Trap 09 +超时后照常放行,随后重 listen 时 `net_listen_locked` 的 TRAP-NET-09 open 重试 +(3 次 × 500ms 沉降)第二次 open 前的 close 恰好充当二段——这就是 TRAP-NET-09 "关闭后需延时才能重开"重试环存在的芯片级原因之一。阶段 2 `sock_reset_poll` 的 WAIT_CLOSE 阶段已内置二段补发(标记=drain 字段 0xF,只补发一次), TC1502b 实证 LISTEN 态复位 GRACEFUL ~180ms 落定。 **对阶段 3 的含义**:任何以 `cmd_done(DONE_CLOSE)` 为判据的等待,若目标 Socket 可能处于 LISTEN,必须容忍"一次 CLOSE 不落 00H"——要么走 sock_reset(已内置), -要么保留 Trap 09 式重试,不得把 DONE_CLOSE 当单次 CLOSE 的充分条件。 +要么保留 TRAP-NET-09 式重试,不得把 DONE_CLOSE 当单次 CLOSE 的充分条件。 ### E3 结果(同场同固件,5 命令 × 5 轮 × 2 遍 = 50 样本,两遍签名逐条一致) @@ -625,7 +625,7 @@ TC1502b 实证 LISTEN 态复位 GRACEFUL ~180ms 落定。 **三条实证**: 1. **残留假阳性坐实**:陈旧错误码 0x17(ERR_VAL,源自"对已关闭 Socket 再 close") 跨后续 SET 命令(不更新状态)持续存活——复核 `docs/问题记录/CH395F_Trap_Records.md` - Trap 25 定案轮的"陈旧残留"形态(RESET_ALL 陈旧 BUSY 变体本矩阵未复现,同类 + TRAP-NET-20 定案轮的"陈旧残留"形态(RESET_ALL 陈旧 BUSY 变体本矩阵未复现,同类 陈旧值持续存活已证)。 2. **OPEN/LISTEN/CLOSE 均在 <2ms 完成**,BUSY 从未观察到——"BUSY→终态"转变对这三条 命令不可观测;驱动 `poll_cmd_status` 20ms 粒度对它们恒白等 ≥20ms(无害但无判别价值)。 @@ -643,26 +643,26 @@ GET_CMD_STATUS 仅保留两用途:INIT_CH395 类慢命令(内核前/测试 ### E4 结果(同场同固件 + 生产路径基线;3 变体 ×10 轮 ×2 遍 = 60 轮) **方法**:每轮先造污染会话(主机 `test/host/ch395f_reuse_host.py --mode e4`:连上 → -发 3000B → RST,Trap 23 中止形态),芯片侧按变体执行复位序列,**立即**原端口重开 +发 3000B → RST,TRAP-NET-18 中止形态),芯片侧按变体执行复位序列,**立即**原端口重开 + LISTEN,主机立即上传 4096B 图案载荷(图案种子随全局轮次变,上一轮残渣必失配)。 PASS 口径 = 消费者在 DISCONNECT/TIM_OUT 前实际读到的字节数=4096 且图案逐字节匹配 (= 生产 net 层释放语义;事后读空仅作取证,不计入 PASS)。 | 变体 | 第 1 遍 | 第 2 遍 | 观察 | |---|---|---|---| -| A 读空 RX→CLOSE→排事件→等 byte0=00H | **10/10** | **10/10** | 19/20 轮 rxdrain=**2048**(残渣恰等于芯片单 socket RX 缓冲整容 = Trap 23 机制直接证据) | +| A 读空 RX→CLOSE→排事件→等 byte0=00H | **10/10** | **10/10** | 19/20 轮 rxdrain=**2048**(残渣恰等于芯片单 socket RX 缓冲整容 = TRAP-NET-18 机制直接证据) | | B CLEAR_RECV_BUF→CLOSE→排事件→等 CLOSED | **10/10** | **10/10** | 与 A 等价,无差异 | | C 什么都不做→CLOSE→等 CLOSED | **0/10** | **0/10** | 首 C 轮(rg20)两遍同签名,随后**自持级联** | **C 变体失败签名(两遍逐字节复现)**:重开后主机载荷连接 SYN 被吞(主机侧 -connect 超时);bench 首次 SINT 读 = **0x04**(陈旧 RECV 位粘滞——Trap 14 +connect 超时);bench 首次 SINT 读 = **0x04**(陈旧 RECV 位粘滞——TRAP-NET-12 "粘滞到被读",close/open 不清 SINT);后续轮污染连接亦 SYN 被吞(主机 `contam CONNECT timeout`、bench `no death event`)——每轮 close 又携带未排空数据 → **级联自持**,open+listen 命令照常返回 SUCCESS 也无法恢复,唯芯片硬件复位 (E13 RSTI←NRST)清除。 **机制定案**:决定性一步 = **close 前 RX 缓冲必须为空**(读空与 CLEAR_RECV_BUF -等价——Trap 25"缓冲重分配无效"指的是 set_send/recv_buf 重分配,不是 CLEAR)。 +等价——TRAP-NET-20"缓冲重分配无效"指的是 set_send/recv_buf 重分配,不是 CLEAR)。 带非空 RX 关闭违反 `docs/芯片手册/CH395F.md` §5.52 bit3=1"读出残留数据后再关闭" 契约 → 芯片拆链停滞 → 下一会话 SYN 被吞/CONNECT 丢失,且自持。排空事件(A/B 有、 C 无)清的是粘滞 SINT 位——对**事件驱动 accept 的正确性**必需(0x04 陈旧 RECV 会 @@ -670,7 +670,7 @@ C 无)清的是粘滞 SINT 位——对**事件驱动 accept 的正确性**必 byte0 已是 00H)——"等 CLOSED"几乎零成本,但为契约必需步。 **生产路径基线**(烧 bench 固件前,Phase 1.5 产品固件 Code=209780): -`MDK-ARM/ftp_abort_probe.py --check-abort --rounds 5` → **5/5**(中止 STOR→450; +`test/host/ftp_abort_probe.py --check-abort --rounds 5` → **5/5**(中止 STOR→450; 设备日志 `sock4 TIMEOUT` → quarantine → `UPLOAD '/ab000N.bin' FAILED` → followup `UPLOAD '/ab_ok.bin' 4096 bytes OK`)——现实现(隔离期持续排空 RX + close + 排空事件 + 等 CLOSED)行为基线全绿,与变体 A 序列一致。 @@ -687,7 +687,7 @@ C 不等价(0/20 + 级联)→ **`ch395f_sock_reset` 内部序列定案**: GRACEFUL 与 QUARANTINE **都必须排 RX**——E4 假设"差异仅在是否排 RX + 最短等待"中 "是否排 RX"一半被否(带脏缓冲关闭在任何释放形态下都危险),两模式差异只剩 -最短等待(100ms/400ms,Trap 23 二阶段实测值)。 +最短等待(100ms/400ms,TRAP-NET-18 二阶段实测值)。 ### E-02 收口台架(2026-10-13,阶段 3 产品固件 Code=211348,`test/host/e02_probe.py`) @@ -700,7 +700,7 @@ GRACEFUL 与 QUARANTINE **都必须排 RX**——E4 假设"差异仅在是否排 | 阶段 | 观察 | |---|---| | 预算耗尽 | `time_sync: no TIME packet in 30s, drop conn` → netTask 侧 `sock_reset QUARANTINE`(FIN + 排渣 + 排空事件 + 等 byte0=00H,400ms 底)+ 本层收口 CLOSED + `auto_relisten` 即时 `net_listener_try` | -| 对端不配合(PC socket 仍开) | 芯片停 **`FIN_WAIT_2`**(byte0=`05H` / byte1=`06H`)长尾——`close` 无 RST(Trap 07 定案更正),监听恢复**受限于对端 close**;复位机在 `net_poll` 步进里等待,不搁浅 | +| 对端不配合(PC socket 仍开) | 芯片停 **`FIN_WAIT_2`**(byte0=`05H` / byte1=`06H`)长尾——`close` 无 RST(TRAP-NET-07 定案更正),监听恢复**受限于对端 close**;复位机在 `net_poll` 步进里等待,不搁浅 | | 对端配合(探针 close 自己的 socket) | 芯片 `TIME_WAIT` 过渡 ~150ms(TC-NET-204 模式 A 同形)→ byte0 回 `00H` → **reconciler 形态④**(本层 CLOSED + 芯片不在 LISTEN)relisten | | 恢复 | 对端 close 后 **~500ms** 内 :8888 重新接入(形态④ 去抖 2 轮 = 200ms + TIME_WAIT ~150ms + open/listen);新连接发完整 8 字节 TIME 包 → 设备落 **`wall clock updated`** | | 探针判定 | `OVERALL: queue=PASS listener=PASS`(双 PASS) | @@ -718,4 +718,18 @@ GRACEFUL 与 QUARANTINE **都必须排 RX**——E4 假设"差异仅在是否排 **结论**:E-02 三段收口齐备——① 挂起 recv 不冻结 netTask(阶段 1.5);② 应用可主动拆会话保监听 (`net_drop_conn`,幂等、EOF 路径不得调);③ 复位机不搁浅(`net_poll` 步进 `ch395f_sock_reset_poll`)。 -芯片侧物理边界(无 RST、`FIN_WAIT_2` 长尾受限于对端)已登记 `docs/问题记录/CH395F_Trap_Records.md` Trap 07 补充。 +芯片侧物理边界(无 RST、`FIN_WAIT_2` 长尾受限于对端)已登记 `docs/问题记录/CH395F_Trap_Records.md` TRAP-NET-07 补充。 + +### E6 结果(2026-10-13 15:20–19:25,U15 插桩固件 Code=211692;用户裁决提前收线 ~4h,原规格 24h) + +- **配置**:`NET_U15_PROBE=1` 两段确认插桩(period=32、log_per_h=1),`U15 probe armed` ×2 确认 + (15:19/15:20 两次 boot);窗口内常态运行 + 每小时 `:7003×10`(gap=100ms)全过 + hour04 E1 压测 + (`:7003×50` gap=0 → 50/50、`:21×50` gap=100 → 50/50,拒连 0)。 +- **判据**:`U15 probe:` 命中 = **0**(含压测)→ 按 §E6 判据,U15 登记**观察项**,阶段 3 第 4 项 + (U15 内化)**不立项**;上层 32 轮盲收保留现状。 +- **零复位**:窗口内除 armed 两次 boot 外无启动锚点、无 Fault。 +- **附带观察(非 U15,登记备查)**:E1 `:21×50` 压测尾 25s 内 7 条 `[ERR][WD] task stall` tripwire + (miss≈2.5s 级、缺失位在 0x80/0x14/0x4 间漂移),无复位自恢复、50/50 连接全过;同窗 + `sock2 TIMEOUT` + `[FTP] error reading next command` = 客户端放弃连接的正常形态。 + 若产品构建复现再查 FTP hammering 下的任务饿死。 +- **收尾**:宏已回 0、产品构建重编烧录回归;插桩代码留仓(宏=0 零成本),需要时重新武装。 diff --git a/docs/网络/N34-自愈审计/网络不稳根因总览.md b/docs/网络/N34-自愈审计/网络不稳根因总览.md index 8f872543..349400f1 100644 --- a/docs/网络/N34-自愈审计/网络不稳根因总览.md +++ b/docs/网络/N34-自愈审计/网络不稳根因总览.md @@ -18,7 +18,7 @@ ## 1. 七个误会(叠加才成为"玄学") -### ① 门铃会丢(Trap 22:丢中断) +### ① 门铃会丢(TRAP-NET-17:丢中断) 快速复连时芯片本应"按门铃"(INT# 拉低)通知有新来访/客人离开,但门铃**会哑**—— 人已经站在屋里,铃没响。传达员只靠门铃干活,于是本层状态**静默滞留**:端口其实 @@ -26,7 +26,7 @@ 死掉"就是它——不是断网,是**没人报信**。裸机芯片台架(剥离 OS/net/FTP)独立复现, 证明这是芯片天性,不是固件 bug(见 `docs/网络/N34-自愈审计/README.md` 结论 1)。 -### ② 门铃总闸是"看一眼就清零"(Trap 25 B3) +### ② 门铃总闸是"看一眼就清零"(TRAP-NET-20 B3) 全局中断寄存器是**一整排灯,读一次全部熄灭**。历史上 open/relisten 里有两处 "丢弃式读一遍"——等于**查自己门铃时把全楼别的住户的门铃线也拔了**,别人的 @@ -98,14 +98,14 @@ sequenceDiagram | 环 | 拆法(已落地) | 证据/落点 | |---|---|---| -| ① 丢中断 | 单一 reconciler:100 ms 巡逻对账五种形态,判据读驱动状态缓存 | Trap 22;实验方案 E2(回读 18.1 µs 背书);实施方案 阶段 3 项 1 | -| ② 总闸读清 | 全局寄存器唯一读者=net_poll(F-NET-7);open/relisten 只清本 Socket SINT | Trap 25 B3;实施方案 阶段 0/2 | +| ① 丢中断 | 单一 reconciler:100 ms 巡逻对账五种形态,判据读驱动状态缓存 | TRAP-NET-17;实验方案 E2(回读 18.1 µs 背书);实施方案 阶段 3 项 1 | +| ② 总闸读清 | 全局寄存器唯一读者=net_poll(F-NET-7);open/relisten 只清本 Socket SINT | TRAP-NET-20 B3;实施方案 阶段 0/2 | | ③ 拆柜台 | 铁律"EOF 不得 close 监听 fd" + 新 API `net_drop_conn`(拆会话保监听) | 生命周期设计说明 §10.10;实验方案 E-02 | | ④ 多所有者 | 重建收敛唯一所有者(net_listener + reconciler),其余只置事实不动手 | README 结论 4(不变量 II);实施方案 阶段 3 项 1 | | ⑤ 队头阻塞 | 忙等统一 `net_sock_reset_sync` 有界排渣(600 ms 硬顶);recv 非阻塞消息化 | 实验方案 E-02(banner 39 ms);阶段 1.5 | | ⑥ 僵尸会话 | keepalive 60/5/3 + 芯片 TIMEOUT → QUARANTINE → 重建(30~90 s 自愈) | 实验方案 E11/E13(用户判断"不该依赖冷启动"被证实) | -| ⑦ 无 RST | 定案入 Trap 07:兜底靠查岗不靠 RST;`net_drop_conn` 本地拆除 + 对账③④ 认领 | Trap 07 定案更正;实验方案 E-02 收口 | -| ⑧ 残渣风暴 | `sock_reset` 标准排渣(读空→CLOSE→排空事件→byte0=00H 确认)+ M3 矛盾态门 | Trap 23/25;实施方案 M3/M5(E8/E9 风暴态免疫) | +| ⑦ 无 RST | 定案入 TRAP-NET-07:兜底靠查岗不靠 RST;`net_drop_conn` 本地拆除 + 对账③④ 认领 | TRAP-NET-07 定案更正;实验方案 E-02 收口 | +| ⑧ 残渣风暴 | `sock_reset` 标准排渣(读空→CLOSE→排空事件→byte0=00H 确认)+ M3 矛盾态门 | TRAP-NET-18/-20;实施方案 M3/M5(E8/E9 风暴态免疫) | ## 5. 一句话总结 diff --git a/docs/网络/Net层阻塞模型架构设计.md b/docs/网络/Net层阻塞模型架构设计.md index 0d8f3db1..051b3c90 100644 --- a/docs/网络/Net层阻塞模型架构设计.md +++ b/docs/网络/Net层阻塞模型架构设计.md @@ -43,7 +43,7 @@ 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 22 对账 + 监听重建唯一入口,N34 阶段 3 起原 `net_listener_pump` 已并入)→ 排空消息队列 | +| **中栏 · netTask 主循环** | ~1ms 节拍:`net_poll`(刷新 readable + 全量刷新驱动状态缓存)→ 3 个非阻塞 tick(`net_listener_retry_tick` D10 → `net_pending_tick` 了结挂起 → `net_reconcile_tick` TRAP-NET-17 对账 + 监听重建唯一入口,N34 阶段 3 起原 `net_listener_pump` 已并入)→ 排空消息队列 | | **下带 · 五路判定** | 每轮对每个挂起 fd 按序判定:数据到达 / 到点(仅显式预算)/ EOF / 同 fd 后续操作 / 网络重建;tripwire 60s 只留痕(N34 阶段 1.5 由 1s 看门狗降级) | | **回环** | 判定完成经 `xTaskNotify` 唤醒调用方(结果 = 字节数 / 负错误码) | @@ -198,7 +198,7 @@ CONNECT → ESTABLISHED 是合法迁移,推断取消会误杀正在建链的 | # | 门禁 | 结果 | |---|---|---| -| 1 | CH395F NET 层套件 | **PASS**:F1 组合 Phase 6/9/11(64B/1460B 回显 11/11、select 回显 11/11、断开重连、KeepAlive + auto-relisten);F2 组合 Phase 8-RECV(100KB 阻塞接收 50/50 块 + CRC32 整包一致 0x9A0E0C8C)。Phase 7/10 未跑(DONTWAIT 路径另由 M3 探针族 L5~L7 覆盖)。TC-NET-604 FAIL = 既有芯片侧问题(Trap 21,2026-09-23 结案非缺陷) | +| 1 | CH395F NET 层套件 | **PASS**:F1 组合 Phase 6/9/11(64B/1460B 回显 11/11、select 回显 11/11、断开重连、KeepAlive + auto-relisten);F2 组合 Phase 8-RECV(100KB 阻塞接收 50/50 块 + CRC32 整包一致 0x9A0E0C8C)。Phase 7/10 未跑(DONTWAIT 路径另由 M3 探针族 L5~L7 覆盖)。TC-NET-604 FAIL = 既有芯片侧问题(2026-09-23 结案非缺陷,判读教训见 `docs/问题记录/CH395F_Trap_Records.md` 台架纪律) | | 2 | M3 基线重锁 | **PASS**:见 §6 | | 3 | e02_probe A/B | **PASS 且优于分片方案**:队列响应性 92.5ms;30s 截止按节拍精确触发(30.1s);监听恢复 6.6s | | 4 | ftp_regression / E-06 假 425 | 随 F1/F2 + M3 压力档覆盖(FTP 半登录态即其场景) | diff --git a/docs/问题记录/CH395F_Trap_Records.md b/docs/问题记录/CH395F_Trap_Records.md index ae418d4c..ae7cf8a8 100644 --- a/docs/问题记录/CH395F_Trap_Records.md +++ b/docs/问题记录/CH395F_Trap_Records.md @@ -1,273 +1,213 @@ -# CH395F 驱动陷阱记录 +# CH395F 驱动陷阱记录(TRAP-NET 系列) -记录开发过程中遇到的所有坑、根因、解决方案,避免重复踩坑。 +CH395F 以太网驱动与 NET 层的实证陷阱:现象、定案根因、修复、回归判据。改驱动/NET 层前先读本文。 -> **2026-10-13 重组审计**:合并同根因条目(14+17、13+21)、删除已被推翻的误判叙述(只留一行误判存档)、 -> 按主题分章并加索引;条目格式统一为 `## Trap NN · 短标题` + 固定小节(状态/现象/根因(定案)/修复/回归判据/教训, -> 视情况取舍)。**编号永不重排**——代码注释大量引用 `Trap NN`,合并条目标题保留双编号; -> Trap 19 从未存在(悬空引用登记在第六章待修清单)。 -> 状态图例:✅ 已修复定案 · ⏳ 待修 · ↩️ 已撤回(非缺陷) · ↦ 已移入他档 · — 从未存在。 +> 编号口径:按 `docs/编号规范.md` 采用 `TRAP-NET-`,连续 01~21。编号沿革(2026-10-13 全库重排): +> 旧 01~11 不变;旧 14→12、15→13、16→14、17→12(并入)、18→15、20→16、22→17、23→18、24→19、 +> 25→20、26→21;旧 12/13/19/21 已移出/并入/删除,无条目。旧形 `Trap NN` 为违规裸形已全库迁移; +> 旧号只存在于历史提交与旧日志。编号不复用、不重排。 +> 状态图例:✅ 已修复定案 · ⏳ 待修。 ## 索引 | 编号 | 标题 | 状态 | 一句话结论 | |---|---|---|---| -| 01 | Socket 配置顺序 | ✅ | `TCP_LISTEN` 时刻一次性扫描:先配数据 Socket 1~7,最后配监听 Socket 0 | -| 02 | Socket 缓冲显式分配 | ✅ | 48 块×512B 几何,Socket 4~7 默认零块,必须显式分配否则数据重叠/中断不触发 | -| 03 | DHCP xid 偏移 | ✅ | xid=`data[4:8]`(Wireshark 旧版显示误导) | -| 04 | UDP 发送后等 SENBUF_FREE | ✅ | 不等则下次写入被静默丢弃 | -| 05 | PHY 自动协商不兼容 | ✅ | 与 RTL8305NBI 直连协商失败,强制 100M 全双工 | -| 06 | KeepAlive 参数 500ms 倍数 | ✅ | 非 500ms 倍数 = 定时器未定义行为;IDLE>INTVL | -| 07 | TCP 关闭重连 | ✅ | 直接 `close`,不要先 `disconnect`;`close` 无 RST(E-02 边界:对端不配合则 `FIN_WAIT_2` 长尾,兜底靠查岗/`net_drop_conn`) | -| 08 | RECV 中断电平触发 | ✅ | 缓冲有数据就一直拉低,`net_poll` 每轮只处理一批 | -| 09 | 沉降窗(link up / close 后) | ✅ | open 失败重试 + `net_sock_reset_sync` 确定性排渣;LISTEN 态关闭是两段 CLOSE | -| 10 | 消息队列值拷贝 | ✅ | 结果必须走 `xTaskNotify` 通知值回传 | -| 11 | accept 拒绝 ESTABLISHED | ✅ | standalone_accept 后状态已 ESTABLISHED,入口检查须放行该组合 | -| 12 | FatFS 路径与 8.3 短名 | ↦ | **非 CH395F 议题**(FTP 改造期归档错位)→ 已移入 `docs/架构设计/STM32F4-Base存储架构说明.md` §10.1 第 14 项 | -| 13/21 | 「recv_len 归零 / 接收冻结」家族 | ↩️ | 合并结案:非缺陷,全部为台架假象;机理与 §5.34 相左已撤回 | -| 14/17 | 残留 DISCONNECT 污染下一连接 | ✅ | 合并:滞后锁存 + SINT 粘滞 → 预清 + 反核实 + close 后排空 | -| 15 | DHCP 后 IP 寄存器提交滞后 | ✅ | 状态位≠寄存器生效:连续 3 次一致才采信 | -| 16 | IP/端口字节序 | ✅ | IP 四字节数组=点分顺序;16 位端口=低字节在前,两套口径 | -| 18 | FUN_PARA bit0 迷思 | ✅ | bit0 保留必须为 0(`0x08`);当年真正起作用的是缓冲显式分配 | -| 19 | — | — | 从未存在(`Test_Guide.md:878` 悬空引用待清) | -| 20 | 单次写超发送缓冲(写穿) | ✅ | 上限=本仓 2 块发送缓冲 1KB(`NET_SEND_CHUNK_MAX`),门控是 SENBUF_FREE | -| 22 | 快速复连丢中断 | ✅ | 芯片丢 CONNECT/DISCONNECT → 周期对账 `net_reconcile_tick`(reconciler 之源) | -| 23 | 上传中止后首笔复连被截断 | ✅ | 残渣投递给下一条连接 → 两级沉降/隔离 + 异常释放回 450 删截断文件 | -| 24 | SET_MAC 顺序 | ✅ | §9.2.1:SET_MAC 必须在 `INIT_CH395` **之前**(INIT 从 EEPROM 加载) | -| 25 | FTP 数据连接三连故障 | ✅ | 四重定案:丢中断窗口探针+幽灵甄别 / closed-with-data / 背靠背小段毒化 TX / 全局寄存器读即清(B3) | +| TRAP-NET-01 | Socket 配置顺序 | ✅ | `TCP_LISTEN` 时刻一次性扫描:先配数据 Socket 1~7,最后配监听 Socket 0 | +| TRAP-NET-02 | Socket 缓冲显式分配 | ✅ | 48 块×512B 几何,Socket 4~7 默认零块,必须显式分配且不重叠 | +| TRAP-NET-03 | DHCP xid 偏移 | ✅ | xid=`data[4:8]`(Wireshark 旧版显示误导) | +| TRAP-NET-04 | UDP 发送后等 SENBUF_FREE | ✅ | 不等则下次写入被静默丢弃 | +| TRAP-NET-05 | PHY 自动协商不兼容 | ✅ | 与 RTL8305NBI 直连协商失败,强制 100M 全双工 | +| TRAP-NET-06 | KeepAlive 参数 500ms 倍数 | ✅ | 非 500ms 倍数 = 定时器未定义行为;IDLE>INTVL | +| TRAP-NET-07 | TCP 关闭重连 | ✅ | 直接 `close`,不要先 `disconnect`;`close` 无 RST(对端不配合则 `FIN_WAIT_2` 长尾,兜底靠查岗/`net_drop_conn`) | +| TRAP-NET-08 | RECV 中断电平触发 | ✅ | 缓冲有数据就一直拉低,`net_poll` 每轮只处理一批 | +| TRAP-NET-09 | 沉降窗(link up / close 后) | ✅ | open 失败重试 + `net_sock_reset_sync` 确定性排渣;LISTEN 态关闭是两段 CLOSE | +| TRAP-NET-10 | 消息队列值拷贝 | ✅ | 结果必须走 `xTaskNotify` 通知值回传 | +| TRAP-NET-11 | accept 拒绝 ESTABLISHED | ✅ | standalone_accept 后状态已 ESTABLISHED,入口检查须放行该组合 | +| TRAP-NET-12 | 残留 DISCONNECT 污染下一连接 | ✅ | 滞后锁存 + SINT 粘滞 → 预清 + 反核实 + close 后排空 | +| TRAP-NET-13 | DHCP 后 IP 寄存器提交滞后 | ✅ | 状态位≠寄存器生效:连续 3 次一致才采信 | +| TRAP-NET-14 | IP/端口字节序 | ✅ | IP 四字节数组=点分顺序;16 位端口=低字节在前,两套口径 | +| TRAP-NET-15 | FUN_PARA bit0 迷思 | ✅ | bit0 保留必须为 0(`0x08`);Socket 可用性取决于缓冲显式分配 | +| TRAP-NET-16 | 单次写超发送缓冲(写穿) | ✅ | 上限=本仓 2 块发送缓冲 1KB(`NET_SEND_CHUNK_MAX`),门控是 SENBUF_FREE | +| TRAP-NET-17 | 快速复连丢中断 | ✅ | 芯片丢 CONNECT/DISCONNECT → 周期对账 `net_reconcile_tick`(reconciler 之源) | +| TRAP-NET-18 | 上传中止后首笔复连被截断 | ✅ | 残渣投递给下一条连接 → 两级沉降/隔离 + 异常释放回 450 删截断文件 | +| TRAP-NET-19 | SET_MAC 顺序 | ✅ | §9.2.1:SET_MAC 必须在 `INIT_CH395` **之前**(INIT 从 EEPROM 加载) | +| TRAP-NET-20 | FTP 数据连接三连故障 | ✅ | 三重定案:丢中断窗口探针+幽灵甄别 / closed-with-data / 背靠背小段毒化 TX | +| TRAP-NET-21 | IP 显示段序与对端地址 | ✅ | `net_inet_ntoa` 段序 MSB 首;对端地址用 `net_getpeername`,勿用 `getsockname` | --- # 第一章 初始化与配置 -## Trap 01 · Socket 配置顺序:先数据 Socket 后监听 Socket +## TRAP-NET-01 · Socket 配置顺序:先数据 Socket 后监听 Socket -- **状态**:✅ 已定案(多连接模式虽已随 `319d00e3` 删除,**顺序规则仍适用于现行 `net_init`**——`net_socket.c` 注释「Trap 01:先配数据 Socket 1~7,再配监听 Socket 0」) -- **现象**:多连接模式下最多接受 3 个客户端,Socket 4~7 的 CONNECT 中断从不触发,SYN 被静默丢弃。 +- **状态**:✅(顺序规则适用于现行 `net_init`——`net_socket.c` 注释「TRAP-NET-01:先配数据 Socket 1~7,再配监听 Socket 0」) +- **现象**:Socket 4~7 的 CONNECT 中断从不触发,新客户端 SYN 被静默丢弃。 - **根因(定案)**:CH395F 在 `TCP_LISTEN` 时刻**一次性扫描**当前所有可用 Socket 建立内部查找表;数据 Socket 若在 `TCP_LISTEN` 之后才配置缓冲区/协议/端口,自动分配逻辑不认识它们。 - **修复**:正确顺序——① 数据 Socket 1~7:`SET_SEND_BUF → SET_RECV_BUF → SET_SOUR_PORT → SET_PROTO_TYPE_TCP`(不调 `OPEN_SOCKET`,连接到达时芯片自动打开);② 监听 Socket 0:`SET_SEND_BUF → SET_RECV_BUF → SET_PROTO_TYPE_TCP → SET_SOUR_PORT → OPEN_SOCKET → TCP_LISTEN`。 - **教训**:`docs/芯片手册/CH395F.md` §9.2.6 要求数据 Socket **先设源端口再设协议类型**(与监听 Socket 顺序相反)。DISCONNECT 后芯片自动关闭 Socket 但保留配置,可再分配。 -- **发现时间**:Phase 8 压力测试(原记 2025-07,早于仓库首提交 `b1d05091` 2026-06-05,日期不可考)。 -## Trap 02 · Socket 缓冲区必须显式分配且不重叠 +## TRAP-NET-02 · Socket 缓冲区必须显式分配且不重叠 -- **状态**:✅ 已定案(现行 `net_init` 固定布局 8×(4收+2发)=48 块即用满) +- **状态**:✅(现行 `net_init` 固定布局 8×(4收+2发)=48 块即用满) - **现象**:多 Socket 数据互相覆盖、收到错误数据或客户端挂死;未分配缓冲的 Socket 发送为固定垃圾内容(`0x0028` 填充)、接收存不进数据、CONNECT/RECV 中断不触发。 - **根因(定案)**:芯片默认只为 Socket 0~3 分配独立收发缓冲(48 块×512B 用满,`docs/芯片手册/CH395F.md` §5.45),Socket 4~7 为零块;未显式分配或误用相同块号导致重叠。 -- **修复**:每个数据 Socket 独占块(历史多连接布局 2发+2收/Socket;现行 `net_init` 固定 6 块/Socket=4收+2发,与 MSS 解耦,见 Trap 21 解耦注)。**必须在 `open_socket` 之前**调 `ch395f_set_send_buf/set_recv_buf`。 -- **教训**:缓冲几何以 §5.45 的 48×512B 为准(此前文档"32×1KB"记录有误,Phase 7 TC704 40/40 实测确认块 0~47 全部有效)。 -- **发现时间**:Phase 8 压力测试(日期同 Trap 01,不可考)。 +- **修复**:每个 Socket 独占块(现行固定 6 块/Socket=4收+2发,与 MSS 解耦)。**必须在 `open_socket` 之前**调 `ch395f_set_send_buf/set_recv_buf`。 +- **教训**:缓冲几何以 §5.45 的 48×512B 为准("32×1KB"说法有误,实测块 0~47 全部有效)。 -## Trap 05 · CH395F 与 RTL8305NBI 自动协商不兼容 +## TRAP-NET-05 · CH395F 与 RTL8305NBI 自动协商不兼容 -- **状态**:✅ 已修复 +- **状态**:✅ - **现象**:`ch395f_get_phy_status()` 恒返回 `PHY_DISCONN`,链路无法建立。 - **根因(定案)**:CH395F PHY 与 RTL8305NBI-CG 直连时自动协商失败(板级组合)。 - **修复**:初始化协议栈后强制 `ch395f_set_phy(CH395F_PHY_100M_FULL)`。 -- **发现时间**:项目初期硬件联调。 -## Trap 06 · KeepAlive 参数必须为 500ms 倍数 +## TRAP-NET-06 · KeepAlive 参数必须为 500ms 倍数 -- **状态**:✅ 已定案(产品参数 60s/5s/3 的转正过程见附录 A「KeepAlive 定案链」) +- **状态**:✅(产品参数 60s/5s/3 的定案链见附录 A) - **现象**:设置 KeepAlive 后 TCP 连接几秒内 TIMEOUT 断开。 - **根因(定案)**:芯片内部定时器以 500ms 为基准,非 500ms 倍数 = 未定义行为;且 `IDLE` 必须 > `INTVL`。 - **修复**:`idle=60000 / intvl=5000 / cnt=3`(均 500 倍数)。 -- **发现时间**:Phase 2 测试(原记 2025-07 不可考;KeepAlive 实验实际对应阶段 11)。 -## Trap 18 · FUN_PARA bit0 迷思:bit0 保留必须为 0 +## TRAP-NET-15 · FUN_PARA bit0 迷思:bit0 保留必须为 0 -- **状态**:✅ 已定案(`net_init` 用 `ch395f_set_fun_para(0x08)`) -- **误判存档**:本节曾断言"bit0=单/多 Socket 模式,必须 0x09 才能用 Socket 1~7"——**无任何出处,已撤回**;当年让 Socket 1~7 真正可用的是缓冲显式分配(Trap 02),不是 bit0。 -- **根因(定案)**: - 1. 官方手册 V1C/V1E/V2.2/V2.3 的 §5.52 位表中 bit0 一律「保留,必须为0」;官方 SDK `CH395INC.H` 无 bit0 宏; - 2. 官方例程直接反例:`EXAM5_TCP_Server` 传 `0x02`(bit0=0)却给 Socket 0~7 全配缓冲;官方 FTP 例程直接传 `0x08`;全 EVT 包无一处 0x09; - 3. 8 Socket 是芯片原生能力(`_SN` 索引寻址),真正的门槛是 §5.45 缓冲分配; - 4. 概念讹传源:bit1 `FUN_PARA_FLAG_TCP_SERVER` 的"服务器多连接"指**一个服务器能接几个客户端**,不是"能用几个 Socket"。 -- **为什么 0x09 当年"看起来有效"**:芯片忽略保留位;TC704 40/40 与 bit0 结论出自同一提交,属自证,无"0x08 下不通过"的对照。留着 bit0=1 的代价:违反保留位规定,后续固件版本一旦启用该位现网设备静默失效。 -- **实测闭环(2026-10-11,N-31 已销项)**:`FUN_PARA=0x08` 下 Phase 7 **34/34 全通过**(`PHASE7_MULTI_COUNT` 降到 5:E-23 未修,8 fd 减 3 常驻监听),并发回显实测落在 sock2/4/5/6/7 → Socket 4~7 在 bit0=0 下完全可用。 -- **台架前置(本轮踩到)**:① PC 网卡落在 Public 防火墙配置文件 → 入站 8081 被静默丢弃,TC701/702/704 全部 `CONNECT TIMEOUT`,极易误判成"0x08 毁了 client";② 上一轮遗留 `tcp_server` 进程占着 8081,新服务器绑不上端口却不报错。**判"芯片行为"前先确认对端真的收到了 SYN**(Trap 25 总纲铁律的又一次命中)。 -- **发现时间**:阶段 7 扩展 8 Socket 测试,2026-08-25;定案更正 2026-10-11。 +- **状态**:✅(`net_init` 用 `ch395f_set_fun_para(0x08)`) +- **现象**:曾以为必须 `0x09`(bit0=1"多 Socket 模式")才能使用 Socket 1~7。 +- **根因(定案)**:bit0 在官方手册 V1C/V1E/V2.2/V2.3 的 §5.52 位表中一律「保留,必须为0」;官方 SDK 无 bit0 宏;官方例程传 `0x02` 却给 Socket 0~7 全配缓冲、官方 FTP 例程直接传 `0x08`。8 Socket 是芯片原生能力(`_SN` 索引寻址),真正的门槛是 §5.45 缓冲分配。概念讹传源:bit1「服务器多连接」指一个服务器能接几个客户端,不是能用几个 Socket。 +- **修复/验证**:`0x08` 下 Phase 7 34/34 全通过,并发回显实测落在 sock2/4/5/6/7 → Socket 4~7 在 bit0=0 下完全可用。保留 bit0=1 的代价:违反保留位规定,后续固件版本一旦启用该位现网设备静默失效。 +- **台架前置**:判"芯片行为"前先确认对端真的收到了 SYN——① PC 网卡落在 Public 防火墙配置文件 → 入站被静默丢弃,全量 `CONNECT TIMEOUT` 极易误判成芯片故障;② 残留 `tcp_server` 进程占端口不报错。 -## Trap 24 · SET_MAC 必须在 INIT_CH395 之前 +## TRAP-NET-19 · SET_MAC 必须在 INIT_CH395 之前 -- **状态**:✅ 已修复(纯顺序调整,无 RAM 变化) -- **现象**:UI 配置的 MAC 与设备实际在线 MAC 不一致("滞后一次上电")。静态审查发现,手册条文确证。 -- **根因(定案)**:`docs/芯片手册/CH395F.md` §9.2.1 初始化顺序 = `SET_MAC → SET_IP/GWIP/MASK → INIT_CH395`;§5.7 `SET_MAC_ADDR` 写芯片内部 EEPROM,§5.13 `INIT_CH395` 从 EEPROM **加载** MAC——SET_MAC 放 INIT 之后则本次上电不生效。原码顺序相反;`ch395f.c` 原注释把四个 SET 标为"芯片已初始化(之后)"加剧误排。 -- **修复**:`net_init()` 把 `ch395f_set_mac_addr()` 移到 `ch395f_init()` 之前;四个 SET 函数注释统一改"必须在 `ch395f_init()` 之前调用";`net_socket.h` 同步。 -- **发现时间**:2026-10-04(静态审查项 P1-7)。 +- **状态**:✅(纯顺序调整,无 RAM 变化) +- **现象**:UI 配置的 MAC 与设备实际在线 MAC 不一致("滞后一次上电")。 +- **根因(定案)**:`docs/芯片手册/CH395F.md` §9.2.1 初始化顺序 = `SET_MAC → SET_IP/GWIP/MASK → INIT_CH395`;§5.7 `SET_MAC_ADDR` 写芯片内部 EEPROM,§5.13 `INIT_CH395` 从 EEPROM **加载** MAC——SET_MAC 放 INIT 之后则本次上电不生效。 +- **修复**:`net_init()` 把 `ch395f_set_mac_addr()` 移到 `ch395f_init()` 之前;四个 SET 函数注释统一"必须在 `ch395f_init()` 之前调用"。 --- # 第二章 中断与事件一致性 -## Trap 08 · RECV 中断是电平触发 +## TRAP-NET-08 · RECV 中断是电平触发 -- **状态**:✅ 已定案 +- **状态**:✅ - **现象**:`net_poll()` 中 RECV 中断无限触发,主循环永远得不到执行。 - **根因(定案)**:接收缓冲区有数据期间 `INT#` 保持低电平,`do{...}while(int_status!=0)` 必然死循环。 - **修复**:`net_poll()` 每轮只处理一批中断,由主循环负责读数据。 -- **推论**:RECV 是条件位(数据读空即落),所以 Trap 25 B3 的全量读清对 RECV 可下轮补、对其余位是永久销毁。 +- **推论**:RECV 是条件位(数据读空即落),所以 B3 的全量读清对 RECV 可下轮补、对其余位是永久销毁。 -## Trap 14/17 · 上一条连接的残留 DISCONNECT 污染下一条连接(合并条目) +## TRAP-NET-12 · 上一条连接的残留 DISCONNECT 污染下一条连接 -- **状态**:✅ 已定案(两级预清 + 排空为 Trap 23/25 排渣契约的前身) -- **合并说明**:Trap 14(2026-09,硬件层 TC-203 观察)与 Trap 17(2026-08-25,生产 `net_close_locked`)为**同一根因的两层表现**,2026-10-13 合并;代码注释常以「Trap 14/17」连用。 -- **现象**: - - 硬件层(14):`close_socket` → 已确认 CLOSED → 新会话第一次读 `GET_INT_STATUS_SN` 读到**上一会话遗留的 DISCONNECT**(0ms 即命中),负向判定/状态机误判(TC-203 三轮重连后 PC 握手实际成功、固件判"未连接")。 - - 生产层(17):单连接模式 close 后立即复用同一 Socket 重连,每次都 `openOK` 但紧接着 `DISCONNECTED, releasing` 秒断——旧连接的 DISCONNECT 在新连接 ESTABLISHED 之后才到达,`handle_disconnect_event` 把新连接误判断开。 -- **根因(定案)**:① `GET_SOCKET_STATUS` 是纯查询**不清中断**;② DISCONNECT 可能在 CLOSED 达成同时或之后才锁存;③ SINT **粘滞到被读**(close/open 都不清,Trap 25 B3 实证)→ 无人消费则跨会话存活。 +- **状态**:✅(两级预清 + 排空为 TRAP-NET-18/25 排渣契约的前身) +- **现象**:close 后立即复用同一 Socket 重连,新连接被上一条连接的 DISCONNECT 事件秒断(生产:`openOK` 后紧接 `DISCONNECTED, releasing`;硬件层:新会话第一次读 SINT 即命中上一会话遗留事件)。 +- **根因(定案)**:① `GET_SOCKET_STATUS` 是纯查询**不清中断**;② DISCONNECT 可能在 CLOSED 达成同时或之后才锁存;③ SINT **粘滞到被读**(close/open 都不清,B3 实证)→ 无人消费则跨会话存活。关闭过渡实测:自身 FIN 的 ACK + 本地 `TIME_WAIT`,0~150 ms。 - **修复**: - 1. **预清**:新会话 OPEN 前读一次全局 + 本 Socket SINT 丢弃陈旧事件(`phase2_tc203()` 双保险;生产 `net_connect_locked` open 后清本 Socket 中断); + 1. **预清**:新会话 OPEN 前读一次全局 + 本 Socket SINT 丢弃陈旧事件(`net_connect_locked` open 后清本 Socket 中断); 2. **排空**:`net_close_locked` 在 `close_socket()` 后轮询 `GET_SOCKET_STATUS` 至 `SOCKET_CLOSED`(上限 500ms,实测 LAN 几毫秒)再读空本 Socket 中断; 3. **反核实**:依据中断做结论前用 `GET_SOCKET_STATUS` 验证实际状态一致(负向事件与 ESTABLISHED 矛盾时以状态为准)。 -- **措辞更正(2026-10-13)**:原 Trap 17 写"芯片要等**对端** FIN/ACK 才置 CLOSED"——按 Trap 07 实测应为:自身 FIN 的 ACK + 本地 `TIME_WAIT` 过渡,0~150 ms。 -- **注意**:B3 修复(2026-10-12)后预清只准读**本 Socket** SINT,不得再全量读 8 个(见第六章 B3)。 -- **发现时间**:17=阶段 7 上板实测 2026-08-25;14=2026-09 TC-203 取证。 +- **注意**:B3 修复后预清只准读**本 Socket** SINT,不得再全量读 8 个(见第六章 B3)。 -## Trap 22 · 快速复连丢 CONNECT/DISCONNECT 中断 → 监听静默死亡 +## TRAP-NET-17 · 快速复连丢 CONNECT/DISCONNECT 中断 → 监听静默死亡 -- **状态**:✅ 已修复定案(周期对账 `net_reconcile_tick` 是 N34 reconciler 的前身;阶段 3 已并入单一 100ms tick) -- **现象(生产固件实测)**:单连接监听 socket 快速复连后**永久失效**(后续 SYN 被拒或超时,10s 内不自恢复),芯片其余部分完全正常(其他监听端口照常工作)。两种表现: - - **拒连**:芯片完成握手但 CONNECT 中断未上报 → 控制块滞留 LISTENING;或本层因 DISCONNECT 中断丢失仍以为有客户端 → 两种情况芯片侧都占住单连接监听,后续 SYN 全被拒; - - **应用连锁**:`net_send` 被拒;或应用读不到 EOF(`g_client` 永不释放,**永不调用 accept**)。 -- **复现(可信判据,纯客户端;工具 `MDK-ARM/tcp_reconnect_probe.py`)**: - - 确定性:产品固件 + 待机 60s,对 `:7003` 以 300ms 快速复连 → 修复前**首 2 次成功、之后永久拒连**(2/10); - - 概率性:FTP `:21` 100ms×50 → 修复前**第 29 轮起永久拒连**;对照 `:21` 0ms/600ms 与 `:8888` 同期正常 → 触发条件 = 复连间隔 <~1s + 轻负载。 -- **机制(设备侧日志实证)**:① 芯片丢 CONNECT 或 DISCONNECT 中断;② 此后本层**收不到任何中断**,纯事件驱动无路可走(把 LISTENING 分支改"重建监听"仍无效——连 DISCONNECT 都没来);③ 故必须**周期回读芯片状态对账**。裸机芯片台架(剥离 OS/net/FTP)独立复现"握手完成但 CONNECT 中断被丢"——芯片天性,不是固件 bug。 +- **状态**:✅(周期对账 `net_reconcile_tick` 是 N34 reconciler 的前身;阶段 3 已并入单一 100ms tick) +- **现象**:单连接监听 socket 快速复连后**永久失效**(后续 SYN 被拒或超时,10s 内不自恢复),芯片其余部分完全正常(其他监听端口照常工作)。两种表现:芯片完成握手但 CONNECT 中断未上报 → 控制块滞留 LISTENING;或 DISCONNECT 中断丢失 → 本层仍以为有客户端。两种情况芯片侧都占住单连接监听;应用侧连锁 `net_send` 被拒、读不到 EOF。 +- **复现(可信判据,纯客户端;工具 `MDK-ARM/tcp_reconnect_probe.py`)**:待机 60s 后 `:7003` 300ms 快速复连 → 修复前首 2 次成功、之后永久拒连(2/10);FTP `:21` 100ms×50 → 第 29 轮起永久拒连。对照 `:21` 0ms/600ms 与 `:8888` 正常 → 触发条件 = 复连间隔 <~1s + 轻负载。 +- **机制(设备侧日志实证)**:① 芯片丢 CONNECT 或 DISCONNECT 中断;② 此后本层**收不到任何中断**,纯事件驱动无路可走;③ 故必须**周期回读芯片状态对账**。裸机芯片台架(剥离 OS/net/FTP)独立复现——芯片天性,不是固件 bug。 - **修复(`net_socket.c`)**: - 1. **周期对账 `net_reconcile_tick()`**(初版 200ms;N34 阶段 3 与 pump 合并为单一 100ms tick):修正失同步形态——①本层 ESTABLISHED 而芯片已无连接→按断开处理;②本层 LISTENING 而芯片已握手→补 accept;③本层 LISTENING 而芯片监听消失→重建;④孤儿监听(B10)→重建。**去抖+冷却**:同一失配连续 2 次才动手,动手后冷却 1s。 - 2. `handle_disconnect_event` LISTENING 分支:由"仅告警"改为"回读芯片状态——已不可监听即重建"。 -- **踩坑记录(务必先读)**:最初修法放在 **accept 路径**(每次 accept 回读自愈):单次状态读把"刚 open/listen 尚未生效"的瞬态误判为失配 → 监听反复重建直至 `relisten give up` → **三个监听口全部死亡(风暴)**。改"周期对账+去抖+冷却"后消除。**结论:凡基于单次状态读的重建动作,必须去抖。** -- **验证(修复前→修复后)**:`:7003` 300ms×10:2/10→**10/10**;`:21` 100ms×50:28/50→**50/50**;`:21` 0ms/600ms×50:**50/50/50/50**;`:7003` 200ms×20:**20/20**;FTP 10×64KB SHA-256:**10/10**;`relisten give up`:**0**。自愈实测计数:`chip closed -> disconnect` 12 次、`w/o CONNECT irq -> accept` 1 次、`listen lost -> relisten` 1 次——三类失同步均真实出现并被自愈。 -- **对应文件**:`Drivers/BSP/NET/net_socket.c`(`net_reconcile_tick`);`MDK-ARM/tcp_reconnect_probe.py`;`docs/任务规划/README.md` N-6(已销项)。 -- **发现时间**:2026-09-22(台架偶发)→ 2026-09-23(确定性复现+当日修复闭环)。 + 1. **周期对账 `net_reconcile_tick()`**(与 pump 合并为单一 100ms tick):修正失同步形态——①本层 ESTABLISHED 而芯片已无连接→按断开处理;②本层 LISTENING 而芯片已握手→补 accept;③本层 LISTENING 而芯片监听消失→重建;④孤儿监听(B10)→重建。**去抖+冷却**:同一失配连续 2 次才动手,动手后冷却 1s。 + 2. `handle_disconnect_event` LISTENING 分支:回读芯片状态——已不可监听即重建。 +- **踩坑记录(务必先读)**:基于单次状态读的重建动作放在 accept 热路径 → 把"刚 open/listen 尚未生效"的瞬态误判为失配 → 监听反复重建直至 `relisten give up` → **三个监听口全部死亡(风暴)**。**结论:凡基于单次状态读的重建动作,必须去抖。** +- **回归基线**:`:7003` 300ms×10:**10/10**;`:21` 100ms×50:**50/50**;`:21` 0ms/600ms×50:**50/50**;`:7003` 200ms×20:**20/20**;FTP 10×64KB SHA-256:**10/10**;`relisten give up`:**0**。三类失同步均有自愈实测计数留痕。 +- **对应文件**:`net_socket.c`(`net_reconcile_tick`);`MDK-ARM/tcp_reconnect_probe.py`。 -## Trap 25 · FTP 数据连接三连故障——四重定案(N-30) +## TRAP-NET-20 · FTP 数据连接三连故障——三重定案 -- **状态**:✅ 已定案并修复(回归 4 轮 11/11;B3 见第六章) -- **现象(生产 FTP 回归,多轮复现)**:同一会话内**第 2 次及以后**的数据连接(LIST/STOR/RETR)两类失败,概率随上一条数据连接的负载与间隔变化(NAND 目录越大越易触发),与提交历史无关(D20 基线同样复现,当年"11/11 通过"是慢速台架+干净目录的运气): - - **吞数据(SWALLOW)**:新连接建立成功、服务端 `net_send` 返回成功,客户端 20s 收不到任何字节; - - **425**:新连接被接受后 ~10-20ms 出现 DISCONNECT(`chip tcp=0x00`),accept 失败 → `425 Can't open data connection`。 -- **定案实验(裸机芯片台架,剥离 OS/net/FTP)**:实验分支 `n30-chip-bench`,`test/board/ch395f_reuse_bench.c` 内核启动前直接调 `ch395f_*` 受控矩阵 + 主机侧 `test/host/ch395f_reuse_host.py`。**剥离后 bug 依旧复现** → 纯芯片操作时序问题。关键现场:`post-listen st=0x05/0x04`(listen 成功但 TCP 态停在旧连接 ESTABLISHED=幽灵态)、`unexpected int=0x10`(只等来旧连接滞后 DISCONNECT)、`no CONNECT within 10s`(芯片状态已 0x04 但中断永远没来)。 -- **芯片级规律**:CH395F 拆链收尾窗口**变长且不可观测**(状态显示 CLOSED 持续 1s+ 也不代表拆完,实测可 >2s 且时长不定)。窗口内重新 `open+listen` 三种形态:① 握手完成但 **CONNECT 中断被丢**(Trap 22 机制的窗口期形态)→ 吞数据;② 状态直接假报 0x04 幽灵态(回读对端端口=旧连接端口);③ 旧连接滞后 DISCONNECT 打到新监听 → relisten 毁掉新连接 → 425。 -- **已证伪的修复**(受控实验全部无效,列出防重踩):close 后固定沉降 100~3000ms(2s 也挂过)/ Socket 轮换(跨 socket 毒化)/ listen 后 close+重试 / 缓冲重分配 / close 前 TX 排空延时 / 禁用对账 tick(对账不是元凶)。 -- **有效修复一:双信源原则——中断只是快路径,状态寄存器才是真相**: +- **状态**:✅(回归 4 轮 11/11;全局寄存器读即清见第六章 B3) +- **现象**:同一会话内**第 2 次及以后**的数据连接(LIST/STOR/RETR)两类失败,概率随上一条数据连接的负载与间隔变化:① **吞数据**——新连接建立成功、服务端 `net_send` 返回成功,客户端 20s 收不到任何字节;② **425**——新连接被接受后 ~10-20ms 出现 DISCONNECT(`chip tcp=0x00`),accept 失败。 +- **定案实验(裸机芯片台架,剥离 OS/net/FTP)**:`test/board/ch395f_reuse_bench.c` 内核启动前直接调 `ch395f_*` 受控矩阵 + 主机侧 `test/host/ch395f_reuse_host.py`。**剥离后 bug 依旧复现** → 纯芯片操作时序问题。关键现场:`post-listen st=0x05/0x04`(listen 成功但 TCP 态停在旧连接 ESTABLISHED=幽灵态)、`unexpected int=0x10`(只等来旧连接滞后 DISCONNECT)、`no CONNECT within 10s`(芯片状态已 0x04 但中断永远没来)。 +- **芯片级规律**:CH395F 拆链收尾窗口**变长且不可观测**(状态显示 CLOSED 持续 1s+ 也不代表拆完,实测可 >2s 且时长不定)。窗口内重新 `open+listen` 三种形态:① 握手完成但 **CONNECT 中断被丢**(TRAP-NET-17 机制的窗口期形态)→ 吞数据;② 状态直接假报 0x04 幽灵态(回读对端端口=旧连接端口);③ 旧连接滞后 DISCONNECT 打到新监听 → relisten 毁掉新连接 → 425。 +- **已证伪的修复**(受控实验全部无效,列出防重踩):close 后固定沉降 100~3000ms / Socket 轮换(跨 socket 毒化)/ listen 后 close+重试 / 缓冲重分配 / close 前 TX 排空延时 / 禁用对账 tick(对账不是元凶)。 +- **定案一:双信源原则——中断只是快路径,状态寄存器才是真相**: 1. **accept 路径内联探针**(`net_lost_connect_probe`,`net_accept_locked` 每 100ms 挂探针):芯片已 ESTABLISHED 而 CONNECT 中断未到 → 连续 2 轮去抖后补 accept; - 2. **幽灵态甄别**:补 accept 前回读对端端口(`GET_REMOT_IPP`),**= 上一条连接端口(`last_peer_port`)→ 幽灵,不 accept**(生产 LIST#4"accept 成功但数据被吞"的直接成因就是对账②曾把幽灵补 accept); + 2. **幽灵态甄别**:补 accept 前回读对端端口(`GET_REMOT_IPP`),**= 上一条连接端口(`last_peer_port`)→ 幽灵,不 accept**; 3. 对账②同步加幽灵甄别;`last_peer_port` 登记点:`handle_disconnect_event` / `handle_timeout_event` / `net_relisten_try`(清零 remote_port 前转登)/ `net_close_locked`(跨 memset 保留——幽灵属芯片 Socket 级,与 fd 复用无关)。 -- **定案二:所谓"kill 模式"是寄存器语义误读(2026-10-11,双向验证)**——误判存档:曾解读"芯片在 accept 后 10-20ms 杀连接",**有误**。真相: - 1. `FUN_PARA=0x08` 置 bit3 `SOCKET_CLOSE=1`(§5.52 ②):产生 DISCONNECT/TIMEOUT 后芯片**不关 Socket、不清缓冲**,就是留给 MCU 读出残留数据后再关; - 2. 探针现场 `kill-check recv_len=1027 re-read st=0x05/0x00`——Socket 仍 OPEN、TCP 已 CLOSED、**RX 缓冲 1027B = STOR 载荷一个不少全在**:芯片按规格**保留现场**,不是杀连接; - 3. 那个 DISCONNECT 是**客户端正常 FIN**(Python ftplib `storbinary` 发完即关数据连接,标准 FTP 行为,局域网小文件 ~5ms 完成)。 - - **425 真根因(竞态+自毁缓冲)**:客户端 5ms 内发完并关闭;accept RPC 赶到时 MCU 态已 CLOSED;旧 `net_accept_locked` 把 CLOSED 一律当"无连接" → `net_relisten_try`(close+open **清空芯片 RX 缓冲**)→ 1027B 被**我方亲手销毁** → 425。芯片全程无辜(此模式)。 - - **修复(closed-with-data accept)**:CLOSED + auto_relisten + **芯片 RX 有数据**(`GET_RECV_LEN_SN`>0)→ 接受这条"已关但可读"连接(日志 `closed-with-data accept (drain pending)`),应用快路径排空,排空后 EOF 自然触发 relisten——对齐 §5.52 ② 契约。**STOR #2 从 4/4 失败 → 4/4 通过**。 - - 连带修正:台架 mode0"accept 后 10-20ms DISCONNECT"大概率同为误读(主机收完数据自己关的);但台架**丢 CONNECT 中断模式**(主机 got=0 + 芯片 0x04 + 无中断,主机端独立证据)仍成立,双信源探针修复保留。 -- **定案三:LIST 吞包 = 背靠背小段写毒化芯片 TX(2026-10-11)**:决定性对照——同一目录同一载荷,LIST #1(FAT 冷、行间天然间隔)通过;LIST #4/#7(FAT 热、每行一次 `net_send` ~60B 背靠背小段)必挂。芯片对同一连接的**背靠背多段小写入**会毒化 TX 整条连接(芯片报 SEND_OK 但对端协议栈全部丢弃,无 RST 无 DISCONNECT);逐块协议(等 SENBUF_FREE 再写)本身合规(§5.45),但芯片对该模式的内部处理不可靠;RETR 1KB 满块连发不受影响。**修复**:`send_list`/`send_nlst` 整份目录**一次性批量发送**(static 1536B 缓冲,snprintf 累积 + 单次写;溢出截断判 -1→550;注意 `lftpd_inet_write` 成功返回**字节数**非 0)。**修复后 ftp_regression 4 轮 11/11** + `ftp_auth_bench.py` 13/13。 -- **与 Trap 22 的关系**:Trap 22 周期对账对常驻监听器有效;FTP 数据通道是"活 1 秒就死"的短命监听器,对账②无幽灵甄别时既救不全(冷却期内 accept 已超时)又帮凶(把幽灵补 accept)。Trap 25 把"看状态补 accept"内联进 accept 热路径(短去抖、无冷却)+ 端口甄别,对账②保留为兜底网。 -- **教训(Trap 25 总纲)**:本轮先后误读三次——把客户端正常 FIN 读成"芯片杀连接"、把 open 重试读成"拆链窗口"、把芯片假 SEND_OK 读成"数据已上线"。铁律:**CH395F 的"成功"(中断位/状态码)只代表芯片本地动作完成,不代表对端收到;判定必须回到对端可见证据(主机侧收发)+ 手册语义(FUN_PARA §5.52、状态码 §5.22)**。 -- **对应文件**:`net_socket.c`(`net_lost_connect_probe`/closed-with-data accept/`net_reconcile_tick`②/`last_peer_port`)、`net_types.h`、`lftpd.c`(批量发送);台架 `test/board/ch395f_reuse_bench.{c,h}` + `test/host/ch395f_reuse_host.py`(实验分支注册编译,主分支只存档)。 -- **发现时间**:2026-10-11(登记簿 N-30)。 +- **定案二:closed-with-data accept(425 真根因=accept 竞态自毁缓冲)**:`FUN_PARA` bit3 `SOCKET_CLOSE=1`(§5.52 ②)下产生 DISCONNECT 后芯片**不关 Socket、不清缓冲**,RX 缓冲里 STOR 载荷一个不少(探针实证 `recv_len=1027 re-read st=0x05/0x00`);那个 DISCONNECT 是**客户端正常 FIN**(ftplib `storbinary` 发完即关,局域网小文件 ~5ms)。旧 `net_accept_locked` 把 CLOSED 一律当"无连接" → `net_relisten_try`(close+open **清空芯片 RX 缓冲**)→ 数据被**我方亲手销毁** → 425。**修复**:CLOSED + auto_relisten + **芯片 RX 有数据**(`GET_RECV_LEN_SN`>0)→ 接受这条"已关但可读"连接(日志 `closed-with-data accept (drain pending)`),应用快路径排空,排空后 EOF 自然触发 relisten——对齐 §5.52 ② 契约。 +- **定案三:LIST 吞包 = 背靠背小段写毒化芯片 TX**:决定性对照——同一目录,LIST #1(行间天然间隔)通过;LIST #4/#7(每行一次 `net_send` ~60B 背靠背小段)必挂。芯片对同一连接的**背靠背多段小写入**会毒化 TX(芯片报 SEND_OK 但对端全部丢弃,无 RST 无 DISCONNECT);RETR 1KB 满块连发不受影响。**修复**:`send_list`/`send_nlst` 整份目录**一次性批量发送**(static 1536B 缓冲;注意 `lftpd_inet_write` 成功返回**字节数**非 0)。 +- **与 TRAP-NET-17 的关系**:周期对账对常驻监听器有效;FTP 数据通道是"活 1 秒就死"的短命监听器,对账②无幽灵甄别时既救不全又帮凶。本条把"看状态补 accept"内联进 accept 热路径(短去抖、无冷却)+ 端口甄别,对账②保留为兜底网。 +- **教训(总纲)**:**CH395F 的"成功"(中断位/状态码)只代表芯片本地动作完成,不代表对端收到;判定必须回到对端可见证据(主机侧收发)+ 手册语义(FUN_PARA §5.52、状态码 §5.22)**。 +- **对应文件**:`net_socket.c`(`net_lost_connect_probe`/closed-with-data accept/`net_reconcile_tick`②/`last_peer_port`)、`net_types.h`、`lftpd.c`(批量发送);台架 `test/board/ch395f_reuse_bench.{c,h}` + `test/host/ch395f_reuse_host.py`(主分支存档)。 --- # 第三章 连接生命周期与自愈 -## Trap 07 · TCP 关闭重连:直接 close,不要 disconnect +## TRAP-NET-07 · TCP 关闭重连:直接 close,不要 disconnect -- **状态**:✅ 已定案(做法保留;原根因说法已推翻,见定案更正) -- **误判存档**:原断言"`disconnect → close` 让 Socket 卡 `FIN_WAIT_2` 数分钟、close 发 RST"——**TC-NET-204 实测推翻**(2026-10-11),机制说法作废,做法保留。 -- **定案(TC-NET-204,三模式各 5 轮 45 项判定全过)**: +- **状态**:✅ 定案(TC-NET-204 三模式各 5 轮 45 项判定) +- **定案**: | 模式 | 关闭序列 | 回 `SOCKET_CLOSED` 耗时 | 重开+重连+回显 | |---|---|---|---| | A | `close` | 0~150 ms(过渡态可见 `TIME_WAIT` 0x0A) | 5/5 OK | | B | `disconnect → close` | **全部 +0 ms** | 5/5 OK | - | C | `disconnect`(不 close) | **10s 内从未回到**(byte0 恒 05H) | 直接 open 必返 **0x20 FAIL**(5/5);补 close 后 +0ms 回 CLOSED | - 1. `FIN_WAIT_2`(0x06) 全程未出现(15 轮×9 采样点); - 2. **`close` 无 RST 语义**:§5.35 只说"关闭前自动断链";手册中芯片主动发 RST 仅 §5.29(监听占用)与 §9.2.6(多连接无可用数据 Socket)两处,均与 close/disconnect 无关; - 3. **观测口径错误**:§5.22 第 1 字节只有 `00H/05H`,第 2 字节(TCP 态)**仅在 byte0=OPEN 时有意义**(官方 SDK 注释明文)——当年把"已 CLOSED 状态下的残留 TCP 态读数"读成了"卡在某个 TCP 态"。 - - **真正原因**(C 模式 5/5 复现):`FUN_PARA` bit3 `SOCKET_CLOSE=1`(§5.52 ②)下产生 DISCONNECT 后**芯片不关 Socket、不清缓冲**,长期停在 `05H/00H`;此时直接 `open_socket` 必返 `0x20` FAIL。**把 Socket 拉回 CLOSED 的动作是 `close`,与 RST 无关。** - - **做法为何仍对**:① §5.35 close 已自动断链,disconnect 纯属多余;② disconnect 会**制造** DISCONNECT 事件,bit3=1 下必须被消费/预清,否则污染下一条连接(Trap 14/17/23 实证);③ 它把芯片推进 closing 过渡态,重开需沉降(Trap 09)。15 轮重开未观察到残留 `DISCONNECT`(0x10)——close 顺带清掉锁存。 - - **与 Trap 17 的张力已消解**:实测支持 Trap 17 一侧(close 后 0~150ms 过渡),不支持"close 发 RST 瞬间回 CLOSED"。 -- **边界补充(2026-10-13,E-02 台架)**:`close` 无 RST 的适用条件—— - - 对端配合(收到 FIN 后其内核 close):芯片走 `TIME_WAIT` ~150ms → byte0 回 00H → relisten → 新连接接入; - - **对端内核存活但不 close**(挂起但连接仍活):本层发 FIN 后芯片停在 **`FIN_WAIT_2`(byte0=05H/byte1=06H)长尾**——没有 RST 就拆不掉,**监听恢复受限于对端 close**; - - 不配合场景的兜底 = 芯片自身 TIMEOUT/KeepAlive 拆僵尸会话(E13 定案:判死窗 30~90s,见 `docs/网络/N34-自愈审计/实验方案.md` E13)——**不是**应用可加速的路径; - - **应用侧出口 = `net_drop_conn(fd)`**(N34 阶段 3):netTask 侧走驱动 `sock_reset` QUARANTINE(FIN+排渣+排空事件+等 byte0=00H,400ms 底)+ 本层收口 CLOSED + `auto_relisten` 即时重监听;**保监听不毁**(单连接模型不得 `net_close` 监听 fd)。对端不 close 时复位机等在 `net_poll` 步进里(`ch395f_sock_reset_poll`),对端 FIN 到达即自动走完——**不搁浅**。 -- **判据**:关闭后轮询 `GET_SOCKET_STATUS` 的**第 1 字节**等待 `sock=0x00` 再 `open_socket`;别拿 byte1 当"卡住"判据。 -- **发现时间**:Phase 2 TCP Client 测试(原记 2025-07 **有误**,实为 2026-07-19;定案更正与 TC-NET-204 实测 2026-10-11;E-02 边界补充 2026-10-13)。 + | C | `disconnect`(不 close) | **10s 内从未回到**(byte0 恒 05H) | 直接 open 必返 **0x20 FAIL**;补 close 后 +0ms 回 CLOSED | + 1. `close` **无 RST 语义**:§5.35 只说"关闭前自动断链";手册中芯片主动发 RST 仅 §5.29(监听占用)与 §9.2.6(多连接无可用数据 Socket)两处,均与 close/disconnect 无关; + 2. **C 模式真因**:`FUN_PARA` bit3 `SOCKET_CLOSE=1`(§5.52 ②)下产生 DISCONNECT 后**芯片不关 Socket、不清缓冲**,长期停在 `05H/00H`;此时直接 `open_socket` 必返 `0x20` FAIL——**把 Socket 拉回 CLOSED 的动作是 `close`**; + 3. **做法**:直接 `ch395f_close_socket()`;`disconnect` 纯属多余且会**制造** DISCONNECT 事件(bit3=1 下必须被消费/预清,否则污染下一条连接,TRAP-NET-12/23 实证); + 4. **观测口径**:§5.22 第 1 字节只有 `00H/05H`,第 2 字节(TCP 态)**仅在 byte0=OPEN 时有意义**——关闭后轮询**第 1 字节**等 `00H` 再 open,别拿 byte1 当"卡住"判据。 +- **边界(E-02 台架)**:`close` 无 RST 的适用条件——对端配合(收到 FIN 后其内核 close):芯片走 `TIME_WAIT` ~150ms → relisten → 新连接接入;**对端内核存活但不 close**:芯片停在 **`FIN_WAIT_2`(byte0=05H/byte1=06H)长尾**——没有 RST 就拆不掉,**监听恢复受限于对端 close**。不配合场景的兜底 = 芯片自身 TIMEOUT/KeepAlive 拆僵尸会话(判死窗 30~90s,`docs/网络/N34-自愈审计/实验方案.md` E13)——**不是**应用可加速的路径。 +- **应用侧出口 = `net_drop_conn(fd)`**(N34 阶段 3):netTask 侧走驱动 `sock_reset` QUARANTINE(FIN+排渣+排空事件+等 byte0=00H,400ms 底)+ 本层收口 CLOSED + `auto_relisten` 即时重监听;**保监听不毁**(单连接模型不得 `net_close` 监听 fd)。对端不 close 时复位机等在 `net_poll` 步进里(`ch395f_sock_reset_poll`),对端 FIN 到达即自动走完——**不搁浅**。 -## Trap 09 · 沉降窗:PHY link up 后 / close 后不能立即 open +## TRAP-NET-09 · 沉降窗:PHY link up 后 / close 后不能立即 open -- **状态**:✅ 已定案(现行形态=open 重试环 + `net_sock_reset_sync`;原 `osDelay(7000)` 启动延时已被就绪屏障取代) -- **现象**:PHY_CHANGE 中断同一时刻 `ch395f_open_socket()` 返错(非 BUSY 超时),首次 listen 失败;3 秒后重试成功。 +- **状态**:✅(现行形态=open 重试环 + `net_sock_reset_sync`;启动错峰已由就绪屏障承担) +- **现象**:PHY_CHANGE 中断同一时刻 `ch395f_open_socket()` 返错(非 BUSY 超时),首次 listen 失败。 - **根因(定案)**:PHY link up 后芯片内部协议栈需额外时间完成初始化;同一 `net_poll()` 迭代内立即 `OPEN_SOCKET` 可能返 `ERR_BUSY` 或其他错误码。与 FUN_PARA 无关。 - **修复(现行形态)**: - 1. `net_listen_locked`/`net_connect_locked` open 失败重试 ×3(D10 `net_listener_retry_tick` 重监听重试环); - 2. open 失败先走 `net_sock_reset_sync`(确定性排渣,内置二段 CLOSE,见下补充)再重试——最短等 100ms ≪ 原 500ms 盲等; - 3. ~~FTP 任务 `osDelay(7000)` 错峰~~ → 已由**就绪屏障**(`SYS_EVT_NET_READY`/`FTP_READY`)取代(2026-10 起)。 -- **补充(2026-10-13,E2/E4 台架):LISTEN 态 Socket 的二段关闭**——本条"close 后需沉降才能重开"的前置形态: + 1. `net_listen_locked`/`net_connect_locked` open 失败重试 ×3(D10 `net_listener_retry_tick` 重试环); + 2. open 失败先走 `net_sock_reset_sync`(确定性排渣,内置二段 CLOSE,见下)再重试——最短等 100ms; + 3. 启动错峰走**就绪屏障**(`SYS_EVT_NET_READY`/`FTP_READY`),不用魔法延时。 +- **补充:LISTEN 态 Socket 的二段关闭**(E2/E4 芯片实验,本条"沉降才能重开"的前置形态): - 对 LISTEN(byte0=05H/byte1=01H)发 `CLOSE_SOCKET`:命令返 SUCCESS 但**只拆监听**(byte1 01H→00H),byte0 恒 05H——按"等 byte0=00H"判据会永远等不到;**第二次 CLOSE** 才把 byte0 拉到 00H。即 LISTEN 态关闭是**两段**:拆监听 → 关 Socket。 - - 驱动侧已内置补发:`ch395f_sock_reset_*` 状态机 `WAIT_CLOSE` 段检测到"TCP 已 00H 而 byte0 仍 05H"自动补发第二次 CLOSE(契约见 `Drivers/BSP/CH395F/ch395f.h`)。 - - E4 定案 60 轮实证"复位后 open 必成"。 + - 驱动侧已内置补发:`ch395f_sock_reset_*` 状态机 `WAIT_CLOSE` 段检测到"TCP 已 00H 而 byte0 仍 05H"自动补发第二次 CLOSE(契约见 `Drivers/BSP/CH395F/ch395f.h`)。E4 60 轮实证"复位后 open 必成"。 - ⚠ 任何**新写的**"close 后等 CLOSED"逻辑必须走驱动复位原语,**不要**手拼"一次 CLOSE + 轮询 byte0"——会在 LISTEN 态上死等。 -- **发现时间**:FTP 改造期(原记 2025-07 不可考);二段 CLOSE 补充 2026-10-13。 -## Trap 11 · net_accept_locked 拒绝 standalone_accept 的 ESTABLISHED +## TRAP-NET-11 · net_accept_locked 拒绝 standalone_accept 的 ESTABLISHED -- **状态**:✅ 已修复定案(现行 `net_accept_locked` 仍保留该放行逻辑) +- **状态**:✅(现行 `net_accept_locked` 保留该放行逻辑) - **现象**:FTP 客户端 TCP 连接成功(standalone accept),220 欢迎语不发送,`lftpd_inet_accept()` 死循环。 - **根因(定案)**:入口检查 `state != LISTENING` 直接拒绝——standalone accept 后 state 已变 ESTABLISHED,永远到不了后续检查。 -- **修复**:放行 `standalone_accept && state == ESTABLISHED` 组合。 -- **注意**:仅影响 standalone_accept 模式(PASV 数据通道、单连接 FTP)。 -- **发现时间**:FTP 改造期(2026-08)。 +- **修复**:放行 `standalone_accept && state == ESTABLISHED` 组合。仅影响 standalone_accept 模式(PASV 数据通道、单连接 FTP)。 -## Trap 23 · 上传中止后首笔复连上传被静默截断(≤2048B)并误报成功 +## TRAP-NET-18 · 上传中止后首笔复连上传被静默截断(≤2048B)并误报成功 -- **状态**:✅ 已修复定案(三阶段全部闭环;回归判据常备:`test/host/ftp_abort_probe.py`) -- **现象**:客户端上传(STOR)中途断开(**RST 或优雅 FIN 均可**),**紧随其后的第一笔复连上传被截断到 ≤2048 字节(= 芯片单 socket RX 缓冲大小),且服务端回报 226 成功** → 客户端据此认为上传成功 = **静默数据损坏**;第二笔及以后恢复正常。 -- **证据(可信复现:生产固件、零固件探针;工具 `test/host/ftp_abort_probe.py`)**:判据 = FTP 协议结果 + 紧接一笔正常上传的 SHA-256 往返 + 控制通道 NOOP 存活探测。RST 中止 **10/10** 复现(请求 4096B → 设备日志 `UPLOAD '/ab_ok.bin' 2048 bytes OK`);优雅 FIN **5/5** 同样复现 → 与 RST/FIN 无关,凡"数据未发完即断开"均触发。对照(无中止)177 轮(8KB~1MB,含 100 轮零间隔浸泡)SHA-256 全匹配(`MDK-ARM/ftp_freeze_probe.py`)→ 中止路径是唯一触发条件。 -- **机制(设备侧日志实证)**:① 中止时数据 socket 释放(RST 时芯片报 `sock4 TIMEOUT`),net 层随即 relisten(监听端口复用);② 中止时在途数据(约一个缓冲=2048B 残渣)**随后才落进芯片 RX 缓冲**;③ 下一笔连接 accept 后 ~10-20ms 出现伪 `DISCONNECTED, releasing (chip tcp=0x00)`,lftpd 数据读循环据此读到 EOF → 把已到的 2048B 当整个文件 → 226;④ 再下一笔芯片已干净 → 正常(**一次性**而非持久故障)。**缺陷位于 fd 复用生命周期**(上一条连接的残留状态投递给下一条连接),不在 FTP 命令逻辑——lftpd 只是把它放大成误报 226。 -- **边界甄别(现场"1~2s"时间假设被推翻)**:gap 500/2000/5000ms 复测设备实存尺寸**全部仍 2048**(12/12)→ **与时间无关**;触发=设备侧**收过数据**的数据连接被对端关闭(纯 FIN 即触发);同 fd 非 FTP 服务(`:7003`)不受影响(不按传输复用)。 +- **状态**:✅(三阶段全部闭环;回归判据常备:`test/host/ftp_abort_probe.py`) +- **现象**:客户端上传(STOR)中途断开(**RST 或优雅 FIN 均可**),**紧随其后的第一笔复连上传被截断到 ≤2048 字节(= 芯片单 socket RX 缓冲大小),且服务端回报 226 成功** = **静默数据损坏**;第二笔及以后恢复正常。 +- **证据(可信复现:生产固件、零固件探针)**:判据 = FTP 协议结果 + 紧接一笔正常上传的 SHA-256 往返 + 控制通道 NOOP 存活探测。RST 中止 **10/10** 复现;优雅 FIN **5/5** 同样复现 → 凡"数据未发完即断开"均触发。对照(无中止)177 轮(8KB~1MB,含 100 轮零间隔浸泡)SHA-256 全匹配(`MDK-ARM/ftp_freeze_probe.py`)→ 中止路径是唯一触发条件。 +- **机制(设备侧日志实证)**:① 中止时数据 socket 释放(RST 时芯片报 `sock4 TIMEOUT`),net 层随即 relisten(监听端口复用);② 中止时在途数据(约一个缓冲=2048B 残渣)**随后才落进芯片 RX 缓冲**;③ 下一笔连接 accept 后 ~10-20ms 出现伪 `DISCONNECTED, releasing (chip tcp=0x00)`,lftpd 读到 EOF → 把已到的 2048B 当整个文件 → 226;④ 再下一笔芯片已干净 → 正常(**一次性**)。**缺陷位于 fd 复用生命周期**(上一条连接的残留状态投递给下一条连接),不在 FTP 命令逻辑。 +- **边界甄别**:gap 500/2000/5000ms 复测实存尺寸**全部仍 2048**(12/12)→ **与时间无关**;触发=设备侧**收过数据**的数据连接被对端关闭(纯 FIN 即触发);同 fd 非 FTP 服务(`:7003`)不受影响(不按传输复用)。 - **修复(三阶段,`net_socket.c` + `lftpd.c`)**: - 1. **异常释放(TIMEOUT/RST)隔离**:`handle_timeout_event` 置 per-fd 隔离标记;`net_relisten_locked` 隔离期内持续排空芯片 RX(`net_drain_chip_rx()`)+ 主动 close + 排空该 socket 中断 + 等 `CLOSED/TCP_CLOSED`,满 `NET_RELISTEN_QUARANTINE_MS`(400ms) 才 open+listen;上限 `NET_RELISTEN_SETTLE_MS`(800ms) 强制放行——**必须小于** `NET_RELISTEN_DEADLINE_MS`(2000ms):实测取 2000ms 会耗尽重试预算 → give up → **监听器死亡**(已留痕); - 2. **正常释放(FIN)沉降**:`handle_disconnect_event` 登记(仅 `recv_bytes != 0` 时——纯连接即关与只发不收免沉降保复连时序),relisten 前最短等 `NET_RELISTEN_GRACEFUL_MS`(**100ms**,实测悬崖:0/30ms 无效、100ms 稳定通过,对 30ms 重试节拍留 3× 余量);日志区分 `relisten settle`/`relisten quarantine`; - 3. **应用侧硬化**:`net_sock_t.released_abnormal` 标记(TIMEOUT 置 1/DISCONNECT 置 0,新 fd 与新监听清零——新连接不得继承上一条的释放原因);`net_pending_tick` 断连了结按标记以 `NET_ERR_CONNRESET` 或 EOF(0) 回传;`lftpd receive_file` 错误路径 **unlink 截断文件** + STOR 回 **450**(原误报 226)。 + 1. **异常释放(TIMEOUT/RST)隔离**:`handle_timeout_event` 置 per-fd 隔离标记;`net_relisten_locked` 隔离期内持续排空芯片 RX(`net_drain_chip_rx()`)+ 主动 close + 排空该 socket 中断 + 等 `CLOSED/TCP_CLOSED`,满 `NET_RELISTEN_QUARANTINE_MS`(400ms) 才 open+listen;上限 `NET_RELISTEN_SETTLE_MS`(800ms) 强制放行——**必须小于** `NET_RELISTEN_DEADLINE_MS`(2000ms):取 2000ms 会耗尽重试预算 → give up → **监听器死亡**; + 2. **正常释放(FIN)沉降**:`handle_disconnect_event` 登记(仅 `recv_bytes != 0` 时——纯连接即关与只发不收免沉降保复连时序),relisten 前最短等 `NET_RELISTEN_GRACEFUL_MS`(**100ms**,实测悬崖:0/30ms 无效、100ms 稳定通过);日志区分 `relisten settle`/`relisten quarantine`; + 3. **应用侧硬化**:`net_sock_t.released_abnormal` 标记(TIMEOUT 置 1/DISCONNECT 置 0,新 fd 与新监听清零——新连接不得继承上一条的释放原因);`net_pending_tick` 断连了结按标记以 `NET_ERR_CONNRESET` 或 EOF(0) 回传;`lftpd receive_file` 错误路径 **unlink 截断文件** + STOR 回 **450**。 - **协议边界(不可判定,留痕)**:FTP STOR 无长度声明,"在途 FIN 关闭"与"正常完成"**逐字节不可区分** → 优雅中止仍回 226;仅 RST/掉链(芯片 TIMEOUT)可识别 → 450。FIN 边界由客户端长度校验(SIZE/RETR 往返)兜底。 -- **验证(终态基线)**:`--check-abort` RST **5/5**(450)+ FIN **5/5**(226 边界不变);followup 全过;正常往返 freeze SHA-256 全 MATCH(t_stor 0.68→0.18s);`:7003` sink 复用 RST×4/FIN×4 全过;`:8888` 真实收包写 RTC 成功。 -- **与 TC-NET-604 的关系**:本条是独立、已可信复现并已修复的真实缺陷;TC-NET-604 已结案为非缺陷(Trap 13/21)。现场"上传失败/文件不完整/像卡死"类报告优先用本条判据复核。 -- **发现时间**:2026-09-23(可信复现首轮);二阶段 09-24、硬化 09-25。 +- **回归基线**:`--check-abort` RST **5/5**(450)+ FIN **5/5**(226 边界不变);followup 全过;正常往返 freeze SHA-256 全 MATCH;`:7003` sink 复用 RST×4/FIN×4 全过;`:8888` 真实收包写 RTC 成功。 +- **与 TC-NET-604 的关系**:本条是独立、已可信复现并已修复的真实缺陷;TC-NET-604 已结案为非缺陷(判读教训见第六章台架纪律)。现场"上传失败/文件不完整/像卡死"类报告优先用本条判据复核。 --- # 第四章 数据通路与数值语义 -## Trap 04 · UDP 发送后必须等待 SENDBUF_FREE +## TRAP-NET-04 · UDP 发送后必须等待 SENDBUF_FREE -- **状态**:✅ 已定案 +- **状态**:✅ - **现象**:UDP 发送后立即接收,接收长度恒 0 或数据错误。 - **根因(定案)**:手册要求每次 `WRITE_SEND_BUF` 后必须等 `SINT_STAT_SENBUF_FREE`,否则下次写入被静默丢弃。 - **修复**:每次 `ch395f_write_send_buf()` 后轮询 `ch395f_get_sock_int_status()` 检查该标志。 -- **发现时间**:广播宣告模式(原记 Phase 6 2025-07 有误,UDP 对应阶段 3)。 -## Trap 13/21 · 「recv_len 归零 / 接收缓冲冻结」家族——非缺陷(合并结案) +## TRAP-NET-14 · NET 层目的 IP 字节序写反 → CONNECT TIMEOUT -- **状态**:↩️ 已撤回(2026-09-23 结案;2026-10-13 把 Trap 13 并入本条——同族误判,当年未同步) -- **合并说明**:Trap 13(2026-08-26,"分批读 recv_len 归零")与 Trap 21(TC-NET-604"恰好填满+排空窗口冻结")是同一轮误读的两半;Trap 13 的机理断言与手册直接相左,一并撤回。 -- **结案结论**:原报告(同连接前序突发后 4096B 突发"前 2KB 正常回显、其余静默无回显")**在当前固件上无法复现,判定为非缺陷**。可信判据(产品固件 + 真实 lftpd FTP 上传,成功 = FTP 协议结果 + **上传后 RETR 回读 SHA-256**)实测 **227 轮全过**(8KB/64KB/256KB/1MB;含零间隔快速重连与 100 轮浸泡),并发生产 UI 流 600 帧 CRC 全过。 -- **为什么原台架会"复现"(教训,务必先读)**: - 1. **测试程序自吞数据**:台架配方里的固件内读者(Phase 9/11 的 64B 读、固件探针读)把数据读走后不回显——PC 端少收被误当"设备停摆"。实测短收量**恰等于被吞字节数**(`4736−4608=128=2×64`);对照组(无额外读者)40/40 全过。 - 2. **判读工具误判**:`probe_classify.py` 把**通过**的运行报成 `STALL`(脚本层实测 `1460/1460` 且内容逐字节匹配)。 - 3. 由此派生的"部分排空致命 / 再武装区间 ≥160ms / 相位刀口"等结论**全部作废**。 -- **Trap 13 历史注**:当年确实观察到 `recv_len` 归零,但机理断言("读部分数据后 recv_len 被重置为 0")**与 §5.34 明文相左**("未读取的数据仍然保留在接收缓冲区中,可以继续读取"),本仓 TC-NET-201 注释也是相反口径;定案前须补一条"部分读 → 再查 `recv_len`"的定向 TC。⚠ 该已撤回机理**仍被当作现行约束**写在 `CH395F_Test_Guide.md` 多处与 `net_socket.c` 注释里——处置登记在第六章待修清单。 -- **台架纪律(保留)**:① 数据完整性以 **SHA-256 往返**为真值,并扣除固件读者吞噬的字节;② 冻结/卡死类走**真实生产路径 + 端到端内容校验**(`MDK-ARM/ftp_freeze_probe.py`),卡住时用控制通道 NOOP 区分"数据面卡死"与"整层卡死";③ **禁止把"固件内非回显读者"与固定期望值混用在同一配方里**——本轮最大教训。 -- **保留的代码变更(与撤回机理无关)**:缓冲块布局与 MSS 解耦(`ch395f.h`)、超限读 OVERSIZE 金丝雀(`ch395f.c`)不回退(防御性/可观测性改进)。 -- **发现/结案**:发现 2026-08-26(阶段 8);结案 2026-09-23;合并 2026-10-13。 - -## Trap 16 · NET 层目的 IP 字节序写反 → CONNECT TIMEOUT - -- **状态**:✅ 已修复定案(字节序总口径,多处引用) +- **状态**:✅(字节序总口径,多处引用) - **现象**:NET 层 `net_connect()` 返回 0 但 `sock0 CONNECT TIMEOUT` 永远连不上;同一芯片同一目标的硬件层 client 完全正常。 -- **根因(定案)**:`net_connect_locked()` 把 `sin_addr.s_addr`(网络字节序)按**低字节在前**拆成 `remote_ip_arr[4]` 得到 `[2,1,168,192]` 交给 `ch395f_set_des_ip()`——但 **`SET_IP_ADDR_SN` 族按点分顺序(`[192,168,1,2]`)写入才生效**(硬件层 `test_parse_ip` 直出点分即成功;官方例程 `CH395IPAddr={192,168,1,101}` 同向)。芯片把 SYN 发往 `2.1.168.192` → 网关丢弃 → TIMEOUT。⚠ 手册 §5.8 总括"所有 IP 输入输出命令字节顺序均为低字节在前"与官方例程自相矛盾,**本仓以实测+例程为准**。 +- **根因(定案)**:`net_connect_locked()` 把 `sin_addr.s_addr`(网络字节序)按**低字节在前**拆成 `remote_ip_arr[4]` 交给 `ch395f_set_des_ip()`——但 **`SET_IP_ADDR_SN` 族按点分顺序(`[192,168,1,2]`)写入才生效**(硬件层 `test_parse_ip` 直出点分即成功;官方例程 `CH395IPAddr={192,168,1,101}` 同向)。芯片把 SYN 发往 `2.1.168.192` → 网关丢弃 → TIMEOUT。⚠ 手册 §5.8 总括"所有 IP 输入输出命令字节顺序均为低字节在前"与官方例程自相矛盾,**本仓以实测+例程为准**。 - **误导性排查**:`GET_REMOT_IPP` 回读是**小端**——把写反的数组读回再按小端解释恰好又得 `192.168.1.2`,回读日志"看起来正确",掩盖真实写反。 - **修复**:① `remote_ip_arr` 改**大端**提取(`arr[0]=(ip>>24)&0xFF …`);② UDP `net_send_locked()` 同样修正;③ 诊断回读按小端语义重组再打印。 - **字节序总口径(务必分清两套)**: @@ -275,143 +215,103 @@ - **16 位端口一律低字节在前**(§5.25/§5.26),两者口径不同,勿把"大端数组"套到端口上; - 回读类命令(GET_REMOT_IPP/GET_IP_INF)若标注"低字节在前",打印前先按小端重组; - 本层 `sin_addr.s_addr`/`remote_ip` = 网络字节序数字;`remote_ip_arr[4]` = 点分顺序(`net_types.h` 注释)。 -- **发现时间**:阶段 7 NET 层 TCP Client 上板实测,2026-08-25。 -## Trap 20 · 单次写超过发送缓冲区(写穿) +## TRAP-NET-16 · 单次写超过发送缓冲区(写穿) -- **状态**:✅ 已修复定案(措辞已按 2026-10-13 更正:不是"硬件 FIFO",是本仓分配的发送缓冲) +- **状态**:✅ - **现象**:单次发送 >1KB 时数据中段错乱、连接被对端 RST、或 `WRITE_SEND_BUF` 后芯片不再产生 SENBUF_FREE——发送卡死或后续字节丢失;只在 >1KB 时稳定复现。 -- **根因(定案)**:分块上限误取 `NET_DMA_MAX_PAYLOAD`(2KB)/软件缓冲(4KB),而每 Socket 的发送缓冲 = 本仓固定 2 块×512B = **1KB**(§5.45 上电默认其实是 4 块=2KB,本仓布局自 2026-09-22 起固定 2 块、与 MSS 解耦);一次性写 2KB/4KB 直接**写穿发送缓冲**(超出部分覆盖/丢失或破坏指针)。 +- **根因(定案)**:分块上限误取 `NET_DMA_MAX_PAYLOAD`(2KB)/软件缓冲(4KB),而每 Socket 的发送缓冲 = 本仓固定 2 块×512B = **1KB**(§5.45 上电默认其实是 4 块=2KB,本仓布局固定 2 块、与 MSS 解耦);一次性写 2KB/4KB 直接**写穿发送缓冲**。 - **修复**:`CH395F_SEND_BUF_SIZE_BYTES` 宏 + `NET_SEND_CHUNK_MAX = min(NET_DMA_MAX_PAYLOAD, CH395F_SEND_BUF_SIZE_BYTES)`(现 1KB);`net_send_locked`/`net_sendto` 分块上限改用它。门控是 **SENBUF_FREE**(不是"等 SEND_OK");接收方向不受影响(只读 `get_recv_len` ≤ 接收缓冲 2KB)。 - **注意**:上限随 `CH395F_SEND_BLOCKS` 自动跟随;调用方可按任意长度调 `net_send`,分块由驱动保证。 -- **发现时间**:阶段 8 大文件传输实现,2026-08-26。 + +## TRAP-NET-21 · IP 显示段序与对端地址(getsockname ≠ 对端) + +- **状态**:✅(N34 阶段 3 收尾修复) +- **现象**:FTP 日志把 IP 打成 `100.1.168.192`(字节反转),且打印的是**本机** IP:21 而非客户端地址——排查连接来源时被误导。 +- **根因(定案)**:① `net_inet_ntoa` 段序反转——`NET_NTOHL` 后主机数字的 **MSB 才是点分首段**,原实现低段在前;② lftpd 打印 `net_getsockname` = 本机监听地址(端口恒 21),对端地址应读 `net_getpeername`。PASV 227 消费点做了 `net_htonl` 再 MSB 拆,**双重反转恰好正确,勿误判**。 +- **修复**:`net_inet_ntoa` 段序改 MSB 首(MCU+模拟器同步);新增 `net_getpeername`(读 CONNECT 事件缓存,零 SPI);lftpd 改打印**对端** IP:port(台架判据:日志 `connection received from 192.168.1.x:<高位端口>`)。 +- **教训**:显示层字节序 bug 不影响协议本身(FTP 功能全程正常),但会系统性误导排障——**日志里的地址也要当被测对象核对**。 --- # 第五章 上层集成与主机侧工具 -## Trap 03 · DHCP 包 xid 偏移错误(主机工具) +## TRAP-NET-03 · DHCP 包 xid 偏移错误(主机工具) -- **状态**:✅ 已修复 +- **状态**:✅ - **现象**:Python DHCP Server 收到 DISCOVER 后无法匹配 Offer/Ack 的 xid。 - **根因(定案)**:误用 `data[232:236]`(Wireshark 某旧版显示误导);标准偏移 xid=`data[4:8]`、msg_type=`data[242]`、client_mac=`data[28:34]`、magic cookie=`data[236:240]`。 - **对应**:`test/host/ch395f_socket_test.py` DHCP server 模式。 -- **发现时间**:阶段 4(原记 Phase 6 2025-07 有误)。 -## Trap 10 · 消息队列值拷贝导致结果无法回传 +## TRAP-NET-10 · 消息队列值拷贝导致结果无法回传 -- **状态**:✅ 已定案(现行契约:`net_*` 错误码走通知值——AGENTS 模块约定) +- **状态**:✅(现行契约:`net_*` 错误码走通知值——AGENTS 模块约定) - **现象**:`net_send/net_recv/net_listen/net_accept/net_close` 等委托 netTask 的 API 永远返回 -1,即使内部成功。 - **根因(定案)**:`osMessageQueue` 传 `net_msg_t` 是**值拷贝**——netTask 修改的是队列副本,调用方栈变量的 `msg.result` 从未被更新。 - **修复**:结果搭 `xTaskNotify(caller, (uint32_t)msg.result, eSetValueWithOverwrite)` 通知值回来,调用方 `xTaskNotifyWait` 取回。**勿给 `net_msg_t` 加"回传用"字段**(调用方只会读到栈垃圾)。 -- **注意**:指针成员(`msg.buf`)不受值拷贝影响;内核级 `_locked` API 直接操作不受影响。⚠ 原注"net_connect() 不使用消息队列"已失效——现行 `net_connect` 同样走消息队列、结果同样走通知值。 -- **发现时间**:FTP 改造期(2026-08)。 +- **注意**:指针成员(`msg.buf`)不受值拷贝影响;内核级 `_locked` API 直接操作不受影响。 -## Trap 12 · FatFS 路径与 8.3 短名(↦ 已移入他档) +## TRAP-NET-13 · DHCP 成功后 IP 寄存器提交滞后 -- **状态**:↦ 移档——**非 CH395F 议题**(FTP 改造期归档错位;`docs/网络/N34-自愈审计/CH395F驱动层审计.md` 早已点破"Trap 12 非 CH395F")。完整内容与根因更正(含按 `Lib/FatFs/ff.c` 删去的两条误判根因)已移入 `docs/架构设计/STM32F4-Base存储架构说明.md` §10.1 第 14 项(2026-10-13)。 -- 编号在此保留仅为旧引用不断链;发现期:FTP 改造期(2026-08)。 - -## Trap 15 · DHCP 成功后 IP 寄存器提交滞后 - -- **状态**:✅ 已定案(测试侧稳定轮询+交叉比对) -- **现象**:`GET_DHCP_STATUS` 已置 0x00 后立即 `GET_IP_INF` 读到的仍是旧静态配置,而芯片实际已按新租约收发——三方数据矛盾构成假阳性(2026-08-24 上板实测)。 +- **状态**:✅(测试侧稳定轮询+交叉比对) +- **现象**:`GET_DHCP_STATUS` 已置 0x00 后立即 `GET_IP_INF` 读到的仍是旧静态配置,而芯片实际已按新租约收发——三方数据矛盾构成假阳性。 - **根因(定案)**:DHCP 状态位仅代表协商完成;IP/MASK/GW 寄存器提交与状态置位**异步**,数百毫秒级窗口(实测 ~1s 内翻转)。 - **修复**:① 连续 3 次读 `GET_IP_INF`(间隔 300ms,上限 8s)一致且非零才采信;② PC 端把收到的宣告 IP 与租约比对兜底。 - **对应**:`test/board/ch395f_test_task.c: phase4_run()`;`docs/测试说明/CH395F_Test_Guide.md` 阶段 4 风险表 #4。 -- **发现时间**:2026-08-24。 --- -# 第六章 2026-10-11 寄存器/数值口径全量审计(A/B 系列) +# 第六章 审计遗留与待修(B 系列,冻结号) -**方法**:四路并行——驱动层逐命令(含官方 SDK 逐字对照)、NET/测试层逐调用点、Trap 01–12 复核、Trap 13–25 复核。三源交叉:`docs/芯片手册/CH395F.md` + WCH 官方例程 `CH395EVT/EXAM/PUB/CH395CMD.c`、`CH395INC.H` + git 历史。 - -## 干净清单(核对过、无异常) - -| 项 | 计数 | 结论 | -|---|---|---| -| `CH395F_CMD_*` 命令码 | 55 | 与 §5 命令表 + SDK `CMDxx_` 逐个一致,无一抄错 | -| 16/32 位量字节序 | 9 处 | 全部低字节在前,与手册明文 + SDK 逐字节一致 | -| 读命令输出字节数 | 14 条 | 无少读/多读(`GET_SOCKET_STATUS`=2、`GET_IP_INF`=20 且顺序 IP→GW→MASK→DNS1→DNS2、`GET_UNREACH`=8、`GET_REMOT_IPP`=6) | -| 常量表 | 60 个 | `ERR_*`18 / `TCP_*`11 / `SOCKET_*`2 / `PHY_*`6 / `PROTO_*`4 / `SINT_STAT_*`6 位号全对;唯一错值见 A3 | -| `GET_GLOB_INT_STATUS_ALL` 拼装方向 | — | 与官方 `CH395CMDGetGlobIntStatus()` 逐字相同;`glob & (1<<(sock+4))` = §5.16 bit4..bit11 | -| `get_socket_status` 缓冲区 | 17 处调用点 | 全部 `uint8_t st[2]`,无 1 字节残留 | -| 中断判据 | 17 掩码 / 32 全局读 / 27 SINT 读 | 位号与 §5.16/§5.23 一致;只用 16 位 ALL 版 | -| 单位与取值范围 | 12 个参数点 | retrans 8/200ms、KeepAlive 60000/5000、ARP 100ms 单位、MSS 1024 全部合规 | -| 缓冲布局 | 3 处组合 | `net_init` 8×(4收+2发)=48 块恰好用满、无交叠(零裕量) | -| 显示假象 | 10 处 `%d.%d.%d.%d` | 只有已定案的 `net_socket.c` getsockname 一处是 MSB 重排假象(见 B5),无新增同类 | - -## 新发现(去重后按严重度) +2026-10-11 寄存器/数值口径全量审计(四路并行:驱动逐命令 + NET/测试逐调用点 + Trap 复核;三源交叉:`docs/芯片手册/CH395F.md` + WCH 官方例程 + git 历史)的未闭环项。A 系列与已修 B 项的结论已沉淀进上文条目,不再重复;B 编号冻结不重排。 | # | 严重度 | 位置 | 问题 | 状态 | |---|---|---|---|---| -| A1 | 中 | `test/board/ch395f_test_task.c:1005` | TC-203 的 Trap 14 反核实写成 `st_chk[0]==CH395F_TCP_ESTABLISHED`——byte0 只有 `00H/05H`,条件**恒假** → 防线永久失效且日志无痕(与 Trap 07 同类:byte0/byte1 语义越界) | ✅ 已修(改 `byte0==OPEN && byte1==ESTABLISHED`) | -| A2 | 中 | 同上 `:1150`/`:1321` | Phase 3/4 把 `GET_RECV_LEN` 返回值直接当读取长度(本地 `rx_buf[300]/[64]`,芯片 RX 2KB)→ 栈溢出 | ✅ 已修(夹到 `sizeof` + 留痕) | -| A3 | 低 | `ch395f.h:253` | `CH395F_GINT_STAT_DHCPV6=0x8000`(bit15);SDK 是 `(1<<12)`、§5.16 明确 `[12:15]` 保留 | ✅ 已修(改 `0x1000` + 注明本芯片恒 0;全仓零引用) | -| A4 | 低 | `ch395f.h/ch395f.c` 多处 | 分组注释字节数错 4 处;KeepAlive 参数注释写"秒"(§5.53/5.54 是 ms);`set_retrans_count` 写"0=默认值5"(§5.17 默认 12)。**实现全部正确,注释误导**——B3 的 1 字节缓冲栈越界正是这类注释发起的 | ✅ 已修 | -| A5 | 低 | `net_socket.c` 7 处注释 | 注释与手册/HEAD 相反:`FUN_PARA(SOCKET_CLOSE=0)`(实为 bit3=1,正是 closed-with-data accept 的契约依据)、"初始化成功后设置 MAC"(§9.2.1 ① 是之前)、"复用默认缓冲"、"布局由 MSS 推导"(Trap 13/21 已解耦)、"等 SEND_OK"(门控是 SENBUF_FREE)、UDP 头 `[0-1] reserved`(§9.2.4 是数据长度) | ✅ 已修 | -| A6 | 低 | `ch395f.c:1176` | `write_send_buf` 超限**静默 return**(void、无日志),且上限取 DMA 2048 而非发送缓冲 1024;读路径有 OVERSIZE 金丝雀、写路径没有 | ✅ 已修(补 `[ERR]` 留痕 + >1KB 告警,行为不变) | -| B1 | ~~中~~→定案 | `net_socket.c`(KeepAlive 使能点) | 原判"产品路径从未启用 Socket KeepAlive → 静默断开无人知晓"(§5.56 服务器侧应在 CONNECT 后开)。2026-10-12 曾推翻前提:禁用 PC 网卡(链路层死亡)芯片 **+2s** 即报 `sock0 TIMEOUT`(=§5.17 重传 8×200ms 量级,与 KeepAlive 无关)→ 依 Trap 25 铁律撤回使能。**2026-10-13 E11 再定案**:用"禁用网卡"注入"链路在、对端静默"(防火墙规则对已建立连接无效,方法学见 `docs/网络/N34-自愈审计/实验方案.md` E11)实测服务器侧开启后 60s/5s/3 → **判死 ≈30.0s**(早于手册公式 75s,以实测为准);短参数 6s/2s/2 → ≈5.1s(参数生效);静默但存活对端**不被误杀**(90s 空闲回显正常) | ✅ **已转正**(`handle_connect_event` LISTENING 分支按 `docs/芯片手册/CH395F.md` §5.56 开启;Phase 11 回归 TC-NET-1101 已更新死对端注入法) | -| B2 | 中低 | `net_socket.c`(`net_connect_locked`) | `ch395f_tcp_connect()` 返回值被丢弃。§9.2.2 ⑧:`GET_CMD_STATUS` SUCCESS 仅表示**命令**执行成功,丢弃它 = 命令级被拒要等 30s recv 超时才暴露 | ✅ 已修(2026-10-12):非 SUCCESS 且**非 BUSY** 才判失败(`poll_cmd_status` 超时返 `ERR_BUSY`=芯片仍在执行,当失败会误杀慢芯片);明确失败 → `[ERR] rejected by chip: 0xNN` + 按 §9.2.2 ⑧ 关 Socket 回滚 + 返 -1。TC701/702 回归无误杀。⚠ 残余:`net_reconcile_tick` 仍只裁 ESTABLISHED/LISTENING,`CONNECTING` 丢中断无人收口(产品不走出向连接、无法实测,按"不加未证明复杂度"暂不动) | -| B3 | 低→**结构性** | `net_listen_locked` + `net_relisten_locked`(后者是每次断连的热路径) | 两处对**全部 8 个 Socket** `GET_INT_STATUS_SN`(读即清)+ 无条件读清全局。机制:§5.15 全局中断寄存器**整体读即清、无按位清除手段**,而 `net_poll` 只在全局位置位时才读该 Socket 的 SINT(门控)→ 丢弃式读全局把别的在用 Socket 的门一并焊死;SINT 又**粘滞到被读**(Trap 14/17 实证)→ 全量读走即**永久销毁**其 CONNECT/DISCONNECT/TIM_OUT/SENBUF_FREE(唯 RECV 是条件位可下轮补,Trap 08)。git 考古:多连接时代引入 → 单连接搬迁 → relisten 复制,**不属于任何已登记 Trap 的修复**;官方 SDK 21 个例程一律"全局位图门控 + 单 Socket 读 + 立刻转存",无一例全量读 8 个 SINT | ✅ 已修(2026-10-12):两处收窄为**只清本 Socket SINT** + **删除丢弃式全局读清**(全局交 `net_poll` 集中读清=AGENTS 模块约定)。回归:Trap 25 11/11×3、Trap 23 RST 5/5+FIN 5/5、Trap 22 全腿与基线一致。⚠ 残余:`net_connect_locked`/`net_close_locked`/`net_relisten_locked` 三处指定预清仍含无条件全局读清(会**搁浅**而非销毁其它 Socket 挂起事件)——它们是 Trap 14/17/23 的两级预清,宜与 B4 一并处置 | -| B4 | 低 | `net_socket.c:446/469` | 全局中断 bit0-3(`UNREACH`/`IP_CONFLI`/`PHY_CHANGE`/`DHCP`)被读清但从不消费;`ch395f_get_unreach_info()` 全仓零调用 → IP 冲突、ARP 不可达、链路抖动三类事件完全静默 | ⏳ 待修(可观测性缺口) | -| B5 | 低 | `net_getsockname` → `lftpd.c` 日志 | `net_getsockname` 的 `s_addr` 组装方向与本层约定相反 + `net_inet_ntoa` 段序反转(NET_NTOHL 后主机数字的 MSB 才是首段,原实现低段在前)→ FTP 日志把 IP 打成 `100.1.168.192`;且 lftpd 打印的是 `getsockname`=**本机 IP:21** 而非对端。⚠ PASV 227 消费点做了 `net_htonl` 再 MSB 拆,**双重反转恰好正确,勿误判** | ✅ 已修(2026-10-13,N34 阶段 3 收尾;代码随本分支收尾批提交):`net_inet_ntoa` 段序改 MSB 首(MCU+模拟器同步);新增 `net_getpeername`(读 CONNECT 事件缓存,零 SPI);lftpd 改打印**对端** IP:port(台架判据:日志 `connection received from 192.168.1.x:<高位端口>`)。PASV 未动(原本正确) | -| B6 | 低 | `net_socket.c:591/1225/3608` | 3 处状态判据只看 `byte1`、未按 §5.22 用 `byte0` 门控;reconcile 的 else-if 链里 `(00H, 陈旧 04H)` 会先命中"补 accept"而非"重建监听" | ⏳ 待修(Trap 07 实测 `byte0=00H` 时 `byte1` 读回 `0x00`,故为低概率) | -| B7 | 低 | `ch395f_test_task.c:1379/1504` | Phase 5 单次写 4093B 超 1KB 发送缓冲与 2048 DMA 载荷 → 整块被丢;注释仍写"4096B=发送缓冲上限"(旧事实)。A6 修后会打 `[ERR]` | ⏳ 待修(chunk 改 ≤1024 或分块) | -| B8 | 低 | `ch395f_test_task.c:1290-1292` | Phase 4 把 sock6 缓冲重排到块 24-27,与 `net_init` 固定布局中 sock4 的 24-27 **交叠**(§5.45 禁止) | ⏳ 待修 | -| B9 | 低 | `ch395f_reuse_bench.c:93`、`net_socket.c:39/262` | bench 用 `get_phy_status()!=0` 判链路(01H=断开也成立,应 `!= CH395F_PHY_DISCONN`);`NET_PHY_CHECK_INTERVAL_MS` 死宏;无芯片版本门控(§5.16 <0x44 不支持 ALL 版、§5.52 bit3/4 需 ≥0x46,实测 0x4A 无影响) | ⏳ 待修(前两项死代码/卫生) | -| B10 | **中** | `net_reconcile_tick` 认领范围 + `handle_disconnect_event` 释放分支 | **孤儿监听 → 端口永久拒连**:释放分支把状态置 CLOSED 等"应用重新监听",但 `ui_net_sink` 只在 `net_recv` 返 0(EOF)时才清 `g_client`,而 EOF 已被事件分支消费 → net 层等应用重建 / 应用等 net 层给 EOF / 对账只裁 ESTABLISHED·LISTENING 直接跳过 / 芯片不会自己回 LISTEN,**四方互等**。实测探针 `TEMP orphan sock2 state=0 port=7003 chip=0x05/0x00` 持续 12s+,`:7003 300ms×10` **0/10**。与 Trap 22 的关系:①③两类都要求本层处于 ESTABLISHED/LISTENING,**孤儿态恰好落在两类之外** | ✅ 已修(2026-10-12):对账新增**形态④**认领孤儿(`in_use`+`state==CLOSED`+`auto_relisten`+`type==STREAM`+`local_port!=0`,芯片侧不在 LISTEN)→ `net_relisten_try`。⚠ `auto_relisten` 是决定性门:少了它出向连接(Phase 7 TC701~703 绑本地口)会被误判孤儿 relisten 成监听器。回归:`:7003` 0/10→**10/10**;其余全腿与基线一致。⚠ ④ 的副作用:`net_recv` 的 EOF 是**边沿**——④抢先 relisten 会把慢消费者的 EOF 永久吞掉(`g_client` 不清、新客户端数据被旧会话静默吸收)。**根治出口 = `net_drop_conn` + 生命周期设计**(N34 阶段 3 已落,`6394853`):原理与分阶段方案 → `docs/子系统设计/网络Socket监听生命周期设计说明.md`(含:芯片为何永不自动回 LISTEN、`fun_para=0x08` 使 `0x05/0x00` 成为合法常驻态、RFC 1122 §4.2.2.18 与 POSIX accept 的规范冲突、12 个重建所有者与 U1~U15 无人负责组合、芯片原生多连接模式(A′)为何对本产品不可用) | +| B2 | 中低 | `net_connect_locked` | `ch395f_tcp_connect()` 返回值曾被丢弃(§9.2.2 ⑧:SUCCESS 仅表示**命令**执行成功)。已修:非 SUCCESS 且**非 BUSY** 才判失败(`ERR_BUSY`=芯片仍在执行,当失败会误杀慢芯片);明确失败 → `[ERR] rejected by chip: 0xNN` + 按 §9.2.2 ⑧ 关 Socket 回滚 + 返 -1 | ⚠ 残余:`net_reconcile_tick` 仍只裁 ESTABLISHED/LISTENING,`CONNECTING` 丢中断无人收口(产品不走出向连接、无法实测,按"不加未证明复杂度"暂不动) | +| B3 | 低→**结构性** | `net_listen_locked` + `net_relisten_locked`(后者是每次断连的热路径) | 对**全部 8 个 Socket** `GET_INT_STATUS_SN`(读即清)+ 无条件读清全局。机制:§5.15 全局中断寄存器**整体读即清、无按位清除手段**,而 `net_poll` 只在全局位置位时才读该 Socket 的 SINT(门控)→ 丢弃式读全局把别的在用 Socket 的门一并焊死;SINT 又**粘滞到被读** → 全量读走即**永久销毁**其 CONNECT/DISCONNECT/TIM_OUT/SENBUF_FREE(唯 RECV 是条件位可下轮补,TRAP-NET-08)。官方 SDK 21 个例程一律"全局位图门控 + 单 Socket 读 + 立刻转存",无一例全量读 8 个 SINT | ✅ 已修:两处收窄为**只清本 Socket SINT** + **删除丢弃式全局读清**(全局交 `net_poll` 集中读清=AGENTS 模块约定)。⚠ 残余:`net_connect_locked`/`net_close_locked`/`net_relisten_locked` 三处指定预清仍含无条件全局读清(会**搁浅**而非销毁其它 Socket 挂起事件)——它们是 TRAP-NET-12/23 的两级预清,宜与 B4 一并处置 | +| B4 | 低 | `net_socket.c` 全局中断分发 | 全局中断 bit0-3(`UNREACH`/`IP_CONFLI`/`PHY_CHANGE`/`DHCP`)被读清但从不消费;`ch395f_get_unreach_info()` 全仓零调用 → IP 冲突、ARP 不可达、链路抖动三类事件完全静默 | ⏳ 待修(可观测性缺口) | +| B6 | 低 | `net_socket.c` 3 处 | 状态判据只看 `byte1`、未按 §5.22 用 `byte0` 门控;reconcile 的 else-if 链里 `(00H, 陈旧 04H)` 会先命中"补 accept"而非"重建监听" | ⏳ 待修(TRAP-NET-07 实测 `byte0=00H` 时 `byte1` 读回 `0x00`,故为低概率) | +| B7 | 低 | `ch395f_test_task.c` Phase 5 | 单次写 4093B 超 1KB 发送缓冲与 2048 DMA 载荷 → 整块被丢;注释仍写旧上限。写路径金丝雀(A6)会打 `[ERR]` | ⏳ 待修(chunk 改 ≤1024 或分块) | +| B8 | 低 | `ch395f_test_task.c` Phase 4 | 把 sock6 缓冲重排到块 24-27,与 `net_init` 固定布局中 sock4 的 24-27 **交叠**(§5.45 禁止) | ⏳ 待修 | +| B9 | 低 | `ch395f_reuse_bench.c`、`net_socket.c` | bench 用 `get_phy_status()!=0` 判链路(01H=断开也成立,应 `!= CH395F_PHY_DISCONN`);`NET_PHY_CHECK_INTERVAL_MS` 死宏;无芯片版本门控(§5.16 <0x44 不支持 ALL 版、§5.52 bit3/4 需 ≥0x46,实测 0x4A 无影响) | ⏳ 待修(前两项死代码/卫生) | +| B10 | **中** | `net_reconcile_tick` 认领范围 + `handle_disconnect_event` 释放分支 | **孤儿监听 → 端口永久拒连**:释放分支把状态置 CLOSED 等"应用重新监听",但 `ui_net_sink` 只在 `net_recv` 返 0(EOF)时才清 `g_client`,而 EOF 已被事件分支消费 → net 层等应用重建 / 应用等 net 层给 EOF / 对账只裁 ESTABLISHED·LISTENING 直接跳过 / 芯片不会自己回 LISTEN,**四方互等**。对账①③都要求本层处于 ESTABLISHED/LISTENING,**孤儿态恰好落在两类之外** | ✅ 已修:对账新增**形态④**认领孤儿(`in_use`+`state==CLOSED`+`auto_relisten`+`type==STREAM`+`local_port!=0`,芯片侧不在 LISTEN)→ `net_relisten_try`。⚠ `auto_relisten` 是决定性门:少了它出向连接会被误判孤儿 relisten 成监听器。⚠ ④ 的副作用:`net_recv` 的 EOF 是**边沿**——④抢先 relisten 会把慢消费者的 EOF 永久吞掉。**根治出口 = `net_drop_conn` + 生命周期设计**(N34 阶段 3 已落):原理与分阶段方案 → `docs/子系统设计/网络Socket监听生命周期设计说明.md` | -## 台架注入陷阱(方法论,2026-10-12 起持续补充) +## 台架注入陷阱(方法论,持续补充) -- **Windows 出站防火墙规则不能模拟"对端静默死亡"**:WFP 对**已建立**连接有状态缓存,中途 `New-NetFirewallRule -Direction Outbound -Action Block` 不影响既有流——实测静置+阻断 250s 芯片毫无反应,看似"KeepAlive 不生效",实则 ACK 一直正常往返。 +- **Windows 出站防火墙规则不能模拟"对端静默死亡"**:WFP 对**已建立**连接有状态缓存,中途 `New-NetFirewallRule -Direction Outbound -Action Block` 不影响既有流——看似"KeepAlive 不生效",实则 ACK 一直正常往返。 - 更糟的副作用:该规则会**吃掉本机后来发出的 FIN** → 设备侧留下**幽灵 ESTABLISHED**,之后所有新连接被 §5.29"向试图连接的远端发送 TCP RESET"直接 RST,表现为"端口拒连"——易误判成设备故障(实为自伤)。撤规则后设备自行恢复。 - **能真正掐断链路的手段**:`Disable-NetAdapter`(链路真 down)。⚠ 该 cmdlet **只有 `-Name`/`-InterfaceDescription`**,没有 `-InterfaceIndex`;网卡名是中文,用 `(Get-NetAdapter -InterfaceIndex N).Name` 取值再传入;禁用/恢复必须写在 `try/finally` 里。 - **仍未覆盖的场景**:"链路仍在、对端静默"(NAT 会话过期、中间节点黑洞)没有安全注入手段(静态 ARP 黑洞被交换机泛洪兜底、删 IP 连带拆掉本机连接)——E11 用禁用网卡的近似注入完成定案。 -- **主机侧回显服务器收不到入站连接 → Phase 7 全量 `CONNECT TIMEOUT`**:本机对 `C:\programdata\anaconda3\python.exe` 有两条 Public 入站 Block 规则(`C:\python314\python.exe` 才是 Allow),而 `.venv\Scripts\python.exe` 的基座解释器正是 Anaconda python。Windows 防火墙**块规则优先于放行规则** → 中途加 Allow 规则**完全无效**,只有 `Set-NetFirewallProfile -Name Public -Enabled False` 才通。症状极具误导性:芯片 ~1.6s `CONNECT TIMEOUT`、`net_connect` 仍返 0、`[CONN] openOK` 照常打印,看着像芯片故障,实为主机防火墙。固定跑法:`try { 关 Public → 复跑 } finally { 开 Public }`。 +- **主机侧回显服务器收不到入站连接 → 全量 `CONNECT TIMEOUT`**:本机对 `C:\programdata\anaconda3\python.exe` 有 Public 入站 Block 规则(`C:\python314\python.exe` 才是 Allow),而 `.venv\Scripts\python.exe` 的基座解释器正是 Anaconda python。Windows 防火墙**块规则优先于放行规则** → 中途加 Allow 规则**完全无效**,只有 `Set-NetFirewallProfile -Name Public -Enabled False` 才通。症状极具误导性:芯片 ~1.6s `CONNECT TIMEOUT`、`net_connect` 仍返 0、`[CONN] openOK` 照常打印,看着像芯片故障,实为主机防火墙。固定跑法:`try { 关 Public → 复跑 } finally { 开 Public }`。 - **Phase 7 两条既有失败与改动无关,别追**:① TC704 `socket create #5` FAIL = E-23(`NET_MAX_SOCKETS=8` 减 3 常驻监听 → 并发上限 5,而 `PHASE7_MULTI_COUNT` 仍是 8);② TC703 要求 PC 服务器**离线**,与 701/702/704 环境互斥,同轮连跑必有一侧失真。 -- **对照组必须从干净态起测**:上一腿若**没死透**就重启下一腿,对照组失真。正确做法:每个样本前 `stlink_flash.py reset` + 等 12s 干净启动,并取多样本再比均值。 +- **对照组必须从干净态起测**:上一腿若**没死透**就重启下一腿,对照组失真。每个样本前 `stlink_flash.py reset` + 等 12s 干净启动,并取多样本再比均值。 - **`[PROF]`/`[PROF2]` 字段不是计数器,别拿来判"任务卡死"**:`netp` = `net_poll` 段最长耗时(µs)、`netn` = 采样瞬间在段内时长(`App/util/task_prof.c`)→ 数值恒定只说明"没刷新最大值"。真正的停摆信号是 `[ERR][WD] task stall`;其 `miss=` 打印的是**累计**数(连续数封顶在容忍值 8),容忍 = 8 轮×100ms = 连续 800ms 有任务缺席即停喂、IWDG 2.5s 溢出(`App/util/task_watchdog.c`)。缺席位图解码:`req=0x77` 为 DEFAULT/NET/FTP/UI/TIMESYNC/ADC,`mask=0x41` → 缺席 NET+FTP+UI+TIMESYNC = "netTask 卡住、所有做 `net_*` 的任务阻塞在队列+通知后面"的指纹。 - **静默黑洞用临时探针抓状态,测完即删**:B10 的孤儿态在日志里毫无痕迹,靠对账跳过分支加一条 `TEMP orphan …` 才直接看到;同一轮还顺手用探针**否掉了自己的第一假设**。假设写盘前先测,比推理便宜。 -- **判读通则(与 Trap 25 同源)**:看到 `TIMEOUT` 先**算时序再归因**——2s 与 60s 一眼可分;否则会把"重传超时"误记成"KeepAlive 生效",把相关性当因果。 +- **判读通则(与 TRAP-NET-20 同源)**:看到 `TIMEOUT` 先**算时序再归因**——2s 与 60s 一眼可分;否则会把"重传超时"误记成"KeepAlive 生效",把相关性当因果。 +- **台架纪律(数据完整性,沉淀自已撤回的「recv_len 归零 / 接收冻结」家族)**:TC-NET-604 及其同族(含旧号 13 的"部分读后 `recv_len` 归零"机理——与 §5.34 明文相左)结案为**非缺陷**,可信判据(产品固件 + 真实 lftpd 上传 + RETR 回读 SHA-256)**227 轮全过**(含零间隔与 100 轮浸泡)。原"复现"的根源全在测试配方自身:固件内非回显读者吞数据(短收量恰等于被吞字节数 `4736−4608=128=2×64`)、分类脚本把通过报成 `STALL`。三条规则:① 完整性以 **SHA-256 往返**为真值,并扣除固件读者吞噬的字节;② 冻结/卡死类走**真实生产路径 + 端到端校验**(`MDK-ARM/ftp_freeze_probe.py`),卡住时用控制通道 NOOP 区分"数据面卡死"与"整层卡死";③ **禁止把"固件内非回显读者"与固定期望值混用在同一配方里**。代码保留的 MSS 解耦(`ch395f.h`)与 OVERSIZE 金丝雀(`ch395f.c`)与撤回机理无关,**勿回退**。 -## 主机侧测试工具过期(2026-10-12 实测) +## 主机侧测试工具过期 -- **`test/host/ftp_abort_probe.py` 已修**:`raw_login`/`followup_ok` 原用匿名登录,N-19 后匿名被 530 拒 → Trap 23 两条腿**假 0/5**(round 1 `PASV failed`,且半认证会话占死 FTP 唯一会话,后续轮全变 `ConnectionRefused`)。改 `admin/admin` 后 RST 5/5、FIN 5/5。**教训:跑既有回归腿前先确认凭据仍有效,否则会把工具过期读成固件回归。** +- **`test/host/ftp_abort_probe.py` 已修**:原用匿名登录,N-19 后匿名被 530 拒 → TRAP-NET-18 两条腿**假 0/5**(round 1 `PASV failed`,且半认证会话占死 FTP 唯一会话,后续轮全变 `ConnectionRefused`)。改 `admin/admin` 后 RST 5/5、FIN 5/5。**教训:跑既有回归腿前先确认凭据仍有效,否则会把工具过期读成固件回归。** - **同族未修**:`test/host/ftp_up_dl_test.py`、`ftp_speed_test.py`、`ftp_test.py` 仍用匿名登录 → 对当前固件必挂登录步;`docs/测试说明/FTP_Test_Guide.md` 推荐的这几个入口因此已失效(现行可用入口是 `test/host/ftp_regression.py`,默认 `admin/admin`)。 -## 文档侧待修清单(其余登记在此) +## 文档侧待修清单 -- **最高优先级**:Trap 13/21 已撤回的"部分读后 `recv_len` 归零"机理**仍被当作现行约束**写在 `CH395F_Test_Guide.md:28/69/930/932/958`、`docs/网络/BSD_Socket_API_函数调用关系.md:196/362`、`net_socket.c` 两处注释里——须同批处置,并补一条"部分读 → 再查 `recv_len`"的定向 TC。 +- ~~已撤回机理仍被当作现行约束~~(**2026-10-13 已处置**:`CH395F_Test_Guide.md` ×7、`BSD_Socket_API_函数调用关系.md` ×2、`net_socket.c` ×4、`ch395f.h` ×1 全部改写为"未证实、保守按一次读空执行")。残余:补一条"部分读 → 再查 `recv_len`"的**定向 TC**。 - 陈旧数字:`CH395F_Test_Guide.md:14/24/645/660/1141/1200`(DMA 4100/载荷 4096,现 2052/2048)、`:72/1201`(`NET_DMA_MAX_PAYLOAD=4096`,现 2048;实际分块上限是 `NET_SEND_CHUNK_MAX`=1024)、`:1020-1040`(TC-NET-1002/1003 期望 4096/4097 单次 DMA)、`docs/网络/BSD_Socket_API_使用指南.md:1246`(`NET_SEND_BUF_SIZE` 宏已不存在)。 -- 悬空引用:`Trap 19` 从未存在(`Test_Guide.md:878` 仍引用);`Test_Guide.md:353/418` 的"见风险 #4/风险表 #1"指向已删除的表。 -- 口径冲突:`Test_Guide.md:1135` 的 TRAP-07 行仍写"卡 FIN_WAIT_2/`close_socket`(RST)"(与 Trap 07 定案冲突);`:45` 的"TC704 40/40"与 `:880` 的"`PHASE7_MULTI_COUNT` ≤5"矛盾(HEAD 宏值仍是 8);`Drivers/BSP/NET/README.md:35` 与 `net_socket.c` 的"正常 FIN 路径零改动"与 Trap 23 二阶段矛盾。 -- ~~机理措辞~~(Trap 17/20 措辞、Trap 25 的 `NET_MSG_RESYNC_LISTEN`/`GET_REMOT_IPP`/§5.32+§5.23① 引用——**2026-10-13 重组已在本文件内更正**;`Test_Guide.md` 侧仍待同步)。 -- ~~已失效条目未标注~~(Trap 01/02/11 对现行初始化仍有效、Trap 09 `osDelay(7000)` 已被就绪屏障取代、Trap 10 注记失效、Trap 12 移入存储架构说明 §10.1——**2026-10-13 重组已全部就地处置**)。 -- ~~日期~~(Trap 01/02/03/04/06/07 的"2025-07"早于仓库首提交 `b1d05091` 2026-06-05;Trap 03/04/06 的 Phase 编号错——**2026-10-13 重组已标注为"不可考/已更正"**)。 -- 证据通则:`*.log` 在 `.gitignore` 内 → 本文所有台架计数(40/40、34/34、227 轮、4 轮 11/11、13/13…)**脚本在库、日志未入库 = 不可复核**;Trap 25 引用的实验分支 `n30-chip-bench`(tip `eddbede`)不含 bench 源文件与 uvprojx 注册,裸机芯片台架无法从任何已提交状态重建(主分支存档见 `test/board/ch395f_reuse_bench.c`)。 +- 悬空引用:`Test_Guide.md:353/418` 的"见风险 #4/风险表 #1"指向已删除的表(旧 19 悬空引用已于 2026-10-13 重排时从 `BSD_Socket_API_函数调用关系.md` 清除)。 +- 口径冲突:`Test_Guide.md:1135` 的 TRAP-NET-07 行仍写"卡 FIN_WAIT_2/`close_socket`(RST)"(与 TRAP-NET-07 定案冲突);`:45` 的"TC704 40/40"与 `:880` 的"`PHASE7_MULTI_COUNT` ≤5"矛盾(HEAD 宏值仍是 8);`Drivers/BSP/NET/README.md:35` 与 `net_socket.c` 的"正常 FIN 路径零改动"与 TRAP-NET-18 二阶段矛盾。 +- 证据通则:`*.log` 在 `.gitignore` 内 → 本文所有台架计数(40/40、34/34、227 轮、4 轮 11/11、13/13…)**脚本在库、日志未入库 = 不可复核**;TRAP-NET-20 引用的实验分支 `n30-chip-bench`(tip `eddbede`)不含 bench 源文件与 uvprojx 注册,裸机芯片台架无法从任何已提交状态重建(主分支存档见 `test/board/ch395f_reuse_bench.c`)。 --- -# 附录 A · 驱动关键点速查(细节以对应 Trap 为准) +# 附录 A · 驱动关键点速查(细节以对应条目为准) -- **初始化顺序**:`SET_MAC → SET_IP/GWIP/MASK → INIT_CH395 → SET_PHY`(Trap 24;IP 族必须在 INIT 前——INIT 读取并锁定寄存器值;SET_PHY 在 INIT 后重建物理链路);`CMD_PING_ENABLE` 不需显式调用。 -- **SPI 通信**:每次事务 `ch395f_spi_begin()/end()` 包裹;分频 8(PCB 约束);大数据走 SPI2 DMA(`CH395F_SPI_DMA_BUF_SIZE`=2048+4=2052,`ch395f.h:148`;历史值 1500/4100 已作废,见 Trap 13/21)。⚠ **TSL 规格偏离(N34 E12,2026-10-13 登记)**:手册 §7.4 要求缓存区命令"长度与数据之间"留 TSL(读 0.4us/写 0.2us),本仓两个缓存区函数把命令头与数据打包进**同一 DMA 突发**(间隔=0)属规格偏离;**实测未发现危害**(460 轮/≈58MB 双向 SHA-256 仅 1 次未复现异常,"两段+TSL"变体与现状无差异)→ 依 Trap 25 铁律暂不改产品路径,偏离与证据登记于此、变体留作待用补丁(完整 A/B 见 `docs/网络/N34-自愈审计/实验方案.md` E12)。 -- **TCP 参数**(必须在 INIT_CH395 之前):重传 `SET_RETRAN_COUNT`(默认 12,最大 20)×`SET_RETRAN_PERIOD`(默认 500ms,最大 1000ms);KeepAlive 参数 500ms 倍数、IDLE>INTVL(Trap 06)。 -- **KeepAlive 定案链**:默认关闭 → 服务器侧应在 `SINT_STAT_CONNECT` 后开启(§5.56)→ 2026-10-12 曾因"链路层死亡已被重传 +2s 覆盖"撤回使能 → **2026-10-13 E11 转正**("链路在、对端静默"场景实测判死 ≈30s,参数生效、存活对端不误杀):`handle_connect_event` LISTENING 分支开启 60s/5s/3;消费链路 TIM_OUT → `handle_timeout_event` QUARANTINE → relisten → 端口恢复(详见第六章 B1 与 `docs/网络/N34-自愈审计/实验方案.md` E11/E13)。 -- **Socket 缓冲**:默认只给 Socket 0~3;Socket 4~7 必须 `open_socket` 前显式分配(Trap 02);现行固定布局 6 块/Socket。 -- **TCP 关闭重连**:直接 `ch395f_close_socket()`,不要先 `disconnect`;轮询 `GET_SOCKET_STATUS` **第 1 字节**等 `00H` 再 open;第 2 字节仅在 byte0=OPEN 时有意义(Trap 07)。 -- **SOCK_TIMEOUT**:`SINT_STAT_SOCK_TIMEOUT` 不应视为致命——记录日志后继续操作(消费侧 QUARANTINE 见 Trap 23/B1)。 -- **UDP 模式**:DesIP=0xFFFFFFFF → UDP Server(接收数据前 8 字节为信息头 `reserved[2] src_port[2] src_ip[4]`,§9.2.4 前 2 字节是数据长度);DesIP=具体 IP → UDP Client(无信息头)。发送后必须等 SENBUF_FREE(Trap 04)。 - -# 附录 B · FTP 支持改造一次性问题(历史存档,Trap 已单列者不重复) - -| # | 问题 | 处置 | -|---|------|------| -| 4 | CH395F 多连接模式不支持多监听 | 架构约束:`fun_para=0x00` 单连接(后定案 `0x08`,见 Trap 18) | -| 5 | PASV 端口 0(net_bind 无自动分配) | `net_socket.c: s_dynamic_port` | -| 10 | `s_file_open` 单文件限制 | `lftpd_io.c` 全局变量(已知限制) | -| 15 | `NET_RECV_TIMEOUT_MS=5s` 控制通道超时断开 | `net_config.h: 30000`(后随阶段 1.5 recv 语义再改) | -| 16 | `receive_file` 0 bytes 当作成功 | `lftpd.c: total==0 则失败` | -| 17 | MobaXterm 连数据端口不发数据 | 外部客户端行为(Python 抓包确认,超时恢复) | +- **初始化顺序**:`SET_MAC → SET_IP/GWIP/MASK → INIT_CH395 → SET_PHY`(TRAP-NET-19;IP 族必须在 INIT 前——INIT 读取并锁定寄存器值;SET_PHY 在 INIT 后重建物理链路);`CMD_PING_ENABLE` 不需显式调用。 +- **SPI 通信**:每次事务 `ch395f_spi_begin()/end()` 包裹;分频 8(PCB 约束);大数据走 SPI2 DMA(`CH395F_SPI_DMA_BUF_SIZE`=2048+4=2052,`ch395f.h:148`;历史值 1500/4100 已作废)。⚠ **TSL 规格偏离(N34 E12 登记)**:手册 §7.4 要求缓存区命令"长度与数据之间"留 TSL(读 0.4us/写 0.2us),本仓两个缓存区函数把命令头与数据打包进**同一 DMA 突发**(间隔=0)属规格偏离;**实测未发现危害**(460 轮/≈58MB 双向 SHA-256 仅 1 次未复现异常,"两段+TSL"变体与现状无差异)→ 依 TRAP-NET-20 铁律暂不改产品路径,偏离与证据登记于此、变体留作待用补丁(完整 A/B 见 `docs/网络/N34-自愈审计/实验方案.md` E12)。 +- **TCP 参数**(必须在 INIT_CH395 之前):重传 `SET_RETRAN_COUNT`(默认 12,最大 20)×`SET_RETRAN_PERIOD`(默认 500ms,最大 1000ms);KeepAlive 参数 500ms 倍数、IDLE>INTVL(TRAP-NET-06)。 +- **KeepAlive 定案链**:默认关闭 → 服务器侧应在 `SINT_STAT_CONNECT` 后开启(§5.56)→ 链路层死亡实测 +2s 即被重传(§5.17 8×200ms)覆盖、与 KeepAlive 无关 → **"链路在、对端静默"场景实测判死 ≈30s**(60s/5s/3,早于手册公式 75s,以实测为准;短参数 6s/2s/2 → ≈5.1s 证明参数生效;静默但存活对端不误杀)→ **已转正**:`handle_connect_event` LISTENING 分支开启 60s/5s/3;消费链路 TIM_OUT → `handle_timeout_event` QUARANTINE → relisten → 端口恢复(方法学与数据见 `docs/网络/N34-自愈审计/实验方案.md` E11/E13)。 +- **Socket 缓冲**:默认只给 Socket 0~3;Socket 4~7 必须 `open_socket` 前显式分配(TRAP-NET-02);现行固定布局 6 块/Socket。 +- **TCP 关闭重连**:直接 `ch395f_close_socket()`,不要先 `disconnect`;轮询 `GET_SOCKET_STATUS` **第 1 字节**等 `00H` 再 open;第 2 字节仅在 byte0=OPEN 时有意义(TRAP-NET-07)。 +- **SOCK_TIMEOUT**:`SINT_STAT_SOCK_TIMEOUT` 不应视为致命——记录日志后继续操作(消费侧 QUARANTINE 见 TRAP-NET-18/B1 链)。 +- **UDP 模式**:DesIP=0xFFFFFFFF → UDP Server(接收数据前 8 字节为信息头 `reserved[2] src_port[2] src_ip[4]`,§9.2.4 前 2 字节是数据长度);DesIP=具体 IP → UDP Client(无信息头)。发送后必须等 SENBUF_FREE(TRAP-NET-04)。 diff --git a/scripts/ram_baseline.tsv b/scripts/ram_baseline.tsv index f865b933..c98e9881 100644 --- a/scripts/ram_baseline.tsv +++ b/scripts/ram_baseline.tsv @@ -1,3 +1,3 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi 2026-10-12 209492 18332 1104 154184 227824 21.7 97864 74.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;console_log.o=4120;ch395f.o=4106;default_task.o=2736;sys_log_mcu.o=1924 -2026-10-13 211348 18756 1104 154232 230104 21.9 97912 74.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;default_task.o=2736;sys_log_mcu.o=1924 +2026-10-13 211456 18760 1104 154232 230216 22.0 97912 74.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;default_task.o=2736;sys_log_mcu.o=1924 diff --git a/simulator/sim/net_socket_sim.c b/simulator/sim/net_socket_sim.c index ed64c237..88a9bf97 100644 --- a/simulator/sim/net_socket_sim.c +++ b/simulator/sim/net_socket_sim.c @@ -835,6 +835,8 @@ uint32_t net_inet_addr(const char *cp) { * 出口参数:buf - 填充字符串 * 返回值:buf 指针(buf 为 NULL 时返回 NULL) * 限定条件:buf 非空且长度足够 + * 函数说明:NTOHL 后主机序数值的高字节是点分第一段(与 MCU 端同步修复, + * N34 E13 §6:原低段在前显示成字节反转) */ char *net_inet_ntoa(uint32_t addr, char *buf) { uint32_t naddr = NET_NTOHL(addr); @@ -844,10 +846,10 @@ char *net_inet_ntoa(uint32_t addr, char *buf) { } snprintf(buf, 16, "%lu.%lu.%lu.%lu", - (unsigned long)(naddr & 0xFF), - (unsigned long)((naddr >> 8) & 0xFF), + (unsigned long)((naddr >> 24) & 0xFF), (unsigned long)((naddr >> 16) & 0xFF), - (unsigned long)((naddr >> 24) & 0xFF)); + (unsigned long)((naddr >> 8) & 0xFF), + (unsigned long)(naddr & 0xFF)); return buf; } @@ -945,3 +947,40 @@ int net_getsockname(int sockfd, struct net_sockaddr *addr, int *addrlen) { s_net_errno = NET_OK; return 0; } + +/* + * 函数功能:获取 Socket 对端地址(IP + Port)(与 MCU 端同名 API——N34 E13 §6) + * 入口参数:sockfd - socket 描述符 + * addr - 输出:对端地址 + * addrlen - 输入输出:地址结构体长度 + * 出口参数:addr/addrlen - 填充 + * 返回值:0 成功,-1 失败(未连接 NOTCONN) + * 限定条件:socket 已连接 + * 函数说明:经 Winsock getpeername 读取;模拟器多连接后端下 conn fd 有真实对端 + */ +int net_getpeername(int sockfd, struct net_sockaddr *addr, int *addrlen) { + struct sockaddr_in wsaddr; + int wslen; + + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS || s_socks[sockfd].in_use == 0) { + s_net_errno = NET_ERR_BADF; + return -1; + } + if (addr == NULL || addrlen == NULL || + *addrlen < (int)sizeof(struct net_sockaddr_in)) { + s_net_errno = NET_ERR_INVAL; + return -1; + } + + wslen = sizeof(wsaddr); + if (getpeername(s_ws[sockfd], (struct sockaddr *)&wsaddr, &wslen) == SOCKET_ERROR) { + s_net_errno = NET_ERR_NOTCONN; + return -1; + } + + from_ws_addr(&wsaddr, addr); + *addrlen = (int)sizeof(struct net_sockaddr_in); + + s_net_errno = NET_OK; + return 0; +} diff --git a/test/board/ch395f_reuse_bench.c b/test/board/ch395f_reuse_bench.c index 830b1ac2..4846704e 100644 --- a/test/board/ch395f_reuse_bench.c +++ b/test/board/ch395f_reuse_bench.c @@ -4,7 +4,7 @@ * @details 直接调用 ch395f_* 命令,循环执行受控时序矩阵(延迟/关闭次序/载荷/Socket 号), * 每步打印芯片状态与中断寄存器,定位"关→开→监听→连接"复用时序中 * 芯片行为的确切规律。配套主机脚本 test/host/ch395f_reuse_host.py。 - * 定案结论与实验矩阵演进 → CH395F_Trap_Records.md Trap 25。 + * 定案结论与实验矩阵演进 → CH395F_Trap_Records.md TRAP-NET-20。 * * 存档状态:本文件在主分支**不注册编译**(未加入 uvprojx),仅作实验工具存档; * 运行方法:在实验分支(如 n30-chip-bench)把本 .c 注册进 uvprojx Test 组、 @@ -99,7 +99,7 @@ static int bench_chip_init(void) { return -1; } ch395f_reset(); - /* 等 PHY link(Trap 09:link up 前 open 必失败;B9 修正:判据是 != DISCONN, + /* 等 PHY link(TRAP-NET-09:link up 前 open 必失败;B9 修正:判据是 != DISCONN, * 旧写法 !=0 对 01H=DISCONN 也成立) */ for (int i = 0; i < 100; i++) { if (ch395f_get_phy_status() != CH395F_PHY_DISCONN) { @@ -151,7 +151,7 @@ static void bench_round(unsigned idx) { idx, (unsigned)sock, (unsigned)port, (unsigned)v->delay_ms, (unsigned)v->tx_len, (unsigned)v->close_mode, (unsigned)HAL_GetTick()); - /* 1. open + listen(Trap 09:close 后需延时重开,失败重试 ×3) */ + /* 1. open + listen(TRAP-NET-09:close 后需延时重开,失败重试 ×3) */ ch395f_set_proto_type(sock, CH395F_PROTO_TYPE_TCP); ch395f_set_sour_port(sock, port); uint8_t ost = CH395F_ERR_SUCCESS; @@ -288,7 +288,7 @@ static void bench_round(unsigned idx) { } } - /* 3. TX 探针(1KB 分块,Trap 20) */ + /* 3. TX 探针(1KB 分块,TRAP-NET-16) */ if (v->tx_len > 0U) { uint16_t sent = 0; while (sent < v->tx_len) { @@ -345,7 +345,7 @@ static void bench_round(unsigned idx) { ch395f_close_socket(sock); } - /* 5. 关闭后拆链观察:1s 内每 50ms 记状态 + 中断(Trap 14 滞后中断取证) */ + /* 5. 关闭后拆链观察:1s 内每 50ms 记状态 + 中断(TRAP-NET-12 滞后中断取证) */ for (int k = 0; k < 20; k++) { uint16_t glob = ch395f_get_glob_int_status_all(); uint8_t late = 0; @@ -618,7 +618,7 @@ static void bench_e3(void) { * A:读空 RX -> CLOSE -> 排空本 socket 事件 -> 轮询等 byte0=00H * B:CLEAR_RECV_BUF -> CLOSE -> 排空事件 -> 等 CLOSED * C:什么都不做 -> CLOSE -> 等 CLOSED - * 每轮先"污染会话"(主机连上发 3000B 后 RST,Trap 23 形态),复位后立即 + * 每轮先"污染会话"(主机连上发 3000B 后 RST,TRAP-NET-18 形态),复位后立即 * open+listen,主机立即上传 4096B 图案载荷;芯片侧按图案自证完整性 * (PASS 口径 = 消费者在 DISCONNECT/TIM_OUT 前实际读到的字节数与图案)。 */ #define E4_SOCK 3U diff --git a/test/board/ch395f_test_task.c b/test/board/ch395f_test_task.c index 514588e1..5ce02b06 100644 --- a/test/board/ch395f_test_task.c +++ b/test/board/ch395f_test_task.c @@ -620,9 +620,9 @@ static void phase2_tc202(void) } /* =================================================================== - * TC-NET-204(硬件层):Trap 07 定案对照——三种关闭次序 A/B/C + * TC-NET-204(硬件层):TRAP-NET-07 定案对照——三种关闭次序 A/B/C * - * 背景:Trap 07(2025-07)断言"`disconnect → close` 会让 Socket 卡 `FIN_WAIT_2` + * 背景:TRAP-NET-07(2025-07)断言"`disconnect → close` 会让 Socket 卡 `FIN_WAIT_2` * 数分钟、无法重开,因为 close 不再发 RST"。该断言有两处可疑: * ① 手册无 RST 语义——§5.35 只说"关闭 Socket 前 CH395 会自动断开 TCP 连接", * 全文 RST 只出现在错误码 CH395_ERR_RST 与 RST/RSTI 引脚; @@ -636,12 +636,12 @@ static void phase2_tc202(void) * * 受控对照(每模式 x PHASE204_LOOP_COUNT 轮,同一 Socket、同一 PC 回显服务): * 模式 A:close only(现行做法) - * 模式 B:tcp_disconnect → close(Trap 07 声称会卡死的序列) + * 模式 B:tcp_disconnect → close(TRAP-NET-07 声称会卡死的序列) * 模式 C:tcp_disconnect only,不 close(检验 FUN_PARA bit3=1 下芯片会不会自己关) * 观测量: * ① 回到 SOCKET_CLOSED(byte0=00H) 的耗时;② 该瞬间 byte1 的读数(验证"无意义"口径) - * ③ 重开 + 重连 + 回显是否成功(Trap 07 的"无法重新打开") - * ④ 重开会话首次读中断是否命中上一条连接残留的 DISCONNECT(Trap 14 轴) + * ③ 重开 + 重连 + 回显是否成功(TRAP-NET-07 的"无法重新打开") + * ④ 重开会话首次读中断是否命中上一条连接残留的 DISCONNECT(TRAP-NET-12 轴) * ⚠ 关闭与重开之间**不主动清中断**(与 2025-07 现场一致),残留事件由重开后的 * 首次中断读取现场暴露(读即清,无法窥视)。 * =================================================================== */ @@ -659,7 +659,7 @@ static void phase2_tc202(void) * 入口参数:sock - Socket 号;lport - 本地端口;mode/round - 仅用于日志定位 * 返回值:1=会话成功(含回显比对通过) 0=任一步失败 uint8_t * 说明:等待 CONNECT 期间读到的其它中断(含上一条连接残留的 DISCONNECT) - * 现场打印,用于 Trap 14"滞后锁存"取证。 */ + * 现场打印,用于 TRAP-NET-12"滞后锁存"取证。 */ static uint8_t tc204_session(uint8_t sock, uint16_t lport, uint8_t mode, uint8_t round) { uint8_t tx[64]; @@ -767,7 +767,7 @@ static uint8_t tc204_session(uint8_t sock, uint16_t lport, uint8_t mode, uint8_t * 入口参数:sock - Socket 号;mode - 0=A close / 1=B disconnect→close / 2=C disconnect only * round - 日志定位 * 返回值:1=观察窗内 byte0 回到 CLOSED 0=未回到 uint8_t - * 说明:关闭与轮询之间不读中断(读即清会抹掉 Trap 14 的滞后事件现场)。 */ + * 说明:关闭与轮询之间不读中断(读即清会抹掉 TRAP-NET-12 的滞后事件现场)。 */ static uint8_t tc204_close_seq(uint8_t sock, uint8_t mode, uint8_t round) { /* 状态采样里程碑(ms):抓 byte0/byte1 的演变 */ @@ -863,7 +863,7 @@ static void phase2_tc204_once(uint8_t mode, uint8_t round) TEST_CHECK(!closed, "TC204 m2 r%u stays OPEN after disconnect (FUN_PARA bit3)", (unsigned)round); if (!closed) { - /* Trap 07 的"无法重新打开"现场:不 close 直接 open,看芯片返回什么 */ + /* TRAP-NET-07 的"无法重新打开"现场:不 close 直接 open,看芯片返回什么 */ uint8_t r = ch395f_open_socket(sock); DBG_INFO("TC204 m2 r%u open-without-close ret=0x%02X", (unsigned)round, r); /* 收尾:显式 close 并测耗时(证明 close 才是把 Socket 拉回 CLOSED 的动作) */ @@ -874,7 +874,7 @@ static void phase2_tc204_once(uint8_t mode, uint8_t round) (unsigned)round, (unsigned)PHASE204_CLOSE_WAIT_MS); } - /* 3. 重开 + 重连 + 回显(Trap 07 的"无法重新打开"判据;Trap 09 沉降 200ms) */ + /* 3. 重开 + 重连 + 回显(TRAP-NET-07 的"无法重新打开"判据;TRAP-NET-09 沉降 200ms) */ HAL_Delay(200); s2 = tc204_session(sock, (uint16_t)(lport + 64U), mode, round); TEST_CHECK(s2, "TC204 m%u r%u reopen+reconnect+echo OK", (unsigned)mode, (unsigned)round); @@ -915,7 +915,7 @@ static void phase2_tc204(void) * DISCONNECT —— 预留兼容路径(SYN_SENT 阶段实测基本不出现) * UNREACH(全局位) —— ARP 失败、对端不在线(仅作备注,不影响判定) * - * 已知陷阱(Trap 14):上一会话 close 的 DISCONNECT 会滞后锁存在 + * 已知陷阱(TRAP-NET-12):上一会话 close 的 DISCONNECT 会滞后锁存在 * SINT_SN(wait-CLOSED 的 STATUS 查询不清中断)。防御双保险: * ① 步 2 前预清两级中断; * ② 判负向后用 GET_SOCKET_STATUS 反核实,ESTABLISHED 即改判环境错误。 @@ -944,7 +944,7 @@ static void phase2_tc203(void) "TC203 step1 cfg proto/ip/port"); if (ch395f_get_cmd_status() != CH395F_ERR_SUCCESS) return; - /* 预清陈旧事件(Trap 14):上一会话 close 的 DISCONNECT 可能滞后锁存 + /* 预清陈旧事件(TRAP-NET-12):上一会话 close 的 DISCONNECT 可能滞后锁存 * 在 SINT_SN 中(wait-CLOSED 轮询的 STATUS 是纯查询、不清中断), * 若不清除,本用例首次读中断会误吃旧事件导致负向误判。 */ (void)ch395f_get_glob_int_status_all(); @@ -996,7 +996,7 @@ static void phase2_tc203(void) elapsed = HAL_GetTick() - tick; - /* 反核实(Trap 14 第二道防线):负向事件与实际状态矛盾时以状态为准。 + /* 反核实(TRAP-NET-12 第二道防线):负向事件与实际状态矛盾时以状态为准。 * 若收到"负向事件"但 socket 实际已 ESTABLISHED,说明刚才读到的是 * 上一会话遗留的陈旧 DISCONNECT,真实结果是连接成功 —— 环境错误。 * ⚠ §5.22:byte0 只有 00H(CLOSED)/05H(OPEN),TCP 态在 **byte1**; @@ -1061,7 +1061,7 @@ static void phase2_run(void) DBG_INFO("--- TC-NET-202: close/reconnect x%d ---", PHASE2_LOOP_COUNT); phase2_tc202(); - DBG_INFO("--- TC-NET-204: close A/B/C control (Trap 07) x3 modes x%d ---", + DBG_INFO("--- TC-NET-204: close A/B/C control (TRAP-NET-07) x3 modes x%d ---", PHASE204_LOOP_COUNT); phase2_tc204(); @@ -1702,7 +1702,7 @@ static void phase15_run(void) { /* 二段关闭实证(2026-10-13 本 TC 二跑定案):LISTEN 态 Socket 的 * CLOSE_SOCKET 返 SUCCESS 但只拆监听(TCP 01->00),byte0 恒 05; - * 再发一次 CLOSE 才落 00。生产 net 层靠 Trap 09 open 重试吸收此行为, + * 再发一次 CLOSE 才落 00。生产 net 层靠 TRAP-NET-09 open 重试吸收此行为, * sock_reset 状态机在 WAIT_CLOSE 阶段自行补发(见 ch395f.c)。 */ uint8_t close_st = ch395f_close_socket(sn); TEST_CHECK(close_st == CH395F_ERR_SUCCESS, diff --git a/test/board/net_latency_test.c b/test/board/net_latency_test.c index a54e15da..7d21cdf0 100644 --- a/test/board/net_latency_test.c +++ b/test/board/net_latency_test.c @@ -1132,7 +1132,7 @@ void net_latency_phase13_run(int32_t sockfd) * ui_loop uiTask 主循环实测周期(本地,不需要 peer,120 样本) * prod_cycle_mcu ui_net_sink 处理一帧 LCDMEM 的 MCU 净成本 * (取 1bpp 位图 + 组帧 + net_send 队列/芯片;不含 10ms 拾取拍) - * relisten peer 断开后自动重监听耗时(量化 D10:Trap 09 重试含 osDelay(500)) + * relisten peer 断开后自动重监听耗时(量化 D10:TRAP-NET-09 重试含 osDelay(500)) * * 端口分工:peer 走 :7003(生产 socket,uiTask 持有),本 Phase 不碰 :8080。 * 段阈值(首轮 M3 实测后按 §4.2 重锁): diff --git a/test/board/net_test_task.c b/test/board/net_test_task.c index f3bdbc67..169c27b2 100644 --- a/test/board/net_test_task.c +++ b/test/board/net_test_task.c @@ -293,7 +293,7 @@ static void phase7_test_timeout(void) /* TC 704:多 Socket 并发 TCP Client。 * Socket 0~7 由索引寻址 + 缓冲显式分配即可全部使用(FUN_PARA bit0 是保留位, - * 与 Socket 个数无关,见 Trap 18「定案更正」);本用例同时打开 Socket 0~7, + * 与 Socket 个数无关,见 TRAP-NET-15);本用例同时打开 Socket 0~7, * 各自连 PC echo server,每路独立收发 PHASE7_EXCH_SIZE 字节回显并比对, * 验证 8 个 Socket 均能独立工作。依赖 PC 端 tcp_server 支持并发连接。 */ #ifndef PHASE7_MULTI_COUNT @@ -539,7 +539,7 @@ static void phase8_run(int32_t sockfd) { uint32_t sent = 0; while (sent < PHASE8_FILE_SIZE) { /* 每次填充一个滑动窗口(≤ s_tx_buf 容量);net_send 内部按硬件发送 FIFO - * (NET_SEND_CHUNK_MAX=1KB,见 Trap 20) 自动拆分写入,无需此处逐 1KB 分块 */ + * (NET_SEND_CHUNK_MAX=1KB,见 TRAP-NET-16) 自动拆分写入,无需此处逐 1KB 分块 */ uint32_t chunk = (PHASE8_FILE_SIZE - sent) > (uint32_t)TEST_BUF_SIZE ? (uint32_t)TEST_BUF_SIZE : (PHASE8_FILE_SIZE - sent); phase8_fill(s_tx_buf, chunk, sent); diff --git a/test/host/README.md b/test/host/README.md index f9929512..f5b53cad 100644 --- a/test/host/README.md +++ b/test/host/README.md @@ -22,6 +22,15 @@ | `ftp_speed_test.py` | FTP 传输速度测试 | | `ftp_capture.py` | FTP 流量抓取 | | `ftp_regression.py` | FTP 回归套件 | +| `ftp_abort_probe.py` | FTP 中止场景探针(TRAP-NET-18;RST→450 / FIN→226,`--check-abort`;2026-10-13 自 `MDK-ARM/` 迁入) | +| `ftp_integrity_test.py` | FTP 上传 SHA-256 完整性多轮回归 | +| `n30_repro.py` | 背靠背 STOR 复现器(TRAP-NET-20 常备回归腿) | +| `relisten_stress.py` | 监听快速复连压力(TRAP-NET-17 / N34 E1:`--port --n --gap-ms [--hold-ms]`) | +| `e11_keepalive_probe.py` | keepalive 黑洞判死探针(N34 E11) | +| `e6_soak.py` | E6 24h 挂机驱动(常态 + 每小时/每 4h E1 压测注入;判据读串口 `U15 probe` 计数) | +| `pasv_probe.py` | PASV 数据通道时序探针 | +| `ch395f_reuse_host.py` | Socket 复用/中止形态 PC 对端(bench 配套) | +| `ftp_auth_bench.py` | FTP 鉴权/锁定台架腿(N-19) | | `iec104_master.py` | **IEC 60870-5-104 主站客户端(B6/B7)**:连设备 :2404 跑 STARTDT → 总召(actcon/四组背景扫描/actterm)→ 经 C_SE 两段写临时压低 OC1 定值确定性触发 YX16 变位(COT=3)→ 恢复 → YK11 两段式 → C_RD 读回;**B7 持久化模式 `--b7-pre/--b7-post/--b7-mark/--b7-check`**(中间 ST-LINK 复位):YK12 两段投退→保护退出/联动、定值与压板态跨复位保持(含非默认 7.5 往返);纯标准库,输出纯 ASCII 可直接重定向归档为台架证据(判据 exit 0/1/2) | ### 时间 / 时延 @@ -106,7 +115,10 @@ - **PYTHONPATH/路径敏感**:脚本按仓库根相对路径读写,**从仓库根执行** - 本目录**被 pre-commit 门禁的 cppcheck / 代码规范排除**(`test/` 豁免); 但各模块的 `run_unit_test.py` 自身**作为独立钩子**参与门禁(见上,共 7 个) -- 台架探针族(TCP 复连 / FTP 中止等)在 `MDK-ARM/*.py`,与本目录分工不同 +- 台架探针族分工(2026-10-13 起):**N34/Trap 常备回归腿在本目录**(`relisten_stress` / + `ftp_abort_probe` / `n30_repro` / `e02_probe` / `e6_soak` 等);`MDK-ARM/*.py` 只余 + 历史一次性探针(`ftp_freeze_probe` / `tcp_reconnect_probe` / `ftp_retr_abort_probe` / + `bench_verdict`) - 全部主机侧单测(`watchdog/`、`sys_clock/`、`cfg_file/`、`phasor/`、`adc_input/`、 `vol_lock/`、`time_sync/`)都需要本机装有 MSVC(VS2019/2022/2026 的 C++ 工具链); 无则脚本报环境错误(退出码 2) diff --git a/test/host/ch395f_reuse_host.py b/test/host/ch395f_reuse_host.py index 7c0c8e63..b5c969dd 100644 --- a/test/host/ch395f_reuse_host.py +++ b/test/host/ch395f_reuse_host.py @@ -2,7 +2,7 @@ # 用法: python ch395f_reuse_host.py [--host IP] [--cycles N] [--mode legacy|e4] # legacy: 按 idx 轮转连接 5000+idx 端口,记录收发结果与时刻。 # e4: N34 E4 sock_reset A/B/C 矩阵配套(2026-10-13):每轮两次连接—— -# ①污染:连上发 3000B 后 RST(SO_LINGER 0,Trap 23 中止形态); +# ①污染:连上发 3000B 后 RST(SO_LINGER 0,TRAP-NET-18 中止形态); # ②载荷:立即重连发 4096B 图案(pat[i]=(seed+i)&0xFF, seed=(rg*97+13)&0xFF)后 FIN。 # 端口 5100+rg,rg=0..(3*rounds-1),变体顺序 A/B/C(与固件 bench 表一致)。 import argparse @@ -103,7 +103,7 @@ def e4_round(host, rg, rounds_per_variant, first_budget): s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0)) except OSError: pass - s.close() # RST: abort mid-session like ftp_abort_probe (Trap 23 form) + s.close() # RST: abort mid-session like ftp_abort_probe (TRAP-NET-18 form) time.sleep(0.05) s = connect_retry(host, port, 20) diff --git a/test/host/e02_probe.py b/test/host/e02_probe.py index f6268f36..4b62a08e 100644 --- a/test/host/e02_probe.py +++ b/test/host/e02_probe.py @@ -14,7 +14,7 @@ :8888 应能再接受新连接(3s 重试窗内);新连接发完整 8 字节 TIME 包 → 设备应落 "wall clock updated"。 -⚠ 定案口径(Trap 07 定案更正 2026-10-11 + 实验方案 E13 §5):CH395F close +⚠ 定案口径(TRAP-NET-07 定案更正 2026-10-11 + 实验方案 E13 §5):CH395F close **无 RST 语义**。若对端内核存活(app 挂起但 socket 不关,内核自动 ACK FIN), 芯片停在 FIN_WAIT_2、监听恢复受限于对端 close——探针第 3 步的 close 正是 模拟"对端配合"这一可恢复边界;不配合场景的兜底是芯片 TIMEOUT/keepalive @@ -67,7 +67,7 @@ def main(): time.sleep(remain) # 3b. 模拟守规矩对端:设备已发 FIN(drop_conn),本端 close 配合拆链。 - # CH395F close 无 RST 语义(Trap 07 定案更正):不配合的内核存活对端 + # CH395F close 无 RST 语义(TRAP-NET-07 定案更正):不配合的内核存活对端 # 会把芯片停在 FIN_WAIT_2,监听恢复受限于对端 close——不在本探针判据内。 try: ts.close() diff --git a/test/host/e6_soak.py b/test/host/e6_soak.py new file mode 100644 index 00000000..3fec8fe1 --- /dev/null +++ b/test/host/e6_soak.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""E6 24h soak driver (U15 mismatch frequency, N34 experiment plan E6). + +Protocol (docs/网络/N34-自愈审计/实验方案.md §E6): + 24h normal operation + periodic E1 stress injection: + - every hour : relisten_stress :7003 n=10 gap=100 (UI-like churn) + - every 4th hour : full E1 (:7003 n=50 gap=0 + :21 n=50 gap=100) +Device-side confirmed hits are counted by the NET_U15_PROBE firmware and +appear in the serial log as "U15 probe: sockN rx=... hits=N" (first hit +immediately, then <=1/hour). Read the verdict from serial.log afterwards. + +Usage: python e6_soak.py [--hours 24] +Exit: 0 = soak completed; nonzero = driver error (device untouched). +""" +import argparse +import os +import subprocess +import sys +import time + +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +STRESS = os.path.join(ROOT, "test", "host", "relisten_stress.py") + + +def log(msg): + print("[%s] %s" % (time.strftime("%m-%d %H:%M:%S"), msg), flush=True) + + +def burst(args_list, tag): + t0 = time.time() + try: + r = subprocess.run([sys.executable, STRESS] + args_list, + capture_output=True, text=True, timeout=900) + out = (r.stdout or "") + (r.stderr or "") + tail = [ln for ln in out.splitlines() if "ok=" in ln or "latency" in ln] + dt = time.time() - t0 + log("%s rc=%d %.0fs %s" % (tag, r.returncode, dt, " | ".join(tail[-2:]))) + except Exception as e: # driver hiccup must not abort the soak + log("%s ERROR %r" % (tag, e)) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--hours", type=int, default=24) + a = ap.parse_args() + + log("E6 soak start: %d h, hourly :7003x10, every-4h E1 full" % a.hours) + t_start = time.time() + for h in range(1, a.hours + 1): + # sleep first: hour boundary cadence + remain = t_start + h * 3600.0 - time.time() + if remain > 0: + time.sleep(remain) + burst(["--port", "7003", "--n", "10", "--gap-ms", "100"], "hour%02d :7003x10" % h) + if h % 4 == 0: + burst(["--port", "7003", "--n", "50", "--gap-ms", "0"], "hour%02d E1 :7003x50g0" % h) + burst(["--port", "21", "--n", "50", "--gap-ms", "100"], "hour%02d E1 :21x50g100" % h) + log("E6 soak done: %.1f h elapsed" % ((time.time() - t_start) / 3600.0)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/MDK-ARM/ftp_abort_probe.py b/test/host/ftp_abort_probe.py similarity index 96% rename from MDK-ARM/ftp_abort_probe.py rename to test/host/ftp_abort_probe.py index bdfeff75..83790823 100644 --- a/MDK-ARM/ftp_abort_probe.py +++ b/test/host/ftp_abort_probe.py @@ -12,7 +12,7 @@ # [--graceful] [--check-abort] # Output: TS / ABORT sent= ctrl= alive= followup= # abort5xx= (only with --check-abort: RST expects 4xx/5xx on the -# aborted STOR = no false 226 (Trap 23 hardening); graceful FIN expects 2xx +# aborted STOR = no false 226 (TRAP-NET-18 hardening); graceful FIN expects 2xx # = protocol boundary, still reported as success) # SUMMARY rounds= pass= fail= # Exit 0 all rounds pass, 1 otherwise. @@ -75,7 +75,7 @@ def main(): help="close the data connection with FIN instead of RST") ap.add_argument("--check-abort", action="store_true", help="also assert the aborted STOR's control reply: RST -> 4xx/5xx " - "(no false 226, Trap 23 hardening); graceful FIN -> 2xx (boundary)") + "(no false 226, TRAP-NET-18 hardening); graceful FIN -> 2xx (boundary)") a = ap.parse_args() npass = nfail = 0 diff --git a/tools/arch_diagram/gen_net_instability_svg.py b/tools/arch_diagram/gen_net_instability_svg.py index ee775945..70aa6ae0 100644 --- a/tools/arch_diagram/gen_net_instability_svg.py +++ b/tools/arch_diagram/gen_net_instability_svg.py @@ -49,13 +49,13 @@ C_GRAY = "#94a3b8" # ----------------------------- 数据(改这里) ----------------------------- # (问题标题, 问题副标, [问题芯片], 拆法标题, [拆法芯片]) RINGS = [ - ("① 门铃会丢", "Trap 22:丢中断", + ("① 门铃会丢", "TRAP-NET-17:丢中断", ["症状:UI 复连永久拒连、FTP 第 29 轮自己死掉,全程不报一个错", "机制:快速复连时 INT# 门铃哑火,netTask 只靠门铃干活 → 状态静默滞留"], "100ms 巡逻对账", ["单一 reconciler:判据读驱动状态缓存(E2 背书 18.1µs),分级去抖", "门铃丢了不要紧,每 100ms 亲自数一遍人头"]), - ("② 门铃总闸看一眼就清零", "Trap 25 B3", + ("② 门铃总闸看一眼就清零", "TRAP-NET-20 B3", ["症状:别的 Socket 的 CONNECT/DISCONNECT 从此永久失踪", "机制:全局中断寄存器整体读即清,open/relisten 丢弃式读一遍=拔全楼门铃线"], "总闸唯一读者", @@ -89,7 +89,7 @@ RINGS = [ ["症状:FIN_WAIT_2 长尾,监听恢复受限于对端 close(E-02 定案)", "机制:芯片 close 只发 FIN;对端内核存活但应用挂死 = 回 ACK 却不起身"], "知道他不轰人,就别指望他轰人", - ["定案写入 Trap 07:兜底靠查岗(keepalive/TIMEOUT),不靠 RST", + ["定案写入 TRAP-NET-07:兜底靠查岗(keepalive/TIMEOUT),不靠 RST", "net_drop_conn 本地拆除 + reconciler 形态③④ 认领僵尸态"]), ("⑧ 垃圾不清越扫越脏", "残渣风暴态", ["症状:同一份固件故障率 2%~54% 摆动(E8 实测),看起来像「玄学」", @@ -104,7 +104,7 @@ SUM_LEFT = ["七环叠加 =「玄学不稳」", SUM_RIGHT = ["逐环拆掉 = 稳(N34 阶段 0~3 已实施)", "不依赖门铃 · 唯一主人 · 永不冻结 · 100ms 对账 · 开张前排渣——台架回归全绿"] -FOOT = ("证据链:docs/问题记录/CH395F_Trap_Records.md(Trap 07/09/14/17/22/23/25) · " +FOOT = ("证据链:docs/问题记录/CH395F_Trap_Records.md(TRAP-NET-07/-09/-12/-17/-18/-20) · " "docs/网络/N34-自愈审计/实验方案.md(E1~E13) · 修复落地见 docs/网络/N34-自愈审计/实施方案.md")