资讯动态

FPGA MPSoC上基于lwIP裸机实现TFTP Server的详细指南

发布时间:2026/9/13 6:26:34 来源:尧图企业网站定制
简介面向FPGA嵌入式开发者提供一套在Xilinx Zynq UltraScale MPSoC系列XCZU2EG/XCZU2CG/XCZU4EV上基于lwIP协议栈实现TFTP服务器的完整Vitis工程实验。压缩包共2736个文件约43MB以C源文件582个与头文件1297个为主便于直接阅读和修改同时还包含目标文件、makefile编译脚本、Tcl/Vivado工程脚本、XSA硬件描述、XPR工程文件、链接脚本及ELF可执行文件等覆盖从硬件配置到软件编译的完整链路目前已有140人学习下载。工程展示了如何裁剪配置lwIP协议栈在MPSoC中实现TFTP文件上传/下载并将数据收发逻辑与FPGA可编程逻辑协同最终通过Vitis工具链完成软硬件部署。通过阅读源码、工程脚本与硬件描述文件可快速复现实验掌握在Zynq UltraScale平台上进行嵌入式网络通信、软硬件协同调试的关键方法。实验还涉及PS-PL数据交互、硬件加速及远程调试场景适合作为课程设计或工程实践的参考。1. 在 FPGA MPSoC 上从零起一个 TFTP Server比你想的更值得一个编译完的 bitstream 超过 200MB 的时候用 JTAG 灌一次 Flash 能吃掉整个午休时间。更实际的做法是把 Zynq UltraScale MPSoC 的 PS 端 GEM 网口先拉起来通过 TFTP 把镜像放到 DDR 里再交给 XilSecure 校验或手动烧写 QSPI。我这次用 XCZU2EG 做实验Vitis 2019.2 建裸机工程不切 Linux不上 Petalinux直接靠 standalone BSP 里的 lwip4 库实现了一个可读可写的 TFTP Server。这个过程很适合正在弄 FPGA 网络通信、想搞清楚 lwIP 裸机移植模型的人也适合刚接触 Vitis 替换 SDK 流程的工程师。跑通之后你会发现坑并不在协议本身而在 PCB、PHY 复位和 BSP 参数这些容易被忽略的细节上。2. lwIP 在 XCZU2EG 上裸机跑的底层模型2.1 先区分裸机 lwIP 和 Linux 网络栈是两回事lwIP 之所以被大量用在 FPGA 嵌入式项目里核心原因是它把 TCP/IP 协议栈做到几十 KB 内存也能跑。XCZU2EG 的 DDR 虽然远不止这么小但裸机环境下没有虚拟内存、没有进程调度lwIP 就必须切换到 NO_SYS 模型运行。在 Vitis 的 standalone BSP 中lwIP 库默认按无 RTOS 方式编译所有网络事件由中断和主循环轮询共同驱动不创建 tcpip_thread。这个模型直接决定了你的代码怎么写。Linux 下你可以放心开多线程一个线程阻塞在recvfrom另一个线程做文件系统操作但裸机 lwIP 里你只有一个主循环所有对网络事件的处理都在中断上下文或者sys_check_timeouts()里被触发。所以 TFTP Server 不能做成「等一个 ACK 再继续发送」的阻塞式流程而要做成状态机发送一个 DATA 块后立刻返回等下一次 ACK 中断进来再发下一块。我在设计时把 TFTP Session 拆成三个状态空闲、发送中、接收中。状态由udp_recv回调驱动主循环只负责喂 lwIP 的定时器和打印调试信息。这样既不需要引入 FreeRTOS也能把 512 字节一块的 TFTP 传输跑到网卡不丢包。2.2 在 Vitis 中生成带 lwip4 的 BSPVitis 2019.2 的工程结构是先有 platform再有 application。Platform 对应.xsa硬件描述文件里面包含 PS 配置、DDR、MIO 和外设地址。创建完 platform 后需要给 standalone 操作系统勾选 lwip4 库BSP 会自动生成liblwip4.a以及xemacpsif的网卡适配层。在 GUI 里操作是右击 platform → Board Support Package Settings → 左侧勾选 lwip4。如果要用命令行批处理XSCT 脚本可以写成这样bsp create -name bsp_lwip -proc psu_cortexa53_0 -os standalone bsp config lwip_lib lwip4 bsp config lwip_dhcp false bsp config lwip_ip_addr 192.168.1.10 bsp config lwip_ip_addr_mask 255.255.255.0 bsp config lwip_ip_addr_gw 192.168.1.1 bsp regenerate这里bsp config lwip_lib lwip4指定 lwIP 版本库比老 SDK 里手动添加源码路径要干净得多。lwip_dhcp false让板卡在启动时直接使用静态 IP避免 DHCP 超时浪费几秒钟。regenerate会重新编译 BSP生成liblwip4.a和其他外设驱动库。2.3 网卡驱动与 netif 注册lwIP 本身并不认识 Xilinx 的 PS GEM它需要通过 netif 适配层接入硬件。Xilinx BSP 里已经封装好了xemacpsif_init你只需要在 app 里调用它完成 netif 注册。一个最小初始化代码是这样#include lwip/init.h #include lwip/netif.h #include netif/xadapter.h static struct netif net_if; static struct netif *netif net_if; static ip_addr_t ipaddr, netmask, gw; void lwip_platform_init(void) { IP4_ADDR(ipaddr, 192, 168, 1, 10); IP4_ADDR(netmask, 255, 255, 255, 0); IP4_ADDR(gw, 192, 168, 1, 1); lwip_init(); netif_add(netif, ipaddr, netmask, gw, NULL, xemacpsif_init, ethernet_input); netif_set_default(netif); netif_set_up(netif); } int main(void) { lwip_platform_init(); tftp_server_init(); while (1) { xemacpsif_input(netif); sys_check_timeouts(); } return 0; }这段代码里netif_add的第一个 IP 参数是板卡地址掩码和网关必须和调试主机在同一网段。xemacpsif_init是 BSP 适配 PS GEM 的入口它内部会注册 DMA 中断、初始化描述符、启动接收通道。ethernet_input是 lwIP 的标准输入函数把网卡收到的struct pbuf交给协议栈处理。主循环里的xemacpsif_input(netif)负责把所有已经收到 RX 队列里的报文取出来送到 lwIP裸机模式下没有 tcpip_thread这一步必须反复执行否则收包只停在 DMA 中断里无法进协议栈。sys_check_timeouts()是触发 lwIP 内部定时器的地方TFTP 超时重传就要靠它。2.4 lwipopts 参数怎么调lwIP 的行为高度依赖lwipopts.h。Vitis BSP 里有一部分参数暴露成 GUI 配置项剩下要自己写在 app 的编译选项里。我的经验是第一版先不动高级项只用 BSP 默认参数。如果 TFTP 传大文件时丢包严重再调这三个。参数默认值推荐值作用MEM_SIZE稍小1MB 以上控制 pbuf 和协议栈内存池总大小PBUF_POOL_SIZE1632~64增大 DMA 缓冲数量抗突发流量MEMP_NUM_UDP_PCB88 以上UDP 控制块数量每个 TFTP 会话占用一个 UDP PCB这三个值如果设得太小TFTP 传到一半会突然回ERROR 2或者直接卡死看起来像协议问题实际是内存池耗尽。调大之后要在链接脚本里同步增加堆大小否则pbuf_alloc申请失败。3. TFTP 协议拆解与 lwIP RAW 回调实现3.1 TFTP 包格式和状态转移TFTP 基于 UDP 端口 69数据传输流程比 FTP 简单得多。协议总共 5 种 opcodeRRQ 读请求、WRQ 写请求、DATA 数据块、ACK 确认块、ERROR 错误块。发端和收端都依赖 block number 做序号管理每发一块必须收到对应 ACK 才能发下一块。Opcode名称方向包结构1RRQClient → Server文件名、mode2WRQClient → Server文件名、mode3DATAServer → Clientblock number、数据4ACKClient → Serverblock number5ERROR双向error code、错误消息TFTP 的 block number 从 1 开始计数每次数据包最多 512 字节。如果收到的数据包小于 512 字节接收方就知道这是最后一包传输结束。协议本身没有连接概念它的「连接」就是发送端记住对方的 IP 和端口。3.2 用 RAW UDP PCB 绑定 69 端口裸机 lwIP 里做 TFTP 服务端最直接的方式是创建一个 UDP PCB 并绑定 69 端口。这个端口由udp_bind绑定内核会保证所有目的端口为 69 的 UDP 包都会进入同一个回调。#include lwip/udp.h #include lwip/pbuf.h #define TFTP_PORT 69 #define TFTP_OP_RRQ 1 #define TFTP_OP_WRQ 2 #define TFTP_OP_DATA 3 #define TFTP_OP_ACK 4 #define TFTP_OP_ERROR 5 typedef enum { TFTP_STATE_IDLE 0, TFTP_STATE_SENDING, TFTP_STATE_RECEIVING } tftp_state_t; static struct udp_pcb *tftp_pcb; static tftp_state_t tftp_state TFTP_STATE_IDLE; static void tftp_recv_cb(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port) { if (p NULL) { return; } if (tftp_state TFTP_STATE_IDLE) { tftp_handle_initial_request(addr, port, p); } else { tftp_handle_ack_or_error(addr, port, p); } pbuf_free(p); } int tftp_server_init(void) { tftp_pcb udp_new(); if (tftp_pcb NULL) { return -1; } if (udp_bind(tftp_pcb, IP_ADDR_ANY, TFTP_PORT) ! ERR_OK) { return -2; } udp_recv(tftp_pcb, tftp_recv_cb, NULL); return 0; }tftp_pcb是全局 UDP 控制块创建后由 lwIP 分配内存。udp_bind的第二个参数IP_ADDR_ANY表示接收任意网卡上发往 69 端口的包。TFTP 的第一个请求必须发给 69 端口但服务器回包时不能继续使用 69 端口作为源端口而要从临时端口回否则客户端会认为包来自错误 TID 而丢弃。这也是为什么tftp_recv_cb需要保存addr和port。3.3 RRQ / WRQ 处理和 DATA / ACK 循环收到 RRQ 和 WRQ 后协议栈需要从struct pbuf中解析文件名和传输模式。RRQ 包格式是opcode(2字节) filename(ASCII \0) mode(ASCII \0)。如果收到 RRQ服务端要从文件系统中读取数据并逐块发送如果收到 WRQ服务端要等待 DATA 包并回 ACK。发送 DATA 块的代码可以这样写static void tftp_send_data_block(struct udp_pcb *peer_pcb, const ip_addr_t *addr, u16_t port, u16_t block, u8_t *data, u16_t len) { struct pbuf *p; u8_t *buf; p pbuf_alloc(PBUF_TRANSPORT, len 4, PBUF_RAM); if (p NULL) { return; } buf (u8_t *)p-payload; buf[0] 0; buf[1] TFTP_OP_DATA; buf[2] (u8_t)(block 8); buf[3] (u8_t)(block 0xFF); memcpy(buf[4], data, len); udp_sendto(peer_pcb, p, addr, port); pbuf_free(p); }这里的关键是block 8和block 0xFF组合成网络字节序的 block number。TFTP 没有 TCP 那样的可靠传输ACK 包如果超时未到服务端必须在超时后重发同一块。裸机环境下可以用sys_timeout注册重发回调或者在一个简单状态机里记录当前 block在sys_check_timeouts()里检查是否超过 500ms。处理 ACK 时同样需要校验对方 IP 和端口。TFTP 规范里要求 TID 必须匹配否则即使包里 block number 正确也要丢弃并回一个 ERROR 包错误码 5。3.4 错误处理和内存边界TFTP 容易出问题的不是正常传输而是异常中断。客户端传完一半断电或者用户按了 CtrlC服务端如果一直等在TFTP_STATE_SENDING下一次新的 RRQ 就进不来。我一般会在状态机里加一个超时时间戳每次收到合法包就刷新超过 3 秒没有新包强制回到TFTP_STATE_IDLE并关闭打开的文件。struct pbuf的内存管理也必须小心翼翼。tftp_recv_cb收到的p如果不在回调里释放会很快吃光PBUF_POOL_SIZE。但如果你把p里的数据拷贝到自己的 buffer 后再释放就没有悬空指针风险。我倾向于在回调里把报文内容全部解析到tftp_session结构体再立即pbuf_free(p)这样主循环里完全不需要接触 pbuf。4. Vitis 集成硬件配置、BSP 参数与链接脚本4.1 从 Vivado 导出 xsa 时怎么配 PS GEMXCZU2EG 内部集成多路千兆以太网 MAC叫 GEM。裸机 lwIP 直接使用 PS 端 GEM 的效率最高因为不占用 PL 逻辑也不需要 AXI Ethernet IP。Vivado block design 里添加 Zynq UltraScale MPSoC IP 后在 PS-PL 配置中把 GEM0 使能接口类型选 RGMIIMIO 位置按板卡原理图分配同时使能 MDIO。这里最容易被忽略的是 MDIO 接口和 PHY 地址。很多开发板 PHY 的地址是0x01或者0x0E但 Vitis 生成的 xparameters.h 里默认值不一定对。如果netif_set_up之后 ping 不通先看 lwIP 打印里的PHY address再到 Vivado 里核对。配置完成后 Generate Output Products再 Generate Bitstream。如果完全不使用 PL不影响 TFTP 功能但硬件导出时依然要勾选Export hardware with bitstream因为 Vitis 的 platform 会把 bit 文件打包进去方便后续单步调试。4.2 链接脚本和 lwIP 内存参数裸机工程的默认链接脚本lscript.ld中堆大小通常只有 1MB 左右。TFTP 使用 UDP 传输不像 TCP 有窗口缓冲但接收大文件时PBUF_POOL_SIZE和 pbuf 数据区域仍然会占用不少堆空间。我习惯把堆调到 1MB 以上栈保持 0x4000 足够。lscript.ld中相关段_STACK_SIZE 0x4000; _HEAP_SIZE 0x100000;这里的_HEAP_SIZE决定malloc可用的内存总量。lwIP 的MEM_SIZE、pbuf pool 以及文件读写 buffer 都会从堆里分配。如果pbuf_alloc(PBUF_RAM, ...)返回 NULL优先看这个值。_STACK_SIZE给到 16KB 主要是为了在 UDP 回调里做一些字符串解析避免局部变量过大导致栈溢出。在 BSP 配置里lwIP 参数也可以直接改BSP 配置项推荐值含义lwip_dhcpfalse关闭 DHCP静态 IPlwip_ip_addr192.168.1.10板卡 IPlwip_ip_addr_mask255.255.255.0子网掩码lwip_ip_addr_gw192.168.1.1网关lwip_mem_size1MB 或更高整体内存池这些配置会在 BSP 重新生成时写入 lwIP 的配置头文件。改完必须bsp regenerateapplication 全部重新编译否则参数不生效。4.3 启动顺序与 Debug 下载流程Vitis 里运行裸机 lwIP 程序需要把 FPGA bitstream 和 ELF 同时加载到目标板。Debug Configuration 中勾选Program FPGA然后在 Application 里指定 ELF 路径。如果直接点 Debug 但不勾选 FPGAPS 端可能处于未知状态GEM 时钟没有初始化网络自然不通。加载完成后串口终端会看到 lwIP 打印 MAC 地址和 IP。此时用ping -c 3 192.168.1.10验证物理链路。能 ping 通再测 TFTP不能 ping 通就回到 PHY 地址和 MDIO 的问题上排查。不要直接在 TFTP 客户端里等超时那样很难定位是协议问题还是链路问题。5. 验证命令和 blksize 优化5.1 先跑通最小读写Linux 主机上可以直接用 tftp-hpa 客户端验证tftp 192.168.1.10 -m binary -c get hello.bin tftp 192.168.1.10 -m binary -c put local.bin-m binary指定二进制模式否则换行符会在明文模式下被改写。服务端回包时只要 block number 对上客户端没有报错就说明 TFTP 解析、文件读写和 UDP 收发链路已经成立。Windows 下可以放一台 TFTPD32但路径分隔符和选项协商行为跟 Linux 工具不完全一致跨平台调试时最好用 Wireshark 抓包对比。5.2 blksize 不是越大越好默认 TFTP 每块 512 字节Mbps 级别实测大约只有 1~2MB/s。标准 TFTP 扩展允许客户端在 RRQ 里加blksize选项。在 lwIP 里实现这个扩展要在tftp_handle_initial_request中解析\0blksize\0value\0。实测时当 blksize 超过 1468 字节会把 UDP 包推入 IP 分片而 lwIP 的分片重组在裸机下需要额外缓冲搞不好性能反而下降。我在 XCZU2EG 上试过 1468 和 1024 两档。1468 刚好塞满以太网 MTU 1500但 GEM 的 RX 描述符必须准备足够大的缓冲否则会丢包。1024 是安全值既不触发 IP 分片也能稳定跑到 3MB/s 左右。所以我在实测中把 blksize 设为 1024让 TFTP 块处理和 GEM DMA 描述符大小对齐整体不掉速也不触发 IP 分片这个值比盲选 1468 更稳。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价