☰
STM32F407 SD卡驱动实战:SPI与SDIO双模式及FatFs移植
2026/10/15 14:04:24 网站建设 项目流程

简介:这份资源面向嵌入式开发初学者与中级工程师,聚焦STM32F407通过SPI总线驱动SD卡的完整实现,解决裸机扇区读写与文件系统移植两类典型需求。压缩包共2009个文件,约45.75MB,以c源码、h头文件、html说明文档为主,辅以uvproj、uvopt等Keil工程文件及sct分散加载、hex与axf固件、gif与png截图等,覆盖从底层驱动到工程编译的完整链路。资源内含两个程序示例:其一直接操作SD卡,演示CMD0、CMD8、CMD55、CMD17、CMD24等命令与CRC校验、数据块结束标记处理;其二移植Fatfs轻量级文件系统,通过f_open、f_read、f_write、f_close等接口实现文件创建、读取与删除,并已实测运行稳定。目前已有911人学习下载,适合希望掌握SPI通信协议、SD卡初始化流程及Fatfs配置策略的开发者作为可靠参考。

1. 从一张死活认不到的卡说起:STM32F407 接 SD 卡到底难在哪

手头有块 STM32F407 的板子,想插张 SD 卡把采集数据存下来,结果初始化死活过不去,串口打印永远是SD_Init failed——这大概是每个第一次用 F407 驱动 SD 卡的人都会撞上的墙。STM32F407 自带 SDIO 控制器,理论上接 4 根数据线就能跑高速模式,但真正落地时你会发现:卡不认、容量算错、写几个扇区就超时、拔插一次文件系统直接崩。问题往往不在代码写得多烂,而在 SD 卡这套协议本身的初始化流程、时序要求和卡类型差异上。这份资源围绕 STM32F407 的 SD 卡驱动展开,覆盖 SPI 与 SDIO 两种接法、底层扇区读写、以及上层 FatFs 文件系统的对接,适合正在做数据记录仪、音频采集、固件升级这类需要本地存储的嵌入式开发者。下面按“先搞懂卡怎么被认出来,再动手把读写跑通,最后把文件系统挂上”的顺序拆一遍。

2. SD 卡协议与 STM32F407 的两种接法:SPI 还是 SDIO

2.1 先弄明白 SD 卡上电后发生了什么

SD 卡不是上电就能读写的存储器,它有一套状态机。卡刚上电处于 idle 状态,主机必须依次发 CMD0 让它复位,CMD8 探测它是不是支持 2.0 协议、供电范围对不对,接着用 CMD55 + ACMD41 反复轮询,直到卡把 busy 位清掉、进入 ready 状态。只有走完这套流程,后面发 CMD58 读 OCR 寄存器、CMD2 拿 CID、CMD3 要 RCA 才有意义。很多人初始化失败,就是 ACMD41 轮询次数不够或者没等够时间——卡内部上电稳压需要时间,尤其是大容量卡,几百毫秒是常事。SPI 模式下这套流程一样,只是命令通过 MOSI 发、响应从 MISO 读,CMD8 的响应格式和 SDIO 模式略有差别,需要单独解析 R7 响应。

2.2 SPI 接法和 SDIO 接法的取舍

SPI 接法占 4 根线(CS、SCK、MISO、MOSI),任意 GPIO 都能模拟,移植性极好,缺点是速度上不去,F407 的 SPI 跑到 21MHz 已经算高,实际连续读写也就 2MB/s 左右。SDIO 接法用 4 位数据线,时钟可以到 48MHz,理论带宽 24MB/s,实际也能跑到 10MB/s 以上,代价是引脚固定(PC8-PC12、PD2),PCB 布线要等长,对电源和信号完整性要求高。选哪个看场景:只是存点传感器日志、配置参数,SPI 足够且省心;要做音频流、图像缓存、高速数据记录,必须上 SDIO。这份资源两种都给了,建议先用 SPI 把协议跑通,理解命令交互,再切 SDIO 提速。

2.3 硬件连接与上拉电阻的坑

SPI 模式下 CS、MISO 建议加上拉,SD 卡在空闲时 MISO 是高阻,没有上拉读到的是浮空电平,容易误判。SDIO 模式下 4 根数据线加 CMD 线都要上拉,典型 10kΩ 到 47kΩ,F407 内部有弱上拉但驱动能力不够,高速通信时还是外部加。电源脚旁边必须并 100nF 加 10uF,卡在写入瞬间电流能冲到 100mA 以上,去耦不到位会出现写一半掉卡。这些硬件细节不做好,后面调代码调到怀疑人生也找不到原因。

// SPI 模式初始化骨架,基于 HAL 库 void SD_SPI_Init(void) { // 1. 配置 SPI 为低速模式,初始化阶段时钟不能超过 400kHz hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // 模式0 hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256; // 约 328kHz HAL_SPI_Init(&hspi1); // 2. 给卡至少 74 个时钟做上电同步 SD_CS_HIGH(); for (uint8_t i = 0; i < 10; i++) { SD_SPI_ReadWriteByte(0xFF); } SD_CS_LOW(); }

这段代码的关键在两点:初始化阶段 SPI 时钟必须压到 400kHz 以下,SD 卡规范强制要求,跑快了卡直接不响应;上电后先发至少 74 个时钟让卡完成内部稳压,再拉低 CS 发命令。BaudRatePrescaler设 256 对应 F407 的 84MHz APB2 时钟,得到约 328kHz,符合要求。等初始化完成、卡进入传输状态后,再把预分频改成 2 或 4 提速。

3. 底层扇区读写:CMD17、CMD24 与多块传输的时序

3.1 单块读写的命令与响应解析

SD 卡所有数据操作以扇区为单位,一个扇区固定 512 字节。读用 CMD17,写用 CMD24,命令里带 32 位扇区地址。发完命令后卡返回 R1 响应,读操作紧接着会有一个数据起始令牌 0xFE,然后 512 字节数据,最后 2 字节 CRC。写操作反过来,主机先发 0xFE,再发 512 字节数据加 CRC,卡回一个数据响应字节,低 5 位表示是否写入成功。SPI 模式下 CRC 默认不校验,但发的时候还得发,通常填 0xFF 占位。这里最容易翻车的是等待响应超时——卡在写内部 Flash 时会拉低 MISO 表示 busy,主机必须等它释放才能发下一条命令,不等就发,命令直接丢。

// 读取一个扇区,返回 0 表示成功 uint8_t SD_ReadSector(uint32_t sector, uint8_t *buf) { uint8_t r1; // 发 CMD17,参数为扇区地址 r1 = SD_SendCmd(CMD17, sector, 0x01); if (r1 != 0x00) return 1; // 命令响应异常 // 等待数据令牌 0xFE,超时约 200ms uint16_t retry = 0xFFFF; do { r1 = SD_SPI_ReadWriteByte(0xFF); } while (r1 == 0xFF && --retry); if (r1 != 0xFE) return 2; // 没等到数据令牌 // 连续读 512 字节 for (uint16_t i = 0; i < 512; i++) { buf[i] = SD_SPI_ReadWriteByte(0xFF); } // 读走 2 字节 CRC SD_SPI_ReadWriteByte(0xFF); SD_SPI_ReadWriteByte(0xFF); SD_CS_HIGH(); SD_SPI_ReadWriteByte(0xFF); // 补 8 个时钟释放总线 return 0; }

SD_SendCmd内部负责拉低 CS、发命令字节、发 4 字节参数、发 CRC、等 R1 响应。注意命令字节最高两位固定为 01,所以 CMD17 实际发出去是 0x51。等待数据令牌的循环用0xFFFF次作为超时,实际调试时如果频繁超时,先查 SPI 时钟是不是太快、CS 时序是不是有问题。读完之后补一个 0xFF 时钟再拉高 CS,是让卡有时间释放数据线,少了这步下次通信可能直接挂。

3.2 多块读写为什么容易出错

单扇区读写跑通后,自然会想用 CMD18 和 CMD25 做多块连续传输,减少命令开销。多块读在最后一个块之后要发 CMD12 停止传输,这个停止命令的时机很讲究——必须在读完最后一块数据、但卡还没开始发下一块之前发出去,早了数据截断,晚了卡已经进入下一块。多块写更麻烦,每写一块卡都要回响应并进入 busy,主机必须逐块等 busy 释放。实际项目里我一般先用单块把文件系统跑稳,只有在确认瓶颈在命令开销时才换多块,而且多块传输一定要配合 DMA,否则 CPU 全耗在搬数据上。

3.3 容量计算:CSD 寄存器里的那个坑

标准容量卡(SDSC)和高容量卡(SDHC/SDXC)的 CSD 寄存器结构不一样,算容量公式也不同。SDSC 用 CSD 里的 C_SIZE、C_SIZE_MULT、READ_BL_LEN 三个字段算,SDHC 直接读 C_SIZE 加 1 再乘 512KB。如果代码里只按一种算,插另一种卡就会得到离谱的容量值,比如 32GB 卡被算成 1GB。判断依据是初始化时 CMD8 的响应和 OCR 寄存器的 CCS 位,CCS 为 1 就是高容量卡。这个坑很隐蔽,因为卡能读写,只是容量显示错,做文件系统时会导致分区表解析失败。

4. 挂载 FatFs:从扇区读写到文件操作

4.1 FatFs 的移植接口只有五个函数

FatFs 是嵌入式领域用得最多的文件系统,移植到 F407 上只需要实现五个底层函数:disk_initialize、disk_status、disk_read、disk_write、disk_ioctl。前两个管卡的初始化和状态查询,中间两个直接调前面写好的扇区读写,最后一个处理获取扇区数量、扇区大小这些控制命令。disk_ioctl里的GET_SECTOR_COUNT必须返回正确容量,否则f_mount会失败或者格式化出问题。很多人卡在这一步,就是因为容量算错,FatFs 认为扇区数不对,直接拒绝挂载。

// FatFs 底层读接口,pdrv 是物理驱动器号 DRESULT disk_read(BYTE pdrv, BYTE *buff, DWORD sector, UINT count) { if (pdrv != 0) return RES_PARERR; for (UINT i = 0; i < count; i++) { if (SD_ReadSector(sector + i, buff + i * 512) != 0) { return RES_ERROR; } } return RES_OK; } // 获取扇区数量,供 f_mount 和格式化使用 DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { switch (cmd) { case GET_SECTOR_COUNT: *(DWORD *)buff = SD_GetSectorCount(); // 前面算好的容量/512 return RES_OK; case GET_SECTOR_SIZE: *(WORD *)buff = 512; return RES_OK; case CTRL_SYNC: return RES_OK; default: return RES_PARERR; } }

disk_read里按扇区循环调用,注意buff指针要按 512 字节偏移,不能直接传同一个地址。GET_SECTOR_COUNT返回的是总扇区数,不是字节数,这个值来自前面 CSD 解析的结果。CTRL_SYNC在 SPI 模式下可以直接返回 OK,SDIO 模式下如果有写缓存才需要真正同步。

4.2 f_mount 之后先做一次格式化验证

挂载成功后别急着写业务数据,先用f_mkfs格式化一次,再f_open建个文件写点内容读回来。这一步能一次性验证扇区读写、容量计算、目录项写入是否都正常。格式化参数里AU_Size给 0 让 FatFs 自动选簇大小,FM_FAT32强制 FAT32 格式,避免小卡被格式成 FAT16 导致后面容量受限。验证通过后再把业务逻辑接上去,出问题也好定位是文件系统层还是应用层。

4.3 写入性能与缓存策略

SPI 模式下每次f_write最终都落到单扇区写,速度很慢。提升办法有两个:一是把 FatFs 的_FS_TINY关掉,用它的窗口缓存减少重复读;二是应用层攒够一个簇再写,比如 4KB 一批。SDIO 模式下开 DMA 加多块写,性能能差出十倍。另外f_sync不要每写一次就调,它会把缓存刷到卡上,频繁调用等于频繁写,寿命和速度都受影响,按业务节奏定期同步就行。

5. 避坑与排查:那些让卡“时好时坏”的细节

5.1 现象:初始化偶尔成功偶尔失败

原因通常是上电时序或电源不稳。SD 卡内部稳压需要时间,如果 MCU 上电比卡快,第一条 CMD0 发出去时卡还没准备好。解决是在初始化最前面加 10ms 以上延时,再发 74 个时钟。电源方面,卡座供电脚并的电容不够,或者和 MCU 共用一路 LDO 被拉低,都会导致偶发失败。用示波器看卡座 VDD,上电瞬间如果有明显跌落,就得单独加去耦。

5.2 现象:能读不能写,或者写几个扇区就超时

先查写保护,SD 卡侧面的物理开关如果拨到 lock,CMD24 会返回写保护错误。排除了物理开关,再看是不是没等 busy 释放就发下一条命令。写操作后卡要擦除内部 Flash,大容量卡单次写可能 busy 几十毫秒,超时设太短就会误判失败。把等待 busy 的超时从默认的几毫秒加到 500ms 试试,如果好了就是这个问题。另外劣质卡或老化卡内部坏块多,写入重试次数暴涨,换张卡对比就能确认。

5.3 现象:热插拔之后文件系统损坏

SD 卡不支持真正的热插拔,拔卡瞬间如果正在写 FAT 表,文件系统直接损坏。解决是在应用层检测卡检测引脚(如果有),拔卡前先f_mount(NULL, ...)卸载,再关电源。没有检测引脚的,至少保证写操作有超时保护,卡拔走后写失败能返回错误而不是死等。已经损坏的卡用读卡器在电脑上修复,或者重新格式化,别指望在 MCU 上修。

5.4 现象:SPI 高速模式下数据错乱

初始化用低速跑通后切高速,结果读出来的数据偶尔错几个字节。先查 SPI 时钟极性相位,SD 卡 SPI 模式固定模式 0(CPOL=0,CPHA=0),设错在低速下可能碰巧能跑,高速必挂。再查 PCB 走线,SCK 和 MISO 太长、没包地,高速下串扰严重。把 SCK 降到 10MHz 如果就好了,说明是信号完整性问题,得改板或加串阻。软件上可以在读数据后加 CRC 校验,SPI 模式虽然默认不校验,但卡返回的数据里 CRC 字段是有效的,自己算一遍能挡住大部分错误。

5.5 现象:FatFs 挂载返回 FR_NO_FILESYSTEM

这个错误基本就是容量或扇区大小不对。先确认disk_ioctl返回的扇区数是不是和卡实际容量匹配,用读卡器在电脑上看卡的真实容量对比。如果容量对但还报这个错,可能是卡上原本的分区表格式 FatFs 不认,比如 exFAT 或者带 GPT 分区表。用f_mkfs重新格式化成 FAT32 再挂载。还有一种情况是disk_read读 0 扇区(MBR)时返回的数据不对,回头查扇区读函数,尤其是多块读的停止时机。

6. 进阶:用 DMA 加双缓冲把 SDIO 读写拉到极限

SPI 方案跑通后,如果项目对存储速度有要求,切到 SDIO 加 DMA 是必经之路。F407 的 SDIO 控制器支持 IDMA 模式,数据直接在 SDIO 和外设之间搬,不占 CPU。配置时把 SDIO 的时钟分频设到 48MHz 以下(F407 SDIO 时钟来自 PLL48CLK,最高 48MHz),数据宽度选 4 位,DMA 用 SDIO 专用的 IDMA 而不是通用 DMA 流。初始化阶段仍然要低速,识别完成后再切高速。

// SDIO + IDMA 多块读配置片段 void SDIO_DMA_ReadBlocks(uint32_t *buf, uint32_t sector, uint32_t count) { // 1. 配置 SDIO 数据控制寄存器 SDIO->DCTRL = 0; // 先清空 SDIO->DLEN = count * 512; // 总字节数 SDIO->DTIMER = 0xFFFFFFFF; // 超时拉满 SDIO->DCTRL = (9 << 4) | // 块大小 2^9 = 512 SDIO_DCTRL_DMAEN | // 使能 DMA SDIO_DCTRL_DTDIR | // 卡到主机 SDIO_DCTRL_DTEN; // 启动传输 // 2. 发 CMD18 多块读 SDIO_SendCmd(CMD18, sector, SDIO_RESP_SHORT); // 3. 等 DMA 完成中断,中断里发 CMD12 停止 while (!dma_done_flag); SDIO_SendCmd(CMD12, 0, SDIO_RESP_SHORT); SDIO_ClearFlag(SDIO_FLAG_DataEnd); }

DLEN必须是 512 的整数倍,DCTRL里的块大小字段填 9 表示 512 字节。DMA 传输完成中断里要立刻发 CMD12,晚了下一次传输会多收一块数据。双缓冲的思路是开两个缓冲区,DMA 填一个的时候 CPU 处理另一个,这样读写和业务逻辑并行,实测连续写能到 8MB/s 以上。注意 SDIO 的 IDMA 和通用 DMA 不能混用,中断优先级也要设好,SDIO 中断被别的外设挡住会导致传输超时。

验证性能时别只看单次读写,用f_write写一个几十 MB 的文件,看总耗时算平均速度。如果速度远低于预期,先查是不是每次写都触发了f_sync,再查 DMA 传输是不是被拆成了单块。我习惯在切换高速模式后先跑一遍全卡读写校验,把每个扇区写已知模式再读回来比对,确认没有偶发错误再上业务。从那以后我每次改 SD 卡驱动,不管改多小,都强制走一遍全卡校验加文件系统挂载测试,宁可多花十分钟,也不想到现场才发现数据存丢了。希望帮到你。

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

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

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

立即咨询