1. 项目缘起与整体设计思路ZYNQ 这颗芯片最让人又爱又恨的地方就是 PS 端Processing System和 PL 端Programmable Logic的耦合关系。爱的是它把 ARM 和 FPGA 揉在一颗芯片里恨的是很多外设资源在 PS 端是固定的比如网口。ZYNQ-7000 系列的 PS 端通常只引出一路千兆以太网通过 MIO 或者 EMIO 接到外部 PHY但实际项目里经常需要两路甚至更多网口——一路接上位机做调试一路接设备做数据采集或者做双网冗余、内外网隔离。这时候就得动脑筋了第二路网口从哪来最直接的办法就是在 PL 端用逻辑资源自己搭一个以太网 MAC然后通过 EMIO 把信号引到 PS 端让 PS 端的软件裸机环境下就是 LWIP去驱动它。听起来很顺但真正动手的时候坑一个接一个EMIO 的引脚约束怎么配、MDIO 总线能不能共用、中断怎么接、LWIP 的底层接口怎么改、PHY 地址冲突怎么办……这些问题在官方文档里往往一笔带过或者散落在各个手册的角落里。这篇内容就是把我自己在 ZYNQ 裸机环境下扩展第二路网口的完整过程整理出来。核心思路是PS 端第一路网口保持原样不动PL 端用 AXI EthernetLite 或者 AXI 1G/2.5G Ethernet Subsystem 搭第二路 MAC通过 EMIO 把 GMII/RGMII 接口引到 PS 端引脚再在 PS 端用 LWIP 做协议栈实现双网口独立工作。适合已经跑通过 ZYNQ 裸机 LWIP 单网口、想进一步扩展网口数量的开发者也适合正在评估 ZYNQ 双网口方案选型的工程师。为什么选 EMIO 而不是直接全部在 PL 端做因为 PS 端有现成的 DMA 和中断控制器数据吞吐和 CPU 占用比纯 PL 方案好很多而且 LWIP 在 PS 端跑内存管理和协议栈处理都更方便。EMIO 本质上是 PS 和 PL 之间的一条“内部延长线”把 PL 端的信号拉到 PS 端的引脚上让 PS 端的外设控制器以为自己接的是普通引脚。这个机制用好了能省掉很多外部走线和引脚资源。2. 核心细节解析与实操要点2.1 EMIO 扩展网口的底层逻辑先把这个事情说透。ZYNQ 的 PS 端以太网控制器GEMGigabit Ethernet MAC本身是支持多路的ZYNQ-7000 一般有 2 个 GEM 控制器但引脚复用有限。GEM0 通常走 MIO 接 PHYGEM1 可以通过 EMIO 引到 PL 端。所以严格来说我们不是“在 PL 端新建一个 MAC”而是把 PS 端的 GEM1 通过 EMIO 路由到 PL 引脚上外部 PHY 芯片接在 PL 端的引脚上。这一点非常关键很多人一开始就搞混了以为要在 PL 端例化一个 MAC IP结果绕了大弯。正确的做法是在 Vivado 的 Block Design 里ZYNQ Processing System 配置中使能 GEM1并把 GEM1 的接口设置为 EMIO。这样 GEM1 的 GMII 接口信号包括 TX/RX 数据、时钟、控制信号就会出现在 PL 端的端口上。然后你在 PL 端把这些信号接到外部 PHY 的引脚上加好约束。PS 端的软件看到的仍然是标准的 GEM1 控制器LWIP 驱动不需要大改只需要把 base address 和中断号配对就行。那为什么标题里说“用 EMIO 在 PL 端扩展第二个网口”因为从物理上看第二个网口确实是在 PL 端引出的PHY 芯片也挂在 PL 端的 Bank 上。但从控制器角度看它仍然是 PS 端的 GEM1。这个认知差是很多坑的根源。2.2 MDIO 总线共用与 PHY 地址规划双网口最容易被忽略的就是 MDIO 总线。MDIO 是管理接口用来读写 PHY 的寄存器配置速率、双工模式、读取链路状态。ZYNQ 的每个 GEM 都有自己的 MDIO 接口但实际硬件设计时为了省引脚经常把两个 PHY 挂在同一组 MDIO 上。这时候问题来了两个 GEM 控制器同时去操作同一组 MDIO会不会冲突答案是硬件上可以共用 MDIO 的时钟和数据线但软件上必须保证同一时刻只有一个控制器在操作 MDIO。更稳妥的做法是两个 PHY 用不同的 PHY 地址PHY Address这样即使共用 MDIO也能通过地址区分。PHY 地址通常由 PHY 芯片的几个配置引脚上下拉决定硬件设计时就要规划好。比如 GEM0 对应的 PHY 地址设为 0x01GEM1 对应的 PHY 地址设为 0x02。在 LWIP 的驱动里每个 GEM 实例需要配置自己的 PHY 地址。如果你用的是 Xilinx 的 emacps 驱动在xemacpsif结构体里会有phyaddr字段。裸机环境下通常是在xparameters.h里定义好或者在初始化代码里手动指定。如果两个 GEM 的 PHY 地址设成一样就会出现“第一个网口正常第二个网口死活连不上”的经典问题。还有一个细节MDIO 的时钟分频。ZYNQ 的 GEM 控制器里有一个network_config寄存器可以设置 MDIO 时钟分频值。如果两个 GEM 共用 MDIO分频值要一致否则一个快一个慢PHY 可能响应不过来。通常设置为 0x08 左右对应 MDC 时钟约 2.5MHz符合 IEEE 802.3 的要求。2.3 中断与 DMA 的分配双网口意味着双份中断和双份 DMA。ZYNQ 的 GEM0 和 GEM1 各有自己的中断号在xparameters.h里能看到XPAR_XEMACPS_0_INTR和XPAR_XEMACPS_1_INTR。裸机 LWIP 初始化时需要为每个网口分别注册中断服务函数。如果只注册了一个另一个网口的中断就会丢失表现为“能收几包然后卡死”或者“发送正常但接收不到”。DMA 方面每个 GEM 有独立的 TX/RX DMA 通道缓冲区描述符BD也是独立的。在xemacpsif里tx_bdspace和rx_bdspace是分开分配的。如果你用的是 Xilinx 的 lwip echo server 模板它默认只初始化一个网口。要支持双网口需要把xemacpsif实例化两份分别绑定到 GEM0 和 GEM1 的 base address然后各自启动。这里有个实操心得DMA 描述符的内存要对齐到 64 字节边界并且要放在非缓存uncached区域。如果放在缓存区域CPU 写描述符后没有 flush cacheDMA 读到的就是旧数据表现为发送卡住或者接收丢包。在 ZYNQ 裸机里通常用Xil_DCacheDisable()或者把描述符放在0xFFFF0000以上的 OCM 区域来规避。3. 实操过程与核心环节实现3.1 Vivado 端配置使能 GEM1 并引出 EMIO打开 Vivado创建 Block Design添加 ZYNQ Processing System IP。双击进入配置第一步在Peripheral I/O Pins里找到Ethernet 1勾选。然后在MIO Configuration里把Ethernet 1的接口从 MIO 改成 EMIO。这时候你会看到 GEM1 的 GMII 接口信号出现在 IP 的端口列表里包括GEM1_GMII_TXD[7:0]、GEM1_GMII_TX_EN、GEM1_GMII_TX_CLK、GEM1_GMII_RXD[7:0]、GEM1_GMII_RX_DV、GEM1_GMII_RX_CLK、GEM1_GMII_RX_ER、GEM1_MDIO_MDC、GEM1_MDIO_MDIO等。第二步把这些信号引出到顶层然后约束到 PL 端的引脚上。这里要注意 Bank 电压。PL 端的 Bank 电压通常是 3.3V 或 1.8V而 GMII 接口的电压取决于 PHY 芯片。如果 PHY 是 3.3V 的Bank 电压要设成 3.3V如果是 1.8V 的Bank 电压要对应。电压不匹配会导致通信不稳定甚至烧毁引脚。第三步如果 PHY 芯片需要复位信号还要在 PL 端加一个 GPIO 或者直接用一个引脚控制。复位时序一般是上电后拉低至少 10ms然后拉高等待 PHY 内部初始化完成。这个复位信号可以在 PS 端用 GPIO 控制也可以在 PL 端用逻辑产生。第四步生成 bitstream导出硬件描述文件xsa然后在 Vitis 里创建裸机工程。3.2 Vitis 端 LWIP 初始化双网口实例化在 Vitis 里新建一个 Empty Application然后添加 LWIP 库。Xilinx 的 LWIP 库默认是单网口的要支持双网口需要手动修改lwipopts.h和xemacpsif的初始化代码。先看lwipopts.h关键参数#define LWIP_NETIF_HOSTNAME 1 #define LWIP_NETIF_STATUS_CALLBACK 1 #define LWIP_NETIF_LINK_CALLBACK 1 #define LWIP_DHCP 0 // 裸机下建议用静态IP避免DHCP超时 #define LWIP_IPV4 1 #define MEM_SIZE (16 * 1024) #define PBUF_POOL_SIZE 64 #define PBUF_POOL_BUFSIZE 1536然后看xemacpsif的初始化。Xilinx 的驱动里xemacpsif_s结构体包含emacps实例、phyaddr、tx_bdspace、rx_bdspace等。我们需要创建两个实例static xemacpsif_s emacpsif0; static xemacpsif_s emacpsif1; void init_emacpsif(xemacpsif_s *emacpsif, u32 baseaddr, u32 phyaddr, u32 intr_id) { emacpsif-emacps.Config.BaseAddress baseaddr; emacpsif-phyaddr phyaddr; emacpsif-emacps.Config.IntrId intr_id; // 分配BD空间注意对齐 emacpsif-tx_bdspace (xemacps_bd*)memalign(64, sizeof(xemacps_bd) * 32); emacpsif-rx_bdspace (xemacps_bd*)memalign(64, sizeof(xemacps_bd) * 32); // 初始化DMA XEmacPs_CfgInitialize(emacpsif-emacps, emacpsif-emacps.Config, baseaddr); // 设置PHY地址 XEmacPs_SetPhyAddress(emacpsif-emacps, phyaddr); // 启动 XEmacPs_Start(emacpsif-emacps); }在main函数里先初始化平台platform_init然后分别初始化两个网口再启动 LWIPinit_emacpsif(emacpsif0, XPAR_XEMACPS_0_BASEADDR, 0x01, XPAR_XEMACPS_0_INTR); init_emacpsif(emacpsif1, XPAR_XEMACPS_1_BASEADDR, 0x02, XPAR_XEMACPS_1_INTR); struct netif netif0, netif1; ip_addr_t ip0, mask0, gw0; IP4_ADDR(ip0, 192, 168, 1, 10); IP4_ADDR(mask0, 255, 255, 255, 0); IP4_ADDR(gw0, 192, 168, 1, 1); netif_add(netif0, ip0, mask0, gw0, emacpsif0, ethernetif_init, ethernet_input); netif_set_default(netif0); netif_set_up(netif0); ip_addr_t ip1, mask1, gw1; IP4_ADDR(ip1, 192, 168, 2, 10); IP4_ADDR(mask1, 255, 255, 255, 0); IP4_ADDR(gw1, 192, 168, 2, 1); netif_add(netif1, ip1, mask1, gw1, emacpsif1, ethernetif_init, ethernet_input); netif_set_up(netif1);注意ethernetif_init是 LWIP 的通用接口它会调用xemacpsif里的low_level_init。在low_level_init里需要根据传入的netif-state来区分是哪个网口然后设置对应的 MAC 地址和 PHY 地址。MAC 地址可以硬编码也可以从 EEPROM 读裸机下通常直接写死两个不同的 MAC。3.3 中断注册与 PHY 链路检测中断注册是双网口最容易漏的一步。在platform_init里Xilinx 的xscugic驱动会初始化中断控制器。然后需要为每个 GEM 注册中断服务函数XScuGic_Connect(intc, XPAR_XEMACPS_0_INTR, (Xil_ExceptionHandler)XEmacPs_IntrHandler, emacpsif0.emacps); XScuGic_Enable(intc, XPAR_XEMACPS_0_INTR); XScuGic_Connect(intc, XPAR_XEMACPS_1_INTR, (Xil_ExceptionHandler)XEmacPs_IntrHandler, emacpsif1.emacps); XScuGic_Enable(intc, XPAR_XEMACPS_1_INTR);PHY 链路检测方面LWIP 有一个netif_set_link_callback可以在链路状态变化时触发。裸机下通常用轮询方式每隔 100ms 读一次 PHY 的 BMSR 寄存器检查 Link Status 位。如果链路断了就调用netif_set_link_down链路恢复了调用netif_set_link_up。这个轮询可以放在一个定时器中断里或者放在主循环里。实测下来PHY 链路检测的轮询周期不要太短100ms 到 500ms 比较合适。太短了浪费 CPU太长了响应慢。另外PHY 的自动协商Auto-Negotiation完成后BMSR 的 Link Status 位才会置位所以上电后要等几百毫秒再开始检测。4. 常见问题与排查技巧实录4.1 双网口常见问题速查表现象可能原因排查方法解决方法第二个网口 ping 不通PHY 地址冲突读两个 PHY 的 ID 寄存器修改硬件上下拉设置不同 PHY 地址发送正常接收丢包DMA 描述符未对齐或缓存未刷新检查 BD 地址是否 64 字节对齐用 memalign 分配或放在 OCM两个网口只能通一个中断未注册或中断号错误查看 xparameters.h 里的中断号为两个 GEM 分别注册中断网口频繁掉线MDIO 时钟分频不一致读 network_config 寄存器统一设置为 0x08上电后第二个网口不亮PHY 复位时序不对示波器看复位引脚拉低至少 10ms 再拉高LWIP 内存耗尽PBUF_POOL_SIZE 太小查看 memp 统计增大到 64 或 128双网口互相干扰共用 MDIO 无互斥加软件互斥锁操作 MDIO 前关中断4.2 独家避坑技巧第一个坑EMIO 的引脚约束不要漏掉时钟信号。GMII 接口有 TX_CLK 和 RX_CLK这两个时钟必须约束到 PL 端的时钟 capable 引脚上。如果约束到普通 IOVivado 会报错或者时序不满足。通常 PHY 会提供 RX_CLK 给 FPGATX_CLK 由 FPGA 提供给 PHY。在约束文件里要用create_clock定义这些时钟否则时序分析过不了。第二个坑LWIP 的ethernetif_init会被调用两次但low_level_init里的 MAC 地址不能一样。如果两个网口 MAC 地址相同交换机会认为网络里有冲突导致两个口都不通。MAC 地址可以在low_level_init里根据netif-num来区分比如netif-num 0时用00:0a:35:00:01:02netif-num 1时用00:0a:35:00:01:03。第三个坑裸机下不要开 DHCP。DHCP 需要 UDP 广播和超时重传裸机环境下没有操作系统调度很容易卡在 DHCP 发现阶段。直接用静态 IP简单可靠。如果确实需要 DHCP建议把 DHCP 超时设短一点并且放在单独的线程或者定时器里轮询。第四个坑PHY 的复位引脚如果由 PL 端控制要注意上电顺序。有些 PHY 芯片要求复位在电源稳定后至少 10ms 才能释放如果复位释放太早PHY 内部寄存器可能没初始化好表现为 MDIO 读不到 ID。可以用 PS 端的 GPIO 或者 PL 端的计数器来产生复位时序。第五个坑双网口同时高速收发时DMA 带宽可能不够。ZYNQ 的 GEM 是千兆的两个口同时跑满就是 2Gbps而 PS 端的 DDR 带宽是共享的。如果 LWIP 的缓冲区太小或者 DMA 描述符太少会出现丢包。建议把PBUF_POOL_SIZE设到 128MEM_SIZE设到 32KB 以上并且把描述符数量增加到 64 个。4.3 调试手段与工具裸机下调试双网口最实用的工具是网口调试助手和串口打印。网口调试助手可以发 UDP 包测试收发串口打印可以看到 LWIP 的调试信息。在lwipopts.h里打开LWIP_DEBUG和NETIF_DEBUG可以看到每个网口的初始化过程、链路状态变化、收发统计。另外Xilinx 的xemacps驱动里有XEmacPs_ReadReg和XEmacPs_WriteReg可以直接读写 GEM 的寄存器。如果怀疑 DMA 有问题可以读XEMACPS_TXSR和XEMACPS_RXSR寄存器看发送和接收的状态位。如果 TXSR 的TXCOMPL位一直不置位说明 DMA 发送卡住了通常是描述符没准备好或者缓存没刷新。还有一个技巧用 ILAIntegrated Logic Analyzer抓 PL 端的 GMII 信号。如果 PS 端软件看起来正常但网口就是不通可以在 Vivado 里插入 ILA抓 TX_EN、TX_DATA、RX_DV、RX_DATA 等信号看 PL 端和 PHY 之间的数据流是否正常。这个手段能快速定位是 PS 端问题还是 PL 端问题。5. 性能优化与扩展思考5.1 双网口吞吐优化裸机 LWIP 的默认配置是面向低吞吐场景的双网口同时跑的时候CPU 占用会很高。优化方向有几个第一增大 PBUF 和 MEM 池。默认的PBUF_POOL_SIZE是 16MEM_SIZE是 4KB双网口下很容易耗尽。建议PBUF_POOL_SIZE设到 128MEM_SIZE设到 32KBMEMP_NUM_PBUF设到 64。第二启用 TCP 校验和卸载。ZYNQ 的 GEM 支持硬件校验和卸载可以在xemacpsif里配置XEMACPS_CHECKSUM_OFFLOAD。这样 TCP/IP 的校验和计算由硬件完成CPU 只需要处理协议栈逻辑吞吐能提升 20% 到 30%。第三使用零拷贝Zero-Copy发送。LWIP 默认的发送流程是pbuf拷贝到 DMA 缓冲区如果改成零拷贝直接把pbuf的地址写到 DMA 描述符里能省一次内存拷贝。不过零拷贝对内存对齐要求更高pbuf的 payload 必须 64 字节对齐。第四中断合并Interrupt Coalescing。ZYNQ 的 GEM 支持接收中断合并可以设置每收到 N 个包才产生一次中断减少中断次数。在XEmacPs_SetOptions里可以配置XEMACPS_RX_COALESCE选项。5.2 从双网口到多网口的扩展如果项目需要三个或更多网口ZYNQ-7000 的 PS 端只有两个 GEM第三个网口就必须在 PL 端用逻辑实现了。这时候可以用 Xilinx 的 AXI 1G/2.5G Ethernet Subsystem IP它包含 MAC 和 PCS/PMA通过 AXI Stream 接口和 PS 端通信。PS 端需要写一个 AXI DMA 驱动把以太网帧从 AXI Stream 搬到内存再交给 LWIP。这个方案复杂度高很多但灵活性也更大适合多网口或者需要特殊协议处理的场景。另一个思路是用 PL 端的软核MicroBlaze跑一个轻量级协议栈专门处理第三路网口然后通过 AXI 总线把数据传给 PS 端。这个方案适合对实时性要求高的场景但开发难度和资源占用都比较大。5.3 实际项目中的经验体会我在实际项目中用这套方案做过一个双网口数据采集设备一路网口接上位机做配置和数据显示另一路接前端传感器做高速数据采集。踩过的坑包括PHY 地址冲突导致第二个网口死活不通、DMA 描述符没对齐导致接收丢包、中断没注册导致第二个网口收几包就卡死。这些问题在单网口环境下都不会遇到只有双网口同时跑的时候才会暴露出来。后来总结下来双网口调试的核心就是隔离和验证先把两个网口分别单独调通确保每个网口的 PHY、MDIO、DMA、中断都正常然后再同时跑。如果同时跑出问题就用 ILA 抓 PL 端信号用串口打印看 PS 端状态逐步缩小范围。不要一上来就双网口同时调那样出了问题很难定位是哪个环节的毛病。最后再分享一个小技巧在xemacpsif的low_level_init里加一句打印把每个网口的 base address、PHY 地址、MAC 地址、中断号都打出来。这样上电后一眼就能看出配置对不对比看寄存器方便多了。这个习惯帮我省了很多调试时间。