Trap21定案部分窗口重通告机制:N-7改非阻塞RX读强制排空至空,netcmd日志入库
This commit is contained in:
@@ -278,6 +278,23 @@ void net_generation_inc(void) {
|
||||
* 3. 处理连接/断开/数据接收/发送完成/超时等事件,更新 Socket 状态机
|
||||
* 4. RECV 中断为电平触发,每次 net_poll() 只处理一批中断,避免主循环饿死
|
||||
*/
|
||||
#if defined(NET_BENCH_CMD_LOG)
|
||||
/* Bench:芯片命令序列日志(2026-09-22 Trap 21 精确定位;netTask 上下文)。
|
||||
* 每个真实 SPI 命令点一行:tag=命令类别(P=poll全局中断 R=recv S=send C=close
|
||||
* A=accept L=listen N=connect U=relisten M=其他),fd=socket,t=FreeRTOS tick,
|
||||
* d=距上一条命令的 tick 间隔,x=结果/中断状态。台架专用,实验后回退。 */
|
||||
static uint32_t s_bench_prev_tick = 0U;
|
||||
static uint8_t s_bench_prev_valid = 0U;
|
||||
|
||||
static void net_bench_cmd_log(char tag, int sock, int detail) {
|
||||
uint32_t now = (uint32_t)xTaskGetTickCount();
|
||||
uint32_t delta = (s_bench_prev_valid != 0U) ? (now - s_bench_prev_tick) : 0U;
|
||||
s_bench_prev_tick = now;
|
||||
s_bench_prev_valid = 1U;
|
||||
DBG_INFO("netcmd: %c fd=%d t=%u d=%u x=%d", tag, sock, now, delta, detail);
|
||||
}
|
||||
#endif
|
||||
|
||||
int net_poll(void) {
|
||||
/* 本次轮询累计发生的事件数(连接/断开/接收/发送/超时各计 1) */
|
||||
int event_count = 0;
|
||||
@@ -310,6 +327,14 @@ int net_poll(void) {
|
||||
/* 读取全局中断状态(2 字节):读操作本身会清除 CH395F 中断并使其拉高 INT#,
|
||||
* 若此后仍有新中断,CH395F 会再次拉低 INT#,由下一次 net_poll() 处理 */
|
||||
int_status = ch395f_get_glob_int_status_all();
|
||||
#if defined(NET_BENCH_CMD_LOG)
|
||||
/* 仅状态跳变时记(电平型 RECV_OK 会每 poll 重复,全记会冲爆日志) */
|
||||
static uint16_t s_bench_last_glob = 0xFFFF;
|
||||
if (int_status != s_bench_last_glob) {
|
||||
s_bench_last_glob = (uint16_t)int_status;
|
||||
net_bench_cmd_log('P', -1, (int)int_status);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* 无有效中断(读到的状态为 0),提前退出 */
|
||||
if (int_status == 0) {
|
||||
@@ -330,6 +355,14 @@ int net_poll(void) {
|
||||
|
||||
/* 读取该 Socket 的中断状态寄存器(SINT) */
|
||||
uint8_t sock_int = ch395f_get_sock_int_status(i);
|
||||
#if defined(NET_BENCH_CMD_LOG)
|
||||
static uint8_t s_bench_last_sint[NET_MAX_SOCKETS] = { 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF };
|
||||
if (sock_int != s_bench_last_sint[i]) {
|
||||
s_bench_last_sint[i] = sock_int;
|
||||
net_bench_cmd_log('p', i, (int)sock_int);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* 连接建立事件:交给状态机处理(监听态/客户端/数据 Socket 分别处理) */
|
||||
if (sock_int & CH395F_SINT_STAT_CONNECT) {
|
||||
@@ -497,6 +530,18 @@ void net_process_messages(void) {
|
||||
break;
|
||||
}
|
||||
|
||||
#if defined(NET_BENCH_CMD_LOG)
|
||||
{
|
||||
char bench_tag = (msg.type == NET_MSG_RECV) ? 'R' :
|
||||
(msg.type == NET_MSG_SEND) ? 'S' :
|
||||
(msg.type == NET_MSG_CLOSE) ? 'C' :
|
||||
(msg.type == NET_MSG_ACCEPT) ? 'A' :
|
||||
(msg.type == NET_MSG_LISTEN) ? 'L' :
|
||||
(msg.type == NET_MSG_CONNECT) ? 'N' : 'M';
|
||||
net_bench_cmd_log(bench_tag, msg.sockfd, (int)msg.result);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* 结果回传:成功=字节数/描述符;失败=具体错误码(如 NET_ERR_WOULDBLOCK)。
|
||||
* 注意消息体按值拷入队列——调用方看不到 netTask 对 msg 的修改,结果只能搭
|
||||
* 通知值这趟车带回;**整个 32 位都是结果**(若压入序列号等高 16 位标签,
|
||||
@@ -2387,6 +2432,9 @@ static int alloc_socket(void) {
|
||||
* 3. 连接成功后使能 KeepAlive 并触发 NET_EVENT_CONNECTED 回调
|
||||
*/
|
||||
static void handle_connect_event(net_sock_t *p_sock) {
|
||||
#if defined(NET_BENCH_CMD_LOG)
|
||||
net_bench_cmd_log('a', (int)p_sock->ch395_sock, 0);
|
||||
#endif
|
||||
if (p_sock->state == NET_SOCK_STATE_LISTENING) {
|
||||
/* 单连接:收到 CONNECT 中断 = 自身有连接到达,转为 ESTABLISHED */
|
||||
DBG_INFO("sock%d standalone accept", p_sock->ch395_sock);
|
||||
@@ -2434,6 +2482,9 @@ static void handle_connect_event(net_sock_t *p_sock) {
|
||||
* 3. 触发 NET_EVENT_DISCONNECTED 回调
|
||||
*/
|
||||
static void handle_disconnect_event(net_sock_t *p_sock) {
|
||||
#if defined(NET_BENCH_CMD_LOG)
|
||||
net_bench_cmd_log('d', (int)p_sock->ch395_sock, 0);
|
||||
#endif
|
||||
int sockfd = (int)(p_sock - s_net_socks);
|
||||
|
||||
if (p_sock->state == NET_SOCK_STATE_LISTENING) {
|
||||
@@ -2510,6 +2561,9 @@ static void handle_send_event(net_sock_t *p_sock) {
|
||||
* 4. 触发 NET_EVENT_TIMEOUT 回调
|
||||
*/
|
||||
static void handle_timeout_event(net_sock_t *p_sock) {
|
||||
#if defined(NET_BENCH_CMD_LOG)
|
||||
net_bench_cmd_log('x', (int)p_sock->ch395_sock, 0);
|
||||
#endif
|
||||
if (p_sock->state == NET_SOCK_STATE_CONNECTING) {
|
||||
/* TCP Client 连接超时 */
|
||||
DBG_ERROR("sock%d CONNECT TIMEOUT", p_sock->ch395_sock);
|
||||
|
||||
+2
-2
@@ -9,11 +9,11 @@
|
||||
|
||||
| # | 事项 | 发现 | 证据与现状 | 出处 |
|
||||
|---|---|---|---|---|
|
||||
| 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 金丝雀 + 注释留痕。**2026-09-22 主因定案(排空节拍剂量 + 最小扰动台架)**:触发 = 前序突发 + 超容突发 + **排空后 ~50–100ms 内的第二条 RX 命令**(满态持续 60ms 单独不触发 40/40 过;单条 GET_RECV_LEN 即触发 10/10 停摆);生产 STOR 块循环(每 1KB 一条 `net_recv`)为结构性第二 RX → 本条停摆**存在 MCU 侧缓解路径**(N-7 排空后 RX 守卫);数据预算缩短保留为兜底方案(N-7 落地后本决策项紧迫性下降)。同台架副产品见 N-6。**残留决策项**:冻结不自恢复(唯一恢复 = 断连 + 客户端重试,重连路径台架已验证);lftpd 数据通道停摆预算现为 30s/读(`LFTPD_TOTAL_BUDGET_MS`,与控制行共用)——是否将数据通道单独缩短至 10–15s 以压缩停摆暴露(代价:慢速健康传输亦被中断;机器客户端不受影响)待用户决策 | `CH395F_Trap_Records.md` Trap 21;`错误码传播审计.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 金丝雀 + 注释留痕。**2026-09-22 精确定位轮定案(11 组台架 110+ 运行 + netcmd 命令序列日志)**:真致命类 = **部分窗口重通告(排空后缓冲残留)**——大 Δ 部分排空(1KB 块读,C/C0 配方)10/10 确定性停摆(零第二命令也致命);小 Δ(64B)部分读呈相位刀口(一次性落点 10/10 过 vs 5/5 停摆仅数 ms 时移之差)且持续重复必停摆(S1–S4/V1/V1F 50/50);排空至空(窗口→满)与 Δ=0 状态读/TX 恒安全(40/40+,含 10ms 节拍 E0)→ 本条停摆**存在 MCU 侧修复路径**(N-7 = 非阻塞 RX 读强制排空至空,无需时间守卫);数据预算缩短保留为兜底方案(N-7 落地后本决策项紧迫性下降)。同台架副产品见 N-6。**残留决策项**:冻结不自恢复(唯一恢复 = 断连 + 客户端重试,重连路径台架已验证);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 注¹ |
|
||||
| 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,待批准实施)**——排空整缓冲(recv≥1KB = 缓冲曾满 → 窗口重通告中)后,该 socket 的后续 RX 命令暂缓至:下一个 RECV_OK(数据真到达 = 芯片再武装完成,事件驱动)或 ~120ms 冷却到期(先到为先);TX 路径不受限。台架定案:第二条 RX 落点 ≈ 排空后 45ms → 10/10 停摆(V1);≈95ms → 10/10 干净(E 系列) | 2026-09-22(V1 最小扰动台架 + E 系列剂量台架,主因定案) | 实施要点:netTask 内 per-socket 排空后状态位(置位 = 本轮排空读到 ≥1KB;清除 = 该 socket RECV_OK 或超时);守卫期内到达的 RX 消息不触芯片(挂起/重排,语义 = DONTWAIT 的 WOULDBLOCK 或按超时等待);实施后台架回归 = V1 配方(Phase 6+11+60ms)由 10/10 停摆转为 10/10 通过。生产受益:lftpd 数据通道 STOR 块循环(结构性第二 RX,间隔 15–70ms 落危险区) | `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 命令序列日志) | 实施要点:非阻塞(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 块循环 |
|
||||
|
||||
## 存储 / 日志管线
|
||||
|
||||
|
||||
@@ -643,12 +643,12 @@ sequenceDiagram
|
||||
**通过标准**:4096B 完全匹配。任一失败 → 标记 FAIL
|
||||
**覆盖原则**:DP-04(2KB 接收缓冲边界 + 单次 DMA 上限) |
|
||||
|
||||
> **TC-NET-604 复现/对照规则(2026-09-22 主因定案:前序突发 + 超容突发 + 填周期内第二条 RX 命令;见 Trap 21)**:
|
||||
> - **复现(预期 10/10 停摆)**:数据 socket 带**第二 RX 命令路径**的构建 + `tcp_client` 全序列——Phase 6/9/11 组合最稳;最小复现配方 = **Phase 6+11 + 60ms 排空间隔**(V1 配方,10/10);
|
||||
> - **对照(预期 10/10 全过)**:Phase 6-only 构建(单 RX 命令路径)+ `tcp_client` 每轮间隔 ≥15s——排空间隔 ≤60ms 均干净(E 系列 40/40,含满态持续 60ms+);
|
||||
> **TC-NET-604 复现/对照规则(2026-09-22 精确定位轮定案:致命类 = 部分窗口重通告(排空后缓冲残留);见 Trap 21)**:
|
||||
> - **复现(预期 10/10 停摆)**:部分排空构型 = 64×10 + 1460B 填充 + 1024B 部分排空(C/C0 配方,Phase 6-only 构建、零第二命令,10/10 确定性停摆 = 生产 lftpd 块循环的台架复现);旧三因子配方(Phase 6/9/11 组合 + 4096B 超容突发;V1 配方 = Phase 6+11+60ms)10/10 维持;
|
||||
> - **对照(预期 10/10 全过)**:Phase 6-only 排空至空构建 + `tcp_client` 每轮间隔 ≥15s——排空至空(窗口→满)任意节拍均安全,10/20/40/60ms 全过(E 系列 40/40,含 10ms E0);Δ=0 状态读/TX 任何偏移/密度均安全(B 系列);
|
||||
> - **Phase 6-only 构建禁止无间隔连跑**:<~1s 重连间隔会触发 Trap 22 失同步(自 run2 起:无 standalone accept、`send -1`、其后全部 connection refused)——快速重连场景须用带第二 RX 路径的构建或留 15s 间隔;
|
||||
> - **MSS/块配额改动前**:先跑复现序列(基线 = 10/10 停摆,为芯片侧已知角,改动后不得恶化/不得出现新停摆模式)+ 新连接独立 4096B 对照(必须过,MSS=512 实验在此失败被回退)+ Phase 6-only 15s 对照(必须过)。
|
||||
> - **注意**:停摆主因已定案为 **MCU SPI 命令节奏**(排空后 ~50–100ms 内的第二条 RX 命令),非几何/时长问题——存在 MCU 侧守卫方案(`docs/待处理内容.md` N-7,未实施);N-7 落地前勿以"芯片侧不可修"为由调整几何参数。
|
||||
> - **注意**:停摆主因已定案为 **部分窗口重通告**(部分排空致缓冲残留——大 Δ 确定性致命、小 Δ 相位刀口/持续重复必停摆),与命令条数/窗口时长无关——存在 MCU 侧修复方案(`docs/待处理内容.md` N-7 = 非阻塞 RX 读强制排空至空,无需时间守卫,未实施);N-7 落地前勿以"芯片侧不可修"为由调整几何参数。
|
||||
|
||||
### TC-NET-605: 断开重连
|
||||
|
||||
|
||||
@@ -540,14 +540,32 @@ NET 层 TCP Client(阶段 7)`net_connect()` 返回 0,但 `net_poll()` 报
|
||||
## 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 台架,二次 2×2 台架复核再 10/10;三因子模型见下)。
|
||||
单条 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 维持。
|
||||
|
||||
### 根因
|
||||
CH395F 片内 TCP 状态机的角(芯片固件,MCU 侧无法修补),与"窗口 = 接收缓冲剩余空间"的硬连线模型共同构成触发结构:
|
||||
CH395F 片内 TCP 状态机的角(芯片固件,MCU 侧无法修补)。2026-09-22 精确定位轮(11 组台架、110+ 运行、芯片命令序列级 netcmd 日志)定案:致命变量是 **RX 排空的结束态(排空后缓冲是否残留)**,与命令条数/类型/窗口时长无关(逐组实证见下):
|
||||
|
||||
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. **触发模式(2026-09-22 二次台架:2×2 交叉 + 排空节拍剂量 + 最小扰动实验,均为确定性而非随机竞态)**:
|
||||
2. **冻结机制(实证签名)**:MCU 的 RX 数据读命令使芯片 RX 缓冲**残留非空**(窗口重通告到部分值——既非 0 也非满)时,芯片 TCP 再武装状态机被破坏:**芯片发送路径永久死亡**(已写入发送缓冲的数据永不出线——SPI 写被接受但线上无段,ACK/窗口更新随之停止),**接收路径与 MCU 侧 SPI 交互完全健康**(命令序列日志:冻结后 MCU 命令流节奏完整,继续到达的数据芯片照常接收、MCU 照常排空),连接保持 ESTABLISHED → 单向死亡;对端剩余数据滞留对端发送缓冲(窗口视图冻结在 0)。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. **致命性模型(2026-09-22 精确定位轮:11 组台架、110+ 运行、芯片命令序列级 netcmd 日志;逐组实证,非日志/生产时基;Δ = 本次读移走的字节数,"残留" = 排空后缓冲剩余)——取代下方旧"三因子/50–100ms"模型**:
|
||||
|
||||
| RX 读模式(构建) | n | 结果 |
|
||||
|---|---|---|
|
||||
| 排空至空(窗口→满),10/20/40/60ms 节拍,任意 Δ(64/1460/2048B;E 系列) | 40/40 | **全过**(含 10ms 节拍 E0:满→空→满 正常循环,排空后 10ms 再排空安全) |
|
||||
| Δ=0 纯状态读(GET_RECV_LEN 空读 / SINT 中断读 / poll 每 tick)与 TX 命令(E 系列 / B 系列 0.4s 静置 ~40 次状态读) | 40/40+ | **全过**(任何偏移/密度) |
|
||||
| 满(零窗)排空 + 一次性 64B 部分读 @~50ms(v3 O1,10ms 栅格) | 10/10 | 过(相位刀口,见下) |
|
||||
| 同上 + netcmd 命令日志(netTask 时移数 ms,O1f) | 5/5 | **全冻结**(一次性落点相位翻转) |
|
||||
| 满排空 + 每 tick 64B 部分读(持续 Δ=64):10ms(S1) / 90ms(S2,+80ms) / 130ms(S3,+120ms) / 170ms(S4,+160ms) / 60ms(V1、V1F) | 50/50(6 构建) | **全冻结**(每事件相位风险累积) |
|
||||
| **部分排空(1460 中排 1024、留 436 残留)、零第二命令(C0)** | **10/10** | **全冻结(第一条命令即致命)** |
|
||||
| 部分排空 + 一次性 64B 部分读(C) | 10/10 | 全冻结 |
|
||||
| 快速重连链(20 连接、0.6s 间隔、窗口内状态读 + canary;F1 链) | 20/20 | 全过("快重连预置"假说证伪) |
|
||||
|
||||
→ **部分窗口重通告(排空后缓冲残留)= 致命类**:小 Δ(64B)呈**相位刀口**——残留态持续 ~10ms 多为安全(v3 O1 10/10)、~20ms 多为致命(O1f 5/5)、45–60ms 多为致命(V1/V1F 10/10):芯片再武装内部子相位周期性,且对构建/时基对齐敏感(O1f:命令日志引入数 ms 时移即 10/10 过 → 5/5 冻);大 Δ(1KB 级 = 生产 lftpd 块读)为**确定性致命**(C0:残留态 10ms 后即被下一次排空清掉仍 10/10 冻结);小 Δ 持续重复 = 数百事件累积 → 必然(S1–S4/V1/V1F 50/50)。**排空至空(窗口→满)恒安全**(40/40,含 10ms 节拍)——芯片设计的正常循环;Δ=0 命令(状态读/中断读/TX)恒安全(B/E 系列)。
|
||||
- **再武装区间(实测下界)**:满排空后的"第二条 RX 数据读"在 +0/+80/+120/+160ms 偏移位均致命(S1–S4 各 10/10)→ 脆弱区间 **≥160ms**,内部致命子相位周期性、相位敏感 → **不存在可移植的单值窗口**;旧"~50–100ms"为两点外推(V1 45ms 致命 / E 95ms 安全),上界被 S3/S4(120/160ms 仍致命)证伪。
|
||||
- **生产触发(实证复现)**:lftpd 数据通道 STOR 块循环 = 每 1KB 一条 `net_recv(1024)`(部分排空、留 ~1KB 残留、间隔 15–70ms)= C/C0 模式 → **10/10 确定性冻结**(C 系列 = 生产停摆的台架复现,无需任何其他因子);RETR(下载,设备为发送方)不经芯片 RX 排空路径,不受本机制影响。
|
||||
- 历史条目(前轮台架,与新模型自洽):MSS=512 时新连接 4096B 突发(8×512 段)确定性停摆恒剩 64B = 高段密度下部分读事件密集;单轮 ≤2KB 无溢出突发在任何几何/命令结构下正常(排空即至空)。
|
||||
|
||||
4. **(历史记录:2026-09-22 二次台架"三因子/第二条 RX 命令"模型——已被上方致命性模型取代,保留作过程记录)**
|
||||
- **三因子模型(MSS=1024 基线;主因已定案)**:① 同连接前序突发(芯片 RX 状态预置;高段密度可等效替代,见 MSS=512 条)+ ② 后续突发超缓冲容量(4096B > 2KB,需 ≥2 个填周期)+ ③ **填周期内第二条 RX 命令**(排空之后同一 socket 的任何额外 SPI 命令——纯状态读 GET_RECV_LEN 即足够)——**三者齐备才触发,缺一不触发**:
|
||||
|
||||
| 变体 | 与 Phase 6-only(单 RX 命令路径)的差异 | 结果 |
|
||||
@@ -557,31 +575,31 @@ CH395F 片内 TCP 状态机的角(芯片固件,MCU 侧无法修补),与"
|
||||
| Phase 6 + 11 + 60ms 间隔(V1,最小扰动) | 每周期加一条 GET_RECV_LEN 状态读(落点 ≈ 排空后 45ms) | **10/10 停摆**(4096B 步,落点与经典模式一致) |
|
||||
| 多路 RX 命令、无前序突发 | 新连接独立 4096B | 过(31ms;probe_mss 对照——前序突发因子缺失) |
|
||||
|
||||
- **主因(本轮定案)**:冻结与缓冲"满"的持续时长**无关**——排空间隔拉到 60ms、缓冲满态持续 60ms+,单命令路径仍 40/40 干净("满了滞后不舀"假设被证伪)。芯片 RX 状态机敏感的是**排空(窗口重通告/再武装)之后 ~50–100ms 内到达的第二条 socket 命令**:V1 第二条命令落点 ≈ 排空后 45ms → 10/10 卡死;E 系列下一条命令落点 ≈ 95ms → 10/10 干净。旧"负载"因子据此降级:重载构建触发是因为 Phase 9/11 **每周期多一条 RX 命令**,而非拖慢排空;"队列竞争"表述彻底作废(队列消息对芯片不可见,MCU 到达芯片状态机的唯一通道 = SPI 命令总线)。
|
||||
- **主因(历史记录:二次台架时的"第二条 RX 命令"定案——已由上方致命性模型精化:真致命类 = 排空后缓冲残留的部分窗口重通告,"第二条命令"是其在当时构建中的表面形态)**:冻结与缓冲"满"的持续时长**无关**——排空间隔拉到 60ms、缓冲满态持续 60ms+,单命令路径仍 40/40 干净("满了滞后不舀"假设被证伪,此条仍然成立)。旧结论"排空后 ~50–100ms 内到达的第二条 socket 命令"为两点外推(V1 落点 45ms → 10/10 卡死;E 系列落点 ≈95ms → 10/10 干净),上界被精确定位轮 S3/S4(+120/+160ms 仍致命)证伪;一次性第二条命令的生死取决于落点相位(v3 O1 10/10 过 / O1f 5/5 冻,同一构型仅时移数 ms)。
|
||||
- MSS=512:新连接 4096B 突发(8×512 段,4 段装不进 2KB 缓冲)**确定性停摆**,恒剩 64B 未达(4032/4096,2/2 同值)——段密度维度:满周期内段数越多,突发内自我预置越快,越易触发(该实验在多 RX 命令构建上执行,与主因模型自洽)。
|
||||
- 单轮无溢出突发(≤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。
|
||||
5. **排除项**:① 驱动层"超限读静默丢弃"排除——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;④ "快速重连残留预置"假说排除——受控 20 连接 0.6s 间隔链 20/20 干净(v2 轮 1/9 单例未复现,按孤立事件处理);⑤ keepalive 命令排除——S1 纯探针构建(无 keepalive)10/10 冻结,V1F(带 keepalive)10/10 = 与部分读机制一致,keepalive 既非触发也非保护。
|
||||
|
||||
### 解决(2026-09-22;同日主因定案更新:存在 MCU 侧缓解路径)
|
||||
### 解决(2026-09-22;精确定位轮定案更新:MCU 侧修复 = 非阻塞 RX 读强制排空至空)
|
||||
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. **MCU 侧主因缓解(2026-09-22 主因定案后新增,待批准 → N-7)**:主因 = 排空后 ~50–100ms 内的第二条 RX 命令(台架 V1 10/10 / E 系列 40/40 定案),可在 net 层**关死该窗口**:排空整缓冲后,该 socket 的后续 RX 命令暂缓,直至 (a) 下一个 RECV_OK(数据真到达 = 芯片再武装完成,事件驱动、零额外时延)或 (b) ~120ms 冷却到期;TX 路径不受限(台架证实排空后的 TX 安全)。生产数据通道为主要受益者:lftpd `receive_file` 块循环每 1KB 一条 `net_recv`(结构性每填周期第二条 RX,间隔 15–70ms 正落台架实证危险区)→ 守卫将其变为事件驱动排空,消除生产停摆主因。
|
||||
4. **MCU 侧修复(2026-09-22 精确定位轮定案 → N-7,待批准实施)**:致命类 = 部分窗口重通告(排空后缓冲残留)→ net 层**强制一切非阻塞(DONTWAIT)RX 读排空至空**:RX 读上限强制为接收缓冲全量(2048B)——"只读一部分"经安全 API 不可表达;应用按自身块粒度消化排空结果(lftpd `receive_file` 块循环 `net_recv(1024)` → `net_recv(2048)`,仍按 1KB 块处理,接收缓冲需 ≥2KB)。依据:排空至空任意节拍均安全(E 系列 40/40,含 10ms E0)→ **无需时间守卫/冷却**(旧"第二命令暂缓 + 120ms 冷却"设计作废:C0 证明部分排空自身即致命(零第二命令也 10/10 冻结),E0 证明排空至空后 10ms 再来一次排空也安全——时间守卫既不足又多余);Δ=0 命令(状态读/中断读/poll/TX)豁免(实测安全,不过度阻塞)。残留约束:阻塞/pending 读若以部分结果落地且未即刻排空至空,仍属致命类(pending 完成点同样强制全量,或应用同 tick 内排空)——N-7 实施时处理。
|
||||
5. **兜底缓解(决策项,未实施)**:冻结不自恢复(对端重传 10s 无效;芯片无限期保持连接)。唯一恢复 = 断开受影响连接(释放芯片 per-socket 状态)+ 对端重试(新连接成功率高;台架实测停摆后重连回显正常)。现 lftpd 数据路径预算 `LFTPD_TOTAL_BUDGET_MS=30000`(每次读 30s)即实际停摆超时——若将**数据通道**预算单独缩短(10–15s,与控制行预算分离),可把停摆暴露时间从 30s 压缩到客户端重试路径;代价 = 慢速但健康的传输也会被中断(机器客户端不受影响,人工客户端受影响)→ 已列 `docs/待处理内容.md` N-1 待决策(N-7 落地后本项紧迫性下降,保留为兜底)。
|
||||
|
||||
### 注意事项
|
||||
- 复现模式 = 三因子齐备(前序突发 + 超容突发 + 填周期内第二条 RX 命令,见触发模式表):**台架复现 TC-NET-604 需要数据 socket 带第二 RX 命令路径的构建**(Phase 6/9/11 组合,或最小复现配方 **Phase 6+11 + 60ms 排空间隔**,V1 配方 10/10);Phase 6-only 构建(单命令路径)在 ≤60ms 排空间隔下均不触发(E 系列 40/40 过)。生产主要风险场景 = **lftpd 数据通道 STOR(上传)**:块循环每 1KB 一条 `net_recv`(结构性第二 RX)+ 快速流式客户端 + 文件 >2KB → 块循环间隔(15–70ms)落入实证危险区(~50–100ms);RETR(下载,设备为发送方)不经过芯片 RX 缓冲填满路径,不受本机制影响;纯新连接大突发(4×1024)在 MSS=1024 下不触发(缺前序预置)。
|
||||
- 复现模式(2026-09-22 精确定位轮更新):**部分排空(读后缓冲残留)= 唯一实证致命模式**——最小台架复现 = 64×10 + 1460B 填充 + 1024B 部分排空(C/C0 配方,Phase 6-only 构建,零第二命令,10/10 冻结);旧"三因子"配方(F1 组合 + 4096B 超容突发)10/10 维持,其中 Phase 9/11 每 tick 64B 读、V1 每周期状态读后的 64B 读、生产块循环 1KB 读均为"部分读"模式的不同剂量形态。生产主要风险场景 = **lftpd 数据通道 STOR(上传)**:块循环每 1KB 一条 `net_recv`(大 Δ 部分排空,间隔 15–70ms)= C 配方 10/10;RETR(下载,设备为发送方)不经芯片 RX 排空路径,不受本机制影响;N-7(排空至空强制)落地后 STOR 块循环转为 E 系列模式(40/40 安全)。
|
||||
- 与 Trap 22 的鉴别(两者都可在断连/重监听后出现):本条 = RX 路径冻结(有 standalone accept、前段回显正常、其后 RECV_OK 永不再来、无 send 失败);Trap 22 = 连接状态失同步(缺 standalone accept + `send -1` + `WARNING: listen` + 监听死亡)。
|
||||
- 冻结是静默的:无 RST、无芯片中断、MCU 侧只见"缓冲空 + 无 RECV_OK + 连接存活"(与正常空闲完全同貌)——MCU 无直接信号,只能靠应用层数据预算(lftpd 30s / 客户端自身超时)发现。
|
||||
- 冻结对 MCU 是静默的:无 RST、无芯片断开事件、无 send 失败;精确定位轮命令序列日志修正旧表述——MCU 侧 RX 路径照常工作(冻结后到达的数据可正常排空,命令流节奏完整),**死的是芯片 TX 路径**(已写入的发送段永不出线);MCU 无直接信号,只能靠应用层数据预算(lftpd 30s / 客户端自身超时)或对端视角(收不到回显/ACK)发现。
|
||||
- E-02 审计"STOR 方向缓冲满 + 重传耗尽 → 必断"的预警方向成立(STOR 方向 = 致命),机制修正为本条 RX 冻结(非 RST 拆连)。
|
||||
- WCH FAQ"发送缓冲必须大于 MSS"为严格不等式;本设计发送缓冲 = MSS(1KB = 1024)取等值——台架实测芯片在缓冲边界可正常成包(L3/L8 基线在 1KB 分块写入下全部正常),该条按保守表述处理,非运行约束。
|
||||
- 本条的冻结是确定性触发序列缺陷(特定段数/段长/前序状态组合),非低概率随机竞态——发现新停摆模式时先核对"同连接突发结构",再怀疑 MCU 侧。
|
||||
- 本条的冻结是确定性触发模式缺陷(排空结束态 = 缓冲残留的部分窗口重通告;小 Δ 落点另有相位刀口),非低概率随机竞态——发现新停摆模式时先核对"是否存在部分排空(net_recv 上限 < 缓冲现有字节数,读后缓冲非空)",再怀疑 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 分包探针,本地工具,不提交)。
|
||||
`Drivers/BSP/CH395F/ch395f.h`(布局解耦 + MSS=1024 维持);`Drivers/BSP/CH395F/ch395f.c`(OVERSIZE 金丝雀);`Drivers/BSP/NET/net_socket.c`(MSS 下发注释;netcmd 芯片命令序列日志——bench 开关 `NET_BENCH_CMD_LOG` 默认关,2026-09-22 精确定位轮工具);`test/ch395f_socket_test.py`(复现:tcp_client 全序列);`MDK-ARM/probe_mss.py`(MSS/芯片 TX 分包探针)+ `probe_{boundary,status,partial,fastrecon}.py`、`probe_classify.py`、`netcmd_extract.py`(精确定位轮台架/离线判读/命令序列提取工具,均本地不提交)。
|
||||
|
||||
### 发现时间
|
||||
2026-09-22(TC-NET-604;根因确认、MSS=512 证伪与回退、布局解耦同日完成,提交于 wip/v2-pending-recv 分支)。
|
||||
2026-09-22(TC-NET-604;根因确认、MSS=512 证伪与回退、布局解耦同日完成,提交于 wip/v2-pending-recv 分支)。2026-09-22 精确定位轮:11 组台架 110+ 运行(E/O/B/F1/S1–S4/C/C0/V1F/O1f)定案"部分窗口重通告"致命性模型,N-7 设计由"排空后 RX 守卫 + 120ms 冷却"改为"非阻塞 RX 读强制排空至空"(无时间守卫),netcmd 命令序列日志工具入库(默认关)。
|
||||
|
||||
## Trap 22:重监听后快速重连丢失 CONNECT 中断 → 监听静默死亡
|
||||
|
||||
|
||||
Reference in New Issue
Block a user