Cubemx文件系统挂载多设备
cubumx版本:6.13.0
芯片:STM32F407VET6
在上一篇文章中介绍了Cubemx的FATFS和SD卡的配置,由于SD卡使用的是SDIO通讯,因此具体驱动不需要自己实现,Cubemx中就可以直接配置然后生成SDIO的驱动,并将SD卡驱动和FATFS绑定。这里我们再实现一个外部FLASH来作为FATFS的另一个存储设备,FLASH的型号为W25Q64,Cubemx的FATFS需要在原来的SD卡配置的基础上增加一个User-defined的配置,用于说明还会在FATFS上挂载一个用户自定义驱动的存储设备。同时挂载多个设备时,FATFS的MIN_SS和MAX_SS范围需要涵盖所有设备的扇区大小,Cubemx的FATFS设置如下:

FATFS参数配置说明
ffconf.h:
/*-----------------------------------------------------------------------------/
/ Function Configurations FATFS功能裁剪配置
/-----------------------------------------------------------------------------*/#define _FS_READONLY 0 /* 0:Read/Write or 1:Read only */
//定义:设置文件系统是否只读。
//0:读写模式(可读写文件)。
//1:只读模式(只能读取文件)。
//影响:只读模式会禁用写入相关函数,例如 f_write、f_sync、f_unlink 等,减小代码体积。#define _FS_MINIMIZE 0 /* 0 to 3 */
//定义:设置精简级别,移除部分 API 函数。
//0:启用所有基本函数。
//1:移除 f_stat、f_getfree、f_unlink、f_mkdir、f_truncate 和 f_rename。
//2:在级别 1 的基础上,移除 f_opendir、f_readdir 和 f_closedir。
//3:在级别 2 的基础上,移除 f_lseek。#define _USE_STRFUNC 2 /* 0:Disable or 1-2:Enable */
//定义:控制字符串相关函数(如 f_gets、f_putc、f_puts 和 f_printf)的启用。
//0:禁用字符串函数。
//1:启用字符串函数,无换行符转换。
//2:启用字符串函数,并在 LF 与 CRLF 之间转换。#define _USE_FIND 0
//定义:控制目录过滤读取功能(f_findfirst 和 f_findnext)。
//0:禁用。
//1:启用。
//2:启用,同时匹配备用文件名。#define _USE_MKFS 1
//定义:控制 f_mkfs(格式化磁盘)函数的启用。
//0:禁用。
//1:启用。#define _USE_FASTSEEK 1
//定义:启用快速文件定位功能。
//0:禁用。
//1:启用(需要文件系统的 SEEK 表支持)。#define _USE_EXPAND 0
//定义:启用 f_expand(扩展文件大小)功能。
//0:禁用。
//1:启用。#define _USE_CHMOD 0
//定义:启用属性修改函数(f_chmod 和 f_utime)。
//0:禁用。
//1:启用(需要 _FS_READONLY = 0)。#define _USE_LABEL 0
//定义:控制卷标操作函数(f_getlabel 和 f_setlabel)。
//0:禁用。
//1:启用。#define _USE_FORWARD 0
//定义:启用 f_forward 函数,用于流式数据转发(例如,直接发送到 UART)。
//0:禁用。
//1:启用。/*-----------------------------------------------------------------------------/
/ Locale and Namespace Configurations 读写文件操作的格式设置
/-----------------------------------------------------------------------------*/#define _CODE_PAGE 850
//定义:设置目标系统使用的 OEM 代码页,用于字符编码。
//常见值:437(美国),850(Latin 1),936(简体中文),932(日文)。
//如果设置不正确,可能导致文件打开失败。#define _USE_LFN 2 /* 0 to 3 */
#define _MAX_LFN 255 /* Maximum LFN length to handle (12 to 255) */
//定义:_USE_LFN:启用长文件名(LFN);_MAX_LFN:设置最长支持的文件名长度(12~255),建议为255。
//启用LFN时必须添加Unicode处理功能(在option/unicode.c中实现),同时LFN工作缓冲区会占用 //(_MAX_LFN + 1)*2字节,如果文件系统类型为exFAT还会额外占用608字节。当_MAX_LFN设置为255时可支 //持完整的长文件名操作。
//_USE_LFN:
//0:禁用。
//1:启用(使用静态缓冲区,不支持线程安全)。
//2:启用(使用堆栈动态缓冲区)。
//3:启用(使用堆动态缓冲区)。#define _LFN_UNICODE 0 /* 0:ANSI/OEM or 1:Unicode */
//定义:切换 API 的字符编码方式。
//0: 使用 ANSI/OEM 编码(通常是非 Unicode 的本地编码,如 ASCII)。
//1: 使用 UTF-16(Unicode 编码),支持更多语言字符。#define _STRF_ENCODE 3
//定义:设置字符串 I/O 函数(如 f_gets、f_puts)在文件中读写的字符编码。
//0:ANSI/OEM。
//1:UTF-16LE。
//2:UTF-16BE。
//3:UTF-8。#define _FS_RPATH 0 /* 0 to 2 */
//定义:支持相对路径。
//0:禁用相对路径。
//1:启用相对路径(支持 f_chdir 和 f_chdrive)。
//2:在级别 1 的基础上启用 f_getcwd。/*---------------------------------------------------------------------------/
/ Drive/Volume Configurations
/----------------------------------------------------------------------------*/#define _VOLUMES 3
//定义:设置支持的逻辑驱动器数(卷)。/* USER CODE BEGIN Volumes */
#define _STR_VOLUME_ID 0 /* 0:Use only 0-9 for drive ID, 1:Use strings for drive ID */
//定义:切换卷标(Volume ID)的格式。如果设置为1启用字符串模式,还需定义_VOLUME_STRS
//0: 卷标只使用数字(如 0:, 1: 表示逻辑驱动器)。
//1: 卷标可以使用字符串。
#define _VOLUME_STRS "RAM","NAND","CF","SD1","SD2","USB1","USB2","USB3"
//定义:每个逻辑驱动器单独的字符串ID。例如:"SD1" 表示第一个 SD 卡设备,"USB1" 表示第一个 USB 存储设 //备。注意:字符串卷标的字符限制为A-Z和0-9,数量应等于_VOLUMES 的值。
/* USER CODE END Volumes */#define _MULTI_PARTITION 0 /* 0:Single partition, 1:Multiple partition */
//定义:是否支持切换一个物理设备上的多个分区。
//0: 不支持多分区。每个逻辑驱动器号固定绑定到一个物理驱动器号。仅能挂载物理驱动器上存在的第一个 //FAT 分区。例如一个SD卡中有C盘和D盘两个分区,挂载时只会挂载SD卡第一个分区盘。
//1: 支持多分区。可以通过 VolToPart[]数组配置逻辑驱动器与物理驱动器、分区的绑定关系。还可以使用 //f_fdisk()函数操作分区表。适用场景:在需要管理多个分区(例如 SD 卡或硬盘有多个分区)的场景下启 //用。#define _MIN_SS 512 /* 512, 1024, 2048 or 4096 */
#define _MAX_SS 4096 /* 512, 1024, 2048 or 4096 */
//定义:设置存储设备扇区大小范围。
//值:SD卡通常为512,外部FLASH可能会更大,所用存储设备扇区大小应该在_MIN_SS和_MAX_SS之间,如果
//_MAX_SS大于_MIN_SS,则FATFS会调用disk_ioctl()函数中的GET_SECTOR_SIZE命令去获取设备扇区大小#define _USE_TRIM 0
//定义:是否支持ATA-TRIM指令。启用该功能需要在disk_ioctl()函数中实现CTRL_TRIM命令,用于触发 //TRIM操作。适用场景:使用SSD或其他支持TRIM的存储设备时,启用此功能可以提高性能和延长设备寿命。
//0: 禁用TRIM功能。
//1: 启用TRIM功能。#define _FS_NOFSINFO 0 /* 0,1,2 or 3 */
//定义:控制对FAT32的FSINFO区的使用。FSINFO是FAT32文件系统中的一个特定结构,用于记录剩余空闲簇
//数和最后分配的簇号。如果文件系统可能损坏或设备需要准确计算空闲空间,设置bit0=1。如果启用了动态 //内存管理或对性能要求较高,可以信任FSINFO(bit0=0 和 bit1=0)
//不同的位设置方式如下:
//bit 0: 是否信任空闲簇计数(free cluster count)。
//0: 使用 FSINFO 中的空闲簇计数(默认)。
//1: 不信任 FSINFO,首次调用 f_getfree() 时执行全盘 FAT 表扫描以计算空闲簇。
//bit 1: 是否信任最后分配簇号(last allocated cluster number)。
//0: 使用 FSINFO 中的最后分配簇号(默认)。
//1: 不信任 FSINFO 中的最后分配簇号。/*---------------------------------------------------------------------------/
/ System Configurations
/----------------------------------------------------------------------------*/#define _FS_TINY 0 /* 0:Normal or 1:Tiny */
//定义: 决定是否使用“Tiny”模式的缓冲区配置。
//0 (Normal):使用标准模式。每个打开的文件对象 (FIL) 包含一个私有的扇区缓冲区。这种方式占用更多 //内存,但文件访问效率较高,特别是对多个文件进行并发操作时。
//1 (Tiny):启用Tiny配置。在Tiny模式下,每个文件对象(FIL)中会移除私有的扇区缓冲区。所有文件共享 //一个公共的扇区缓冲区,这个缓冲区存储在文件系统对象 (FATFS) 中。好处是减少内存消耗,适合低内存环 //境,但多个文件并发操作时性能可能会下降。#define _FS_EXFAT 1
//定义:支持 exFAT 文件系统(需启用 LFN)。
//0:不支持
//1:支持#define _FS_NORTC 0
#define _NORTC_MON 6
#define _NORTC_MDAY 4
#define _NORTC_YEAR 2015
//定义:禁用RTC时间戳功能,禁用后文件修改时间将会被设置为_NORTC_YEAR、_NORTC_MON、_NORTC_MDAY
//但是对只读文件不起作用
//0:禁用
//1:不禁用#define _FS_LOCK 2 /* 0:Disable or >=1:Enable */
//定义:文件锁控制,避免并发操作问题。当_FS_READONLY为1时,本设置必须为0
//0:禁用。
//>0:启用,并设置最大同时打开的文件数量。#define _FS_REENTRANT 1 /* 0:Disable or 1:Enable */
//定义:决定是否启用文件系统的可重入性(线程安全)。启用可重入性后涉及文件/目录访问的函数调用需要通 //过同步对象(如信号量、互斥锁)控制访问。因此需要用户提供以下函数的实现:ff_req_grant():请求同 //步对象;ff_rel_grant():释放同步对象;ff_cre_syncobj():创建同步对象;ff_del_syncobj():删除 //同步对象;
//0:禁用可重入性。不提供线程安全机制。如果多个线程同时访问同一个文件系统卷(比如读写同一个文 //件),可能会导致数据损坏。
//1:启用可重入性。增加线程同步机制,确保多个线程访问同一文件系统卷时不发生冲突。
#define _USE_MUTEX 0 /* 0:Disable or 1:Enable */
//定义: 决定是否使用互斥锁作为文件系统可重入性的同步机制。
//0:禁用互斥锁。需要实现其他同步机制(如信号量)。
//1:启用互斥锁。使用互斥锁作为线程同步的主要工具。
#define _FS_TIMEOUT 1000 /* Timeout period in unit of time ticks */
//定义: 定义同步操作的超时时间。取决于系统的时间单位(通常是“时钟节拍”,一般为1ms)。如果线程在等 //待同步对象时超过了指定的时间,就会返回超时错误。
#define _SYNC_t osSemaphoreId_t
//定义: 定义同步对象的类型。该类型取决于具体操作系统的同步机制,比如信号量或互斥锁。在 FreeRTOS //中,可以定义为 SemaphoreHandle_t。在 CMSIS RTOS中,可以定义为 osSemaphoreId_t。需要在ff.h的//范围内包含操作系统的头文件,以确保同步对象类型定义有效。/* define the ff_malloc ff_free macros as FreeRTOS pvPortMalloc and vPortFree macros */
#if !defined(ff_malloc) && !defined(ff_free)
#define ff_malloc pvPortMalloc
#define ff_free vPortFree
//设置FATFS的动态分配内存和动态释放内存函数,这里直接使用FreeRTOS的相关函数,也说明了Cubemx配置
//FATFS时必须也要配置FreeRTOS
#endif
FATFS会为每一个存储设备对象分配一个单独的win缓冲区,大小一般为_MAX_SS个字节。在不开启_FS_TINY的情况下,存储设备对象每打开一个FIL文件,还会为该文件分配一个私有的扇区缓冲区。在Tiny模式下,存储设备对象打开的所有文件共享该对象的win缓冲区;
需要注意的是这里的文件缓冲区和f_mkfs ( const TCHAR* path, BYTE opt,DWORD au, void* work, UINT len )格式化缓冲区不是一个概念,格式化时使用的缓冲区是专门用于格式化操作的,与 FATFS 文件系统的内部缓冲区无直接关系。主要用于存储临时数据(例如扇区数据)在格式化文件系统过程中使用,格式化之后就不需要了。如果内存需求较大或设备内存有限,可以使用malloc动态分配格式化缓冲区内存,格式化结束后释放。缓冲区大小必须要为大于等于MAX_SS;
FATFS中,比较重要的两个数据类型是FATFS存储设备对象和FIL文件对象,如下:
ff.h:
/* File system object structure (FATFS) */typedef struct {BYTE fs_type; /* 文件系统类型标志,0表示未挂载,其他值表示FAT12、FAT16、FAT32 或 exFAT等 */BYTE drv; /* 物理驱动器号(逻辑盘号) */BYTE n_fats; /* FAT表的数量(1或2)大多数情况为 2,表示有主FAT和备份FAT*/BYTE wflag; /* win[]缓冲区状态标志,标志位bit0=1表示缓冲区已修改,需要同步写回到存储设备*/BYTE fsi_flag; /* FSINFO节点状态标志(仅适用于FAT32)bit7=1: 禁用FSINFO节点。
bit0=1: FSINFO节点已被修改,需要写回*/WORD id; /* 文件系统挂载 ID,用于标识挂载的卷。每次挂载或重新格式化时都会更新此ID,用于防止错误访问已卸载的卷 */WORD n_rootdir; /* 根目录条目数(仅适用于FAT12/16,每个条目为32字节,默认大小为 512条目)在FAT32中根目录大小是动态分配的,此参数为0。*/WORD csize; /* 每个簇包含的扇区数(簇大小) */
#if _MAX_SS != _MIN_SSWORD ssize; /* 扇区大小(以字节为单位,值为512、1024、2048、4096) */
#endif
#if _USE_LFN != 0WCHAR* lfnbuf; /* 长文件名(LFN)工作缓冲区,在ffconf.h中决定在堆还是栈中分配 */
#endif
#if _FS_EXFATBYTE* dirbuf; /* 目录条目块缓冲区(仅适用于exFAT),大小为几百个字节,按ffconf.h的定义为608字节,分配方式和lfnbuf一致 */
#endif
#if _FS_REENTRANT_SYNC_t sobj; /* 同步对象标识符(在多线程模式下启用) */
#endif
#if !_FS_READONLYDWORD last_clst; /* 最后分配的簇号。FAT 文件系统分配文件时用于快速查找下一个空簇 */DWORD free_clst; /* 空闲簇的数量。用于加速 f_getfree 函数计算可用空间 */
#endif
#if _FS_RPATH != 0DWORD cdir; /* 当前目录的起始簇号(根目录为 0)。用于跟踪当前工作目录。 */
#if _FS_EXFATDWORD cdc_scl; /* 包含目录的起始簇号(仅适用于 exFAT)。当cdir为0(在根目录)时无效。 */DWORD cdc_size; /* 包含目录的大小和链状态。bit31-bit8: 目录大小(以字节为单位)。
bit7-bit0: 链状态。 */DWORD cdc_ofs; /* 包含目录的偏移量。当cdir为0(在根目录)时无效。*/
#endif
#endifDWORD n_fatent; /* FAT 表的总条目数(簇总数 + 2)用于计算卷的实际容量。 */DWORD fsize; /* FAT 表的大小(以扇区为单位)*/DWORD volbase; /* 卷的起始扇区号。用于支持多分区模式 */DWORD fatbase; /* FAT 表的起始扇区号 */DWORD dirbase; /* 根目录的起始扇区号(在FAT32中为起始簇号) */DWORD database; /* 数据区的起始扇区号。文件和目录的实际数据存储区 */DWORD winsect; /* 记录当前存储在文件系统对象的win[]缓冲区中的逻辑扇区号*/BYTE win[_MAX_SS]; /* 磁盘访问缓冲区(用于 FAT、目录和文件数据)。win[]是一个扇区缓存,用于暂时存储存储设备中的某个扇区数据。每次需要读取或写入文件系统元数据(如目录表、FAT 表等)时,系统会先将目标扇区加载到 win[];当需要修改文件系统元数据时,修改首先发生在 win[]缓冲区中,而不是直接写回存储设备。修改完成后,需要调用sync操作(如f_sync())将 win[] 中的数据写回存储设备的对应扇区。_FS_TINY 配置为1时不创建文件的私有缓冲区,win[]缓冲区用于所有文件传输操作 */
} FATFS;/* File object structure (FIL) */typedef struct {_FDID obj; /* 对象标识符,用于管理和验证文件对象。) */BYTE flag; /* 用于表示文件的状态,如打开模式、访问权限等。 */BYTE err; /* 当操作出错时,通过此标志提供具体错误信息 */FSIZE_t fptr; /* 指示当前文件读/写操作的位置(以字节为单位,从存储设备的第0字节开始计算) */DWORD clust; /* 当前读/写位置的簇号,当fptr为0时,clust无效 */DWORD sect; /* 记录文件私有缓冲区buf[]中当前数据所对应的逻辑扇区号,0表示无效,用于加速数据访问,避免频繁的磁盘读写 */
#if !_FS_READONLYDWORD dir_sect; /* 记录文件的目录项所在扇区位置,用于更新文件的目录项(如文件长度、时间戳等)在写入文件时,便于快速找到文件的目录信息。 */BYTE* dir_ptr; /* 指向文件目录项在文件系统窗口win[]中的位置,用于直接修改文件的目录项数据,用于文件写入或文件关闭时的目录项更新 */
#endif
#if _USE_FASTSEEKDWORD* cltbl; /* 指向簇链映射表的指针,用于支持快速定位文件的特性(快速查找)*/
#endif
#if !_FS_TINYBYTE buf[_MAX_SS]; /* 文件的私有数据读/写缓冲区,功能类似与FATFS数据类型中的win[],使用私有缓冲区可以加快文件读写速度 */
#endif
} FIL;
W25Q64驱动如下:
HAL库W25Qxx系列芯片驱动-CSDN博客
在实现完驱动之后,我们需要手动将驱动和FATFS绑定在一起,绑定步骤如下:
STM32CubeMX学习笔记(25)——FatFs文件系统使用(操作SPI Flash)_stm32 fatfs-CSDN博客
CubeMX配置STM32实现FatFS文件系统(五)_stm32cubemx文件系统-CSDN博客
关于FATFS设备驱动绑定的具体实现如下:
注意这里我把SD卡和SPI_FLASH的驱动都写到了user_diskio.c中,但其实SD卡的驱动本质上还是调用的Cubemx生成的sd_diskio.c中的函数,因此我们挂载设备时使用SD_Driver还是USER_Driver都可以。
user_diskio.c:
/* Private typedef -----------------------------------------------------------*/
/* Private define ------------------------------------------------------------*/
#define SD_CARD 0 // 外部SD卡 这里定义的值和lun参数要对应上
#define SPI_FLASH 1 // 外部SPI Flash#define SPI_FLASH_OFFSET 0 // 外部SPI Flash偏移量,偏移后的空间给FATFS,偏移前的空间用于存储类似固件等非FATFS控制下的信息
/* Private variables ---------------------------------------------------------*//*** @brief Initializes a Drive* @param pdrv: Physical drive number (0..)* @retval DSTATUS: Operation status*/
DSTATUS USER_initialize (BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{/* USER CODE BEGIN INIT */uint16_t i;DSTATUS status = STA_NOINIT; switch (pdrv) { case SD_CARD: /* SD卡 */ {status = SD_Driver.disk_initialize(pdrv);break;}case SPI_FLASH: /* SPI Flash */ {/* 初始化SPI Flash */// 检查初始化函数返回值if(BSP_W25Qx_Init() != W25Qx_OK) {status = STA_NOINIT;break;}/* 获取SPI Flash芯片状态 */status = USER_status(SPI_FLASH); break;}default:status = STA_NOINIT;}return status;/* USER CODE END INIT */
}/*** @brief Gets Disk Status* @param pdrv: Physical drive number (0..)* @retval DSTATUS: Operation status*/
DSTATUS USER_status (BYTE pdrv /* Physical drive number to identify the drive */
)
{/* USER CODE BEGIN STATUS */DSTATUS status = STA_NOINIT;switch (pdrv) {case SD_CARD: /* SD卡 */ {status = SD_Driver.disk_status(pdrv);break;}case SPI_FLASH: { uint8_t ID[4]; //设备ID缓存数组BSP_W25Qx_Read_ID(ID);/* SPI Flash状态检测:读取SPI Flash 设备ID */if((ID[0] == W25Q64_FLASH_ID >> 8) && (ID[1] == (W25Q64_FLASH_ID&0xFF))){/* 设备ID读取结果正确 */status &= ~STA_NOINIT;}else{/* 设备ID读取结果错误 */status = STA_NOINIT;;}break;}default:status = STA_NOINIT;}return status;/* USER CODE END STATUS */
}/*** @brief Reads Sector(s)* @param pdrv: Physical drive number (0..)* @param *buff: Data buffer to store read data* @param sector: Sector address (LBA)* @param count: Number of sectors to read (1..128)* @retval DRESULT: Operation result*/
DRESULT USER_read (BYTE pdrv, /* Physical drive nmuber to identify the drive */BYTE *buff, /* Data buffer to store read data */DWORD sector, /* Sector address in LBA */UINT count /* Number of sectors to read */
)
{/* USER CODE BEGIN READ */DRESULT status = RES_PARERR;switch (pdrv) {case SD_CARD: /* SD卡 */ {status = SD_Driver.disk_read(pdrv,buff,sector,count);break;}case SPI_FLASH:{/* 扇区偏移SPI_FLASH_OFFSET,外部Flash文件系统放在SPI Flash后面的空间,前面的SPI_FLASH_OFFSET空间可以用于存储固件等非文件系统控制下的信息 */sector += SPI_FLASH_OFFSET; if (BSP_W25Qx_Read(buff, sector <<12, count<<12) != W25Qx_OK){status = RES_ERROR;} else {status = RES_OK;} break;}default:status = RES_PARERR;}return status;/* USER CODE END READ */
}/*** @brief Writes Sector(s)* @param pdrv: Physical drive number (0..)* @param *buff: Data to be written* @param sector: Sector address (LBA)* @param count: Number of sectors to write (1..128)* @retval DRESULT: Operation result*/
#if _USE_WRITE == 1
DRESULT USER_write (BYTE pdrv, /* Physical drive nmuber to identify the drive */const BYTE *buff, /* Data to be written */DWORD sector, /* Sector address in LBA */UINT count /* Number of sectors to write */
)
{/* USER CODE BEGIN WRITE *//* USER CODE HERE */uint32_t write_addr; DRESULT status = RES_PARERR;if (!count) {return RES_PARERR; /* Check parameter */}switch (pdrv) {case SD_CARD: /* SD卡 */ {status = SD_Driver.disk_write(pdrv,buff,sector,count);break;}case SPI_FLASH:{/* 扇区偏移SPI_FLASH_OFFSET,外部Flash文件系统放在SPI Flash后面的空间,前面的空间可以用于存储固件等非文件系统控制下的信息 */sector += SPI_FLASH_OFFSET;write_addr = sector << 12; // 假设扇区大小4096字节for (UINT i = 0; i < count; i++) { //默认文件缓冲区为1个扇区大小时这个循环没用,因为一次只会写入一个扇区uint32_t current_addr = write_addr + (i << 12);// 擦除当前扇区对应的块if (BSP_W25Qx_Erase_Block(current_addr) != W25Qx_OK) {return RES_ERROR;}}// 写入所有扇区if (BSP_W25Qx_Write((uint8_t *)buff, write_addr, count << 12) != W25Qx_OK) {return RES_ERROR;}status = RES_OK;break;}default:status = RES_PARERR;}return status;/* USER CODE END WRITE */
}
#endif /* _USE_WRITE == 1 *//*** @brief I/O control operation* @param pdrv: Physical drive number (0..)* @param cmd: Control code* @param *buff: Buffer to send/receive control data* @retval DRESULT: Operation result*/
#if _USE_IOCTL == 1
DRESULT USER_ioctl (BYTE pdrv, /* Physical drive nmuber (0..) */BYTE cmd, /* Control code */void *buff /* Buffer to send/receive control data */
)
{/* USER CODE BEGIN IOCTL */DRESULT status = RES_PARERR;switch (pdrv) {case SD_CARD: /* SD卡 */ {status = SD_Driver.disk_ioctl(pdrv,cmd,buff);break;}case SPI_FLASH:{switch (cmd) {/* 扇区数量: */case GET_SECTOR_COUNT:*(DWORD * )buff = W25Q64FV_SUBSECTOR_NUM - SPI_FLASH_OFFSET; break;/* 扇区大小 */case GET_SECTOR_SIZE :*(WORD * )buff = 4096;break;/* 同时擦除扇区个数 */case GET_BLOCK_SIZE :*(DWORD *)buff = 1;break; }status = RES_OK;break;}default:status = RES_PARERR;}return status;/* USER CODE END IOCTL */
}
#endif /* _USE_IOCTL == 1 */
如果设置了RTC,还可以将RTC的获取时间函数和FATFS的相关函数绑定
fatfs.c:
/*** @brief Gets Time from RTC* @param None* @retval Time in DWORD*/
DWORD get_fattime(void)
{/* USER CODE BEGIN get_fattime */return 0;/* USER CODE END get_fattime */
}

注意进行驱动绑定后,如果你的user_diskio.c中实现了不止一种设备的驱动或者设备的逻辑驱动器路径不为0:/,在注册设备时需要将Disk_drvTypeDef数据类型变量disk的lun参数进行修改,原因是绑定设备时我们会通过disk这个全局变量将FATFS_LinkDriver(const Diskio_drvTypeDef *drv, char *path)函数将我们FATFS的驱动层user_diskio.c或者sd_diskio.c这类.c文件中的底层设备驱动函数和FATFS的中间层diskio.c文件中的中间设备驱动函数进行绑定,而在使用f_open()、f_close()这些FATFS操作函数时我们都是先调用diskio.c中的下述几个函数:
DSTATUS disk_initialize (BYTE pdrv);
DSTATUS disk_status (BYTE pdrv);
DRESULT disk_read (BYTE pdrv, BYTE* buff, DWORD sector, UINT count);
DRESULT disk_write (BYTE pdrv, const BYTE* buff, DWORD sector, UINT count);
DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void* buff);
然后通过上述几个中间设备驱动函数调用全局变量disk从而间接调用我们自己实现的底层设备驱动,我们以disk_status和disk_initialize这两个中间设备驱动函数为例,我们可以看出来其调用的是disk全局变量中关于设备驱动函数数组drv[]中的驱动,向底层驱动函数中传入的参数就是lun[]数组中的值,根据ff.c中关于f_open()、f_close()这些FATFS操作函数的实现我们可以知道,当设备的逻辑驱动路径为0-9时,中间设备驱动函数传入的参数BYTE pdrv就是对路径0:/-9:/解析后的数字0-9,而FATFS设备驱动绑定时就是一个设备对应全局变量disk中一个drv[]数组和一个lun[]数组中的元素值,由于每次调用FATFS_LinkDriver(const Diskio_drvTypeDef *drv, char *path)设备驱动绑定函数时都默认lun参数为0,因此每一个设备对应disklun[]数组元素的值都为0,这样向底层驱动函数中传入的参数就是0。
diskio.c:
/*** @brief Gets Disk Status* @param pdrv: Physical drive number (0..)* @retval DSTATUS: Operation status*/
DSTATUS disk_status (BYTE pdrv /* Physical drive number to identify the drive */
)
{DSTATUS stat;stat = disk.drv[pdrv]->disk_status(disk.lun[pdrv]);return stat;
}/*** @brief Initializes a Drive* @param pdrv: Physical drive number (0..)* @retval DSTATUS: Operation status*/
DSTATUS disk_initialize (BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{DSTATUS stat = RES_OK;if(disk.is_initialized[pdrv] == 0){stat = disk.drv[pdrv]->disk_initialize(disk.lun[pdrv]);if(stat == RES_OK){disk.is_initialized[pdrv] = 1;}}return stat;
}ff_gen_drv.h:
/*** @brief Disk IO Driver structure definition*/
typedef struct
{DSTATUS (*disk_initialize) (BYTE); /*!< Initialize Disk Drive */DSTATUS (*disk_status) (BYTE); /*!< Get Disk Status */DRESULT (*disk_read) (BYTE, BYTE*, DWORD, UINT); /*!< Read Sector(s) */
#if _USE_WRITE == 1DRESULT (*disk_write) (BYTE, const BYTE*, DWORD, UINT); /*!< Write Sector(s) when _USE_WRITE = 0 */
#endif /* _USE_WRITE == 1 */
#if _USE_IOCTL == 1DRESULT (*disk_ioctl) (BYTE, BYTE, void*); /*!< I/O control operation when _USE_IOCTL = 1 */
#endif /* _USE_IOCTL == 1 */}Diskio_drvTypeDef;/*** @brief Global Disk IO Drivers structure definition*/
typedef struct
{uint8_t is_initialized[_VOLUMES];const Diskio_drvTypeDef *drv[_VOLUMES];uint8_t lun[_VOLUMES];volatile uint8_t nbr;}Disk_drvTypeDef;ff_gen_drv.c:
Disk_drvTypeDef disk = {{0},{0},{0},0};/*** @brief Links a compatible diskio driver/lun id and increments the number of active* linked drivers.* @note The number of linked drivers (volumes) is up to 10 due to FatFs limits.* @param drv: pointer to the disk IO Driver structure* @param path: pointer to the logical drive path* @param lun : only used for USB Key Disk to add multi-lun managementelse the parameter must be equal to 0* @retval Returns 0 in case of success, otherwise 1.*/
uint8_t FATFS_LinkDriverEx(const Diskio_drvTypeDef *drv, char *path, uint8_t lun)
{uint8_t ret = 1;uint8_t DiskNum = 0;if(disk.nbr < _VOLUMES){disk.is_initialized[disk.nbr] = 0;disk.drv[disk.nbr] = drv;disk.lun[disk.nbr] = lun;DiskNum = disk.nbr++;path[0] = DiskNum + '0';path[1] = ':';path[2] = '/';path[3] = 0;ret = 0;}return ret;
}/*** @brief Links a compatible diskio driver and increments the number of active* linked drivers.* @note The number of linked drivers (volumes) is up to 10 due to FatFs limits* @param drv: pointer to the disk IO Driver structure* @param path: pointer to the logical drive path* @retval Returns 0 in case of success, otherwise 1.*/
uint8_t FATFS_LinkDriver(const Diskio_drvTypeDef *drv, char *path)
{return FATFS_LinkDriverEx(drv, path, 0);
}
这样就有一个问题,由于我们自己编写底层驱动函数时如果涉及到多个不同存储设备的使用,那么我们往往会在user_diskio.c中通过传入的参数pdrv结合switch判断来切换不同的存储设备底层驱动,这里传入的参数pdrv为该设备对应的lun参数,但我们看到FATFS_LinkDriver(const Diskio_drvTypeDef *drv, char *path)设备驱动绑定函数时都默认lun参数为0,因此当用switch对存储设备底层驱动进行切换时,如果case的值不为0就不会执行其中的代码。因此我们对存储设备进行FATFS初始化时建议不使用FATFS_LinkDriver函数,而是使用uint8_t FATFS_LinkDriverEx(const Diskio_drvTypeDef *drv, char *path, uint8_t lun)函数,这里我们可以自定义存储设备的lun参数值,和user_diskio.c中的switch-case判断值对应即可。例如user_diskio.c中SD卡的case判断为0执行,SPI_FLASH卡中的case判断为1执行,那对这两个设备进行初始化时就和USER_DRIVER绑定,然后lun参数分别设置为0和1,如下:
/* USER CODE END Header */
#include "fatfs.h"uint8_t retSD; /* Return value for SD */
char SDPath[4]; /* SD logical drive path */
FATFS SDFatFS; /* File system object for SD logical drive */
FIL SDFile; /* File object for SD */
uint8_t retUSER; /* Return value for USER */
char USERPath[4]; /* USER logical drive path */
FATFS USERFatFS; /* File system object for USER logical drive */
FIL USERFile; /* File object for USER *//* USER CODE BEGIN Variables *//* USER CODE END Variables */void MX_FATFS_Init(void)
{/*## FatFS: Link the SD driver ###########################*///retSD = FATFS_LinkDriver(&SD_Driver, SDPath);/*## FatFS: Link the USER driver ###########################*///retUSER = FATFS_LinkDriver(&USER_Driver, USERPath);/* USER CODE BEGIN Init *//*## FatFS: Link the USER_2 driver ###########################*/retSD = FATFS_LinkDriverEx(&USER_Driver, SDPath,0);retUSER = FATFS_LinkDriverEx(&USER_Driver, USERPath,1);/* additional user code for init *//* USER CODE END Init */
}
配置并绑定完SD卡和SPI_FLASH存储设备的底层驱动之后,我们发现外部FLASH的USERFatFS结构体中关于卷起始扇区volbase的值为0x3F = 63,第0个扇区为MBR(含分区表和引导代码),第1到第62扇区通常为保留未用区域,可能包含自定义数据或二级引导程序。FAT文件系统的引导扇区(BPB)从第63扇区处开始,至于为什么这样设置,可以说是一种文件系统的标准。

测试文件系统使用SD卡和SPI_FLASH多设备程序和对应结果如下:
这里注意保证分配给使用FATFS任务的堆栈足够大,否则容易产生堆栈溢出进入hardfault,我这里给FATFS_Task任务4K字节堆大小还是会溢出,最后分配了10K字节正常运行,建议实际跑操作系统任务时用StackOverflowHookHook钩子函数调试:
void FATFS_Task(void *argument)
{Mount_FatFs(&SDFatFS, SDPath, 1 ,FM_EXFAT,NULL,FATFS_Init_workBuffer_SIZE); //挂载时会自动调用相关存储设备初始化函数FatFs_GetDiskInfo(SDPath);/*----------------------- 文件系统测试:写测试 -----------------------------*/FatFs_WriteTXTFile(SDPath,"test.txt",SDFile,2025,1,14); /*------------------- 文件系统测试:读测试 ------------------------------------*/FatFs_ReadTXTFile(SDPath,"test.txt",SDFile);
/*------------------- 文件系统测试:删除测试 ------------------------------------*/FatFs_DeleteFile(SDPath,"test.txt",SDFile);// /*****外部FLASH文件系统测试*****/Mount_FatFs(&USERFatFS, USERPath, 1 ,FM_ANY ,NULL,FATFS_Init_workBuffer_SIZE); //挂载时会自动调用相关存储设备初始化函数FatFs_GetDiskInfo(USERPath);/*----------------------- 文件系统测试:写测试 -----------------------------*/FatFs_WriteTXTFile(USERPath,"test3.txt",USERFile,2025,1,14); /*------------------- 文件系统测试:读测试 ------------------------------------*/FatFs_ReadTXTFile(USERPath,"test3.txt",USERFile);
// /*------------------- 文件系统测试:删除测试 ------------------------------------*/FatFs_DeleteFile(USERPath,"test3.txt",USERFile);while (1){// 队列为空时,任务可以进入挂起或等待osDelay(5);}
}void LED_Task(void *argument)
{uint32_t task_cnt=0;printf("***gpioProcess_MainTask is running!!");/* Infinite loop */for(;;){if (task_cnt%5==0) {HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);}task_cnt++;osDelay(30);}
}freertos.c:
void vApplicationStackOverflowHook(xTaskHandle xTask, signed char *pcTaskName)
{printf("Stack overflow in task: %s\n", pcTaskName);/* Run time stack overflow checking is performed ifconfigCHECK_FOR_STACK_OVERFLOW is defined to 1 or 2. This hook function iscalled if a stack overflow is detected. */
}


相关文章:
Cubemx文件系统挂载多设备
cubumx版本:6.13.0 芯片:STM32F407VET6 在上一篇文章中介绍了Cubemx的FATFS和SD卡的配置,由于SD卡使用的是SDIO通讯,因此具体驱动不需要自己实现,Cubemx中就可以直接配置然后生成SDIO的驱动,并将SD卡驱动和…...
Java知识速记 == 与equals
Java知识速记 与equals 1. 操作符概述 操作符用于比较基本数据类型的值,或者比较引用类型的对象是否指向同一内存地址。对于基本数据类型,例如int、float等,会比较其值;但对于对象,只会比较两个对象的引用ÿ…...
[Linux]从零开始的STM32MP157 U-Boot移植
一、前言 在上一次教程中,我们了解了STM32MP157的启动流程与安全启动机制。我们还将FSBL的相关代码移植成功了。大家还记得FSBL的下一个步骤是什么吗?没错,就是SSBL,而且常见的我们将SSBL作为存放U-Boot的地方。所以本次教程&…...
fatal: unable to access ‘https://github
fatal: unable to access ‘https://github.com/protocolbuffers/protobuf.git/’: Failed to connect to github.com port 443: Connection timed out 下载项目的时候出现了这个问题,本以为是网络或者什么的问题,没想到是sudo,sudo sudo git clone -b …...
【apt源】RK3588 平台ubuntu20.04更换apt源
RK3588芯片使用的是aarch64架构,因此在Ubuntu 20.04上更换apt源时需要使用针对aarch64架构的源地址。以下是针对RK3588芯片在Ubuntu 20.04上更换apt源到清华源的正确步骤: 步骤一:打开终端 在Ubuntu 20.04中,按下Ctrl Alt T打…...
前端 | 深入理解Promise
1. 引言 JavaScript 是一种单线程语言,这意味着它一次仅能执行一个任务。为了处理异步操作,JavaScript 提供了回调函数,但是随着项目处理并发任务的增加,回调地狱 (Callback Hell) 使异步代码很难维护。为此,ES6带来了…...
【数据结构】_链表经典算法OJ:合并两个有序数组
目录 1. 题目描述及链接 2. 解题思路 3. 程序 3.1 第一版 3.2 第二版 1. 题目描述及链接 题目链接:21. 合并两个有序链表 - 力扣(LeetCode) 题目描述: 将两个升序链表合并为一个新的 升序 链表并返回。 新链表是通过拼接给…...
C++ 字母大小写转换两种方法统计数字字符的个数
目录 题目: 代码1: 代码2: 题目: 大家都知道一些办公软件有自动将字母转换为大写的功能。输入一个长度不超过 100 100 且不包括空格的字符串。要求将该字符串中的所有小写字母变成大写字母并输出。 输入格式 输入一行&#x…...
制造企业的成本核算
一、生产成本与制造费用的区别 (1)生产成本,是直接用于产品生产,构成产品实体的材料成本。 包括企业在生产经营过程中实际消耗的原材料、辅助材料、备品备件、外购半成品、燃料、动力包装物以及其它直接材料,和直接参加产品生产的工人工资,以及按生产工人的工资总额和规…...
快速提升网站收录:利用网站FAQ页面
本文转自:百万收录网 原文链接:https://www.baiwanshoulu.com/48.html 利用网站FAQ(FrequentlyAskedQuestions,常见问题解答)页面是快速提升网站收录的有效策略之一。以下是一些具体的方法和建议,以帮助你…...
【LeetCode 刷题】回溯算法-组合问题
此博客为《代码随想录》二叉树章节的学习笔记,主要内容为回溯算法组合问题相关的题目解析。 文章目录 77. 组合216.组合总和III17.电话号码的字母组合39. 组合总和40. 组合总和 II 77. 组合 题目链接 class Solution:def combinationSum3(self, k: int, n: int) …...
Spring的AOP的JoinPoint和ProceedingJoinPoint
Spring的AOP的JoinPoint 在Spring AOP中,JoinPoint 是一个核心接口,用于表示程序执行过程中的一个连接点(如方法调用或异常抛出)。它提供了访问当前被拦截方法的关键信息的能力。以下是关于 JoinPoint 的详细说明: 一…...
终极版已激活!绿话纯净,打开即用!!!
今天我想和大家聊聊一个非常实用的工具——视频转换大师最终版。 视频转换大师终极版,堪称一款全能型的视频制作神器,集视频转换与编辑功能于一体。它搭载的视频增强器技术,能够最大限度地保留原始视频质量,甚至还能实现质量的进…...
【2025年最新版】Java JDK安装、环境配置教程 (图文非常详细)
文章目录 【2025年最新版】Java JDK安装、环境配置教程 (图文非常详细)1. JDK介绍2. 下载 JDK3. 安装 JDK4. 配置环境变量5. 验证安装6. 创建并测试简单的 Java 程序6.1 创建 Java 程序:6.2 编译和运行程序:6.3 在显示或更改文件的…...
C++ strcpy和strcat讲解
目录 一. strcpy 代码演示: 二.strcat 代码演示: 一. strcpy 使⽤字符数组可以存放字符串,但是字符数组能否直接赋值呢? ⽐如: char arr1[] "abcdef"; char arr2[20] {0}; arr2 arr1;//这样这节赋值可…...
STM32 01 LED
一、点亮一个LED 在STC-ISP中单片机型号选择 STC89C52RC/LE52RC;如果没有找到hex文件(在objects文件夹下),在keil中options for target-output- 勾选 create hex file。 如果要修改编程 :重新编译-下载/编程-单片机重…...
[LeetCode]day10 707.设计链表
707. 设计链表 - 力扣(LeetCode) 题目描述 你可以选择使用单链表或者双链表,设计并实现自己的链表。 单链表中的节点应该具备两个属性:val 和 next 。val 是当前节点的值,next 是指向下一个节点的指针/引用。 如果…...
【图床配置】PicGO+Gitee方案
【图床配置】PicGOGitee方案 文章目录 【图床配置】PicGOGitee方案为啥要用图床图床是什么配置步骤下载安装PicGoPicGo配置创建Gitee仓库Typora中的设置 为啥要用图床 在Markdown中,图片默认是以路径的形式存在的,类似这样 可以看到这是本地路径&#x…...
AI软件外包需要注意什么 外包开发AI软件的关键因素是什么 如何选择AI外包开发语言
1. 定义目标与需求 首先,要明确你希望AI智能体做什么。是自动化任务、数据分析、自然语言处理,还是其他功能?明确目标可以帮助你选择合适的技术和方法。 2. 选择开发平台与工具 开发AI智能体的软件时,你需要选择适合的编程语言、…...
ArkTS语言介绍
文章目录 一、基本知识声明类型运算符语句函数函数声明可选参数Rest参数返回类型函数的作用域函数调用函数类型箭头函数(又名Lambda函数)闭包函数重载类字段方法构造函数可见性修饰符对象字面量抽象类接口接口属性接口继承抽象类和接口泛型类型和函数泛型类和接口泛型约束泛型…...
基于 oneM2M 标准的空气质量监测系统的互操作性
论文标题 英文标题: Interoperability of Air Quality Monitoring Systems through the oneM2M Standard 中文标题: 基于 oneM2M 标准的空气质量监测系统的互操作性 作者信息 Jonnar Danielle Diosana, Gabriel Angelo Limlingan, Danielle Bryan Sor…...
lstm部分代码解释1.0
这段代码是使用 Python 中的 Pandas 和 NumPy 库对数据进行读取和处理的操作。以下是对每一行代码的详细解释: 第一行代码 Python复制 df pd.read_csv("output.csv") 功能:使用 Pandas 的 read_csv 函数读取一个名为 output.csv 的文件&am…...
Flutter常用Widget小部件
小部件Widget是一个类,按照继承方式,分为无状态的StatelessWidget和有状态的StatefulWidget。 这里先创建一个简单的无状态的Text小部件。 Text文本Widget 文件:lib/app/app.dart。 import package:flutter/material.dart;class App exte…...
电路研究9.2.6——合宙Air780EP中HTTP——HTTP GET 相关命令使用方法研究
这个也是一种协议类型: 14.16 使用方法举例 根据之前多种类似的协议的相关信息: HTTP/HTTPS:超文本传输协议(HTTP)用于Web数据的传输,而HTTPS是HTTP的安全版本,使用SSL/TLS进行加密。与FTP相比&…...
【力扣】283.移动零
AC截图 题目 思路 遍历nums数组,将0删除并计数,最后在nums数组尾部添加足量的零 有一个问题是,vector数组一旦erase某个元素,会导致迭代器失效。好在有解决办法,erase会返回下一个有效元素的新迭代器。 代码 class …...
合并2个排序的链表
合并2个排序的链表 递归解法和迭代解法 /*** 节点实体类*/ class ListNode {public int val;public String name;public ListNode next;public ListNode(int val) {this.val val;} }/*** 链表节点类*/ class Node {// next存的是下个节点的引用Node next;// 值int val;//为赋…...
白话DeepSeek-R1论文(二)| DeepSeek-R1:AI “升级打怪”,从“自学成才”到“全面发展”!
最近有不少朋友来询问Deepseek的核心技术,今天开始陆续针对DeepSeek-R1论文中的核心内容进行解读,并且用大家都能听懂的方式来解读。这是第二篇趣味解读。 DeepSeek-R1:AI “升级打怪”,从“自学成才”到“全面发展”!…...
linux设置mysql远程连接
首先保证服务器开放了mysql的端口 然后输入 mysql -u root -p 输入密码后即可进入mysql 然后再 use mysql; select user,host from user; update user set host"%" where user"root"; flush privileges; 再执行 select user,host from user; 即可看到变…...
并发模式:驾驭多线程的艺术
并发模式:驾驭多线程的艺术 在并发编程中,不同的任务之间需要协作和通信,才能高效地完成工作。为了更好地组织和管理并发任务,软件工程师们总结出了一些经典的并发模式,例如生产者-消费者模式、发布-订阅模式等。本文将深入探讨这些常见的并发模式,并结合实例进行讲解,…...
Gurobi基础语法之 addConstr, addConstrs, addQConstr, addMQConstr
在新版本的 Gurobi 中,向 addConstr 这个方法中传入一个 TempConstr 对象,在模型中就会根据这个对象生成一个约束。更重要的是:TempConstr 对象可以传给所有addConstr系列方法,所以下面先介绍 TempConstr 对象 TempConstr TempC…...
