恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
ZYNQ 7020 lwIP裸机TCP服务器移植与实现要点
首页
资讯中心
/
ZYNQ 7020 lwIP裸机TCP服务器移植与实现要点
ZYNQ 7020 lwIP裸机TCP服务器移植与实现要点
发布时间:2026/9/12 11:59:49
简介ZYNQ 7020 lwIP实现TCP Server驱动的SDK工程包面向嵌入式开发者和FPGA学习者解决在Xilinx ZYNQ-7020 SoC平台上使用SDK与lwIP协议栈构建稳定TCP服务端驱动的实际需求。ZYNQ 7020集成ARM Cortex-A9双核处理器该工程正适合软硬件协同设计的网络通信场景。包内共1042个文件以419个.h头文件和224个.c源码文件为主体涵盖驱动源码、外设初始化、lwIP协议栈集成及网络应用逻辑另含92个.o目标文件、HDF硬件定义文件、makefile构建脚本、TCL工程脚本、xpr工程文件及README等可导入Xilinx SDK直接查看或重新编译目录结构也便于定位驱动代码与构建脚本。压缩包整体仅11.74MB便于传输保存目前已有486人学习下载。工程覆盖从硬件平台导入、驱动初始化、网络接口配置到监听端口并处理客户端请求的完整链路代码经测试可直接编译运行并涉及中断服务例程、同步机制与错误处理等关键环节。对于希望复用ZYNQ网络外设驱动、快速搭建TCP通信原型的开发者这是份具参考价值的工程范例也可作为学习lwIP移植与ZYNQ嵌入式网络开发的良好起点。1. 没跑通过 ZYNQ 的 TCP 服务器别急着怀疑板子做 ZYNQ 7020 嵌入式网络开发的人十有八九是在把 PS 侧跑起来、想用网口跟 PC 通信时卡在 lwIP 上。这个标题里的 tcp_server 看起来是个“例程”实际上它同时踩中了三个深坑ZYNQ 的 PS 网口默认根本不在地址空间中、lwIP 裸机移植模型在 SDK 里要自己勾选库、以及 TCP 服务器并非串口那样开了就能用——你得把 DMA 描述符、中断和内存池都配到同一个节拍上。本文会先讲清楚 ZYNQ 7020 上 lwIP 裸机移植的硬件基础然后沿着 Vivado 工程到 SDK 代码的完整链路给出一个能直接上板验证的 TCP 服务器实现路径重点落在三处PS 端 GEM 控制器的地址映射、lwIP 在 standalone BSP 中的使能方式、以及服务器循环里每个 API 的阻塞语义。面向想用 PS lwIP 做设备联网、数据采集或远程控制的嵌入式工程师也包括那些在网络协议栈边缘试探、被connect失败折磨的 FPGA 开发者。2. lwIP 裸机移植模型为什么选它而不是 Linux 网络栈2.1 在 ZYNQ 7020 上处理 TCP/IP第一反应不该是 LinuxZYNQ 7020 的 PS 侧是双核 Cortex-A9片上内存和 DDR 资源都有限。跑 Linux 加网络栈确实强大但启动时间、镜像体积、驱动维护成本对很多设备端场景都是负担。SDK 裸机环境下lwIP 的优势在于它是 event-driven 的事件驱动协议栈配合 standalone BSP 里自带的链路层驱动能做到很小的内存占用就完成 TCP 通信。注意lwIP 本身只负责 IP / TCP / UDP 层的协议处理网卡的底层驱动需要由 BSP 提供。SDK 在创建 BSP 时勾选了 lwIP 库之后会自动把xemacpsif_dma.c这套基于 AXI DMA 的驱动包含进来。裸机模型的核心是它不用操作系统来调度而是用一个tcpip_thread类似的主循环配合中断完成收发包所以开发者要面对的编程模型是回调 轮询而不是阻塞等待。2.2 地址映射PS 网口不是你想开就能开的ZYNQ 7020 的千兆以太网控制器GEM是挂在 PS 内部的高性能端口上的不是 PL 里的 AXI 外设。GEM0 和 GEM1 的寄存器基地址分别位于0xE000B000和0xE000C000。但 SDK 的 lwIP 驱动不会自动探测这些地址它依赖的是 BSP 生成的xparameters.h里的参数。很多时候 TCP 服务器没反应不是代码逻辑错而是你建的 Vivado 工程里根本没有使能 GEM 外设或者没有正确连接 MIO 引脚到外部 PHY。常见做法是在 IP integrator 里双击ZYNQ7 Processing System在 Peripheral I/O Pins 里勾选ENET 0或 ENET 1再把 MIO 配置成 GMII 或 RGMII 模式。如果这个配置漏了xparameters.h里就不会有XPAR_XEMACPS_0_IS_GEM相关的宏SDK 那边编译虽然能过但底层驱动拿不到有效基地址运行时直接挂死。/* xparameters.h 中与 GEM 相关的宏SDK 自动生成 */ #define XPAR_XEMACPS_0_IS_GEM 1 #define XPAR_XEMACPS_0_GEM_BASEADDR 0xE000B000 #define XPAR_XEMACPS_0_MIO_OFFSET 54 #define XPAR_XEMACPS_0_PHY_TYPE 1GEM 基地址不是绕不开的话题。裸机移植模型里lwIP 的初始化流程会调用xemacpsif_init这个函数会读取上述宏来配置 DMA、MAC 地址和 MDIO 接口。你在 SDK 里看到netif_add失败八成是baseaddr这个参数拿到的是 0也就是 BSP 没有感知到这个外设。解决办法不是去改代码而是回头把 Vivado 工程改对重新生成硬件描述文件。2.3 lwIP 的内存池配置不对TCP 收发直接就丢PS 侧裸机环境是没有 MMU 帮你做地址转换的lwIP 使用的内存池是固定的静态数组。lwipopts.h里有一组关键配置默认值对于 ZYNQ 7020 来说往往不够。以 C 代码的方式做 TCP 接收数据到达时会触发中断驱动程序将数据放入 PBUF 结构里然后通知协议栈处理。如果MEM_SIZE定义太小或者PBUF_POOL_SIZE太小唯一的征兆就是 TCP 连接建立成功但数据传一会儿就断了。SDK 的 lwIP 库默认给PBUF_POOL_SIZE是 4 到 8 个这在千兆网卡的突发流量下几乎必挂。一般我会调到 32 以上同时把TCP_MSS设为 1460确保 TCP 分段能直接放满一个以太网 MTU。在lwipopts.h里除了池大小之外还有几个和裸机中断强相关的宏。比如NO_SYS必须为 1因为你没有 RTOS不能用信号量那套SYS_LIGHTWEIGHT_PROT必须为 0否则会在临界区处理上出问题。这些参数决定了 lwIP 是运行在“raw API”模式还是“sequential API”模式。ZYNQ 7020 SDK 默认支持的是 raw API即你不用创建线程而是注册回调函数在中断上下文里完成协议栈的调用。很多从 Linux 网络编程转过来的开发者会在这里迷路习惯性地想用 socket 那套阻塞写法实际上在裸机上你要处理的是tcp_accept、tcp_recv这几个回调函数。在开始写服务器逻辑之前先把 GEM 和 lwIP 的关系厘清看起来是 SDK 在帮你做事情实际只是提供了一套封装好的适配层底层仍然是你在控制硬件中断。3. Vivado 工程搭建和网络外设配置一个按键失误导致整个网口进不了系统3.1 使用 IP integrator 配置 ZYNQ PS使能千兆以太网控制器打开 Vivado创建 RTL 工程后第一步是在 IP integrator 里添加ZYNQ7 Processing System。这个过程没有任何神秘感双击这个 IP进入配置界面后最重要的操作发生在Peripheral I/O Pins选项卡中。要保证以太网能用你需要找到ENET 0若板载 PHY 连接到 GEM0或ENET 1勾选它。同时在MIO Configuration里观察ENET 0的 MIO 引脚号会根据你的板子走线自动分配比如常见的是 MIO 16 到 MIO 27 用于 RGMII 接口的信号线包括RXD[3:0]、TXD[3:0]、RX_CTL、TX_CTL、RXC、TXC。一切确认完毕再检查时钟配置。ZYNQ 7020 的 PS 侧 CPU_6x4x 时钟域里需要确保ENET的时钟源被定义为IO PLL或ARM PLL频率会根据 PHY 的工作模式决定RGMII 需要 125MHzGMII 需要 2.5MHz 到 25MHz。# Vivado Tcl 配置示例使能 GEM0并强制设定 MIO 到 RGMII 模式 set_property -dict [list \ CONFIG.PCW_ENET0_EN {1} \ CONFIG.PCW_ENET0_PERIPHERAL_EN {1} \ CONFIG.PCW_ENET0_GRP_MDIO_EN {1} \ CONFIG.PCW_ENET0_GRP_MDIO_IO {MIO 52 53} \ CONFIG.PCW_ENET0_PERIPHERAL_IO {MIO 16 27} \ CONFIG.PCW_ENET0_ENET0_IO {MIO 16 27} \ CONFIG.PCW_ENET0_PERIPHERAL_CLKSRC {IO PLL} \ CONFIG.PCW_ENET0_PERIPHERAL_DIV {0x8} \ ] [get_bd_cells processing_system7_0]每次创建新工程时用 IP integrator 自带的预设比如ZedBoard、ZC702这些模板可以省掉大半的引脚配置工作。但如果是自己画的板子上面这段 Tcl 设置得手动处理尤其是PCW_ENET0_ENET0_IO这个选项直接定义了 GEM0 的引脚走向。很多人直接在图形界面上勾选却忘了点OK确认导致最后生成的比特流里根本没有以太网的引脚约束SDK 里xparameters.h也自然缺失对应宏。3.2 DDR 配置与地址映射lwIP 的内存大头ZYNQ 7020 的裸机工程虽小但 lwIP 的缓冲池、描述符以及代码本身都需要放进 DDR3 或 DDR2 里运行。Vivado 里的 DDR 配置是在DDR Configuration选项卡里完成的你需要根据板载内存颗粒的型号选择Memory Part比如 MT41K256M16 HA-125 是常见的 DDR3 颗粒。如果这部分配置不对SDK 下载程序后lwIP 初始化时访问内存会宕机因为控制器拿到的地址是错的。DDR 总容量和地址映射直接决定了lscript.ld里各段的基址。在裸机工程里通常ps7_ddr_0的基址是0x00100000如果不小心把 lwIP 的堆放在0x00000000这个地址段即 OCM 里那空间完全不够用。/* lscript.ld 中内存区域划分示例 */ MEMORY { ps7_ddr_0 : ORIGIN 0x00100000, LENGTH 0x1FF00000 ps7_ram_0 : ORIGIN 0x00000000, LENGTH 0x00030000 ps7_ram_1 : ORIGIN 0xFFFF0000, LENGTH 0x0000FE00 }建议把 lwIP 的堆放在 DDR 里而非 OCM 的ps7_ram_0里。原因很直接OCM 只有 256KB而 lwIP 在做大数据收发时PBUF 池可以被设计到 100KB 以上。DDR 配置完毕后在 SDK 中打开lscript.ld确认.heap和.stack被分配到ps7_ddr_0段中这样 lwIP 动态分配的内存才不至于越界。3.3 UART 和中断调试 tcp_server 的两条命脉无论你用哪个 PHY 芯片ZYNQ 的 GEM 中断默认连接到 GIC通用中断控制器的 54 号中断对应 GEM0。没有中断网卡驱动根本无法知道数据帧到达。在 Vivado 里确认 ZYNQ PS 的 IRQ 输出正确连接到pl_ps_irq0或者irq_0这是给 PL 的中断输入要确保它被引出。注意SDK 的 BSP 在生成 lwIP 库时会自动处理中断连接它通过XScuGic驱动把 GEM 的中断注册到 CPU。UART 则是另一条命脉。在裸机 tcp_server 调试过程中你能看到的唯一输出通道就是串口通常是 MIO 14 和 MIO 15。lwip_err、netif_set_up这些函数的返回值如果不用xil_printf打印出来你会完全在黑盒子里摸索。在Peripheral I/O Pins里勾选UART1并且把PCW_UART1_BAUD_RATE设置为 115200硬件配置部分才算完整。在实际项目中我习惯先把 UART 调通、打印一个简单的 “hello” 再继续。这个动作能排除 Vivado 阶段 80% 的问题——有时候硬件没问题只是 MicroBlaze 或 PS 时钟配置错误串口就有输出。把 UART 和 ENET 都使能后生成比特流导出硬件到 SDK进入软件设计。4. SDK 驱动封装与 lwIP 应用代码从 BSP 配置到 TCP 服务器跑通4.1 在 BSP 设置中勾选 lwIP 库并调整以下三个关键参数打开 SDK新建一个 Application Project在 Board Support Package 界面里点击Manage BSP或直接在选择列表里勾选lwip和xilnet相关库。BSP 生成后在lwip141或lwip211版本号取决于你的 SDK 版本这个库的配置里检查lwipopts.h是否被自动包含。关键参数之一是NO_SYS默认是 1代表裸机模式这个值对了传输才能正常如果 SDK 版本不同出现了NO_SYS为 0 的情况你需要手动把它改回 1。另一个核心参数是内存池大小。打开lwipopts.h你会看到类似下面的宏定义。建议把MEM_SIZE设置为(1024 * 1500)也就是 1.5MB 左右虽然这个值听起来很大但 ZYNQ 的 DDR 完全承受得住。同时PBUF_POOL_SIZE增加到 32否则在 TCP 窗口较大的时候接收方向的内存会被耗尽。/* lwipopts.h 关键配置片段 */ #define NO_SYS 1 #define MEM_STATIC 1 #define MEM_SIZE (1024 * 1024 * 2) #define MEMP_NUM_PBUF 32 #define PBUF_POOL_SIZE 32 #define TCP_MSS 1460 #define TCP_WND (4 * TCP_MSS) #define TCP_SND_BUF (4 * TCP_MSS) #define SYS_LIGHTWEIGHT_PROT 0如果打算跑高吞吐量把这个TCP_SND_BUF再往上调整到 8 倍 MSS。但要注意发送缓冲区越大重传时的内存占用越高这对裸机环境下的稳定性是考验。我一般用TCP_SND_BUF (8 * TCP_MSS)配合 TCP_WND 4 倍 MSS可以在不拖垮 CPU 的情况下跑到近百兆吞吐。4.2 从 lwIP 的 echo 服务器示例出发改造出自己的 TCP 服务器结构SDK 自带的 lwIP 示例库中有一个echo_server或tcp_perf_server直接从那里开始改是最快的方法。它已经帮你做好了platform_init、netif_add和tcpip_thread这些底层初始化。你需要做的是在echo_server的基础上把回调函数的逻辑改写成你自己的协议处理。核心结构通常是这样的先调用tcp_new()创建一个 TCP PCB协议控制块然后调用tcp_bind()将它绑定到端口 5001 之类的端口上。接着tcp_listen()把这个 PCB 变成监听模式最后注册tcp_accept回调。当有客户端连接时lwIP 调用这个回调在回调内部再注册tcp_recv回调来接收数据。如果你见过 Linux 网络编程的accept和recv函数这里的 raw API 本质上是把它们拆散到回调函数里了。/* 基于 lwIP raw API 的 TCP 服务器初始化 */ static struct tcp_pcb *server_pcb; void tcp_server_init(void) { err_t err; server_pcb tcp_new(); // 分配 TCP 控制块 if (server_pcb NULL) { xil_printf(tcp_new failed\r\n); return; } err tcp_bind(server_pcb, IP_ADDR_ANY, 5001); // 绑定所有本地地址和端口 if (err ! ERR_OK) { xil_printf(tcp_bind failed: %d\r\n, err); return; } server_pcb tcp_listen(server_pcb); // 进入监听状态 tcp_accept(server_pcb, tcp_server_accept); // 注册 accept 回调 xil_printf(TCP server listening on port 5001\r\n); }代码里tcp_bind返回的错误最常见是ERR_USE意思是端口被占用。排查方法是检查是否在同一个 BSP 里跑起了两个服务器实例或者是上一次运行时没有正确关闭 PCB导致状态没被清理。由于裸机环境没有进程退出概念重复调用tcp_server_init可能会造成控制块泄漏需要先释放旧的 PCB。4.3 数据接收与发送理解tcp_recv回调在中断上下文里的坑TCP 连接建立后数据到达会触发 lwIP 内部的tcp_recv回调函数。这个回调是在 lwIP 的主循环中被调用的但它可能运行在中断保护的上下文里。在这个回调里你要立刻处理掉pbuf中的数据然后调用tcp_recved()函数告知协议栈数据已经被应用层消费否则 TCP 的接收窗口会被填满客户端发送会停止。这是新手最容易忽略的关键点。/* TCP 接收回调 */ err_t tcp_server_recv(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err) { if (p NULL) { /* 对端关闭连接释放 PCB */ tcp_close(pcb); return ERR_OK; } /* 这里处理 p-payload 中的数据 */ tcp_recved(pcb, p-len); // 告知协议栈数据已取走 tcp_write(pcb, p-payload, p-len, 1); // 回显数据 pbuf_free(p); // 释放 pbuf 缓冲 return ERR_OK; }注意tcp_write并不保证数据立即发送它只是把数据拷贝到发送缓冲队列中真正发送动作发生在这个回调返回之后。如果回调里调用tcp_write的返回值为ERR_MEM说明发送缓冲区不够你需要等待tcp_sent回调触发后再继续写入。处理大文件传输时不能只在tcp_recv里塞数据而要在tcp_sent回调里实现流量控制否则用裸机 lwIP 传大文件的体验会非常差。pbuf_free的调用时机也比想象的更微妙tcp_recv给你的这个p在使用完毕之后必须释放。如果只调用了tcp_recved而忘了释放缓冲只需要几次收发内存就泄漏了。我的习惯是先用pbuf_copy_partial把数据提取到自己的应用层 buffer然后再统一释放这样可以避免在回调里处理耗时的业务逻辑。4.4 设置静态 IP 和 MAC 地址避免产生环回干扰和冲突默认的 SDK 示例代码会从xparameters.h里读取 MAC 地址高位但如果你不显式设置MAC 地址可能全是 0。连接到路由器或交换机时全 0 的 MAC 地址可能导致报文被丢弃。使用netif-hwaddr[0]显式赋值是比较可靠的做法。同时 IP 地址默认示例是 192.168.1.10如果你的 PC 不在这个网段TCP 客户端根本找不到服务器。/* netif 的 IP 和 MAC 设置片段 */ #define SERVER_IP_ADDR 192.168.1.10 #define SERVER_NETMASK 255.255.255.0 #define SERVER_GW 192.168.1.1 struct netif server_netif; ip4_addr_t ipaddr, netmask, gw; IP4_ADDR(ipaddr, 192, 168, 1, 10); IP4_ADDR(netmask, 255, 255, 255, 0); IP4_ADDR(gw, 192, 168, 1, 1); netif_add(server_netif, ipaddr, netmask, gw, NULL, xemacpsif_init, tcpip_input); netif_set_default(server_netif); netif_set_up(server_netif);程序启动后串口打印的 IP 地址要跟实际网卡设置一致才能通。如果在调试时发现能 ping 通但 TCP 连接不上请检查防火墙或路由器 AP 隔离这不是 ZYNQ 的问题。5. 验证与踩坑QEMU 仿真、SecureCRT 监听和吞吐监控三板斧5.1 用 QEMU 在没有网线的地方验证 TCP 连通性ZYNQ SDK 内置的 QEMU 可以模拟 ARM Cortex-A9 处理器和外设其中网络部分是通过 tap 设备连接到宿主机上的。使用 QEMU 验证 lwIP 的时间成本很低不需要上板不用插网线。在 SDK 的Run配置里选择Single Application Debug然后在Target Setup里勾选Use QEMU。注意 QEMU 模式下的 GEM 驱动和实际板卡略有差异它使用的虚拟 PHY 是xemacps仿真模型所以 MAC 时序和实际 PHY 不同。但对于测试 TCP connect 和简单回显逻辑QEMU 已经够用。qemu-system-arm -machine xilinx-zynq-a9 -m 1G \ -serial mon:stdio -net nic -net tap,ifnametap0,scriptno,downscriptno \ -kernel tcp_server.elfQEMU 启动后在宿主机上直接用 netcat 连接即可验证 TCP 端口是否工作正常。nc -vz 192.168.1.10 50015.2 三个必查参数lwIP: 0x1、TCP_PCB状态和pbuf泄漏在串口终端连接上开发板之后用xil_printf打印 lwIP 协议栈内部的统计信息是最直接的验证手段。常见的做法是周期性地打印tcpActivePCBs和tcp_tw_pcbs的数量来确认连接没有被残留 TIME-WAIT 状态的 PCB 占满。ZYNQ 7020 裸机上没有操作系统帮你回收 TCP 连接资源如果客户端频繁断开重连经过几天运行就会发现tcp_new失败。解决方案是把MEMP_NUM_TCP_PCB从默认的4调整为16或更高。QEMU 里跑通不算完真正的边缘用例在硬件上比如当 PHY 芯片的 link 状态变化时拔网线再插回lwIP 驱动需要在netif里处理链路中断。GEM 驱动会周期性地读 PHY 寄存器来感知链路状态如果你的代码里没有写xemacps_phy检测逻辑则会出现插拔网线后 TCP 连接虽然存在但完全无法收发数的情况。5.3 用 ethtool 和 tcpdump 双端验证吞吐与重传前面几步都通过后如果想要确认服务器达到了可用性能可以用 PC 端的tcpdump和ethtool做一个快速的双端验证。先确认板子的网口速率实为千兆而不是降级到了百兆。在 PC 上执行sudo ethtool eth0输出里的Speed: 1000Mb/s与实际 PHY 配置一致才算正常。此时用iperf测试吞吐是一个业内通用做法但 SDK 示例不一定自带 iperf 服务器。你可以写一个简单的 TCP 服务器循环接收数据并丢弃在 PC 上运行客户端即可得到初步吞吐。# PC 端 Python 脚本持续向 ZYNQ 发送 1KB 数据包 import socket s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((192.168.1.10, 5001)) payload bx * 1024 for i in range(100000): s.send(payload) s.close()然后在 PC 上的抓包结果里查看是否有大量 TCP Retransmission。若存在大量重传说明接收方的内存池或者 DMA 描述符数量不足报文的ps7_netif处理赶不上入口速率。把TX和RX的描述符数量由默认的16或32提高一倍是成本最低的解决方式。注意描述符数量的调整在两个位置一是xemacpsif_dma.c里RX BD和TX BD的数组大小二是xemacps_hw.h中的XEMACPS_RX_BD_CNT宏两处必须同时改否则驱动访问越界。关于tcp_poll定时器的使用也值得多写一笔。lwIP 的 raw API 中如果长时间不调用tcp_recv或tcp_write某些 PCB 可能处于半开状态而不被感知。在服务器中注册一个tcp_poll回调周期最长为 5 秒用于检测 PCB 是否已失效是防止裸机长时间运行连接表耗尽的好习惯。本文还有配套的精品资源点击获取