ZYNQ-7010裸机TFTP服务器:基于lwIP与SDK库实现在线升级
2026/9/13 15:55:41 网站建设 项目流程

简介:一套基于赛灵思ZYNQ-7010平台与SDK开发的LWIP TFTP服务器驱动工程资源包,面向嵌入式网络开发者,用于在ZYNQ上快速搭建轻量级文件传输服务,解决固件更新、配置下发等常见需求。资源共1080个文件,以H头文件与C源代码为主,支撑驱动和协议栈实现;makefile、TCL等工程脚本与约束文件辅助构建和硬件配置;SDK项目文件便于直接导入,压缩包约11.93MB。已有210人学习该资源,可作为ZYNQ网络驱动开发的有效参考。包内包含完整的SDK工程与LWIP协议栈配置,涵盖TFTP服务器驱动源码、预编译库以及硬件设计相关文件,可导入SDK后直接编译运行;实现过程注重线程安全与错误处理,并给出多客户端请求的同步示例,适合需要深入理解嵌入式协议栈集成、驱动开发流程和调试方法的开发者学习借鉴。

1. 为什么在 ZYNQ-7010 上把 TFTP 服务器做成 SDK 库而不是应用

拿到 ZYNQ-7010 板子之后,很多人的第一个验证动作不是测网速,而是能不能通过 TFTP 把镜像拉进 DDR 直接跑,省掉反复插拔 SD 卡。这个 zip 里没有一整套应用层 tftp 源码,而是libxil.aliblwip4.alibxilffs.a三个预编译库,外加ps7_init.csystem.bdMakefile.adapter。看到liblwip4.a就应该明白,它已经把 lwip 协议栈按裸机方式编进了驱动库,libxilffs.a则让文件读写直接走 FATFS 接口。也就是说,驱动库把“以太网链路”和“文件系统”这两块最容易出问题的基础设施先定死了,剩下要写的只是 TFTP 收发状态机。这套方案适合做在线升级、配置文件下发、bootloader 网络加载的工程师,而不是只想跑一个 hello world 的人。

2. 先拆 libxil.a、liblwip4.a 与 ps7_init.c:这套 SDK 驱动库怎么配合

2.1 库和文件的职责边界

解压后看到一堆名字很抽象的文件,先别急着丢进工程。207d...a062...是两个生成目录,里面装的是 BSP 依赖,真正影响链接结果的是下面这几个文件:

  • libxil.a:Standalone BSP 的基础库,包含 UART、中断控制器XScuGicxil_printf、cache 操作等。只要是 standalone 工程,几乎所有功能都挂在它上面。
  • liblwip4.a:lwip 4.x 协议栈的预编译产物。它不只是 TCP/IP 协议栈,还包含了 Zynq PS 侧 GEM 以太网控制器的适配层,所以你才能直接调udp_newnetif_add,不用自己对着XEmacPs寄存器写驱动。
  • libxilffs.a:FAT 文件系统的封装。TFTP 服务端收到上传文件后要落盘,或者下载文件时要读盘,都用它导出的f_openf_readf_write
  • ps7_init.c:上电后第一段要执行的初始化代码,配置 DDR、PLL、MIO 和以太网引脚,必须最先运行。
  • system.bdsystem.bxml:Vivado 里的 Block Design 和 SDK 的硬件描述。前者告诉你 ZYNQ-7010 的 PS 侧配置了哪些外设,后者是 SDK 读取硬件信息的依据。

把这几个文件放在同一层目录,说明硬件设计、BSP 初始化代码和协议栈库是一起导出的。这种方式的最大好处是驱动库和硬件描述绑定,换板子时只要替换ps7_init.csystem.hdf,应用层 tftp 代码不用动。

2.2 为什么先看 ps7_init.c 的 MIO 与 PLL,而不是直接调 udp_new

ZYNQ-7010 的以太网控制器挂在 PS 侧,通过 GEM0/GEM1 和 MIO 引脚输出。liblwip4.a里的驱动是通过寄存器访问外设的,但如果ps7_init.c没有把 PLL 配置出外设时钟,或者 MIO 没接到 GEM,那网络驱动初始化时大概率读到全 0,表现为 lwip 起不来、ping 不通。

我拿到资源后会先执行一个很土但有效的检查:

# 在解压目录里看以太网相关配置是否真的存在 grep -niE "eth|gem|mio" ps7_init.c | head -30

如果输出里有ETH0MIO_PIN这类符号,说明硬件初始化代码覆盖了网络引脚。如果完全没有,就要回去看system.bd里 GEM 是不是被勾选掉了。ps7_init.c不是给你读着玩的,它决定 lwip 驱动面对的外设时序是否合法。很多人调了一晚上网口,最后发现是 MIO 没有拉出来。

2.3 用 XSCT 把三份库重新生成进 BSP

这个 zip 已经带了编译好的库,但实际开发中一定会碰到改 lwip 配置、换 BSP 版本、或者团队里对不上库版本的问题。与其手工点 Vivado SDK 界面,不如用 XSCT 脚本一次性把 BSP 重建出来。

先确认你从system.bd生成了system.hdf,然后在 XSCT 命令行里执行:

setws /home/tftp_ws repo -set /home/tftp_ws/embeddedsw hsi open_hw_design /home/tftp_ws/system.hdf hsi set_repo_path /home/tftp_ws/embeddedsw hsi create_sw_design tftp_server -os standalone -proc ps7_cortexa9_0 hsi add_library lwip hsi add_library xilffs hsi generate_bsp -dir ./bsp

这段脚本的作用是:打开硬件描述文件、指定 lwip 和 xilffs 库、最后生成一个完整的 BSP 目录。hsi generate_bsp生成出来的bsp/lib下面会有libxil.aliblwip4.alibxilffs.a,和你 zip 里的三份库同源。

如果不需要重新生成,直接把解压出来的三个.a文件加入链接器路径,并让ps7_init.c参与编译即可。BSP 里常见的 lwip 参数可以按这样设置:

参数推荐值影响
lwip_dhcp0固定 IP,tftp 调试更稳定
lwip_pbuf_pool_size128收发包缓冲区数量,太小会丢包
lwip_checksum_lowmem0使用硬件校验,省 CPU
lwip_udp_rmem4096UDP 接收窗口,直接影响 TFTP 吞吐

lwip_dhcp设为 0 后,lwip 不会在启动时发 DHCP discover,避免上电后等超时。裸机调试阶段固定 IP 是最省心的,等后面要量产再恢复到 DHCP 也不迟。

3. 在裸机 lwip4 上实现 tftp_server 驱动:状态机与 UDP 回调

3.1 raw API 和 netconn API 的取舍

lwip 4 提供了两套网络接口,netconn偏上层,带阻塞语义,适合有 RTOS 的环境;rawAPI 是基于回调的,事件到哪个 PCB 就直接调哪个函数。ZYNQ-7010 上默认的 SDK standalone 环境没有线程调度器,如果用netconn,就额外要 sys_arch 层,还要维护一个 idle 循环。而 TFTP 本身只有 RRQ、WRQ、DATA、ACK、ERROR 五种报文,用 raw UDP 回调写起来最直接,也没有上下文切换开销。

另一个考虑是这套驱动会用在 FSBL 或裸机 bootloader 里,那里对代码体积和入口函数数量都敏感。raw API 编译出来比 netconn 小,函数调用关系清晰,出问题时断点也好打。所以我建议直接基于udp_pcb写,不走 socket 抽象。

3.2 TFTP 协议状态机梳理

TFTP 是建立在 UDP 上的简单协议,初始请求发给固定端口 69,之后通信会切换到新的端口。对于服务器驱动,需要先把状态机理清:

opcode类型方向包内容
1RRQclient -> server文件名 + 0 + 模式 + 0
2WRQclient -> server文件名 + 0 + 模式 + 0
3DATAserver -> client块号(2B) + 数据(0-512B)
4ACKclient -> server块号(2B)
5ERROR双向错误码(2B) + 错误信息 + 0

读文件时,服务器每次发一个 DATA 包,块号从 1 开始;收到客户端 ACK 后,再发下一块。如果 DATA 数据小于 512 字节,说明文件结束,整个会话自动终止。写文件时相反,客户端发 DATA,服务器回 ACK,最后一个 ACK 确认短包。

这里最容易踩的坑是块号溢出和重传超时。块号只有 16 位,大于 65535 时要回绕到 1,FATFS 里几百 MB 的文件完全可能触发。TFTP 超时没有内建重传,客户端超时后会重发请求,伺服端要保证状态一致,不能因为收到了新的 DATA 就重新f_open

3.3 注册 UDP PCB 并处理 RRQ/WRQ

下面是一个精简但可以跑的 tftp_server 驱动核心。为了控制篇幅,我先把接收回调和对 RRQ 的响应写出来,文件句柄和端口都放到全局变量里,方便断点观察:

#include "lwip/udp.h" #include "lwip/pbuf.h" #include "lwip/inet.h" #include "ff.h" #include "xil_printf.h" #include "string.h" static struct udp_pcb *tftp_pcb; static FIL g_tftp_file; static u16_t g_tftp_block; static ip_addr_t g_peer_addr; static u16_t g_peer_port; static void tftp_send_error(u8_t code, const char *msg) { struct pbuf *p; u8_t *q; p = pbuf_alloc(PBUF_TRANSPORT, 4 + strlen(msg) + 1, PBUF_RAM); if (!p) { return; } q = (u8_t *)p->payload; q[0] = 0; q[1] = 5; q[2] = 0; q[3] = code; strcpy((char *)q + 4, msg); udp_sendto(tftp_pcb, p, &g_peer_addr, g_peer_port); pbuf_free(p); } static void tftp_send_data(void) { struct pbuf *p; u16_t len; p = pbuf_alloc(PBUF_TRANSPORT, 516, PBUF_RAM); if (!p) { return; } ((u8_t *)p->payload)[0] = 0; ((u8_t *)p->payload)[1] = 3; /* DATA */ ((u8_t *)p->payload)[2] = g_tftp_block >> 8; ((u8_t *)p->payload)[3] = g_tftp_block & 0xff; f_read(&g_tftp_file, ((u8_t *)p->payload) + 4, 512, &len); p->len = len + 4; udp_sendto(tftp_pcb, p, &g_peer_addr, g_peer_port); pbuf_free(p); } static void tftp_recv(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port) { char *filename; char *mode; u16_t opcode; if (p->len < 8) { pbuf_free(p); return; } opcode = ntohs(((u16_t *)p->payload)[0]); filename = (char *)p->payload + 2; mode = filename + strlen(filename) + 1; g_peer_addr = *addr; g_peer_port = port; if (opcode == 1) { /* RRQ */ if (f_open(&g_tftp_file, filename, FA_READ) != FR_OK) { tftp_send_error(1, "file not found"); } else { g_tftp_block = 1; tftp_send_data(); } } else if (opcode == 2) { /* WRQ */ if (f_open(&g_tftp_file, filename, FA_WRITE | FA_CREATE_ALWAYS) != FR_OK) { tftp_send_error(2, "access violation"); } else { g_tftp_block = 0; /* 等待客户端发第一个 DATA 块 */ } } else { tftp_send_error(4, "illegal opcode"); } pbuf_free(p); } int tftp_server_init(u16_t port) { tftp_pcb = udp_new(); if (!tftp_pcb) { xil_printf("udp_new failed\r\n"); return -1; } udp_bind(tftp_pcb, IP_ADDR_ANY, port); udp_recv(tftp_pcb, tftp_recv, NULL); return 0; }

tftp_send_data里先分配 516 字节的 pbuf,因为 TFTP 最大包是 2 字节 opcode、2 字节块号、512 字节数据。f_read读到 512 字节以内时,修改p->len为实际长度,这样udp_sendto就不会多发尾部垃圾。tftp_recv里的filenamemode直接指向 pbuf payload,在pbuf_free之前必须完成字符串解析,因此后面打开文件和组包都在同一个回调里做。

WRQ 分支这里只打开了文件,真正接收数据还需要在tftp_recv里处理 opcode 3(DATA)和 opcode 4(ACK),把收到的 DATA 用f_write落盘。裸机环境下没有多线程竞争,但是注意 TFTP 客户端超时后会处于重试状态,如果上一个会话的g_tftp_file还没关闭,新的 RRQ 会覆盖句柄,可能造成 FATFS 文件描述符泄漏。

3.4 lwip 初始化与 tftp 服务启动顺序

协议栈要先启动,网络接口要 up,然后才能绑定 69 端口。把下面这段放在 main 函数前面:

#include "lwip/init.h" #include "lwip/netif.h" #include "lwip/ip4_addr.h" static struct netif g_netif; void tftp_platform_init(void) { ip4_addr_t ip, mask, gw; IP4_ADDR(&ip, 192, 168, 1, 100); IP4_ADDR(&mask, 255, 255, 255, 0); IP4_ADDR(&gw, 192, 168, 1, 1); lwip_init(); netif_add(&g_netif, &ip, &mask, &gw, NULL, xemacpsif_init, xemacpsif_input); netif_set_default(&g_netif); netif_set_up(&g_netif); tftp_server_init(69); }

xemacpsif_initxemacpsif_input是 Xilinx SDK 里 lwip BSP 导出的函数,前者初始化 GEM 并启动 DMA,后者把网卡收到的报文喂给 lwip。代码里的 IP、掩码、网关都用IP4_ADDR宏硬编码,这样板卡每次启动都在已知地址上,方便 tftp 客户端连。

裸机主循环里需要周期性调用xemacpsif_input,否则网络接收回调永远不触发:

while (1) { xemacpsif_input(&g_netif); Xil_DCacheFlush(); }

Xil_DCacheFlush是为了保证 DMA 写入的数据能被 CPU 读到,Zynq-7010 的 C-A9 有 cache 一致性风险。如果发现 TFTP 传输速度异常低,或者偶发数据错位,多半是这里 flush/invalidate 没配对。

4. 从 TFTP 下载到 ZYNQ Flash:BOOT.bin 与 image.ub 的升级流程

4.1 为什么 TFTP 驱动不要直接写 Flash

把 TFTP 驱动和 Flash 编程耦合在一起是新手最容易犯的错误。TFTP 是锁步协议,客户端发一个 DATA,服务器必须在超时窗口内回 ACK,否则客户端会重发。擦除和编程 QSPI Flash 往往要几十到几百毫秒,直接放在 TFTP 回调里写,很容易超时,而且 TFTP 重传会带着相同的块号再次进来,导致同一块数据写两次,文件系统目录项会错乱。

更稳妥的架构是:TFTP 驱动只负责把文件搬到 DDR 缓冲区,传完后再做完整性校验,最后按分区写入 Flash。一个典型的 QSPI 分区表如下:

起始偏移分区内容长度建议
0x0000000FSBL + bitstream1 MB
0x0100000裸机应用或 image.ub4 MB 以上
0x0300000回滚备份区4 MB 以上
0x0500000配置文件256 KB

把 bootloader、内核、根文件系统分开,升级时只覆盖 app 或 image.ub,避免整个 Flash 被写穿。

4.2 在 FSBL 阶段挂 TFTP 还是在应用阶段再挂

如果做在线升级,我会建议把 tftp_server 驱动同时编译进 FSBL。FSBL 上电先初始化 DDR,然后检查一个 GPIO 或者启动原因寄存器。如果是普通启动,直接跳转到 Flash 里的 app;如果是升级模式,则初始化 lwip 并启动 TFTP 服务器,等上位机传新的 image.ub 到 DDR 的固定地址。

这样做的好处是:即使当前 app 已经跑飞了,只要硬件没坏,依然能进入升级模式。FSBL 里挂 lwip 会增加约 100 KB 左右代码体积,但 ZYNQ-7010 的 DDR 和 QSPI 都装得下,换来的是稳定的恢复入口。

4.3 主机侧触发传输的三种命令

传 BOOT.bin 或 image.ub 时,最常用的是 Windows 自带 tftp,但它只支持单条命令,而且不能指定 blksize:

tftp -i 192.168.1.100 PUT BOOT.bin

Linux 下的 tftp-hpa 支持交互模式,适合手工调试:

tftp 192.168.1.100 binary put BOOT.bin quit

如果是写自动化升级脚本,建议用 curl,它能设置 TFTP blksize 并返回非零退出码,方便 CI 捕获错误:

curl -T app.bin tftp://192.168.1.100/app.bin --tftp-blksize 1468

--tftp-blksize 1468是协商更大的 UDP payload,减少 ACK 交互次数,实测吞吐比默认 512 字节快很多。但这个参数需要两头都支持,如果板卡上的 tftp_server 驱动没有解析 option,curl 会回退到 512,不影响正确性。

4.4 Flash 写入顺序和回滚策略

收到完整文件后,先别急着擦 Flash。校验文件长度,或者在 TFTP 包外自定义一个校验包头,比如前 32 字节放 magic number 和长度的 SHA-256 值。确认无误后再执行:

  1. 按目标分区起始地址逐 sector 擦除。
  2. 从 DDR 缓冲区按 page 写入 QSPI。
  3. 读回整段数据,和 DDR 缓冲区逐字节比对。
  4. 写一个 boot flag 到独立 sector,记录当前版本号。
  5. 软复位,FSBL 读取新的 boot flag,从新分区启动。

回滚就是把 Flash 里预留两个 app 分区,始终保留上一个可用版本。每次升级只覆盖非活跃分区,校验失败时 boot flag 不变,下次启动继续用旧版。这套方法不依赖 Linux,在裸机 SDK 工程里同样成立。

5. tftp_server 调试到稳定的三个细节:IP、端口与 DMA 描述符

5.1 固定 IP 和防火墙里的 ephemeral 端口

TFTP 最反直觉的地方是服务器收到客户端的 RRQ 后,会从一个新的本地端口发 DATA,而不是继续用 69。所以调试时别只放行 UDP 69,很多防火墙默认拦截高位端口,表现出来就是put卡在发送第一个 DATA 包之后。

先用固定 IP 把环境跑通。BSP 里lwip_dhcp设为 0,然后在板卡串口打印出实际 IP,本地ping 192.168.1.100通了再测 TFTP。如果 ping 通但 tftp 超时,在主机侧用tcpdump -nn -vvv -i eth0 port 69看交互包,重点确认服务器回包是不是从一个 10000+ 端口出来。

5.2 pbuf 池大小和 UDP checksum

lwip 在 BSP 里默认的PBUF_POOL_SIZE是 32,对 TFTP 来说勉强够用。如果传输大文件时传到一半挂住,先看是不是报pbuf_alloc失败。我一般会把lwip_pbuf_pool_size调到 128,同时把lwip_checksum_lowmem设为 0,让 Zynq 的 GEM 硬件计算 UDP checksum,CPU 只负责协议处理,吞吐能提升将近一倍。

注意,如果板卡和主机之间经过了交换机或路由器,UDP checksum 错误可能有伪装来源。若出现大量 checksum error,先换一根网线直连,排除中间链路改动包头的可能。

5.3 DMA cache 一致性与描述符中断

Zynq-7010 上跑裸机 lwip,最容易翻车的是 RX DMA 描述符。GEM 把数据写到 DDR 后,如果 CPU 读到的是 cache 里的旧数据,那整个 TFTP 流程会表现出随机丢包。Xilinx 自带的xemacpsif驱动内部做了Xil_DCacheInvalidate,但如果你改过中断优先级,或者把网络中断和 TFTP 处理放在不同优先级下,就容易漏掉tx完成中断。

调试时可以先不再调optimize -O2,在udp_sendto返回处加一个Xil_DCacheFlush(),再看是否反复断在同一个位置。要是问题消失,基本可以确认是 DMA 描述符的 cache 一致性没保住。最终修复建议是把xemacpsif的 tx/rx ring 和 pbuf pool 都放到非 cacheable 内存段,或者统一在收发路径上显式 flush 和 invalidate。

把 tftp_server 的回调代码、XSCT 脚本和ps7_init.c一起放进版本管理。下次换板子或换 BSP 版本,直接执行脚本重新生成 lib,再对比三个.a文件的构建时间,就能判断是库不匹配还是硬件描述变了,十分钟内可以重新产出完全一致的 SDK 驱动工程。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询