b445cbb464
- 合并同根因: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 两文件通过
62 KiB
62 KiB
CH395F 驱动陷阱记录
记录开发过程中遇到的所有坑、根因、解决方案,避免重复踩坑。
2026-10-13 重组审计:合并同根因条目(14+17、13+21)、删除已被推翻的误判叙述(只留一行误判存档)、 按主题分章并加索引;条目格式统一为
## Trap NN · 短标题+ 固定小节(状态/现象/根因(定案)/修复/回归判据/教训, 视情况取舍)。编号永不重排——代码注释大量引用Trap NN,合并条目标题保留双编号; Trap 19 从未存在(悬空引用登记在第六章待修清单)。 状态图例:✅ 已修复定案 · ⏳ 待修 · ↩️ 已撤回(非缺陷) · — 从未存在。
索引
| 编号 | 标题 | 状态 | 一句话结论 |
|---|---|---|---|
| 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 01 · Socket 配置顺序:先数据 Socket 后监听 Socket
- 状态:✅ 已定案(多连接模式虽已随
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,早于仓库首提交
b1d050912026-06-05,日期不可考)。
Trap 02 · Socket 缓冲区必须显式分配且不重叠
- 状态:✅ 已定案(现行
net_init固定布局 8×(4收+2发)=48 块即用满) - 现象:多 Socket 数据互相覆盖、收到错误数据或客户端挂死;未分配缓冲的 Socket 发送为固定垃圾内容(
0x0028填充)、接收存不进数据、CONNECT/RECV 中断不触发。 - 根因(定案):芯片默认只为 Socket 0
3 分配独立收发缓冲(48 块×512B 用满,7 为零块;未显式分配或误用相同块号导致重叠。docs/芯片手册/CH395F.md§5.45),Socket 4 - 修复:每个数据 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 17 真正可用的是缓冲显式分配(Trap 02),不是 bit0。 - 根因(定案):
- 官方手册 V1C/V1E/V2.2/V2.3 的 §5.52 位表中 bit0 一律「保留,必须为0」;官方 SDK
CH395INC.H无 bit0 宏; - 官方例程直接反例:
EXAM5_TCP_Server传0x02(bit0=0)却给 Socket 0~7 全配缓冲;官方 FTP 例程直接传0x08;全 EVT 包无一处 0x09; - 8 Socket 是芯片原生能力(
_SN索引寻址),真正的门槛是 §5.45 缓冲分配; - 概念讹传源:bit1
FUN_PARA_FLAG_TCP_SERVER的"服务器多连接"指一个服务器能接几个客户端,不是"能用几个 Socket"。
- 官方手册 V1C/V1E/V2.2/V2.3 的 §5.52 位表中 bit0 一律「保留,必须为0」;官方 SDK
- 为什么 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.7SET_MAC_ADDR写芯片内部 EEPROM,§5.13INIT_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)。
第二章 中断与事件一致性
Trap 08 · RECV 中断是电平触发
- 状态:✅ 已定案
- 现象:
net_poll()中 RECV 中断无限触发,主循环永远得不到执行。 - 根因(定案):接收缓冲区有数据期间
INT#保持低电平,do{...}while(int_status!=0)必然死循环。 - 修复:
net_poll()每轮只处理一批中断,由主循环负责读数据。 - 推论:RECV 是条件位(数据读空即落),所以 Trap 25 B3 的全量读清对 RECV 可下轮补、对其余位是永久销毁。
Trap 14/17 · 上一条连接的残留 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把新连接误判断开。
- 硬件层(14):
- 根因(定案):①
GET_SOCKET_STATUS是纯查询不清中断;② DISCONNECT 可能在 CLOSED 达成同时或之后才锁存;③ SINT 粘滞到被读(close/open 都不清,Trap 25 B3 实证)→ 无人消费则跨会话存活。 - 修复:
- 预清:新会话 OPEN 前读一次全局 + 本 Socket SINT 丢弃陈旧事件(
phase2_tc203()双保险;生产net_connect_lockedopen 后清本 Socket 中断); - 排空:
net_close_locked在close_socket()后轮询GET_SOCKET_STATUS至SOCKET_CLOSED(上限 500ms,实测 LAN 几毫秒)再读空本 Socket 中断; - 反核实:依据中断做结论前用
GET_SOCKET_STATUS验证实际状态一致(负向事件与 ESTABLISHED 矛盾时以状态为准)。
- 预清:新会话 OPEN 前读一次全局 + 本 Socket SINT 丢弃陈旧事件(
- 措辞更正(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 取证。
Trap 22 · 快速复连丢 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
:21100ms×50 → 修复前第 29 轮起永久拒连;对照:210ms/600ms 与:8888同期正常 → 触发条件 = 复连间隔 <~1s + 轻负载。
- 确定性:产品固件 + 待机 60s,对
- 机制(设备侧日志实证):① 芯片丢 CONNECT 或 DISCONNECT 中断;② 此后本层收不到任何中断,纯事件驱动无路可走(把 LISTENING 分支改"重建监听"仍无效——连 DISCONNECT 都没来);③ 故必须周期回读芯片状态对账。裸机芯片台架(剥离 OS/net/FTP)独立复现"握手完成但 CONNECT 中断被丢"——芯片天性,不是固件 bug。
- 修复(
net_socket.c):- 周期对账
net_reconcile_tick()(初版 200ms;N34 阶段 3 与 pump 合并为单一 100ms tick):修正失同步形态——①本层 ESTABLISHED 而芯片已无连接→按断开处理;②本层 LISTENING 而芯片已握手→补 accept;③本层 LISTENING 而芯片监听消失→重建;④孤儿监听(B10)→重建。去抖+冷却:同一失配连续 2 次才动手,动手后冷却 1s。 handle_disconnect_eventLISTENING 分支:由"仅告警"改为"回读芯片状态——已不可监听即重建"。
- 周期对账
- 踩坑记录(务必先读):最初修法放在 accept 路径(每次 accept 回读自愈):单次状态读把"刚 open/listen 尚未生效"的瞬态误判为失配 → 监听反复重建直至
relisten give up→ 三个监听口全部死亡(风暴)。改"周期对账+去抖+冷却"后消除。结论:凡基于单次状态读的重建动作,必须去抖。 - 验证(修复前→修复后):
:7003300ms×10:2/10→10/10;:21100ms×50:28/50→50/50;:210ms/600ms×50:50/50/50/50;:7003200ms×20:20/20;FTP 10×64KB SHA-256:10/10;relisten give up:0。自愈实测计数:chip closed -> disconnect12 次、w/o CONNECT irq -> accept1 次、listen lost -> relisten1 次——三类失同步均真实出现并被自愈。 - 对应文件:
Drivers/BSP/NET/net_socket.c(net_reconcile_tick);MDK-ARM/tcp_reconnect_probe.py;docs/任务规划/README.mdN-6(已销项)。 - 发现时间:2026-09-22(台架偶发)→ 2026-09-23(确定性复现+当日修复闭环)。
Trap 25 · FTP 数据连接三连故障——四重定案(N-30)
- 状态:✅ 已定案并修复(回归 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。
- 吞数据(SWALLOW):新连接建立成功、服务端
- 定案实验(裸机芯片台架,剥离 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(对账不是元凶)。
- 有效修复一:双信源原则——中断只是快路径,状态寄存器才是真相:
- accept 路径内联探针(
net_lost_connect_probe,net_accept_locked每 100ms 挂探针):芯片已 ESTABLISHED 而 CONNECT 中断未到 → 连续 2 轮去抖后补 accept; - 幽灵态甄别:补 accept 前回读对端端口(
GET_REMOT_IPP),= 上一条连接端口(last_peer_port)→ 幽灵,不 accept(生产 LIST#4"accept 成功但数据被吞"的直接成因就是对账②曾把幽灵补 accept); - 对账②同步加幽灵甄别;
last_peer_port登记点:handle_disconnect_event/handle_timeout_event/net_relisten_try(清零 remote_port 前转登)/net_close_locked(跨 memset 保留——幽灵属芯片 Socket 级,与 fd 复用无关)。
- accept 路径内联探针(
- 定案二:所谓"kill 模式"是寄存器语义误读(2026-10-11,双向验证)——误判存档:曾解读"芯片在 accept 后 10-20ms 杀连接",有误。真相:
FUN_PARA=0x08置 bit3SOCKET_CLOSE=1(§5.52 ②):产生 DISCONNECT/TIMEOUT 后芯片不关 Socket、不清缓冲,就是留给 MCU 读出残留数据后再关;- 探针现场
kill-check recv_len=1027 re-read st=0x05/0x00——Socket 仍 OPEN、TCP 已 CLOSED、RX 缓冲 1027B = STOR 载荷一个不少全在:芯片按规格保留现场,不是杀连接; - 那个 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.py13/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 07 · TCP 关闭重连:直接 close,不要 disconnect
- 状态:✅ 已定案(做法保留;原根因说法已推翻,见定案更正)
- 误判存档:原断言"
disconnect → close让 Socket 卡FIN_WAIT_2数分钟、close 发 RST"——TC-NET-204 实测推翻(2026-10-11),机制说法作废,做法保留。 - 定案(TC-NET-204,三模式各 5 轮 45 项判定全过):
模式 关闭序列 回 SOCKET_CLOSED耗时重开+重连+回显 A close0~150 ms(过渡态可见 TIME_WAIT0x0A)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 FIN_WAIT_2(0x06) 全程未出现(15 轮×9 采样点);close无 RST 语义:§5.35 只说"关闭前自动断链";手册中芯片主动发 RST 仅 §5.29(监听占用)与 §9.2.6(多连接无可用数据 Socket)两处,均与 close/disconnect 无关;- 观测口径错误:§5.22 第 1 字节只有
00H/05H,第 2 字节(TCP 态)仅在 byte0=OPEN 时有意义(官方 SDK 注释明文)——当年把"已 CLOSED 状态下的残留 TCP 态读数"读成了"卡在某个 TCP 态"。
- 真正原因(C 模式 5/5 复现):
FUN_PARAbit3SOCKET_CLOSE=1(§5.52 ②)下产生 DISCONNECT 后芯片不关 Socket、不清缓冲,长期停在05H/00H;此时直接open_socket必返0x20FAIL。把 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-自愈审计/实验方案.mdE13)——不是应用可加速的路径; - 应用侧出口 =
net_drop_conn(fd)(N34 阶段 3):netTask 侧走驱动sock_resetQUARANTINE(FIN+排渣+排空事件+等 byte0=00H,400ms 底)+ 本层收口 CLOSED +auto_relisten即时重监听;保监听不毁(单连接模型不得net_close监听 fd)。对端不 close 时复位机等在net_poll步进里(ch395f_sock_reset_poll),对端 FIN 到达即自动走完——不搁浅。
- 对端配合(收到 FIN 后其内核 close):芯片走
- 判据:关闭后轮询
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)。
Trap 09 · 沉降窗:PHY link up 后 / close 后不能立即 open
- 状态:✅ 已定案(现行形态=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 无关。 - 修复(现行形态):
net_listen_locked/net_connect_lockedopen 失败重试 ×3(D10net_listener_retry_tick重监听重试环);- open 失败先走
net_sock_reset_sync(确定性排渣,内置二段 CLOSE,见下补充)再重试——最短等 100ms ≪ 原 500ms 盲等; 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 态上死等。
- 对 LISTEN(byte0=05H/byte1=01H)发
- 发现时间:FTP 改造期(原记 2025-07 不可考);二段 CLOSE 补充 2026-10-13。
Trap 11 · net_accept_locked 拒绝 standalone_accept 的 ESTABLISHED
- 状态:✅ 已修复定案(现行
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)。
Trap 23 · 上传中止后首笔复连上传被静默截断(≤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)不受影响(不按传输复用)。 - 修复(三阶段,
net_socket.c+lftpd.c):- 异常释放(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 → 监听器死亡(已留痕); - 正常释放(FIN)沉降:
handle_disconnect_event登记(仅recv_bytes != 0时——纯连接即关与只发不收免沉降保复连时序),relisten 前最短等NET_RELISTEN_GRACEFUL_MS(100ms,实测悬崖:0/30ms 无效、100ms 稳定通过,对 30ms 重试节拍留 3× 余量);日志区分relisten settle/relisten quarantine; - 应用侧硬化:
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)。
- 异常释放(TIMEOUT/RST)隔离:
- 协议边界(不可判定,留痕):FTP STOR 无长度声明,"在途 FIN 关闭"与"正常完成"逐字节不可区分 → 优雅中止仍回 226;仅 RST/掉链(芯片 TIMEOUT)可识别 → 450。FIN 边界由客户端长度校验(SIZE/RETR 往返)兜底。
- 验证(终态基线):
--check-abortRST 5/5(450)+ FIN 5/5(226 边界不变);followup 全过;正常往返 freeze SHA-256 全 MATCH(t_stor 0.68→0.18s);:7003sink 复用 RST×4/FIN×4 全过;:8888真实收包写 RTC 成功。 - 与 TC-NET-604 的关系:本条是独立、已可信复现并已修复的真实缺陷;TC-NET-604 已结案为非缺陷(Trap 13/21)。现场"上传失败/文件不完整/像卡死"类报告优先用本条判据复核。
- 发现时间:2026-09-23(可信复现首轮);二阶段 09-24、硬化 09-25。
第四章 数据通路与数值语义
Trap 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 归零 / 接收缓冲冻结」家族——非缺陷(合并结案)
- 状态:↩️ 已撤回(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 全过。
- 为什么原台架会"复现"(教训,务必先读):
- 测试程序自吞数据:台架配方里的固件内读者(Phase 9/11 的 64B 读、固件探针读)把数据读走后不回显——PC 端少收被误当"设备停摆"。实测短收量恰等于被吞字节数(
4736−4608=128=2×64);对照组(无额外读者)40/40 全过。 - 判读工具误判:
probe_classify.py把通过的运行报成STALL(脚本层实测1460/1460且内容逐字节匹配)。 - 由此派生的"部分排空致命 / 再武装区间 ≥160ms / 相位刀口"等结论全部作废。
- 测试程序自吞数据:台架配方里的固件内读者(Phase 9/11 的 64B 读、固件探针读)把数据读走后不回显——PC 端少收被误当"设备停摆"。实测短收量恰等于被吞字节数(
- 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 输入输出命令字节顺序均为低字节在前"与官方例程自相矛盾,本仓以实测+例程为准。 - 误导性排查:
GET_REMOT_IPP回读是小端——把写反的数组读回再按小端解释恰好又得192.168.1.2,回读日志"看起来正确",掩盖真实写反。 - 修复:①
remote_ip_arr改大端提取(arr[0]=(ip>>24)&0xFF …);② UDPnet_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。
Trap 20 · 单次写超过发送缓冲区(写穿)
- 状态:✅ 已修复定案(措辞已按 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 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.pyDHCP server 模式。 - 发现时间:阶段 4(原记 Phase 6 2025-07 有误)。
Trap 10 · 消息队列值拷贝导致结果无法回传
- 状态:✅ 已定案(现行契约:
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)不受值拷贝影响;内核级_lockedAPI 直接操作不受影响。⚠ 原注"net_connect() 不使用消息队列"已失效——现行net_connect同样走消息队列、结果同样走通知值。 - 发现时间:FTP 改造期(2026-08)。
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.hFF_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)。
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。
第六章 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 历史。
干净清单(核对过、无异常)
| 项 | 计数 | 结论 |
|---|---|---|
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),无新增同类 |
新发现(去重后按严重度)
| # | 严重度 | 位置 | 问题 | 状态 |
|---|---|---|---|---|
| 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: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 |
台架注入陷阱(方法论,2026-10-12 起持续补充)
- 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.6sCONNECT TIMEOUT、net_connect仍返 0、[CONN] openOK照常打印,看着像芯片故障,实为主机防火墙。固定跑法:try { 关 Public → 复跑 } finally { 开 Public }。 - Phase 7 两条既有失败与改动无关,别追:① TC704
socket create #5FAIL = 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 实测)
test/host/ftp_abort_probe.py已修:raw_login/followup_ok原用匿名登录,N-19 后匿名被 530 拒 → Trap 23 两条腿假 0/5(round 1PASV 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。 - 陈旧数字:
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 09osDelay(7000)已被就绪屏障取代、Trap 10 注记失效、Trap 12 根因按 ff.c 更正——2026-10-13 重组已全部就地标注)。日期(Trap 01/02/03/04/06/07 的"2025-07"早于仓库首提交b1d050912026-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(tipeddbede)不含 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-自愈审计/实验方案.mdE12)。 - 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_eventLISTENING 分支开启 60s/5s/3;消费链路 TIM_OUT →handle_timeout_eventQUARANTINE → relisten → 端口恢复(详见第六章 B1 与docs/网络/N34-自愈审计/实验方案.mdE11/E13)。 - Socket 缓冲:默认只给 Socket 0
3;Socket 47 必须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 抓包确认,超时恢复) |