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
+5
View File
@@ -1162,6 +1162,11 @@ void ch395f_read_recv_buf(uint8_t sock, uint8_t *p_data, uint16_t len) {
/* 计算总长度:1字节命令 + 1字节Socket + 2字节长度 + 数据 */
total = 4U + len;
if (total > CH395F_DMA_BUF_SIZE) {
/* 异常金丝雀:接收读不应超过整个接收缓冲(2KB,recv_len 不可能超过缓冲大小);
* 触发即调用方参数异常。静默丢弃会把芯片接收窗口锁死在 0(见 Trap 21),
* 故必须留痕。正常路径不会到达此处。 */
DBG_ERROR("read_recv_buf OVERSIZE: sock %u req %u > max %u, read dropped",
(unsigned)sock, (unsigned)len, (unsigned)(CH395F_DMA_BUF_SIZE - 4U));
return;
}
+22 -9
View File
@@ -112,17 +112,27 @@ extern "C" {
/*
* TCP MSS(CH395F_CMD_SET_TCP_MSS 参数)
* 由 net_init 经 ch395f_set_tcp_mss() 下发;此处作为单一真值源,
* DMA 缓冲大小亦由此推导(见下)。当前选取依据:8 Socket 共享 24KB 缓冲时,
* 各 Socket 接收缓冲 = 2×MSS,故 MSS 取 1024 为最大值(详见 net_socket.c 分配注释)。
* 由 net_init 经 ch395f_set_tcp_mss() 下发(须在 ch395f_init 之前);此处为单一真值源。
* 维持 1024:8 Socket 共享 24KB 缓冲时,单 Socket 接收缓冲 = 2×MSS = 2KB(手册 §9.2.8
* 满足上限的最大值)。TC-NET-604 台架验证(2026-09-22,详见 Trap 21):芯片接收冻结角
* (WCH FAQ:"收不到数据且 TCP 窗口不更新")不受 MSS 裕量影响——降 512 反而更差
* (新连接 4096B 突发即确定性停摆,剩 64B 未达);MSS=1024 时新连接 4096B 正常,
* 仅"同连接前序突发 + 新突发 + 任务负载"模式触发。MSS 裕量假设被证伪,恢复 1024
* (每次满缓冲周期段数少 = 触发概率低)。MSS 只影响芯片分包与窗口动态,
* 与下方缓冲块布局解耦。
*/
#define CH395F_TCP_MSS 1024U
/* 内部缓冲 RAM 每块字节数(手册 §5.45:48 块 × 512B = 24KB) */
#define CH395F_RAM_BLOCK_SIZE 512U
/* 单 Socket 缓冲块数由 MSS 推导:接收 = 2×MSS(满足手册 §9.2.8 接收 ≥ 2×MSS),发送 = 1×MSS;向上取整 */
#define CH395F_RECV_BLOCKS ((2U * CH395F_TCP_MSS + CH395F_RAM_BLOCK_SIZE - 1U) / CH395F_RAM_BLOCK_SIZE)
#define CH395F_SEND_BLOCKS ((CH395F_TCP_MSS + CH395F_RAM_BLOCK_SIZE - 1U) / CH395F_RAM_BLOCK_SIZE)
/* 单 Socket 缓冲块数:固定布局(2026-09-22 与 MSS 解耦;此前由 MSS 推导,改 MSS 会
* 连带静默缩小布局):接收 4 块=2KB、发送 2 块=1KB,8 Socket × 6 块 = 48 块恰好用满。
* 布局满足手册 §9.2.8:接收 2KB = 2×MSS(1024)(建议下沿,零裕量——用满 48 块的代价,
* 其携带的窗口冻结角见 Trap 21)/ ≥ MSS(必须)/ ≤ 6×MSS(建议);
* 发送 1KB = MSS(WCH FAQ 要求"发送缓冲 > MSS",此处取等值——台架实测芯片在
* 缓冲边界可正常成包,验证记录见 Trap 21)。 */
#define CH395F_RECV_BLOCKS 4U
#define CH395F_SEND_BLOCKS 2U
/* 单 Socket 硬件收发缓冲字节数(= 块数 × 块大小)。写入发送 FIFO 的单次长度严禁超过此值,
* 否则会写穿有限的发送缓冲(见 Trap 20)。 */
#define CH395F_SEND_BUF_SIZE_BYTES (CH395F_SEND_BLOCKS * CH395F_RAM_BLOCK_SIZE)
@@ -131,10 +141,13 @@ extern "C" {
/*
* SPI DMA 事务限制
* CH395F_SPI_DMA_MAX_PAYLOAD: 单次 DMA 事务最大有效载荷(总缓冲 - 4 字节命令头)
* 接收方向需一次性读空整个 Socket 接收缓冲(= 2×MSS,避免 Trap 13 部分读导致数据不可见),
* 故总缓冲 = 2×MSS + 4(4 为命令头开销),有效载荷 = 2×MSS。
* 接收方向需一次性读空整个 Socket 接收缓冲(= 接收缓冲大小,避免 Trap 13 部分读
* 导致剩余数据不可见),故总缓冲 = 接收缓冲大小 + 4(4 为命令头开销),
* 有效载荷 = 接收缓冲大小(2KB)。
* 2026-09-22:与 MSS 解耦(此前 = 2×MSS+4,MSS 下调会把 DMA 缓冲缩到装不下整缓冲
* → 整缓冲读被截断丢弃 → 窗口锁死,见 Trap 21)。
*/
#define CH395F_SPI_DMA_BUF_SIZE (CH395F_TCP_MSS * 2U + 4U)
#define CH395F_SPI_DMA_BUF_SIZE (CH395F_RECV_BUF_SIZE_BYTES + 4U)
#define CH395F_SPI_DMA_MAX_PAYLOAD (CH395F_SPI_DMA_BUF_SIZE - 4U)
/* 其他命令 */
+6 -5
View File
@@ -168,10 +168,11 @@ static bool s_mac_is_zero(const uint8_t *p_mac) {
* bit3=1: DISCONNECT/TIMEOUT 时不自动清空缓冲区,由软件排空数据后手动关闭 */
ch395f_set_fun_para(0x09);
/* 设置 TCP MSS
/* 设置 TCP MSS(2026-09-22 TC-NET-604:试 512 裕量方案被台架证伪,恢复 1024,见 Trap 21)
* 手册(§5.45):CH395F 内部缓冲共 48 块 × 512B = 24KB。下方为 8 个 Socket 各分配 6 块
* (接收 4 块=2KB + 发送 2 块=1KB),按手册 §9.2.8(接收 ≥ 2×MSS 建议)取 MSS=1024,用满 48 块。
* MSS 真值源为 ch395f.h 的 CH395F_TCP_MSS,DMA 缓冲大小亦由其推导。 */
* (接收 4 块=2KB + 发送 2 块=1KB),块布局固定(与 MSS 解耦,ch395f.h)。
* MSS=1024 为满足布局的最大值(接收=2×MSS);MSS 裕量不能消除芯片窗口冻结角
* (512 方案反使新连接突发也停摆),真值源为 ch395f.h 的 CH395F_TCP_MSS。 */
ch395f_set_tcp_mss(CH395F_TCP_MSS);
/* 设置 TCP 重传参数(必须在 INIT_CH395 之前调用;P0-a:周期 500→200ms。
@@ -223,8 +224,8 @@ static bool s_mac_is_zero(const uint8_t *p_mac) {
/* 多 Socket 模式:为 Socket 0~7 各分配独立收发缓冲(必须在 OPEN_SOCKET 之前)
* 手册(§5.45):CH395F 内部缓冲共 48 块 × 512B = 24KB;芯片上电默认仅给 Socket 0~3
* 分配(每 Socket 接收 8 块/发送 4 块),Socket 4~7 为 NULL,故用到 8 个 Socket 必须重分配。
* 单 Socket 块数由 MSS 推导(ch395f.h):接收 CH395F_RECV_BLOCKS(=2×MSS/512=4 块=2KB)、
* 发送 CH395F_SEND_BLOCKS(=1×MSS/512=2 块=1KB),每 Socket 共 6 块,MSS=1024 用满 48 块:
* 单 Socket 块数为固定布局(ch395f.h,2026-09-22 起与 MSS 解耦):接收 CH395F_RECV_BLOCKS(=4 块=2KB)、
* 发送 CH395F_SEND_BLOCKS(=2 块=1KB),每 Socket 共 6 块,用满 48 块:
* blocks_per_socket = CH395F_RECV_BLOCKS + CH395F_SEND_BLOCKS
* recv_start = i * blocks_per_socket, send_start = recv_start + CH395F_RECV_BLOCKS
* Socket 7 占块 42~47,恰好用尽 0~47,互不重叠。
+1 -1
View File
@@ -9,7 +9,7 @@
| # | 事项 | 发现 | 证据与现状 | 出处 |
|---|---|---|---|---|
| N-1 | **TC-NET-604:4096B 经 2KB 芯片缓冲的回显残留停滞**——4096B 单段(2×2KB,芯片 socket RX 缓冲=2KB)回显:前半 2048B 正常回显,后半在 10s 内未被再读/回显,连接停摆至对端超时拆连。**前置 1460B 全 MSS 交换后触发(2/2 复现);独立 4096B(无前置交换)正常(117.8ms 全匹配)**→ 芯片 TCP 层"缓冲满时段的交付/重交付"路径问题,方向性触发条件 = 前序满 MSS 段 | 2026-09-22 17:00/17:03(L3 台架 F1 组合) | **A/B 归因:切片构建(2438de8,L3 前)同一序列逐字节同现**(11 回显 PASS、4096B 10s 超时、Phase 11 级联 FAIL,时序差 2ms)→ 既有芯片侧问题,**L3 pending 无责**(DONTWAIT 快速路径逐位未变)。复现探针(本地未跟踪工件,MDK-ARM/ 下):完整序列 = `test/ch395f_socket_test.py tcp_client --ip 192.168.1.100 --port 8080 --count 10`(F1 组合构建);独立 4096B = `MDK-ARM/probe_4096.py`。规避:Phase 6/8 同建时避免 4096B 子项紧随满 MSS 段;根治待芯片侧定位(`CMD_GET_SOCKET_STATUS_SN`/SINT 序列 + 缓冲水位观测) | `错误码传播审计.md` E-02 阶段二"台架新发现①";`链路延迟测试说明.md` §6.3 |
| N-1 | **TC-NET-604:芯片"缓冲恰好填满 + 排空"后 RX 窗口冻结(根因已确认,代码侧已闭环)**——同连接前序突发(64B×10+1460B)后 4096B 突发:前 2KB 正常回显,其余永不被芯片确认/交付,停摆 10s、无 RST、缓冲全程空、窗口不更新;断开重连后恢复正常(台架 10/10 复现,F1 组合) | 2026-09-22 17:00/17:03(L3 台架 F1 组合) | **根因 = CH395F 片内 TCP 状态机角(WCH 官方 FAQ 留档的已知局限,W5500 同类),非我方代码缺陷**:① 驱动"超限读静默丢弃"排除(DMA 缓冲 2052B 可容整缓冲,失败运行 0 次丢弃事件)② L3 排除(A/B 逐字节同现)③ "RX>2×MSS" 裕量假设**证伪**——MSS=512 实验原序列 10/10 仍停摆,且新连接 4096B 确定性停摆恒剩 64B(已回退,维持 MSS=1024:新连接 4096B 31ms 正常)。复现探针(本地未跟踪工件,MDK-ARM/ 下):完整序列 = `test/ch395f_socket_test.py tcp_client --ip 192.168.1.100 --port 8080 --count 10`(F1 组合构建);独立 4096B = `MDK-ARM/probe_4096.py`。代码侧闭环(已提交):缓冲块布局与 MSS 解耦 + 超限读 OVERSIZE 金丝雀 + 注释留痕。**残留决策项**:冻结不自恢复(唯一恢复 = 断连 + 客户端重试,重连路径台架已验证);lftpd 数据通道停摆预算现为 30s/读(`LFTPD_TOTAL_BUDGET_MS`,与控制行共用)——是否将数据通道单独缩短至 10–15s 以压缩停摆暴露(代价:慢速健康传输亦被中断;机器客户端不受影响)待用户决策 | `CH395F_Trap_Records.md` Trap 21;`错误码传播审计.md` E-02;`链路延迟测试说明.md` §6.3 |
| N-2 | **49s 全系统日志静默(defaultTask/NAND syslog 排空停滞)**——串口日志 49s 无行(testTask 10s 周期行/netTask 事件行/[STACK] 行全缺),期间 net 栈照常处理外部连接(FTP banner 92ms)→ 日志管线(defaultTask 排空 → NAND+UART)停滞,队列满后各任务日志行被丢;系统功能不受损 | 2026-09-22 17:23:52~17:24:41(L3 台架 F2 运行中,gen 181) | **A/B 归因:L3 不触碰 defaultTask/syslog 路径 → 既有存储侧现象**。待办:① 复现条件定位(NAND 写/GC 长尾 vs UART TX 卡滞——对照 `CH395F` 无关的 SD2506 侧日志)② 日志队列满的丢弃计数/告警(现静默丢行,排障不可见)③ 与 S8(非阻塞日志)合并评估 | `错误码传播审计.md` E-02 阶段二"台架新发现②";`链路延迟测试说明.md` §6.7 注 |
| N-3 | **L5 队列往返基线(1223/988µs)的 L3 后重验**——L5 测 DONTWAIT 队列全程;L3 对该路径逐位不变、pending tick 无 pending 时为 µs 级指针读 → 机制上无影响,但基线数值属分片时代,严格重锁需跑一轮 Phase 13(需 PC peer,窗口与 N-1 定位可并案) | 2026-09-22(L3 收尾留档) | 低风险:10ms/100ms 阈值余量 >8×;下次 Phase 13 窗口顺带重锁即可 | `链路延迟测试说明.md` §6.4 L5 注¹ |
@@ -318,6 +318,9 @@ DTU-RemoteLCD 客户端;输出每帧 `req2first / total / recv 批次到达时
## 6. 实测基线(2026-09-22 台架,已重锁)
> 2026-09-22 TC-NET-604 闭环注记:该轮代码变更(缓冲块布局与 MSS 解耦 + 超限读 OVERSIZE 金丝雀,见 Trap 21)
> 不改变任何芯片行为参数(MSS=1024、块布局 4/2、DMA 缓冲 2052B 均不变)→ 本节全部基线继续有效,无需重锁。
### 6.1 M1 段(Phase 12,SPI 层)
| 段(seg) | 实测 med(n=100) | 实测 min/max | warn(med >) | crit(med >) |
@@ -699,6 +702,8 @@ sock4 auto-relisten ok / relisten retry ok ← 监听器随后自行恢复
6. **S4** 发送块配额与 MSS:48 块按实际使用的 3 个 socket 重新分配(各 16 块),MSS → 1460
(3209B 从 4 段降到 1~2 段);**约束**:单次写长度必须 ≤ 发送缓冲(Trap 20 铁律),
改分配策略需同步 `net_bind` 重复分配路径,改前先读 `CH395F_Trap_Records.md`,改后跑 Phase 8 吞吐用例;
**另(Trap 21)**:MSS 是芯片 RX 冻结角的敏感变量(MSS=512 实验使新连接 4096B 突发也停摆,已证伪回退)——
任何 MSS/块配额变更必须先跑 TC-NET-604 复现序列(`test/ch395f_socket_test.py tcp_client` 全序列)+ 独立 4096B 对照,全 PASS 方可采用;
7. **S5** 去掉一次 3200B 拷贝(`g_tx` 直接承接位图,省 ~3.2KB 常驻 RAM;改前跑 `scripts/ram_report.py`);
8. **S7** uiTask:`ui_net_sink_poll()` 前置到 `ui_port_step` 前 + while 排空(省按键一整拍);
9. **S8** 日志改非阻塞(`fputc` → 环形缓冲 + TX 中断/DMA;处理溢出丢弃与 HardFault 冲刷);
+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 分支)。
+2 -2
View File
@@ -153,7 +153,7 @@ E-60"write_enable WRTC2 与手册矛盾"经三方复核(手册表头 D7~D0 顺
#### E-02 netTask 队列头阻塞:一个消费者的阻塞调用冻结全体
- **位置**:`Drivers/BSP/NET/net_socket.c:357`(`net_process_messages` 一次取一条,处理完才取下一条)、`:1019-1067`(`net_recv_locked` 阻塞等待循环,30s 超时在 netTask 内空转)、`:961-984`(send 每 chunk 1s 停滞等待);触发源 `App/time_sync.c:84`(flags=0 阻塞 recv)、`lftpd_inet.c:106-109`(read_line 阻塞 recv——**FTP 客户端控制连接空闲 >30s 即触发,常见工况**)、`:135/:156`(阻塞 send)
- **模式**:P4 变体(串行化头阻塞,非丢消息)
- **影响**:最长 30s 全网队列停摆——uiTask 100Hz 轮询、FTP 收发/accept、timeSync 全部排在队列里得不到处理;事件处理不受影响(等待循环内仍调 `net_poll`,`:1020`),受影响的**恰是队列**;与 E-06 叠加 → PASV 假 425;STOR 上传方向芯片 2KB 缓冲满 + 重传 8×500ms=4s 耗尽 → 必断
- **影响**:最长 30s 全网队列停摆——uiTask 100Hz 轮询、FTP 收发/accept、timeSync 全部排在队列里得不到处理;事件处理不受影响(等待循环内仍调 `net_poll`,`:1020`),受影响的**恰是队列**;与 E-06 叠加 → PASV 假 425;STOR 上传方向:台架实证(2026-09-22,Trap 21)实际行为为"缓冲恰好填满 + 排空"后**芯片 RX 窗口静默冻结**(不 RST/不拆连、数据永不被确认、不自恢复;唯一恢复 = 断连 + 客户端重试)——原"重传耗尽必断"预测不成立(芯片自身发送侧干净,拆连逻辑不触发),结论方向(STOR 方向 = 致命)成立
- **修复方向**:消费端阻塞 recv 改 `DONTWAIT` 分片 + 调用方重投 + 总超时(ui_sink 模式推广到 lftpd/timeSync);或 `net_process_messages` 在长阻塞间隙按 tick 继续排空队列;`NET_RECV_TIMEOUT_MS` 按服务可容忍量级分离(既有 P2-8 是本条的子集)
- **已修复(2026-09-22,两阶段)**:
**阶段一(缓解,两提交)**:落地方案为方向①的变体——"分片"用**有界阻塞片**而非 DONTWAIT 重投:新增 `net_recv_to` 接口(`msg.timeout_ms` 携带片长;片超时以独立返回码 -2 区分,无跨任务 errno 竞态);调用方累计总预算、片间 `vTaskDelay(1)` 让出 CPU(防 Normal 优先级忙循环饿死 BelowNormal 的 uiTask,M3 实证);5ms 片长 = 其他任务最坏等待(100ms 片下受压帧 ~750ms、5ms 片 ~10ms,`docs/测试说明/链路延迟测试说明.md` §8.3);30s 总预算保持旧超时语义
@@ -167,7 +167,7 @@ E-60"write_enable WRTC2 与手册矛盾"经三方复核(手册表头 D7~D0 顺
- **功能套件(Phase 6/9/11 组合 + Phase 8-RECV 独占组合,分两次构建)**:64B/1460B 回显 11/11、select 回显 11/11、断开重连、KeepAlive 断连 + 自动重监听全 PASS;**Phase 8-RECV 100KB 阻塞接收(pending 主路径)50/50 块(2048B/块)逐块校验 + CRC32 整包一致**(0x9A0E0C8C),收齐后主动 close + auto-relisten 正常
- **产品构建 `@test`**:PASS(Required 全中/Forbidden 零命中);netTask 运行栈峰值 **488B/4096(余量 88%)**、静态最坏链 432B——零占用设计实测落点
- **RAM/Flash 代价(map A/B,同一测试配置)**:SRAM +200B(192B `s_net_pending[8]` + 4B `s_net_task_handle` + 4B 他项)、Flash +608B(code +528/RO +80)——远低于内存预算阈值
- **台架新发现(与 L3 无关,A/B 归因后转 `docs/待处理内容.md` N-1/N-2)**:① TC-NET-604 4096B 经 2KB 芯片缓冲的回显残留停滞(前置 1460B 全 MSS 交换后触发,独立 4096B 正常;**切片构建复现行为逐字节相同** → 既有芯片侧问题,L3 无责)② 台架运行中一次 49s 全系统日志静默(net 栈期间照常处理连接 = defaultTask/NAND syslog 排空停滞,既有存储侧现象,L3 无责)
- **台架新发现(与 L3 无关,A/B 归因后转 `docs/待处理内容.md` N-1/N-2)**:① TC-NET-604 前序突发后 4096B 停摆 = CH395F 片内 TCP RX 冻结角(2026-09-22 确认为 WCH 官方 FAQ 留档的已知局限,MSS=512 裕量方案被证伪;根因/闭环见 Trap 21,代码侧已闭环提交,残留数据通道预算决策项在 N-1)② 台架运行中一次 49s 全系统日志静默(net 栈期间照常处理连接 = defaultTask/NAND syslog 排空停滞,既有存储侧现象,L3 无责)
#### E-03 全局 `s_net_errno` 跨任务覆写竞态(09-21 修复的残留)
- **位置**:写方 = `net_socket.c:39` 单全局;netTask 侧 `:362/365/375/385/410` + **notify 值构造 `:419-423`**(`s_notified` 分支读同一全局);6 个队列包装器在**调用方上下文** `:660/758/818/1233/1286/1333`(仅失败路径写);6 个直调 API 在**调用方上下文**失败时写(`net_socket :442/449`、`net_bind :482-538`、`net_sendto/net_recvfrom`、`net_getsockname :1953-1964`),**成功不写** → stale 滞留(`:1609` 文档化)。读端:`ui_net_sink.c:460`(uiTask 100Hz,BelowNormal 被抢占窗口最大)、`test/net_test_task.c:230/381`