
一直做单网口的裸机lwIP某天项目方突然提了一个需求ZYNQ上要跑两个独立网口一路走实时数据采集一路走后台管理控制两边互不干扰。说实话第一次听到“ZYNQ裸机双网口”这个词的时候我第一反应是“这行吗”ZYNQ本身带了两个千兆以太网MAC这个没疑问但裸机环境下的lwIP协议栈平时大家能调通一路就不错了两路同时跑起来中断、内存、MAC地址、PHY芯片全是坑。这篇文章把我从硬件配置到最终调通双网口的完整过程整理一遍。内容包括Vivado工程怎么把两个GEM都使能、BSP里lwIP怎么初始化两个netif、两路中断怎么共存、以及我在实际调试中碰到的一堆让人头疼的“玄学问题”。写出来主要是给准备在ZYNQ上做裸机双网口的朋友当一份操作手册避免大家再浪费几个通宵踩我踩过的坑。硬件上有点基础的工程师应该都能照着做下来如果你只跑过单网口这文章正好帮你把双网口的缺口补齐。1. 项目背景与方案选型1.1 为什么是双网口一个很现实的需求场景单网口的板子其实能覆盖大部分网络通信场景。一个网口既能传数据又可以做设备管理顶多是把两种应用混在同一个IP上用不同端口号区分。但实际项目落地时单网口有几个非常难受的限制第一带宽挤占。假如一路在跑类似图像传输、批量文件上传这种大流量数据另一路要维持心跳或者定时上报状态。同一个网口上两种流量完全没法隔离大流量一来小包直接延迟飙升严重时甚至丢包。把数据流和管理流分开到两个网口可以做到物理隔离互不干扰。第二冗余需求。某些产品对网络可靠性要求比较高比如电力设备、工业控制器双网口可以做成主备冗余。一个断线了另一个自动接管系统不用停机。裸机环境下这个主备切换逻辑完全由自己写反而比Linux下更可控。第三成本预算不变但接口增多。客户想用一个FPGA芯片同时承担控制和采集ZYNQ的PS侧自带了两个千兆以太网控制器不加外部扩展芯片就能实现双物理网口硬件BOM成本几乎不增加。我这次的项目就是典型的“数据采集管理控制”双链路需求。一路下接设备数据包另一路上联网关做设备管理。两个网口独立IP独立VLAN处理哪怕管理网络故障数据采集链路也不能受影响。所以双网口是硬需求不是可选项。1.2 为什么选裸机而不是Linux实时与资源的取舍同样实现双网口用嵌入式Linux似乎更顺理成章——PetaLinux跑起来内核里eth0、eth1都枚举好应用层两张socket一开就完事了。Linux方案成熟调试省事这也是为什么网上ZYNQ双网口的大部分教程都是Linux方向。但我的项目不能用Linux。原因有两个一个是实时性另一个是启动速度。裸机环境下网络中断一来CPU会立刻响应不会因为进程调度、系统调用、协议栈软中断这些东西引入不确定延迟。我们的数据采集链路对单包处理时延有严格约束Linux在极端负载下很容易出现几毫秒甚至几十毫秒的抖动裸机基本能做到微秒级响应。另一个是上电启动设备要求上电几百毫秒内就要能通过网络发包Linux光起内核就不止这个时间裸机压根不用纠结。所以“裸机双网口”这个组合看着别扭其实是权衡了实时性、启动速度、资源占用之后的合理选择。协议栈选lwIP也是因为它在裸机上跑得动、开源、可裁剪是嵌入式领域的事实标准。1.3 双网口方案的总体架构实际搭建之前先把这个方案的总体架构梳理清楚硬件层面ZYNQ PS自带两个千兆以太网控制器GEM0和GEM1外部两个PHY芯片分别对应两个物理网口。控制器通过RGMII接口把数据传到PHYPHY再和变压器/网口座连接。软件层面裸机上跑lwIP协议栈为GEM0和GEM1各创建一个netif网络接口两个接口共用同一套协议栈实例。中断层面GEM0和GEM1各自有独立的中断信号挂到同一个中断控制器GIC上各自驱动各自的DMA收发通道。应用层面两个网口各跑独立的TCP/UDP服务绑定各自的IP和端口。这个架构看起来清爽但落到代码上有一堆细节要处理。接下来按顺序从硬件配置到软件实现逐个讲。2. 硬件底层与Vivado工程配置2.1 ZYNQ的GEM0/GEM1到底是怎么个结构ZYNQ-7000的PSProcessing System里有两个独立的千兆以太网MAC控制器官方叫法分别是Gigabit Ethernet MAC 0和Gigabit Ethernet MAC 1对应设备树或硬件平台里的GEM0、GEM1。这两个控制器本质上是一样的硬件IP支持RGMII、GMII、MII和SGMII接口标准但各自有独立的寄存器空间、DMA通道和中断号。要特别注意这两个MAC控制器在PS内部是硬核不占FPGA逻辑资源除非走EMIO。这意味着哪怕你不启用Vivado里的任何FPGA逻辑只要PS能把引脚引出、外部接好PHYCPU就能直接通过内存映射访问这两个以太网控制器。每个GEM的接口信号分两块数据接口用于收发以太网帧我们通常用RGMII接口四路数据线时钟线控制线。MDIO接口用于读写外部PHY芯片的寄存器配置PHY的工作模式、速度、状态等。这两个信号组都通过MIO或EMIO引出。MIO是PS侧专用引脚可以不经FPGA直接连接外部设备EMIO则要先从PS引出到PL可编程逻辑再由PL侧引脚定义连接出去。MIO引脚数量有限且功能固定EMIO相对灵活。2.2 MIO还是EMIO引脚规划的关键抉择第一次做ZYNQ引脚分配时最容易困惑的就是GEM0和GEM1到底应该接MIO还是接EMIO先看MIO的方式。ZYNQ的MIO引脚是PS开始通电时就固定的其中一个MIO引脚区域可以映射到以太网的RGMII接口。GEM0可以占用MIO16到MIO27GEM1可以占用MIO28到MIO39。用MIO的好处是信号不经过FPGA时序容易保证驱动强度直接由PS控制软件配置最省事。但MIO有一个大问题引脚占用。MIO总共只有54个UART、SD卡、USB、SPI、I2C、GPIO样样都要往上挂。如果你把GEM0和GEM1都放在MIO上UART和SD卡这些几乎每个系统都少不了的外设可能就无处安放了。所以大部分ZYNQ实际设计里至少有一路甚至两路以太网要走EMIO。走EMIO的话RGMII信号从PS出来后要先经过PL内部的连线再通过FPGA引脚输出到外部PHY。这样灵活是灵活但会引入几个隐患需要额外的引脚约束手工指定每个RGMII信号对应哪个FPGA引脚、什么电平标准。EMIO路径有PL布线延迟极端情况下高速信号时序容易紧张需要对EMIO输出加约束或做I/O延迟补偿。PL部分虽然没逻辑功能但必须配置成“透传”模式否则信号不通。我这次项目的板子在设计时已经优化过GEM0走了MIOGEM1走了EMIO分别对着两个PHY芯片的RGMII口。如果你拿到手的开发板是双网口板子拿到后先打开官方的硬件工程看板级约束确认每个GEM的引脚接法千万别凭感觉配。2.3 Vivado里使能双网口的具体配置新建Vivado工程以后具体操作步骤如下。在IP Integrator里加入ZYNQ7 Processing System IP核双击打开配置界面进入Peripheral IO Pins页面。这里会看到两大类接口一类是MIO的分配另一类是EMIO。我的做法是第一步勾选GEM0和GEM1两个以太网控制器。如果你走MIO直接选对应的引脚组如果走EMIO则在EMIO标签下勾选“GEM0”和“GEM1”对应的信号组。注意GEM0和GEM1总共就可能占用两组RGMII信号别只勾其中一个然后另一个漏掉。第二步配置每个GEM的接口标准。在Zynq Processing System配置页里找到“Gigabit Ethernet”相关的子项每个GEM可以选择接口类型。工程里用的是千兆RGMII所以选择RGMII。有些板子用的是SGMII光纤接口那就要额外配置GTX收发器完全另一套玩法。第三步确认MDIO接口的引脚分配。MDIO是管理通道通常紧跟在RGMII信号附近。如果两个PHY都挂在同一组MDIO总线上也可以只使用一个GEM的MDIO信号另一个GEM单独成组。项目里两个GEM各自有独立的MDIO信号线配置时不要把两者混在一起。第四步检查时钟。ZYNQ PS侧有以太网专用时钟源一般由一个内部的125MHz参考时钟经过PLL产生。在配置界面里能看到以太网时钟相关的选项默认会自动生成一般不用动。但外部PHY芯片的时钟模式需要注意——RGMII有两种时钟模式一种是PHY提供125MHz时钟给MACMAC被动接收另一种是MAC提供125MHz时钟给PHYMAC主动发送。ZYNQ通常配置为MAC提供时钟也就是GEM的tx_clk输出125MHz。PHY那边的硬件设计要与之匹配软件上基本不需要额外处理。配置完成后生成比特流并导出到Vitis注意在导出硬件时勾选“Include bitstream”。虽然我们裸机程序运行时也可以不加载bitstream但EMIO引脚的复用关系依赖PL配置实际应用中建议还是把bit流一起烧进去避免信号悬空。3. 裸机驱动与lwIP移植3.1 BSP生成与lwIP库版本选择硬件导出到Vitis以后创建Platform工程时选择“Bare Metal”操作系统Vitis会为ZYNQ生成对应的BSPBoard Support Package。默认的BSP里包含了串口驱动、GIC中断驱动、DDR初始化、以及xemacps的驱动等。接下来要单独添加lwIP库。在Vitis的Board Support Package设置界面里点击“Manage Libraries”可以看到一堆官方库找到lwip211或者lwip202不同Vitis版本带的lwIP版本略有差异勾选添加。这里多说一句官方裸机BSP里有一个lwIP库还有一个配套的lwip_example模板但这个模板默认只创建了一个netif也就是单网口。我们做双网口不能直接用它跑通的模板必须自己改初始化代码。lwIP的版本选择上新版Vitis默认提供lwIP 2.x版本。lwIP 2.x提供了较完整的netif多接口支持比1.x时代友好得多代码也相对规范。如果工程创建时选lwip202这类的BSP版本那么协议栈本身是允许创建多个网络接口的关键在于Xilinx提供的硬件适配层xemacpsif、xemacpsif_dma需要为每个接口配置独立的实例。如果你用的老版本VitisBSP里还是lwip141那劝你尽早升级或者手动替换协议栈。lwIP 1.4.1的多接口支持极其原始在双网口场景下会遇到大量ARP和路由选择上的问题调试成本远高于升版本的成本。3.2 创建双netif的初始化流程lwIP的多网口模式说白了就是同一协议栈实例下有多个netif结构体每个netif对应一个物理网口。协议栈在做IP选路的时候先查路由表再根据目标IP选择从哪个netif发出数据包。在Xilinx的裸机以太网适配层里每个netif对应一个硬件MAC实例。我们需要给两个MAC分别调用它的初始化函数。先看初始化入口代码怎么组织#include xparameters.h #include netif/xemacpsif.h #include lwip/init.h #include lwip/netif.h #include lwip/tcpip.h #define ETH0_IPADDR0 192 #define ETH0_IPADDR1 168 #define ETH0_IPADDR2 0 #define ETH0_IPADDR3 10 #define ETH0_NETMASK0 255 #define ETH0_NETMASK1 255 #define ETH0_NETMASK2 255 #define ETH0_NETMASK3 0 #define ETH1_IPADDR0 192 #define ETH1_IPADDR1 168 #define ETH1_IPADDR2 1 #define ETH1_IPADDR3 10 #define ETH1_NETMASK0 255 #define ETH1_NETMASK1 255 #define ETH1_NETMASK2 255 #define ETH1_NETMASK3 0 struct netif eth0_netif, eth1_netif; ip_addr_t ipaddr0, netmask0, gw0; ip_addr_t ipaddr1, netmask1, gw1;注意传统单网口模板里通常只有一组netif变量和一个ip_addr_t。到了双网口必须分成两组独立变量。接着在main函数里配置lwIP并添加两个netifint main(void) { lwip_init(); IP4_ADDR(ipaddr0, ETH0_IPADDR0, ETH0_IPADDR1, ETH0_IPADDR2, ETH0_IPADDR3); IP4_ADDR(netmask0, ETH0_NETMASK0, ETH0_NETMASK1, ETH0_NETMASK2, ETH0_NETMASK3); IP4_ADDR(gw0, 0, 0, 0, 0); netif_add(eth0_netif, ipaddr0, netmask0, gw0, NULL, xemacpsif_init, ethernet_input); IP4_ADDR(ipaddr1, ETH1_IPADDR0, ETH1_IPADDR1, ETH1_IPADDR2, ETH1_IPADDR3); IP4_ADDR(netmask1, ETH1_NETMASK0, ETH1_NETMASK1, ETH1_NETMASK2, ETH1_NETMASK3); IP4_ADDR(gw1, 0, 0, 0, 0); netif_add(eth1_netif, ipaddr1, netmask1, gw1, NULL, xemacpsif_init, ethernet_input); netif_set_default(eth0_netif); netif_set_up(eth0_netif); netif_set_up(eth1_netif); ... }这里最核心的是xemacpsif_init。为什么两个netif都调用同一个函数名在较新的Xilinx适配层里xemacpsif_init函数内部会根据传入的struct netif指针关联到哪个硬件MAC实例。但代码里有一个硬伤默认适配层为每个netif分配DMA缓冲区的实例可能只有一个。你需要打开xemacpsif_dma.c确认它是用了静态变量还是数组结构。我用的Vitis 2022.2版本xemacpsif_dma.c里已经支持了多个实例但如果你拿到的模板是旧的就得自己把里面的静态TX/RX缓冲区结构改成数组。具体做法是把类似这样的定义static XEmacPs_Config *EmacPsConfig; static XEmacPs EmacPsInstance;改成static XEmacPs_Config *EmacPsConfig[2]; static XEmacPs EmacPsInstance[2];然后各个函数里对EmacPsInstance的引用都改成带索引的数组访问。这项工作不复杂但是极其容易漏改编译能过运行起来却不知道死在哪个指针访问上。3.3 lwipopts.h关键参数配置lwIP的可裁剪性通过lwipopts.h体现。双网口相比单网口最直接的差异是内存需求变大。这个文件里几个参数需要特别关注。首先是MEM_SIZE这是lwIP堆的总大小用于各种动态内存分配。单网口时常见配置是(1600 * 50)左右也就是能够容纳几十个最大尺寸数据包的层级。到了双网口两个网口可能同时在收包、同时需要为到来的TCP连接分配PCB内存需求按比例增长。我把MEM_SIZE配置为(1600 * 80)实际跑起来看使用率还留有余量。然后是PBUF_POOL_SIZE这是池化分配的内存数量专门用于接收缓冲区。接收中断从DMA拿回来的数据包要挂在队列里交给协议栈处理如果池的大小不够接收路径会直接丢包。双网口下建议把PBUF_POOL_SIZE调到至少PBUF_POOL_BUFSIZE支持的范围比如30个起步。我实测在两边同时跑收发压力测试时PBUF_POOL_SIZE20就会偶发丢包改成40后稳定下来。TCP_WNDTCP窗口大小和TCP_SND_BUF发送缓冲区大小也要注意。双网口跑TCP时两路各自维护收发窗口每路都有独立的缓冲需求。单网口时窗口大小无所谓双网口时协议栈的内存池是按全局分配的窗口开太大内存峰值会很高。建议小的嵌入式场景先保持TCP_WND2048、TCP_SND_BUF2048稳定后按需调整。另外裸机环境通常定义NO_SYS1表示lwIP运行在无操作系统的“raw API”模式下所有收发直接由中断驱动、主循环轮询处理。这个配置不用改保持1即可。还有一点调试刚调通时建议临时把这些打印宏打开#define LWIP_DEBUG 1 #define ETHARP_DEBUG LWIP_DBG_ON #define IP_DEBUG LWIP_DBG_ON #define TCP_DEBUG LWIP_DBG_ON等系统稳定了再关掉。lwIP的调试信息对排查双网口路由、ARP问题很有帮助尤其是ARP调试信息能直接看到两个接口的ARP请求和应答过程基本能定位大多数“网口不通”的实质问题。4. 双网口应用层实现与数据通路4.1 两个网口的MAC地址与IP规划MAC地址是每个网卡在以太网世界的身份证。ZYNQ的GEM0和GEM1如果不设置MAC地址会读到一个无效的全F地址或者全0地址。所以在初始化每个netif之前必须给对应的硬件MAC写入唯一的MAC地址。ZYNQ芯片内部有eFUSE里面存了专用MAC地址一般只能保证一个有效MAC。做双网口时正确的做法是一个网口使用从eFUSE读取的“原生”MAC另一个网口自定义一个本地管理的MAC地址。本地管理MAC的第二个字节最低位应设置为1表示这不是全球唯一地址避免和别家产品冲突。比如读出来的原生MAC是00:0A:35:xx:xx:xx自定义MAC可以写成02:00:35:01:xx:xx。IP地址规划上我推荐把两个网口划分到不同的子网。比如eth0用192.168.0.xeth1用192.168.1.x。这样做的好处是从任何一个网口进来的数据包目标IP属于哪个子网一目了然ARP广播也局限在各自的VLAN/网段内互不干扰后续代码维护逻辑清晰。网关和路由方面裸机lwIP的路由表比较简单默认网关只能指向一个网口。所以除非你有特殊需求否则把默认路由配置到管理网口数据采集网口走纯二层或者显式路由。代码里netif_set_default(eth0_netif)就是干这个的。数据包发送时lwIP会根据目标IP地址查找匹配的netif接口。如果目标IP在某个网口的子网内直接从对应网口发出不在任何子网内的则从默认网关对应的网口发出。理解这个逻辑你就知道IP规划不是拍脑袋定的它决定了两个网口的数据流向是否能在协议栈层面自动分发。4.2 双DMA中断的处理机制以太网都是中断驱动收包双网口的核心难点在于如何让两个MAC的中断互不干扰地工作。ZYNQ的两个GEM控制器各有独立的中断线分别连接到GIC控制器的不同中断ID上。在Vitis BSP生成的xparameters.h里可以找到这两个中断ID典型的定义是#define XPAR_XEMACPS_0_INTR (某个ID) #define XPAR_XEMACPS_1_INTR (另一个ID)在裸机中断系统里我们需要用XScuGic统一管理所有中断。初始化流程是先初始化GIC然后把两个GEM的中断处理函数分别注册到对应的中断ID上。中断处理函数的注册代码类似这样XScuGic_Config *GicConfig XScuGic_LookupConfig(XPAR_SCUGIC_0_DEVICE_ID); XScuGic_CfgInitialize(Gic, GicConfig, GicConfig-CpuBaseAddress); XScuGic_Connect(Gic, XPAR_XEMACPS_0_INTR, (Xil_InterruptHandler)xemacpsif_isr, eth0_netif); XScuGic_Enable(Gic, XPAR_XEMACPS_0_INTR); XScuGic_Connect(Gic, XPAR_XEMACPS_1_INTR, (Xil_InterruptHandler)xemacpsif_isr, eth1_netif); XScuGic_Enable(Gic, XPAR_XEMACPS_1_INTR);这里最关键的是传入中断回调的参数不同——eth0的中断回调参数是eth0_netifeth1的中断回调参数是eth1_netif。中间层xemacpsif_isr通过这个参数区分当前是哪个MAC触发的中断再调用对应实例的DMA处理函数完成收包或发包。如果你配反了两个网口都会跑到同一个netif的接收队列里另一个网口永远收不到包。DMA收发环节有个容易踩坑的点DMA描述符缓冲区的大小和数量。Xilinx适配层给每个网口分配了收发描述符环ring默认数量可能不够。吞吐量大的场景建议增大TX/RX描述符数量。一般RX描述符建议至少16个TX描述符8到16个。描述符数量增加会多占内存但接收突发包时的丢包率会明显下降。中断优先级也值得设置。ZYNQ的GIC支持中断优先级以太网中断属于实时性较高的中断建议设置为较高的优先级低于TTC定时器中断但高于串口等慢速外设。如果两个以太网中断优先级相同且同时到达GIC会按中断ID顺序处理不会死锁只是响应略有先后。4.3 应用层怎么使用两个网口底层双netif建立好之后应用层用起来其实跟单网口差别不大。lwIP的socket API在NO_SYS模式下使用netconn API或raw API都会自动通过路由表匹配到正确的netif。你只管bind IP和端口剩下的选路、ARP、发送都由协议栈处理。我项目里的管理网口跑了一个简单的TCP服务器监听502端口用于Modbus TCP协议通信。数据采集网口跑的是UDP广播接收接收下位机发来的实时数据帧。两个服务在主循环里轮询各自的接收队列while (1) { xemacpsif_poll(eth0_netif); xemacpsif_poll(eth1_netif); tcp_server_poll(); udp_capture_poll(); }裸机下没有操作系统lwIP的处理都在这个主循环里被驱动起来。两个网口的数据都会进入各自的接收队列应用层轮询时按netif来取。这块逻辑上不算难但要记得对两个网口的协议栈处理都要调用。如果主循环只poll了eth0那eth1的包就会一直躺在DMA缓冲区里看起来就是“另一个网口收不到数据”。实际测试中两路同时跑一路TCP长连接持续下行文件一路UDP 1000包/秒的实时数据。CPU负载大概占用了ZYNQ单核的六成左右两个网口基本不互相拖累。裸机虽然简单只要内存规划到位性能反而比Linux下还清爽。5. 调试实录与常见问题排查5.1 第一个网口能通第二个网口死活起不来这是双网口最经典的现象。eth0能ping通eth1连接PHY之后状态寄存器一直不对或者干脆读不到PHY的ID。排查第一步永远是确认PHY模式。Xilinx裸机适配层里有一个PHY初始化函数它会通过MDIO总线读取PHY的ID寄存器来识别芯片型号。如果MDIO总线上只有一个PHY芯片地址匹配正确识别顺利但双网口板子上有两个PHY它们共享或者各自独立的MDIO总线地址可能冲突。很多外部PHY芯片的地址是用硬件上下拉电阻配置的比如地址0x07、0x04你要确认软件里用的PHY地址和板子上实际硬件配置一致。Xilinx的驱动宏XPAR_XEMACPS_0_PHY_ADDR对应的值经常和实际板子不匹配这一项要人工核对。如果PHY地址对但依然起不来检查MDIO读写是否超时。MDIO时钟频率取决于PS的MDC分频寄存器配置默认可能太高接的PHY不支持那么快的MDC。我在调试时把MDC分频改大之后第二个网口瞬间就活了。具体修改方式是调整适配层里对MDIO控制寄存器的配置或者查PHY芯片数据手册找它支持的最大MDC频率。还有一种常见情况两个GEM复用EMIO信号时PL侧约束没写全。RGMII的十几根信号线必须全部约束到正确的物理引脚上漏一根信号就虚接。这种问题表面上看是MAC侧不出数据实际上问题在FPGA引脚布线。排查方法是打开硬件工程对照板卡的原理图逐个确认GEM1对应的RGMII引脚约束不能想当然。5.2 ARP不同两个网口互相“打架”网口链路状态显示正常本地ping对端也发出去了但对方抓包发现根本没收到ARP请求或者收到了却不回。双网口ARP问题最常见的原因是IP和netif的绑定关系混乱。前面提过默认网关只能指向一个接口。如果两条链路都配置了同一个网关或者目标IP同时落在两个netif的子网判定范围内lwIP选路时会先查默认接口所有数据都从默认接口出去另一个网口就变成了“摆设”。我实际踩过这个坑当时把两个网口配成了同一个网段的IP比如192.168.1.10和192.168.1.20以为没啥问题。结果lwIP的ARP缓存表对同一个网段只维护一个活动接口两个网口争抢ARP表项数据流量大的那个网口会把另一个的ARP表项冲掉表现为“一会儿通一会儿不通”。把所有ARP表项相关调试打开才看清楚是两个接口在互相“踢”对方的ARP缓存。解决方法是把两路IP规划到不同网段这是最干净的方案。还有一种情况是外部环境没有交叉网线连接交换机的隔离问题。双网口板子调试时建议直连两个独立的交换机或独立网口不要插在同一个傻瓜交换机的两个口上否则可能出现广播风暴或者STP选路导致数据不同步干扰判断。5.3 网络跑一会就死缓存池耗尽中断风暴双网口联网运行几分钟后整个网络通信罢工串口打印还在但网络突然全挂。这种情况十有八九是lwIP内存耗尽。裸机lwIP最容易被忽略的问题是PBUF_POOL_SIZE和MEM_SIZE的配合。如果池化缓冲区用完了驱动在接收中断里申请新缓冲区失败会直接把包丢掉而且没有明显的报错。双网口下收包速率翻倍内存消耗速度也翻倍原先单网口勉强够用的内存池到双网口直接崩。建议调优时在串口上周期打印lwip的内存统计信息。lwIP提供了MEM_STATS宏打开后能输出当前堆剩余量。另外我习惯在socket_poll循环里加上一个简单的计数器每收放多少包之后打印一次统计。一旦发现剩余内存在持续下降而不是稳定在一个区间就说明某处存在内存泄漏。最常见的内存泄漏原因是TCP断开连接时PCB没有正确释放或者接收回调里没有及时调用pbuf_free。两个网口同时高频收发时这类错误会被放大更容易暴露。还有一类“断网”其实是中断风暴。当一个GEM的DMA描述符处理不及时会产生大量RX overrun中断。如果GIC里注册的中断处理函数执行时间过长或者中断嵌套优先级配置不当会导致另一个网口的中断饿死。排查时打开中断统计寄存器看看两个GEM各自的中断计数是否均衡。如果不均衡考虑把两个网口的中断优先级设为相同然后优化中断处理函数耗时把DMA描述符搬运的循环尽量精简。5.4 双网口吞吐量和丢包率实测所有问题排查完之后我把双网口跑了一轮性能实测这里给出一个参考数据供大家对比。测试环境ZYNQ-7020平台双核Cortex-A9跑在667MHz。eth0走MIO接千兆PHYeth1走EMIO接另一颗千兆PHY。lwIP 2.xTCP窗口2048发送缓冲区2048PBUF_POOL_SIZE40。两端直连服务器服务器端分别打流。测试结果TCP单网口吞吐约450Mbps双网口同时打流两个口加起来约700Mbps。UDP小包128字节单网口丢包率0.005%双网口同时收包时另外一个网口丢包率约0.01%。这个丢包主要发生在中断处理来不及搬运DMA描述符的时候。长时运行72小时两路连接稳定在线无死机无内存持续下降。实测下来双网口的裸机稳定性完全够用关键是把内存池规划好、中断处理逻辑理顺。如果对UDP小包丢包率还有更苛刻的要求再增大RX描述符数量并把PBUF_POOL_SIZE调更大一般能满足场景需要。6. 双网口扩展从裸机走向更多玩法双网口裸机跑通之后这个平台可以做的扩展其实很多。比如把两个网口做成主备冗余接口间增加链路健康检测备份网口在空闲时关闭或待命主接口故障后自动切换。这个切换在裸机下完全可控——只需要在应用层周期性检查PHY的link状态寄存器一旦发现断链修改默认netif并把数据通道切到备用接口几百毫秒内完成切换比很多Linux方案还要快。另一个常见的扩展方向是两网口分别对应不同VLAN或者不同报文优先级。ZYNQ的GEM控制器本身支持VLAN标签处理裸机lwIP里也可以手动加入VLAN过滤逻辑。甚至可以在裸机上做双网口的二层桥接把两个网口当作一个透明网桥数据包从一个口进来、从另一个口转发出去。这在一些数据过滤采集场景里非常实用相当于一个低延迟的串接式网络探针。从个人经验看双网口的裸机实现一旦稳定你会明显感受到比Linux轻量的好处。硬件上ZYNQ两个GEM本身就是为双路应用准备的裸机只要处理好内存和中断完全能撑起复杂的网络应用。调试的过程虽然熬人但每踩通一个坑对底层网络栈的理解就更深一层。最后分享一个小技巧调试双网口这类多接口系统时强烈建议在板子上同时扩展一个串口输出把每个网口的状态变化link up/down、IP分配、PHY寄存器错误都打到串口上。串口是裸机系统里最可靠的眼睛很多难缠的问题都是靠串口日志定位的。