前言
这篇文章记录了一个基于 STM32F407 的 TFTP Server 项目从模板工程到最终可用版本的完整实现过程。项目的目标并不只是"把 TFTP 跑起来",而是做成一个真正能用于现场调试和文件管理的小系统:
- 板端基于
STM32CubeMX生成模板 - 网络协议栈使用
LwIP - 文件系统使用
FatFs - 任务调度使用
FreeRTOS - 上位机使用
Python + Tkinter,提供图形化界面 - 支持文件列表、上传、下载、删除、取消传输、设备重启
最终效果是:STM32F407 插上网线和存储介质后,可以像一个轻量文件服务器一样,通过上位机对设备文件进行管理。
一、项目目标
整个项目主要解决下面几个需求:
- 在
STM32F407上实现一个稳定的TFTP Server - 通过
FatFs将 TFTP 读写直接映射到本地文件系统 - 使用
FreeRTOS管理系统初始化顺序和后台任务 - 提供一个易用的上位机工具,而不是只靠命令行
- 解决大文件传输、取消传输、会话残留、栈溢出、超时等实际工程问题
二、系统架构
1. 板端软件栈
板端使用如下软件组件:
STM32CubeMX:生成外设初始化代码HAL:底层外设驱动FreeRTOS:任务调度LwIP:网络协议栈FatFs:文件系统TFTP Server:基于lwIP官方tftp_server.c
整体逻辑如下:
text
STM32F407
├─ SDIO / 存储设备
│ └─ FatFs
├─ ETH
│ └─ LwIP
│ └─ TFTP Server
└─ FreeRTOS
├─ 文件系统初始化任务
├─ 网络初始化任务
└─ TFTP 服务任务
2. 上位机软件栈
上位机使用纯 Python 标准库实现,无需安装额外第三方依赖:
socket:实现 TFTP 客户端tkinter:图形化界面threading:异步上传下载queue:线程间状态和进度通信
三、板端实现思路
1. FreeRTOS 启动顺序控制
在带文件系统和网络协议栈的嵌入式项目里,启动顺序非常关键。如果顺序混乱,很容易出现:
- 网络先启动,但配置文件还没读
- TFTP 服务先注册,但 FatFs 还没挂载
- 上位机请求过来时,底层资源还没就绪
因此项目里增加了一套基于 EventGroup 的启动同步机制,大致顺序如下:
- 初始化
FatFs - 加载
config.json - 按配置初始化
LwIP - 启动
TFTP Server
这样可以保证 TFTP 服务启动时:
- 存储已经挂载
- 网络参数已经生效
- 配置文件已经完成读取或恢复
2. FatFs 文件系统接入
文件系统部分的核心是把 TFTP 的文件操作回调映射到 FatFs:
- 打开文件:
f_open - 读取文件:
f_read - 写入文件:
f_write - 关闭文件:
f_close
这使得 TFTP 可以直接对设备内的文件进行上传和下载,而不需要额外再写一层"缓存数据库"或"中间抽象层"。
3. TFTP Server 接入
项目没有手写完整 TFTP 协议栈,而是复用了 lwIP 官方提供的 tftp_server.c,只实现它要求的 4 个回调:
openclosereadwrite
这是一种很适合嵌入式项目的做法:
- 协议逻辑交给成熟库
- 设备逻辑只关注文件读写
- 调试范围更清晰
4. 虚拟控制文件设计
为了让上位机不仅能传文件,还能做控制操作,项目设计了几个"虚拟文件"接口:
filelist.txt
用于获取设备上的文件列表_tftp_delete/<filename>
用于删除指定文件_tftp_reboot.req
用于请求设备重启config.json
用于配置文件上传和替换
这种设计有个很大的优点:
- 仍然保持 TFTP 的标准文件读写模型
- 上位机实现简单
- 设备端逻辑清晰
四、配置文件机制
项目中引入了 config.json 来配置网络和业务参数,例如:
- IP 地址
- 网关
- 子网掩码
- TFTP 开关
- 串口记录参数
- Modbus TCP 参数
为了避免配置文件损坏后设备无法启动,还做了三层保护:
- 优先加载
config.json - 如果失败,则尝试恢复
_config_backup.json - 如果备份也无效,则创建一份内置默认配置
这样即使用户上传了错误配置,也不会轻易把设备"搞死"。
五、上位机 GUI 设计
1. 为什么不只做命令行
命令行当然可以完成上传下载,但实际使用中会有几个问题:
- 普通用户不愿意记命令
- 没有传输状态提示
- 出错时不够直观
- 不适合频繁点选设备文件
所以最终把上位机做成了一个带界面的工具。
2. GUI 功能
图形界面支持:
- 输入设备 IP、端口、超时、重试次数
- 刷新远程文件列表
- 选择远程文件下载
- 选择本地文件上传
- 删除远程文件
- 请求设备重启
- 显示活动日志
- 显示上传下载进度条
- 支持取消正在进行的传输
3. 上传文件名逻辑
早期版本里,上传时可能误用 GUI 中的 Remote name 文本框内容,导致:
- 用户明明选择了本地文件
modbus_144.csv - 实际上传到设备的却是另一个旧文件名
最终修正为:
- 上传时默认始终使用本地文件名
- 选中文件后自动同步更新
Remote name
这样更符合用户直觉,也更不容易误操作。
六、工程中遇到的核心问题
这个项目真正有价值的地方,不只是功能实现,而是踩过并解决了一系列典型工程问题。
1. tcpip_thread 栈溢出
最开始点击刷新文件列表时,系统直接进了:
c
vApplicationStackOverflowHook()
问题根因是:
lwIP的tftp_server.c回调运行在tcpip_thread- 默认
TCPIP_THREAD_STACKSIZE太小 - 文件列表生成和格式化输出又增加了额外栈占用
最终处理:
- 增大
TCPIP_THREAD_STACKSIZE - 增大
DEFAULT_THREAD_STACKSIZE - 避免在热点路径里使用较重的格式化输出
2. 文件名过长
一开始删除文件会报:
text
Filename too long/not NULL terminated
原因是 lwIP 默认:
c
#define TFTP_MAX_FILENAME_LEN 20
但像 _tftp_delete/xxx.bin 这种虚拟文件名很容易超过 20。
最终通过在 lwipopts.h 中覆盖该宏解决。
3. 只允许一个连接
取消传输或请求超时后,马上再发起下一次请求,经常会碰到:
text
Only one connection at a time is supported
这是 lwIP 官方 TFTP 服务器的行为:同一时间只允许一个会话。
解决方式有两层:
- 缩短设备端 TFTP 会话超时
- GUI 端在取消后增加"释放等待期"
这样用户不会在旧会话尚未释放时立即触发新请求。
4. 下载时报 Wrong block number
这个问题出现在早期 PC 端下载重传逻辑中:
- 客户端在等待下一包
DATA时错误地重发了旧的ACK - 而设备端官方服务器只接受当前块号对应的 ACK
最终将客户端下载逻辑改为:
- 等待服务器自己重发
- 不主动乱发旧 ACK
之后问题消失。
5. 大文件下载首包超时
现象是:
- 小文件和删除操作都正常
- 大文件下载时直接
download request timed out
根因是 lwIP 内存池不足,首个 DATA 包分配失败后没有明显错误输出。
最终通过增大以下参数解决:
MEM_SIZEMEMP_NUM_PBUFPBUF_POOL_SIZE
这也是嵌入式网络项目里非常典型的问题。
6. 没有进度条,用户误以为卡死
上传下载大文件时,如果只有一个"等待"状态,用户通常会觉得程序卡住了。
因此最终把上传和下载都做成了真实进度条:
- 按实际传输字节推进
- 已知总大小时显示百分比
- 未知总大小时显示动态状态
7. 用户中途不想等,需要取消
大文件传输时,必须允许用户中断操作。
最终实现:
- GUI 提供
Cancel Transfer - 后台线程轮询取消信号
- socket 传输循环主动退出
- 取消后进入短暂释放等待期,避免旧会话残留影响下一次操作
七、最终功能列表
当前版本已经支持以下能力:
板端
STM32F407 + LwIP + FatFs + FreeRTOS- TFTP 文件上传
- TFTP 文件下载
- TFTP 删除文件
- TFTP 获取文件列表
- TFTP 上传配置文件
- TFTP 请求设备重启
- 启动顺序同步
- 配置文件恢复与默认配置创建
上位机
- 图形化界面
- 刷新文件列表
- 双击下载
- 选择本地文件上传
- 真实进度条
- 取消传输
- 活动日志显示
- 取消后的设备释放等待机制
八、项目中的经验总结
这个项目有几个很值得记录的经验。
1. 嵌入式网络项目,功能实现只是第一步
真正麻烦的往往不是"能不能跑起来",而是:
- 会不会偶发超时
- 会不会因资源不足而静默失败
- 用户误操作时会不会进入异常状态
2. lwIP 默认配置不适合直接上生产
默认参数更偏向演示和轻量场景:
- 栈小
- 堆小
- pbuf 少
一旦进入真实文件传输场景,几乎一定要调。
3. GUI 不是"锦上添花",而是稳定性的一部分
良好的 GUI 不只是好看,它还能减少误操作:
- 进度条减少焦虑
- 取消按钮提高可控性
- 冷却等待减少协议冲突
- 文件名自动同步减少错误上传
4. 设计"虚拟文件接口"非常适合 TFTP
相比额外定义一套私有命令协议,直接用虚拟文件做控制动作:
- 简单
- 稳定
- 易调试
- 和标准 TFTP 客户端也兼容
九、后续可继续优化的方向
虽然当前版本已经可用,但还有一些可继续优化的方向:
- 上传取消后自动删除设备端半截文件
- 下载取消后自动删除本地未完成文件
- GUI 中文文案和编码统一
- 增加速度显示和剩余时间估算
- 增加文件覆盖确认
- 增加配置文件在线编辑器
- 增加多设备管理能力
十、结语
这个 STM32F407 TFTP Server 项目本质上是一次非常典型的嵌入式系统工程实践:
- 底层有外设和文件系统
- 中间有 RTOS 和网络协议栈
- 上层有真实用户界面
- 中间穿插着超时、栈、内存、协议状态机等工程问题
如果只看"实现一个 TFTP",这件事并不复杂;但如果目标是"做一个用户敢用、现场能跑、出问题还能定位"的系统,那就必须把这些细节都一一补齐。
这也是这个项目最有价值的地方。
附:项目关键点一览
板端关键模块
FreeRTOS启动同步FatFs文件系统挂载config.json加载与恢复LwIP网络初始化TFTP回调文件映射
上位机关键模块
- TFTP 客户端协议实现
- Tkinter 图形界面
- 异步传输线程
- 进度条更新
- 取消传输
- 会话释放等待
如果你也在做类似项目,希望这篇记录能帮你少踩几个坑。