简介:面向Zynq-7000 SoC开发者的PS端Ethernet驱动资源包,聚焦TCP客户端和UDP客户端实现,覆盖Vivado 18.2下IP核配置、AXI总线连接、SDK软件栈编写等环节,适合嵌入式网络通信及FPGA+PS协同设计的学习与项目参考,可帮助快速搭建PS端网络通信环境。压缩包29.83MB,共2000个文件,以1933个h头文件、1115个c源码、459个o目标文件为主体,另有makefile、xml、tcl、xci等工程配置,包含lwIP协议栈相关实现以及ps7_init等硬件初始化代码,工程目录结构完整。已有5805人浏览学习。包内udpclient、tcpclient和project05_ps_Ethernet工程可帮助理解MAC/IP配置、套接字编程、硬件中断处理及DMA性能优化,适合直接查找和构建Zynq-7000 GbE通信应用的参考实现。
1. 这是一个什么样的工程:Zynq-7000 PS端以太网,从LwIP驱动到TCP/UDP客户端
很多第一次在 Zynq-7000 上做网络通信的开发者,往往会卡在这样一个路口:硬件上明明已经有可用的以太网控制器,却不知道 PS(Processing System)端怎么把数据包发出去。手头这份资源解决的就是这个问题——它把 Zynq-7000 PS 内置的千兆以太网控制器 GEM 从头做了一遍驱动级封装,并给出可直接改用的 TCP 客户端和 UDP 客户端两个工程。你不再需要从硬件手册里逐个翻寄存器,也不用在 LwIP 协议栈的移植细节里打转,拿到工程后只要照着流程生成硬件平台、配置 PHY,就能在自己的板卡上跑通 socket 收发。适合三类人:正在做采集或控制板卡、需要以客户端身份上报数据的嵌入式工程师;想把 FPGA 数据通过网口低成本传出去的硬件工程师;以及刚接触 Zynq PS 端软件开发、想找一个能读懂驱动调用链的初学者。
2. 整体框架与资源剖析:从GEM控制器到LwIP协议栈
2.1 PS端以太网硬件链路:GEM、PHY、MDIO
Zynq-7000 的 PS 部分集成了一组硬核以太网控制器,官方名称叫 GEM(Gigabit Ethernet MAC)。它不是挂在 PL 上的软核,而是固化在 PS 内部的硬件外设,因此不需要消耗 FPGA 逻辑资源,也不需要在 PL 里例化 IP。我们使用的型号通常是 Zynq-7000 系列中带千兆网口的版本,比如经典的双核 Cortex-A9 搭配一个或两个 GEM 控制器。每个 GEM 对外挂接一个 PHY 芯片,常见的有 RTL8211 或 KSZ9031,通过 RGMII 或 SGMII 接口互联。
GEM 和 PHY 之间的管理通道是 MDIO,也就是读取 PHY 寄存器状态、配置 PHY 工作模式所用的两线接口。驱动初始化时,必须先通过 MDIO 写入 PHY 的寄存器,完成自协商和速率设定,否则即便 GEM 的 DMA 已经跑起来,物理层也无法正常收发数据。这个顺序是不能颠倒的,很多同学在调试时先起 DMA、后配 PHY,导致链路层一直是 down 状态。
2.2 软件层叠:BSP、驱动、协议栈和应用
软件架构从上到下分为四层。最下面是 BSP,由硬件平台导出,包含寄存器地址、中断号、时钟频率等定义。BSP 上方是 GEM 的设备驱动,负责初始化 DMA 描述符、配置 MAC 地址、使能中断、完成收发数据包。再往上是 LwIP 协议栈,它把驱动收上来的裸数据包拿到协议层处理,TCP 的握手、重传、UDP 的校验和解析都在这层完成。最顶层是应用代码,也就是我们要实现的 TCP 客户端和 UDP 客户端。
这份资源里的驱动和应用是分文件组织的,驱动层文件不依赖具体业务,换一个 PHY 芯片只要改寄存器配置。LwIP 部分采用了常见的 noapps 配置,即协议栈自身不携带上层服务器逻辑,只提供一个轻量级 socket 接口,这样便于我们自由地用 socket API 写客户端。这里特别提一下,LwIP 在裸机环境下有两种运行方式:一种是不断调用tcp_tmr轮询定时器,另一种是通过中断唤醒sys_check_timeouts。本工程默认选用的是轮询模式,这样逻辑简单、调试直观。
2.3 工程目录结构和关键文件
拿到资源后,第一件事是把目录结构理清楚。典型布局如下:
project_root/ ├─ hardware/ # 硬件平台文件 │ ├─ zynq_eth_wrapper.xsa # 硬件配置输出 │ └─ ps7_init/ # PS初始化代码 ├─ sdk/ │ ├─ baremetal_bsp/ # BSP包 │ ├─ lwip_tcp_client/ # TCP客户端工程 │ └─ lwip_udp_client/ # UDP客户端工程 ├─ src/ │ ├─ eth_driver/ │ │ ├─ xemacps.c # GEM驱动初始化 │ │ ├─ xemacps_dma.c # DMA描述符管理 │ │ └─ phy_rtl8211.c # PHY配置 │ └─ net_app/ │ ├─ tcp_client.c │ └─ udp_client.c └─ README.mdxemacps.c里封装了EmacPsInit、EmacPsSendFrame和EmacPsReceiveFrame三个核心函数;phy_rtl8211.c负责把 RTL8211 寄存器配置到千兆自适应模式。应用层中,tcp_client.c包含 TCP 状态机处理和发送逻辑,udp_client.c则相对简单,只有绑定、发送、接收回调。目录不含复杂脚本,所以迁移到自己的工程只需要把sdk和src复制并重新设置 BSP 路径。
3. 核心模块拆解:一个网络包从PS发出到接收的完整流程
3.1 基础初始化:时钟、引脚和DMA描述符
PS 端以太网初始化的第一步不是操作寄存器,而是确保引脚复用正确。在 Zynq-7000 中,GEM0 的 TXD、RXD、RGMII 控制信号都映射在 MIO 或 EMIO 上,如果引脚没有分配到 GEM 功能,后续所有寄存器操作都收不到 PHY 的响应。常见做法是通过硬件平台(xsa)里的引脚配置工具,把 MIO 48~63 配置为 GEM0 的 RGMII,再把 MDIO 的 MIO 52、53 配置为 MDIO 功能。
接着初始化 GEM 控制器本身。下面的代码展示了基本时钟和 DMA 描述符的开启顺序:
#include "xemacps.h" #include "xemacps_hw.h" static XEmacPs GmacInstance; static u8 RxBuf[1600 * 8] __attribute__((aligned(64))); static u8 TxBuf[1600 * 8] __attribute__((aligned(64))); s32 EmacPsInit(void) { XEmacPs_Config *Cfg; Cfg = XEmacPs_LookupConfig(XPAR_PS7_ETHERNET_0_DEVICE_ID); XEmacPs_CfgInitialize(&GmacInstance, Cfg, Cfg->BaseAddress); // 设置MAC地址,这里使用板卡预设的物理地址 u8 Mac[6] = {0x00, 0x0A, 0x35, 0x00, 0x01, 0x02}; XEmacPs_SetMacAddress(&GmacInstance, Mac, 0); // 为接收和发送各分配8个DMA描述符 XEmacPs_SetQueueLevel(&GmacInstance, XEMACPS_QSEL_RECEIVE, 8); XEmacPs_SetQueueLevel(&GmacInstance, XEMACPS_QSEL_TRANSMIT, 8); // 把收发缓冲区地址提交给DMA XEmacPs_AlignedDmaWrite(&GmacInstance, XEMACPS_BD_RX, (UINTPTR)RxBuf); XEmacPs_AlignedDmaWrite(&GmacInstance, XEMACPS_BD_TX, (UINTPTR)TxBuf); return 0; }这里的关键点是缓冲区必须 64 字节对齐。GEM 的 DMA 描述符要求收发包地址的低 6 位为 0,如果不对齐,DMA 搬运时会抛出地址错误中断,常见表现是驱动一直收不到包。上述代码中,__attribute__((aligned(64)))的写法可以直接保证对齐,比运行时手动计算偏移更可靠。SetQueueLevel指的是 DMA 描述符数量,数量越大吞吐越高,但会占用更多内存,在裸机上 8 个描述符通常足够支撑千兆线速试验。
PHY 初始化一般在 MAC 初始化之后,通过 MDIO 写 PHY 寄存器。以 RTL8211 为例,需要先复位芯片,再设置自适应模式:
static void PhyInit(void) { // 复位PHY PhyWriteReg(0, 0x8000); // 等复位完成 for (volatile int i = 0; i < 100000; i++); // 开启千兆全双工自适应 PhyWriteReg(0, 0x1940); }PhyWriteReg底层是调用 GEM 的 MDIO 控制寄存器XEmacPs_WritePhy,完成一个 MDIO 访问循环。PHY 的英文寄存器编号 0 是控制寄存器,位 15 为复位,位 6 为速度选择,0x1940的含义是开启 1000M 全双工。这里要强调的是,不同 PHY 的寄存器和复位时序不同,如果换用 KSZ9031,需要把写寄存器里的自适应模式值改为该芯片对应的控制字。
3.2 TCP客户端实现:连接、发送、轮询接收
TCP 客户端应用的核心是三步:创建 socket、连接远端服务器、发送数据。本工程基于 LwIP 的 socket API 实现,代码比较直观:
#include "lwip/sockets.h" #define SERVER_PORT 9000 #define SERVER_IP "192.168.1.100" int TcpClientTask(void) { int sock, ret; struct sockaddr_in server_addr; char txbuf[128]; char rxbuf[256]; sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) return -1; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(SERVER_PORT); server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); ret = connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)); if (ret < 0) { close(sock); return -1; } while (1) { memset(txbuf, 0xAA, sizeof(txbuf)); send(sock, txbuf, sizeof(txbuf), 0); int len = recv(sock, rxbuf, sizeof(rxbuf), 0); if (len > 0) { // 处理接收数据 } // 裸机轮询,需要处理超时 vTaskDelay(100); // 这里实质是轮询延时 } close(sock); return 0; }在裸机环境下,connect会阻塞一段时间等待 TCP 三次握手完成,这个等待依赖 LwIP 的定时器推进。若没有调用sys_check_timeouts(),connect 永远无法完成。所以在另一个定时中断或主循环里必须周期执行sys_check_timeouts(),本工程放在了一个 1ms 周期的系统节拍函数中。send和recv的成功返回,只代表数据交给协议栈或驱动,不代表对方已经应用层收到,这一点在调试时容易被忽略。
发送速率上,由于是裸机轮询,100ms 发送一次 128 字节,非常保守。如果需要提高吞吐,可以缩短轮询周期或改成中断驱动,但要注意协议栈缓冲区足够大。
3.3 UDP客户端实现:绑定、发送、接收回调
UDP 客户端比 TCP 简单很多,无需连接,直接通过sendto发送报文至目标端口。核心代码如下:
int UdpClientTask(void) { int sock; struct sockaddr_in local, remote; char data[512]; sock = socket(AF_INET, SOCK_DGRAM, 0); if (sock < 0) return -1; memset(&local, 0, sizeof(local)); local.sin_family = AF_INET; local.sin_addr.s_addr = htonl(INADDR_ANY); local.sin_port = htons(8000); // 本地绑定端口 bind(sock, (struct sockaddr *)&local, sizeof(local)); memset(&remote, 0, sizeof(remote)); remote.sin_family = AF_INET; remote.sin_port = htons(9000); remote.sin_addr.s_addr = inet_addr("192.168.1.100"); while (1) { memcpy(data, "udp packet from zynq", 20); sendto(sock, data, 20, 0, (struct sockaddr *)&remote, sizeof(remote)); int len = recvfrom(sock, data, sizeof(data), 0, (struct sockaddr *)&remote, (socklen_t *)&remote_lens); // 这里处理UDP应答 vTaskDelay(50); } close(sock); return 0; }UDP 的bind是必要的,它决定了本机监听哪个端口。如果不 bind,内核会随机分配端口,导致对端无法区分是哪台设备发来的。这里有一个比较反直觉的点:recvfrom在没有数据时返回 0 或 -1,并不会像 TCP 那样触发错误回调,所以应用层会频繁空转。更好的做法是利用 LwIP 的回调接口,将recvfrom放到 UDP 接收回调中,但本工程为演示简单,采用轮询调用。
3.4 中断与轮询的选择:什么时候用哪种
在 Zynq 裸机环境下,GEM 驱动既支持中断模式,也支持轮询模式。本工程采用的是轮询收包,在主循环中调用XEmacPs_ReceiveFrame查询是否有新包。轮询模式的好处是代码简单,不会出现中断上下文和主循环共享变量的互斥问题;缺点是 CPU 占用率高,且 UDP 包流量较大时会因为查询不及时而丢包。
中断模式则需要在初始化时配置 GIC(通用中断控制器),并在中断服务函数里把收到的数据包放入队列,主循环再从队列里取数据。本工程为了降低入门门槛,没有在初期引入队列,而是直接把轮询作为默认方案。如果你要处理高速 UDP 流,建议改成中断加环形缓冲区,这也是第 6 章的改造方向之一。
4. 从工程导入到板卡验证:完整落地步骤
4.1 生成硬件平台和导出BSP
硬件平台是 Zynq 开发的起点。在 FPGA 设计工具中,需要创建一个包含 PS 的电路原理图,并在 PS 配置界面中使能 Ethernet 0 外设,选择 RGMII 接口,设置引脚复用和 MDIO 时钟。完成后点击生成硬件平台,输出一个.xsa文件。导出到软件环境后,工具会自动生成ps7_init_gpl.c和对应的寄存器宏,这些文件包含了 PS 的 DDR 控制、MIO 配置和时钟初始化代码。
实际落地时,我发现很多同学会把 XSA 文件直接丢给软件工程,但忘了在硬件设计里确认以太网参考时钟的频率。Zynq-7000 的 GEM 需要 125MHz 的参考时钟,有的板子从外部晶振提供,有的需要 PS 内部的FCLK_CLK1输出。如果时钟不对,PHY 的链路状态会一直抖动,表现为协商成功但收发不稳定。因此拿到工程后,务必先检查硬件平台里的以太网参考时钟属性。
4.2 导入驱动与应用工程并配置lwIP
打开软件 SDK 后,首先导入 BSP。在 BSP 设置界面,勾选使用 lwip 库,版本可以选择标准支持版本。然后导入本资源中的两个应用工程:lwip_tcp_client和lwip_udp_client。导入后,需要检查 BSP 生成的lwipopts.h,关键的配置项如下表:
| 配置项 | 推荐值 | 作用 |
|---|---|---|
| NO_SYS | 1 | 裸机模式,不跑操作系统线程 |
| MEM_SIZE | 160000 | 协议栈内部内存池大小 |
| PBUF_POOL_SIZE | 32 | 收发缓冲池数量 |
| TCP_WND | 64 * 1024 | TCP 接收窗口 |
| TCP_SND_BUF | 32 * 1024 | TCP 发送缓冲区 |
| CHECKSUM_GEN_IP | 1 | 开启 IP 校验和生成 |
| CHECKSUM_GEN_UDP | 1 | 开启 UDP 校验和生成 |
NO_SYS设为 1 后,LwIP 不依赖 RTOS,socket API 内部是轻量级实现。MEM_SIZE直接影响 TCP 大数据吞吐,如果一个 TCP 连接发送超过 30KB 时出现发送失败,多半是MEM_SIZE不够。注意,PBUF_POOL_SIZE和 DMA 描述符数量要匹配,协议栈的 pbuf 是从内存池分配的,而驱动 DMA 需要把内存地址写给描述符,两者如果大小不一致,会在高速收包时出现丢包。
编译之前,还需要修改应用代码里的 IP 地址段。tcp_client.c中固化的SERVER_IP是 192.168.1.100,你要改成对端服务器的实际 IP。同时本机 IP 地址在驱动初始化时设置,代码里通常默认是192.168.1.10,如果你的网络网段不同,要同步修改。这里有一个容易被忽略的细节:如果 PC 端服务器和板卡在同一个局域网,必须保证两个 IP 在同一个子网且没有 IP 冲突,否则连接会卡在 ARP 解析阶段。
4.3 编译下载与测试方法
编译完成后,通过 JTAG 下载 BOOT.bin 或直接在 SDK 中运行。运行时推荐用串口终端打印日志,查看驱动初始化有没有报错,常见错误是找不到 PHY,打印PHY ID mismatch。
测试 TCP 客户端时,先在电脑上启动一个 TCP 服务器。在 Linux 下可以用nc命令快速调试:
nc -l 9000在 Windows 下可以用网络调试助手。启动板卡应用后,TCP 服务器端收到AAAA...数据,同时板卡能收到服务器回包,说明链路通畅。为了验证 TCP 连接状态,可以在 PC 端抓包,观察三次握手的 SYN、SYN-ACK、ACK 是否正常。如果没有抓包工具,可以在服务器端查看连接列表,确认存在一条ESTABLISHED状态的连接。
UDP 测试更简单,电脑上先用nc -u -l 9000监听 UDP 端口,板卡启动后即可收到广播的 UDP 数据。收包测试则需要把发送代码中的sendto改成先recvfrom,再在 PC 端用nc -u 192.168.1.10 8000发送数据。建议先用网线直连,避免路由器或交换机上有无效的组播过滤,减少变量。
5. 避坑记录:地址对齐、超时和PHY初始化
5.1 现象:TCP连接卡死在SYN_SENT,服务器一直收不到SYN
抓包看到板卡只发出了 SYN,但服务器回应的 SYN-ACK 到不了板卡。原因分析后定位到 GEM 的接收中断没有使能,驱动侧虽然发出了 SYN,但无法接收响应。根本原因是初始化时XEmacPs_EnableInterrupts被注释掉了,或者中断控制器没配置。
解决方法是确保中断驱动模式下,中断控制器的优先级和触发类型都正确设置。如果是轮询模式,则要检查主循环里是否调用了XEmacPs_ReceiveFrame,如果轮询频率太低,响应包会积压在 DMA 描述符里,超过描述符数量后触发溢出错误。我调试时把ReceiveFrame放在了一个 10ms 周期的定时器里,导致 SYN-ACK 回来发现缓冲区已满,后续 TCP 状态机因为收不到 ACK 超时重传,整个连接建立过程变得极慢。后来把轮询放到主循环无条件执行,问题才消失。
5.2 现象:收到的TCP数据是乱的,有时连续几包相同
这是典型的 DMA 描述符未清空导致的问题。GEM 驱动在接收帧后,需要更新描述符的控制字,标记该描述符可被 DMA 再次使用。如果应用层复制数据后没有重新写描述符的flags,DMA 认为缓冲区仍不可用,就会停止接收。具体表现为第一包数据正常,第二包开始出现重复或截断。
解决方法是每次recv返回后,调用XEmacPs_ReleasePacket或手动将描述符的NORMAL位置位。在驱动代码里,这通常发生在中断服务函数末尾。不要把这个操作放在协议栈回调里,因为协议栈回调执行完才释放缓冲区,若回调中阻塞,DMA 接收就会暂停。
5.3 现象:UDP丢包率高达50%,但网络链路正常
排查发现是PBUF_POOL_SIZE设得太小,同时驱动中断优先级较低。当高速连续 UDP 包到达时,协议栈来不及从 DMA 描述符中把 pbuf 申请出来,新包就会因为描述符被占满而丢弃。另一个原因是 LwIP 的UDP_CHECKSUM_GEN和UDP_CHECKSUM_CHKSUM同时开启导致双倍校验计算,CPU 性能不足产生瓶颈。
解决方案是把PBUF_POOL_SIZE从 8 提升到 32,同时将 GEM 中断优先级调整到高于串口和通用定时器。如果是轮询模式,则要增加轮询频率,或者改为中断驱动。抓包后发现,丢包集中在 PC 端高速发送开始后的前几十微秒,这对 UART 打印日志的影响非常敏感,所以调试时尽量避免在接收路径中加打印,打印一条耗时几毫秒的日志足以让后续多包丢失。
5.4 现象:DMA描述符读写报错,地址错乱
发生这个问题时,驱动在初始化阶段就打印了错误。原因是XEmacPs_AlignedDmaWrite传入了未对齐的缓冲区地址。有些时候编译器会忽略aligned属性,尤其在数组首地址是从堆中分配的时候。malloc返回的地址通常只保证 8 字节对齐,无法满足 64 字节 DMA 对齐要求。
解决方法是改用静态全局数组配合__attribute__((aligned(64)))。如果必须动态分配,可以手动计算对齐偏移:分配size+63字节,然后将地址向上取整到 64 的倍数。这属于隐藏较深的坑,因为指针打印出来可能看起来是对的,但低 6 位不为零。我在 code review 时见过同事用(u32)buf & ~0x3F做地址强制对齐,但没有保留原始指针,导致释放时崩溃。正确做法是保存原始地址,仅在对齐地址上运行 DMA。
5.5 现象:PHY自适应协商成功,但板卡Ping不通
链路状态显示千兆全双工,但 Ping 不通。抓包显示板卡没有回应 ICMP 请求,但物理层是通的。问题出在 MAC 地址配置上,初始化代码把 MAC 地址设置成了某个组播或广播地址,导致驱动不接收单播报文。Zynq 的 GEM 控制器允许设置多个 MAC 地址,如果只有第一个地址寄存器被写入,但实际上传的是第二个地址,那么网卡不会识别本机地址。
解决方式是调用XEmacPs_SetMacAddress后,再调用XEmacPs_GetMacAddress读回来,确认写入生效。比较稳妥的办法是,使用 U-Boot 中读取到的 MAC 地址,通过XEmacPs_SetMacAddress设置给 GEM。如果所有寄存器都正确,仍然不通,需要检查 PHY 的交叉线缆或直通线连接,在千兆模式下线序要求更严格,这已经不是软件问题。
6. 二次开发思路:改成TCP服务器、多客户端或HTTPS
6.1 从TCP客户端到TCP服务器:换一种连接角色
本资源自带的 TCP 客户端只支持主动连接单一服务器。如果想要板卡被电脑连接,改成 TCP 服务器。改动点很小:把connect换成bind+listen+accept。在 LwIP 的 socket API 中,TCP 服务器需要先bind到固定端口,再调用listen使端口处于监听状态,然后阻塞在accept直到有客户端连入。代码片段如下:
int TcpServerTask(void) { int srv_sock, cli_sock; struct sockaddr_in addr, cli_addr; socklen_t len = sizeof(cli_addr); srv_sock = socket(AF_INET, SOCK_STREAM, 0); addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_ANY); addr.sin_port = htons(9000); bind(srv_sock, (struct sockaddr *)&addr, sizeof(addr)); listen(srv_sock, 4); while (1) { cli_sock = accept(srv_sock, (struct sockaddr *)&cli_addr, &len); // 处理client_sock上的收发,处理完后close } }注意accept在裸机 LwIP 中同样依赖sys_check_timeouts推进,必须在主循环里周期调用时间管理。改成服务器后,原来的SERVER_IP就不需要了,但需要确认TCP_WND够大,否则客户端连续大量发送时,LwIP 的接收窗口小于一个 TCP 分段,吞吐会骤降。
6.2 多任务与多连接:把轮询变成中断驱动收发
原生轮询模式不适合同时处理多个 socket。如果想支持多客户端或同时收发多个数据流,建议把 GEM 中断打开,在中断服务函数中编写一个无锁环形缓冲区,中断收到数据包后把 pbuf 直接塞入队列,主循环中的多个 socket 任务通过select或accept事件去队列取数据。这样收包频率不再受主循环节奏限制,CPU 利用率也更健康。
Zynq-7000 的中断路径走 GIC,中断号在硬件平台中自动分配,一般为 51 或 53。驱动初始化时需要调用XScuGic_Connect把EmacPsIsr挂到对应中断键上。中断服务函数里要读取 GEM 的中断状态寄存器,清掉对应的 bit,并调用XEmacPs_ReceiveFrame收包。注意,收发 DMA 描述符的操作需要在关中断状态下进行,避免同一描述符被并发修改。
6.3 性能调优:调整描述符数量和缓冲区大小
Zynq-7000 的 GEM 支持设置 DMA 描述符数量,实际吞吐量受限于描述符数量和数据包缓冲大小。默认情况下,描述符数量 16 个,每个缓冲区 1600 字节,可以满足小流量。要跑千兆满速,建议把发送描述符数量提升到 128,接收描述符数量提升到 128,并将PBUF_POOL_SIZE同步增加到 256。注意每增加一个描述符,内存占用会增加约 1600 字节,128 个接收描述符需要约 200KB 内存,这在部分 DDR 较小的板卡上不可接受,需要实测后折衷。
还有一个容易被忽略的参数是TX_REQ_BUF_SIZE,它控制 DMA 每次从内存抓取数据的长度。如果设小了大于一次突发传输,DMA 会被拆成多次,影响吞吐。同时,RX_BUF_SIZE决定单个 skb 缓冲能容纳的最大帧长,默认 1600 字节能吃到 1518 字节的以太网帧,但如果开启 VLAN,需要调整到 1600 以上。这个参数不是越大越好,过大浪费时间在缓冲区清理上。
6.4 验证技巧:用脚本模拟对端和抓包
二次开发后,需要一个可重复的验证环境。推荐在 PC 端写一个 Python 脚本同时模拟多个 TCP 客户端或 UDP 对端,避免每次都用网络助手手工点。下面是一段简单的 UDP 性能测试脚本:
import socket import time s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.bind(('192.168.1.100', 9000)) s.settimeout(5) for i in range(100): s.sendto(b'ping', ('192.168.1.10', 8000)) data, addr = s.recvfrom(1024) if data != b'pong': print('unexpected reply', data) print('done')配合 Wireshark 抓包,可以确认每个 UDP 包是否被板卡正确接收。如果只是验证协议栈是否工作,还可以在板卡应用里加入一个计数器,记录接收到的包数量和字节数,通过串口输出。我曾做过一个比较笨的实验:把板卡接收到的字节数除以 1500,结果发现实际收到的包数比预期多出 20%,原因是驱动把 CRC 也算进长度里了,后来在获取长度时减去 4 字节才对齐。从那以后我每次做以太网驱动二次开发,都强制先抓包、后统计,用抓包结果校正软件计数器,不再相信寄存器里的长度值。希望这份资源的拆解能帮你在 Zynq PS 以太网这条路上少走几个来回。
本文还有配套的精品资源,点击获取