别再只会插拔了!深入浅出聊聊SD卡与单片机通信的‘暗号’:命令、响应与数据块
别再只会插拔了!深入浅出聊聊SD卡与单片机通信的‘暗号’:命令、响应与数据块
想象一下,当你第一次尝试用STM32驱动SD卡时,面对那些神秘的CMD指令、复杂的响应格式和令人头疼的数据块传输,是不是感觉像在破解某种古老密码?别担心,今天我们就来揭开这层神秘面纱,用最接地气的方式理解SD卡与单片机之间的"对话"。
1. 认识SD卡的"语言体系"
SD卡本质上是一个高度结构化的存储设备,它和单片机之间的通信遵循一套严格的协议。这套协议就像两个陌生人初次见面时的社交礼仪,需要特定的"开场白"(初始化)、"确认身份"(命令响应)和"交换信息"(数据传输)。
SD卡通信的核心要素:
- 命令(CMD):单片机发给SD卡的指令,相当于你说的话
- 响应(Response):SD卡对命令的回复,就像对方的回答
- 数据块(Data Block):实际传输的内容,好比你们交换的实物
提示:SD卡支持两种通信模式:SPI模式和SD模式。本文以更常见的SPI模式为例讲解,原理同样适用于SD模式。
2. 破解SD卡的"暗号":命令详解
2.1 命令格式解析
每条SD卡命令都是一个48位的固定格式数据包,可以分解为:
| 字段 | 长度(bit) | 说明 |
|---|---|---|
| 起始位 | 1 | 固定为0 |
| 传输位 | 1 | 1表示主机→卡 |
| 命令索引 | 6 | 命令编号(如CMD0,CMD1等) |
| 参数 | 32 | 命令参数 |
| CRC7 | 7 | 校验位(SPI模式可省略) |
| 结束位 | 1 | 固定为1 |
在STM32 HAL库中,发送命令的函数通常长这样:
void SD_SendCmd(uint8_t cmd, uint32_t arg, uint8_t crc) { uint8_t frame[6]; frame[0] = 0x40 | cmd; // 01xxxxxx frame[1] = (arg >> 24) & 0xFF; frame[2] = (arg >> 16) & 0xFF; frame[3] = (arg >> 8) & 0xFF; frame[4] = arg & 0xFF; frame[5] = crc; HAL_SPI_Transmit(&hspi1, frame, 6, 1000); }2.2 关键命令解析
必须掌握的5个核心命令:
CMD0 (GO_IDLE_STATE)
- 作用:让SD卡进入空闲状态
- 参数:0x00000000
- 相当于说:"嘿,咱们重新认识一下"
CMD8 (SEND_IF_COND)
- 作用:检查SD卡电压兼容性
- 参数:0x000001AA (2.7-3.6V, 检查模式0xAA)
- 相当于问:"你能在3.3V下工作吗?"
CMD16 (SET_BLOCKLEN)
- 作用:设置数据块长度(通常512字节)
- 参数:块长度(如0x00000200)
- 相当于约定:"我们每次搬运512字节的货物"
CMD17 (READ_SINGLE_BLOCK)
- 作用:读取单个数据块
- 参数:块地址
- 相当于请求:"请给我第X号仓库的货物"
CMD24 (WRITE_BLOCK)
- 作用:写入单个数据块
- 参数:块地址
- 相当于指令:"请把这些货物存到第X号仓库"
3. 听懂SD卡的"回话":响应解析
3.1 响应类型
SD卡有5种响应格式,最常见的是R1响应(1字节):
| 位 | 名称 | 说明 |
|---|---|---|
| 7 | IDLE | 卡处于空闲状态 |
| 6 | ERASE_RESET | 擦除序列被清除 |
| 5 | ILLEGAL_CMD | 非法命令 |
| 4 | CRC_ERROR | CRC校验失败 |
| 3 | ERASE_SEQ_ERROR | 擦除序列错误 |
| 2 | ADDRESS_ERROR | 地址错误 |
| 1 | PARAMETER_ERROR | 参数错误 |
| 0 | 保留 | 总是0 |
在代码中检查响应的典型实现:
uint8_t SD_GetResponse(uint8_t expected) { uint8_t response; uint32_t timeout = 0xFFFF; while(timeout--) { HAL_SPI_Receive(&hspi1, &response, 1, 100); if(response != 0xFF) break; // 0xFF表示无响应 } if((response & 0x80) || (response != expected)) { // 错误处理 return SD_ERROR; } return SD_OK; }3.2 典型响应场景
场景1:初始化时的对话
单片机: CMD0 (重置) SD卡: 0x01 (IDLE状态) 单片机: CMD8 (检查兼容性) SD卡: 0x01 (IDLE状态) + 额外4字节确认 单片机: CMD55+ACMD41 (初始化) SD卡: 0x00 (准备就绪)场景2:读取数据时的对话
单片机: CMD17 (读取块) SD卡: 0x00 (命令接受) ...等待数据令牌0xFE... SD卡: [512字节数据] + [2字节CRC]4. "货物交接"的艺术:数据块传输
4.1 数据包结构
每个数据块都有严格的封装格式:
[起始令牌0xFE] + [512字节数据] + [2字节CRC]在STM32中读取数据块的典型代码:
uint8_t SD_ReadBlock(uint32_t blockAddr, uint8_t *buffer) { // 发送读取命令 SD_SendCmd(CMD17, blockAddr, 0xFF); // 等待数据令牌 uint8_t token; do { HAL_SPI_Receive(&hspi1, &token, 1, 100); } while(token == 0xFF); if(token != 0xFE) return SD_ERROR; // 读取数据 HAL_SPI_Receive(&hspi1, buffer, 512, 1000); // 丢弃CRC uint8_t crc[2]; HAL_SPI_Receive(&hspi1, crc, 2, 100); return SD_OK; }4.2 写入数据流程
写入操作更复杂,需要确认SD卡是否准备好:
- 发送CMD24 + 地址
- 等待SD卡响应0x00
- 发送起始令牌0xFE
- 发送512字节数据
- 发送2字节CRC
- 接收数据响应令牌
- 等待SD卡完成编程
uint8_t SD_WriteBlock(uint32_t blockAddr, uint8_t *buffer) { // 发送写入命令 SD_SendCmd(CMD24, blockAddr, 0xFF); if(SD_GetResponse(0x00) != SD_OK) return SD_ERROR; // 发送数据包 uint8_t token = 0xFE; HAL_SPI_Transmit(&hspi1, &token, 1, 100); HAL_SPI_Transmit(&hspi1, buffer, 512, 1000); // 发送伪CRC uint8_t crc[2] = {0xFF, 0xFF}; HAL_SPI_Transmit(&hspi1, crc, 2, 100); // 获取数据响应 uint8_t dataResp; HAL_SPI_Receive(&hspi1, &dataResp, 1, 100); if((dataResp & 0x1F) != 0x05) return SD_ERROR; // 等待编程完成 uint8_t busy; do { HAL_SPI_Receive(&hspi1, &busy, 1, 100); } while(busy == 0x00); return SD_OK; }5. 实战技巧与常见问题
5.1 初始化流程优化
一个健壮的初始化流程应该包含以下步骤:
- 发送≥74个时钟周期(SPI模式下)
- CMD0 → 进入IDLE状态
- CMD8 → 检查电压兼容性
- CMD55 + ACMD41 → 初始化SD卡
- CMD58 → 读取OCR寄存器(可选)
- CMD16 → 设置块长度
注意:不同类型(SDSC/SDHC/SDXC)的卡初始化过程可能略有不同,建议查阅具体规格书。
5.2 错误处理策略
常见错误及解决方案:
- 无响应:检查硬件连接、SPI配置、CS信号
- 非法命令:确认卡类型和支持的命令集
- CRC错误:检查数据传输完整性
- 超时:适当增加等待时间或重试机制
在项目中,我习惯为每个关键操作添加超时检测:
#define SD_TIMEOUT 100000 uint8_t SD_WaitReady(void) { uint8_t response; uint32_t timeout = SD_TIMEOUT; do { HAL_SPI_Receive(&hspi1, &response, 1, 100); if(timeout-- == 0) return SD_TIMEOUT; } while(response != 0xFF); return SD_OK; }5.3 性能优化技巧
- 多块传输:使用CMD18/25代替CMD17/24
- 预取数据:在需要前提前读取相邻块
- 缓存管理:实现简单的读写缓存机制
- 减少擦除:合理规划写入位置
// 多块读取示例 uint8_t SD_ReadMultiBlocks(uint32_t startAddr, uint8_t *buffer, uint32_t blockCount) { SD_SendCmd(CMD18, startAddr, 0xFF); if(SD_GetResponse(0x00) != SD_OK) return SD_ERROR; while(blockCount--) { // 等待数据令牌 uint8_t token; do { HAL_SPI_Receive(&hspi1, &token, 1, 100); } while(token == 0xFF); if(token != 0xFE) return SD_ERROR; // 读取数据 HAL_SPI_Receive(&hspi1, buffer, 512, 1000); buffer += 512; // 丢弃CRC uint8_t crc[2]; HAL_SPI_Receive(&hspi1, crc, 2, 100); } // 发送停止传输命令 SD_SendCmd(CMD12, 0, 0xFF); return SD_GetResponse(0x00); }