当前位置: 首页 > news >正文

嵌入式开发必备:FatFS文件系统移植与实战应用详解

1. 从零开始:为什么嵌入式项目绕不开FatFS?

如果你在玩ESP32、STM32这类微控制器,想把传感器数据存到SD卡里,或者从U盘里读取一个配置文件,那你大概率会碰到FatFS。这几乎是嵌入式圈子里处理FAT文件系统的“事实标准”,轻量、可移植、开源,从8位单片机到ARM Cortex-M系列,到处都能看到它的身影。我第一次接触它是在一个电池供电的采集设备上,需要把每分钟的环境数据以CSV格式写到SD卡,项目不大,但要求稳定可靠,断电不能丢数据。当时试过自己写简单的日志函数,但很快就遇到了文件大小限制、中文名乱码、突然断电导致文件损坏等一系列头疼问题。这才回过头来,老老实实研究FatFS。

FatFS的全称是FAT File System Module,由ChaN先生开发维护。它不是一个完整的操作系统级驱动,而是一个中间件,为你实现了FAT12/FAT16/FAT32/exFAT这些文件系统的读写逻辑。你的任务,就是为它提供最底层的“磁盘I/O接口”——告诉它怎么从你的SD卡、SPI Flash或者U盘里读写一个扇区。剩下的创建文件、读写数据、遍历目录这些“高级”操作,FatFS都帮你包了。这就像你买了一个智能马桶盖(FatFS),你只需要把它安装到你家的马桶上(实现底层驱动),它就能提供加热、清洗等各种功能,而不需要你去重新发明一个马桶。

网上关于FatFS的碎片信息很多,但要么是简单的f_open、f_write函数列表,要么是某个特定平台(如STM32CubeMX)的配置截图,缺乏系统性的“为什么这么做”的梳理。这篇笔记,我就结合自己踩过的坑和项目经验,把FatFS那些核心函数的用法、背后的逻辑以及实际调试中容易忽略的细节,掰开揉碎了讲清楚。无论你是刚入门,还是想解决某个具体问题,比如“为什么创建文件失败?”或者“怎么安全地处理断电?”,希望这篇笔记都能给你一个清晰的路线图。

2. 核心基石:FatFS的模块架构与移植关键点

在直接调用f_open之前,我们必须理解FatFS是怎么组织起来的。很多人移植失败,问题往往不是出在函数调用,而是底层没打通。

2.1 FatFS的层次化设计

FatFS的代码结构非常清晰,主要分为两层:

  1. 应用层 (Application Layer):就是我们直接调用的f_open,f_read,f_write等函数所在的ff.cff.h。这一层是平台无关的,你拿到任何单片机上,这部分代码都一样。
  2. 底层接口层 (Disk I/O Layer):这是移植的关键,对应diskio.cdiskio.h。FatFS通过这里定义的几个函数来操作物理存储设备。它抽象了硬件细节,FatFS核心只关心“请把第1234号扇区的数据读出来”或“请把这块数据写到第5678号扇区”。

diskio.h中定义了三个核心函数原型:

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);

你的移植工作,90%就是实现这五个函数。pdrv是物理驱动器编号,FatFS支持多磁盘,通常从0开始。LBA_t是逻辑块地址(扇区号),sectorcount都是以扇区为单位。这里就是第一个坑:你的底层驱动必须统一使用扇区(通常是512字节)作为操作单位。如果你的SD卡驱动是字节寻址的,就需要在disk_read/disk_write里做转换。

2.2 移植实战:以SPI模式SD卡为例

假设我们在STM32上通过SPI接口连接SD卡。diskio.c的实现大致如下:

1. disk_initialize – 初始化驱动器

DSTATUS disk_initialize (BYTE pdrv) { if (pdrv != 0) return STA_NOINIT; // 我们只支持一个驱动器 // 调用你的SD卡底层初始化函数 SD_Error err = SD_Init(); if (err != SD_OK) { return STA_NOINIT; // 初始化失败 } // 获取SD卡信息,比如扇区大小、总扇区数,可以存到全局变量供后续使用 if(SD_GetCardInfo(&SDCardInfo) != SD_OK) { return STA_NOINIT; } // 检查扇区大小是否为512字节(FatFS默认) if(SDCardInfo.CardBlockSize != 512) { // 可能需要通过disk_ioctl设置或处理,这是一个潜在问题点 } return 0; // 初始化成功,返回0 }

注意disk_initialize可能会被FatFS多次调用,你的底层初始化函数需要是幂等的,即多次调用效果和一次调用相同,不能重复初始化导致硬件错误。

2. disk_status – 获取驱动器状态这个函数通常很简单,就是检查磁盘是否就绪。对于SD卡,你可以检查一下SPI通信是否正常,或者卡片是否在位(如果硬件支持检测)。

DSTATUS disk_status (BYTE pdrv) { if (pdrv != 0) return STA_NOINIT; // 简单实现:假设初始化成功后就一直在线 // 更健壮的实现可以检查GPIO电平判断卡是否拔出 // if (SD_CardDetect_Pin == 0) return STA_NODISK; return 0; }

3. disk_read / disk_write – 数据搬运工这是性能关键,必须正确实现。

DRESULT disk_read (BYTE pdrv, BYTE* buff, LBA_t sector, UINT count) { if (pdrv != 0) return RES_PARERR; for (UINT i = 0; i < count; i++) { // 注意:SD_ReadBlock 是你的底层函数,它应该接收一个扇区号(LBA)和一个缓冲区。 // 这里sector是起始扇区号,每次循环后递增。 if (SD_ReadBlock(buff, sector + i, 1) != SD_OK) { // 假设一次读一个扇区 return RES_ERROR; } buff += 512; // 指针移动到下一个扇区缓冲区位置 } return RES_OK; }

关键点buff指针是由FatFS管理的内存缓冲区。确保你的DMA或SPI传输不会越界。对于写操作,尤其要注意写保护。在disk_write中,应先检查卡是否写保护,并妥善处理写错误。

4. disk_ioctl – 控制与信息获取这个函数是FatFS获取磁盘特性和发送控制命令的通道。它通过cmd参数来区分操作。

DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void* buff) { if (pdrv != 0) return RES_PARERR; switch (cmd) { case GET_SECTOR_COUNT: // 获取总扇区数 *(LBA_t*)buff = SDCardInfo.CardCapacity / 512; // 计算扇区数 return RES_OK; case GET_SECTOR_SIZE: // 获取扇区大小(字节) *(WORD*)buff = 512; // 我们假设是512 return RES_OK; case GET_BLOCK_SIZE: // 获取擦除块大小(扇区数),对Flash有用 *(DWORD*)buff = 1; // 对于SD卡,可以设为1 return RES_OK; case CTRL_SYNC: // 同步命令,确保缓存数据落盘 // 对于SD卡,如果底层有缓存,这里应确保缓存写入物理介质。 // 简单实现可以直接返回RES_OK。 return RES_OK; // 还有其他命令,如CTRL_TRIM(用于SSD),可根据需要实现 default: return RES_PARERR; } }

实现好这五个函数,并正确配置ffconf.h中的选项(如支持长文件名、使用动态工作区等),FatFS的移植就算完成了。你可以写一个简单的测试程序,调用f_mount来挂载磁盘,如果返回FR_OK,恭喜你,最难的一关已经过了。

3. 文件操作核心函数详解:从打开到关闭的完整生命周期

挂载成功之后,我们终于可以进入文件操作的世界。FatFS的函数返回值都是FRESULT类型,一个枚举值。养成习惯,每次调用后都检查返回值,这是调试时最直接的线索。

3.1 挂载与卸载:文件系统的入口与出口

在操作任何文件之前,必须挂载(Mount)一个逻辑驱动器到FatFS的工作区。

FATFS fs; // 文件系统对象,需要用户定义 FRESULT res = f_mount(&fs, "0:", 1); // 挂载物理驱动器0到逻辑路径"0:"
  • 第一个参数&fs:一个FATFS类型的结构体变量。它包含了该文件系统的所有状态信息(如当前目录、空闲簇链等)。这个变量必须在整个挂载周期内有效,通常是全局变量或静态变量。
  • 第二个参数"0:":逻辑驱动器路径。冒号是必须的。"0:"对应diskio.c中的pdrv=0。你可以挂载多个驱动器,如"1:"
  • 第三个参数1:挂载选项。1表示立即挂载(强制)。0表示延迟挂载,只在首次访问该驱动器时才执行挂载操作,可以加快启动速度。

经验之谈:对于SD卡这种可能热插拔的设备,我倾向于在每次进行文件操作前都尝试挂载(如果之前已挂载则先卸载f_mount(NULL, path, 0)),而不是一直保持挂载状态。这样可以更灵活地处理卡被拔出的情况。但频繁挂载卸载会有性能开销,需权衡。

卸载使用相同的函数,但第一个参数传NULL

f_mount(NULL, "0:", 0); // 卸载"0:"驱动器

卸载会同步文件系统缓存,确保数据写回磁盘,是个好习惯。

3.2 打开与创建:f_open的多种模式

f_open是文件操作的起点,它决定了你将以何种方式与文件交互。

FIL fil; // 文件对象,类似标准C的FILE* FRESULT res = f_open(&fil, "0:/data/log.txt", FA_READ | FA_OPEN_EXISTING);
  • 第一个参数&fil:文件对象指针,后续的读、写、定位操作都基于这个对象。
  • 第二个参数路径:支持绝对路径(0:/dir/file)和相对路径(./file,相对于当前目录)。
  • 第三个参数模式:这是重中之重,由多个标志位组合而成。

模式标志位解析:

  • FA_READ:以只读方式打开。不能写。
  • FA_WRITE:以只写方式打开。不能读。注意FA_READ | FA_WRITE读写模式,这是最常用的。
  • FA_OPEN_EXISTING:打开已存在的文件。如果文件不存在,则返回FR_NO_FILE
  • FA_CREATE_NEW:创建新文件。如果文件已存在,则返回FR_EXIST这是原子性的,适合创建唯一文件
  • FA_CREATE_ALWAYS:总是创建。如果文件存在,会截断它(清空内容);如果不存在,则创建。小心!这会无条件清空原有文件。
  • FA_OPEN_ALWAYS:总是打开。如果文件存在,打开它(读写指针在文件头);如果不存在,则创建。它不会清空已有文件,适合“读取或追加”的场景。
  • FA_OPEN_APPEND:同FA_OPEN_ALWAYS,但打开后读写指针自动定位到文件末尾。这是追加写的便捷模式。

组合示例与典型用途:

  • 读取配置文件FA_READ | FA_OPEN_EXISTING
  • 创建新的日志文件(确保不覆盖)FA_WRITE | FA_CREATE_NEW
  • 追加日志到文件(最常用)FA_WRITE | FA_OPEN_APPENDFA_WRITE | FA_OPEN_ALWAYS(打开后需手动f_lseek到末尾)。
  • 读写一个数据库文件FA_READ | FA_WRITE | FA_OPEN_ALWAYS

一个常见的坑:试图用FA_WRITE | FA_OPEN_EXISTING模式去写一个不存在的文件,会返回FR_NO_FILE。如果你不确定文件是否存在,又想写入,应该使用FA_WRITE | FA_OPEN_ALWAYS

3.3 数据读写:f_read与f_write的细节

打开文件后,读写操作相对直观,但缓冲区管理和字节数处理有讲究。

f_read – 读取数据

UINT br; // 实际读取到的字节数 char buffer[128]; res = f_read(&fil, buffer, sizeof(buffer), &br); if (res == FR_OK) { // 成功读取了 br 个字节到 buffer 中 // 注意:br 可能小于 sizeof(buffer),这表示读到了文件末尾 }
  • 关键参数&br:这是一个输出参数,函数会把你实际成功读取的字节数填到这里。必须检查这个值,它可能小于你请求的字节数(比如文件剩余内容不足)。
  • 文件指针:每次成功读取后,文件内部的读写指针会自动向后移动br字节。下次读操作会从新的位置开始。

f_write – 写入数据

UINT bw; // 实际写入的字节数 const char *data = "Hello, FatFS!\n"; res = f_write(&fil, data, strlen(data), &bw); if (res == FR_OK && bw == strlen(data)) { // 成功写入了全部数据 } else { // 写入失败或只写入部分数据(如磁盘满) }
  • 立即写入?不!这里有一个极其重要的机制:f_write通常只是将数据写入FatFS内部的缓存,或者单片机自己的内存缓冲区,并没有立即写入物理磁盘。这是为了性能。如果此时突然断电,这部分数据就丢失了。
  • 确保落盘:有两种方法强制数据写回磁盘:
    1. f_sync(&fil);:同步这个特定文件对象的缓存。
    2. f_close(&fil);:关闭文件时,会自动调用f_sync
  • 磁盘满处理f_write可能因为磁盘空间不足而只写入部分数据(bw < 请求字节数)。健壮的程序需要检查bw并处理。FatFS的f_write在空间不足时会返回FR_DISK_FULL,但在这之前可能已经写入了部分数据。

3.4 文件指针操控:f_lseek与f_truncate

  • f_lseek – 移动读写指针
// 将读写指针移动到文件开头后第1024个字节处 res = f_lseek(&fil, 1024); // 移动到文件末尾,常用于追加 res = f_lseek(&fil, f_size(&fil));

这个函数对于随机访问文件(如存储结构体记录)非常有用。注意,你可以将指针移动到超过当前文件大小的位置,但这不会立即改变文件大小。只有随后执行了f_write,文件才会被扩展到指针所在的位置,中间的空隙会被填充为0。

  • f_truncate – 截断文件
// 将文件大小设置为当前读写指针的位置(如果指针在中间,则后面的内容被丢弃) res = f_truncate(&fil);

常用于快速清空文件内容(先f_lseek(&fil, 0),再f_truncate(&fil)),或者缩小文件。

3.5 关闭与同步:善始善终

操作完成后,必须关闭文件。

res = f_close(&fil);

f_close会做三件事:

  1. 将文件对象内部缓冲区中未写入的数据同步到磁盘(调用f_sync)。
  2. 更新文件的目录项信息(如文件大小、修改时间)。
  3. 释放文件对象关联的资源。

务必关闭每一个打开的文件对象。在资源紧张的嵌入式系统中,未关闭的文件对象会占用内存,并可能导致后续文件操作失败(比如达到最大打开文件数限制)。对于需要频繁写入确保数据安全的场景(如日志),可以在每次f_write后手动调用f_sync(&fil),但这会严重影响性能。

4. 目录操作与文件信息管理

除了文件,管理目录和获取文件属性也是常见需求。

4.1 目录的创建、删除与遍历

  • 创建目录f_mkdir("0:/myfolder")
    • 可以创建多级目录,但父目录必须存在。如果想创建0:/a/b/c,而/a/b不存在,会失败。需要逐级创建或使用FA_FATFSFF_USE_MKFSf_mkfs之外的更高级功能(通常没有)。
  • 删除目录f_unlink("0:/myfolder")f_rmdir("0:/myfolder")
    • 目录必须为空才能删除。f_unlink也可用于删除文件,f_rmdir专用于目录。
  • 打开和遍历目录:这需要用到DIR对象和FILINFO结构体。
DIR dir; FILINFO fno; res = f_opendir(&dir, "0:/"); // 打开根目录 if (res == FR_OK) { for (;;) { res = f_readdir(&dir, &fno); // 读取一个目录项 if (res != FR_OK || fno.fname[0] == 0) break; // 错误或读完 // fno.fname 是文件名(短文件名格式) // 如果启用了长文件名(FF_USE_LFN),长文件名在 fno.altname 或 fno.fname 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结构体还包含了文件大小(fsize)、最后修改日期时间(fdate,ftime)、属性(fattrib)等信息。属性是一个字节,其中各位表示:AM_RDO(只读)、AM_HID(隐藏)、AM_SYS(系统)、AM_DIR(目录)、AM_ARC(存档)。

4.2 获取与设置文件信息

  • 获取文件状态f_stat("0:/file.txt", &fno)
    • 无需打开文件,直接获取其FILINFO。常用于检查文件是否存在、获取大小。
  • 重命名/移动f_rename("0:/old.txt", "0:/new.txt")
    • 可以在同一目录下重命名,也可以跨目录移动(如果目标目录存在)。不能跨驱动器移动
  • 修改属性f_chmod("0:/file.txt", AM_RDO, AM_RDO)将文件设为只读。
    • 第二个参数是要设置的属性位,第三个参数是要清除的属性位掩码。

4.3 长文件名(LFN)支持的坑与配置

FatFS默认使用经典的8.3短文件名格式(如LOGFILE~1.TXT)。要支持长文件名,需要在ffconf.h中开启FF_USE_LFN,并设置为1(栈存储)或2(堆存储)。开启后,fno.fname直接就是长文件名。

但是,这里有巨坑!

  1. 编码问题:长文件名默认使用OEM代码页(通常是西文)。要支持中文等双字节字符,需要设置FF_CODE_PAGE为合适的值(如简体中文是936),并确保ffsystem.c中包含了对应的编码转换表(cc936.c)。这会显著增加代码体积。
  2. 内存消耗:LFN会消耗更多RAM,因为每个FILINFO对象和文件对象内部都需要缓冲区来存储长名。FF_USE_LFN=2时,FatFS会用malloc动态分配内存,在无操作系统的环境下要确保堆大小足够。
  3. 性能影响:读写带有长文件名的目录时,速度会变慢。

建议:在资源紧张的嵌入式系统中,如果不需要用户直接通过文件名交互,可以关闭LFN以节省资源。如果需要,务必仔细测试中文字符的创建、读取和显示。

5. 高级话题与实战避坑指南

掌握了基本操作,我们来看看那些容易让人栽跟头的高级问题和优化技巧。

5.1 文件系统格式化:最后的武器

当SD卡损坏或需要清空所有数据时,你可能需要格式化。FatFS提供了f_mkfs函数。

// 格式化驱动器0:,使用默认参数(FAT32,自动簇大小) res = f_mkfs("0:", FM_FAT32, 0, workbuf, sizeof(workbuf));
  • 警告:格式化会擦除磁盘上所有数据!务必谨慎,最好有用户确认或仅在出厂设置中使用。
  • 参数详解
    • FM_FAT32:指定文件系统类型。还有FM_FAT,FM_EXFAT等。
    • 0:簇大小(扇区数),0表示自动选择。
    • workbuf:工作缓冲区,大小至少为FF_MAX_SS(通常512)。这个缓冲区在格式化期间使用,必须是4字节对齐的,且生命周期覆盖整个f_mkfs调用。
  • 格式化失败常见原因
    1. 磁盘未初始化或硬件错误。
    2. 磁盘有写保护。
    3. 工作缓冲区太小或未对齐。
    4. 选择的文件系统类型与磁盘容量不兼容(如小容量SD卡强行用exFAT)。

5.2 断电安全与事务保护

嵌入式设备最怕突然断电。FatFS本身不是为事务安全设计的数据库,但我们可以通过一些策略来提升数据安全性。

策略一:关键数据后立即同步对于非常重要的配置或状态文件,在f_write后立即调用f_sync()。虽然慢,但安全。

f_write(&fil, critical_data, sizeof(critical_data), &bw); if (bw == sizeof(critical_data)) { f_sync(&fil); // 确保数据落盘 }

策略二:写前备份,原子替换这是更健壮的方法,适用于更新配置文件。

  1. 将新内容写入一个临时文件(如config.tmp)。
  2. 对临时文件调用f_sync(),确保数据写入物理介质。
  3. 删除旧文件(config.cfg)。
  4. 重命名临时文件为正式文件名(f_rename("0:/config.tmp", "0:/config.cfg"))。

在FAT文件系统上,重命名是一个相对原子操作(如果断电发生在重命名过程中,最多是临时文件和正式文件同时存在或都不存在,不会产生半截文件)。但注意,删除旧文件后,重命名前如果断电,数据就丢了。更复杂的方案需要引入版本号或校验和。

策略三:启用FF_FS_READONLY以外的全功能ffconf.h中,确保FF_FS_READONLY == 0以启用写功能,同时FF_FS_MINIMIZE级别不要设得太高,以免关闭了必要的函数。

5.3 性能优化技巧

  1. 使用合适的簇大小:在格式化时,簇大小影响空间利用率和读写速度。大簇(如32KB)适合存储大文件,连续读写快,但小文件浪费空间。小簇节省空间,但读写碎片化文件可能慢。FatFS自动选择通常不错。
  2. 批量读写:尽量一次读写多个扇区的数据,减少disk_read/disk_write的调用次数。在应用层,可以用较大的缓冲区(如4KB)进行f_read/f_write
  3. 避免频繁打开关闭:如果需要对一个文件进行多次操作,保持打开状态,而不是每次操作都f_openf_close
  4. 关闭不需要的功能:在ffconf.h中,根据需求关闭FF_USE_LFNFF_USE_FINDFF_USE_MKFSFF_USE_STRFUNC等,可以节省代码空间和RAM。

5.4 常见错误码(FRESULT)解析与调试

  • FR_OK (0): 成功。
  • FR_DISK_ERR: 底层磁盘I/O错误。检查disk_read/disk_write的实现,SD卡连接是否可靠,电源是否稳定。
  • FR_INT_ERR: FatFS内部断言失败。通常意味着文件系统结构损坏,或者你在文件未关闭时进行了非法操作(如卸载文件系统)。
  • FR_NOT_READY: 磁盘驱动未初始化或介质未就绪。检查disk_initializedisk_status
  • FR_NO_FILE: 文件未找到。检查路径和文件名是否正确,模式是否用了FA_OPEN_EXISTING但文件不存在。
  • FR_NO_PATH: 路径未找到。检查目录是否存在。
  • FR_INVALID_NAME: 文件名无效。可能是包含了非法字符(如:*?"<>|),或者路径格式错误。
  • FR_DENIED: 操作被拒绝。原因可能是:试图删除非空目录、在只读模式下尝试写、磁盘写保护、文件系统已满。
  • FR_EXIST: 文件已存在。当使用FA_CREATE_NEW模式打开一个已存在的文件时返回。
  • FR_INVALID_OBJECT: 文件/目录对象无效。可能是未初始化的FIL/DIR变量,或文件已关闭。
  • FR_WRITE_PROTECTED: 磁盘被写保护。
  • FR_INVALID_DRIVE: 驱动器号无效。检查f_mount的路径参数。
  • FR_NOT_ENABLED: 功能未启用。例如调用了f_mkfsFF_USE_MKFS未定义。
  • FR_NO_FILESYSTEM: 磁盘上没有找到有效的FAT卷。可能是未格式化,或分区表损坏。

调试方法

  1. 逐层排查:从应用层(f_open失败)到底层(disk_read失败)。先用f_mount测试底层驱动是否正常。
  2. 简化测试:创建一个最简单的程序,只做挂载、打开文件、写字符串、关闭文件。排除其他代码干扰。
  3. 检查堆栈:FatFS的函数调用可能较深,确保单片机的堆栈空间足够,特别是启用长文件名和使用动态内存时。
  4. 逻辑分析仪/调试器:对于SPI SD卡,用逻辑分析仪抓取SPI波形,检查命令和响应是否符合SD协议。对于SDIO,可以单步调试底层驱动。

6. 实战案例:构建一个简单的数据记录器

让我们把这些知识点串起来,设计一个用于环境监测的简易数据记录器。需求是:每小时将时间戳、温度、湿度写入SD卡的一个CSV文件,每天生成一个新文件,要求断电后数据不丢失(至少是上一次成功写入的数据)。

6.1 硬件与软件规划

  • 硬件:STM32F4 + SD卡(SPI模式) + RTC(实时时钟) + 温湿度传感器。
  • 软件:FatFS (R0.15) + FreeRTOS(可选,用于任务管理)。
  • 文件命名规则LOG_20240515.CSV(年月日)。
  • 数据格式2024-05-15 14:30:00, 25.6, 45.2\n

6.2 核心代码实现

首先,在系统初始化时挂载文件系统:

FATFS fs; FRESULT fr; // 初始化SD卡硬件... fr = f_mount(&fs, "0:", 1); if (fr != FR_OK) { printf("Mount error: %d\n", fr); // 可能需要尝试格式化或报错 return; }

每小时执行一次的记录任务函数:

void log_sensor_data(void) { FIL fil; UINT bw; FRESULT fr; char filename[30]; char buffer[100]; RTC_TimeTypeDef sTime; RTC_DateTypeDef sDate; // 1. 获取当前日期时间 HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN); HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN); // 2. 生成当天日志文件名 sprintf(filename, "0:/LOG_%04d%02d%02d.CSV", sDate.Year + 2000, sDate.Month, sDate.Date); // 3. 打开文件(追加模式) fr = f_open(&fil, filename, FA_WRITE | FA_OPEN_APPEND); if (fr != FR_OK) { // 如果文件不存在,可能是今天第一次写,用CREATE_ALWAYS创建(但APPEND应该也能创建) // 更稳妥:可以先f_stat检查,不存在则用CREATE_NEW打开一次然后关闭,再用APPEND打开。 // 这里简单处理,如果APPEND失败,尝试用CREATE_ALWAYS fr = f_open(&fil, filename, FA_WRITE | FA_CREATE_ALWAYS); if (fr != FR_OK) { printf("Open/Create file failed: %d\n", fr); return; } // 如果是新创建的文件,可以写入CSV表头 f_puts("Timestamp,Temperature(C),Humidity(%)\n", &fil); } // 4. 构造数据行 // 假设获取了温度temp和湿度hum float temp = read_temperature(); float hum = read_humidity(); int len = sprintf(buffer, "%04d-%02d-%02d %02d:%02d:%02d, %.1f, %.1f\n", sDate.Year + 2000, sDate.Month, sDate.Date, sTime.Hours, sTime.Minutes, sTime.Seconds, temp, hum); // 5. 写入数据 fr = f_write(&fil, buffer, len, &bw); if (fr != FR_OK || bw != len) { printf("Write failed or incomplete: fr=%d, bw=%u\n", fr, bw); f_close(&fil); return; } // 6. 关键步骤:同步到磁盘,确保断电安全 fr = f_sync(&fil); if (fr != FR_OK) { printf("Sync failed: %d\n", fr); } // 7. 关闭文件 fr = f_close(&fil); if (fr != FR_OK) { printf("Close failed: %d\n", fr); } printf("Data logged successfully.\n"); }

6.3 优化与健壮性改进

  1. 错误恢复:如果f_openf_write因SD卡拔出失败,可以在函数中加入重试逻辑,或者设置一个故障标志,在主循环中尝试重新初始化SD卡和挂载文件系统。
  2. 文件大小管理:每天一个文件可能变得很大。可以在f_open后检查文件大小f_size(&fil),如果超过某个阈值(如1MB),则关闭当前文件,用一个新的带序号的文件(如LOG_20240515_002.CSV)继续写入。
  3. 缓冲区刷新:我们每次写入后都调用f_sync,这很安全但费电、磨损SD卡、速度慢。对于电池供电设备,可以改为每写入10条记录同步一次,或者在进入低功耗模式前同步。这需要在数据安全性和功耗/寿命之间权衡。
  4. 日期变更处理:如果设备在午夜运行,需要检测日期变化并切换到新文件。可以在每次记录前检查当前日期,如果与上次记录日期不同,则关闭旧文件(如果还开着),用新日期创建文件。

这个案例涵盖了FatFS的大部分核心操作:挂载、打开(多种模式)、写入、同步、关闭。在实际项目中,你还需要考虑SD卡热插拔检测、文件系统损坏时的恢复(如尝试f_mkfs)等更复杂的情况。FatFS是一个强大的工具,理解其原理和细节,能让你在嵌入式存储方案中游刃有余。

http://www.jsqmd.com/news/1305946/

相关文章:

  • 终极Wand-Enhancer实战指南:5步掌握WeMod专业版功能解锁与远程控制
  • 倒计时一天!智源/TileRT/腾讯/华为/智元创新多方集结,共探 AI 编译的多层级协同优化
  • 2026年7月北京苹果手机维修服务指南|iPhone全系进水、屏幕、电池、主板原装检修 - 苹果手机品牌电脑维修
  • DLP4500 EVM实战指南:从硬件连接到光学投影的常见问题与解决方案
  • 英雄联盟Akari助手:3分钟快速上手的游戏自动化工具
  • 编程用哪个AI大模型好?实测GPT-5.6和Claude的真实体验
  • Slurm作业调度实战:从sbatch到sacct的完整生命周期管理
  • 2026年2月28日周六时间管理全攻略
  • 2026年 上海小件行李搬运配送团队推荐榜单:同城/跨城速运,专业打包与安心守护优选 - 优企名品
  • 如何用MZmine3免费开源质谱数据分析软件加速你的科研发现
  • 天猫店群自动化管理系统:综合代码架构自愈,异常自动恢复不中断
  • 手机卡托又薄又小还高光,嘉腾闪测仪把多参数检测做到秒级批量完
  • 含金量高财务岗位证书有哪些?2026年财务人考证与职业升级指南
  • PrimeTime静态时序分析:get_cells命令深度解析与应用实战
  • 三维设计云桌面方案
  • 大麦抢票脚本完整指南:告别手动抢票的终极解决方案
  • 巴中水电维修怎么找正规平台?资质报价验收三步筛选(2026) - 家修助手
  • 工业级端侧健康监测算法落地实践:从PPG信号处理到低功耗推理优化
  • 恭城县外墙漏水维修_2026桂北恭城瑶乡城市漏水维修价格行情与靠谱吗 - 雨婺虹房屋维修
  • 模糊控制与神经模糊在车辆导航中的MATLAB实现
  • 2026即墨区家装防水修缮商家盘点:漏水维修避坑指南与本土服务商横向解析 - 国麟测评
  • 平均延迟上升 9% 就回滚?多维度数据可视化揭示 Web 服务缓存更新真相
  • 上海生产管理系统服务商:专业服务助力企业高效运营
  • AI CRM怎么选?从销帮帮CRM看业务融合型AI的评估逻辑
  • 深入解析Cyclone IV FPGA内部架构:从逻辑单元到时钟网络的工程实践指南
  • GPT-5.6 Sol资源优化:Codex限额重置下的工程实践指南
  • Windows系统下Neo4j图数据库安装配置与排错全指南
  • # 2026年8月肇庆非急救医疗转运行业发展解析及合规企业服务实录 - 平台推荐官
  • 终极PS Vita管理助手:QCMA完整使用指南
  • 2026年口碑好的ISO9001质量管理体系认证咨询机构推荐! - 优质品牌商家