1. 项目背景与需求分析
在工业物联网设备开发中,我们经常遇到嵌入式设备需要上传日志文件的需求。最近我在STM32F407平台上实现了一个典型案例:设备通过CH395Q以太网控制器联网,需要将存储在U盘中的CSV日志文件(大小从几百KB到几MB不等)上传到远程服务器。
经过技术选型对比,最终选择了FTP协议而非HTTP协议,主要基于以下几点考量:
- 连接特性:FTP支持长连接传输,而HTTP是短连接,对于大文件传输需要反复建立连接
- 传输效率:FTP在传输过程中保持控制连接和数据连接,传输效率更高
- 主动控制:FTP客户端可以主动发起传输,而HTTP需要服务器发起请求
- 断点续传:FTP协议原生支持断点续传(虽然本项目未实现)
2. 技术方案设计
2.1 硬件平台配置
- 主控芯片:STM32F407ZGT6(Cortex-M4内核,192KB RAM,1MB Flash)
- 网络模块:CH395Q以太网控制器(内置TCP/IP协议栈)
- 存储介质:通过USB Host接口连接的U盘(FAT32格式)
- 文件系统:FatFS R0.14b
2.2 软件架构设计
code复制┌───────────────────────┐
│ Application │
├───────────────────────┤
│ FTP Client Library │ ← 移植的ftplib
├───────────────────────┤
│ CH395Q Driver Adaptor│ ← 网络接口适配层
├───────────────────────┤
│ FatFS Interface │ ← 文件系统适配层
├───────────────────────┤
│ STM32 HAL Libraries │
└───────────────────────┘
2.3 关键设计决策
-
库选择:采用ftplib 4.0-1(原为Linux库)而非寻找STM32专用库,因为:
- 代码结构清晰(仅3个核心文件)
- 功能完整支持FTP协议
- 便于移植和裁剪
-
传输模式:选择PASV(被动)模式,因为:
- 更适应NAT网络环境
- 避免客户端防火墙拦截
- CH395Q更适合作为连接发起方
-
数据格式:采用二进制模式传输,确保文件内容不会因编码转换而损坏
3. 移植实现过程
3.1 源码结构调整
从ftplib 4.0-1中提取三个核心文件:
ftplib.c- FTP协议实现主体ftplib.h- 头文件qftp.c- 示例代码(改造为我们的接口)
在STM32工程中创建ftpclient目录存放这些文件,并配置编译选项。
3.2 系统接口适配
3.2.1 网络接口适配
c复制// 替换Linux的shutdown函数
int shutdown(uint8_t sock, int how) {
return CH395CloseSocket(sock); // CH395Q专用关闭socket接口
}
// 实现socket发送超时控制
int setsockopt(int sock, int level, int optname,
const void *optval, socklen_t optlen) {
if(optname == SO_SNDTIMEO) {
uint32_t timeout = *(uint32_t*)optval;
CH395SetSocketTimeout(sock, timeout);
return 0;
}
return -1;
}
3.2.2 文件系统适配
c复制// 替换标准文件操作接口
int open(const char *pathname, int flags) {
FIL *fp = malloc(sizeof(FIL));
if(f_open(fp, pathname, FA_READ) != FR_OK) {
free(fp);
return -1;
}
return (int)fp; // 返回文件指针作为fd
}
ssize_t read(int fd, void *buf, size_t count) {
UINT br;
f_read((FIL*)fd, buf, count, &br);
return br;
}
3.3 内存管理优化
原库使用动态内存分配,在资源受限的STM32上容易导致内存碎片,改为静态分配:
c复制// 定义控制块静态存储区
static netbuf ctrl_netbuf;
static char ctrl_buffer[2048]; // 控制通道缓冲区
// 修改库的初始化代码
nControl = &ctrl_netbuf;
nControl->buf = ctrl_buffer;
nControl->cavail = sizeof(ctrl_buffer);
3.4 多socket管理
CH395Q支持最多8个socket,分配方案如下:
- Socket 0:主业务TCP通信
- Socket 1:FTP控制连接
- Socket 2:FTP数据连接
- 其余socket保留
4. 调试与问题解决
4.1 典型问题记录
问题1:TCP连接失败
现象:FtpConnect()返回失败,错误码显示连接被拒绝
排查:
- 使用Wireshark抓包发现根本没有连接尝试
- 检查代码发现socket索引冲突(业务代码已占用Socket 0)
- FTP端口错误(使用了默认端口而非21)
解决方案:
c复制// 修改ftplib.c中的FtpConnect函数
if(CH395SocketConnect(sock, server_ip, FTP_PORT) != SOCK_OK) {
return 0; // 连接失败
}
问题2:登录后文件列表为空
现象:LIST命令返回成功但无文件数据
根本原因:CH395Q的中断状态未及时读取导致数据发送不完整
关键修复:
c复制int FtpSendCmd(const char *cmd, char expresp, netbuf *nControl) {
// 增加中断状态读取
ch395_socket_tcp_client_interrupt(nControl->handle);
if(send(nControl->handle, cmd, strlen(cmd), 0) <= 0) {
return 0;
}
// ...原有代码...
}
问题3:传输过程中看门狗复位
现象:大文件传输时系统重启
分析:文件传输占用CPU时间过长导致看门狗超时
解决方案:
c复制while((bytes_read = read(file_fd, buffer, chunk_size)) > 0) {
if(send(data_sock, buffer, bytes_read, 0) <= 0) {
break;
}
IWDG_ReloadCounter(); // 每次发送后喂狗
taskYIELD(); // 如果使用RTOS,主动让出CPU
}
4.2 性能优化技巧
- 缓冲区大小调整:
c复制#define CTRL_BUF_SIZE 2048 // 控制通道缓冲区
#define DATA_BUF_SIZE 8192 // 数据通道缓冲区
经过测试,8KB数据缓冲区在STM32F407上性能和内存占用达到最佳平衡。
- 传输分块策略:
c复制// 分块读取和发送文件
uint8_t file_buffer[DATA_BUF_SIZE];
while(1) {
UINT br;
FRESULT res = f_read(&file, file_buffer, sizeof(file_buffer), &br);
if(res != FR_OK || br == 0) break;
int sent = 0;
while(sent < br) {
int n = send(data_sock, file_buffer + sent, br - sent, 0);
if(n <= 0) break;
sent += n;
IWDG_ReloadCounter();
}
}
- 错误恢复机制:
c复制// 实现简单的重试逻辑
int retries = 3;
while(retries--) {
if(FtpSendCmd(cmd, resp, nControl)) {
break;
}
vTaskDelay(pdMS_TO_TICKS(1000)); // 延迟1秒后重试
}
5. 实际应用建议
5.1 生产环境注意事项
-
看门狗配置:建议将看门狗超时时间设置为5-10秒,确保有足够时间完成单次数据块传输
-
内存管理:
- 确保TCP发送缓冲区不小于4KB
- 为FTP任务分配独立的内存池
- 监控堆栈使用情况
-
网络稳定性处理:
c复制// 检测网络状态
if(CH395GetSocketStatus(sock) != SOCK_ESTABLISHED) {
FtpReconnect(); // 实现重连逻辑
}
5.2 调试技巧
- Wireshark过滤规则:
code复制ftp || ftp-data || tcp.port==21
- CH395Q调试接口:
c复制// 打印socket状态
void PrintSocketStatus(uint8_t sock) {
uint8_t status = CH395GetSocketStatus(sock);
const char *status_str[] = {
"CLOSED", "LISTEN", "SYN_SENT", "SYN_RCVD",
"ESTABLISHED", "FIN_WAIT_1", "FIN_WAIT_2", "CLOSE_WAIT",
"CLOSING", "LAST_ACK", "TIME_WAIT"
};
printf("Socket %d status: %s\n", sock, status_str[status]);
}
- 日志记录建议:
c复制// 在FtpSendCmd中添加调试输出
printf("[FTP] CMD: %s", cmd);
// 在回调函数中记录传输进度
void progress_callback(uint32_t xfered, void *arg) {
printf("Transferred: %lu bytes\n", xfered);
}
6. 扩展思考
6.1 性能瓶颈分析
通过测试发现主要瓶颈在于:
- CH395Q的SPI接口速度(最高30Mbps)
- FatFS文件读取效率
- TCP窗口大小限制
优化方向:
- 启用CH395Q的QIO模式提高SPI时钟
- 使用f_read的快速版本(需要修改FatFS配置)
- 调整TCP MSS和窗口大小参数
6.2 替代方案比较
-
HTTP PUT方法:
- 优点:无需额外库,使用现有的HTTP客户端
- 缺点:需要自行实现断点续传,服务器支持要求
-
MQTT文件传输:
- 优点:复用现有MQTT连接
- 缺点:需要分片处理,协议开销大
-
自定义协议:
- 优点:完全可控,可高度优化
- 缺点:开发成本高,需要配套服务器
6.3 未来改进方向
- 实现断点续传功能
- 添加TLS加密传输支持
- 开发基于事件驱动的异步接口
- 支持SD卡和SPI Flash的多存储介质
在实际项目中,这个FTP客户端解决方案已经稳定运行了6个月以上,日均传输文件约50个,平均大小800KB,完全满足了工业现场的数据采集需求。整个移植过程最大的收获是对网络协议栈的深入理解和嵌入式系统资源管理的实践经验。
