【STM32】两万字详解SD卡移植最新版本FatFs文件系统(ff16)

目录

[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 卡协议。

文章篇幅原因,代码等详细介绍可以跳转到之前的文章进行查看:

【STM32】吃透 SDIO 驱动,八万字详解 SD 卡底层原理与实战开发-CSDN博客

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 的移植可以继续往下看:

【STM32】四万字详解最新版本(ff16)FatFs文件系统移植(库函数版本)-CSDN博客

下面我们对SD卡进行移植文件系统的过程其实和这个差不多。

3. 移植使用

3.1 前期准备

这里我实在我之前移植好的 SD 卡程序的基础上进行移植 FatFs 文件系统:

基于STM32使用SDIO读写SD卡数据.zip资源-CSDN下载

将工程中测试部分删掉,留下SDIO_SD_Card文件即可:

找到 FatFs 官网,下载其当前最新版本的文件系统:

FatFs - Generic FAT Filesystem Module

找到图示位置,我们可以下载当前最新版本,点击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卡内容,并且文件日期,由于我们是个固定值此时也是:

SD开移植FatFs文件系统·最新版本ff16资源-CSDN下载

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

相关推荐
bai5459364 小时前
STM32 CubeIDE 使用串口中断模式
stm32·单片机·嵌入式硬件
fanged5 小时前
STM32(4)--时钟树
stm32·单片机·嵌入式硬件
__万波__5 小时前
STM32L475蜂鸣器实验
stm32·单片机·嵌入式硬件
List<String> error_P8 小时前
STM32 GPIO HAL库常用函数
stm32·单片机·hal库
小痞同学9 小时前
【铁头山羊STM32】HAL库 5.SPI部分
stm32·单片机·嵌入式硬件
蓬荜生灰9 小时前
STM32(5)-- 新建寄存器版工程
stm32·单片机·嵌入式硬件
大神与小汪10 小时前
STM32上进行Unix时间戳转换
stm32·嵌入式硬件·unix
嗯嗯=10 小时前
STM32单片机学习篇1
stm32·单片机·嵌入式硬件
橙露12 小时前
STM32 单片机实战:基于 HAL 库的串口通信与中断处理详解
stm32·单片机·嵌入式硬件