2026/9/2 2:47:22

STM32F030C8T6 SPI接口读写SD卡并移植FatFs完整指南

STM32F030C8T6 SPI接口读写SD卡并移植FatFs完整指南 简介基于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卡读写做扎实这篇内容围绕STM32F030C8T6SPIFatFs的完整方案展开从选型、硬件、初始化、文件系统移植到实际性能按我的真实踩坑顺序来写。1. 为什么是F030C8T6 SPI SD卡这个组合1.1 选型逻辑F030本身没有SDIOSPI是唯一务实路线很多人第一反应是“做SD卡读写不如直接用F103F103有SDIO”。这句话对一半F103系列中的大容量型号确实带SDIOSDIO读写速度和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转接板SCKPA5CLKPin 5MISOPA6DOPin 7MOSIPA7DIPin 2CSPA4CSPin 3VCC3.3VVDDPin 4GNDGNDVSSPin 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分为硬件片选SSOE1和软件片选SSOE0用硬件NSS虽然省了一个GPIO但当主从设备通信异常时NSS电平容易受外部干扰而且状态机里多一个NSS状态判断调试起来反而麻烦。软件片选的优势是时机完全可控。SD卡的SPI模式协议非常介意片选信号与命令时序之间的配合比如发送CMD0前要先把CS拉低一段时间再发时钟。用GPIO操作简单直观出问题也好定位。所以我实际用的CS是PA4普通推挽输出。3. SD卡SPI初始化从SD模式切过来的那一瞬3.1 上电时序的执行顺序SD卡出厂默认工作在SD总线模式想让它进入SPI模式关键在于上电后的“切换序列”。很多新手直接上电就发CMD0卡完全不响应就是因为漏了前导时钟。正确顺序是拉高CSSPI主机发送至少74个时钟脉冲推荐80个即发10个0xFF字节拉低CS发送CMD0命令等待卡返回0x01表示进入空闲态发送CMD8命令区分SD v1.1和SD v2.0卡循环发送CMD55ACMD41等待卡返回0x00表示初始化完成发送CMD58读取OCR判断卡是SDSC还是SDHC。第1步里的“CS保持高电平”非常重要。此时卡还在SD模式高电平的CS意味着卡处于非选状态但可以接收时钟。这是SPI模式切换的前提条件漏掉这一步后面所有命令都会超时。3.2 命令细节与CRCSPI模式下的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的参数是0x000001AACRC是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); // HCS1 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 * 512SDHC卡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_COUNTFatFs挂载时要靠它知道卡的总容量。这个值从CSD寄存器里解析SDSC和SDHC的C_SIZE位段不一样不能照搬网上某个公式。我的做法是在disk_initialize成功后再读一次CSDCMD9把C_SIZE、C_SIZE_MULT、READ_BL_LEN都解析出来再乘上512字节块大小。4.2 ffconf.h关键项配置F030只有8KB RAM这决定了FatFs的配置不能照抄F103的工程模板。我实际用的配置项如下宏定义值说明FF_FS_READONLY0需要写入必须为0FF_USE_LFN0关闭长文件名一次能省几百字节RAMFF_MIN_SS/FF_MAX_SS512只支持512字节扇区FF_VOLUMES1只挂载一张卡FF_USE_MKFS1允许格式化新卡FF_USE_STRFUNC1支持f_printf写日志FF_FS_TINY1共用文件系统扇区缓冲节省RAMFF_CODE_PAGE437默认ASCII避免中文编码表占用FlashFF_FS_TINY这个选项值得多说一句。普通模式FatFs里每个打开的文件对象FIL自带一个512字节扇区缓冲如果同时打开3个文件光缓冲就占1.5KB RAM。开启TINY模式后所有文件对象共享文件系统全局的扇区缓冲文件个数多的时候省内存效果很明显。代价是速度略有下降因为文件系统层和底层驱动共用缓冲区多了一些拷贝操作。在F030上我选择TINY模式RAM占用更可控。8KB RAM实际分配启动时默认堆栈约1.5KBSPI收发缓冲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 Class10SPI时钟设为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方案最常见的故障我的排查顺序是先确认CS高电平时的前导时钟是否足够。示波器看SCK波形少于74个脉冲就补确认CMD0的CRC是0x95CMD8的CRC是0x87。这两个固定CRC写错最隐蔽返回响应全是0xFF确认SPI模式配置是Mode 0CPOL0, CPHA0SD卡SPI模式只认这个配成Mode 3会随机失败确认上电时SPI时钟频率是否足够低。如果直接用12MHz初始化很多卡会初始化失败改回2分频以下最后检查上拉电阻。卡刚上电时微弱逻辑电平可能因为分布电容拉低导致始化失败。整个排查链路里示波器能解决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闪烁提示进度稳得很。本文还有配套的精品资源点击获取