ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32F030与SPI接口驱动SD卡并移植FatFs文件系统完整指南

STM32F030与SPI接口驱动SD卡并移植FatFs文件系统完整指南 简介面向嵌入式开发者的一份STM32F030C8T6单片机通过SPI接口读写SD卡的完整工程围绕FatFs文件系统的移植与应用展开。资源基于HAL库完成SPI1初始化细致处理时钟极性、相位以及数据传输时序实现SD卡识别、扇区读写与异常检测挂载FatFs后可直接调用f_open、f_write、f_read、f_close等接口进行文件的创建、写入、读取与删除同时保留了底层驱动扩展接口方便后续移植。压缩包共928个文件以563个C源文件、251个头文件为主辅以启动文件、链接脚本、工程配置与少量说明文档整体8.32MB包含Keil与IAR工程配置结构清晰。已有2239人学习下载代码注释完整既适合学习SPI协议与SD卡驱动原理的工程师也可直接迁移到实际项目中作为存储模块基础用于物联网、工业控制等场景还可作为课程设计与毕业设计的参考模板。 做嵌入式数据记录类的项目绕不开一个很经典的组合STM32 SD卡 FatFs文件系统。这次我用的主控是STM32F030C8T6通过SPI接口驱动SD卡再挂上FatFs做文件管理实现数据的存储、读取和文件组织。这套方案在设备日志记录、传感器数据采集、配置参数掉电保存这些场景里非常实用尤其是对成本敏感、资源不多的小型项目来说几乎是最稳妥的选型。文章里我会把整个过程的方案选型、硬件连接、FatFs移植、读写实现和问题排查完整整理出来希望能给准备做SD卡存储的朋友一些实际帮助少走我在调试中踩过的那些弯路。1. 项目定位与方案选型1.1 为什么选STM32F030C8T6这颗芯片F030C8T6属于STM32F0系列Cortex-M0内核主频48MHz片上64KB Flash、8KB RAM封装是LQFP48外设资源对这个项目来说刚刚好。最关键的是它的价格非常友好批量采购几块钱就能拿到开发板和样品也好买非常适合做小批量产品或者个人DIY项目。选它之前我其实对比过STM32F103C8T6也就是很多人熟知的“蓝丸”那颗芯片。F103性能更强外设也更丰富网上教程铺天盖地按理说会更省心。但F103现在价格被炒得偏高F030货源充足且SPI外设、定时器、UART这些基础外设完全够用功耗也更低。对于SD卡读写这种以低速IO操作为主的场景48MHz主频完全是够用的没必要为了高性能多花钱。1.2 为什么用SPI而不是SDIO驱动SD卡很多人在SD卡方案里会纠结一个问题为什么不用SDIO接口SDIO的传输速度比SPI快一个数量级能达到几十MB/s看起来更“专业”但F030这颗芯片压根没有SDIO外设想用SDIO只能换更高端的STM32F4系列。另外SDIO对硬件布线要求更高代码也更复杂需要处理数据线方向切换和CRC校验很多入门者在这上面被折腾得够呛。SPI驱动SD卡的优势其实很明显引脚占用少只要4根线CS、SCK、MOSI、MISO代码逻辑简单接口通用性强同一套SPI驱动代码还能复用到SPI Flash、LCD屏幕等器件上。对于我的项目来说数据写入频率本身不高一次写几十KB日志SPI跑4MHz左右的速度已经完全满足需求完全没有必要上SDIO。这里顺便说明一个关键点SD卡上电之后默认是SD总线模式只有在CS拉低的同时发送特定的复位命令CMD0才会切换到SPI模式。所以用SPI驱动SD卡本质上是在操作的是一种“兼容模式”速度上限在25MHz左右但可靠性经过大量测试非常成熟。2. 硬件设计与连接要点2.1 SPI通信模式的选择与初始化SPI协议定义了四种工作模式由时钟极性CPOL和时钟相位CPHA组合决定。CPOL决定空闲时SCK的电平CPHA决定数据在时钟的哪个边沿被采样。SD卡在SPI模式下约定使用模式0或模式3绝大多数例程用的是模式0。模式0的含义是CPOL0空闲时SCK为低电平CPHA0数据在SCK上升沿采样在下降沿输出。这个模式的好处是SPI外设的默认配置就是它不容易出错。我用STM32CubeMX初始化SPI1关键参数如下主模式、8位数据宽度、MSB先行、预分频设成128对应375kHz初始时钟后面会说明原因、CPOLLow、CPHA1Edge。有一段时间我搞错过CPHA的配置导致读取的数据全部错位后来对照SD卡时序图才发现问题所以在CubeMX里确认这两项非常关键。2.2 引脚连接与硬件注意事项SPI1在STM32F030C8T6上可以映射到PA5SCK、PA6MISO、PA7MOSICS片选没有强制要求可以用任意GPIO控制我选的是PA4。SD卡模块和MCU的接线如下SD卡模块引脚STM32引脚功能说明VCC3.3V电源供电注意SD卡必须3.3V不能接5VGNDGND共地CSPA4片选信号低电平有效SCKPA5SPI时钟MOSIPA6主机输出、从机输入MISOPA7主机输入、从机输出硬件上有几个容易踩的坑。第一SD卡在写入时电流峰值能达到100mA左右电源引脚旁边必须加一个10μF的钽电容或者电解电容再并联一个0.1μF陶瓷电容做退耦否则供电不稳会导致写入数据出错。第二SD卡的MISO引脚在空闲状态下是高阻态需要接一个10kΩ左右的上拉电阻否则SPI读取时可能出现电平不确定的问题。第三CS线用软件GPIO控制比硬件NSS更灵活这是我在实际项目中一直坚持的做法。还有一点需要注意SPI通信距离天生不适合长距离传输常用于板内或模块间的短距离连接。如果SD卡和MCU之间有超过10cm的排线建议降低SPI时钟频率到1MHz以下并且在信号线上串接33Ω左右的电阻来抑制反射。2.3 软件片选与硬件片选的取舍STM32的SPI外设有硬件NSS功能可以在发送数据时自动拉低片选信号看起来很方便但在SD卡这种需要“长时间保持选中状态”的场景下并不好用。SD卡的命令、响应、数据块传输是一个连续的时序过程中途CS一旦被拉高通信立刻失败。所以我用的是软件片选把PA4配置成推挽输出GPIO在发送命令前手动拉低在操作完成后拉高。这样做的好处是时序完全可控调试时也方便用逻辑分析仪观察片选信号。如果你的设计里还有其他SPI从设备软件片选也能轻松实现一主多从的切换只需要在切换时把前一个设备的CS拉高再拉低下一个设备的CS即可。3. FatFs文件系统移植实录3.1 FatFs到底解决了什么问题如果不使用文件系统直接往SD卡写数据就需要自己管理扇区地址记录哪个扇区写了什么数据。这种方式在小数据量时还能凑合一旦数据文件多了、需要删除或修改的时候没有文件索引机制会非常痛苦。FatFs的作用就是帮你做这件事它在SD卡上创建FAT文件系统结构让SD卡变成一台“小硬盘”可以用文件名来管理数据。FatFs是一个开源的FAT文件系统模块作者是ChaN日本开发者对外提供了一组标准接口函数底层只需要实现disk_initialize、disk_read、disk_write、disk_ioctl、get_fattime这几个函数就能跑起来。它不依赖具体硬件平台代码量不大在嵌入式领域属于“半标配”的东西。3.2 底层接口函数的编写FatFs的源码可以从官网或者GitHub下载我使用的是R0.15版本。下载后把ff.c、ff.h、ffsystem.c、ffunicode.c和diskio.c加入工程重点修改diskio.c和ffconf.h这两个文件。diskio.c里需要实现的核心函数如下DSTATUS disk_initialize(BYTE pdrv) { if (pdrv ! 0) return STA_NOINIT; if (SD_Init() 0) { return RES_OK; } return STA_NOINIT; } DSTATUS disk_status(BYTE pdrv) { return RES_OK; } DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { if (SD_ReadBlocks(sector, buff, count) 0) { return RES_OK; } return RES_ERROR; } DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count) { if (SD_WriteBlocks(sector, buff, count) 0) { return RES_OK; } return RES_ERROR; } DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { switch (cmd) { case GET_SECTOR_COUNT: *(DWORD *)buff sd_card_info.total_sectors; 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; } }这里有一个容易被忽略的细节GET_SECTOR_COUNT必须正确返回SD卡的总扇区数这个数值在SD卡初始化时通过读取CSD寄存器获取。如果这个值不对格式化时容量就会异常甚至无法完成格式化。有的SD卡CSD寄存器里报告的不是512字节的扇区数需要转换成“总字节数/512”这个转换逻辑要写对。get_fattime函数也要实现它是给文件系统写入时间戳用的。对于不需要真实时间的项目返回一个固定值即可比如0x20240101表示2024年1月1日否则编译器会报链接错误。3.3 配置文件里的几个关键开关ffconf.h是FatFs的配置文件里面对应了非常多编译开关不是全开就好的要根据项目裁剪。我建议重点关注这几个配置项我的设置原因FF_USE_MKFS1启用格式化功能SD卡首次使用时需要格式化FF_USE_LFN2支持长文件名否则文件名只能8.3格式FF_USE_FASTSEEK0不需要快速跳转功能节省内存FF_MIN_SS / FF_MAX_SS512SD卡扇区固定512字节设置为512可简化代码FF_FS_TINY0使用标准FatFs模式兼容性更好FF_USE_LFN如果要打开需要确认编译器环境支持字符串拼接和宽字符操作通常在MDKKeil中要勾选C99模式并在ffconf.h里选择合适的LFN编码方式。如果工程里原本只处理ASCII文件名可以先用FF_USE_LFN0跑通后续再升级。4. 数据读写核心实现4.1 SD卡初始化流程的完整代码SD卡在SPI模式下初始化有严格时序要求我整理了一个标准的初始化序列实测对市面上大多数SD卡和TF卡兼容性都不错uint8_t SD_Init(void) { uint8_t resp; uint8_t i; // 1. SPI慢速初始化时钟控制在400kHz以下 SPI_SetClockDiv(128); // 2. 最少发送74个时钟周期发送至少10个0xFF SD_CS_HIGH(); for (i 0; i 10; i) { SPI_WriteByte(0xFF); } // 3. 拉低CS进入SPI模式 SD_CS_LOW(); // 4. 发送CMD0让卡进入SPI模式 resp SD_SendCmd(CMD0, 0x00000000, 0x95); if (resp ! 0x01) return 1; // 5. 发送CMD8区分SD V2.0以上版本 resp SD_SendCmd(CMD8, 0x000001AA, 0x87); if (resp 0x01) { // V2.0及以上版本读取返回的4字节数据 for (i 0; i 4; i) { SPI_ReadByte(); } // 6. 循环发送ACMD41内部先CMD55再ACMD41直到R1响应为0x00 for (i 0; i 100; i) { resp SD_SendCmd(CMD55, 0x00000000, 0x01); if (resp ! 0x01) return 2; resp SD_SendCmd(ACMD41, 0x40000000, 0x01); if (resp 0x00) break; HAL_Delay(1); } if (resp ! 0x00) return 3; } else { // V1.x版本使用CMD1初始化 for (i 0; i 100; i) { resp SD_SendCmd(CMD1, 0x00000000, 0x01); if (resp 0x00) break; HAL_Delay(1); } if (resp ! 0x00) return 4; } // 7. 发送CMD16设置块长度为512字节 resp SD_SendCmd(CMD16, 0x00000200, 0x01); if (resp ! 0x00) return 5; // 8. 读取CSD寄存器获取卡容量 SD_ReadCSD(); // 9. 初始化完成提高SPI时钟到工作频率 SPI_SetClockDiv(4); // 约12MHz return 0; }注意这段代码里的ACMD41操作命令前必须发送CMD55将下一条命令标记为“应用相关命令”这是SD卡协议的一个特殊设计。很多人在网上抄代码时忽略了CMD55结果ACMD41永远得不到0x00响应卡在初始化这里。4.2 命令发送与响应判断uint8_t SD_SendCmd(uint8_t cmd, uint32_t arg, uint8_t crc) { uint8_t buffer[6]; uint8_t resp; uint8_t i; buffer[0] 0x40 | cmd; buffer[1] (arg 24) 0xFF; buffer[2] (arg 16) 0xFF; buffer[3] (arg 8) 0xFF; buffer[4] arg 0xFF; buffer[5] crc | 0x01; // 发送命令前先给8个时钟周期 SPI_WriteByte(0xFF); SD_CS_LOW(); for (i 0; i 6; i) { SPI_WriteByte(buffer[i]); } // 等待响应最多等8个字节 for (i 0; i 8; i) { resp SPI_ReadByte(); if ((resp 0x80) 0) { return resp; // 高位置0表示有效响应 } } return 0xFF; }SPI模式下SD卡的响应是单字节的R1响应最高位为0表示有效。因为卡不是立即返回响应的需要等待若干个时钟周期所以这里要循环读字节直到读到合法响应或超时。我在调试时曾经因为等待次数太少导致高速卡偶尔初始化失败后来把这个循环放宽到8个字节后问题解决。4.3 文件读写的完整示例初始化完成后文件读写的代码就简单了FatFs把底层扇区操作封装成了文件操作接口。举个例子我要往SD卡根目录下的data.log文件里追加写入一条传感器数据FIL file; UINT bytes_written; FRESULT res; char buffer[64]; float temperature 25.6f; // 挂载文件系统 f_mount(fs, 0:, 1); // 打开文件使用FA_OPEN_APPEND表示追加写入 res f_open(file, 0:data.log, FA_OPEN_APPEND | FA_WRITE); if (res FR_OK) { sprintf(buffer, temp%.2f\r\n, temperature); res f_write(file, buffer, strlen(buffer), bytes_written); f_close(file); }读取文件内容的思路也类似用f_open打开文件后调用f_read把内容读入缓冲区处理完再f_close。这里有几个调用细节值得说明第一f_mount的第一个参数是指向FATFS结构体的指针在挂载期间这个结构体不能被其它变量覆盖第二文件名里的盘符“0:”要和磁盘驱动号对应FatFs默认支持多个物理驱动器pdrv编号从0开始第三写完数据后一定要f_close否则数据可能留在缓冲区里没有真正写入SD卡突然断电会导致数据丢失。另外如果你的数据文件长时间连续写入比如每秒写一条日志建议在f_write之后定期调用f_sync刷新文件缓冲区或者直接控制文件大小写满一个文件就新建下一个避免单个文件过大导致FAT表操作变慢。5. 踩坑记录与排查清单5.1 初始化失败的经典问题SD卡初始化失败是新手最常见的问题我在这上面花了整整两天时间。综合来看原因集中在三个方向。第一是初始化时序问题SPI时钟太快导致卡不响应。SD卡在SPI模式上电后要求主机先发送至少74个时钟周期且初始化阶段时钟不能超过400kHz。如果直接用工作频率去初始化部分卡会直接不识别。解决办法是初始化阶段用128分频成功后再切到4分频。第二是CS电平状态问题。在上电时CS必须保持高电平让卡先进入SD模式然后再拉低CS发CMD0切换SPI模式。如果CS一开始就拉低卡不会正确响应CMD0。这一点在我用的某款TF卡上表现得特别明显上电瞬间CS被默认拉低导致初始化必然失败。第三是CMD8和ACMD41的兼容性问题。老版本的SD卡不支持CMD8而新版本卡对ACMD41的要求又各有差异。我的做法是按照4.1节的顺序先发CMD8试探卡的版本再根据响应走不同的初始化分支这样能覆盖绝大多数卡。5.2 读写数据错误与文件系统损坏排查读写过程中出现数据全部为0xFF或者0x00大概率是以下原因造成的。电源问题排第一。SD卡在写操作瞬间电流变化很大如果电源纹波太严重会让SPI逻辑电平产生毛刺写入的数据就会出错。排查方法很简单示波器看3.3V电源如果写入时有明显压降就需要加强退耦电容。其次是CS与SCK时序问题。SPI模式下SD卡要求在片选拉低拉高时SCK保持稳定的空闲电平和正常的时钟时序。如果CS信号有毛刺或者代码里拉低CS之后没有延时直接发数据也会导致时序违规。最稳妥的做法是在CS操作和SPI数据收发之间留出几个空时钟周期。还有文件系统损坏的情况。表现是f_mount返回FR_NO_FILESYSTEM或者FR_DISK_ERR。这种情况多半是之前断电导致FAT表被写坏或者格式化时用的扇区大小和disk_ioctl返回的不一致。解决办法是重新格式化或者用上位机软件比如winhex对SD卡做低级格式化再重新使用。5.3 问题速查表下面是我在实际调试中总结的常见问题速查表适合现场排查快速定位现象可能原因排查方法CMD0无响应SPI时钟过快、CS电平错误降低SPI时钟到400kHz检查CS上电状态ACMD41超时忘记发CMD55、卡版本兼容问题确认先发CMD55再发ACMD41走V1.x分支初始化成功但挂载失败卡上没有文件系统或格式不支持先格式化SD卡为FAT16/FAT32写入文件后断电丢失未调用f_close或f_sync在关键写入后调用f_sync读取数据全是FFMISO引脚无上拉、接线不对检查MISO上拉电阻用万用表量电平偶发性读写错误电源纹波大、SPI频率过高加大退耦电容降低SPI时钟频率我还发现一个容易忽略的问题用TF卡MicroSD加卡套插入SD卡槽时部分卡套的机械结构会导致引脚接触不良尤其在震动环境下。如果是长期运行的项目建议直接用贴片式的TF卡座稳定性比卡套好很多。最后再分享一点个人体会整个项目做下来最大的收获是理解了“协议栈分层”的思想。SD卡底层的SPI时序是物理层命令交互是协议层FatFs是文件系统层每一层只负责自己的事接口清晰调试起来就能快速定位问题。很多人在移植的时候遇到问题就怀疑硬件、怀疑代码其实先做好分层定位就能把问题范围缩小很多。如果你手头正好有这个芯片我建议按照这篇文章的顺序从硬件连接到文件写入一步步走一遍跑通了之后再考虑加DMA或者低功耗优化你会发现SD卡存储其实没有想象中那么难。本文还有配套的精品资源点击获取
返回列表