资讯详情

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

📅 2026/10/30 4:48:49 | 华诺云谱 👁 阅读
【CubeMX+STM32】SD卡 文件系统读写 FatFs+SDIO+DMA (F103版本)
本篇将使用CubeMXKeil创建一个SD卡的 FatFSSDIODMA 文件系统读写工程。目录一、简述二、CubeMX 配置 FatFSSDIO DMA三、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【STM32CubeMX】 新建一个工程 STM32F103篇参考2【STM32CubeMX】USART1 DMA收发、printf参考3 STM32串口通信 -- bsp_UART.c 文件的移植和函数使用-CSDN博客1、使能 SDIOMode工作模式。选择SD 4 bits Wide bus, 即4位并行数据总线。SDIOCLK Clock divide factorSDIO 时钟分频系数。默认值是 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 CardCODE_PAGE选择Simplified Chinese GBK(简体中文)USE_LFN选择Enabled with dynamic working buffer on theSTACK长文件名称、缓存放在STACK因此STACK得设置大一些建议 0x1000)篇外说明本工程使用的 FatFs 版本为R0.11。(此配置页第一行Version选项组中 可看到 FatFs 版本号)R0.11 版本只支持 FAT12、FAT16、FAT32不支持 exFATF103 对应的 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配置界面 下方配置里可以设置是否使用检测引脚。本示例不使用检测引脚 本示例不使用检测引脚 下面参数默认空着即可。不管你的硬件电路是否有此检测引脚都可空着不影响后面操作。如果你的 SD卡座电路已预留此检测引脚可以在这里指定引脚使用CubeMX生成并管理。6、点击生成当不指定上一节中的检测引脚在点击生成工程后会先弹出警告不用管它点击Yes即可将会正常生成工程代码上不会有任何影响。如果不想有弹窗可以随便指定一个空闲的引脚这样在生成时就不会弹警告了但是需要在生成的工程代码里注释掉引脚检测功能这里不详述可自行搜索方法。操作有点烦人不建议此方法。好了SD卡文件系统通信所需的 SDIO FatFS DMA 已完成配置。三、修正代码bug1、打开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个函数在本工程中正常可用。但本篇暂时用不上这里就进行不示范了。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 等查询详细用法。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 生成好了。我们使用时只需调用相关函数实现对应功能即可。下面我们编写一个函数整体地示范其使用方法判断是否需要格式化格式化挂载文件系统创建文件文件写入数据文件读取数据在main()的上方/* USER CODE BEGIN 0 */ 内编写以下函数建议直接复制)。已带详细中文注释、流程提示通读两三次即可理解SD卡的操作流程。/****************************************************************************** * 函 数 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(检查2MX_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卡的基础信息、块数量 、块大小、卡容量。/****************************************************************************** * 函 数 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的重定向。调用位置参考如下图至此代码编写完成可以编译、烧录了。五、实验效果程序运行后串口助手输出如下如有错漏 望指正~~~
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑