news 2026/9/13 6:26:30

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FPGA MPSoC上基于lwIP裸机实现TFTP Server的详细指南

简介:面向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 的 BSP

Vitis 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 种 opcode:RRQ 读请求、WRQ 写请求、DATA 数据块、ACK 确认块、ERROR 错误块。发端和收端都依赖 block number 做序号管理,每发一块,必须收到对应 ACK 才能发下一块。

Opcode名称方向包结构
1RRQClient → Server文件名、mode
2WRQClient → Server文件名、mode
3DATAServer → Clientblock number、数据
4ACKClient → Serverblock number
5ERROR双向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需要保存addrport

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 >> 8block & 0xFF组合成网络字节序的 block number。TFTP 没有 TCP 那样的可靠传输,ACK 包如果超时未到,服务端必须在超时后重发同一块。裸机环境下可以用sys_timeout注册重发回调,或者在一个简单状态机里记录当前 block,在sys_check_timeouts()里检查是否超过 500ms。

处理 ACK 时同样需要校验对方 IP 和端口。TFTP 规范里要求 TID 必须匹配,否则即使包里 block number 正确也要丢弃,并回一个 ERROR 包,错误码 5。

3.4 错误处理和内存边界

TFTP 容易出问题的不是正常传输,而是异常中断。客户端传完一半断电,或者用户按了 Ctrl+C,服务端如果一直等在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 GEM

XCZU2EG 内部集成多路千兆以太网 MAC,叫 GEM。裸机 lwIP 直接使用 PS 端 GEM 的效率最高,因为不占用 PL 逻辑,也不需要 AXI Ethernet IP。Vivado block design 里添加 Zynq UltraScale+ MPSoC IP 后,在 PS-PL 配置中把 GEM0 使能,接口类型选 RGMII,MIO 位置按板卡原理图分配,同时使能 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,静态 IP
lwip_ip_addr192.168.1.10板卡 IP
lwip_ip_addr_mask255.255.255.0子网掩码
lwip_ip_addr_gw192.168.1.1网关
lwip_mem_size1MB 或更高整体内存池

这些配置会在 BSP 重新生成时写入 lwIP 的配置头文件。改完必须bsp regenerate,application 全部重新编译,否则参数不生效。

4.3 启动顺序与 Debug 下载流程

Vitis 里运行裸机 lwIP 程序,需要把 FPGA bitstream 和 ELF 同时加载到目标板。Debug Configuration 中勾选Program FPGA,然后在 Application 里指定 ELF 路径。如果直接点 Debug 但不勾选 FPGA,PS 端可能处于未知状态,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 更稳。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 6:26:00

SQL Server CDC完整实操:从开启到维护避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 6:24:17

继续教育AI写作工具测评与学术论文效率提升指南

1. 继续教育场景下的AI写作需求解析 在继续教育领域,论文写作是每个学习者必须跨越的门槛。无论是职称评定、学历提升还是专业认证,学术论文的质量往往直接关系到最终成果的认可度。但现实情况是,大多数继续教育学员都面临着工作与学习的时间…

作者头像 李华
网站建设 2026/9/13 6:22:54

软件端与PLC通信协议及优化实践详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 6:22:41

51单片机四路模拟量报警器设计:基于ADC0832的数据采集与阈值控制

简介:面向污水处理厂气体检测的电子鼻系统硬件设计方案,以STC89C51单片机为核心,搭配ADC0832扩展4路模拟量输入,完成硫化氢、氨气、甲烷、一氧化碳浓度采集与超限报警。适合单片机课程设计、电子竞赛或工程实训,帮助掌…

作者头像 李华
网站建设 2026/9/13 6:22:40

二叉树路径查找算法与实现详解

1. 二叉树路径问题概述在计算机科学中,二叉树是一种基础且重要的数据结构,它由节点组成,每个节点最多有两个子节点,分别称为左子节点和右子节点。二叉树路径问题是指从根节点到某个叶子节点的所有节点序列,这类问题在算…

作者头像 李华