3. SPI模式初始化流程:上电时序、CMD0复位、CMD8电压检测、ACMD41初始化
各位同学,今天我们来啃一块硬骨头——SD卡在SPI模式下的初始化流程。说实话,这块内容我当年刚接触时也踩了不少坑。你想想看,SD卡本身是个黑盒子,你发命令它回响应,中间任何一个时序不对,卡就不理你了。我个人的习惯是,先把整个流程在脑子里过一遍,再动手写代码。
SPI模式初始化,说白了就是让SD卡从「未知状态」进入「SPI工作模式」的过程。整个过程分为四个关键步骤:上电时序 → CMD0复位 → CMD8电压检测 → ACMD41初始化。每一步都有讲究,我们一个一个来看。
3.1 上电时序——别急着发命令
很多新手上来就给SD卡发CMD0,结果卡死活没反应。为什么?因为卡还没准备好。SD卡上电后需要一段稳定时间,这个时间至少1毫秒。但光等1ms还不够,你还需要发送至少74个SPI时钟周期的空闲时钟,让卡内部的时钟同步电路稳定下来。
核心要点:上电后,先拉高CS片选信号,然后发送至少74个时钟周期的0xFF。我习惯发80个周期,多几个总没错。
// 上电时序示例代码
void SD_SPI_PowerOn(void) {
// 1. 初始化SPI接口:模式0,时钟频率100-400kHz
SPI_Init(SPI_MODE0, 400000);
// 2. 拉高CS片选
CS_HIGH();
// 3. 延时至少1ms
delay_ms(2); // 我习惯给2ms,留点余量
// 4. 发送80个空闲时钟周期
for (int i = 0; i < 10; i++) {
SPI_TransferByte(0xFF);
}
}
小技巧:上电时的SPI时钟频率不要超过400kHz。这是SD卡规范明确要求的。等初始化完成后,再切换到更高的时钟频率(比如20MHz)。
3.2 CMD0复位——让卡进入SPI模式
上电完成后,第一件事就是发送CMD0。CMD0的作用是让SD卡进入空闲状态(Idle State)。但这里有个关键点:SD卡默认工作在SD总线模式下,你需要通过CMD0的特定参数告诉它切换到SPI模式。
CMD0的格式是固定的:6个字节。第一个字节是命令索引(0x40),后面4个字节是参数(全0),最后一个字节是CRC校验。在SPI模式下,CMD0的CRC是固定的0x95。
// CMD0命令帧
// 字节0: 0x40 | 0x00 = 0x40 (命令索引0,起始位+传输位)
// 字节1-4: 0x00 0x00 0x00 0x00 (参数,全0)
// 字节5: 0x95 (CRC7 + 结束位1)
uint8_t cmd0[] = {0x40, 0x00, 0x00, 0x00, 0x00, 0x95};
SPI_SendBuffer(cmd0, 6);
发送完CMD0后,卡会返回一个响应字节。如果响应是0x01,表示卡进入了空闲状态,初始化成功。如果一直收不到0x01,说明卡没响应,可能是时序问题或者硬件连接有问题。
注意:CMD0发送后,卡可能不会立即响应。你需要持续发送时钟(0xFF)并读取MISO引脚,直到收到有效响应。我曾经遇到过一块卡需要等待几十个时钟周期才响应,差点以为卡坏了。
3.3 CMD8电压检测——区分卡的类型
CMD8是初始化流程中最容易被忽略的一步。它的作用是检测SD卡支持的电压范围。说白了,就是问卡:「你支持2.7V到3.6V的工作电压吗?」
CMD8的参数格式比较特殊:前12位是保留位(全0),中间8位是电压范围(0x01表示2.7V~3.6V),最后12位是检查模式(一般填0xAA)。
// CMD8命令帧 (电压范围2.7-3.6V,检查模式0xAA)
// 参数 = 0x000001AA
uint8_t cmd8[] = {0x48, 0x00, 0x00, 0x01, 0xAA, 0x87};
SPI_SendBuffer(cmd8, 6);
CMD8的响应有两种情况:
- 有响应(0x01 + 4字节数据):说明卡支持SDHC/SDXC标准,是高容量卡。后续ACMD41需要设置HCS位(bit30)。
- 无响应(超时):说明卡是旧版SDSCv1或MMC卡。这类卡不支持CMD8,需要走兼容流程。
经验之谈:我建议在代码中把CMD8的响应结果保存下来。因为后续ACMD41的参数需要根据这个结果来决定是否设置HCS位。很多移植问题就出在这里——明明插的是SDHC卡,却忘了设HCS位。
3.4 ACMD41初始化——让卡准备好工作
ACMD41是初始化流程的最后一步,也是最耗时的一步。它的作用是让卡完成内部初始化,包括电压匹配、容量识别等。ACMD41是一个特殊应用命令,发送前必须先发送CMD55,告诉卡「接下来的命令是应用命令」。
ACMD41的发送流程:
- 发送CMD55(0x77 + 参数0x00000000 + CRC)
- 等待CMD55响应(应为0x01)
- 发送ACMD41(0x69 + 参数 + CRC)
- 等待ACMD41响应
ACMD41的参数中,bit30是HCS位(高容量支持),bit31一般置1。如果之前CMD8有响应,HCS位要设为1。
// ACMD41循环发送,直到卡就绪
uint8_t SD_ACMD41(uint8_t hcs) {
uint32_t timeout = 1000; // 超时计数
uint8_t resp;
do {
// 发送CMD55
SPI_CS_LOW();
uint8_t cmd55[] = {0x77, 0x00, 0x00, 0x00, 0x00, 0x01};
SPI_SendBuffer(cmd55, 6);
resp = SPI_ReadResponse();
SPI_CS_HIGH();
if (resp != 0x01) {
return resp; // CMD55失败
}
// 发送ACMD41
SPI_CS_LOW();
uint32_t arg = 0x40000000; // bit31 = 1
if (hcs) arg |= 0x40000000; // bit30 = HCS
uint8_t cmd41[] = {
0x69,
(uint8_t)(arg >> 24),
(uint8_t)(arg >> 16),
(uint8_t)(arg >> 8),
(uint8_t)(arg),
0x01 // CRC (SPI模式下忽略)
};
SPI_SendBuffer(cmd41, 6);
resp = SPI_ReadResponse();
SPI_CS_HIGH();
if (resp == 0x00) {
return 0x00; // 初始化成功
}
delay_ms(10);
timeout--;
} while (timeout > 0);
return 0xFF; // 超时失败
}
优化建议:ACMD41的循环等待时间可能很长,有些卡需要几百毫秒才能完成初始化。我建议在循环中加入适当的延时(比如10ms),避免CPU空转。另外,超时时间建议设为1秒以上,我见过一些慢卡需要800ms才就绪。
3.5 完整初始化流程总结
好了,我们把四个步骤串起来,看看完整的初始化流程:
| 步骤 | 命令 | 预期响应 | 说明 |
|---|---|---|---|
| 1 | 上电 + 74+时钟 | 无 | 等待卡稳定,发送空闲时钟 |
| 2 | CMD0 | 0x01 | 进入空闲状态,切换到SPI模式 |
| 3 | CMD8 | 0x01 + 数据 / 超时 | 检测电压范围,区分卡类型 |
| 4 | CMD55 + ACMD41 | 0x00 | 循环发送直到卡就绪 |
避坑指南:我曾经在移植代码时犯过一个低级错误——在ACMD41循环中忘了重新拉低CS片选。结果卡一直返回0x01(忙状态),我查了整整一天才发现是CS没控制好。记住:每个命令发送前都要确保CS拉低,发送完成后拉高。
初始化完成后,卡就进入了数据传输模式(Data Transfer Mode)。这时候你可以读取CSD寄存器获取卡容量信息,也可以开始读写数据了。不过那是下一章的内容,我们先把初始化吃透。
嗯,最后说一句:初始化流程看似简单,但每个细节都可能导致失败。我建议你在调试时用逻辑分析仪抓一下SPI波形,看看命令和响应是否对齐。很多时候,问题出在时序上,而不是代码逻辑上。