
简介面向STM32开发者这份代码包围绕HAL库给出了SDIODMA中断方式读写SD卡的完整工程针对1bit模式下的时钟分频、数据捕获边沿、硬件流控等关键参数给出了具体配置适合需要移植或扩展SD卡读写的嵌入式学习者。压缩包内共112个文件以C源码和H头文件为主体涵盖stm32f4xx系列HAL库的SD、DMA、RCC、TIM等驱动模块同时附有CubeMX的.ioc配置、Keil工程文件以及清理脚本整体仅1015KB轻量而完整。目前已有666人学习可作为SDIO底层驱动、FATFS上层接入或DMA中断流程理解的实际参考。通过学习该工程可直接查看SDIO初始化、DMA请求与中断回调的配合方式再结合描述中列出的时钟分频旁路、硬件流控等选项含义能有效减少自行调参的时间。1. 项目概述与方案选型为什么非要用SDIODMA做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.6VSTM32的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方向是外设到内存PeripheralToMemoryTX方向是内存到外设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时钟是72MHzSDIO的时钟分频就在这个基础上做。有些朋友在配置时钟时忽略了SDIO的时钟树映射导致实际SDIO_CK频率跟预期不符卡初始化老是超时。务必打开RCC的时钟树确认SDIO时钟源已经启用并且没有超过芯片允许的最大频率。调试时可以通过示波器或者逻辑分析仪实测CLK引脚确认引脚频率在目标范围内。这个细节看起来不值一提但它能直接解释“为什么卡偶尔能初始化、偶尔不能”的玄学问题。4. 移植文件系统之前的底层读写验证4.1 初始化与读寄存器测试不管最终要不要用文件系统先把裸机SDIO读写跑通是第一步。HAL库提供了一套完整的SD卡驱动接口初始化函数就是HAL_SD_Init内部会自动完成卡识别、电压匹配、初始化序列。我在验证底层时最喜欢干的一件事是先调用HAL_SD_GetCardStatus和HAL_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可以大于1DMA会按照配置的传输长度自动搬运block_cnt * 512字节。这里有个容易忽略的细节DMA传输长度用的是字节数不是块数。比如要读4块那DMA会去搬2048字节。如果拿块数去配置DMA长度DMA只会搬4个字节后面的数据全是乱的。HAL库内部会根据block数去设置DMA的传输长度但保险起见自己写底层时一定要明确这个换算关系。多块读写测试通过后才能说明SDIODMA这条链路是稳的。如果不稳先不要怀疑文件系统回头查DMA配置、SDIO时钟、卡的质量按这个优先级来。5. 接入FatFS让SD卡变成真正的“U盘”5.1 FatFS文件系统层配置底层读写没问题下一步就是挂文件系统。STM32CubeMX支持直接添加FatFS中间件它会把底层接口文件和HAL库对接好省去很多移植工作。在Middleware and Software Components里勾选FatFS然后在配置页面选择SD Card作为底层驱动。CubeMX会自动生成fatfs_platform.c和user_diskio.c这几个文件其中user_diskio.c里要实现5个函数USER_initialize、USER_status、USER_read、USER_write、USER_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_open、f_write、f_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配置里只启用了FATFSFAT12/16/32挂载会失败。要么在电脑上重新格式化成FAT32要么在ffconf.h里打开FF_FS_EXFAT不过那样会增大代码体积一般嵌入式场景FAT32完全够用没必要开exFAT。7. 项目扩展向更高性能或RTOS环境迁移这一套SDIODMA方案并不是只能用在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接口如果你已经熟悉了SDIODMA这套流程以后调试Wi-Fi模块的SDIO通信会顺畅很多因为底层DMA搬运逻辑是一模一样的。不过那是另一个话题本文就先到这——先把SD卡的读写跑稳比什么都实在。本文还有配套的精品资源点击获取