Files
STM32F4-Base/docs/问题记录/CH395F_Trap_Records.md
T
wandering1 de1b50efe9 修复 P1-7:MAC 写入顺序改为 INIT_CH395 之前
- 手册 §9.2.1:SET_MAC(步骤①) 必须先于 INIT_CH395(步骤⑤);SET_MAC 写
  芯片 EEPROM,INIT 从中加载 MAC——原顺序反了则本次上电 MAC 不生效
  (回落芯片内置/上次值),UI 显示与设备在线身份不符
- net_socket.c net_init():SET_MAC 移到 ch395f_init() 之前(对齐步骤①)
- ch395f.c:四个 SET(MAC/IP/GWIP/MASK)限定条件注释由"芯片已初始化"
  订正为"必须在 ch395f_init() 之前调用"
- net_socket.h / BSD_Socket_API_函数调用关系.md:同步顺序描述
- CH395F_Trap_Records.md:新增 Trap 24 沉淀根因与修复
- 待处理内容.md:P1-7 销项(§1/§2 阶段 A/§3.1/§4);阶段 A 清空后重编号
- 验证:Keil 构建 0 错 0 警;pre-commit 17/17 全通过
2026-10-04 23:07:00 +08:00

51 KiB
Raw Blame History

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 之后才配置缓冲区/协议/端口,自动分配逻辑不认识它们。

正确顺序

  1. 先配置数据 Socket 1~7:SET_SEND_BUF → SET_RECV_BUF → SET_SOUR_PORT → SET_PROTO_TYPE_TCP
  2. 最后配置监听 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/host/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(违反此序曾导致 MAC 不生效,见 Trap 24)
  • 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 03 分配独立收发缓冲(共 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 静默丢弃

现象

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 或其他错误码。

解决方案

  1. 应用层重试兜底:FTP Server (lftpd_start) 的 while(1) 循环中,listen 失败后 vTaskDelay(3000) 重试,3 秒足够 PHY 稳定
  2. 启动延时消峰: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(&notified);    // 等待通知
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, &notified, 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。

根因

  1. ffconf.h 中 FF_USE_LFN = 0(长文件名禁用),最大文件名 8+3 字符
  2. FatFS 不接受 /file 格式的路径(需要 file 或 0:file)
  3. lftpd_io_canonicalize_path("/", "file") 输出必然带 / 前缀
  4. 根目录路径 "/" 剥离后为空字符串,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 接收缓冲区。

解决方案

  1. CH395F_DMA_BUF_SIZE 从 1500 增大到 4100(支持一次读 4096 字节 header+data)
  2. receive_file 缓冲区从 1024 增大到 4096,一次性读空 CH395F 接收缓冲区
  3. 不能使用 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 之后。

解决

  1. 预清:新会话 OPEN 前读一次 GET_GLOB_INT_STATUS_ALL + GET_INT_STATUS_SN 丢弃陈旧事件;
  2. 反核实:依据中断做结论前,用 GET_SOCKET_STATUS 验证实际状态一致(如"负向事件 vs ESTABLISHED"矛盾时以状态为准)。

参考

test/board/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 内完成翻转)。

解决

  1. 稳定轮询:连续 3 次读 GET_IP_INF(间隔 300ms,上限 8s)结果一致且非零才采信;
  2. 交叉比对:PC 端将收到的宣告 IP 与所分配租约比对,不一致即报错兜底。

参考

test/board/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,使回读日志看起来"目的地址正确",掩盖了真实写反。

解决

  1. net_connect_locked() 提取 remote_ip_arr 改为 大端:arr[0]=(ip>>24)&0xFF … arr[3]=ip&0xFF(ip 为网络字节序 s_addr)。
  2. 同样修正 UDP 发送路径 net_send_locked() 中 set_des_ip 的目的 IP 数组(同样低字节在前 bug)。
  3. 诊断回读 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/board/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 回读仅确认目的地址正确,掩盖不了此事件串扰。

解决

  1. 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。
  2. 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,必须把 FUN_PARA 从单 Socket 模式切到多 Socket 模式,否则 open_socket(1~7) 被芯片拒绝/无响应,17 完全不可用。

根因

CMD_SET_FUN_PARA 的 bit0 决定 Socket 数量模式:0=单 Socket 模式(仅 Socket 0,8KB 缓冲全归它);1=多 Socket 模式(Socket 07 全部可用,缓冲需自行分配)。之前 net_init 用 0x08(bit0=0)即单 Socket 模式,故 17 不存在。注意 bit1(TCP Server 单/多连接)与 bit0 是两回事:本方案只翻 bit0(多 Socket),bit1 保持 0(TCP Server 单连接),即"8 个 Socket 可用,但 Socket 0 作服务端时只接 1 客户端"。

解决

  1. net_init:ch395f_set_fun_para(0x09)(bit0=1, bit1=0, bit3=1)。
  2. ch395f_init() 之后,循环 07 用 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),恰好用尽 047 块、互不重叠。
  3. MSS 选取依据 §9.2.8:接收缓冲(2KB) ≥ 2×MSS(1024) 满足建议、≥ MSS 满足必须;发送缓冲(1KB) ≤ 8KB 满足必须。MSS 取 1024 为 8-Socket 共享 24KB 时的最大值。
  4. net_init 缓冲分配顺序遵循 Trap 01:先配数据 Socket,再配监听 Socket 0(本 Net 层 Socket 按需 open,故统一在 init 一次性配好)。UDP 路径 net_socket() 中对 Socket 4~7 的兜底重设也同步为同一布局。
  5. 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)实测确认块 047 全部有效(此前文档中"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/board/net_test_task.c(新增 TC704 八 Socket 并发回显);test/host/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)"当成硬件发送缓冲上限。

解决

  1. ch395f.h 新增硬件缓冲字节宏:CH395F_SEND_BUF_SIZE_BYTES = CH395F_SEND_BLOCKS × CH395F_RAM_BLOCK_SIZE(=1KB),并补充 CH395F_RECV_BUF_SIZE_BYTES。
  2. net_socket.c 新增 NET_SEND_CHUNK_MAX = min(NET_DMA_MAX_PAYLOAD, CH395F_SEND_BUF_SIZE_BYTES),即以硬件发送 FIFO 为硬上限(当前 1KB < 2KB DMA 载荷,故生效 1KB)。
  3. net_send_locked 与 net_sendto 的单次写长度上限均由原来的 NET_DMA_MAX_PAYLOAD/NET_SEND_BUF_SIZE 改为 NET_SEND_CHUNK_MAX。send_ready 门控保证只有 FIFO 空闲时才写入,故写入 ≤1KB 安全(写满后等 SEND_OK 再写下一块)。
  4. 接收方向不受影响: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)—— 结论:非缺陷(2026-09-23 结案)

结案结论

原报告(阶段 8 大文件传输期间:同连接前序突发后 4096B 突发"前 2KB 正常回显、其余静默无回显")在当前固件上无法复现,判定为非缺陷。可信判据(产品固件 + 真实 lftpd FTP 上传,成功 = FTP 协议结果 + 上传后 RETR 回读比对 SHA-256)实测:227 轮全过(8KB/64KB/256KB/1MB;含零间隔快速重连与 100 轮浸泡),并发生产 UI 流(:7003)600 帧 CRC 全过,设备侧日志无 FAIL/ERROR/WARN。

为什么原台架会"复现"——教训,务必先读

2026-09-22"精确定位轮"给出的全部"冻结/停摆/WEDGE"结论均为台架假象,已撤回:

  1. 测试程序自吞数据:台架配方里的固件内读者(Phase 9/11 的 64B 读、固件探针读)把对端数据读走后不回显——PC 端自然少收,被误当成"设备停摆"。实测短收量恰等于被吞字节数(例:4736−4608 = 128 = 2×64 探针读);所谓"V1 配方必冻结"就是 Phase 11 吞掉整包(设备日志 avail=64 len=64,PC 侧下一轮 rx=0/64)。对照组(仅 Phase 6、无额外读者)40/40 全过。
  2. 判读工具误判:probe_classify.py A/C 模式把通过的运行报成 STALL(C0 的脚本层实测为 1460/1460 + 尾段 2100/2100,且内容逐字节匹配)。
  3. 由此派生的"部分排空致命 / 再武装区间 ≥160ms / 相位刀口 / C 配方复现生产停摆"等结论全部作废;MSS=512 实验的"停摆"亦未经可信判据复核。

现在怎么做(台架纪律)

  1. 判读一律用 MDK-ARM/bench_verdict.py:以脚本层 STEP 行为真值,并扣除固件读者吞噬的字节;
  2. 冻结/卡死类问题走真实生产路径 + 端到端内容校验:MDK-ARM/ftp_freeze_probe.py(FTP 上传 + RETR 回读 SHA-256);卡住时用控制通道 NOOP 区分"数据面卡死"与"整层卡死";
  3. 禁止把"固件内非回显读者"与固定期望值混用在同一配方里——这是本轮最大的教训。

保留的代码变更(与被撤回的机理无关)

缓冲块布局与 MSS 解耦(ch395f.h)、超限读 OVERSIZE 金丝雀(ch395f.c)保留:属防御性/可观测性改进,不回退。

相关

  • 真实缺陷见 Trap 23(上传中止后首笔复连上传被静默截断,已可信复现并修复);
  • 发现:2026-08-26(阶段 8);结案:2026-09-23。

Trap 22:快速复连丢 CONNECT/DISCONNECT 中断 → 监听静默死亡(2026-09-23 复现并修复)

现象(生产固件实测)

单连接监听 socket 在快速复连后永久失效(后续 SYN 被拒或超时,10s 内不自恢复),而芯片/网栈其余部分完全正常(其他监听端口照常工作)。两种表现:

  • 拒连(ConnectionRefused):芯片完成握手但 CONNECT 中断未上报 → 控制块滞留 LISTENING;或本层因 DISCONNECT 中断丢失仍以为有客户端 → 两种情况都会让芯片侧那条连接一直占住单连接监听,后续 SYN 全被拒;且丢事件后本层收不到任何中断,无法靠事件自愈。
  • 应用侧连锁:net_send 被拒(send -1);或应用读不到 EOF(其 g_client 永不释放,因而永不调用 accept)。

复现(可信用例,纯客户端判据;工具 MDK-ARM/tcp_reconnect_probe.py)

  • 确定性复现:产品固件 + 待机 60s 后,对 UI 监听 :7003 以 300ms 间隔快速复连 → 修复前首 2 次成功、之后永久拒连(10 轮 2/10,10s 恢复窗口内无自恢复);
  • 概率复现:FTP 控制 :21 以 100ms 间隔连 50 次 → 修复前第 29 轮起永久拒连;
  • 对照:FTP :21 在 0ms / 600ms 间隔下通过;:8888(对时)同期亦正常 → 触发与"复连间隔 <~1s + 轻负载(待机)"相关,与 2026-09-22 记录的边界一致。

机制(设备侧日志实证)

  1. 芯片丢 CONNECT 中断 → 无 standalone accept,控制块滞留 LISTENING;或丢 DISCONNECT 中断 → 本层仍以为 ESTABLISHED;
  2. 此后本层收不到任何中断(芯片静默持有/丢弃该连接),纯事件驱动无路可走——实测把 handle_disconnect_event 的 LISTENING 分支从"仅告警"改为"重建监听"后仍然无效(因为连 DISCONNECT 都没来);
  3. 故必须周期回读芯片状态对账才能发现失同步。

修复(已实施,Drivers/BSP/NET/net_socket.c)

  1. 周期对账 net_reconcile_tick()(net_process_messages 每轮调用;周期 200ms):对在用 socket 回读芯片状态,修正三类失同步—— ① 本层 ESTABLISHED 而芯片已无连接 → 按断开处理(应用读到 EOF 后自动重监听); ② 本层 LISTENING 而芯片已完成握手 → 补做 standalone accept(复用 handle_connect_event); ③ 本层 LISTENING 而芯片监听已消失 → 重建监听(net_relisten_try)。 去抖 + 冷却:同一失配需连续 2 次(≈400ms)才动手;同一 socket 动手后冷却 1s。
  2. handle_disconnect_event 的 LISTENING 分支:由"仅告警"改为"回读芯片状态——已不可监听即重建"(仍在监听则判陈旧事件丢弃)。

踩坑记录(务必先读)

最初的修法放在 accept 路径(每次 accept 时回读状态并自愈):单次状态读会把"刚 open/listen 尚未生效"的瞬态误判为失配 → 监听被反复重建直至 relisten give up → 三个监听口全部死亡(风暴)。改为"周期对账 + 去抖 + 冷却"后消除。 结论:凡基于单次状态读的重建动作,必须去抖。

验证(修复前 → 修复后)

用例 修复前 修复后
待机后 UI :7003 快速复连 ×10 2/10(之后永久拒连) 10/10
FTP :21 间隔 100ms ×50 28/50(第 29 轮起死亡) 50/50
FTP :21 间隔 0ms / 600ms ×50 通过 50/50 / 50/50
UI :7003 间隔 200ms ×20 — 20/20
FTP 正常上传 10×64KB(SHA-256 往返) 通过 10/10
上传中止 RST / FIN 回归(Trap 23) 通过 5/5 + 5/5
设备侧 relisten give up / 重建风暴 — 0

自愈实测计数(整轮会话):chip closed -> disconnect 12 次、connection completed w/o CONNECT irq -> accept 1 次、listen lost -> relisten 1 次——三类失同步均真实出现并被自愈;relisten give up 0 次。

对应文件

Drivers/BSP/NET/net_socket.c(net_reconcile_tick / handle_disconnect_event LISTENING 分支);MDK-ARM/tcp_reconnect_probe.py(可信用例);docs/待处理内容.md N-6(✅ 已销项,2026-09-26 清理)。

发现时间

2026-09-22(台架偶发 1/1)→ 2026-09-23(生产固件确定性复现 + 当日修复闭环)。

Trap 23:上传中止后首笔复连上传被静默截断(≤2048B)并误报成功

现象

客户端上传(STOR)中途断开(用户取消 / 网络断;RST 或优雅 FIN 均可),紧随其后的第一笔复连上传被截断到 ≤2048 字节(= 芯片单 socket RX 缓冲大小),且服务端回报 226 上传成功 → 客户端(ftplib 等)据此认为上传成功 = 静默数据损坏;第二笔及以后恢复正常。

证据(可信复现:生产固件、零固件探针;工具 MDK-ARM/ftp_abort_probe.py)

  • 判据 = FTP 协议结果 + 紧接一笔正常上传的 SHA-256 往返校验(STOR→RETR 比对) + 控制通道 NOOP 存活探测;
  • RST 中止:10/10 复现首笔复连上传截断(请求 4096B → 设备日志 UPLOAD '/ab_ok.bin' 2048 bytes OK);
  • 优雅 FIN 中止:5/5 同样复现 → 与 RST/FIN 无关,凡"数据未发完即断开"均触发;
  • 中止那一笔自身也被记为 UPLOAD ... 2048 bytes OK + 226(FTP 无长度声明,语义边界;但客户端会误判成功);
  • 对照(无中止):177 轮(8KB/64KB/256KB/1MB,含 100 轮零间隔浸泡)全部 SHA-256 匹配(MDK-ARM/ftp_freeze_probe.py),并发 UI 流 600 帧 CRC 全过 → 中止路径是唯一触发条件。

机制(设备侧日志实证)

  1. 中止时数据 socket 被释放(RST 时芯片上报 sock4 TIMEOUT),net 层随即 auto-relisten ok + relisten retry ok(监听端口复用);
  2. 下一笔连接:sock4 standalone accept → CMD STOR → 紧接着一条陈旧 sock4 DISCONNECTED, releasing (chip tcp=0x00)(旧连接收尾状态落在新连接上);
  3. lftpd 数据读循环据此读到 EOF → 把已到的 2048B 当作整个文件 → UPLOAD ... 2048 bytes OK + 226;
  4. 再下一笔连接时芯片状态已干净 → 正常(故为"一次性"而非持久故障)。

影响

  • 静默数据损坏:上传被中断后"重试的第一笔"会生成截断文件且客户端收到成功回报(FTP 客户端通常不做长度校验)→ 现场可能表现为"上传的文件损坏/不完整";
  • 主风险面 = 生产路径 STOR(上传);RETR(下载)不涉及;
  • 与 TC-NET-604 的关系:本条(Trap 23)是独立、已可信复现并已修复的真实缺陷;TC-NET-604 已结案为非缺陷(见 Trap 21)。现场"上传失败/文件不完整/像卡死"类报告,应优先用本条的判据复核:FTP 协议结果 + 上传后 RETR 回读 SHA-256(+ 卡住时控制通道 NOOP 探活)。

复现

& '.venv\Scripts\python.exe' MDK-ARM\ftp_abort_probe.py --rounds 10 --send 8192 --gap 1
& '.venv\Scripts\python.exe' MDK-ARM\ftp_abort_probe.py --rounds 5  --send 8192 --gap 1 --graceful

修复方向(待办 N-8,全部闭环)

  1. ✅(2026-09-23 一阶段)relisten 前让芯片 socket 真正沉降——隔离期:持续排空 RX + 主动 close + 等 CLOSED/TCP_CLOSED,隔离满 400ms 才 open+listen;
  2. ✅(2026-09-23/24)新连接的陈旧中断防御——陈旧 DISCONNECT 以芯片状态回读甄别(D20:芯片 TCP 仍 ESTABLISHED 判陈旧丢弃)+ 释放残留经两级沉降/隔离(100ms/400ms)后不再投递给下一条连接;
  3. ✅(2026-09-25)应用侧兜底——异常释放(TIMEOUT/RST)的挂起 recv 以 NET_ERR_CONNRESET 了结(正常 FIN 仍回 0——协议边界)→ STOR 异常结束回 450 并删除截断文件;
  4. ✅ 回归判据——ftp_abort_probe.py followup:RST 10/10 + FIN 5/5(一阶段)、8/8 + 5/5 + 5/5(二阶段);--check-abort 断言中止 STOR 应答码(RST→450 / FIN→226 边界);177 轮对照当时全匹配,其后各轮正常路径回归全过。

发现时间

2026-09-23(可信复现首轮:真实生产路径 FTP 中止/复连场景,由 MDK-ARM/ftp_abort_probe.py 自动判定)

修复实施与验证(2026-09-23 当日闭环)

真因(设备侧日志实证,含正常对照):

  1. 客户端中止上传时对端仍有在途数据;芯片给出 sock4 TIMEOUT(此刻 MCU 侧 recv=0),net 层随即释放该 socket 并复监听;
  2. 那些在途段随后才落进芯片 RX 缓冲(约一个缓冲 = 2048B 残渣),而正常传输时 DISCONNECT 到达刻芯片里也仍有数据(对照实测 recv=64512/65536,由应用读空 → 完整);
  3. 中止后立即 open+listen,残渣被下一条连接当作自己的数据读走:新上传只“收到”2048B 就被判定结束并回报 226(首笔复连必中招,第二笔起芯片已干净 → 恢复正常)。

修复(Drivers/BSP/NET/net_socket.c,仅异常释放路径,正常 FIN 路径零改动):

  1. handle_timeout_event(ESTABLISHED 超时释放)置 per-fd 隔离标记 s_relisten_quarantine[];
  2. net_relisten_locked 对带隔离标记的 socket:隔离期内持续排空芯片 RX(新增 net_drain_chip_rx(),分块读空、零新增静态缓冲)+ 主动 ch395f_close_socket 并排空该 socket 中断 + 等芯片回到 CLOSED/TCP_CLOSED,隔离满 NET_RELISTEN_QUARANTINE_MS(400ms) 才 open+listen;
  3. 上限 NET_RELISTEN_SETTLE_MS(800ms) 强制放行 —— 必须小于 NET_RELISTEN_DEADLINE_MS(2000ms):实测取 2000ms 会把重试预算耗尽 → 重试轮询"give up" → 监听器死亡(:21 拒连),已修正并在注释中留痕;
  4. 隔离状态在 net_relisten_clear 内一并清除(关闭/新建 fd 时不残留)。

验证(可信判据:FTP 协议结果 + 紧接一笔上传的 SHA-256 往返 + 控制通道存活):

  • 修复前:RST 中止 0/10、优雅 FIN 中止 0/5(首笔复连上传全部被截断且回报成功);
  • 修复后:RST 中止 10/10、优雅 FIN 中止 5/5 全部通过;
  • 正常路径回归:30×64KB 零间隔 30/30、20×64KB 20/20,SHA-256 全匹配;
  • 启动门禁:App init OK / net init: OK、FORBID 0 行。

第二阶段:正常释放(FIN)路径——边界甄别与 100ms 沉降(2026-09-24)

边界再甄别(现场"1~2s"时间假设被推翻):中止后 gap 500/2000/5000ms 台架复测 → 设备实存尺寸全部仍 2048(各 4/4,合计 12/12)→ 与时间无关;监听重开本征即时(t_accept 30~47ms)。真实边界:

维度 结论 证据
触发 设备侧收过数据的数据连接被对端关闭(纯 FIN 即触发,不要求 RST) 纯 close()(FIN)4/4 复现;RST 变体 5/5
受影响路径 仅正常 DISCONNECT 释放路径缺芯片状态沉降 RST(TIMEOUT)路径已有一阶段 400ms 隔离保护
投递形式 芯片上一条连接的收尾残留,投递给复用同一 fd 的下一条连接(accept 后约 10~20ms 一条伪 DISCONNECT) 设备日志:standalone accept → DISCONNECTED, releasing (chip tcp=0x00)
影响面 一次性(首条新连接中招,第二条起干净);同 fd 非 FTP 服务(:7003 UI sink)不受影响(其 socket 不按传输复用) 一次性 3/3 全对;:7003 复用 8/8 全过
时间 无关(gap 5s 仍失败)→ 现场"1~2s"只是人工重触的常见窗口,不是芯片机制 gap 500/2000/5000ms 共 12/12 全部失败

修复(第二阶段,Drivers/BSP/NET/net_socket.c +36/−7,netTask 串行上下文,零新增缓冲):

  1. relisten 前沉降分两级:异常释放(芯片 TIMEOUT)=清理 + 最短等 400ms(NET_RELISTEN_QUARANTINE_MS,一阶段行为不变);正常释放(芯片 DISCONNECT)=清理 + 最短等 100ms(NET_RELISTEN_GRACEFUL_MS,新增);
  2. 正常释放沉降仅在本连接收过数据时登记(recv_bytes != 0):纯连接即关(Trap 22 的 0ms 复连用例)与只发不收的下载连接无残留 → 免沉降保住快速复连时序;
  3. 日志区分 relisten settle(正常)/ relisten quarantine(异常),便于台架分辨触发路径。

沉降值实测悬崖(只清理不等待 = 0ms):0ms 无效(4/4 仍截断)→ 30ms 仍失败(4/4)→ 100ms 通过(多轮稳定)→ 取 100ms(对 30ms 重试节拍 NET_RELISTEN_RETRY_MS 留 3× 余量)。

验证(第二阶段后;设备日志 UPLOAD '<path>' N bytes OK 为权威真值):

用例 修复前 修复后
FIN 关闭后首笔上传 ×8 0/8(实存 2048 + 回报 226) 8/8(实存 4096,sha OK)
纯 close() 变体 ×4 中招 4/4
ftp_abort_probe --graceful ×5 0/5 5/5
ftp_abort_probe(RST)×5 5/5(一阶段) 5/5
无中止对照 ×4 / 一次性连发 3 笔 ×1 4/4 / 第 1 笔坏 4/4 / 3/3
正常往返 freeze 4096×4 / 64KB×2 通过 4/4 / 2/2(t_stor 0.68→0.18s,t_retr 1.13→0.19s)
非 FTP :7003 sink 复用 ×8(RST+FIN) 8/8 8/8
Trap 22::21 0ms×50 / 100ms×50,:7003 300ms×10,:8888 0ms×20 50/50 / 50/50 / 10/10 / 20/20 50/50 / 50/50 / 10/10 / 20/20
启动门禁 / 控制台 / 堆 — App init OK / net init: OK,FORBID 0,CONSOLE dropped 0,heap free=8776 min_ever=4816

结论:缺陷位于 fd 复用生命周期(芯片上一条连接的残留状态投递给下一条连接),不在 FTP 命令逻辑——lftpd 只是把它放大成误报 226。RST/TIMEOUT 半边由一阶段隔离处理,DISCONNECT 半边由本阶段沉降处理,合计覆盖全部"收过数据后关闭"的释放路径。已知残余边界:中止那一笔 STOR 自身仍回报 226(STOR 无长度声明、在途 FIN 与正常完成不可区分)——后续硬化见待办 #3。

对应文件:Drivers/BSP/NET/net_socket.c(handle_disconnect_event 沉降登记 + net_relisten_locked 两级沉降);MDK-ARM/ftp_abort_probe.py / MDK-ARM/bench_diag/bench_diag_abort_gap.py(边界甄别与回归判据)。

后续硬化(2026-09-25):异常结束回 5xx(待办 #3 闭环)

目标:中止那一笔 STOR 自身不再误报 226——异常结束(连接重置/超时)时回 450 并删除截断文件,把"静默损坏"变成"显式失败"(纵深防御;客户端按协议重试)。

实现(3 个工程文件 + 1 个台架脚本):

  1. net_types.h:net_sock_t 增加释放来源标记 released_abnormal(RAM +8B:1B×8 socket,BSS,运行堆零影响);
  2. net_socket.c:handle_timeout_event(异常释放/TIMEOUT)置 1;handle_disconnect_event(正常释放/DISCONNECT)置 0;net_socket()(新 fd)与 net_relisten_locked 成功尾(新监听)清零——新连接不得继承上一条连接的释放原因;
  3. net_socket.c net_pending_tick 断连了结(③):排空缓冲后,标记为 1 以 NET_ERR_CONNRESET 了结(对端未发正常 FIN,不是正常 EOF),标记为 0 仍 EOF(0);
  4. lftpd.c receive_file:err < 0 时 unlink 截断文件(原先残留于盘)并返回错误码 → STOR 处理器回 450;
  5. MDK-ARM/ftp_abort_probe.py 增加 --check-abort:RST 中止要求中止 STOR 控制应答 4xx/5xx,优雅 FIN 要求 2xx(协议边界,见下)。

协议边界(不可判定,留痕):FTP STOR 无长度声明,"在途 FIN 关闭"与"正常完成"逐字节不可区分(芯片均报 DISCONNECT、缓冲均排空)→ 优雅中止仍回 226(中止方客户端通常也不等应答);仅 RST/掉链(芯片 TIMEOUT)可识别 → 450。FIN 边界由客户端长度校验(SIZE/RETR 往返)兜底。

各消费端负值处理复核(均按"负值=结束"与 0 同路,行为不变):

  • lftpd 控制通道 read_line:负值 → line_len != 0 → 会话清理,与 0 同路(控制通道不 close,auto_relisten 恢复);
  • time_sync(:8888):n<0 → break 弃连接,与 0 路径一致;8B 时间包已收齐时仍写 RTC;
  • UI sink(:7003):DONTWAIT 非阻塞,不走 L3 pending,零影响;
  • 测试套件:产品构建不启用;测试对端干净关闭,无影响。

验证(硬化后,2026-09-25 台架):

检查项 结果
ftp_abort_probe --check-abort --rounds 5(RST) 5/5——中止 STOR → 450(原 226),followup 5/5,控制存活 5/5
设备侧证据 sock4 TIMEOUT → [ERR][FTP] UPLOAD '/ab0001.bin' FAILED → 450;followup UPLOAD '/ab_ok.bin' 4096 bytes OK
--check-abort --graceful --rounds 5(FIN) 5/5——中止 STOR → 226(边界不变),followup 5/5
正常往返 freeze 4096×4 / 64KB×2 4/4 / 2/2(SHA-256 全 MATCH)
Trap 22::21 0ms×50 / 100ms×50,:7003 300ms×10,:8888 0ms×20 50/50 / 50/50 / 10/10 / 20/20
非 FTP :7003 sink 复用(RST×4 / FIN×4,400 帧在途) 4/4 / 4/4(ack 2000/2000,LCD 3209/3209,CRC 全过)
时间同步 :8888 真实收包 wall clock updated (epoch=…) 写 RTC 成功,监听恢复
构建 / 回读 / 启动门禁 / 控制台 / 堆 0 错 0 警、No difference found、FORBID 0、CONSOLE dropped 0、heap free=8776 min_ever=4816(不变)

对应文件:Drivers/BSP/NET/net_types.h(released_abnormal)、Drivers/BSP/NET/net_socket.c(三处置位/清除 + net_pending_tick 了结分支)、Drivers/BSP/NET/lftpd/lftpd.c(receive_file 错误路径 unlink)、MDK-ARM/ftp_abort_probe.py(--check-abort)。


Trap 24:MAC 写入顺序违反手册 §9.2.1(SET_MAC 必须在 INIT_CH395 之前)

现象(静态审查发现;手册确证)

net_init() 原顺序为 ch395f_init()(CMD_INIT_CH395)→ ch395f_set_mac_addr(),与手册 §9.2.1 规定的初始化顺序相反。后果:INIT_CH395 已按 EEPROM 旧值加载 MAC,其后写入 EEPROM 的新 MAC 本次上电不生效(回落芯片内置/上次值,实际"滞后一次上电")→ UI 配置的 MAC 与设备实际在线 MAC 不一致,设备在线身份错误、网口排查困难。本轮未做"UI 改 MAC + 重启"台架复现,结论由手册条文确证。

根因

手册 §9.2.1「初始化 CH395,必要操作」顺序为: ① SET_MAC_ADDR → ② SET_IP_ADDR → ③ SET_GWIP_ADDR → ④ SET_MASK_ADDR → ⑤ INIT_CH395。

  • §5.7:SET_MAC_ADDR 把 MAC 写入芯片内部 EEPROM(约 30us);
  • §5.13:INIT_CH395 初始化 CH395 的 MAC、PHY 以及 TCP/IP 协议栈(约 5ms)。

即 INIT_CH395 会从 EEPROM 加载 MAC,故 SET_MAC 必须先于 INIT_CH395。原码把 MAC 放在 INIT 之后。

加剧误排的注释:ch395f.c 原把 set_mac_addr/set_ip_addr/set_gwip_addr/set_mask_addr 的限定条件标为"芯片已初始化"——与手册矛盾(这四个 SET 实际都在 INIT 之前下发),易被误读为"须在 init 之后调用"。

解决

  1. Drivers/BSP/NET/net_socket.c net_init():把 ch395f_set_mac_addr() 从 ch395f_init() 之后移到之前(置于 SET_IP/GWIP/MASK 之前,对齐手册步骤①);
  2. ch395f.c:四个 SET 函数的限定条件注释统一改为"必须在 ch395f_init() 之前调用";
  3. net_socket.h:net_init 注释同步(MAC 于协议栈初始化之前下发)。

影响面

  • 仅 net_init() 一处调用(产品路径);改动为纯顺序调整,无新增/删除指令、无 RAM 变化;
  • net_stack_bring_up() 在 net_init 后回读芯片 MAC 写 UI 影子(net_cfg_set_active_mac)——修复后回读值 = 实际在线 MAC,UI 显示与线上身份一致。

对应文件

Drivers/BSP/NET/net_socket.c(net_init SET_MAC 顺序)、Drivers/BSP/NET/net_socket.h(net_init 注释)、Drivers/BSP/CH395F/ch395f.c(四个 SET 的限定条件注释);docs/网络/BSD_Socket_API_函数调用关系.md(§4.1 / §7 顺序)。

发现时间

2026-10-04(静态审查项 P1-7;依据手册 §9.2.1 / §5.7 / §5.13 定案)。