简介面向STM32开发者的SDIODMA读写SD卡工程基于HAL库给出1位总线模式下的完整配置与可编译例程重点覆盖DMA传输、中断响应以及SDIO时钟相关参数设置包括数据捕获边沿、时钟分频器旁路、空闲模式时钟输出、硬件流控和分频因子等适合正在学习STM32存储外设、需要打通SD卡底层读写链路的初中级开发者参考。压缩包共112个文件约1015KB以73个头文件和27个C源文件为主体配合Keil工程、CubeMX配置、启动汇编、HEX输出及少量说明文本目录结构便于对照外设初始化、DMA链路与中断处理。已有668人学习可作为课程实验、项目原型或SD卡驱动移植的参考材料帮助读者快速理解HAL库下SDIODMA的工程组织、参数含义与调试切入点。1. 先把SDIODMA跑通它解决的是CPU被SD卡读写拖住的问题STM32 上用 HAL 库把 SDIO 和 DMA 接起来读写 SD 卡最容易误判的地方是把“能读到数据”当成“链路稳定”。SDIO 负责和卡做命令、响应与 4 位数据搬运DMA 负责把 FIFO 里的块数据搬到内存CPU 只处理状态和回调两者任何一处时序、对齐或 Cache 没处理表现都可能是偶发错块、挂载失败甚至写盘后重新上电文件丢失。这份资源面向已经能用 HAL 点灯、串口准备把采样数据、运行日志或配置文件落到 SD 卡的场景也适合拿现有工程对照检查 SDIO 时钟、DMA 通道、块对齐和 FatFS 的 diskio 实现。它不教你从零写驱动而是给出一套可对照的 SDIODMA 读写路径把“能读一个扇区”推进到“能反复写盘并验证”。2. STM32的SDIODMA底层账时钟、通道和4位总线怎么对上2.1 为什么SD卡读写适合走DMA而不是轮询SD 卡的块传输单位通常是 512 字节SDIO 外设内部 FIFO 深度有限轮询方式读一块数据要在 FIFO 非空和满标志之间反复查状态CPU 被切得很碎。单块、低频读写时这种开销还能忍一旦做连续采样记录轮询会把主循环拖到几十毫秒级控制环和通信任务都受影响。DMA 的价值不是让卡变快而是把“从 SDIO FIFO 搬到内存”这段固定动作交给 DMA 控制器CPU 只在启动、完成和错误回调里介入。选型上我一般这样分上电自检、读配置、偶尔写一行日志用阻塞式HAL_SD_ReadBlocks/HAL_SD_WriteBlocks更省事连续写文件、批量导出数据、读取大段波形用HAL_SD_ReadBlocks_DMA/HAL_SD_WriteBlocks_DMA。DMA 模式不是“配置完就万事大吉”它要求缓冲地址、数据宽度、DMA 通道映射和中断优先级同时正确否则现象往往是“函数返回 HAL_OK数据却不对”。方式CPU占用适合场景常见风险轮询 FIFO高验证卡是否在线主循环被拖慢FIFO 溢出阻塞式 HAL 读写中少量配置、低频日志卡忙时阻塞时间不可控SDIODMA中断低连续记录、批量导出缓冲对齐、Cache、通道映射出错DMARTOS 信号量低多任务文件系统回调里做重活、重入可执行的检查步骤不复杂先在 CubeMX 里确认 SDIO 的 RX/TX DMA 请求已经勾上模式选 Normal 而不是 Circular内存地址递增、外设地址不递增数据宽度按总线和 HAL 要求设成 Word再把 DMA 和 SDIO 的 NVIC 中断打开。后面调不通时先回到这张表逐项核对比盲改应用代码有效。2.2 SDIO时钟与DMA请求映射怎么对SDIO 的卡识别阶段不能一上来就跑高速。常见做法是初始化阶段用较低时钟等卡识别、总线宽度切换完成后再切到较高时钟。SDIO_CK 的分频关系按对应系列参考手册计算别把 CubeMX 里的分频值当成“越大越快”或“越小越稳”它和 SDIOCLK 源频率绑在一起。卡座、排线、上拉电阻和 3.3V 供电质量都会影响高速下的误码率4 位总线比 1 位总线更敏感。DMA 请求映射是第二个容易照抄出错的地方。不同 STM32 系列的 SDIO 收发对应不同 DMA 控制器和通道有的系列还区分 DMA1/DMA2 和请求复用号。正确做法是查对应型号的参考手册 DMA 请求映射表或者在 CubeMX 里让工具分配后回看生成的MX_DMA_Init和句柄字段。把 SDIO_RX 配到 TX 通道、两个方向抢同一个通道、DMA 优先级压过控制环都会出现“单次能跑、连续跑就错”的现象。配置项建议落点原因初始化时钟低速先保证识别卡上电和 CMD 阶段对时序敏感传输时钟识别成功后再提高排线差时高速会偶发 CRC 错总线宽度先 1 位验证再切 4 位4 位对 DAT1~DAT3 上拉和布线更敏感DMA 模式NormalSDIO 块传输有明确起止循环模式容易越界DMA 优先级低于实时控制中断避免长块传输压住采样或电机环中断优先级SDIO 与 DMA 成对检查只开一个会出现永远等不到完成注意DMA 通道映射不要按“同系列差不多”来猜换一个型号就可能变。生成工程后把 DMA 句柄里的Instance、Channel、Request三个字段对一遍能省掉很多玄学调试。2.3 HAL句柄与状态机先看懂这几个字段HAL 的 SD 句柄里State、ErrorCode、SDCardInfo这三个位置决定你能不能安全发起下一次传输。HAL_SD_ReadBlocks_DMA返回HAL_OK只表示请求已经启动不表示数据已经进内存真正的完成点在HAL_SD_RxCpltCallback写方向在HAL_SD_TxCpltCallback。很多翻车代码是在启动 DMA 后紧接着读缓冲读到的是旧数据或半截数据然后怀疑卡坏了。发起下一次传输前我习惯加一层卡状态检查HAL_SD_GetCardState(hsd) HAL_SD_CARD_TRANSFER再继续如果返回忙或错误先看hsd.ErrorCode再决定重试、HAL_SD_Abort还是重新初始化。SDCardInfo.BlockSize和LogBlockSize用来确认当前卡按什么块大小工作FatFS 的扇区大小要和它对上。下面这段只做状态判断不贴完整工程方便嵌到现有代码里。/* 启动前做一次状态检查避免在上一次 DMA 未完成时重入 */ if (HAL_SD_GetCardState(hsd) ! HAL_SD_CARD_TRANSFER) { /* 卡忙或状态机未回到传输态先处理错误不要直接再发请求 */ return -1; } /* 清掉本次传输的标志回调里只置位不做耗时操作 */ sd_rx_done 0; sd_rx_error 0; if (HAL_SD_ReadBlocks_DMA(hsd, (uint8_t *)buf, block_addr, block_cnt) ! HAL_OK) { /* 这里失败通常是状态机、DMA 通道或参数不合法 */ return -2; }这段代码的逻辑是“先确认卡在传输态再清软件标志再启动 DMA”。block_addr按 HAL 的约定传逻辑块号block_cnt是块数缓冲长度至少block_cnt * 512字节。启动失败时不要只看返回值把hsd.ErrorCode和 DMA 通道状态一起看启动成功后也不能立刻读缓冲要等回调把sd_rx_done置起来并检查sd_rx_error是否被错误回调置位。3. 从CubeMX到第一个扇区把SDIODMA工程搭起来3.1 时钟树与外设参数SDIO 4位、DMA、NVIC的落点配置阶段先把“能识别卡”和“能高速传”分开。时钟树里给 SDIO 选一个稳定时钟源初始化分频先保守SDIO 外设模式选 4 位总线但应用里可以先按 1 位跑一次识别确认卡和卡座没问题后再切 4 位。DMA 方向 RX/TX 各配一个通道模式 Normal优先级不要高于实时控制中断。NVIC 里 SDIO 全局中断和对应 DMA 通道中断都要开只开一个就会出现“数据传完了但回调没进”的黑匣子现象。配置位置关键项常见取值/动作SDIO 外设总线宽度先 1 位验证再SDMMC_BUS_WIDE_4BSDIO 外设时钟分频初始化保守识别后按信号质量提高DMA RX方向/模式外设到内存Normal内存递增DMA TX方向/模式内存到外设Normal内存递增NVICSDIO/DMA两个都使能优先级成对检查GPIOCMD/DAT 上拉按卡座和板级设计确认不要悬空生成代码后先不要急着挂 FatFS。先跑单块读确认HAL_SD_Init返回HAL_OKHAL_SD_GetCardInfo能拿到容量和块大小再用 DMA 读一个块。这个顺序能把“卡没识别”“DMA 没通”“文件系统没挂上”三类问题拆开不然一个FR_NO_FILESYSTEM会把排查方向带偏。3.2 生成代码后先补的三处句柄、回调、超时CubeMX 生成的是骨架应用层至少要补三处一个静态对齐的 DMA 缓冲、完成/错误回调里的软件标志、带超时的等待逻辑。缓冲不要放在临时栈上DMA 还在搬的时候函数返回栈被复用就会偶发错块。带 D-Cache 的芯片还要在启动前后做 Cache 维护后面单独讲这里先把裸机路径跑通。/* 512 字节块缓冲按 32 字节对齐兼容常见 Cache line */ static uint32_t sd_buf[128] __attribute__((aligned(32))); static volatile uint8_t sd_rx_done; static volatile uint8_t sd_rx_error; /* DMA 接收完成回调只置标志不在这里解析文件系统 */ void HAL_SD_RxCpltCallback(SD_HandleTypeDef *hsd) { if (hsd-Instance SDIO) { sd_rx_done 1; } } /* 错误回调记录错误码交给应用层处理 */ void HAL_SD_ErrorCallback(SD_HandleTypeDef *hsd) { if (hsd-Instance SDIO) { sd_rx_error 1; } } /* 同步封装的单块 DMA 读返回 0 表示成功 */ int sd_read_one_block_dma(uint32_t blk, uint32_t *buf) { uint32_t tick HAL_GetTick(); if (HAL_SD_GetCardState(hsd) ! HAL_SD_CARD_TRANSFER) { return -1; } sd_rx_done 0; sd_rx_error 0; if (HAL_SD_ReadBlocks_DMA(hsd, (uint8_t *)buf, blk, 1) ! HAL_OK) { return -2; } /* 超时不要用死等按卡等级和块数给余量 */ while (!sd_rx_done !sd_rx_error) { if ((HAL_GetTick() - tick) 500) { HAL_SD_Abort(hsd); return -3; } } if (sd_rx_error) { return -4; } /* 等待卡回到传输态避免下一次请求撞上忙状态 */ tick HAL_GetTick(); while (HAL_SD_GetCardState(hsd) ! HAL_SD_CARD_TRANSFER) { if ((HAL_GetTick() - tick) 500) { return -5; } } return 0; }这段逻辑的关键在参数和时序blk是逻辑块号不是字节偏移1表示一次读一个 512 字节块500ms是保守超时实际按卡和块数调整。回调里只置标志解析、打印、写文件都放回主循环避免在中断里做重活。HAL_SD_Abort是超时后的后悔药但不要频繁调用先看ErrorCode判断是 CRC、超时还是 DMA 传输出错。3.3 单块读写实测从块0读512字节第一次实测建议只读不写。块 0 通常是分区表或引导区读出来看前几个字节是否符合预期即可不要拿块 0 做写测试。写测试选卡末尾的保留块或专用测试卡写前先读回备份写后立刻读回比较。下面这段把读块 0 和写测试块分开避免误写。uint32_t first_word 0; int ret; /* 读逻辑块 0只做观察不做写入 */ ret sd_read_one_block_dma(0, sd_buf); if (ret 0) { first_word sd_buf[0]; /* 常见 MBR 结尾有 0x55AA但不要把它当成唯一判据 */ printf(blk0 first word: 0x%08lX\r\n, (unsigned long)first_word); } /* 写测试选一个确认无用的块号先读回备份再写 */ uint32_t test_blk 2048; /* 仅示例按实际卡容量改 */ uint32_t backup[128]; if (sd_read_one_block_dma(test_blk, backup) 0) { for (int i 0; i 128; i) { sd_buf[i] 0xA5A50000u | (uint32_t)i; } /* 写方向用对应的 DMA 接口完成回调是 TxCpltCallback */ if (HAL_SD_WriteBlocks_DMA(hsd, (uint8_t *)sd_buf, test_blk, 1) HAL_OK) { /* 等待写完成标志实际工程里同样加超时 */ while (!sd_tx_done !sd_tx_error) { } } /* 读回比较确认不是“写成功但数据没落盘” */ if (sd_read_one_block_dma(test_blk, sd_buf) 0) { for (int i 0; i 128; i) { if (sd_buf[i] ! (0xA5A50000u | (uint32_t)i)) { printf(verify fail at %d\r\n, i); break; } } } }读块 0 的返回值只能说明 DMA 路径通了不能说明文件系统可用写测试块的回读比较才能说明写方向、DMA 完成判断和卡状态切换都对。test_blk不要照抄按卡容量选一个不会覆盖数据的块量产代码里不要保留这种裸块写测试改成专用测试分区或测试文件。写完成后等HAL_SD_GetCardState回到传输态再进行下一次读否则容易在忙状态启动请求。4. 多块与文件系统DMA传输、FatFS挂载和写盘闭环4.1 多块读写为什么要按块对齐并拆缓冲多块 DMA 的第一条约束是块对齐SDIO 按 512 字节块工作缓冲长度必须是块大小的整数倍起始地址按 DMA 和 Cache 要求对齐。第二条约束是单次传输上限DMA 的 NDTR 位宽、HAL 内部拆分策略和卡的实际响应都会限制一次能发多少块。工程里更稳的做法是应用层分片比如一次 16 块、32 块按缓冲大小和实时性调不要一口气把整个文件塞进一个请求。/* 分片读把大请求拆成不超过 max_blocks 的小请求 */ int sd_read_chunked(uint32_t start_blk, uint8_t *dst, uint32_t total_blocks, uint32_t max_blocks) { uint32_t done 0; while (done total_blocks) { uint32_t n total_blocks - done; if (n max_blocks) { n max_blocks; } /* 每次只处理一个分片缓冲按 512 字节块推进 */ int ret sd_read_one_block_dma(start_blk done, (uint32_t *)(dst done * 512)); if (ret ! 0) { return ret; } done n; } return 0; }上面这个示例为了突出分片逻辑实际每次仍调用单块读工程里应换成HAL_SD_ReadBlocks_DMA(..., n)并把等待逻辑改成按n块给超时。max_blocks的建议值要看 DMA NDTR 位宽和缓冲大小常见保守做法是十几到几十块dst必须按 32 字节对齐长度按块向上取整。多块请求失败时先退回单块读对比能快速判断是分片参数问题还是卡本身问题。4.2 FatFS挂载与f_mkfs/f_open/f_write的DMA配合FatFS 的disk_read/disk_write是同步接口底层可以调用 DMA但必须在函数返回前等到完成。裸机里用标志加超时RTOS 里用信号量或事件组不要在 DMA 回调里直接跑 FatFS。挂载顺序一般是f_mount、f_open、f_write、f_sync、f_close测试卡才用f_mkfs而且格式化前确认卡里没有要保留的数据。f_write成功只代表数据进了 FatFS 缓存掉电前不f_sync文件长度和 FAT 表可能还没落盘。/* FatFS diskio 的读封装启动 DMA 后同步等待返回 RES_OK 才继续 */ DRESULT disk_read_dma(BYTE *buff, DWORD sector, UINT count) { uint32_t tick HAL_GetTick(); if (HAL_SD_GetCardState(hsd) ! HAL_SD_CARD_TRANSFER) { return RES_ERROR; } sd_rx_done 0; sd_rx_error 0; if (HAL_SD_ReadBlocks_DMA(hsd, buff, sector, count) ! HAL_OK) { return RES_ERROR; } while (!sd_rx_done !sd_rx_error) { if ((HAL_GetTick() - tick) 1000) { HAL_SD_Abort(hsd); return RES_ERROR; } } return sd_rx_error ? RES_ERROR : RES_OK; }参数上buff要是 32 字节对齐的持久缓冲sector是 FatFS 的扇区号count是扇区数通常一个扇区 512 字节。1000ms超时按最大分片调整不要无限等。写方向同理封装disk_write_dma完成后再返回RES_OK。多任务环境要给 diskio 加互斥FatFS 本身不保证同一卷重入文件写入用f_write分多次写最后一次f_sync后再f_close这样掉电测试才有意义。4.3 吞吐与CPU占用怎么量一个可复现的计时框架测吞吐不要只看“感觉快”用周期计数器或毫秒计时都行关键是记录块数、耗时和返回值。下面用 DWT 周期计数做粗测换算成 KB/s如果芯片没有 DWT用HAL_GetTick也能看趋势。CPU 占用则看 DMA 传输期间主循环还能跑多少空闲计数或者用 GPIO 翻转配合逻辑分析仪看高电平时间别只凭肉眼。/* 读 n 块并计时cycles 用 DWT 周期计数 */ uint32_t t0 DWT-CYCCNT; int ret sd_read_chunked(4096, (uint8_t *)big_buf, 64, 16); uint32_t cycles DWT-CYCCNT - t0; if (ret 0) { /* 按实际内核频率换算时间再算 KB/s */ float sec (float)cycles / (float)SystemCoreClock; float kbps (64.0f * 512.0f / 1024.0f) / sec; printf(read 64 blocks, %.2f KB/s\r\n, kbps); }记录表至少留四列测试项、块数、耗时、返回值。单块读、16 块读、64 块读、文件写入各跑一轮再看提高分片后吞吐是否上升、错误是否增加。吞吐受卡本身写入擦除影响很大写方向比读方向慢是正常现象如果读方向也远低于预期先查时钟分频、4 位总线是否真的切成功、DMA 是否被其它高优先级请求抢占。5. 避坑与排查SDIODMA最容易翻车的五处5.1 现象卡初始化失败或挂载返回FR_NO_FILESYSTEM现象是HAL_SD_Init返回错误或者初始化过了但f_mount返回FR_NO_FILESYSTEM。原因通常不在 FatFS 本身而是卡没被正确识别、4 位总线信号质量差、卡座供电不足、CMD/DAT 上拉不对或者卡里本来就没有可识别的文件系统。初始化时钟一上来太快也会让识别阶段偶发失败换一张卡又能过很容易误判成代码没问题。解决顺序是先把总线切回 1 位、降低初始化时钟确认HAL_SD_GetCardInfo能拿到容量和块大小再检查卡座 3.3V 和上拉排线尽量短最后才切 4 位和提高时钟。挂载失败时先用裸块读看块 0 是否有数据再决定要不要在测试卡上执行f_mkfs。格式化只对确认无数据的测试卡做量产代码不要一上来就格式化。5.2 现象DMA传输完成但数据全0或错位现象是回调进了、HAL_OK也返回了缓冲里却是全 0、旧数据或每隔几个字节错位。原因常见于 DMA 数据宽度和地址递增配错、缓冲没有按 32 字节对齐、DMA 通道映射错、启动前没清标志或者把块号当成了字节偏移。带 D-Cache 的芯片还会出现 CPU 读到旧 Cache 行的问题DMA 明明写了内存CPU 看到的还是缓存里的旧值。解决时先用阻塞式HAL_SD_ReadBlocks读同一块做对比排除卡和块号问题再检查 DMA 的MemInc、PeriphInc、DataWidth和通道请求最后处理 Cache。缓冲用静态数组并加对齐属性收发前后按 Cache line 做 clean/invalidate。错位问题重点看块号单位HAL 接口一般传逻辑块号不要自己再乘 512。5.3 现象跑一段时间HardFault或数据偶发损坏现象是单次测试正常连续跑几分钟后 HardFault或者文件中间偶尔出现一段乱码。原因可能是 DMA 缓冲放在栈上、函数返回后栈被复用回调里做了耗时操作导致重入多个任务同时调用 diskio中断优先级安排不当Cache 行和邻近变量共享导致 invalidate 误伤。还有一类是写完没等卡回到传输态就发下一次请求状态机被撞坏。解决上DMA 缓冲一律静态分配或由内存池固定分配生命周期覆盖整次传输回调和应用层用标志/信号量解耦FatFS 调用加互斥invalidate 的地址和长度按 Cache line 向上取整缓冲前后留隔离区。HardFault 先看栈指针和调用栈再看是不是在中断里跑了printf或文件操作。连续传输测试至少跑几百次别用一次成功下结论。5.4 现象拔插后不识别或只能复位恢复现象是运行中拔卡再插回程序还在等旧状态挂载失败或直接卡死。原因是 SDIO 状态机没有重新初始化卡检测引脚没处理电源没有重新上电GPIO 复用状态还停留在旧配置。热插拔时如果正在写盘文件系统损坏的概率很高这类操作不能当成正常流程。解决是加卡检测引脚或定期探测检测到拔出后停止文件操作调用HAL_SD_DeInit再重新HAL_SD_Init必要时控制卡座电源重新上电。写盘过程中不要拔卡测试也要在文件关闭并f_sync后再拔。重新初始化后先读块 0 或读卡信息确认状态回到传输态再挂载不要直接继续写旧文件句柄。5.5 现象写盘成功但重新上电文件丢失现象是f_write返回FR_OK运行中读文件也正常断电重启后文件长度变 0 或目录里没有。原因是数据只在 FatFS 缓存和 MCU 内部没有f_sync或者 DMA 写完成判断错实际卡还在忙又或者掉电时 FAT 表更新到一半。写保护开关、卡质量差也会造成“看起来写了实际没落盘”。解决是写完关键数据后调用f_sync再f_close并检查每一步返回值对可靠性要求高的日志采用追加写加定期 sync或双文件轮换。掉电测试至少做三次写入后立即断电、写入后延时断电、写入后正常关闭再断电每次重新挂载读回长度和校验值。写后读回比较是最便宜的验证不要只看f_write的返回值。这套资源适合拿来对照初始化顺序和 diskio 封装需要时从项目页取用即可不必把它当成唯一答案。6. 进阶验证双缓冲、Cache维护和掉电测试把读写钉死6.1 双缓冲与DMA半传输的配合单缓冲时DMA 写完才能处理处理期间卡空闲。双缓冲用两块 512 字节缓冲做乒乓DMA 写 A 时处理 B回调切到另一块再启动下一次请求。下面只给骨架重点是别让 DMA 写正在处理的缓冲。static uint8_t pingpong[2][512] __attribute__((aligned(32))); static volatile uint8_t active; /* 回调里切缓冲并启动下一块实际工程要加状态机和超时 */ void sd_pingpong_next(uint32_t next_blk) { active ^ 1; HAL_SD_ReadBlocks_DMA(hsd, pingpong[active], next_blk, 1); }active只允许回调和应用层其中一方修改处理缓冲前确认 DMA 已切走。双缓冲重叠的是处理时间不会突破卡本身的物理极限块数越多收益越明显。6.2 Cache与MPU带D-Cache的MCU必须做的两步带 D-Cache 的芯片发送前对缓冲做 clean接收前先 clean 旧脏行接收完成后再 invalidate。地址和长度按 Cache line 对齐缓冲前后留隔离区避免 invalidate 误伤邻近变量。SCB_CleanDCache_by_Addr((uint32_t *)buf, sizeof(buf)); /* 发送前 */ /* 启动 DMA 接收完成后 */ SCB_InvalidateDCache_by_Addr((uint32_t *)buf, sizeof(buf));MPU 可以把 DMA 缓冲区配成非缓存或写通减少维护动作但配置错会让问题更隐蔽。先按 clean/invalidate 跑通再考虑 MPU。6.3 掉电与拔插压力测试一张检查表测试项操作通过标准单块写后立即断电写一块不 sync直接断电记录可复现的丢失边界文件写后 sync 断电f_write后f_sync再断电重挂载后长度和校验一致连续写 100 次每次写不同长度并 sync无 HardFault无错块写盘时拔卡仅测试卡记录恢复流程重新初始化后能挂本文还有配套的精品资源点击获取