一、FATFS核心特性
跨平台支持
支持FAT12/FAT16/FAT32格式,兼容Windows文件系统;
采用标准C语言编写,代码量小且支持RTOS。
配置灵活性
通过宏定义实现功能裁剪,例如:
FF_FS_READONLY:设为1时禁用写操作相关函数(如f_write()、f_unlink());
FF_FS_MINIMIZE:设置不同精简级别(0-3),逐步移除非必要API(如目录操作、文件统计等)。
二、移植关键步骤
存储介质初始化
需初始化底层存储设备(如SD卡、SPI Flash),涉及存储地址映射与读写函数实现;
在user_diskio.c中实现底层接口:disk_initialize(初始化)、disk_read(读)、disk_write(写)。
文件系统挂载
调用f_mount()函数挂载存储设备(如f_mount(fs, "0:", 1)),分配逻辑驱动器号(如"0:")。
三、应用层操作
文件读写流程
创建文件:f_open(&file, "0:/file.txt", FA_CREATE_ALWAYS | FA_WRITE);
数据写入:使用f_write()时需将非字符类型数据(如int/float)转换为字符格式;
资源释放:操作完成后需调用f_close()关闭文件。
辅助功能配置
支持长文件名需启用FF_USE_LFN并选择编码方式(如UTF-8);
多卷管理需配置FF_VOLUMES参数。
四、注意事项
存储介质格式化:首次使用前需通过工具或代码格式化,以创建文件分配表和目录结构;
内存管理:合理分配缓冲区以避免内存溢出,尤其在动态内存模式下;
实时性优化:在RTOS中需确保文件系统操作与任务调度兼容。
开始移植
我们先使用STM32CubeMX创建一个纯净的工程
配置好SPI1
然后在初始化一个片选引脚(PC13)
之后在中间件里设置好FATFS(选择User-defind就好,其他的可以不用改)
最后选择MDK-ARM,生成工程
底层驱动兼容SFUD
一、通用驱动兼容性
广泛硬件支持:兼容市面主流SPI Flash芯片(如W25Q64、SST25VF等),支持SPI/QSPI接口的基础读写擦除操作,无需针对不同芯片重复开发驱动。
接口标准化:通过统一API(如sfud_read、sfud_erase)屏蔽底层硬件差异,降低代码与硬件的耦合度。
二、参数自动检测
动态识别能力:运行时自动读取Flash的厂商ID、容量、擦除粒度等参数,减少因芯片停产或型号变更导致的维护风险。
SFDP协议支持:基于JEDEC标准的SFDP(Serial Flash Discoverable Parameters)规范,自动解析设备特性表,避免手动配置。
三、轻量级设计
低资源占用:标准模式代码量约5.5KB ROM/0.2KB RAM,最小模式可压缩至3.6KB ROM/0.1KB RAM,适用于STM32等资源受限的MCU。
可裁剪性:通过宏定义关闭非必要功能(如QSPI支持),进一步优化存储和运行效率。
四、扩展性与易用性
多设备管理:支持通过注册机制同时驱动多个Flash设备,适用于多存储介质的复杂场景(如主控+备份存储)。
分层架构设计:与FAL(Flash抽象层)组件无缝集成,提供统一的Flash访问接口,简化文件系统(如FATFS)或OTA升级功能的实现。
五、开发效率提升
快速移植:仅需实现底层SPI接口函数(如spi_send、spi_recv),缩短从零开发驱动的时间。
动态适配优势:新增Flash型号时无需修改驱动代码,仅需更新设备参数表或依赖SFDP自动识别。
场景 | SFUD作用 |
外部存储扩展(如W25Q32) | 提供统一接口,简化文件系统(FATFS)或日志存储的实现流程 |
多Flash设备管理 | 通过注册机制区分不同存储介质,支持并行操作 |
特性 | 嵌入式开发价值 |
兼容性 | 覆盖90%以上主流SPI Flash型号,降低硬件选型限制 |
低资源消耗 | 适配低端MCU(如STM32F103),节省ROM/RAM资源 |
维护成本低 | 动态参数检测减少代码修改频率,提升长期项目可持续性 |
移植SFUD
SFUD会检测单片机上的Flash芯片,所以我们这里选择SFUD驱动做一个兼容,就算更换芯片后也不需要修改底层代码。
SFUD: https://github.com/armink/SFUD.git - Gitee.com
把代码克隆到本地,然后把sfud文件夹复制出来,放到工程目录下,里面共有3个目录(inc,port,src)
将port和src内的C文件加到Keil工程内
然后添加inc路径
我们只需要实现底层的函数接口
进入到sfud_port.c文件内,实现spi_write_read函数
#include <sfud.h>
#include <stdarg.h>
#include "main.h"
static char log_buf[256];
extern SPI_HandleTypeDef hspi1;
#define SPI_Start() (HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET))
#define SPI_Stop() (HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET))
void sfud_log_debug(const char* file, const long line, const char* format, ...);
/**
* SPI write data then read data
*/
static sfud_err spi_write_read(const sfud_spi* spi, const uint8_t* write_buf, size_t write_size, uint8_t* read_buf, size_t read_size) {
sfud_err result = SFUD_SUCCESS;
uint8_t send_data, read_data;
/**
* add your spi write and read code
*/
SPI_Start();
if (write_size > 0) {
if (HAL_OK != HAL_SPI_Transmit(&hspi1, (uint8_t*)write_buf, write_size, 1000))
result = SFUD_ERR_TIMEOUT;
}
if (read_size > 0) {
if (HAL_OK != HAL_SPI_Receive(&hspi1, (uint8_t*)read_buf, read_size, 1000))
result = SFUD_ERR_TIMEOUT;
}
SPI_Stop();
return result;
}
实现sfud_spi_port_init函数,注释后面提示Required的都是需要配置的
关于delay的配置,就随便用软件的方式延时一下就好了
void delay(void) {
uint16_t count = 10000;
while (count--) {
__NOP();
}
}
sfud_err sfud_spi_port_init(sfud_flash* flash) {
sfud_err result = SFUD_SUCCESS;
/**
* add your port spi bus and device object initialize code like this:
* 1. rcc initialize
* 2. gpio initialize
* 3. spi device initialize
* 4. flash->spi and flash->retry item initialize
* flash->spi.wr = spi_write_read; //Required
* flash->spi.qspi_read = qspi_read; //Required when QSPI mode enable
* flash->spi.lock = spi_lock;
* flash->spi.unlock = spi_unlock;
* flash->spi.user_data = &spix;
* flash->retry.delay = null;
* flash->retry.times = 10000; //Required
*/
flash->spi.wr = spi_write_read;
flash->retry.delay = delay;
flash->retry.times = 10000;
return result;
}
测试SFUD和芯片是否移植成功
在main.c内引入 sfud.h文件
int fputc(int ch, FILE* f) {
HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, 1000);
return ch;
}
// sfud测试
void es(void) {
printf("擦除数据\r\n");
const sfud_flash* flash = sfud_get_device(0);
sfud_erase(flash, 0, 100);
}
void ws(void) {
//获取到设备Flash
const sfud_flash* flash = sfud_get_device(0);
uint8_t buff[300] = {0};
for (uint16_t i = 0; i < 300; i++) {
buff[i] = i;
}
//先擦除在写入
sfud_erase_write(flash, 0, 300, buff);
}
void rs(void) {
const sfud_flash* flash = sfud_get_device(0);
uint8_t buff[300] = {0};
sfud_read(flash, 0, 300, buff);
for (uint16_t i = 0; i < 300; i++) {
printf("%d ", buff[i]);
if (i % 10 == 0) {
printf("\r\n");
}
}
}
// sfud测试
int main(void) {
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_SPI1_Init();
//MX_FATFS_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
sfud_init();
rs();
HAL_Delay(1000);
ws();
HAL_Delay(1000);
rs();
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1) {
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
先读取数据在写入数据,再读出来,看看数据是否一致,读取数据就是把1变0,擦除就是把0变1,所以默认都是1,所以读出来都是255
配置FATFS
打开user_diskio.c 文件,修改USER_initialize和USER_status函数
/**
* @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 */
Stat &= ~STA_NOINIT;
return Stat;
/* 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 */
Stat &= ~STA_NOINIT;
return Stat;
/* USER CODE END STATUS */
}
实现USER_read函数
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 */
const sfud_flash* flash = sfud_get_device(0);
uint32_t read_addr = sector * 4096;
sfud_read(flash, read_addr, count * 4096, buff);
return RES_OK;
/* USER CODE END READ */
}
实现USER_write函数
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 */
const sfud_flash* flash = sfud_get_device(0);
uint32_t write_addr = sector * 4096;
sfud_erase_write(flash, write_addr, count * 4096, buff);
/* USER CODE HERE */
return RES_OK;
/* USER CODE END WRITE */
}
实现USER_ioctl函数
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 res = RES_OK;
switch (cmd) {
case CTRL_SYNC:
break;
case GET_SECTOR_COUNT:
*((DWORD*)buff) = 1024;
break;
case GET_SECTOR_SIZE:
*((WORD*)buff) = 4096;
break;
case GET_BLOCK_SIZE:
*((DWORD*)buff) = 1;
break;
default:
res = RES_PARERR;
}
return res;
/* USER CODE END IOCTL */
}
打开ffconf.h文件,修改__MAX_SS为4096
进去到fatfs.c文件,USERPath是文件的起始路径,这里使用 \,前面在转义一下
如果我们这里设置起始路径的话,在ff_gen_drv.c文件内需要注释掉path代码,如果不设置的话会给我们一个默认的起始路径,这里就不需要注释掉了
回到main.c文件内,取消注释FATFS初始化函数
测试FATFS文件系统
// fatfs测试
void mkfs(void) {
uint8_t res = f_mkfs(USERPath, 1, _MAX_SS);
printf("mkfs res: %d\r\n", res);
}
FATFS fs;
void mnt(void) {
uint8_t res = f_mount(&fs, USERPath, 1);
printf("mnt res: %d\r\n", res);
}
FIL file;
void create_file(char* file_name, char* content) {
int res = 0;
int bwritten = 0;
printf("Create file :%s\r\n", file_name);
// 创建一个a.txt文件
res = f_open(&file, file_name, FA_WRITE | FA_CREATE_ALWAYS);
printf("Create file res:%d\r\n", res);
// 写入数据到文件中
res = f_write(&file, content, strlen(content), (UINT*)&bwritten);
printf("write file res:%d\r\n", res);
// 关闭文件
f_close(&file);
}
void list_files(char* path) {
printf("file list:\r\n");
FILINFO fno;
DIR dir;
FRESULT res;
res = f_opendir(&dir, path);
if (res == FR_OK) {
while (1) {
res = f_readdir(&dir, &fno);
if (res != FR_OK || fno.fname[0] == 0)
break;
printf("%s\r\n", fno.fname);
}
f_closedir(&dir);
}
}
void read_file(char* file_name, char* content) {
int res = 0;
int bread = 0;
printf("Show File Content:%s\r\n", file_name);
res = f_open(&file, file_name, FA_READ);
printf("Open file res : %d\r\n", res);
res = f_read(&file, content, 100, (UINT*)&bread);
printf("Read file res : %d\r\n", res);
f_close(&file);
}
// fatfs测试
int main(void) {
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_SPI1_Init();
MX_FATFS_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
sfud_init();
mnt();
mkfs();
char content_a[100] = "hello world\r\nhello fatfs\r\nhello violet\r\n";
create_file("a.txt", content_a);
char content_b[100] = "37193719731831300\r\n";
create_file("b.txt", content_b);
char content_c[100] = "dadajdlajldjajajflajf\r\n";
create_file("c.txt", content_c);
list_files(USERPath);
char read_content[100] = {0};
read_file("a.txt", read_content);
printf("%s\r\n", read_content);
memset(read_content, 0, sizeof(read_content));
read_file("b.txt", read_content);
printf("%s\r\n", read_content);
memset(read_content, 0, sizeof(read_content));
read_file("c.txt", read_content);
printf("%s\r\n", read_content);
// rs();
// HAL_Delay(1000);
// ws();
// HAL_Delay(1000);
// rs();
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1) {
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
结果
测试成功,完成了对FATFS的移植了,喜欢的话点个关注吧。