246 lines
7.4 KiB
Markdown
246 lines
7.4 KiB
Markdown
# CH390 / lwIP 固定次数 ping 失败问题修复复盘
|
||
|
||
## 1. 问题现象
|
||
|
||
在 TCP2UART 固件运行后,设备初期可以正常 ARP 和 ping,但连续 ping 一段时间后不再响应。
|
||
|
||
典型现象:
|
||
|
||
- 设备 IP:`192.168.31.100`
|
||
- 设备 MAC:`02:00:00:00:00:01`
|
||
- 对端/网关 IP:`192.168.31.1`
|
||
- 对端/网关 MAC:`00:e0:4c:28:1e:60`
|
||
- 失败后设备仍持续发送 TCP SYN/RST 或 client timeout 相关流量,说明 TX、任务调度和应用层并未整体死机。
|
||
- 失败后对端继续向设备 MAC 发送 ICMP/ARP,但设备不再回复。
|
||
|
||
关键抓包:
|
||
|
||
- `WiresharkLog/04290150.pcapng`
|
||
- `seq=1884..1891` 共 8 次 ping reply 正常。
|
||
- 第 9 次 `seq=1892` 开始无 reply。
|
||
- `WiresharkLog/04290206.pcapng`
|
||
- 曾把 `PBUF_POOL_SIZE` / `MEMP_NUM_TCPIP_MSG_INPKT` 从 8 临时扩大到 16。
|
||
- 成功 ping 从 8 次变为 `seq=1900..1915` 共 16 次。
|
||
- 第 17 次 `seq=1916` 开始无 reply。
|
||
|
||
这个“成功次数随池大小移动”的现象证明:问题不是 CH390 随机丢包,也不是 PHY/TX 死掉,而是每次成功处理 ping 后都有某个 pbuf 引用没有释放,最终耗尽 `PBUF_POOL`。
|
||
|
||
## 2. 排查过程中的重要结论
|
||
|
||
### 2.1 CH390 RX 读包路径曾存在风险,但不是最终根因
|
||
|
||
早期排查时发现 CH390 RX 路径与参考驱动存在若干不一致,已修正:
|
||
|
||
- `ch390_receive_packet()` 按参考序列读取 RX ready:先读 `MRCMDX` dummy,再读 `MRCMDX1`。
|
||
- 校验 RX header 的 `Head` 字节必须为 `CH390_PKT_RDY`。
|
||
- CH390 RX SRAM 中的 `rx_len` 包含 Ethernet FCS,交给 lwIP 前需要减去 4 字节。
|
||
- `ch390_rx_reset()` 显式写 `MPTRCR_RST_RX` 复位 RX memory pointer。
|
||
|
||
这些修正确保 CH390 RX FIFO 读包更接近参考实现,但无法解释“固定 8 次/16 次后失败”。
|
||
|
||
### 2.2 扩大 lwIP 池只能延迟问题
|
||
|
||
曾临时将如下配置从 8 提到 16:
|
||
|
||
```c
|
||
#define PBUF_POOL_SIZE 16
|
||
#define MEMP_NUM_PBUF 16
|
||
#define MEMP_NUM_TCPIP_MSG_INPKT 16
|
||
```
|
||
|
||
结果成功 ping 次数也从 8 变成 16。这说明扩大池子只是延迟耗尽,不能作为根修复。
|
||
|
||
最终已恢复为 8:
|
||
|
||
```c
|
||
#define PBUF_POOL_SIZE 8
|
||
#define MEMP_NUM_PBUF 8
|
||
#define MEMP_NUM_TCPIP_MSG_INPKT 8
|
||
```
|
||
|
||
### 2.3 `tcpip_input()` 异步队列不是最终根因
|
||
|
||
项目启用了 lwIP core locking。为避免每个 RX 包占用 `MEMP_TCPIP_MSG_INPKT`,配置已改为同步输入:
|
||
|
||
```c
|
||
#define LWIP_TCPIP_CORE_LOCKING 1
|
||
#define LWIP_TCPIP_CORE_LOCKING_INPUT 1
|
||
```
|
||
|
||
这样 `tcpip_input()` 会在 core lock 下同步调用 `ethernet_input()`,不再通过 `TCPIP_MSG_INPKT` 邮箱异步排队。
|
||
|
||
但用户后续验证仍然固定 8 次后停止,且每次成功 ping 都已经有 reply,因此说明 RX 包确实已经进入 ICMP 处理路径,问题更可能是 reply 输出路径增加了 pbuf 引用但未释放。
|
||
|
||
## 3. 最终根因
|
||
|
||
最终根因位于:
|
||
|
||
```text
|
||
Drivers/LwIP/src/netif/ethernet.c
|
||
```
|
||
|
||
原 `ethernet_output()` 实现:
|
||
|
||
```c
|
||
q = pbuf_alloc(PBUF_RAW_TX, SIZEOF_ETH_HDR, PBUF_RAM);
|
||
if (q == NULL) {
|
||
LINK_STATS_INC(link.memerr);
|
||
LINK_STATS_INC(link.drop);
|
||
return ERR_MEM;
|
||
}
|
||
|
||
pbuf_chain(q, p);
|
||
|
||
ethhdr = (struct eth_hdr *)q->payload;
|
||
SMEMCPY(ðhdr->dest, dst, sizeof(struct eth_addr));
|
||
SMEMCPY(ðhdr->src, src, sizeof(struct eth_addr));
|
||
ethhdr->type = lwip_htons(eth_type);
|
||
|
||
return netif->linkoutput(netif, q);
|
||
```
|
||
|
||
问题在 `pbuf_chain(q, p)`。
|
||
|
||
lwIP 的 `pbuf_chain()` 会执行:
|
||
|
||
```c
|
||
pbuf_ref(t);
|
||
```
|
||
|
||
也就是给被挂接的原始 pbuf `p` 引用计数加 1。
|
||
|
||
ICMP echo reply 路径会复用 RX pbuf:
|
||
|
||
```text
|
||
ethernetif_poll()
|
||
-> tcpip_input()
|
||
-> ethernet_input()
|
||
-> ip4_input()
|
||
-> icmp_input()
|
||
-> ip4_output_if()
|
||
-> etharp_output()
|
||
-> ethernet_output()
|
||
```
|
||
|
||
`icmp_input()` 末尾本身会 `pbuf_free(p)`,这部分是正确的。但在原实现中,`ethernet_output()` 通过 `pbuf_chain(q, p)` 给 `p` 额外加了一次引用,却没有在 `linkoutput()` 返回后释放临时 header pbuf `q`。
|
||
|
||
因此每次 ping 的引用计数变化是:
|
||
|
||
```text
|
||
RX pbuf 初始 ref = 1
|
||
pbuf_chain(q, p) 后 ref = 2
|
||
icmp_input() 末尾 pbuf_free(p) 后 ref = 1
|
||
=> p 永远没有回到 0,PBUF_POOL 泄漏 1 个
|
||
```
|
||
|
||
所以:
|
||
|
||
- `PBUF_POOL_SIZE=8` 时,8 次 ping reply 后耗尽。
|
||
- 临时扩大到 16 时,16 次 ping reply 后耗尽。
|
||
|
||
## 4. 修复方案
|
||
|
||
修复 `ethernet_output()`,在同步 `linkoutput()` 完成后释放临时 header pbuf 链:
|
||
|
||
```c
|
||
err_t ethernet_output(struct netif *netif,
|
||
struct pbuf *p,
|
||
const struct eth_addr *src,
|
||
const struct eth_addr *dst,
|
||
u16_t eth_type)
|
||
{
|
||
struct pbuf *q;
|
||
struct eth_hdr *ethhdr;
|
||
err_t err;
|
||
|
||
LWIP_ASSERT("netif != NULL", netif != NULL);
|
||
LWIP_ASSERT("p != NULL", p != NULL);
|
||
LWIP_ASSERT("src != NULL", src != NULL);
|
||
LWIP_ASSERT("dst != NULL", dst != NULL);
|
||
|
||
q = pbuf_alloc(PBUF_RAW_TX, SIZEOF_ETH_HDR, PBUF_RAM);
|
||
if (q == NULL) {
|
||
LINK_STATS_INC(link.memerr);
|
||
LINK_STATS_INC(link.drop);
|
||
return ERR_MEM;
|
||
}
|
||
|
||
pbuf_chain(q, p);
|
||
|
||
ethhdr = (struct eth_hdr *)q->payload;
|
||
SMEMCPY(ðhdr->dest, dst, sizeof(struct eth_addr));
|
||
SMEMCPY(ðhdr->src, src, sizeof(struct eth_addr));
|
||
ethhdr->type = lwip_htons(eth_type);
|
||
|
||
err = netif->linkoutput(netif, q);
|
||
pbuf_free(q);
|
||
return err;
|
||
}
|
||
```
|
||
|
||
为什么这里可以释放 `q`:
|
||
|
||
- 本项目 `low_level_output()` 是同步发送。
|
||
- 它会立即遍历 pbuf 链,把数据复制到 `s_tx_buffer`。
|
||
- 随后调用 `ch390_runtime_send_packet()` 把连续 buffer 发给 CH390。
|
||
- `low_level_output()` 返回后不再持有 pbuf 指针。
|
||
|
||
因此 `ethernet_output()` 在 `linkoutput()` 返回后释放 `q` 是正确的。
|
||
|
||
`pbuf_free(q)` 会同时:
|
||
|
||
- 释放临时 Ethernet header pbuf `q`;
|
||
- 解除 `pbuf_chain()` 对原始 RX pbuf `p` 增加的引用;
|
||
- 之后 `icmp_input()` 末尾的 `pbuf_free(p)` 可以真正把 RX pbuf 归还 `PBUF_POOL`。
|
||
|
||
## 5. 不要做的错误修复
|
||
|
||
### 5.1 不要在 `netif->input()` 成功后手动释放 pbuf
|
||
|
||
驱动层当前逻辑是正确的:
|
||
|
||
```c
|
||
input_err = ch390_netif.input(p, &ch390_netif);
|
||
if (input_err != ERR_OK) {
|
||
pbuf_free(p);
|
||
}
|
||
```
|
||
|
||
`netif->input()` 返回 `ERR_OK` 时,pbuf ownership 已经交给 lwIP。此时驱动不能再 `pbuf_free(p)`,否则会造成 double-free 或 use-after-free。
|
||
|
||
### 5.2 不要只扩大 `PBUF_POOL_SIZE`
|
||
|
||
扩大池子只会让失败次数从 8 变 16、32……不会修复泄漏。
|
||
|
||
### 5.3 不要继续优先怀疑 CH390 PHY/TX
|
||
|
||
抓包中失败后设备仍持续发送 TCP SYN/RST,说明 TX 和任务仍活着。固定次数失败更符合 pbuf 引用泄漏。
|
||
|
||
## 6. 验证结果
|
||
|
||
修复后 Keil 构建通过:
|
||
|
||
```text
|
||
"TCP2UART\TCP2UART.axf" - 0 Error(s), 0 Warning(s).
|
||
Program Size: Code=93376 RO-data=2768 RW-data=456 ZI-data=56032
|
||
```
|
||
|
||
用户烧录验证后确认问题已修复。
|
||
|
||
## 7. 后续排查建议
|
||
|
||
如后续再次出现固定次数网络停止,优先检查:
|
||
|
||
1. 是否存在 `pbuf_chain()` / `pbuf_ref()` 后没有配对 `pbuf_free()` 的路径。
|
||
2. 是否有 ARP pending queue 长时间持有 pbuf。
|
||
3. 是否有 TCP `recvmbox` / 应用桥接队列背压长期持有 pbuf。
|
||
4. 是否有人在 `netif->input()` 成功后错误释放 pbuf,导致内存破坏。
|
||
|
||
推荐排查点:
|
||
|
||
- `Drivers/LwIP/src/netif/ethernet.c`
|
||
- `Drivers/LwIP/src/core/ipv4/icmp.c`
|
||
- `Drivers/LwIP/src/core/ipv4/etharp.c`
|
||
- `Drivers/LwIP/src/core/pbuf.c`
|
||
- `Drivers/LwIP/src/netif/ethernetif.c`
|