资讯动态

STM32 SDIO+DMA读写SD卡:从初始化到FATFS的完整工程

发布时间:2026/10/16 10:43:54 来源:尧图企业网站定制
简介这是一份面向STM32与HAL库开发者的SD卡读写工程源码包聚焦SDIO加DMA中断模式下的数据交互并围绕1bit总线模式给出可落地的工程配置。代码中完整展示了时钟捕获边沿、分频器旁路、空闲时钟输出、硬件流控等SDMMC关键参数的设置方法适合需要移植SD卡驱动、扩展外部存储或深入理解SDIO时序的嵌入式学习者对照参考。压缩包共112个文件以h头文件、c源文件和uvprojx工程文件为主同时包含ioc图形化配置、s启动文件、hex固件、bat清理脚本等整体约1MB分类清晰可直接在Keil中打开编译便于逐文件查看初始化、中断和DMA回调逻辑。目前已有668人学习下载通过这份工程不仅能快速验证SDIO加DMA的读写流程还能在此基础上继续扩展多块读写、FatFS文件系统、SD卡状态检测等应用适合作为课程设计或项目起步的参考模板。 请严格按照要求输出以下是完整正文1. SDIODMA 读写 SD 卡一块 32 位芯片能把 SD 卡跑多快先讲一个实测现象。某条日志采集设备之前长期用 SPI 模式写 SD 卡写入速率只有 200KB/s 出头主控大部分时间被字节搬运打断日志还经常在掉电时把文件分配表写花。换到 STM32 的 SDIO 外设加 DMA 之后写入速度上了一个量级CPU 只在发命令和收回调时碰一次数据剩下都是外设自己在搬。这套「STM32HAL」的 SDIODMA 读写 SD 卡工程把 SDIO 初始化、4-bit 宽模式、DMA 完成回调、FATFS 挂载以及回环校验一起打包成了能直接烧录的模板。适合两类人想从 SPI 切到 SDIO 的嵌入式工程师以及拿到芯片工程后要在三天内把 SD 卡驱动调通的朋友。下面跟着我把它拆开。2. 架构设计SD 卡协议栈、SDIO 外设和 DMA 到底怎么配合很多人在调试 SDIO 时会盯着数据线波形那是方向错了。更合适的方式是把这条链路看成一个流水线CPU 只负责下订单DMA 是搬运工SDIO 是打包车间SD 卡是远端仓库。每层各干各的才不会一卡就全卡。2.1 先分清三层SD 协议、SDIO 外设、HAL 驱动SD 卡访问在嵌入式里至少分三层。最底层是 SD 卡物理协议CMD 线上要按 SD 规范发命令、收响应数据线上则是按块传输。第二层是 STM32 的 SDIO 外设它负责把你在寄存器里写好的内容变成符合时序的命令序列把数据线上的并行字节搬进内部 FIFO。第三层是 HAL 库提供HAL_SD_Init、HAL_SD_ReadBlocks_DMA这类接口把寄存器的繁琐操作封装掉。很多人从 SPI 驱动 SD 卡切入很容易跳过 SD 协议层直接拿 MMC 命令序列去对照调试切到 SDIO 之后就蒙了。根因就是把协议、外设、HAL 这三层混在一层去理解。SDIO 外设只负责到“块设备”这一层它并不知道芯片里跑的是 FAT32 还是 exFAT那是 FATFS 的事情。认清这个层次调试时就能快速判断问题出在初始化阶段、DMA 传输阶段还是文件系统阶段。2.2 四种访问模式的取舍SPI、1-bit、4-bit 分别适合什么场景SD 卡常见的访问模式有三种。SPI 模式兼容性最广但它本质是半双工低速串行速率上限通常只有几百 KB/s 到 1MB/s 左右适合 bootloader 这种不追求吞吐量的场景。1-bit SDIO 占用一根数据线配合 48MHz 时钟能跑出几 MB/s但吞吐量受限。4-bit SDIO 四根数据线同时采样是多数 STM32 工程的默认选择实测写入速度能到 10MB/s 以上取决于芯片型号和卡本身。选择哪种模式不只看速度。4-bit 模式需要四根数据线布线整齐板子上如果有电平转换器上升沿质量会直接影响高速模式下的误码。很多板子省引脚把 SDIO D1/D2/D3 拉得很长结果初始化一切正常连续读写几百 KB 之后就开始报 CRC 错误最后只能退回到 1-bit 模式。所以刚接触 SDIO 时优先用 CubeMX 生成默认的 4-bit 配置不要自作主张换 IO。2.3 SDIO 的时钟链路和 DMA 请求方向SDIO 内部有一条从适配时钟到 SDIO_CK 的分频链路。HAL 库的ClockDiv字段就是分频系数配合 SDIO_CK 上允许的最高 48MHz 使用。初始化阶段必须把分频做好让 SDIO_CK 落在 400kHz 附近等到HAL_SD_Init返回成功之后再把分频调小切到高速。有些工程把ClockDiv直接改成 0上电后卡连识别都过不去就是这个原因。数据通路方面SDIO 外设有 READ 和 WRITE 两条 DMA 请求线。写卡时内存数据经 DMA 搬运到 SDIO FIFO读卡时 FIFO 数据经 DMA 搬运到内存。DMA 请求方向配反读写就会出现奇怪的错位数据。参考配置如下配置项推荐值说明SDIO 总线宽度4-bit速度和布线难度的折中初始化时钟分频400kHz 附近保证老卡也能正常识别高速阶段时钟24MHz 或 48MHz视芯片主频和板子信号质量DMA 数据宽度32 位SDIO 数据 FIFO 按字读取DMA 模式Normal 单次传输每次读写独立便于调试缓冲区对齐4 字节起步推荐 32 字节满足 DMA 传输要求2.4 从 f_open 到写进 SD 卡的一条完整链路应用层调用f_write时FATFS 会先把数据写到内存中的文件系统缓冲然后按文件分配表找到可用扇区最终调用disk_write把块地址和数据指针交给我们。disk_write再调用HAL_SD_WriteBlocks_DMASDIO 外设把块命令和写入数据串起来DMA 负责把数据搬进 FIFO由硬件自动完成整块写入。这条链路上最容易出问题的是数据指针和扇区编号。FATFS 从disk_ioctl拿到GET_SECTOR_COUNT和GET_SECTOR_SIZE之后会按照这个参数计算簇、分配 FAT 表。如果你在disk_ioctl里返回的块大小不是 512文件系统初始化就会失败。这也是整套工程里最值得先看的部分。3. 核心实现拆解从 HAL 初始化到 FATFS 挂载的完整链路这一章直接面对源码。写这套工程时我先验证裸机下的 SDIO 读写再挂 FATFS防止文件系统的问题和驱动的问题混在一起。3.1 先做对初始化句柄结构和高速模式切换SDIO 的初始化写法并不复杂但参数暗藏玄机。参考代码SD_HandleTypeDef hsd; hsd.Instance SDIO1; hsd.Init.ClockEdge SDIO_CLOCK_EDGE_RISING; /* 上升沿采样 */ hsd.Init.ClockBypass SDIO_CLOCK_BYPASS_DISABLE; /* 使用内部分频不旁路 */ hsd.Init.ClockDiv 2; /* 初始化阶段低速 */ hsd.Init.BusWide SDIO_BUS_WIDE_1B; /* 先用 1-bit 完成识别 */ hsd.Init.BlockSize SDIO_BLOCKSIZE_512; /* 固定 512 字节扇区 */ if (HAL_SD_Init(hsd) ! HAL_OK) { Error_Handler(); } if (HAL_SD_ConfigWideBusOperation(hsd, SDIO_BUS_WIDE_4B) ! HAL_OK) { Error_Handler(); }说明一下每个参数。ClockEdge选上升沿采样这是绝大多数板子的安全配置。ClockBypass通常保持 DIABLE意思是 SDIO_CK 由内部分频得到适合调试如果选 ENABLE时钟直接从外部输入需要外部时钟源。ClockDiv配合适配时钟计算实际频率2 只是起步值最终目标是让初始化阶段时钟在 400kHz 左右。BusWide初始化阶段必须用 1-bit因为 SD 卡上电时只有 CMD 线上能通信数据线状态还没确定等到HAL_SD_Init成功之后再调用HAL_SD_ConfigWideBusOperation切到 4-bit这是 SD 卡协议的规定动作顺序不能颠倒。初始化成功之后还可以顺手把卡信息读出来SD_CardInfo card; HAL_SD_GetCardInfo(hsd, card); /* card.BlockNbr 是总扇区数card.BlockSize 是扇区大小 */BlockNbr会用到 FATFS 的挂载上。如果你在这里发现BlockNbr异常通常是卡没识别干净也可能是板子上 SDIO 引脚虚焊先别往下走。3.2 DMA 读写调用和回调处理DMA 模式写入块数据核心代码是这样#define SECTOR_SIZE 512 #define TEST_SECTORS 4 static uint8_t sd_tx_buf[TEST_SECTORS][SECTOR_SIZE] __attribute__((aligned(32))); static uint8_t sd_rx_buf[TEST_SECTORS][SECTOR_SIZE] __attribute__((aligned(32))); static volatile uint8_t sd_xfer_done 0; int SD_DMA_Write(uint32_t sector, uint8_t *src) { sd_xfer_done 0; if (HAL_SD_WriteBlocks_DMA(hsd, src, sector, TEST_SECTORS) ! HAL_OK) { return -1; } uint32_t tick HAL_GetTick(); while (!sd_xfer_done) { if (HAL_GetTick() - tick 1000) { return -2; /* 超时退出避免死等 */ } } return 0; } void HAL_SD_TxCpltCallback(SD_HandleTypeDef *hsd) { sd_xfer_done 1; } void HAL_SD_RxCpltCallback(SD_HandleTypeDef *hsd) { sd_xfer_done 1; } void HAL_SD_ErrorCallback(SD_HandleTypeDef *hsd) { sd_xfer_done 0; }这段代码有几个值得注意的地方。HAL_SD_WriteBlocks_DMA只是把请求提交给 SDIO 和 DMA真正搬完数据需要等 DMA 完成中断触发HAL_SD_TxCpltCallback。所以调用之后必须等待不能直接返回。缓冲区用aligned(32)是为了满足 DMA 外设对内存对齐的要求常见的坑是传入栈上的普通 uint8_t 数组栈地址不保证按 4 字节对齐结果数据写到一半触发总线错误。回调函数里必须按sd_xfer_done标志位来区分状态但回调里做太多事情会让中断变长。我在工程里通常只是置一个标志位由主循环或者上层任务去继续处理。HAL_SD_ErrorCallback不能忽略至少要把错误状态打印出来否则卡初始化失败时程序会停在等待标志位的地方现象和死机一模一样。3.3 把 SDIO 封装成 FATFS 的磁盘读写层FATFS 与底层硬件之间有一层抽象叫 diskio。这层接口真正把 SDIO 块读写能力提供给文件系统DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { if (HAL_SD_ReadBlocks_DMA(hsd, buff, sector, count) ! HAL_OK) { return RES_ERROR; } while (!sd_xfer_done) { } return RES_OK; } DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count) { if (HAL_SD_WriteBlocks_DMA(hsd, buff, sector, count) ! HAL_OK) { return RES_ERROR; } while (!sd_xfer_done) { } return RES_OK; } DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { switch (cmd) { case GET_SECTOR_COUNT: *(DWORD *)buff hsd.SCard.BlockNbr; return RES_OK; case GET_SECTOR_SIZE: *(WORD *)buff hsd.SCard.BlockSize; return RES_OK; case GET_BLOCK_SIZE: *(DWORD *)buff 1; return RES_OK; } return RES_ERROR; }这里返回给 FATFS 的核心参数有两个。GET_SECTOR_COUNT告诉文件系统这个卡一共多少扇区文件系统据此划分分区和簇GET_SECTOR_SIZE必须返回 512SD 卡的物理扇区大小固定为 512 字节。GET_BLOCK_SIZE表示擦除块大小SD 卡上返回 1 即可。FATFS 挂载本身不复杂但是要先加载并注册底层接口典型调用如下FATFS fs; FIL file; UINT written; f_mount(fs, , 0); if (f_open(file, log.txt, FA_CREATE_ALWAYS | FA_WRITE) FR_OK) { f_write(file, hello from sdio dma\r\n, 20, written); f_close(file); } f_mount(NULL, , 0);这段代码在逻辑上没问题但初学者很容易陷入一个循环f_mount 失败改 FATFS 配置还是失败最后回头才发现 SDIO 在disk_read中一直没有把数据读回来所以文件系统拿不到正确的分区表。我建议先用第 4 章的裸读写测试验证磁盘层再挂文件系统能省掉大半调崩溃的时间。3.4 为什么不建议用轮询模式HAL 库也提供了HAL_SD_ReadBlocks和HAL_SD_WriteBlocks的轮询版本不需要 DMA。但轮询方式在传输每个块时都会占用 CPU拷完一个块才能接着发下一个。实测同一个 SD 卡轮询模式吞吐量大概只有 DMA 模式的 1/3 到 1/2而且 CPU 占用率很高。这个工程之所以坚持 SDIODMA就是要把大块数据传输从 CPU 手里解放出来好让主循环去做日志解析、协议处理等更有价值的事情。需要强调的是DMA 模式并没有改变 SD 卡时序本身。SDIO 外设发送的命令、等待的响应、CRC 校验规则与轮询模式完全一致区别只在于 FIFO 到内存之间的数据搬运由谁负责。理解了这一点后续排查速度异常时方向就不会跑偏。4. 跑通流程把工程下载下来后的验证路径这一章告诉你烧录后怎么判断这套工程真的跑通了。直接跑文件系统有风险万一代码有问题你会同时面对 FATFS 错误和 SDIO 错误两层迷障。所以验证分两步走。4.1 硬件接线与必备引脚拿到工程源码后先看硬件连接。SDIO 需要的引脚一共有 6 根SDIO_CLK、SDIO_CMD、SDIO_D0、SDIO_D1、SDIO_D2、SDIO_D3。另外需要板子给卡座提供 3.3V 电源和地线。如果你用的是带电平转换的卡座注意转换方向是双向的CMD 和 DATA 线都要上拉电阻不然高速传输时可能出现边沿变差。对照原理图确认这组引脚之后再把串口 1 接出来波特率 115200方便看打印信息。工程代码里会输出类似“SD init ok”、“sector count 15261744”这样的信息谁能直接读卡过不了关。4.2 裸读写回环自测先证明 DMA 链路是通的我的习惯是先把 DMA 读写做成回环验证不涉及文件系统。具体做法是写一份测试模式到第 0 扇区再读回来比对uint8_t pattern[512]; uint8_t readback[512]; for (int i 0; i 512; i) { pattern[i] (uint8_t)(i * 7 1); } memset(readback, 0, sizeof(readback)); SD_DMA_Write(0, pattern); SD_DMA_Read(0, readback); if (memcmp(pattern, readback, 512) ! 0) { printf(ERROR: data mismatch\r\n); } else { printf(INFO: dma loopback ok\r\n); }这个测试能分辨很多问题。写入后立刻读回如果数据不一致无非三个原因DMA 写方向错了、DMA 读方向错了、SDIO 时钟模式有问题。如果数据读回全 0xFF多半是读命令没发出去卡还在初始化状态。如果第一次对第二次第三方可能就是缓冲区复用或回调时序问题。这一步过了DMA 链路才算通。4.3 上 FATFS 建文件并打印卡容量裸读写通了再挂 FATFS。格式化时调用f_mkfs如果卡是全新卡大概率没有文件系统需要先格式化一遍。工程里通常提供一个测试命令直接调用FATFS fs; DIR dir; FILINFO fno; FRESULT fr; fr f_mount(fs, , 0); if (fr FR_NO_FILESYSTEM) { /* 先格式化为 FAT32 */ f_mkfs(, FM_FAT, 0, work_buf, sizeof(work_buf)); f_mount(NULL, , 0); f_mount(fs, , 0); } fr f_opendir(dir, ); if (fr FR_OK) { while (f_readdir(dir, fno) FR_OK fno.fname[0]) { printf(file: %s, size%lu\r\n, fno.fname, fno.fsize); } }格式化要注意f_mkfs的work_buf必须是一块大内存通常至少 512 字节建议给到 4KB否则格式化时 FATFS 会返回FR_NOT_ENOUGH_CORE。格式化之后重新挂载就能看到文件系统信息也能新建文件了。4.4 用串口观察连续多次大块写入的稳定性单独读写一块成功不代表连续多块没问题。我在测试时会让设备连续写 1MB 数据然后通过串口打印每 512 字节块的地址和校验值。校验值只用一个累加和就够了不需要上 CRC。如果中途出现累加和不匹配就可以确认 DMA 长传输有丢块或错位结合第 5 章的排查方向去处理。这个步骤看现象时要留意卡的产品类型不同品牌卡对 SDIO 时序的抗干扰能力差别很大。有些卡在 48MHz 时钟下跑不了几分钟就开始报错换成 24MHz 就稳定。这不算代码 bug属于板级信号质量问题。5. 避坑记录SDIODMA 最容易踩的 5 个坑这一章是血泪经验。每一条都是实际遇到过的按“现象 → 原因 → 解决”来写。5.1 f_mount 总是返回 FR_DISK_ERR卡却能被初始化识别现象是HAL_SD_Init返回正常卡容量也能读到但f_mount返回FR_DISK_ERR底层报RES_ERROR。原因是初始化时只用低速时钟完成了卡识别切换高速模式后SDIO_CK 频率超过卡或者板子的极限后续读扇区时序全部乱掉FATFS 拿到错误数据。解决方法是把初始化后的时钟分频调大比如把 48MHz 降到 24MHz同时检查HAL_SD_ConfigWideBusOperation是否在HAL_SD_Init之后立刻被调用SD 卡必须先握手成功再切 4-bit。5.2 卡死在 while(!sd_xfer_done)DMA 回调和错误回调都没触发现象是代码停在等标志位的地方串口没有任何异常打印。原因是sd_xfer_done置位依赖 DMA 中断而 DMA 中断没有配置或优先级被屏蔽。很多工程只在 CubeMX 里勾选了 DMA 通道没有在 NVIC 里使能相应中断线。解决方法是检查 CubeMX 生成的 NVIC 配置确认 DMA 中断已使能并且给 SDIO 和 DMA 一个合理的抢占优先级。我一般会把 DMA 中断设为比串口中断更高的优先级避免高速传输时被串口打印拖慢。5.3 读回数据错位最后一块数据是 0xFF 损坏现象是连续写多个扇区时前面的块都正确最后一块经常丢数据或读回 0xFF。原因是 DMA 传输结束但 SDIO 内部 FIFO 还没排空或者在读取时缓存了数据没有及时失效。解决方法是读操作前把目标缓冲区先做一次无效化确保缓存中的旧数据不会覆盖 DMA 写入的内容写操作后等待HAL_SD_TxCpltCallback再复用缓冲区。这个坑在支持 D-Cache 的芯片上更容易出现但也可能只是因为最后一次 DMA 请求没被完全应答超时后重新初始化一次 SDIO 即可。5.4 高速模式下偶发 CRC 错误降速后消失现象是长时间读写之后HAL_SD_ErrorCallback被触发读取状态寄存器能看到数据 CRC 错误位置 1。原因是 SDIO_CK 频率过高板子的 D0 到 D3 线之间信号偏移过大或者上拉电阻值选得不对。解决方法是先降频排查把高速阶段时钟从 48MHz 降到 24MHz如果问题消失说明是板级噪声再检查 SDIO 数据线是否做了等长设计上拉电阻是否在 10k 到 47k 之间。这不是软件玄学是硬件时序容限问题。5.5 64GB 以上 SDXC 卡插上去只能读不能写现象是小容量卡一切正常大容量卡初始化成功但文件操作报FR_NO_FILESYSTEM或者写失败。原因是很多 SDXC 卡出厂是 exFAT 格式老版本 FATFS 默认不支持 exFAT需要在ffconf.h中打开FF_FS_EXFAT宏。解决方法是打开 exFAT 支持并重新编译或者在电脑上把卡重新格式化为 FAT32。嵌入式场景里用 64GB 卡本来就不划算走 FAT32 兼容性更稳。6. 二次开发思路从裸机日志到带超时的状态机跑通这套工程之后下一步通常是把它接进业务代码。最直接的改造是把现在的while(!sd_xfer_done)等待改成带超时的状态机volatile uint8_t sd_xfer_done 0; int sd_write_with_timeout(uint32_t sector, uint8_t *buf, uint32_t timeout_ms) { uint32_t tick HAL_GetTick(); sd_xfer_done 0; if (HAL_SD_WriteBlocks_DMA(hsd, buf, sector, 1) ! HAL_OK) { return -1; } while (!sd_xfer_done) { if (HAL_GetTick() - tick timeout_ms) { return -2; } } return 0; }这样改的好处是主循环不用死等 SD 卡回应。SD 卡写入本来就是慢设备操作卡初始化、擦除、写 Flash 都可能需要几十毫秒状态机返回超时后上层可以安排重试或者丢弃这一帧日志不会拖垮整个系统。接着可以把读写缓冲从单缓冲改成双缓冲写满一块提交给 DMA另一块继续给日志解析模块填充实现流水线写入。接 RTOS 时要注意缓冲区所有权问题。SDIO DMA 传输期间缓冲区被外设占用其他任务不能再写这块内存。我一般会为文件系统挂一个互斥量日志任务先拿锁再写文件避免多个任务同时调用 SDIO 时序造成数据错乱。如果项目里日志量很大还可以在回调里直接发送信号量给日志任务让写入动作完全被阻塞在文件系统层之下系统行为更可控。另外提一个容易忽略的点掉电保护。DMA 正在写扇区时突然掉电FAT 表可能损坏。二次开发时最好给 SD 卡供电加一个检测电路掉电瞬间先把写入任务停住再把缓存落盘而不是立刻截断电源。这套工程已经把所有驱动细节整理成可以直接打开的工程下载之后按第 4 章的步骤烧录验证就能看到实际效果。我后来每次做 SD 卡相关项目都强制先跑一遍裸读写回环再上 FATFS从那次以后踩的坑明显少了。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑