# CH395F 驱动陷阱记录

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

---

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

### 现象
多连接模式下最多接受 3 个客户端（Socket 1~3），Socket 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 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 全双工：
```c
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`。

### 正确值
```c
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 各分配 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_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` 或其他错误码。

### 解决方案
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）携带返回值：

**Producer（netTask）**：
```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 accept），220 欢迎语不发送，`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 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("")` 同样失败

### 解决方案
```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 端口 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 | `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/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/ch395f_test_task.c: phase4_run()`；`docs/CH395F_Test_Guide.md` 阶段 4 风险表 #4。
