1. 项目概述:为什么我们需要FatFS?
如果你正在捣鼓一个嵌入式项目,比如用STM32、ESP32或者树莓派Pico做一个数据记录仪、一个音乐播放器,或者一个带屏幕的设备,你大概率会遇到一个头疼的问题:怎么管理SD卡或者SPI Flash里的文件?直接读写扇区?那太原始了,你得自己处理文件分配表、目录项、碎片,想想就头大。这时候,一个轻量级、可移植的文件系统就成了刚需,而FatFS,就是嵌入式圈子里经久不衰的“瑞士军刀”。
FatFS是一个为小型嵌入式系统设计的通用FAT文件系统模块。它完全用ANSI C编写,与平台无关,这意味着你可以把它轻松地移植到几乎任何单片机或微处理器上。它遵循FAT12、FAT16和FAT32规范,支持长文件名、多卷(多个磁盘/分区),并且内存占用极小。我最早接触它是在STM32F103上读写SD卡,后来在ESP32、甚至一些国产MCU上都用过,其稳定性和易用性让我印象深刻。这份笔记,就是我多年使用FatFS过程中,对那些核心API函数、配置选项和踩过的坑的一次系统性梳理。无论你是刚入门的新手,还是想深化理解的老鸟,希望这些从实战中总结的经验,能让你在项目里少走弯路。
2. FatFS整体架构与移植要点
2.1 模块组成与依赖关系
FatFS的源码结构非常清晰,主要包含两个部分:核心源码 (ff.c,ff.h,ffconf.h) 和与底层磁盘I/O的接口层 (diskio.c,diskio.h)。理解这个架构是正确使用它的第一步。
核心源码 (ff.c/.h):这部分实现了完整的FAT文件系统逻辑,包括文件操作(打开、读、写、关闭)、目录操作、路径解析等。你几乎不需要修改这里的代码。
配置文件 (ffconf.h):这是FatFS的“大脑”。所有功能开关、参数配置都在这里。比如是否支持长文件名、是否支持可重入(多任务)、使用什么编码、扇区大小、缓冲区大小等。你的大部分定制化工作都会围绕这个文件展开。
磁盘I/O接口层 (diskio.c/.h):这是FatFS与你的硬件(如SD卡、SPI Flash、NAND Flash)之间的桥梁。FatFS核心通过调用这里定义的几个函数来读写物理存储介质。移植FatFS,本质上就是实现diskio.c中的这几个函数。它们是:
disk_initialize:初始化磁盘驱动。disk_status:获取磁盘状态。disk_read:读取一个或多个扇区。disk_write:写入一个或多个扇区。disk_ioctl:设备控制,如获取扇区大小、扇区数量、擦除等。
注意:
ff.c和diskio.c之间通过ff.h中定义的DSTATUS、DRESULT等类型和diskio.h中声明的函数原型进行通信。确保你的diskio.c正确包含了diskio.h和ff.h。
2.2 移植实战:以SPI接口SD卡为例
假设我们要在STM32上通过SPI接口连接SD卡。移植步骤如下:
获取源码:从FatFS官网(elm-chan.org)下载最新版本。将
source文件夹下的ff.c,ff.h,ffconf.h,diskio.c,diskio.h复制到你的项目。配置
ffconf.h:根据项目需求调整。一个基础配置可能如下:#define _FS_TINY 0 // 使用标准缓冲区模式,而非tiny模式(更通用) #define _FS_READONLY 0 // 设为1则只读,我们需读写,故为0 #define _FS_MINIMIZE 0 // 禁用最小化功能,保留完整API #define _USE_STRFUNC 1 // 启用字符串函数(如f_puts, f_gets) #define _USE_FIND 1 // 启用文件查找功能 #define _USE_MKFS 1 // 启用格式化功能(非常有用!) #define _USE_FASTSEEK 1 // 启用快速定位优化 #define _USE_LABEL 1 // 支持卷标操作 #define _USE_FORWARD 0 // 一般用不到,设为0 #define _CODE_PAGE 936 // 简体中文代码页(需包含cc936.c) #define _USE_LFN 2 // 启用长文件名,2=动态分配缓冲区(推荐) #define _MAX_LFN 255 // 长文件名最大长度 #define _VOLUMES 1 // 支持的物理驱动器数量(我们只有一个SD卡)实操心得:
_USE_LFN设置为2(动态堆分配)比设置为1(静态数组)更灵活,但需要你的系统支持malloc/free。如果内存紧张或没有堆管理器,可以设为1并定义_LFN_UNICODE和静态缓冲区。实现
diskio.c:这是核心移植工作。你需要根据你的SD卡驱动(可能是HAL库或标准库)来填充那几个函数。// 首先,包含必要的头文件和声明你的SD卡驱动句柄 #include “diskio.h” #include “ff.h” #include “sd_spi.h” // 你的SD卡SPI驱动头文件 extern SPI_HandleTypeDef hspi1; // 假设SPI1用于SD卡 extern SD_HandleTypeDef hsd; // 你的SD卡驱动句柄 // 定义驱动器号。FatFS支持多卷,这里驱动器0对应我们的SD卡 #define DEV_SD 0 DSTATUS disk_initialize (BYTE pdrv) { if (pdrv != DEV_SD) return STA_NOINIT; // 检查驱动器号 if (SD_Init(&hsd) != SD_OK) { // 调用你的SD卡初始化函数 return STA_NOINIT; } return 0; // 成功返回0 } DSTATUS disk_status (BYTE pdrv) { if (pdrv != DEV_SD) return STA_NOINIT; // 这里可以检查写保护、卡是否在位等,简单实现直接返回0 return 0; } DRESULT disk_read (BYTE pdrv, BYTE* buff, LBA_t sector, UINT count) { if (pdrv != DEV_SD) return RES_PARERR; if (SD_ReadBlocks(&hsd, buff, sector, count, SD_TIMEOUT) != SD_OK) { return RES_ERROR; } return RES_OK; } DRESULT disk_write (BYTE pdrv, const BYTE* buff, LBA_t sector, UINT count) { if (pdrv != DEV_SD) return RES_PARERR; if (SD_WriteBlocks(&hsd, (uint8_t*)buff, sector, count, SD_TIMEOUT) != SD_OK) { return RES_ERROR; } return RES_OK; } DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void* buff) { if (pdrv != DEV_SD) return RES_PARERR; switch (cmd) { case CTRL_SYNC: // 确保写入完成(对于SD卡,通常写操作已是同步的) // 可以调用SD_WaitWriteOperation等函数 return RES_OK; case GET_SECTOR_SIZE: // 获取扇区大小(通常为512字节) *(WORD*)buff = 512; return RES_OK; case GET_BLOCK_SIZE: // 获取擦除块大小(对于SD卡,通常一个扇区就是一个块) *(DWORD*)buff = 1; return RES_OK; case GET_SECTOR_COUNT: // 获取总扇区数(关键!) if (SD_GetCardInfo(&hsd, &CardInfo) == SD_OK) { // 假设有获取卡信息的函数 *(DWORD*)buff = CardInfo.CardCapacity / 512; return RES_OK; } return RES_ERROR; default: return RES_PARERR; } }踩坑记录:
disk_ioctl中的GET_SECTOR_COUNT必须正确实现!很多“卡容量识别不对”、“无法创建大文件”的问题都源于这里返回的值错误。务必根据你的存储介质实际容量计算总扇区数。添加必要的文件:如果启用了长文件名和中文(
_USE_LFN和_CODE_PAGE),需要将source目录下的cc936.c(或其他对应代码页文件)和ffunicode.c也加入工程。
完成以上步骤,FatFS的移植就基本完成了。接下来,就可以在应用代码中调用FatFS的API了。
3. 核心API函数详解与使用模式
FatFS的API设计得非常简洁,所有函数都以f_前缀开头。要使用它们,你首先需要声明一个FATFS对象(代表一个逻辑驱动器的工作区)和FIL对象(代表一个打开的文件)。
3.1 挂载与卸载:文件系统的入口与出口
在对任何文件进行操作前,必须先将物理驱动器“挂载”到一个FATFS对象上。
FATFS fs; // 声明一个FATFS对象 FRESULT res; // 用于接收函数返回结果 // 挂载驱动器0(即我们的SD卡)到fs对象 res = f_mount(&fs, “0:”, 1); // 第三个参数为1表示立即挂载 if (res != FR_OK) { printf(“Mount failed: %d\n”, (int)res); // 处理错误,可能是卡未初始化、文件系统损坏等 }f_mount:第一个参数是FATFS对象指针,第二个是路径(如“0:”表示驱动器0),第三个是挂载选项(0=延迟挂载,1=立即挂载)。- 返回值
FRESULT:所有FatFS函数都返回此枚举类型。FR_OK(0) 表示成功,其他值表示错误(如FR_NO_FILESYSTEM,FR_DISK_ERR等)。务必检查每次调用的返回值! FATFS对象的作用:它保存了该卷的FAT表、目录信息等缓存,是FatFS管理该驱动器的上下文。一个FATFS对象对应一个逻辑卷。
当不再需要访问该卷时,应卸载它以释放资源(主要是缓冲区内存)。
f_mount(NULL, “0:”, 0); // 第一个参数传NULL即可卸载注意事项:在嵌入式系统中,特别是使用RTOS时,要确保对同一驱动器的挂载/卸载、文件操作是线程安全的。FatFS本身不是线程安全的,除非你在
ffconf.h中启用了_FS_REENTRANT并提供了同步函数(如信号量)。
3.2 文件操作:打开、读写、关闭
这是最常用的部分,模式类似于标准C库的fopen/fread/fwrite/fclose。
打开文件 (f_open):
FIL file; // 声明一个文件对象 // 以读写方式打开(如果不存在则创建)根目录下的”data.txt” res = f_open(&file, “0:/data.txt”, FA_READ | FA_WRITE | FA_OPEN_ALWAYS); if (res != FR_OK) { /* 处理错误 */ }- 模式标志:
FA_READ: 读访问。FA_WRITE: 写访问。FA_OPEN_EXISTING: 打开已存在的文件(不存在则失败)。FA_CREATE_NEW: 创建新文件(存在则失败)。FA_CREATE_ALWAYS: 总是创建(覆盖已存在的文件)。FA_OPEN_ALWAYS: 打开文件,若不存在则创建(非常适合日志文件)。FA_OPEN_APPEND: 同FA_OPEN_ALWAYS,但初始文件指针在末尾。
读取文件 (f_read):
char buffer[128]; UINT bytes_read; // 实际读取到的字节数 res = f_read(&file, buffer, sizeof(buffer) - 1, &bytes_read); if (res == FR_OK) { buffer[bytes_read] = ‘\0’; // 添加字符串结束符 printf(“Read %u bytes: %s\n”, bytes_read, buffer); }- 参数:文件对象指针,缓冲区指针,要读取的字节数,指向实际读取字节数的指针。
- 关键点:
bytes_read可能小于请求的字节数,这表示已到达文件末尾(EOF)。这是正常情况,不是错误。
写入文件 (f_write):
char data[] = “Hello, FatFS!\n”; UINT bytes_written; res = f_write(&file, data, strlen(data), &bytes_written); if (res == FR_OK && bytes_written == strlen(data)) { printf(“Write successful.\n”); }- 参数与
f_read类似。同样需要检查bytes_written是否等于期望值。
移动文件指针 (f_lseek)与截断文件 (f_truncate):
// 将文件指针移动到文件开头后100字节处 res = f_lseek(&file, 100); // 从当前位置截断文件(常用于清空文件或调整大小) res = f_truncate(&file);关闭文件 (f_close):
res = f_close(&file);- 非常重要:
f_close会确保所有缓存的写入操作被提交到磁盘。如果不调用f_close就直接断电,可能导致数据丢失或文件系统损坏。
3.3 目录操作与文件查找
创建目录 (f_mkdir):
// 在根目录下创建名为”logs”的目录 res = f_mkdir(“0:/logs”); if (res == FR_EXIST) { printf(“Directory already exists.\n”); }打开目录与读取目录项 (f_opendir,f_readdir):
DIR dir; // 目录对象 FILINFO fno; // 文件信息对象 res = f_opendir(&dir, “0:/”); // 打开根目录 if (res != FR_OK) return; while (1) { res = f_readdir(&dir, &fno); // 读取下一项 if (res != FR_OK || fno.fname[0] == 0) break; // 错误或遍历完毕 if (fno.fattrib & AM_DIR) { // 是目录 printf(“[DIR] %s\n”, fno.fname); } else { // 是文件 printf(“[FILE] %s (Size: %lu)\n”, fno.fname, fno.fsize); } } f_closedir(&dir);FILINFO结构体包含了文件名、属性、大小、修改时间等信息。如果启用了长文件名,需要使用fno.lfname和fno.lfsize。
查找文件 (f_findfirst,f_findnext): 这是比循环f_readdir更便捷的查找方式,支持通配符。
DIR dir; FILINFO fno; // 查找根目录下所有 .txt 文件 res = f_findfirst(&dir, &fno, “0:/”, “*.txt”); while (res == FR_OK && fno.fname[0]) { printf(“Found: %s\n”, fno.fname); res = f_findnext(&dir, &fno); } f_closedir(&dir);3.4 文件系统管理:格式化与信息获取
格式化 (f_mkfs): 当插入一张新卡或者文件系统严重损坏时,可能需要格式化。
// 对驱动器0进行格式化,使用默认参数(FAT32,簇大小自动) BYTE work[_MAX_SS]; // 格式化需要的工作缓冲区,大小至少为一个扇区 res = f_mkfs(“0:”, FM_FAT32, 0, work, sizeof(work)); if (res != FR_OK) { printf(“Format failed: %d\n”, (int)res); }- 警告:格式化会清除所有数据!务必谨慎使用,最好在产品中通过某种安全机制(如按键组合)来触发。
- 参数
FM_FAT32指定文件系统类型。也可以传FM_ANY让FatFS自动选择(通常选FAT32)。
获取空闲空间 (f_getfree):
FATFS *pfs; DWORD fre_clust, fre_sect, tot_sect; // 注意:第一个参数是路径,第二个参数接收指向FATFS对象的指针(可用于后续操作) res = f_getfree(“0:”, &fre_clust, &pfs); if (res == FR_OK) { tot_sect = (pfs->n_fatent - 2) * pfs->csize; // 总扇区数 fre_sect = fre_clust * pfs->csize; // 空闲扇区数 printf(“Total: %lu KB, Free: %lu KB\n”, tot_sect / 2, fre_sect / 2); // 假设扇区512字节,/2得KB }4. 高级功能与性能优化技巧
4.1 长文件名与中文支持
默认情况下,FatFS只支持经典的8.3短文件名(如”DATA~1.TXT”)。要支持长文件名和中文,需要:
- 在
ffconf.h中设置_USE_LFN为非0值,并设置_CODE_PAGE为正确的代码页(如936对应GBK简体中文)。 - 将
ffunicode.c和对应代码页文件(如cc936.c)加入工程。 - 确保你的编译器支持多字节字符或Unicode。当使用长文件名时,
FILINFO的fname字段存储短名,lfname存储长名。
FILINFO fno; fno.lfname = malloc(256); // 为长文件名分配缓冲区 fno.lfsize = 256; res = f_readdir(&dir, &fno); if (res == FR_OK && fno.lfname[0]) { printf(“Long name: %s\n”, fno.lfname); } free(fno.lfname);内存考量:长文件名支持会增加一些ROM和RAM开销。如果资源极其紧张,可以考虑只使用短文件名。
4.2 可重入与多任务支持
在RTOS(如FreeRTOS)环境下,多个任务可能同时调用FatFS函数。由于FatFS内部有静态变量(如当前路径),直接并发调用会导致数据混乱。启用可重入功能可以解决此问题。
- 在
ffconf.h中定义_FS_REENTRANT为1,并设置_FS_TIMEOUT(等待超时时间)。 - 实现
ff_req_grant、ff_rel_grant、ff_delays这几个函数。通常它们封装了操作系统的信号量和延时函数。// 示例:使用FreeRTOS信号量 SemaphoreHandle_t fatfs_sem; int ff_req_grant (FF_SYNC_t sobj) { return (xSemaphoreTake(*(SemaphoreHandle_t*)sobj, _FS_TIMEOUT) == pdTRUE); } void ff_rel_grant (FF_SYNC_t sobj) { xSemaphoreGive(*(SemaphoreHandle_t*)sobj); } void ff_delays (FF_SYNC_t sobj, DWORD ms) { vTaskDelay(pdMS_TO_TICKS(ms)); } - 在挂载文件系统前,创建并初始化这个同步对象,并将其赋值给
FATFS对象的sobj成员。fatfs_sem = xSemaphoreCreateMutex(); fs.sobj = &fatfs_sem; f_mount(&fs, “0:”, 1);
4.3 性能优化:缓冲区与快速定位
缓冲区配置 (ffconf.h):
_MAX_SS: 定义最大扇区大小。对于大多数SD卡是512,但一些高容量卡可能支持4096。设为512兼容性最好。_MIN_SS: 最小扇区大小,通常也设为512。_USE_TRIM: 如果底层设备支持(如SSD),启用此选项可以在删除文件时发送TRIM命令,有助于维持性能。_FS_TINY: 如果设为1,FatFS会使用一个扇区大小的公共缓冲区,而不是每个打开的文件对象都有自己的缓冲区。这可以极大节省RAM(每个FIL对象节省约512字节),但会轻微降低性能,因为读写需要频繁切换缓冲区。在RAM紧张的8位/16位MCU上强烈推荐启用。
快速定位 (f_lseek与_USE_FASTSEEK): 当文件很大时,普通的f_lseek需要从FAT表链头开始遍历,速度很慢。启用_USE_FASTSEEK(在ffconf.h中设为1)后,FIL对象会维护一个“簇链接映射表”(cltbl)。首次快速定位时,会构建这个映射表(可能较慢),后续的定位操作将变得极快。
DWORD cltbl[100]; // 映射表缓冲区,大小要足够容纳文件的簇链 file.cltbl = cltbl; // 关联到文件对象 file.cltbl[0] = 100; // 第一个元素存储表的大小(这里是100) // 之后调用 f_lseek 就会使用快速定位算法 f_lseek(&file, 1000000); // 跳转到大文件中间位置适用场景:主要用于需要频繁随机读写大文件的场合,如音频视频播放器的跳转。对于小文件或顺序读写,收益不大。
5. 实战问题排查与调试心得
即使按照文档操作,在实际项目中还是会遇到各种稀奇古怪的问题。下面是我总结的一些常见“坑”和解决方法。
5.1 常见错误码解析与应对
FatFS的函数返回FRESULT类型错误码。在ff.h中有定义。遇到错误时,不要慌,先打印错误码。
FR_DISK_ERR(1): 底层磁盘I/O错误。这是最常遇到的错误之一。- 排查步骤:
- 检查
disk_read/disk_write函数的实现,确保扇区地址和计数传递正确。 - 检查硬件连接:SPI的CS、CLK、MISO、MOSI线是否接触良好?上拉电阻是否合适?
- 检查SD卡本身:换一张卡试试。有些劣质卡或假卡兼容性极差。
- 检查电源:SD卡工作时峰值电流可能较大,确保供电稳定。
- 降低SPI时钟频率试试。高速率下布线不良容易出错。
- 检查
- 排查步骤:
FR_NO_FILESYSTEM(13): 没有找到有效的FAT卷。- 可能原因:
- 卡没有被格式化。用
f_mkfs格式化。 - 卡被格式化成exFAT、NTFS等FatFS不支持的格式。需要在电脑上格式化为FAT32。
disk_ioctl的GET_SECTOR_COUNT返回的值完全错误,导致FatFS无法正确解析MBR/DBR。- 卡的分区表损坏。
- 卡没有被格式化。用
- 可能原因:
FR_INVALID_DRIVE(11): 驱动器号无效。- 检查
f_mount或文件路径中的驱动器前缀(如“0:”)是否与diskio.c中定义的驱动器号匹配。
- 检查
FR_NOT_ENABLED(12): 功能未启用。- 例如,尝试使用长文件名,但
_USE_LFN未在ffconf.h中启用。检查配置文件。
- 例如,尝试使用长文件名,但
FR_NO_FILE(4): 文件未找到。- 检查路径和文件名是否正确,注意大小写(默认不区分大小写,但路径分隔符和扩展名要写对)。长文件名要注意编码。
FR_EXIST(8): 文件或目录已存在。- 在使用
FA_CREATE_NEW模式打开文件,或创建目录时,如果目标已存在,就会返回此错误。这是正常情况,应根据业务逻辑处理(如改用FA_CREATE_ALWAYS覆盖)。
- 在使用
FR_DENIED(7): 操作被拒绝。- 可能原因:试图删除一个非空的目录;在只读模式下尝试写入;磁盘已满;文件系统写保护。
5.2 数据损坏与掉电保护
嵌入式设备常面临意外断电的风险,不当的文件操作可能导致FAT表或目录项损坏,甚至整张卡无法识别。
预防措施:
- 及时同步:不要过于频繁地
f_sync(或f_close),但也不能一直不调用。对于关键数据,在写入重要信息后调用f_sync(&file)强制将缓存写入磁盘。 - 使用事务性操作:对于非常重要的配置数据,可以采用“写两份,读回校验”的策略,或者先写到一个临时文件,校验无误后再重命名为正式文件。
- 启用
_FS_NORTC并维护时间:如果RTC不可靠,在ffconf.h中定义_FS_NORTC为1,并实现get_fattime函数返回一个固定值,避免无效时间戳扰乱文件系统工具。 - 正确处理
f_close:在系统进入低功耗或复位前,确保所有打开的文件都已正确关闭。
诊断工具: 当怀疑文件系统损坏时,可以将SD卡拔下来,插入电脑,用系统自带的磁盘检查工具(Windows的chkdsk,Linux的fsck)进行修复。注意:电脑的修复工具可能会改变磁盘结构(如将FAT32转换为exFAT),修复后的卡可能又无法被FatFS识别。因此,定期备份重要数据是王道。
5.3 内存与栈溢出排查
FatFS本身很节省内存,但在启用长文件名、多缓冲区等功能后,对栈空间的需求会增加。
典型症状:程序运行一段时间后死机,或者进行某些文件操作(如遍历含长文件名的目录)时崩溃。
排查方法:
- 检查
ffconf.h中的_MAX_LFN和缓冲区配置。过大的_MAX_LFN会消耗更多栈空间(如果长文件名缓冲区在栈上分配)。 - 在RTOS中,确保执行FatFS API的任务有足够的栈深度。建议将文件操作任务的栈大小设置得比常规任务大(例如,至少1KB以上)。
- 使用
f_open时,FIL对象最好定义为全局变量或静态变量,避免在栈上分配过大的结构体(FIL结构体本身有几百字节)。 - 如果启用了
_FS_TINY,公共缓冲区FatFs->win是全局的,注意其大小(一个扇区)。
5.4 一个完整的调试案例:SD卡容量识别错误
现象:32GB的SD卡,在FatFS中f_getfree报告的总容量只有几十MB。
排查过程:
- 首先怀疑
ffconf.h中_MAX_SS或_MIN_SS设置不对,但检查后均为512。 - 在
disk_ioctl的GET_SECTOR_COUNT命令处添加调试打印,发现返回的扇区数远小于实际值。 - 检查底层SD卡驱动
SD_GetCardInfo函数。发现该函数在计算容量时,对于高容量卡(SDHC/SDXC,容量>2GB)的算法有误。SDHC/SDXC的容量计算公式是:块数 * 块大小(通常为512)。而一些旧的驱动可能错误地使用了标准容量卡的计算公式。 - 修正SD卡驱动的容量计算逻辑后,问题解决。
根本原因:底层驱动与FatFS之间的接口 (disk_ioctl) 返回了错误信息,导致FatFS基于错误的数据进行解析。这提醒我们,在移植时,必须确保底层驱动返回的数据准确无误,尤其是扇区大小和扇区数量这两个核心参数。
6. 进阶应用:结合具体场景的代码片段
理论说再多,不如看几个实际场景的代码片段来得直观。
6.1 场景一:数据记录仪(循环覆盖写入)
设备需要每分钟记录一条传感器数据到文件,但存储空间有限,希望写满后覆盖最旧的数据。
FIL file; UINT bw; char record[64]; FRESULT res; static DWORD file_size = 0; const DWORD MAX_FILE_SIZE = 1024 * 1024; // 最大1MB // 1. 挂载(启动时做一次) f_mount(&fs, “0:”, 1); // 2. 打开或创建数据文件(追加模式) res = f_open(&file, “0:/datalog.csv”, FA_WRITE | FA_OPEN_ALWAYS); if (res != FR_OK) { /* 处理 */ } // 3. 如果文件太大,截断到开头(模拟循环) f_lseek(&file, 0); // 先跳到开头获取大小(f_lseek返回当前指针,但我们需要大小) // 更准确的做法:用 f_size(&file) 获取大小,或用 f_lseek 跳到末尾再用 f_tell f_lseek(&file, f_size(&file)); // 跳到末尾 if (f_size(&file) > MAX_FILE_SIZE) { f_lseek(&file, 0); // 回到开头 f_truncate(&file); // 截断文件(清空) f_lseek(&file, 0); // 指针回到开头 } // 4. 构造一条记录并写入 snprintf(record, sizeof(record), “%lu, %.2f, %.2f\n”, get_timestamp(), read_temperature(), read_humidity()); res = f_write(&file, record, strlen(record), &bw); if (res == FR_OK && bw == strlen(record)) { f_sync(&file); // 立即同步,防止掉电丢失 } // 5. 在系统空闲或定期关闭文件(这里示例是每次写后都sync,文件保持打开) // 系统关闭前: f_close(&file);6.2 场景二:固件升级(从SD卡读取并更新)
设备通过SD卡中的firmware.bin文件进行固件升级。
FIL fw_file; UINT br; uint32_t fw_size; uint32_t checksum = 0; uint8_t buffer[512]; // 1. 打开固件文件 if (f_open(&fw_file, “0:/firmware.bin”, FA_READ) != FR_OK) { printf(“Firmware file not found.\n”); return; } fw_size = f_size(&fw_file); // 2. 验证固件头(例如,包含魔数、版本号、CRC等) f_read(&fw_file, buffer, 128, &br); // 读取头部信息 if (!validate_firmware_header(buffer)) { f_close(&fw_file); printf(“Invalid firmware header.\n”); return; } f_lseek(&fw_file, 0); // 重置指针,准备开始烧录 // 3. 擦除Flash erase_flash_sectors(APP_START_ADDR, fw_size); // 4. 分块读取并写入Flash uint32_t addr = APP_START_ADDR; while (addr < APP_START_ADDR + fw_size) { UINT to_read = sizeof(buffer); if (f_read(&fw_file, buffer, to_read, &br) != FR_OK || br == 0) { break; } // 计算校验和(可选) for (UINT i = 0; i < br; i++) checksum += buffer[i]; // 编程Flash write_flash(addr, buffer, br); addr += br; // 可以在这里添加进度提示 } f_close(&fw_file); // 5. 校验(例如,对比计算的校验和与文件尾存储的校验和) if (checksum == expected_checksum) { printf(“Firmware update successful.\n”); // 设置标志,重启后跳转到新固件 set_boot_flag(); system_reset(); } else { printf(“Checksum error! Update failed.\n”); }6.3 场景三:配置文件读写(INI格式)
许多设备需要读写简单的文本配置文件。
// 读取配置项函数 FRESULT read_config_string(const char* path, const char* section, const char* key, char* value, size_t max_len) { FIL file; char line[128]; char current_section[64] = “”; FRESULT res = f_open(&file, path, FA_READ); if (res != FR_OK) return res; while (f_gets(line, sizeof(line), &file)) { // 去除行尾换行符 line[strcspn(line, “\r\n”)] = 0; // 跳过空行和注释 if (line[0] == ‘;’ || line[0] == ‘#’ || line[0] == 0) continue; // 检查是否是节声明 [section] if (line[0] == ‘[‘) { char* end = strchr(line, ‘]’); if (end) { *end = 0; strncpy(current_section, line + 1, sizeof(current_section)-1); } continue; } // 如果当前节匹配 if (strcmp(current_section, section) == 0) { char* delim = strchr(line, ‘=’); if (delim) { *delim = 0; // 去除键名和键值两端的空格 char* k = line; while (*k == ‘ ‘) k++; char* k_end = k + strlen(k) - 1; while (k_end > k && *k_end == ‘ ‘) k_end--; *(k_end+1) = 0; char* v = delim + 1; while (*v == ‘ ‘) v++; char* v_end = v + strlen(v) - 1; while (v_end > v && *v_end == ‘ ‘) v_end--; *(v_end+1) = 0; if (strcmp(k, key) == 0) { strncpy(value, v, max_len - 1); value[max_len - 1] = ‘\0’; f_close(&file); return FR_OK; } } } } f_close(&file); return FR_NO_FILE; // 未找到 } // 使用示例 char ssid[32]; if (read_config_string(“0:/config.ini”, “wifi”, “ssid”, ssid, sizeof(ssid)) == FR_OK) { printf(“WiFi SSID: %s\n”, ssid); }这些场景覆盖了FatFS最典型的几种用法:顺序追加、随机读取、文本解析。掌握这些模式,你就能应对绝大多数嵌入式存储需求了。FatFS的简洁API背后是足够强大的功能,只要理解其原理并注意细节,它就能成为你项目中稳定可靠的存储基石。