TC-NET-604 根因闭环:芯片 RX 窗口冻结角(Trap 21),MSS=512 裕量方案证伪回退,缓冲布局与 MSS 解耦 + 超限读金丝雀

- 根因:CH395F 片内 TCP 状态机角(WCH 官方 FAQ 留档,W5500 同类):窗口=接收缓冲剩余空间,
  缓冲恰好填满 + 排空时序交叠后芯片 RX 静默冻结(不 RST、不自恢复),触发模式 = 同连接前序突发 +
  新突发 + 任务负载;驱动超限读丢弃与 L3 pending 均经插桩台架排除
- MSS=512 裕量实验证伪(原序列 10/10 仍停摆,且新连接 4096B 确定性停摆恒剩 64B)→ 维持 1024
- 缓冲块布局(RX 4/TX 2 块)与 SPI DMA 缓冲(接收缓冲+4)与 MSS 解耦,杜绝改 MSS 连带缩小布局
- ch395f_read_recv_buf 超限守卫改为 OVERSIZE 留痕(原静默 return 会把窗口锁死 0 且无从诊断)
- 文档:Trap 21 全记录(含 MSS=512 负结果/64B 残留/FAQ 引文/触发模式);N-1 改写(残留决策项 =
  lftpd 数据通道停摆预算 30s→10-15s 待用户决策);E-02 STOR 预测机制修正;延迟说明基线免重锁注记
- 台架复验:新连接 4096B 31ms PASS(4x1024 粒度)、竞争序列停摆复现(芯片角,已文档化)、
  停摆后重连 520ms 恢复(auto-relisten + 回显正常);芯片行为参数零变化 → M3 基线全部继续有效
This commit is contained in:
2026-09-22 18:57:48 +08:00
parent 7ff2958b3b
commit 93bae35e21
7 changed files with 80 additions and 19 deletions
+39 -2
View File
@@ -519,7 +519,7 @@ NET 层 TCP Client(阶段 7)`net_connect()` 返回 0,但 `net_poll()` 报
大文件/批量发送(如阶段 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**(由 MSS=1024 推导,见 `ch395f.h`)。当某次调用 `net_send/sendto` 传入 >1KB 数据,驱动会在 `send_ready` 置位后一次性 `WRITE_SEND_BUF(2KB/4KB)`,直接**写穿 1KB 发送 FIFO**(超出部分覆盖/丢失或破坏 FIFO 指针),导致后续数据错位。原代码误把"SPI DMA 缓冲上限(2KB)"或"软件 API 缓冲(4KB)"当成硬件发送缓冲上限。
`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`。
@@ -528,7 +528,7 @@ NET 层 TCP Client(阶段 7)`net_connect()` 返回 0,但 `net_poll()` 报
4. 接收方向不受影响:`net_recv_locked` 只读 `ch395f_get_recv_len`(≤ 接收 FIFO 2KB)并受调用方 `len` 截断,不会写穿。
### 注意事项
- 该上限由 `CH395F_TCP_MSS` 推导,若将来调大 MSS 使发送缓冲 >2KB,则 `NET_SEND_CHUNK_MAX` 自动跟随(仍取硬件缓冲与 DMA 载荷的较小者),无需改此处逻辑。
- 该上限随 `CH395F_SEND_BLOCKS`(2026-09-22 起固定 2 块=1KB,与 MSS 解耦,见 Trap 21);若将来发送缓冲改变,`NET_SEND_CHUNK_MAX` 自动跟随(仍取硬件缓冲与 DMA 载荷的较小者),无需改此处逻辑。
- 调用方(阶段 8、FTP `lftpd`)仍可按任意长度调用 `net_send`,实际分块由驱动保证 ≤ 硬件 FIFO。
### 对应文件
@@ -536,3 +536,40 @@ NET 层 TCP Client(阶段 7)`net_connect()` 返回 0,但 `net_poll()` 报
### 发现时间
阶段 8 大文件传输实现,2026-08-26。
## Trap 21:接收缓冲"恰好填满 + 排空"窗口冻结(TC-NET-604)
### 现象
单条 TCP 连接在同连接前序突发(64B×10 + 1460B)之后,再发 >2KB 突发(4096B):前 ~2KB 正常回显,其余数据芯片不再接收/确认,对端重传 10 秒无效;连接不 RST(芯片自身发送侧干净,重传耗尽拆连逻辑不触发),芯片接收缓冲全程为空(无 RECV_OK 事件),窗口值不再更新——"静默冻结"。对端断开重连(新 Socket)后恢复正常。复现:F1 测试构建(Phase 6/9/11)+ `test/ch395f_socket_test.py tcp_client` 全序列,10/10(2026-09-22 台架)。
### 根因
CH395F 片内 TCP 状态机的角(芯片固件,MCU 侧无法修补),与"窗口 = 接收缓冲剩余空间"的硬连线模型共同构成触发结构:
1. **结构性前提**:窗口=剩余空间(手册 L1281)→ 守规矩的对端永远把窗口灌满 → 每个稳态轮次缓冲**恰好填满**(2048/2048,窗口=0)是结构性必达状态,无法通过几何裕量规避。
2. **冻结机制**:缓冲恰好填满的状态与"在途段接收 / 窗口重通告 / MCU 排空"时序交叠时,芯片接收路径状态机卡死——停止对后续段的确认与接收(数据滞留线上、对端重传),缓冲保持空、窗口不更新、连接保持。WCH 在 CH395 系列官方 FAQ 中留档了同类现象:"接收缓冲区需要大于 2 倍设置的 MSS 值,否则……若对端设备不会根据 tcp 窗口来分包发送数据时,有可能会造成 CH395 收不到数据且 TCP 窗口不更新现象"(www.cnblogs.com/sw111/p/18750601,2025-03)。WIZnet W5500(同类:每 Socket 小型缓冲 + 片内 TCP + 窗口=剩余空间)官方文档同为"缓冲满则挂起连接等待空闲空间"模型,zero-window 停摆是该类芯片的经典病理(docs.wiznet.io W5500 TCP Application 文档;Wireshark 论坛 49849 实例)。
3. **触发模式(实测,均为确定性而非随机竞态)**:
- MSS=1024:新连接 4096B 突发(4×1024 段)正常(31ms 全通);仅"同连接前序突发 + 新突发 + 任务负载(netTask 队列竞争)"模式触发(10/10)。
- MSS=512:新连接 4096B 突发(8×512 段,4 段装不进 2KB 缓冲)**确定性停摆**,恒剩 64B 未达(4032/4096,2/2 同值)——满周期内段数越多越易触发,且一旦触发必现。
- 单轮无溢出突发(≤2KB,如 1460B)在任何几何下均正常。
4. **排除项**:① 驱动层"超限读静默丢弃"排除——SPI DMA 缓冲 = 2×MSS+4 = 2052B(载荷 2048 = 整个接收缓冲),整缓冲读单事务完成,失败运行中丢弃事件 0 次(Trap 13 所记 1500B DMA 缓冲为 8-Socket 改造前旧值,改造后扩至 2052B);② L3 pending 化排除(切片构建 A/B 行为逐字节一致);③ "RX > 2×MSS"裕量假设排除——MSS 1024→512 实验(2KB=4×MSS、1KB 裕量)既未消除原复现序列的停摆(10/10),又新增新连接停摆 → 已回退 1024。
### 解决(2026-09-22)
芯片侧固件缺陷,MCU 无法修补;策略 = 低风险几何 + 可观测 + 断开重试:
1. **MSS 维持 1024**(布局允许最大值;实测 1024 下新连接 4096B 正常、每满周期段数少 = 触发概率最低)。MSS=512 裕量方案被证伪并回退。
2. **缓冲块布局与 MSS 解耦**(`ch395f.h`):`CH395F_RECV_BLOCKS=4` / `CH395F_SEND_BLOCKS=2` 固定(此前由 MSS 推导,改 MSS 会连带缩小布局);`CH395F_SPI_DMA_BUF_SIZE = 接收缓冲 + 4`(此前 2×MSS+4,改 MSS 会缩到装不下整缓冲)。
3. **超限读金丝雀**(`ch395f_read_recv_buf`):正常路径不可能发起超过接收缓冲(2KB)的读;守卫命中即留痕 `read_recv_buf OVERSIZE`(原为静默 return,会把窗口锁死在 0 且无从诊断)。
4. **生产缓解(决策项,未实施)**:冻结不自恢复(对端重传 10s 无效;芯片无限期保持连接)。唯一恢复 = 断开受影响连接(释放芯片 per-socket 状态)+ 对端重试(新连接成功率高;台架实测停摆后重连回显正常)。现 lftpd 数据路径预算 `LFTPD_TOTAL_BUDGET_MS=30000`(每次读 30s)即实际停摆超时——若将**数据通道**预算单独缩短(10–15s,与控制行预算分离),可把停摆暴露时间从 30s 压缩到客户端重试路径;代价 = 慢速但健康的传输也会被中断(机器客户端不受影响,人工客户端受影响)→ 已列 `docs/待处理内容.md` N-1 待决策。
### 注意事项
- 复现模式 = "前序突发 + 新突发 + 任务负载";生产中同数据连接带前序部分传输的 FTP STOR/RETR 是主要风险场景;纯新连接大突发(4×1024)在 MSS=1024 下不触发。
- 冻结是静默的:无 RST、无芯片中断、MCU 侧只见"缓冲空 + 无 RECV_OK + 连接存活"(与正常空闲完全同貌)——MCU 无直接信号,只能靠应用层数据预算(lftpd 30s / 客户端自身超时)发现。
- E-02 审计"STOR 方向缓冲满 + 重传耗尽 → 必断"的预警方向成立(STOR 方向 = 致命),机制修正为本条 RX 冻结(非 RST 拆连)。
- WCH FAQ"发送缓冲必须大于 MSS"为严格不等式;本设计发送缓冲 = MSS(1KB = 1024)取等值——台架实测芯片在缓冲边界可正常成包(L3/L8 基线在 1KB 分块写入下全部正常),该条按保守表述处理,非运行约束。
- 本条的冻结是确定性触发序列缺陷(特定段数/段长/前序状态组合),非低概率随机竞态——发现新停摆模式时先核对"同连接突发结构",再怀疑 MCU 侧。
### 对应文件
`Drivers/BSP/CH395F/ch395f.h`(布局解耦 + MSS=1024 维持);`Drivers/BSP/CH395F/ch395f.c`(OVERSIZE 金丝雀);`Drivers/BSP/NET/net_socket.c`(MSS 下发注释);`test/ch395f_socket_test.py`(复现:tcp_client 全序列);`MDK-ARM/probe_mss.py`(MSS/芯片 TX 分包探针,本地工具,不提交)。
### 发现时间
2026-09-22(TC-NET-604;根因确认、MSS=512 证伪与回退、布局解耦同日完成,提交于 wip/v2-pending-recv 分支)。