Files
STM32F4-Base/docs/CH395F_Trap_Records.md
2026-07-22 11:14:22 +08:00

16 KiB
Raw Blame History

CH395F 驱动陷阱记录

记录开发过程中遇到的所有坑、根因、解决方案,避免重复踩坑。


Trap 01Socket 4~7 自动分配不到

现象

多连接模式下最多接受 3 个客户端Socket 13Socket 47 的 CONNECT 中断从不触发,新客户端 SYN 被静默丢弃。

根因

net_listen() 中数据 Socket 的配置顺序错误。CH395F 在 TCP_LISTEN 时刻一次性扫描当前所有可用 Socket 并建立内部查找表。如果数据 Socket 4~7 在 TCP_LISTEN 之后才配置缓冲区/协议/端口,自动分配逻辑不认识它们。

正确顺序

  1. 先配置数据 Socket 1~7SET_SEND_BUF → SET_RECV_BUF → SET_SOUR_PORT → SET_PROTO_TYPE_TCP
  2. 最后配置监听 Socket 0SET_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.cnet_listen()

发现时间

Phase 8 压力测试2025-07


Trap 02数据 Socket 缓冲区重叠

现象

多连接模式下多个 Socket 数据互相覆盖,收到错误数据或客户端挂死。

根因

CH395F 默认只为 Socket 03 各分配 4 个缓冲区块(共 32 块用完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.cnet_listen()

发现时间

Phase 8 压力测试2025-07


Trap 03DHCP 包 xid 偏移错误

现象

Python DHCP Server 收到 DISCOVER 包后无法匹配 Offer/Ack 的 xid握手失败。

根因

错误使用了 data[232:236] 作为 xidWireshark 某旧版显示误导),实际 DHCP 标准偏移为 data[4:8]

正确偏移

  • xiddata[4:8]
  • msg_typedata[242]
  • client_macdata[28:34]
  • magic cookiedata[236:240]

对应文件

test/ch395f_socket_test.py → DHCP server 模式

发现时间

Phase 6, 2025-07


Trap 04UDP 发送后必须等待 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 05CH395F 与 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 06TCP KeepAlive 参数必须为 500ms 倍数

现象

设置 KeepAlive 后 TCP 连接几秒内 TIMEOUT 断开。

根因

CH395F 内部定时器以 500ms 为基准单位,传入非 500ms 倍数的值导致未定义行为。 且 IDLE 必须 > INTVL

正确值

ch395f_set_keepalive_idle(60000);   // 60s500 倍数)
ch395f_set_keepalive_intvl(5000);   // 5s500 倍数)
ch395f_set_keepalive_cnt(3);        // 3 次

对应文件

Drivers/BSP/NET/net_socket.cnet_init()

发现时间

Phase 2 测试2025-07


Trap 07TCP 关闭重连直接 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=0x00open_socket

发现时间

Phase 2 TCP Client 测试2025-07


Trap 08RECV 中断电平触发,避免无限循环

现象

net_poll() 中 RECV 中断无限触发主循环echo 等)永远得不到执行。

根因

CH395F 的 RECV 中断是电平触发的——只要接收缓冲区有数据就保持 INT# 低电平。 do { ... } while(int_status != 0) 会无限循环。

解决方案

net_poll() 使用 do { ... } while(0) 每次只处理一批中断,由主循环负责读取数据。

对应文件

Drivers/BSP/NET/net_socket.cnet_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 DMAch395f_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次最大20SET_RETRAN_PERIOD默认500ms最大1000ms总重传时间 = 次数 × 周期
  • KeepAlive 参数必须为 500ms 的倍数IDLE默认20000msINTVL默认15000ms且 IDLE > INTVL传入非 500 倍数的值导致 CH395F 内部定时器异常
  • KeepAlive 默认关闭:需在 SINT_STAT_CONNECT 后调用 ch395f_set_keepalive_enable(sock, 1) 启用

Socket 4~7 缓冲区

  • CH395F 默认只给 Socket 03 各分配 4 个缓冲区块(共 32 块用完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 → closedisconnect 发 FIN 推入 FIN_WAIT_2close 不再发 RSTSocket 卡住数分钟
  • close 直接:在 ESTABLISHED 或 CLOSE_WAIT 下 close 发 RST 立即终止,瞬间回到 CLOSED
  • 关闭后需轮询 ch395f_get_socket_status() 等待 sock=0x00open_socket

SOCK_TIMEOUT 处理

  • 长时间无数据时可能触发 SINT_STAT_SOCK_TIMEOUT不应视为致命错误——记录日志后继续操作即可,不要因此关闭 Socket

UDP 模式区分

  • DesIP=0xFFFFFFFF → UDP Server 模式:接受任意来源数据,接收数据前 8 字节为信息头(reserved[2] src_port[2] src_ip[4]),回发前需设置 SET_DES_IPSET_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 = 0xTaskNotifyGive 通知调用方。调用方的栈变量 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携带返回值

ProducernetTask

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 影响
  • 内核级 APInet_send_sock / net_recv_sock / net_listen_locked)直接操作,同样不受影响

Trap 11net_accept_locked 拒绝 standalone_accept 的 ESTABLISHED 状态

现象

FTP 客户端 TCP 连接成功standalone accept220 欢迎语不发送,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 12FatFS FF_USE_LFN=0 + 路径 / 前缀 → FR_INVALID_NAME

现象

FTP STOR 上传文件时 f_open("/upload_test.txt", FA_WRITE | FA_CREATE_ALWAYS) 返回 err=6FR_INVALID_NAMEupload_test.txt 文件名 11+3 字符 > 8.3 限制同样触发 err=6。

根因

  1. ffconf.hFF_USE_LFN = 0(长文件名禁用),最大文件名 8+3 字符
  2. FatFS 不接受 /file 格式的路径(需要 file0: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 端口 0net_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 NET_RECV_TIMEOUT_MS=5s 控制通道超时断开 配置 net_config.h: 30000
16 receive_file 0 bytes 当作成功 逻辑 lftpd.c: total==0 则失败
17 MobaXterm 连数据端口不发数据(客户端特殊行为) 外部 Python 抓包确认,超时恢复

Trap 13CH395F 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=1500header 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 成功