【CubeMX+STM32】SD卡 文件系统读写 FatFs+SDIO+DMA

news2025/2/9 1:28:38

本篇,将使用CubeMX+Keil,创建一个SD卡的 FatFS+SDIO+DMA 文件系统读写工程。

目录

一、简述

二、CubeMX 配置 FatFS+SDIO + DMA

三、Keil 编辑代码

四、实验效果


实现效果,如下图:


一、简述

上两篇,已循序渐进讲解了SD、SDIO、基础读写、DMA读写。

这里不再啰嗦,有兴趣的可翻看之前的篇章:

1、CubeMX_STM32_SD卡_基础读写

2、CubeMX_STM32_SD卡_DMA读写

本篇,再向前一步,展示CubeMX添加 FatFs 文件系统,对SD卡进行文件方式的读写。

对于FatFs,这里只示范操作、使用,不详述背后细节原理。

SD卡的接线原画图,使用通用的接线:


二、CubeMX 配置 SDIO + DMA + FatFS

新建工程部分,略过。

  • 参考1:【STM32+CubeMX】 新建一个工程(STM32F407)_stm32f407 cubemx-CSDN博客
  • 参考2:【STM32+CubeMX】USART1 DMA收发、printf
  • 参考3:  STM32串口通信 -- bsp_UART.c 文件的移植和函数使用-CSDN博客

1、使能SDIO、DMA

  • Mode :  选择 SD 4 bits Wide bus ;
  • 其它参数 :F4系列不用修改配置,默认即可。F103系列,需把时钟分频系数修改为 6,即SDIOCLK Clock divide factor这一项,默认0,修改为6, 不然会通信失败。
  • DMA Settings :  添加SDIO_RX、SDIO_TX这两项;  本页其它参数默认;

2、开启SDIO全局中断,修改中断优先级

3、FatFs 使能、参数配置

  • Mode:  打勾 SD Card
  • 参数 CODE_PAGE:简体中文
  • 参数 USE_LFN:长文件名称(缓存放在STACK,因此STACK得设置大一些,如>0x1000)
  • 参数 FS_EXFAT:ENABLE  (挂载、格式化时,会自动选择合适的FAT16、FAT32、exFAT)

4、FatFS 使用DMA

5、FatFS 是否使用检测引脚

让我们先回看一下原理图:

SD卡座的第9脚,用于检测是否已插入SD卡,如果已插入SD卡,CD脚会输出低电平。

不建议使用这个功能!因为:SD卡与U盘不同,没有完善的保护电路,不应该进行热插拔!而且,做SD卡项目调试时,默认状态应该是一直插着SD卡的。如果项目有需要,也可以自己写几行引脚电平检测,当引脚电平为低时,再对SD卡进行操作,这样更灵活。

回到FatFs配置界面 ,下方配置里,可以设置是否使用检测引脚:

  1. 如果你的SD卡已连接检测引脚,想使用CubeMX生成管理,可以在这里指定引脚。
  2. 如果不需要这个检测功能,就让它默认空着即可。不管是否已连接此引脚,都可空着。

CubeMX生成时注意:

        当不指定检测引脚,在最后生成工程时,会有弹窗警告,不用管它,到时点击Yes即可,将会正常生成工程,代码上不会有任何影响。

        如果不想有弹窗,可以随便指定一个空闲的引脚,这样在生成时就不会弹警告了,但是需要在生成的工程代码里,注释掉引脚检测功能,这里不详述,可自行搜索方法。操作有点烦人,不建议此方法。

6、时钟设置

进入时钟树配置页面。

如果之前没配置过SDIO、USB,这时就会弹窗:是否自动配置所需时钟?

选择:NO ,手动修改即可。

不推荐 Yes,因为它将针对已使能的SDIO、USB进行必须值的配置,而已设置好的系统时钟,将会被修改成其它值。

F4系列,如果板子用25M晶振,使用下图配置即可;如果是8M晶振,修改晶振、分频两处为8即可。

重点:箭头所指的Q值,它用于控制USB 、SDIO和随机数生成器的时钟,这个时钟配置成 48M !  因此,箭头的Q值设置为 7; 

好了,SD卡文件系统通信所需的 SDIO + FatFS + DMA 已完成配置。

重新生成工程,这时,会有弹窗提示,因为我们没有指定SD卡的检测引脚。

点击 Yes 确认,继续生成即可!


三、Keil 编辑代码

1、打开keil 工程,先重新编译一次。

  • 正常情况,编译是0 Error的。
  • 如果有Error,  应该是新建工程时,路径、名称有中文了,重新开建工程,改为英文即可。

2、重要修改:SD卡的初始化,使用 1-bit 模式

CubeMX生成的SDIO初始化代码,有一个bug,需要手动修改,操作如下: 

  • 右击 main.c 文件中函数 MX_SDIO_SD_Init(), 
  • 在弹出菜单中:Go To Ddfinition Of ...;  将跳转到SD卡初始化函数内部;

跳转到 sdio.c文件的 MX_SDIO_SD_Init() 初始化函数内部后,

把下图位置中的 4B,改为 1B ;

因为初始化时,需要低速率,1线通信即可。如果不修改,初始化过程会导致程序卡死。

重要!CubeMX每次重新生成后,都要手动修改一次

3、常用函数

我们上两篇介绍SD卡的读写时,共用过6个函数,如下表。

这6个函数,在本工程中,还是可用的。但本篇暂时用不上,这里就进行不示范了。

1、获取SD卡信息
HAL_SD_CardInfoTypeDef pCardInfo = {0};          // SD卡信息结构体
HAL_SD_GetCardInfo(&hsd, &pCardInfo);            // 获取 SD 卡的信息

2、读数据
HAL_SD_ReadBlocks(&hsd, aOldData, 7, 2, 3000);   //  SD卡的句柄、数据、块地址、块数量、超时ms

3、写数据
HAL_SD_WriteBlocks(&hsd, aTestData, 7, 2, 3000)  //  SD卡的句柄、数据、块地址、块数量、超时ms

4、读数据_DMA
HAL_SD_ReadBlocks_DMA(&hsd, aOldData, 7, 2);    // 读取SD卡指定块的数据; 参数:SD句柄、数据地址、块起始地址、需要读取的块数量;

5、写数据_DMA
HAL_SD_WriteBlocks_DMA(&hsd, aTestData, 7, 2);  // 向指定块写入数据; 参数:SD句柄、数据地址、块起始地址、需要写入的块数量;

6、擦除数据
HAL_SD_Erase(&hsd, 7, 8)  //  SD卡的句柄、块起始地址、块结束地址

而FatFS常用的几个操作函数,如下表:

函数参数的具体作用,可以通过 Kimi 进行查询。

FRESULT f_res;

1、挂载文件系统
f_res = f_mount(&myFatFs, "0:", 1);                                   // 在SD卡上挂载文件系统; 参数:文件系统对象、驱动器路径、读写模式(0只读、1读写)

2、格式化
static uint8_t aMountBuffer[4096];                                   // 格式化时所需的临时缓存; 块大小512的倍数; 值越大格式化越快, 如果内存不够,可改为512或者1024; 当需要在函数内定义这种大缓存时,要用static修饰,令缓存存放在全局数据区内,不然,可能会导致stack溢出。
f_res = f_mkfs("0:", 0, 0, aMountBuffer, sizeof(aMountBuffer));      // 格式化SD卡; 参数:驱动器路径、文件系统(0自动\1FAT12\2FAT16\)、簇大小(0为自动选择)、格式化临时缓冲区、缓冲区大小

3、打开文件
f_res = f_open(&myFile, "0:Test.txt", FA_CREATE_ALWAYS | FA_WRITE);  // 打开文件; 参数:要操作的文件对象、路径和文件名称、打开模式;

4、关闭文件
f_close(&myFile);                                                    // 不再读写,关闭文件

5、文件写入数据
f_res = f_write(&myFile, aWriteBuf, sizeof(aWriteBuf), &num);        // 向文件内写入数据; 参数:文件对象、数据缓存、申请写入的字节数、实际写入的字节数

6、文件读取数据
f_res = f_read(&myFile, aReadData, sizeof(aReadData), &num);         // 从文件中读取数据; 参数:文件对象、数据缓冲区、请求读取的最大字节数、实际读取的字节数


4、具体操作示例代码

第一步:编写FatFs的示范函数

在main()的上方,/* USER CODE BEGIN 0 */ 下面,编写以下代码(建议直接复制):

这个函数的作用是:判断是否需要格式化、挂载文件系统、创建文件、写入数据、读出数据。

// SD卡的FatFS文件系统挂载、格式化、读写测试
void FatFsTest(void)
{
    static FATFS myFatFs;                                                 // FatFs 文件系统对象; 这个结构体占用598字节,有点大,需用static修饰(存放在全局数据区), 避免stack溢出
    static FIL myFile;                                                    // 文件对象; 这个结构体占用570字节,有点大,需用static修饰(存放在全局数据区), 避免stack溢出
    static FRESULT f_res;                                                 // 文件操作结果
    static uint32_t num;                                                  // 文件实际成功读写的字节数
    static uint8_t aReadData[1024] = {0};                                 // 读取缓冲区; 这个数组占用1024字节,需用static修饰(存放在全局数据区), 避免stack溢出
    static uint8_t aWriteBuf[] =  "测试; This is FatFs Test ! \r\n";      // 要写入的数据

    // 重要的延时:避免烧录期间的复位导致文件读写、格式化等错误
    HAL_Delay(1000);                                                      // 重要:稍作延时再开始读写测试; 避免有些仿真器烧录期间的多次复位,短暂运行了程序,导致下列读写数据不完整。

    // 1、挂载测试:在SD卡挂载文件系统
    printf("\r\n\r\n");
    printf("1、挂载 FatFs 测试 ****** \r\n");
    f_res = f_mount(&myFatFs, "0:", 1);                                   // 在SD卡上挂载文件系统; 参数:文件系统对象、驱动器路径、读写模式(0只读、1读写)
    if (f_res == FR_NO_FILESYSTEM)                                        // 检查是否已有文件系统,如果没有,就格式化创建创建文件系统
    {
        printf("SD卡没有文件系统,开始格式化…...\r\n");
        static uint8_t aMountBuffer[4096];                                // 格式化时所需的临时缓存; 块大小512的倍数; 值越大格式化越快, 如果内存不够,可改为512或者1024; 当需要在函数内定义这种大缓存时,要用static修饰,令缓存存放在全局数据区内,不然,可能会导致stack溢出。
        f_res = f_mkfs("0:", 0, 0, aMountBuffer, sizeof(aMountBuffer));   // 格式化SD卡; 参数:驱动器、文件系统(0-自动\1-FAT12\2-FAT16\3-FAT32\4-exFat)、簇大小(0为自动选择)、格式化临时缓冲区、缓冲区大小; 格式化前必须先f_mount(x,x,1)挂载,即必须用读写方式挂载; 如果SD卡已格式化,f_mkfs()的第2个参数,不能用0自动,必须指定某个文件系统。
        if (f_res == FR_OK)                                               // 格式化 成功
        {
            printf("SD卡格式化:成功 \r\n");
            f_res = f_mount(NULL, "0:", 1);                               // 格式化后,先取消挂载
            f_res = f_mount(&myFatFs, "0:", 1);                           // 重新挂载
            if (f_res == FR_OK)
                printf("FatFs 挂载成功 \r\n");                            // 挂载成功
            else
                return;                                                   // 挂载失败,退出函数
        }
        else
        {
            printf("SD卡格式化:失败 \r\n");                              // 格式化 失败
            return;
        }
    }
    else if (f_res != FR_OK)                                              // 挂载异常
    {
        printf("FatFs 挂载异常: %d; 检查MX_SDIO_SD_Init()是否已修改1B\r", f_res);
        return;
    }
    else                                                                  // 挂载成功
    {
        if (myFatFs.fs_type == 0x03)                                      // FAT32; 1-FAT12、2-FAT16、3-FAT32、4-exFat
            printf("SD卡已有文件系统:FAT32\n");
        if (myFatFs.fs_type == 0x04)                                      // exFAT; 1-FAT12、2-FAT16、3-FAT32、4-exFat
            printf("SD卡已有文件系统:exFAT\n");                         
        printf("FatFs 挂载成功 \r\n");                                    // 挂载成功
    }

    // 2、写入测试:打开或创建文件,并写入数据
    printf("\r\n");
    printf("2、写入测试:打开或创建文件,并写入数据 ****** \r\n");
    f_res = f_open(&myFile, "0:text.txt", FA_CREATE_ALWAYS | FA_WRITE);   // 打开文件; 参数:要操作的文件对象、路径和文件名称、打开模式;
    if (f_res == FR_OK)
    {
        printf("打开文件 成功 \r\n");
        printf("写入测试:");
        f_res = f_write(&myFile, aWriteBuf, sizeof(aWriteBuf), &num);     // 向文件内写入数据; 参数:文件对象、数据缓存、申请写入的字节数、实际写入的字节数
        if (f_res == FR_OK)
        {
            printf("写入成功  \r\n");
            printf("已写入字节数:%d \r\n", num);                         // printf 写入的字节数
            printf("已写入的数据:%s \r\n", aWriteBuf);                   // printf 写入的数据; 注意,这里以字符串方式显示,如果数据是非ASCII可显示范围,则无法显示
        }
        else
        {
            printf("写入失败 \r\n");                                      // 写入失败
            printf("错误编号: %d\r\n", f_res);                           // printf 错误编号
        }
        f_close(&myFile);                                                 // 不再读写,关闭文件
    }
    else
    {
        printf("打开文件 失败: %d\r\n", f_res);
    }

    // 3、读取测试:打开已有文件,读取其数据
    printf("3、读取测试:打开刚才的文件,读取其数据 ****** \r\n");
    f_res = f_open(&myFile, "0:text.txt", FA_OPEN_EXISTING | FA_READ);    // 打开文件; 参数:文件对象、路径和名称、操作模式; FA_OPEN_EXISTING:只打开已存在的文件; FA_READ: 以只读的方式打开文件
    if (f_res == FR_OK)
    {
        printf("打开文件 成功 \r\n");
        f_res = f_read(&myFile, aReadData, sizeof(aReadData), &num);      // 从文件中读取数据; 参数:文件对象、数据缓冲区、请求读取的最大字节数、实际读取的字节数
        if (f_res == FR_OK)
        {
            printf("读取数据 成功 \r\n");
            printf("已读取字节数:%d \r\n", num);                         // printf 实际读取的字节数
            printf("读取到的数据:%s\r\n", aReadData);                    // printf 实际数据; 注意,这里以字符串方式显示,如果数据是非ASCII可显示范围,则无法显示
        }
        else
        {
            printf("读取 失败  \r\n");                                    // printf 读取失败
            printf("错误编号:%d \r\n", f_res);                           // printf 错误编号
        }
    }
    else
    {
        printf("打开文件 失败 \r\n");                                     // printf 打开文件 失败
        printf("错误编号:%d\r\n", f_res);                                // printf 错误编号
    }

    f_close(&myFile);                                                     // 不再读写,关闭文件
    f_mount(NULL, "0:", 1);                                               // 不再使用文件系统,取消挂载文件系统
}

编写完成后,位置如下图:

第二步:编写SD卡信息获取函数

在刚才函数的下方,再编写一个SD卡信息获取函数(建议直接复制)。

这个函数的作用是:获取SD卡的基础信息、块数量 、块大小、卡容量。

// 获取SD卡信息
// 注意: 本函数需要在f_mount()执行后再调用,因为CubeMX生成的FatFs代码, 会在f_mount()函数内对SD卡进行初始化
void SDCardInfo(void)
{
    HAL_SD_CardInfoTypeDef pCardInfo = {0};                    // SD卡信息结构体
    uint8_t status = HAL_SD_GetCardState(&hsd);                // SD卡状态标志值
    if (status == HAL_SD_CARD_TRANSFER)
    {
        HAL_SD_GetCardInfo(&hsd, &pCardInfo);                  // 获取 SD 卡的信息
        printf("\r\n");
        printf("*** 获取SD卡信息 *** \r\n");
        printf("卡类型:%d \r\n", pCardInfo.CardType);         // 类型返回:0-SDSC、1-SDHC/SDXC、3-SECURED
        printf("卡版本:%d \r\n", pCardInfo.CardVersion);      // 版本返回:0-CARD_V1、1-CARD_V2
        printf("块数量:%d \r\n", pCardInfo.BlockNbr);         // 可用的块数量
        printf("块大小:%d \r\n", pCardInfo.BlockSize);        // 每个块的大小; 单位:字节
        printf("卡容量:%lluG \r\n", ((uint64_t)pCardInfo.BlockSize * pCardInfo.BlockNbr) / 1024 / 1024 / 1024);  // 计算卡的容量; 单位:GB
    }
}

第三步:在 main()函数内,调用刚才那两个函数

调用位置,如下图:

至此,代码编写完成,可以编译、烧录了。


四、实验效果

程序运行后,串口助手输出如下:

如有错漏 ,望指正~~~!

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

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

相关文章

51单片机之使用Keil uVision5创建工程以及使用stc-isp进行程序烧录步骤

一、Keil uVision5创建工程步骤 1.点击项目,新建 2.新建目录 3.选择目标机器,直接搜索at89c52选择,然后点击OK 4.是否添加起吊文件,一般选择否 5.再新建的项目工程中添加文件 6.选择C文件 7.在C文件中右键,添加…

aws(学习笔记第二十七课) 使用aws API Gateway+lambda体验REST API

aws(学习笔记第二十七课) 使用aws API Gatewaylambda体验REST API 学习内容: 使用aws API Gatewaylambda 1. 使用aws API Gatewaylambda 作成概要 使用api gateway定义REST API,之后再接收到了http request之后,redirect到lambda进行执行。…

5 前端系统开发:Vue2、Vue3框架(上):Vue入门式开发和Ajax技术

文章目录 前言一、Vue框架(简化DOM操作的一个前端框架):基础入门1 Vue基本概念2 快速入门:创建Vue实例,初始化渲染(1)创建一个入门Vue实例(2)插值表达式:{{表…

快速在wsl上部署学习使用c++轻量化服务器-学习笔记

知乎上推荐的Tinywebserver这个服务器,快速部署搭建,学习c服务器开发 仓库地址 githubhttps://link.zhihu.com/?targethttps%3A//github.com/qinguoyi/TinyWebServerhttps://link.zhihu.com/?targethttps%3A//github.com/qinguoyi/TinyWebServer 在…

2025年Android NDK超全版本下载地址

Unity3D特效百例案例项目实战源码Android-Unity实战问题汇总游戏脚本-辅助自动化Android控件全解手册再战Android系列Scratch编程案例软考全系列Unity3D学习专栏蓝桥系列ChatGPT和AIGC 👉关于作者 专注于Android/Unity和各种游戏开发技巧,以及各种资源分…

React 设计模式:实用指南

React 提供了众多出色的特性以及丰富的设计模式,用于简化开发流程。开发者能够借助 React 组件设计模式,降低开发时间以及编码的工作量。此外,这些模式让 React 开发者能够构建出成果更显著、性能更优越的各类应用程序。 本文将会为您介绍五…

B站自研的第二代视频连麦系统(上)

导读 本系列文章将从客户端、服务器以及音视频编码优化三个层面,介绍如何基于WebRTC构建视频连麦系统。希望通过这一系列的讲解,帮助开发者更全面地了解 WebRTC 的核心技术与实践应用。 背景 在文章《B站在实时音视频技术领域的探索与实践》中&#xff…

使用Python实现PDF与SVG相互转换

目录 使用工具 使用Python将SVG转换为PDF 使用Python将SVG添加到现有PDF中 使用Python将PDF转换为SVG 使用Python将PDF的特定页面转换为SVG SVG(可缩放矢量图形)和PDF(便携式文档格式)是两种常见且广泛使用的文件格式。SVG是…

[渗透测试]热门搜索引擎推荐— — shodan篇

[渗透测试]热门搜索引擎推荐— — shodan篇 免责声明:本文仅用于分享渗透测试工具,大家使用时,一定需要遵守相关法律法规。 除了shodan,还有很多其他热门的,比如:fofa、奇安信的鹰图、钟馗之眼等&#xff0…

基于物联网技术的智能寻车引导系统方案:工作原理、核心功能及系统架构

本文专为IT技术员、软件开发工程师及智能停车领域专业人士打造,旨在深入剖析智能寻车引导系统的构建与优化过程。如需获取详细解决方案可前往文章最下方获取,如有项目需求及技术合作可私信作者。 智能寻车引导系统是一种集智能化、自动化于一体的停车管理…

【React】合成事件语法

React 合成事件是 React 为了处理浏览器之间的事件差异而提供的一种跨浏览器的事件系统。它封装了原生的 DOM 事件,提供了一致的事件处理机制。 合成事件与原生事件的区别: 合成事件是 React 自己实现的,封装了原生事件。合成事件依然可以通…

Redis02 - 持久化

Redis持久化 文章目录 Redis持久化一:持久化简介1:Redis为什么要进行持久化2:Redis持久化的方式 二:RDB持久化介绍1:手动触发RDB2:自动触发RDB3:redis.conf中进行RDB的配置4:RDB优缺…

初始JavaEE篇 —— Spring Web MVC入门(上)

找往期文章包括但不限于本期文章中不懂的知识点: 个人主页:我要学编程程(ಥ_ಥ)-CSDN博客 所属专栏:JavaEE 目录 RequestMappingg 注解介绍 Postman的介绍与使用 PostMapping 与 GetMapping 注解 构造并接收请求 接收简单参数 接收对象…

Leetcode—487. 最大连续1的个数 II【中等】Plus

2025每日刷题&#xff08;210&#xff09; Leetcode—487. 最大连续1的个数 II 实现代码 class Solution { public:int findMaxConsecutiveOnes(vector<int>& nums) {int zeros 0;int ans 0;for(int l 0, r 0; r < nums.size(); r) {if(nums[r] 0) {zeros;…

【MySQL】窗口函数详解(概念+练习+实战)

文章目录 前言1. SQL窗口函数 1.1 窗口函数概念1.2 窗口函数语法1.3 常见窗口函数 1.3.1 聚合窗口函数1.3.2 专用窗口函数 1.4 窗口函数性能比较 2. LeetCode 例题 2.1 LeetCode SQL 178&#xff1a;分数排名2.2 LeetCode SQL 184&#xff1a;最高工资2.3 LeetCode SQL 185&am…

前端组件标准化专家Prompt指令的最佳实践

前端组件标准化专家Prompt 提示词可作为项目自定义提示词使用&#xff0c;本次提示词偏向前端开发的使用&#xff0c;如有需要可适当修改关键词和示例 推荐使用 Cursor 中作为自定义指令使用Cline 插件中作为自定义指令使用在力所能及的范围内使用最好的模型&#xff0c;可以…

18爬虫:关于playwright相关内容的学习

1.如何在python中安装playwright 打开pycharm&#xff0c;进入终端&#xff0c;输入如下的2个命令行代码即可自动完成playwright的安装 pip install playwright ——》在python中安装playwright第三方模块 playwright install ——》安装playwright所需的工具插件和所支持的…

docker Error response from daemon: Get “https://registry-1.docker.io/v2/ 的问题处理

docker Error response from daemon: Get "https://registry-1.docker.io/v2/ 的问题处理 最近pull 数据 发现 docker 有如下错误 文章目录 docker Error response from daemon: Get "https://registry-1.docker.io/v2/ 的问题处理报错问题检查网络连接解决方案&…

【Linux系统】线程:线程的优点 / 缺点 / 超线程技术 / 异常 / 用途

1、线程的优点 创建和删除线程代价较小 创建一个新线程的代价要比创建一个新进程小得多&#xff0c;删除代价也小。这种说法主要基于以下几个方面&#xff1a; &#xff08;1&#xff09;资源共享 内存空间&#xff1a;每个进程都有自己独立的内存空间&#xff0c;包括代码段…

123,【7】 buuctf web [极客大挑战 2019]Secret File

进入靶场 太熟悉了&#xff0c;有种回家的感觉 查看源代码&#xff0c;发现一个紫色文件 点下看看 点secret 信息被隐藏了 要么源代码&#xff0c;要么抓包 源代码没有&#xff0c;抓包 自己点击时只能看到1和3处的文件&#xff0c;点击1后直接跳转3&#xff0c;根本不出…