☰
STM32 SD卡SPI驱动与FatFs移植实战:从初始化到稳定读写
2026/9/28 12:45:14 网站建设 项目流程

1. 别被"5分钟"骗了:FatFs移植真正的时间花在哪

先把结论摆在前面:FatFs这个文件系统本身,从拿到源码到在STM32上跑通f_mount,如果你熟悉流程,确实用不了5分钟。但真正让大多数人卡住的,从来不是FatFs,而是它底下那层SD卡驱动——SPI模式初始化时序、CMD命令响应、块读写超时,这些才是吃时间的黑洞。所以这篇东西我会把重心放在"驱动适配"上,FatFs的移植反而讲得快,因为那部分真的就是复制粘贴加改几个宏。

FatFs是ChaN写的一个轻量级FAT文件系统模块,纯C、无平台依赖、可裁剪,ROM占用在几KB到十几KB之间,RAM占用更小,非常适合STM32这类资源受限的MCU。它支持FAT12/FAT16/FAT32和exFAT(需要单独配置),能读写文件、创建目录、获取文件信息,对于做数据记录、配置存储、固件升级包读取这类需求来说,是性价比极高的方案。

适合谁看:手上有STM32板子和一张SD卡、想快速把文件读写跑起来的人;已经移植过但卡在f_mount返回FR_NOT_READY或者FR_DISK_ERR的人;以及想搞清楚"为什么我的SD卡SPI模式初始化老失败"的人。如果你连SPI都没配过,建议先把SPI收发跑通再来看这篇,不然会有点吃力。

我下面讲的流程基于STM32F1/F4系列加标准外设库或HAL库都适用,SD卡走SPI模式(不是SDIO),因为SPI模式兼容性最好,接线简单,代价是速度慢一些。如果你追求速度,后面我会提SDIO的取舍。

2. 移植前必须想清楚的三个取舍

2.1 SPI模式还是SDIO模式,这不是随便选的

很多人一上来就问"哪个快",但选型不能只看速度。SPI模式只需要4根线(CS、SCK、MISO、MOSI),任意GPIO都能模拟,STM32的硬件SPI随便挑一个就行,接线容错率高,调试时用逻辑分析仪一抓就能看到命令交互。缺点是速度受限于SPI时钟,一般跑到18MHz到24MHz就算不错了,实际读写速度大概在几百KB/s到1MB/s出头。

SDIO模式是SD卡的原生接口,4位数据线并行,理论速度能到十几MB/s,STM32F4的SDIO配合DMA跑高速卡能到10MB/s以上。但SDIO的坑在于:时钟配置、DMA对齐、卡兼容性,尤其是某些杂牌卡在SDIO下初始化会莫名其妙失败,换SPI模式反而正常。

我的建议很直接:做数据记录、配置存储、偶尔读个文件,SPI模式足够,别折腾SDIO。只有当你需要连续高速写日志(比如音频采样、高速ADC数据流)时,才值得上SDIO。这篇按SPI模式讲,因为它是"5分钟能跑通"的那条路。

2.2 硬件SPI还是软件模拟SPI

STM32的硬件SPI外设用起来省心,配置好时钟极性和相位就行,收发由硬件完成,CPU占用低。但硬件SPI有个坑:不同STM32系列的SPI引脚是固定的,如果你的PCB已经把SD卡接在了某个不能复用为SPI的引脚上,那就只能用软件模拟。

软件模拟SPI的好处是引脚随便选,坏处是速度慢、代码里要手动翻转电平、时序靠延时保证。实测软件模拟SPI在72MHz的F1上大概能跑到1MHz左右的SCK,读写速度会明显下降。所以能硬件SPI就硬件SPI,这是第一优先级。

2.3 用现成驱动还是自己写

网上有大量STM32 SD卡SPI驱动的现成代码,但质量参差不齐。有些驱动在初始化时延时不规范,遇到高速卡就挂;有些没有处理CMD响应中的忙状态,写块时偶发失败。我的做法是:参考现成驱动的框架,但CMD命令的发送和响应解析自己重写一遍,因为这部分是稳定性的核心,必须自己心里有数。

下面这张表是我在实际项目中对比过的几种方案,供你选型参考:

方案接线复杂度实测读写速度稳定性适合场景
硬件SPI + 自写驱动低500KB/s~1MB/s高数据记录、配置存储
软件模拟SPI低100~300KB/s中引脚受限的板子
SDIO + DMA中5~10MB/s中高高速数据流、音频
SDIO + 中断中3~6MB/s中中等速率需求

3. SD卡SPI模式的初始化:90%的失败都在这

3.1 上电后的74个时钟为什么不能省

SD卡协议里有一条硬性规定:卡上电后,在CS为高、MOSI为高的状态下,必须至少给它发送74个SCK时钟,让卡完成内部上电初始化。这74个时钟不是随便说说的,少了卡可能不响应后续命令。

很多人用硬件SPI时忘了这一步,直接拉低CS发CMD0,结果卡毫无反应。正确做法是:SPI配置好后,先不管CS,直接发10个字节的0xFF(10×8=80个时钟,留点余量),然后再拉低CS开始发命令。

// SD卡上电初始化,发送至少74个时钟 static void SD_PowerOnDelay(void) { uint8_t i; SD_CS_HIGH(); // CS拉高 for(i = 0; i < 10; i++) { SPI_ReadWriteByte(0xFF); // 发80个时钟 } }

注意:这10个0xFF必须在CS为高的时候发,如果CS是低的,卡会把这些字节当成命令的一部分,直接乱套。

3.2 CMD0、CMD8、ACMD41这条命令链的逻辑

SD卡SPI模式的初始化是一条固定的命令链,顺序不能乱:

  1. CMD0:复位卡到idle状态。响应应该是0x01。
  2. CMD8:发送接口条件,检查卡是否支持2.7V~3.6V电压和是否是SDHC卡。响应0x01表示支持,同时会返回卡的电压范围。如果返回0x05(illegal command),说明是老卡(SD v1.x),后面ACMD41的参数要改。
  3. CMD58:读OCR寄存器,判断卡是不是高容量卡(SDHC/SDXC)。这个命令在ACMD41成功之后发。
  4. ACMD41:发送主机容量支持信息,让卡完成初始化。这个命令要循环发送,直到响应从0x01变成0x00。循环时每次要延时一段时间,不能死循环猛发。
  5. CMD58:再次读OCR,确认卡初始化完成,并获取CCS位判断容量类型。

这里最容易出问题的是ACMD41的循环。有些人写了个while(response != 0)死等,结果卡一直返回0x01,程序卡死。正确做法是加超时计数,比如循环200次还没成功就报错退出。

// ACMD41循环等待初始化完成 uint8_t SD_InitACM41(void) { uint8_t rsp; uint16_t retry = 0; do { rsp = SD_SendCmd(CMD55, 0, 0x01); // 先发CMD55 rsp = SD_SendCmd(CMD41, 0x40000000, 0x01); // HCS位置1 retry++; Delay_ms(10); } while((rsp != 0x00) && (retry < 200)); if(retry >= 200) return 1; // 超时失败 return 0; }

3.3 响应解析:R1、R3、R7别搞混

SD卡SPI模式下的响应有好几种格式,搞混了就会解析出错:

  • R1:1字节,最高位为0,其余位是错误标志。CMD0、CMD55、ACMD41、CMD58都返回R1。
  • R3:R1 + 4字节OCR。CMD58返回这个。
  • R7:R1 + 4字节。CMD8返回这个,包含电压信息和检查模式。

解析R1的时候,要注意卡在忙的时候会返回0xFF(MISO一直高),所以读响应要先等最高位变成0。我见过有人直接读一个字节就当响应,结果读到0xFF还以为卡坏了。

// 等待并读取R1响应,带超时 uint8_t SD_GetR1Response(void) { uint8_t rsp; uint16_t retry = 0; do { rsp = SPI_ReadWriteByte(0xFF); retry++; } while((rsp & 0x80) && (retry < 200)); // 最高位为1说明还在忙 return rsp; }

3.4 一个真实的排查案例

我之前遇到一块板子,SD卡初始化时CMD0能返回0x01,CMD8返回0x01,但ACMD41怎么循环都返回0x01,最后超时。换了三张卡都一样,说明不是卡的问题。

排查过程:先用逻辑分析仪抓SPI波形,发现CMD55和CMD41的命令字节发出去是对的,但CMD41的参数是0x40000000,这个HCS位是给SDHC卡用的。问题在于——这块板子的SPI时钟太快了,初始化阶段SCK跑到了18MHz,而SD卡协议规定初始化阶段SCK不能超过400kHz。

把SPI时钟分频调到400kHz以下,ACMD41立刻就通过了。初始化完成后,再把SPI时钟提到高速。这个坑非常典型:初始化必须低速,读写才能高速。

// 初始化阶段低速SPI SPI_Init(SPI_BaudRatePrescaler_256); // 72MHz/256 ≈ 281kHz SD_Init(); // 初始化完成后切换到高速 SPI_Init(SPI_BaudRatePrescaler_4); // 72MHz/4 = 18MHz

4. 块读写:单块和多块的差异与陷阱

4.1 CMD17读单块和CMD18读多块

读操作相对简单。CMD17读单块,发完命令后等一个数据起始令牌0xFE,然后连续读512字节,最后读2字节CRC(SPI模式下可以忽略但必须读掉)。

CMD18读多块,发完命令后可以连续读多个块,每块前面都有一个0xFE令牌,读完想要的块数后发CMD12停止传输。这里有个坑:CMD12发完之后,卡可能还在发送剩余数据,必须等到卡不忙了才能发下一条命令。

// 读单块 uint8_t SD_ReadBlock(uint32_t sector, uint8_t *buf) { if(SD_SendCmd(CMD17, sector, 0x01) != 0x00) return 1; if(SD_WaitToken(0xFE) != 0) return 2; // 等数据令牌 SPI_ReadBytes(buf, 512); SPI_ReadWriteByte(0xFF); // 读掉CRC SPI_ReadWriteByte(0xFF); return 0; }

4.2 CMD24写单块:忙状态判断是关键

写操作比读操作麻烦,因为写完数据后卡会进入忙状态,MISO会被拉低一段时间。如果你不等卡忙完就发下一条命令,命令会丢失。

写单块的流程:发CMD24,等响应0x00,发一个0xFE数据令牌,发512字节数据,发2字节CRC,然后等卡忙完(MISO变高)。

// 写单块 uint8_t SD_WriteBlock(uint32_t sector, const uint8_t *buf) { if(SD_SendCmd(CMD24, sector, 0x01) != 0x00) return 1; SPI_ReadWriteByte(0xFF); // 一个字节的间隔 SPI_ReadWriteByte(0xFE); // 数据令牌 SPI_WriteBytes(buf, 512); SPI_ReadWriteByte(0xFF); // CRC SPI_ReadWriteByte(0xFF); uint8_t rsp = SPI_ReadWriteByte(0xFF); if((rsp & 0x1F) != 0x05) return 2; // 数据响应错误 // 等卡忙完 uint16_t retry = 0; while(SPI_ReadWriteByte(0xFF) != 0xFF) { retry++; if(retry > 60000) return 3; // 超时 } return 0; }

提示:写块时的忙等待超时值要设得足够大。SD卡内部擦除一个块可能需要几百毫秒,如果超时设小了,会误判为写失败。我一般设60000次循环,配合SPI速度,大概能等几百毫秒。

4.3 多块写的预擦除:ACMD23的妙用

CMD25写多块时,如果直接连续写,卡内部会边写边擦除,速度慢且容易出错。更好的做法是先发ACMD23告诉卡"我要写N个块",让卡提前擦除这些块,然后再发CMD25连续写。

ACMD23的参数是要写的块数。发完之后再发CMD25,卡就知道要写多少块,内部可以优化擦除策略。实测这个操作能让多块写的稳定性明显提升,尤其是写大文件的时候。

4.4 扇区号还是字节地址:SDHC和SDSC的区别

这是另一个高频坑。SDSC卡(标准容量,小于2GB)的读写命令参数是字节地址,而SDHC/SDXC卡(高容量)的参数是扇区号。如果你用SDHC卡但按字节地址传参,读写会完全错位。

判断方法:初始化时CMD58返回的OCR寄存器,bit30是CCS位。CCS=1表示SDHC/SDXC,用扇区号;CCS=0表示SDSC,用字节地址。FatFs的disk_read/disk_write接口传进来的是扇区号,所以对于SDSC卡,你需要把扇区号乘以512再传给底层驱动。

// 根据卡类型转换地址 if(SD_CardType == SDHC) { addr = sector; // 扇区号直接用 } else { addr = sector * 512; // 字节地址 }

5. FatFs移植:真正5分钟的部分

5.1 源码文件怎么裁剪

FatFs的源码就几个文件:ff.c、ff.h、ffconf.h、diskio.c、diskio.h,还有可选的ffunicode.c(长文件名支持)。如果你不需要中文文件名和长文件名,可以把_USE_LFN设为0,这样能省不少ROM。

ffconf.h里几个关键配置:

宏建议值说明
_FS_TINY0用普通模式,性能更好
_USE_LFN0或1不需要长文件名就设0
_MAX_SS512扇区大小,SD卡固定512
_VOLUMES1挂载几个卷
_FS_RPATH0不需要相对路径就关掉
_FS_MINIMIZE0不裁剪功能

5.2 diskio.c里要改的五个函数

diskio.c是FatFs和底层驱动的桥梁,需要实现五个函数:

  • disk_initialize:调用你的SD_Init()
  • disk_status:返回卡状态,简单返回0就行
  • disk_read:调用SD_ReadBlock,注意多扇区时循环读
  • disk_write:调用SD_WriteBlock,多扇区时循环写
  • disk_ioctl:处理CTRL_SYNC、GET_SECTOR_COUNT、GET_SECTOR_SIZE、GET_BLOCK_SIZE

最容易出错的是disk_ioctl里的GET_SECTOR_COUNT,这个要返回卡的总扇区数。如果你返回0,f_mount可能成功但后续操作会出问题。总扇区数可以通过读CSD寄存器获取,或者简单点,用容量除以512估算。

DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { switch(cmd) { case CTRL_SYNC: return RES_OK; case GET_SECTOR_COUNT: *(DWORD*)buff = SD_SectorCount; return RES_OK; case GET_SECTOR_SIZE: *(WORD*)buff = 512; return RES_OK; case GET_BLOCK_SIZE: *(DWORD*)buff = 1; return RES_OK; default: return RES_PARERR; } }

5.3 f_mount之后为什么还是读写失败

f_mount成功只代表FatFs挂载了逻辑驱动器,不代表底层驱动没问题。很多人f_mount返回FR_OK,但一调f_open就返回FR_DISK_ERR,问题出在disk_read上。

排查顺序:先确认disk_read单独调用能不能读到正确数据(比如读扇区0,看最后两字节是不是0x55AA),再确认disk_ioctl的GET_SECTOR_COUNT返回值对不对,最后检查ffconf.h里的_MAX_SS是不是512。

我遇到过一次,f_mount成功但f_open失败,查了半天发现是disk_read里多扇区读的时候,扇区号没有递增,每次都在读同一个扇区。这种低级错误在调试时反而容易被忽略。

6. 那些文档里不会写的实操心得

6.1 杜邦线是SD卡调试的隐形杀手

SPI模式虽然接线简单,但对信号质量敏感。用杜邦线接SD卡模块,线长超过10厘米,高速SPI下就会出现读写不稳定。表现是:低速能读,高速读一会儿就出错;或者写小文件正常,写大文件就失败。

解决办法:要么把SPI速度降下来,要么把线缩短,要么在SCK和MOSI上串22欧姆到100欧姆的电阻做阻抗匹配。我自己的板子是把SD卡座直接画在PCB上,走线尽量短,问题就没了。

6.2 卡的热插拔检测不能省

产品里SD卡是要插拔的,如果不做检测,卡拔了之后程序还在读写,会卡死在忙等待里。最简单的做法是用一个GPIO检测卡座的CD引脚,或者定期读CID寄存器判断卡是否还在。

FatFs本身不处理热插拔,你需要在应用层做。我的做法是:每次读写前先调一次disk_status,如果返回STA_NOINIT就重新初始化。虽然有点耗时,但能避免卡死。

6.3 写文件后必须f_sync或f_close

FatFs有缓存机制,f_write之后数据可能还在缓存里没写到卡上。如果这时候断电,数据就丢了。所以写完关键数据后,要么调f_sync强制刷盘,要么调f_close。f_close会顺便更新目录项,更彻底。

做数据记录的项目里,我一般每写一批数据就f_sync一次,虽然会损失一点速度,但数据安全性高很多。

6.4 长文件名和中文支持要额外开FF_USE_LFN

如果你需要中文文件名,_USE_LFN要设为1或2,还要把ffunicode.c加进工程,并且配置_CODE_PAGE为936(简体中文)。注意_USE_LFN设为1时用的是静态缓冲区,设为2时用栈上的动态缓冲区,后者更省RAM但需要栈够大。

开了长文件名之后,ROM占用会增加不少,STM32F103C8这种64KB Flash的片子要留意空间。

6.5 格式化用f_mkfs还是PC格式化

新卡或者文件系统损坏的卡,可以用f_mkfs在MCU上格式化。但f_mkfs格式化出来的卡,在PC上可能识别不了,因为默认的簇大小和PC不一样。我的建议是:能用PC格式化就用PC格式化,选FAT32,簇大小默认。只有在没有PC的现场,才用f_mkfs应急。

f_mkfs调用时要注意,它会擦除整个卡,耗时可能几十秒,期间不能断电。

7. 从能跑到跑稳:几个进阶优化方向

7.1 用DMA减轻CPU负担

SPI收发如果用查询方式,CPU会一直被占用。STM32的SPI支持DMA,配置好DMA通道后,读写512字节可以由DMA完成,CPU可以去干别的事。对于需要同时处理其他任务的项目,DMA是必须的。

配置DMA时注意:SPI的TX和RX要分别配DMA通道,传输完成中断里处理后续逻辑。DMA的缓冲区要4字节对齐,否则在某些STM32系列上会出错。

7.2 多扇区读写减少命令开销

FatFs的disk_read/disk_write一次可能请求多个扇区。如果你在底层用CMD18/CMD25做多块读写,比循环调用单块命令快很多。实测读1MB数据,多块比单块循环快30%以上。

实现多块读写时,注意CMD18读完之后要发CMD12停止,并且等卡忙完。CMD25写多块时,最后要发一个0xFD停止令牌。

7.3 文件系统碎片化对写入速度的影响

FAT文件系统用久了会产生碎片,新写入的文件可能分散在多个不连续的簇上,导致写入速度下降。对于数据记录类应用,建议定期整理或者用固定大小的文件循环覆盖,避免碎片化。

一个实用技巧:创建文件时预分配空间(f_expand),这样文件在磁盘上是连续的,写入速度稳定。

7.4 掉电保护:不只是f_sync

工业场景下掉电是常态。除了f_sync,还可以考虑:用双备份文件,写新文件成功后再删旧文件;或者在文件头加校验和,读取时校验,损坏就丢弃。FatFs本身没有日志功能,掉电保护要靠应用层设计。

我在一个电表数据记录项目里,用的是"写临时文件→f_sync→重命名"的流程,重命名是原子操作,能保证要么是旧文件要么是新文件,不会出现半截文件。

8. 常见问题速查

现象可能原因排查方向
f_mount返回FR_NOT_READYSD卡初始化失败检查74时钟、SPI速度、接线
f_mount成功但f_open返回FR_DISK_ERRdisk_read有问题单独测disk_read,检查扇区号递增
写小文件正常,写大文件失败多扇区写有bug或SPI不稳定检查CMD25流程、降低SPI速度
读到的数据全是0xFF卡没响应或CS控制错误检查CS时序、命令发送
偶尔读写失败信号质量或忙等待超时不够缩短接线、加大超时
中文文件名乱码没开长文件名或代码页不对配置_USE_LFN和_CODE_PAGE
格式化后PC不识别f_mkfs参数与PC不一致用PC格式化,或调整簇大小

SD卡和FatFs这套组合,说简单也简单,说坑多也真多。核心就一句话:初始化阶段慢下来、命令响应等到位、忙状态判准确、写后记得刷盘。把这四点做到,基本就能从"能跑"到"跑稳"。至于那"5分钟",等你把驱动调通之后,再移植FatFs确实就是复制文件改宏定义的事——难的是前面那层驱动,那才是真正花时间的地方。

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

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

立即咨询