本文深入对比 eMMC 与 UFS 两种 Android 设备主流存储介质的硬件架构、协议差异、关键技术,并通过代码示例展示 Linux 下如何读取存储设备的健康信息。
一、为什么手机需要专门的存储芯片?
PC 用 SSD,服务器用 NVMe,而手机这种空间极度受限的设备,需要把"控制器 + NAND 闪存"封装到一颗 BGA 芯片里,这就是 eMMC 和 UFS 的本质 --- 嵌入式存储。
它们和原始 NAND 闪存的区别是:控制器已经集成在芯片内部,主机端只需要标准协议就可以读写,不用去操心 NAND 的坏块管理、磨损均衡、ECC 等繁琐工作。
二、eMMC 详解
1. eMMC 的来历
eMMC(embedded MultiMediaCard)由 JEDEC 标准化,本质上是把 MMC 卡(老旧的存储卡格式)封装到 BGA 中,直接焊在主板上。
历代版本:
| 版本 | 顺序读 (MB/s) | 顺序写 (MB/s) | 总线宽度 | 频率 |
|---|---|---|---|---|
| 4.5 | ~140 | ~50 | 8 bit | 200 MHz |
| 5.0 | ~250 | ~90 | 8 bit | 200 MHz |
| 5.1 | ~350 | ~150 | 8 bit | 200 MHz HS400 |
2. eMMC 接口信号
主机 (Host) eMMC 设备
+-----+ +-------+
| |--CLK---------> | |
| | | |
| |<--CMD-------->| | (双向命令线)
| | | |
| |<--DAT0~DAT7-->| | (8 位数据线)
| | | |
| |--RST_n-------->| |
| | | |
+-----+ +-------+
3. eMMC 工作模式
eMMC 设备有 3 个独立的硬件分区(注意!这里的"分区"是硬件层面的,不是 GPT 分区):
-
Boot Area 1 / 2 --- 各 4MB,启动 BootROM 时从这里读
-
RPMB (Replay Protected Memory Block) --- 防回放保护区,存密钥
-
User Area --- 主要存储区,GPT 在这上面
+--------+
| Boot 1 | 4 MB -> BootROM 读这里
+--------+
| Boot 2 | 4 MB
+--------+
| RPMB | 4 MB -> 防重放,用 HMAC-SHA256 保护
+--------+
| |
| User | 剩下的所有空间
| Area |
| |
+--------+
切换访问区域是通过 CMD6 配置 PARTITION_CONFIG 寄存器实现的。
4. eMMC 命令系统
eMMC 命令以 CMDn 表示,每个命令都是 48 位:
[47] 起始位 (0)
[46] 传输方向 (主机->设备 = 1)
[45:40] 命令索引 (CMDn)
[39:8] 参数 (32 位)
[7:1] CRC7
[0] 结束位 (1)
常用命令:
| 命令 | 名称 | 作用 |
|---|---|---|
| CMD0 | GO_IDLE_STATE | 复位设备 |
| CMD1 | SEND_OP_COND | 询问 OCR |
| CMD2 | ALL_SEND_CID | 读取 CID(厂商/型号/序列号) |
| CMD3 | SET_RELATIVE_ADDR | 分配 RCA |
| CMD6 | SWITCH | 切换 ExtCSD |
| CMD8 | SEND_EXT_CSD | 读取 ExtCSD (512 字节) |
| CMD17 | READ_SINGLE_BLOCK | 读一个块 |
| CMD18 | READ_MULTIPLE | 读多个块 |
| CMD24 | WRITE_BLOCK | 写一个块 |
| CMD25 | WRITE_MULTIPLE | 写多个块 |
5. 读取 eMMC 健康度
eMMC 5.0+ 支持通过 ExtCSD 读取使用寿命:
c
// ExtCSD offsets (JEDEC JESD84-B51)
#define EXT_CSD_DEVICE_LIFE_TIME_EST_TYP_A 268
#define EXT_CSD_DEVICE_LIFE_TIME_EST_TYP_B 269
#define EXT_CSD_PRE_EOL_INFO 267
/*
* Pre EOL Info:
* 0x01 = Normal (剩 80%+)
* 0x02 = Warning (已用 80%~90% 备用块)
* 0x03 = Urgent (备用块基本耗尽)
*
* Life Time Est:
* 0x01 = 0~10%
* 0x02 = 10~20%
* ...
* 0x0A = 90~100%
* 0x0B = 已超出预期寿命
*/
在 Linux 上读取 ExtCSD:
bash
# 需要 mmc-utils
mmc extcsd read /dev/mmcblk0
# 输出片段:
# eMMC Life Time Estimation A [EXT_CSD_DEVICE_LIFE_TIME_EST_TYP_A]: 0x01
# eMMC Life Time Estimation B [EXT_CSD_DEVICE_LIFE_TIME_EST_TYP_B]: 0x01
# eMMC Pre EOL information [EXT_CSD_PRE_EOL_INFO]: 0x01
三、UFS 详解
1. UFS 的革命性
UFS(Universal Flash Storage)是 JEDEC 推出的下一代标准,它的设计借鉴了 PCIe / SATA 的全双工架构,而 eMMC 是半双工。这带来了根本性的性能提升。
历代版本:
| 版本 | 顺序读 (MB/s) | 顺序写 (MB/s) | 通道数 | 链路速率 |
|---|---|---|---|---|
| 2.0 | 850 | 250 | 1-2 | HS-G2 (5.8 Gbps) |
| 2.1 | 860 | 255 | 1-2 | HS-G3 (11.6 Gbps) |
| 3.0 | 2100 | 410 | 2 | HS-G4 (11.6 Gbps) |
| 3.1 | 2100 | 1200 | 2 | HS-G4 (11.6 Gbps) |
| 4.0 | 4200 | 2800 | 2 | HS-G5 (23.2 Gbps) |
2. UFS 协议栈
UFS 协议是分层的,类似网络七层模型:
+---------------------------+
| Application Layer | UFS Command Set (SCSI 子集)
+---------------------------+
| UTP (Transport Protocol) | UPIU (UFS Protocol Information Unit)
+---------------------------+
| UniPro | L1.5 - L4 (类似 InfiniBand)
+---------------------------+
| M-PHY | 物理层,差分对,LVDS
+---------------------------+
3. UFS 物理接口
UFS 使用 M-PHY 差分对,信号完整性远超 eMMC 的单端信号:
主机 (Host) UFS 设备
+-----+ +-------+
| |--TX_DP/DN1-----> | |
| |--TX_DP/DN2-----> | | (双通道发送)
| | | |
| |<--RX_DP/DN1----- | |
| |<--RX_DP/DN2----- | | (双通道接收)
| | | |
| |--REF_CLK-------> | |
| |--RESET---------> | |
+-----+ +-------+
关键点:UFS 是全双工(同时收发),而 eMMC 是半双工。
4. UFS 的关键特性
(1) 命令队列 (Command Queue)
UFS 支持最多 32 条命令同时排队,主机不需要等一个命令完成才发下一个。
eMMC: CMD1 -> wait -> CMD2 -> wait -> CMD3 -> ... (串行)
UFS: CMD1 ┐
CMD2 ├-> 设备同时处理 -> 乱序返回
CMD3 ┘
(2) Multi-LUN
UFS 设备内部可以划分多个逻辑单元(Logical Unit),类似 SAS 硬盘:
- LUN0 ~ LUN7: 普通存储区
- B0: Boot LUN A
- B1: Boot LUN B
- B2: RPMB
每个 LUN 独立寻址,互不影响。
(3) HPB(Host Performance Booster)
UFS 3.1 新增,把 FTL(Flash Translation Layer)映射表的一部分缓存到主机 DRAM,减少随机读延迟。
正常路径: Host -> UFS Controller -> 查 FTL(SRAM) -> 读 NAND -> 返回数据
HPB 路径: Host -> 命中 HPB Cache(自带映射) -> UFS 直接读 NAND -> 返回
实测随机读 IOPS 可提升 30~50%。
(4) Write Booster
把一部分 TLC/QLC 闪存当 SLC 用,作为高速写入缓存,需要时再回刷到普通区。
5. UPIU 包结构
c
// UFS Protocol Information Unit
struct upiu_header {
uint8_t transaction_type; // 0x01 = Command, 0x21 = Response ...
uint8_t flags;
uint8_t lun; // 目标 LUN
uint8_t task_tag; // 命令标签 (0~31)
uint8_t cmd_set_type; // 0 = SCSI
uint8_t query_function;
uint8_t response;
uint8_t status;
uint8_t total_ehs_length;
uint8_t device_info;
uint16_t data_segment_length;
};
struct upiu_cmd {
struct upiu_header header;
uint32_t expected_data_xfer_length;
uint8_t cdb[16]; // SCSI CDB
};
主机发送 SCSI 命令(比如 READ(10))时,把它封装到 UPIU 中,再交给 UniPro 传输。
四、eMMC vs UFS 对比
| 维度 | eMMC | UFS |
|---|---|---|
| 双工模式 | 半双工 | 全双工 |
| 命令队列 | 不支持(eMMC5.1有有限支持) | 支持 32 命令并发 |
| 接口类型 | 并行(CLK+CMD+8DAT) | 差分串行(M-PHY) |
| 信号完整性 | 一般 | 优秀 |
| 功耗 | 低 | 略高 |
| 多逻辑单元 | 仅 3 个固定分区 | 多个 LUN |
| 顺序读峰值 | ~350 MB/s | ~4200 MB/s (UFS 4.0) |
| 随机读 IOPS | ~12K | ~100K+ |
五、Android 中的 sysfs 接口
在 Android 系统中,可以通过 sysfs 拿到很多存储信息:
eMMC
bash
adb shell
# 厂商 CID
cat /sys/block/mmcblk0/device/cid
# 14010038302042414c00010000000000
# 型号
cat /sys/block/mmcblk0/device/name
# H4G2a
# 厂家 ID
cat /sys/block/mmcblk0/device/manfid
# 0x000015 -> Samsung
# 健康度
cat /sys/block/mmcblk0/device/life_time
# 0x01 0x01
UFS
bash
# 厂商
cat /sys/devices/.../host0/target0:0:0/0:0:0:0/vendor
# SAMSUNG
# 型号
cat /sys/devices/.../host0/target0:0:0/0:0:0:0/model
# KLUDG4UHDC-B0E1
# 健康度 (UFS 3.1+ 通过 dQueryDescr 读 Health Descriptor)
cat /sys/class/scsi_host/host0/eh_deadline
六、实战:写一个读取 UFS 健康描述符的程序
通过 ioctl 发送 SCSI Query Request,读取 Health Descriptor。
c
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <scsi/sg.h>
// Vendor Specific Query (具体 OPCODE 与平台相关)
#define UFS_QUERY_HEALTH_OPCODE 0xC0
struct ufs_health {
uint8_t length;
uint8_t descriptor_type;
uint8_t pre_eol_info; // 0x01 / 0x02 / 0x03
uint8_t device_life_a; // 0x01 ~ 0x0B
uint8_t device_life_b;
uint8_t reserved[27];
} __attribute__((packed));
int read_ufs_health(const char *dev, struct ufs_health *health) {
int fd = open(dev, O_RDWR);
if (fd < 0) return -1;
uint8_t cdb[10] = {
UFS_QUERY_HEALTH_OPCODE, 0, 0, 0,
0, 0, 0, 0, sizeof(*health), 0
};
uint8_t sense[32];
sg_io_hdr_t io_hdr = {
.interface_id = 'S',
.cmd_len = sizeof(cdb),
.mx_sb_len = sizeof(sense),
.dxfer_direction = SG_DXFER_FROM_DEV,
.dxfer_len = sizeof(*health),
.dxferp = health,
.cmdp = cdb,
.sbp = sense,
.timeout = 5000,
};
int ret = ioctl(fd, SG_IO, &io_hdr);
close(fd);
return ret;
}
int main(int argc, char **argv) {
struct ufs_health h = {0};
if (read_ufs_health(argv[1], &h) == 0) {
printf("Pre-EOL Info : 0x%02X\n", h.pre_eol_info);
printf("Life Time A (10%%/u): 0x%02X\n", h.device_life_a);
printf("Life Time B (10%%/u): 0x%02X\n", h.device_life_b);
} else {
fprintf(stderr, "Failed to read UFS health\n");
}
return 0;
}
注:不同厂商的 UFS 健康描述符 OPCODE 可能不同,需查阅平台手册。
七、给开发者的实用经验
- 写放大要注意:UFS 的 GC(垃圾回收)发生时,瞬时写性能会跳水。不要把日志疯狂打到磁盘
- fsync 的代价:fsync 会触发 FUA(Force Unit Access),延迟从微秒级跳到毫秒级,慎用
- 大文件用 fallocate:预分配空间可以减少碎片,提升后续写入速度
- trim/discard 要开启:让 FTL 知道哪些块可以擦除,延长寿命
- 避开 4KB 对齐问题:UFS 内部页通常是 4KB 或 16KB,小于这个粒度的写会触发 RMW(Read-Modify-Write)
八、总结
eMMC 已经是过去式,UFS 是当下中高端 Android 设备的标配,而 UFS 4.0 / UFS 5.0 还在不断突破带宽天花板。理解它们的硬件架构,在做存储性能优化、定位卡顿问题、甚至选择硬件时都极有价值。
下篇我们继续讲 Android 的 A/B 无缝更新,以及它如何巧妙利用 UFS 的多 LUN 设计。