【CubeMX+STM32】SD卡 文件系统读写 FatFs+SDIO+DMA (F103版本)

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

目录

一、简述

[二、CubeMX 配置 FatFS+SDIO + DMA](#二、CubeMX 配置 FatFS+SDIO + DMA)

[三、Keil 编辑代码](#三、Keil 编辑代码)

四、实验效果


实现效果,如下图:


一、简述

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

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

1、CubeMX_STM32_SD卡_基础读写

2、CubeMX_STM32_SD卡_DMA读写

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

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

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


二、CubeMX 配置 SDIO + DMA + FatFS

本示例需要用到 printf 输出调试信息至串口助手,因此,基础工程必须已做好 uart1 + printf 的处理!

基础工程的新建,可参考下方链接:

1、使能 SDIO

  • Mode :工作模式。选择 SD 4 bits Wide bus , 即4位并行数据总线。
  • SDIOCLK Clock divide factor :SDIO 时钟分频系数。默认值是 0**, 改为:** 6 。( F103系列,需把时钟分频系数修改为 6 或 8,不然初始化会失败。而F407系列可使用默认的 0 值)

​

2、启用DMA

  • 点击 ADD 按钮,添加:SDIO。

注意:F103系列,SDIO的 读、写 共用同一DMA通道。而F407则有RX、TX两个通道。

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

  • 打勾启用 SDIO 中断,并修改中断响应优先级:1
  • 顺手修改 SysTick 中断优先级:0(非必须项。但强烈建议修改为 0, 即最高响应级别)

4、FatFs 使能、参数配置

  • Mode: 打勾 SD Card
  • CODE_PAGE: 选择 Simplified Chinese GBK (简体中文)
  • USE_LFN: 选择Enabled with dynamic working buffer on the ++STACK++(长文件名称、缓存放在STACK,因此STACK得设置大一些,建议 0x1000)

篇外说明:

本工程使用的 FatFs 版本为 R0.11。 (此配置页第一行 Version选项组中, 可看到 FatFs 版本号)

R0.11 版本,只支持 FAT12、FAT16、FAT32,不支持 exFAT !

F103 对应的 CubeMX / F1 固件包,应该是考虑到 F103系列资源有限,使用较精简的 FatFs版本。

若使用 R0.12 及以上版本,选项组 System Parameters 里会多出 FS_EXFAT 选项。

将 FS_EXFAT 设为Enabled后, FatFs 可增加支持 exFAT。挂载时自动识别 FAT6/FAT32/exFAT, 格式化时也可选择对应格式。

exFAT格式:支持大于32GB 的SD卡,或大于4GB 的单文件。

但是 exFAT 的实现比 FAT32 更复杂,会显著增加代码体积(Flash) 和 RAM 的消耗。FAT32也足够正常使用了,因此无须过度在意exFAT。

5、FatFS 是否使用检测引脚

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

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

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

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

本示例,不使用检测引脚 !!

  1. 本示例,不使用检测引脚 !下面参数,默认空着即可。不管你的硬件电路是否有此检测引脚,都可空着,不影响后面操作。
  2. 如果你的 SD卡座电路已预留此检测引脚,可以在这里指定引脚,使用CubeMX生成并管理。

6、点击生成

当不指定上一节中的检测引脚,在点击 生成工程 后,会先弹出警告,不用管它,点击Yes即可,将会正常生成工程,代码上不会有任何影响。

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

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


三、修正代码bug

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 每次重新生成后,这里都要手动修改一次++。


四、编写示范代码

1、常用函数介绍 ( 可跳过 )

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

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

cpp 复制代码
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常用的几个操作函数,如下表:

函数参数的具体作用,可以通过 DeepSeek、Kimi 等查询详细用法。

cpp 复制代码
FRESULT f_res;

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

2、格式化
f_res = f_mkfs("0:", 0, 0);                                          // 格式化SD卡; FatFs R0.11旧接口 f_mkfs(path, sfd, au):参数:驱动器路径、分区规则sfd(0-FDISK分区式,适用SD卡等存储卡)、簇大小au(0为自动选择); 格式化前必须先f_mount(x,x,1)读写方式挂载

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);         // 从文件中读取数据; 参数:文件对象、数据缓冲区、请求读取的最大字节数、实际读取的字节数

2、编写 FatFs 操作示范代码 ( 挂载、格式化、读、写 )

SD卡的底层初始化、FatFs的初始化,都已由 CubeMX 生成好了。

我们使用时,只需调用相关函数实现对应功能即可。

下面,我们编写一个函数,整体地示范其使用方法:

  1. 判断是否需要格式化
  2. 格式化
  3. 挂载文件系统
  4. 创建文件
  5. 文件写入数据
  6. 文件读取数据

在main()的上方,/* USER CODE BEGIN 0 */ 内,编写以下函数(建议直接复制)。

已带详细中文注释、流程提示,通读两三次,即可理解SD卡的操作流程。

cpp 复制代码
/******************************************************************************
 * 函  数: FatFsTest
 * 功  能: SD卡的FatFs文件系统综合测试
 *          1、挂载:以读写方式挂载FatFs;若卡上没有文件系统(FR_NO_FILESYSTEM),则先格式化再重新挂载
 *          2、写入:打开(不存在则创建)SD卡根目录的 text.txt,写入测试字符串
 *          3、读取:重新打开 text.txt,读回数据并用printf打印
 * 参  数: 无
 * 返 回: 无; 挂载/格式化失败时直接return退出,错误信息经串口打印提示
 * 备  注: 1、测试会改写SD卡根目录的 text.txt; 若触发格式化,则整卡数据会被重建;
 *          2、FATFS/FIL/缓冲区等大对象均已用static修饰(存放在全局数据区),避免栈溢出;
 *          3、函数开头的HAL_Delay(1000)用于避开仿真器烧录期间的多次复位;
 *          4、测试结束后已用f_mount(NULL,...)取消挂载。
******************************************************************************/
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");
        f_res = f_mkfs("0:", 0, 0);                                       // 格式化SD卡; FatFs R0.11旧接口 f_mkfs(path, sfd, au):参数:驱动器路径、分区规则sfd(0-FDISK分区式,适用SD卡等存储卡)、簇大小au(0为自动选择); 格式化前必须先f_mount(x,x,1)读写方式挂载
        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\r\n", f_res);
        printf("检查1:是否已插入 TF 卡。\r\n");
        printf("检查2:MX_SDIO_SD_Init()是否已修改1B。\r\n");

        return;
    }
    else                                                                  // 挂载成功
    {
        if (myFatFs.fs_type == 0x03)                                      // FAT32; 1-FAT12、2-FAT16、3-FAT32、4-exFat
            printf("SD卡已有文件系统:FAT32\r\n");
        if (myFatFs.fs_type == 0x04)                                      // exFAT; 1-FAT12、2-FAT16、3-FAT32、4-exFat
            printf("SD卡已有文件系统:exFAT\r\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);                                               // 不再使用文件系统,取消挂载文件系统
}

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

3、编写 SD卡 信息获取函数

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

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

cpp 复制代码
/******************************************************************************
 * 函  数: SDCardInfo
 * 功  能: 获取并打印SD卡信息:卡类型、卡版本、块数量、块大小、总容量
 * 参  数: 无
 * 返 回: 无
 * 备  注: 1、须在f_mount()挂载之后调用:FatFs的f_mount()内部才会完成SD卡的初始化,之前调用读不到有效信息;
 *          2、卡未插入或未就绪(状态非TRANSFER)时,本函数静默返回,不打印任何信息。
******************************************************************************/
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
    }
}

4、在 main()函数内,调用刚才那两个函数

注意,这两个示范函数,都使用了 printf 输出调试信息,

因此,在调用这两个函数前,需要先初始化UART1,和做好printf的重定向。

调用位置,参考如下图:

至此,代码编写完成!

可以编译、烧录了。


五、实验效果

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

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

相关推荐
动词ing2 小时前
【51单片机】独立按键控制LED灯亮灭
单片机·嵌入式硬件·51单片机
Industio_触觉智能2 小时前
瑞芯微工业ADC采集实战-上|AD7616&RK3506 FlexBus高速并行采集落地
嵌入式硬件·硬件架构·硬件工程·adc·瑞芯微·rk3506·flexbus
女神下凡2 小时前
芯参谋(79):嵌入式学习进阶_(硬件与软件)深入实践指南
arm开发·单片机·嵌入式硬件·学习·fpga开发·设计规范
Zhao136824553912 小时前
AT7456E 中科微电子 OSD图形叠加芯片/发生器芯片 无人机显示开发
嵌入式硬件
‎ദ്ദിᵔ.˛.ᵔ₎2 小时前
STM32 USART 串口
stm32·单片机·嵌入式硬件
smartpi_ai3 小时前
高端智能沙发如何接入大模型语音:JL-17T 离在线语音模块方案实录
人工智能·单片机·语音识别
牢姐与蒯3 小时前
WSL2 迁移教程:把Ubuntu+WSL2从C盘挪到别的盘
linux·运维·服务器·网络·ubuntu·1024程序员节
路上有只喵4 小时前
认识元器件 · 单片机(MCU/MPU/SOC) · 实战篇5
单片机·嵌入式硬件·fpga开发
1-1=04 小时前
.NET Core 配置与选项
.net·.netcore·1024程序员节