STM32F103VET6实战:SPI模式驱动SD卡与FatFs文件系统移植全解析
当我们需要在嵌入式系统中实现数据存储功能时,SD卡因其体积小、容量大、价格低廉等优势成为首选。本文将深入探讨如何在STM32F103VET6平台上通过SPI接口驱动SD卡,并完整移植FatFs文件系统。
1. 硬件准备与基础概念
在开始之前,我们需要明确几个关键点。STM32F103VET6是一款基于ARM Cortex-M3内核的微控制器,具有512KB Flash和64KB RAM,完全能够胜任文件系统操作的需求。SPI(Serial Peripheral Interface)是一种同步串行通信接口,相比SDIO模式,SPI实现更简单,对硬件要求更低。
所需硬件清单:
- STM32F103VET6开发板(带TFT屏幕和SD卡槽)
- 8GB或以下容量的SD卡(建议使用Class4或Class6速度等级)
- USB转TTL串口模块(用于调试输出)
- 杜邦线若干
SPI引脚连接参考:
| SD卡引脚 | STM32 SPI1引脚 | 功能说明 |
|---|---|---|
| CS | PA4 | 片选信号 |
| DI | PA7 | 数据输入 |
| DO | PA6 | 数据输出 |
| CLK | PA5 | 时钟信号 |
注意:不同开发板的SD卡槽可能使用不同的SPI接口,请根据实际硬件原理图确认连接方式。
2. FatFs文件系统架构解析
FatFs是一个专为小型嵌入式系统设计的通用FAT文件系统模块,具有以下特点:
- 完全独立于平台,纯C语言实现
- 代码占用空间小(根据配置不同约3-10KB)
- 支持FAT12/FAT16/FAT32/exFAT
- 支持多卷(物理驱动器和分区)
- 支持多种编码(包括UTF-8)
FatFs的核心架构分为三个层次:
- 应用层:提供文件操作API如f_open、f_read等
- 中间层:处理FAT文件系统逻辑
- 设备层:与物理存储设备交互的接口
我们需要重点关注的是设备层的移植,主要实现diskio.c中的五个关键函数:
DSTATUS disk_status(BYTE pdrv); DSTATUS disk_initialize(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);3. SPI模式下的SD卡驱动实现
SD卡在SPI模式下的通信遵循特定的协议和时序要求。以下是实现过程中的关键点:
3.1 SD卡初始化流程
- 硬件初始化:
void SD_SPI_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; // 使能SPI和GPIO时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1 | RCC_APB2Periph_GPIOA, ENABLE); // 配置SPI引脚 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); // 配置CS引脚 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // SPI配置 SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_InitStructure.SPI_CRCPolynomial = 7; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }- SD卡初始化序列:
- 发送至少74个时钟周期(不选中卡)
- 发送CMD0(GO_IDLE_STATE)使卡进入SPI模式
- 发送CMD8(SEND_IF_COND)检查电压范围
- 发送ACMD41(SD_SEND_OP_COND)初始化卡
- 发送CMD58(READ_OCR)读取OCR寄存器
- 发送CMD16(SET_BLOCKLEN)设置块长度(通常为512字节)
提示:SD卡在初始化阶段需要使用低速时钟(通常<400kHz),初始化完成后可切换到高速模式。
3.2 读写操作实现
单块读取函数示例:
uint8_t SD_ReadSingleBlock(uint32_t sector, uint8_t* buffer) { uint8_t response; // 发送CMD17(READ_SINGLE_BLOCK) response = SD_SendCommand(CMD17, sector << 9); if(response != 0x00) { return response; } // 等待数据开始标记 while(SD_SPI_ReadByte() != 0xFE); // 读取512字节数据 for(uint16_t i = 0; i < 512; i++) { buffer[i] = SD_SPI_ReadByte(); } // 读取并丢弃2字节CRC SD_SPI_ReadByte(); SD_SPI_ReadByte(); return 0; }多块写入函数示例:
uint8_t SD_WriteMultipleBlocks(uint32_t sector, uint8_t* buffer, uint32_t count) { uint8_t response; // 发送CMD25(WRITE_MULTIPLE_BLOCK) response = SD_SendCommand(CMD25, sector << 9); if(response != 0x00) { return response; } for(uint32_t block = 0; block < count; block++) { // 发送数据开始标记 SD_SPI_WriteByte(0xFC); // 写入512字节数据 for(uint16_t i = 0; i < 512; i++) { SD_SPI_WriteByte(buffer[block * 512 + i]); } // 写入2字节伪CRC SD_SPI_WriteByte(0xFF); SD_SPI_WriteByte(0xFF); // 等待写入完成 while(SD_SPI_ReadByte() != 0xFF); } // 发送停止传输命令 SD_SPI_WriteByte(0xFD); return 0; }4. FatFs移植关键步骤
4.1 diskio.c接口实现
disk_initialize函数:
DSTATUS disk_initialize(BYTE pdrv) { if(pdrv != 0) return STA_NOINIT; static uint8_t initialized = 0; if(initialized) return 0; SD_Init(); initialized = 1; return 0; }disk_read函数:
DRESULT disk_read(BYTE pdrv, BYTE* buff, LBA_t sector, UINT count) { if(pdrv != 0) return RES_PARERR; for(UINT i = 0; i < count; i++) { if(SD_ReadSingleBlock(sector + i, buff + i * 512) != 0) { return RES_ERROR; } } return RES_OK; }disk_ioctl函数:
DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void* buff) { if(pdrv != 0) return RES_PARERR; switch(cmd) { case CTRL_SYNC: return RES_OK; case GET_SECTOR_COUNT: *(DWORD*)buff = SD_GetSectorCount(); 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; } }4.2 ffconf.h配置调整
以下是几个关键配置项:
#define FF_FS_READONLY 0 // 0:启用读写功能 #define FF_FS_MINIMIZE 0 // 0:启用所有功能 #define FF_USE_STRFUNC 1 // 1:启用字符串操作函数 #define FF_USE_MKFS 1 // 1:启用格式化功能 #define FF_USE_FASTSEEK 1 // 1:启用快速定位功能 #define FF_CODE_PAGE 936 // 936:简体中文 #define FF_USE_LFN 1 // 1:启用长文件名支持 #define FF_MAX_SS 512 // 最大扇区大小 #define FF_MIN_SS 512 // 最小扇区大小5. 应用层开发与调试技巧
5.1 文件操作示例
基本文件操作流程:
- 挂载文件系统
- 打开/创建文件
- 读写操作
- 关闭文件
- 卸载文件系统
示例代码:
FATFS fs; FIL fil; UINT bw; // 挂载文件系统 f_mount(&fs, "0:", 1); // 创建并写入文件 f_open(&fil, "0:/test.txt", FA_CREATE_ALWAYS | FA_WRITE); f_write(&fil, "Hello, STM32!", 13, &bw); f_close(&fil); // 读取文件内容 char buffer[20]; f_open(&fil, "0:/test.txt", FA_READ); f_read(&fil, buffer, sizeof(buffer), &bw); f_close(&fil); // 卸载文件系统 f_mount(NULL, "0:", 0);5.2 常见问题排查
问题1:SD卡初始化失败
- 检查硬件连接是否正确
- 确保在初始化阶段使用低速时钟
- 确认SD卡格式为FAT32(建议使用SD Formatter工具格式化)
问题2:文件系统挂载失败
- 检查disk_initialize是否成功
- 确认ffconf.h中的扇区大小设置与SD卡一致
- 尝试使用f_mkfs格式化SD卡
问题3:写入速度慢
- 初始化完成后提高SPI时钟频率
- 使用多块写入代替单块写入
- 减少文件系统操作(如f_sync)的频率
6. 性能优化与高级功能
6.1 提高读写速度
通过以下方法可以显著提高SD卡的读写性能:
- 提高SPI时钟频率:初始化完成后可提升至最高18MHz(SD卡规范限制)
- 使用DMA传输:减少CPU开销
- 批量读写操作:减少单次操作的开销
- 合理设置缓存:利用文件系统的缓存机制
DMA配置示例:
void SD_SPI_DMA_Init(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel3); // SPI1_RX DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)0; // 运行时设置 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = 512; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel3, &DMA_InitStructure); SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Rx, ENABLE); }6.2 实现文件系统监控
通过定期检查文件系统状态,可以实现更健壮的存储系统:
FRESULT check_fs_health(void) { FATFS* fs; DWORD free_clust; // 获取空闲簇数量 if(f_getfree("0:", &free_clust, &fs) != FR_OK) { return FR_DISK_ERR; } // 检查文件系统一致性 if(fs->fs_type == 0) { return FR_NO_FILESYSTEM; } return FR_OK; }在实际项目中,我发现SD卡在频繁断电情况下容易出现文件系统损坏。通过添加适当的异常处理机制和定期维护(如碎片整理),可以显著提高数据存储的可靠性。