7.4 KiB
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.pcapngseq=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:先读MRCMDXdummy,再读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:
#define PBUF_POOL_SIZE 16
#define MEMP_NUM_PBUF 16
#define MEMP_NUM_TCPIP_MSG_INPKT 16
结果成功 ping 次数也从 8 变成 16。这说明扩大池子只是延迟耗尽,不能作为根修复。
最终已恢复为 8:
#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,配置已改为同步输入:
#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. 最终根因
最终根因位于:
Drivers/LwIP/src/netif/ethernet.c
原 ethernet_output() 实现:
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() 会执行:
pbuf_ref(t);
也就是给被挂接的原始 pbuf p 引用计数加 1。
ICMP echo reply 路径会复用 RX pbuf:
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 的引用计数变化是:
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 链:
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 pbufp增加的引用; - 之后
icmp_input()末尾的pbuf_free(p)可以真正把 RX pbuf 归还PBUF_POOL。
5. 不要做的错误修复
5.1 不要在 netif->input() 成功后手动释放 pbuf
驱动层当前逻辑是正确的:
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 构建通过:
"TCP2UART\TCP2UART.axf" - 0 Error(s), 0 Warning(s).
Program Size: Code=93376 RO-data=2768 RW-data=456 ZI-data=56032
用户烧录验证后确认问题已修复。
7. 后续排查建议
如后续再次出现固定次数网络停止,优先检查:
- 是否存在
pbuf_chain()/pbuf_ref()后没有配对pbuf_free()的路径。 - 是否有 ARP pending queue 长时间持有 pbuf。
- 是否有 TCP
recvmbox/ 应用桥接队列背压长期持有 pbuf。 - 是否有人在
netif->input()成功后错误释放 pbuf,导致内存破坏。
推荐排查点:
Drivers/LwIP/src/netif/ethernet.cDrivers/LwIP/src/core/ipv4/icmp.cDrivers/LwIP/src/core/ipv4/etharp.cDrivers/LwIP/src/core/pbuf.cDrivers/LwIP/src/netif/ethernetif.c