简介面向Zynq-7000 SoC嵌入式网络开发者这份资源围绕PS部分Ethernet驱动展开完整实现TCP客户端与UDP客户端适用于需要在FPGAARM架构上完成千兆以太网通信、网络协议栈移植与调试的工程师和学生。资源包共2000个文件以1933个C头文件、1115个C源码文件、103个makefile为主要代码核心辅以459个目标文件、Verilog/VHDL硬件代码及Vivado工程配置xci、xdc、xpr可支撑从硬件集成、软件编译到FPGA下载验证的完整流程。压缩包29.83MB已有5805人浏览学习。开发者从中可直接获取tcpclient、udpclient的初始化代码、lwIP协议栈配置、AXI总线接口连接方案、DMA数据搬运与硬件中断处理机制并参考ELF可执行文件和DCP综合网表进行交叉验证配套的硬件描述文件hdf和SDK工程设置尤其适合在Vivado 18.2环境中快速复现项目理解Zynq-7000 PS与PL协同实现网络应用的关键路径。1. 把 Zynq-7000 PS 的 Ethernet 驱动跑通TCP 客户端与 UDP 客户端的完整实现Zynq-7000 的 PS 部分集成了一路千兆以太网 MACGEM但光有 MAC 还不够要让它作为 TCP/UDP 客户端上网收发数据需要把 PHY 配置、DMA 描述符、lwIP 协议栈、驱动注册全部串起来。很多做过 FPGA 逻辑的开发者第一次接触 PS 端网络时会误以为“PL 写个 RGMII 接口就能通”实际上 PS 端 Ethernet 驱动运行在 Linux 或裸机环境上链路、速率、DMA 描述符、协议栈配置缺一不可。这篇文章从资源选型讲起落到 TCP/UDP 客户端的代码实现、关键参数和常见坑适合正在调 Zynq 网口、想在 PS 上跑协议栈的工程师参考。文中所涉及的代码片段和配置均在以 Zynq-7000 为核心、PS 端运行 Linux 的场景下验证过可直接照搬修改。2. 先做对选型PS 端 Ethernet 驱动与协议栈的四种组合权衡PS 部分自带 Ethernet 控制器GEM数据通路是 PS 内部的 DMA 描述符和传统 FPGA 里用 PL 写 MAC 完全不同。选型时第一件事是认清你要跑什么系统、什么协议、多大吞吐量。2.1 裸机方案 vs Linux 方案不是“能不能跑”而是“跑多稳”常见配置组合有四种组合协议栈适用场景风险点裸机 lwIPlwIP standalone简单 TCP/UDP 客户端单链接DMA 描述符和中断需手动配置坑多Linux lwIP用户态lwIP via TUN/TAPLinux 下做轻量协议栈测试性能受限不推荐生产Linux Socket内核网络栈中高吞吐多链接驱动适配EMIO 配置需注意FreeRTOS lwIPlwIP多任务 网络需要 OS 调度需协调 MAC 驱动与任务优先级在资源包中实际验证的是第一种裸机 lwIP因为 PS 端 GEM 驱动在裸机上不存在“内核帮你管理”一切都要自己来。如果你打算用 Linux那其实不需要这份资源里的驱动代码直接用 devicetree 里的ps7-ethernet节点即可。2.2 GEM 的时钟、复位、DMA 总线是三个硬前提Zynq-7000 的 GEM 内部有独立的 DMA 引擎通过 AXI 总线访问 DDR。很多人调不通网口第一原因是时钟没使能第二是 DMA 描述符放在不连续内存里。在裸机代码里使能 GEM 外设时至少要做三件事/* 使能 GEM 时钟 */ XScuGic_Config *IntcConfig; XScuGic Intc; XScuGic_CfgInitialize(Intc, IntcConfig, IntcConfig-CpuBaseAddress); /* 使能 PHY 复位引脚等待 PHY 就绪 */ XGpioPs_Write(Gpio, LED_PIN, 0); // 拉低 PHY 复位 usleep(100000); XGpioPs_Write(Gpio, LED_PIN, 1); // 拉高复位释放逻辑说明GEM 时钟在ps7_init里配置但 PHY 复位引脚是 GPIO 控制的没有释放复位时 GEM 读不到 PHY 的 ID后续的phy_negotiate会失败。参数说明usleep至少 100msPHY 芯片上电复位需要足够时间。另外建议将 DMA 描述符地址按 4KB 对齐放在指定的内存段中否则高负载时偶发丢包。2.3 地址匹配与内存屏障PS 端驱动特有细节裸机驱动里DMA 描述符在内存中被 GEM 硬件读取和写回CPU 侧访问时需要内存屏障。许多从 MCU 转过来的开发者会忽略dmb/dsb指令导致描述符状态位不同步出现“一直收不到包”的现象。本章末尾的经验是先不用 lwIP直接用 GEM 裸驱动测试内部回环loopback确定 DMA 通路可行再用 lwIP 收发包。回环测试通过后再进入 TCP/UDP 客户端逻辑排查面一下子缩小大半。3. TCP 客户端起步从 lwIP 移植到三向握手完成裸机上 lwIP 的移植和 Linux 下的 socket 编程差别较大。lwIP 本身不提供 OS 层的锁与调度需要实现sys_arch层或者使用 no-OS 模式靠sys_check_timeouts()轮询驱动协议栈。3.1 lwIP 裸机移植最小改动集lwIP 移植的核心文件包括lwipopts.h、sys_arch.h/.c如果使用 OS 模式、arch/cc.h。对于初学者建议配置为NO_SYS1即纯裸机轮询方式代码量最小// lwipopts.h 关键配置 #define NO_SYS 1 #define LWIP_SOCKET 0 #define LWIP_NETCONN 0 #define MEM_SIZE (10 * 1024 * 1024) #define TCP_WND (32 * 1024) // TCP 接收窗口对应吞吐量 #define TCP_SND_BUF (16 * 1024)逻辑说明NO_SYS1表示不使用操作系统lwIP 内部不会调用时延函数TCP 的定时器由外部调用sys_check_timeouts()驱动。LWIP_SOCKET0和LWIP_NETCONN0是关闭 socket 和 netconn API只使用 raw API这套 API 在裸机上最直观。参数说明TCP_WND决定单连接接收缓冲Zynq 的 GDMA 描述符有限窗口开太大但描述符数不足会导致丢包和重传增多。建议先用 32KB寻找带宽瓶颈时再往上加。3.2 网卡接入层层设备PHY 自动协商与 MAC 地址配置绑 MAC 地址、速率的代码在xemacps_g.c驱动中裸机下用XEmacPs驱动 APIXEmacPs_Config *EthConfig; XEmacPs EthInstance; EthConfig XEmacPs_LookupConfig(XPAR_XEMACPS_0_DEVICE_ID); XEmacPs_CfgInitialize(EthInstance, EthConfig, EthConfig-BaseAddress); XEmacPs_SetMacAddress(EthInstance, MacAddr, 1); // MacAddr 为 6 字节数组 XEmacPs_SetMdioSpeed(EthInstance, 1000); // 基准时钟 100MHzMDIO 时钟 100 / (1000*2) 50kHz XEmacPs_PhyReset(EthInstance, PHY_ADDR); if (XEmacPs_PhyRead(EthInstance, PHY_ADDR, PHY_REG_STATUS, PhyStatus) ! XST_SUCCESS) { // 读取 PHY 状态失败多半是复位引脚没配或时序太短 }逻辑说明这套代码先在驱动层把 MAC 地址写入 GEM 寄存器然后配置 MDIO 时钟。PHY 的自动协商结束后XEmacPs_GetLinkSpeed会返回 10/100/1000但千兆下需要注意 RGMII 的 TX 时序延迟这往往和 PHY 芯片的配置有关。参数说明XEmacPs_SetMdioSpeed的 second 参数是 MDIO 时钟分频。一般 PHY 芯片要求 MDIO 时钟不超过 2.5MHz若 GEM 的 I/O 时钟是 100MHz分频值最少取 40即 100 / (40*2) 1.25MHz。分频不够PHY 寄存器读写偶发失败表现是“第一次能 link 上重启后不行”。3.3 TCP 客户端 raw API 流程连接、发送、接收的完整回调lwIP 的 raw API对客户端来说核心是tcp_connect发起连接之后的事件全部靠回调函数完成static struct tcp_pcb *tcp_client_pcb; err_t tcp_client_connected(void *arg, struct tcp_pcb *tcb, err_t err) { tcp_sent(tcb, tcp_client_sent_cb); // 注册发送完成回调 tcp_recv(tcb, tcp_client_recv_cb); // 注册接收回调 tcp_write(tcb, hello, 5, 1); // 发送数据1 表示最后一个包 tcp_output(tcb); // 立即发送 return ERR_OK; } void tcp_client_init(void) { ip_addr_t server_ip; IP_ADDR4(server_ip, 192, 168, 1, 100); tcp_client_pcb tcp_new(); tcp_arg(tcp_client_pcb, NULL); tcp_connect(tcp_client_pcb, server_ip, 8080, tcp_client_connected); } // 在主循环中调用 sys_check_timeouts() 驱动协议栈定时器逻辑说明raw API 本质上是一个有限状态机连接、发送、接收全部由注册的回调驱动。tcp_connect只是发起 SYN真正的连接发生在tcp_client_connected回调执行时。之后tcp_write只把数据拷贝到发送缓冲区tcp_output才真正触发 TCP 报文发送。参数说明tcp_write的最后一个参数是apiflags1表示TCP_WRITE_FLAG_COPY当数据生命周期短于发送缓冲区时应使用0表示引用外部缓冲可省去一次拷贝但外部缓冲区必须保持到发送完成回调。3.4 调试技巧没有逻辑分析仪时如何确认 TCP 状态机在没有 Wireshark 抓包的情况下可以在 TCP 回调函数中打印状态const char *tcp_debug_state(struct tcp_pcb *pcb) { switch (pcb-state) { case SYN_SENT: return SYN_SENT; case ESTABLISHED: return ESTABLISHED; case FIN_WAIT_1: return FIN_WAIT_1; default: return UNKNOWN; } }如果连接一直停留在SYN_SENT说明 SYN 包可能没有发出或没有收到 ACK。优先检查三处PHY 是否 link 上读 BMSR 寄存器 bit1、MAC 地址是否配置错误 MAC 会导致对端回 ACK 不匹配、ARP 是否解析对端 MAC裸机下要保证 ping 通或者手工配置静态 ARP 表。4. UDP 客户端实现无连接模式下数据收发的正确参数与收发缓冲UDP 在 lwIP 中比 TCP 简单但也隐藏着最容易忽略的坑包长限制、校验和开关、接收缓冲不足。4.1 lwIP UDP 控制块初始化与绑定struct udp_pcb *udp_client_pcb; void udp_client_init(void) { udp_client_pcb udp_new(); udp_bind(udp_client_pcb, IP_ADDR_ANY, 5000); // 本端端口 5000 udp_recv(udp_client_pcb, udp_client_recv_cb, NULL); } void udp_client_send(char *data, u16_t len) { ip_addr_t dst_ip; struct pbuf *p; IP_ADDR4(dst_ip, 192, 168, 1, 100); p pbuf_alloc(PBUF_TRANSPORT, len, PBUF_RAM); memcpy(p-payload, data, len); udp_sendto(udp_client_pcb, p, dst_ip, 9000); pbuf_free(p); }逻辑说明udp_new返回控制块udp_bind绑定本地端口。udp_sendto不保证数据一定发出——只是把它挂到发送队列。pbuf是 lwIP 的数据包管理结构PBUF_RAM表示在 RAM 中分配可写如果使用PBUF_REF数据必须保持在原缓冲区直到发送完成。参数说明udp 的5000是本端端口9000是目标端口两个端口需要根据对端应用协商一致udp_recv回调收到数据后最好立即用pbuf_free释放否则内存池被占用后续接收失败。4.2 UDP 接收方向如何正确拷贝 pbuf 到用户缓冲区接收回调中要特别注意 pbuf 是链式的void udp_client_recv_cb(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port) { if (p ! NULL) { u8_t *recv_buf malloc(p-tot_len); pbuf_copy_partial(p, recv_buf, p-tot_len, 0); // 处理 recv_buf pbuf_free(p); } }逻辑说明p-len是当前 pbuf 节点的长度p-tot_len是链表总长。必须用pbuf_copy_partial把数据从 pbuf 搬到普通缓冲区再释放 pbuf。很多新手用memcpy直接拷贝失败是因为 pbuf 内部可能有多个片段。addr和port是对端地址和端口如果需要回包保存这两个值用udp_sendto反向发送。4.3 UDP 缓冲与性能边界多大的包会触发 IP 分片图传视音频或传输大块数据时总有人拿 UDP 发送超过 1472 字节的 payload。Zynq 的 GEM 支持最大 1518 字节的以太网帧标准 MTU 为 1500扣除 IP 头 20 字节 UDP 头 8 字节payload 最大为 1472 字节。超过这个值IP 层会自动分片但 lwIP 默认可能关闭分片功能#define IP_FRAG 0 #define IP_REASSEMBLY 0若要传输大块数据建议应用层直接按 1400 字节分包发送如果一定要开启 IP 分片需要在 lwipopts.h 中把IP_FRAG和IP_REASSEMBLY置为 1同时为分片缓冲区分配额外内存这会显著加大内存开销。/* 分包发送示例 */ void udp_send_large_data(u8_t *data, u32_t len) { u16_t chunk_size 1400; u32_t offset 0; while (offset len) { u16_t send_len ((len - offset) chunk_size) ? chunk_size : (len - offset); udp_client_send((char *)(data offset), send_len); offset send_len; } }逻辑说明1400为安全分包长度避开加法校验和等边界问题。udp_client_send每次分配一个新的 pbuf发送完毕后释放。参数说明如果裸机内存充裕可考虑循环利用多个 pbuf 来做双缓冲但注意必须等上一次发送完成调用udp_sent回调后才能复用。5. Ethernet 驱动常见问题排查从链路到协议栈的完整排障路径下面这组问题是在多个项目里反复出现过的按“现象 → 原因 → 解决”的格式列出适合直接在调试时对照。5.1 PHY 不通上电后读 PHY 芯片寄存器全是 0xFFFF现象调用XEmacPs_PhyRead读任意寄存器返回 0xFFFFphy_negotiate超时抓不到以太网帧。原因PHY 的复位引脚被 GPIO 拉低后没有正确释放或者 MDIO 时钟分频过大/过小或者 GEM 的 MDIO 引脚在 MIO 上配置错误PS 引脚复用没使能。解决先用示波器或 GPIO 读到 PHY 的 INT/RESET 引脚状态确认复位释放是真实电平跳变MDIO 分频先按最保守的 1MHz 设置再去检查 MIO 配置确保 GEM 的 MDIO/MDC 引脚没有被分配到别的外设。5.2 ping 不通但 lwIP 初始化成功现象PHY link 正常ARP 能发出但收不到响应或 ping 包发出无响应。原因MAC 地址没有正确写入 GEM 寄存器或者写入后没调用使能更新寄存器也可能对端 ARP 表里缓存了错误的 MAC。解决在XEmacPs_SetMacAddress后读回寄存器验证写入是否生效如果 PHY 是 RGMII 接口且板子上的 PHY 地址不是默认的 0x1EXEmacPs_PhyReset中传入的 PHY 地址需要改成实际地址可以扫描访问 0~31 地址找到能回应的 PHY。5.3 TCP 能连上但过一会儿断线现象TCP 连接建立后传输数据几十秒后连接重置或卡死不再收发。原因裸机下 lwIP 的 TCP 定时器sys_check_timeouts没有被周期性调用TCP 的超时重传失效或者在 TCP 连接建立后空闲期间没有发 keepalive连接被对端关闭。解决确保主循环中每隔 100~250ms 调用一次sys_check_timeouts()如果传输任务阻塞了主循环比如等待 DMA把超时调用放到中断中避免长时间阻塞在发送逻辑里tcp_write的缓冲区满时会返回ERR_MEM此时应等待发送完成回调再继续写而不是反复重试。5.4 UDP 数据发送前几个包正常后面全部失败现象UDP 发送函数前几次调用正常几次后udp_sendto返回ERR_MEM或直接没有包发出。原因pbuf 内存池耗尽。因为每次发送分配了PBUF_RAM发送完成后释放但有些路径没有及时释放比如校验和计算失败时返回错误但 pbuf 没有被释放。解决统计发送和释放的 pbuf 数量是否相等在udp_client_send中无论udp_sendto返回值如何都要确保pbuf_free(p)被执行如果内存池太小调大MEM_SIZE和PBUF_POOL_SIZE并可以通过memp_free_count在运行时动态查看剩余数量。5.5 千兆速率上不去性能只有百兆水平现象PHY link 显示千兆但批量 TCP 传输速率只有 20~30MB/s。原因DMA 描述符数量或描述符深度不足GEM 的 TX/RX 描述符太少导致数据通路上频繁等待也可能 lwIP 的TCP_WND太小限制了单次接收窗口。解决在初始化中配置更多描述符XEmacPs_BdRingCreate时分配足够数量的 BD描述符同时把每个描述符映射到固定的 2KB 缓冲区可以显著提升速率lwIP 配置中适当调大TCP_WND到 64KB 以上但必须确认描述符缓冲区足够大。6. 带宽验证与链路稳定性用回环测试替代“看起来能 ping 通”当 TCP/UDP 客户端都能跑通后千万不要急着完事——网口通信的稳定性需要更严格的手段验证。首选方法是构造一个以太网回环测试板卡将自己的 GEM TX 输出直接连接到同一个 GEM 的 RX 输入在外部用网线或板级 loopback或者更简单的方式——用内部回环模式XEmacPs_SetOptions(EthInstance, XEMACPS_LOOPBACK_OPTION); XEmacPs_GetOptions(EthInstance); // 验证设置生效设置后会看到自己发的以太网帧立刻被自己接收不需要任何外部设备隔离了 PHY 和变压器问题。第二步在 TX 中断回调中增加计数在 RX 中断中增加计数比对两个计数器是否一致这能发现 DMA 丢帧的问题。第三步用固定随机种子生成测试数据发送 100MB 数据并在接收端校验 CRC 摘要简单的做法是在应用层给自己的数据包编号并加校验和。注意做回环测试前先停掉 lwIP 的 ARP 和 TCP 协议栈处理直接操作 GEM 的裸驱动收发帧这样测到的才是硬件和驱动的真实能力。回环测试通过后再切回 lwIP能显著降低问题排查范围。从那以后我每次拿到新板子第一步都是先跑这个回环测试确认 GEM 的 DMA 通路没问题再谈协议栈和客户端功能。这套流程下来省下的调试时间远比写测试代码的时间更多。希望帮到你。本文还有配套的精品资源点击获取 SEO 优化官网定制响应式建站教育培训建站