ZYNQ裸机双网口通信实战:手把手教你用SDK和LWIP配置PS+PL以太网(附完整源码)

张开发
2026/6/6 18:19:35 15 分钟阅读

分享文章

ZYNQ裸机双网口通信实战:手把手教你用SDK和LWIP配置PS+PL以太网(附完整源码)
ZYNQ裸机双网口通信实战从零构建PSPL以太网通信框架第一次拿到ZYNQ开发板时看着板载的两个以太网接口我就在想如果能同时利用PS端和PL端的网络接口实现数据并行传输那该多酷啊但真正动手时才发现从BSP配置到LWIP调优从双网卡初始化到数据收发每个环节都藏着不少坑。本文将带你一步步构建完整的双网口通信系统分享我在实际项目中总结的避坑指南。1. 开发环境准备与基础工程搭建1.1 硬件平台选型与配置我使用的是Xilinx ZYNQ-7000系列开发板具体型号为ZC706。这块板子的优势在于PS端集成千兆以太网MAC控制器PL端可通过EMAC Lite IP扩展第二个网络接口自带GMII-to-RGMII IP核简化PHY连接关键硬件连接检查清单确认PS端ETH0通过RGMII直连PHY芯片检查PL端ETH1使用GMII接口连接PHY确保两个PHY的复位电路独立可控1.2 Vivado工程配置要点在Vivado中创建工程时这些配置直接影响后续开发# 在Block Design中添加ZYNQ Processing System IP set_property -dict [list \ CONFIG.PCW_USE_M_AXI_GP0 {1} \ CONFIG.PCW_USE_S_AXI_HP0 {1} \ CONFIG.PCW_USE_FABRIC_INTERRUPT {1} \ CONFIG.PCW_ENET0_PERIPHERAL_ENABLE {1} \ CONFIG.PCW_ENET1_PERIPHERAL_ENABLE {1}] [get_bd_cells processing_system7_0]特别要注意的是必须启用两个ENET控制器并为PL端ETH1分配正确的基地址。我遇到过因为地址冲突导致PHY初始化失败的情况后来通过以下命令确认地址映射# 在XSCT中查看IP地址分配 connect targets -set -filter {name ~ ARM*#0} mrd 0xE000B000 82. LWIP库的深度定制与优化2.1 关键参数配置实战在SDK中创建BSP时勾选lwip141库后需要修改lwipopts.h中的关键参数/* 内存配置 */ #define MEM_SIZE (64*1024) // 双网口需要更大内存 #define PBUF_POOL_SIZE 32 // 增加PBUF缓存数量 #define TCP_SND_BUF (8*1024) // 增大发送缓冲区 /* 协议栈特性 */ #define LWIP_NETIF_STATUS_CALLBACK 1 // 启用网卡状态回调 #define LWIP_TCP_TIMESTAMPS 0 // 禁用时间戳选项节省资源常见配置误区对比参数默认值推荐值作用TCP_WND20488192增大TCP窗口提升吞吐量TCP_MSS14601440避免IP分片MEMP_NUM_TCP_PCB510支持更多并发连接2.2 双网口初始化代码剖析在lwip_tcp.c中我重构了网卡初始化逻辑使其支持动态配置int init_ethernet(struct netif *netif, uint8_t eth_num, const ip_addr_t *ipaddr, const ip_addr_t *netmask, const ip_addr_t *gw, const unsigned char *mac) { if(!xemac_add(netif, ipaddr, netmask, gw, mac, eth_num ? PLATFORM_EMAC1_BASEADDR : PLATFORM_EMAC_BASEADDR)) { kprintf(ETH%d init failed!\n, eth_num); return -1; } netif_set_up(netif); return 0; }这段代码的亮点在于使用统一接口初始化两个网卡根据eth_num自动选择基地址返回错误码便于问题定位3. 双网卡数据收发架构设计3.1 状态机与回调机制实现我设计了一个轻量级状态机管理连接状态typedef enum { TCP_STATE_INIT, TCP_STATE_LISTEN, TCP_STATE_CONNECTED, TCP_STATE_CLOSING } tcp_state_t; struct tcp_conn { struct tcp_pcb *pcb; tcp_state_t state; uint32_t last_activity; struct pbuf *pending_data; };关键回调函数注册tcp_recv(pcb, tcp_recv_callback); tcp_sent(pcb, tcp_sent_callback); tcp_err(pcb, tcp_err_callback); tcp_poll(pcb, tcp_poll_callback, 2); // 每2个轮询间隔检查一次3.2 零拷贝数据收发优化传统的数据发送需要多次内存拷贝我通过以下方式优化err_t tcp_send_direct(struct tcp_pcb *pcb, void *data, u16_t len) { struct pbuf *p pbuf_alloc(PBUF_RAW, len, PBUF_REF); p-payload data; p-flags | PBUF_FLAG_REF; // 标记为引用外部内存 err_t err tcp_write(pcb, p, len, TCP_WRITE_FLAG_COPY); pbuf_free(p); return err; }这种方法减少了内存拷贝次数实测吞吐量提升约30%。但要注意确保data在传输完成前不被释放不适合超大包超过MSS4. 实战调试与性能调优4.1 常见问题排查指南问题1PHY链路无法UP检查硬件连接用示波器测量MDIO时钟验证PHY地址miiutil工具读取PHY ID确认复位时序添加50ms延时再初始化问题2TCP连接频繁断开调整Keepalive参数#define TCP_KEEPIDLE_DEFAULT (7200000UL) // 2小时 #define TCP_KEEPINTVL_DEFAULT (75000UL) // 75秒 #define TCP_KEEPCNT_DEFAULT 9启用窗口缩放选项#define LWIP_WND_SCALE 1 #define TCP_RCV_SCALE 24.2 性能测试数据对比在不同帧大小下的吞吐量测试结果帧大小(Byte)单网口(Mbps)双网口(Mbps)提升比例6478.2142.682%512412.5812.397%1024876.81682.492%1500942.11875.299%测试环境开发板ZC706PHY芯片Marvell 88E1512测试工具iperf2协议栈lwIP 1.4.15. 进阶技巧负载均衡与故障转移5.1 基于哈希的分流算法为实现双网口的负载均衡我实现了简单的五元组哈希分流uint8_t select_interface(uint32_t src_ip, uint32_t dst_ip, uint16_t src_port, uint16_t dst_port) { uint32_t hash src_ip ^ dst_ip ^ (src_port 16) ^ dst_port; return (hash % 2); // 返回0或1选择网口 }实际应用中还可以根据当前链路质量动态调整实现基于权重的分配策略支持手动指定优先网口5.2 链路状态监测机制通过定期发送探测包检测链路状态void link_monitor_task(void *arg) { while(1) { if(netif_is_link_up(netif0)) { send_keepalive(netif0, KEEPALIVE_INTERVAL); } if(netif_is_link_up(netif1)) { send_keepalive(netif1, KEEPALIVE_INTERVAL); } vTaskDelay(pdMS_TO_TICKS(1000)); } }当检测到链路中断时立即切换流量到备用网口记录断线时间戳尝试自动恢复连接6. 工程代码结构优化建议6.1 模块化设计实践推荐的项目目录结构├── app │ ├── netif # 网络接口管理 │ ├── protocol # 协议栈封装 │ └── utils # 工具函数 ├── bsp # 板级支持包 ├── lwip_port # LWIP移植层 └── drivers # 外设驱动每个模块应提供清晰的接口头文件实现独立的初始化函数包含自测试用例6.2 调试信息分级输出我习惯使用分级日志系统#define LOG_LEVEL_ERROR 0 #define LOG_LEVEL_WARNING 1 #define LOG_LEVEL_INFO 2 #define LOG_LEVEL_DEBUG 3 void net_log(int level, const char *fmt, ...) { if(level CURRENT_LOG_LEVEL) { va_list args; va_start(args, fmt); vprintf(fmt, args); va_end(args); } }典型用法NET_LOG(LOG_LEVEL_DEBUG, TCP %s state change: %d - %d\n, tcp_desc(pcb), old_state, new_state);7. 从原型到产品可靠性增强7.1 看门狗与异常恢复为防止协议栈死锁我添加了硬件看门狗void tcp_watchdog_init(void) { XScuWdt_Config *cfg XScuWdt_LookupConfig(XPAR_SCUWDT_0_DEVICE_ID); XScuWdt_CfgInitialize(wdt, cfg, cfg-BaseAddr); XScuWdt_LoadWdt(wdt, WDT_TIMEOUT); XScuWdt_Start(wdt); } void tcp_watchdog_feed(void) { if(XScuWdt_IsWdtExpired(wdt)) { xil_printf(WDT reset!\n); XScuWdt_RestartWdt(wdt); } }在TCP轮询任务中定期喂狗超时时间建议设置为正常轮询间隔的3-5倍。7.2 内存泄漏检测lwIP虽然小巧但内存管理仍需谨慎。我使用以下方法检测泄漏void mem_debug_init(void) { #if MEM_STATS lwip_stats.mem.used 0; #endif } void check_mem_leak(void) { #if MEM_STATS if(lwip_stats.mem.used ! 0) { xil_printf(Memory leak detected: %d bytes\n, lwip_stats.mem.used); } #endif }结合mem_malloc/mem_free的钩子函数可以精确定位泄漏位置。

更多文章