
简介一套基于赛灵思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.a、liblwip4.a、libxilffs.a三个预编译库外加ps7_init.c、system.bd和Makefile.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.aStandalone BSP 的基础库包含 UART、中断控制器XScuGic、xil_printf、cache 操作等。只要是 standalone 工程几乎所有功能都挂在它上面。liblwip4.alwip 4.x 协议栈的预编译产物。它不只是 TCP/IP 协议栈还包含了 Zynq PS 侧 GEM 以太网控制器的适配层所以你才能直接调udp_new、netif_add不用自己对着XEmacPs寄存器写驱动。libxilffs.aFAT 文件系统的封装。TFTP 服务端收到上传文件后要落盘或者下载文件时要读盘都用它导出的f_open、f_read、f_write。ps7_init.c上电后第一段要执行的初始化代码配置 DDR、PLL、MIO 和以太网引脚必须最先运行。system.bd和system.bxmlVivado 里的 Block Design 和 SDK 的硬件描述。前者告诉你 ZYNQ-7010 的 PS 侧配置了哪些外设后者是 SDK 读取硬件信息的依据。把这几个文件放在同一层目录说明硬件设计、BSP 初始化代码和协议栈库是一起导出的。这种方式的最大好处是驱动库和硬件描述绑定换板子时只要替换ps7_init.c和system.hdf应用层 tftp 代码不用动。2.2 为什么先看 ps7_init.c 的 MIO 与 PLL而不是直接调 udp_newZYNQ-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如果输出里有ETH0、MIO_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.a、liblwip4.a、libxilffs.a和你 zip 里的三份库同源。如果不需要重新生成直接把解压出来的三个.a文件加入链接器路径并让ps7_init.c参与编译即可。BSP 里常见的 lwip 参数可以按这样设置参数推荐值影响lwip_dhcp0固定 IPtftp 调试更稳定lwip_pbuf_pool_size128收发包缓冲区数量太小会丢包lwip_checksum_lowmem0使用硬件校验省 CPUlwip_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 模式 02WRQclient - server文件名 0 模式 03DATAserver - client块号(2B) 数据(0-512B)4ACKclient - server块号(2B)5ERROR双向错误码(2B) 错误信息 0读文件时服务器每次发一个 DATA 包块号从 1 开始收到客户端 ACK 后再发下一块。如果 DATA 数据小于 512 字节说明文件结束整个会话自动终止。写文件时相反客户端发 DATA服务器回 ACK最后一个 ACK 确认短包。这里最容易踩的坑是块号溢出和重传超时。块号只有 16 位大于 65535 时要回绕到 1FATFS 里几百 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里的filename和mode直接指向 pbuf payload在pbuf_free之前必须完成字符串解析因此后面打开文件和组包都在同一个回调里做。WRQ 分支这里只打开了文件真正接收数据还需要在tftp_recv里处理 opcode 3DATA和 opcode 4ACK把收到的 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_init和xemacpsif_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 FlashBOOT.bin 与 image.ub 的升级流程4.1 为什么 TFTP 驱动不要直接写 Flash把 TFTP 驱动和 Flash 编程耦合在一起是新手最容易犯的错误。TFTP 是锁步协议客户端发一个 DATA服务器必须在超时窗口内回 ACK否则客户端会重发。擦除和编程 QSPI Flash 往往要几十到几百毫秒直接放在 TFTP 回调里写很容易超时而且 TFTP 重传会带着相同的块号再次进来导致同一块数据写两次文件系统目录项会错乱。更稳妥的架构是TFTP 驱动只负责把文件搬到 DDR 缓冲区传完后再做完整性校验最后按分区写入 Flash。一个典型的 QSPI 分区表如下起始偏移分区内容长度建议0x0000000FSBL bitstream1 MB0x0100000裸机应用或 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但它只支持单条命令而且不能指定 blksizetftp -i 192.168.1.100 PUT BOOT.binLinux 下的 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 驱动没有解析 optioncurl 会回退到 512不影响正确性。4.4 Flash 写入顺序和回滚策略收到完整文件后先别急着擦 Flash。校验文件长度或者在 TFTP 包外自定义一个校验包头比如前 32 字节放 magic number 和长度的 SHA-256 值。确认无误后再执行按目标分区起始地址逐 sector 擦除。从 DDR 缓冲区按 page 写入 QSPI。读回整段数据和 DDR 缓冲区逐字节比对。写一个 boot flag 到独立 sector记录当前版本号。软复位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 checksumlwip 在 BSP 里默认的PBUF_POOL_SIZE是 32对 TFTP 来说勉强够用。如果传输大文件时传到一半挂住先看是不是报pbuf_alloc失败。我一般会把lwip_pbuf_pool_size调到 128同时把lwip_checksum_lowmem设为 0让 Zynq 的 GEM 硬件计算 UDP checksumCPU 只负责协议处理吞吐能提升将近一倍。注意如果板卡和主机之间经过了交换机或路由器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 驱动工程。本文还有配套的精品资源点击获取