STM32 SDIO+DMA驱动SD卡:CubeMX配置与FatFS移植实战
2026/9/9 21:21:33 网站建设 项目流程

简介:面向STM32开发者,这份代码包围绕HAL库给出了SDIO+DMA中断方式读写SD卡的完整工程,针对1bit模式下的时钟分频、数据捕获边沿、硬件流控等关键参数给出了具体配置,适合需要移植或扩展SD卡读写的嵌入式学习者。压缩包内共112个文件,以C源码和H头文件为主体,涵盖stm32f4xx系列HAL库的SD、DMA、RCC、TIM等驱动模块,同时附有CubeMX的.ioc配置、Keil工程文件以及清理脚本,整体仅1015KB,轻量而完整。目前已有666人学习,可作为SDIO底层驱动、FATFS上层接入或DMA中断流程理解的实际参考。通过学习该工程,可直接查看SDIO初始化、DMA请求与中断回调的配合方式,再结合描述中列出的时钟分频旁路、硬件流控等选项含义,能有效减少自行调参的时间。

1. 项目概述与方案选型:为什么非要用SDIO+DMA

做STM32项目凡是涉及到数据记录、固件升级、音频播放、GUI图片资源加载的,基本都逃不过SD卡。很多朋友一开始图省事,直接用SPI接口读写SD卡,毕竟初始化简单、引脚少,但一旦数据量上来,SPI那点吞吐量立刻成瓶颈。这次项目用的是STM32内置的SDIO外设,配合DMA搬运数据,跑下来读取速度比SPI快了一个数量级,实测连续读速度能到十几MB/s(取决于卡和时钟配置),写速度也远不是SPI能比的。

这个方案的核心是:SDIO负责跟SD卡打交道,DMA负责把数据从SDIO外设的数据寄存器搬到内存(或者反向),CPU全程不用干预字节搬运。对于需要长时间记录传感器数据、或者要快速加载外部Flash里的资源文件这种场景,这种组合能省下大把CPU时间,让主循环专门去跑业务逻辑。

这篇文章适合已经在用HAL库、对STM32CubeMX有基本了解,但第一次在SDIO上踩坑的人。我会把从CubeMX配置到FatFS挂载的完整链路讲一遍,重点放在DMA配置和实际调试中容易翻车的地方,这些坑基本是官方例程不会明说的。

2. 从SPI转向SDIO:硬件连接与DMA的介入方式

2.1 SDIO的引脚分配和硬件注意事项

SDIO不是所有引脚都能随便映射的,不同芯片可用的SDIO引脚是固定的。以ST官方常见的F103/F407/F429系列为例,SDIO通常使用PC8-PC12这5个引脚,分别对应D0、D1、D2、D3、CLK,另外还需要一个CMD引脚,一般是PD2。如果你的板子把SDIO映射到了其他引脚,那就得确认芯片型号是否支持,别到时候编译过了板子不跑。

硬件连接上有个非常容易被忽视的点:SDIO引脚的推挽能力和上拉电阻。D0-D3、CMD这些信号线必须接上拉电阻,一般10k左右,CLK不需要上拉。很多开发板自带的SD卡槽已经把上拉电阻画好了,但如果你是自己画板子或者用杜邦线飞线,一定要补上,否则初始化时卡经常识别不到,或者识别到了读写一段时间就掉卡。

还有一个细节是供电。SD卡工作电压标准是2.7-3.6V,STM32的IO也是3.3V,所以直接供3.3V没问题。但千万别用5V给SD卡供电,尤其是TF卡转SD卡的卡套,有些廉价卡套没有电平转换,插上去直接烧卡。如果板子上是5V供电,必须经过LDO降到3.3V。

2.2 为什么坚持用DMA而不是轮询或中断

很多HAL库的SD卡例程用的是阻塞式读写,也就是CPU循环等待SDIO状态寄存器,一个字节一个字节地搬。这种方式代码简单,但有两个致命问题:第一,搬运过程中CPU被完全占死,哪怕你只是读4KB数据,也得几千个时钟周期耗在等待上;第二,SDIO的FIFO深度有限,如果CPU响应不及时,很容易发生FIFO上溢或下溢,直接导致数据错误。

中断方式会好一点,每个数据块传输完成触发一次中断,然后在中断里继续搬运,但每传输一块都要进一次中断,频繁的上下文切换同样会吃掉不少CPU周期,而且多块传输时一旦中断响应不及时,同样可能丢数据。

DMA的优势在于,它是在外设和内存之间直接建立一条搬运通道,传输多少字节、什么时候结束,都是DMA控制器自己管。CPU只需要在发起传输时设置好源地址、目的地址、传输长度,然后就可以去睡大觉或者干别的事,等DMA传输完成中断过来再去处理结果。特别是在SDIO多块读写时,DMA能连续地把整块数据搬完,中间不需要CPU参与,速度和稳定性都远优于前两种方式。

3. CubeMX配置要点:初始化顺序比想象中更重要

3.1 SDIO外设参数设置

在STM32CubeMX中,选择SDIO后需要配置几个关键参数。首先是时钟分频因子,这决定了SDIO_CK的频率。SD卡协议规定,初始化阶段时钟不能超过400kHz,初始化完成后可以切换到高速模式,最高能到25MHz(有的卡支持50MHz)。CubeMX里的SDIO时钟源一般是外设时钟(如PCLK2 72MHz),通过分频得到实际时钟。

我习惯在初始化阶段设较大分频值(比如72/128≈0.5MHz),等卡初始化完成后再通过HAL_SD_ConfigSpeed之类的接口切换到高速模式,或者直接在CubeMX里把分频设成能跑通的值。注意,如果你的卡比较老,强行上25MHz可能会出现读返回CRC错误,这时适当加大分频系数降速是首选方案。

另一个关键参数是数据总线宽度。CubeMX里可以选1位或4位,4位模式下D0-D3全部参与数据传输,速度能翻4倍。F103系列SDIO支持4位模式,建议直接选4 bit。但如果硬件上只连了D0,那就只能用1位模式。

3.2 DMA请求的映射与优先级设置

SDIO有独立的DMA请求线,在CubeMX中切换到DMA设置标签页,添加SDIO的RX和TX两个请求通道。这里要注意,STM32的DMA控制器分为DMA1和DMA2,不同外设的请求映射是固定的。SDIO一般挂在DMA2上,选择DMA2 Channel 4(不同型号可能不同,要查手册或参考CubeMX自动分配)。

DMA方向要选对:RX方向是外设到内存(PeripheralToMemory),TX方向是内存到外设(MemoryToPeripheral)。外设地址SDIO的数据寄存器地址不需要我们手填,HAL库在初始化时会自动处理,我们只需要在配置界面确认模式和优先级。

优先级我习惯设为High。因为SD卡的读写属于实时性要求相对高的操作,如果DMA优先级不够,在有多个DMA通道同时工作时,SD卡的传输可能会被其他DMA抢占,导致FIFO数据溢出。所以优先给SDIO的DMA通道分配高优先级,能省掉后面很多诡异的调试时间。

DMA模式必须选择Circular吗?这里要重点说一下。对于SDIO多块读写,我们通常不会一次读一整张卡,而是按块(默认512B)读取,因此DMA传输是单次模式,不是循环模式。用循环模式的话,DMA会不停地把外设数据搬到同一块内存,造成覆盖,数据完全错乱。只有在某些连续流式采集场景下,循环模式才有意义,而SD卡读写用Normal模式足够。

3.3 时钟树与SDIO时钟源

打开CubeMX的Clock Configuration页面,能看到SDIO的时钟源来自哪个总线。F103系列中SDIO挂载在APB2总线上,所以PCLK2的时钟决定了SDIO外设时钟。如果APB2时钟是72MHz,SDIO的时钟分频就在这个基础上做。有些朋友在配置时钟时忽略了SDIO的时钟树映射,导致实际SDIO_CK频率跟预期不符,卡初始化老是超时。

务必打开RCC的时钟树确认SDIO时钟源已经启用,并且没有超过芯片允许的最大频率。调试时可以通过示波器或者逻辑分析仪实测CLK引脚,确认引脚频率在目标范围内。这个细节看起来不值一提,但它能直接解释“为什么卡偶尔能初始化、偶尔不能”的玄学问题。

4. 移植文件系统之前的底层读写验证

4.1 初始化与读寄存器测试

不管最终要不要用文件系统,先把裸机SDIO读写跑通是第一步。HAL库提供了一套完整的SD卡驱动接口,初始化函数就是HAL_SD_Init,内部会自动完成卡识别、电压匹配、初始化序列。

我在验证底层时最喜欢干的一件事是:先调用HAL_SD_GetCardStatusHAL_SD_GetCardInfo,把卡的类型、块数、块大小打出来。如果这两个函数返回的都是正常值,说明SD卡的初始化已经成功,CMD线与DATA线基本没问题。

下一步是直接读取卡的CSD寄存器,看看里面的厂商信息、容量字段是否跟卡的标称一致。很多时候初始化看起来通过了,但读CSD返回的数据是乱的,比如容量显示为0或者超过合理范围,这种通常是数据线时序问题,优先检查时钟频率和上拉电阻。

4.2 单块读写验证:确认DMA通路正常

初始化通过后,不要急着挂文件系统,先做单块读写验证。往0号块写入一串特殊数据,再读回来比对,如果一致,说明DMA通路和SDIO数据通路都OK。

写单个块用HAL_SD_WriteBlocks,这个函数有两个版本,阻塞式和中断式,还有基于DMA的版本。在HAL库里,基于DMA的接口是HAL_SD_WriteBlocks_DMA。调用时传入块地址、数据缓冲区、块数量,以及一个传输完成回调函数。

注意DMA传输是异步的,发起后函数立即返回,此时绝对不能马上操作缓冲区,必须等传输完成回调触发后,缓冲区中的数据才是安全可用的。如果回调里什么都没做,而主循环立刻去读缓冲区内容,大概率读到的是旧数据或者半新半旧的数据。这个“缓冲区生命周期”问题,是DMA开发里最常见的坑。

单块验证代码如下:

uint8_t tx_buf[512] = {0x00, 0x11, 0x22, 0x33}; uint8_t rx_buf[512] = {0}; // 写单块 HAL_SD_WriteBlocks_DMA(&hsd, tx_buf, 0, 1); // 等待传输完成(用信号量或标志位) while (!write_done_flag); write_done_flag = 0; // 读单块 HAL_SD_ReadBlocks_DMA(&hsd, rx_buf, 0, 1); while (!read_done_flag); read_done_flag = 0; // 比对 if (memcmp(tx_buf, rx_buf, 512) == 0) { // OK } else { // Error }

回调函数里不需要做太多,只需把标志位置位。如果你用的是带RTOS的环境,可以在回调里释放信号量,让等待读写的任务继续跑。

4.3 多块读写:连续DMA传输的字节数边界

单块OK后,接着测多块。SD卡支持多块连续读写,只要地址连续就行。HAL_SD_ReadBlocks_DMA(&hsd, buf, start_block, block_cnt)里的block_cnt可以大于1,DMA会按照配置的传输长度自动搬运block_cnt * 512字节。

这里有个容易忽略的细节:DMA传输长度用的是字节数,不是块数。比如要读4块,那DMA会去搬2048字节。如果拿块数去配置DMA长度,DMA只会搬4个字节,后面的数据全是乱的。HAL库内部会根据block数去设置DMA的传输长度,但保险起见,自己写底层时一定要明确这个换算关系。

多块读写测试通过后,才能说明SDIO+DMA这条链路是稳的。如果不稳,先不要怀疑文件系统,回头查DMA配置、SDIO时钟、卡的质量,按这个优先级来。

5. 接入FatFS:让SD卡变成真正的“U盘”

5.1 FatFS文件系统层配置

底层读写没问题,下一步就是挂文件系统。STM32CubeMX支持直接添加FatFS中间件,它会把底层接口文件和HAL库对接好,省去很多移植工作。

在Middleware and Software Components里勾选FatFS,然后在配置页面选择SD Card作为底层驱动。CubeMX会自动生成fatfs_platform.cuser_diskio.c这几个文件,其中user_diskio.c里要实现5个函数:USER_initializeUSER_statusUSER_readUSER_writeUSER_ioctl

默认生成的代码里,读写函数一般都是对应到HAL_SD_ReadBlocks的阻塞式版本。既然我们已经用了DMA,建议把读写函数改成调用HAL_SD_ReadBlocks_DMA,然后在等待标志位。这样文件系统的读写也能享受DMA带来的CPU解放优势。

具体实现思路是在USER_read中增加一个超时等待:

DRESULT USER_read( BYTE pdrv, /* Physical drive nmuber to identify the drive */ BYTE *buff, /* Data buffer to store read data */ LBA_t sector, /* Start sector in LBA */ UINT count /* Number of sectors to read */ ) { uint32_t timeout = HAL_GetTick() + 1000; read_done_flag = 0; if (HAL_SD_ReadBlocks_DMA(&hsd, buff, sector, count) != HAL_OK) { return RES_ERROR; } while (!read_done_flag) { if (HAL_GetTick() > timeout) { return RES_ERROR; } } return RES_OK; }

注意这里的buff必须满足DMA的对齐要求。一般建议使用32字节对齐的缓冲区,或者在CubeMX配置里开启内存保护单元(如果芯片支持MPU),避免DMA写到了Cache里但CPU读的是主存这种缓存一致性问题,尤其是带Cache的M7内核芯片。如果是M3/M4,没有这个烦恼,但缓冲区越大,越要留意其地址是否在DMA可访问的RAM区域内。

5.2 挂载与文件操作实测

文件系统配置好之后,就可以执行f_mount挂载,然后f_openf_writef_read了。我最终的测试流程是:往SD卡里写一个10MB的随机数据文件,然后再读出来做CRC校验,连续测试5遍。这一步能同时测试文件系统层、SDIO层、DMA层和卡的稳定性,通过后基本可以放心量产了。

实际操作中还发现一个有意思的现象:SD卡直接格式化时如果簇大小选得太大,FAT表会比较小,写小文件效率反而低。如果项目里大量写4KB~16KB的小文件,建议格式化时用默认簇大小或者手动选小一点的簇,这个跟STM32无关,但真的很影响实际体验。

5.3 只读场景与掉电保护

如果你的项目只是从SD卡读取资源文件,不涉及写入,建议把FatFS配置成只读模式,能显著减少代码体积和出bug的概率。在ffconf.h里把FF_FS_READONLY设为1,然后底层写函数直接返回RES_OK,省掉很多麻烦。

如果涉及写入,就必须考虑掉电保护。SD卡在写FAT表和写数据块之间存在时间窗口,如果此时突然断电,极容易造成文件系统损坏。低成本的做法是写完一个完整文件后立即调用f_sync刷新缓存,而不是等到f_close才关闭。高频写日志的场景更要注意,可以在每次写几行后就f_sync一次,牺牲一点速度换取数据安全。

6. 常见问题与排查技巧实录

6.1 DMA传输完成但数据全是0xFF或0x00

先说数据全是0xFF的情况。这通常是SDIO时钟没跑起来,或者卡根本没有进入数据传输状态。先用逻辑分析仪看CLK有没有波形,如果没有,检查CubeMX里SDIO的时钟使能位是不是被优化掉了,或者分频配置不对。

数据全是0x00的情况很多是DMA方向配置反了。读操作时DMA方向应该是外设到内存,如果在CubeMX里误配成内存到外设,那么DMA会去读内存地址,搬进来的自然全是0。检查DMA配置里的方向位就能发现。

还有一种是读取缓冲区用的是局部变量且没有保持到DMA完成。DMA传输是异步的,函数返回后局部变量就失效了,但DMA还在往那个地址搬数据,轻则数据错乱,重则硬件错误。解决办法是使用静态数组或者全局数组,并通过信号量或标志位同步等待。

6.2 卡初始化失败:HAL_SD_Init返回错误

初始化失败的原因里,硬件接触不良占了很大比例。SD卡槽的弹片氧化、TF卡没插到底、延长线太长,都会导致CMD或DATA信号不稳定。先换一张卡、换一根短线试试,不要一上来就怀疑代码。

除了硬件,初始化时序也很关键。卡上电后需要一段时间稳定,如果代码在卡上电后立刻执行HAL_SD_Init,有可能因为卡还没准备好而失败。在初始化前加一个延时,至少10ms,有些老卡甚至需要100ms。我的做法是在HAL_SD_Init之前调用HAL_Delay(100),实测对某些“挑卡”的板子有奇效。

还有一个常见坑:如果你的CubeMX里同时开启了SDIO的DMA和SD卡的轮询检测,两个功能会打架,导致初始化时DMA还没准备好,卡已经进错误状态。检查DMA初始化代码是否在SDIO初始化之前完成,实在不行把DMA初始化的优先级调到RCC初始化之后、SDIO初始化之前。

6.3 读写超时或CRC错误

出现CRC错误,首先要想到的是时钟太快。降低SDIO_CK分频系数,比如从25MHz降到12.5MHz,再测一次,如果好了,那就是卡或走线的信号完整性不行。很多高速模式下的CRC错误都是因为PCB走线过长、没有包地或者上拉阻值不对,软件降速是解决问题的最快手段。

如果降速仍然报错,检查DMA是否配置为循环模式,以及缓冲区是8位还是32位访问。DMA搬运数据时,外设数据宽度是32位(SDIO的数据寄存器是32位的),内存侧如果设成8位,DMA会拆成4次搬运,性能会下降但一般不会报错。更需要注意传输长度是否与数据宽度匹配,如果不匹配,DMA可能在搬运到一半时提前结束,后续数据就丢了一部分,对应到文件系统层就表现为某个文件的大小不对。

6.4 文件系统挂载成功但打开文件失败

这种情况大多不是文件系统本身的问题,而是底层读写函数没有正确返回块数量。FatFS的USER_read的返回值必须是RES_OK,如果底层HAL调用返回了HAL_SD_ERROR_TIMEOUT但你没有做转换,FatFS会认为读取失败,导致f_open找不到文件。

另外,如果你的卡之前被格式化成了exFAT,而FatFS配置里只启用了FATFS(FAT12/16/32),挂载会失败。要么在电脑上重新格式化成FAT32,要么在ffconf.h里打开FF_FS_EXFAT,不过那样会增大代码体积,一般嵌入式场景FAT32完全够用,没必要开exFAT。

7. 项目扩展:向更高性能或RTOS环境迁移

这一套SDIO+DMA方案并不是只能用在F103小容量芯片上。如果你后续换到F407、H743或者G4系列,CubeMX的配置方法几乎一致,只是DMA通道号和时钟树映射不同而已,核心思路完全能平移到新平台。

如果项目里接了RTOS,比如FreeRTOS或者RT-Thread,推荐用信号量配合DMA传输完成回调,这样读写SD卡的任务在等待期间会主动让出CPU,其他低优先级任务能继续运行,系统的实时性会好很多。我在FreeRTOS上实现的代码是,在HAL_SD_RxCpltCallback里调用osSemaphoreRelease,在任务里osSemaphoreAcquire等待,效果比全程阻塞好得多。

还有一个扩展方向是SDIO Wi-Fi模块,像ESP32-S3这类模组也支持SDIO接口,如果你已经熟悉了SDIO+DMA这套流程,以后调试Wi-Fi模块的SDIO通信会顺畅很多,因为底层DMA搬运逻辑是一模一样的。不过那是另一个话题,本文就先到这——先把SD卡的读写跑稳,比什么都实在。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询