恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Zynq lwIP TCP调试全攻略:从环境配置到Wireshark抓包定位
首页
资讯中心
/
Zynq lwIP TCP调试全攻略:从环境配置到Wireshark抓包定位
Zynq lwIP TCP调试全攻略:从环境配置到Wireshark抓包定位
发布时间:2026/10/14 18:14:19
上个月调某块Zynq开发板的网络功能遇到的第一个问题不是代码而是PHY芯片地址对不上——客户端能搜到ARPTCP连接却一直建立不起来。那次Zynq的lwIP TCP调试折腾了整整两天最后把所有排查思路串了一遍才发现硬件配置和协议栈配置是两套东西任何一个对不上表现都千奇百怪。现在回头看Zynq上跑lwIP的TCP调试最核心的其实就三件事环境配好、代码入口走对、调试手段跟得上。很多刚接触Zynq的人把精力花在FPGA逻辑上以为ARM侧的网络只是简单模板结果一跑起来全是“连不上”“发出去没回包”“一传数据就断”这类问题。这篇文章就把我实际调试Zynq lwIP TCP的完整链路写出来从环境搭建到代码走读从串口日志到抓包定位再补上几个高频问题的根因分析希望能帮你少走弯路。1. 为什么Zynq上调试TCP会先栽在环境上1.1 先搞清Zynq网络调试的硬件路径Zynq的PS端自带以太网MAC控制器数据从ARM侧交给MAC再通过MDIO管理PHY芯片最后从RJ45口出去。整个链路里FPGA逻辑没有直接参与除非你把PHY放在了PL侧否则大部分调试都是纯软件层面的活儿。很多人忽略的是PHY芯片的地址配置。PHY地址由MDIO接口上的strap引脚决定同一个PHY在不同的板卡上可能被配成0、1、4甚至别的值。SDK里的BSP会根据开发板的实际硬件值生成默认配置但你换板子、换PHY芯片或者参考工程不对这个地址就会错。地址不对的典型现象是网口灯可能亮但lwIP初始化后link状态一直不对TCP自然起不来。调试第一步永远是从硬件路径确认开始网线直连还是过交换机、PHY地址、MAC控制器挂在哪条时钟域上。先确认这些再去碰代码。1.2 搭建可复现的lwIP运行环境我用的是某块Zynq-7000系列的评估板开发工具链是常见的那一套。新建应用工程时记得在Board Support Package里勾上lwIP库。这个步骤看似简单但BSP版本和库版本不同生成的接口不太一样。环境搭建的几个关键点硬件平台描述文件里的网口基地址、中断号要和实际板卡一致。lwIP库在BSP里有独立的配置面板可以打开或关闭DHCP、调整TCP窗口大小。板卡上的MAC地址是lwIP软件配置的6字节数组不是从EEPROM读出来的物理地址。静态IP、子网掩码、网关三个值必须一次性配对否则后续排查很容易被误导。建议你第一次跑直接用静态IP别开DHCP。DHCP虽然方便但会引入超时重传、租约更新这些变量调试时很难分清是协议栈问题还是网络问题。我一般习惯把板卡IP设为192.168.1.10电脑设192.168.1.2子网掩码统一255.255.255.0网关可以写不存在的192.168.1.1只要不同网段通信就不用管网关。1.3 网络参数配置里的三处隐蔽坑第一处是MAC地址。lwIP默认的MAC地址是00:00:00:00:00:00的话有些交换机会直接把帧丢掉。常见做法是写一个类似02:00:00:00:00:01的本地管理地址避免和真实网卡冲突。第二处是link speed和duplex。BSP里可以强制100Mbps全双工也可以自适应。大部分情况自适应没问题但碰到旧交换机或者交叉线强制百兆反而稳定。如果你发现连接时通时不通先试试强制百兆全双工。第三处是bind地址。创建TCP服务时如果bind到具体的IP地址而板卡IP是DHCP分配的话服务就只监听那个旧IP别人改了IP之后完全连不上。所以裸机调Loopback服务时一定要bind到IP_ADDR_ANY上。2. 从hello world到TCP回环代码结构逐段拆解2.1 裸机环境下如何选APIlwIP提供三种APIRAW API、Sequential API和Socket API。Zynq裸机调试最常见的是RAW API因为不依赖操作系统也没有线程阻塞概念所有回调都是在协议栈上下文中触发逻辑直接出问题也容易用断点定位。如果你跑的是FreeRTOS或者Linux那可以用Socket API但实际上Zynq的SDK里裸机BSP默认就是RAW API。做TCP回环实验RAW API足够用最大的好处是你不需要处理锁、信号量这些并发问题整个调用链是单线程轮询加中断推动的。2.2 一段可复现的TCP回环服务代码先贴一个最常见的TCP服务器回环代码骨架。它做的事情是初始化协议栈配置网卡创建一个监听在5001端口的TCP服务器收到什么数据就原样回发。#include lwip/init.h #include lwip/tcp.h #include lwip/ip_addr.h #include netif/xemacpsif.h #define TCP_LOCAL_PORT 5001 static struct netif netif_instance; static struct tcp_pcb *server_pcb; static err_t tcp_echo_recv(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err) { if (p ! NULL) { tcp_write(pcb, p-payload, p-len, 1); tcp_output(pcb); tcp_recved(pcb, p-len); pbuf_free(p); } else { tcp_close(pcb); } return ERR_OK; } static err_t tcp_echo_accept(void *arg, struct tcp_pcb *pcb, err_t err) { tcp_recv(pcb, tcp_echo_recv); return ERR_OK; } int main(void) { ip_addr_t ipaddr, netmask, gw; lwip_init(); IP4_ADDR(ipaddr, 192, 168, 1, 10); IP4_ADDR(netmask, 255, 255, 255, 0); IP4_ADDR(gw, 192, 168, 1, 1); netif_add(netif_instance, ipaddr, netmask, gw, NULL, xemacpsif_init, tcpip_input); netif_set_default(netif_instance); netif_set_up(netif_instance); server_pcb tcp_new(); tcp_bind(server_pcb, IP_ADDR_ANY, TCP_LOCAL_PORT); server_pcb tcp_listen(server_pcb); tcp_accept(server_pcb, tcp_echo_accept); while (1) { xemacpsif_input(netif_instance); } return 0; }这段代码的核心是主循环里不断调用xemacpsif_input把网卡收到的数据包喂给lwIP协议栈。协议栈处理完TCP状态机后会触发accept或recv回调。2.3 回调函数背后发生了什么tcp_echo_accept在客户端发起SYN并完成三次握手后被调用。此时TCP连接已经建立但这个连接对应的PCB和监听PCB是不同实例。如果你在accept回调里不加tcp_recv注册那后续数据进来不会有任何处理函数去接收连接大概率会卡死在窗口更新上。tcp_echo_recv收到数据时参数p是一块pbuf内存。p-payload指向实际数据p-len表示长度。回环逻辑中我调用了tcp_write把数据放入发送缓冲区tcp_output立刻触发发送。之后必须调用tcp_recved告知协议栈已经取走数据释放接收窗口。如果漏掉这一步对端的发送窗口会越来越小最后两边都卡住。这里有一个很多新手容易忽略的细节tcp_write并不保证发送成功它只是把数据拷贝到TCP发送队列真正的发送由tcp_output或者后续ACK驱动。如果返回值是ERR_MEM说明发送缓冲区不够需要等tcp_sent回调腾出空间后再重试。3. 完整调试链路串口日志、断点与抓包三件套3.1 串口日志怎么打才有效Zynq裸机程序最现成的调试出口是UART串口。lwIP本身支持LWIP_DEBUG但默认是关闭的。在lwipopts.h里把LWIP_DEBUG打开再针对TCP模块开启LWIP_TCP_DEBUG和LWIP_ETHARP_DEBUG就能看到协议栈内部的状态迁移和ARP交互。不过全量打开日志会把串口刷爆运行几秒钟就丢数据。我实际调试时的做法是只开必要模块并且把重要业务日志用xil_printf打印比如连接建立、接收长度、发送返回值、当前pcb状态。日志格式要固定方便后面用串口助手或tcpdump对照时间戳。举个例子客户端一连接上串口打印一条线上日志[APP] client connected: 192.168.1.2:54321 [APP] recv 10 bytes, send ret0有了这个日志再配合抓包就能快速定位是协议栈没收到包还是业务层没处理。3.2 调试点断点要下在关键回调和状态变化处调试器里不要在主循环上一直单步不然网络超时早把状态机搞乱了。有效做法是在recv回调入口、tcp_output调用处、以及xemacpsif_input加断点。先用断点确认协议栈是否进入接收路径再看pcb结构体里的state字段。TCP PCB的state字段常见值有SYN_SENT、ESTABLISHED、FIN_WAIT_1、CLOSE_WAIT。如果连接一直处于SYN_RCVD但无法进入ESTABLISHED说明第三次握手的ACK没被正确处理。这时候看PC端的抓包大概率是ACK丢了或者校验和错误。调试器还有一个用处是查看内存统计。lwIP内部的memp池有统计数组可以在断点处手动查memp_stats看TCP_PCB池、PBUF池有没有耗尽。很多莫名其妙的连接断开根因就是内存池为空建不了新PCB或者分配不了pbuf。3.3 Wireshark抓包如何定位异常连接PC端用Wireshark抓包是TCP调试最关键的一环。抓包目的不是看有没有包而是看包的状态组合符不符合TCP正常流程。正常建立连接的抓包顺序是客户端发起SYN。板卡回复SYN-ACK。客户端回复ACK。如果只看到SYN板卡没有SYN-ACK说明板卡侧根本没有监听端口或者协议栈没把SYN喂到TCP层。如果SYN-ACK发出来但客户端直接回RST通常是客户端连接的端口和服务器bind的端口不一致或者被防火墙拒绝。数据传输阶段正常情况是数据包带PSH标志板卡收到后回复ACK。如果数据发出去后一直有TCP Dup ACK说明中间有丢包但TCP重传机制会兜底。如果客户端一发送数据就收到RST基本可以断定是板卡侧主动关闭连接常见原因是recv回调抛出ERR_MEM或tcp_close被误触发。我习惯用Wireshark过滤规则锁定特定流tcp.port 5001这个过滤会把所有涉及5001端口的握手、数据、ACK都显示出来节奏非常清晰。如果板卡IP是192.168.1.10也可以直接ip.addr 192.168.1.10。4. 实测中遇到的五个问题及根因分析4.1 问题一连接一建立就立刻收到RST现象客户端TCP连接显示成功但几乎是同一瞬间服务端返回RST串口没有任何日志提示。排查过程先看抓包确认RST来源是板卡回的还是客户端系统回的。我遇到的那次RST确实是从板卡方向回来串口只有lwIP的内存池输出没有业务日志。进一步定位到代码问题出在accept回调里没有对pcb做非空判断而且这个连接在三次握手完成后因为接收缓冲区分配失败被协议栈强制关闭。裸机环境下MEMP_NUM_TCP_SEG这个池子太小多客户端场景下尤其容易触发。解决方式是在lwipopts.h里调大#define MEMP_NUM_TCP_SEG 128 #define MEMP_NUM_TCP_PCB 16 #define PBUF_POOL_SIZE 64同时必须检查tcp_accept回调的返回值。假如回调返回ERR_MEM协议栈会终止这个连接并发送RST这就是根源。4.2 问题二Ping通但TCP连不上现象电脑能ping通板卡IP但TCP客户端连接总是超时。排查过程Ping通说明ICMP和ARP链路正常问题范围缩小到TCP端口或监听逻辑。用Wireshark抓包后发现客户端SYN包正常发出板卡没有回应。在板卡串口加上调试日志查看tcp_bind和tcp_listen的返回值。我的问题是有趣的创建PCB时用了tcp_new_ip_type(IPADDR_TYPE_V6)监听在IPv6地址上而客户端访问的是IPv4地址两边完全对不上。这提醒我裸机lwIP虽然默认支持IPv4但如果你不小心启用了IPv6IP_ADDR_ANY在某些版本里会同时匹配两个地址族导致bind结果和预期不一致。建议直接用tcp_new()它在很多裸机BSP里默认创建IPv4 PCB。如果确认地址族没问题再看防火墙。Windows和Linux默认都可能阻止对未知IP的TCP入站连接尤其当板卡IP不在本机网段时。可以临时用一台开启了TCP Server接待的小工具或者Python脚本做简单回环测试排除系统防火墙干扰。4.3 问题三收发不全或者收到重复数据现象板卡发回的数据客户端能收到但明显丢包或者顺序混乱偶发重复包。排查过程TCP本身有序号控制网络正常时不会乱序和重复。如果出现这种情况多半是应用层没有正确处理pbuf。lwIP的pbuf可以是一条链表p-len只是当前pbuf节点长度不是整个数据包的总长度。回环服务里如果简单认为p-payload就是完整一包数据就会在pbuf链较长时漏掉数据。正确写法是遍历pbuf链static err_t tcp_echo_recv(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err) { if (p NULL) { tcp_close(pcb); return ERR_OK; } while (p ! NULL) { tcp_write(pcb, p-payload, p-len, 1); p p-next; } tcp_output(pcb); tcp_recved(pcb, p-tot_len); pbuf_free(p); return ERR_OK; }注意两个细节tcp_write的pbuf引用计数和pbuf_free的调用顺序。如果先pbuf_free再tcp_write发送队列里的数据指针已经失效就会出现发出去的内容随机变化。4.4 问题四内存池耗尽导致丢包现象长时间运行后TCP连接突然失去响应串口报memp不足。排查过程lwIP采用静态内存池方式管理协议栈关键结构。PBUF_POOL_SIZE太小或者MEMP_NUM_TCP_SEG不够都会在高负载下分配失败。我遇到的情况比较典型客户端每100ms发送一个64KB文件的分片瞬间产生了大量队列缓冲。板卡回环速率跟不上发送队列积压最终池子耗尽PCB进入Error状态。tcp_err回调里打印错误码后发现是ERR_MEM。解决思路有两个方向。业务层面应用层做流控收到数据先回ACK处理完再回复业务包。协议栈层面增大池子并优化内存对齐#define MEM_LIBC_MALLOC 0 #define MEM_ALIGNMENT 4 #define MEM_SIZE (4 * 1024 * 1024) #define PBUF_POOL_SIZE 256 #define TCP_SND_BUF (16 * 1024) #define TCP_WND (16 * 1024)Zynq裸机上DDR通常够大给lwIP多分几MB没问题性能反而更稳定。4.5 问题五ARP缓存表老化导致丢包现象板卡和电脑重启后第一次Ping通过几分钟又断再次Ping又通。排查过程抓包发现板卡一直在往一个旧的MAC地址发包而电脑的IP已经换了MAC。原因很直接lwIP的ARP缓存有老化机制默认超时时间不长。板卡在缓存超时后没有重新发送ARP请求而是直接发IP包交换机发现目的MAC不存在包就丢了。这种情况在调试静态IP、手动改电脑IP时特别常见。解决方法是配置lwipopts.h里ARP_QUEUEING同时把应用层业务设计成周期性确认机制。如果实在想省事开发阶段可以直接在电脑上用固定IP不常改或者强制板卡每次发送前刷新ARP。另一个非常隐蔽的点是网线直连时板卡MAC偶尔会缓存成空地址。可以在板卡主循环里每隔几秒加一个Ping操作主动刷新ARP表。5. 进阶让Zynq网络跑得更稳更快的实战经验5.1 静态IP与MAC、网关的整体规划调试阶段我强烈建议把所有网络参数写死在代码里不要读EEPROM不要读SD卡配置文件。这样每次复位后环境都一致问题可以复现。IP、MAC、网关、端口四个值在脑海里要形成一个固定映射排查时第一反应就是核对这几个值。整套配置我习惯写成一组宏#define BOARD_MAC0 0x02 #define BOARD_MAC1 0x00 #define BOARD_MAC2 0x00 #define BOARD_MAC3 0x00 #define BOARD_MAC4 0x00 #define BOARD_MAC5 0x01 #define BOARD_IP 192.168.1.10 #define BOARD_PORT 5001然后在main里用xemacps_set_mac和netif_set_addr设置对应值。5.2 让TCP传输更快的窗口与延迟优化lwIP默认的TCP_WND和TCP_SND_BUF偏保守拷贝到Zynq上如果不调吞吐量很难上去。实测中把TCP_WND调到32KB、TCP_SND_BUF调到32KB回环速率能明显上升。还有一个是Nagle算法。如果客户端频繁发送小包Nagle会强制合并造成明显延迟。在需要交互式响应的场景下在接收回调里对pcb设置tcp_nagle_disable(pcb);这个函数会让每个tcp_write都立刻tcp_output小包延迟从几十毫秒降到毫秒级。代价是网络报文数增加但调试阶段无所谓优先保证实时性。不要只用tcp_write记得配合tcp_output。有些场景下tcp_write只是把数据追加到发送队列真正的发送发生在ACK到达或TCP定时器触发时。如果你希望快速看到回包手动调用tcp_output是最直接的。5.3 中断与轮询的取舍裸机上xemacpsif_input在while(1)里轮询代码简单但数据量一大主循环就会被网络接收占满。更合理的方案是把xemacpsif_input放到定时中断或网卡接收中断里执行。实测中中断驱动的好处是主循环可以同时处理串口命令、状态灯、其他业务逻辑网络吞吐也稳定。但要注意中断里不能调用可能阻塞的函数tcp_close这类操作尽量延迟到主循环做。我自己在调试时有一个技巧用一个全局标识记录网络事件主循环检测到标识后再处理业务这样即使中断里发生重入问题也能降低复杂度。5.4 后续扩展多客户端和断线重连如果只是回环调试单客户端足够。但实际项目往往要接上位机、手机App、云平台多客户端连接必不可少。实现套路是在accept回调里为每个新连接创建一个独立的PCB并挂上独立的recv和err回调。记得在tcp_err回调里做资源释放否则连接异常断开后PCB不会自动回收。断线重连也一样监听PCB要保持常驻不要把监听PCB关闭。很多人的误区是accept回调里把server_pcb的引用直接赋给了连接PCB导致后续新连接找不到监听者结果就是只能连一次。我自己的实际体会是Zynq上调试lwIP TCP最怕的不是代码复杂而是环境变量太多。把PHY地址、MAC、IP、端口、内存池大小这些变量全部固定下来再配合串口日志和Wireshark绝大多数问题都能在半小时内定位。最后再分享一个小技巧调试时先用一条短数据包验证通路比如发4字节字符通了之后再上大文件。这样即使出了问题复现路径也最短排查起来不会一头扎进性能优化里出不来。