ARTICLE DETAIL

资讯详情

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

STM32F103 SPI读写SD卡实战:从硬件初始化到FAT32文件系统移植

STM32F103 SPI读写SD卡实战:从硬件初始化到FAT32文件系统移植 简介面向STM32F103单片机开发者和嵌入式学习者提供一套采用SPI通讯方式实现SD卡FAT32读写的完整工程。内容涵盖SPI接口初始化、SD卡命令序列发送、FATFS文件系统移植以及文件打开、读写、关闭等标准调用流程并给出驱动结构体与错误处理思路便于直接移植和二次开发。工程内还包含LCD显示及定时器相关模块可作为扩展人机交互和时间控制的基础。资源共369个文件以C源码、头文件、Keil工程配置为主同时可见编译生成的o/crf目标文件、hex烧录文件、map映射表等压缩包约13.66MB目录结构清晰方便定位底层驱动与应用示例。目前已有547人学习适合希望快速理解STM32与SD卡SPI交互、FAT32读写实现细节的读者借助源码可较快掌握从底层驱动到文件系统的整体方案。1. STM32F103用SPI读写SD卡从4根线到FAT32文件落盘STM32F103做数据采集、控制盒或者上位机通讯时几乎都会遇到同一个需求把运行日志、采集数据和配置参数落盘。SD卡是成本最低的答案SPI又是F103上接入SD卡代价最小的方式SPI1只用PA4PA7四个引脚代码量比SDIO小得多。文件系统选FAT32是因为多数SD卡出厂就是FAT32电脑插卡即读配合FatFs嵌入式文件系统库C语言文件读写操作代码能控制得很小。下面按SPI硬件初始化、SD卡命令握手、FatFs移植、文件读写实战、排错与提速的顺序展开代码用标准外设库v3.50风格用CubeMX生成HAL工程时原理一致。新手能照着跑通老手能在写保护误报和吞吐优化里找到自己踩过的坑。2. SPI协议与引脚配置F103的SD卡接入先过硬件关2.1 硬件SPI还是软件模拟SPI先看时钟和引脚占用SD卡SPI模式对时序的要求不复杂主机做主模式8位数据MSB先行时钟极性CPOL0、相位CPHA0也就是常说的SPI Mode 0。多数卡的协议层在Mode 0和Mode 3下都能识别但我习惯固定用Mode 0和多数例程兼容逻辑分析仪抓波形时也统一。F103的SPI1挂在APB2总线上主频72MHz分频后最高能到18MHz普通microSD卡数据读写完全够用。选硬件SPI还是软件模拟SPI我一般按两个条件判断SPI1的PA5/PA6/PA7是否被其他外设占用以及是不是要分时挂多个SPI设备。引脚被占用就上软件模拟SPIF103在72MHz下GPIO翻转速度足够模拟出400kHz的初始化频率和18MHz的数据频率代价是CPU全被占住没有冲突就优先硬件SPI代码短、时序稳定后面想提速还能配DMA。注意Mode 0是针对SD卡SPI模式的约定换W25Q64这类Flash时要按对应手册重新确认时钟极性和相位。2.2 SPI1初始化代码与软件片选用标准外设库v3.50的写法SPI1初始化如下void SPI1_Init(void) { GPIO_InitTypeDef g; SPI_InitTypeDef s; RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1 | RCC_APB2Periph_GPIOA, ENABLE); g.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_7; // SCK、MOSI g.GPIO_Mode GPIO_Mode_AF_PP; g.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, g); g.GPIO_Pin GPIO_Pin_6; // MISO:输入带上拉 g.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, g); g.GPIO_Pin GPIO_Pin_4; // CS:软件片选 g.GPIO_Mode GPIO_Mode_Out_PP; g.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, g); s.SPI_Direction SPI_Direction_2Lines_FullDuplex; s.SPI_Mode SPI_Mode_Master; s.SPI_DataSize SPI_DataSize_8b; s.SPI_CPOL SPI_CPOL_Low; // CPOL0 s.SPI_CPHA SPI_CPHA_1Edge; // CPHA0,Mode 0 s.SPI_NSS SPI_NSS_Soft; // 软件片选,禁用硬件NSS s.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_256; s.SPI_FirstBit SPI_FirstBit_MSB; s.SPI_CRCPolynomial 7; SPI_Init(SPI1, s); SPI_Cmd(SPI1, ENABLE); }分频先给到25672MHz除以256后约280kHz低于SD卡协议规定的400kHz初始化上限。时钟使能要同时打开SPI1和GPIOA两个外设只开一个直接挂死。MISO配成输入上拉而不是浮空避免卡还没初始化时引脚电平不确定导致读进杂波。SPI_NSS配置为SPI_NSS_Soft后硬件NSS完全失效片选完全由PA4的电平控制这就是软件片选的含义。相比硬件NSS自动拉低拉高软片选的最大优势是传输边界完全由代码掌握不会因为发送缓冲空了半路把NSS抬起导致命令截断。接线表如下F103引脚SD卡卡座信号说明PA4CS软件片选低有效PA5CLKSPI时钟PA6DOMISO输入上拉PA7DIMOSI推挽输出3V3VDD单独供电旁路22uF电容GNDVSS与单片机共地卡座原理图上常见的CD卡检测和WP写保护两个脚SPI方案里可以不接。但卡座的VDD不能和其他外设的3.3V混用写操作瞬间电流大共用一条细走线会把电平拉垮。若卡座带WP检测脚并且接了固定的低电平也会引发写保护误报后面排错章节专门说这个。2.3 SD卡初始化CMD0到CMD16的SPI命令握手SD卡上电后必须先做初始化握手。实质是用74个以上时钟脉冲让卡退出上电后的未知状态用CMD0把卡切到SPI模式用CMD8确认协议版本再循环ACMD41等待上电完成。命令字节统一是016位命令号后面跟4字节参数和1字节CRC。先封装字节收发函数static u8 SPI_ReadWrite(u8 dat) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendByte(SPI1, dat); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) RESET); return SPI_I2S_ReceiveByte(SPI1); }然后写命令发送函数。SD_CS(x)用GPIO_WriteBit控制PA4电平这里不再展开#define SD_OK 0 #define SD_ERR_CMD0 1 #define SD_ERR_CMD8 2 #define SD_ERR_ACMD41 3 static u8 SD_Command(u8 cmd, u32 arg, u8 crc, u8 extra_len) { u8 r, i; SD_CS(0); SPI_ReadWrite(cmd | 0x40); // 命令字节,最高两位固定01 SPI_ReadWrite(arg 24); SPI_ReadWrite(arg 16); SPI_ReadWrite(arg 8); SPI_ReadWrite(arg); SPI_ReadWrite(crc); // 非CMD0/CMD8时CRC被忽略 for (i 0; i 8; i) { // 等待R1响应,最多8字节 r SPI_ReadWrite(0xFF); if ((r 0x80) 0) break; // R1响应的bit70 } for (i 0; i extra_len; i) SPI_ReadWrite(0xFF); // 丢弃R7/R3的附加字节 SD_CS(1); return r; }extra_len表示R1之后还要读掉几个字节。CMD8的R7响应除R1外还有4字节所以传4CMD58的R3同理传4CMD0和ACMD41只有R1传0。这个参数容易漏残留字节会留在卡的命令状态机上下一帧命令的第一个字节就可能读到旧响应导致CRC判断全部错乱。初始化主流程如下。CMD0返回0x01才是正确进入SPI模式CMD8返回0x05是非法命令说明是SD V1.x老卡可以继续走ACMD41流程u8 SD_Init(void) { u8 r 0, v2 0; u16 retry; SD_CS(1); for (u16 i 0; i 16; i) // 74个以上时钟脉冲 SPI_ReadWrite(0xFF); if (SD_Command(CMD0, 0x00000000, 0x95, 0) ! 0x01) return SD_ERR_CMD0; // 卡没进SPI模式 r SD_Command(CMD8, 0x000001AA, 0x87, 4); if (r 0x01) v2 1; // 支持CMD8,SD V2.0以上 else if (r ! 0x05) return SD_ERR_CMD8; // 0x05非法命令老卡,继续 for (retry 0; retry 100; retry) { SD_Command(CMD55, 0x00000000, 1, 0); // ACMD41前必须先CMD55 r SD_Command(CMD41, 0x40000000, 1, 0); if (r 0x00) break; // 0x00表示上电完成 delay_ms(10); } if (retry 100) return SD_ERR_ACMD41; // 卡长时间忙碌 if (v2) { r SD_Command(CMD58, 0x00000000, 1, 4); // 读OCR,判断CCS位是否SDHC } SD_Command(CMD16, 512, 1, 0); // 块长度固定为512 return SD_OK; }CMD55和CMD41必须成对出现而且每一轮循环都要重新发CMD55只发一次后面重复CMD41卡会直接无视。CMD41的参数0x40000000中的0x40表示主机支持SDHCSDSC老卡会忽略这个位。ACMD41循环是整个初始化里最容易卡住的地方新卡几十次就能通过扩容卡或翻新卡会拖到接近上限排查时优先怀疑卡本身。2.4 初始化后把SPI时钟提到18MHzSD卡协议规定初始化期间主机时钟必须低于400kHz数据阶段可以放开。F103的SPI1时钟分频由CR1寄存器的BR位控制直接改寄存器即可void SPI1_SetSpeed(u8 br) { SPI1-CR1 0xFFC7; // 清BR[5:3] SPI1-CR1 | br; // 写入新分频系数 }初始化完成后调用SPI1_SetSpeed(SPI_BaudRatePrescaler_4)72MHz除以4得18MHz。SPI_BaudRatePrescaler_2对应36MHz但F103数据手册明确SPI1最高18MHz强行用36MHz会出现偶发数据错位。切换时钟后等TXE标志置位再发第一个命令字节防止时钟切换瞬间产生多余边沿。CubeMX生成的HAL工程里这一步等价于重新初始化SPI并把BaudRatePrescaler改成4效果一样。3. FAT32文件系统与FatFs移植diskio.c的对接是核心3.1 为什么FAT32加FatFs是F103的默认答案F103的RAM通常在20KB到48KB之间自己实现一个完整的FAT32文件系统并不现实。FAT32是目前microSD卡出厂最常见的格式电脑直接识别数据拔卡就能看。FAT16受2GB容量上限限制64GB以上的大卡出厂格式很多是exFATF103方案不建议直接上exFAT需要FatFs打开_USE_EXFAT宏FAT表更大20KB内存跑起来很吃力。常见做法是先把大卡用电脑格回FAT32或者直接选32GB以内的卡。自己写FAT32驱动更不值得。目录项、FAT表、长文件名、断电恢复这些边界情况任何一个处理不到位就会出现文件大小对不上、目录扫不出来这类诡异问题。FatFs把这一层全部封装好f_open/f_read/f_write的用法和PC端C语言文件读写操作代码几乎一致。F103项目里最常见的落地路径就是移植FatFs再在diskio.c里接上自己写的SD卡驱动核心工作量不在API调用而在磁盘层对接。3.2 ffconf.h关键宏与F103适配参数FatFs的行为集中在ffconf.h里配置。移植到F103时下面这几个宏必须逐个确认宏推荐值影响与代价_USE_LFN2支持长文件名2表示缓冲区放调用者栈约1KB_FS_READONLY0必须为0否则所有写操作返回写保护错误_USE_MKFS1启用f_mkfs新卡格式化需要_MAX_SS512扇区上限设4096时每个卷多占约4KB RAM_VOLUMES1卷数量只有一张卡就设1_USE_STRFUNC1启用f_printf调试打日志方便_USE_LFN取值0表示完全关闭长文件名1表示缓冲区放静态区2表示放栈上。F103的栈默认1KB左右如果主函数里还有大数组_USE_LFN设2可能爆栈此时改回1代价是多占约1KB静态RAM。_MAX_SS设到4096时FatFs内部要为每个卷分配两个扇区缓冲RAM直接翻倍只有明确知道卡是4K扇区才需要这样配。提示这些宏修改后必须重新编译整个工程FatFs没有运行时配置改ffconf.h不重编会得到行为不一致的镜像。3.3 diskio.c对接从扇区到文件系统的最后一公里磁盘层要实现disk_initialize、disk_status、disk_read、disk_write、disk_ioctl、get_fattime这六个函数。F103上最直接的映射方式如下DSTATUS disk_initialize(BYTE pdrv) { return SD_Init() SD_OK ? 0 : STA_NOINIT; } DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) { for (UINT i 0; i count; i) { if (SD_ReadSector(buff i * 512, sector i) ! SD_OK) return RES_ERROR; } return RES_OK; } DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count) { for (UINT i 0; i count; i) { if (SD_WriteSector((u8 *)buff i * 512, sector i) ! SD_OK) return RES_ERROR; } return RES_OK; } DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { switch (cmd) { case GET_SECTOR_SIZE: *(WORD *)buff 512; return RES_OK; case GET_BLOCK_SIZE: *(DWORD *)buff 128; return RES_OK; case GET_WRITE_PROTECT: *(WORD *)buff 0; return RES_OK; case CTRL_SYNC: return RES_OK; } return RES_PARERR; } DWORD get_fattime(void) { return 0x20250101u; // 固定时间戳,非0即可 }SD_ReadSector和SD_WriteSector内部用CMD17和CMD24单扇区传输发命令后等0xFE数据头再搬512字节CRC字节可以丢弃。这里sector参数是LBA_t类型新版FatFs里是64位F103上转给CMD17时强转u32即可卡容量远不到2TB没有截断风险。disk_ioctl的GET_WRITE_PROTECT分支要特别小心很多从网上抄来的disk_ioctl把这里写死成返回1卡没锁也一样报写保护。get_fattime返回固定时间戳就行但不要返回00会让FatFs认为时间无效文件列表里日期全乱排查问题时容易误判文件新旧。4. FAT32文件读写实战从f_mount到f_write的最小工程4.1 挂载检查与f_mkfs格式化文件操作之前先挂载文件系统。每次换卡或者重新上电都要重新挂载挂载失败最常见的返回值是FR_NO_FILESYSTEM也就是卡上根本不是FAT格式FATFS fs; FRESULT res; res f_mount(fs, 0:, 1); // 最后一个参数1表示立即挂载 if (res FR_NO_FILESYSTEM) { // 卡上没有FAT文件系统 res f_mkfs(0:, 0, 0); // 格式化,参数按FatFs版本 if (res ! FR_OK) return res; res f_mount(fs, 0:, 1); // 格式化后必须重新挂载 }f_mount的第一个参数是FATFS结构体指针FatFs会把它登记进内部数组换卡之后要重新调用f_mount否则拿到的还是旧卡的文件系统信息。f_mkfs的参数在不同版本FatFs里有差异R0.11到R0.14的签名是f_mkfs(路径, 分区方式, 分配单元)R0.15之后改成结构体加工作缓冲区编译报错时先核对版本再改参数。4.2 日志追加写入f_lseek到文件末尾再f_write日志场景最常用的是追加写。f_open的模式标志决定了文件打开方式模式标志行为FA_CREATE_NEW文件已存在则返回FR_EXISTFA_CREATE_ALWAYS无条件新建覆盖旧文件FA_OPEN_ALWAYS存在则打开不存在则新建FA_OPEN_APPEND新版FatFs的追加标志等价于打开后定位到末尾遵循旧版本兼容性的话用f_lseek加f_size手动定位u8 write_log(const u8 *data, u16 len) { FIL file; UINT wrote 0; FRESULT res; res f_open(file, 0:/run.log, FA_OPEN_ALWAYS | FA_WRITE); if (res ! FR_OK) return res; f_lseek(file, f_size(file)); // 文件指针移到末尾 res f_write(file, data, len, wrote); if (wrote len) res FR_INT_ERR; // 卡空间不足,写入字节数不够 f_sync(file); // 缓冲落盘,防掉电丢数据 f_close(file); return res; }f_close不能省FatFs很多写操作是先在扇区缓冲里攒着的f_close才把缓冲和目录项写回卡。如果是周期性掉电的设备每次写完调用f_sync比最后统一f_close更能抗断电。f_write返回后检查wrote和len是否相等不等说明卡满了。4.3 读取与目录遍历读取文件用FA_READ打开循环f_read直到返回字节数小于请求数即到文件末尾。目录扫描需要f_opendir和f_readdir配合DIR dir; FILINFO fi; if (f_opendir(dir, 0:/) FR_OK) { for (;;) { if (f_readdir(dir, fi) ! FR_OK || fi.fname[0] 0) break; // 读到空项说明遍历完成 if (fi.fattrib AM_DIR) printf(/%s/ 目录\n, fi.fname); else printf(/%s %lu 字节\n, fi.fname, fi.fsize); } }开了_USE_LFN之后FILINFO需要额外设置lfname和lfsize两个字段否则只返回短文件名中文长文件名会显示成波浪号加序号的形式。FILINFO建议定义成全局变量或者每次循环都重新清零复用一个残留了旧lfname指针的局部变量会踩到野指针。4.4 FRESULT返回值速查与排查方向返回值数值含义优先排查方向FR_DISK_ERR1底层读写失败SPI线序、MISO电平、卡供电FR_NOT_READY3设备未就绪disk_initialize返回值非0ACMD41超时FR_NO_FILE4文件不存在路径写错、文件被改名FR_DENIED7访问被拒绝以写方式打开只读文件FR_EXIST8文件已存在FA_CREATE_NEW重复创建FR_WRITE_PROTECTED10卡被识别为写保护见第5章排错FR_NO_FILESYSTEM13卡上不是FAT格式先f_mkfs格式化FR_NOT_READY但SD_Init本身返回SD_OK的情况基本是disk_initialize里没把SD_Init的结果映射成正确状态值。另一个容易踩的是FR_INVALID_OBJECT常见诱因是同一个FATFS结构体被两个线程同时使用进了RTOS之后要给f_open到f_close整段加互斥锁FatFs本身不提供线程安全。5. 进阶排错写保护误报、SPI时序验证与多扇区提速5.1 卡没锁却返回FR_WRITE_PROTECTED先查这三个地方microSD卡本身没有物理写保护触点写保护状态来自卡套侧边的Lock滑块和卡内部状态。先把卡从卡套里取出来确认滑块没有滑到锁定位再回到程序里查。第一个原因是编译期配置ffconf.h里_FS_READONLY写成1时任何带FA_WRITE的f_open都会得到FR_WRITE_PROTECTED这个错误在抄源码包时特别常见。第二个原因是disk_ioctl的GET_WRITE_PROTECT分支写死返回1正确做法是把缓冲里的值置0。第三个原因是SPI通信本身出错卡的状态寄存器被误读成写保护此时往往伴随初始化失败或读写出错重点看MISO线上的波形。5.2 用逻辑分析仪验证SPI时序与R1响应排错不要靠猜。初始化阶段抓SCLK、MOSI、MISO三根线正常波形里CMD0之后MISO应出现0x01CMD8之后出现0x01开头的R1随后4字节的0x00 0x00 0x01 0xAA。R1一直是0xFF说明命令没被卡收到检查CS拉低的时机和上电时钟个数R1返回0x05是CRC被卡拒绝CMD0的0x95和CMD8的0x87不要随意改。线长超过10cm时波形上升沿变钝把SPI时钟降到4MHz以下再抓一次排除反射干扰。手里备一张确定完好的卡能快速区分是卡的问题还是时序问题。5.3 多块传输与缓冲重叠把读写速度提上去18MHz下用单扇区CMD17和CMD24读写每512字节都要付一次命令响应开销大文件吞吐很难看。改成多块传输读用CMD18连续读写用CMD25连续写一次搬运多个扇区读完发CMD12结束。FatFs对顺序读会合并扇区请求disk_read的count参数大于1时走CMD18分支单扇区保持CMD17写入侧同理。缓冲方面F103可以用两段RAM交替SPI由DMA搬运文件系统在另一段缓冲里解析传输与解析重叠吞吐能再提一节注意DMA缓冲区起始地址要按4字节对齐SPI的FIFO才不会错位。本文还有配套的精品资源点击获取
返回列表