简介:这份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 位 |
| 典型吞吐 | 5~20 MB/s | 0.3~2 MB/s |
| CPU 占用 | 低(配 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 没有 SDIO,F4/F7/H7 的编号也不一样,务必查对应型号的复用表,不要照抄。
提示:如果你用的是 SPI 模式,把上面替换成普通 SPI 的 SCK/MISO/MOSI 配置,CS 用普通推挽输出,速率先设到 400 kHz 以下做初始化,识别成功后再升到 10~25 MHz。
3. 底层初始化:让卡从“上电”走到“可读写”
3.1 初始化流程与关键命令
SD 卡上电后不是立刻就能读写的,它要经过一串命令协商:CMD0 复位、CMD8 判断电压和版本、ACMD41 等待卡就绪、CMD2 拿 CID、CMD3 分配 RCA、CMD9 读 CSD、CMD7 选中卡,最后用 ACMD6 把数据线宽度设成 4 位。任何一步超时,后面全是白搭。
用 HAL 库的话,HAL_SD_Init内部会走完这套流程,但前提是你的时钟配置和上电延时对。我一般会在初始化前手动加 1~2 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位+DMA | SPI 模式 |
|---|---|---|
| 单扇区读 | 约 0.1 ms | 约 1 ms |
| 连续写吞吐 | 3~8 MB/s | 0.3~1 MB/s |
| 30 分钟连续写 | 无错误 | 偶发超时 |
| 断电恢复 | 文件系统完好 | 需重新挂载 |
6.3 一个容易忽略的验证技巧
在disk_write里加一个计数器,记录累计写入扇区数,同时用另一个定时器记录运行时间。跑压力测试时把这两个值通过串口打出来,一旦写入速度突然掉到接近零,说明卡内部在做垃圾回收或已经出错,这时候继续写只会加速损坏。看到速度异常就主动停止并同步,比等它彻底卡死再排查要省事得多。
我自己踩过最深的一次坑,是早期做数据记录时没加f_sync,实验室断电测试看着都正常,一到现场真实断电,最后几分钟数据全丢。后来养成习惯:凡是掉电可能发生的场景,写完关键数据立刻同步,宁可牺牲一点吞吐。这个习惯帮我省掉了无数次返工。希望帮到你。
本文还有配套的精品资源,点击获取