为什么选择 SPI 模式与 FATFS?
SD 卡支持 SDIO 和 SPI 两种通信模式。对于大多数 MCU(如 STM32、ESP32、AVR),SPI 模式具有以下优势:
- 引脚占用少(仅需 4 根线),硬件资源要求低。
- 协议相对简单,易于用 GPIO 模拟或硬件 SPI 实现。
- 兼容性广,几乎所有 SD 卡都支持 SPI 模式。
而 FATFS 是一个轻量级、开源的文件系统模块,支持 FAT12/16/32 和 exFAT,非常适合嵌入式环境。它提供了文件创建、读写、删除等标准 API,让开发者无需关心底层扇区操作。
硬件连接与 SPI 配置
典型电路连接
| SD 卡引脚 | SPI 功能 | MCU 引脚 | |-----------|----------|----------| | CS | 片选 | GPIO | | DI | MOSI | SPI_MOSI | | DO | MISO | SPI_MISO | | SCLK | 时钟 | SPI_SCK | | VCC | 电源 | 3.3V | | GND | 地 | GND |
注意:SD 卡必须使用 3.3V 供电,若 MCU 为 5V,需加电平转换电路。
SPI 参数设置
- 时钟频率:初始化时使用 400kHz 以下(SD 卡要求),初始化完成后可提升至 10-25MHz。
- 模式:SPI Mode 0(CPOL=0, CPHA=0)或 Mode 3(CPOL=1, CPHA=1),多数卡支持 Mode 0。
- 数据位:8 位,MSB 先行。
SD 卡初始化流程
SD 卡在 SPI 模式下初始化需要遵循特定时序,否则无法进入 SPI 模式。
初始化步骤
- 上电延时:至少 1ms,然后发送至少 74 个时钟脉冲(CS 拉高)。
- 进入 SPI 模式:CS 拉低,发送 CMD0(GO_IDLE_STATE),等待响应 0x01。
- 发送 CMD8(SEND_IF_COND):检测卡类型(SD v2.0 及以上),并检查电压范围。
- 发送 CMD58(READ_OCR):读取 OCR 寄存器,确认支持 3.3V。
- 发送 CMD55 + CMD41(ACMD41):初始化卡,循环发送直到响应为 0x00。
- 发送 CMD58 再次读取,检查 CCS 位(卡容量类型)。
- 发送 CMD16(SET_BLOCKLEN):设置块长度为 512 字节(仅对 SDSC 卡有效)。
代码示例(基于 STM32 HAL 库)
// SD 卡 SPI 初始化函数
uint8_t SD_Init(void) {
uint8_t response;
uint8_t cmd[6];
uint32_t timeout;
// 1. 上电延时和时钟脉冲
HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);
HAL_Delay(1);
for (int i = 0; i < 10; i++) {
SPI_WriteByte(0xFF); // 发送 80 个时钟
}
// 2. 进入 SPI 模式:发送 CMD0
SD_SendCmd(0x40, 0x00000000, 0x95); // CMD0 带 CRC
response = SD_ReadByte();
if (response != 0x01) return 1; // 期望进入空闲态
// 3. 发送 CMD8 检查 SD v2.0
SD_SendCmd(0x48, 0x000001AA, 0x87); // CMD8 参数 0x1AA
response = SD_ReadByte();
if (response == 0x01) {
// 读取 4 字节返回数据
for (int i = 0; i < 4; i++) SD_ReadByte();
} else {
return 2; // 可能是 SD v1.0 或 MMC
}
// 4. 循环发送 ACMD41
timeout = 10000;
do {
SD_SendCmd(0x77, 0x00000000, 0x01); // CMD55
SD_ReadByte(); // 忽略响应
SD_SendCmd(0x69, 0x40000000, 0x01); // ACMD41 带 HCS 位
response = SD_ReadByte();
timeout--;
} while (response != 0x00 && timeout > 0);
if (timeout == 0) return 3; // 初始化超时
// 5. 读取 OCR 确认 CCS
SD_SendCmd(0x7A, 0x00000000, 0x01); // CMD58
response = SD_ReadByte();
uint8_t ocr[4];
for (int i = 0; i < 4; i++) ocr[i] = SD_ReadByte();
if (ocr[0] & 0x40) {
// CCS=1,SDHC/SDXC 卡,块长度固定 512
} else {
// CCS=0,SDSC 卡,需要设置块长度
SD_SendCmd(0x50, 512, 0x01); // CMD16
SD_ReadByte();
}
return 0; // 成功
}
// 发送命令函数
void SD_SendCmd(uint8_t cmd, uint32_t arg, uint8_t crc) {
SPI_WriteByte(cmd);
SPI_WriteByte((arg >> 24) & 0xFF);
SPI_WriteByte((arg >> 16) & 0xFF);
SPI_WriteByte((arg >> 8) & 0xFF);
SPI_WriteByte(arg & 0xFF);
SPI_WriteByte(crc);
}
FATFS 文件系统的移植
获取源码
从 FatFs 官网 下载最新源码,包含 ff.c, ff.h, diskio.c, diskio.h 等文件。
底层接口实现
FATFS 需要开发者提供 6 个底层函数:disk_initialize, disk_status, disk_read, disk_write, disk_ioctl, get_fattime。
// diskio.c 中的实现示例
// 初始化磁盘
DSTATUS disk_initialize(BYTE pdrv) {
if (pdrv != 0) return STA_NOINIT;
if (SD_Init() == 0) {
return 0; // 成功
} else {
return STA_NOINIT;
}
}
// 读扇区
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_ReadSector(sector + i, buff + i * 512) != 0) {
return RES_ERROR;
}
}
return RES_OK;
}
// 写扇区
DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count) {
if (pdrv != 0) return RES_PARERR;
for (UINT i = 0; i < count; i++) {
if (SD_WriteSector(sector + i, (BYTE *)buff + i * 512) != 0) {
return RES_ERROR;
}
}
return RES_OK;
}
// 其他函数:disk_ioctl 需支持 GET_SECTOR_COUNT, GET_SECTOR_SIZE, GET_BLOCK_SIZE
DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) {
if (pdrv != 0) return RES_PARERR;
switch (cmd) {
case GET_SECTOR_COUNT:
*(LBA_t *)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;
}
}
配置 ffconf.h
-
FF_USE_MKFS:设为 1 以支持格式化(可选)。 -
FF_USE_LFN:设为 1 支持长文件名(需额外缓冲区)。 -
FF_VOLUMES:设为 1,只用一个磁盘。 -
FF_MIN_SS和FF_MAX_SS:设为 512。
文件读写示例
// 挂载文件系统
FATFS fs;
FRESULT res = f_mount(&fs, "", 1);
if (res != FR_OK) {
// 挂载失败,可能需要格式化
f_mkfs("", 0, 0); // 格式化
res = f_mount(&fs, "", 1);
}
// 创建并写入文件
FIL file;
res = f_open(&file, "data.txt", FA_CREATE_ALWAYS | FA_WRITE);
if (res == FR_OK) {
char buf[] = "Hello SD Card!\r\n";
UINT bytes_written;
f_write(&file, buf, sizeof(buf) - 1, &bytes_written);
f_close(&file);
}
// 读取文件
res = f_open(&file, "data.txt", FA_READ);
if (res == FR_OK) {
char buf[64];
UINT bytes_read;
f_read(&file, buf, sizeof(buf), &bytes_read);
buf[bytes_read] = '\0';
f_close(&file);
}
注意事项与调试技巧
- 电源稳定性:SD 卡在写入时电流较大,需在 VCC 和 GND 之间加 10µF 和 100nF 去耦电容。
- 电平匹配:确保所有信号线电平在 3.3V,避免损坏 SD 卡。
- 初始化时序:CMD0 的 CRC 必须正确(0x95),否则卡不会响应。
- 响应超时:ACMD41 循环可能持续数百毫秒,设置合理超时(如 1 秒)。
- SPI 频率:初始化后提升频率时,确保 PCB 走线质量,否则可能通信失败。
- 调试工具:使用逻辑分析仪抓取 SPI 波形,对比 SD 卡规范中的时序图。
- FATFS 挂载失败:检查底层读写函数是否正确,特别是扇区编号和缓冲区对齐。
总结
通过 SPI 模式驱动 SD 卡并移植 FATFS,可以轻松实现嵌入式系统的数据存储功能。本文提供了从硬件连接到软件实现的完整流程,开发者可根据实际 MCU 调整代码。掌握这一技术后,可扩展至多文件管理、日志系统、固件升级等高级应用。