1. 为什么工程师手里该常备mmc_utils
先讲个真实场景。有次我拿到一批号称“全新原装”的eMMC芯片,上机后系统能启动,但跑存储压力测试时总有几片掉盘,要么就是写入速度忽快忽慢。用常规手段根本查不出问题,因为从Linux内核上层看,这些设备都报“正常”。后来我直接用mmc_utils把每片芯片的extcsd读出来,一眼就看到其中一片的life time estimation字段已经显示接近磨损极限,还有一片的revocation flag状态异常。这玩意儿要是不读底层数据,光靠跑应用层测试,排查周期至少翻三倍。
这就是我想写这篇东西的原因。mmc_utils是Linux内核社区维护的一套eMMC用户态工具,专门用来直接访问MMC设备的管理功能。它做的事情,说白了两类:一类是“读”,把eMMC内部那些寄存器、分区信息、健康状态全部拉出来给你看;另一类是“写”,像分区切换、安全擦除、RPMB密钥写入这些操作,全靠它执行。在Android设备上,特别是在做系统移植、存储调试、产线老化验证、售后返修分析的工程师手里,这是绕不开的利器。
标题里写的“20+个实用命令”,不是凑数。我按实际使用频率把这些命令分成四个维度:基础信息读取、extcsd深度解析、性能与健康检验、RPMB安全操作。从最初拿到一颗芯片“什么都不知道”,到最后能亲手完成RPMB安全分区写入,这条路走通了,eMMC在你的项目里就不再是黑盒。
这篇文章适合三类人:正在做Android平台BSP或者存储驱动的工程师、负责产线测试与售后分析的质量工程师、以及那些对eMMC底层机制感兴趣的进阶玩家。看完之后,你能把mmc_utils的命令串成一套完整的排查方法,而不是零散地抄几个命令用。
2. 环境准备:把mmc_utils塞进Android设备
2.1 源码获取与交叉编译的两种姿势
mmc_utils的源码托管在Linux内核官方仓库的tools/mmc目录下,它不依赖复杂的库,编译极其轻量。你在内核源码树里直接make就能出来,也可以单独拉取镜像仓库。
获取源码之后,交叉编译是最常见的需求。在Android项目里,通常有两种做法。
第一种,用Android NDK直接编译静态可执行文件。这样的好处是产出的二进制不依赖目标设备的动态链接库,push进去就能跑,特别适合在eng或者userdebug版本的设备上做临时调试。NDK工具链准备好之后,写一个简单的编译脚本:
#!/bin/bash NDK_PATH=/path/to/your/ndk API_LEVEL=android-28 ARCH=aarch64-linux-android export CC=$NDK_PATH/toolchains/llvm/prebuilt/linux-x86_64/bin/${ARCH}${API_LEVEL}-clang make clean make CC=$CC注意,如果目标Android设备是32位ARM,把ARCH换成armv7a-linux-androideabi即可。静态链接做完之后,文件体积会比动态版大不少,但胜在省心。
第二种,如果设备上已经有完整的root权限和可写的system分区,你想把mmc_utils当作系统工具来用,那就直接放进Android源码树里,用Android.bp或者Android.mk写进编译系统。举个例子,在device目录下新建一个mmc_utils模块目录,Android.mk里这样写:
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := mmc_utils LOCAL_MODULE_CLASS := EXECUTABLES LOCAL_SRC_FILES := mmc.c mmc_cmds.c mmc_cmds.h mmc.h LOCAL_CFLAGS := -Wno-unused-parameter LOCAL_MODULE_PATH := $(TARGET_OUT_VENDOR_EXECUTABLES) include $(BUILD_EXECUTABLE)编译完会直接装到vendor/bin目录下,作为系统的一部分长期存在。我自己的习惯是:工程调试阶段用NDK静态编译版本,稳定量产之后再决定要不要集成进系统。
2.2 权限模型与设备节点:命令能不能跑通的第一步
mmc_utils能工作,依赖两样东西:合适的设备节点和足够高的权限。
Android设备上,eMMC对应的块设备节点通常是/dev/block/mmcblk0,有些平台可能是mmcblk1,具体要看硬件拓扑。建议先用下面的命令确认:
ls -l /dev/block/mmcblk* for i in /sys/class/mmc_host/mmc*/mmc*; do echo $i; cat $i/name; done第二行命令会枚举出系统识别到的所有MMC控制器和设备名称,比如某台设备的输出可能是:
/sys/class/mmc_host/mmc0/mmc0:0001 AGND3R这个“AGND3R”就是eMMC内部存储的设备名称,配合/proc/partitions可以看到完整的分区布局。
权限方面,很多mmc_utils命令需要打开设备节点并执行IOCTL,普通shell用户访问/dev/block/mmcblk0会报Permission denied。这种情况下,要么确保设备已经root,要么直接切到adb root模式再执行。实际操作中,我的建议是始终先跑一条最简单的命令验证权限链:
adb root adb shell mmc extcsd read /dev/block/mmcblk0如果这条能正常输出大段十六进制数据,说明设备和权限都没问题,后面的操作可以放心折腾。如果报错,九成是设备节点路径不对,还有一成是SELinux策略把IOCTL调用给拦了,需要临时把SELinux切到Permissive模式验证一下:
adb shell setenforce 0总结一下环境准备的核心结论:交叉编译选静态链接最省心、确认设备节点用sysfs枚举最可靠、权限问题先root再放SELinux。这三步走完,命令本身才不会成为后续的拦路虎。
3. 基础信息读取:认识你手里的eMMC芯片
3.1 CID、CSD与固件版本:给芯片“验明正身”
我刚接触eMMC调试时,最常被问到的一个问题是:“我怎么知道这颗eMMC是哪个厂家的?容量对不对?走的是什么总线模式?”这些问题,用mmc_utils的几兄弟命令就能在几十毫秒内给出答案。
读CID,获取芯片唯一标识:
mmc cid read /dev/block/mmcblk0输出会包含三大部分信息:制造商ID(Manufacturer ID)、产品名(Product Name)、序列号(Serial Number)。制造商ID是JEDEC标准指定的,比如0x11就是三星,0x15就是东芝铠侠,0x45就是闪迪。看完这个,A厂B厂混用导致的问题一眼就能发现。我在产线抽检时就遇到过,BOM表里报的是三星,实际贴片的芯片读出来是另一家,要不是用cid read把序列号拉出来核对,整批货就漏过去了。
读CSD,获取容量和速度等级:
mmc csd read /dev/block/mmcblk0CSD里有几个字段特别重要:READ_BL_LEN决定了单次可读块的字节数,C_SIZE和C_SIZE_MULT组合计算出来的就是总容量,TRAN_SPEED表示最大传输速率。CSD里的容量计算方式和extcsd不太一样,它是老一代的“固定几何参数”,对于现代大容量eMMC来说,extcsd里的SEC_COUNT才是更精确的容量来源。所以CSD适合快速验证,extcsd适合精确分析。
查看制造商与产品版本信息:
mmc fwrev read /dev/block/mmcblk0 mmc hwrev read /dev/block/mmcblk0一条命令显示固件版本(FW Revision),一条显示硬件版本(HW Revision)。这两个值在排查“同批次芯片表现不一致”时非常好用。比如某段时间产线反馈写入性能下降,读一下hwrev发现这批芯片的硬件版本和之前的不同,就知道要找供应商核对版本切换记录,问题定位效率提升好几个量级。
3.2 status get与bootpart get:快速判断设备当前状态
有时候你需要的不是一长串寄存器描述,而是“这设备现在到底处于什么状态”。mmc_utils提供了一组快速状态查询命令。
mmc status get /dev/block/mmcblk0这条命令读取设备当前的32位状态寄存器。最高关注的就是bit 12-13的CURRENT_STATE字段,它直接告诉你eMMC正处在kernel自举阶段、数据传输状态、还是休眠状态。有一次我调一个低功耗唤醒异常的问题,怀疑eMMC没进入sleep状态导致功耗居高不下,就是用这条命令发现设备一直停留在transfer状态,后来定位到是驱动里缺少sleep命令的下发逻辑,跟mmc_utils配合把问题坐实了。
查看启动分区配置:
mmc bootpart get /dev/block/mmcblk0输出会展示当前boot partition的使能状态、访问权限配置、以及boot ACK是否开启。这个命令在做Android启动流程调试时几乎是必备操作——如果你发现设备从bootloader阶段就无法启动,第一件事就是确认boot partition的配置是否被意外篡改过。
我还想补充一个技巧:上面这些命令里,你可以把/dev/block/mmcblk0替换成实际的分区设备节点,比如/dev/block/mmcblk0boot0、/dev/block/mmcblk0boot1,它们分别对应eMMC的两个硬件启动分区。后续读RPMB时,设备节点就变成/dev/block/mmcblk0rpmb,不要搞混了。
4. 深入extcsd:一张表读懂eMMC的“体检报告”
4.1 extcsd read命令与关键字段索引表
extcsd(Extended CSD Register)是eMMC内部一片512字节的扩展寄存器区域,里面记录了大量设备出厂参数、运行状态、健康信息、以及可配置项。它是eMMC调试最核心的信息来源。
mmc extcsd read /dev/block/mmcblk0这条命令输出的十六进制数据按字节偏移排列。新手看到一长串hex就头大,但只要掌握几个关键偏移量,就能像查字典一样读取你想要的信息。下面是我多年调试中频繁查阅的字段清单,整理出来供你保存:
| 字节偏移 | 字段名称 | 关键信息 |
|---|---|---|
| 0x00 | S_CMD_SET | 命令集版本,通常为0(标准MMC命令集) |
| 0x08 | SEC_COUNT | 用户数据区总扇区数(512B/扇区),决定实际容量 |
| 0x0D | TRIM_MULT | TRIM操作的粒度倍数 |
| 0x10 | MAX_ENH_SIZE_MULT | 增强分区最大尺寸倍数 |
| 0x1B | PARTITION_CONFIG | 当前分区配置,含BOOT_PARTITION_ENABLE、PARTITION_ACCESS等位 |
| 0x1C | BOOT_BUS_CONDITIONS | Boot总线宽度与时序配置 |
| 0x1D | BOOT_CONFIG_PROT | Boot配置写保护标志 |
| 0x22 | ERASE_GROUP_DEF | 擦除组定义使能标志 |
| 0x24 | ERASED_MEM_CONT | 擦除后内存默认值 |
| 0x2A | BOOT_WP_STATUS | Boot分区写保护状态 |
| 0x2B | BOOT_WP_BOOT_REG | Boot分区写保护配置寄存器 |
| 0x30 | USER_WP | 用户区写保护配置 |
| 0x31 | FW_CONFIG | 固件配置 |
| 0x32 | RPMB_SIZE_MULT | RPMB分区大小倍数 |
| 0x33 | WR_REL_SET | 写可靠性设置 |
| 0x34 | WR_REL_PARAM | 写可靠性参数 |
| 0x35 | SANITIZE_START | 启动Sanitize操作 |
| 0x3C | BKOPS_EN | 后台操作(Background Operations)使能标志 |
| 0x3D | BKOPS_START | 手动触发BKOPS |
| 0x3E | SANITIZE_STATE | Sanitize操作状态 |
| 0x40 | EXT_CSD_TRIM_UNIT | TRIM单元大小 |
| 0x49 | MAX_PRE_LOADING_DATA_SIZE | 预加载数据最大尺寸 |
| 0x50 | CMDQ_MODE_EN | Command Queue模式使能 |
| 0x52 | CMDQ_DEPTH | Command Queue深度 |
| 0x56 | FFU_FEATURES | 固件更新特性支持 |
| 0x5A | PRODUCTION_STATE_AWARE | 生产状态感知 |
| 0x5C | DEVICE_LIFE_TIME_EST_TYP_A | 设备生命周期估算(典型使用场景A) |
| 0x5D | DEVICE_LIFE_TIME_EST_TYP_B | 设备生命周期估算(典型使用场景B) |
| 0x60 | eMMC_POWER_OPTIMIZATION | 功耗优化配置 |
| 0x61 | PACKED_FAILURE_INDEX | 打包命令失败索引 |
| 0x62 | PACKED_CMD_STATUS | 打包命令状态 |
| 0x63 | PRE_EOL_INFO | 预寿命终止信息 |
| 0x64 | SECURE_REMOVAL_TYPE | 安全移除类型 |
| 0x66 | SECURE_TRIM_MULT | 安全TRIM倍数 |
| 0x67 | SECURE_ERASE_MULT | 安全擦除倍数 |
| 0x68 | SECURE_FEATURE_SUPPORT | 安全特性支持 |
| 0x69 | TRIM_MULT | TRIM倍数(用户分区) |
| 0x6A | MIN_PREFETCH | 最小预取数据量 |
| 0x9A | BKOPS_STATUS | 后台操作状态 |
| 0xA5 | CONTROL | 控制寄存器 |
| 0xA6 | POWER_OFF_LONG_TIME | 长时间断电时间 |
| 0xA7 | POWER_OFF_SHORT_TIME | 短时间断电时间 |
这张表不需要死记,重点是学会“什么场景查什么字段”。比如怀疑设备快报废了,直接看0x63、0x5C、0x5D;怀疑启动分区配置被改坏了,看0x1B;想知道支不支持Command Queue,看0x50。
4.2 通过extcsd判断器件寿命与健康度
eMMC内部其实有一套健康监测机制,不像有些人以为的“存储芯片哪有什么健康度可言”。上面表格里的PRE_EOL_INFO和DEVICE_LIFE_TIME_EST就是关键。
PRE_EOL_INFO位于偏移0x63,它指示设备距离寿命终止还有多久,取值含义如下:
- 0x00:正常,设备寿命未出现明显消耗
- 0x01:已消耗80%的保留块(Reserved Blocks),进入警告区
- 0x02:已消耗90%的保留块,属于严重警告
- 0x03:已消耗100%的保留块,设备即将无法保证可靠写入
DEVICE_LIFE_TIME_EST_TYP_A和TYP_B则用0到0x0A的数值估算设备生命周期消耗程度。这两个值分别对应两种不同的“典型使用温度场景”,A类通常是高温场景估算,B类代表常温场景估算。数值10意味着设备寿命已经估算耗尽。
我在老化和返修分析时,会把这几个字段输出打印出来,再配合mmc status get的状态值,可以快速判断“返修设备的eMMC是不是罪魁祸首”。有次售后反馈设备频繁死机,拿回来读extcsd,PRE_EOL_INFO已经到0x02,寿命都快干涸了,死在写入时段的概率当然高。这类判断,输出一行命令就能让硬件部门无话可说。
4.3 SEC_COUNT容量校验与分区布局的坑
容量校验是产线测试里绕不开的一环。extcsd的0x08偏移处,SEC_COUNT字段描述了设备用户数据区的总扇区数。但是注意,这个字段是4字节小端序,逐字节读出来之后要拼成完整的32位值。
比如十六进制输出里,0x08到0x0B的四个字节为:
00 80 3A 00拼出来的值就是0x003A8000,换算成十进制是3833856,乘以每扇区512字节,得到总容量约1.96GB。再对比采购规格书里的标称容量,就能判断容量是否缩水。
不过这里藏着一个非常深的坑:很多Android设备会用extcsd的USER_WP或者BOOT_WP字段做硬件写保护,也有平台会修改ERASE_GROUP_DEF。如果你在做容量相关的自动化脚本,一定要把0x1B这个PARTITION_CONFIG字段读出来,确认当前是不是处于增强用户数据区(Enhanced User Data Area)模式,因为增强区的容量计算方式和普通用户区不同。我碰到过一次,产线脚本把增强区和普通区的容量加在一起跟标称值比,结果差了100多MB怎么都对不上,查了整整半天,最后发现是分区配置把一部分容量划给了增强区。这个坑,写自动化测试的同学务必提前预判。
5. 性能与健康快速检验:不用跑benchmark也能发现问题
5.1 bsr读改写:临时改总线速度的调试技巧
eMMC的工作速度由总线模式、时钟频率和数据线宽度共同决定。测试过程中临时调总线速度,mmc_utils里有BCK(Back-End Check)相关命令,但我更常用的是直接读写Bus Speed Mode寄存器。
查看当前总线速度模式:
mmc bsr read /dev/block/mmcblk0这条命令会返回设备当前工作的总线速度模式,一般包括:
- 0:默认速度(Default Speed,DS),时钟不超过26MHz
- 1:高速模式(High Speed,HS),时钟不超过52MHz
- 2:HS200,时钟不超过200MHz
- 3:HS400,时钟不超过400MHz
改写总线速度模式:
mmc bsr write /dev/block/mmcblk0 2这里有个务必记住的原则:bsr write切换模式之后,通常需要重新初始化设备才能完全生效,切到更高的HS400模式时尤其如此,不是随便改个数字就能跑起来。所以这个命令更适合在uboot或者kernel启动阶段配合驱动初始化流程做验证,而不是在系统运行中“热切换”。我有一次在Android运行状态下把模式从HS200切成HS400,结果设备直接不响应了,重启才恢复正常。从此之后,我调整总线速度都是先确认设备支持,再配合软复位流程来操作。
5.2 bist命令:从主机侧对数据总线做自检
BIST(Built-In Self-Test)是eMMC 5.0开始加入的功能,它允许主机引导设备内部的数据总线进行自检。mmc_utils封装了这个能力:
mmc bist read /dev/block/mmcblk0 mmc bist write /dev/block/mmcblk0 1在排查硬件信号完整性问题时,这个命令比拿示波器量信号快得多。它能验证host controller到eMMC之间的数据线有没有虚焊、短路、或者时序问题。产线第一轮贴片回来,先用bist跑一遍,可以把大部分焊接不良的板子在功能测试之前就筛掉。
bist命令的使用需要硬件支持,不是每颗eMMC都有。跑之前先查extcsd里有没有对应的BIST模式支持位。如果设备不支持,命令会直接报错退出,这本身也是一个有效的筛选信息。
5.3 erase、trim、secure erase与sanitize的正确使用场景
这几个命令是eMMC运维里最容易被滥用的一组。先看它们各自的作用:
mmc erase 0x0 0x1000 /dev/block/mmcblk0这是最基本的擦除命令,参数分别是起始地址和结束地址(按扇区计)。它会将指定的用户数据区域擦除为全1或全0,具体取决于extcsd里的ERASED_MEM_CONT字段。
mmc trim 0x0 0x1000 /dev/block/mmcblk0trim是逻辑层面的“标记失效”,把那些操作系统认为已经删除但物理上还被占用的块标记为可回收,后续垃圾回收机制会真正释放它们。这跟SSD上的trim概念一样,不会立即清空数据,但能改善后续写入性能。
mmc secure erase 0x0 0x1000 /dev/block/mmcblk0 mmc secure trim 0x0 0x1000 /dev/block/mmcblk0secure版本会执行物理销毁或加扰处理,这样即使拆下芯片做数据恢复也恢复不出有效信息。适合处理含敏感信息的数据区。
mmc sanitize /dev/block/mmcblk0sanitize是对整个设备做一次彻底擦除,会把所有存储单元重置到出厂默认状态。它的特点是耗时很长,一次操作可能几十秒到几分钟不等,期间会阻塞该设备的IO。
这组命令里,我的经验总结是:系统升级、数据清除场景优先用trim;返修报废设备彻底清数据,用secure erase或者sanitize;日常调试验证性能时,erase就够了,别动不动就secure erase,因为安全擦除的耗时是普通擦除的数倍甚至数十倍,产线节奏会很难扛。
6. RPMB安全分区:从读计数器到写入密钥
6.1 RPMB是什么?为什么不能像普通分区一样写
RPMB(Replay Protected Memory Block)是eMMC里一块带有重放保护机制的安全存储区域。它跟普通用户分区最大区别在于:每次读写都需要一次基于HMAC-SHA256的认证握手,密钥是出厂时写在设备内部的,主机侧不知道密钥就无法写入任何数据。
这种机制天然适合保存防回滚计数器、密钥材料、安全启动校验值这类数据。Android设备里的keymaster、widevine、以及bootloader的rollback index,很多就存放在RPMB分区里。
mmc_utils对RPMB的访问依赖内核的MMC子系统支持,同时设备节点需要指向对应的RPMB块设备,一般就是/dev/block/mmcblk0rpmb。不过在Android设备上,RPMB节点经常被上层权限策略隐藏,如果你ls看不到,就先从sysfs里确认设备是否存在:
ls -l /sys/class/block/mmcblk0rpmb能读到这个节点,RPMB的访问通道才可能打开。
6.2 rpmb read-counter、rpmb write-key与rpmb read:先读后写的基本功
对于一块还没烧录过认证密钥的全新RPMB分区,写入密钥是第一步,且这一步是不可逆的。一旦烧录了密钥,后续所有写入操作都要用这把密钥计算MAC,密钥遗忘就等于RPMB分区永久锁定。实际操作中,很多厂商会为同一批次设备烧录同一个测试密钥,量产固件里再通过安全通道写入各自的唯一密钥。
查看当前写计数器:
mmc rpmb read-counter /dev/block/mmcblk0rpmb写计数器是RPMB实现重放保护的核心——每次写入成功,计数器递增,回放旧的认证消息会因为计数器不匹配而被拒绝。所以排查RPMB写入失败时,第一步永远是读计数器,确认计数器的值是否符合预期。
烧录认证密钥:
mmc rpmb write-key /dev/block/mmcblk0rpmb <key_file>key_file是一个32字节的二进制文件,就是你要写入的HMAC密钥。如果设备已经有密钥了,再执行写密钥操作会报错并拒绝执行。这也是RPMB机制的一部分——密钥只能写一次。
读RPMB数据:
mmc rpmb read /dev/block/mmcblk0rpmb <address> <blocks> <output_file>RPMB的地址单位是256字节(一个RPMB frame的大小),所以读之前要搞清你要访问的帧号。我第一次用的时候习惯性按512字节扇区去算地址,结果怎么读都不对,后来才意识到RPMB的寻址粒度不是普通块设备的512字节。这个细节,踩过的人应该都懂。
6.3 rpmb write实操:完整流程与必须注意的陷阱
执行RPMB写操作的完整流程,我按实际经验拆成五步。
第一步,确认当前密钥状态。对一块新RPMB分区,先用read-counter确认计数器为0,说明还没写入过数据,密钥大概率也没烧录。如果计数器不为0,说明已经初始化过了,你后续写入操作得匹配原有密钥。
第二步,准备32字节密钥文件。可以用openssl生成:
openssl rand -out rpmb_key.bin 32务必保存好这个文件,它的丢失等于RPMB分区永久失效。
第三步,写入密钥:
mmc rpmb write-key /dev/block/mmcblk0rpmb rpmb_key.bin写入成功后会返回OK。再次执行read-counter,如果计数器变为非0,说明设备与密钥已完成绑定。
第四步,准备待写入的数据文件。RPMB每个frame是256字节,写入数据必须是256字节的整数倍。用dd生成一个测试数据:
dd if=/dev/urandom of=test_data.bin bs=256 count=1第五步,执行写入:
mmc rpmb write /dev/block/mmcblk0rpmb <address> <key_file> test_data.bin这里的address就是你要写入的帧号。写入完成后,再用6.2节里的rpmb read命令把数据读回来比对(前提是你的设备支持读取,有些安全策略会把读也锁住)。比对一致,说明整条链路已经打通。
这里有几个血泪教训要单独列出来:
- 写密钥之前,确认目标设备不是返修机。返修机可能已经有旧密钥,盲目烧新密钥会直接把RPMB锁死。
- 写操作期间绝对不能断电。RPMB写入一旦中断,轻则计数器错乱,重则整个RPMB分区访问异常。产线要写RPMB,务必加UPS和防掉电机制。
- 部分平台的RPMB需要先使能才能访问,命令是
mmc rpmb enable /dev/block/mmcblk0。如果你的rpmb read/write命令报出“operation not supported”之类的错误,先跑一下enable再试。
7. RPMB之外的“写入类”命令:bootpart、gp add、cmdq enable
7.1 bootpart enable与set:切换启动分区别手滑
Android设备的bootloader通常从eMMC的boot0或boot1分区加载。判断从哪个boot分区启动,就是靠BOOT_PARTITION_ENABLE这几位。
查看当前配置:
mmc bootpart get /dev/block/mmcblk0使能boot0为启动分区:
mmc bootpart enable 1 1 /dev/block/mmcblk0这条命令的两个数字参数,第一个是分区编号(1表示boot0,2表示boot1),第二个是boot ACK的使能标志(设为1时,eMMC会在boot操作完成后发送确认信号)。
bootpart操作最需要警惕的是:改动之后eMMC在下次上电就会按新配置执行引导。如果你当前系统就是从boot0启动的,而你在调试时把启动分区切到了boot1,而boot1里恰好没有可用bootloader,那么这块板子直接变砖。我早期吃过一次亏,从那以后每次切换之前都先把两个boot分区的镜像都备份好,并且准备海选工具随时救砖。
7.2 gp add与硬件分区管理:定制化的存储布局
eMMC除用户分区和boot分区外,还支持最多四个通用目的分区(General Purpose Partitions),简称GP分区。通过GP分区可以独立管理一部分存储空间,常用于安全启动镜像、日志存储或者车机系统的特定数据区。
创建GP分区:
mmc gp add <gp_partition_index> <size_in_blocks> /dev/block/mmcblk0其中gp_partition_index取值1到4。大小参数同样遵循分区粒度的对齐要求,建议先用extcsd里的MAX_ENH_SIZE_MULT计算好可用空间再动手。
GP分区一旦划分,想要收回是很麻烦的,所以生产项目上基本是“预先规划好,生产时就分好”。调试平台时可以随便折腾,真机上要按需求书的布局来。
7.3 cmdq enable与cache enable:性能开关不能乱开
Command Queue和Cache是两个能显著影响eMMC性能的特性,但它们的启用必须遵循硬件逻辑。
打开Command Queue:
mmc cmdq enable /dev/block/mmcblk0打开Cache:
mmc cache enable /dev/block/mmcblk0这两个命令操作之后,如果驱动不配套,设备可能在下次IO请求时直接报错,严重时挂起整个存储栈。以CMDQ为例,它要求内核的MMC子系统完整支持命令队列协议,还要适配的host controller,Android平台kernel版本过旧的话,贸然enable只会给驱动带来额外的兼容性压力。我的建议是:需要验证性能收益时,在产品测试固件里先enable跑一轮完整测试,确认稳定再合入默认配置,不要贪图省事直接在产品固件里打开。
8. 命令执行失败排查:我从报错堆里总结的经验
8.1 设备节点与权限问题:最常见的失败原因
报错信息“open /dev/block/mmcblk0 failed: Permission denied”,十有八九是权限问题。先确认当前是不是root:id命令输出UID为0就是root。如果已经是root,再检查SELinux,临时放行用setenforce 0试跑一条命令。如果两条都排除,那就看节点是否存在:ls -l /dev/block/mmcblk0,不存在的话可能是设备节点没创建,需要手动mknod,或者内核配置里没打开MMC支持。
8.2 命令不支持与参数格式错误:别急着怀疑eMMC坏了
有时候你执行mmc extcsd read能正常输出,但执行mmc rpmb read却报“Invalid argument”。这种情况先别怀疑芯片坏了,优先确认设备是否支持RPMB:通过extcsd的0x32偏移查一下RPMB_SIZE_MULT,如果这个字段是0,说明RPMB分区本身就不存在,命令当然不可用。
还有一种典型情况:地址或块数参数传得不对。RPMB按帧(256B)寻址,而普通erase命令按扇区(512B)寻址,两者混用会直接得到Invalid argument错误。碰到参数类报错,先拿mmc --help或者mmc cmd --help查看参数格式的绝对值,别靠猜。
8.3 内核版本与mmc_utils版本不匹配
mmc_utils更新迭代很快,老内核上的某些命令实现可能与新版本工具不兼容。典型例子是早期的mmc_utils不支持CMDQ相关命令,新内核新增的FFU(固件更新)等功能也需要对应版本的工具才支持。遇到“Operation not supported”或“Unknown command”时,去内核tools/mmc目录看看当前版本,或者直接拉一份最新的mmc_utils源码重新编译,能解决大部分奇奇怪怪的兼容问题。
9. 高频实用命令速查:我把它们按使用频率分了梯队
为了让你后续排查时能快速找到对应命令,我把本文涉及的命令整理成一张速查表,按我的使用频率从高到低排列。
| 使用频率 | 命令 | 用途 |
|---|---|---|
| 极高 | mmc extcsd read | 读取扩展寄存器全部字段,健康与配置分析的起点 |
| 极高 | mmc status get | 获取设备当前状态 |
| 极高 | mmc cid read | 读取芯片唯一标识与厂家信息 |
| 极高 | mmc csd read | 读取CSD寄存器,获取基础容量与速度参数 |
| 高 | mmc bootpart get | 查询当前启动分区配置 |
| 高 | mmc bootpart enable | 切换启动分区使能状态 |
| 高 | mmc rpmb read-counter | 查询RPMB写计数器 |
| 高 | mmc rpmb read | 读取RPMB数据 |
| 高 | mmc erase | 用户分区数据擦除 |
| 高 | mmc trim | 逻辑块标记失效 |
| 中 | mmc rpmb write | 写入RPMB数据 |
| 中 | mmc rpmb write-key | 烧录RPMB认证密钥 |
| 中 | mmc rpmb enable | 使能RPMB访问 |
| 中 | mmc bsr read | 读取总线速度模式 |
| 中 | mmc bsr write | 改写总线速度模式 |
| 中 | mmc sanitize | 全设备物理擦除 |
| 中 | mmc secure erase | 安全擦除指定区域 |
| 中 | mmc secure trim | 安全TRIM指定区域 |
| 低 | mmc fwrev read | 读取固件版本 |
| 低 | mmc hwrev read | 读取硬件版本 |
| 低 | mmc bist read/write | 内部数据总线自检 |
| 低 | mmc gp add | 创建通用目的分区 |
| 低 | mmc cmdq enable | 使能命令队列 |
| 低 | mmc cache enable | 使能设备缓存 |
| 低 | mmc sleep/awake | 手动控制设备休眠与唤醒 |
| 低 | mmc hwreset enable | 使能硬件复位功能 |
| 低 | mmc bkops enable | 使能后台操作 |
最后两条命令展开说明一下。mmc sleep和mmc awake在低功耗调试时非常有用,通过在shell里手动控制eMMC进入和退出休眠状态,可以快速验证功耗电流的波形变化,不用反复开关机。mmc bkops enable则用于触发设备自动后台整理,能减少前台访问时的卡顿感,但需要驱动和固件都完整支持。
10. 个人踩坑记录与经验总结
写到最后,我想分享几个在真实项目里反复踩过的坑,这些经验比命令本身更值钱。
第一,任何会改变设备状态的写操作,执行前都要有“救砖预案”。我见过不止一个新手工程师,拿着mmc_utils在样机上把bootpart切了、把RPMB密钥烧了,然后设备变成一块砖头,只能返厂重新替换eMMC。正确做法是:先备份完整的分区镜像,同时确保手里有可以进入刷机模式的工具链,再碰写类操作。
第二,RPMB密钥管理是产线的核心风险点。如果同一批设备烧录相同密钥,一旦密钥泄露,整批设备的安全防护形同虚设。如果每台设备烧录独立密钥,密钥的生成、分发、存储流程会变得异常复杂。从工程角度,我建议至少做到“不同项目、不同批次、不同密钥”,密钥文件加权限控制并留好安全备份。
第三,eMMC是个“无限逼近物理极限”的器件。读extcsd时看到的生命周期估算、剩余寿命这些参数,不是精确的物理测量值,而是设备内部固件基于磨损均衡算法的估算。所以它们适合用来做趋势分析和异常预警,不适合作为“绝对阈值”来卡控。比如PRE_EOL_INFO到了0x01,不一定代表立刻会挂,但你的测试策略就要从常规测试切到加强老化测试了。
第四,mmc_utils输出的hex数据,建议直接用脚本做二次解析。手工盯着一堆十六进制看,效率太低。我平时会用一段Python脚本,把extcsd的原始输出转成结构化字段表,自动高亮那些异常标志位。建议你也建立一套自己的解析工具链,能把大量设备的extcsd批量收集起来,做横向对比、筛选异常,这在产线抽检中特别管用。
mmc_utils只是打开eMMC这扇大门的一把钥匙,真正值钱的是你对它背后工作机制的理解。一次次把命令跑通、把字段查明白、把报错理清楚之后,eMMC在你手上就不再是一个按下电源键就自动工作的黑盒,而是每一个状态、每一段时序、每一条安全机制都可解释、可控制、可预期的存储系统。这就是底层工程师最爽的时刻。