Files
STM32F4-Base/docs/CH395F_Trap_Records.md
2026-08-24 16:16:18 +08:00

434 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CH395F 驱动陷阱记录
记录开发过程中遇到的所有坑、根因、解决方案,避免重复踩坑。
---
## Trap 01Socket 4~7 自动分配不到
### 现象
多连接模式下最多接受 3 个客户端Socket 1~3Socket 4~7 的 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 0~3 各分配 4 个缓冲区块(共 32 块用完Socket 4~7 为零块。`net_listen()` 未对 Socket 4~7 显式分配独立缓冲区块,后续代码误用相同块号(如 28~31导致数据重叠。
### 解决方案
每个数据 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 03DHCP 包 xid 偏移错误
### 现象
Python DHCP Server 收到 DISCOVER 包后无法匹配 Offer/Ack 的 xid握手失败。
### 根因
错误使用了 `data[232:236]` 作为 xidWireshark 某旧版显示误导),实际 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 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 全双工:
```c
ch395f_set_phy(CH395F_PHY_100M_FULL);
```
### 对应文件
`Drivers/BSP/CH395F/ch395f.c`
### 发现时间
项目初期硬件联调
---
## Trap 06TCP KeepAlive 参数必须为 500ms 倍数
### 现象
设置 KeepAlive 后 TCP 连接几秒内 TIMEOUT 断开。
### 根因
CH395F 内部定时器以 500ms 为基准单位,传入非 500ms 倍数的值导致未定义行为。
`IDLE` 必须 > `INTVL`
### 正确值
```c
ch395f_set_keepalive_idle(60000); // 60s500 倍数)
ch395f_set_keepalive_intvl(5000); // 5s500 倍数)
ch395f_set_keepalive_cnt(3); // 3 次
```
### 对应文件
`Drivers/BSP/NET/net_socket.c``net_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=0x00``open_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.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 各分配 4 个缓冲区块(共 32 块用完Socket 4~7 为零块
- 多连接模式下使用 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_2close 不再发 RSTSocket 卡住数分钟
-`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 09PHY 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` 或其他错误码。
### 解决方案
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`,从未被更新。
```c
// 调用方ftpTask
msg.result = -1;
osMessageQueuePut(&msg); // 值拷贝到队列
xTaskNotifyWait(&notified); // 等待通知
return msg.result; // 永远是 -1
// 处理方netTask
osMessageQueueGet(&msg); // 拿到的是队列副本
msg.result = actual_result; // 修改的是副本
xTaskNotify(...); // 通知回去了,但值丢了
```
### 解决方案
不通过消息队列回传结果,改用 **FreeRTOS 通知值**notification value携带返回值
**ProducernetTask**
```c
xTaskNotify(msg.caller, (uint32_t)msg.result, eSetValueWithOverwrite);
```
**Consumer调用方**
```c
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 accept220 欢迎语不发送,`lftpd_inet_accept()` 死循环。客户端连接成功但永远收不到任何 FTP 响应。
### 根因
`net_accept_locked()` 入口检查:
```c
if (p_listen_sock->state != NET_SOCK_STATE_LISTENING) {
return -1; // ← 直接拒绝!
}
```
standalone accept 后 state 已变为 `ESTABLISHED`,永远到不了后续的 `standalone_accept` 检查——死锁。
### 解决方案
```c
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=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("")` 同样失败
### 解决方案
```c
/* 剥离 "/" 前缀,根目录用 "." 表示 */
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=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 14close 后 DISCONNECT 滞后锁存,污染下次会话首次中断读取
### 现象
`close_socket` → 轮询 `GET_SOCKET_STATUS` 已确认 CLOSED 后,紧接的下一会话(重新 open + connect在第一次读 `GET_INT_STATUS_SN` 时读到**上一会话遗留的 DISCONNECT 事件**0ms 即命中),导致负向判定/状态机误判。实测TC-202 三轮重连后立即跑 TC-203PC 端握手实际成功(连接被接受),固件却因陈旧 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/ch395f_test_task.c: phase2_tc203()`(双保险实现);同类思路见 Phase 1 入口的 PHY_CHANGE 预清。
---
## Trap 15DHCP 成功后 IP 寄存器提交滞后
### 现象
`GET_DHCP_STATUS` 已置 0x00 后立即 `GET_IP_INF`,回读到的仍是**旧静态配置**(如 192.168.1.100而芯片实际已按新租约192.168.1.77)正常收发——串口打印与 Socket6 广播宣告均带出过期 IP。2026-08-24 上板实测:固件各检查全 PASS6/6但 PC 侧 HELLO 发往租约地址才得到应答,三方数据矛盾构成假阳性。
### 根因
DHCP 状态位仅代表协商流程完成IP/MASK/GW 寄存器的提交与状态置位是异步的,存在数百毫秒级窗口(实测 ~1s 内完成翻转)。
### 解决
1. **稳定轮询**:连续 3 次读 `GET_IP_INF`(间隔 300ms上限 8s结果一致且非零才采信
2. **交叉比对**PC 端将收到的宣告 IP 与所分配租约比对,不一致即报错兜底。
### 参考
`test/ch395f_test_task.c: phase4_run()``docs/CH395F_Test_Guide.md` 阶段 4 风险表 #4