资讯动态

STM32 HAL库SDIO DMA读写SD卡:从原理到性能优化实战指南

发布时间:2026/9/9 21:10:14 来源:尧图企业网站定制
简介面向嵌入式开发中需要高效读写存储卡的场景提供一套基于意法半导体STM32系列芯片硬件抽象层库的完整工程示例采用安全数字输入输出接口与直接存储器访问传输方式并开启中断配合工作。工程适合具备一定单片机开发基础、希望深入理解直接存储器访问与中断在存储驱动中实际用法的学习者使用。资源包共一百一十二个文件以C语言源码和头文件为主另含集成开发环境工程配置、图形化初始化配置文件、文本说明文档及辅助批处理脚本压缩包约一点零兆字节目录清晰。目前已有666人学习下载可作为实际项目移植参考。内容全面覆盖一位宽模式下的接口初始化、直接存储器访问使能、中断回调处理等关键环节还对时钟分频旁路、数据捕获边沿、空闲时钟输出、硬件流控等参数配置给出明确说明。通过该工程读者可以清晰掌握硬件抽象层库中相关驱动框架与直接存储器访问流程的配合方式快速复用代码并缩短开发周期。 做嵌入式开发尤其是用STM32跟SD卡打交道绕不开的一个话题就是SDIO。如果你还在用SPI模式读SD卡那我可以很肯定地告诉你换个思路用SDIODMA速度和CPU占用率完全是两个世界。这篇文章我打算把“STM32HAL库SDIODMA读写SD卡”这套组合从原理到实操讲透包括CubeMX怎么配、HAL库有哪些隐藏坑、实际调试中我最常碰到的问题以及最后实测的读写数据对比。适合刚把HAL库基础过完、准备在项目里正经用大容量存储的朋友也适合已经在写但被各种莫名其妙问题卡住的老哥。1. 先理清楚SDIO、SD卡和DMA是怎么配合的1.1 SDIO接口组成和它为什么比SPI快SDIO全称是Secure Digital Input/Output从名字能看出来它跟SD卡规范是同源的接口标准。跟SPI模式最大的区别在于SDIO是一个并行总线物理上包含CMD线命令线、CLK线时钟线和D0到D3四根数据线。4位模式下一个时钟周期能同时传4个bit而SPI模式无论怎么提频一个周期只传1个bit。这还不算完SDIO的数据传输是块传输block的连续读多个块时命令开销很小不像SPI模式读一个块就要发一次命令。STM32的SDIO外设最高能跑到多少以F407为例SDIO时钟最高可以到48MHz4位模式下的理论带宽能做到24MB/s左右。当然这是理论值实际受SD卡本身限制、SDIO Controller的限制以及线材影响但即便是打折后跑个10MB/s上下也是轻轻松松的事。相比之下SPI模式在STM32上通常只能跑到2~4MB/s而且还要占用SPI外设很多时候SPI还要分给屏幕、FLASH等外设用就不够分了。这里要提醒一下SDIO虽然叫“SDIO”但它跟WiFi模块那种SDIO接口不是一回事。STM32上的SDIO外设主要用于访问SD卡、MMC卡和eMMC设备驱动的是存储类设备。部分是类似的概念但寄存器、协议细节还是有差别的别搞混了。1.2 DMA在SDIO读写里到底解决了什么SDIO外设本身自带一个FIFOCPU往这个FIFO写数据或者从FIFO读数据就能和SD卡完成数据交换。问题来了FIFO是很小的一般就32个字节。如果你直接操作寄存器轮询搬数据CPU就得不停地查FIFO状态、搬数据、再查、再搬。读一个512字节的块CPU被迫等几百上千次FIFO状态翻转这期间CPU什么都干不了。DMA的引入就是为了解决这个搬运问题。配置好DMA后SDIO和内存之间的数据搬运完全由DMA控制器接管CPU只需要在传输开始前设置好源地址、目的地址和数据长度传输结束后收到一个中断就行。整个过程中CPU处于释放状态可以做别的任务这就是DMA模式核心的价值降低CPU占用率同时提高数据吞吐的稳定性。在STM32上SDIO通常跟DMA2绑定的具体是哪个Stream要看型号和CubeMX自动生成的配置。以F407为例SDIO_TX走DMA2 Stream3SDIO_RX走DMA2 Stream6。F1系列略有不同F103的SDIO DMA通道是DMA2通道4和5。所以别跨平台硬套代码用CubeMX重新生成一下最保险。2. CubeMX配置阶段必须注意的细节2.1 SDIO外设参数怎么填才不出幺蛾子在CubeMX里选中SDIO外设后会出现几个关键参数时钟分频ClockDiv、数据宽度Data Width、传输模式Transfer Mode以及一个叫“SDIO Speed”的选项。很多新手上来就把时钟分频设成最小想跑最高速度结果就是卡初始化直接失败。原因是SD卡上电后的初始化阶段主机必须用低速时钟一般在400kHz左右和卡通信等CMD0、CMD2、CMD3、ACMD41这些初始化命令走完之后才能把时钟提上去跑高速。HAL库的处理逻辑是在HAL_SD_Init()内部会用低速时钟完成卡的初始化识别流程然后根据你配置的时钟参数切换工作时钟。问题在于CubeMX默认的ClockDiv并不总是合适。我踩过的坑是用默认配置去驱动一张较老的SDHC卡初始化时好时坏后来手动把分频调大、保证初始化阶段时钟在400kHz左右问题就消失了。具体计算方法是SDIO_CK SDIOCLK / (2 × ClockDiv)。对于F407SDIO时钟源是48MHz如果ClockDiv设为118则SDIO_CK约为200kHz这个值在低速阶段是安全的。初始化完后如果你想跑24MHz那么ClockDiv最少得设为248MHz/(2×2)12MHz注意这里有个下限约束不同芯片约束不太一样。数据宽度选1位还是4位如果没特殊原因选4位。4位模式是SD卡常见的位宽配置性能远好于1位CubeMX里默认也是4位。传输模式选DMA这是我们的目标。2.2 DMA通道配置和数据对齐的讲究启用SDIO后到DMA配置页去分配通道。CubeMX会自动把SDIO的TX和RX事件映射到对应的DMA请求上。这里你要注意几个参数的设置ModeNormal模式不是Circular。因为每次块传输都是一个独立的事务完成后要重新启动DMA。Data Width对于SDIO来说外设侧和数据侧都建议设为Word32位。因为SDIO的FIFO是32位宽的如果设成Byte模式效率会成倍下降而且容易触发FIFO错误。Increment Address内存侧地址递增外设侧固定。这是默认行为不需要改。Priority建议设High。SDIO优先级太低的话如果总线带宽被其他DMA通道占用可能出现FIFO上溢/下溢导致数据损坏。数据对齐是个容易被忽略的硬性问题SDIO的DMA传输要求内存buffer按32位对齐也就是说buffer的首地址必须是4的倍数。如果传入一个未对齐的地址轻则性能下降重则直接触发HardFault。我的习惯是在全局定义缓冲区时显式加上__attribute__((aligned(4)))或者直接用uint32_t数组当底层buffer。// 4字节对齐全局缓冲区用于SDIO DMA读写 uint8_t sd_dma_buffer[512] __attribute__((aligned(4)));2.3 中断优先级和回调机制SDIO的DMA传输完成后会产生两个层面的中断DMA传输完成中断和SDIO自身的传输完成中断。HAL库建议两个都开。你可能会想只开DMA中断不就行了吗HAL库内部的逻辑不完全是这样它的一些状态机判断既依赖DMA回调也依赖SDIO中断状态。实测下来只开DMA中断、关SDIO中断在某些HAL版本里会导致HAL_SD_GetCardState()一直返回忙碌状态。中断优先级的设置也有讲究。我建议DMA中断优先级高于一般业务中断但低于系统节拍中断如果用了RTOS。具体数值取决于你的项目核心原则是不要让其他高频中断把DMA的服务流程拖死。曾经遇到过一个奇怪的现象卡偶尔写失败查了很久发现是一个定时器中断太频繁SDIO的DMA回调被无限推迟导致FIFO溢出从此我把DMA中断优先级抬高后就没再犯过。3. HAL库代码从初始化到真正读写3.1 初始化顺序和HAL_SD_Init的隐藏问题CubeMX生成的代码会帮你调用MX_SDIO_SD_Init()和MX_DMA_Init()但HAL_SD_Init()并不会自动调用需要你手动在初始化阶段执行一次。调用HAL_SD_Init()之后强烈建议检查返回值即使返回值是HAL_OK也不能保证卡一定可用最好再调HAL_SD_GetCardState()确认状态是HAL_SD_CARD_TRANSFER。这里有一个HAL库的经典坑部分HAL版本比如1.25及之前的F4系列HAL库里HAL_SD_Init()内部虽然会调用SD_InitCard()但这个过程里有时序上的问题导致某些批次的卡第一次初始化会失败。社区里流传的解决办法有两个一是初始化前先对SDIO外设做一次软件复位并延时几十毫秒二是如果HAL_SD_Init()返回错误延时100ms再重试一次。实测下来“失败重试一次”的方案很有效适合常年随身带各种杂牌卡的项目。HAL_StatusTypeDef status HAL_SD_Init(hsd); if (status ! HAL_OK) { HAL_Delay(100); status HAL_SD_Init(hsd); } if (status ! HAL_OK) { Error_Handler(); }另外还需要做HAL_SD_ConfigDMA()调用把DMA句柄和SDIO句柄绑定。CubeMX生成的代码会通过HAL_SD_MspInit()把hsd里的hdma字段灌进去所以正常流程下这一步都是自动完成的。但如果你手动改过MspInit别忘了检查DMA句柄有没有正确挂上。3.2 块读写的完整代码实现DMA模式下的读一个块核心就是调用HAL_SD_ReadBlocks_DMA()然后等待传输完成。不等传输完成就继续操作是几乎所有问题的根源。uint8_t sd_dma_buffer[512] __attribute__((aligned(4))); volatile uint8_t sd_io_done 0; void HAL_SD_RxCpltCallback(SD_HandleTypeDef *hsd) { sd_io_done 1; } void HAL_SD_TxCpltCallback(SD_HandleTypeDef *hsd) { sd_io_done 1; } void HAL_SD_ErrorCallback(SD_HandleTypeDef *hsd) { sd_io_done 2; } // 读一个块块号从0开始 int sd_read_block(uint32_t block_addr, uint8_t *dst) { if (dst NULL) return -1; // 注意dst需要4字节对齐如果不是先拷贝到内部buffer sd_io_done 0; HAL_StatusTypeDef status HAL_SD_ReadBlocks_DMA(hsd, dst, block_addr, 1); if (status ! HAL_OK) return -1; uint32_t timeout HAL_GetTick() 2000; while (sd_io_done 0) { if (HAL_GetTick() timeout) { return -2; // 超时 } } if (sd_io_done 2) { return -3; // 错误 } return 0; // 成功 } // 写一个块 int sd_write_block(uint32_t block_addr, uint8_t *src) { if (src NULL) return -1; sd_io_done 0; HAL_StatusTypeDef status HAL_SD_WriteBlocks_DMA(hsd, src, block_addr, 1); if (status ! HAL_OK) return -1; uint32_t timeout HAL_GetTick() 2000; while (sd_io_done 0) { if (HAL_GetTick() timeout) { return -2; } } if (sd_io_done 2) { return -3; } return 0; }注意到我用了sd_io_done作为回调标志并且区分了完成和错误。这套代码已经把超时、状态检查全部封装进去了项目里可以直接拿来做底层的读写函数。如果你跑的是裸机这个方案足够了如果你用FreeRTOS可以把sd_io_done换成信号量在回调里osSemaphoreRelease等信号量的方式比忙等更优雅。3.3 配合FatFs实现文件系统只读写裸块其实不够用绝大多数项目里要存的是文件。把上述disk_read和disk_write接到FatFs的diskio.c里就能在SD卡上创建文件系统。这里有几个关键适配点disk_read和disk_write的参数里sector对应我们的块号count对应块数底层直接用HAL_SD_ReadBlocks_DMA配合循环就完成了。注意FatFs传入的buffer不一定是4字节对齐的所以在DMA模式下最好加一个中转buffer先把数据读进对齐的buffer再memcpy给FatFs。DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { // FatFs传入的buff可能未对齐用中转buffer处理 for (UINT i 0; i count; i) { uint8_t tmp[512] __attribute__((aligned(4))); int ret sd_read_block(sector i, tmp); if (ret ! 0) return RES_ERROR; memcpy(buff i * 512, tmp, 512); } return RES_OK; }写操作同理。虽然多了两次memcpy会损失一点性能但换来的是稳定性我认为完全值得。等你在DMA模式里因为buffer不对齐而莫名其妙地读错数据的时候就会明白这个中转buffer有多香了。4. 调试实录最常见的几个问题和排查对策4.1 卡初始化失败怎么查都查不到原因这是最多人问的问题也是我早期被折磨到怀疑人生的地方。卡初始化失败有几个典型原因我按排查顺序列一下第一供电问题。SD卡的工作电压是3.3V但很多STM32开发板上的SD卡槽是直接接3.3V的看起来没问题。然而像STM32F407这类芯片的SDIO引脚是5V容忍的如果卡槽那边的电压被拉到5VSD卡直接就罢工了。检查VDD是否稳定在3.3V最好用示波器看卡槽供电脚的波形看有没有上电瞬间的跌落。第二上电时序问题。有些卡要求VDD稳定后等待几毫秒才能接收命令。如果代码里上电后立刻进入初始化流程卡还没准备好当然会失败。我的习惯是在HAL_SD_Init()之前延时50ms以上。第三CMD线上的干扰。如果SDIO信号线走线过长又没有上拉电阻CMD信号可能不稳定。高速模式下尤其明显。我之前画的一块板子把SD卡槽放得很远信号线走了十几厘米结果就是高速模式下疯狂出错低速模式就正常。解决办法是缩短走线或者降低时钟。初始化失败后HAL库的错误信息其实很有限。我的排查建议是接一个逻辑分析仪直接抓CMD和CLK线上的波形看看初始化命令序列有没有完整发送。很多时候SD卡根本没有收到命令或者收到的命令残缺这时候就能一眼看出是硬件问题还是代码问题。4.2 写卡正常但读回来的数据全是0xFF这个现象很有意思写操作返回成功感觉一切正常但读出来全是0xFF。第一反应是写入的数据丢了但后来我发现真正的原因是写入命令完成后马上发读命令SD卡内部的FLASH编程还没完成读出来的自然不是刚写进去的数据。在SD卡协议里主机发出写命令后卡会进入编程状态此时卡不会响应用新的读命令。HAL库的HAL_SD_WriteBlocks_DMA()实际上把数据写入卡的缓冲区就算完了并不会等你把缓冲区数据真正编程进FLASH。所以写完之后必须轮询卡的状态直到卡回到transfer状态才能继续下一个操作。// 等待SD卡内部状态就绪 uint32_t timeout HAL_GetTick() 5000; while (HAL_SD_GetCardState(hsd) ! HAL_SD_CARD_TRANSFER) { if (HAL_GetTick() timeout) { return -1; } }这条经验特别重要。FatFs在写文件后也会通过disk_status来查询卡状态如果你在底层适配时省略了状态等待就会遇到数据丢失的幻觉问题。4.3 DMA传输过程中程序卡死在中断里卡死这个问题比较折磨人。表面现象是程序跑一会儿就卡住了在线调试发现卡在某个中断里出不来大部分卡在HAL_DMA_IRQHandler()。原因通常有两个。第一DMA中断标志没清干净。在HAL库的DMA处理流程里如果在中断回调还没执行完时又来了新的DMA中断可能导致标志位异常。解决方案是进入DMA中断服务函数后先清理标志位再处理回调逻辑。虽然HAL库已经处理过但某些版本还是会有残留问题我习惯在回调函数开头主动调用__HAL_DMA_CLEAR_FLAG()清掉所有标志。第二回调函数里执行了耗时过长的操作。比如我在HAL_SD_RxCpltCallback里直接调用printf输出调试信息结果因为打印速度太慢DMA FIFO溢出导致死锁。这不属于HAL库的bug是我自己给自己挖的坑。回调函数里只做“置标志位”这样轻量级的操作不要让任何耗时的东西出现在中断上下文里尤其是打印、延时、内存分配这些操作。5. 实测数据与稳定性验证5.1 轮询模式和DMA模式的性能对比我在一块F407开发板上用MicroSD卡Class 10SanDisk Ultra 16GB做了实测。测试方式是连续读取1000个512字节块512KB分两种模式跑记录总耗时和CPU占用情况。轮询模式下全程用HAL_SD_ReadBlocks()配合while循环等待FIFO状态整体读速度为1.8MB/s左右CPU占用率在测试期间几乎拉满。DMA模式下用HAL_SD_ReadBlocks_DMA()读速度可以到4.2MB/s左右CPU占用率降到20%以下大部分时间都在等ADC采样和按键扫描。如果你用的是F4、F7、H7这类带高速外设的芯片SDIODMA带来的收益会更大。F1系列的SDIO本身DMA带宽有限但依然比SPI轮询强很多。实测下来即使只做1位SDIO模式也比SPI速度快将近一倍。这里要再提一个点DMA模式下写入速度通常只有读取速度的一半甚至更低。因为SD卡内部写编程FLASH需要时间而这个时间跟主机接口是SPI还是SDIO没啥关系。Class 10的卡实测写入速度在3.5MB/s左右基本符合SD卡的标称。5.2 让系统长期稳定运行的几个习惯对于SD卡这种有时候会“抽风”的存储介质我总结了几条习惯能显著提高稳定性第一所有SD卡读写相关函数都加上超时机制。永远不要用无限while循环去等待某个标志SD卡偶发不响应是完全正常的没有超时机制程序就会挂死。超时时间根据操作类型设置块读给500ms块写给2000ms文件系统挂载给5000ms基本够用。第二定期重新检查卡状态。在长期运行的项目里SD卡读取过程中偶尔会跳出transfer状态变成error状态。一个鲁棒的做法是每次访问前检查HAL_SD_GetCardState()如果状态不是transfer就尝试重新初始化。第三注意DMA缓冲区的作用域。DMA传输期间CPU不能动那块buffer。如果你传入的是一个局部变量函数返回后buffer被回收DMA还在往里面写数据这就会产生一个非常隐蔽的悬垂指针数据被改写你根本发现不了。所以我强烈建议DMA用到的buffer要么是全局的要么是静态局部变量并且在传输完成之前不要复用这块内存。第四如果用的是带Cache的高性能型号比如H7必须处理Cache一致性问题。DMA写内存后CPU读到的可能还是Cache里的旧数据需要通过SCB_CleanDCache和SCB_InvalidateDCache在合适时机做Cache维护。F1/F4没有这个问题直接用就完了。第五文件系统的“安全弹出”当然是不存在的但你可以做一个简单的优雅停机逻辑在系统掉电前先同步文件f_sync再取消挂载f_mount(NULL, path, 0)然后延迟几十毫秒最后关SDIO时钟。这样可以把丢失数据或者损坏文件系统的概率降到最低。这套流程我在好几个项目里都在用确实能防止“最后一次写操作丢数据”的经典事故。最后再分享一个实战心得别迷信新卡、贵卡一定就好。有些廉价卡在极端温度或者电压波动下掉盘反而是旧款工业卡在板子里一直安安稳稳。如果做的是工业级产品除了在代码上做好异常处理选卡的时候也要花点心思。我就因为在消费级卡上吃过亏后来换成工业级SLC卡之后同一套代码几个月都不用重启一次。SDIODMA这套组合只要把初始化时序、DMA配置和回调处理几个关键点吃透剩下的就是熟练活。如果你在调试中遇到了新问题建议先抓波形再翻代码最后才是怀疑芯片。很多时候问题都不是芯片本身而是你对时序的理解还差着那么一点点。本文还有配套的精品资源点击获取

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

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

免费获取报价