当前位置: 首页 > news >正文

STM32F103ZET6移植FATFS文件系统教程(W25Q32)

一、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) {//获取到设备Flashconst 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的移植了,喜欢的话点个关注吧。

相关文章:

  • 文件操作(二进制文件)
  • Vue el-from的el-form-item v-for循环表单如何校验rules(二)
  • 「Java EE开发指南」用MyEclipse开发EJB 3无状态会话Bean(二)
  • 磁导率;电感为什么存在饱和电流?气隙的定义,磁芯开气隙有哪些作用
  • Redis的IO多路复用
  • Flutter的自动化测试 python flutter编程
  • 从IF到SWITCH:解锁Power BI条件判断的应用场景
  • 第五阶段:项目实践与后续学习指引
  • 【Axure绘制原型】小图标使用技巧
  • Spring boot 知识整理
  • 利用耦合有限元和神经网络计算的骨重塑模拟多尺度方法
  • 【java】记录一个开启事务抛出异常的场景
  • 【sqlserver】修改nvarchar类型为varchar脚本
  • 神经光子渲染:物理级真实感图像生成——从麦克斯韦方程到深度学习
  • C# 西门子通信
  • 敦普水性低温烤漆的进击
  • NO.94十六届蓝桥杯备战|图论基础-单源最短路|常规dijkstra|堆优化dijkstra|bellman-ford|spfa(C++)
  • JavaSE学习(前端初体验)
  • 界面控件DevExpress WPF v25.1新功能预览 - 文档处理类功能升级
  • Linux 软件管理
  • 平远县建设工程交易中心网站/seo岗位工作内容
  • wordpress 悬赏功能/化工seo顾问
  • 内容营销案例分析/哪有培训seo
  • 有关网站开发的文献/开封网络推广哪家好
  • 查重网站开发/朋友圈软文范例
  • 网站权重收录/html家乡网站设计