3、文件系统基础(FatFS):从零开始理解存储管理
说实话,做嵌入式开发这么多年,我见过太多工程师在数据存储上栽跟头。有人直接用地址写Flash,结果数据一乱,整个系统就崩了。也有人自己写文件管理,最后发现比写业务逻辑还累。嗯,这时候你就需要一个成熟的文件系统——FatFS就是嵌入式领域最常用的选择。
3.1 FatFS文件系统简介
FatFS,全称是Fat File System,专门为嵌入式系统设计的轻量级文件系统。它支持FAT12、FAT16、FAT32,甚至exFAT。我最早接触它是在一个农业灌溉项目里,当时需要记录每小时的土壤湿度数据。如果不用文件系统,你得自己管理地址、擦除、磨损均衡……想想就头大。
FatFS的核心优势是什么?说白了就是:
- 代码量小:完整版也就几千行C代码,ROM占用不到10KB
- 移植简单:你只需要实现底层读写函数,其他都帮你封装好了
- 兼容性好:PC上读出来的SD卡,插到嵌入式设备上照样能用
重要提醒:FatFS不是实时文件系统。如果你需要毫秒级的写入响应,得考虑其他方案。但在99%的灌溉控制器场景里,它完全够用。
3.2 文件与目录的基本概念
先聊聊最基础的东西。文件系统里,文件和目录是两个核心概念。
文件:就是一段连续的数据,有个名字。比如"2024-06-15_soil_data.csv"。文件有属性:只读、隐藏、系统、归档。我在项目中习惯把配置文件设为只读,防止误操作。
目录:也叫文件夹,用来组织文件。你可以把不同传感器的数据放到不同目录下。比如:
/data
/soil
sensor1.csv
sensor2.csv
/weather
temp.csv
humidity.csv
/config
system.cfg
这里有个坑,我曾经遇到过:FatFS的路径分隔符是/,不是Windows的\。第一次用的时候我写成了反斜杠,结果文件死活打不开。嗯,记住这个细节。
3.3 f_open / f_close / f_read / f_write API详解
这四个函数是FatFS最常用的API。我一个个说。
3.3.1 f_open —— 打开或创建文件
函数原型:
FRESULT f_open (
FIL* fp, // 文件对象指针
const TCHAR* path, // 文件路径
BYTE mode // 访问模式
);
mode参数很关键,常用的有:
| 模式 | 含义 | 我常用的场景 |
|---|---|---|
| FA_READ | 只读 | 读取配置文件 |
| FA_WRITE | 只写 | 很少单独用 |
| FA_OPEN_EXISTING | 打开已有文件 | 追加日志时用 |
| FA_CREATE_NEW | 创建新文件(已存在则报错) | 防止覆盖历史数据 |
| FA_CREATE_ALWAYS | 总是创建新文件(覆盖旧文件) | 初始化时用 |
| FA_OPEN_ALWAYS | 打开或创建 | 最常用,省心 |
举个例子,打开一个日志文件,如果不存在就创建:
FIL fil;
FRESULT res = f_open(&fil, "0:/log/data.txt", FA_WRITE | FA_OPEN_ALWAYS);
if (res != FR_OK) {
// 处理错误
printf("文件打开失败,错误码:%d\n", res);
}
我的小技巧:每次打开文件后,先检查返回值。我曾经偷懒没检查,结果文件系统满了,写入失败,数据丢了整整一周。从那以后,我每个f_open后面都跟着错误处理。
3.3.2 f_close —— 关闭文件
函数原型:
FRESULT f_close (FIL* fp);
这个函数看似简单,但很多人会忘。不关闭文件会有什么后果?
- 缓存数据没刷入存储介质
- 文件描述符泄漏(FatFS默认支持同时打开的文件数有限)
- 下次打开同一个文件可能失败
我习惯在函数末尾统一关闭文件,或者用goto语句做清理。比如:
FIL fil;
FRESULT res = f_open(&fil, "test.txt", FA_WRITE | FA_CREATE_ALWAYS);
if (res != FR_OK) goto exit;
// 写入数据...
f_close(&fil);
exit:
// 清理工作
return res;
3.3.3 f_read —— 读取文件数据
函数原型:
FRESULT f_read (
FIL* fp, // 文件对象指针
void* buff, // 读取缓冲区
UINT btr, // 要读取的字节数
UINT* br // 实际读取的字节数
);
注意br参数,它返回实际读到的字节数。如果小于btr,说明读到文件末尾了。我经常用这个来判断是否读完:
uint8_t buffer[64];
UINT bytes_read;
while (1) {
res = f_read(&fil, buffer, sizeof(buffer), &bytes_read);
if (res != FR_OK || bytes_read == 0) break;
// 处理buffer中的数据
}
3.3.4 f_write —— 写入文件数据
函数原型:
FRESULT f_write (
FIL* fp, // 文件对象指针
const void* buff, // 写入数据缓冲区
UINT btw, // 要写入的字节数
UINT* bw // 实际写入的字节数
);
写入时有个常见问题:bw可能小于btw。这通常意味着存储介质满了。我曾经在一个项目中没检查这个,结果数据写到一半停了,日志文件不完整,排查了好久才发现是SD卡满了。
正确的写法:
UINT bytes_written;
res = f_write(&fil, data, sizeof(data), &bytes_written);
if (res != FR_OK || bytes_written != sizeof(data)) {
// 写入失败或未完全写入
printf("写入错误!\n");
}
3.4 长文件名支持配置
FatFS默认只支持8.3格式的短文件名(比如"DATA001.TXT")。但现实中,我们更想要"2024-06-15_土壤湿度_传感器1.csv"这样的名字。这就需要开启长文件名(LFN)支持。
配置方法很简单,在ffconf.h中修改:
#define FF_USE_LFN 2 // 1:静态缓冲区 2:动态缓冲区 3:从栈分配
我个人推荐用2(动态缓冲区),这样内存利用率高。但要注意:
- 开启LFN后,ROM占用会增加约2-4KB
- 文件名最大支持255个字符
- 需要额外配置
FF_MAX_LFN(默认255)
警告:开启长文件名后,f_open的路径参数必须用Unicode编码(UTF-8或UTF-16)。如果你用的是普通ASCII字符串,文件名会乱码。我刚开始就踩过这个坑,文件名显示成"??????.csv",后来才发现是编码问题。
另外,ffconf.h里还有几个相关配置:
| 宏定义 | 说明 | 我的建议 |
|---|---|---|
| FF_LFN_UNICODE | 0:ASCII 1:UTF-16 2:UTF-8 | 用2,兼容性好 |
| FF_LFN_BUF | 长文件名缓冲区大小 | 默认255够用 |
| FF_USE_FIND | 文件搜索功能 | 建议开启,方便遍历目录 |
嗯,到这里文件系统的基础就讲完了。下一节我们会实际动手,在STM32上移植FatFS,并实现一个简单的日志记录功能。到时候你会发现,这些API用起来其实很顺手。