单片机读写 SD 卡:SPI 模式与 FATFS 文件系统实战指南
👁 8 阅读 · 2026-08-21 · 嵌入式 存储
在嵌入式系统中,SD 卡因其大容量、低成本、易携带等优势,成为数据存储的首选方案。本文深入讲解基于 SPI 接口的 SD 卡读写原理,从 SD 卡初始化、SPI 模式配置到 FATFS 文件系统的移植与使用,提供完整的代码示例和调试经验,帮助开发者快速实现数据记录、固件升级等应用。
## 为什么选择 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 模式。
### 初始化步骤
1. **上电延时**:至少 1ms,然后发送至少 74 个时钟脉冲(CS 拉高)。
2. **进入 SPI 模式**:CS 拉低,发送 CMD0(GO_IDLE_STATE),等待响应 0x01。
3. **发送 CMD8**(SEND_IF_COND):检测卡类型(SD v2.0 及以上),并检查电压范围。
4. **发送 CMD58**(READ_OCR):读取 OCR 寄存器,确认支持 3.3V。
5. **发送 CMD55 + CMD41**(ACMD41):初始化卡,循环发送直到响应为 0x00。
6. **发送 CMD58** 再次读取,检查 CCS 位(卡容量类型)。
7. **发送 CMD16**(SET_BLOCKLEN):设置块长度为 512 字节(仅对 SDSC 卡有效)。
### 代码示例(基于 STM32 HAL 库)
```c
// 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 官网](http://elm-chan.org/fsw/ff/) 下载最新源码,包含 `ff.c`, `ff.h`, `diskio.c`, `diskio.h` 等文件。
### 底层接口实现
FATFS 需要开发者提供 6 个底层函数:`disk_initialize`, `disk_status`, `disk_read`, `disk_write`, `disk_ioctl`, `get_fattime`。
```c
// 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。
## 文件读写示例
```c
// 挂载文件系统
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 调整代码。掌握这一技术后,可扩展至多文件管理、日志系统、固件升级等高级应用。