STM32 SD卡稳定读写实战:从初始化到FATFS避坑指南
简介这份STM32的SD卡程序资源面向嵌入式开发初学者与需要为项目扩展存储能力的工程师围绕SPI/SDIO接口通信、SD卡协议命令、初始化流程、块读写操作、DMA传输、中断驱动以及FATFS文件系统等核心知识点展开帮助读者在STM32平台上建立可靠的SD卡读写功能。压缩包共106个文件以44个c源文件与51个h头文件为主体另含txt说明、uvproj与uvopt工程文件、hex固件、s启动文件及bat批处理脚本整体约204KB工程结构完整可直接导入Keil等IDE编译调试。资源中附有SD卡实验示例涵盖初始化、单块与多块读写、错误处理等环节注释与说明文档有助于理解代码逻辑适合作为学习参考与项目移植基础。目前已有1180人学习下载可供需要快速上手SD卡驱动开发的读者借鉴排错思路与实现细节。1. STM32 的 SD 卡程序亲测可用从“能识别”到“能稳定读写”到底差在哪很多人第一次在 STM32 上接 SD 卡都会经历同一个剧本卡插上初始化返回成功读几个扇区也正常于是以为大功告成结果一上 FATFS 连续写文件跑几分钟就卡死或者断电重启后文件系统直接损坏。标题里说的“亲测可用”真正的门槛从来不是“能读到 ID”而是在真实硬件、真实供电、真实文件系统负载下长时间不翻车。这篇笔记面向两类人一类是刚拿到板子和 SD 卡模块、想先把裸机读写跑通的新手另一类是把 SD 卡当数据记录介质、需要评估稳定性和吞吐的从业者。我会按“硬件怎么接 → 底层怎么初始化 → 扇区读写怎么调 → FATFS 怎么挂 → 坑在哪 → 怎么验证”的顺序讲所有参数都给到能直接改的程度。SD 卡在 STM32 上跑的是 SDIO 或 SPI 两种通道选型、时序、DMA 配置完全不同下面会分开说清楚。2. 先定通道再动手SDIO 与 SPI 的选型账2.1 两种通道的真实差距SDIO 是 STM32 内置的专用外设4 位数据线并行理论带宽能到几十 MB/s配合 DMA 后 CPU 占用极低适合做高速数据记录。SPI 则是“用普通外设模拟协议”只走 1 位数据线速率受 SPI 时钟和软件开销限制实测通常只有几百 KB/s 到 2 MB/s 左右。但 SPI 有一个 SDIO 比不了的优势引脚少、时序宽容、几乎任何 STM32 都有。如果你的板子 SDIO 引脚被别的外设占了或者你用的是小容量型号SPI 就是唯一选择。我一般的判断标准是要连续写日志、采样率超过 100 KB/s就上 SDIO DMA只是偶尔存配置、导出小文件SPI 足够且更省心。对比项SDIO 4 位SPI 模式数据线4 位并行1 位典型吞吐520 MB/s0.32 MB/sCPU 占用低配 DMA中高引脚数6 根左右4 根时序调试难度较高较低适合场景高速记录配置/小文件2.2 硬件连接与上拉电阻SDIO 模式下CMD 和 DAT0~DAT3 都需要上拉典型 10kΩ47kΩ。很多模块板已经焊好但如果你是自己画板漏掉上拉是最常见的“初始化偶尔成功偶尔失败”的元凶。SPI 模式下 MISO 可以不拉但 MOSI、SCK、CS 建议保留上拉避免上电瞬间误触发。供电同样关键。SD 卡在写入瞬间电流会冲到 100 mA 以上如果和 MCU 共用一条细走线电压跌落会让卡内部复位表现为“写一半没响应”。常见做法是给 SD 卡单独一路 3.3V并在卡座电源脚就近放 10µF 0.1µF 去耦。/* SDIO 引脚配置示例以常见 F4 系列为例引脚按你的板子改 */ /* PC8~PC12: D0~D3, CMD, CLK全部复用推挽 上拉 */ GPIO_InitTypeDef gpio {0}; __HAL_RCC_GPIOC_CLK_ENABLE(); __HAL_RCC_GPIOD_CLK_ENABLE(); gpio.Mode GPIO_MODE_AF_PP; /* 复用推挽 */ gpio.Pull GPIO_PULLUP; /* 内部上拉外部最好再并 10k */ gpio.Speed GPIO_SPEED_FREQ_VERY_HIGH; gpio.Alternate GPIO_AF12_SDIO; /* 具体 AF 号查对应型号手册 */ gpio.Pin GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12; HAL_GPIO_Init(GPIOC, gpio); gpio.Pin GPIO_PIN_2; /* CMD 在 PD2 的情况 */ HAL_GPIO_Init(GPIOD, gpio);这段代码只做了一件事把 SDIO 的时钟、命令、数据线全部配成复用推挽并打开上拉。GPIO_SPEED_FREQ_VERY_HIGH是必须的SDIO 时钟在高速模式下能到 48 MHz用低速档会出现数据线采样错误。Alternate的 AF 编号每个系列不同F1 没有 SDIOF4/F7/H7 的编号也不一样务必查对应型号的复用表不要照抄。提示如果你用的是 SPI 模式把上面替换成普通 SPI 的 SCK/MISO/MOSI 配置CS 用普通推挽输出速率先设到 400 kHz 以下做初始化识别成功后再升到 1025 MHz。3. 底层初始化让卡从“上电”走到“可读写”3.1 初始化流程与关键命令SD 卡上电后不是立刻就能读写的它要经过一串命令协商CMD0 复位、CMD8 判断电压和版本、ACMD41 等待卡就绪、CMD2 拿 CID、CMD3 分配 RCA、CMD9 读 CSD、CMD7 选中卡最后用 ACMD6 把数据线宽度设成 4 位。任何一步超时后面全是白搭。用 HAL 库的话HAL_SD_Init内部会走完这套流程但前提是你的时钟配置和上电延时对。我一般会在初始化前手动加 12 ms 延时让卡内部稳压器稳定再开始发命令。/* SDIO 外设与卡初始化 */ SD_HandleTypeDef hsd {0}; hsd.Instance SDIO; hsd.Init.ClockEdge SDIO_CLOCK_EDGE_RISING; hsd.Init.ClockBypass SDIO_CLOCK_BYPASS_DISABLE; hsd.Init.ClockPowerSave SDIO_CLOCK_POWER_SAVE_DISABLE; hsd.Init.BusWide SDIO_BUS_WIDE_1B; /* 先 1 位识别后再切 4 位 */ hsd.Init.HardwareFlowControl SDIO_HARDWARE_FLOW_CONTROL_ENABLE; hsd.Init.ClockDiv 118; /* 初始低速约 400kHz */ HAL_Delay(2); /* 上电稳定延时 */ if (HAL_SD_Init(hsd) ! HAL_OK) { Error_Handler(); /* 初始化失败先查供电和上拉 */ } /* 识别成功后切 4 位宽 高速时钟 */ HAL_SD_ConfigWideBusOperation(hsd, SDIO_BUS_WIDE_4B);ClockDiv是分频系数公式是SDIOCLK / (2 × (ClockDiv 2))具体 SDIOCLK 取决于你的时钟树。初始阶段一定要慢400 kHz 左右最稳等卡识别完成再提速。BusWide先设 1 位是血泪经验有些卡在 1 位模式下能识别直接上 4 位反而握手失败分两步走能快速定位是卡的问题还是线的问题。3.2 单扇区与多扇区读写初始化成功后先做单扇区读写验证再做多扇区。单扇区用HAL_SD_ReadBlocks/HAL_SD_WriteBlocks多扇区用带DMA后缀的版本。注意块大小固定 512 字节这是 SD 卡的物理扇区不要改成别的值。uint8_t buf[512] __attribute__((aligned(4))); /* 4 字节对齐DMA 必须 */ /* 写第 0 扇区测试用正式别写 0 扇区那是分区表 */ if (HAL_SD_WriteBlocks(hsd, buf, 100, 1, 1000) ! HAL_OK) { /* 写失败查卡是否写保护、供电是否跌落 */ } /* 读回来比对 */ uint8_t rd[512] __attribute__((aligned(4))); if (HAL_SD_ReadBlocks(hsd, rd, 100, 1, 1000) ! HAL_OK) { /* 读失败查时钟是否过快、数据线是否接触不良 */ } if (memcmp(buf, rd, 512) ! 0) { /* 数据不一致多半是 DMA 缓冲区没对齐或时钟太快 */ }aligned(4)不是可选项。SDIO 的 DMA 要求缓冲区地址 4 字节对齐不对齐时轻则数据错位重则直接 HardFault。最后一个参数是超时毫秒数写操作给 1000 ms 比较保险读给 500 ms 够用。测试扇区选 100 这种靠前但不碰分区表的位置避免把文件系统元数据写坏。注意多扇区读写时HAL_SD_WriteBlocks_DMA是异步的调用后要等HAL_SD_TxCpltCallback回调才能进行下一次操作连续调用会返回忙。4. 挂上 FATFS从扇区到文件4.1 磁盘 IO 层对接FATFS 不直接碰硬件它通过diskio.c里的几个函数和底层对接。核心是disk_read、disk_write、disk_ioctl三个。把上面验证过的扇区读写包进去就行。/* diskio.c 关键实现 */ DSTATUS disk_status(BYTE pdrv) { return 0; /* 简化处理实际可返回卡是否存在 */ } DRESULT disk_read(BYTE pdrv, BYTE *buff, DWORD sector, UINT count) { if (HAL_SD_ReadBlocks(hsd, buff, sector, count, 2000) ! HAL_OK) return RES_ERROR; return RES_OK; } DRESULT disk_write(BYTE pdrv, const BYTE *buff, DWORD sector, UINT count) { if (HAL_SD_WriteBlocks(hsd, (uint8_t *)buff, sector, count, 5000) ! HAL_OK) return RES_ERROR; return RES_OK; } DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { switch (cmd) { case CTRL_SYNC: return RES_OK; /* 等待写完成DMA 模式下要加 */ case GET_SECTOR_COUNT: *(DWORD *)buff hsd.SdCard.LogBlockNbr; return RES_OK; case GET_SECTOR_SIZE: *(WORD *)buff 512; return RES_OK; case GET_BLOCK_SIZE: *(DWORD *)buff 1; /* 擦除块大小给 1 最安全 */ return RES_OK; } return RES_PARERR; }GET_SECTOR_COUNT返回卡的总扇区数FATFS 靠它算容量。GET_BLOCK_SIZE如果填错格式化时会出现容量异常。CTRL_SYNC在 DMA 写模式下必须真正等待写完成否则掉电时最后一批数据会丢这是数据记录场景最隐蔽的坑之一。4.2 挂载、格式化与文件写入挂载前先确认卡里已有 FAT32 文件系统没有就先格式化。格式化会清空整张卡正式产品里不要自动执行。FATFS fs; FIL fil; UINT bw; FRESULT fr; fr f_mount(fs, 0:, 1); /* 1 立即挂载 */ if (fr FR_NO_FILESYSTEM) { /* 卡没有文件系统格式化会清空数据 */ BYTE work[512]; f_mkfs(0:, FM_FAT32, 0, work, sizeof(work)); fr f_mount(fs, 0:, 1); } if (fr ! FR_OK) { /* 挂载失败查 diskio 返回值、卡容量是否被正确识别 */ } fr f_open(fil, 0:/log.txt, FA_OPEN_ALWAYS | FA_WRITE); if (fr FR_OK) { f_lseek(fil, f_size(fil)); /* 追加到文件末尾 */ f_write(fil, hello sd\r\n, 10, bw); f_sync(fil); /* 关键立即落盘 */ f_close(fil); }f_sync是数据安全的核心。FATFS 有缓存不调用f_sync时数据可能还在内存里断电就没了。数据记录场景建议每写一批就f_sync一次代价是吞吐下降但换来的是掉电不丢数据。如果追求吞吐可以每 N 条同步一次N 根据你能容忍丢多少数据来定。提示f_mkfs的 work 缓冲区至少 512 字节且要对齐。格式化大卡可能耗时几秒到几十秒期间不要断电。5. 避坑与排查那些让程序“时好时坏”的细节5.1 初始化偶尔失败现象同一块板子有时上电识别成功有时失败复位几次又好了。原因通常是上拉电阻缺失或供电跌落卡在上电瞬间没进入正确状态。解决CMD 和数据线补 10kΩ 上拉SD 卡电源脚就近加 10µF 电容初始化前加 2 ms 以上延时。5.2 写文件跑几分钟后卡死现象连续写日志前几分钟正常之后f_write一直返回错误或程序卡在等待。原因多半是 DMA 缓冲未对齐或没有等待上一次传输完成就发起下一次。解决所有 DMA 缓冲区加aligned(4)写操作严格等回调或轮询标志位不要连续调用。5.3 断电后文件系统损坏现象正常读写没问题一断电重启卡挂载失败或文件变成乱码。原因是 FATFS 缓存未落盘或写操作进行到一半被打断。解决关键数据写完后立即f_sync产品里加掉电检测检测到电压下降先停止写入并同步。5.4 换一张卡就不工作现象某张卡能用换一张同容量同品牌的就不行。原因是不同卡的初始化时间和时序容忍度不同尤其是低速卡和高速卡混用。解决初始化阶段时钟压到 400 kHz 以下识别完成后再提速ClockDiv不要一上来就设很小。5.5 读数据偶发错位现象读回来的数据整体偏移几个字节或某几个字节随机错。原因是 SDIO 时钟太快超过了当前布线能承受的频率。解决把ClockDiv调大一档降速测试如果降速后正常说明是信号完整性问题检查走线长度和上拉。6. 怎么验证“亲测可用”一套可复现的压力测试6.1 分阶段验证法不要一上来就跑 FATFS 大文件。我一般分四步第一步只做初始化打印卡容量和 CID确认识别稳定第二步单扇区读写 1000 次比对数据第三步多扇区 DMA 读写测吞吐第四步挂 FATFS 连续写文件 30 分钟以上中途随机断电几次看恢复后文件系统是否完好。每一步过了再进下一步出问题能立刻定位到层。6.2 吞吐与稳定性实测表下面是我在一套常见配置下的实测参考值你的数值会因卡和时钟不同而有差异但量级可以参考。测试项SDIO 4位DMASPI 模式单扇区读约 0.1 ms约 1 ms连续写吞吐38 MB/s0.31 MB/s30 分钟连续写无错误偶发超时断电恢复文件系统完好需重新挂载6.3 一个容易忽略的验证技巧在disk_write里加一个计数器记录累计写入扇区数同时用另一个定时器记录运行时间。跑压力测试时把这两个值通过串口打出来一旦写入速度突然掉到接近零说明卡内部在做垃圾回收或已经出错这时候继续写只会加速损坏。看到速度异常就主动停止并同步比等它彻底卡死再排查要省事得多。我自己踩过最深的一次坑是早期做数据记录时没加f_sync实验室断电测试看着都正常一到现场真实断电最后几分钟数据全丢。后来养成习惯凡是掉电可能发生的场景写完关键数据立刻同步宁可牺牲一点吞吐。这个习惯帮我省掉了无数次返工。希望帮到你。本文还有配套的精品资源点击获取