在环境监测设备开发中,数据存储是个绕不开的话题。我去年做过一个农业大棚监控项目,需要每5分钟记录一次温湿度数据。最初尝试用EEPROM存储,结果不到一周就爆满了。后来改用STM32+SD卡方案,配合FATFS文件系统,存储容量直接提升到GB级别,实测连续工作三个月都没问题。
FATFS作为轻量级文件系统,特别适合STM32这类资源有限的MCU。它完全符合FAT标准,生成的SD卡文件可以直接插到电脑上读取。这里有个实用建议:选择Class10以上的高速SD卡,我在测试中发现低速卡在频繁写入时容易出错。具体硬件连接很简单:
移植FATFS需要准备这三个核心文件:
重点在于实现diskio.c里的六个底层函数:
c复制DSTATUS disk_initialize (BYTE pdrv);
DSTATUS disk_status (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);
我遇到过的一个坑是SD卡初始化时序问题。有些国产SD卡需要更长的初始化延时,这时可以修改disk_initialize函数:
c复制// 增加重试机制
for(int i=0; i<5; i++){
if(SD_Init() == 0) break;
HAL_Delay(100);
}
ffconf.h中有几个关键参数需要关注:
c复制#define _USE_LFN 1 // 支持长文件名
#define _CODE_PAGE 936 // 中文编码
#define _FS_LOCK 5 // 最大打开文件数
建议开启_FS_REENTRANT选项,配合RTOS使用时更稳定。我在FreeRTOS项目中就遇到过同时写文件导致崩溃的情况,开启这个选项后问题解决。
环境监测数据通常采用"时间戳+数值"的格式存储。这里分享一个优化技巧:先用f_lseek预留空间,再批量写入。比单次写入效率提升3倍以上:
c复制void log_sensor_data(float temp, float humi){
static FIL file;
char buffer[64];
UINT bytes_written;
// 构造数据行
int len = sprintf(buffer, "%lu,%.1f,%.1f\n",
HAL_GetTick(), temp, humi);
// 追加写入
if(f_open(&file, "data.csv", FA_OPEN_APPEND | FA_WRITE) == FR_OK){
f_write(&file, buffer, len, &bytes_written);
f_close(&file);
}
}
长期运行的数据采集设备需要考虑存储空间管理。我的方案是:
实现代码片段:
c复制void file_rotate(){
DIR dir;
FILINFO fno;
char oldest_file[20] = "DATA_9999.CSV";
// 查找最旧文件
f_opendir(&dir, "/");
while(f_readdir(&dir, &fno) == FR_OK){
if(strcmp(fno.fname, oldest_file) < 0){
strcpy(oldest_file, fno.fname);
}
}
// 删除最旧文件
if(strcmp(oldest_file, "DATA_9999.CSV") != 0){
f_unlink(oldest_file);
}
}
根据使用场景不同,数据读取有三种常用方式:
这里重点说下随机访问的优化技巧。先建立索引文件可以大幅提升查询速度:
c复制// 索引文件结构
typedef struct {
uint32_t timestamp;
uint32_t file_offset;
} DataIndex;
SD卡操作常见错误及解决方案:
我的错误处理模板:
c复制FRESULT res = f_open(&file, path, mode);
if(res != FR_OK){
if(res == FR_NO_PATH){
f_mkdir("/data");
res = f_open(&file, path, mode);
}
else if(res == FR_DISK_ERR){
f_mount(0, "", 0); // 卸载
HAL_Delay(100);
f_mount(&fs, "", 1); // 重新挂载
res = f_open(&file, path, mode);
}
return res;
}
通过实测对比不同方案的写入速度:
| 方案 | 速度(KB/s) | CPU占用率 |
|---|---|---|
| 单次写入 | 12.5 | 35% |
| 缓存后批量写入 | 78.2 | 18% |
| 启用DMA传输 | 152.4 | 9% |
推荐采用DMA+双缓冲方案:
c复制#define BUF_SIZE 512
uint8_t buf1[BUF_SIZE], buf2[BUF_SIZE];
uint16_t buf1_cnt = 0, buf2_cnt = 0;
uint8_t *active_buf = buf1;
// 数据收集时写入缓冲区
void collect_data(uint8_t data){
*active_buf++ = data;
if(++buf1_cnt >= BUF_SIZE){
// 切换缓冲区并启动DMA写入
if(active_buf == buf1){
active_buf = buf2;
SD_Write_DMA(buf1, BUF_SIZE);
}else{
active_buf = buf1;
SD_Write_DMA(buf2, BUF_SIZE);
}
}
}
长期使用后SD卡可能出现碎片化,建议:
这里给出空间检查函数:
c复制uint32_t get_free_space(){
FATFS *fs;
DWORD fre_clust;
f_getfree("", &fre_clust, &fs);
return (fre_clust * fs->csize) / 2; // 单位KB
}
在工业现场部署时,我总结出几个实用经验:
一个健壮的数据存储实现应该包含这些要素:
c复制typedef struct {
FIL file;
uint32_t last_write_time;
uint16_t error_count;
uint8_t is_mounted;
} StorageHandle;
void storage_task(){
static StorageHandle hdl;
// 状态检测
if(!hdl.is_mounted){
if(f_mount(&fs, "", 1) == FR_OK){
hdl.is_mounted = 1;
}
}
// 超时处理
if(HAL_GetTick() - hdl.last_write_time > 60000){
f_sync(&hdl.file);
}
}