
1. 项目缘起与整体方案设计ZYNQ 千兆网 UDP 通信这个事说简单也简单说坑多那也是真多。我前前后后做过好几个基于 ZYNQ 的数据采集和传输项目从 7010 到 7045 都用过千兆网这块踩过的坑能写满一个笔记本。这次就把整个流程从头到尾捋一遍从 Vivado 里的 Block Design 配置到 LWIP 协议栈的移植和调优再到实际测试中遇到的各种奇葩问题尽量讲透。先说说为什么选 UDP 而不是 TCP。很多人一上来就觉得 TCP 可靠、有重传、有拥塞控制肯定更好。但在 ZYNQ 做高速数据采集的场景下UDP 反而是更务实的选择。原因很简单TCP 的重传机制和滑动窗口在嵌入式端实现起来开销大而且一旦丢包延迟会急剧上升对于实时性要求高的数据流来说TCP 的“可靠”反而变成了负担。UDP 虽然不保证送达但胜在简单、延迟低、吞吐量容易做上去。实际项目中我们通常会在应用层自己做轻量级的确认和重传或者干脆接受少量丢包用上层协议来补。整个方案的核心思路是这样的PL 端负责数据采集和预处理通过 AXI-Stream 或者 AXI-Lite 把数据送到 PS 端PS 端跑裸机程序或者 Linux用 LWIP 协议栈把数据打包成 UDP 包通过 ZYNQ 内置的 GEMGigabit Ethernet MAC发送出去。接收方向反过来PC 端发指令ZYNQ 收到后解析并执行相应动作。这个架构的好处是软硬件分工明确PL 做它擅长的并行处理PS 做协议栈和逻辑控制。注意ZYNQ 的 GEM 控制器支持 RGMII 和 SGMII 两种接口模式板级设计时一定要确认 PHY 芯片的型号和接口方式这个在后面配置时会直接影响管脚约束和时钟方案。方案选型上我强烈建议先用裸机跑通 LWIP 的 UDP 回环测试再上 Linux 或者 Petalinux。裸机的好处是调试直观没有操作系统调度带来的不确定性出了问题容易定位。等裸机跑通了再迁移到 Linux 下心里就有底了。很多人一上来就搞 Petalinux结果网络不通连是硬件问题还是软件问题都分不清白白浪费时间。2. Vivado 工程配置与硬件设计要点2.1 Block Design 搭建与 GEM 配置打开 Vivado 之后新建工程选好对应的 ZYNQ 芯片型号。这一步千万别选错不同型号的 GEM 数量和管脚分配可能不一样。比如 ZYNQ-7020 有两个 GEM但有些板子只引出了一个。选错型号后面管脚约束会报一堆错。在 Block Design 里添加 ZYNQ Processing System IP双击进去配置。重点看这几个地方Peripheral I/O Pins使能 GEM0 或者 GEM1取决于你的板子网口接在哪个控制器上。一般板子上丝印会标 ETH0、ETH1对应 GEM0、GEM1。GEM 配置在 GEM 的配置页面里选择 RGMII 接口勾选 MDIO。如果你的 PHY 支持 1000M 全双工把速度设成 1000Mbps。有些 PHY 需要额外的复位信号这个在管脚约束里要加上。Clock ConfigurationGEM 的时钟来源要搞清楚。RGMII 模式下通常需要 125MHz 的时钟给 TX 和 RX。这个时钟可以由 PS 的 PLL 产生也可以从外部晶振直接引。我一般用 PS 内部 PLL 生成省事。但要注意如果 PHY 需要 25MHz 或者 50MHz 的参考时钟得单独配置一个时钟输出。InterruptsGEM 的中断要连到 PS 的 GIC 上LWIP 的收发中断就靠它了。在 ZYNQ 配置的 Interrupts 页面里把 GEM 的中断勾上。配置完 PS 之后别忘了添加复位模块和时钟模块。Processor System Reset 和 Clocking Wizard 是标配用来给 PL 端的逻辑提供复位和时钟。如果 PL 端有自定义 IP也要在这里连好。2.2 管脚约束与时钟约束管脚约束这块是新手最容易翻车的地方。RGMII 接口的管脚包括TXC发送时钟125MHzTXD[3:0]发送数据4 位TX_CTL发送控制表示数据有效RXC接收时钟由 PHY 提供125MHzRXD[3:0]接收数据RX_CTL接收控制MDC管理时钟MDIO管理数据在 XDC 文件里这些管脚都要一一对应到具体的 FPGA 引脚上。管脚号从原理图里查别偷懒一定要对着原理图一个一个核对。我见过有人把 TXD 和 RXD 搞反了结果网络死活不通查了半天才发现是管脚约束写错了。时钟约束也很关键。RXC 是 PHY 提供的 125MHz 时钟需要在 XDC 里创建对应的时钟约束create_clock -period 8.000 -name rx_clk [get_ports RXC]TXC 是 FPGA 输出的一般由 PS 的时钟经过 BUFG 驱动。如果时序不满足Vivado 会报 timing violation这时候要检查时钟路径和管脚延迟。实操心得RGMII 接口的时序要求比较严格特别是 TX 方向。如果 PHY 支持内部延迟可以在 PHY 寄存器里配置 TXC 的延迟这样 FPGA 端就不用额外加延迟逻辑了。具体看 PHY 的数据手册一般都有说明。2.3 生成比特流与导出硬件配置完成后Validate Design 一下确保没有错误。然后 Generate Output Products再 Create HDL Wrapper。接着跑 Synthesis 和 Implementation。如果 Implementation 变红先看 Timing Report大概率是时钟约束没写对或者管脚分配有问题。生成比特流之后导出硬件。File - Export - Export Hardware勾选 Include bitstream。这一步会生成一个 .xsa 文件后面在 Vitis 或者 SDK 里要用到。注意如果后面要用 Petalinux导出的 .xsa 文件就是创建工程的输入。所以这一步一定要做而且比特流要包含进去。3. LWIP 协议栈移植与 UDP 通信实现3.1 裸机 LWIP 工程搭建打开 Vitis新建 Platform Project选择刚才导出的 .xsa 文件。然后新建 Application Project模板选 LWIP Echo Server 或者 Empty Application。我一般选 Empty然后自己加 LWIP 的库这样可控性更强。LWIP 的配置主要在 lwipopts.h 文件里。这个文件决定了协议栈的行为比如是否启用 UDP、TCP、DHCP、内存池大小等等。对于 UDP 通信关键配置项有#define LWIP_UDP 1 #define LWIP_TCP 0 #define MEMP_NUM_UDP_PCB 4 #define UDP_TTL 255 #define LWIP_NETIF_TX_SINGLE_PBUF 1 #define MEM_SIZE (16*1024) #define PBUF_POOL_SIZE 16 #define PBUF_POOL_BUFSIZE 1536这里解释几个关键参数。MEMP_NUM_UDP_PCB是 UDP 协议控制块的数量如果你要同时监听多个端口这个值要相应增加。PBUF_POOL_SIZE是 pbuf 池的大小每个 pbuf 大概 1.5KB16 个就是 24KB 左右。如果发送大数据量这个值要调大否则会出现 pbuf 分配失败。LWIP_NETIF_TX_SINGLE_PBUF设为 1 可以让发送时尽量用一个 pbuf减少链式操作的开销。3.2 网络接口初始化与 PHY 配置LWIP 初始化流程大概是这样的先初始化 lwip然后添加网络接口设置 IP 地址、子网掩码、网关最后启动网络接口。ZYNQ 的 GEM 驱动在 xemacps 里Vitis 的 BSP 已经帮我们封装好了。struct netif server_netif; ip_addr_t ipaddr, netmask, gw; IP4_ADDR(ipaddr, 192, 168, 1, 10); IP4_ADDR(netmask, 255, 255, 255, 0); IP4_ADDR(gw, 192, 168, 1, 1); lwip_init(); netif_add(server_netif, ipaddr, netmask, gw, NULL, xemacpsif_init, ethernet_input); netif_set_default(server_netif); netif_set_up(server_netif);PHY 的配置通过 MDIO 接口进行。ZYNQ 的 GEM 驱动会自动扫描 PHY 地址但有时候 PHY 地址不是默认的 0需要在 xemacpsif 的初始化代码里手动指定。如果网络不通第一件事就是用xemacps_phy_read读一下 PHY 的 ID 寄存器确认 MDIO 通信是否正常。避坑指南有些板子的 PHY 复位信号没有接到 FPGA 上而是通过 RC 电路自动复位。这种情况下上电后 PHY 可能需要一段时间才能稳定工作。如果程序跑得太快PHY 还没准备好网络初始化就会失败。解决办法是在初始化前加个延时比如usleep(100000)等 PHY 稳定后再配置。3.3 UDP 数据收发实现UDP 收发的核心是udp_new、udp_bind、udp_recv和udp_sendto这几个函数。下面是一个简单的回环测试代码struct udp_pcb *upcb; void udp_receive_callback(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port) { if (p ! NULL) { // 回显数据 udp_sendto(pcb, p, addr, port); pbuf_free(p); } } void udp_server_init(void) { err_t err; upcb udp_new(); if (upcb ! NULL) { err udp_bind(upcb, IP_ADDR_ANY, 8080); if (err ERR_OK) { udp_recv(upcb, udp_receive_callback, NULL); } } }发送大数据时要注意 pbuf 的分配方式。如果数据超过 MTU1500 字节需要分片。LWIP 会自动处理 IP 分片但分片会降低效率而且一旦某个分片丢失整个包就废了。所以实际项目中我一般把 UDP 包大小控制在 1400 字节以内避免分片。void udp_send_data(struct udp_pcb *pcb, ip_addr_t *addr, u16_t port, u8_t *data, u16_t len) { struct pbuf *p pbuf_alloc(PBUF_TRANSPORT, len, PBUF_RAM); if (p ! NULL) { pbuf_take(p, data, len); udp_sendto(pcb, p, addr, port); pbuf_free(p); } }实操心得pbuf_alloc的第三个参数用PBUF_RAM还是PBUF_POOL有讲究。PBUF_RAM从堆里分配适合大数据块PBUF_POOL从池里分配速度快但大小固定。发送时用PBUF_RAM更灵活接收时 LWIP 默认用PBUF_POOL。4. 性能调优与实测数据分析4.1 吞吐量优化策略裸机 LWIP 的 UDP 吞吐量能做到多少我实测下来ZYNQ-7020 在 1000Mbps 模式下UDP 发送能跑到 600-800Mbps接收大概 400-600Mbps。这个数字受很多因素影响比如 CPU 主频、编译器优化等级、内存带宽等等。想再往上提有几个方向可以试提高 CPU 主频ZYNQ-7020 的 PS 端 ARM Cortex-A9 最高能跑到 866MHz但默认可能是 666MHz。在 Vivado 的 PS 配置里把主频拉满吞吐量能提升 20% 左右。开启编译器优化Vitis 里把优化等级设成-O2或者-O3别用-O0。-O3有时候会带来奇怪的 bug-O2比较稳妥。调整 pbuf 池大小PBUF_POOL_SIZE和MEM_SIZE适当加大减少分配失败的概率。使用零拷贝发送如果数据在 PL 端已经准备好了可以直接把 AXI-Stream 的数据搬到 pbuf 里避免二次拷贝。这个需要改驱动稍微麻烦点但效果明显。4.2 实测数据与瓶颈分析我用 iperf3 在 PC 端打流测试过ZYNQ 作为 UDP 服务端PC 端发送 1000Mbps 的 UDP 流ZYNQ 接收端的丢包率大概在 5%-10%。这个丢包主要来自几个方面丢包原因占比解决办法pbuf 分配失败40%增大 PBUF_POOL_SIZE中断处理不及时30%提高中断优先级减少中断处理时间内存带宽瓶颈20%优化数据路径减少 DDR 访问PHY 或链路问题10%检查网线、PHY 配置中断处理这块特别关键。GEM 的中断频率很高如果中断处理函数里做了太多事情比如打印调试信息就会导致后续中断丢失。我的做法是中断里只做最简单的标记把实际处理放到主循环里。void gem_isr(void *arg) { // 只清除中断标志不做其他事 XEmacPs_ClearInterruptStatus(emacps, XEMACPS_IXR_RX_OVR_MASK); // 设置一个标志主循环里处理 rx_flag 1; }4.3 LWIP 内存配置与调优LWIP 的内存管理有两种方式MEM_LIBC_MALLOC和MEM_USE_POOLS。默认是用 LWIP 自己的内存堆通过MEM_SIZE定义大小。如果MEM_SIZE太小pbuf_alloc会失败太大又浪费内存。我一般设成 16KB 到 64KB 之间根据实际数据量调整。还有一个容易忽略的参数是TCPIP_MBOX_SIZE和DEFAULT_UDP_RECVMBOX_SIZE。这两个是消息邮箱的大小如果收发频繁邮箱满了会导致数据丢失。我一般设成 16 或者 32。#define TCPIP_MBOX_SIZE 32 #define DEFAULT_UDP_RECVMBOX_SIZE 16 #define DEFAULT_TCP_RECVMBOX_SIZE 16 #define DEFAULT_ACCEPTMBOX_SIZE 16避坑指南修改 lwipopts.h 之后一定要重新编译整个 BSP 和 Application有时候 Vitis 的增量编译会漏掉一些文件导致配置没生效。保险起见Clean 一下再 Build。5. 常见问题排查与避坑实录5.1 网络不通的排查思路网络不通是最常见的问题排查起来要有条理。我一般按这个顺序来检查 PHY 是否正常用 MDIO 读 PHY 的 BMSR 寄存器看 Link 状态位是否置起。如果 Link 没起来检查网线、PHY 供电、复位信号。检查 MAC 配置确认 GEM 的速度模式设置正确RGMII 模式下要确保 TXC 和 RXC 的时钟正常。检查 IP 配置PC 和 ZYNQ 要在同一个网段子网掩码一致。用ping测试连通性。检查防火墙PC 端的防火墙有时候会拦截 UDP 包测试时先关掉。检查 ARP如果 ping 不通用 Wireshark 抓包看 ARP 请求有没有回应。如果 ARP 都不通说明链路层有问题。5.2 常见问题速查表现象可能原因解决办法PHY Link 灯不亮网线问题、PHY 复位、供电换网线、检查复位电路、测 PHY 电压ping 不通IP 配置错误、防火墙、ARP 问题检查 IP、关防火墙、抓包分析UDP 丢包严重pbuf 不足、中断丢失、带宽瓶颈增大 pbuf 池、优化中断、降低发送速率发送大包失败MTU 限制、分片问题控制包大小在 1400 字节以内程序跑飞内存溢出、堆栈不足增大堆栈、检查内存分配Vivado Implementation 变红时序不满足、管脚冲突检查时钟约束、管脚分配5.3 独家避坑技巧说几个我踩过的坑都是血泪教训。第一个坑PHY 地址不对。有些板子的 PHY 地址不是 0而是 1 或者 7。ZYNQ 的驱动默认从地址 0 开始扫描如果 PHY 地址不是 0就找不到。解决办法是在xemacpsif_init里手动指定 PHY 地址或者修改XEmacPs_PhyRead的调用参数。第二个坑MDIO 时钟太快。ZYNQ 的 MDIO 时钟是从 GEM 的时钟分频来的默认分频系数可能太小导致 MDIO 通信不稳定。在xemacpsif的配置里把 MDIO 时钟分频调大比如设成 64 或者 128。第三个坑缓存一致性问题。ZYNQ 的 PS 端有 L1 和 L2 缓存DMA 传输的数据如果不做缓存维护CPU 读到的可能是旧数据。发送前要Xil_DCacheFlush接收后要Xil_DCacheInvalidate。这个坑在裸机下特别隐蔽因为有时候数据看起来是对的但偶尔会出错。// 发送前刷新缓存 Xil_DCacheFlushRange((u32)tx_buffer, tx_len); // 接收后无效化缓存 Xil_DCacheInvalidateRange((u32)rx_buffer, rx_len);第四个坑中断优先级配置。ZYNQ 的 GIC 中断优先级如果配得太低会被其他中断打断导致网络中断处理延迟。在XScuGic_SetPriorityTriggerType里把 GEM 中断的优先级设高一点比如 0xA0。实操心得调试网络问题时Wireshark 是最好的朋友。在 PC 端抓包能看到 ZYNQ 到底发了什么、收了什么。如果 ZYNQ 根本没发包那就是 ZYNQ 端的问题如果发了但 PC 没收到那就是链路或者 PC 端的问题。抓包能省掉很多瞎猜的时间。6. 从裸机到 Linux 的迁移建议裸机跑通之后如果项目需要更复杂的网络功能比如多线程、文件系统、Web 服务器那就得上 Linux 了。Petalinux 是 Xilinx 官方的 Linux 发行版配置起来还算方便。迁移的时候LWIP 那套代码就用不上了Linux 下有完整的 TCP/IP 协议栈。UDP 通信直接用 socket API 就行int sockfd socket(AF_INET, SOCK_DGRAM, 0); struct sockaddr_in servaddr; servaddr.sin_family AF_INET; servaddr.sin_addr.s_addr htonl(INADDR_ANY); servaddr.sin_port htons(8080); bind(sockfd, (struct sockaddr *)servaddr, sizeof(servaddr));Linux 下的性能调优和裸机不太一样。重点在几个地方网络缓冲区大小、中断亲和性、CPU 频率调节。ethtool可以看和改网卡的 Ring Buffer 大小/proc/sys/net/core/rmem_max和wmem_max可以改 socket 缓冲区上限。Petalinux 生成 boot.bin 和 image.ub 的步骤这里就不展开了网上教程很多。重点提醒一句制作 SD 卡的时候FAT32 分区里放 boot.bin 和 image.ubEXT4 分区里放 rootfs。boot.scr 是可选的如果要用脚本配置启动参数就加上。注意Petalinux 2025.1 版本对 ZYNQ 的支持有一些变化设备树和内核配置和旧版本不太一样。如果从旧版本迁移设备树里的 GEM 节点要仔细核对特别是 PHY 的 compatible 属性和复位管脚配置。从裸机到 Linux最大的变化是调试方式。裸机下可以用 JTAG 单步调试Linux 下更多靠 printk 和 ftrace。网络问题在 Linux 下反而好查一些因为工具多tcpdump、netstat、ss 都能用。我个人在实际操作中的体会是ZYNQ 千兆网 UDP 通信这个事硬件配置是基础LWIP 移植是关键性能调优是锦上添花。先把基础打通再逐步优化别一上来就追求极致性能。遇到问题多抓包、多查手册大部分坑都是因为细节没注意到。最后再分享一个小技巧如果手头没有示波器测 RGMII 时序可以用 Vivado 的 ILA 抓一下 TXC 和 TXD 的信号看看时序关系对不对这个比盲猜靠谱多了。