# 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`