ZYNQ裸机LWIP从TCP到UDP改造实战与避坑指南

发布时间:2026/10/15 4:39:25
ZYNQ裸机LWIP从TCP到UDP改造实战与避坑指南 1. 项目缘起与整体设计思路1.1 为什么要在裸机环境下折腾UDP做过嵌入式网络开发的人都有一个共识在资源受限的平台上跑通网络协议栈从来都不是一件轻松的事。这次要聊的项目是在ZYNQ平台上把原本基于LWIP协议栈的TCP通信方式改成UDP通信方式而且整个系统是裸跑的没有操作系统支撑。先说说这个项目的背景。ZYNQ系列芯片内部集成了ARM处理器和FPGA逻辑资源很多做工业控制、数据采集、视频传输的朋友都会选它。在裸机环境下使用LWIP协议栈通常默认的示例代码都是TCP Server或者TCP Client模式。TCP的好处是可靠传输有重传机制、有拥塞控制、有连接管理但代价就是协议栈本身占用的内存和CPU资源更多而且每次通信都需要维护连接状态。UDP就不一样了。UDP是无连接协议发送数据之前不需要建立连接收到数据也不需要确认。对于周期性上报传感器数据、实时控制指令下发、视频流传输这类场景UDP的低延迟和低开销优势非常明显。我这次要做的就是把一个已经跑通的TCP通信工程改成UDP方式同时保证底层网络驱动、PHY芯片配置、DMA描述符这些环节都不出问题。这个内容适合谁看如果你正在用ZYNQ做裸机网络开发或者你手头有一个基于LWIP的TCP工程想改成UDP再或者你刚接触LWIP协议栈想搞清楚UDP在裸机环境下到底怎么配置那这篇经验分享应该能帮你省下不少调试时间。1.2 整体方案选型与架构拆解在动手改代码之前我先梳理了一下整个系统的架构。ZYNQ的裸机网络通信底层依赖的是GEMGigabit Ethernet MAC控制器外接PHY芯片通过MIO或者EMIO接口连接。LWIP协议栈跑在ARM核上通过中断或者轮询方式处理数据包收发。原来的TCP方案LWIP的配置里需要开启LWIP_TCP宏同时lwipopts.h里要配置TCP相关的参数比如TCP_MSS、TCP_WND、TCP_SND_BUF这些。改成UDP之后这些TCP相关的配置可以关掉但UDP相关的宏必须打开比如LWIP_UDP。另外MEMP_NUM_UDP_PCB这个参数决定了同时能有多少个UDP控制块如果只是简单的点对点通信默认值通常够用但如果要做多播或者多个端口监听就需要适当调大。还有一个关键点TCP模式下LWIP内部会维护大量的连接状态结构体内存占用比较大。改成UDP之后内存压力会小很多但这也意味着可靠性需要应用层自己保证。比如数据包丢了怎么办顺序乱了怎么办这些在TCP里是协议栈自动处理的在UDP里就得自己写逻辑。我选择的方案是保留原有的GEM驱动和PHY配置只修改LWIP的应用层接口和协议栈配置。具体来说把tcp_echo或者tcp_server的示例代码替换成udp_echo或者自定义的UDP收发函数。这样改动量最小风险也最可控。提示在裸机环境下改协议栈配置一定要先备份原来的lwipopts.h文件。这个文件里的参数非常多改错一个可能导致协议栈完全跑不起来而且报错信息往往很隐晦。2. 核心细节解析与实操要点2.1 LWIP协议栈的关键配置项LWIP的配置文件lwipopts.h是整个协议栈行为的控制中心。从TCP改成UDP有几个宏必须重点关注。首先是LWIP_UDP这个宏必须定义为1否则UDP相关的代码根本不会被编译进去。其次是LWIP_TCP如果你确定不再需要TCP可以把它定义为0这样能省下不少代码空间和内存。但要注意有些LWIP的内部机制可能依赖TCP的某些结构体关掉之后需要检查编译是否通过。然后是MEMP_NUM_UDP_PCB这个参数定义了UDP协议控制块的数量。每个UDP socket都需要一个PCB如果你只用一个端口收发数据设置为4就够了。但如果要做广播或者多播可能需要调到8或者更多。还有一个容易被忽略的参数是UDP_TTL这是UDP数据包的生存时间。默认值通常是255但在某些网络环境下如果路由器跳数限制比较严格可能需要调小。不过对于局域网内的通信这个值一般不用改。内存池方面MEMP_NUM_PBUF和PBUF_POOL_SIZE决定了数据包缓冲区的数量。UDP通信如果数据量比较大比如每秒要发几百个包这两个值需要适当调大否则会出现pbuf_alloc失败的情况。// lwipopts.h 中与UDP相关的关键配置 #define LWIP_UDP 1 #define LWIP_TCP 0 #define MEMP_NUM_UDP_PCB 8 #define UDP_TTL 255 #define PBUF_POOL_SIZE 32 #define MEMP_NUM_PBUF 322.2 UDP控制块与端口绑定在LWIP里UDP通信的核心是udp_pcb结构体。创建一个UDP通信端点需要调用udp_new()分配一个PCB然后调用udp_bind()绑定本地IP和端口最后用udp_recv()注册接收回调函数。这里有一个细节udp_bind()的时候IP地址可以指定为具体的地址也可以用IP_ADDR_ANY表示监听所有网络接口。在ZYNQ裸机环境下通常只有一个网口所以用IP_ADDR_ANY或者具体的本地IP都可以。端口号的选择也有讲究。如果只是内部测试可以用1024以上的端口避免和系统保留端口冲突。如果是正式产品最好遵循行业惯例比如视频流常用5000或者8000系列端口。注册接收回调函数是UDP通信的关键一步。udp_recv()的原型是void udp_recv(struct udp_pcb *pcb, udp_recv_fn recv, void *recv_arg);其中recv是一个函数指针当有数据包到达绑定的端口时LWIP会调用这个函数。回调函数的参数包括PCB指针、数据包pbuf、发送方IP和端口。你可以在回调里解析数据然后决定是否回复。注意回调函数是在LWIP的上下文里执行的如果你在回调里做了耗时操作比如大量数据拷贝或者复杂计算可能会阻塞其他网络包的处理。建议在回调里只做数据标记把实际处理放到主循环里。2.3 数据发送的两种方式UDP发送数据有两种常用方式一种是udp_send()另一种是udp_sendto()。前者发送到PCB绑定时指定的目标地址后者可以动态指定目标IP和端口。在裸机环境下我通常推荐用udp_sendto()因为这样更灵活。比如你的设备需要向多个上位机发送数据就可以在每次发送时指定不同的目标地址而不需要创建多个PCB。发送之前需要申请一个pbuf把数据拷贝进去然后调用发送函数。发送完成后LWIP会自动释放pbuf不需要手动释放。但要注意如果发送失败比如网络不可达udp_sendto()会返回错误码这时候需要根据错误码决定是否重试。struct pbuf *p pbuf_alloc(PBUF_TRANSPORT, data_len, PBUF_RAM); if (p ! NULL) { pbuf_take(p, data, data_len); err_t err udp_sendto(upcb, p, dest_ip, dest_port); if (err ! ERR_OK) { // 发送失败处理 } pbuf_free(p); }这里有一个经验pbuf_alloc的第二个参数是数据长度第三个参数是pbuf类型。PBUF_RAM表示数据存储在内存堆里适合小数据量。如果数据量很大比如超过1500字节可能需要用PBUF_POOL或者链式pbuf。3. 实操过程与核心环节实现3.1 从TCP示例工程剥离出UDP框架假设你手头有一个已经跑通的TCP Echo工程工程里有一个tcp_echo.c文件里面实现了TCP Server的初始化和数据收发。现在要改成UDP第一步是把TCP相关的代码全部注释掉或者删除然后新建一个udp_echo.c文件。在udp_echo.c里首先定义一个全局的udp_pcb指针static struct udp_pcb *g_udp_pcb NULL;然后写一个初始化函数在系统启动时调用void udp_echo_init(void) { err_t err; ip4_addr_t ipaddr, netmask, gw; // 设置本地IP、掩码、网关 IP4_ADDR(ipaddr, 192, 168, 1, 10); IP4_ADDR(netmask, 255, 255, 255, 0); IP4_ADDR(gw, 192, 168, 1, 1); // 初始化LWIP lwip_init(); // 添加网络接口 netif_add(g_netif, ipaddr, netmask, gw, NULL, xemacif_init, ethernet_input); netif_set_default(g_netif); netif_set_up(g_netif); // 创建UDP PCB g_udp_pcb udp_new(); if (g_udp_pcb NULL) { return; } // 绑定端口 err udp_bind(g_udp_pcb, IP_ADDR_ANY, LOCAL_PORT); if (err ! ERR_OK) { return; } // 注册接收回调 udp_recv(g_udp_pcb, udp_receive_callback, NULL); }这段代码里xemacif_init是ZYNQ的GEM驱动初始化函数ethernet_input是LWIP的以太网输入函数。这两个函数在原来的TCP工程里应该已经有了直接复用即可。3.2 接收回调函数的实现细节接收回调函数是UDP通信的核心。当有数据包到达绑定的端口时LWIP会调用这个函数。回调函数的签名是固定的void udp_receive_callback(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port) { if (p NULL) { return; } // 处理接收到的数据 // p-payload 指向数据内容p-len 是数据长度 // 如果需要回复可以在这里调用udp_sendto // 释放pbuf pbuf_free(p); }这里有几个关键点。第一p可能为NULL这通常表示某种错误直接返回即可。第二p-payload指向数据内容但要注意pbuf可能是链式的如果数据长度超过单个pbuf的容量需要遍历整个链。第三处理完数据后必须调用pbuf_free(p)释放内存否则会内存泄漏。我在实际调试中发现如果接收回调里做了太多事情比如打印大量调试信息会导致后续数据包丢失。因为串口打印是阻塞的而网络数据包到达是异步的。所以建议在回调里只做数据拷贝把处理逻辑放到主循环里。3.3 主循环与网络轮询裸机环境下没有操作系统LWIP的协议栈处理需要手动轮询。通常的做法是在主循环里调用xemacif_input()或者类似的函数检查是否有新的数据包到达。while (1) { // 处理网络输入 if (g_netif_flags XEMACIF_INPUT_FLAG) { xemacif_input(g_netif); } // 其他任务 // ... // 周期性发送UDP数据 if (timer_expired(send_timer)) { udp_send_data(); timer_reset(send_timer); } }这里的xemacif_input是ZYNQ GEM驱动的接收处理函数它会从DMA描述符里读取数据包然后交给LWIP处理。如果中断模式也可以在中断服务函数里设置标志位主循环检查标志位后再调用。发送定时器可以用简单的计数器实现也可以用ZYNQ的私有定时器。如果对时间精度要求不高用计数器就够了。实操心得在调试UDP通信时建议先用网络调试助手在PC上发数据给ZYNQ确认接收正常。然后再让ZYNQ主动发数据给PC确认发送正常。最后再测试双向通信。这样分步骤调试比一上来就搞双向通信效率高得多。4. 常见问题与排查技巧实录4.1 网络不通的排查思路UDP通信调不通原因可能有很多。我一般按照从底层到上层的顺序排查。第一步检查PHY芯片是否正常。用PHY的寄存器读取函数确认链路状态寄存器显示Link Up。如果Link Down检查网线、PHY地址、MDIO接口配置。第二步检查IP地址和网关配置。在PC上ping ZYNQ的IP如果ping不通说明底层网络还没通。这时候可以用Wireshark抓包看看ZYNQ有没有发出ARP请求PC有没有回应。第三步检查UDP端口是否绑定成功。udp_bind的返回值如果是ERR_OK说明绑定成功。如果返回ERR_USE说明端口已经被占用。第四步检查防火墙。PC上的防火墙可能会拦截UDP数据包尤其是Windows防火墙。调试时可以先临时关闭防火墙。4.2 数据包丢失的常见原因UDP本身不保证可靠传输但如果在局域网内丢包严重那就不正常了。常见原因有这几个pbuf数量不足。如果PBUF_POOL_SIZE设置太小高并发时会出现pbuf_alloc失败导致数据包被丢弃。可以适当调大这个值。接收回调处理太慢。如果回调里做了耗时操作LWIP的接收队列会满后续数据包会被丢弃。网络带宽饱和。如果发送频率太高超过了百兆或者千兆网的实际吞吐量丢包是必然的。可以降低发送频率或者压缩数据。DMA描述符配置问题。ZYNQ的GEM驱动使用DMA描述符环如果描述符数量太少高速收发时会出现描述符耗尽。4.3 常见问题速查表问题现象可能原因排查方法解决方案ping不通PHY链路未建立读取PHY状态寄存器检查网线、PHY地址配置ping不通IP地址冲突在PC上arp -a查看更换IP地址UDP绑定失败端口被占用检查返回值更换端口号收不到数据防火墙拦截临时关闭防火墙添加防火墙规则收不到数据回调未注册检查udp_recv调用确保回调函数正确注册发送失败目标地址不可达检查目标IP和网关确认目标设备在线丢包严重pbuf不足查看pbuf_alloc返回值调大PBUF_POOL_SIZE丢包严重处理太慢检查回调函数耗时优化回调逻辑4.4 调试工具与技巧调试UDP通信我常用的工具有三个网络调试助手、Wireshark、串口打印。网络调试助手用来手动发送和接收UDP数据包验证基本通信是否正常。Wireshark用来抓包分析可以看到每个数据包的源地址、目标地址、端口、长度、内容。串口打印用来输出LWIP内部的调试信息比如LWIP_DEBUG开启后可以看到协议栈的处理流程。这里有一个小技巧在lwipopts.h里开启UDP_DEBUG和IP_DEBUG可以看到UDP和IP层的详细日志。但要注意调试信息会占用大量串口带宽正式发布时要关掉。还有一个技巧如果PC和ZYNQ不在同一个网段UDP通信可能会失败。因为UDP广播包通常不会被路由器转发。所以调试时最好让PC和ZYNQ在同一个交换机下或者配置正确的网关和路由。5. 性能优化与进阶建议5.1 提高UDP吞吐量的几个手段如果UDP通信的数据量比较大比如要做视频流传输吞吐量就是一个关键指标。我试过几个优化手段效果比较明显。第一个是增大pbuf的大小。默认的PBUF_POOL_BUFSIZE通常是1500左右如果每次发送的数据超过这个值LWIP会分配多个pbuf组成链。链式pbuf的处理效率比单个pbuf低。可以把PBUF_POOL_BUFSIZE调到2048或者更大减少链式pbuf的出现。第二个是使用零拷贝发送。如果数据已经在内存里可以用pbuf_alloced_custom或者直接构造pbuf避免pbuf_take的内存拷贝。这个优化在高带宽场景下效果很明显。第三个是调整DMA描述符数量。ZYNQ的GEM驱动里XEMACPS_RX_BD_NUM和XEMACPS_TX_BD_NUM决定了DMA描述符环的大小。适当增大这两个值可以减少描述符耗尽的概率。5.2 应用层可靠性的简单实现UDP不保证可靠但很多场景又需要一定的可靠性。我的做法是在应用层加一个简单的确认和重传机制。比如发送方在数据包里加一个序列号接收方收到后回复一个ACK包ACK包里带上收到的序列号。发送方如果在一定时间内没收到ACK就重传。这个机制不需要太复杂对于局域网内的通信重传一两次通常就能成功。还有一种做法是加前向纠错编码比如在发送的数据里加入冗余信息接收方即使丢了部分数据包也能恢复。但这个实现起来比较复杂适合对实时性要求极高的场景。5.3 从UDP扩展到多播和广播UDP天然支持多播和广播。在LWIP里要接收多播数据需要调用igmp_joingroup加入多播组。要发送广播数据把目标地址设为255.255.255.255或者子网广播地址即可。多播在工业控制里很有用比如一个控制器同时向多个执行器发送指令。广播则适合设备发现场景比如新设备上线时广播一个发现包等待服务器回应。不过要注意多播和广播会增加网络负载如果网络里设备很多可能会造成广播风暴。所以实际使用时要合理规划多播组和广播域。6. 个人实操体会与避坑总结这个项目从TCP改到UDP前前后后花了大概一周时间其中大部分时间都花在调试网络不通和丢包问题上。踩过的坑主要有这几个第一个坑是PHY地址配错。ZYNQ的GEM控制器可以连接多个PHY每个PHY有一个地址。如果地址配错MDIO读写会失败链路永远起不来。我一开始就是没注意这个查了半天才发现。第二个坑是lwipopts.h里的MEM_SIZE设置太小。TCP改成UDP后虽然协议栈本身的内存需求小了但如果应用层的数据缓冲区设得太大还是会内存不足。后来我把MEM_SIZE从默认的1600调到了4096问题就解决了。第三个坑是接收回调里调用了printf。在裸机环境下printf通常是阻塞的如果回调里打印大量信息会导致后续数据包丢失。后来我把调试信息改成写入环形缓冲区在主循环里统一输出问题就消失了。第四个坑是没开LWIP_UDP宏。这个听起来很蠢但确实发生了。因为原来的工程是TCP的lwipopts.h里LWIP_UDP默认是0。我改了应用层代码但忘了改这个宏结果编译出来的固件根本没有UDP功能。所以改配置的时候一定要把相关的宏都检查一遍。最后分享一个小技巧在调试UDP通信时可以在PC上开两个网络调试助手一个用来发数据一个用来收数据。这样可以同时观察发送和接收的情况比只用一个工具效率高很多。另外Wireshark的过滤器可以设置为udp.port 你的端口号这样只显示相关的数据包避免被其他流量干扰。这个工程后续还可以继续扩展比如加入DHCP自动获取IP或者加入SNMP简单网络管理协议方便远程监控设备状态。如果要做产品化还需要考虑网络异常时的自动恢复机制比如链路断开后自动重连。这些内容等以后有机会再单独整理分享。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询