1. 为什么需要FATFS文件系统存储传感器数据?
在嵌入式系统中,传感器采集的数据往往需要长期保存。比如环境监测设备需要记录温湿度数据,工业设备需要存储运行参数。直接使用SD卡的块存储方式存在三个明显问题:数据难以管理、无法被PC直接读取、断电容易丢失。FATFS文件系统正好解决了这些痛点。
我曾在智能农业项目中遇到过这样的场景:需要每5分钟记录一次土壤温湿度,持续存储3个月。最初尝试直接写入SD卡原始扇区,结果发现:
- 数据检索像大海捞针
- 导出到电脑需要专用解析工具
- 意外断电导致最后1小时数据丢失
改用FATFS后,数据以CSV文件形式存储,Excel可以直接打开分析,文件系统自带缓存确保数据完整性。实测在STM32F407上,每秒可稳定写入100条传感器记录。
2. FATFS文件系统移植要点
2.1 硬件驱动适配
FATFS需要底层提供5个基础函数:
// 磁盘状态检测 DSTATUS disk_status(BYTE pdrv); // 磁盘初始化 DSTATUS disk_initialize(BYTE pdrv); // 数据读取 DRESULT disk_read(BYTE pdrv, BYTE* buff, LBA_t sector, UINT count); // 数据写入 DRESULT disk_write(BYTE pdrv, const BYTE* buff, LBA_t sector, UINT count); // 控制命令 DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void* buff);以STM32的SDIO接口为例,关键配置如下:
- 使能SDIO时钟(通常48MHz)
- 配置GPIO为复用推挽输出
- 设置DMA传输(提高效率)
- 实现SD卡初始化流程(发送CMD0、CMD8等)
特别注意:SD卡物理扇区大小固定为512字节,但高容量卡(SDHC)寻址方式不同,需要特殊处理。
2.2 文件系统配置
ffconf.h中的重要参数:
#define FF_USE_STRFUNC 1 // 启用字符串操作 #define FF_USE_MKFS 1 // 启用格式化功能 #define FF_USE_LABEL 1 // 支持卷标 #define FF_USE_FIND 1 // 支持文件查找 #define FF_CODE_PAGE 936 // 中文编码 #define FF_USE_LFN 2 // 长文件名支持 #define FF_MAX_SS 512 // 扇区大小 #define FF_MIN_SS 512实际项目中遇到过因FF_MAX_SS设置错误导致的文件损坏问题。某次使用4GB SD卡时,忘记修改该参数,结果写入的文件在电脑上无法识别。后来发现大容量卡需要保持512字节扇区设置。
3. CSV文件写入实战
3.1 文件创建与格式化
完整的文件操作流程应该是:
FATFS fs; // 文件系统对象 FIL file; // 文件对象 FRESULT res; // 挂载文件系统 res = f_mount(&fs, "0:", 1); if(res == FR_NO_FILESYSTEM) { // 格式化SD卡 MKFS_PARM opt; opt.fmt = FM_FAT32; f_mkfs("0:", &opt, work, sizeof(work)); // 重新挂载 f_mount(NULL, "0:", 0); f_mount(&fs, "0:", 1); } // 创建CSV文件 res = f_open(&file, "data.csv", FA_OPEN_APPEND | FA_WRITE); if(res != FR_OK) { // 错误处理 }常见坑点:
- 首次使用需要格式化
- 挂载失败可能是SD卡接触不良
- 打开模式要用FA_OPEN_APPEND避免覆盖
3.2 数据格式处理
标准的CSV格式要求:
- 数值之间用逗号分隔
- 每行以\r\n结尾
- 字符串建议用双引号包裹
示例温度数据写入:
float temp = 25.6; float humi = 65.2; uint32_t timestamp = 1625097600; char buffer[128]; int len = sprintf(buffer, "\"%lu\",\"%.1f\",\"%.1f\"\r\n", timestamp, temp, humi); UINT bw; f_write(&file, buffer, len, &bw);实测发现,直接使用f_printf性能较差。改用sprintf格式化后再写入,速度可提升3倍以上。
3.3 高效写入策略
为提高写入可靠性,建议:
- 启用写入缓存(ffconf.h中FF_FS_TINY=0)
- 定时调用f_sync强制刷盘
- 错误重试机制
优化后的写入流程:
#define MAX_RETRY 3 void write_data(FIL* fp, const char* data) { UINT bw; int retry = 0; do { res = f_write(fp, data, strlen(data), &bw); if(res == FR_OK) { // 每10次写入同步一次 if(++write_count % 10 == 0) { f_sync(fp); } break; } } while(++retry < MAX_RETRY); if(retry == MAX_RETRY) { // 触发错误处理 } }4. 错误处理与优化
4.1 常见错误码解析
| 错误码 | 含义 | 解决方案 |
|---|---|---|
| FR_DISK_ERR | 底层磁盘错误 | 检查SD卡连接 |
| FR_INT_ERR | 文件系统内部错误 | 重新挂载文件系统 |
| FR_NOT_READY | 存储设备未就绪 | 重新初始化SD卡 |
| FR_NO_FILE | 文件不存在 | 先创建文件 |
| FR_LOCKED | 文件被锁定 | 关闭其他文件句柄 |
4.2 性能优化技巧
通过实际测试对比(STM32F407@168MHz):
| 优化方法 | 写入速度(条/秒) | CPU占用率 |
|---|---|---|
| 基础实现 | 85 | 45% |
| 启用DMA | 120 | 30% |
| 批量写入 | 210 | 25% |
| 关闭长文件名 | 230 | 20% |
具体优化措施:
- 使用SDIO+DMA模式
- 合并多次写入(积累10条数据写一次)
- 关闭不必要的功能(如长文件名)
- 提高SPI/SDIO时钟频率(最高不超过25MHz)
4.3 电源管理
突然断电是数据损坏的主因。建议:
- 添加大容量电容(至少1000uF)
- 实现掉电检测电路
- 在中断服务中立即执行f_sync()
- 使用带写保护的SD卡座
5. 完整示例代码
以下是一个经过实际验证的稳定版本:
#include "fatfs.h" #include <string.h> FATFS fs; FIL file; char work[FF_MAX_SS]; void log_init() { FRESULT res; // 挂载文件系统 res = f_mount(&fs, "0:", 1); if(res == FR_NO_FILESYSTEM) { // 格式化 MKFS_PARM opt = {FM_FAT32, 0, 0, 0, 0}; f_mkfs("0:", &opt, work, sizeof(work)); // 重新挂载 f_mount(NULL, "0:", 0); f_mount(&fs, "0:", 1); } // 创建CSV文件头 res = f_open(&file, "data.csv", FA_WRITE | FA_OPEN_ALWAYS); if(res == FR_OK) { if(f_size(&file) == 0) { f_puts("timestamp,temperature,humidity\r\n", &file); } f_close(&file); } } void log_data(float temp, float humi) { static uint32_t write_count = 0; char buffer[64]; UINT bw; // 格式化数据 int len = sprintf(buffer, "%lu,%.1f,%.1f\r\n", HAL_GetTick(), temp, humi); // 写入文件 if(f_open(&file, "data.csv", FA_WRITE | FA_OPEN_APPEND) == FR_OK) { f_write(&file, buffer, len, &bw); // 定期同步 if(++write_count % 20 == 0) { f_sync(&file); } f_close(&file); } }这个方案在多个工业现场稳定运行超过2年,单文件最大记录数超过100万条。关键点在于:
- 首次运行自动格式化
- 追加写入模式
- 定期同步确保数据安全
- 简洁的CSV格式
6. 进阶技巧
6.1 多文件循环存储
当单个文件过大时,可以采用分文件存储:
#define MAX_FILE_SIZE (1024*1024) // 1MB void write_data() { static uint16_t file_num = 0; char filename[16]; if(f_size(&file) > MAX_FILE_SIZE) { f_close(&file); sprintf(filename, "data_%03d.csv", ++file_num); f_open(&file, filename, FA_WRITE | FA_CREATE_ALWAYS); } // 正常写入数据... }6.2 数据压缩存储
对于低频采样场景,可以使用RLE压缩:
void write_compressed(float value, uint8_t count) { char buf[32]; int len = sprintf(buf, "%.1f*%d,", value, count); f_write(&file, buf, len, &bw); }6.3 掉电保护实现
硬件设计建议:
- 在VBUS串联二极管
- 并联4700uF电容
- 使用电压检测芯片(如STM32的PVD)
软件实现:
void PVD_IRQHandler() { if(__HAL_PVD_GET_FLAG()) { f_sync(&file); HAL_SuspendTick(); while(1); // 等待完全断电 } }在实际项目中,这些技巧可以将数据丢失概率降低到0.1%以下。特别是在工业振动监测等严苛环境中,稳定可靠的数据存储至关重要。