STM32软件模拟SPI驱动SD卡全流程解析(基于FATFS文件系统)

张开发
2026/4/16 11:17:07 15 分钟阅读

分享文章

STM32软件模拟SPI驱动SD卡全流程解析(基于FATFS文件系统)
1. 为什么需要软件模拟SPI驱动SD卡在嵌入式开发中SD卡因其大容量、便携性和低成本成为存储方案的首选。但实际项目中常遇到硬件资源紧张的情况比如硬件SPI接口被其他外设占用或者MCU引脚数量有限需要复用。这时候软件模拟SPI就成了救命稻草。我曾在智能家居网关项目里遇到过这种情况——STM32的硬件SPI被射频模块占用但系统又需要记录设备日志到SD卡。通过GPIO模拟SPI的方案成功用普通IO口实现了数据存储成本比换用更大封装的MCU节省了30%。软件模拟SPI的核心优势在于引脚可任意配置。你可以把CLK接到PA5MOSI接到PB3完全根据PCB布线情况灵活安排。但要注意三个关键点时钟频率需控制在SD卡支持范围内通常≤25MHz必须严格遵循SPI时序规范GPIO速度要配置为最高速模式2. SD卡与SPI协议基础解析2.1 SD卡的物理结构拆开一张SD卡内部其实是个微型电脑包含存储阵列、控制器和接口电路。通过SPI通信时我们主要与控制器打交道。这里有个容易忽略的细节——不同容量SD卡的寻址方式不同标准容量卡SDSC≤2GB按字节地址寻址高容量卡SDHC4GB-32GB按块地址寻址每块512字节扩展容量卡SDXC≥64GB本文不推荐使用实测发现部分32GB卡实际是SDXC标准会导致初始化失败。建议选择16GB及以下容量的SDHC卡。2.2 SPI通信机制SPI协议有四种模式SD卡仅支持模式0(CPOL0,CPHA0)和模式3(CPOL1,CPHA1)。软件模拟时需要特别注意时钟极性匹配这里分享一个调试技巧// 模式3的时钟设置示例 void SPI_CLK_Set(void) { GPIO_SetBits(CLK_PIN); // 空闲时高电平 Delay_us(1); GPIO_ResetBits(CLK_PIN); // 下降沿采样 Delay_us(1); }常见错误是忘记配置GPIO的上拉电阻。MISO线必须加上拉10kΩ左右否则可能无法正确读取数据。我曾因此浪费两小时排查通信失败问题。3. 软件模拟SPI的完整实现3.1 GPIO配置要点使用标准库配置GPIO时有几点需要特别注意CLK和MOSI配置为推挽输出MISO配置为浮空输入或上拉输入CS引脚初始化为高电平所有引脚速度设为50MHzvoid GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // CLK配置 GPIO_InitStructure.GPIO_Pin GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); // MISO配置关键 GPIO_InitStructure.GPIO_Pin GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; // 上拉输入 GPIO_Init(GPIOB, GPIO_InitStructure); }3.2 时序函数实现字节读写是SPI最基础的操作这里给出经过实测稳定的代码uint8_t SPI_ReadWriteByte(uint8_t TxData) { uint8_t i, RxData 0; for(i0; i8; i) { // 设置MOSI (TxData 0x80) ? MOSI_HIGH() : MOSI_LOW(); TxData 1; // 产生上升沿 CLK_HIGH(); Delay_us(1); // 保持时间根据SD卡型号调整 // 读取MISO RxData 1; if(MISO_READ()) RxData | 0x01; // 产生下降沿 CLK_LOW(); Delay_us(1); } return RxData; }延时优化技巧标准SD卡1μs延时足够低速TF卡需要3-5μs延时通过示波器观察波形确保高低电平时间满足SD卡规格书要求4. FATFS文件系统集成4.1 移植关键步骤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_ioctl的实现。必须正确处理以下控制命令case CTRL_SYNC: // 同步缓存 case GET_SECTOR_COUNT: // 获取扇区数 case GET_SECTOR_SIZE: // 获取扇区大小 case GET_BLOCK_SIZE: // 获取擦除块大小4.2 性能优化技巧通过实测发现三个提升速度的方法块读写优化将多个单块读写合并为多块读写缓存机制在RAM中缓存FAT表DMA传输如果有硬件SPI可用// 多块读取示例 DRESULT disk_read(BYTE pdrv, BYTE* buff, LBA_t sector, UINT count) { SD_Error status; status SD_ReadMultiBlocks(buff, sector, 512, count); while(SD_GetStatus() ! SD_TRANSFER_OK); return (status SD_RESPONSE_NO_ERROR) ? RES_OK : RES_ERROR; }5. 完整代码解析5.1 初始化流程SD卡初始化需要遵循特定序列发送至少74个时钟周期发送0xFFCMD0进入SPI模式CMD8检查电压范围ACMD41初始化卡CMD58读取OCR寄存器SD_Error SD_Init(void) { // 发送80个时钟周期 for(int i0; i10; i) SD_WriteByte(0xFF); // CMD0进入SPI模式 if(SD_GoIdleState() ! SD_RESPONSE_NO_ERROR) return SD_RESPONSE_FAILURE; // ...后续初始化步骤 }5.2 数据读写示例实现一个完整的文件操作流程FATFS fs; FIL fil; UINT bw; // 挂载文件系统 f_mount(fs, 0:, 1); // 创建文件 f_open(fil, test.txt, FA_CREATE_ALWAYS | FA_WRITE); // 写入数据 f_write(fil, Hello World!, 12, bw); // 关闭文件 f_close(fil);6. 调试与问题排查6.1 常见问题清单现象可能原因解决方案初始化失败电压不足确保3.3V供电读写数据错误时序不匹配调整时钟延时文件系统挂载失败卡未格式化使用电脑格式化为FAT32偶尔读写失败电源干扰增加100nF去耦电容6.2 逻辑分析仪调试推荐使用Saleae逻辑分析仪抓取SPI波形重点关注CS信号是否在传输期间保持低电平CLK频率是否超过SD卡限制MOSI/MISO数据是否对齐时钟边沿曾经通过波形分析发现一个隐蔽问题CS信号在字节传输间隔出现毛刺导致SD卡误判命令结束。通过增加CS保持时间解决了该问题。最后建议大家在PCB设计时将SD卡走线尽可能短CLK信号周围铺地隔离。软件上做好错误重试机制这样的系统才能稳定运行。

更多文章