
简介基于STM32的SD卡读取与FATFS文件系统应用工程面向嵌入式入门开发者覆盖从SD卡硬件初始化、扇区读写到FATFS挂载与文件操作的整体流程可帮助理解SPI/SDIO通信时序、底层驱动封装与文件系统移植方法。资源包共229个文件以C源码和头文件为主各83个头文件用于接口声明C源码实现驱动与逻辑另有汇编启动文件、KEIL工程配置、多语言编码映射文件等压缩包仅1.48MB目录结构清晰便于按功能模块快速查找。已有3372人学习适合正在学习STM32或计划在项目中加入文件存储功能的开发者。工程内提供可直接编译的KEIL项目、SD卡底驱、FATFS配置及文件读写示例详细演示f_open、f_read、f_write、f_close等API调用流程并融合错误处理与调试信息可帮助读者理清硬件接线、通信时序、参数配置和异常排查等关键环节达到学练结合的效果后续还能扩展用于数据记录、远程监控等场景。 做嵌入式项目凡是要落地的数据十有八九最后都会想到SD卡。仪表记录日志、采集器存曲线、设备远程升级放固件副本、甚至最朴素的“把几个配置塞进一张能拔出来插电脑的卡里”SD卡都是成本最低、最通用的大容量存储介质。我最早在STM32上跑通SD卡和FATFS文件系统是在做环境采集终端的时候——上午写完的代码下午把卡插电脑上直接能看到CSV文件那种踏实感跟只靠固定内存攒数据完全不是一个体验。这篇就把我实际用STM32读SD卡、移植FATFS文件系统的思路完整写一遍包括接口选型、底层时序、移植要点以及一堆实测里踩过的坑。适合刚开始接触文件系统的嵌入式开发者也适合被各种诡异问题卡住的老手拿来对照。1. 为什么是FATFS文件系统选型的现实逻辑1.1 裸扇区读写不是不能用但你在给自己挖坑如果你直接在STM32上对SD卡做扇区读写第一感觉其实挺爽的读第0扇区512字节全是FF写进去再读回来数据对了成就感满满。但真正要把“一个文件”或“一组数据”管起来麻烦立刻出现文件从哪个扇区开始、长度是多少、文件目录怎么组织、删除后空间怎么回收、掉电的时候写了一半怎么办……这些全部要自己实现工作量不亚于重造一个简易文件系统。我早期做过一版直接写固定扇区地址的日志方案当时觉得结构简单、效率高。后来要增加文件数量、支持按文件名读取历史数据那个扇区地址表差点把我改崩溃。经验一句话如果你的需求只是“把一块区域当存储”裸读写没问题但只要你提到“文件”“目录”“文件名”这些词就该停手去选文件系统了。1.2 FATFS到底解决了什么问题FATFS是ChaN写的开源FAT/exFAT文件系统模块整个源码核心就是ff.c、ff.h、diskio.c这几个文件全部用ANSI C写成不绑定任何硬件平台。它最大的特点是不关心底层是SD卡、U盘、Nor Flash还是RAM盘只要你提供初始化、读扇区、写扇区、ioctl这几个基础能力它就能在上面给你完整的文件、目录、长文件名、时间戳、追加写、格式化等能力。最关键的一点是用FATFS格式化和写入的卡拔下来插到电脑上Windows、Linux、macOS直接就能读。不需要专用工具、不需要转换软件客户和同事拿到一张卡就能看数据。这一点在做产品时极其重要它是“能把技术方案交付给非技术人”的关键。1.3 为什么不是LittleFS或SPIFFS嵌入式圈子里文件系统不止FATFS一个但适合SD卡的并不多。LittleFS更适合MCU内部Flash和SPI Nor Flash主打掉电安全和磨损均衡SPIFFS面向小容量Nor Flash不支持目录如今也已经比较老旧。它们都很优秀但设计目标主要是裸Flash不是“移动介质”。SD卡芯片和电脑之间存在天然的兼容性要求FATFS在这方面几乎没有对手。所以我的结论很简单介质是SD卡首选就是FATFS不用犹豫。2. SPI还是SDIO接口选型的隐性成本2.1 两种模式最本质的差别SD卡协议原生是SDIO模式4bit数据总线STM32里对应SDIO外设速度上限很高。但代价是引脚多CLK、CMD、D0到D3共6根还要处理DMA、FIFO、硬件CRC调试链路相对长。SPI模式则相当于把SD卡“降级”成一个SPI从设备主机只需要SCK、MOSI、MISO、CS四根线命令简单逻辑分析仪一抓就懂但传输位宽只有1bit速度上限低一截。对比项SPI模式SDIO模式引脚数4根6根CLK/CMD/D0-D3常见传输速度10-20MHz位时钟实际吞吐约1-2MB/s4bit48MHz吞吐可达十几MB/s协议复杂度低命令帧结构简单高需处理DMA、FIFO、硬件CRC排错难度低逻辑分析仪容易看高出问题点比较隐蔽适合场景日志、配置、低频采集图片、音频、连续大数据流2.2 我为什么在日志场景选了SPI很多教程默认用SDIO因为“看起来快”。但我的建议是除非你的连续写入速度需要超过2-3MB/s否则优先选SPI。原因有三个。第一是代码量SPI驱动代码一个晚上能调通SDIO光初始化顺序和DMA搬运就够摸索几天。第二是故障点SPI模式下出问题用逻辑分析仪抓CLK和MISO波形对比SD卡时序很快就能定位SDIO高速模式下差分信号、DMA配置、FIFO溢出参在一起不太好查。第三是实际需求一秒钟写几KB日志的应用SPI的1MB/s左右吞吐完全够用没必要自找麻烦。当然如果项目是做图像采集、音频录音、高速数据记录那直接上SDIO不要在SPI上死磕。选型时先估算“峰值写入速率”再决定接口这个顺序很关键。2.3 硬件连接里必须处理的细节直连SD卡模块时第一注意电平一定是3.3V别从5V引脚取电SPI的MOSI、SCK信号线也不能直接接5V电平否则有烧卡风险。第二注意上拉CS、MISO这类信号线建议上拉4.7k-10k避免浮空时读到随机电平。第三注意卡座信号CD卡检测引脚配置成上拉输入插卡后电平变化可以直接作为“卡在位”状态WP写保护引脚如果不用务必上拉到无效电平不然就会出现一张没上锁的卡被系统判断为写保护。如果你板上其他外设和SD卡共享同一个SPI总线还要保证切换设备时CS严格独立控制。SD卡的CS保持高电平SCK上多余的脉冲才不会把卡“打”出状态。这个问题在带屏幕的板子上很常见稍不提防就会表现为“屏幕正常但读卡不稳定”。3. 底层驱动实战从74个时钟周期到扇区读写3.1 初始化时序里最容易卡死的两个点SD卡上电后先别急着高速通信。它内部的稳压器需要时间稳定所以规范要求主机先发送至少74个时钟脉冲而且这阶段时钟频率不能超过400kHz。很多代码里都有这个“先发10个0xFF”的动作但如果你不知道这是在等卡内部稳定后面遇到诡异问题时会一头雾水。我实际跑过的初始化流程是这样初始化SPI外设分频到100-400kHzCPOL0、CPHA0CS拉高先发至少10字节0xFF等效80个时钟脉冲CS拉低发送CMD00x40 00 00 00 00 95等待R1返回0x01表示卡进入idle状态发送CMD80x48 00 00 01 AA 87判断是否SD V2.0正常会返回0x01并跟4字节“00 00 01 AA”循环发送CMD55ACMD41参数带0x40000000表示支持SDHC直到返回0x00卡初始化完成发送CMD58读OCRbit30为1说明是SDHC/SDXC后续按块寻址把SPI速度提升到正常工作速率这个流程里最容易出问题的有两个点。一是ACMD41循环没有超时卡异常时死等二是CMD0和CMD8的CRC值写错。SPI模式下主机一般不用计算CRC但CMD0和CMD8这两个特殊命令还是要求CRC正确因为卡此时还没完全切到SPI模式。CMD0的CRC固定是0x95CMD8固定是0x87直接查表填上就行。3.2 读写单块命令的本质数据流永远不要断SD卡读单块用的是CMD17。主机发完命令帧等R1返回0x00然后继续给SCK时钟直到读到起始令牌0xFE接下来读512字节数据最后再读2字节CRC并丢弃。写单块用CMD24发命令等R1返回0x00发0xFE起始令牌再送512字节再补2字节CRC最后要持续读SPI直到卡不再拉低总线表示busy结束。有一件事很多教程不提SPI模式下卡不校验数据区的CRC主机发的CRC也可以填0xFF但数据流是连续的你多读2字节、少读2字节都会导致后面数据错位。我第一次调写函数时因为没把卡回的那2字节CRC读完结果连续写两个扇区时第二个扇区地址完全不正常。这类问题用逻辑分析仪看波形最明显。3.3 地址问题扇区号不是字节地址SD卡命令里的地址字段传的是“扇区号”不是字节地址。对SDSC卡地址等于扇区号乘以512对SDHC/SDXC卡地址就是扇区号本身。很多新手第一次读第二个扇区时直接传0x200结果卡返回的数据完全对不上。FATFS往底层传的参数本来就是扇区号千万别在disk_read里手滑乘512再传下去否则SDHC卡会跳错扇区。总扇区数一般从CSD寄存器解析标准SD卡驱动模板里都有现成公式不要自己拍脑袋写固定值。16GB卡和64GB卡的CSD布局不同分支处理错了容量会差得离谱FATFS后面的格式化和对文件系统识别都会跟着出错。4. FATFS移植的暗坑mount、open、读写背后的真实逻辑4.1 diskio层要填的六个函数FATFS不认识SD卡它通过diskio.c调用底层能力。移植时你至少要实现这几个函数disk_initialize负责初始化SD卡硬件disk_status返回卡当前状态disk_read和disk_write负责按扇区读写disk_ioctl要支持GET_SECTOR_COUNT、GET_SECTOR_SIZE、GET_BLOCK_SIZE写卡还要CTRL_SYNCget_fattime返回当前时间戳没有RTC时就返回一个固定日期比如2024年1月1日格式按FATFS的位域规定来。这里有个容易忽略的问题disk_ioctl只实现了一部分FATFS可能能挂载但格式化不成功或者格式化后容量显示不对。GET_SECTOR_COUNT必须在卡初始化时就从CSD算出来否则f_mkfs会把卡格式化成错误容量。这套diskio接口在GD32、AT32这些国产MCU上同样成立底层换成对应SPI或SDIO外设实现就行应用层代码基本不用动。这也是FATFS最值钱的地方。4.2 ffconf.h里那几个开关移植FATFS时ffconf.h才是真正影响项目体验的地方。FF_USE_LFN控制长文件名支持设为0不支持长文件名文件超过8.3格式就访问不了设为1或2打开支持但FIL和DIR结构体会增加几百字节RAM。FF_CODE_PAGE设为936才能支持GBK中文文件名默认437只能处理英文。FF_USE_MKFS打开后才能在STM32侧直接格式化SD卡FF_USE_STRFUNC打开后f_printf才能用不然编译都过不了。RAM紧张时要特别注意STM32F103C8这类只有20KB RAM的芯片打开LFN后一个FIL对象就要几百字节再加上其他缓冲区一不小心就堆栈溢出。这种问题表现起来很隐蔽经常是文件操作到一半就HardFault排查半天才发现是内存不够。4.3 f_mount的版本差异和挂载时机FATFS R0.12之前f_mount第三个参数传1表示立即挂载R0.12之后f_mount只负责注册工作区真正的挂载在第一次访问卷时自动发生。所以很多老教程里的代码移植过来后f_open会返回FR_NOT_READY第一反应以为是SD卡没初始化好其实只是卷还没挂载。最稳妥的写法是在f_mount之后调用一次f_getfree强制触发挂载并检查返回值。如果卡里有分区表还要注意路径无分区格式化的卡用“0:/xxx.txt”带MBR分区的卡有效卷可能挂在“0:1:”。这个坑在64G以上的大卡上特别容易遇到因为大卡经常被工具分成多个区。4.4 文件操作示例FATFS fs; FIL file; UINT bw; FRESULT res; res f_mount(fs, 0:, 0); if (res ! FR_OK) { // 卷挂载失败可以打印错误码排查 return; } res f_open(file, 0:/log.txt, FA_OPEN_ALWAYS | FA_WRITE); if (res FR_OK) { f_lseek(file, f_size(file)); // 追加模式定位到文件末尾 f_printf(file, temp%.1f\r\n, temp); f_sync(file); // 关键数据写入后同步 f_close(file); }这个例子里有几个细节值得强调。FA_OPEN_ALWAYS会打开已存在文件但不会清空它指针默认在文件开头所以直接f_write会覆盖原内容要追加必须先用f_lseek定位到文件末尾。很多人栽在这个地方写了几次发现文件永远只有最后一条记录。另一个细节是f_syncf_close只关闭句柄不代表数据已经全部刷到卡上在长时间运行的设备里每写完一个关键记录就f_sync一次掉电丢数据的概率会小很多。5. 实测翻车现场SD卡开发中的典型故障排查链路5.1 初始化失败CMD0永远等不到0x01排查顺序很重要先确认SPI外设真的在发数据用示波器或逻辑分析仪测MISO和SCK。如果读回来的全是0xFF先查MISO引脚配置很多STM32的MISO要设成上拉输入配置成浮空输入时读回的一直是FF。接下来查CS控制看代码里CS拉低后SCK脉冲有没有正常出来。再查分频器有没有生效初始化阶段SPI时钟必须低于400kHz。最后还要确认主时钟本身没问题晶振匹配电容错得离谱时系统时钟完全不对SPI分频出来的速率根本不是你预期值。5.2 卡能初始化但f_open返回FR_NO_FILESYSTEM这表示卡里的数据不是FATFS能识别的FAT文件系统。可以先调用f_mkfs在板上格式化或者把卡插到电脑上格式化成FAT32再插回来。有个细节超过32GB的卡Windows自带格式化工具默认给exFATFATFS默认配置不支持exFAT会继续报错。要么用第三方工具格式化成FAT32要么在ffconf.h里打开FF_USE_EXFAT并解决授权问题。特别注意exFAT支持涉及授权商用产品里不是代码设个1就完事。5.3 “卡没锁但写保护”是什么鬼SD卡侧边的锁是一个机械滑块它触发的是卡座上的写保护探针并不会让卡内部变成只读。平板状态下卡的写保护状态由卡座的WP引脚电平决定。如果这个引脚悬空或者接错就会出现“插哪张卡都写保护”的现象。排查顺序换一张卡排除卡本身问题量卡座的WP引脚电平检查卡槽的机械开关是否接触不良。有些转接卡的锁开关方向做反了滑块在OFF位置时反而把WP拉低这类非标卡也别用在产品里。5.4 读数据偶尔出错尤其是多块读先降低SPI时钟试试很多卡在20MHz以上并不稳定降到10MHz或8MHz就好了。然后检查电源纹波SD卡写入时电流尖峰很大电源余量不足会出现bit翻转这种问题看代码根本没用直接在卡槽附近加10-100uF电容能缓解。接线用杜邦线的话长度越短越好优先用焊接或排线。还有DMA缓冲区要对齐4字节H7、F7这类带D-Cache的芯片DMA读写前要做Cache clean或invalidate否则你看到的是“RAM里的数据是新的卡里却是旧的”这种诡异现象。6. 性能与可靠性日志场景里摸出来的几个优化习惯6.1 攒够一个扇区再写性能差了一个数量级SD卡是按512字节扇区访问的FATFS底层读写最终也是按扇区进行。如果你的f_write一次只写20字节底层可能要做读-改-写还要频繁更新FAT表性能会非常难看。实测下来攒够512字节甚至2-4KB再一次性f_write写入速率能提升好几倍。做数据记录项目时可以在用户态维护一个缓冲区攒够一定量再落盘。6.2 写完后f_sync比f_close更可靠长时间运行的设备里打开文件后每写完一个关键记录就f_sync一次而不是集中到某个时刻才f_close。f_close关闭句柄不代表数据都刷进卡了f_sync才是真正把FATFS内部缓存刷到介质上的操作。对断电可靠性要求高的项目这个习惯能少丢很多数据。6.3 批量生产前先给SD卡做兼容矩阵我后来养成一个习惯开发阶段就把手边能找到的卡全都测一遍不同品牌、不同容量、不同速度等级跑读写几百次记录哪些卡有问题。同时可以在电脑上用H2testw这类工具验证卡是否为扩容卡。很多“偶发读错误”其实不是代码问题而是卡本身不合格。产品发货前把这个测试做了能省掉大量售后排查时间。6.4 两个可选的进阶方向如果SPI读写的性能不够可以换SDIO加多块读写指令CMD18/CMD25配合FATFS的连续簇写吞吐能再上一个台阶。如果板上有RTC把get_fattime做成真实时间文件时间戳会非常有用后续按日期找日志会省很多事。如果从头再让我做一遍这个项目我大概率还是会先选SPIFATFS起步把文件系统跑通再根据速度瓶颈决定要不要换SDIO。这个顺序到现在都不过时。本文还有配套的精品资源点击获取