47 KiB
CH395F 驱动陷阱记录
记录开发过程中遇到的所有坑、根因、解决方案,避免重复踩坑。
Trap 01:Socket 4~7 自动分配不到
现象
多连接模式下最多接受 3 个客户端(Socket 13),Socket 47 的 CONNECT 中断从不触发,新客户端 SYN 被静默丢弃。
根因
net_listen() 中数据 Socket 的配置顺序错误。CH395F 在 TCP_LISTEN 时刻一次性扫描当前所有可用 Socket 并建立内部查找表。如果数据 Socket 4~7 在 TCP_LISTEN 之后才配置缓冲区/协议/端口,自动分配逻辑不认识它们。
正确顺序
- 先配置数据 Socket 1~7:
SET_SEND_BUF → SET_RECV_BUF → SET_SOUR_PORT → SET_PROTO_TYPE_TCP - 最后配置监听 Socket 0:
SET_SEND_BUF → SET_RECV_BUF → SET_PROTO_TYPE_TCP → SET_SOUR_PORT → OPEN_SOCKET → TCP_LISTEN
注意事项
- 数据 Socket 不调
OPEN_SOCKET,由 CH395F 在连接到达时自动打开 - DISCONNECT 后 CH395F 自动关闭 Socket,但保留协议/端口/缓冲区配置,可被再次分配
- 手册 §9.2.6 要求数据 Socket 先设源端口再设协议类型(与监听 Socket 顺序相反)
对应文件
Drivers/BSP/NET/net_socket.c → net_listen()
发现时间
Phase 8 压力测试,2025-07
Trap 02:数据 Socket 缓冲区重叠
现象
多连接模式下多个 Socket 数据互相覆盖,收到错误数据或客户端挂死。
根因
CH395F 默认只为 Socket 03 各分配独立收发缓冲(共 48 块 × 512B 用满,见手册 §5.45),Socket 47 为零块。net_listen() 未对 Socket 47 显式分配独立缓冲区块,后续代码误用相同块号(如 2831)导致数据重叠。
解决方案
每个数据 Socket 独占 4 个缓冲区块(2 发 + 2 收),按 ds * 4 基址分配,确保不重叠:
| Socket | 发送块 | 接收块 |
|---|---|---|
| 1 | 4~5 | 6~7 |
| 2 | 8~9 | 10~11 |
| 3 | 12~13 | 14~15 |
| 4 | 16~17 | 18~19 |
| 5 | 20~21 | 22~23 |
| 6 | 24~25 | 26~27 |
| 7 | 28~29 | 30~31 |
对应文件
Drivers/BSP/NET/net_socket.c → net_listen()
发现时间
Phase 8 压力测试,2025-07
Trap 03:DHCP 包 xid 偏移错误
现象
Python DHCP Server 收到 DISCOVER 包后无法匹配 Offer/Ack 的 xid,握手失败。
根因
错误使用了 data[232:236] 作为 xid(Wireshark 某旧版显示误导),实际 DHCP 标准偏移为 data[4:8]。
正确偏移
- xid:
data[4:8] - msg_type:
data[242] - client_mac:
data[28:34] - magic cookie:
data[236:240]
对应文件
test/ch395f_socket_test.py → DHCP server 模式
发现时间
Phase 6, 2025-07
Trap 04:UDP 发送后必须等待 SENDBUF_FREE
现象
UDP 发送后立即执行接收操作,接收长度始终为 0 或数据错误。
根因
CH395F 手册要求每次 WRITE_SEND_BUF 后必须等待 SINT_STAT_SENBUF_FREE 中断,否则下次写入或接收操作可能失败。
解决方案
每次 ch395f_write_send_buf() 后轮询 ch395f_get_sock_int_status() 检查 SINT_STAT_SENBUF_FREE 标志。
对应文件
Drivers/BSP/CH395F/ch395f.c / test code in ch395f_test.c
发现时间
Phase 6 广播宣告模式,2025-07
Trap 05:CH395F 与 RTL8305NBI 自动协商不兼容
现象
ch395f_get_phy_status() 始终返回 PHY_DISCONN,链路无法建立。
根因
CH395F PHY 与 RTL8305NBI-CG 直连时自动协商失败。
解决方案
初始化协议栈后强制设为 100M 全双工:
ch395f_set_phy(CH395F_PHY_100M_FULL);
对应文件
Drivers/BSP/CH395F/ch395f.c
发现时间
项目初期硬件联调
Trap 06:TCP KeepAlive 参数必须为 500ms 倍数
现象
设置 KeepAlive 后 TCP 连接几秒内 TIMEOUT 断开。
根因
CH395F 内部定时器以 500ms 为基准单位,传入非 500ms 倍数的值导致未定义行为。
且 IDLE 必须 > INTVL。
正确值
ch395f_set_keepalive_idle(60000); // 60s(500 倍数)
ch395f_set_keepalive_intvl(5000); // 5s(500 倍数)
ch395f_set_keepalive_cnt(3); // 3 次
对应文件
Drivers/BSP/NET/net_socket.c → net_init()
发现时间
Phase 2 测试,2025-07
Trap 07:TCP 关闭重连直接 close,不要 disconnect
现象
disconnect → close 后 Socket 卡在 FIN_WAIT_2 数分钟,无法重新打开。
根因
disconnect 发送 FIN 将 Socket 推入 FIN_WAIT_2,此后 close 不再发 RST,必须等远端发 FIN 才能关闭。
解决方案
直接调用 ch395f_close_socket(),在 ESTABLISHED 或 CLOSE_WAIT 下会发 RST 立即终止。
关闭后轮询 ch395f_get_socket_status() 等待 sock=0x00 再 open_socket。
发现时间
Phase 2 TCP Client 测试,2025-07
Trap 08:RECV 中断电平触发,避免无限循环
现象
net_poll() 中 RECV 中断无限触发,主循环(echo 等)永远得不到执行。
根因
CH395F 的 RECV 中断是电平触发的——只要接收缓冲区有数据就保持 INT# 低电平。
do { ... } while(int_status != 0) 会无限循环。
解决方案
net_poll() 使用 do { ... } while(0) 每次只处理一批中断,由主循环负责读取数据。
对应文件
Drivers/BSP/NET/net_socket.c → net_poll()
附:CH395F 驱动关键点
初始化顺序
- 必须按手册9.2.1节顺序:
SET_MAC → SET_IP/GWIP/MASK → INIT_CH395 → SET_PHY - IP/网关/掩码必须在
INIT_CH395之前设置,INIT 会读取并锁定当前寄存器值到协议栈 SET_PHY必须在INIT_CH395之后,复位 MAC/PHY 建立物理链路,不影响已锁定的协议栈参数CMD_PING_ENABLE不需要显式调用,INIT 后默认可用
SPI 通信
- 每次 SPI 事务需调用
ch395f_spi_begin()/ch395f_spi_end()包裹 - 大数据量收发使用 SPI2 DMA:
ch395f_write_send_buf()和ch395f_read_recv_buf()已改造为 DMA 批量传输(DMA1_Stream3 RX, DMA1_Stream4 TX),命令和配置操作仍使用逐字节轮询 - DMA 缓冲区:
s_spi2_dma_tx_buf[1500]/s_spi2_dma_rx_buf[1500],4 字节对齐
TCP 参数配置(必须在 INIT_CH395 之前)
- 重传参数:
SET_RETRAN_COUNT(默认12次,最大20)和SET_RETRAN_PERIOD(默认500ms,最大1000ms),总重传时间 = 次数 × 周期 - KeepAlive 参数必须为 500ms 的倍数:
IDLE(默认20000ms)、INTVL(默认15000ms),且 IDLE > INTVL;传入非 500 倍数的值导致 CH395F 内部定时器异常 - KeepAlive 默认关闭:需在
SINT_STAT_CONNECT后调用ch395f_set_keepalive_enable(sock, 1)启用
Socket 4~7 缓冲区
- CH395F 默认只给 Socket 0
3 分配独立收发缓冲(共 48 块 × 512B 用满),Socket 47 为零块 - 多连接模式下使用 Socket 4~7 时,必须在
open_socket之前显式分配:ch395f_set_send_buf(sock, start_block, count)ch395f_set_recv_buf(sock, start_block, count)
- 不分配会导致发送数据为固定垃圾内容(
0x0028填充)、接收缓冲区无法存储数据
TCP 关闭重连
- 直接调用
ch395f_close_socket(),不要先调ch395f_tcp_disconnect() - ❌
disconnect → close:disconnect 发 FIN 推入 FIN_WAIT_2,close 不再发 RST,Socket 卡住数分钟 - ✅
close直接:在 ESTABLISHED 或 CLOSE_WAIT 下 close 发 RST 立即终止,瞬间回到 CLOSED - 关闭后需轮询
ch395f_get_socket_status()等待sock=0x00再open_socket
SOCK_TIMEOUT 处理
- 长时间无数据时可能触发
SINT_STAT_SOCK_TIMEOUT,不应视为致命错误——记录日志后继续操作即可,不要因此关闭 Socket
UDP 模式区分
- DesIP=0xFFFFFFFF → UDP Server 模式:接受任意来源数据,接收数据前 8 字节为信息头(
reserved[2] src_port[2] src_ip[4]),回发前需设置SET_DES_IP和SET_DES_PORT - DesIP=具体 IP → UDP Client 模式:只接收指定 IP:Port 的数据,接收数据无信息头
UDP 发送缓冲
- 每次
ch395f_write_send_buf()后必须等待SINT_STAT_SENBUF_FREE中断,否则下次写入会被 CH395F 静默丢弃
Trap 09:PHY link up 后立即 open_socket 失败
现象
net_listen() 在 PHY_CHANGE 中断同一时刻调用 ch395f_open_socket() 返回错误码(非 BUSY 超时),导致首次 listen 失败。3 秒后重试成功。
[NET] PHY_CHANGE: 0x08 ← PHY 刚 link up (100M FULL)
[ERR] error listening on socket ← 同一秒 open_socket 失败
[FTP] waiting for connection... ← 3秒后重试成功
根因
CH395F 的 PHY link up 后,内部 TCP/IP 协议栈需要额外时间完成初始化(ARP 缓存、路由表等)。在 PHY_CHANGE 中断触发的同一 net_poll() 迭代内立即调用 OPEN_SOCKET,芯片可能返回 ERR_BUSY 或其他错误码。
解决方案
- 应用层重试兜底:FTP Server (
lftpd_start) 的while(1)循环中,listen 失败后vTaskDelay(3000)重试,3 秒足够 PHY 稳定 - 启动延时消峰:FTP 任务启动时
osDelay(7000),等 netTask 完成net_init() → PHY 协商 → 首轮 net_poll()后再发起 listen,此时 PHY 已稳定 2 秒以上,首次就成功
注意事项
- 此问题在单连接模式 (
FUN_PARA=0x00) 和多连接模式 (0x02) 下均存在,与 FUN_PARA 无关 - 如果多个应用任务同时启动 listen,可能全部首次失败、全部重试成功——建议各任务错峰启动
Trap 10:消息队列值拷贝导致结果无法回传
现象
net_send() / net_recv() / net_listen() / net_accept() / net_close() 等线程安全 API(通过消息队列委托 netTask 执行)永远返回 -1,即使 netTask 内部操作成功。
根因
CMSIS-RTOS v2 osMessageQueue 传递 net_msg_t 结构体是值拷贝。调用方 puts 后 msg.result = -1(栈变量),netTask 侧 osMessageQueueGet 拿到的是队列中的副本,修改副本的 msg.result = 0 后 xTaskNotifyGive 通知调用方。调用方的栈变量 msg.result 仍然是 -1,从未被更新。
// 调用方(ftpTask)
msg.result = -1;
osMessageQueuePut(&msg); // 值拷贝到队列
xTaskNotifyWait(¬ified); // 等待通知
return msg.result; // 永远是 -1!
// 处理方(netTask)
osMessageQueueGet(&msg); // 拿到的是队列副本
msg.result = actual_result; // 修改的是副本
xTaskNotify(...); // 通知回去了,但值丢了
解决方案
不通过消息队列回传结果,改用 FreeRTOS 通知值(notification value)携带返回值:
Producer(netTask):
xTaskNotify(msg.caller, (uint32_t)msg.result, eSetValueWithOverwrite);
Consumer(调用方):
uint32_t notified;
xTaskNotifyWait(0, 0, ¬ified, portMAX_DELAY);
msg.result = (int)notified;
注意事项
- 指针成员(
msg.buf)不受值拷贝影响——netTask 拿到的是同一个指针,可以读写调用方的缓冲区 net_connect()不使用消息队列(在 netTask 内直接调用_locked版本),不受此 bug 影响- 内核级 API(
net_send_sock/net_recv_sock/net_listen_locked)直接操作,同样不受影响
Trap 11:net_accept_locked 拒绝 standalone_accept 的 ESTABLISHED 状态
现象
FTP 客户端 TCP 连接成功(standalone accept),220 欢迎语不发送,lftpd_inet_accept() 死循环。客户端连接成功但永远收不到任何 FTP 响应。
根因
net_accept_locked() 入口检查:
if (p_listen_sock->state != NET_SOCK_STATE_LISTENING) {
return -1; // ← 直接拒绝!
}
standalone accept 后 state 已变为 ESTABLISHED,永远到不了后续的 standalone_accept 检查——死锁。
解决方案
if (p_listen_sock->state != NET_SOCK_STATE_LISTENING &&
!(p_listen_sock->standalone_accept && p_listen_sock->state == NET_SOCK_STATE_ESTABLISHED)) {
return -1;
}
允许 standalone_accept + ESTABLISHED 组合通过。
注意事项
- 不影响多连接模式(多连接监听 Socket 状态始终为 LISTENING,由数据 Socket 承载连接)
- 仅影响 standalone_accept 模式(PASV 数据通道、单连接 FTP)
Trap 12:FatFS FF_USE_LFN=0 + 路径 / 前缀 → FR_INVALID_NAME
现象
FTP STOR 上传文件时 f_open("/upload_test.txt", FA_WRITE | FA_CREATE_ALWAYS) 返回 err=6(FR_INVALID_NAME)。upload_test.txt 文件名 11+3 字符 > 8.3 限制同样触发 err=6。
根因
ffconf.h中FF_USE_LFN = 0(长文件名禁用),最大文件名 8+3 字符- FatFS 不接受
/file格式的路径(需要file或0:file) lftpd_io_canonicalize_path("/", "file")输出必然带/前缀- 根目录路径
"/"剥离后为空字符串,f_stat("")同样失败
解决方案
/* 剥离 "/" 前缀,根目录用 "." 表示 */
static const char *to_fatfs_path(const char *path) {
if (path == NULL || *path == '\0') return ".";
if (*path == '/') {
path++;
if (*path == '\0') return ".";
}
return path;
}
所有 lftpd_io_* 函数调用 FatFS 前统一使用 to_fatfs_path(path)。
注意事项
- 长文件名需设置
FF_USE_LFN = 1并配置FF_LFN_UNICODE,会显著增加 RAM 占用 CWD .和CWD /同样受此问题影响,修复后正常工作
附:FTP 支持改造完整问题清单
| # | 问题 | 类型 | 修复位置 |
|---|---|---|---|
| 1 | 消息队列值拷贝,6 个 API 永远返回 -1 | Trap 10 | net_socket.c |
| 2 | PHY link up 后 open_socket 失败 | Trap 09 | freertos.c + lftpd.c |
| 3 | net_accept_locked 拒绝 ESTABLISHED |
Trap 11 | net_socket.c |
| 4 | CH395F 多连接模式不支持多监听 | 架构 | net_socket.c: fun_para=0x00 |
| 5 | PASV 端口 0(net_bind 无自动分配) | 缺功能 | net_socket.c: s_dynamic_port |
| 6 | FatFS 路径 / 不兼容 + LFN 禁用 |
Trap 12 | lftpd_io.c: to_fatfs_path() |
| 7 | cmd_pasv 连关连开 listen 失败 |
Trap 09 子类 | lftpd.c: 3次重试 |
| 8 | ftpTask 启动时 PHY 未稳定 |
Trap 09 子类 | freertos.c: osDelay(7000) |
| 9 | 会话结束后立即重建监听失败 | Trap 09 子类 | lftpd.c: vTaskDelay(200ms) |
| 10 | s_file_open 单文件限制 |
已知限制 | lftpd_io.c (全局变量) |
| 11 | CH395F DMA buffer 太小,分批读导致 recv_len 归零 | Trap 13 | ch395f.c + lftpd.c |
| 12 | malloc 嵌入式环境失败 |
Trap 13 子类 | lftpd.c: static buffer |
| 13 | net_listen_locked 缓冲区覆盖(s0/s1 争用 block 0-3) |
Trap 02 翻版 | net_socket.c: 单连接用默认 buf |
| 14 | CH395F TIMEOUT 立即触发(数据连接无数据) | Trap 09 子类 | net_socket.c: CLOSED 状态处理 |
| 15 | 发送单次写 >1KB 写穿 1KB 硬件发送 FIFO | Trap 20 | net_socket.c: NET_SEND_CHUNK_MAX |
| 15 | NET_RECV_TIMEOUT_MS=5s 控制通道超时断开 |
配置 | net_config.h: 30000 |
| 16 | receive_file 0 bytes 当作成功 |
逻辑 | lftpd.c: total==0 则失败 |
| 17 | MobaXterm 连数据端口不发数据(客户端特殊行为) | 外部 | Python 抓包确认,超时恢复 |
Trap 13:CH395F recv_len 分批读取时归零
现象
MobaXterm 上传文件时 CH395F 收到数据(s1 int=0x04 RECV_OK 持续触发),但 ch395f_get_recv_len(1) 第一次返回 2920,读 1024 字节后第二次永远返回 0。GINT=0x0020 反复触发但无数据可读——死循环。
根因
ch395f_read_recv_buf 读部分数据后,CH395F 的 recv_len 寄存器被重置为 0(非递减)。缓冲区中剩余的 1896 字节仍然存在(RECV_OK 标志为真),但 ch395f_get_recv_len 报告 0。
同时 CH395F_DMA_BUF_SIZE=1500(header 4 + max 1496 data),无法一次读取完整的 4096 字节 CH395F 接收缓冲区。
解决方案
CH395F_DMA_BUF_SIZE从 1500 增大到 4100(支持一次读 4096 字节 header+data)receive_file缓冲区从 1024 增大到 4096,一次性读空 CH395F 接收缓冲区- 不能使用
malloc(嵌入式堆可能不可用),改用static unsigned char s_recv_buf[4096]
注意事项
- CH395F 接收缓冲区默认 socket 0: 4096B, socket 1: 4096B。读空后客户端继续发数据,recv_len 重新递增——正常
- 清理中断状态后
handle_timeout_event中的ch395f_close_socket必须显式调用,确保下次open_socket成功
Trap 14:close 后 DISCONNECT 滞后锁存,污染下次会话首次中断读取
现象
close_socket → 轮询 GET_SOCKET_STATUS 已确认 CLOSED 后,紧接的下一会话(重新 open + connect)在第一次读 GET_INT_STATUS_SN 时读到上一会话遗留的 DISCONNECT 事件(0ms 即命中),导致负向判定/状态机误判。实测:TC-202 三轮重连后立即跑 TC-203,PC 端握手实际成功(连接被接受),固件却因陈旧 DISCONNECT 抢先判"未连接"。
根因
wait-CLOSED轮询用的GET_SOCKET_STATUS是纯查询,不清中断;- TCP 拆除过程中断(DISCONNECT)可能在与 CLOSED 状态达成几乎同时、甚至之后才锁存;
- 该事件无人消费,跨会话存活到下一次
open_socket之后。
解决
- 预清:新会话 OPEN 前读一次
GET_GLOB_INT_STATUS_ALL+GET_INT_STATUS_SN丢弃陈旧事件; - 反核实:依据中断做结论前,用
GET_SOCKET_STATUS验证实际状态一致(如"负向事件 vs ESTABLISHED"矛盾时以状态为准)。
参考
test/ch395f_test_task.c: phase2_tc203()(双保险实现);同类思路见 Phase 1 入口的 PHY_CHANGE 预清。
Trap 15:DHCP 成功后 IP 寄存器提交滞后
现象
GET_DHCP_STATUS 已置 0x00 后立即 GET_IP_INF,回读到的仍是旧静态配置(如 192.168.1.100),而芯片实际已按新租约(192.168.1.77)正常收发——串口打印与 Socket6 广播宣告均带出过期 IP。2026-08-24 上板实测:固件各检查全 PASS(6/6),但 PC 侧 HELLO 发往租约地址才得到应答,三方数据矛盾构成假阳性。
根因
DHCP 状态位仅代表协商流程完成;IP/MASK/GW 寄存器的提交与状态置位是异步的,存在数百毫秒级窗口(实测 ~1s 内完成翻转)。
解决
- 稳定轮询:连续 3 次读
GET_IP_INF(间隔 300ms,上限 8s)结果一致且非零才采信; - 交叉比对:PC 端将收到的宣告 IP 与所分配租约比对,不一致即报错兜底。
参考
test/ch395f_test_task.c: phase4_run();docs/CH395F_Test_Guide.md 阶段 4 风险表 #4。
Trap 16:NET 层 TCP Client 目的 IP 字节序写反 → CONNECT TIMEOUT
现象
NET 层 TCP Client(阶段 7)net_connect() 返回 0,但 net_poll() 报 sock0 CONNECT TIMEOUT,连接永远建立不起来;而硬件层 TCP Client(阶段 2,同一 net_init/FUN_PARA=0x08,同一目标 192.168.1.2:8081)完全正常。PC 同网段、防火墙关闭、服务端正常监听。
根因
net_connect_locked() 把 sockaddr_in.sin_addr.s_addr(网络字节序,net_inet_addr 返回)按 低字节在前 拆成 remote_ip_arr[4],得到 [2,1,168,192],再交给 ch395f_set_des_ip()。但 CH395F 的目的 IP/端口寄存器是大端(网络字节序)(阶段 2 用 test_parse_ip("192.168.1.2") 直接得到 [192,168,1,2] 即成功,可证)。于是芯片把 SYN 发往 2.1.168.192——一个跨网段、不可达的地址,SYN 被网关丢弃 → 无 SYN-ACK → CONNECT TIMEOUT。
误导性排查:CMD_GET_REMOT_IPP_SN 回读的是小端字节序(ch395f.c 注释 "IP和端口均为低字节在前")。把写反的 [2,1,168,192] 读回再按小端解释,恰好又得到 192.168.1.2,使回读日志看起来"目的地址正确",掩盖了真实写反。
解决
net_connect_locked()提取remote_ip_arr改为 大端:arr[0]=(ip>>24)&0xFF … arr[3]=ip&0xFF(ip 为网络字节序 s_addr)。- 同样修正 UDP 发送路径
net_send_locked()中set_des_ip的目的 IP 数组(同样低字节在前 bug)。 - 诊断回读
GET_REMOT_IPP时按其小端语义还原:dip=(rip[3]<<24)|…|rip[0],再按>>24…打印,方可显示芯片实际目的 IP。
注意事项
net_inet_addr返回网络字节序;sin_addr.s_addr一律按网络字节序理解。- 任何写 CH395F IP/端口寄存器(set_des_ip / set_ip_addr / set_gwip_addr / set_mask_addr)都传大端数组,与
test_parse_ip产物一致。 - 回读类命令(GET_REMOT_IPP / GET_IP_INF 等)若文档标注"低字节在前",打印时务必先按小端重组再格式化,否则会误判。
对应文件
Drivers/BSP/NET/net_socket.c(net_connect_locked 提取 remote_ip_arr 大端;net_send_locked UDP 目的 IP 同样修正;[CONN] 回读按小端重组);Drivers/BSP/NET/net_types.h(remote_ip_arr 注释改为大端);test/ch395f_test_task.c(test_parse_ip 可作为大端构建范本)。
发现时间
阶段 7 NET 层 TCP Client 上板实测,2026-08-25。
Trap 17:单连接模式 client 重连被上一条连接的残留 DISCONNECT 秒断
现象
阶段 7 TC701(首次 TCP Client 连接 + 回显)正常;TC702(close 后同 Socket 0 重连 ×3,每轮不同本地端口)每次都 [CONN] openOK 成功、但紧接着 sock0 DISCONNECTED, releasing,连接被秒断,PC 端看到"已建立连接但对端立即关闭、未收到数据"。阶段 6 的 auto_relisten/断线处理逻辑同源,但 auto_relisten 标志本身不是元凶(client 该标志为 0)。
根因
单连接模式只有 Socket 0,新旧连接复用它。net_close_locked() 发 close_socket() 后芯片要等对端 FIN/ACK 才把 Socket 置 SOCK_CLOSED 并上报 DISCONNECT 中断(耗时数毫秒)。TC702 几乎在 TC701 关闭同时复用 Socket 0 去 tcp_connect:旧连接的 DISCONNECT 中断在 TC702 已 ESTABLISHED 之后才到达,net_poll() 的 handle_disconnect_event() 把状态置 CLOSED → 新连接被误判为"刚建立就断开"。GET_REMOT_IPP 回读仅确认目的地址正确,掩盖不了此事件串扰。
解决
net_close_locked():close_socket()后轮询ch395f_get_socket_status()直到CH395F_SOCKET_CLOSED,再读ch395f_get_sock_int_status()/ch395f_get_glob_int_status_all()清掉中断状态(上限 500ms,LAN 通常几毫秒),彻底排空上一条连接残留的DISCONNECT。net_connect_locked():open_socket()成功后再次读空本 Socket 中断状态,作为双保险。
注意事项
- 任何"关闭→复用同一 Socket 0 重新建连"的场景都必须先排空旧连接的中断,否则旧
DISCONNECT会串扰新连接。 - 若关闭后阻塞等待影响实时性(多连接模式另有 Socket),应改为非阻塞的"代次/epoch"机制;单连接模式直接排空即可。
net_close_locked内阻塞osDelay仅在 netTask 上下文,单连接模式无其它并发 Socket,安全。
对应文件
Drivers/BSP/NET/net_socket.c(net_close_locked 排空残留 DISCONNECT;net_connect_locked open 后清中断)。
发现时间
阶段 7 NET 层 TCP Client 上板实测,2026-08-25。
Trap 18:多 Socket 模式切换 + 8 个 Socket 缓冲分配
现象
阶段 7 仅测了 Socket 0(单连接模式只有 Socket 0 可用)。要验证 Socket 17 也能作为 client,必须把 7 完全不可用。FUN_PARA 从单 Socket 模式切到多 Socket 模式,否则 open_socket(1~7) 被芯片拒绝/无响应,1
根因
CMD_SET_FUN_PARA 的 bit0 决定 Socket 数量模式:0=单 Socket 模式(仅 Socket 0,8KB 缓冲全归它);1=多 Socket 模式(Socket 07 全部可用,缓冲需自行分配)。之前 7 不存在。注意 bit1(TCP Server 单/多连接)与 bit0 是两回事:本方案只翻 bit0(多 Socket),bit1 保持 0(TCP Server 单连接),即"8 个 Socket 可用,但 Socket 0 作服务端时只接 1 客户端"。net_init 用 0x08(bit0=0)即单 Socket 模式,故 1
解决
net_init:ch395f_set_fun_para(0x09)(bit0=1, bit1=0, bit3=1)。ch395f_init()之后,循环 07 用47 块、互不重叠。ch395f_set_send_buf(sock, i*6+4, 2)/ch395f_set_recv_buf(sock, i*6, 4)给每个 Socket 分配独立收发缓冲。按手册 §5.45(48 块 × 512B = 24KB)+ §9.2.8 最优分配:每 Socket 6 块 = 接收 4 块(2KB) + 发送 2 块(1KB),ch395f_set_tcp_mss(1024),恰好用尽 0- MSS 选取依据 §9.2.8:接收缓冲(2KB) ≥ 2×MSS(1024) 满足建议、≥ MSS 满足必须;发送缓冲(1KB) ≤ 8KB 满足必须。MSS 取 1024 为 8-Socket 共享 24KB 时的最大值。
net_init缓冲分配顺序遵循 Trap 01:先配数据 Socket,再配监听 Socket 0(本 Net 层 Socket 按需 open,故统一在 init 一次性配好)。UDP 路径net_socket()中对 Socket 4~7 的兜底重设也同步为同一布局。- PC 端
tcp_server改为每连接一线程的并发回显(--max-conn为并发计数),否则 Phase 7 全量连接会触发累计计数上限而拒掉 4~7(Trap 19)。
注意事项
- 4~7 的缓冲必须显式分配,否则其 CONNECT/RECV 中断不触发(Trap 01/02)。
- 上述布局以手册 §5.45 的 48×512B 缓冲几何为准,并已由 Phase 7 TC704(40/40 PASS)实测确认块 0
47 全部有效(此前文档中"32×1KB"记录有误)。Socket 47 分配与 8 路并发回显均验证通过。 - 多 Socket 模式下 TCP Server 单连接语义不变:Socket 0 监听且连接停留在本 Socket,Sockets 1~7 由应用当作独立 client 使用。
net_poll用GET_GLOB_INT_STATUS_ALL(2 字节,支持 07)并遍历 07,Socket n 中断位即bit(n+4)(Socket 03→bit47,Socket 47→bit811),原代码正确无需改动。
对应文件
Drivers/BSP/NET/net_socket.c(net_init FUN_PARA + 缓冲分配 + MSS);test/net_test_task.c(新增 TC704 八 Socket 并发回显);test/ch395f_socket_test.py(tcp_server 并发 + --max-conn)。
发现时间
阶段 7 扩展 8 Socket 测试,2026-08-25。
Trap 20:发送缓冲区单次写入超过硬件 FIFO 上限(写穿)
现象
大文件/批量发送(如阶段 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(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)"当成硬件发送缓冲上限。
解决
ch395f.h新增硬件缓冲字节宏:CH395F_SEND_BUF_SIZE_BYTES = CH395F_SEND_BLOCKS × CH395F_RAM_BLOCK_SIZE(=1KB),并补充CH395F_RECV_BUF_SIZE_BYTES。net_socket.c新增NET_SEND_CHUNK_MAX = min(NET_DMA_MAX_PAYLOAD, CH395F_SEND_BUF_SIZE_BYTES),即以硬件发送 FIFO 为硬上限(当前 1KB < 2KB DMA 载荷,故生效 1KB)。net_send_locked与net_sendto的单次写长度上限均由原来的NET_DMA_MAX_PAYLOAD/NET_SEND_BUF_SIZE改为NET_SEND_CHUNK_MAX。send_ready门控保证只有 FIFO 空闲时才写入,故写入 ≤1KB 安全(写满后等SEND_OK再写下一块)。- 接收方向不受影响:
net_recv_locked只读ch395f_get_recv_len(≤ 接收 FIFO 2KB)并受调用方len截断,不会写穿。
注意事项
- 该上限随
CH395F_SEND_BLOCKS(2026-09-22 起固定 2 块=1KB,与 MSS 解耦,见 Trap 21);若将来发送缓冲改变,NET_SEND_CHUNK_MAX自动跟随(仍取硬件缓冲与 DMA 载荷的较小者),无需改此处逻辑。 - 调用方(阶段 8、FTP
lftpd)仍可按任意长度调用net_send,实际分块由驱动保证 ≤ 硬件 FIFO。
对应文件
Drivers/BSP/CH395F/ch395f.h(CH395F_SEND_BUF_SIZE_BYTES 等);Drivers/BSP/NET/net_socket.c(NET_SEND_CHUNK_MAX + net_send_locked/net_sendto 分块上限)。
发现时间
阶段 8 大文件传输实现,2026-08-26。
Trap 21:接收缓冲"恰好填满 + 排空"窗口冻结(TC-NET-604)
现象
单条 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 侧无法修补)。2026-09-22 精确定位轮(11 组台架、110+ 运行、芯片命令序列级 netcmd 日志)定案:致命变量是 RX 排空的结束态(排空后缓冲是否残留),与命令条数/类型/窗口时长无关(逐组实证见下):
-
结构性前提:窗口=剩余空间(手册 L1281)→ 守规矩的对端永远把窗口灌满 → 每个稳态轮次缓冲恰好填满(2048/2048,窗口=0)是结构性必达状态,无法通过几何裕量规避。
-
冻结机制(实证签名):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 实例)。
-
致命性模型(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 无溢出突发在任何几何/命令结构下正常(排空即至空)。
-
(历史记录:2026-09-22 二次台架"三因子/第二条 RX 命令"模型——已被上方致命性模型取代,保留作过程记录)
-
三因子模型(MSS=1024 基线;主因已定案):① 同连接前序突发(芯片 RX 状态预置;高段密度可等效替代,见 MSS=512 条)+ ② 后续突发超缓冲容量(4096B > 2KB,需 ≥2 个填周期)+ ③ 填周期内第二条 RX 命令(排空之后同一 socket 的任何额外 SPI 命令——纯状态读 GET_RECV_LEN 即足够)——三者齐备才触发,缺一不触发:
变体 与 Phase 6-only(单 RX 命令路径)的差异 结果 Phase 6/9/11(每周期多路 RX 命令) Phase 9/11 各加一路 RX 命令/周期 10/10 停摆(4096B 步:前 2048B 回显正常,余停) Phase 6-only + 排空间隔 10/20/40/60ms(E 系列) 只拉长排空间隔,不加第二条命令 40/40 全过(每档 10/10,含满态持续 60ms+) Phase 6 + 11 + 60ms 间隔(V1,最小扰动) 每周期加一条 GET_RECV_LEN 状态读(落点 ≈ 排空后 45ms) 10/10 停摆(4096B 步,落点与经典模式一致) 多路 RX 命令、无前序突发 新连接独立 4096B 过(31ms;probe_mss 对照——前序突发因子缺失) -
主因(历史记录:二次台架时的"第二条 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)在任何几何/命令结构下均正常。
-
-
排除项:① 驱动层"超限读静默丢弃"排除——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 侧修复 = 非阻塞 RX 读强制排空至空)
- MSS 维持 1024(布局允许最大值;实测 1024 下新连接 4096B 正常、每满周期段数少 = 预置积累最慢)。MSS=512 裕量方案被证伪并回退。
- 缓冲块布局与 MSS 解耦(
ch395f.h):CH395F_RECV_BLOCKS=4/CH395F_SEND_BLOCKS=2固定(此前由 MSS 推导,改 MSS 会连带缩小布局);CH395F_SPI_DMA_BUF_SIZE = 接收缓冲 + 4(此前 2×MSS+4,改 MSS 会缩到装不下整缓冲)。 - 超限读金丝雀(
ch395f_read_recv_buf):正常路径不可能发起超过接收缓冲(2KB)的读;守卫命中即留痕read_recv_buf OVERSIZE(原为静默 return,会把窗口锁死在 0 且无从诊断)。 - 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 实施时处理。 - 兜底缓解(决策项,未实施):冻结不自恢复(对端重传 10s 无效;芯片无限期保持连接)。唯一恢复 = 断开受影响连接(释放芯片 per-socket 状态)+ 对端重试(新连接成功率高;台架实测停摆后重连回显正常)。现 lftpd 数据路径预算
LFTPD_TOTAL_BUDGET_MS=30000(每次读 30s)即实际停摆超时——若将数据通道预算单独缩短(10–15s,与控制行预算分离),可把停摆暴露时间从 30s 压缩到客户端重试路径;代价 = 慢速但健康的传输也会被中断(机器客户端不受影响,人工客户端受影响)→ 已列docs/待处理内容.mdN-1 待决策(N-7 落地后本项紧迫性下降,保留为兜底)。
注意事项
- 复现模式(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+ 监听死亡)。 - 冻结对 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 分块写入下全部正常),该条按保守表述处理,非运行约束。
- 本条的冻结是确定性触发模式缺陷(排空结束态 = 缓冲残留的部分窗口重通告;小 Δ 落点另有相位刀口),非低概率随机竞态——发现新停摆模式时先核对"是否存在部分排空(net_recv 上限 < 缓冲现有字节数,读后缓冲非空)",再怀疑 MCU 侧。
对应文件
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 精确定位轮:11 组台架 110+ 运行(E/O/B/F1/S1–S4/C/C0/V1F/O1f)定案"部分窗口重通告"致命性模型,N-7 设计由"排空后 RX 守卫 + 120ms 冷却"改为"非阻塞 RX 读强制排空至空"(无时间守卫),netcmd 命令序列日志工具入库(默认关)。
Trap 22:重监听后快速重连丢失 CONNECT 中断 → 监听静默死亡
现象
轻负载构建(Phase 6 only)+ 带历史的重监听 socket:上一条连接关闭后约 600ms 到达的第二条连接,芯片侧硬件完成 TCP 握手(PC 连接成功、数据可流入),但 MCU 侧无 standalone accept(socket 控制块滞留 LISTENING)→ net_send 全部被状态机拒绝(echo: send -1 != recv N),PC 侧回显 0 字节;接收路径仍可读出数据(RECV_OK 正常、缓冲可排空)。断开时 DISCONNECT 中断命中 handle_disconnect_event 的 LISTENING 分支 → 仅记 WARNING: listen sockN got disconnect 而无任何处置 → 芯片在 DISCONNECT 后自动关闭 Socket(FUN_PARA 默认行为)→ MCU 未再重开 → 监听静默死亡:此后所有 SYN 被拒(connection refused),不自恢复(仅复位/网络重建可恢复)。
复现:Phase 6-only 构建 + test/ch395f_socket_test.py tcp_client 无间隔连续运行 → run1 全过、run2 失同步、run3 起全部 connection refused(2026-09-22 台架 A 组;出现 1/1,未做 10/10 规模复验)。
机制(实证边界;芯片侧微观机理未解)
- 芯片硬件完成握手但 CONNECT 中断未上报(或 MCU 侧丢失)→
handle_connect_event未执行 → 状态滞留 LISTENING。 - 实证边界(2×2 台架):失同步仅现于"轻负载 + 重监听历史 + 快速重连(~600ms)"组合;重负载(Phase 6/9/11)下断连后 ~500ms 重连仍同步正常(B' 组 20/20 standalone accept、10/10 全连接成功);轻负载 + 15s 重连间隔亦全正常(A' 组 20/20)。即中断丢失窗口 ≈ "close → 重监听 → SYN" 间隔 < ~1s 且 netTask 相对空闲。芯片侧丢中断的具体原因未解(列为后续观察项,不阻塞下述修复)。
- 后果链确定:状态滞留 LISTENING →
net_send被拒(send -1)→ 断开时 DISCONNECT 落 LISTENING 分支(现有代码仅告警)→ 芯片自动关闭 Socket → MCU 不再重开 → 监听死亡。 - 与 Trap 21 的关系:独立失效模式——Trap 21 = RX 路径冻结(缓冲空、无 RECV_OK、窗口冻结、连接状态正常);本条 = 连接状态失同步(数据可流但发送被拒,且可致监听死亡)。日志签名鉴别:Trap 21 无 standalone accept 缺失、无 send -1;本条三者齐现(缺 standalone accept +
send -1+WARNING: listen)。
修复(方案,未实施 → N-6)
handle_disconnect_eventLISTENING 分支:回读芯片 socket 状态,非可监听状态(即芯片已自动关闭)时执行net_relisten_try重建(重 OPEN + TCP_LISTEN)——D20 陈旧事件修复的 LISTENING 分支姊妹情形(D20 只处理 ESTABLISHED 分支)。- 可选加固:重监听完成后清除该 socket 残留中断位(
handle_connect_event已有同模式),防陈旧位致下一次事件误判。 - 应用侧规避(可立即执行):单连接 socket 断连后重连留 ≥1~2s 退避——客户端快速重试 / lftpd 快速 PASV 序列是生产主要风险场景。
对应文件
Drivers/BSP/NET/net_socket.c(handle_disconnect_event LISTENING 分支);test/net_test_task.c(复现:Phase 6-only 构建 + 快速 tcp_client);docs/待处理内容.md N-6(修复待办登记)。
发现时间
2026-09-22(Trap 21 二次 2×2 复核台架副产品,A 组 run2)。