## 为什么选择 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 调整代码。掌握这一技术后,可扩展至多文件管理、日志系统、固件升级等高级应用。