1. 为什么选择SDIO驱动SD卡而非SPI?
作为一名长期从事STM32开发的工程师,我最初也是从SPI方式驱动SD卡入门的。但实际项目中发现,当需要频繁读写大容量数据时(比如采集传感器数据存储为CSV文件),SPI的瓶颈就非常明显了。以常见的Class 4 SD卡为例:
- SPI模式(1bit总线):实测写入速度约300-500KB/s
- SDIO模式(4bit总线):实测写入速度可达2-3MB/s
速度差异主要来自三个方面:
- 总线宽度:SPI是单线传输,SDIO支持4线并行
- 时钟频率:SPI通常工作在18MHz以下,SDIO可达到25MHz
- 协议开销:SDIO有专门的命令通道和数据通道
重要提示:STM32F103的SDIO接口最高支持24MHz时钟,但实际使用中发现超过18MHz后稳定性下降。建议先以12MHz调试,稳定后再逐步提高。
2. 硬件设计与准备工作
2.1 硬件连接要点
SDIO标准引脚定义如下表所示:
| SD卡引脚 | STM32引脚 | 功能说明 |
|---|---|---|
| DAT2 | PC10 | 数据线2 |
| DAT3 | PC11 | 数据线3(兼作CS) |
| CMD | PD2 | 命令线 |
| CLK | PC12 | 时钟线 |
| DAT0 | PC8 | 数据线0 |
| DAT1 | PC9 | 数据线1 |
| VSS | GND | 地线 |
| VDD | 3.3V | 电源(注意:不可接5V) |
实际布线时要注意:
- 走线长度尽量等长(特别是时钟线)
- 电源引脚需加100nF去耦电容
- 在DAT0-DAT3线上串联22Ω电阻(防信号反射)
2.2 硬件调试技巧
第一次上电前建议:
- 用万用表检查所有连线无短路
- 先不插SD卡,测量VDD电压是否为3.3V±5%
- 插入SD卡后,监测电流应在50mA以内(空载状态)
常见硬件问题排查:
- 如果初始化失败,先检查CLK是否有波形
- 数据传输错误时,用示波器看DAT0-DAT3信号质量
- 电源不稳会导致枚举失败,可尝试加大电源滤波电容
3. SDIO协议核心流程解析
3.1 卡初始化序列
完整的初始化流程如下(以SDHC卡为例):
c复制// 简化后的初始化代码框架
SD_Error SD_Init(void)
{
// 1. 硬件复位(拉低CLK 74个周期)
SDIO_DeInit();
SDIO_CmdInitStructure.SDIO_Argument = 0;
SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_GO_IDLE_STATE;
SDIO_SendCommand(&SDIO_CmdInitStructure);
// 2. 发送CMD8检查电压范围
SDIO_CmdInitStructure.SDIO_Argument = 0x1AA;
SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_SEND_IF_COND;
SDIO_SendCommand(&SDIO_CmdInitStructure);
// 3. 发送ACMD41激活初始化流程
do {
SDIO_CmdInitStructure.SDIO_Argument = 0x40000000; // HCS=1
SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_APP_CMD;
SDIO_SendCommand(&SDIO_CmdInitStructure);
SDIO_CmdInitStructure.SDIO_Argument = 0x40000000;
SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_SD_APP_OP_COND;
SDIO_SendCommand(&SDIO_CmdInitStructure);
} while((resp & 0x80000000) == 0);
// 4. 获取CID信息
SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_ALL_SEND_CID;
SDIO_SendCommand(&SDIO_CmdInitStructure);
// 5. 设置相对地址(RCA)
SDIO_CmdInitStructure.SDIO_Argument = 0x0000;
SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_SET_REL_ADDR;
SDIO_SendCommand(&SDIO_CmdInitStructure);
// 6. 选择卡并进入传输状态
SDIO_CmdInitStructure.SDIO_Argument = RCA << 16;
SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_SEL_DESEL_CARD;
SDIO_SendCommand(&SDIO_CmdInitStructure);
// 7. 设置总线宽度为4bit
SDIO_CmdInitStructure.SDIO_Argument = 0x00000002; // 4bit模式
SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_APP_CMD;
SDIO_SendCommand(&SDIO_CmdInitStructure);
SDIO_CmdInitStructure.SDIO_Argument = 0x00000002;
SDIO_CmdInitStructure.SDIO_CmdIndex = SD_CMD_SET_BUS_WIDTH;
SDIO_SendCommand(&SDIO_CmdInitStructure);
return SD_OK;
}
3.2 关键参数说明
-
时钟分频计算:
- STM32F103主频72MHz
- 期望SDIO时钟12MHz时:
code复制分频系数 = 72 / (2 + CLKDIV) 取CLKDIV=2 → 72/(2+2)=18MHz 再通过SDIO_ClockEdgeConfig()选择下降沿采样
-
DMA配置要点:
c复制DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&SDIO->FIFO; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)Buffer; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 读方向 DMA_InitStructure.DMA_BufferSize = BlockSize * Blocks; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_Init(DMA2_Channel4, &DMA_InitStructure);
4. FatFs文件系统移植
4.1 底层接口实现
需要实现的六个关键函数:
c复制// 磁盘初始化
DSTATUS disk_initialize(BYTE pdrv)
{
if(SD_Init() != SD_OK) return STA_NOINIT;
return 0;
}
// 读扇区
DRESULT disk_read(BYTE pdrv, BYTE* buff, LBA_t sector, UINT count)
{
if(SD_ReadBlock(buff, sector * 512, count * 512) != SD_OK)
return RES_ERROR;
return RES_OK;
}
// 写扇区(需实现写等待)
DRESULT disk_write(BYTE pdrv, const BYTE* buff, LBA_t sector, UINT count)
{
if(SD_WriteBlock((BYTE*)buff, sector * 512, count * 512) != SD_OK)
return RES_ERROR;
while(SD_GetStatus() == SD_TRANSFER_BUSY); // 等待写入完成
return RES_OK;
}
// 其他必要函数
DSTATUS disk_status(BYTE pdrv) { /*...*/ }
DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void* buff) { /*...*/ }
4.2 性能优化技巧
-
启用写入缓存:
c复制FATFS fs; f_mount(&fs, "", 1); DWORD clust; f_getfree("", &clust, &fs); // 获取空闲簇数 -
合理设置簇大小:
- 对于16GB SD卡,推荐簇大小16KB
- 修改ffconf.h中的
_MAX_SS和_MIN_SS
-
定时调用sync函数:
c复制void SD_SyncTask(void) { static uint32_t last = 0; if(HAL_GetTick() - last > 5000) { // 每5秒同步 f_sync(&file); last = HAL_GetTick(); } }
5. 实测性能对比
使用1MB文件写入测试:
| 模式 | 写入时间 | 速度 |
|---|---|---|
| SPI(1bit) | 2.1s | 476KB/s |
| SDIO(4bit) | 0.4s | 2.5MB/s |
实际项目中发现,连续写入小文件时(如每100ms写入1KB数据),SPI模式会出现明显延迟累积,而SDIO模式依然能保持稳定。
6. 常见问题解决方案
6.1 初始化失败
现象:卡在ACMD41循环
- 检查电源电压(需3.3V±5%)
- 尝试降低时钟频率(设为6MHz调试)
- 确认SD卡格式为FAT32(容量>2GB时)
6.2 数据校验错误
排查步骤:
- 用示波器检查CLK和DAT信号质量
- 检查DMA缓冲区是否4字节对齐
c复制__align(4) uint8_t buffer[512]; // 确保对齐 - 尝试在SDIO初始化后添加延迟
6.3 FatFs返回FR_DISK_ERR
可能原因:
- 底层读写函数未正确处理多扇区操作
- 卡未正确初始化(调用disk_initialize)
- 文件系统损坏(可尝试重新格式化)
7. 进阶优化方向
-
启用DMA双缓冲:
c复制// 在SDIO初始化后添加 SDIO_DMACmd(ENABLE); DMA_DoubleBufferModeConfig(DMA2_Channel4, (uint32_t)Buffer0, DMA_Memory_0); DMA_DoubleBufferModeConfig(DMA2_Channel4, (uint32_t)Buffer1, DMA_Memory_1); DMA_DoubleBufferModeCmd(DMA2_Channel4, ENABLE); -
动态时钟调整:
c复制void SD_SetSpeed(SD_Speed speed) { if(speed == SD_SPEED_HIGH) { SDIO_ClockSet(24, SDIO_CLOCK_EDGE_RISING); // 24MHz } else { SDIO_ClockSet(6, SDIO_CLOCK_EDGE_RISING); // 6MHz } } -
错误恢复机制:
c复制SD_Error SD_RetryRead(uint32_t addr, uint8_t *buf, uint32_t len) { SD_Error status; uint8_t retry = 3; do { status = SD_ReadBlock(buf, addr, len); if(status == SD_OK) break; SD_DeInit(); SD_Init(); } while(--retry); return status; }
移植过程中最耗时的部分是调试SDIO的初始化流程。建议先用逻辑分析仪抓取命令序列,对照SD协议文档逐条验证。当看到CMD8返回0x1AA响应时,说明硬件层已经正常工作,剩下的就是按照协议流程逐步实现。