STM32F030C8T6 SPI接口读写SD卡并移植FatFs完整指南
2026/9/2 2:47:15 网站建设 项目流程

简介:基于STM32F030C8T6的SPI SD卡读写与FatFs文件系统完整工程,面向嵌入式开发者和单片机学习者,解决在ARM Cortex-M0平台上通过SPI接口操作SD卡并实现文件管理的问题。工程采用HAL库编写,清晰展示SPI初始化、SD卡命令时序、FatFs挂载以及文件创建、读写、删除等完整逻辑,并配有必要的错误检测与异常处理机制,可直接移植到同类项目或作为课程设计参考。资源共928个文件,以C源码(563个)和头文件(251个)为主,另含汇编启动文件、IAR与Keil工程配置文件、链接脚本等,整体压缩包约8.32MB,工程结构完整,便于按模块研读。已有2239人学习下载,适合需要快速掌握STM32 SPI通信与FatFs集成的开发者,既能理解底层硬件时序,也能学会文件系统上层调用,可有效缩短开发调试周期。 去年有段时间我一直在做一个户外环境记录仪的小项目:传感器数据每隔几秒采样一次,本地存到SD卡里,跑满一个月把卡取出来直接读文件。项目对BOM成本卡得很死,控制器选了STM32F030C8T6,这块芯片没有SDIO外设,所以SD卡读写就只能走SPI,再往上挂FatFs文件系统。整套方案调通之后,生成的日志文件在电脑上直接双击就能打开,比起裸存二进制然后用专用解析工具,体验完全是两个级别。

如果你也在用F030、F103这类入门级MCU做数据记录、日志存储、配置导入导出,或者只是想把SD卡读写做扎实,这篇内容围绕STM32F030C8T6+SPI+FatFs的完整方案展开,从选型、硬件、初始化、文件系统移植到实际性能,按我的真实踩坑顺序来写。

1. 为什么是F030C8T6 + SPI SD卡这个组合

1.1 选型逻辑:F030本身没有SDIO,SPI是唯一务实路线

很多人第一反应是“做SD卡读写不如直接用F103,F103有SDIO”。这句话对一半,F103系列中的大容量型号确实带SDIO,SDIO读写速度和CPU占用都比SPI好。但问题是:F103系列的单片机现在价格贵、货源波动大,而F030C8T6只要两三块钱,48MHz主频、8KB RAM、64KB Flash,做数据记录完全够用。SPI模式SD卡的最高通信速率虽然只有几MHz级别,但一次读一个扇区也就几百微秒,对每秒采一次样的记录设备来说绰绰有余。

F030C8T6内部只有SPI1和SPI2两个SPI外设,没有SDIO,所以想用SD卡,唯一正规路线就是SPI模式。很多工程师会把目光直接放在SD卡的SDIO模式,觉得SPI慢、不专业,但放在这个项目里,SPI的好处反而更明显:引脚少(4根线),布线简单,消费级SD卡全部兼容SPI模式,不需要额外驱动芯片,PCB改版成本低。

1.2 这个方案的性能边界:记录型场景完全够用

SPI模式SD卡,真正能跑到的吞吐量大约是4~6Mbps,也就是每秒约0.5MB字节级传输。如果按一个日志文件每秒写100字节来算,还很奢侈。就算采样率高到每秒1KB,连续跑一年也就是几十GB,完全不构成压力。真正会有问题的是“高采样率+大文件连续写”,比如音频采样、图像抓拍这种每秒几十KB的写入,SPI模式就会感觉捉襟见肘。

所以这个组合适合的数据特征:单条数据量小、写入频率低、数据要可靠落盘、事后要能被电脑直接打开。这类需求在物联网节点、农业气象站、设备运行日志里非常常见。如果你的项目数据量真的很大,就不该选F030,直接换带SDIO的MCU或上eMMC更实际。选型这件事,先想清楚数据量级,再决定方案,比纠结用哪种总线更重要。

2. 硬件连接:上拉电阻比接线本身更关键

2.1 最小接线表

STM32F030C8T6的SPI1引脚,最常用的一组是PA5(SCK)、PA6(MISO)、PA7(MOSI),外加任意一个GPIO做CS。我用的是PA4做软件片选。F030的SPI1默认复用功能就是AF0,这几根引脚出厂就能直连SPI外设,不需要额外配置复用重映射。具体接线如下:

信号MCU引脚SD卡(MicroSD转接板)
SCKPA5CLK(Pin 5)
MISOPA6DO(Pin 7)
MOSIPA7DI(Pin 2)
CSPA4CS(Pin 3)
VCC3.3VVDD(Pin 4)
GNDGNDVSS(Pin 1/6)

注意一点:SD卡是3.3V逻辑,MCU也是3.3V,可以直接连。如果MCU是5V供电,需要通过电平转换电路,不能直接用电阻分压或者二极管钳位就完事,CMOS输入在5V系统中可能因为阈值范围不稳导致随机通信失败。

2.2 为什么SPI模式数据线必须上拉

SD卡在SPI模式下,卡内部的输出驱动是开漏结构,配合外部上拉电阻才能保证正确的电平。我给MISO、MOSI、SCK、CS四根线全部接了10kΩ上拉到3.3V。这个电阻值不需要很精确,10kΩ最通用。如果某些卡在高速SPI下异常,可以考虑换成4.7kΩ,但千万别省这几个电阻,否则会出现“第一张卡能识别,第二张卡死活进不了SPI模式”的怪问题。

另一个容易被忽略的细节:MicroSD转接板上通常自带上拉电阻,但直插裸卡或自己画板子接焊盘时,板上不一定有电阻。设计时先确认转接板原理图,没有就自行补上。不加上拉,初始化阶段卡返回的信号可能会出现边沿过缓,SPI采样点稍微偏移就出错。

2.3 软件片选与硬件片选的抉择

F030的SPI外设有NSS引脚,但我在项目里只用普通GPIO做软件片选。原因在于SPI的NSS分为硬件片选(SSOE=1)和软件片选(SSOE=0),用硬件NSS虽然省了一个GPIO,但当主从设备通信异常时,NSS电平容易受外部干扰,而且状态机里多一个NSS状态判断,调试起来反而麻烦。

软件片选的优势是时机完全可控。SD卡的SPI模式协议非常介意片选信号与命令时序之间的配合,比如发送CMD0前要先把CS拉低一段时间,再发时钟。用GPIO操作简单直观,出问题也好定位。所以我实际用的CS是PA4,普通推挽输出。

3. SD卡SPI初始化:从SD模式切过来的那一瞬

3.1 上电时序的执行顺序

SD卡出厂默认工作在SD总线模式,想让它进入SPI模式,关键在于上电后的“切换序列”。很多新手直接上电就发CMD0,卡完全不响应,就是因为漏了前导时钟。

正确顺序是:

  1. 拉高CS,SPI主机发送至少74个时钟脉冲(推荐80个,即发10个0xFF字节);
  2. 拉低CS,发送CMD0命令,等待卡返回0x01表示进入空闲态;
  3. 发送CMD8命令,区分SD v1.1和SD v2.0卡;
  4. 循环发送CMD55+ACMD41,等待卡返回0x00表示初始化完成;
  5. 发送CMD58读取OCR,判断卡是SDSC还是SDHC。

第1步里的“CS保持高电平”非常重要。此时卡还在SD模式,高电平的CS意味着卡处于非选状态,但可以接收时钟。这是SPI模式切换的前提条件,漏掉这一步,后面所有命令都会超时。

3.2 命令细节与CRC

SPI模式下的SD卡命令帧固定6字节:1字节命令索引,4字节参数,1字节CRC7校验。有个容易忽略的坑:进入SPI模式之前,卡还处于SD模式,所以CMD0和CMD8必须带正确的CRC,之后因为卡已经切进SPI模式,命令帧的CRC在规范里是可选检查的(大多数卡不检查),所以后面发CMD55、ACMD41、CMD17等指令时CRC可以不填或填固定值。

CMD0的6字节固定crc是0x40 0x00 0x00 0x00 0x00 0x95,这个0x95是CRC7运算结果。CMD8的参数是0x000001AA,CRC是0x43 0x00 0x00 0x01 0xAA 0x87。把这两个CRC算错,卡会忽略命令,直接表现为初始化卡住。项目里我刚开始就是漏算了CMD8的CRC,卡总是返回0xFF,排查了半小时。

3.3 初始化完整代码(精简)

下面这段是我项目里使用的初始化代码,基于标准库写的伪代码,可以直接套进自己的HAL工程:

static uint8_t SD_ReadByte(void) { uint8_t data = 0xFF; SPI_SendReceive(&data, 1); return data; } static void SD_WriteByte(uint8_t data) { SPI_SendReceive(&data, 1); } static uint8_t SD_SendCMD(uint8_t cmd, uint32_t arg, uint8_t crc) { uint8_t frame[6]; frame[0] = 0x40 | cmd; frame[1] = (arg >> 24) & 0xFF; frame[2] = (arg >> 16) & 0xFF; frame[3] = (arg >> 8) & 0xFF; frame[4] = arg & 0xFF; frame[5] = crc; for (int i = 0; i < 6; i++) SD_WriteByte(frame[i]); // 等待响应,最多等8个字节,且每字节前都要发时钟 for (int i = 0; i < 8; i++) { uint8_t resp = SD_ReadByte(); if ((resp & 0x80) == 0) return resp; } return 0xFF; // 超时 } uint8_t SD_Init(void) { // 初始化SPI时钟 SPI_InitTypeDef spiconf; spiconf.Mode = SPI_Mode_Master; spiconf.Direction = SPI_Direction_2Lines_FullDuplex; spiconf.DataSize = SPI_DataSize_8b; spiconf.CLKPolarity = SPI_CPOL_Low; // SPI Mode 0 spiconf.CLKPhase = SPI_CPHA_1Edge; // SPI Mode 0 spiconf.NSS = SPI_NSS_Soft; spiconf.BaudRatePrescaler = SPI_BaudRatePrescaler_256; // 低速初始化 spiconf.FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &spiconf); SPI_Cmd(SPI1, ENABLE); // 1. 上电前导时钟 SD_CS_HIGH(); for (int i = 0; i < 10; i++) SD_WriteByte(0xFF); // 2. CMD0进入SPI模式 SD_CS_LOW(); uint8_t r1 = SD_SendCMD(0, 0x00000000, 0x95); if (r1 != 0x01) return 0; // 3. CMD8检查v2.0卡 r1 = SD_SendCMD(8, 0x000001AA, 0x87); if (r1 == 0x01) { // 卡是SD v2.0,继续ACMD41 } else if (r1 == 0xFF) { // 老卡不支持CMD8,直接跳过 return 0; } else { return 0; } // 4. ACMD41初始化,循环等待就绪 uint8_t retry = 0; do { SD_SendCMD(55, 0x00000000, 0x01); // CRC固定值 r1 = SD_SendCMD(41, 0x40000000, 0x01); // HCS=1 if (++retry > 100) return 0; } while (r1 != 0x00); // 5. CMD58判断卡类型 r1 = SD_SendCMD(58, 0x00000000, 0x01); uint32_t ocr = 0; for (int i = 0; i < 4; i++) ocr = (ocr << 8) | SD_ReadByte(); sd_type = SD_TYPE_SDSC; if (ocr & (1 << 30)) // CCS位 sd_type = SD_TYPE_SDHC; // 6. 配置块大小512字节 SD_SendCMD(16, 512, 0x01); // 7. 提高SPI时钟 spiconf.BaudRatePrescaler = SPI_BaudRatePrescaler_4; // 12MHz SPI_Init(SPI1, &spiconf); return 1; }

3.4 类型判断:SDSC和SDHC地址算法不一样

初始化最后一步必须区分SDSC(标准容量)和SDHC(高容量)。因为读写命令里的扇区地址含义不同:

  • SDSC卡:CMD17/CMD24的参数是字节地址,读写第N个扇区时需要传入N * 512
  • SDHC卡:CMD17/CMD24的参数就是扇区号,直接传N就行。

如果地址类型搞错,会出现“刚开始读到第1个扇区正常,读到第2个扇区开始错乱”的现象,因为SDSC卡收到超过实际容量的字节地址可能返回错误或被截断。在FatFs的disk_read/disk_write接口里,一定要根据这个标识做地址转换。

4. FatFs配置:8KB内存下的取舍

4.1 diskio.c要实现的六个函数

FatFs本身是上层文件系统逻辑,它不直接操作硬件,而是通过diskio层调用底层驱动。移植时需要实现六个函数:

函数作用
disk_status返回磁盘状态
disk_initialize调用SD_Init完成卡初始化
disk_read读扇区
disk_write写扇区
disk_ioctl处理扇区数、块大小等控制命令
get_fattime返回当前时间戳

最常出问题的是disk_read和disk_write。因为FatFs可能一次请求多个扇区,如果SPI底层没实现多块读(CMD18)和多块写(CMD25),就需要在接口层自己做循环,把多块请求拆成一个个单块读写。我图省事,一开始就是每个扇区单独发CMD17,结果性能测试时发现连续读64KB文件耗时高得吓人。后来改成多块读,耗时降了一大截。

disioctl还需要处理GET_SECTOR_COUNT,FatFs挂载时要靠它知道卡的总容量。这个值从CSD寄存器里解析,SDSC和SDHC的C_SIZE位段不一样,不能照搬网上某个公式。我的做法是在disk_initialize成功后再读一次CSD(CMD9),把C_SIZE、C_SIZE_MULT、READ_BL_LEN都解析出来,再乘上512字节块大小。

4.2 ffconf.h关键项配置

F030只有8KB RAM,这决定了FatFs的配置不能照抄F103的工程模板。我实际用的配置项如下:

宏定义说明
FF_FS_READONLY0需要写入,必须为0
FF_USE_LFN0关闭长文件名,一次能省几百字节RAM
FF_MIN_SS/FF_MAX_SS512只支持512字节扇区
FF_VOLUMES1只挂载一张卡
FF_USE_MKFS1允许格式化新卡
FF_USE_STRFUNC1支持f_printf写日志
FF_FS_TINY1共用文件系统扇区缓冲,节省RAM
FF_CODE_PAGE437默认ASCII,避免中文编码表占用Flash

FF_FS_TINY这个选项值得多说一句。普通模式FatFs里每个打开的文件对象FIL自带一个512字节扇区缓冲,如果同时打开3个文件,光缓冲就占1.5KB RAM。开启TINY模式后,所有文件对象共享文件系统全局的扇区缓冲,文件个数多的时候省内存效果很明显。代价是速度略有下降,因为文件系统层和底层驱动共用缓冲区,多了一些拷贝操作。在F030上,我选择TINY模式,RAM占用更可控。

8KB RAM实际分配:启动时默认堆栈约1.5KB,SPI收发缓冲512字节,FatFs文件系统工作区(文件系统对象+扇区缓冲)约1KB,剩余还有充足空间给业务逻辑。但前提是关闭长文件名,不启用exFAT,否则RAM立刻爆掉。

4.3 新卡挂载与格式化处理

FatFs的f_mount不是真正挂载磁盘,只是注册一个文件系统对象。所以第一次调用f_mount返回FR_NOT_ENABLED或者后续f_open返回FR_NO_FILESYSTEM,大概率是卡上还没有FAT文件系统。处理方式是先f_mount,接着调用f_mkfs格式化成FAT32,再重新挂载。

f_mkfs在F030上跑得会比想象中慢,格式化一张32GB卡可能要十几秒,给用户反馈进度时要有心理预期,别让看门狗误触发复位。我的固化逻辑是:

FATFS fs; FRESULT res = f_mount(&fs, "", 1); if (res == FR_NO_FILESYSTEM) { f_mkfs("", 0, 0, 0, NULL, 512); // 0号盘,0号分区表,默认扇区大小 f_mount(&fs, "", 1); }

注意f_mkfs前要先卸载一次文件系统对象,否则可能报FR_MKFS_ABORTED。另外,格式化会擦掉整张卡的数据,量产时这步建议做成“上电检测到特定按键才格式化”,避免误操作丢数据。

5. 实测读写性能和三个提速手段

5.1 12MHz SPI下的实测数据

我用的SD卡是闪迪32GB Class10,SPI时钟设为12MHz。实测数据如下:

操作耗时/扇区(512B)备注
单扇区读约0.6msCMD17 + 等待数据令牌 + 串行读
单扇区写约2.5msCMD24 + 数据块 + 卡内编程时间
多块读(64扇区)约4.5msCMD18 + 连续数据流
多块写(64扇区)约35msCMD25 + 每块等待忙信号

注意单扇区写比读慢4倍左右,这是SD卡物理特性决定的,每次写都要做擦除再编程。如果应用需要反复更新同一个配置文件的几千字节,这一点会让卡寿命和速度都受影响,所以频繁写入的日志我都是定期合并写。

5.2 提速手段:DMA、连续写、预分配

三个手段按性价比排序:

第一,SPI收发改用DMA。F030的DMA外设可以在SPI发送时自动从内存搬运数据,CPU不用每个字节都停在那里等。实测读扇区时CPU占用明显下降,对需要同时处理传感器采样的场景很有帮助。DMA配置时要保证SPI处于使能状态,收发缓冲区地址对齐到4字节,并且传输完成中断里要做SPI空闲等待。

第二,写日志时尽量做成“连续扇区写入”。FatFs在FAT32下给文件分配簇时,如果每次追加都重新申请簇,容易分配不连续,底层写操作就会变成多次单扇区写。可以预先把日志文件扩展到一个固定大小,再通过f_lseek定位到末尾写,这样FAT表更新频率大大降低,写入连续度也会提高。

第三,调整SPI时钟分频。初始化时用低速(SPI_BaudRatePrescaler_256),等卡初始化完成后再把分频改成4分频,也就是12MHz。实测F030的SPI外设在12MHz下配SPI Mode 0,配Class10卡基本稳定,4分频再高到24MHz会偶尔出现读回数据错误。不必追求极限速度,稳定更重要。

5.3 降低f_sync频率的边界

FatFs写文件时,数据先写入文件对象缓冲区,只有f_sync、f_close或缓冲区满时才会真正写盘。每次调f_sync都会刷新FAT表,增加一次写操作。如果每条日志都f_sync,写入速度会差3倍以上。

我的做法是设置一个定时同步周期,比如每5秒同步一次,同时关闭文件的写缓存选项。掉电时最多丢最后5秒的日志,对记录类应用完全能接受。如果你要求“任意时刻掉电不丢数据”,那就只能用每次写后f_sync的方案,代价是写性能下降,不要指望两者兼得。

6. 调试过程中容易踩的几个坑

6.1 卡不识别:按这个顺序排查

SD卡不识别是SPI方案最常见的故障,我的排查顺序是:

  1. 先确认CS高电平时的前导时钟是否足够。示波器看SCK波形,少于74个脉冲就补;
  2. 确认CMD0的CRC是0x95,CMD8的CRC是0x87。这两个固定CRC写错最隐蔽,返回响应全是0xFF;
  3. 确认SPI模式配置是Mode 0(CPOL=0, CPHA=0),SD卡SPI模式只认这个,配成Mode 3会随机失败;
  4. 确认上电时SPI时钟频率是否足够低。如果直接用12MHz初始化,很多卡会初始化失败,改回2分频以下;
  5. 最后检查上拉电阻。卡刚上电时微弱逻辑电平可能因为分布电容拉低,导致始化失败。

整个排查链路里,示波器能解决80%的问题。没有示波器就先降SPI时钟,再检查CRC和上拉,顺序不要反。

6.2 读出来乱码、文件系统损坏

能识别卡但读出来乱码,通常不是文件系统问题,而是SPI采样时序不稳定。我遇到过一张品质一般的卡,调到12MHz后FAT目录项偶尔读错,表现为文件名乱码、文件大小异常。把分频从4改回8,也就是6MHz后故障消失。

另一种情况是disk_read在等待数据令牌时没有循环等待。SD卡发出CMD17后并不是立刻返回数据,中间有几十微秒的准备时间。代码里必须循环读字节直到读到0xFE数据开始令牌,只读一次字节就开工的话,大概率读到0xFF或者下个扇区的开头,表现为数据错位。

6.3 写数据时掉电导致文件系统错乱

FatFs写文件的过程中,如果突然断电,可能造成FAT表损坏或文件分配链断裂。SPI模式下这个问题更突出,因为写入比读取慢,FAT表刷新窗口更长。

针对这个问题的三个经验:

第一,使用双备份文件机制。写日志时先写A文件,写完改名;下一次写B文件。启动时检查两个文件的完整性,选择最近有效的那份。这个逻辑在应用层实现,不依赖文件系统底层的掉电保证。

第二,加一颗大电容或者用电源监控芯片,让MCU检测到电压跌落时能继续供电几百毫秒,在掉电中断里把当前文件关闭并f_sync。这会显著提高可靠性,成本只多几毛钱。

第三,不要手动修改FatFs底层。FatFs本身对FAT表更新顺序做了优化,我们自己改不如把f_sync调用点设计好。我实际跑下来,配合掉电监测,日志文件损坏率从“一个月一两次”降到“几乎为0”。

最后再说个实用小技巧:上电后先f_mount,如果返回FR_NO_FILESYSTEM就自动f_mkfs。这样用户换新卡或者把卡格式化顺手清空后,设备上电不用接电脑,自己就能重建文件系统。但记得在f_mkfs前留一个按键确认,防止用户插错卡直接清空数据。这个坑我踩过一回,从那以后格式化操作必须先确认,再用第二颗LED闪烁提示进度,稳得很。

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

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

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

立即咨询