539 lines
28 KiB
Markdown
539 lines
28 KiB
Markdown
# 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 各分配独立收发缓冲(共 48 块 × 512B 用满,见手册 §5.45),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 分配独立收发缓冲(共 48 块 × 512B 用满),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(¬ified); // 等待通知
|
||
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, ¬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()` 入口检查:
|
||
|
||
```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 | 发送单次写 >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/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。
|
||
|
||
---
|
||
|
||
## 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/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 1~7 也能作为 client,必须把 `FUN_PARA` 从单 Socket 模式切到多 Socket 模式,否则 `open_socket(1~7)` 被芯片拒绝/无响应,1~7 完全不可用。
|
||
|
||
### 根因
|
||
`CMD_SET_FUN_PARA` 的 **bit0** 决定 Socket 数量模式:`0`=单 Socket 模式(仅 Socket 0,8KB 缓冲全归它);`1`=多 Socket 模式(Socket 0~7 全部可用,缓冲需自行分配)。之前 `net_init` 用 `0x08`(bit0=0)即单 Socket 模式,故 1~7 不存在。注意 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()` 之后,循环 0~7 用 `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~47 块、互不重叠。
|
||
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)实测确认块 0~47 全部有效(此前文档中"32×1KB"记录有误)。Socket 4~7 分配与 8 路并发回显均验证通过。
|
||
- 多 Socket 模式下 TCP Server 单连接语义不变:Socket 0 监听且连接停留在本 Socket,Sockets 1~7 由应用当作独立 client 使用。
|
||
- `net_poll` 用 `GET_GLOB_INT_STATUS_ALL`(2 字节,支持 0~7)并遍历 0~7,Socket n 中断位即 `bit(n+4)`(Socket 0~3→bit4~7,Socket 4~7→bit8~11),原代码正确无需改动。
|
||
|
||
### 对应文件
|
||
`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**(由 MSS=1024 推导,见 `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_TCP_MSS` 推导,若将来调大 MSS 使发送缓冲 >2KB,则 `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。
|