3. SPI模式初始化流程:上电时序、CMD0复位、CMD8电压检测、ACMD41初始化

各位同学,今天我们来啃一块硬骨头——SD卡在SPI模式下的初始化流程。说实话,这块内容我当年刚接触时也踩了不少坑。你想想看,SD卡本身是个黑盒子,你发命令它回响应,中间任何一个时序不对,卡就不理你了。我个人的习惯是,先把整个流程在脑子里过一遍,再动手写代码。

SPI模式初始化,说白了就是让SD卡从「未知状态」进入「SPI工作模式」的过程。整个过程分为四个关键步骤:上电时序 → CMD0复位 → CMD8电压检测 → ACMD41初始化。每一步都有讲究,我们一个一个来看。

SPI模式初始化流程 ① 上电时序 延时 ≥ 1ms + 74个时钟周期 ② CMD0 复位 发送 0x40 0x00 0x00 0x00 0x00 0x95 ③ CMD8 电压检测 检查卡是否支持 2.7V~3.6V CMD8 响应? 无响应 SDSCv1 / MMC (旧卡,兼容处理) 有响应 SDHC / SDXC (支持高容量) ④ ACMD41 初始化 循环发送直到卡就绪 (busy=0)

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的发送流程:

  1. 发送CMD55(0x77 + 参数0x00000000 + CRC)
  2. 等待CMD55响应(应为0x01)
  3. 发送ACMD41(0x69 + 参数 + CRC)
  4. 等待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波形,看看命令和响应是否对齐。很多时候,问题出在时序上,而不是代码逻辑上。