
目录
[1. SD卡简介](#1. SD卡简介)
[2. FatFs文件系统](#2. FatFs文件系统)
[3. 移植使用](#3. 移植使用)
[3.1 前期准备](#3.1 前期准备)
[3.2 修改ffconf.h](#3.2 修改ffconf.h)
[3.2.1 FF_CODE_PAGE](#3.2.1 FF_CODE_PAGE)
[3.2.2 FF_USE_STRFUNC](#3.2.2 FF_USE_STRFUNC)
[3.2.3 FF_FS_READONLY](#3.2.3 FF_FS_READONLY)
[3.2.4 FF_VOLUMES](#3.2.4 FF_VOLUMES)
[3.2.5 FF_MIN_SS/FF_MAX_SS](#3.2.5 FF_MIN_SS/FF_MAX_SS)
[3.2.6 FF_USE_MKFS](#3.2.6 FF_USE_MKFS)
[3.2.7 FF_USE_LFN](#3.2.7 FF_USE_LFN)
[3.3 修改diskio.c](#3.3 修改diskio.c)
[3.3.1 宏定义修改](#3.3.1 宏定义修改)
[3.3.2 初始化磁盘驱动器](#3.3.2 初始化磁盘驱动器)
[3.3.3 获取磁盘状态](#3.3.3 获取磁盘状态)
[3.3.4 从磁盘驱动器读扇区](#3.3.4 从磁盘驱动器读扇区)
[3.3.5 从磁盘驱动器写扇区](#3.3.5 从磁盘驱动器写扇区)
[3.3.6 控制设备实现指定功能](#3.3.6 控制设备实现指定功能)
[3.4 主函数](#3.4 主函数)
[3.4.1 f_mount()函数](#3.4.1 f_mount()函数)
[3.4.2 f_mkfs()函数](#3.4.2 f_mkfs()函数)
[3.4.3 f_open()函数](#3.4.3 f_open()函数)
[3.4.4 f_write()函数](#3.4.4 f_write()函数)
[3.4.5 f_read()函数](#3.4.5 f_read()函数)
[3.4.6 测试](#3.4.6 测试)
1. SD卡简介
SD卡一般都支持SDIO和SPI两种接口,SD卡使用9-pin接口通信,其中3根电源线,1根时钟线,1根命令线和4根数据线:

这里我们使用SDIO的通讯接口,SDIO(Secure Digital Input/Output)是基于 SD 卡标准扩展的高速串行通信接口,相比 SPI 模式,SDIO 接口支持 1/4 位总线宽度、更高的传输速率(最高可达 25MHz@4 位模式),且完全兼容 SD 卡协议。
文章篇幅原因,代码等详细介绍可以跳转到之前的文章进行查看:
2. FatFs文件系统
FatFs 是一款专为嵌入式系统设计的轻量级 FAT 文件系统模块,仅需通过简单的底层接口适配,即可在 STM32、51、ARM 等 MCU 上实现对 SD 卡、Flash、U 盘等存储介质的标准化文件操作(如创建 / 读写文件、目录管理等)。
完全兼容 FAT12/FAT16/FAT32 文件系统规范,支持:
- SDSC 卡(≤2GB):适配 FAT12/FAT16;
- SDHC/SDXC 卡(2GB~2TB):适配 FAT32;
- 可识别 PC 格式化的存储介质,无需额外适配。
同样详细介绍可以参考之前的文章,这一篇是W25Q64的文件系统移植,只看前两个小节即可,当然想要了解 W25Q64 的移植可以继续往下看:
下面我们对SD卡进行移植文件系统的过程其实和这个差不多。
3. 移植使用
3.1 前期准备
这里我实在我之前移植好的 SD 卡程序的基础上进行移植 FatFs 文件系统:
将工程中测试部分删掉,留下SDIO_SD_Card文件即可:

找到 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 相关函数示例代码 | 没用到 |
上表可以看出我们实际需要修改的其实就两个文件:ffconf.h 和 diskio.c,这里我们现将这些文件复制到工程当中,找到刚下载好的FatFs文件的source文件夹下的文件:

先把这些文件包含进来:

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

不过我们发现我们注释完还是有一堆报错,那是因为这里的一些文件都是调用的前面哪些文件的一些测试用例,也先注释掉即可,下面我们在进行移植SD卡文件的时候再进行修改。
按照上面更改完后,再次运行会发现还有一个报错,那是因为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时钟去获取真实时间):
cpp
// 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位
);
}

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

3.2.2 FF_USE_STRFUNC
找到 FF_CODE_PAGE 宏定义将其设置为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 |
3.2.3 FF_FS_READONLY
找到 FF_FS_READONLY 宏定义将其设置为0,启用文件系统的读写功能:
| 赋值 | 模式 | 功能说明 |
|---|---|---|
| 0 | 读写模式(默认) | 启用文件系统的读 + 写全部功能,支持创建、修改、删除文件 / 目录等操作 |
| 1 | 只读模式 | 仅保留文件读取功能,禁用所有写入 / 修改类操作,同时精简相关 API 代码占用 |
3.2.4 FF_VOLUMES
找到 FF_VOLUMES 根据自身想要使用多少设备,例如SD卡,Flash等进行设置,就是我们上面移植代码时,switch中注释掉部分:

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

3.2.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,进行更改:

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

3.2.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:

3.3 修改diskio.c
3.3.1 宏定义修改
把之前的宏定义注释掉,并且把switch也全部注释掉,自己声明两个宏定义:
cpp
#define SD_CARD 0 //用于识别SD卡
#define SPI_FLASH 1 //用于识别外部FLASH

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

这里默认未初始化状态:
cpp
DSTATUS status = STA_NOINIT;
我们知道 SD_Init() 初始化成功返回 SD_OK,因此这里在做一下判断,如果成功标记为就绪,如果失败保持未初始化状态:
cpp
DSTATUS disk_initialize (BYTE pdrv)
{
DSTATUS status = STA_NOINIT;
switch (pdrv)
{
case SD_CARD:
if(SD_Init() == SD_OK)//调用SDIO驱动的初始化函数
{
status &= ~STA_NOINIT;//初始化成功,标记为就绪
}
else
{
status = STA_NOINIT;//初始化失败,保持未初始化
}
break;
case SPI_FLASH:
break;
default:
status = STA_NOINIT;
}
return status;
}
3.3.3 获取磁盘状态
找到 disk_status() 函数,我们在这里进行判断设备状态,先将之前的注释去掉,将宏定义加进去,整理一下:
cpp
/*-----------------------------------------------------------------------*/
/* 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;
}
FatFs 在操作驱动器前,会先调用该函数检查驱动器状态,我们在调用 SD_Init() 的时候其实以及完成了这个功能(当然也可以更加细化操作,严谨一点),这里直接给就绪:
cpp
DSTATUS disk_status (BYTE pdrv)
{
DSTATUS status = STA_NOINIT; // 默认状态:未初始化
switch (pdrv)
{
case SD_CARD :
status &= ~STA_NOINIT; // 清除"未初始化"标志,标记为就绪
break;
case SPI_FLASH :
break;
default:
status = STA_NOINIT;
}
return status;
}
3.3.4 从磁盘驱动器读扇区
上面初始化完成后,我们开始读取磁盘数据,先整理一下:
cpp
/**
* @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;
}
读取数据需要使用到 SD_ReadMultiBlocks() 函数,其函数原型:
cpp
SD_Error SD_ReadMultiBlocks(uint8_t *readbuff, uint32_t ReadAddr, uint16_t BlockSize, uint32_t NumberOfBlocks)
- readbuff:输出缓冲区(存放读取的数据);
- ReadAddr:读取起始地址(字节地址,高容量卡自动转换为块地址);
- BlockSize:块大小(固定 512,高容量卡强制覆盖为 512);
- NumberOfBlocks:要读取的块数。
这里读取数据缓冲区也就是磁盘读取的 buff,起始地址为 sector*SDCardInfo.CardBlockSize,其中SDCardInfo:
cpp
typedef struct
{
SD_CSD SD_csd;//存储 SD 卡的硬件特性参数
SD_CID SD_cid;//存储 SD 卡的唯一身份信息
uint64_t CardCapacity; //SD 卡的总容量
uint32_t CardBlockSize; //SD 卡的单块(扇区)大小(字节),SD 卡标准规定默认值为 512 字节
uint16_t RCA;//SD 卡初始化后,主机(STM32)为卡分配的唯一逻辑地址
uint8_t CardType;//标识 SD 卡的类型,是地址转换、协议适配的核心参数
} SD_CardInfo;
CardBlockSize代表的是 SD 卡的单块(扇区)大小(字节),SD 卡标准规定默认值为 512 字节,块大小也就是 SDCardInfo.CardBlockSize,要读取的块数也就是 count,创建一个参数用于接收返回值:
cpp
SD_Error sd_state;
sd_state = SD_ReadMultiBlocks(buff,sector * SDCardInfo.CardBlockSize, SDCardInfo.CardBlockSize,count);
然后等待数据读取,返回是否读取成功:
cpp
sd_state = SD_ReadMultiBlocks(buff,sector * SDCardInfo.CardBlockSize, SDCardInfo.CardBlockSize,count);
sd_state = SD_WaitReadOperation();
while(SD_GetStatus() != SD_TRANSFER_OK);
if (sd_state == SD_OK)
status = RES_OK;
else
status = RES_ERROR;
当然我们也可以给出具体值,进行宏定义操作:
cpp
#define SD_BLOCKSIZE 512// SD卡默认扇区大小(固定512字节)
将块大小替换掉:
cpp
/* 调用SDIO驱动的多块读函数 */
SD_state=SD_ReadMultiBlocks(buff,(uint64_t)sector*SD_BLOCKSIZE,SD_BLOCKSIZE,count);//数据缓冲区,起始地址(LBA扇区→字节地址),单块大小(512字节),读取的扇区数量
if(SD_state==SD_OK)
{
/* Check if the Transfer is finished */
SD_state=SD_WaitReadOperation();// 等待读操作完成(必须!否则数据未传输完就返回)
while(SD_GetStatus() != SD_TRANSFER_OK);
}
if(SD_state!=SD_OK)
status = RES_PARERR;
else
status = RES_OK;
这里需要注意一下,我们 SD 卡的数据读写操作,是通过 DMA 进行数据传输的,需要注意,我们配置的是要求缓冲区地址是 4 字节对齐的,如果不对齐 DMA 传输会出错,因此我们在进行上面数据读写之前,还要进行缓冲区地址的对齐,我们通过与操作,判断缓冲区地址的最后 2 位是否为 0(非 0 则未对齐):
cpp
(DWORD)buff&3
举个例子
假如地址为:0x20000001,最后两位为01,01&11=1结果非零地址未对齐;
假如地址为:0x20000000,最后两位为00,00&11=0结果为零地址对齐;
若未对齐,先将数据读入 4 字节对齐的临时缓冲区scratch:
cpp
DWORD scratch[SD_BLOCKSIZE / 4]; // 4字节对齐的临时缓冲区(512/4=128个DWORD)
再通过memcpy拷贝到用户缓冲区:
cpp
while (count--)
{
res = disk_read(SD_CARD,(void *)scratch, sector++, 1);// 先读入对齐的临时缓冲区
if (res != RES_OK)
{
break;
}
memcpy(buff, scratch, SD_BLOCKSIZE);// 再拷贝到用户缓冲区(非对齐)
buff += SD_BLOCKSIZE;
}
此时代码:
cpp
/**
* @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;//默认返回参数错误
SD_Error SD_state = SD_OK;
switch (pdrv)
{
case SD_CARD :
if((DWORD)buff&3)//缓冲区地址非4字节对齐时的兼容(STM32 DMA要求4字节对齐)
{
DRESULT res = RES_OK;
DWORD scratch[SD_BLOCKSIZE / 4];// 4字节对齐的临时缓冲区(512/4=128个DWORD)
while (count--)
{
res = disk_read(SD_CARD,(void *)scratch, sector++, 1);// 先读入对齐的临时缓冲区
if (res != RES_OK)
{
break;
}
memcpy(buff, scratch, SD_BLOCKSIZE);// 再拷贝到用户缓冲区(非对齐)
buff += SD_BLOCKSIZE;
}
return res;
}
/* 调用SDIO驱动的多块读函数 */
SD_state=SD_ReadMultiBlocks(buff,(uint64_t)sector*SD_BLOCKSIZE,SD_BLOCKSIZE,count);//数据缓冲区,起始地址(LBA扇区→字节地址),单块大小(512字节),读取的扇区数量
if(SD_state==SD_OK)
{
/* Check if the Transfer is finished */
SD_state=SD_WaitReadOperation();// 等待读操作完成(必须!否则数据未传输完就返回)
while(SD_GetStatus() != SD_TRANSFER_OK);
}
if(SD_state!=SD_OK)
status = RES_PARERR;
else
status = RES_OK;
break;
case SPI_FLASH :
break;
default:
status = RES_PARERR;
}
return status;
}
3.3.5 从磁盘驱动器写扇区
也是先简单整理一下:
cpp
#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,就无法进行写操作,写入操作和读取操作差不多,只不过一个调用写函数,一个调用读函数:
cpp
sd_state = SD_WriteMultiBlocks((uint8_t *)buff,sector * SDCardInfo.CardBlockSize, SDCardInfo.CardBlockSize,count);
sd_state = SD_WaitWriteOperation();
while(SD_GetStatus() != SD_TRANSFER_OK);
if (sd_state == SD_OK)
status = RES_OK;
else
status = RES_ERROR;
写操作同样需要,地址对齐,操作和读类似,只不过读是先读在拷贝,写是先拷贝在读:
cpp
if((DWORD)buff&3) // 判断缓冲区地址是否4字节对齐(同disk_read逻辑)
{
DRESULT res = RES_OK;
// 定义4字节对齐的临时缓冲区(512/4=128个DWORD,天然对齐)
DWORD scratch[SD_BLOCKSIZE / 4];
while (count--)
{
// 步骤1:将用户非对齐缓冲区的数据拷贝到对齐的临时缓冲区
memcpy( scratch,buff,SD_BLOCKSIZE);
// 步骤2:递归调用自身,写入1个扇区(此时scratch是对齐的)
res = disk_write(SD_CARD,(void *)scratch, sector++, 1);
if (res != RES_OK) {
break; // 写入失败则退出循环
}
// 步骤3:缓冲区指针后移,准备写入下一个扇区
buff += SD_BLOCKSIZE;
}
return res; // 返回最终写入状态
}
3.3.6 控制设备实现指定功能
同样的现将函数整理一下:
cpp
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,其是设备能用于文件系统的扇区总数,由于不同 SD 卡,容量可能是不一样的,这里我们根据 SD 卡的总容量,除以单块的大小知道总扇区数量,这里也是用到了 SDCardInfo 用来获取相关参数:
cpp
/* 扇区数量 */
case GET_SECTOR_COUNT:
*(DWORD * )buff = SDCardInfo.CardCapacity/SDCardInfo.CardBlockSize;
break;
区块扇区的大小:
cpp
/* 扇区大小 */
case GET_SECTOR_SIZE :
*(WORD * )buff = SDCardInfo.CardBlockSize;
break;
同时擦除扇区的个数这里设为1:
cpp
case GET_BLOCK_SIZE :
*(DWORD * )buff = 1;
break;
此时代码:
cpp
DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void *buff)
{
DRESULT status = RES_PARERR;
switch (pdrv)
{
case SD_CARD :
switch (cmd)
{
// Get R/W sector size (WORD)
case GET_SECTOR_SIZE :
*(WORD * )buff = SD_BLOCKSIZE;
break;
// Get erase block size in unit of sector (DWORD)
case GET_BLOCK_SIZE :
*(DWORD * )buff = 1;
break;
case GET_SECTOR_COUNT:
*(DWORD * )buff = SDCardInfo.CardCapacity/SDCardInfo.CardBlockSize;
break;
case CTRL_SYNC :
break;
}
status = RES_OK;
break;
case SPI_FLASH :
break;
default:
status = RES_PARERR;
}
return status;
}
3.4 主函数
在主函数开始前,我们还需要在了解几个函数。
3.4.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; 仅挂载时有效,卸载时无意义。 |
我们在调用前先声明一下 SD 卡的工作区域:
cpp
FATFS fs;
其中对于 FATFS 的结构体可以在ff.h文件查看:

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

这里简单翻译一下:
cpp
/* 文件函数返回码(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;
这里我们创建一个变量用来接收结果:
cpp
FRESULT res_sd;
此时的调用:
cpp
res_sd = f_mount(&fs,"0:",1);
printf("文件系统第一次挂载: %d\r\n",res_sd);
3.4.2 f_mkfs()函数
其作用就是格式化存储介质,在指定逻辑驱动器上创建 FAT/exFAT 文件系统卷,该函数的函数原型:
cpp
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。 |
| opt | 指定包含格式化选项的 MKFS_PARM 结构体。若传入空指针,则函数使用所有选项的默认值。 | 参数 | BYTE n_fat | 指定 FAT/FAT32 卷的 FAT 表副本数量,有效值为 1 或 2;默认值(0)或无效值均会设为 1,exFAT 类型下该成员无效。 |
| opt | 指定包含格式化选项的 MKFS_PARM 结构体。若传入空指针,则函数使用所有选项的默认值。 | 参数 | UINT align | 指定卷数据区(文件分配池,通常为闪存介质的擦除块边界)的对齐方式,单位为扇区;有效值为 1~32768 且为 2 的幂。若传入 0(默认值)或无效值,函数会通过底层 disk_ioctl 函数获取块大小。 |
| opt | 指定包含格式化选项的 MKFS_PARM 结构体。若传入空指针,则函数使用所有选项的默认值。 | 参数 | UINT n_root | 指定 FAT 卷的根目录项数量,有效值最大为 32768 且需对齐到 "扇区大小 / 32";默认值(0)或无效值均设为 512,FAT32/exFAT 类型下该成员无效。 |
| opt | 指定包含格式化选项的 MKFS_PARM 结构体。若传入空指针,则函数使用所有选项的默认值。 | 参数 | 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由于我使用的是32GB的卡,因此使用FM_FAT32:
cpp
mkfs_opt.fmt = FM_FAT32; // 文件系统类型:FAT32
然后是对于 n_fat,它是 FatFs 格式化时指定 FAT 表的冗余副本数量,简单来说就是创建副本(备份),其中取值:
| 取值 | 实际生效值 | 含义 | 适用场景 |
|---|---|---|---|
| 1 | 1 | 仅创建 1 份 FAT 表(无备份) | 对空间极致节省、只读 / 少写场景 |
| 2 | 2 | 创建 2 份 FAT 表(主 FAT + 备份 FAT),格式化 / 写操作时同步更新两份 | 可写场景、需容错 |
| 0/≥3 | 1 | 无效值,FatFs 自动修正为 1(仅保留 1 份 FAT 表) | 无实际意义 |
这里我们是主+备份,因此创建为2:
cpp
mkfs_opt.n_fat = 2; // FAT表数量:2
接着是 align,其作用是格式化时指定 文件系统数据区的起始对齐偏移(单位:扇区) ,核心目的是让文件系统的 "数据区"(存储文件内容的区域)对齐到存储介质的擦除块边界(,避免 "跨擦除块写入" 导致的性能下降或数据损坏,其取值规则:
| 取值 | 实际行为 | 适用场景 |
|---|---|---|
| 0(默认值) | FatFs 自动调用 disk_ioctl 的 GET_BLOCK_SIZE 指令,获取擦除块大小,并按此值对齐 | 99% 的嵌入式场景 |
| 1~32768(2 的幂) | 强制按指定扇区数对齐(如设 1=4KB、设 2=8KB),需手动匹配擦除块大小 | 需精准控制对齐的特殊场景 |
| 非 2 的幂 / 超出范围 | 视为无效值,降级为 "取值 0" 的逻辑(自动获取块大小对齐) | 无实际意义 |
这里我们直接给0即可,当然设为1进行强制对齐也可以:
cpp
mkfs_opt.align = 0, // 扇区对齐:自动,或者1强制
然后是 n_root,这个参数仅对 FAT16/FAT12(FM_FAT) 有效,是格式化时指定根目录可容纳的最大文件 / 文件夹项数量;FAT32/exFAT 中根目录被设计为 "动态簇链"(无固定大小),因此该参数无效。
其取值规则:
| 取值情况 | 实际行为 | 适用场景 |
|---|---|---|
| 0(默认值) | FatFs 自动设为 512(标准值,也是嵌入式场景的最优默认值) | 无特殊需求的场景(推荐) |
| 有效值(≤32768 + 对齐扇区大小/32) | 按指定值创建根目录(如 128/256/512/1024),需保证总大小≤扇区整数倍 | 需定制根目录容量的场景 |
| 无效值(超 32768 / 未对齐) | 自动降级为 512 | 无实际意义,不推荐 |
| FAT32/exFAT 类型 | FAT32/exFAT 中根目录被设计为 "动态簇链"(无固定大小),该参数被完全忽略(设任何值都无效) | 无需关注此参数 |
这里我们随便设即可:
cpp
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 场景 |
这里我们使用的是SD卡,其默认为512:
cpp
mkfs_opt.au_size = 512// 分配单元大小
配置如下:
cpp
/* 格式化配置(适配SDHC/SDXC卡,默认FAT32) */
MKFS_PARM mkfs_opt = {
.fmt = FM_FAT32, // 文件系统类型:FAT32(兼容SDHC/SDXC)
.n_fat = 2, // FAT表数量:2(冗余备份,提高可靠性)
.align = 0, // 扇区对齐:自动适配SD卡物理扇区
.n_root = 0, // FAT32无需配置根目录项数(设0自动)
.au_size = 512 // 分配单元大小:512字节(SD卡默认扇区)
};
// 格式化0:驱动器,使用工作缓冲区
res_sd = f_mkfs("0:", &mkfs_opt, work_buf, sizeof(work_buf));
其中 work_buf 是格式化缓冲区,其作用实在 f_mkfs 执行格式化时,通过这块临时内存区域完成文件系统元数据的计算、写入和校验,缓冲区大小至少≥1 个扇区(否则无法完整缓存一个扇区的元数据),通过__attribute__((aligned(4))),然后再完善一下别的判断条件。
3.4.3 f_open()函数
f_open 是 FatFs 中打开 / 创建文件的核心函数,其函数原型:
cpp
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文件类型如下:
cpp
/* 文件对象结构体(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;

我们在操作文件前需要先声明一下,告诉我们需要操作那个文件:
cpp
FIL fnew;
然后是 path 起一个想要打开的文件名,由于我们在上面修改ffconf.h文件已经修改过中文格式,因此这里可以直接使用中文类型,最后一个参数是对该文件赋予什么权限,这里我们给其覆盖写权限:
cpp
res_sd = f_open(&fnew, "0:FatFs读写测试文件.txt", FA_CREATE_ALWAYS | FA_WRITE);
3.4.4 f_write()函数
f_write 函数用于向已打开的文件写入数据,其函数原型为:
cpp
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你想要写入的数据,随便写点:
cpp
BYTE WriteBuffer[] = "这是一个SD卡移植FatFs文件系统实验\r\n"; /* 写缓冲区*/
那么:
cpp
res_sd = f_write(&fnew, WriteBuffer, strlen((char*)WriteBuffer), &fnum);
3.4.5 f_read()函数
读和写其实用法上差不读,只不过换成读了:

cpp
res_sd = f_read(&fnew, ReadBuffer, sizeof(ReadBuffer), &fnum);
3.4.6 测试
此时完整的主函数:
cpp
#include "stm32f10x.h"
#include "Delay.h"
#include "Bsp_LED_Gpio.h"
#include "Bsp_Usartx.h"
#include "SDIO_SD_Card.h"
#include "ff.h"
#include <string.h>
#include <stdio.h>
FATFS fs; /* FatFs文件系统对象 */
FIL fnew; /* 文件对象 */
FRESULT res_sd; /* 文件操作结果 */
UINT fnum; /* 文件成功读写数量 */
BYTE ReadBuffer[1024]={0}; /* 读缓冲区 */
BYTE WriteBuffer[] = /* 写缓冲区*/
"这是一个SD卡移植FatFs文件系统实验\r\n";
// 格式化工作缓冲区(4KB,对齐4字节,适配SD卡扇区)
uint8_t work_buf[4096] __attribute__((aligned(4)));
int main(void)
{
LED_GPIO_Config();
USART_Config();
printf("\r\n****** 这是一个SD卡 文件系统实验 ******\r\n");
// 在外部SD卡挂载文件系统(盘符统一用0:,opt=1立即挂载)
res_sd = f_mount(&fs,"0:",1);
printf("文件系统第一次挂载: %d\r\n",res_sd);
/*----------------------- 格式化测试 ---------------------------*/
/* 如果没有文件系统就格式化创建文件系统 */
if(res_sd == FR_NO_FILESYSTEM)
{
printf("SD卡还没有文件系统,开始格式化...\r\n");
// 格式化前必须卸载文件系统(opt=0,取消挂载)
res_sd = f_mount(NULL, "0:", 0);
if(res_sd != FR_OK)
{
printf("卸载文件系统失败: %d\r\n",res_sd);
while(1); // 格式化前置条件失败,卡死报错
}
/* 格式化配置(适配SDHC/SDXC卡,默认FAT32) */
MKFS_PARM mkfs_opt = {
.fmt = FM_FAT32, // 文件系统类型:FAT32(兼容SDHC/SDXC)
.n_fat = 2, // FAT表数量:2(冗余备份,提高可靠性)
.align = 0, // 扇区对齐:自动适配SD卡物理扇区
.n_root = 0, // FAT32无需配置根目录项数(设0自动)
.au_size = 512 // 分配单元大小:512字节(SD卡默认扇区)
};
// 格式化0:驱动器,使用工作缓冲区
res_sd = f_mkfs("0:", &mkfs_opt, work_buf, sizeof(work_buf));
if(res_sd == FR_OK)
{
printf("SD卡已成功格式化为FAT32文件系统。\r\n");
/* 格式化后重新挂载(先卸载再挂载,确保状态干净) */
res_sd = f_mount(NULL, "0:", 0); // 卸载(opt=0)
res_sd = f_mount(&fs, "0:", 1); // 重新挂载
if(res_sd != FR_OK)
{
printf("格式化后重新挂载失败: %d\r\n",res_sd);
while(1);
}
}
else
{
printf("格式化失败!!! 错误码: %d\r\n",res_sd);
while(1);
}
}
else if(res_sd != FR_OK)
{
printf("!!SD卡挂载文件系统失败。错误码: %d\r\n",res_sd);
printf("!!可能原因:SD卡初始化不成功 / 盘符错误。\r\n");
while(1);
}
else
{
printf("》文件系统挂载成功,可以进行读写测试\r\n");
}
/*----------------------- 文件系统测试:写测试 -----------------------------*/
printf("\r\n****** 即将进行文件写入测试... ******\r\n");
// 打开/创建文件(0:盘符,FA_CREATE_ALWAYS=覆盖创建,FA_WRITE=可写)
res_sd = f_open(&fnew, "0:FatFs读写测试文件.txt", FA_CREATE_ALWAYS | FA_WRITE);
if ( res_sd == FR_OK )
{
printf("》打开/创建FatFs读写测试文件.txt成功,开始写入数据。\r\n");
// 写入数据:strlen(WriteBuffer) 排除末尾\0,符合预期
res_sd = f_write(&fnew, WriteBuffer, strlen((char*)WriteBuffer), &fnum);
if(res_sd == FR_OK)
{
f_sync(&fnew); // 强制将缓存数据刷入SD卡,避免数据丢失
printf("》文件写入成功,写入字节数:%d\n",fnum);
printf("》写入的数据:\r\n%s\r\n",WriteBuffer);
}
else
{
printf("!!文件写入失败:错误码 %d\n",res_sd);
}
// 关闭文件并检查返回值
res_sd = f_close(&fnew);
if(res_sd != FR_OK)
{
printf("!!关闭文件失败:错误码 %d\n",res_sd);
}
}
else
{
printf("!!打开/创建文件失败,错误码:%d\r\n",res_sd);
}
/*------------------- 文件系统测试:读测试 ------------------------------------*/
printf("\r\n****** 即将进行文件读取测试... ******\r\n");
// 打开已存在的文件(FA_OPEN_EXISTING=仅打开已存在,FA_READ=只读)
res_sd = f_open(&fnew, "0:FatFs读写测试文件.txt", FA_OPEN_EXISTING | FA_READ);
if(res_sd == FR_OK)
{
printf("》打开文件成功。\r\n");
// 清空读缓冲区,避免残留旧数据
memset(ReadBuffer, 0, sizeof(ReadBuffer));
// 读取文件数据到缓冲区
res_sd = f_read(&fnew, ReadBuffer, sizeof(ReadBuffer), &fnum);
if(res_sd == FR_OK)
{
printf("》文件读取成功,读取字节数:%d\r\n",fnum);
printf("》读取的数据:\r\n%s \r\n", ReadBuffer);
}
else
{
printf("!!文件读取失败:错误码 %d\n",res_sd);
}
// 关闭文件
res_sd = f_close(&fnew);
if(res_sd != FR_OK)
{
printf("!!关闭文件失败:错误码 %d\n",res_sd);
}
}
else
{
printf("!!打开文件失败,错误码:%d\r\n",res_sd);
}
/* 不再使用文件系统,取消挂载 */
f_mount(NULL, "0:", 0);
while (1)
{
// 主循环空操作
}
}
运行看一下结果:

通过读卡器看一下SD卡内容,并且文件日期,由于我们是个固定值此时也是:


