4. SDIO模式初始化流程:CMD2获取CID、CMD3获取RCA、CMD9获取CSD、CMD7选中卡
好,咱们接着往下走。上一章我们把SD卡从空闲状态拉到了准备就绪,说白了就是让卡知道「我要开始干活了」。但这时候,主机对卡还一无所知——不知道它多大容量、什么品牌、支不支持高速模式。
所以这一章,我们要做四件关键的事:获取CID、获取RCA、获取CSD、选中卡。这四个步骤,是SDIO模式下初始化最核心的流程。我个人习惯把这四个命令称为「四步确认法」,少了哪一步,后面读写数据都会出问题。
核心要点:SDIO初始化流程中,CMD2、CMD3、CMD9、CMD7这四个命令必须按顺序执行,缺一不可。顺序错了,卡可能直接不响应。
4.1 整体流程概览
先看一张流程图,把整个脉络理清楚。你想想看,这就像你去酒店入住:先报名字(CMD2获取CID),然后前台给你房卡(CMD3获取RCA),你再确认一下房间信息(CMD9获取CSD),最后刷卡进门(CMD7选中卡)。
嗯,这里要注意:CMD2和CMD9都是读寄存器操作,但读的内容完全不同。CMD2读的是CID寄存器(128位),CMD9读的是CSD寄存器(128位)。我刚开始做的时候,曾经把这两个命令搞混过,结果解析出来的数据完全不对。
4.2 CMD2:获取CID(卡标识寄存器)
CMD2的全称是ALL_SEND_CID。为什么叫「ALL」?因为这条命令是广播给总线上所有卡的。在SDIO模式下,总线上可能挂多张卡,但CMD2会让所有卡同时返回自己的CID。
CID寄存器里存了什么?说白了就是卡的身份证信息:
| 位域 | 宽度(位) | 描述 |
|---|---|---|
| MID | 8 | 制造商ID(如Sandisk=0x02, Toshiba=0x11) |
| OID | 16 | OEM/应用ID(两个ASCII字符) |
| PNM | 40 | 产品名称(5个ASCII字符) |
| PRV | 8 | 产品版本(BCD码,如0x10表示1.0) |
| PSN | 32 | 产品序列号 |
| MDT | 12 | 生产日期(年+月) |
| CRC | 7 | CRC校验 |
| 保留 | 1 | 固定为1 |
实战技巧:我在项目中遇到过,有些国产SD卡的MID值不在标准列表中。这时候不要慌,只要CRC校验通过,数据就是有效的。你可以把未知的MID记录下来,后续做兼容性处理。
发送CMD2的代码很简单,但要注意响应格式:
// 发送CMD2,获取CID
// 参数:无(固定为0)
// 响应:R2(长响应,136位)
uint8_t sd_cmd2_get_cid(uint8_t *cid_buf) {
sd_cmd_t cmd;
cmd.index = 2; // CMD2
cmd.arg = 0x00000000; // 参数固定为0
cmd.resp_type = RESP_R2; // 长响应
if (sd_send_cmd(&cmd) != SD_OK) {
return SD_ERR_CMD_TIMEOUT;
}
// 从响应中提取128位CID数据
// R2响应包含16字节的CID + 1字节CRC
memcpy(cid_buf, cmd.response, 16);
return SD_OK;
}
这里有个坑:R2响应是136位的,包含128位数据+8位CRC。但实际提取时,我们只需要前128位。我曾经在这里栽过跟头,把CRC位也当成了CID数据的一部分,结果解析出来的产品名称全是乱码。
4.3 CMD3:获取RCA(相对卡地址)
拿到CID之后,下一步就是给卡分配一个「短地址」。CMD3的作用就是让卡返回一个16位的RCA,后续所有针对这张卡的通信,都用这个RCA来标识。
为什么不用CID直接通信?因为CID有128位,每次通信都带这么长的地址太浪费带宽。RCA只有16位,效率高得多。
注意:CMD3是点对点命令,不是广播。发送CMD3时,参数中要带上之前获取的CID(高32位),这样卡才能确认是在跟它说话。如果总线上只有一张卡,参数可以填0。
// 发送CMD3,获取RCA
// 参数:cid_high - CID的高32位(用于匹配卡)
// 返回:16位RCA
uint16_t sd_cmd3_get_rca(uint32_t cid_high) {
sd_cmd_t cmd;
cmd.index = 3;
cmd.arg = cid_high; // 用CID高32位匹配卡
cmd.resp_type = RESP_R6; // 短响应,包含RCA
if (sd_send_cmd(&cmd) != SD_OK) {
return 0; // 返回0表示失败
}
// R6响应中,RCA在高16位
uint16_t rca = (cmd.response[0] >> 16) & 0xFFFF;
return rca;
}
嗯,这里有个细节:RCA是卡自己生成的,不是主机分配的。主机只是发个命令,卡内部会生成一个唯一的16位地址返回。我见过有些卡每次上电生成的RCA都不一样,这很正常。
4.4 CMD9:获取CSD(卡特定数据寄存器)
有了RCA之后,我们就可以针对某张卡查询它的详细参数了。CMD9就是干这个的——读取CSD寄存器。
CSD寄存器里包含的信息非常丰富:
- 存储容量:通过C_SIZE、C_SIZE_MULT、READ_BL_LEN计算
- 速度等级:TRAN_SPEED字段标识最大传输速度
- 块大小:READ_BL_LEN和WRITE_BL_LEN
- 擦除块大小:ERASE_BLK_EN和SECTOR_SIZE
- 写保护:WP_GRP_ENABLE和WP_GRP_SIZE
关键公式:SD卡容量 = (C_SIZE + 1) × 2^(C_SIZE_MULT + 2) × 2^(READ_BL_LEN) 字节
这个公式我每次写驱动都要翻手册确认,因为不同版本的CSD格式略有差异。CSD 1.0和CSD 2.0的计算方式不一样,SDHC/SDXC卡用的是CSD 2.0。
// 发送CMD9,获取CSD
// 参数:rca - 目标卡的相对地址
// 输出:csd_buf - 16字节CSD数据
uint8_t sd_cmd9_get_csd(uint16_t rca, uint8_t *csd_buf) {
sd_cmd_t cmd;
cmd.index = 9;
cmd.arg = (uint32_t)rca << 16; // RCA放在高16位
cmd.resp_type = RESP_R2; // 长响应
if (sd_send_cmd(&cmd) != SD_OK) {
return SD_ERR_CMD_TIMEOUT;
}
memcpy(csd_buf, cmd.response, 16);
return SD_OK;
}
// 解析CSD,获取容量(以MB为单位)
uint32_t sd_parse_csd_capacity(uint8_t *csd) {
// 这里以CSD 2.0为例(SDHC/SDXC)
uint32_t c_size = ((csd[7] & 0x3F) << 16) | (csd[8] << 8) | csd[9];
// 容量 = (C_SIZE + 1) × 512K 字节
uint64_t capacity = (uint64_t)(c_size + 1) * 512 * 1024;
return (uint32_t)(capacity / (1024 * 1024)); // 转换为MB
}
我个人习惯在解析CSD之后,把关键参数打印出来做验证。比如容量对不对、速度等级是否匹配。有一次我发现一张标称32GB的卡,解析出来只有28GB,后来发现是卡被分区过,CSD里记录的是分区后的容量。
4.5 CMD7:选中卡
最后一步,也是很多人容易忽略的一步——选中卡。CMD7的作用是把卡从Standby状态切换到Transfer状态。只有处于Transfer状态的卡,才能进行读写操作。
CMD7的格式很简单:
// 发送CMD7,选中卡
// 参数:rca - 要选中的卡地址
// 如果rca=0,表示取消选中当前卡
uint8_t sd_cmd7_select_card(uint16_t rca) {
sd_cmd_t cmd;
cmd.index = 7;
cmd.arg = (uint32_t)rca << 16;
cmd.resp_type = (rca == 0) ? RESP_NONE : RESP_R1;
return sd_send_cmd(&cmd);
}
重要:选中卡之后,一定要检查响应状态。如果卡返回的R1响应中STATE位不是tran状态,说明卡还没有准备好。我曾经遇到过卡返回prg状态的情况,这时候强行读写数据,会导致总线挂死。
还有一个技巧:取消选中卡也很常用。比如你要切换操作另一张卡,先发CMD7(rca=0)取消当前卡,再发CMD7选中新卡。这个操作在SDIO多卡环境中特别重要。
4.6 完整的初始化序列
把上面四个步骤串起来,就是一个完整的SDIO初始化序列:
uint8_t sdio_init_card(sd_card_t *card) {
uint8_t cid[16], csd[16];
uint16_t rca;
// 步骤1: 获取CID
if (sd_cmd2_get_cid(cid) != SD_OK) {
return SD_ERR_INIT_FAIL;
}
memcpy(card->cid, cid, 16);
// 步骤2: 获取RCA
uint32_t cid_high = (cid[0] << 24) | (cid[1] << 16) | (cid[2] << 8) | cid[3];
rca = sd_cmd3_get_rca(cid_high);
if (rca == 0) {
return SD_ERR_INIT_FAIL;
}
card->rca = rca;
// 步骤3: 获取CSD
if (sd_cmd9_get_csd(rca, csd) != SD_OK) {
return SD_ERR_INIT_FAIL;
}
memcpy(card->csd, csd, 16);
card->capacity_mb = sd_parse_csd_capacity(csd);
// 步骤4: 选中卡
if (sd_cmd7_select_card(rca) != SD_OK) {
return SD_ERR_INIT_FAIL;
}
card->state = CARD_STATE_TRANSFER;
return SD_OK;
}
避坑指南:我曾经在STM32F4平台上移植这个流程时,发现CMD2总是超时。排查了半天,原来是SDIO时钟频率设置太高了。初始化阶段,时钟频率不要超过400KHz,等卡进入Transfer状态后再切换到高速模式。这个坑很多新手都会踩。
好了,到这里SDIO模式的初始化流程就走完了。你现在应该能理解,为什么我说这四个命令是「四步确认法」——每一步都在为后续的数据传输铺路。拿到CID确认身份,分配RCA简化通信,读取CSD了解能力,最后选中卡进入工作状态。环环相扣,少一步都不行。