刚开始接触eMMC驱动的时候,我一直觉得CMD6就是一条“切换命令”:发送出去,设备换个模式,完事。直到后来调HS400切换,板子要么死在半路、要么时钟上去后疯狂报CRC错误,我才意识到,CMD6根本没有表面上那么简单。它更像eMMC内部的一把“总钥匙”,所有关键的工作模式、分区访问、总线宽度、卡内寄存器的修改,都是通过它完成的。搞懂CMD6的工作机制,不只是为了跑通初始化流程,更是为了在遇到切时序失败、切分区后读不到数据这类问题时,不至于瞎猜。
这篇文章就从我实际调试的经验出发,把CMD6的协议细节、常用场景、驱动封装、常见坑一次讲透。适合刚接触eMMC的嵌入式开发,也适合已经在调HS200/HS400但频繁踩坑的同行。
1. 看懂CMD6:eMMC的“配置空间写指令”
1.1 CMD6不只是“切换”那么简单
eMMC内部除了用户可见的数据块地址空间,还有一块额外的配置/状态区域,规范里叫EXT_CSD(Extended CSD Register)。这块区域承载了设备的工作模式、时序能力、分区配置、健康状态、掉电通知等大量信息。问题是,普通的数据读命令CMD17/CMD18访问的是块数据地址,没法直接操作EXT_CSD;普通写命令也受分区、写保护、ECC逻辑的影响。于是标准专门定义了CMD6,也就是SWITCH命令,用来直接读写EXT_CSD的指定字节。
你可以把它类比成PCI设备里的配置空间写操作。PCI设备里有独立的配置寄存器,要改中断、改BAR地址,走的是配置读写命令;eMMC内部要改时序、改分区访问,走的都是CMD6。它和数据通路完全剥离开,设计上就保证了“配置修改”这件事不被数据流干扰,也不会被NAND的写缓存机制搞乱。
CMD6的一个关键设计是:用“偏移地址+写入值”的方式操作。它不是把整个EXT_CSD当作内存块来改,而是先指定要改哪个字节(Index),再指定写入什么值(Value)。这样就让驱动层面对设备内部结构的依赖降到最低,也方便后续协议版本扩展新能力,而不需要改变命令本身。
1.2 参数拆解:32位参数里藏的信息
CMD6的Argument是32位,实际有效字段分布在特定bit位上。我第一次对着规范数这些位的时候,眼睛都快花了,后来自己整理成一张图就好记了。各位含义如下:
- bit[31:28]:保留位,必须填0。
- bit[27]:MODE,0表示读取EXT_CSD的一个字节,1表示写入EXT_CSD的一个字节。
- bit[26:24]:CMD_SET,通常填0(Normal Command Set),用来区分不同命令集。
- bit[23:16]:Index,要访问的EXT_CSD字节偏移。
- bit[15:8]:Value,要写入的值。
- bit[7:0]:保留位,必须填0。
构造参数的C代码大概长这样:
static u32 mmc_cmd6_arg(int mode, int cmd_set, int index, int value) { return ((mode & 0x1) << 27) | ((cmd_set & 0x7) << 24) | ((index & 0xff) << 16) | ((value & 0xff) << 8); }很多人第一次用CMD6时会犯一个低级错误:把Value和Index的位搞反,写成index << 8 | value << 16,结果就是卡收到一个完全莫名其妙的指令,轻则参数错误,重则把某些不想动的配置改掉。我的习惯是先按位构造,再打印出十六进制参数对照规范核对一遍。比如要写Index=185、Value=1的HS时序切换命令,构造出来应该是0x03B90100这样的格式,看到参数再心里默算一遍Index和Value,比直接写代码靠谱得多。
1.3 两种模式的区别和选择
MODE=0时,CMD6不修改任何EXT_CSD,只把指定Index的那个字节返回在响应里。这个模式非常适合初始化阶段的“能力探测”,比如读EXT_CSD[185]看卡是否支持HS400,读EXT_CSD[183]看当前总线宽度配置。MODE=1时,命令真正生效,设备会在DATO线上拉低表示busy,一直持续到内部操作完成。这个busy等待如果没处理好,就是后面各种诡异问题的根源。
选择上没什么悬念:想确认状态就读,想改状态就写。但有一个经验必须强调——凡是涉及“写入”的场景,尽量先读一次原始值,做好掩码再写。比如后面要说的分区切换,低3位是分区选择,但高5位还有BOOT_PARTITION_ENABLE等配置,直接写成目标分区的值,往往会清掉原本正确的启动配置。读-改-写,这是CMD6操作的最基本原则。
2. 用CMD6切换时序:HS200/HS400/DDR52的正确姿势
2.1 时序切换的底层逻辑
eMMC里有一组“时序模式”:Legacy(默认速度)、High Speed(HS)、HS200、DDR52、HS400,更新的协议还有HS400ES。这些模式不是随便切就能进的,每个模式对主机时钟频率、总线宽度、DDR/SDR模式、甚至采样窗口都有不同要求。而切换动作本身,就是向EXT_CSD[185](HS_TIMING域)写入对应的模式值。
这里要区分两个概念:EXT_CSD[185]读出来的值是“能力位图”,表示设备支持哪些模式;写入的值则是“当前模式选择”。我遇到过有人读出来是0x07,就以为写3没问题,但实际写入值和读出能力位图的编码并不完全一一对应。参考常见的定义:
- 0x00:Legacy / Default Speed
- 0x01:High Speed(SDR)
- 0x02:HS200
- 0x03:HS400
- 0x05:DDR52
DDR52写5,这个当初坑了我很久。一开始我按照能力位图的习惯,认为DDR52应该写4或者6,结果切过去以后卡直接不认数据。后来翻了规范才确认,DDR52在HS_TIMING写入值里就是5。这种“能力位图和写入值编码不同”的细节,靠猜是不行的,必须对着标准文档来。
2.2 手动切到HS400的完整演练
从复位状态开始,想一路切到HS400,完整流程大致是下面这样。我把每一步涉及的命令、参数、主机侧注意事项都列出来,方便直接参考。
首先是在400kHz时钟下完成基础初始化:CMD0复位、CMD1查询OCR、CMD2读CID、CMD3分配RCA、CMD9读CSD、CMD7选择设备。这些步骤和CMD6没有直接关系,但没完成它们之前,后续一切命令都是空谈。
接下来读EXT_CSD,确认卡的能力。用MODE=0,Index=185,把返回值解出来:
# 伪命令示例:读取 EXT_CSD[185] CMD6, arg = (0 << 27) | (0 << 24) | (185 << 16) | (0 << 8) 响应中返回的字节,解析bit2是否为1,判断HS400支持如果bit2没置位,后面就别试HS400了,就算写进去也会被拒。接着按顺序执行:
- 发送CMD6,MODE=1,Index=185,Value=1,切换到High Speed状态。卡返回R1B,等待busy结束后,把主机时钟从400kHz抬到52MHz。
- 发送CMD6,MODE=1,Index=183,Value=6。Index=183是总线宽度配置,值6代表8bit DDR模式。这一步很多人会忘,直接跳到HS400会出问题,因为HS400只支持8bit总线。
- 发送CMD6,MODE=1,Index=185,Value=5,把卡切到DDR52。命令发送完成后,主机必须立刻把时钟降到52MHz,并把自己配置成DDR模式。
- 发送CMD6,MODE=1,Index=185,Value=3,切到HS400。此时再把时钟从52MHz DDR提到200MHz DDR。
这里有一个关键经验:时钟的调整顺序要和卡内部状态变化保持同步。从低切到高时,先让卡进入新状态,主机再把时钟提上去;从高切到低时,最好先把主机时钟降下来,再发切换命令。如果顺序反了,比如还在200MHz HS200总线上,卡却已经被切成了DDR52,后续命令的时序就完全不匹配了,轻则命令超时,重则直接CRC风暴。
2.3 驱动里的封装:参考内核的mmc_switch实现
很多通用内核的mmc驱动已经把上述流程封装成了mmc_switch()函数。它做的事情说白了就三步:构造CMD6参数、发送命令、等待busy结束。但真正工程化的时候,有一个细节需要注意:等待busy不能只靠host控制器的DAT0检测,很多控制器对R1B命令的busy处理并不完善,尤其在高负载、DMA忙的情况下会提前返回。所以可靠的做法是,在CMD6发完后,再用CMD13轮询卡的状态,直到READY_FOR_DATA置位。
简化后的伪代码大概长这样:
static int mmc_switch(struct mmc_host *host, int index, int value) { struct mmc_cmd cmd = {0}; u32 status; int err; cmd.opcode = 6; cmd.arg = mmc_cmd6_arg(1, 0, index, value); cmd.resp_type = MMC_RESP_R1B; err = host->send_cmd(host, &cmd); if (err) return err; /* 等待卡真正结束busy */ do { err = mmc_send_status(host, &status); if (err) return err; } while (!(status & R1_READY_FOR_DATA)); return 0; }实际项目里,mmc_switch()通常还要带上timeout_ms参数,因为有些操作(比如BKOPS的切换、RPMB内部校验)耗时很长,固定短超时会把本来正常工作的设备误判成错误。我习惯根据操作类型区分超时:纯时序切换给几十毫秒足够,分区切换和掉电通知这类给几百毫秒,宁可慢一点也不能误杀。
3. 用CMD6切换分区:BOOT/RPMB/User Area
3.1 分区配置域:EXT_CSD[179]的低3位
eMMC的物理存储空间被分成多个区域:用户数据区(User Area)、Boot分区1和2、RPMB(Replay Protected Memory Block)、以及通用分区(GPP)。控制器访问哪个区域,不是靠物理地址切换,而是通过EXT_CSD[179](PARTITION_CONFIG)的低3位来配置。这就又是一个典型的CMD6应用场景。
访问分区的编码通常是这样:
- 0x0:User Area
- 0x1:Boot分区1
- 0x2:Boot分区2
- 0x3:RPMB
切分区的命令很简单:
static int mmc_switch_part(struct mmc *mmc, int access) { u32 arg; u8 old_config; /* 先读回当前配置,避免覆盖高5位 */ old_config = mmc_read_ext_csd(mmc, 179); arg = mmc_cmd6_arg(1, 0, 179, (old_config & 0xe0) | (access & 0x07)); return mmc_send_cmd(mmc, 6, arg, MMC_RESP_R1B); }这个读-改-写的过程特别重要。EXT_CSD[179]高5位包含BOOT_ACK、BOOT_PARTITION_ENABLE等信息,这些是启动时固件配置好的,如果直接写一个低3位目标值,等于把整字节覆盖,很可能把boot配置清掉。我见过一个案例:有人为了切到RPMB读数据,直接写0x03,读完倒是一切正常,但下一次开机后Boot配置丢了,系统起不来。排查了半天才发现是写分区时把高位置零了。
3.2 切到BOOT0之后:地址空间已经变了
发送CMD6切到Boot分区后,设备对外的行为会发生一个很多人容易忽略的变化:当前可见的存储空间不再是用户区,而是Boot分区,原本用户区的逻辑地址全部不可见了。这时候你发CMD17读地址0,读出来的内容实际上是Boot分区的地址0,不是一个“地址不变、内容变”的简单替换。
这带来的坑非常直接:有些代码在切到Boot分区后忘了“重新认识新空间”,还用之前用户区的文件系统偏移去读,结果拿到一堆看似正常但完全无意义的数据。在UBoot或裸机里验证Boot分区是否能正常启动时,我的习惯是:切到Boot分区后,只做基于块地址的小范围读写验证,不要加载任何用户区的映射关系。等切回User Area后,再重新初始化文件系统相关内容。
3.3 RPMB的特殊性:写保护与指令帧
RPMB分区支持防重放保护,访问它不能只用简单的CMD17/CMD18。你需要先通过CMD6把访问目标切成RPMB,然后像读写普通块一样发送RPMB帧,帧里要带MAC、写计数器、随机数等,这些不是CMD6本身能覆盖的。CMD6在这里只负责“让卡知道接下来要服务RPMB”的切换动作。
实际调试RPMB时,还有两个细节值得注意。第一,RPMB有写计数器,每次写操作计数器会增加,主机必须维护好这个值,否则后续帧的MAC校验永远失败。第二,有的厂商实现了RPMB的写保护,你在CMD6切过去之后,写操作会被卡直接拒绝,这时不要急着怀疑CMD6,先读EXT_CSD里分区保护相关状态确认一下。另外登录操作需要先通过CMD6设置相应的访问权限?RPMB访问在部分设备上还需要在CMD6之外维护读取和校验,但这里不展开说,避免跑偏。
3.4 软复位后分区会被重置吗
一个经验:很多eMMC设备在软复位(CMD0或硬件复位引脚触发)后,分区访问会被重置为User Area。这意味着如果你的启动流程是先切到Boot分区读取数据,再切回用户区继续启动,就必须在重启流程里显式地重新执行切换,不能依赖上一次配置。同理,有些平台在深度睡眠唤醒后,也会因为复位流程丢分区配置,所以分区相关的CMD6切换代码要放在每次重新初始化路径里,而不是只在开机时执行一次。
4. 工程实操中的坑:Busy等待、R1B响应与时序细节
4.1 为什么“命令发出去了”不等于“切换完成了”
CMD6在MODE=1时,设备的响应类型是R1B。R1B和普通R1最大的区别就是多了一个Busy信号,设备在内部操作未完成前,会把DAT0线拉低,告诉主机“先别着急”。很多SoC的eMMC控制器其实没法直接通过命令响应感知这个Busy信号,它只是完成了“发出去了”这一步。如果驱动在CMD6返回后立刻发下一个命令,比如马上写数据或者读状态,这时卡还在忙,命令可能直接超时,也可能被卡忽略。
我处理这个问题时,通常采取“双重保险”策略:在控制器层面,把CMD6配置成R1B且启用控制器自带的Busy检测;在驱动层面,发送后用CMD13轮询状态,直到READY_FOR_DATA位为1。控制器检测和软件轮询都做,虽然慢了一点,但稳定性很高。你在调试时如果发现“切完HS400后CMD13读回乱码”,大概率就是Busy没等够。
4.2 为什么HS400要“切两次”:DDR52是必经之路
从直觉上看,从HS200切到HS400只差一个值,直接写INDEX=185、VALUE=3不就行了?实际不行。协议里明确规定,HS400必须从DDR52过渡,不能从HS200直接跳过去。
原因在于HS400不仅仅提升了时钟频率,还改变了DQS信号的时序以及卡端输出驱动器的配置方式。DDR52阶段相当于一个“中间姿态”,让卡把DDR模式下的信号路径建立起来,然后才允许进入HS400。如果你省掉DDR52那一步,卡不会明确报错,但进入HS400后你大概率会遇到采样点完全对不上、数据眼图乱掉的问题。
正确路径是HS200 -> DDR52 -> HS400。在DDR52阶段,时钟保持52MHz,但总线已经工作在DDR模式;随后切到HS400,再把时钟升到200MHz DDR。不少平台在HS400模式还需要额外的硬件校准,这个环节建议等卡从Busy中恢复后再做,不要赶时间。
4.3 常见问题速查表
随手整理一张表,都是实战里经常遇到的:
| 错误现象 | 可能原因 | 解决思路 |
|---|---|---|
| 切HS400后卡死,CMD13不返回 | Busy没等够,或DQS采样错误 | 用CMD13轮询替代单次等待,做信号tuning |
| 切完HS400后CRC错误频繁 | 时钟提得太快,或总线宽度不是8bit DDR | 确认EXT_CSD[183]已写0x6,再检查host的DDR配置 |
| CMD6返回错误位 | 参数构造错误,Index/Value位搞反 | 打印arg十六进制值,对照规范逐位核对 |
| 切到Boot分区后读数据异常 | 分区访问值写错,或高5位被覆盖 | 读-改-写方式操作EXT_CSD[179] |
| 从HS200切到HS400直接写3成功,但性能不对 | 跳过DDR52过渡,实际没进入真HS400 | 完整执行HS200 -> DDR52 -> HS400流程 |
| CMD6响应正常,但后续所有读写都失败 | 主机时钟没有同步切换到目标模式 | 检查host当前clk、总线宽度、DDR/SDR模式是否和卡一致 |
这张表里的问题,我基本都实际踩过。其中最隐蔽的其实是“没进入真HS400但命令没报错”的情况,因为驱动看起来一切正常,但性能只有预期的一半,而且偶发读写超时。排查到最后才发现是切换序列顺序不对,这种问题靠读log很难发现,只能按时序流程一步步比对。
5. 调试技巧与底层命令验证
5.1 用逻辑分析仪抓真实波形
排查CMD6问题,最快的办法是直接从物理层看。把逻辑分析仪的探头接到eMMC的CLK、CMD、DAT0三根线上,在切换时序时抓一段波形,然后解析CMD线的命令帧。
CMD6的帧相对好认:命令号是6,后面跟32位参数。你把参数解出来,和驱动里期望的Index、Value一对,大部分问题立刻水落石出。我之前遇到一个“偶发切分区失败”的问题,代码翻来覆去查了好几遍都找不到原因,抓波形发现,偶尔CMD6的参数里Index被写成了178而不是179——原来是上层在传参时结构体偏移算错了。这种问题如果不看波形,真的会怀疑人生。
抓波形时要注意采样率,eMMC跑HS400时CLK是200MHz,一般分析仪至少要500MHz采样率才够看,否则波形失真会影响判断。
5.2 切换后一定要读回验证
除了抓波形,软件上还有一个笨但有效的方法:CMD6发出后,再用MODE=0的方式把目标EXT_CSD字节读回来,确认写入生效。比如切完时序后读EXT_CSD[185],切完分区后读EXT_CSD[179]。
不过这里要特别提醒:不是所有EXT_CSD字节在当前模式下读回都等于你写的值。HS_TIMING域在某些卡实现里,读回的是“能力位图”而不是“当前工作模式”,所以不要简单断言“读出来等于3就是切到HS400了”。我一般结合后续实际读写性能和CMD13状态综合判断,而不是单靠读回一致。
5.3 给驱动加日志,比猜重要得多
调试初期,建议在调用CMD6的统一入口处加一条带参数的日志,把mode、index、value、构造出的完整arg打出来。这样每次切换动作都有据可查。日志格式像这样:
mmc_switch: mode=1 idx=185 val=3 arg=0x03B90103看到这一行,你就能和标准里的值一一核对。等系统稳定了再把这层日志关掉,避免影响性能。
调时序的时候,我还习惯“步步为营”:先在HS模式下把所有读写流程跑稳,再试HS200,最后才碰HS400。一次直接从Legacy跳到HS400,出了问题你根本分不清是CMD6参数错、时钟配置错、PHY调节错还是GPIO复用错。每进一个模式,都跑一遍完整的读写校验,确认稳定了再往下一档走。这个节奏看起来慢,实际上是最快的。
最后分享一点个人体会
CMD6这条命令本身并不难,难的是它牵动的“状态一致性”。每做一次切换,我会强迫自己检查四个点:命令参数对不对、响应类型是不是R1B、有没有等Busy、主机侧的时钟和总线宽度有没有跟上。只要这四个点对齐,eMMC的初始化流程就已经成功了一大半。后来我调其他存储协议时,遇到类似的模式切换问题,也习惯用这套“参数-响应-等待-主机侧配合”的排查思路,屡试不爽。希望这篇关于CMD6的整理,能帮你在调试中少走几步弯路。