目录

1.  文件系统

1.1  简介

1.2  优点

1.2.1  记录有效数据的位置

1.2.2  确定存储介质的剩余空间

1.2.3  确应以何种格式来解读数据

1.3  流程

2.  FatFs文件系统

2.1  简介

2.2  函数讲解

2.2.1  ffconf.h

2.2.2  diskio.c

2.2.2.1  获取磁盘状态——disk_status

2.2.2.2  初始化磁盘驱动器——disk_initialize

2.2.2.3  从磁盘驱动器读扇区——disk_read

2.2.2.4  从磁盘驱动器写扇区——disk_write

2.2.2.5  控制设备实现指定功能——disk_ioctl

2.2.2.6  获取当前时间——get_fattime

2.3  系统移植

2.3.1  前期准备

2.3.2  复制FatFs文件到工程中

2.3.3  修改ffconf.h

2.3.3.1  FF_CODE_PAGE 

2.3.3.2  FF_USE_STRFUNC

2.3.3.3  FF_FS_READONLY

2.3.3.4  FF_VOLUMES

2.3.3.5  FF_MIN_SS/FF_MAX_SS

2.3.3.6  FF_USE_MKFS

2.3.3.7  FF_USE_LFN

2.3.4  修改diskio.c

2.3.4.1  宏定义修改

2.3.4.2  初始化磁盘驱动器

2.3.4.3  获取磁盘状态

2.3.4.4  从磁盘驱动器读扇区

2.3.4.5  从磁盘驱动器写扇区

2.3.4.6  控制设备实现指定功能

2.3.5  测试

2.3.5.1  f_mount()函数

2.3.5.2  f_mkfs()函数

2.3.5.3  f_open()函数

2.3.5.4  f_write()函数

2.3.5.5  f_read()函数


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 - Generic FAT Filesystem Module

        这里我们可以在跳转查看每个函数的使用以及作用等信息:

        对于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的代码,这里我使用我之前移植好的代码:

【OTA】手把手写BootLoader程序·硬件SPI读写Flash芯片(W25Q64)-CSDN博客

        通过上面官网下载所需要的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); 

        测试一下:

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

STM32学习笔记_时光の尘的博客-CSDN博客

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐