☰
ZYNQ裸机双网口实战:从LWIP配置到中断优化的完整路径
2026/10/4 3:07:22 网站建设 项目流程

在嵌入式开发里,一提到ZYNQ,很多人第一反应就是上Linux跑复杂应用。但现实是,很多工业场景根本不需要操作系统,或者说,出于实时性、稳定性和成本考虑,裸机方案反而是更佳选择。尤其是“双网口”这个需求,听起来只比单网口多一个口,实际做起来完全是两码事——MAC地址怎么分配,中断怎么分发,LWIP怎么跑两个实例,内存够不够用,每个问题都能让人折腾好几天。

这篇文章就基于我实际做过的ZYNQ裸机双网口项目,把从硬件配置到软件实现的完整路径讲清楚。适合正在用ZYNQ做数据采集、工业网关、协议转换的同学,也适合刚接触LWIP、想看看裸机方案到底怎么落地的朋友。我会尽量把关键代码和踩坑点都写明白,让大家少走弯路。

1. ZYNQ裸机双网口:为什么需要它

1.1 双网口到底解决什么问题

双网口在工业设备里太常见了。比如一个边缘网关,一个口接PLC、传感器这类现场总线设备,另一个口接上层管理网络,两个网络物理隔离,互不干扰。又比如某些数据处理设备,一口收数据、一口发数据,形成数据流的中转站,这时候如果只有单网口,收发相互抢占带宽,延迟和丢包都会很难看。

还有一类场景是冗余设计。两个网口同时工作,一条链路断了自动切换到另一条,这种在电力、轨道交通里面非常普遍。裸机方案做双网口,不是为了省掉Linux的成本,而是为了拿到更可控的实时性和更低的资源占用,毕竟裸机下没有调度器的干扰,中断响应能做到微秒级。

1.2 为什么选择裸机而不是Linux

我见过很多项目上来就直接用PetaLinux,结果发现光是配置启动方式、制作image.ub、调试设备树就花掉一半工期。如果产品功能就是把两个网口的数据做转发或者简单协议处理,Linux那套复杂的网络协议栈和驱动框架其实是杀鸡用牛刀。

裸机LWIP的优势在于三点:第一,资源占用极低,ZYNQ的DDR只要给几十MB甚至十几MB就够跑,启动速度毫秒级;第二,确定性好,没有系统调度和内核抢占,每个数据包的延迟可预测;第三,开发和调试简单,一个SDK就能搞定全部工作,不用折腾交叉编译环境、根文件系统这些乱七八糟的东西。当然,裸机的缺点也明显,比如没有文件系统、没有虚拟内存保护,驱动和应用代码耦合度更高,但权衡下来,在很多轻量级场景里,裸机双网口是性价比非常高的方案。

1.3 硬件平台与软件环境准备

我这次用的是Xilinx ZYNQ-7000系列的XC7Z020,开发板是常规的双网口配置,两个RGMII接口分别挂在PS端的GE0和GE1上。软件环境是Vivado 2020.2,SDK里带的LWIP版本是lwip141,这个版本很成熟,配合BSP生成的双网口驱动,整体比较稳定。

如果你用更新的Vivado版本,LWIP源码可能会有些细微差异,但核心逻辑是一样的。硬件上,务必确认PHY芯片的型号和地址,常见的有RTL8211、YT8512等,PHY地址一般通过硬件引脚配置,比如0x00、0x01这种,后面软件初始化时要用到。这个细节很容易埋坑,我稍后会细说。

2. Vivado硬件工程的搭建与验证

2.1 PS端配置要点

双网口在Vivado里配置其实不复杂,关键是把两个ENET控制器都打开,并且正确设置MIO或者EMIO引脚。我的板子上GE0走的是MIO16-27,GE1走的是MIO28-39,RGMII接口,需要使用PS端的GEM0和GEM1控制器。

在ZYNQ PS配置界面里,需要打开ENET 0和ENET 1,选择RGMII模式,然后确保MDIO的引脚分配正确。很多人在这一步会忽略MDIO的复用问题,比如MDIO和某些GPIO冲突,导致PHY读不到寄存器,后面所有调试都是白费。另外,GEM的时钟源也要选好,一般用PS端的FCLK或独立的时钟引脚,确保PHY输出的125MHz时钟能被正确采到。

还有一个常被忽略的点:如果两个网口的PHY共用一条MDIO总线,MDIO地址必须不同。否则驱动去读PHY寄存器时会读到同一个芯片,导致两个网口的功能完全一样,数据逻辑上就串了。

2.2 硬件工程的HDL验证

生成比特流前,我习惯先看眼地址分配和I/O标准,特别是PHY的复位引脚,有些板卡把PHY的RST接到了PS的EMIO上,有些直接拉了硬件复位,这直接关系到软件初始化的时序。如果PHY一直处于复位状态,MDIO读写会全部超时。

在检验硬件配置正确性时,常用的方法是在SDK里写一个很简单的裸机程序,只做PS初始化,然后通过MDIO去读取PHY的ID寄存器,看看能不能读到预期的值。这一步最快能确认PHY地址、MDIO引脚这些硬件通路是否OK。如果读不到PHY ID,多半是硬件配置或者PHY复位没处理好,别急着往下写LWIP代码。

2.3 生成SDK工程与BSP前的检查清单

在导出硬件配置生成SDK工程前,一定要检查以下几点:

  • PL侧是否用到了以太网的AXI接口?裸机方案里GEM0/GEM1走的是PS内部互联,不需要额外PL逻辑,所以可以不生成PL比特流,纯PS即可。
  • 确保使能了GIC(通用中断控制器),LWIP双网口要处理两个GEM的中断,离不开GIC。
  • DDR的型号和速率配置是否和板卡实际型号匹配,这关系到内存带宽和稳定性。
  • MIO分配是否冲突,特别是MIO和QSPI、SDIO等外设是否共用引脚,避免配置冲突。

把这些问题提前在Vivado里全部确认掉,可以避免后面软件调试时出现定位不清的奇奇怪怪的bug。

3. LWIP协议栈在BSP中的配置与裁剪

3.1 BSP中生成LWIP库的方式

在SDK里创建BSP后,在Board Support Package设置中勾选lwip141库。这里需要特别注意的是,对于双网口,实际上是在同一个LWIP库中跑两个实例,而不是启动两个操作系统进程。所以在BSP里只需要一个LWIP库,但要确保它支持多接口(multi-interface)。

lwip141库在Xilinx的适配版里有不少补丁,比如对多实例的支持、对ZYNQ DMA的适配等。生成BSP后,建议检查一下lwipopts.h的内容,因为这个文件直接决定协议栈行为。Xilinx默认给的lwipopts.h已经做了不少裁剪,但双网口场景需要额外调整,核心是内存大小和接口数量相关配置。

3.2 调整lwipopts.h中的关键宏

lwipopts.h里面有几个宏对双网口影响非常大,我直接列一下我项目里的配置值:

#define NO_SYS 1 #define LWIP_NETCONN 0 #define LWIP_SOCKET 0 #define MEM_SIZE (600 * 1024) #define PBUF_POOL_SIZE 64 #define PBUF_POOL_BUFSIZE 1600 #define MEMP_NUM_PBUF 64 #define MEMP_NUM_NETBUF 32 #define MEMP_NUM_NETCONN 8 #define MEMP_NUM_TCP_PCB 8 #define MEMP_NUM_TCP_SEG 32 #define MEMP_NUM_UDP_PCB 8 #define LWIP_DHCP 1

NO_SYS设为1表示裸机模式,LWIP_NETCONN和LWIP_SOCKET关闭,这样用raw API开发,效率高且不依赖RTOS。MEM_SIZE要往大了调,因为两个网口都会动态申请内存来做收发缓冲,默认的150KB肯定不够用。PBUF_POOL_SIZE决定数据包池数量,双网口并发吞吐时,64个算是起步,我开了100左右才比较稳。

PBUF_POOL_BUFSIZE要基于MTU来定,1500字节的MTU加上以太网头、DMA描述符对齐等因素,我直接设成1600,避免意外溢出。这些参数没有绝对标准,但要根据自己的数据包大小、并发量做调整,一次性配大点省得调试中频繁改。

3.3 内存池与DMA缓冲区的分配策略

ZYNQ的GEM控制器发送和接收数据用的是Scatter-Gather DMA,需要一份DMA描述符表(BD)和对应的数据缓冲。Xilinx的驱动在初始化时会调用一个内存分配函数来申请这些描述符和数据缓冲区。

我踩过一个坑:把DMA描述符和数据缓冲区定义到DDR里,但地址对齐和要求没满足,导致偶尔出现收发异常。在ZYNQ上,DMA访问DDR需要保证物理地址正确,而且最好按8字节或甚至更高字节对齐。Xilinx驱动里有专门的宏和函数做这件事,尽量别自己用malloc随便分配。实测下来,用静态数组定义接收缓冲区,并用#pragma或属性指定内存对齐,比动态申请更稳定,尤其当系统跑了一段时间后内存碎片增多时,静态分配的优势很明显。

4. 双网口初始化的具体代码实现

4.1 两个EMAC设备的初始化和MAC地址分配

Xilinx的LWIP适配层在xemacpsif_dma.c和xemacpsif.h这些文件里提供了两个网口的初始化接口,核心函数是lwip_init()之前要先完成网口初始化。在我的实现中,我用一个结构体来分别保存两个网口的配置参数,包括MAC地址、IP地址、PHY地址等。

struct netif g_netif[2]; static void assign_mac_addresses(void) { // 从板载EEPROM或者固定配置读取MAC // 实际产品建议每台设备一个唯一MAC u32_t base_mac_hi = 0x000A35; // OUI示例 u32_t base_mac_lo = 0x010203; g_emac_config[0].mac_address[0] = 0x00; g_emac_config[0].mac_address[1] = 0x0A; g_emac_config[0].mac_address[2] = 0x35; g_emac_config[0].mac_address[3] = 0x01; g_emac_config[0].mac_address[4] = 0x02; g_emac_config[0].mac_address[5] = 0x03; g_emac_config[1].mac_address[0] = 0x00; g_emac_config[1].mac_address[1] = 0x0A; g_emac_config[1].mac_address[2] = 0x35; g_emac_config[1].mac_address[3] = 0x01; g_emac_config[1].mac_address[4] = 0x02; g_emac_config[1].mac_address[5] = 0x04; }

这里有个比较容易出现的问题:如果两个网口的MAC地址相同,交换机或者路由器会认为是环路,导致其中一个网口无法正常通信。我最初调试时顺手把MAC配成了一样,结果GE0能通,GE1死活不通,查了很久才意识到是MAC冲突,所以双网口一定要确保MAC唯一。生产环境建议从EEPROM、SFPD或板载存储中读取每台设备的唯一MAC,避免批量部署时地址重了。

4.2 两个网口的PHY初始化与link状态检测

PHY初始化是双网口里的重要环节。LWIP的Xilinx适配层会自动调用phy_init来配置PHY芯片,但它只能处理单PHY的情况,对于双网口,需要手动为两个GEM分别初始化对应的PHY。

我封装了一个函数来分别完成两个PHY的初始化、自协商和link状态检查:

int init_phy(int emac_index) { XEmacPs *emac = &g_emac[emac_index]; int phy_addr = (emac_index == 0) ? 0x01 : 0x00; u32_t phy_id = 0; u32_t timeout = 0; // 检查PHY ID是否可读,确认MDIO通路正常 while (XEmacPs_PhyRead(emac, phy_addr, PHY_REG_ID0, &phy_id) != XST_SUCCESS) { if (++timeout > 0x100000) { xil_printf("PHY %d read failed!\n", emac_index); return XST_FAILURE; } } XEmacPs_PhyRead(emac, phy_addr, PHY_REG_ID1, &phy_id); xil_printf("PHY %d ID: 0x%08X\n", emac_index, phy_id); // 复位PHY XEmacPs_PhyWrite(emac, phy_addr, PHY_REG_CR, 0x8000); usleep(10000); // 启动自协商 XEmacPs_PhyWrite(emac, phy_addr, PHY_REG_CR, 0x1200); usleep(100000); // 等待link up u32_t status; do { XEmacPs_PhyRead(emac, phy_addr, PHY_REG_ISR, &status); } while ((status & 0x04) == 0 && timeout++ < 0xFFFFFF); return XST_SUCCESS; }

这里面的PHY寄存器地址因芯片而异,比如RTL8211的PHY ID寄存器是0x02/0x03,而YT8512的寄存器布局不同。如果你用的PHY不一样,一定要查一下其数据手册,不要硬套代码。方便的是,Xilinx SDK中自带了若干PHY芯片驱动,可以直接在BSP设置中选上对应的PHY型号,让驱动自动适配。

另外,自协商等待时间不能太短,有些PHY上电后要几百毫秒才能稳定出link,我试过只等50ms,结果两个网口都link不上,白白多了半小时定位过程。等到1秒以上是比较稳妥的。

4.3 创建两个netif实例并注册到LWIP

LWIP的多接口支持需要为每个网口创建独立的netif结构体。在Xilinx的裸机适配层里,有xemacpsif.h提供的xemacpsif_add接口,不过它默认只加一个接口,双网口时我建议直接自己封装初始化逻辑。

简化后的流程是:

void lwip_dual_netif_init(void) { struct ip_addr ipaddr, netmask, gateway; // 网口0配置 IP4_ADDR(&ipaddr, 192, 168, 1, 10); IP4_ADDR(&netmask, 255, 255, 255, 0); IP4_ADDR(&gateway, 192, 168, 1, 1); netif_add(&g_netif[0], &ipaddr, &netmask, &gateway, (void *)&g_emac_config[0], ethernetif0_init, tcpip_input); netif_set_default(&g_netif[0]); netif_set_up(&g_netif[0]); // 网口1配置 IP4_ADDR(&ipaddr, 192, 168, 2, 10); IP4_ADDR(&netmask, 255, 255, 255, 0); IP4_ADDR(&gateway, 192, 168, 2, 1); netif_add(&g_netif[1], &ipaddr, &netmask, &gateway, (void *)&g_emac_config[1], ethernetif1_init, tcpip_input); netif_set_up(&g_netif[1]); }

注意这里ethernetif0_init和ethernetif1_init的差异,本质上都指向同一个底层初始化函数,但传入的参数不同,这样注册到LWIP里的两个netif才能对应到不同的GEM控制器。如果不仔细区分,两个netif都指向同一个GEM,那所有数据都会走到一个网口上,表现就是另一个口完全没用。

tcpip_input这个函数是裸机LWIP里收包后交给协议栈的入口,两个网口都会用同一个入口,但内核会根据接收到的netif指针来区分数据包来自哪个网口,所以不用担心数据混淆。

5. LWIP双网口的收发路径与内存管理

5.1 数据包的发送和接收流程

LWIP的raw API收发路径是:网口驱动通过DMA把数据收到内存缓冲区,然后抛给tcpip_input,协议栈处理后再调用网口的发送函数etharp_output或tcp_output向下发送。双网口的实现里,每个网口的DMA中断是独立的,但收到的数据包都会进同一个协议栈核心,由IP层根据目的IP、路由表决定从哪个网口发出。

这带来一个问题:如果没有配置正确的路由表,从网口0收到数据后,协议栈在回包时可能会选择默认路由,也就是走网口0发出去,哪怕这个数据包是从网口1进来的。所以,在双网口应用中,需要检查路由表配置,必要时用netif_set_default指定默认网口,或者手动添加静态路由来区分不同子网的流量走向。

我刚把双网口调通时,就遇到过一个奇怪的现象:两个网口都能收到数据,但只有默认网口能回包。后来才意识到是路由表问题,因为LWIP在发送回包时会查询路由表,如果没有匹配项,就走到默认netif上去了。解决办法很简单,在代码里加上静态路由:

ip_route_add(&dest_net, &netmask, &gateway);

或者更简单的做法,干脆把两个网口设置到不同网段,业务场景下大部分情况会是网口0收、网口1发这样的分工,这时候把默认网口设成发送网口即可。

5.2 内存池调整对双网口吞吐的影响

双网口的吞吐量受内存池深度影响非常明显。刚开始我用默认的内存池参数,UDP小包测试时两个网口都正常,但一旦用iperf打满速,吞吐就断崖式下降,甚至出现丢包重启的情况。

后来我把PBUF_POOL_SIZE从默认的16改到96,PBUF_POOL_BUFSIZE调整到1800,MEMP_NUM_TCP_SEG从8改成32,重新测试,网口吞吐从原来的十几MB/s提升到了接近满速。这里的原理是:如果内存池太小,当收发速率高时,DMA收到的数据包没有空闲的PBUF可分配,驱动就只能丢弃数据包,或者DMA缓冲区被占满导致新数据无处存放。

裸机环境没有操作系统帮忙动态扩容,所以内存池一定要提前配置够。一个经验值是:内存池总大小尽量是整个链路BDP(带宽延迟积)的两倍左右,如果只做简单的UDP转发,MEM_SIZE给到1MB基本够用,但如果是TCP大量传输,最好给2MB以上。当然,这也取决于你的DDR容量,ZC702这种小容量板卡就要算着用了。

5.3 DMA描述符与收包缓冲区的静态分配建议

我在项目里给每个网口分配了独立的DMA描述符表和收包缓冲区,没有用动态分配,具体方式是定义全局静态数组:

#define RX_BD_CNT 128 #define TX_BD_CNT 128 #define RX_BUFFER_SIZE 2048 // 两个网口的DMA描述符表 u32_t rx_bd_table[2][RX_BD_CNT] __attribute__((aligned(64))); u32_t tx_bd_table[2][TX_BD_CNT] __attribute__((aligned(64))); u8_t rx_buffers[2][RX_BD_CNT][RX_BUFFER_SIZE] __attribute__((aligned(64)));

这里的关键是对齐属性,ZYNQ的GEM DMA控制器要求描述符表按16字节对齐,但为了保险起见,我直接64字节对齐。缓冲区大小2048是因为有些网络包带VLAN标记,MTU会超过1500,留些余量更安全。

使用静态数组的好处是地址固定,方便调试,不会出现因堆区碎片导致DMA访问异常的情况。坏处是占用的内存不能释放,在设计系统内存分配时要考虑到这部分的占用。我在一个系统里同时跑TCP服务器和UDP转发,两个网口各128个BD,内存占用大概是在可接受范围内的。

6. 中断与ARM GIC配置:双网口稳定性的关键

6.1 GIC中断号与优先级设置

ZYNQ的GEM0和GEM1在GIC里都有独立的中断号,分别是54和55(具体以芯片手册为准)。在BSP的XScuGic配置中,需要为两个中断分别注册服务函数。

这里的坑在于:很多人习惯只初始化一个GIC,第二个网口的中断没有分配到正确的CPU接口,导致第二个网口的中断服务函数永远不触发。我在platform_init或主函数里,手动初始化GIC后,要用XScuGic_Connect分别连接两个中断源:

static XScuGic g_InterruptController; void init_gic_dual_emac(void) { XScuGic_Config *IntcConfig; IntcConfig = XScuGic_LookupConfig(XPAR_SCUGIC_0_DEVICE_ID); XScuGic_CfgInitialize(&g_InterruptController, IntcConfig, IntcConfig->CpuBaseAddress); Xil_ExceptionInit(); Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT, (Xil_ExceptionHandler)XScuGic_InterruptHandler, &g_InterruptController); Xil_ExceptionEnable(); // 注册两个网口的中断 XScuGic_Connect(&g_InterruptController, XPAR_XEMACPS_0_INTR, (Xil_ExceptionHandler)emac0_isr, &g_emac[0]); XScuGic_Enable(&g_InterruptController, XPAR_XEMACPS_0_INTR); XScuGic_Connect(&g_InterruptController, XPAR_XEMACPS_1_INTR, (Xil_ExceptionHandler)emac1_isr, &g_emac[1]); XScuGic_Enable(&g_InterruptController, XPAR_XEMACPS_1_INTR); }

这里XPAR_XEMACPS_0_INTR和XPAR_XEMACPS_1_INTR在xparameters.h中会有定义。如果硬件平台不支持第二个网口,这两个宏可能不存在,编译直接报错,所以要先确认Vivado里打开了两个ENET控制器。

中断优先级方面,建议把网口中断优先级设为较高值,比如0xA0(可抢占),同时确保主程序里不被其他优先级较低的长任务阻塞。如果主循环里做大数据量处理或延时操作,网口中断会被延迟,造成DMA缓冲区溢出或丢包,这一点在裸机环境下要特别注意。

6.2 中断服务函数中要做的事

LWIP裸机方案下,中断服务函数不宜做太多工作,尽量把数据包从DMA取出来,再送到tcpip_input,像协议栈处理逻辑、TCP重传这些耗时操作不要放在中断里做。

Xilinx的适配层已经写好了emacps_isr,它会把收到的数据包塞进PBUF,然后调tcpip_input。自己写ISR时也要遵循这个模式:

void emac0_isr(void *CallbackRef) { XEmacPs *emac = (XEmacPs *)CallbackRef; XEmacPs_RxQueue(emac); }

底层驱动在中断里只做数据搬运,具体的协议栈处理是在tcpip_input里通过sys_check_timeouts和主循环配合完成的。裸机模式下,LWIP要求在主循环中周期性调用sys_check_timeouts()来处理定时任务,比如ARP缓存过期、TCP重传定时器等。如果主循环里不调用这个函数,TCP连接会变得极不稳定。

我的主循环结构大概是这样的:

while (1) { sys_check_timeouts(); // 应用层数据处理 app_process(); // 其他任务 }

这个循环调用的频率不能太低,我一般控制在5ms以内一次,否则TCP的延迟会明显变大。实际项目里如果主循环事情很多,可以考虑把sys_check_timeouts放到定时器中断里,但要注意竞态问题,在裸机下不加锁比较容易出问题,我在早期版本确实遇到过,后来还是乖乖放主循环了。

6.3 双网口同时收发时的性能表现

在调通双网口之后,我做了个简单的性能验证:一个网口持续向另一个网口转发UDP数据,包大小从64字节到1400字节都测了一遍。结果在1400字节大包下,两个网口同时满速收发,CPU占用率大约在40%左右,这个数字对于裸机方案来说还算正常,但如果带协议转换或业务处理,CPU压力会更高。

如果你发现CPU占用偏高或者丢包严重,优先检查DMA中断频率。小包场景下,每秒中断数可能高达几十万次,CPU大量时间都在响应中断,此时可以考虑用NAPI式的聚合收包。Xilinx的驱动默认是每收一个包中断一次,对高速小包场景不友好。尤其是在双网口场景下,两个网口同时小包收发,中断风暴会造成主循环根本得不到执行时间,表现为协议栈假死。

更实用的方案是把收发队列深度加大,用DMA的批处理能力。Xilinx驱动有XEmacPs_IntrEnable里可以开启接收队列中断聚合功能,减少中断次数,但需要在驱动层做配置。这块的优化空间很大,如果项目对性能要求高,建议专门花时间调一下。

7. 实测中的常见问题与排查方法

7.1 PHY Link Up但收不到数据

这个现象我遇到过两次,一次是PHY地址配错,一次是MDIO引脚共享导致两个PHY被读到同一个。排查顺序一般是:

  • 先看串口打印的PHY ID,确认是不是预期芯片。
  • 用XEmacPs驱动读PHY的link状态寄存器,确认物理层已经up。
  • 若link up了但收不到数据,检查是不是把两个网口的MAC配成了相同值。
  • 确认DMA描述符表地址是否正确,有没有对齐问题。
  • 最后才考虑是不是lwipopts配置导致PBUF耗尽。

另外,一定要在初始化完成后主动打印每个网口的netif状态,比如netif->flags有没有包含NETIF_FLAG_LINK_UP,这个标志位能帮你快速定位是不是驱动未正确通知协议栈链路状态。

7.2 双网口DHCP导致IP分配异常

如果两个网口都开DHCP,且连接同一个路由器,可能会拿到不同网段的IP,应用层如果假设两个网口同网段就出问题了。裸机LWIP的DHCP功能相对基础,不支持复杂的DNS和路由推送,生产环境我建议两个网口都用静态IP,并且规划好不同网段,减少冲突和干扰。

另外,DHCP回复超时在裸机LWIP里是很常见的问题,原因是DHCP的定时器靠主循环的sys_check_timeouts驱动,如果主循环阻塞时间过长,DHCP状态机可能超时。我调试时曾经在主循环中加入了一个100ms的延时测试,结果网口怎么都拿不到IP。把延时去掉恢复正常后,我才意识到主循环的查询频率对LWIP协议栈行为有多重要。

7.3 TCP吞吐上不去的排查思路

双网口TCP吞吐上不去,我总结了三个主要检查方向:

第一,内存池配置。TCP窗口大小需要足够的内存缓冲来支撑,特别是ACK重排和重传队列会占用大量PBUF。如果MEMP_NUM_TCP_SEG太小,协议栈在发送大量数据时会频繁等待内存,吞吐就上不去。

第二,DMA描述符数量。收包BD数量少导致DMA在中断处理完前不能再接收更多数据,形成瓶颈。我建议对收包性能要求高的口单独配置更多的RX BD数量,而不是两个口平均分配。

第三,网卡中断调度。因为两个网口共用CPU核心,如果中断处理耗时太长,第二个网口的数据就会被延迟处理,表现为两个网口间TCP传输速度波动大。可以考虑在驱动上做优化,或者把部分业务逻辑用PL硬件加速,不过这属于后续优化方向了。

7.4 常见问题排查速查表

现象可能原因解决方案
两个网口只有其中一个能ping通MAC地址冲突、路由表配置不对确保MAC唯一,添加静态路由或正确设置默认网口
link up了但抓包无数据PHY寄存器配置异常,DMA没启动检查PHY地址、寄存器读写,确认DMA中断使能
高负载下两个网口同时丢包内存池太小或DMA描述符不足增大MEM_SIZE、PBUF_POOL_SIZE、BD数量
其中一个网口在跑一段时间后失去响应中断未正确处理或主循环阻塞优化主循环,检查是否有死循环或长时间临界区
DHCP总是失败主循环调用sys_check_timeouts频率太低缩短主循环周期,或单独用定时器触发协议栈回调
两个PHY读到同一个IDMDIO地址相同修改PHY硬件地址或调整MDIO时序

8. 进一步扩展:从双网口到整机方案

8.1 双网口与Bootloader在线升级的结合

ZYNQ裸机双网口方案做好之后,很自然会想到结合Bootloader做在线升级。这样产品在现场可以通过网口远程更新固件,不需要拆机下载、JTAG烧写,运维成本降一大截。

Xilinx的启动流程是BootROM引导FSBL,FSBL再加载应用程序。在线升级的思路就是:把应用程序镜像暂存在DDR或Flash的另一分区,校验通过后更新FSBL的启动分区,下次上电时启动新固件。双网口在其中可以起到“一个口承载业务数据、另一个口承载升级数据”的分工,升级过程对业务影响最小。

这种方案的实现要点是:Bootloader里要有Flash读写驱动和网络协议栈,能够在开机时进入升级模式,接收镜像并写入Flash。FSBL本身也可以支持通过网络加载应用程序,这就是Xilinx的TFTP网络引导方式,不过裸机环境下通常是自己实现一个简化版,避免依赖完整的Bootloader改造。

我在实际项目中把升级流程做成:设备启动后等待3秒,如果有升级命令则进入升级模式,等待接收新镜像;没有命令则直接启动正常业务。双网口下,升级数据走管理口,业务数据走业务口,逻辑清晰,升级过程出问题也不会影响正常业务网络。

8.2 LWIP版本对比:lwip141 vs lwip202

Xilinx的BSP里通常提供lwip141库,对应LWIP 1.4.1版本,虽然老,但Xilinx做了大量定制和稳定性验证。LWIP 2.x在IP分片、TCP性能上有不少改进,但BSP适配和稳定性需要自己验证,我建议项目工期紧就用lwip141,想做长期演进再评估lwip202。

我在一个功能需求比较多时尝试过把lwip202自己适配到ZYNQ上,工作量主要是重写sys_arch和网卡驱动接口,周期大概一周左右。对于只做TCP/UDP收发、普通协议转换的项目来说,lwip141完全够用,没必要自找麻烦。当然,如果你需要IPv6或SOCKET层API,lwip141就不行了,LWIP 2.x会方便很多。

8.3 裸机方案与FreeRTOS+LWIP的取舍

有些项目会纠结要不要上FreeRTOS。如果只是双网口数据收发和简单业务逻辑,裸机足够了,而且代码简单,好查问题。如果业务比较复杂,比如同时要管理多个外设、有复杂的任务调度和互斥需求,那上FreeRTOS再跑LWIP更合理。

FreeRTOS+LWIP的优势是任务可以阻塞等待信号量,不会因为某个业务阻塞而影响其他任务。但代价是引入调度器、队列、信号量等机制,RAM占用变大,调试难度也高一些。我在两个项目里分别用了裸机和FreeRTOS方案,结论是:如果主循环体量不大、业务逻辑清晰,裸机最高效;反之,别硬扛,上RTOS更符合项目管理成本。这个没有绝对标准,根据团队技术栈和产品复杂度来定即可。

9. 总结性经验与踩坑实录

做ZYNQ裸机LWIP双网口,最花时间的部分其实不是LWIP本身的配置,而是把硬件初始化、DMA缓冲、中断机制、内存池大小这些底层细节调到恰到好处。我回想调试过程,有几次都是代码本身没逻辑错误,纯粹是某个宏没配够大或者某个地址没对齐导致偶发问题。

有几个经验想特别分享给正在做类似项目的朋友:

第一,先把单网口完全调通,再做双网口。我在项目开始时直接照搬双网口代码,结果出问题时根本分辨不清是驱动的问题还是双网口特有逻辑的问题。后来退回单网口调通链路、PHY、LWIP,再逐步扩展第二个网口,整个定位过程顺畅了很多。

第二,尽量用固定IP而不是DHCP。裸机LWIP的DHCP实现虽然可用,但在产品环境中容易出现各种边缘问题。两个网口固定在不同网段,配合静态路由,整体逻辑会非常清晰。

第三,调试时多用串口打印关键寄存器和状态。比如PHY ID、link状态、netif flags、内存使用率,这些信息能帮你快速缩小问题范围。我甚至在驱动里临时加过统计计数器,记录每个网口收发了多少包、丢弃了多少包,定位问题特别好用。

第四,如果打算做长期产品,建议在硬件设计阶段就把双网口的MAC地址方案考虑清楚。用板载EEPROM存MAC、序列号等信息,软件启动时读取,这样设备才能批量生产、统一管理。我在项目后期遇到上百台设备MAC冲突的问题,纯软件已经没法绕过,只能靠硬件存储来解决。

双网口做完之后,整个系统的架构能力其实就打开了。你可以把它扩展成多网口网关,也可以在ZYNQ里加PL逻辑做协议加速,甚至可以把一个网口桥接到PS的DMA通道上,做高速数据采集。基础稳了,上面的应用就有更多可能性。最后再说一个非常实在的建议:在调试优化之前,先去Xilinx官方文档里把GEM和DMA的数据手册部分仔细读一遍,很多看似玄学的问题,其实是寄存器配置不到位,硬件本身的行为非常清晰。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询