1. 项目概述:为什么嵌入式开发者需要深入了解SD卡的SPI模式?
如果你在玩单片机、树莓派或者任何嵌入式项目,想把数据存到一张小小的SD卡里,那你大概率会用到SPI(Serial Peripheral Interface)协议。你可能已经用过了像SD.h这样的库,点几下鼠标就能读写文件,感觉挺简单。但当你遇到一些“玄学”问题——比如卡初始化失败、读写速度慢得离谱、或者在某些极端条件下数据莫名其妙出错——你就会发现,不了解底层协议,调试起来简直像在抓瞎。
SD卡主要有两种通信模式:SD模式和SPI模式。SD模式速度快,引脚多,通常用在手机、相机里。而SPI模式,则是我们嵌入式开发者的“老朋友”。它协议简单,只需要4根线(甚至3根线也能凑合),几乎所有的MCU(从51单片机到STM32,再到ESP32)都原生支持。这份资料,就是一份关于SD卡SPI模式的“底层通讯手册”精要解读。它不讲怎么调用高级API,而是深入到字节和时钟周期,告诉你主机(你的MCU)和从机(SD卡)之间每一个比特是怎么“对话”的。理解了这个,你就能自己写驱动、优化性能、甚至解决那些库解决不了的硬件级兼容性问题。
2. SPI模式核心设计思路:化繁为简的嵌入式哲学
2.1 从SD模式到SPI模式的切换逻辑
SD卡上电后,默认处于SD模式。这是一个更复杂、更高效的模式,需要6根数据线。那么,它如何知道我们想用SPI模式呢?答案就在CMD0(GO_IDLE_STATE)这条复位命令上。
这里有一个关键硬件交互细节:片选信号CS(或叫SS)的电平状态。在发送CMD0命令的整个过程中(包括命令本身和等待响应),如果主机将CS信号线拉低(通常低电平有效),这就向SD卡发送了一个明确的硬件信号:“嘿,我要用SPI模式跟你聊天”。如果CS为高,卡会认为主机希望继续使用SD模式,从而忽略这条命令。
注意:这个切换是“单向的”。一旦卡进入SPI模式,在当前上电周期内就无法再跳回SD模式。唯一的复位方法是断电重启。这意味着在你的固件设计里,初始化阶段就必须明确模式选择,且后续所有操作都要基于SPI协议进行。
2.2 SPI模式下的总线事务模型:命令、响应与数据块
SPI模式下的所有通信,都由主机(MCU)发起和控制,遵循“一问一答”的范式。每次通信称为一个“总线事务”,都以主机拉低CS信号开始,拉高CS信号结束。
一个完整的事务通常包含以下令牌(Token)序列:
- 命令令牌(Command Token):主机发送一个6字节的命令帧,告诉卡要做什么(如读、写、查询状态)。
- 响应令牌(Response Token):卡收到命令后,会回复一个或多个字节的响应,告诉主机命令是否被接受、执行状态或错误信息。
- 数据令牌(Data Token)(可选):如果命令涉及数据传输(如读块、写块),则会紧跟数据块。读操作时,由卡发送数据令牌;写操作时,由主机发送数据令牌。
- 数据响应令牌(Data Response Token)(仅写操作):主机发送完一个数据块后,卡会回复一个字节,确认数据是否被成功接收。
这种结构化的对话方式,使得通信过程清晰可控。与SD模式一个显著的不同是,在SPI模式下,卡总是会对命令做出响应。在SD模式下,如果卡忙或不支持该命令,可能直接超时无响应;而在SPI模式下,无论如何都会回一个响应字节(比如R1格式),其中包含错误位,这让软件层面的错误处理更加直接。
2.3 CRC保护与非保护模式:效率与可靠性的权衡
在SD模式中,命令、响应和数据都强制使用CRC(循环冗余校验)来确保传输的准确性。但SPI模式提供了一个贴心的“非保护模式”。
在非保护模式下,命令和数据帧中虽然仍然保留CRC字段的位置,但发送方可以填入任意值(通常填0或固定值),接收方则会直接忽略这些CRC位,不做校验。这大大简化了主机端软件的实现,因为不需要实时计算CRC值,尤其对于低端MCU来说,节省了宝贵的计算资源。
实操心得:对于大多数应用场景(尤其是教学、原型开发),强烈建议开启非保护模式(使用CMD59)。这能极大降低驱动开发的复杂度。只有在高可靠性要求、或电气环境非常恶劣(干扰大)的情况下,才需要考虑启用CRC保护。但要注意,切换模式的CMD59命令本身,其CRC必须是正确的。而让卡进入SPI模式的CMD0命令,无论后续是否用CRC,其CRC字段都必须是一个固定的正确值
0x95,这是一个硬性规定。
3. 核心细节解析:命令、响应与数据传输的魔鬼细节
3.1 命令令牌的精确构成
SPI模式下的所有命令都是固定的6字节(48位)。其格式必须严格遵守:
| 字节位置 | 内容 | 说明 |
|---|---|---|
| 字节1 | 01xxxxxx | 起始位0+传输位1+6位命令索引(如CMD17是10001=0x11) |
| 字节2-5 | 参数 | 命令参数,如要读写的扇区地址。大端格式(MSB First)。 |
| 字节6 | CRC7 + 结束位1 | 7位CRC校验码(非保护模式下可忽略) + 固定的停止位1。 |
例如,发送CMD17(READ_SINGLE_BLOCK)读取地址为0x00010000的扇区,在非保护模式下,命令帧如下:0x51, 0x00, 0x01, 0x00, 0x00, 0xFF
0x51:01(起始+传输) +10001(CMD17索引=17) =01010001= 0x51。0x00, 0x01, 0x00, 0x00: 32位地址0x00010000,大端传输。0xFF: 非保护模式下,CRC字段和结束位可填充0xFF(二进制11111111),其中最后一位1是停止位。
3.2 响应令牌的家族与解读
卡通过响应令牌告知主机命令执行结果。SPI模式下主要有几种响应格式,看懂它们是调试的关键。
3.2.1 R1响应(1字节)这是最常用的响应,在绝大多数命令后返回(除了CMD13)。它是一个字节,最高位(bit7)始终为0,低7位是错误标志位(为1表示有错误)。
Bit7 | Bit6 | Bit5 | Bit4 | Bit3 | Bit2 | Bit1 | Bit0 0 | E | C | X | A | P | I | IDLE- IDLE (Bit0): 卡处于空闲状态,通常在上电或CMD0后,初始化完成前该位为1。
- ERASE_RESET (Bit1): 擦除序列被清除。
- ILLEGAL_COMMAND (Bit2): 收到非法命令代码。这是最常见错误,通常意味着命令索引错误、或当前卡状态不支持该命令。
- COM_CRC_ERROR (Bit3): 上一个命令的CRC校验错误(如果开启了CRC)。
- ERASE_SEQ_ERROR (Bit4): 擦除命令序列错误。
- ADDRESS_ERROR (Bit5): 地址参数错误(如未对齐、超出范围)。
- PARAMETER_ERROR (Bit6): 参数错误(如块长度设置非法)。
例如,收到响应0x01,表示卡处于空闲态(IDLE=1),无其他错误。收到0x04,表示非法命令(ILLEGAL_COMMAND=1)。
3.2.2 R1b响应格式同R1,但后面可能跟随一串忙信号。卡在响应字节后,将DO(MISO)线持续拉低(输出0),表示正忙(如正在擦除、编程)。主机必须持续提供时钟,并检测DO线变为高电平(0xFF)时,表示卡准备就绪。对于写操作,等待这个忙信号是必须的。
3.2.3 R2响应(2字节)这是CMD13(SEND_STATUS)的专用响应。第一字节与R1相同,第二字节提供了更详细的错误状态,如写保护违规、内部ECC失败等。当读写操作出现问题时,发送CMD13查询R2响应是定位问题的好方法。
3.2.4 R3响应(5字节)这是CMD58(READ_OCR)的响应。第一字节是R1,后面4个字节是OCR(Operating Conditions Register)寄存器的内容,包含卡支持的电压范围、上电完成状态等信息。在初始化时,通过CMD58轮询OCR的Bit31(卡上电完成位),是判断卡是否初始化就绪的标准方法。
3.3 数据读写的完整流程与超时管理
3.3.1 单块读操作(CMD17)流程
- 主机拉低CS。
- 主机发送CMD17命令帧(含地址)。
- 主机持续发送时钟(同时接收数据),等待卡返回R1响应。超时时间(Ncr)通常建议为64个时钟周期。如果超时未收到任何非0xFF的响应,应视为通信失败。
- 收到正确的R1响应(如
0x00)后,等待卡发送数据起始令牌0xFE。等待数据起始超时,这个时间较长,由卡的CSD寄存器中的TAAC和NSAC字段决定,通常需要几毫秒到几百毫秒。实现时必须设置足够长的超时等待。 - 收到
0xFE后,连续读取数据块(通常512字节)。然后是2字节的CRC16(非保护模式下可忽略,但仍需读取以消耗时钟)。 - 主机拉高CS,结束事务。
3.3.2 单块写操作(CMD24)流程
- 主机拉低CS。
- 主机发送CMD24命令帧(含地址)。
- 等待并接收R1响应(应为
0x00)。 - 主机发送数据起始令牌
0xFE。 - 主机发送数据块(如512字节)。
- 主机发送2字节CRC16(非保护模式下可填任意值,如
0xFF, 0xFF)。 - 主机接收数据响应令牌(1字节)。格式为
0bxxx0AAA1,中间三位AAA表示状态:010(0x05): 数据被接受。101(0x0B): CRC错误被拒绝。110(0x0D): 写错误被拒绝。
- 在数据响应之后,卡会进入编程状态,并通过DO线输出忙信号(持续低电平)。主机必须持续提供时钟,直到DO线变高。这个忙期可能长达几十甚至几百毫秒,期间CS必须保持低电平。
- 编程完成后,主机可发送CMD13查询最终状态,确认写入成功。
- 主机拉高CS。
避坑指南:写操作的“忙”等待:很多新手驱动写不对,问题就出在“忙”等待上。不能在发送完数据后立即拉高CS,这会导致编程过程中断,数据损坏。必须用一个循环,在发送完数据响应令牌后,持续读取DO线,直到读到
0xFF。同时,这个循环要有超时机制,防止卡死。
4. 实操过程:从零实现SD卡SPI驱动关键环节
4.1 硬件连接与初始化序列
假设我们使用一颗普通的3.3V SD卡和一颗STM32 MCU,连接如下:
- MCU MOSI -> SD DI (Data In)
- MCU MISO -> SD DO (Data Out)
- MCU SCK -> SD CLK
- MCU GPIO -> SD CS (Chip Select)
- SD VDD -> 3.3V
- SD VSS -> GND
初始化序列(上电或复位后必须执行):
- 硬件延时:上电后,等待至少74个时钟周期(约1ms)让卡稳定。在此期间,CS置高,时钟频率应低于400kHz。
- 进入SPI模式:
- 拉低CS。
- 发送CMD0 (
0x40, 0x00, 0x00, 0x00, 0x00, 0x95)。注意CRC字段固定为0x95。 - 等待R1响应。期望收到
0x01(IDLE状态位为1),表示卡已进入SPI空闲态。 - 如果收到
0xFF(无响应)或错误响应,检查硬件连接、电源、和时序。
- 检查电压兼容性并激活初始化:
- 发送CMD8 (
0x48, 0x00, 0x00, 0x01, 0xAA, 0x87) 查询是否支持V2.0标准。响应R7包含信息。此步可选,但有助于识别卡类型。 - 发送CMD59 (
0x7B, 0x00, 0x00, 0x00, 0x00, 0xFF) 关闭CRC(参数0x00)。响应应为0x01。 - 发送ACMD41(应用特定命令)激活卡。ACMD41是CMD55+CMD41的组合: a. 先发CMD55 (
0x77, 0x00, 0x00, 0x00, 0x00, 0xFF),响应应为0x01。 b. 再发CMD41 (0x69, 0x40, 0x00, 0x00, 0x00, 0xFF)(对于支持高容量的卡,参数带0x40000000)。响应应为0x00。如果仍是0x01,说明卡还在初始化中,需要重复发送CMD55+CMD41,直到响应为0x00。必须设置超时(如1秒),防止死循环。
- 发送CMD8 (
- 读取OCR确认初始化完成:
- 发送CMD58 (
0x7A, 0x00, 0x00, 0x00, 0x00, 0xFF)。 - 接收5字节R3响应。检查第一字节是否为
0x00,并检查后续OCR字节的Bit31(上电完成位)是否为1。同时可以检查Bit[23:0]确认电压范围是否兼容。
- 发送CMD58 (
- 设置扇区大小:
- 通常SD卡默认块大小为512字节。为保险起见,发送CMD16 (
0x50, 0x00, 0x00, 0x02, 0x00, 0xFF) 设置块大小为512(参数0x00000200)。响应应为0x00。
- 通常SD卡默认块大小为512字节。为保险起见,发送CMD16 (
至此,SD卡初始化完成,可以正常进行读写操作。
4.2 单扇区读写函数实现示例(伪代码风格)
以下是用C语言实现的简化版读写函数,展示了核心逻辑:
// 假设已有底层SPI收发函数:spi_txrx(uint8_t data) #define SD_CMD0 0 #define SD_CMD16 16 #define SD_CMD17 17 #define SD_CMD24 24 #define SD_CMD55 55 #define SD_CMD41 41 #define SD_CMD58 58 #define SD_ACMD41 41 // 实际是CMD55+CMD41 #define DATA_START_TOKEN 0xFE #define DATA_RES_MASK 0x1F #define DATA_RES_ACCEPTED 0x05 uint8_t sd_send_cmd(uint8_t cmd, uint32_t arg) { uint8_t crc = 0xFF; // 非保护模式 if (cmd == 0) crc = 0x95; // CMD0需要特殊CRC // 发送6字节命令帧 spi_txrx(0x40 | cmd); // 起始位+命令索引 spi_txrx((arg >> 24) & 0xFF); spi_txrx((arg >> 16) & 0xFF); spi_txrx((arg >> 8) & 0xFF); spi_txrx(arg & 0xFF); spi_txrx(crc); // 等待响应(跳过Ncr个字节的填充位) uint8_t retry = 20; uint8_t response; do { response = spi_txrx(0xFF); retry--; } while ((response & 0x80) && retry); // 等待最高位为0 return response; } uint8_t sd_read_sector(uint32_t sector_addr, uint8_t *buffer) { // SD卡地址以字节为单位,通常我们按512字节扇区操作,所以地址左移9位(乘以512) uint32_t addr = sector_addr << 9; uint8_t response; cs_low(); response = sd_send_cmd(SD_CMD17, addr); if (response != 0x00) { cs_high(); return response; // 返回错误码 } // 等待数据起始令牌0xFE uint16_t retry = 60000; // 超时计数,根据时钟频率调整 while (spi_txrx(0xFF) != DATA_START_TOKEN) { if (--retry == 0) { cs_high(); return 0xFF; // 超时错误 } } // 读取512字节数据 for (uint16_t i = 0; i < 512; i++) { buffer[i] = spi_txrx(0xFF); } // 跳过2字节CRC spi_txrx(0xFF); spi_txrx(0xFF); cs_high(); return 0; // 成功 } uint8_t sd_write_sector(uint32_t sector_addr, const uint8_t *buffer) { uint32_t addr = sector_addr << 9; uint8_t response; cs_low(); response = sd_send_cmd(SD_CMD24, addr); if (response != 0x00) { cs_high(); return response; } // 发送数据起始令牌 spi_txrx(DATA_START_TOKEN); // 发送512字节数据 for (uint16_t i = 0; i < 512; i++) { spi_txrx(buffer[i]); } // 发送2字节伪CRC spi_txrx(0xFF); spi_txrx(0xFF); // 获取数据响应令牌 response = spi_txrx(0xFF); if ((response & DATA_RES_MASK) != DATA_RES_ACCEPTED) { cs_high(); return response; // 数据被拒绝 } // 等待编程完成(忙等待) while (spi_txrx(0xFF) == 0x00) { // 空循环,直到收到非0x00(即0xFF) } cs_high(); return 0; // 成功 }4.3 多块读写与停止传输
多块读(CMD18)和多块写(CMD25)可以提升连续读写的效率。流程与单块类似,但启动后,卡会连续传输多个数据块,直到收到停止命令。
- 多块读停止:发送CMD12(STOP_TRANSMISSION)。命令格式为
0x4C, 0x00, 0x00, 0x00, 0x00, 0xFF。发送后,需要读取一个字节的R1b响应,并等待忙信号结束。 - 多块写停止:在发送完最后一个数据块后,不发送
0xFE起始令牌,而是发送一个特殊的停止传输令牌0xFD。然后同样需要等待数据响应和忙信号。
注意事项:在多块操作中,主机有责任管理好数据流。对于写操作,如果卡在编程过程中遇到错误(如写保护),它会通过数据响应令牌报告。此时主机应使用ACMD22(SEND_NUM_WR_BLOCKS)来查询成功写入的块数,以便进行错误恢复。
5. 常见问题与排查技巧实录
5.1 初始化失败问题排查表
| 现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 发送CMD0后无响应(始终收到0xFF) | 1. 硬件连接错误(CS、MOSI、MISO、CLK) 2. 电源问题(电压不足、电流不够) 3. 时钟频率过快(初始化时应<400kHz) 4. CS信号时序不对(应在命令前拉低并保持) | 1. 用万用表或逻辑分析仪检查线路通断和电平(SD卡是3.3V,确保MCUIO口也是3.3V电平)。 2. 测量SD卡VCC引脚电压,确保在2.7-3.6V之间。可并联一个100uF电容缓冲。 3. 将SPI时钟分频到最低(如系统时钟/256)。 4. 确保在发送命令字节之前拉低CS,并在整个事务完成之后拉高。 |
CMD0响应为0xFF以外的值(非0x01) | 1. 卡已损坏或不支持SPI模式。 2. 上电复位不充分。 | 1. 换一张卡测试。 2. 尝试对卡进行完整的断电再上电,并确保有足够长的上电延时(>1ms)。 |
| ACMD41一直返回0x01(卡在空闲态) | 1. 未先发送CMD55就发送CMD41。 2. 卡初始化过程缓慢(尤其是大容量卡)。 3. CMD41参数不正确(对于SDHC/SDXC卡,需要设置HCS位)。 | 1.确保ACMD41是CMD55+CMD41的组合,必须成对发送。 2. 增加重试次数和超时时间(可重试数百次)。 3. 尝试发送带HCS位(bit30)的CMD41参数,如 0x40000000。 |
| CMD58读取OCR,Bit31始终不为1 | 卡初始化未完成。 | 继续重复发送CMD55+CMD41,直到CMD58返回的OCR Bit31为1。 |
5.2 读写操作中的典型故障
问题:读数据时,始终等不到数据起始令牌0xFE。
- 分析:可能地址非法、卡未初始化完成、或发送读命令后等待时间不足(
TAAC未满足)。 - 解决:确认初始化流程正确完成。检查读命令中的地址参数是否正确(是否乘以了512)。大幅增加等待
0xFE的超时时间,有些低速卡需要几十毫秒的准备时间。可以用逻辑分析仪抓取SPI波形,看卡是否回复了错误响应(R1非0)。
问题:写数据后,数据响应令牌不是0x05(接受)。
- 分析:
0x0B表示CRC错误(如果开启了CRC);0x0D表示写错误。 - 解决:如果是CRC错误,检查是否在非保护模式下误开启了CRC,或CRC计算有误。如果是写错误,最常见的原因是写保护开关被锁定(SD卡侧面的物理开关),或者尝试写入的扇区是写保护的(通过CMD28/29设置)。检查物理开关,并使用CMD13读取状态确认。
问题:写操作后,读取刚写入的数据发现错误或全为0。
- 分析:大概率是忙等待环节出了问题。在卡内部编程(Flash写入)完成前就拉高了CS或进行了其他操作。
- 解决:确保在收到数据响应令牌后,持续读取字节直到收到
0xFF。这个等待循环不能有CS的跳变。同时,在忙等待后,可以发送一次CMD13查询状态,确认“写保护违规”、“卡ECC失败”等位是否被置起。
问题:多块读写时,数据错乱或无法停止。
- 分析:停止传输命令(CMD12或令牌
0xFD)使用不当,或主机在数据传输过程中CS信号不稳定。 - 解决:对于多块读,必须在读完所需数据后发送CMD12,并读取其响应。对于多块写,最后一个块之后发送的是
0xFD令牌,而非0xFE。确保在整个多块操作序列中,CS信号始终保持低电平。
5.3 性能优化与可靠性提升技巧
- 提升SPI时钟速度:初始化完成后,可以通过CMD16测试,逐步提高SPI时钟频率(如从4MHz到25MHz)。但需注意,长导线、劣质卡或干扰环境可能导致高速下通信失败。
- 使用多块读写:对于连续的大文件操作,使用CMD18/CMD25比反复调用单块命令快得多,因为它减少了命令-响应开销。
- 合理处理超时:所有等待响应、等待数据令牌、忙等待的循环都必须有超时退出机制,防止程序因卡死而僵住。超时值可以参考SD物理层规范,并留有余量。
- 电源去耦:在SD卡的VCC和GND引脚附近,放置一个0.1uF和一个10uF的电容,能有效抑制电源噪声,尤其在写操作瞬间电流较大时,避免电压跌落导致操作失败。
- 上拉电阻:在SPI总线的MOSI、MISO、CLK线上,根据需要添加4.7kΩ - 10kΩ的上拉电阻到3.3V,可以提高信号质量,特别是在总线空闲或热插拔时。
理解SD卡SPI模式的底层协议,就像拿到了与存储设备对话的密码本。它让你从库函数的“黑盒”使用者,转变为能够自主掌控通信细节的开发者。当项目遇到棘手的存储问题时,这份深入的理解将成为你最强有力的调试工具。