资讯动态

STM32 HAL库 SDIO+DMA 读写SD卡 避坑指南

发布时间:2026/9/9 21:41:10 来源:尧图企业网站定制
简介面向STM32嵌入式开发者本套源码以HAL库实现SDIODMA方式读写SD卡采用1bit数据线模式并开启DMA传输与中断适合需要掌握存储外设驱动或进行SD卡数据记录的工程场景。压缩包共112个文件约1015KB其中73个h头文件与27个c源文件构成核心驱动和标准外设库ioc与uvprojx文件可分别用STM32CubeMX和Keil工程直接打开hex文件支持烧录验证另有bat批处理与txt说明便于环境整理和配置参考。SDIO关键参数如时钟捕获边沿、时钟分频因子、硬件流控等均有对应配置项便于理解寄存器级工作原理和移植调优。资源已有666人学习下载对希望通过完整工程实例快速上手STM32存储读写开发的读者有较高借鉴价值。 用STM32HAL库做SD卡数据记录仪那段时间我一开始图省事用的SPI方式日志一多、卡稍微差一点写着写着就卡在超时等待上。后来把整套逻辑切到SDIODMA模式问题一下子清爽了很多。这篇就把我踩过的一些坑和最后能稳定复现的做法整理出来给正在用STM32调SD卡、尤其是想把HAL库的SDIODMA彻底用明白的朋友参考。先提个醒这里说的SDIO是STM32片内那个SDIO外设不是市面上那些“SDIO接口的WiFi模块”。它既能接SD卡也能接MMC、SDIO设备我们日常最常用的场景就是接SD存储卡。搭配DMA之后数据搬运基本不用CPU管读多块、写多块都能连续跑这对F103、F407这些经典芯片来讲是一个非常成熟的方案。1. 为什么读写SD卡我最后选了SDIODMA而不是SPI或轮询1.1 SDIO通道的带宽优势不是靠提高主频换来的很多朋友一开始和我一样觉得读写SD卡嘛SPI也行。确实SPI协议简单CubeMX里两根线一拉就能跑但SD卡在SPI模式下是半双工、单线传输而且每次读写之前都要发一堆0xFF和命令握手实际吞吐很难上去。我的经验是F103的SPI最高跑到18MHz左右顺序读取SD卡也只能到1MB/s上下换成SDIO的4位模式之后带宽优势一下就拉开了。SDIO走的是专用命令/数据通道CMD线负责命令DAT0~DAT3四根数据线并行传数据同样是18MHz的时钟理论吞吐就是SPI模式的四倍。实际项目中我用SDIO 4bit读同一张卡稳定在4~6MB/s左右比SPI那种“能读但是慢”的体验舒服太多了。如果换成F4/F7这类主频更高的芯片SDIO时钟还能拉到更高差距更明显。顺手放一个我实测过的对比表格方便你估量工作方式总线宽度典型读速度CPU占用情况SPI模式单线0.8~1.5MB/s高需频繁发指令与等待SDIO轮询4bit2~4MB/s较高while等待浪费严重SDIO DMA4bit4~6MB/s以上低搬运和等待交给DMA1.2 DMA的真正价值把“等待卡响应”的时间交还CPU有人会问SDIO本身带宽高我理解但为什么非要DMA轮询不也能读到数据吗能但代价很大。SD卡读写不是连续高速的每个块之间都要等卡内部处理、等CRC校验、等响应轮询模式下CPU就卡在while里干等。实际项目里你可能同时要跑显示刷新、按键扫描、通信协议栈CPU一旦被SD卡读写拖住整个系统响应就会变差。DMA的作用就是让外设直接和内存打交道SDIO的FIFO有数据DMA自动搬走搬满一个缓冲区再触发中断。这样主循环只需要在提交DMA请求之后等一个标志位其他时间该干嘛干嘛。我做过一个日志记录设备用DMA模式后主循环的响应时间从十几毫秒降到一两毫秒改动只花了半天。1.3 开始之前先确认两件事芯片有没有SDIO外设、卡槽上拉这一条真的值得单独说。我见过不止一个朋友拿着STM32F103C8T6在CubeMX里找SDIO找半天找不到原因很直接F103C8T6这款芯片没有SDIO外设。STM32F1系列里只有高密度型号才带SDIO比如F103RCT6、F103ZET6这些C8T6、R8T6这类中小容量型号是没有的。如果你手里只有C8T6又想用4bit SDIO建议直接换F103RCT6或者F407系列。选型不对后面再折腾都是白费。第二个容易忽略的是卡槽上拉电阻。SDIO的CMD和DAT线规范上是需要上拉到VDD的很多开发板上已经集成但自己画板子时经常漏掉。尤其是CMDT线没有上拉卡初始化时响应根本送不回来你可能查半天代码也找不到原因。2. CubeMX里SDIO和DMA的配置顺序以及每个参数为什么这样设2.1 引脚和时钟出错率最高的两个地方在CubeMX里把SDIO勾上之后引脚映射一般比较傻瓜D0~D3、CK、CMD这六根线别接错就行。注意一下复用功能比如F103上SDIO引脚和部分调试引脚、片上外设共用如果你同时用了不该用的功能编译能过运行时就是没反应。时钟配置我建议先看SDIO时钟来源。F103的SDIO时钟源是PCLK2也就是APB2总线时钟通常配置成72MHz。F4系列内部是SDMMCCLK路径不一样但CubeMX都会自动算好。关键点是后面那个分频系数直接决定SDIO_CK是多少很多人就是栽在这。2.2 SDIO_CK分频系数怎么算初始化阶段宁可慢SDIO_CK的计算方式很固定就是SDIO_CK SDIOCLK / (2 * CLKDIV)。以F103为例SDIOCLK72MHz设CLKDIV2SDIO_CK就是72/(2*2)18MHz设CLKDIV1出来就是36MHz。问题在于普通SD卡的工作频率上限默认是25MHz高速卡才支持50MHz。如果你一上来就把CLKDIV设成1用36MHz去跟卡通信很多卡在初始化阶段能过一进数据阶段就开始乱码、超时或者干脆认不到卡。我的做法是初始化阶段CLKDIV设成3或4也就是9MHz左右先把卡认稳等HAL_SD_Init跑完再根据卡的CSD信息判断是否支持高速支持再把CLKDIV适当调小。CubeMX里如果图形界面没有直接暴露这个分频值不要急HAL_SD_Init里会自动处理但你可以通过CubeMX里的ClockDivider参数去控制一般取值0~127默认0风险最大。2.3 DMA通道、数据宽度和中断优先级不能随意CubeMX里添加DMA请求时会看到SDIO_RX和SDIO_TX两个方向对应读卡和写卡两条DMA都要加。如果只加RX不加TX读卡一切正常写卡就卡死在HAL_SD_WriteBlocks_DMA里。DMA数据宽度建议直接设成Word也就是32位。因为SDIO的数据寄存器是32位FIFODMA这边用Word宽度一次搬一个完整的字效率最高。如果设成Byte也能跑但DMA请求次数会多好几倍中断频率上去了稳定性反而不如Word。中断优先级方面DMA传输完成的优先级可以稍微设高一点但不建议高过系统滴答定时器。不然SDIO在高速传输时中断被无关的高优先级任务频繁抢占可能出现CRC失败或数据错位。我一般在CubeMX里给DMA中断设一个中等优先级回调里只做标志位置位这种轻量操作不跑重活。3. HAL库背后发生了什么从卡上电到读到第一个扇区3.1 卡识别阶段那串CMD指令要依次通过很多人都知道调用HAL_SD_Init但不清楚它内部做了什么导致出问题时不知道往哪个方向查。实际上任何SD卡接入后主机都要走一遍完整的识别流程上电后先延时几毫秒发送CMD0让卡进入空闲态紧接着发CMD8检查卡是否支持SD Specification 2.0响应里带0x1AA才算正常然后反复发ACMD55ACMD41通过卡返回的OCR里的busy位判断卡是否上电完成这一步完成后再发CMD2读CID、CMD3获取RCA最后CMD7选中卡。这些指令全部通过卡才算处于传输状态才能开始读写扇区。HAL_SD_Init把整个过程封装好了但你要知道如果卡一直卡在ACMD41上多半是供电电压不对或者上拉电阻缺失卡能识别但读不出容量就要检查命令响应和CRC是否正常。3.2 数据阶段的CMD17/18/24/25是如何被封装到HAL里的进入传输状态之后任何读写操作都离不开四类指令CMD17读单块、CMD18读多块、CMD24写单块、CMD25写多块。HAL库封装的HAL_SD_ReadBlocks_DMA和HAL_SD_WriteBlocks_DMA内部逻辑就是把这些命令发出去然后启动DMA搬运。F103这种芯片还好比较简单新版HAL里可能会多一个Timeout参数写代码前先看一下你当前库头文件里的函数签名。比如/* 具体是否有最后一个Timeout参数以当前HAL库头文件为准 */ HAL_StatusTypeDef status HAL_SD_ReadBlocks_DMA(hsd, (uint8_t*)buff, sector, count, TIMEOUT); if (status ! HAL_OK) { return RES_ERROR; }3.3 为什么判断传输完成不能只看DMA中断标志这一点新手特别容易踩。DMA传输完成中断触发了不代表SD卡数据阶段已经绝对结束因为SDIO外设那边还有自己的状态机需要等到数据结束标志起来整个事务才算完。HAL库的做法是通过SDIO中断和DMA回调共同驱动一个状态所以你在业务代码里判断时最好用下面这种模式if (HAL_SD_ReadBlocks_DMA(hsd, (uint8_t*)buff, sector, count, TIMEOUT) ! HAL_OK) { return RES_ERROR; } while (HAL_SD_GetState(hsd) ! HAL_SD_STATE_READY) { /* 等待SDIO状态机回到READY */ }等于说DMA完成只是数据搬完了SDIO状态机回到READY才是事务正式结束。如果只等DMA回调标志下一个读写请求可能发太快SDIO还没从上一个操作里缓过来就会偶发失败。4. FatFS接入diskio.c必须改对的几个函数4.1 disk_initialize和disk_status先过一遍读裸扇区没问题之后大多数人会直接挂FatFS这里最关键的移植文件就是diskio.c。建议先看disk_initialize和disk_status这两个函数。disk_initialize里要调用HAL_SD_Init完成卡初始化同时调用HAL_SD_ConfigWideBusOperation(SDIO_BUS_WIDE_4B)切换到4位总线。注意如果你只调HAL_SD_Init却不切换总线宽度卡会一直跑在默认的1bit模式速度完全发挥不出来而且你自己可能还不知道因为功能上能正常读写。disk_status函数则是告诉FatFS当前卡还在不在。常见写法是返回RES_OK或者STA_NOINIT。我习惯在卡检测引脚上做一次读取卡拔掉了就返回STA_NOINIT这样FatFS在读写前能及时感知异常而不是等到超时报错。4.2 disk_read/disk_write的缓冲区对齐问题以及中转方案disk_read和disk_write是读写核心但这里藏着一个非常隐蔽的坑DMA要求缓冲区地址按4字节对齐而FatFS层面的用户缓冲区不一定对齐。FatFS在ffconf.h里提供了FF_DMA_ALIGNMENT之类的配置但老版本FatFS不一定有而且就算有底层传进来的缓冲区也可能因为各种中间层原因不符合要求。最稳妥的办法是在disk_read/disk_write内部准备一个全局、静态、对齐的DMA缓冲区先把数据从SD卡DMA到中转缓冲再memcpy到FatFS传进来的buff。__align(4) static uint8_t sd_dma_buf[512]; /* 如果编译器是GCC可以写成 __attribute__((aligned(4))) */ DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { if (HAL_SD_ReadBlocks_DMA(hsd, sd_dma_buf, sector, count, TIMEOUT) ! HAL_OK) { return RES_ERROR; } while (HAL_SD_GetState(hsd) ! HAL_SD_STATE_READY) {} memcpy(buff, sd_dma_buf, count * 512); return RES_OK; }这种方法多了一次内存拷贝损失一点性能但换来的是稳定性和对各种编译器的兼容性。F103这类芯片跑文件系统瓶颈本来就在SD卡本身多一次memcpy几乎感觉不到差别。4.3 disk_ioctl不要只写个return RES_OKdisk_ioctl是FatFS用于获取设备信息的接口很多人图省事直接return RES_OK结果后面f_mkfs、f_stat这些功能全部异常。至少要把GET_SECTOR_COUNT、GET_BLOCK_SIZE、CTRL_SYNC这三个命令实现。GET_SECTOR_COUNT返回卡的扇区总数可以直接取hsd.SdCard.BlockNbrGET_BLOCK_SIZE返回擦除块大小一般填1就行CTRL_SYNC需要等待SDIO状态回到READY确保数据真正落盘。把这三个命令实现之后格式化SD卡、查询文件信息才不会出错。另外提一句如果f_mount返回FR_NO_FILESYSTEM别急着怪FatFS先用逻辑分析仪看disk_read读出来的扇区数据是不是全0xFF。很多卡之前被树莓派、相机格式化过分区表FatFS默认配置不一定能识别MBR里的FAT分区我一般会把SD卡重新整卡格式化成FAT32再上机能省掉一堆玄学问题。5. 我实际踩过的坑从现象到根因的排查链路5.1 CMD0都过不去先给CMD和DAT3上拉有次我自己画板SDIO初始化死在卡识别阶段HAL_SD_Init返回HAL_ERROR。用示波器看CMD线发现命令发出去了但卡完全没有回应。查了半天最终确认是卡槽的CMDT线缺少上拉电阻。SD卡规范要求CMD和DAT线都要有上拉如果你用开发板板上一般画好了自己画板时这个电阻特别容易被忽略。给CMD线和DAT3各加一个10k到VDD的上拉之后问题立刻解决。这个经验让我养成了习惯SDIO调不通第一件事先用万用表量引脚电平而不是反复改代码。5.2 读回数据全是0xFF分频和总线宽度要一起查另一种非常典型的故障是卡能识别容量也能读出来但读扇区数据全是0xFF或者读出来偶尔正确偶尔乱码。这种时候我基本锁定两个原因。第一是SDIO_CK分频过高主控时钟超过25MHz和数据线上的信号完整性问题叠加导致CRC错误。解决办法很简单把初始化分频系数调大降到18MHz甚至9MHz再试。第二是总线宽度没切到4位模式或者切换后DMA数据宽度设置不匹配。你可以把HAL_SD_ConfigWideBusOperation的返回值打印出来能走通但数据乱码十有八九是分频太高。5.3 加了DMA反而死机回调里别做重活还有一次SDIO轮询正常加上DMA之后系统就频繁死机。后来发现是我在DMA传输完成回调里直接调用了FatFS函数比如f_write。中断上下文里做文件系统这种重量级操作不仅可能重入还会因为中断优先级问题导致栈溢出或者状态错乱。正确的做法是回调里只置一个全局标志volatile uint8_t sd_dma_done 0; void HAL_SD_RxCpltCallback(SD_HandleTypeDef *hsd) { sd_dma_done 1; }主循环检测到标志位后再继续后续逻辑。这个习惯帮我避免了很多“怪毛病”不只是SD卡串口DMA、ADC DMA我都统一用这套思路。5.4 F7/H7这类带Cache芯片的额外一步Clean和Invalidate如果你用的是F7、H7这类带D-Cache的内核DMA模式还会遇到一个更隐蔽的坑数据明明被DMA搬进内存了CPU读到却是旧值或者CPU写进去的数据DMA搬出去时根本没搬对。这是因为Cache缓存了内存的数据DMA直接操作内存时Cache和RAM内容不一致了。解决办法是读写前做好缓存维护读卡完成后调用SCB_InvalidateDCache_by_Addr写卡前调用SCB_CleanDCache_by_Addr并且缓冲区尽量按32字节对齐。__attribute__((aligned(32))) uint8_t cache_safe_buf[512]; /* 写卡前确保CPU写过的数据落回RAM */ SCB_CleanDCache_by_Addr((uint32_t *)cache_safe_buf, sizeof(cache_safe_buf)); HAL_SD_WriteBlocks_DMA(hsd, cache_safe_buf, sector, count, TIMEOUT); /* 读卡后让CPU从RAM重新加载数据 */ HAL_SD_ReadBlocks_DMA(hsd, cache_safe_buf, sector, count, TIMEOUT); SCB_InvalidateDCache_by_Addr((uint32_t *)cache_safe_buf, sizeof(cache_safe_buf));最后分享一个我自己的小习惯每次换一张新卡我都会先用工具把整卡格成FAT32再上机做一次顺序写、掉电重启、再读回校验的回归。SDIO的串扰和卡批次差异很难在原理上完全规避做一次全流程校验比出一堆现象再去查根因节省时间得多。如果你也在调SDIODMA卡住的时候不妨回头检查上拉、分频、总线宽度和DMA缓冲区多半就是这几个位置的事。本文还有配套的精品资源点击获取

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

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

免费获取报价