☰
STM32F030C8T6 SPI读写SD卡与FatFs文件系统移植详解
2026/10/2 22:35:28 网站建设 项目流程

简介:在物联网、工业控制、消费电子等场景中,嵌入式设备常需外置存储来记录运行数据。STM32F030C8T6单片机通过SPI接口读写SD卡,并集成FatFs文件系统的完整工程代码,基于HAL库开发,面向需要为嵌入式系统增加外部存储的开发者,适合从入门到进阶阶段学习参考。资源共928个文件,压缩包8.32MB,以C源码和头文件为主,辅以汇编启动文件、ICF链接脚本、UVProjx工程文件以及IAR/CMSIS库文件,完整覆盖SPI初始化、SD卡底层驱动、FatFs移植与文件操作接口,并包含硬件抽象层配置。已有2239人学习下载。工程内目录组织清晰,既可直接编译烧录验证读写效果,也能作为二次开发模板,帮助理解SD卡初始化时序、SPI通信配置、文件系统挂载及文件读取写入等API的具体用法,避免从零编写底层驱动,是掌握嵌入式存储方案落地的高效参考资料。

1. 方案整体设计:为什么是SPI + SD卡 + FatFs

这套组合在嵌入式项目里太常用了。STM32F030C8T6属于F0系列入门级芯片,主频最高48MHz,Flash 64KB,RAM 8KB,成本压得很低。把它和SD卡通过SPI接起来,再跑一个FatFs文件系统,就能解决一个很实际的问题——单片机在运行过程中产生的数据往哪儿存。

物联网设备要记录传感器历史曲线、工控设备要保存参数配置、便携仪表要存储采样日志、字库和图片需要外部存储扩展……这些都是SPI + SD卡 + FatFs方案的典型应用场景。只要数据量超过单片机内部Flash的容量,或者需要把数据导出到电脑上分析,这套方案就是最低成本、最成熟的选择之一。

1.1 SPI协议的核心特性

SPI全称Serial Peripheral Interface,是摩托罗拉早年定义的一种同步串行外设接口。它的本质就是一根时钟线加两根数据线的移位寄存器互联:主机提供SCK时钟,MOSI(Master Output Slave Input)往从机发数据,MISO(Master Input Slave Output)从从机收数据,再配合一个片选信号CS选中目标设备。

SPI和I2C、UART最大的区别在于:它是全双工、同步、高速的。同一时刻主机既能发也能收,不会出现I2C那种一帧数据还要靠应答位来塞时钟的情况。在典型配置下,STM32F030C8T6的SPI1最高可以跑到18Mbit/s左右,虽然不如SDIO模式的性能,但对于日志写入、配置存储这类应用绰绰有余。

SPI有四种工作模式,由时钟极性CPOL和时钟相位CPHA组合而来:

  • Mode 0:CPOL=0,CPHA=0,空闲时SCK为低电平,数据在第一个边沿采样
  • Mode 1:CPOL=0,CPHA=1,空闲时SCK为低电平,数据在第二个边沿采样
  • Mode 2:CPOL=1,CPHA=0,空闲时SCK为高电平,数据在第一个边沿采样
  • Mode 3:CPOL=1,CPHA=1,空闲时SCK为高电平,数据在第二个边沿采样

SD卡在SPI模式下固定使用Mode 0。这个细节很多人会忽略,如果底层SPI初始化配成了Mode 3,卡初始化大概率会失败。

1.2 SD卡为什么用SPI模式而不是SDIO

SD卡原生支持两种访问方式:SDIO模式和SPI模式。STM32F030C8T6这颗芯片根本没有SDIO外设接口,所以只能走SPI。对于没有SDIO控制器的MCU来说,SPI模式几乎是唯一选择。

SPI模式访问SD卡需要额外做一套初始化流程——SD卡上电后默认进入SDIO模式,必须向它发送特定的命令序列,比如CMD0(GO_IDLE_STATE),让它切换到SPI模式。之后还要发送CMD8、ACMD41这些命令完成初始化。整个过程要求主机遵循严格时序,比如CMD0发完之后要持续给至少74个时钟周期才能让卡完成内部初始化。

SD卡SPI模式下支持的最大速率通常是25MHz左右,但实际使用中考虑到走线、电平匹配和软件开销,控制在4~12MHz比较稳妥。STM32F030C8T6的SPI1分频后跑8~9MHz是最常用的选择。

1.3 FatFs文件系统解决的问题

裸片SD卡存储数据只能按原始扇区地址进行读写,这意味着你记住的是“第1000个扇区存什么”,而不是“temperature.txt存在哪个文件里”。一旦数据要导出到电脑上分析,或者要同时管理多个数据文件,这是不可接受的。

FatFs是一个开源的FAT文件系统模块,由ChaN开发,专门面向嵌入式设备。它支持FAT12、FAT16、FAT32格式,占用资源也很少,经过裁剪后ROM消耗在10KB左右,RAM根据配置大约几百字节到1KB多点,STM32F030C8T6的Flash和RAM完全扛得住。

FatFs通过一层底层驱动接口(diskio.c)把文件系统逻辑和具体存储介质隔离开来。只要实现了disk_initialize、disk_read、disk_write、disk_ioctl这几个函数,FatFs就能像标准库一样用f_open、f_write、f_read来操作文件。这正是这个方案里最核心的移植工作。

提示:FatFs本身提供完整的文件分配表管理、目录结构、长文件名解析等功能,代码量不小,但全部是与硬件无关的逻辑。真正需要你自己写的只有底层SPI驱动和SD卡命令层。

2. 硬件准备与接线细节

搞嵌入式,硬件是第一步。STM32F030C8T6有很多封装形态,我建议直接用最小系统板加TF卡模块来搭原型,这样不用自己画PCB也能快速验证代码。

2.1 元件清单

  • STM32F030C8T6最小系统板一块
  • Micro SD卡模块一块(自带电平转换的最好,没有的话需要处理电平)
  • Micro SD卡或者TF卡一张,容量建议4GB~32GB
  • 杜邦线若干
  • 逻辑分析仪或者示波器(调试SPI时序用,可选但推荐)

SD卡模块很多是直接把卡座的引脚引出来的,没有电平转换电路。这种模块接5V单片机一般没问题,但STM32F030C8T6是3.3V IO,供电电压也必须是3.3V,所以直接接3.3V供电就行。

2.2 SPI引脚分配与接线表

我实测的接法是STM32F030C8T6的SPI1,对应的默认引脚是:

功能引脚接SD卡模块
SCKPA5SCK
MOSIPA7MOSI/DIN
MISOPA6MISO/DO
CSPA4CS
GNDGNDGND
3.3V3.3VVCC

注意,这里CS用的是SPI硬件片选引脚PA4,但我在工程里是用普通的GPIO输出控制片选,不用SPI外设的NSS自动管理。原因后面展开讲。如果你用CubeMX配置,PA4作为GPIO输出,PA5、PA6、PA7复用为SPI1功能。

2.3 供电与上拉的注意事项

SD卡模块的VCC直接接3.3V,不要接5V。SD卡本身是3.3V器件,虽然有些模块带电平转换芯片,但5V直接灌到SPI信号线上会烧坏MCU引脚和卡座。

另外一个坑是:SD卡SPI模式下,MISO(DO)引脚的输出驱动能力较弱,最好在模块的DO引脚到3.3V之间接一个10kΩ上拉电阻。很多现成的SD卡模块已经内置了上拉,但如果你用的是自己画的板子或者光秃秃的卡座,这个上拉不能省。不加上拉的话,卡在初始化阶段读回来的数据经常会莫名其妙多几个0xFF或0x00,非常折磨人。

供电和上拉都搞定以后,建议先用万用表量一下SPI四个引脚对地电压,确认没有短路,再上电测试。我见过好几块所谓“坏”的SD卡模块,最后排查出来是模块上电容虚焊导致的3.3V供电波动。

注意:STM32F030C8T6的GPIO大部分容忍5V,但MISO是输入引脚,如果SD卡模块没有做过电平转换,且模块又是5V供电,那么MISO上5V高电平会直接怼进PA6,长期使用有风险。这个方案里统一用3.3V供电,就绕开了这个问题。

3. FatFs移植与SPI底层驱动实现

写好SPI底层驱动并成功移植FatFs,是这个项目最花时间的地方。我会把代码逻辑拆开讲,包括为什么这样写。

3.1 获取FatFs源码与目录结构

去FatFs官网下载ff14源码包,解压后需要关注这几个文件:

  • ff.h / ff.c:文件系统核心实现,不用改
  • ffconf.h:配置文件,需要个性化修改
  • diskio.h / diskio.c:底层驱动接口,需要自己实现
  • integer.h:数据类型定义,一般不用动

移植时,ffconf.h里有几个宏很重要。我用的是:

#define FF_USE_LFN 1 #define FF_USE_STRFUNC 1 #define FF_USE_MKFS 1 #define FF_USE_FASTSEEK 1 #define FF_VOLUMES 1 #define FF_MIN_SS 512 #define FF_MAX_SS 512

其中FF_USE_LFN要按需打开,短文件名对小项目够用,但要在文件名里带日期会很不方便,所以我开了长文件名支持。对应地,需要提供ff_mem_alloc和ff_mem_free两个内存操作函数。因为STM32F030C8T6的RAM只有8KB,我直接用C库的malloc/free,但要确保RAM堆设置足够,一般256字节就够了。

3.2 SPI初始化的两种方式

初始化SPI外设,常见的有两条路:一条是用STM32CubeMX生成代码,另一条是直接手工写寄存器。

CubeMX方式配置路径是SPI1 -> Mode选择Full-Duplex Master,然后在Parameter Settings里设置:

参数值
Data Size8 Bits
First BitMSB First
Prescaler16(即8MHz,主频48MHz)
CPOLLow
CPHA1 Edge
NSSSoftware
CRCDisabled

注意,SD卡SPI模式要求Mode 0,也就是CPOL=Low、CPHA=1Edge。如果配置成其他模式,SD卡会完全无法通信。

手工写寄存器的初始化代码也给出参考,实际效果一样:

void SPI1_Init(void) { RCC->AHBENR |= RCC_AHBENR_GPIOAEN; RCC->APB2ENR |= RCC_APB2ENR_SPI1EN; GPIOA->MODER |= (GPIO_MODER_MODER5_1 | GPIO_MODER_MODER6_1 | GPIO_MODER_MODER7_1); GPIOA->MODER &= ~(GPIO_MODER_MODER5_0 | GPIO_MODER_MODER6_0 | GPIO_MODER_MODER7_0); GPIOA->AFR[0] |= (0x5 << (5 * 4)) | (0x5 << (6 * 4)) | (0x5 << (7 * 4)); GPIOA->OSPEEDR |= (GPIO_OSPEEDER_OSPEEDR5 | GPIO_OSPEEDER_OSPEEDR6 | GPIO_OSPEEDER_OSPEEDR7); SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_SSI | SPI_CR1_SSM | (8 << 3); SPI1->CR2 = SPI_CR2_DS_0 | SPI_CR2_DS_1 | SPI_CR2_DS_2; SPI1->CR1 |= SPI_CR1_SPE; }

这里有个容易踩的坑:SPI1->CR1的BR[2:0]位没有清零就通过移位赋值,导致分频系数错误。正确做法是先把CR1的BR位清零,再赋值。所以最稳的还是CubeMX生成,手工改寄存器适合对F0寄存器特别熟的人。

3.3 底层SPI收发字节函数

SPI收发是同一个函数,因为SPI是全双工——你往数据寄存器写一个字节,同时就能读到从机返回的一个字节。核心就一个函数:

uint8_t SPI1_ReadWriteByte(uint8_t data) { while ((SPI1->SR & SPI_SR_TXE) == 0); SPI1->DR = data; while ((SPI1->SR & SPI_SR_RXNE) == 0); return (uint8_t)SPI1->DR; }

如果读到的数据总是0xFF,而发送正常,很可能是MISO引脚配置错误,或者接线虚连,用万用表量一下最直接。如果读到的数据全是0x00,大概率是SCK极性和相位配错了。

3.4 文件系统与diskio接口实现

FatFs要求diskio.c实现这几个函数:

DSTATUS disk_initialize(BYTE pdrv); DSTATUS disk_status(BYTE pdrv); DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count); DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count); DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff);

其中disk_initialize的核心就是调用之前提到的SD卡初始化命令序列:发送CMD0让卡进入SPI模式,发送CMD8检查卡是否支持SDHC,发送ACMD41反复查询卡是否完成上电初始化。CMD8这个命令比较关键,不支持SDHC的旧卡(一般为2GB以下)会返回错误,这时候应该直接跳过SDHC检测逻辑。

disk_read和disk_write就是打包扇区地址,向SD卡发送CMD17(读单个扇区)、CMD18(读多个扇区)、CMD24(写单个扇区)、CMD25(写多个扇区)等命令。FatFs默认按扇区访问,512字节一个扇区,所以sector地址就是LBA地址,直接映射到CMD17/CMD24的参数里。

disk_ioctl里最常用的是CTRL_SYNC和GET_SECTOR_SIZE。CTRL_SYNC在写操作完成后调用,作用是等待SD卡完成内部刷写,避免数据丢失;GET_SECTOR_SIZE在f_mkfs或挂载时被调用,返回512即可。

4. 实操踩坑实录:初始化、挂载与文件读写

理论说了一大堆,真正写代码的时候,有几个坑是百分之百会遇到的。

4.1 初始化失败总是卡在CMD8

我先贴一下我的SD卡初始化函数的核心流程:

uint8_t SD_Init(void) { uint8_t retry = 0; // 1. 给SD卡至少74个时钟周期 for (uint8_t i = 0; i < 10; i++) { SPI1_ReadWriteByte(0xFF); } // 2. 发送CMD0,进入SPI模式 // CS拉低 SD_CS_LOW(); SD_SendCmd(CMD0, 0x00000000, 0x95); // 3. 发送CMD8,判断是否支持SDHC // ... // 4. 循环发送ACMD41,直到卡就绪 for (retry = 0; retry < 100; retry++) { // ... } // 5. 读取卡OCR,设置块大小 SD_SendCmd(CMD16, 512, 0xFF); SD_CS_HIGH(); SPI1_ReadWriteByte(0xFF); }

最难搞的是第一步。很多新手把上电延时写成了简单的delay(100ms),以为延时够了,但实际上系统复位后SPI时钟还没跑起来,SD卡可能还没进入SPI模式。正确做法是上电后让CS保持高电平,然后疯狂给SCK发0xFF,持续至少74个周期。

操作顺序一定是:先给时钟和0xFF让卡自检,再拉低CS,再发命令。反过来的话,卡可能一直处于空闲状态不响应。

如果CMD8的返回值不对(不是0x01),大概率是卡不支持SDHC协议,或者你的卡是SDXC,容量超过32GB,格式也不兼容FAT32。实测下来,64GB及以上容量的SDXC卡在SPI模式下经常出现CMD8能通过但ACMD41超时的情况,建议换一张4GB~32GB的卡测试。

4.2 挂载失败与格式化陷阱

初始化成功后,调用f_mount立刻失败,返回FR_NO_FILESYSTEM。这通常意味着卡上没有合法的FAT分区或者没有格式化过。用读卡器插到电脑上,把SD卡格式化为FAT32格式(不是exFAT),默认分配单元大小即可。

我因为这个问题折腾过一整个下午。第一次用Windows自带格式化工具格式化成exFAT,FatFs默认配置只支持FAT12/16/32,直接挂载失败。后来用SDFormatter工具格式化成FAT32,所有问题迎刃而解。

如果手头只有一张卡,不想拔来拔去,也可以直接用FatFs自带的f_mkfs函数在单片机里格式化。在f_mount失败后调用f_mkfs,再重新f_mount,一样能搞定。但注意f_mkfs会摧毁卡上所有数据,别误格式化到重要的卡。

4.3 文件写入与读出完整示例

挂载成功之后,写文件就是一个典型的三步操作:

FIL file; FRESULT res; UINT bw; res = f_open(&file, "log.txt", FA_CREATE_ALWAYS | FA_WRITE); if (res == FR_OK) { res = f_write(&file, "hello stm32 sd card\n", 20, &bw); f_close(&file); }

所有操作完成后,一定要f_close。FatFs有个缓存机制,f_close之前数据可能还停留在内部缓冲区里,这时候断电,文件大小和内容对不上。f_close不仅释放文件对象,更重要的是把脏缓存刷回SD卡。

读文件也类似,f_open的打开方式换成FA_READ,然后f_read把数据读进缓冲区:

BYTE buf[512]; UINT br; res = f_open(&file, "log.txt", FA_READ); if (res == FR_OK) { res = f_read(&file, buf, sizeof(buf), &br); f_close(&file); }

我在一次实际项目里就是用这个流程把电池管理系统的历史曲线数据周期写入SD卡,每天生成一个以日期命名的文件,断电后重新上电还能继续追加。用FatFs的f_getfree检查剩余空间,空间不足时自动删除最早的日志文件,这样整个存储方案闭环了。

5. 常见问题与排查技巧速查

回到标题里那些热搜词,SD卡SPI通信失败可以说占据了80%的踩坑案例。我整理了一个排查表,按优先级排列,可以照着顺序查。

5.1 初始化失败排查顺序

现象可能原因解决方法
卡一直返回0xFF上电时钟不足74周期循环发送0xFF,持续延时,确保CS高
CMD0返回0x7FSPI模式配置错误检查CPOL、CPHA,必须Mode 0
CMD8返回错误卡为SDXC或协议不支持换4GB~32GB FAT32格式卡
ACMD41一直超时供电不稳或SCK频率太高降低SPI时钟到250kHz初始化,再提速
初始化成功但f_mount失败卡未格式化电脑格式化为FAT32,或用f_mkfs
读扇区全0x00MISO引脚配置错误检查GPIO复用功能AFR设置

其中“SCK频率太高导致ACMD41超时”是最容易忽略的。SD卡刚上电时内部逻辑还没稳定,高频时钟容易出错。标准做法是:初始化阶段把SPI时钟降到250kHz~400kHz,等初始化完成后再切换到8MHz。这个细节我是在数据手册里看到的,实测初始化失败率从50%降到几乎为0。

5.2 文件写入后数据损坏怎么办

断电导致文件系统损坏是最常见的故障。FatFs默认把多个扇区的写操作放在缓冲里,如果中途断电,文件分配表和目录项可能不一致,表现为文件打不开、文件夹变成乱码。

处理办法有两个层面。项目层面,可以考虑采用“双区轮流写”策略——第一份数据写在A区,下一份写B区,写完后交替,这样即使一个区损坏,至少还有一个区的数据可用。代码层面,每次写完关键数据后调用f_sync,它会立即将文件信息和缓冲区内容刷回SD卡,即使后续断电,文件内容也是完整的。

另外,使用的SD卡模块如果接触不良,写入时卡座瞬间断电,也会导致损坏。这个问题的排查方法是:插紧卡后,用一根扎带把卡固定住,观察是否还会出现问题。如果真的频繁出问题,换一个带卡扣的卡座模块,比在代码里打补丁更彻底。

5.3 性能优化与常用技巧

  1. 批量读写:FatFs的f_read和f_write支持一次读写多个扇区,把缓冲增大到4096字节,一次写多个扇区比写一个扇区快得多。但STM32F030C8T6的RAM只有8KB,缓冲太大吃不消,实测2048字节最合适。
  2. 快速定位:如果只需要在文件尾部追加数据,用FA_OPEN_ALWAYS + FA_WRITE打开文件,然后f_lseek到文件末尾,避免从头扫描。应用场景是日志系统。
  3. 打开文件数量:FatFs默认最大同时打开文件数是FF_FS_LOCK,如果项目里要开多个文件,记得在ffconf.h里加大这个值。

注意:SPI DMA方式可以进一步降低CPU占用,但STM32F030C8T6的SPI1 DMA在F0上需要额外配置DMA通道,代码复杂度会上升。对于大多数日志存储应用,普通阻塞式SPI已经足够,DMA属于锦上添花。

6. 最后的实操体会

做一个STM32F030C8T6 + SPI SD卡 + FatFs项目,真正花时间的往往不是文件系统移植本身,而是把SD卡初始化协议调通。我的经验是:先不要一上来就整个FatFs,而是先用裸SPI读写控制层,把CMD0、CMD8、ACMD41这些初始化命令都打印出来确认无误,再用一个简单的扇区读写测试验证能写能读,最后才挂FatFs。这样一层层排查下来,问题定位会快很多。

另外,芯片选型上STM32F030C8T6做这块确实很经济,但RAM 8KB是个约束,FatFs默认缓冲区不能开太大。如果后期需要存储更大量数据或更复杂的数据结构,可以考虑换成有SDIO接口的F103系列或者直接带TF卡的ESP32,不过对于日志记录、配置文件存储、字库升级这类场景,F030这套方案性价比极高,只要底层SPI和SD卡初始化设计得稳,运行几个月都很可靠。

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

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

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

立即咨询