3. eMMC协议基础:命令帧、响应与数据包
做eMMC嵌入式开发,打交道最多的就是这三样东西:命令、响应和数据。我们先把它们拆开讲清楚,后面排查时序问题时就心里有数了。
3.1 命令帧格式:从第一个bit讲起
主机发给eMMC的任何指令,都是一个45bit的帧。你想想看,这45bit其实挺紧凑的。具体结构如下表:
| 位域 | 宽度 | 说明 |
|---|---|---|
| 起始位 | 1 bit | 恒为0,用于帧同步 |
| 传输方向 | 1 bit | 主机→设备为1 |
| 命令索引 | 6 bits | 指定操作,如CMD8、CMD18 |
| 参数 | 32 bits | 命令的附加数据 |
| CRC7 | 7 bits | 校验前40bit数据 |
| 终止位 | 1 bit | 恒为1,标记帧结束 |
举个例子,CMD8的索引是8(二进制001000),参数里包含了VHS电压范围和Check Pattern。我调试时经常要手动拆解命令帧来对比逻辑分析仪抓到的波形。嗯,这里要注意,CRC7只覆盖命令索引和参数,不包含起始位和终止位。起始位和终止位是硬件自动插入的,你写软件时不用操心这个。
// 举个例子:手动构造CMD8命令帧
// 索引=8,参数=0x1AA(VHS=2.7-3.6V, pattern=0xAA)
// 计算前40bit [0:起始][1:方向][2:7索引][8:39参数]的CRC7
uint8_t cmd_index = 8;
uint32_t argument = 0x000001AA;
uint8_t crc7 = calc_crc7(cmd_index, argument);
// 输出帧:0 1 001000 00000000000000000000000110101010 <crc7> 1
调试经验:我个人习惯把命令帧的bit0(起始)和bit44(终止)单独拿出来看波形。如果逻辑分析仪看到这俩bit跳变不正常,基本是主机信号质量问题,跟设备没关系。
3.2 响应类型:R1、R2、R3、R4、R5
eMMC设备发回的响应,根据命令不同分为5种。别看种类多,其实就是响应内容和CRC策略的排列组合。我做HS400调试时经常用到的是R1和R2。
| 类型 | 长度 | 是否带CRC | 典型用途 | 我的看法 |
|---|---|---|---|---|
| R1 | 48bit | 是(7bit CRC) | 大多数CMD的通用响应 | 这个最常用,结构跟命令帧是一样的,只是方向相反 |
| R2 | 136bit | 是(7bit CRC) | 获取CID、CSD寄存器 | 长度最长,我早期被它坑过,因为中间没有暂停位 |
| R3 | 48bit | 否 | OCR寄存器(如CMD1) | 说白了,上电握手专用,硬件层面就不校验它 |
| R4 | 48bit | 否 | CMD5(IO操作) | V4规范后才引入,HS400模式下很少用 |
| R5 | 48bit | 否 | 中断 | 不用太纠结,我做项目几年都没用过它 |
为什么R2有136bit?因为它要一次性返回CID(128bit)外加CRC和终止位。我记得有一次调试DDR模式,设备就是不给R2,最后发现是板子上的电压纹波太大,设备没法稳定输出。
3.3 数据包结构与CRC
eMMC的数据传输是以扇区为单位的。数据传输模式下来回的每个数据块,结构是这样的:
- 主机发送单块或多块读/写命令(如CMD18、CMD25)
- 设备确认命令后,数据线开始传数据块
- 每个数据块512字节(默认),尾部跟2字节CRC16
- 多块传输时,块与块之间没有间隔,直到主机发停止命令
数据块内部结构:
| 字段 | 长度 | 说明 |
|---|---|---|
| 起始位 | 1 bit | 恒为0 |
| 数据方向 | 1 bit | 设备→主机为0,主机→设备为1 |
| 数据体 | 512 bytes | 用户数据 |
| CRC16 | 16 bits | 多项式0x1021 |
| 终止位 | 1 bit | 恒为1 |
CRC16用的是CCITT标准多项式。别问我为什么不用CRC32,这就是规范定的,咱照着做就行。单块读的操作步骤如下:
// 单块读操作数据包流程:
// 1. 主机发送CMD17 (读单块) + 块地址
// 2. 设备返回R1响应(状态+CRC7)
// 3. 主机收到R1后,开启数据线接收
// 4. 设备发起数据包:[0][0][512字节数据][CRC16][1]
// 5. 主机计算CRC16,与收到的CRC16对比
核心要点:命令和R1/R4/R5响应用CRC7校验,R2也用CRC7但数据更长,R3/R4/R5的OCR部分不校验。只有数据块才用CRC16。CRC7算法可以复用SD/MMC通用的查找表方式,我一般直接把预计算表写在头文件里。
3.4 CRC校验的实用技巧
CRC在实际调试中非常有用。为什么?逻辑分析仪抓到错误数据时,通过CRC校验结果可以快速定位问题是出在主机发送端还是设备接收端。
CRC7的计算:
- 生成多项式:x⁷ + x³ + 1(即0x89左移一位,消除最高位)
- 输入数据按MSB first方式移位
- 初始值:全0
- 输出不取反,不需要xor out
下面是我常用的CRC7查找表实现:
static const uint8_t crc7_table[256] = {
0x00, 0x09, 0x12, 0x1B, 0x24, 0x2D, 0x36, 0x3F,
// ... 一共256个预计算值
};
uint8_t calc_crc7(const uint8_t *data, size_t len) {
uint8_t crc = 0;
for (size_t i = 0; i < len; i++) {
crc = crc7_table[crc ^ data[i]];
}
return crc;
}
曾经踩过的坑:我曾经因为忘记把CRC7的初始值设为0,结果CMD8的响应死活不对。后来查了半天的spec才发现:SD/eMMC的CRC7初始值是固定全0,跟一些其他协议不一样。建议你们把这个初始值写死在代码里,不要留参数接口,避免后面接手的人踩坑。
CRC16相比CRC7要正规得多:
- 生成多项式:x¹⁶ + x¹² + x⁵ + 1(即0x1021)
- 初始值:0x0000
- 不取反,不进位
- 输入输出都是MSB first
这里有个小技巧:无论单块还是多块传输,CRC16都是在每个数据块完成后计算并发送。多块模式中,每个块的数据线和CRC线之间是严格连续的,没有空闲周期。我见过有些控制器在CRC计算上偷懒,导致HS400高速模式下CRC校验通过率下降,最后只能降频保稳定。
调试建议:逻辑分析仪抓HS400数据时,我习惯同时看命令线(CMD)和数据线的CRC状态。CMD线出现CRC错误往往是主机信号反射问题,数据线CRC错误多半是DQS时序偏移或Vref调节不到位。别乱改驱动代码,优先排查硬件信号完整性。