1. 官方例程跑不通才是常态:这锅不全在你
先聊句实在话。Xilinx自带的lwIP例程,尤其是那些基于ZYNQ的Echo Server Demo,能在你手上板子一次跑通的概率真不高。我见过太多人卡在同一关:例程编译过了、下载了、串口也打印了,但PC端UDP工具死活收不到数据,或者板子一跑就死机。问题几乎都不在你写的代码上,而在三处:硬件配置和官方开发板对不上、BSP里的lwIP库没配置对、DMA地址和Cache一致性问题没处理干净。
这篇文章就按我的实操路径来写,目标很具体:用Vitis在ZYNQ上从零建一个能跑的lwIP UDP回环工程,收发都能通,并且把源码里每段关键逻辑讲清楚。适合刚摸到ZYNQ、被SDK/Vitis和lwIP配置折磨得想砸板子的人,也适合已经在跑Linux但有段时间想用裸机方案做低延迟小数据量通信的开发者。
1.1 UDP和lwIP在ZYNQ上是怎样一条通路
在动手写代码之前,得先把数据从网口到应用层这条路走明白。ZYNQ-7000的PS端有一颗GEM以太网控制器(Gigabit Ethernet MAC),它通过RGMII接口接到外部的PHY芯片,PHY再连RJ45座子。GEM这个MAC负责以太网帧的收发、CRC校验、MAC地址过滤,但它不处理IP协议——协议栈的活儿由软件来做,也就是lwIP。
lwIP是一个轻量级TCP/IP协议栈,专门给嵌入式系统用,完整实现了ARP、IP、ICMP、UDP、TCP这些协议,但资源和代码量都远小于Linux内核网络栈。裸机环境下它有两种运行模式:一种是NO_SYS=0,跑在RTOS或者裸机轮询调度之上,lwIP内部有自己的线程/进程模型;另一种是NO_SYS=1,完全不用操作系统,lwIP以回调方式工作。ZYNQ的BSP里Xilinx提供了适配好的lwIP库,默认是带Xilinx适配层的,它把GEM的DMA收发和lwIP的PBUF内存管理结合起来了,我们主要调用socket API或者netconn API就能收发数据,底层细节由驱动和适配层包掉。
数据收发的真实路径是这样的:网线进来 -> PHY解调模拟信号成数字帧 -> GEM的DMA引擎把帧搬到DDR -> lwIP驱动层把这包数据包装成PBUF -> 协议栈逐层解析(以太网头、IP头、UDP头)-> 最后触发UDP接收回调函数,你的应用代码在回调里拿到payload。发送方向恰好反过来。所以只要GEM的DMA、DDR地址映射、中断路由这三个环节不出错,UDP跑通就只是时间问题。
2. Vitis平台工程的硬件配置:DDR、中断、以太网一个都不能少
很多人在Vitis里建工程时图省事,直接拿官方BSP包里的模板来改,这就容易踩坑。正确做法是自己从头建Platform工程,因为你手里板子的DDR颗粒型号、PHY地址、复位引脚、时钟频率和官方板子很可能不一样。
2.1 用Vivado导出的XSA建Platform工程
先在Vivado里完成硬件工程,这里有几个必须检查的项:
- ZYNQ7 Processing System的配置里,DDR型号务必选成你板子实际用的型号。选错会导致内存测试失败或者跑着跑着死机,这是ZYNQ上最常见也最隐蔽的坑。常见板卡如米尔的、正点原子的、黑金的,DDR颗粒各有不同,别想当然。
- 以太网接口使能后,必须确认PHY的MDIO地址。比如有的板子PHY地址是0x0,有的是0x1,有的用0x7。这个地址在BSP的xparameters.h里有宏定义,如果和实际硬件不一致,link状态永远是down。
- GEM的DMA中断要确认已经使能。在Vivado的ZYNQ配置界面里,PS-PL中断和PS内部中断的映射很多教程没说清,GEM0的接收发送共用一条中断线,通常连接到GIC的SPI中断上,编号是54(GEM0)或55(GEM1)。Vitis BSP会自动读这个配置,但我们最好心里有数。
- ETH的复位引脚。多数板子把PHY的复位脚接到PS的EMIO或者MIO上,有的板子设计成上电自动复位不需要软件控制,有的就需要你在MIO配置里把GPIO方向和初值设对。这个查你板子的原理图,别等PHY没起来再到处找原因。
Vivado综合、实现、生成bitstream之后,File > Export Hardware,记得勾选Include bitstream,得到XSA文件。
然后打开Vitis,用这个XSA创建Platform工程。这里有一个需要注意的细节:如果板子上电时序或者DDR训练有问题,XSA里包含的FSBL(First Stage Boot Loader)在第一次启动时可能会卡住,表现为串口没输出。遇到这种情况,先把启动模式拨到JTAG,用SDK的Xilinx System Debugger连上去看bootrom状态,不要把时间浪费在反复烧写SD卡上。
2.2 BSP里lwIP库的配置选项详解
Platform工程创建好后,Vitis会自动生成BSP,但默认BSP里没有lwIP库,需要手动添加。
在platform.spr文件所在工程的Board Support Package设置里,点Add Library,找到lwip211(Xilinx移植的lwIP 2.1.1版本),添加。然后展开配置项,有几个参数值得逐个说:
| 配置项 | 默认值 | 建议值 | 说明 |
|---|---|---|---|
| lwip/MEM_SIZE | 1MB | 1MB | 协议栈堆内存,包含PBUF池、TCP段、UDP控制块等所有动态内存 |
| lwip/PBUF_POOL_SIZE | 32 | 64 | PBUF池数量,数据包收发缓存。收发频繁、数据量大的场景建议加大 |
| lwip/LWIP_NETCONN | 1 | 1 | 启用netconn API。socket API依赖netconn,建议保持 |
| lwip/LWIP_SOCKET | 0 | 1 | 启用socket API。如果只想用netconn,保持0也行 |
| lwip/IP_REASSEMBLY | 1 | 0 | IP分片重组。UDP大包超过MTU需要分片时才需要,裸机场景一般关闭 |
| lwip/NO_SYS | 0 | 1 | 关键!Xilinx的裸机lwIP适配层建议设为1,即无操作系统模式 |
| lwip/CHECKSUM_CHECK_NET | 1 | 1 | 接收时校验IP和UDP/TCP的校验和,建议开启 |
| lwip/CHECKSUM_GEN_NET | 1 | 1 | 发送时生成校验和,必须开启 |
| xil_lwip/XIL_NETIF_FULL | 0 | 0 | 是否使用完整网络接口驱动。0使用轻量级netif驱动,适合裸机 |
| xil_lwip/EMAC_DMA_RX_PING_BUFFER | 不适用 | 512 | 接收DMA描述符数量 |
我在第一次配置时没有动这些默认值,直接就跑了例程,UDP能收到数据但PC端发给板子超过一定长度的大包就丢。后来查下来就是PBUF_POOL_SIZE太小,接收队列满了之后新包进不来。这个值在内存够用的情况下宁可多配一些。
另外强烈建议把lwip/NO_SYS保持为1。Xilinx的lwIP裸机适配层在没有OS时以轮询方式处理协议栈,配合GEM中断收发缓冲区,在单核ZYNQ上完全够用,逻辑清晰,不容易出并发问题。如果你在ZYNQ上跑FreeRTOS再叠lwIP,那NO_SYS要设为0,复杂度立刻上几个台阶,第一次跑UDP不要碰那种组合。
2.3 平台工程的地址映射检查
BSP生成后,打开platform/src/cortexa9_0/bsp/ps7_cortexa9_0/xparameters.h,找到这几个宏并记下来:
#define XPAR_XEMACPS_0_BASEADDR 0xE000B000 #define XPAR_XEMACPS_0_DMA_CTRL_BASEADDR 0xE000B200 #define XPAR_XEMACPS_0_DMA_TX_BASEADDR 0xE000B400 #define XPAR_XEMACPS_0_DMA_RX_BASEADDR 0xE000B600 #define XPAR_XEMACPS_0_INTR_CPU_NUM 54GEM0基地址0xE000B000是PS自带外设的固定地址,DMA控制寄存器在后面偏移处,这些不用改。关键是中断号54,BSP会根据Vivado里的配置自动生成,如果板子用GEM1则是55。一会儿写代码时,XScuGic的初始化里要关联这个中断号,做中断向量表连接。
DDR的物理地址在ZYNQ上是0x00100000到0x1FFFFFFF(512MB颗粒)或者到0x3FFFFFFF(1GB颗粒),如果你的例程里出现过类似0x100000开头的缓冲区地址,那正是DDR的起始段。后面分配DMA缓冲区时就要落在DDR区间内,并且要和Cache行对齐。
3. 物理地址与Cache一致性问题:ZYNQ上最容易翻车的地方
裸机环境下没有MMU页表转换,虚拟地址就是物理地址,所以平时你不需要关心“物理地址映射”这个名词。但在做DMA收发时,Cache一致性是实实在在的大坑。
3.1 为什么DMA缓存区要对齐且禁用Cache
GEM的DMA引擎是直接读写DDR的。CPU从DDR读数据时,D-Cache会缓存一份副本;DMA把网线上来的数据写进DDR后,CPU再读自己的Cache副本,读到的是旧数据,于是你收到的UDP payload全是垃圾或者全部是0。反过来,CPU往DDR写数据,DMA也可能拿到的是Cache回写前的旧值。
Xilinx的lwIP适配层在分配DMA缓冲区时,会根据配置决定使用缓存一致性处理方式。如果没做处理,必须手工保证一致。最简单粗暴的办法是:把lwIP使用的内存区域设为非Cacheable。在BSP的xil_cache函数之外,更彻底的方式是在Platform的链接脚本里,把lwIP的堆段放到一个uncached区域。
具体做法是:在Vitis里打开lwip211_v1_0/src/xlwipconfig.h,找到LWIP_RAM_BASE或者类似宏,或者在BSP的xparameters.h里找到一个XPAR_AXI_7SDDR_0_S_AXI_BASEADDR宏,配合Xil_SetTlbAttributes()函数,把DDR的某一整块区域设置为非缓存。
#define DDR_BASE_ADDR 0x10000000 #define DDR_SPAN_SIZE 0x10000 /* 64KB, 存放DMA描述符和缓冲区 */ #define UNCACHE_BASE_ADDR 0x10000000 Xil_SetTlbAttributes(UNCACHE_BASE_ADDR, NORM_NON_CACHE);这个函数在xil_mmu.h里,需要包含Mutex头文件。执行后,从0x10000000开始的1MB(按需设定)DDR空间就是非Cacheable的。lwIP适配层在初始化DMA缓冲区时,用的就是这部分地址。
如果你的工程里已经没有多余DDR空间,也可以选择不关Cache,但就要在每次收发前后手动Xil_DCacheFlush()和Xil_DCacheInvalidate()。lwIP的Xilinx适配层其实已经帮你在xemacpsif_dma.c里做了部分flush/invalidate操作,前提是在lwIP库配置里把xil_lwip/ENABLE_CACHE相关的选项打开。我的建议很简单:裸机+lwIP头一次调,直接把DDR缓冲区设为非Cacheable,省掉所有心力。性能损失在100Mbps速率下完全可接受,等你跑通了再考虑优化Cache策略。
3.2 中断路由和GIC初始化时序
ZYNQ的GIC(通用中断控制器)负责把PS外设的中断分发到CPU。裸机下需要三步走:初始化GIC、注册中断回调、使能中断。
XScuGic GicInstance; XScuGic_Config *GicConfig; GicConfig = XScuGic_LookupConfig(XPAR_SCUGIC_0_DEVICE_ID); XScuGic_CfgInitialize(&GicInstance, GicConfig, GicConfig->CpuBaseAddress); /* * 中断号54对应GEM0。外设中断通常是边沿敏感,GEM的DMA中断 * 对上升沿敏感,所以触发方式不能用LEVEL_HIGH,否则会丢中断。 */ XScuGic_Connect(&GicInstance, XPAR_XEMACPS_0_INTR_CPU_NUM, (Xil_ExceptionHandler)XEmacPs_IntrHandler, (void *)&EmacPsInstance); XScuGic_Enable(&GicInstance, XPAR_XEMACPS_0_INTR_CPU_NUM); Xil_ExceptionInit(); Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_IRQ_INT, (Xil_ExceptionHandler)XScuGic_InterruptHandler, &GicInstance); Xil_ExceptionEnable();有一处值得特别说一下:GEM的中断标志在XEmacPs_IntrHandler内部会读ISR寄存器再写ICR寄存器清除,所以在GIC层不要再去配置中断为电平触发。如果配置错了,会出现中断风暴,表现为CPU一直在中断上下文里出不来,串口卡死,类似死机。
4. 源码解析:一个能直接用的UDP回环服务器
现在进入正题。这套代码是我在ZYNQ-7020上实测能够ping通、UDP收发回环正常的完整实现。为了大家方便读懂,我按层次拆开讲,从全局定义到网络线程再到UDP回调,最后贴上完整框架代码。每个部分都会直接注释“为什么这么做”。
4.1 全局定义和头文件
#include "xparameters.h" #include "xil_cache.h" #include "xscugic.h" #include "xemacps.h" #include "xemacpsif.h" #include "lwip/init.h" #include "lwip/udp.h" #include "lwip/ip_addr.h" #include "netif/xadapter.h" #include "platform.h" #include "xil_printf.h" #define UDP_SERVER_PORT 5001 #define UDP_BUFFER_SIZE 1460 /* 小于以太网MTU1500,扣掉IP头20和UDP头8 */xemacpsif.h是Xilinx适配GEM和lwIP的桥梁头文件,里面定义了初始化网络接口的函数xemacpsif_reset用于复位MAC和PHY,以及xemacpsif_config返回的结构体。netif/xadapter.h里有platform_setup_netif函数,负责把lwIP的网络接口结构体和Xilinx的驱动绑定起来。
4.2 初始化:从platform到lwIP
int main(void) { struct netif *netif_ptr; ip_addr_t ipaddr, netmask, gw; unsigned char mac_ethernet_address[] = { 0x00, 0x0a, 0x35, 0x00, 0x01, 0x02 }; platform_setup(); /* 初始化PS时钟、MIO、DDR,在platform.c里 */ setup_uart(); /* 串口打印重定向 */ xil_printf("UDP lwIP example on ZYNQ...\r\n"); /* 以下三行地址需要和你PC网卡在同一个子网 */ IP4_ADDR(&ipaddr, 192, 168, 1, 10); IP4_ADDR(&netmask, 255, 255, 255, 0); IP4_ADDR(&gw, 192, 168, 1, 1); lwip_init(); /* lwIP内部内存池、协议表初始化 */ netif_ptr = platform_setup_netif(mac_ethernet_address); netif_set_default(netif_ptr); netif_set_up(netif_ptr); netif_set_link_up(netif_ptr); /* 强制链路状态,部分PHY回读link位异常时需要 */ start_application(); /* 创建UDP服务 */ xil_printf("UDP server start. Target IP: 192.168.1.10\r\n"); while(1); return 0; }这里要提醒两件事。
第一,MAC地址如果没烧写eFUSE,裸机下必须在代码里显式指定。platform_setup_netif如果拿到的是全0 MAC,PHY依然能link,但ARP会出问题,PC端arp -a查不到板子,UDP发出去也没有响应。我在调试时遇到过这种,网口灯是亮的,但PC ping不通,绝大多数情况和ARP有关。
第二,netif_set_link_up默认在PHY检测到link后会自动调用,但如果你的PHY支持速率自动协商而驱动没有及时更新,会出现needs to poll的情况。所以建议初始化后直接用下面这样的小延时循环强制标志位置位:
void start_application(void) { int timeout = 0; while (0 == netif_is_link_up(netif_ptr)) { if (++timeout > 1000000) { xil_printf("PHY link timeout, use force up\r\n"); break; } } udp_echoserver_init(); }4.3 UDP回环服务:netconn API还是raw回调
lwIP有两种主流编程模型:
- netconn API(类似socket风格):
代码可读性好,有阻塞/超时机制。XPipe斩断底层细节,但引入内存拷贝开销,因为收到数据先到内部缓冲区再拷贝给你的应用。struct netconn *conn = netconn_new(NETCONN_UDP); netconn_bind(conn, NULL, UDP_SERVER_PORT); netconn_recv(conn, &buf); netconn_send(conn, buf); - raw UDP回调(直接基于udp_pcb):
零拷贝进入回调,数据由PBUF直接递交,效率高,一个PING-PONG的UDP服务用这种方式最合适。struct udp_pcb *pcb = udp_new(); udp_bind(pcb, IP_ADDR_ANY, UDP_SERVER_PORT); udp_recv(pcb, udp_echo_recv_callback, NULL);
ZYNQ上裸机跑UDP回环,没有多线程安全问题,选raw回调方式更合理——回调里直接就把数据原样发回去了,无需额外缓冲区拷贝。如果以后改成TCP或者高吞吐转发,再考虑netconn。
UDP服务整体的初始化代码如下:
struct udp_pcb *udp_server_pcb; static void udp_echo_recv_callback(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port) { struct pbuf *resp_p; err_t err; if (p == NULL) { return; } xil_printf("UDP recv %d bytes from %d.%d.%d.%d:%d\r\n", p->len, (int)(ip4_addr1(addr)), (int)(ip4_addr2(addr)), (int)(ip4_addr3(addr)), (int)(ip4_addr4(addr)), port); /* 直接回传收到的PBUF。注意:udp_sendto会立即封装以太网帧并交给GEM发送, 该函数内部会复制数据,所以p本身不需要再分配新缓存。 */ err = udp_sendto(pcb, p, addr, port); if (err != ERR_OK) { xil_printf("udp_sendto error: %d\r\n", err); } pbuf_free(p); /* 注意释放PBUF,防止内存池耗尽 */ } void udp_echoserver_init(void) { udp_server_pcb = udp_new(); udp_bind(udp_server_pcb, IP_ADDR_ANY, UDP_SERVER_PORT); udp_recv(udp_server_pcb, udp_echo_recv_callback, NULL); }关于udp_sendto是否复制数据,我特意查过lwIP 2.1.1源码:如果UDP_SENDTO走的是udp_sendto_if到udp_output,在构造IP分片和以太网帧的过程中会按需分配PBUF再复制,尤其当使能了IP分片或校验和生成时。所以回调里pbuf_free(p)是对的,不会出现use-after-free。
4.4 数据收发方向的完整链路和PBUF生命周期
理清PBUF在两次收发中的生命周期,比背API还重要。
接收方向:GEM收到以太网帧 -> DMA写入DDR -> 硬件产生中断 ->XEmacPs_IntrHandler判断接收完成 -> 调xemacpsif_rx把DMA描述符链表里的缓冲区转成一个lwIP的PBUF ->tcpip_input()进入协议栈 -> 协议栈解析出UDP payload后调用注册的回调函数。在回调返回前,这PBUF一直有效,返回后协议栈会释放或复用。
发送方向:udp_sendto接收应用层传入的PBUF -> 协议栈加UDP头、IP头 -> 交给netif驱动层 ->xemacpsif_tx把PBUF数据物理地址填入GEM DMA描述符 -> 启动DMA发送 -> 发送完成后DMA中断处理里pbuf_free释放这个PBUF。
注意,发送完成后释放PBUF其实发生在中断上下文,所以在回调里udp_sendto后立刻pbuf_free(p)是正确配对,不会造成二次释放;而udp_sendto内部为发送额外分配的PBUF由驱动释放,不需要你操心。
4.5 源码里容易被忽略的线程优先级问题
虽然NO_SYS=1模式下只有主循环这一个“线程”,但lwIP内部有个tcpip_timeout机制,超时处理(ARP缓存老化、ARP请求重发等)是在tcpip_thread里做的。裸机下没有tcpip_thread,Xilinx适配层把超时处理放在了xemacpsif_configure或xadapter_platform的netif_poll()里。如果你在主循环里完全不做任何调用,ARP缓存可能会过期且不会刷新,影响长期通信稳定性。
在while(1)主循环里加一个sys_check_timeouts()之类的调用,或者按Xilinx例程的做法,放入带延时标志的逻辑:
while(1) { /* 轮询处理lwIP内部超时,比如ARP表老化 */ sys_check_timeouts(); Xil_DCacheFlush(); /* 如果在DMA=Cacheable模式下需要,NC模式可省略 */ mdelay(1); }实际上在Xilinx的适配层代码里,sys_check_timeouts就是xdeltaga/...中调用tcpip_timeout的函数,裸机下周期调用即可。加了这个之后,长时间运行UDP通道会稳定很多。
5. 上板实测与排错:link down、ping不通、UDP无响应的逐一排查
代码写完了,下载回来,各种现象怎么判读?我总结出一个排查链路,基本能覆盖80%的问题。
5.1 串口打印和PHY link状态
第一个看串口。正常情况会看到PHY link up这类信息。如果没有,先在XEmacPs驱动的sxemacps_phy.c里确认PHY地址对不对。可以用一根以太网线把板子直接连电脑,不用过交换机,减少变量。
常见PHY芯片型号和速查:
| PHY型号 | 常见MDIO地址 | 寄存器1默认值 |
|---|---|---|
| Marvell 88E1518 | 0x0 | 0x3D7 |
| Realtek RTL8211F | 0x1 | 0x3D7 |
| 板载TI DP83848 | 0x1 | 0x3000 |
如果你的板子addr和宏定义不符,改宏或者直接改xemacps_phy.c里的PhyAddr变量重编译。
5.2 PC端的ARP和PING
串口看不到link up?先查PHY供电、复位引脚。查PC网卡事件里“网络电缆被拔出”有没有消失。还不行就示波器打PHY的TX_CLK有没有125MHz。
串口显示link up了,PC端执行arp -d清空ARP缓存,再ping 192.168.1.10 -t。注意几点:
- PC网卡IP必须设置成192.168.1.x,如果是USB转网口的适配器,有些廉价适配器不能完全透传ARP,建议用板载网卡。
- ping通不一定代表UDP一定通,但ping不通时TCP/UDP基本不可能通。因为ping走的是ICMP,依赖和UDP相同的ARP解析和IP收发路径。
- ping能通但发UDP数据包无响应,重点检查UDP端口号是否和代码一致。我用网络调试助手测试时,第一次就犯过端口填错,折腾了半小时。
5.3 UDP数据能发不能收的常见原因
- 发送方向没问题、接收方向没数据:多半是PBUF池耗尽。回调里
pbuf_free没执行或者执行晚,导致PBUF_POOL_SIZE被占完。用xil_printf在回调入口加打印,看有没有进入回调。 - PC能收到板子回的UDP,但内容乱码或长度对不上:大概率Cache一致性问题。特别是改了DMA缓冲内存属性后,还有部分代码路径使用了带Cache的buffer。建议把IGID和
Xil_SetTlbAttributes的NC区域大小调大一点,把lwIP的DMA缓冲全部覆盖进去。 - 偶尔能通,频繁丢包:检查一下你的网线是不是千兆环境下的百兆自适应问题。有些PHY在自适应时如果PC强制千兆,而PHY协商上千兆但信号质量不好,就会出现高错包率。在PC网卡属性里强制百兆全双工,问题直接消失。
5.4 一个真实案例:flash blank check after erase unsuccessful
这句报错实际出现在用JTAG固化flash时,不是UDP本身的问题,但很多人会被它挡在上板调试前。出现这个提示,多半是flash型号不对或者时钟太快导致编程失败。在Vivado的Hardware Manager里,把Bitstream的配置频率降到15MHz再试,基本能解决。如果还不行,检查flash型号和板子是否匹配。
有时候玄学报错,重新给板上电把启动模式拨到JTAG,然后Power Cycle而不是复位,也能解决。
6. 性能调优:从“能跑”到“跑得快”
跑通了只是第一步。如果你后续要做高吞吐传输,有几个点值得调整。
6.1 GEM DMA描述符数量和缓冲区大小
xemacpsif_dma.c里的描述符数量默认256条。每条描述符默认缓冲长度是1600字节。对于100Mbps以下的负载,256×1600字节大约是400KB的DMA缓冲池。当PBUF_POOL_SIZE也较小时,接收路径在高压下会丢包。可以调大描述符数量到512,同时注意DDR空间。
6.2 是否开启D-Cache
前面为图省事把DMA区设成了非Cacheable。如果带宽要求高,可以把DMA缓冲恢复为Cacheable,并在发送前手动刷新Cache、接收后失效Cache。Xilinx的适配层在xemacpsif_dma.c的xemacpsif_flush_cache函数里已经做了部分工作,但前提是BSP配置里开了ENABLE_CACHE。
我实测过的数据:在ZYNQ-7020上,关闭Cache时UDP单向吞吐大约75Mbps(千兆口),开启D-Cache并正确flush/invalidate后能到接近线速的92Mbps。但如果flush漏了路径,吞吐可能不升反降,甚至数据错乱,务必做足验证。
6.3 中断合并(Interrupt Coalescing)
GEM支持中断合并,即在一段时间内累积多个包再触发一次中断,减少CPU进出中断开销。Xilinx的lwIP驱动里XEmacPs_SetInterruptCoalesce接口可以用,在低中断频率和高实时性之间找一个平衡值。对UDP批量接收场景,把TX和RX的coalesce时间窗配置为50~100us,整体CPU占用能降一半。
XEmacPs_SetInterruptCoalesce(&EmacPsInstance, 0x10, 0x10);这里的参数含义参见UG585,第二个参数是时间阈值,第三个是帧数阈值,任一达到即触发中断。不同ZYNQ型号寄存器布局略有差异,参考datasheet调整。
6.4 向TCP/DHCP/SNTP扩展
UDP跑通后,换到TCP其实是同一套骨架。把udp_new()换成tcp_new(),注册tcp_accept、tcp_recv、tcp_sent回调,再处理一下连接状态机就行。如果只是做配置下发,甚至可以直接用telnet。要在裸机上做可重编程配置下发,还可以顺手把lwIP的DHCP客户端打开,避免手动设IP。
7. 一点实战体会
在ZYNQ上跑UDP,说起来简单,实际上是把硬件配置、中断系统、DMA内存管理、协议栈配置、PHY驱动、网络调试这几座小山头连起来过一次。前面我提到的那些坑——PHY地址不对、Cache一致性、DMA描述符不够、中断触发方式配错——每一个都让我在那块板子上度过不少晚上。但也正因如此,跑通那一刻,串口打印出收到的UDP数据,电脑端的调试助手几乎同时弹出来自板子的回包,那种感觉确实是拿钱都不换的。
如果你刚上手就卡在某个奇怪现象上,我的建议是别急着怀疑代码逻辑,先退回到最小可运行状态重新走一遍:用官方模板、单板直连电脑、固定IP、UDP回环,把链路拆开一层层验证。硬件链路靠网口灯和PHY寄存器判断,协议链路靠ARP和PING判断,应用链路靠UDP回环判断。每一层都有独立的现象可以观测,按层定位永远比瞎试有效。
这套源码和调试方法,我在7020和7010两套板子上都用过,都能正常工作。你照着自己的板子改一下DDR配置、PHY地址和IP网段,大概率也能一次跑通。后面的扩展,无论是加TCP、开DHCP、做高速转发,还是把裸机换成FreeRTOS再叠lwIP,都是顺着这条路继续往前走而已。