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

news2025/5/24 14:18:41

一、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的移植了,喜欢的话点个关注吧。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2336702.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

STM32 模块化开发实战指南:系列介绍

本文是《STM32 模块化开发实战指南》系列的导读篇,旨在介绍整个系列的写作目的、适用读者、技术路径和每一篇的主题规划。适合从事 STM32、裸机或 RTOS 嵌入式开发的个人开发者、初创工程师或企业项目团队。 为什么要写这个系列? 在嵌入式开发中,很多人刚开始都是从点亮一个…

信刻电子档案蓝光光盘刻录安全检测长期归档

信刻一直致力于为档案馆、各行业档案部门&#xff0c;提供跨网数据交换、电子档案数据磁光异质备份归档解决方案。所研制的电子档案光盘智能长期归档系统&#xff0c;满足国产环境下”刻、管、存、检、用”全生命周期管理应用需求&#xff0c;能够提供一份离线归档、一份近线存…

vue3中,element-plus中el-input的v-model和value的用法示例

el-input的v-model&#xff0c;邦定响应式变量 <el-col :span"6"><el-form-item label"检验类别" prop"verifyType"><el-input v-model"applyAllInfo.applyBasicInfo.verifyTypeName" readonly /></el-form-item…

L2-052 吉利矩阵分

L2-052 吉利矩阵 - 团体程序设计天梯赛-练习集 所有元素为非负整数&#xff0c;且各行各列的元素和都等于 7 的 33 方阵称为“吉利矩阵”&#xff0c;因为这样的矩阵一共有 666 种。 本题就请你统计一下&#xff0c;把 7 换成任何一个 [2,9] 区间内的正整数 L&#xff0c;把矩…

Linux:Makefile

编译器gcc 使用方式&#xff1a;gcc [ 选项 ] 要编译的⽂件 [ 选项 ] [ ⽬标⽂件 ] 编译分为以下几个步骤&#xff1a; 1.预处理(进⾏宏替换) 预处理功能主要包括宏定义,⽂件包含,条件编译,去注释等。 预处理指令是以#号开头的代码⾏。 实例: gcc –E hello.c –o hello…

基于双闭环PID控制器的永磁同步电机控制系统匝间故障Simulink仿真

欢迎微♥关注“电击小子程高兴的MATLAB小屋”获取巨额优惠 1.模型简介 本仿真模型基于MATLAB/Simulink&#xff08;版本MATLAB 2013Rb&#xff09;软件。建议采用matlab2013 Rb及以上版本打开。&#xff08;若需要其他版本可联系代为转换&#xff0c;高于该版本的matlab均可正…

硬件电路设计之51单片机(2)

声明&#xff1a;绘制原理图和PCB的软件为嘉立创EDA。根据B站尚硅谷嵌入式之原理图&PCB设计教程学习所作个人用笔记。 目录 一、原理图详解 1、TypeC接口 &#xff08;1&#xff09;TypeC接口介绍 &#xff08;2&#xff09;TypeC原理图 2、5V转3.3V 3、单片机电源开…

SpringAI+DeepSeek大模型应用开发——1 AI概述

AI领域常用词汇 LLM&#xff08;LargeLanguage Model&#xff0c;大语言模型&#xff09; 能理解和生成自然语言的巨型AI模型&#xff0c;通过海量文本训练。例子&#xff1a;GPT-4、Claude、DeepSeek、文心一言、通义干问。 G&#xff08;Generative&#xff09;生成式: 根据上…

经济指标学习(一)

系列文章目录 文章目录 系列文章目录1、市净率**一、定义与计算****二、核心意义****三、应用场景****四、局限性****五、分类与衍生指标****总结** 2、市销率**一、定义与计算****二、核心意义****三、优缺点分析****四、适用场景****五、与其他指标的对比****六、实际应用案例…

理解 results = model(source, stream=True) 的工作原理和优势

1. 核心概念解析 (1) streamTrue 的作用 生成器模式&#xff1a;当处理视频或图像序列时&#xff0c;streamTrue 会将结果包装成一个 生成器&#xff08;Generator&#xff09;&#xff0c;逐帧生成 Results 对象&#xff0c;而不是一次性返回所有结果。内存优化&#xff1a;…

国内互联网大厂推出的分布式数据库 的详细对比,涵盖架构、性能、适用场景、核心技术等维度

以下是 国内互联网大厂推出的分布式数据库 的详细对比&#xff0c;涵盖架构、性能、适用场景、核心技术等维度&#xff1a; 一、主流分布式数据库列表 大厂数据库名称类型适用场景发布时间腾讯云TDSQL分布式HTAP金融、电商、游戏、政企2010年阿里云OceanBase分布式HTAP银行核…

Android 项目配置文件解释

Android 项目配置文件解释 目录 Android 项目配置文件解释1. `plugins` 块2. `android` 块3. `dependencies` 块为什么需要 JDK 和 Kotlin1. plugins 块 plugins {id com.android.applicationid org.jetbrains.kotlin.android }id com.android.application:应用 Android 应用…

亚马逊热销变维权?5步搭建跨境产品的安全防火墙

“产品热卖&#xff0c;引来维权”——这已经悄然成为越来越多跨境卖家的“热销烦恼”。曾经拼品拼量&#xff0c;如今却要步步谨慎。商标侵权、专利投诉、图片盗用……这些问题一旦发生&#xff0c;轻则下架、账号被限&#xff0c;重则冻结资金甚至封店。 别让“热销”变“受…

C语言——分支语句

在现实生活中&#xff0c;我们经常会遇到作出选择和判断的时候&#xff0c;在C语言中也同样要面临作出选择和判断的时候&#xff0c;所以今天&#xff0c;就让我们一起来了解一下&#xff0c;C语言是如何作出选择判断的。 目录 1.何为语句&#xff1f; 2.if语句 2.1 if语句的…

绿盟二面面试题

5000篇网安资料库https://mp.weixin.qq.com/s?__bizMzkwNjY1Mzc0Nw&mid2247486065&idx2&snb30ade8200e842743339d428f414475e&chksmc0e4732df793fa3bf39a6eab17cc0ed0fca5f0e4c979ce64bd112762def9ee7cf0112a7e76af&scene21#wechat_redirect 1. 原理深度&…

deepseek生成流程图

目录 Mermaid流程图需求询问框架交互显示流程图markdown在线网站 可能会出现的问题语法报错 在职场中&#xff0c;借助AI生成图表是提升效率的重要技能&#xff0c;本篇我们讲解如何使用deepseek生成流程图 Mermaid流程图 需求 学习太差劲了&#xff0c;我想要一个比较好的学…

大塔集团乔迁开新局 企业赋能贯全程

2025年4月15 日&#xff0c;在佛山市佛山大道北175号&#xff0c;大塔集团乔迁开业盛大启幕&#xff0c;业界目光聚焦于此。 点睛仪式 揭牌仪式 彩绸飘扬、嘉宾云集&#xff0c;现场气氛热烈非凡&#xff0c;这一标志性时刻&#xff0c;宣告着大塔集团正式踏上全新发展征程。 …

Spark-SQL核心编程(二)(三)

Spark-SQL核心编程&#xff08;二&#xff09; DSL 语法 DataFrame 提供一个特定领域语言(domain-specific language, DSL)去管理结构化的数据。 可以在 Scala, Java, Python 和 R 中使用 DSL&#xff0c;使用 DSL 语法风格不必去创建临时视图了。 1.创建一个 DataFrame val d…

继承:(开始C++的进阶)

我们今天来学习C的进阶&#xff1a; 面向对象三大特性&#xff1a;封装&#xff0c;继承&#xff0c;多态。 封装我们在前面已经学了&#xff0c;我们细细理解&#xff0c;我们的类的封装&#xff0c;迭代器的封装&#xff08;vector的迭代器可以是他的原生指针&#xff0c;li…

oracle数据库单个表空间达到32G后程序报错表空间不足问题排查、处理

oracle数据库单个表空间达到32G后程序报错表空间不足问题排查、处理 系统宕机tomcat日志报错表空间无法增长&#xff0c;排查发现oralce表空间文件到了32G。 通过AI查了下&#xff0c;“oracle是否支持表空间达到32G后&#xff0c;自动创建新的表空间文件” 答复是oralce不支…