新增Trap21机制图解文档:读必读空方案、5处读路径落点与RAM账,N-7细化
This commit is contained in:
+1
-1
@@ -13,7 +13,7 @@
|
||||
| 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 注¹ |
|
||||
| N-6 | **Trap 22:快速重连致 CONNECT 中断丢失 → 单连接监听静默死亡(P1,修复待批准)**——轻负载 + 重监听历史 + 断连后 <~1s 重连:芯片握手正常但 MCU 无 standalone accept(状态滞留 LISTENING)→ `net_send` 全失败(-1)→ 断开时 DISCONNECT 落 LISTENING 分支仅告警 → 芯片自动关闭 Socket → 端口永久拒连(不自恢复) | 2026-09-22(Trap 21 二次台架 A 组 run2;出现 1/1,未做 10/10 规模复验) | 修复方案:`handle_disconnect_event` LISTENING 分支回读芯片 socket 状态,非可监听即 `net_relisten_try` 重建(D20 的 LISTENING 分支姊妹情形)+ 可选:重监听后清残留中断位;应用侧规避:单连接 socket 断连后重连 ≥1~2s 退避(lftpd 快速 PASV / 客户端快速重试是生产风险场景)。生产影响:数据通道监听静默死亡 → 后续数据连接全拒,严重度等同端口失效 | `CH395F_Trap_Records.md` Trap 22 |
|
||||
| N-7 | **Trap 21 生产修复:net 层非阻塞 RX 读强制排空至空(P1,待批准实施)**——致命类 = 部分窗口重通告(排空后缓冲残留):大 Δ(1KB 块读,C/C0 配方)10/10 确定性停摆;小 Δ(64B)相位刀口(一次性落点 10/10 过 vs 5/5 停摆仅数 ms 时移之差)且持续重复必停摆(50/50);排空至空(窗口→满)恒安全(40/40,含 10ms 节拍 E0)、Δ=0 命令(状态读/中断读/poll/TX)恒安全 → **无需时间守卫/冷却**(旧"排空后 RX 暂缓 + 120ms 冷却"设计作废:C0 证明部分排空自身即致命,E0 证明排空至空后 10ms 再排空安全) | 2026-09-22(精确定位轮:E/O/B/F1/S1–S4/C/C0/V1F/O1f 共 11 组台架 110+ 运行 + netcmd 命令序列日志) | 实施要点:非阻塞(DONTWAIT)RX 读上限强制为接收缓冲全量(2048B)——"只读一部分"经安全 API 不可表达(再武装区间内部分排空不可能出现);应用按自身块粒度消化排空结果(lftpd `receive_file` 块循环 `net_recv(1024)` → `net_recv(2048)`,仍按 1KB 块处理;lftpd 块接收缓冲需扩至 ≥2KB 静态缓冲,RAM +1KB 计入预算);阻塞/pending 读完成点同样强制全量(或应用同 tick 内排空);Δ=0 命令(状态读/poll/TX)不受限。台架回归:C/C0 配方(1024B 部分排空)由 10/10 停摆 → 10/10 通过;V1 配方(Phase 6+11+60ms)由 10/10 停摆 → 10/10 通过(Phase 11 每 tick 64B 读被规则强制为排空至空) | `CH395F_Trap_Records.md` Trap 21;`Drivers/BSP/NET/lftpd/lftpd.c` receive_file 块循环 |
|
||||
| N-7 | **Trap 21 生产修复:net 层非阻塞 RX 读强制排空至空(P1,待批准实施)**——致命类 = 部分窗口重通告(排空后缓冲残留):大 Δ(1KB 块读,C/C0 配方)10/10 确定性停摆;小 Δ(64B)相位刀口(一次性落点 10/10 过 vs 5/5 停摆仅数 ms 时移之差)且持续重复必停摆(50/50);排空至空(窗口→满)恒安全(40/40,含 10ms 节拍 E0)、Δ=0 命令(状态读/中断读/poll/TX)恒安全 → **无需时间守卫/冷却**(旧"排空后 RX 暂缓 + 120ms 冷却"设计作废:C0 证明部分排空自身即致命,E0 证明排空至空后 10ms 再排空安全) | 2026-09-22(精确定位轮:E/O/B/F1/S1–S4/C/C0/V1F/O1f 共 11 组台架 110+ 运行 + netcmd 命令序列日志) | 实施要点(2026-09-22 复核细化,完整方案见 `问题记录/CH395F_Trap21_机制图解.md` §5):net 层新增 `net_drain_recv()` 助手,替换 5 处 `min(货架现有, 调用方len)` 读路径——`net_recv_locked:1205`、`net_pending_tick:1514`/`:1536`、`net_pending_settle_close:1618`、`net_recvfrom:2072`:先交付中转筐 → 筐空则 `avail = get_recv_len()` 后**按 avail 全量读空**入筐 → 只交 `min(avail, len)` 给调用方,余量留筐、下次不发芯片命令直接交付(上层筐大小与解析逻辑零改动);中转筐按生产并发 socket 配 4 槽 × 2048B = 8KB(主 SRAM 余量 47.2KB,占 17%);槽位全占时该读暂缓(WOULDBLOCK / pending 继续等),不发命令即无残余。**只改 lftpd 数据筐(1KB→2KB)不足以覆盖**:控制行读、UI 64B、时间同步 16B 为同型雷(且单纯加大控制行缓冲会丢流水线命令)。台架回归:C/C0 与 V1 配方由 10/10 停摆 → 10/10 通过,E 系列保持 40/40;Δ=0 命令(状态读/poll/TX)不受限 | `CH395F_Trap_Records.md` Trap 21;`CH395F_Trap21_机制图解.md`;`Drivers/BSP/NET/net_socket.c`(5 处读路径);`Drivers/BSP/NET/lftpd/lftpd.c` receive_file 块循环 |
|
||||
|
||||
## 存储 / 日志管线
|
||||
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
# CH395F Trap 21 机制图解:"搬一半货"为什么会把芯片的嘴封死
|
||||
|
||||
> 面向:需要理解/评审本问题的工程与管理人员。逐组台架数据与技术定案见 `CH395F_Trap_Records.md` Trap 21。
|
||||
> 整理日期:2026-09-22(精确定位轮定案后)。
|
||||
|
||||
## 0. 一句话
|
||||
|
||||
CH395F 每个 socket 有一个 2KB 收货货架;**MCU 每次取货必须一次取空**。只要"取一半、留一半",芯片的 TCP 引擎就可能走进死角、**永久封死它的发送嘴**——它还能收货(RX 正常),但再也发不出一个字节(回显、ACK、窗口更新全停),连接却不 RST。对端看到的就是"静默死亡"。
|
||||
|
||||
## 1. 现场长什么样
|
||||
|
||||
- **PC 侧**:传输到一半,设备回显/应答戛然而止(0 字节),但 TCP 连接**没断**——PC 还能发数据,设备侧也照收。
|
||||
- **PC 的 TCP**:窗口停在 0 不动,剩余数据卡在 **PC 自己的发送缓冲**里(不是"重传失败",是在**等一个永远不会更新的窗口**),等 10 秒、100 秒都一样。
|
||||
- **设备侧**:MCU 一点异常都没有——SPI 命令照发、节奏 10ms 不变、芯片照收数据、连接 ESTABLISHED、无 FAIL/ERROR。
|
||||
- **唯一恢复方式**:对端断开重连(新连接 = 新 socket = 干净状态)。
|
||||
|
||||
这是最难查的那类故障:**所有软件指标都健康,功能已经死了**。
|
||||
|
||||
## 2. 芯片里到底发生了什么(比喻版)
|
||||
|
||||
### 2.1 货架 + 告示牌
|
||||
|
||||
- 每个 socket 有 **2KB 收货货架**(RX 缓冲)。
|
||||
- 货架旁挂**告示牌**:TCP 窗口 = 货架剩余空位(手册 L1281)。
|
||||
- 对端是守规矩的快递员:**告示牌写多少空位,他就塞多少货**。所以只要 MCU 不及时搬,货架必然被塞满(2048/2048,窗口 = 0)——这是"结构性必达",躲不开。
|
||||
- MCU 是搬运工:定期 `net_recv` 把货搬进屋。
|
||||
|
||||
### 2.2 "搬空"与"搬一半" = 芯片的一次对账
|
||||
|
||||
搬运工每搬走一批货,芯片都要做一次**对账**(重算空位 → 改写告示牌 → 更新 TCP 序号,即"窗口重通告 / 再武装"):
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["对端把 2KB 货架塞满<br/>窗口 = 0"] --> B["MCU 发起取货<br/>READ_RECV_BUF"]
|
||||
B --> C{"取完后货架上还有货吗?"}
|
||||
C -->|"没有(取空)<br/>告示牌 → 2048"| D["芯片对账一次到位<br/>✅ 正常循环<br/>40/40 全过,含 10ms 快取"]
|
||||
C -->|"有残余 N 字节<br/>告示牌 → 部分值(非 0 非满)"| E["芯片对账被打断<br/>❌ 发送引擎永久锁死"]
|
||||
E --> F["SPI 写回显仍返回成功<br/>但线上 0 字节<br/>ACK / 窗口更新全停"]
|
||||
F --> G["对端窗口永远停在 0<br/>剩余数据卡在对端发送缓冲<br/>连接仍 ESTABLISHED(无 RST)"]
|
||||
```
|
||||
|
||||
| 搬运方式 | 告示牌变化 | 芯片状态 | 台架结果 |
|
||||
|---|---|---|---|
|
||||
| **搬空**(货架 0,告示牌 2048) | 一次到位 | 对账干净结束 | ✅ 任意节拍安全(10/20/40/60ms,40/40) |
|
||||
| **搬一半**(留 436,告示牌 588→1612 这种"半吊子"数字) | 改到"部分值" | 对账被打断,引擎走进死角 | ❌ 可能永久封嘴(大 Δ 必然,小 Δ 看落点) |
|
||||
|
||||
**关键:芯片怕的不是"货架满",也不是"搬得慢",而是"取货取一半、在货架上留下残余"这个动作本身。**
|
||||
|
||||
### 2.3 嘴被封死,耳朵还灵(单向死亡)
|
||||
|
||||
- **发送引擎彻底锁死**:MCU 通过 SPI 把回显写进芯片发送缓冲,芯片回答"收到了"(SPI 写成功、字节数正确),**但线上一个包都不发**;ACK、窗口更新一并停止。
|
||||
- **接收引擎完全正常**:继续收、继续被 MCU 搬空(命令序列日志实证:冻结之后到达的数据,芯片照收、MCU 照搬,10ms 命令节奏完整)。
|
||||
- 连接双方都认为"连接还在"(无 RST)→ 对端只能看到"对方不说话了"。
|
||||
|
||||
### 2.4 为什么 64B 是"刀尖",1KB 是"必死"
|
||||
|
||||
芯片的对账动作有几个内部小阶段(子相位),像齿轮咬合的缝隙:
|
||||
|
||||
- **小 Δ(一次只搬 64B)**:告示牌只改一点点,落点撞上缝隙就出事、错开就没事——这就是**相位刀口**:
|
||||
- 同一套"满排空 + 一次性 64B 部分读"配置:**非日志构建 10/10 全过**;
|
||||
- 只打开命令日志、netTask 时序挪了几毫秒 → **5/5 全部封嘴**。
|
||||
- 但**反复搬**(每 10ms / 60ms 搬一次 64B)→ 几十上百次总有撞上 → **50/50 必死**(6 套构建、5 种节拍)。
|
||||
- **大 Δ(一次搬 1KB,= 生产 lftpd 块读)**:告示牌大幅改写,缝隙躲不开 → **10/10 确定性封嘴**;而且**哪怕 10ms 后立刻把剩下的 436B 也搬空,也救不回来**(C0 对照实测)——伤害在"取一半"那一瞬间就已造成。
|
||||
|
||||
### 2.5 为什么"只读状态"没事
|
||||
|
||||
只问"还有多少货"(空读长度)、只读中断标志(poll)、只往发送缓冲写回显(TX)——**货架一个字节没动,告示牌不用改**:
|
||||
|
||||
- 台架:Δ=0 状态读(0.4 秒内连读约 40 次)+ 每 tick 的 poll + 每次排空后的 TX,**全部 40/40 干净**。
|
||||
|
||||
所以修复方案里这些**不需要拦**——拦了只会白增延迟。
|
||||
|
||||
## 3. 台架证据一览
|
||||
|
||||
| 行为 | 结果 |
|
||||
|---|---|
|
||||
| 每次搬空,10/20/40/60ms 节拍(任意 Δ:64B/1460B/2048B) | ✅ 40/40 |
|
||||
| Δ=0 状态读 / poll / TX | ✅ 40/40+ |
|
||||
| 满排空 + **一次性** 64B 部分读(非日志构建) | ✅ 10/10(相位运气) |
|
||||
| 同上 + 命令日志(时序挪几毫秒) | ❌ 5/5 封嘴 |
|
||||
| 满排空 + **每 tick** 64B 部分读(10/90/130/170/60ms,6 套构建) | ❌ 50/50 封嘴 |
|
||||
| **部分排空(1460 里搬 1024、留 436),零第二命令** | ❌ **10/10 封嘴** |
|
||||
| 部分排空 + 一次性 64B 再读 | ❌ 10/10 封嘴 |
|
||||
| 快速重连链(20 连接、0.6s 间隔) | ✅ 20/20("重连预置"假说证伪) |
|
||||
|
||||
再武装脆弱区间实测**下界 ≥160ms**(+0/+80/+120/+160ms 偏移位均致命),且相位敏感 → 不存在"可移植的单值时间窗"。
|
||||
|
||||
## 4. 为什么生产一定会踩:代码里的"正常写法"
|
||||
|
||||
问题在于:**"我筐多大就读多少"是 socket 编程的标准写法**,而 CH395F 恰恰惩罚这个写法。net 层有 **5 处**同型读路径(每一处单独看都写得完全正确):
|
||||
|
||||
| 位置 | 谁在用 | 一次要多少 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `net_recv_locked`(net_socket.c:1203-1209) | 非阻塞 / 有数据即返的读 | 调用方 `len` | **主触发**:`read_len = min(货架现有, len)` |
|
||||
| `net_pending_tick` ① 数据到达(1512-1518) | pending(阻塞)读完成 | 调用方 `len` | 同型 |
|
||||
| `net_pending_tick` ③ 断连排空(1534-1540) | 断连前最后一次排空 | 调用方 `len` | 同型 |
|
||||
| `net_pending_settle_close`(1615-1618) | close 前排空 | 调用方 `len` | 同型 |
|
||||
| `net_recvfrom`(2047-2073,UDP) | UDP 读 | 调用方 `len` | 同型 |
|
||||
|
||||
而调用方给的筐,天生就小:
|
||||
|
||||
| 调用方 | 筐 | 场景与风险 |
|
||||
|---|---|---|
|
||||
| `lftpd.c:187` `receive_file`(数据通道 STOR) | **1024B** | 每 1KB 一次 → 大 Δ 部分排空 = **10/10 必死**(生产停摆本体) |
|
||||
| `lftpd_inet.c:130` 控制行读 | `buffer_len-1`(行缓冲) | 客户端流水线发命令时留残余 |
|
||||
| `ui_net_sink.c:40` UI 帧 | **64B** | UI 帧大于 64B 就留残余 |
|
||||
| `time_sync.c:96` 时间包 | **16B**(栈上) | 多包/重传时留残余 |
|
||||
|
||||
**结论:只修 lftpd 那一处不够**——控制行、UI、时间同步都是同一颗雷,且以后新写的读取方会再踩一次。这就是"必须在 net 层做成结构性不可能"的理由。
|
||||
|
||||
## 5. 解决方案
|
||||
|
||||
### 5.1 核心规则
|
||||
|
||||
**MCU 每一次从芯片货架取货,都必须一次取空。** 上层要小口吃,就让 net 层先把货整批取进"中转筐",再一勺一勺喂给它。
|
||||
|
||||
### 5.2 落点:5 处 `min()` 收敛成一个助手
|
||||
|
||||
net 层新增 `net_drain_recv(sockfd, buf, len)`,替换第 4 节表中的 5 处:
|
||||
|
||||
1. 该 socket 的中转筐有货 → **直接交付**(不发芯片命令);
|
||||
2. 中转筐空 → `avail = ch395f_get_recv_len(fd)`(Δ=0,安全);
|
||||
3. **按 `avail` 全量读空**进中转筐(`read_len == avail`,恒排空至空);
|
||||
4. 拷 `min(avail, len)` 给调用方,**余量留在筐里**,下次直接交付。
|
||||
|
||||
驱动层无需改动(一次 SPI 事务本来就能搬满 2KB)。
|
||||
|
||||
### 5.3 中转筐设计与 RAM 账
|
||||
|
||||
- 每槽 2048B;**按生产并发 socket 数配槽**:lftpd 控制 + lftpd 数据 + UI sink + 时间同步 = 4 槽 = **8KB**。
|
||||
- RAM 实测(2026-09-23 map):主 SRAM 静态 82,760 / 131,072 B → **余量 47.2KB**,8KB 约占余量 17%,可接受(预算口径见 `docs/内存管理/`)。
|
||||
- **槽位全占**时:该次读**暂缓**(DONTWAIT 返回 WOULDBLOCK;pending 继续等待),**不发芯片命令 → 不产生残余 → 不会封嘴**;槽位由该 socket 的下一次读立即释放,不会长期饿死。
|
||||
- 上层**零改动**:调用方的筐仍是原大小,每次拿到的字节数范围不变 → 按行/按帧/按字节的解析逻辑完全不受影响。
|
||||
|
||||
### 5.4 被否决的方案(及原因)
|
||||
|
||||
| 方案 | 为什么不选 |
|
||||
|---|---|
|
||||
| 各调用方把筐加大到 2KB、一次读 2KB | ① 5 个调用方都要改,且控制行读会**丢流水线命令**(找到 `\r\n` 就返回,多读进来的后半段被丢弃);② 没有强制手段,未来新调用方会再踩;③ UI / 时间同步加筐纯属浪费 |
|
||||
| 让驱动把残余留在 SPI DMA 缓冲里 | DMA 缓冲被**每一次 SPI 事务复用**(poll、TX、别的 socket),残余会被静默覆盖 → 更隐蔽的数据损坏 |
|
||||
| 读了不交付(把残余丢掉) | 文件传输直接损坏,不可接受 |
|
||||
|
||||
### 5.5 明确**不需要**的东西
|
||||
|
||||
- ❌ 不需要"排空后暂缓 RX 命令 + 120ms 冷却"(旧 N-7 方案作废):只要"读必读空",压根不存在残余窗口;C0 证明部分排空**自身**即致命(冷却救不了第一条命令),E0 证明排空至空后 10ms 再排空也安全(冷却纯属多余)。
|
||||
- ❌ 不需要拦状态读 / poll / TX(实测安全),保住 10ms 级低延迟。
|
||||
|
||||
### 5.6 回归判据(改完必须过)
|
||||
|
||||
1. **C/C0 配方**(64×10 + 1460B 填充 + 1024B 部分排空):修复前 10/10 封嘴 → 修复后 **10/10 通过**;
|
||||
2. **V1 配方**(Phase 6+11 + 60ms 节拍):修复前 10/10 封嘴 → 修复后 **10/10 通过**;
|
||||
3. **E 系列**(排空至空,10/20/40/60ms):保持 **40/40 通过**;
|
||||
4. 产品态 FTP 大文件上传(>2KB)浸泡:不再出现"回显中断但连接不断"。
|
||||
|
||||
## 6. 给非工程读者的一句话交底
|
||||
|
||||
芯片里有一条怪规矩:**取货必须一次取空**。取一半,它就可能把嘴封死——还能收货,但再也不说话,而连接显示一切正常。我们的上传程序按标准写法"一次取 1KB",正好踩上。修法同样直白:**每次取货都取空,先放进自己的中转筐,再慢慢喂给程序**;实测这样 10ms 一次快取也平安无事。
|
||||
|
||||
## 7. 相关文件
|
||||
|
||||
- 定案与逐组台架数据:`docs/问题记录/CH395F_Trap_Records.md` Trap 21
|
||||
- 修复条目(待批准实施):`docs/待处理内容.md` N-7
|
||||
- 复现/对照规则:`docs/测试说明/CH395F_Test_Guide.md` TC-NET-604
|
||||
- 涉及代码:`Drivers/BSP/NET/net_socket.c`(5 处读路径)、`Drivers/BSP/NET/lftpd/lftpd.c`、`Drivers/BSP/NET/lftpd/lftpd_inet.c`、`Drivers/BSP/UI/ui_net_sink.c`、`App/time_sync.c`、`Drivers/BSP/CH395F/ch395f.c`(`read_recv_buf` OVERSIZE 金丝雀)
|
||||
@@ -542,6 +542,8 @@ NET 层 TCP Client(阶段 7)`net_connect()` 返回 0,但 `net_poll()` 报
|
||||
### 现象
|
||||
单条 TCP 连接在传输中途**静默单向死亡**:设备侧回显/应答流戛然而止(PC 侧 0 字节),连接不 RST(两侧均保持 ESTABLISHED)、无芯片断开事件;**芯片接收路径仍然健康**(2026-09-22 精确定位轮实证:冻结后继续到达的数据芯片照常接收、MCU 照常排空,命令流节奏完整),**芯片发送路径永久死亡**(已写入的发送数据不出线——SPI 写被芯片接受但线上无任何段;ACK/窗口更新随之停止)→ 对端剩余数据滞留**对端发送缓冲**(窗口视图冻结在 0,对端"重传"实为等窗口的空等,10s 无效)。对端断开重连(新 Socket)后恢复正常。2026-09-22 精确定位轮最小复现:64×10 + 1460B 填充 + **1024B 部分排空**(C/C0 配方,Phase 6-only 构建、零第二命令,10/10);原三因子配方(F1 组合 + 4096B 超容突发)10/10 维持。
|
||||
|
||||
> **通俗图解与修复方案**(比喻版机理 + net 层"读必读空"设计 + 代码落点 + RAM 账):见 `CH395F_Trap21_机制图解.md`。
|
||||
|
||||
### 根因
|
||||
CH395F 片内 TCP 状态机的角(芯片固件,MCU 侧无法修补)。2026-09-22 精确定位轮(11 组台架、110+ 运行、芯片命令序列级 netcmd 日志)定案:致命变量是 **RX 排空的结束态(排空后缓冲是否残留)**,与命令条数/类型/窗口时长无关(逐组实证见下):
|
||||
|
||||
|
||||
Reference in New Issue
Block a user