# 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(&ethhdr->dest, dst, sizeof(struct eth_addr));
SMEMCPY(&ethhdr->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(&ethhdr->dest, dst, sizeof(struct eth_addr));
  SMEMCPY(&ethhdr->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`
