尧图精选

STM32F4+FreeRTOS+FATFS+LWIP实现嵌入式FTP服务器完整指南

🕒 发布时间:2026/9/9 23:17:21 📁 来源:尧图网络
简介面向 STM32F4 开发者的嵌入式网络通信工程基于 FreeRTOS 多任务系统整合 FATFS 文件系统与 LWIP 协议栈通过 socket 编程实现完整 FTP 服务器功能使开发板可与 Linux 或 Windows 主机双向传输文件适合需要搭建嵌入式 FTP 服务、学习 RTOS 移植或协议栈集成的工程师与学习者。压缩包共 322 个文件以 150 个 h 头文件和 130 个 c 源文件为主涵盖硬件驱动、文件系统、网络接口及 FTP 应用逻辑另有 txt/readme/pdf 等说明文档以及 ewd/ewp 等 IAR 工程配置整个压缩包仅 1.87MB便于快速获取与部署。工程包含从以太网底层驱动、文件系统到 FTP 应用逻辑的完整代码可帮助读者理解 FreeRTOS 任务划分、FATFS 卷挂载、LWIP 网络初始化、socket 收发及文件读写等关键环节对照工程编译烧录即可在开发板上复现双机文件传输场景。目前已有 3178 人学习适合作为毕业设计、项目原型或个人学习 STM32 网络开发的实践参考。 做嵌入式网络设备这几年我越来越觉得“能远程取走设备里的文件”是个被低估的需求。STM32F4FreeRTOSFATFSLWIP这套组合跑通FTP服务器功能之后很多日常问题都变得简单了设备日志直接导出到电脑固件升级不用再拆外壳连调试器产线上拷贝参数文件也省了一堆麻烦。这套方案并不算新但真正从零把它跑稳中间要处理的RTOS任务调度、文件系统并发、协议栈内存分配这些问题远比预想中琐碎。这篇文章就是我完整实现过程的记录从需求分析、工程骨架搭建到FTP协议状态机设计、实测数据与踩坑记录一次性讲清楚希望能给正在做类似嵌入式网络应用的朋友省点时间。1. 为什么要在板子上搭FTP服务器需求拆解与方案选型1.1 什么场景下非用FTP不可很多人第一反应是设备调试用串口不就行了但真做产品就会发现串口有几个硬伤速度慢、距离受限、还得专门接线路。尤其设备已经在现场部署或者外壳已经封装好的情况下远程通过网络取文件几乎是唯一可行的路。我手头当时的需求是这样的一台基于STM32F407的采集设备本地用SD卡存日志和采集数据希望技术人员在办公室就能把设备里的数据文件下载下来同时支持把新的配置文件、字库文件上传到设备里。设备本身需要联网上报数据所以以太网接口已经有了硬件上只差一套文件传输协议。对比了几个方案方案优点缺点串口/Ymodem实现简单无网络依赖需要拆设备接线速度约10KB/s体验差HTTP上传下载与Web功能天然整合浏览器就能操作需要额外实现Web Server而且大文件传输不够稳定进度展示麻烦FTP服务器客户端工具成熟FileZilla/WinSCP都支持断点续传、目录操作齐全需要自己实现FTP协议栈工作量大一些TFTP代码极少UDP传输不支持目录浏览、认证可靠性一般只适合特定升级场景我最后选了FTP。核心原因有三条一是PC端FTP客户端是标配操作人员不需要额外培训二是FTP的命令集和状态机虽然多但嵌入式场景只需要实现其中一小部分三是FTP基于TCP传输可靠性有保证文件传一半断了也知道怎么回事不会有TFTP那种“传坏了还没提示”的问题。1.2 芯片资源和协议栈选型如何匹配STM32F4系列带MAC控制器外接一颗PHY芯片我用的是LAN8720ARMII接口就能跑以太网不需要外挂MACPHY一体芯片BOM成本能压下来。SD卡走SDIO接口用四线模式配合DMA实测读速度能做到1MB/s左右写速度会低一些但完全够用。协议栈这块LwIP是嵌入式圈子事实标准支持RAW API和Netconn API两层接口。FreeRTOS负责线程调度FATFS负责文件系统。这三样都开源、资料多、配合成熟单个组件拿出来都有大量现成代码可以参考组合起来反而比用某个厂商的半闭源协议栈更容易排查问题。这里有个重要决策LwIP并没有官方实现FTP服务器网上能搜到一些独立的小型FTP server工程但多数是裸机移植、没有结合RTOS或者只支持最简单的LIST/RETR/STOR。所以“基于LwIP写FTP服务逻辑”这件事是躲不开的。好消息是FTP协议本身不复杂命令通道和数据通道分开处理状态机清晰只要理清思路工作量并没有想象中大。2. 工程骨架搭建CubeMX生成之外还要处理的集成细节2.1 CubeMX配置阶段的关键参数用STM32CubeMX生成基础工程时有几个参数直接关系到后续能不能跑得稳ETH外设选择RMII接口PHY地址要看硬件原理图LAN8720A一般是0x00或1地址错了PHY初始化会卡死。时钟配置System Clock跑到168MHzETH需要50MHz的PHY参考时钟RMII模式下该时钟由外部晶振或MCO引脚提供我板子上用的50M有源晶振方案省去MCO配置。这里最容易踩坑RMII对50MHz参考时钟的精度要求很高如果用MCO输出要确认PLL配置正确。SDIO四线模式、时钟分频跑在48MHz以下实测24MHz更稳。开DMA中断否则CPU占用率会高到影响FTP吞吐。FATFS选择SD卡驱动开启长文件名支持_USE_LFN这步忘了后面LIST中文文件名全是乱码。LWIP开启DHCP客户端方便现场接入局域网但调试阶段建议先配静态IP减少一个变量。内存配置先全都用默认后面实测再调。FreeRTOSHeap大小我给的是configTOTAL_HEAP_SIZE 60 * 1024FTP任务加上LwIP内核线程、文件系统缓存60KB够用且有余量。CubeMX生成的工程默认只把各模块初始化好了但FTP服务器相关的逻辑全要自己写。此时整个工程只有一个空壳接下来就是往这个骨架里填真正干活的东西。2.2 各模块集成的几个关键顺序很多人拿到CubeMX工程后急着写业务代码结果一跑就死机其实大部分问题出在模块集成的先后顺序上。这里有个我屡试不爽的顺序先单独验证SD卡FATFS挂载文件系统能创建文件、写一段数据、再读出来。再单独验证以太网LwIPPC能ping通板子能建立TCP连接。然后验证FreeRTOS下LwIP跑通创建TCP Server任务PC能连上并收发数据。最后才加FTP命令解析和文件系统读写。这一步一步来的原因很简单每一步引入的变量都有限出问题能快速定位。直接一把梭把四样堆一起死机了你都不知道是SD卡驱动问题还是协议栈问题还是任务优先级问题。另外有个容易忽略的点中断跟FreeRTOS的配合。LwIP的以太网接收中断在裸机工程里可以直接在中断里调用tcpip_input()但在FreeRTOS下这样做有风险——中断上下文里不能调用可能阻塞的API。正确做法是让中断只做标志位或者通过taskNOTIFY唤醒LwIP线程tcpip_thread本身跑在低优先级任务上用队列机制接收包。CubeMX生成的代码一般会处理好这个问题但如果你自己整合裸机例程一定要检查中断里是否调了不该调的函数。3. FTP协议状态机实现命令通道与数据通道的分工协作3.1 FTP的工作机制两条TCP连接FTP和HTTP最大的区别是它用了两条TCP连接一条控制连接默认21端口专门传命令和响应一条数据连接传文件列表和文件内容。控制连接贯穿整个会话数据连接则是每次传输临时建立传完就关闭。这意味着FTP服务器代码要同时管理多个socket状态再加上FTP有主动模式PORT和被动模式PASV两种建立数据连接的方式很容易把人绕晕。我的建议很直接嵌入式FTP服务器只做被动模式PASV就够了。理由有三条第一PASV模式下数据连接由客户端发起服务器不需要暴露额外端口给外部设备去连接穿透NAT时更友好第二主动模式需要服务器主动连接客户端的端口这在很多办公网络环境里直接被防火墙拦掉第三实现上PASV就是服务器监听一个临时端口等客户端来连逻辑更简单。FileZilla默认就是用PASV模式主流客户端都兼容。3.2 命令解析与控制连接状态流转FTP控制连接的本质是一个基于文本协议的会话客户端发一行命令服务器回三字状态码说明文字。状态码设计得很有规律2xx成功、3xx需要进一步输入、4xx临时错误、5xx永久错误。我实现的控制连接处理流程是这样的typedef enum { FTP_STATE_WAIT_USER, // 等待用户名 FTP_STATE_WAIT_PASS, // 等待密码 FTP_STATE_LOGGED_IN, // 已登录 FTP_STATE_PASV_WAIT, // 已进入被动模式等待数据连接 } ftp_state_t;主循环不断读取28个字节为一行缓存解析出命令码和参数然后用strcmp去匹配。初始状态为FTP_STATE_WAIT_USER收到USER命令后返回“331 User name okay, need password”切换到FTP_STATE_WAIT_PASS收到正确的PASS后返回“230 User logged in”进入FTP_STATE_LOGGED_IN。嵌入式设备通常不需要多用户系统用户名密码做一套简单的固定账号校验就够了。调试时我fastidiously建议关掉密码直接USER匿名就登录等通了再开认证。以下是核心命令清单嵌入式场景基本只需要实现这几条命令功能典型响应USER/PASS用户认证230 Logged inSYST返回系统类型215 UNIX Type: L8PWD显示当前目录257 / is current directoryCWD切换目录250 Directory changedTYPE I设置二进制传输模式200 Type set to IPASV进入被动模式227 Entering Passive Mode (h1,h2,h3,h4,p1,p2)LIST列出目录内容150/226RETR下载文件150/226STOR上传文件150/226DELE删除文件250 File deletedMKD/RMD创建/删除目录250 Directory created/removedSIZE获取文件大小213 12345QUIT退出221 Goodbye3.3 PASV模式与数据连接的真实处理细节PASV命令处理是整个FTP服务器里最容易出bug的地方。服务器需要做这几件事创建新的TCP监听socket绑定端口0让内核分配一个临时端口监听这个端口但不要阻塞等客户端连接否则控制连接的处理会卡住把IP和端口号编码成h1,h2,h3,h4,p1,p2格式通过控制连接返回给客户端客户端拿到端口号后主动连接此时数据socket才真正建立。227响应格式是个坑。端口号是16位要拆成两个字节端口号为(p1 8) | p2。比如端口49152则p1192, p20。如果这里直接传十进制整数客户端会一脸懵。LIST命令的数据连接逻辑也值得展开。收到LIST命令时先通过控制连接返回“150 Opening data connection”然后在数据连接上流式传输目录列表传完后用226表示结束。如果在控制连接收到LIST时数据连接还没建立会导致FTP客户端一直卡在“连接中”。处理方式数据监听socket用非阻塞方式LIST命令发出后在数据socket上等待一段超时时间比如5秒客户端连不上就直接返回425错误。目录列表格式要跟Unix风格兼容FileZilla才能正确解析。FATFS的f_readdir能拿到文件名、大小、修改日期这几个字段我把它们格式化成-rw-r--r-- 1 owner group 12345 Feb 21 12:34 test.bin drwxr-xr-x 1 owner group 0 Feb 21 12:34 logs注意行尾用\r\nFTP协议要求控制连接和数据连接都用CRLF结尾。就因为这个换行符我第一次调试时LIST出来的列表全部挤在一行里FileZilla直接乱码排查了好久才发现是裸\n的问题。4. 多任务竞态与内存瓶颈FATFS互斥、LWIP调优实测4.1 文件系统多任务访问的互斥方案FATFS本身不是线程安全的这是移植到RTOS环境时的第一个坎。如果在FTP任务下载文件的同时日志任务恰好在往另一个文件里写数据两个任务同时操作SDIO外设轻则数据错乱重则SD卡挂起、DMA缓冲区踩踏。FATFS官方已经预留了线程安全的接入点_FS_REENTRANT这个配置项打开之后编译系统会要求你实现f_mount时传入的FF_SYNC_t同步对象。我在ffconf.h里做了如下配置#define _FS_REENTRANT 1 #define _FS_TIMEOUT 1000 #define _SYNC_t SemaphoreHandle_t然后在diskio.c或者专门一个文件里实现同步函数int ff_cre_syncobj(BYTE vol, _SYNC_t* sobj) { *sobj xSemaphoreCreateMutex(); return (*sobj ! NULL); } int ff_req_grant(_SYNC_t sobj) { return (xSemaphoreTake(sobj, _FS_TIMEOUT) pdTRUE); } void ff_rel_grant(_SYNC_t sobj) { xSemaphoreGive(sobj); } int ff_del_syncobj(_SYNC_t sobj) { vSemaphoreDelete(sobj); return 1; }这样FATFS每次访问文件系统对象时都会先拿互斥锁确保同一时刻只有一个任务在操作SD卡。这里建议用xSemaphoreCreateMutex()而不是xSemaphoreCreateBinary()因为互斥量自带优先级继承能避免一个低优先级任务持有锁时高优先级任务长时间等锁导致系统“假死”的问题。另外文件系统挂载点也要设计好。FATFS支持多卷我挂载的是0:对应的SD卡FTP访问路径统一加0:前缀。注意FTP返回给客户端的路径不应该带这个前缀要在协议层转换——CWD /logs对应FATFS的0:/logs。4.2 LwIP内存池与收发窗口的调优LwIP默认配置对FTP这种需要吞吐量的应用来说偏保守不调的话实测下载速度只有100-200KB/s。重点把lwipopts.h里这几个参数改掉#define MEMP_NUM_TCP_SEG 64 #define TCP_SND_BUF (8 * TCP_MSS) #define TCP_WND (8 * TCP_MSS) #define PBUF_POOL_SIZE 32 #define TCP_MSS 1460原理上说TCP_WND决定远端一次性能发多少数据过来而不用等待确认TCP_SND_BUF则决定我们能积攒多少数据一次性发出去。这两个值太小时传输就会变成经典的“小包慢速”模式每发一个包要等一个ACKRTT高一点的话吞吐量直接打骨折。还容易忽略的是sys_check_timeouts()的调用。LwIP有TCP超时重传、延迟ACK等定时任务如果你用的是自编译的FreeRTOS移植版本记得在某个周期任务里周期性调用sys_check_timeouts()不然TCP一旦丢包重传机制永远不会触发表现为“传大文件传到一半就卡住不动了”。CubeMX生成的代码如果开了LWIP_TIMERS会在tcpip_thread里自动处理但想确认一下这个选项是否打开。4.3 收发缓冲区的伤势设计FATFS每次读写都有一个最小粒度SD卡是512字节扇区。如果FTP的数据socket每次从LwIP收20字节、再调用一次f_write写20字节SD卡会崩溃给你看刷一页flash要擦整个扇区性能惨不忍睹。所以在FTP层和数据层之间加了一层环形缓冲区双缓冲设计STOR上传方向从数据socket读数据攒够4096字节才调用f_write一次RETR下载方向用f_read一次读4096字节再通过数据socket发送。4096这个数怎么来的经验值配合SDIO的DMA传输效率最高也正好是FATFS建议的_MAX_SS逻辑扇区大小的整数倍避免跨扇区边界时内部做拼接。实测下来读写速度都稳定了很多CPU占用也明显下降。如果缓冲区设置得太大内存又hold不住。STM32F407的内部RAM有192KB但FreeRTOS任务栈、LwIP内存池、FTP缓冲区全都在里面分一次开两个4KB的缓冲区就比较合适。想再优化可以用DMA双缓冲PingPong模式让SDIO在搬数据的同时CPU处理另一块缓冲但对FTP应用来说意义不大瓶颈早就从CPU转到了网络和SD卡本身。5. 实测数据、日志保存与踩坑记录5.1 实际传输速度与稳定性测试测试环境STM32F407主频168MHzSDIO四线直插SanDisk 16GB Class10 TF卡文件系统FAT32LwIP开启TCP_NODELAYPC通过千兆交换机连接板子FileZilla 3.63。测试项目实测结果LIST100个文件的目录350ms下载10MB大文件约12秒平均833KB/s上传10MB大文件约18秒平均555KB/s大量小文件100个40KB批量上传总耗时约3分钟平均约22KB/s长时间高负载连续传输1小时无断连无内存泄漏SD卡温度正常大文件的速度基本吃满了SDIO四线读取带宽网络没有成为瓶颈。小文件慢是FATFS每次创建/关闭文件的开销决定的协议栈侧没什么优化空间属于换谁都一样的情况。内存占用统计FTP任务栈给了2048字8KB实测峰值用掉约70%LwIP协议栈总内存池占用约20KBFATFS内部缓存WORKING_AREA用了8KBFreeRTOS全局堆剩下约15KB给其他任务用。整体余量还可以但如果同时开HTTP服务器或者OTA升级任务内存就得重新算一遍。5.2 三个让我抓狂的坑第一个坑是数据连接不关闭导致socket泄漏。FTP每传一个文件就开一条数据连接传完应该立刻关闭。我在第一版代码里RETR命令发送完文件后只发了226响应忘了关数据socket运行半小时后MEMP_NUM_NETCONN耗尽新的FTP连不上来系统日志里全是pbuf_alloc: Out of memory。排查方法是在tcp_abort和socket.close后加日志打印连接号观察每隔几次传输就多一个未释放的连接。修复很简单数据连接在任何路径下传完、出错、超时都要强制关闭并释放资源。第二个坑跟SD卡拔出/写保护有关。用户用FTP传文件时如果SD卡写入失败FATFS会返回FR_DISK_ERR如果协议层没做错误处理直接返回“226 Transfer complete”客户端就会显示上传成功实际上文件根本没写进去等发现时数据已经丢了。我在STOR数据写入循环里加了返回值检查任何一次f_write返回错误就立刻中止传输并返回“451 Requested action aborted: local error”同时把这个错误事件记录到系统日志里。第三个坑是FTP命令大小写和多余空白符。一开始用strncmp(cmd, PASV, 4)直接比较结果有的客户端发来的是pasv有的带行尾空格还有的带CRLF。后来统一改成每行命令先trim掉首尾空白把命令词转成大写再解析参数。FileZilla、WinSCP、Windows自带的FTP命令工具全都测一遍基本不会再出现“一个客户端能用另一个客户端灵异报错”的诡异问题。5.3 如何记录FTP操作日志既然设备带FTP顺手把FTP操作日志写到SD卡上是特别有用的功能——出了问题能追溯是谁在什么时候做了什么操作。我的做法是在会话结束QUIT或超时断开时把这条会话里记录的命令序列合并成一行追加到syslog.txt[2025-02-21 14:23:31] FTP session from 192.168.1.100: USER admin, PASV, LIST /logs, RETR /data/a.bin, QUIT日志通过FATFS打开文件追加写注意互斥锁保护避免跟正在传输的文件产生冲突。这个功能在调试阶段帮了大忙有一次用户报告“文件传完后变成0字节”翻日志发现是传完文件到关闭文件的间隙断电文件大小没刷新到目录项里。后来在STOR传完后主动调用f_sync把文件缓存落盘再加一个写完后重新f_open/f_truncate校准大小的流程彻底解决问题。6. 后续可以怎么扩展断点续传、多客户端与Web化FTP服务器跑稳之后这个工程的底层能力其实是“TCP服务器文件系统操作”的组合往上可以延伸出不少东西。断点续传REST命令FTP协议原生支持REST命令实现思路是在RETR/STOR前读取客户端发来的偏移量然后用f_lseek跳到指定位置再继续读写。对大文件传输很有用但嵌入式设备用得少我暂时没做等真有人需要再说。多客户端并发目前实现的是一个控制连接对应一个任务任务栈从FreeRTOS的heap动态分配。并发两个客户端同时上传下载是够用的再多客户端会受限于RAM。建议限制同一时刻最大FTP会话数为2超出直接拒绝避免资源被耗尽。做这个限制的原因很现实F4系列RAM就那么大并发会话多了内存碎片会越来越严重不如直接做会话上限来得稳妥。FTP状态页联动可以再开一个HTTP服务设备内置一个简单的状态页面显示FTP当前连接数、传输速度、SD卡剩余空间。这个纯属锦上添花但确实让现场运维的人好感度大增。做这个的前提是前面说的内存余量够如果不够就把FTP任务栈的内存池让出来在HTTP和FTP之间做启动参数选择不同应用场景加载不同协议栈。升级固件的延伸FTP能把文件传到板子上自然就能做OTA。流程是FTP上传app.bin到固定目录 → 写一个标志文件 → 重启后bootloader检测到标志从SD卡读取固件刷写Flash。这个方案难度不大可靠性却很高比用串口升级省事太多。我个人在实际操作中最深刻的体会是这种“四件套”工程问题从来不出在单个组件而出在组件之间的接口上——RTOS的调度会不会让LwIP收包延迟、FATFS的锁会不会在中断上下文里被误调、TCP的接收缓冲会不会跟SDIO的DMA缓冲区冲突。把接口想清楚跑起来就顺了。如果你也在做类似的板子建议先把FATFS和LwIP各自的demo工程跑通再加RTOS整合最后才做FTP业务逻辑这个顺序能少踩一半的坑。最后再分享一个小技巧FTP相关调试别急着上板子先用PC上的Python写个最小FTP客户端模拟器脚本控制收发数据比打开FileZilla手动点来点去效率高得多排查协议问题快很多。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →