【STM32】四万字详解最新版本(ff16)FatFs文件系统移植(库函数版本)
本文详细介绍了FatFs文件系统的移植与应用过程。首先阐述了文件系统的基本概念及其优势,包括数据定位、空间管理和格式解析等功能。随后重点讲解了FatFs文件系统的特点、配置方法和移植步骤,包括ffconf.h的参数配置和diskio.c的驱动实现。通过实际案例演示了在STM32平台上对W25Q64 Flash芯片的文件系统初始化、格式化、文件读写等操作。文章提供了完整的代码实现和详细的参数说明,特

目录
2.2.2.2 初始化磁盘驱动器——disk_initialize
2.2.2.5 控制设备实现指定功能——disk_ioctl
1. 文件系统
1.1 简介
文件系统是操作系统 / 嵌入式系统中管理存储设备(硬盘、Flash、SD 卡等)数据的逻辑层,本质是一套 “将物理存储抽象为可理解的文件 / 目录结构,并定义数据读写、空间管理、容错规则” 的规范与软件实现。
简单来说:没有文件系统的存储设备只是一堆 “裸数据块”,文件系统则为其建立了 “文件名、路径、层级、属性” 等人类 / 程序可识别的规则,是物理存储与上层应用之间的 “翻译官”。
我们为什么要进行文件系统呢?这样做有什么好处呢?
1.2 优点
1.2.1 记录有效数据的位置
举个例子,假如我们使用常规 Flash 存储(裸机直接操作)直接存储的数据,可以发现直接存储的数据是直接操作 Flash 的物理扇区 / 块,通过地址偏移读写数据,数据位置全靠开发者手动记录,如果我们想要进行数据增删 / 地址修改,就需要关联代码需同步改地址,维护成本极高,并且如果我们想要定位 “某份日志存在哪个地址”,需逐地址读取验证,不能快速定位。
而如果使用文件系统,其可以通过文件名 + 路径替代物理地址,数据位置由文件系统内核自动管理,无需人工记录,就相当于你给存储的数据起一个名称,然后将所有名称汇总到一起形成一个目录,当新增 / 删除文件时无需修改任何地址配置,文件系统自动更新数据位置映射:

1.2.2 确定存储介质的剩余空间
假如我们想要快速获取总空间 / 已用空间,就需要手动实现 “扇区占用状态检测”,遍历所有地址判断已用 / 空闲空间,区分有效数据 / 无效数据,并且对于碎片化空间无法统计(如某扇区用了 100 字节,剩余 3996 字节无法复用,裸机难以识别)。
文件系统内置空间管理模块,提供标准化 API 直接获取剩余空间,无需手动遍历,并且可以自动统计碎片化空间,跨存储介质兼容(SD 卡 / SPI Flash / 片内 Flash 均用同一套 API)。
1.2.3 确应以何种格式来解读数据
裸机存储的是 “裸字节流”,无任何格式标记,读取时需手动约定解析规则,一旦规则丢失 / 修改,数据无法解读,例如我们举个例子:
// 裸机写入混合数据(无格式标记)
uint8_t buf[64] = {0};
// 前4字节:设备ID;接下来2字节:温度;接下来10字节:时间
*(uint32_t*)buf = 0x12345678; // 设备ID
*(uint16_t*)(buf+4) = 25; // 温度
memcpy(buf+6, "2025-12-16", 10); // 时间
spi_flash_write(0x00010000, buf, 64);
// 读取时需严格按约定解析,否则数据错乱
uint8_t read_buf[64] = {0};
spi_flash_read(0x00010000, read_buf, 64);
uint32_t dev_id = *(uint32_t*)read_buf; // 必须按“前4字节”解析
uint16_t temp = *(uint16_t*)(read_buf+4); // 必须按“第5-6字节”解析
char *time = (char*)(read_buf+6); // 必须按“第7-16字节”解析
// 若约定规则改变(如温度改4字节),所有解析代码需同步修改,否则数据解读错误
文件系统通过 文件名 + 文件属性 + 标准化格式 明确数据解读规则,甚至可通过文件扩展名直接定义格式,如:
// 1. 按文件扩展名定义数据格式,直观易解读
// 配置文件(JSON格式):明确数据结构,解析规则可通用
f_open(&file, "0:config/device_cfg.json", FA_WRITE | FA_CREATE_ALWAYS);
char cfg_json[] = "{\"dev_id\":0x12345678, \"temp\":25, \"time\":\"2025-12-16\"}";
f_write(&file, cfg_json, strlen(cfg_json), &bw);
// 日志文件(TXT格式):纯文本,直接可读
f_open(&file, "0:log/run_log.txt", FA_WRITE | FA_CREATE_ALWAYS);
char log_txt[] = "2025-12-16 设备ID:0x12345678 温度:25℃";
f_write(&file, log_txt, strlen(log_txt), &bw);
// 二进制数据(BIN格式):通过文件名备注格式
f_open(&file, "0:data/sensor_data.bin", FA_WRITE | FA_CREATE_ALWAYS);
f_write(&file, bin_data, bin_len, &bw);
// 2. 读取时按格式解析,规则清晰
// JSON配置文件:用通用JSON解析库解析,无需手动约定字节位置
f_open(&file, "0:config/device_cfg.json", FA_READ);
f_read(&file, read_buf, sizeof(read_buf), &br);
cJSON *root = cJSON_Parse((char*)read_buf); // 通用解析规则,跨设备通用
uint32_t dev_id = cJSON_GetObjectItem(root, "dev_id")->valueint;
uint16_t temp = cJSON_GetObjectItem(root, "temp")->valueint;
// TXT日志文件:直接打印/读取,无需解析
f_open(&file, "0:log/run_log.txt", FA_READ);
f_read(&file, read_buf, sizeof(read_buf), &br);
printf("日志内容:%s\n", read_buf); // 直接可读,无格式歧义
1.3 流程
为了方便理解文件系统,我们举个例子:
首先,我们来看一张磁盘分区表,我们下面的设计也是类比此表来的:

假定现在有一个空的完全没有存放数据的磁盘,大小为 100KB,我们将其想象为线形的空间地址。为了存储管理上的便利,我们人为的将这 100KB的空间均分成 100 份,每份 1KB。我们来依次存储这样几个文件:A.TXT(大小10KB),B.TXT(大小 53.6KB),C.TXT(大小 20.5KB)。
首先能够想到,我们可以顺序的在这 100KB空间中存放这 3 个文件。同时不要忘了,我们还要记下他们的大小和开始的位置,这样下次要用时才能找的到,这就像是目录。
为了便于查找,我们假定用第 1K的空间来存储他们的特征(属性)。还有,我们设计的存储单位是 1KB,所以,A.TXT我们需要 10 个存储单位(簇或者说扇区),B.TXT需要 54 个簇,C.TXT需要 21 个簇:

我们再考虑如何来写这三个文件的目录。对于每个文件而言,一定要记录的有:文件名,开始簇,大小,创建日期、时间,修改日期、时间,文件的读写属性等。想象一下,这里大小能不能用结束簇来计算呢?
一定不能,因为文件的大小不一定就是整数个簇的大小,否则的话像 B.TXT 的内容就是 54KB 的内容了(实际内容只有53.6KB,有一些是无效空间),少了固然不行,可多了也是不行的。那么我们怎么记录呢?
可以想象一下,为了管理上的方便,我们用数据库的管理方式来管理我们的目录。于是我把 1KB 再分成 10 份,假定开始簇号为 0,定义每份 100B 的各个位置的代表含义如图

设计完成后,我们来进行一些删除操作,把 B.txt 删了,b.txt 的空间随之释放。这时候空间如图:


在存入一个文件 D.txt(大小为 60.3KB),总共 100 簇的空间只用了 31 簇,还有 68 簇剩余,但是我们可以看出此时剩余空间已经没有61KB大小的完整空间了,这时候要怎么办呢?
首先我们允许文件的不连续存储。目录中依然只记录开始簇和文件的大小。那么我们怎么记录文件占用那些簇呢,以文件映射簇不太方便,因为文件名是不固定的。我们换个思想,可以用簇来映射文件,在整个存储空间的前部留下几簇来记录数据区中数据与簇号的关系。对于上例因为总空间也不大,所以用前部的 1Kb 的空间来记录这种对应,假设删除前 3 个文件都存储,空间分配如图:


第一簇用来记录数据区中每一簇的被占用情况,暂时称其为文件分配表。结合文件分配表和文件目录就可以达到完全的文件读取了。我们想到,把文件分配表做成一个数据表,如图:

用上图的组织方式是完全可以实现对文件占有簇的记录的。但还不够效率。比如文件名在文件分配表中记录太多,浪费空间,而实际上在目录中已经记录了文件的开始簇了。所以可以改良一下,用链的方式来存放占有簇的关系,如图:

这样做有什么意义呢?
如文件 A.txt 我们根据目录项中指定的 A.txt 的首簇为 2,然后找到文件分配表的第 2 簇记录,上面登记的是 3,我们就能确定下一簇是 3。找到文件分配表的第 3 簇记录,上面登记的是 4,我们就能确定下一簇是 4......直到指到第 11 簇,发现下一个指向是FF,就是结束。文件便丝毫无误读取完毕。
那么我们在回去 B.txt 删除的情况,当 B.txt 删除以后,存入一个大小为 60.3KB 的 D.txt 利用上述思路:

此时文件分配表,当我们分配完第 65 簇以后,直接跳过 C.txt 的数据簇,来到第 87 簇,这样就可以完成数据存储:

2. FatFs文件系统
上面我们大概了解了一下什么是文件系统,下面我们来介绍一起其一种用法——FatFs文件系统。
2.1 简介
FatFs 是由日本工程师 ChaN 开发的轻量级、开源、可移植的 FAT 文件系统模块,专为嵌入式系统设计,适配 8/16/32 位 MCU(如 STM32/AT32/51 单片机),核心目标是在资源受限的嵌入式环境中实现 FAT 系列文件系统(FAT12/FAT16/FAT32)的标准化操作。
| 特性 | 详情说明 |
|---|---|
| 跨平台兼容 | 支持 8/16/32 位 MCU,可移植到几乎所有嵌入式系统(STM32/51/ESP32/RTOS 等) |
| FAT 协议支持 | 完整支持 FAT12/FAT16/FAT32,兼容 exFAT(需单独扩展) |
| 资源占用极低 | 代码量约 10KB,RAM 占用仅几百字节(可配置裁剪) |
| 无操作系统依赖 | 支持裸机运行(也可适配 FreeRTOS/RT-Thread 等 RTOS) |
| 多盘符支持 | 可同时挂载多个存储设备(如 SD 卡 + SPI Flash,盘符分别为 "0:"/"1:") |
| 可选功能裁剪 | 支持长文件名(LFN)、中文文件名、时间戳、只读 / 隐藏属性等,可按需开启 / 关闭 |
| 标准化 API | 提供类 POSIX 的文件操作接口(f_open/f_read/f_write/f_close 等),易上手 |
| 容错性 | 支持 FAT 表备份、文件同步(f_sync),降低掉电数据丢失风险 |
那么FatFs文件系统究竟是什么呢?你可以理解为是C语言文件操作的一个封装,如:
fopen 打开一个文件
fclose 关闭一个文件
fgetc 从文件中读取一个字符
fputc 写一个字符到文件中去
fgets 从文件中读取一个字符串
fputs 写一个字符串到文件中去
fprintf 往文件中写格式化数据
fscanf 格式化读取文件中数据
fread 以二进制形式读取文件中的数据
fwrite 以二进制形式写数据到文件中去
getw 以二进制形式读取一个整数
putw 以二进制形式存贮一个整数
feof 文件结束
ferror 文件读/写出错
clearerr 清除文件错误标志
ftell 了解文件指针的当前位置
rewind 反绕
fseek 随机定位
但又不完全是,因为 FatFs 是 “封装”,但分两层,C 语言文件操作封装对应 FatFs 的上层接口层;而 FatFs 更核心的价值是底层对 FAT 协议的封装,两层封装共同构成了完整的 FatFs:
| 封装层级 | 封装对象 | 核心作用 |
|---|---|---|
| 应用层 | 标准文件操作逻辑 | 把 “打开 / 读写 / 关闭文件” 封装为 f_open/f_read/f_write 等 C 语言函数,像操作 PC 文件一样简单 |
| FATFS模块 | FAT 文件系统协议 | 把 FAT 协议的 “簇管理、FAT 表维护、扇区读写” 等复杂逻辑封装成内核,无需开发者手动解析 FAT 表 |
| 底层接口 | 不同存储介质的底层驱动 | 把 SPI Flash/SD 卡 / EMMC 的硬件读写封装为统一的 diskio.c 接口,实现 “一次封装,多硬件适配” |

至于为什么封装呢?就例如我们在使用串口的时候对 printf 的封装,因为 printf 是 C 标准库函数,默认指向 “控制台输出”(PC 的终端),但 STM32 没有原生的 “控制台”,直接调用printf会:要么程序卡死(找不到输出设备);要么输出到空(无任何效果),而我们通过重定向fputc函数(C 标准库的底层输出接口),让printf的内容通过串口发送,本质是 “封装底层硬件操作,适配标准化接口”。
那么我们从哪里获取FatFs文件系统呢?这里有为我们封装好的:
这里我们可以在跳转查看每个函数的使用以及作用等信息:

对于FatFs文件系统,找到图示位置,我们可以下载当前最新版本,点击Changes可以查看版本变更信息:

也可以通过图示位置查找之前版本:

这里我以当前最新版本为例,下载完解压缩如下:

这里主要查看source文件:

对于其中的文件我整理了一个表格:
| 文件名 | 功能名 | 说明 |
|---|---|---|
| ffconf.h | FATFS 模块配置文件 | 根据需求来配置 |
| ff.h | FATFS 和应用模块共用的包含文件 | 不需要修改 |
| ff.c | FATFS 模块源代码(文件系统 API) | 不需要修改 |
| diskio.h | FATFS 和 disk I/O 模块共用的包含文件 | 不需要修改 |
| diskio.c | FATFS 和 disk I/O 模块接口层文件 | 与硬件相关代码 |
| ffunicode.c | FATFS 所支持的字体代码转换表 | 不需要修改 |
| ffsystem.c | FATFS 的 OS 相关函数示例代码 | 没用到 |
2.2 函数讲解
上表可以看出我们实际需要修改的其实就两个文件:ffconf.h 和 diskio.c。
2.2.1 ffconf.h
其代码如下:
/*---------------------------------------------------------------------------/
/ Configurations of FatFs Module
/---------------------------------------------------------------------------*/
#define FFCONF_DEF 80386 /* Revision ID */
/*---------------------------------------------------------------------------/
/ Function Configurations
/---------------------------------------------------------------------------*/
#define FF_FS_READONLY 0
/* This option switches read-only configuration. (0:Read/Write or 1:Read-only)
/ Read-only configuration removes writing API functions, f_write(), f_sync(),
/ f_unlink(), f_mkdir(), f_chmod(), f_rename(), f_truncate(), f_getfree()
/ and optional writing functions as well. */
#define FF_FS_MINIMIZE 0
/* This option defines minimization level to remove some basic API functions.
/
/ 0: Basic functions are fully enabled.
/ 1: f_stat(), f_getfree(), f_unlink(), f_mkdir(), f_truncate() and f_rename()
/ are removed.
/ 2: f_opendir(), f_readdir() and f_closedir() are removed in addition to 1.
/ 3: f_lseek() function is removed in addition to 2. */
#define FF_USE_FIND 0
/* This option switches filtered directory read functions, f_findfirst() and
/ f_findnext(). (0:Disable, 1:Enable 2:Enable with matching altname[] too) */
#define FF_USE_MKFS 0
/* This option switches f_mkfs(). (0:Disable or 1:Enable) */
#define FF_USE_FASTSEEK 0
/* This option switches fast seek feature. (0:Disable or 1:Enable) */
#define FF_USE_EXPAND 0
/* This option switches f_expand(). (0:Disable or 1:Enable) */
#define FF_USE_CHMOD 0
/* This option switches attribute control API functions, f_chmod() and f_utime().
/ (0:Disable or 1:Enable) Also FF_FS_READONLY needs to be 0 to enable this option. */
#define FF_USE_LABEL 0
/* This option switches volume label API functions, f_getlabel() and f_setlabel().
/ (0:Disable or 1:Enable) */
#define FF_USE_FORWARD 0
/* This option switches f_forward(). (0:Disable or 1:Enable) */
#define FF_USE_STRFUNC 0
#define FF_PRINT_LLI 0
#define FF_PRINT_FLOAT 0
#define FF_STRF_ENCODE 0
/* FF_USE_STRFUNC switches string API functions, f_gets(), f_putc(), f_puts() and
/ f_printf().
/
/ 0: Disable. FF_PRINT_LLI, FF_PRINT_FLOAT and FF_STRF_ENCODE have no effect.
/ 1: Enable without LF-CRLF conversion.
/ 2: Enable with LF-CRLF conversion.
/
/ FF_PRINT_LLI = 1 makes f_printf() support long long argument and FF_PRINT_FLOAT = 1/2
/ makes f_printf() support floating point argument. These features want C99 or later.
/ When FF_LFN_UNICODE >= 1 with LFN enabled, string API functions convert the character
/ encoding in it. FF_STRF_ENCODE selects assumption of character encoding ON THE FILE
/ to be read/written via those functions.
/
/ 0: ANSI/OEM in current CP
/ 1: Unicode in UTF-16LE
/ 2: Unicode in UTF-16BE
/ 3: Unicode in UTF-8
*/
/*---------------------------------------------------------------------------/
/ Locale and Namespace Configurations
/---------------------------------------------------------------------------*/
#define FF_CODE_PAGE 932
/* This option specifies the OEM code page to be used on the target system.
/ Incorrect code page setting can cause a file open failure.
/
/ 437 - U.S.
/ 720 - Arabic
/ 737 - Greek
/ 771 - KBL
/ 775 - Baltic
/ 850 - Latin 1
/ 852 - Latin 2
/ 855 - Cyrillic
/ 857 - Turkish
/ 860 - Portuguese
/ 861 - Icelandic
/ 862 - Hebrew
/ 863 - Canadian French
/ 864 - Arabic
/ 865 - Nordic
/ 866 - Russian
/ 869 - Greek 2
/ 932 - Japanese (DBCS)
/ 936 - Simplified Chinese (DBCS)
/ 949 - Korean (DBCS)
/ 950 - Traditional Chinese (DBCS)
/ 0 - Include all code pages above and configured by f_setcp()
*/
#define FF_USE_LFN 0
#define FF_MAX_LFN 255
/* The FF_USE_LFN switches the support for LFN (long file name).
/
/ 0: Disable LFN. FF_MAX_LFN has no effect.
/ 1: Enable LFN with static working buffer on the BSS. Always NOT thread-safe.
/ 2: Enable LFN with dynamic working buffer on the STACK.
/ 3: Enable LFN with dynamic working buffer on the HEAP.
/
/ To enable the LFN, ffunicode.c needs to be added to the project. The LFN feature
/ requiers certain internal working buffer occupies (FF_MAX_LFN + 1) * 2 bytes and
/ additional (FF_MAX_LFN + 44) / 15 * 32 bytes when exFAT is enabled.
/ The FF_MAX_LFN defines size of the working buffer in UTF-16 code unit and it can
/ be in range of 12 to 255. It is recommended to be set 255 to fully support the LFN
/ specification.
/ When use stack for the working buffer, take care on stack overflow. When use heap
/ memory for the working buffer, memory management functions, ff_memalloc() and
/ ff_memfree() exemplified in ffsystem.c, need to be added to the project. */
#define FF_LFN_UNICODE 0
/* This option switches the character encoding on the API when LFN is enabled.
/
/ 0: ANSI/OEM in current CP (TCHAR = char)
/ 1: Unicode in UTF-16 (TCHAR = WCHAR)
/ 2: Unicode in UTF-8 (TCHAR = char)
/ 3: Unicode in UTF-32 (TCHAR = DWORD)
/
/ Also behavior of string I/O functions will be affected by this option.
/ When LFN is not enabled, this option has no effect. */
#define FF_LFN_BUF 255
#define FF_SFN_BUF 12
/* This set of options defines size of file name members in the FILINFO structure
/ which is used to read out directory items. These values should be suffcient for
/ the file names to read. The maximum possible length of the read file name depends
/ on character encoding. When LFN is not enabled, these options have no effect. */
#define FF_FS_RPATH 0
/* This option configures support for relative path feature.
/
/ 0: Disable relative path and remove related API functions.
/ 1: Enable relative path and dot names. f_chdir() and f_chdrive() are available.
/ 2: f_getcwd() is available in addition to 1.
*/
#define FF_PATH_DEPTH 10
/* This option defines maximum depth of directory in the exFAT volume. It is NOT
/ relevant to FAT/FAT32 volume.
/ For example, FF_PATH_DEPTH = 3 will able to follow a path "/dir1/dir2/dir3/file"
/ but a sub-directory in the dir3 will not able to be followed and set current
/ directory.
/ The size of filesystem object (FATFS) increases FF_PATH_DEPTH * 24 bytes.
/ When FF_FS_EXFAT == 0 or FF_FS_RPATH == 0, this option has no effect.
*/
/*---------------------------------------------------------------------------/
/ Drive/Volume Configurations
/---------------------------------------------------------------------------*/
#define FF_VOLUMES 1
/* Number of volumes (logical drives) to be used. (1-10) */
#define FF_STR_VOLUME_ID 0
#define FF_VOLUME_STRS "RAM","NAND","CF","SD","SD2","USB","USB2","USB3"
/* FF_STR_VOLUME_ID switches support for volume ID in arbitrary strings.
/ When FF_STR_VOLUME_ID is set to 1 or 2, arbitrary strings can be used as drive
/ number in the path name. FF_VOLUME_STRS defines the volume ID strings for each
/ logical drive. Number of items must not be less than FF_VOLUMES. Valid
/ characters for the volume ID strings are A-Z, a-z and 0-9, however, they are
/ compared in case-insensitive. If FF_STR_VOLUME_ID >= 1 and FF_VOLUME_STRS is
/ not defined, a user defined volume string table is needed as:
/
/ const char* VolumeStr[FF_VOLUMES] = {"ram","flash","sd","usb",...
*/
#define FF_MULTI_PARTITION 0
/* This option switches support for multiple volumes on the physical drive.
/ By default (0), each logical drive number is bound to the same physical drive
/ number and only an FAT volume found on the physical drive will be mounted.
/ When this feature is enabled (1), each logical drive number can be bound to
/ arbitrary physical drive and partition listed in the VolToPart[]. Also f_fdisk()
/ will be available. */
#define FF_MIN_SS 512
#define FF_MAX_SS 512
/* This set of options configures the range of sector size to be supported. (512,
/ 1024, 2048 or 4096) Always set both 512 for most systems, generic memory card and
/ harddisk, but a larger value may be required for on-board flash memory and some
/ type of optical media. When FF_MAX_SS is larger than FF_MIN_SS, FatFs is
/ configured for variable sector size mode and disk_ioctl() needs to implement
/ GET_SECTOR_SIZE command. */
#define FF_LBA64 0
/* This option switches support for 64-bit LBA. (0:Disable or 1:Enable)
/ To enable the 64-bit LBA, also exFAT needs to be enabled. (FF_FS_EXFAT == 1) */
#define FF_MIN_GPT 0x10000000
/* Minimum number of sectors to switch GPT as partitioning format in f_mkfs() and
/ f_fdisk(). 2^32 sectors maximum. This option has no effect when FF_LBA64 == 0. */
#define FF_USE_TRIM 0
/* This option switches support for ATA-TRIM. (0:Disable or 1:Enable)
/ To enable this feature, also CTRL_TRIM command should be implemented to
/ the disk_ioctl(). */
/*---------------------------------------------------------------------------/
/ System Configurations
/---------------------------------------------------------------------------*/
#define FF_FS_TINY 0
/* This option switches tiny buffer configuration. (0:Normal or 1:Tiny)
/ At the tiny configuration, size of file object (FIL) is reduced FF_MAX_SS bytes.
/ Instead of private sector buffer eliminated from the file object, common sector
/ buffer in the filesystem object (FATFS) is used for the file data transfer. */
#define FF_FS_EXFAT 0
/* This option switches support for exFAT filesystem. (0:Disable or 1:Enable)
/ To enable exFAT, also LFN needs to be enabled. (FF_USE_LFN >= 1)
/ Note that enabling exFAT discards ANSI C (C89) compatibility. */
#define FF_FS_NORTC 0
#define FF_NORTC_MON 1
#define FF_NORTC_MDAY 1
#define FF_NORTC_YEAR 2025
/* The option FF_FS_NORTC switches timestamp feature. If the system does not have
/ an RTC or valid timestamp is not needed, set FF_FS_NORTC = 1 to disable the
/ timestamp feature. Every object modified by FatFs will have a fixed timestamp
/ defined by FF_NORTC_MON, FF_NORTC_MDAY and FF_NORTC_YEAR in local time.
/ To enable timestamp function (FF_FS_NORTC = 0), get_fattime() need to be added
/ to the project to read current time form real-time clock. FF_NORTC_MON,
/ FF_NORTC_MDAY and FF_NORTC_YEAR have no effect.
/ These options have no effect in read-only configuration (FF_FS_READONLY = 1). */
#define FF_FS_CRTIME 0
/* This option enables(1)/disables(0) the timestamp of the file created. When
/ set 1, the file created time is available in FILINFO structure. */
#define FF_FS_NOFSINFO 0
/* If you need to know the correct free space on the FAT32 volume, set bit 0 of
/ this option, and f_getfree() on the first time after volume mount will force
/ a full FAT scan. Bit 1 controls the use of last allocated cluster number.
/
/ bit0=0: Use free cluster count in the FSINFO if available.
/ bit0=1: Do not trust free cluster count in the FSINFO.
/ bit1=0: Use last allocated cluster number in the FSINFO if available.
/ bit1=1: Do not trust last allocated cluster number in the FSINFO.
*/
#define FF_FS_LOCK 0
/* The option FF_FS_LOCK switches file lock function to control duplicated file open
/ and illegal operation to open objects. This option must be 0 when FF_FS_READONLY
/ is 1.
/
/ 0: Disable file lock function. To avoid volume corruption, application program
/ should avoid illegal open, remove and rename to the open objects.
/ >0: Enable file lock function. The value defines how many files/sub-directories
/ can be opened simultaneously under file lock control. Note that the file
/ lock control is independent of re-entrancy. */
#define FF_FS_REENTRANT 0
#define FF_FS_TIMEOUT 1000
/* The option FF_FS_REENTRANT switches the re-entrancy (thread safe) of the FatFs
/ module itself. Note that regardless of this option, file access to different
/ volume is always re-entrant and volume control functions, f_mount(), f_mkfs()
/ and f_fdisk(), are always not re-entrant. Only file/directory access to
/ the same volume is under control of this featuer.
/
/ 0: Disable re-entrancy. FF_FS_TIMEOUT have no effect.
/ 1: Enable re-entrancy. Also user provided synchronization handlers,
/ ff_mutex_create(), ff_mutex_delete(), ff_mutex_take() and ff_mutex_give(),
/ must be added to the project. Samples are available in ffsystem.c.
/
/ The FF_FS_TIMEOUT defines timeout period in unit of O/S time tick.
*/
/*--- End of configuration options ---*/
可以看出都是一些宏定义配置,这里将其功能整理成了一个表格,自行查阅:
| 配置宏定义 | 取值范围 | 默认值 | 功能解释 | 注意事项 |
|---|---|---|---|---|
| 基础功能配置 | ||||
| FF_FS_READONLY | 0/1 | 0 | 读写模式开关:0 = 读写模式(支持所有写操作 API);1 = 只读模式(移除所有写操作 API) | 设为 1 时,f_write/f_unlink 等写操作函数不可用 |
| FF_FS_MINIMIZE | 0/1/2/3 | 0 | 基础 API 精简级别:0 = 全功能;1 = 移除 f_stat/f_mkdir 等;2 = 额外移除目录操作函数;3 = 额外移除 f_lseek | 级别越高,代码体积越小,功能越少 |
| FF_USE_FIND | 0/1/2 | 0 | 过滤目录读取功能开关:0 = 禁用;1 = 启用 f_findfirst/f_findnext;2 = 额外支持 altname 匹配 | 需目录操作功能时启用 |
| FF_USE_MKFS | 0/1 | 0 | 文件系统格式化功能(f_mkfs)开关:0 = 禁用;1 = 启用 | 启用后可在嵌入式端格式化存储介质(如 SD 卡) |
| FF_USE_FASTSEEK | 0/1 | 0 | 快速定位功能开关:0 = 禁用;1 = 启用 | 启用后 f_lseek 效率更高,适合大文件操作 |
| FF_USE_EXPAND | 0/1 | 0 | 文件扩容功能(f_expand)开关:0 = 禁用;1 = 启用 | 用于预分配文件空间 |
| FF_USE_CHMOD | 0/1 | 0 | 文件属性 / 时间修改功能开关:0 = 禁用;1 = 启用 f_chmod/f_utime | 需同时设置 FF_FS_READONLY=0 |
| FF_USE_LABEL | 0/1 | 0 | 卷标操作功能开关:0 = 禁用;1 = 启用 f_getlabel/f_setlabel | 用于读取 / 设置存储介质卷标(如 SD 卡名称) |
| FF_USE_FORWARD | 0/1 | 0 | 数据转发功能(f_forward)开关:0 = 禁用;1 = 启用 | 用于直接将文件数据转发到设备(如 UART/USB),无需缓存 |
| 字符串 / 格式化配置 | ||||
| FF_USE_STRFUNC | 0/1/2 | 0 | 字符串操作 API 开关:0 = 禁用;1 = 启用(无 LF-CRLF 转换);2 = 启用(带 LF-CRLF 转换) | 启用后支持 f_gets/f_puts/f_printf |
| FF_PRINT_LLI | 0/1 | 0 | f_printf 支持 long long 类型开关:0 = 禁用;1 = 启用 | 需 C99 及以上编译器,且 FF_USE_STRFUNC≠0 |
| FF_PRINT_FLOAT | 0/1/2 | 0 | f_printf 支持浮点类型开关:0 = 禁用;1/2 = 启用 | 需 C99 及以上编译器,且 FF_USE_STRFUNC≠0 |
| FF_STRF_ENCODE | 0/1/2/3 | 0 | 字符串 API 编码格式:0 = 当前 CP 的 ANSI/OEM;1 = UTF-16LE;2 = UTF-16BE;3 = UTF-8 | 仅 FF_USE_STRFUNC≠0 且 LFN 启用时生效 |
| 区域 / 命名空间配置 | ||||
| FF_CODE_PAGE | 437/936/950 等 | 932 | 字符编码页:932 = 日文;936 = 简体中文;950 = 繁体中文;0 = 支持所有编码 | 配置错误会导致文件打开失败,中文场景需设为 936 |
| FF_USE_LFN | 0/1/2/3 | 0 | 长文件名(LFN)支持开关:0 = 禁用;1 = 静态缓冲区(BSS);2 = 动态缓冲区(栈);3 = 动态缓冲区(堆) | 启用需添加 ffunicode.c,堆模式需实现内存管理函数 |
| FF_MAX_LFN | 12~255 | 255 | 长文件名缓冲区大小(UTF-16 编码单元) | 建议设 255 以完全支持 LFN 规范,栈模式需注意栈溢出 |
| FF_LFN_UNICODE | 0/1/2/3 | 0 | LFN 启用时 API 字符编码:0 = 当前 CP 的 ANSI/OEM;1 = UTF-16;2 = UTF-8;3 = UTF-32 | 仅 FF_USE_LFN≠0 时生效 |
| FF_LFN_BUF | 数值 | 255 | FILINFO 结构中长文件名缓冲区大小 | 仅 LFN 启用时生效 |
| FF_SFN_BUF | 数值 | 12 | FILINFO 结构中短文件名缓冲区大小 | 仅 LFN 启用时生效 |
| FF_FS_RPATH | 0/1/2 | 0 | 相对路径支持开关:0 = 禁用;1 = 启用(支持。和..);2 = 额外支持 f_getcwd | 启用后可使用相对路径访问文件 |
| FF_PATH_DEPTH | 数值 | 10 | exFAT 卷的最大目录深度 | 仅 FF_FS_EXFAT=1 且 FF_FS_RPATH=1 时生效 |
| 驱动器 / 卷配置 | ||||
| FF_VOLUMES | 1~10 | 1 | 逻辑驱动器(卷)数量 | 如同时挂载 SD 卡和 SPI Flash 需设为 2+ |
| FF_STR_VOLUME_ID | 0/1/2 | 0 | 卷 ID 字符串支持开关:0 = 禁用;1/2 = 启用 | 启用后可使用字符串(如 "SD")代替数字作为盘符 |
| FF_VOLUME_STRS | 字符串数组 | "RAM"... | 逻辑驱动器对应的卷 ID 字符串 | 仅 FF_STR_VOLUME_ID≥1 时生效,数量需≥FF_VOLUMES |
| FF_MULTI_PARTITION | 0/1 | 0 | 多分区支持开关:0 = 禁用;1 = 启用 | 启用后可挂载物理驱动器的多个分区,需实现 VolToPart 数组 |
| FF_MIN_SS/FF_MAX_SS | 512/1024 等 | 512 | 支持的最小 / 最大扇区大小 | 多数场景设为 512,不一致时需实现 disk_ioctl 的 GET_SECTOR_SIZE 命令 |
| FF_LBA64 | 0/1 | 0 | 64 位 LBA 支持开关:0 = 禁用;1 = 启用 | 需同时设置 FF_FS_EXFAT=1,支持大容量存储(>2TB) |
| FF_MIN_GPT | 数值 | 0x10000000 | f_mkfs/f_fdisk 切换为 GPT 分区的最小扇区数 | 仅 FF_LBA64=1 时生效 |
| FF_USE_TRIM | 0/1 | 0 | ATA-TRIM 功能开关:0 = 禁用;1 = 启用 | 需实现 disk_ioctl 的 CTRL_TRIM 命令,优化 SSD/Flash 性能 |
| 系统配置 | ||||
| FF_FS_TINY | 0/1 | 0 | 精简缓冲区配置:0 = 普通模式;1 = 精简模式 | 精简模式下 FIL 对象体积减小,共用 FATFS 的扇区缓冲区 |
| FF_FS_EXFAT | 0/1 | 0 | exFAT 支持开关:0 = 禁用;1 = 启用 | 需同时设置 FF_USE_LFN≥1,禁用 ANSI C (C89) 兼容性 |
| FF_FS_NORTC | 0/1 | 0 | RTC 时间戳支持开关:0 = 启用(需实现 get_fattime);1 = 禁用 | 禁用后文件时间戳固定为 FF_NORTC_* 配置值 |
| FF_NORTC_MON/DAY/YEAR | 数值 | 1/1/2025 | FF_FS_NORTC=1 时的固定月份 / 日期 / 年份 | 仅 FF_FS_NORTC=1 时生效 |
| FF_FS_CRTIME | 0/1 | 0 | 文件创建时间戳开关:0 = 禁用;1 = 启用 | 启用后 FILINFO 可获取文件创建时间 |
| FF_FS_NOFSINFO | 0/1/2/3 | 0 | FAT32 空闲空间计算规则:bit0=1:不依赖 FSINFO;bit1=1:不依赖最后分配簇 | 设为 1 可获取更准确的空闲空间,但首次挂载会扫描整个 FAT 表 |
| FF_FS_LOCK | 0/≥1 | 0 | 文件锁功能开关:0 = 禁用;≥1 = 启用(值为最大同时打开文件数) | 需 FF_FS_READONLY=0,防止重复打开 / 非法操作文件 |
| FF_FS_REENTRANT | 0/1 | 0 | 可重入(线程安全)开关:0 = 禁用;1 = 启用 | 启用需实现 ff_mutex_* 同步函数,FF_FS_TIMEOUT 为超时时间(系统滴答) |
| FF_FS_TIMEOUT | 数值 | 1000 | 可重入模式下的超时时间(单位:系统滴答) | 仅 FF_FS_REENTRANT=1 时生效 |
2.2.2 diskio.c
其代码如下:
/*-----------------------------------------------------------------------*/
/* Low level disk I/O module SKELETON for FatFs (C)ChaN, 2025 */
/*-----------------------------------------------------------------------*/
/* If a working storage control module is available, it should be */
/* attached to the FatFs via a glue function rather than modifying it. */
/* This is an example of glue functions to attach various exsisting */
/* storage control modules to the FatFs module with a defined API. */
/*-----------------------------------------------------------------------*/
#include "ff.h" /* Basic definitions of FatFs */
#include "diskio.h" /* Declarations FatFs MAI */
/* Example: Declarations of the platform and disk functions in the project */
#include "platform.h"
#include "storage.h"
/* Example: Mapping of physical drive number for each drive */
#define DEV_FLASH 0 /* Map FTL to physical drive 0 */
#define DEV_MMC 1 /* Map MMC/SD card to physical drive 1 */
#define DEV_USB 2 /* Map USB MSD to physical drive 2 */
/*-----------------------------------------------------------------------*/
/* Get Drive Status */
/*-----------------------------------------------------------------------*/
DSTATUS disk_status (
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
DSTATUS stat;
int result;
switch (pdrv) {
case DEV_RAM :
result = RAM_disk_status();
// translate the reslut code here
return stat;
case DEV_MMC :
result = MMC_disk_status();
// translate the reslut code here
return stat;
case DEV_USB :
result = USB_disk_status();
// translate the reslut code here
return stat;
}
return STA_NOINIT;
}
/*-----------------------------------------------------------------------*/
/* Inidialize a Drive */
/*-----------------------------------------------------------------------*/
DSTATUS disk_initialize (
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
DSTATUS stat;
int result;
switch (pdrv) {
case DEV_RAM :
result = RAM_disk_initialize();
// translate the reslut code here
return stat;
case DEV_MMC :
result = MMC_disk_initialize();
// translate the reslut code here
return stat;
case DEV_USB :
result = USB_disk_initialize();
// translate the reslut code here
return stat;
}
return STA_NOINIT;
}
/*-----------------------------------------------------------------------*/
/* Read Sector(s) */
/*-----------------------------------------------------------------------*/
DRESULT disk_read (
BYTE pdrv, /* Physical drive nmuber to identify the drive */
BYTE *buff, /* Data buffer to store read data */
LBA_t sector, /* Start sector in LBA */
UINT count /* Number of sectors to read */
)
{
DRESULT res;
int result;
switch (pdrv) {
case DEV_RAM :
// translate the arguments here
result = RAM_disk_read(buff, sector, count);
// translate the reslut code here
return res;
case DEV_MMC :
// translate the arguments here
result = MMC_disk_read(buff, sector, count);
// translate the reslut code here
return res;
case DEV_USB :
// translate the arguments here
result = USB_disk_read(buff, sector, count);
// translate the reslut code here
return res;
}
return RES_PARERR;
}
/*-----------------------------------------------------------------------*/
/* Write Sector(s) */
/*-----------------------------------------------------------------------*/
#if FF_FS_READONLY == 0
DRESULT disk_write (
BYTE pdrv, /* Physical drive nmuber to identify the drive */
const BYTE *buff, /* Data to be written */
LBA_t sector, /* Start sector in LBA */
UINT count /* Number of sectors to write */
)
{
DRESULT res;
int result;
switch (pdrv) {
case DEV_RAM :
// translate the arguments here
result = RAM_disk_write(buff, sector, count);
// translate the reslut code here
return res;
case DEV_MMC :
// translate the arguments here
result = MMC_disk_write(buff, sector, count);
// translate the reslut code here
return res;
case DEV_USB :
// translate the arguments here
result = USB_disk_write(buff, sector, count);
// translate the reslut code here
return res;
}
return RES_PARERR;
}
#endif
/*-----------------------------------------------------------------------*/
/* Miscellaneous Functions */
/*-----------------------------------------------------------------------*/
DRESULT disk_ioctl (
BYTE pdrv, /* Physical drive nmuber (0..) */
BYTE cmd, /* Control code */
void *buff /* Buffer to send/receive control data */
)
{
DRESULT res;
int result;
switch (pdrv) {
case DEV_RAM :
// Process of the command for the RAM drive
return res;
case DEV_MMC :
// Process of the command for the MMC/SD card
return res;
case DEV_USB :
// Process of the command the USB drive
return res;
}
return RES_PARERR;
}
这里的一些函数功能可以直接在其官网点击跳转查看:

2.2.2.1 获取磁盘状态——disk_status
简单整理一下:
| 项目 | 详情说明 | |
|---|---|---|
| 函数作用 | 用于查询指定物理驱动器的当前状态 | |
| 函数原型 | DSTATUS disk_status (BYTE pdrv); | |
| 参数(pdrv) | 用于标识目标设备的物理驱动器编号;单驱动器系统中该值始终为 0 | |
| 返回值 | STA_NOINIT |
含义:设备未初始化,无法正常工作; 置位场景:系统复位、介质移除、disk_initialize 函数执行失败; 清零场景:disk_initialize 函数执行成功; 注意:异步发生的介质变更需捕获并更新该标志,否则自动挂载功能失效;无介质变更检测的系统,需在介质变更后调用 f_mount 显式重新挂载卷 |
| STA_NODISK |
含义:驱动器中无存储介质,没有设备; 注意:不可移除型驱动器该标志始终清零;FatFs 不读取该标志 |
|
| STA_PROTECT |
含义:存储介质处于写保护状态; 注意:无写保护功能的驱动器该标志始终清零;STA_NODISK 置位时该标志无效 |
|
| 返回值类型 | DSTATUS(由多个状态标志组合而成,FatFs 仅关注 STA_NOINIT 和 STA_PROTECT) | |
2.2.2.2 初始化磁盘驱动器——disk_initialize
| 项目 | 详情说明 | |
|---|---|---|
| 函数作用 | 用于初始化存储设备,使其就绪以支持通用的读写操作 | |
| 函数原型 | DSTATUS disk_initialize (BYTE pdrv); | |
| 参数(pdrv) | 用于标识目标设备的物理驱动器编号;单驱动器系统中该值始终为 0 | |
| 返回值 | STA_NOINIT |
函数执行成功时,该标志会被清零;若初始化失败,该标志保持置位状态(代表设备未就绪);其他状态标志含义参考 disk_status 函数 |
| STA_NODISK | 驱动器中无存储介质,没有设备;注意:不可移除型驱动器该标志始终清零;FatFs 不读取该标志 | |
| STA_PROTECT | 存储介质处于写保护状态;注意:无写保护功能的驱动器该标志始终清零;STA_NODISK 置位时该标志无效 | |
| 返回值类型 | DSTATUS(由多个状态标志组合而成,具体状态标志说明可参考 disk_status 函数) | |
| 备注 |
该函数需由 FatFs 模块管控,应用程序在 FatFs 运行期间严禁调用此函数,否则可能破坏卷中的 FAT 结构; 若需重新初始化文件系统,应调用 f_mount 函数而非直接调用此函数 |
|
2.2.2.3 从磁盘驱动器读扇区——disk_read
| 项目 | 详情说明 | |
|---|---|---|
| 函数作用 | 用于从存储设备中读取数据 | |
| 函数原型 | DRESULT disk_read (BYTE pdrv, BYTE* buff, LBA_t sector, UINT count); | |
| 参数 | pdrv | 用于标识目标设备的物理驱动器编号 |
| buff | 指向存储读取数据的字节数组首地址的指针;读取数据的总大小为「扇区大小 × count」字节 | |
| sector | LBA(逻辑块寻址)格式的起始扇区编号;LBA_t 是 DWORD 或 QWORD 的别名,具体取决于配置选项 | |
| count | 需要读取的扇区数量 | |
| 返回值 | RES_OK(0) |
函数执行成功 |
| RES_ERROR | 读取操作期间发生无法恢复的硬件错误 | |
| RES_PARERR | 传入的参数无效 | |
| RES_NOTRDY | 设备尚未完成初始化 | |
| 返回值类型 | DRESULT | |
| 功能说明 |
存储设备(存储卡、硬盘、光盘等)的读写操作以 “扇区” 为基本单位,FatFs 支持 512~4096 字节的扇区大小; 若 FatFs 配置为固定扇区大小(FF_MIN_SS == FF_MAX_SS,最常见场景),通用读写函数仅需适配该固定扇区大小; 若配置为可变扇区大小(FF_MIN_SS < FF_MAX_SS),需在 disk_initialize 执行成功后,通过 disk_ioctl 函数查询介质的实际扇区大小 |
|
| 注意事项(内存地址) |
buff 定义为 BYTE*,其内存地址未必按字边界对齐,若总线架构(如 DMA 控制器)不支持非对齐访问,需在本函数中处理,推荐方案:将字传输转换为字节传输;也可通过限制 f_read 调用方式规避(如避免跨扇区的长读取、保证 buff 地址字对齐); 若 buff 指向 DMA 无法访问的内存(如紧耦合内存 / 栈空间),需采用双缓冲传输,或避免将文件 I/O 缓冲区、FATFS/FIL 结构体定义为栈上局部变量 |
|
| 性能注意事项 | 多扇区读取请求不应拆分为单个扇区事务执行,否则会大幅降低读取吞吐量 | |
2.2.2.4 从磁盘驱动器写扇区——disk_write
| 项目 | 详情说明 | |
|---|---|---|
| 函数作用 | 用于向存储设备写入数据 | |
| 函数原型 | DRESULT disk_write (BYTE pdrv, const BYTE* buff, LBA_t sector, UINT count); | |
| 参数 | pdrv | 用于标识目标设备的物理驱动器编号 |
| buff | 指向待写入字节数组首地址的指针;待写入数据的总大小为「扇区大小 × count」字节 | |
| sector | LBA(逻辑块寻址)格式的起始扇区编号;LBA_t 是 DWORD 或 QWORD 的别名,具体取决于配置选项 | |
| count | 需要写入的扇区数量 | |
| 返回值 | RES_OK (0) | 函数执行成功 |
| RES_ERROR | 写入操作期间发生无法恢复的硬件错误 | |
| RES_WRPRT | 存储介质处于写保护状态 | |
| RES_PARERR | 传入的参数无效 | |
| RES_NOTRDY | 设备尚未完成初始化 | |
| 返回值类型 | DRESULT | |
| 功能说明 |
buff 定义为 BYTE*,其指向的内存地址未必按字边界对齐,相关注意事项可参考 disk_read 函数的说明; 多扇区写入请求(count > 1)不应拆分为单个扇区事务执行,否则会大幅降低文件写入吞吐量; FatFs 允许磁盘控制层实现延迟写入特性:函数返回时无需等待介质写入完成(可在后台执行写入或暂存至回写缓存),写入完成请求通过 disk_ioctl 函数的 CTRL_SYNC 命令触发;实现延迟写入可提升文件系统的写入吞吐量; 函数返回后,buff 指向的写入数据缓冲区将失效 |
|
| 备注 |
应用程序严禁直接调用此函数,否则可能破坏卷中的 FAT 结构; 当 FF_FS_READONLY == 1(FatFs 配置为只读模式)时,无需实现此函数 |
|
2.2.2.5 控制设备实现指定功能——disk_ioctl
用于辅助FATFS中其他功能:
| 项目 | 详情说明 | |
|---|---|---|
| 函数作用 | 用于控制存储设备的专有特性,以及执行通用读写之外的各类杂项功能 | |
| 函数原型 | DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void* buff); | |
| 参数 | pdrv | 用于标识目标设备的物理驱动器编号 |
| cmd | 控制命令码(用于指定要执行的具体控制操作) | |
| buff | 指向与命令码对应的参数 / 数据缓冲区的指针;若命令无参数需要传递,则无需关注该参数 | |
| 返回值 | RES_OK (0) | 函数执行成功 |
| RES_ERROR | 执行过程中发生错误 | |
| RES_PARERR | 命令码或参数无效 | |
| RES_NOTRDY | 设备尚未完成初始化 | |
| 返回值类型 | DRESULT | |
| 功能说明 |
该函数是 FatFs 与底层存储设备的 “扩展控制接口”,负责处理通用读写之外的设备控制逻辑 FatFs 模块仅依赖 5 个设备无关的核心命令(注:原文未列出具体命令,常见核心命令包括 GET_SECTOR_COUNT/GET_SECTOR_SIZE/GET_BLOCK_SIZE/CTRL_SYNC/CTRL_TRIM 等); 不同 cmd 对应不同的操作逻辑,buff 的用途随 cmd 变化(如传递参数、存储返回数据) |
|
2.2.2.6 获取当前时间——get_fattime
| 项目 | 内容 |
|---|---|
| 函数名 | get_fattime |
| 函数用途 | 调用 get_fattime 函数用于获取当前时间。 |
| 函数原型 | DWORD get_fattime (void); |
| 返回值说明 | 需将当前本地时间以位段形式封装到 DWORD 类型值中返回。位段定义如下: |
| 功能补充说明 | 即使系统不支持实时时钟(RTC),该函数也应返回任意有效的时间值;若返回 0,文件将不具备有效的时间戳。 |
| 快速说明 | 当 FF_FS_READONLY == 1(文件系统只读)或 FF_FS_NORTC == 1(禁用 RTC)时,无需实现该函数。 |
位段详细定义:
| 位范围 | 含义 | 取值范围 | 示例 |
|---|---|---|---|
| bit31:25 | 年份(以 1980 年为基准) | 0..127 | 37 代表 2017 年 |
| bit24:21 | 月份 | 1..12 | - |
| bit20:16 | 日期(当月的第几天) | 1..31 | - |
| bit15:11 | 小时 | 0..23 | - |
| bit10:5 | 分钟 | 0..59 | - |
| bit4:0 | 秒数 / 2(秒数除以 2 后的值) | 0..29 | 25 代表 50 秒 |
2.3 系统移植
2.3.1 前期准备
首先先找一个SPI读写W25Q64的代码,这里我使用我之前移植好的代码:
通过上面官网下载所需要的FatFs文件。
2.3.2 复制FatFs文件到工程中
找到刚下载好的FatFs文件的source文件夹下的文件:

全部复制到我们SPI工程当中,这里创建一个FatFs专门存放这些文件:

先把这些文件包含进来:


此时我们编译一下会发现一个报错:

那是因为 platform.h 和 storage.h 这两个头文件,并非 FatFs 必需的核心文件,是开发者编写底层驱动时自定义的平台 / 存储相关头文件,因此默认不存在,我们这里直接删掉或者注释掉都可以:

不过我们发现我们注释完还是有一堆报错,那是因为这里的一些文件都是调用的前面哪些文件的一些测试用例,也先注释掉即可,然后把宏定义 DEV_FLASH 改为 DEV_RAM:

按照上面更改完后,再次运行会发现还有一个报错,那是因为FatFs 内核需要 get_fattime() 函数提供当前时间戳,但该函数是用户需自定义实现的接口,默认未定义。
解决方法一:我们直接在 ffconf.h 文件找到 FF_FS_NORTC 将其值改为1,禁用当前时间戳功能,采用文件固定的时间戳:

其中:
| 宏定义 | 当前值 | 功能说明 |
|---|---|---|
| FF_FS_NORTC | 0 |
时间戳功能开关: 0 = 启用(必须实现 get_fattime() 函数读取 RTC 时间); 1 = 禁用(文件时间固定为 FF_NORTC_*) |
| FF_NORTC_MON | 1 | 禁用时间戳时的固定月份(1-12); FF_FS_NORTC=0 时此值无效 |
| FF_NORTC_MDAY | 1 | 禁用时间戳时的固定日期(1-31);FF_FS_NORTC=0 时此值无效 |
| FF_NORTC_YEAR | 2025 | 禁用时间戳时的固定年份;FF_FS_NORTC=0 时此值无效 |
此时在运行就会发现报错消失:

剩下的警告不用管,那是因为我们diskio.c文件声明了变量但是未调用,等我们后续将注释的解开用上就会消失。
方法二:前面也说了,这里缺少get_fattime,我们找到官网把复制其函数原型,给添加进来:

这里我们随便放一个返回值(注意此时也是固定时间戳,想要获取当前时间戳,需要RTC时钟去获取真实时间):
// FatFs要求的时间戳获取函数(FF_FS_NORTC=0时必须实现)
DWORD get_fattime (void)
{
// 返回格式:年(7位)|月(4位)|日(5位)|时(5位)|分(6位)|秒/2(5位)
// 示例:2025年1月1日 00:00:00(与FF_NORTC_*对应)
return (DWORD)(
(2025 - 1980) << 25 | // 年份:从1980开始计算,占7位
1 << 21 | // 月份:1,占4位
1 << 16 | // 日期:1,占5位
0 << 11 | // 小时:0,占5位
0 << 5 | // 分钟:0,占6位
0 >> 1 // 秒数:0,除以2后占5位
);
}

2.3.3 修改ffconf.h
2.3.3.1 FF_CODE_PAGE
找到 FF_CODE_PAGE 宏定义设置语言类型为:简体中文(DBCS 双字节编码),其对应GBK/GB2312 编码:

2.3.3.2 FF_USE_STRFUNC
找到 FF_USE_STRFUNC 宏定义将其设置为1,这样我们就可以使用如:f_gets()、f_putc()、f_puts()、f_printf()这些函数:
| 配置项 | 当前值 | 核心作用 |
|---|---|---|
| FF_USE_STRFUNC | 0 |
字符串操作 API 开关(控制 f_gets()/f_putc()/f_puts()/f_printf()): 0 = 禁用(所有字符串 API 不可用); 1 = 启用(无 LF→CRLF 换行转换); 2 = 启用(带 LF→CRLF 换行转换) |
| FF_PRINT_LLI | 0 |
f_printf()长整型支持: 0 = 禁用(不支持 long long 类型参数); 1 = 启用(需 C99 及以上编译器) |
| FF_PRINT_FLOAT | 0 |
f_printf()浮点型支持: 0 = 禁用(不支持 float/double 类型参数); 1/2 = 启用(需 C99 及以上编译器) |
| FF_STRF_ENCODE | 0 |
字符串 API 的文件字符编码假设(仅 FF_USE_STRFUNC≠0且 FF_LFN_UNICODE≥1 时生效): 0 = 当前编码页(如 936)的 ANSI/OEM 编码; 1 = UTF-16LE; 2 = UTF-16BE; 3 = UTF-8 |
2.3.3.3 FF_FS_READONLY
找到 FF_FS_READONLY 宏定义将其设置为0,启用文件系统的读写功能:
| 赋值 | 模式 | 功能说明 |
|---|---|---|
| 0 | 读写模式(默认) | 启用文件系统的读 + 写全部功能,支持创建、修改、删除文件 / 目录等操作 |
| 1 | 只读模式 | 仅保留文件读取功能,禁用所有写入 / 修改类操作,同时精简相关 API 代码占用 |
2.3.3.4 FF_VOLUMES
找到 FF_VOLUMES 根据自身想要使用多少设备,例如SD卡,Flash等进行设置,就是我们上面移植代码时,switch中注释掉部分:

这里我们不需要这么多,只需要Flash和SD卡(预留),因此将值改为2:

2.3.3.5 FF_MIN_SS/FF_MAX_SS
对于这两个值分别代表了扇区(簇)的大小的上下限:
| 宏名 | 取值 | 含义 |
|---|---|---|
| FF_MIN_SS | 512 | FatFs 能兼容的最小扇区大小(下限),不可低于 512(FatFs 强制规范) |
| FF_MAX_SS | 4096 | FatFs 能兼容的最大扇区大小(上限),仅支持 2 的幂(512/1024/2048/4096) |
其中,若FF_MAX_SS > FF_MIN_SS(如当前512<4096):FatFs进入可变扇区大小模式;必须在disk_ioctl()函数中实现 GET_SECTOR_SIZE 指令(返回对应设备的实际扇区大小);
若FF_MAX_SS == FF_MIN_SS:FatFs进入固定扇区大小模式;disk_ioctl()的 GET_SECTOR_SIZE 指令不会被调用,disk_read/disk_write需强制以该固定值为单位工作。
举个例子:
| 场景 | 配置建议 | 原因 |
|---|---|---|
| 仅适配 SD 卡 |
FF_MIN_SS=512 FF_MAX_SS=512 |
固定 512 字节(SD 卡行业标准),无需实现GET_SECTOR_SIZE。 |
| 仅适配 SPI Flash |
FF_MIN_SS=4096 FF_MAX_SS=4096 |
固定 4096 字节(SPI Flash 硬件擦除单元),避免地址换算错误。 |
| 同时适配 SD 卡 + SPI Flash |
FF_MIN_SS=512 FF_MAX_SS=4096 |
可变模式,通过GET_SECTOR_SIZE区分设备,兼容两种硬件。 |
这里我们按照同时适配 SD 卡 + SPI Flash,进行更改:

2.3.3.6 FF_USE_MKFS
该宏定义的操作是0(禁止)或者1(使能)格式化函数 f_mkfs 的使用,如果不使能,那么我们初始化时无法进行格式化操作:

2.3.3.7 FF_USE_LFN
用于开关长文件名(LFN)支持功能,其中:
- 0: 禁用长文件名功能。FF_MAX_LFN 无任何作用。
- 1: 启用长文件名功能,使用 BSS 段的静态工作缓冲区。始终不支持线程安全。
- 2: 启用长文件名功能,使用栈(STACK)上的动态工作缓冲区。
- 3: 启用长文件名功能,使用堆(HEAP)上的动态工作缓冲区。
| 类型 | 规则 |
|---|---|
| 短文件名 | 8 个字符主名 + 3 个字符扩展名(如 test.txt),仅支持 ASCII 字符,仅能创建 / 访问短文件名,超出规则会被自动截断(如 longfilename.txt → longfi~1.txt) |
| 长文件名 | 最长 255 个字符,支持中文 / 特殊字符,兼容 Windows/macOS |
这里我们后续想要使用中文字符,所以这里给个1,最长大小255:

2.3.4 修改diskio.c
2.3.4.1 宏定义修改
把之前的宏定义注释掉,并且把switch也全部注释掉,自己声明两个宏定义:

2.3.4.2 初始化磁盘驱动器
首先我们找到 disk_initialize() 函数,将其注释解除,将我们上面声明的宏定义放进去,然后开始对SPI的初始化操作,调用初始化函数:
case SPI_FLASH:
/* 初始化SPI Falsh */
SPI_FLASH_Init();
break;
然后我们声明一个变量作为该函数的返回值,我们由上面知道 disk_initialize() 函数的返回值有三种状态:
| 返回值 | STA_NOINIT |
函数执行成功时,该标志会被清零; 若初始化失败,该标志保持置位状态(代表设备未就绪); 其他状态标志含义参考 disk_status 函数。 |
| STA_NODISK | 驱动器中无存储介质,没有设备;注意:不可移除型驱动器该标志始终清零;FatFs 不读取该标志 | |
| STA_PROTECT | 存储介质处于写保护状态;注意:无写保护功能的驱动器该标志始终清零;STA_NODISK 置位时该标志无效 |

这里默认未初始化状态:
DSTATUS status = STA_NOINIT;
不过对于我们初始化函数来说,我们在之前编写的时候并没有返回值信息,这就需要我们另一个函数——disk_status 函数去进行当前状态信息的判断(下面讲如何判断,这里先调用),我们这里直接获取其返回值信息即可:
DSTATUS disk_initialize (BYTE pdrv)/* Physical drive nmuber to identify the drive */
{
DSTATUS status = STA_NOINIT;
uint16_t i;
switch (pdrv)
{
case SD_CARD:
break;
case SPI_FLASH:
/* 初始化SPI Falsh */
SPI_FLASH_Init();
/*做一小段延时*/
i = 500;
while(i--);
/* 唤醒SPI Flash */
SPI_Flash_WAKEUP();
/* 获取SPI Flash芯片状态 */
status=disk_status(SPI_FLASH);
break;
default:
status = STA_NOINIT;
}
return status;
}
这里需要注意的一点是,如果W25Q64处于低功耗模式下,除了唤醒指令,其他指令无法进行操作,因此为了防止芯片处于低功耗模式而出现错误,这里强制唤醒一下,唤醒前加一小段延时,防止刚初始化就发送消息,出现数据出错。
2.3.4.3 获取磁盘状态
找到 disk_status() 函数,我们在这里进行判断设备状态,先将之前的注释去掉,将宏定义加进去,整理一下:
/*-----------------------------------------------------------------------*/
/* Get Drive Status 获取设备状态 */
/*-----------------------------------------------------------------------*/
DSTATUS disk_status (BYTE pdrv)/* Physical drive nmuber to identify the drive */
{
DSTATUS status = STA_NOINIT;
switch (pdrv)
{
case SD_CARD :
break;
case SPI_FLASH :
break;
default:
status = STA_NOINIT;
}
return status;
}
那么我们要怎么判断状态呢?我们在编写W25Q64的时候知道,其芯片固定ID为 0xEF4017,并且我们也可以通过命令读取ID:

而如果我们能够读取ID,那就说明我们初始化是正常的,这样就可以,通过对比我们读取的ID是不是当前芯片ID进行判断:
/*-----------------------------------------------------------------------*/
/* Get Drive Status 获取设备状态 */
/*-----------------------------------------------------------------------*/
DSTATUS disk_status (BYTE pdrv)/* Physical drive nmuber to identify the drive */
{
DSTATUS status = STA_NOINIT;
switch (pdrv)
{
case SD_CARD :
break;
case SPI_FLASH :
/* SPI Flash状态检测:读取SPI Flash 设备ID */
if(sFLASH_ID == SPI_FLASH_ReadID())
{
/* 设备ID读取结果正确 */
status = 0;
}
else
{
/* 设备ID读取结果错误 */
status = STA_NOINIT;;
}
break;
default:
status = STA_NOINIT;
}
return status;
}
2.3.4.4 从磁盘驱动器读扇区
上面初始化完成后,我们开始读取磁盘数据,先整理一下:
/**
* @brief 读取指定驱动器的扇区数据
* @param pdrv,物理驱动器编号(用于标识要操作的驱动器)
* @param buff,存储读取数据的数据缓冲区指针
* @param sector,起始扇区地址(LBA格式)
* @param count,要读取的扇区数量
* @retval DRESULT类型的操作结果:
* RES_OK:表示操作成功
* RES_ERROR:表示读写错误
* RES_WRPRT:表示介质写保护
* RES_NOTRDY:表示设备未就绪
* RES_PARERR:表示参数错误
*/
DRESULT disk_read (BYTE pdrv, BYTE *buff, LBA_t sector, UINT count)
{
DRESULT status = RES_PARERR;//默认返回参数错误
switch (pdrv)
{
case SD_CARD :
break;
case SPI_FLASH :
break;
default:
status = RES_PARERR;
}
return status;
}
在开始读取数据之前我们先对W25Q64做一个内存规划,我们知道对于 W25Q64 其有 8MByte 的存储区域,我们将前 2MB 预留用于其他功能(如存储固件、配置参数、字库等),后 6MB(8-2=6)专门用于文件系统,避免文件系统数据覆盖关键程序 / 配置。
而我们又知道 2MB = 2*1024*1024字节 = 2097152 字节,而且我们又知道 W25Q64 的最小擦除单元是 4KB = 4*1024字节,这里我们就定义 FatFs 文件系统的扇区(簇)为4KB,因此 2097152/4096 = 512,也就是说我们文件系统所存储的扇区需要从 W25Q64 首个扇区,偏移512个扇区,因次我们需要将首地址加512:
sector+=512;
然后直接调用SPI Flash读取函数即可:
case SPI_FLASH :
/* 扇区偏移2MB,外部Flash文件系统空间放在SPI Flash后面6MB空间 */
sector+=512;
SPI_FLASH_BufferRead(buff, sector <<12, count<<12);
status = RES_OK;
break;
其实这里应当还有一个SPI Flash读取成功的返回值判断,然后才输出成功读取标志,不过我们之前函数,没有做返回值,这里无论读取成功与否都直接返回成功了。
2.3.4.5 从磁盘驱动器写扇区
也是先简单整理一下:
#if FF_FS_READONLY == 0
DRESULT disk_write (BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count)
{
DRESULT status = RES_PARERR;
switch (pdrv)
{
case SD_CARD :
break;
case SPI_FLASH :
break;
default:
status = RES_PARERR;
}
return RES_PARERR;
}
#endif
这里我们也可以看出 FF_FS_READONLY 如果不设置为0,就无法进行写操作,写入操作和读取操作差不多,只是多加了一次写入前的扇区擦除:
case SPI_FLASH :
/* 扇区偏移2MB,外部Flash文件系统空间放在SPI Flash后面6MB空间 */
sector+=512;
SPI_FLASH_SectorErase(sector<<12);
SPI_FLASH_BufferWrite((u8 *)buff,sector<<12,count<<12);
status = RES_OK;
break;
2.3.4.6 控制设备实现指定功能
同样的现将函数整理一下:
DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void *buff)
{
DRESULT status = RES_PARERR;
switch (pdrv)
{
case SD_CARD :
break;
case SPI_FLASH :
break;
default:
status = RES_PARERR;
}
return RES_PARERR;
}
在开始编写前我们先来到官网,找到 disk_ioctl() 函数的介绍,找到其的一些相关指令:

其中上方的表格为 FatFs 定义的通用指令,不依赖具体存储设备(如 SPI Flash、SD 卡),所有底层存储驱动都需按统一规则实现这些指令,下面的表格为非标准指令,这些指令有些应用可能会用上,这里对标准指令翻译了一下:
| 指令 | 作用 | 描述 |
|---|---|---|
| CTRL_SYNC | 让存储设备把 “暂存的写数据” 真正写到硬件里,避免数据丢失 | 确保设备已完成挂起的写操作。若磁盘 I/O 层或存储设备带有回写缓存,必须立即将脏缓存数据提交到介质。若每次写操作在 disk_write 函数中已完成,则此指令无需处理。 |
| GET_SECTOR_COUNT | 告诉 FatFs:设备能用于文件系统的扇区总数(决定文件系统最大可用空间) | 将驱动器上可用扇区数量(最大允许的 LBA 地址 + 1)读取到 buff 指向的 LBA_t 变量中。此指令被 f_mkfs 和 f_fdisk 函数用于确定要创建的卷 / 分区大小。 |
| GET_SECTOR_SIZE | 告诉 FatFs:设备一次能读写的最小字节数(扇区大小) | 将最小通用读写数据单元(扇区大小)读取到 buff 指向的 WORD 变量中。合法的扇区大小为 512、1024、2048、4096。仅当 FF_MAX_SS > FF_MIN_SS 时需要此指令;若 FF_MAX_SS == FF_MIN_SS,此指令不会被调用,且 disk_read/disk_write 函数必须以 FF_MAX_SS 字节为单位工作。 |
| GET_BLOCK_SIZE | 告诉 FatFs:设备一次最少能擦除多少个扇区(优化擦除效率) | 将闪存介质的擦除块大小(以扇区为单位)读取到 buff 指向的 DWORD 变量中。擦除块大小需是 2 的幂(范围 1~32768);若未知或非闪存介质,返回 1。此指令被未指定块大小的 f_mkfs 函数使用,用于尝试将数据区对齐到建议的块边界。注意:FatFs 无 FTL(闪存转换层),因此磁盘 I/O 层或存储设备必须自带 FTL。 |
| CTRL_TRIM | 告诉设备:某段扇区的数据没用了,可以擦除(提升闪存写入效率) | 通知磁盘 I/O 层或存储设备:某块扇区上的数据不再需要,可以被擦除。扇区块由 buff 指向的 LBA_t 数组(<起始 LBA>、<结束 LBA>)指定。此指令与 ATA 设备的 Trim 指令功能一致。若不支持此功能或非闪存介质,无需处理此指令。FatFs 不检查此指令的结果码,即使扇区块未被擦除,文件函数也不受影响。此指令在删除簇链时被调用,且仅当 FF_USE_TRIM == 1 时需要。 |
为什么需要这些指令呢?那是因为 FatFs 不直接操作硬件,而是通过发送这些指令给 disk_ioctl 函数,获取设备信息或控制设备行为,保证文件系统适配不同存储介质(SPI Flash/SD 卡 / U 盘)的通用性。
由于我们会使用 disk_write 进行写操作,因此 CTRL_SYNC 暂时不需要配置,然后对于 GET_SECTOR_COUNT,其是设备能用于文件系统的扇区总数,我们上面知道,我们在 Flash 中预留了6M的空间用于存放文件系统,我们之前计算过2M的空间占512个扇区,那么6M的空间就占了1536个扇区:
6M = 6*1024*1024个字节 = 6291456字节
一个扇区为:4*1024字节
扇区数量:6291456/4096 = 1536
然后对于 GET_SECTOR_SIZE 其是扇区的大小,之前也说了按照 W24Q64 扇区的大小进行配置,也就是4096。
对于 GET_BLOCK_SIZE 同时能擦除扇区的个数,这里也按照 W24Q64 进行配置,只能同时擦除1个。
所以此时 Flash 的配置如下:
case SPI_FLASH :
switch(cmd)
{
/* 扇区数量:1536*4096/1024/1024=6(MB) */
case GET_SECTOR_COUNT:
*(DWORD * )buff = 1536;
break;
/* 扇区大小 */
case GET_SECTOR_SIZE :
*(WORD * )buff = 4096;
break;
/* 同时擦除扇区个数 */
case GET_BLOCK_SIZE :
*(DWORD * )buff = 1;
break;
}
status = RES_OK;
break;
其中对于buff需要进行强制类型转换一下,因为我们 FatFs 在声明函数:
DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void *buff)
我们可以看到buff是 void *(无类型指针),这是 FatFs 设计的通用接口,不同指令需要读写不同类型、不同长度的数据(如扇区大小是 2 字节、扇区总数是 4 字节),但 void* 本身无法直接解引用赋值 / 读取,必须先转换为具体类型指针,其强转类型可以再 ff.h 查看:

2.3.5 测试
我们来到主函数测试一下功能。
2.3.5.1 f_mount()函数
首先在官网找到这个函数:

f_mount函数的作用就是挂载逻辑驱动器,也就是为 FatFs 模块分配并注册文件系统工作区,其中:
| 参数 | 作用 | 嵌入式场景 |
|---|---|---|
| fs | 指向要注册并清空的文件系统对象的指针;传入 NULL 表示注销已注册的文件系统对象 | 需提前定义 FATFS fs(工作区),传入 &fs 表示挂载;传入 NULL 等价于调用 f_unmount;每个逻辑驱动器需独立的 FATFS 对象(如 SD 卡和 SPI Flash 各一个)。 |
| path | 指向以空字符结尾的字符串,指定逻辑驱动器;无驱动器号的字符串表示默认驱动器 |
FatFs 中逻辑驱动器用 "/0:"(驱动器 0)、"/1:"(驱动器 1)表示; 传入 "" 或 "/" 表示默认驱动器(通常是驱动器 0); 对应你代码中的 SD_CARD/SPI_FLASH(需绑定逻辑驱动器号)。 |
| opt |
挂载选项: 0 = 暂不挂载(首次访问卷时自动挂载); 1 = 强制挂载卷,检查是否就绪; 若 fs 为 NULL,此参数无效 |
opt=0:延迟挂载,首次读写文件时才检测硬件,节省初始化时间; opt=1:立即检测硬件(如 SPI Flash 是否存在),失败返回 FR_NOT_READY; 仅挂载时有效,卸载时无意义。 |
我们在调用前先声明一下SPI Flash的工作区域:
FATFS fs_Flash;
其中对于 FATFS 的结构体可以在ff.h文件查看:

这些结构体的作用,可以结合上面1.3的流程进行理解。简单来说就是我们想把磁盘分配成什么样式的。
从官网介绍我们知道,在调用完 f_mount后,无论成功与否他都会返回响应结果,返回的结果相关含义我们可以在ff.h进行查看:

这里简单翻译一下:
/* 文件函数返回码(FRESULT):FatFs所有文件操作函数的返回值类型 */
typedef enum {
FR_OK = 0, /* (0) Function succeeded:操作成功 */
FR_DISK_ERR, /* (1) A hard error occurred in the low level disk I/O layer:底层磁盘I/O层发生硬件错误 */
FR_INT_ERR, /* (2) Assertion failed:断言失败(FatFs内部逻辑错误) */
FR_NOT_READY, /* (3) The physical drive does not work:物理驱动器未就绪(如SD卡未插入、SPI Flash通信失败) */
FR_NO_FILE, /* (4) Could not find the file:找不到指定文件 */
FR_NO_PATH, /* (5) Could not find the path:找不到指定路径(目录不存在) */
FR_INVALID_NAME, /* (6) The path name format is invalid:路径名格式无效(如含非法字符、超长) */
FR_DENIED, /* (7) Access denied due to a prohibited access or directory full:访问被拒绝(权限禁止/目录满) */
FR_EXIST, /* (8) Access denied due to a prohibited access:文件/目录已存在(创建/重命名时冲突) */
FR_INVALID_OBJECT, /* (9) The file/directory object is invalid:文件/目录对象无效(如操作已关闭的文件句柄) */
FR_WRITE_PROTECTED, /* (10) The physical drive is write protected:物理驱动器写保护(无法写入/删除) */
FR_INVALID_DRIVE, /* (11) The logical drive number is invalid:逻辑驱动器编号无效(如指定"/2:"但仅注册了0/1驱动器) */
FR_NOT_ENABLED, /* (12) The volume has no work area:卷无工作区(未调用f_mount注册FATFS对象) */
FR_NO_FILESYSTEM, /* (13) Could not find a valid FAT volume:找不到有效的FAT卷(驱动器未格式化) */
FR_MKFS_ABORTED, /* (14) The f_mkfs function aborted due to some problem:f_mkfs格式化函数因错误中止 */
FR_TIMEOUT, /* (15) Could not take control of the volume within defined period:卷控制超时(如抢占总线超时) */
FR_LOCKED, /* (16) The operation is rejected according to the file sharing policy:文件被锁定(共享策略拒绝操作) */
FR_NOT_ENOUGH_CORE, /* (17) LFN working buffer could not be allocated, given buffer size is insufficient or too deep path:内存不足(LFN缓冲区分配失败/路径过深/缓冲区太小) */
FR_TOO_MANY_OPEN_FILES, /* (18) Number of open files > FF_FS_LOCK:打开文件数超过FF_FS_LOCK限制 */
FR_INVALID_PARAMETER /* (19) Given parameter is invalid:传入参数无效(如空指针、非法数值) */
} FRESULT;
这里我们创建一个变量用来接收结果:
FRESULT res_Flash;//文件操作结果
此时我们主函数:
#include "stm32f10x.h"
#include "Delay.h"
#include "Bsp_LED_Gpio.h"
#include "Bsp_Usartx.h"
#include "SPI_gpio.h"
#include "W25Qx_Flash.h"
#include "ff.h"
FATFS fs_Flash; //FatFs文件系统对象
FRESULT res_Flash;//文件操作结果
int main(void)
{
LED_GPIO_Config();//LED引脚初始化
USART_Config();//串口初始化
printf("这是一个SPI FLASH 文件系统实验!!!\r\n");
//在外部SPI Flash挂载文件系统,文件系统挂载时会对SPI设备初始化
res_Flash = f_mount(&fs_Flash,"1:",1);
printf("res = %d",res_Flash);
while(1)
{
}
}
此时下载看一下,f_mount返回值为13:

我们找到其返回值的序号13,其表达的意思是:
FR_NO_FILESYSTEM, /* (13) Could not find a valid FAT volume:找不到有效的FAT卷(驱动器未格式化) */
出现这个的原因是因为 SPI Flash 出厂时是空白的(全 0xFF),无任何 FAT 分区表、引导扇区、FAT 表等文件系统结构,FatFs 挂载 / 访问时无法识别,直接返回 FR_NO_FILESYSTEM,因此在挂载文件系统之前先格式化数据,我们来到官网找到这个函数:

2.3.5.2 f_mkfs()函数
其作用就是格式化存储介质,在指定逻辑驱动器上创建 FAT/exFAT 文件系统卷,该函数的函数原型:
FRESULT f_mkfs (
const TCHAR* path, /* [输入] 逻辑驱动器编号 */
const MKFS_PARM* opt,/* [输入] 格式化选项结构体 */
void* work, /* [出/入] 格式化工作缓冲区 */
UINT len /* [输入] 工作缓冲区大小(字节) */
);
| 参数 | 功能 | |||
|---|---|---|---|---|
| path | 指向以空字符结尾的字符串,指定待格式化的逻辑驱动器。若字符串中不含驱动器编号,则表示指定默认驱动器。格式化过程中,该逻辑驱动器可以是已挂载状态,也可以是未挂载状态。 | |||
| opt |
指定包含格式化选项的 MKFS_PARM 结构体。若传入空指针,则函数使用所有选项的默认值。 |
参数 | BYTE fmt | FAT 类型标志的组合,包括 FM_FAT(FAT12/FAT16)、FM_FAT32、FM_EXFAT,以及这三者的按位或组合 FM_ANY。若未启用 exFAT,FM_EXFAT 会被忽略。这些标志指定要创建的 FAT 卷类型;若指定两种及以上类型,将根据卷大小和簇大小(au_size)选择其中一种。FM_SFD 标志指定以 SFD 格式在驱动器上创建卷,默认值为 FM_ANY。 |
| BYTE n_fat | 指定 FAT/FAT32 卷的 FAT 表副本数量,有效值为 1 或 2;默认值(0)或无效值均会设为 1,exFAT 类型下该成员无效。 | |||
| UINT align | 指定卷数据区(文件分配池,通常为闪存介质的擦除块边界)的对齐方式,单位为扇区;有效值为 1~32768 且为 2 的幂。若传入 0(默认值)或无效值,函数会通过底层 disk_ioctl 函数获取块大小。 | |||
| UINT n_root | 指定 FAT 卷的根目录项数量,有效值最大为 32768 且需对齐到 “扇区大小 / 32”;默认值(0)或无效值均设为 512,FAT32/exFAT 类型下该成员无效。 | |||
| DWORD au_size | 指定簇(分配单元)的大小,单位为字节。FAT/FAT32 卷的有效值为 “扇区大小~128× 扇区大小” 且为 2 的幂;exFAT 卷最大支持 16MB 且为 2 的幂。若传入 0(默认值)或无效值,函数会根据卷大小使用默认簇大小。 | |||
| work | 指向格式化过程中使用的工作缓冲区。若 FF_USE_LFN == 3 且传入空指针,函数会在内部分配 len 字节的堆内存作为缓冲区。 | |||
| len | 工作缓冲区的大小(字节),至少需等于 FF_MAX_SS(最大扇区大小)。更大的缓冲区可减少对驱动器的写入次数,从而加快格式化速度。 | |||
让我们来看看此函数怎么配置,首先是path,也就是设备驱动编号,这里我们需要驱动SPI Flash,也就是“1:”,然后是opt,这里面有五个函数,我们可以在ff.h找到:

拓展:
FAT 表是什么?
FAT(File Allocation Table,文件分配表)是 FAT16/FAT32 文件系统的核心元数据,相当于文件系统的 “地图”:
- 记录每个簇(文件存储的最小单位)的状态(空闲 / 已占用 / 坏块);
- 记录文件的簇链(比如一个文件占用簇 10→11→12,FAT 表会标注这串关联关系);
- FAT 表损坏 = 文件系统瘫痪(无法找到 / 访问文件)。
这里简单介绍一下 fmt 参数所对应的文件类型:
| 宏 | 全称 / 含义 | 核心特征 |
|---|---|---|
| FM_FAT | FAT16(兼容 FAT12) |
传统 FAT 文件系统,适配小容量存储 FAT12(≤4MB)和 FAT16(4MB~32MB),无簇数下限,8.3 短文件名优先 |
| FM_FAT32 | FAT32 | 大容量 FAT 扩展,适配≥32MB 存储,簇数需≥65525,支持长文件名(LFN) |
| FM_EXFAT | Extended FAT(扩展 FAT) | 微软为大容量 / 高性能设计,支持单文件>4GB、无限簇数,需 LFN 支持 |
| FM_SFD | Super Floppy Disk(超级软盘模式) | 非独立文件系统,是 FAT16/FAT32 的 “无分区” 格式化模式,直接格式化整个存储介质 |
这里只是简单说明一下,详细的可以在网上找一下。
对于fmt由于我们只有6MB的大小,则不适配 FM_FAT32 和 FM_EXFAT,对于 FM_SFD 可以说是 FAT16/FAT32 的延伸,以 FAT16 为例,如果我们只调用 FAT16,那么存储布局就是:第 0 扇区(分区表)+ 512~2047 扇区(文件系统),如果再加上 FM_SFD ,那么就变成了:512~2047 扇区:文件系统(无分区表):
| 方式 | 存储布局 | 优缺点 |
|---|---|---|
| 有分区表(普通模式) | 第 0 扇区:分区表512~2047 扇区:文件系统 | 优点:兼容通用 FAT 设备;缺点:占用空间、逻辑复杂、易因分区表错误格式化失败 |
| 无分区表(FM_SFD) | 512~2047 扇区:文件系统(无分区表) | 优点:无空间浪费、逻辑简单、格式化成功率 100%;缺点:仅适配嵌入式专属场景 |
根据自己需要,这里我用的普通模式:
mkfs_opt.fmt = FM_FAT16; // 文件系统类型:FAT16(6MB空间推荐)
然后是对于 n_fat,它是 FatFs 格式化时指定 FAT 表的冗余副本数量,简单来说就是创建副本(备份),其中取值:
| 取值 | 实际生效值 | 含义 | 适用场景 |
|---|---|---|---|
| 1 | 1 | 仅创建 1 份 FAT 表(无备份) | 对空间极致节省、只读 / 少写场景 |
| 2 | 2 | 创建 2 份 FAT 表(主 FAT + 备份 FAT),格式化 / 写操作时同步更新两份 | 可写场景、需容错 |
| 0/≥3 | 1 | 无效值,FatFs 自动修正为 1(仅保留 1 份 FAT 表) | 无实际意义 |
这里我们是主+备份,因此创建为2:
mkfs_opt.n_fat = 2; // FAT表数量:2
接着是 align,其作用是格式化时指定 文件系统数据区的起始对齐偏移(单位:扇区),核心目的是让文件系统的 “数据区”(存储文件内容的区域)对齐到存储介质的擦除块边界(如 W25Q64 的 4KB 扇区 / 擦除块),避免 “跨擦除块写入” 导致的性能下降或数据损坏,其取值规则:
| 取值 | 实际行为 | 适用场景 |
|---|---|---|
| 0(默认值) | FatFs 自动调用 disk_ioctl 的 GET_BLOCK_SIZE 指令,获取擦除块大小,并按此值对齐 | 99% 的嵌入式场景 |
| 1~32768(2 的幂) | 强制按指定扇区数对齐(如设 1=4KB、设 2=8KB),需手动匹配擦除块大小 | 需精准控制对齐的特殊场景 |
| 非 2 的幂 / 超出范围 | 视为无效值,降级为 “取值 0” 的逻辑(自动获取块大小对齐) | 无实际意义 |
这里我们直接给0即可,当然设为1进行强制对齐也可以:
mkfs_opt.align = 0, // 扇区对齐:自动,或者1强制
这里需要注意一点:align 与 sector += 512 的关系
我们在之前对Flash保留了2M的空间用于处理别的数据,那么当我们进行数据对齐的时候会冲突吗?两者不冲突,align=0 会在 512 扇区的基础上,自动保证数据区对齐到 1 扇区边界。这里需要明白:
sector += 512 是文件系统整体的起始偏移(避开前 2MB);
align 是文件系统内部数据区的对齐(在 512 扇区基础上,再对齐到擦除块边界);
然后是 n_root,这个参数仅对 FAT16/FAT12(FM_FAT) 有效,是格式化时指定根目录可容纳的最大文件 / 文件夹项数量;FAT32/exFAT 中根目录被设计为 “动态簇链”(无固定大小),因此该参数无效。
先理解 FAT16 根目录的特性:
FAT16 的根目录是固定大小、固定位置的区域(位于 FAT 表之后、数据区之前),每个文件 / 文件夹占用 1 个 “目录项”(32 字节),因此:根目录总大小 = n_root × 32 字节
且根目录大小必须对齐到扇区大小(例如我们是 4096 字节),这也是 FatFs 要求 n_root 需对齐到 扇区大小/32 的原因(4096/32=128,即 n_root 需是 128 的倍数)。
其取值规则:
| 取值情况 | 实际行为 | 适用场景 |
|---|---|---|
| 0(默认值) | FatFs 自动设为 512(标准值,也是嵌入式场景的最优默认值) | 无特殊需求的场景(推荐) |
| 有效值(≤32768 + 对齐扇区大小/32) | 按指定值创建根目录(如 128/256/512/1024),需保证总大小≤扇区整数倍 | 需定制根目录容量的场景 |
| 无效值(超 32768 / 未对齐) | 自动降级为 512 | 无实际意义,不推荐 |
| FAT32/exFAT 类型 | FAT32/exFAT 中根目录被设计为 “动态簇链”(无固定大小),该参数被完全忽略(设任何值都无效) | 无需关注此参数 |
这里我们设为 0,或者显式设为512代表的意思是一样的:
mkfs_opt.n_root = 0, // 根目录项数,或者直接设为512
最后是 au_size(Allocation Unit Size,分配单元大小),它是格式化时指定 FAT/FAT32/exFAT 文件系统的簇大小(单位:字节),簇是文件系统管理文件存储的最小单位—— 无论文件多小,至少占用 1 个簇;文件超过 1 个簇时,会占用连续 / 离散的多个簇。
先理清簇与扇区的关系:
扇区:存储介质的物理最小读写单位(如 W25Q64 是 4096 字节 / 扇区);
簇:文件系统的逻辑最小分配单位,必须是扇区大小的整数倍且为 2 的幂(如 1 扇区 = 4KB、2 扇区 = 8KB、4 扇区 = 16KB);
举例:若 au_size=4096(1 簇 = 1 扇区),一个 100 字节的小文件也会占用 4096 字节;若 au_size=8192(1 簇 = 2 扇区),则占用 8192 字节。
其取值规则:
| 取值情况 | 实际行为 | 适用场景 |
|---|---|---|
| 0(默认值) | FatFs 根据卷大小自动计算最优簇大小(小容量卷默认 = 扇区大小) | 无特殊需求的场景(兼容性优先) |
| 有效值(FAT/FAT32:扇区大小~128× 扇区大小,且为 2 的幂) | 强制按指定字节数设置簇大小,需严格匹配 “扇区大小整数倍 + 2 的幂” | 需精准控制空间 / 性能的场景(推荐嵌入式手动指定) |
| 无效值(非 2 的幂 / 超出范围) | 降级为 “取值 0” 的逻辑(根据卷大小自动分配默认簇大小) | 无实际意义 |
| exFAT 卷 | 有效值范围更大(扇区大小~16MB,2 的幂),其余规则同 FAT/FAT32 | 大容量 exFAT 场景 |
这里我们自己分配:
mkfs_opt.au_size = 4096 // 分配单元大小:1个扇区(4KB)或者直接写为0
配置如下:
//配置格式化参数
MKFS_PARM mkfs_opt = {
.fmt = FM_FAT, // 文件系统类型:FAT16
.n_fat = 2, // FAT表数量:2(默认冗余)
.align = 0, // 扇区对齐:自动
.n_root = 512, // 根目录项数:FAT32无需配置
.au_size = 4096 // 分配单元大小:1个扇区(4KB)
};
//执行格式化
res_Flash = f_mkfs("1:", &mkfs_opt, work_buf, sizeof(work_buf));
其中 work_buf 是格式化缓冲区,其作用实在 f_mkfs 执行格式化时,通过这块临时内存区域完成文件系统元数据的计算、写入和校验,缓冲区大小至少≥1 个扇区(否则无法完整缓存一个扇区的元数据),通过__attribute__((aligned(4))),然后再完善一下别的判断条件,此时我们在运行发现:

可以发现成功挂载,并且复位一下,会发现显示文件系统已存在,然后不需要再格式化操作了:

此时主函数代码:
#include "stm32f10x.h"
#include "Delay.h"
#include "Bsp_LED_Gpio.h"
#include "Bsp_Usartx.h"
#include "SPI_gpio.h"
#include "W25Qx_Flash.h"
#include "ff.h"
FATFS fs_Flash; //FatFs文件系统对象
FRESULT res_Flash; //文件操作结果
uint8_t work_buf[4096] __attribute__((aligned(4)));//格式化工作缓冲区
int main(void)
{
LED_GPIO_Config();//LED引脚初始化
USART_Config();//串口初始化
// SPI_FLASH_Init();
// SPI_FLASH_BulkErase();
printf("这是一个SPI FLASH 文件系统实验!!!\r\n");
//在外部SPI Flash挂载文件系统,文件系统挂载时会对SPI设备初始化
res_Flash = f_mount(&fs_Flash,"1:",1);
printf("文件系统第一次挂载:%d\r\n",res_Flash);
if(res_Flash == FR_NO_FILESYSTEM)
{
printf("FLASH 还没有文件系统实验,开始格式化...\r\n");
//先卸载文件系统(格式化前必须卸载)
res_Flash = f_mount(NULL, "1:", 0);
if(res_Flash != FR_OK)
{
printf("卸载文件系统失败:%d\r\n",res_Flash);
while(1); // 格式化前置条件失败,卡死报错
}
//配置格式化参数
MKFS_PARM mkfs_opt = {
.fmt = FM_FAT, // 文件系统类型:FAT16
.n_fat = 2, // FAT表数量:2(默认冗余)
.align = 0, // 扇区对齐:自动
.n_root = 512, // 根目录项数:FAT32无需配置
.au_size = 4096 // 分配单元大小:1个扇区(4KB)
};
//执行格式化
res_Flash = f_mkfs("1:", &mkfs_opt, work_buf, sizeof(work_buf));
printf("文件系统格式化:%d\r\n",res_Flash);
if(res_Flash == FR_OK)
{
printf("文件系统格式化成功,重新挂载!!!\r\n");
res_Flash = f_mount(&fs_Flash, "1:", 1);
printf("文件系统第二次挂载:%d\r\n",res_Flash);
}
else
{
printf("文件系统格式化失败!!!\r\n");
}
}
else if(res_Flash == FR_OK)
{
printf("FLASH 已存在文件系统,挂载成功!!!\r\n");
}
while(1)
{
}
}
2.3.5.3 f_open()函数
f_open 是 FatFs 中打开 / 创建文件的核心函数,其函数原型:
FRESULT f_open (
FIL* fp, /* [输出] 指向空白文件对象结构体的指针 */
const TCHAR* path, /* [输入] 文件名(以空字符结尾的字符串) */
BYTE mode /* [输入] 打开模式标志位 */
);
对于其内的一些参数:
| 参数 | 中文说明 & 注意事项 |
|---|---|
| fp | 指向空白FIL类型文件对象的指针;若传入空指针,函数返回FR_INVALID_OBJECT(无效对象) |
| path |
指定要打开 / 创建的文件名(空字符结尾); 若传入空指针,返回FR_INVALID_DRIVE(无效驱动器); 注意这里的文件路径也要带上,例如你想在Flash创建一个test.txt文件,那么这里就需要写1:test.txt |
| mode |
控制文件访问类型(读 / 写)和打开方式的标志位,支持以下组合: ・访问权限:FA_READ(只读) FA_WRITE(只写) 组合则为读写 ・打开 / 创建策略: FA_OPEN_EXISTING:仅打开已存在文件(默认,文件不存在则失败) FA_CREATE_ALWAYS:新建文件(若文件已存在,清空并覆盖) FA_CREATE_NEW:仅新建文件(文件已存在则失败) FA_OPEN_ALWAYS:打开文件(不存在则新建) FA_OPEN_APPEND:同FA_OPEN_ALWAYS,但读写指针默认定位到文件末尾 |
详细可以查看官网有关 f_open() 的介绍:

首先对于参数 fp 其FIL文件类型如下:
/* 文件对象结构体(FIL) */
typedef struct {
FATFS* fs; /* 指向关联的文件系统对象的指针(**禁止修改字段顺序**) */
WORD id; /* 所属文件系统的挂载ID(**禁止修改字段顺序**) */
BYTE flag; /* 状态标志位 */
BYTE err; /* 终止标志(错误码) */
DWORD fptr; /* 文件读写指针(文件打开时置0) */
DWORD fsize; /* 文件大小 */
DWORD sclust; /* 文件起始簇(0表示无簇链;文件大小为0时恒为0) */
DWORD clust; /* 读写指针当前所在簇(fptr为0时无效) */
DWORD dsect; /* 缓冲区buf[]中缓存的扇区号(0表示无效) */
#if !_FS_READONLY /* 仅文件系统非只读时生效 */
DWORD dir_sect; /* 包含该文件目录项的扇区号 */
BYTE* dir_ptr; /* 指向win[]中该文件目录项的指针 */
#endif
#if _USE_FASTSEEK /* 仅启用快速定位功能时生效 */
DWORD* cltbl; /* 指向簇链接映射表的指针(文件打开时置空) */
#endif
#if _FS_LOCK /* 仅启用文件锁定功能时生效 */
UINT lockid; /* 文件锁定ID(从1开始,对应文件信号量表Files[]的索引) */
#endif
#if !_FS_TINY /* 仅非精简模式时生效 */
BYTE buf[_MAX_SS]; /* 文件私有数据的读写缓冲区 */
#endif
} FIL;

我们在操作文件前需要先声明一下,告诉我们需要操作那个文件:
FIL fnew_Flash; //文件对象
然后是 path 起一个想要打开的文件名,由于我们在上面修改ffconf.h文件已经修改过中文格式,因此这里可以直接使用中文类型,最后一个参数是对该文件赋予什么权限,这里我们给其覆盖写权限:
res_Flash = f_open(&fnew_Flash, "1:FatFs读写测试文件.txt",FA_CREATE_ALWAYS | FA_WRITE );
验证一下:

2.3.5.4 f_write()函数
f_write 函数用于向已打开的文件写入数据,其函数原型为:
FRESULT f_write (
FIL* fp, /* [输入] 指向已打开文件对象结构体的指针 */
const void* buff, /* [输入] 指向待写入数据的指针 */
UINT btw, /* [输入] 期望写入的字节数(UINT类型范围内) */
UINT* bw /* [输出] 指向用于接收实际写入字节数的变量指针 */
);
| 参数 | 中文说明 & 关键注意事项 |
|---|---|
| fp | 指向已打开文件对象的指针;若传入空指针,函数返回FR_INVALID_OBJECT(无效对象) |
| buff | 待写入数据的内存地址(如字符数组、缓冲区等) |
| btw | 期望写入的字节数(UINT 类型);若需提升写入速度,应尽可能按 “大数据块” 写入(减少 SPI / 磁盘交互次数) |
| bw |
指向 UINT 类型变量的指针,用于接收实际写入的字节数;注意:无论函数返回何种错误码,该值始终有效;若*bw == btw,说明写入完全成功,函数返回FR_OK |

fp就相当于文件标志位,上面已经声明过了,buff你想要写入的数据,随便写点:
BYTE WriteBuffer[] = "新建文件系统测试文件,aaa,1111,22222";
btw和bw一个你想要写入的字节个数,一个你实际写入的字节个数,如果这两个不一样,那就证明,写入错误了,创建一个变量用于存放实际写入的个数:
UINT fnum_Flash; //文件成功读写数量 */
那么:
res_Flash=f_write(&fnew_Flash,WriteBuffer,sizeof(WriteBuffer),&fnum_Flash);
我们来验证一下,可以发现文件成功写入:

下面我们将写入的数据读出来看一下。
2.3.5.5 f_read()函数
读和写其实用法上差不读,只不过换成读了:

res_Flash = f_read(&fnew_Flash, ReadBuffer, sizeof(ReadBuffer), &fnum_Flash);
测试一下:

可以发现读写成功,移植完成,对于其他一些函数的运用可以查看其官网:



更多推荐



所有评论(0)