芯参谋(84):Linux eMMC 驱动开发指南

本文要解决的是「一颗 eMMC 焊到板子上之后,从硬件上电、内核枚举、模式切换、 块设备挂载、一直到产线烧录与 OTA 的整条链路,怎么做才不会踩坑」。

很多团队在 eMMC 上的问题并不是「不会写驱动」,而是分散在多个环节的细节没有对齐: 硬件上电顺序没按规范做、EXT_CSD 没读全就切了 HS400、Cache 开着但掉电策略没配套、 U-Boot 与内核分区表不一致、产线第一次烧录用的工具跟量产工具不是同一个...... 这些坑每一个单独查资料都能解决,但串起来看才能真正省时间。

  • 驱动开发者:需要理解 MMC 子系统的分层、控制器驱动怎么接、模式切换怎么调、设备树怎么写。
  • BSP / 内核移植工程师:需要 config 清单、DTS 样例、U-Boot 配套、与启动链路的衔接。
  • 硬件 / PCB 工程师:需要知道驱动行为对硬件的约束(电源时序、门控、复位、差分走线、测试点)。
  • 产线与测试工程师:需要首件烧录 SOP、掉电测试方法、寿命与健康度采集手段。
  • 存储产品经理:需要容量/分区规划依据、寿命指标口径、模式与性能的取舍关系。

不在本文范围:eMMC 器件内部的 FTL 算法、ECC 纠错算法、NAND 阵列时序电路; 这些由器件 datasheet 与《eMMC 软件设计规范》负责。本文只在「驱动需要用到它的哪个字段」这个层面引用。

0.2 术语与缩略语

下表把全文会反复出现的名词一次性讲清,后文不再解释。标星号的三条是最容易误解的。

缩写 / 术语 英文全称 含义与工程含义
eMMC embedded MultiMediaCard 焊在板上的不可移除存储器件,内置 FTL,对主机呈现为块设备
FTL Flash Translation Layer 器件内部的地址翻译与磨损均衡层;主机不可见,也不可绕过
RPMB Replay Protected Memory Block 带 HMAC 认证与防回放计数器的安全存储区,容量仅数百 KB
HS200 High Speed 200 eMMC 4.5 引入的单数据线 200 bit/s 模式,是 HS400 的过渡基础
HS400 High Speed 400 eMMC 5.0 引入的 DDR 双数据线模式,理论 400 MB/s 级别接口速率
HS400EX HS400 Enhanced Strobe eMMC 5.1 引入,用 DS 信号提供采样时钟,抑制 WR skew
DS Data Strobe eMMC 输出的数据选通信号,控制器据此在数据眼中心采样
OCR Operation Conditions Register 卡上电后的能力与电压登记表,CMD1 用它做初始化握手(★)
CMD1 Send Operating Conditions Reg eMMC 专属初始化命令,循环发送直到 OCR 的 CCS 位为 1
CID / CSD / CSR Card IDentification / Card Specific Data / Card Status 三张 128bit 寄存器;CSR 在 CMD13 查询
RCA Relative Card Address CMD3 分配的主机侧短地址,4 字节高 16 位为 RCA
EXT_CSD Extended Card Specific Data 512 字节扩展寄存器,速率模式、Cache、分区配置、寿命全在这里(★)
CMD6 SWITCH 写 EXT_CSD 的专用命令,参数带 R1b,切模式与开关 Cache 全用它
ADTC Addressed Data Transfer Command 带地址的数据传输类命令,如 CMD17/18/25/35
DCMD Device Control Command 设备自终止类命令,如 CMD6,地址固定为 0
rpmb 分区 RePlay Protected Memory Block partition eMMC 硬件分区之一,经 /dev/mmcblk0rpmb 或 TEE 访问
SDHCI SD Host Controller Interface SD/MMC 控制器的标准寄存器与编程模型规范
CMDQ Command Queuing 把读写命令放进硬件队列,控制器批量取指执行,见 9.5 节
CQE Command Queue Engine 承载 CMDQ 的硬件引擎,寄存器组叫 CQEQ
fio - Linux 侧性能压测工具,用于建立基线与回归对比
GPT / MBR GUID Partition Table / Master Boot Record 分区表格式;eMMC 量产推荐 GPT
PARTUUID Partition UUID GPT 分区唯一标识,设备树里用它而非 /dev/mmcblk0p2
Boot0 / Boot1 Hardware partition 0/1 eMMC 出厂即有的两个启动分区,不计入标称容量
BKOPS Background Operations 让 Cache 开关在安全时机自动生效的机制
WR skew Write Timing Skew 写入数据相对时钟的偏移,靠 DS 增强选通抑制

三个最容易误解的点,先记住: ① eMMC 里的 NAND 不能像 SPI NAND 那样直接写,MTD / UBI 那套在 eMMC 上 完全用不上 ,内核只把它当块设备; ② eMMC 的硬件分区(Boot0/Boot1/RPMB)不算在标称容量里 , 128 GB 的卡实际可用只有 128 GB 减去约 16 MB; ③ 切到 HS400 需要重新训练采样点,不是把频率调高就行。

0.3 内核版本与接口差异

MMC 子系统这几年的改动集中在「CQE 命令队列」与「缓存策略」两块,控制器驱动接口相对稳定。 下面按版本列出对本文内容有实际影响的差异点,避免用新内核的命令去查老内核的树。

内核版本 关键变化 对本文的影响
4.14 及更早 无 CQE,quirks 少 本文第 7/9 章的 CQE 内容不适用;模式切换逻辑分散在各控制器驱动里
4.19 mmc_of_parse() 统一大部分 DT 解析;引入 mmc-hs400-enhanced-strobe 设备树写法基本定型,本文第 5 章写法适用
5.4 控制器驱动从 sdhci.c 拆出 sdhci-of-dwcmsh.c 等 看错文件找不到 DSR 处理的排查思路要更新
5.10 引入 mmc_cqe 的 DT 描述(mmc-cqe)与更多 SoC 支持 第 9.5 章 CQE 内容适用
5.15 缓存与回写策略从 bootloader 传到内核并完善 第 10 章 Cache 一致性方案需与 U-Boot 声明对齐
6.1 DTS 顶层 mmc 节点属性清理,废弃一批老写法 老设备树需迁移,见 5.4 节
6.6 mmc-regulator 增加 regulator-allow-set-load 电源门控方案需检查 13.3 节列出的新属性

版本相关的三条硬规则

① 结构体字段名随版本变化,mmc_host 在 4.x 与 6.x 之间成员差别很大, 直接照抄老代码的字段初始化会编译失败或语义错位;

② 本文所有内核配置项与 DT 属性都标注了「最低版本」,低于该版本时请先确认属性是否被解析;

③ 线上设备的内核版本一旦定了,就不要在补丁里引用新版本独有的 API,需要兼容时用 #if LINUX_版本号 V1.0 >= KERNEL_VERSION(x,y,0) 包起来并写清理由。

0.4 硬件平台与参考器件

本文以「ARM64 主控 + 1 颗 eMMC + 独立 LDO 供电 + SDHCI 兼容控制器」为基准平台。 下表列出参考器件的规格区间,具体型号参数以对应料号正式 datasheet 为准。

维度 基准取值 说明
eMMC 容量 32 GB / 64 GB / 128 GB 覆盖主流量产档位;小于 4 GB 的器件已极少
eMMC 版本 5.1(HS400EX) 向下兼容 4.51(HS400)、4.5(HS200)
封装 FBGA-153,0.4 mm 球径 需 6 层 PCB;HS400 差分线需 100 欧姆阻抗控制
接口模式 HS400(双 DDR 数据线) 本文主线;HS200 / HS 作为降级路径
主控 四核 A53 + SDHCI 兼容 MMC 控制器 示例平台;实际项目以 SoC 手册为准
总线宽度 8 bit DAT + 1 bit CMD eMMC 固定 1 bit CMD,数据线 1/4/8 bit 三档
供电 VCC 3.3 V / VCCQ 1.8~3.3 V / VDDIO 1.8~3.3 V VCCQ 需与 SoC IO 电平一致
存储形态 裸 eMMC / eMCP(eMMC+LPDDR 合封)/ uMCP(uMMC+LPDDR) 合封形态软件栈完全一致,仅 DRAM 侧有差异

本文与配套文档的分工

《eMMC 电路设计指南》 → 怎么接、怎么供电、怎么走线(硬件全貌)

《eMMC 软件设计规范》 → 器件协议:命令序列、EXT_CSD 位定义、寿命口径(器件层)

本文 → 内核配置、设备树、框架、模式切换、块设备路径、测试、调试、量产(系统集成层)

冲突时的优先级:datasheet > 电路设计指南 > 软件设计规范 > 本文。

第 1 章 · eMMC 器件与协议基础

在动手写驱动之前,先把「eMMC 到底是什么、它有哪些模式、上电后怎么活过来」讲清楚。 这一章的内容决定了后面所有代码的时序约束。

1.1 eMMC 是什么:NAND + FTL + Cache

从主机视角看,eMMC 就是一个能挂到 /dev 下的块设备;但从硬件视角看,它是 「NAND 阵列 + 内部控制器(FTL、ECC、缓存、调度)+ 主机接口」三层叠在一起的东西。 理解这三层的分工,才能明白哪些行为是主机能控制的、哪些只能「请求并等待」。

主机接口层(Host Interface)CMD(1 bit) + DAT7:0 + CLK + RST_n + VDDIO + 供电域器件内控制器(FTL / Cache / Scheduler)FTL:LBA 与物理块地址翻译Cache:eMMC 内部可缓存(可关闭)ECC:片内纠错,主机不可见Scheduler:写合并与调度NAND 阵列(Raw NAND)裸存储:擦除块 / 页 / SLC / TLC;坏块由器件内部管理,主机不可见主机能控制的:命令序列、模式切换、Cache 开关、块设备挂载与调度主机控制不了的:FTL 地址翻译、坏块替换、ECC 强度、磨损均衡算法

图 1 · eMMC 内部三层结构与主机可见边界

这张图解释了三个常见现象的成因:

  • 为什么 eMMC 不能像 SPI NAND 那样做「整片擦除」 ------FTL 在中间,主机看到的是 LBA, blkdiscard 只能「告知」器件某段逻辑块可回收,不能强制释放物理块。
  • 为什么 eMMC 的坏块不用主机管 ------坏块管理在 FTL 里,主机只会在 EXT_CSD[230](Pre-EOL Info)看到寿命状态。
  • 为什么「写完立刻断电」会丢数据------数据可能还在器件 Cache 里没落 NAND, 需要主机显式发 Cache Flush(见 10.3 节)。

1.2 卡上存储区布局:Boot / RPMB / 用户区

eMMC 内部把存储划成若干硬件分区,各分区大小在出厂时由器件内部固件固化、 不可通过 EXT_CSD 改写(只有「是否使能」可配)。理解这一点,才能解释分区表里那些「凭空多出来」的设备节点。

Boot0约 4 MBBoot1约 4 MBRPMB约 128 KB用户区 User Area标称容量减约 16 MBBoot0 / Boot1:器件出厂即存在,切分点固化在器件内部;Bootloader 厂商约定用其中一个,另一个留作 A/B 备份RPMB:Replay Protected Memory Block,HMAC 认证 + 单调递增计数器,主机可申请 Key 后访问标称 128 GB 卡的容量核算用户区约等于 128 GB 减 Boot0(4 MB) 减 Boot1(4 MB) 减 RPMB(128 KB) 减器件内部保留(数 MB)结论:分区表可用容量永远略小于标称容量,做容量规划时按用户区算,不要按标称算说明:各分区实际大小以具体料号 datasheet 的 partition table 描述符为准,不同厂商可能不同

图 2 · eMMC 硬件分区布局与容量核算

设备节点 所属分区 可否被分区表引用 典型用途
/dev/mmcblk0 用户区(整块) 不要直接写 整卡容器;dd 到它会清空所有分区
/dev/mmcblk0boot0 Boot0 硬件分区 绕过分区表 Bootloader 存放区(主)
/dev/mmcblk0boot1 Boot1 硬件分区 绕过分区表 Bootloader 备份区(A/B 方案)
/dev/mmcblk0rpmb RPMB 硬件分区 需先 Key 编程 安全存储:密钥、计数器、授权票据
/dev/mmcblk0p1 ~ pN 用户区内的 GPT/MBR 分区 正常引用 boot / rootfs / data / userdata

量产最危险的三条命令

复制代码
dd if=/dev/zero of=/dev/mmcblk0 bs=1M        # 清空用户区所有分区,设备变砖
dd if=/dev/zero of=/dev/mmcblk0boot0     # 覆盖 Bootloader,板子变砖且不进系统
sgdisk --zap-all /dev/mmcblk0            # 抹掉分区表,同样不可逆

这三条只能在产线首件 / 返修工位 由受控脚本执行,禁止开放给现场调试终端。 开发机上的 shell 别名、脚本里的通配符,以及误把 /dev/mmcblk1(SD 卡枚举) 当成 eMMC,都是高频事故源。建议产线脚本在执行前打印 mmcblk0 的 NAME / 容量 / /sys/block/*/device 链接做二次确认。

1.3 速度模式谱系:从 HS 到 HS400EX

eMMC 的「速度模式」是一组**时序参数(频率 + 数据线数 + 采样方式 + 电压)**的组合。 每种模式都对应 datasheet 里一张时序表和一组 EXT_CSD 位。搞混模式和频率的关系, 是「切了模式但没提速」的根因。

模式 引入版本 数据线 采样方式 典型最高频率 理论峰值 驱动侧要点
MMC / High Speed 早期 1 / 4 / 8 bit CLK 上升沿 26 MHz 约 25 MB/s(8 bit) Linux 里对应 MMC_TIMING_LEGACY / HIGH_SPEED
HS200 4.5 1 bit(单线) CLK 上升沿 + 训练采样点 200 MHz 约 25 MB/s(时序余量大) 需要 CMD21 训练;对应 MMC_TIMING_HS200
HS400 5.0 2 bit(双 DDR) CLK 上下沿 + 训练采样点 200 MHz 约 200 MB/s 需要 DS 或控制器采样点训练;MMC_TIMING_HS400
HS400EX 5.1 2 bit(双 DDR) DS 增强选通(抑制 WR skew) 200 MHz 与 HS400 同,裕量更好 需额外 DS 引脚接线;DT 用 mmc-hs400-enhanced-strobe

两个反直觉的事实

① HS200 的峰值带宽其实和 HS 接近 , 它的价值是把时序窗口从「固定 7.5 ns」放宽到「可训练」,为 HS400 打基础; 真正的带宽跃升发生在切到 HS400 的双数据线 DDR 那一刻。

② HS400 不是「把频率调高」得到的 ------HS 与 HS200 都是单数据线, HS400 是靠「双数据线 + 上下沿采样」把每周期搬运量翻倍再叠加频率。 因此从 HS200 切 HS400 时,采样点必须重新训练,不能沿用 HS200 的训练结果。

Legacy / MMC小于 26 MHz1/4/8 bitHigh Speed小于 52 MHz1/4/8 bitHS200200 MHz1 bit SDRHS400200 MHz2 bit DDRHS400EX200 MHz2 bit DDR + DS切换手段:CMD1(初始化 OCR)→ CMD6 写 EXT_CSD(模式)→ CMD6(Cache 与电压切换)→ CMD21(训练)每次切换都必须等 R1b 的 busy 应答结束(DAT0 拉低期间禁止发下一条命令),否则器件状态机会锁死

图 3 · eMMC 速度模式切换阶梯与每一级的切换手段

1.4 命令集与 R1 / R1b 响应

主机与卡之间的所有交互都通过「命令索引 + 参数 + CRC」构成的一条命令完成。 驱动代码里看到的 struct mmc_command 就是这个结构。 工程上必须记住的差别只有两点:响应类型 与是否带数据。

响应类型 含义 代表命令 驱动等待要点
无响应 R0 纯命令,不回任何状态 CMD0 GO_IDLE_STATE、CMD4 SET_DCR 发完即返回;失败只能靠超时发现
R1 短格式状态,含 CMD 索引 CMD1(OCR 阶段)、CMD9、CMD13 检查 resp[0] 最高位是否为命令索引
R1b R1 + busy 信号(DAT0 拉低) CMD6 SWITCH、CMD1 电压切换后 必须轮询等待 busy 结束;切模式可长达 2 s
R2 48 bit,含 CID CMD2 ALL_SEND_CID、CMD3 分配 RCA 读 128 bit CID
R3 32 bit,含 R1 状态内容 CMD6 的 R1b 变体、SD 专有命令 与 R1 相同的响应内容
R6 / R7 R1 + 相对地址 / 电压窗口 CMD16 设定块长、CMD17/18 传参校验 R7 的 19 位电压窗口用于校验卡状态
带数据 R1b 数据 + busy 结尾 CMD25 写多个块 写完必须等 busy,此时数据才算真正落到 NAND 或 Cache

R1b 等待不足等于卡死,这是 eMMC 调试里最高频的「卡不动」根因

场景:切 Cache 时 CMD6 发出,busy 信号持续 300 ms; 驱动里 timeout_ms 填了 10 ms,函数返回 -ETIMEDOUT。 上层继续发下一条命令,而器件还在 busy 状态机里------后续所有命令被忽略,看起来像「卡死了」。

正确做法:CMD6 的 timeout_ms 给足 切模式不小于 1000 ms、切 Cache 不小于 500 ms、切电压不小于 100 ms (按 datasheet 取值),并在超时后走「整卡 reset 恢复」路径,而不是继续用。

1.5 EXT_CSD 关键字段速览

EXT_CSD 是 512 字节的扩展寄存器区,通过 CMD6 的读参数模式读出。 Linux 里对应 struct mmc_ext_csd,由 mmc_read_ext_csd() 填充。 驱动开发者最常打交道的字段如下。

字节 字段名 含义 驱动里的用途
7 REV EXT_CSD 版本 判定支持 4.5 / 5.0 / 5.1 特性;mmc_ext_csd.rev
183 SEC_FEATURE_SUPPORT 安全特性集(AFU / RPMB) 判断 RPMB 与认证特性是否可用
192 DEVICE_TYPE 卡类型:HS200 / HS400 / 5.1 card->ext_csd.hs200 与 .hs400 的判定依据
196 HS_200_TIMING HS200 时序支持位 与 DEVICE_TYPE 一起确认能否切 HS200
212 DSTR_SUPPORT DS 信号支持位 判定能否启用 HS400EX
215 HS400_TUNING_BLOCK_T HS400 训练块类型 决定 CMD21 训练数据的解析方式
224 HESW Boot / RPMB 硬件分区使能 EXT_CSD_PARTITION_CONFIG,控制是否上报 boot/rpmb 节点
225 HS_200_TUNING_BLOCK 训练块地址 训练块就在用户区里,需先由 U-Boot 预置
228 / 239 CACHE_SIZE / CACHE_SIZE_E1 Cache 大小(两处拼成完整值) mmc_cache_size()
234 CACHE_UNIT_SIZE Cache 写粒度 决定 flush / 失效的最小单位
241 BKOPS 后台操作支持与自陷使能 控制 BKOPS_EN / BKOPS_DIS 自动生效时机
246 CACHE_CTRL Cache 使能与失效 CACHE_ENABLE 位就是 cache_enable() 写的
249 PARTITION_TABLE_SET 分区表状态 产线烧录后需确认都为 1(完成标记)
268 / 269 PRE_EOL_LIFE_TIME_EST 预估寿命 A / B / C 健康度上报的数据来源,见 18.4 节
21/22/23 C_SIZE / C_SIZE_MULT / ERASE_GRP_DEF 容量计算三要素 容量 = (C_SIZE+1) × 256 × C_SIZE_MULT × 512B

手算校验容量:驱动和产线都用得上

复制代码
C_SIZE_EXT = ((EXT_CSD[183:185] & 0x00FFFF) << 8) | EXT_CSD[185]   /* [185:186] 共 22 bit */
容量 = (C_SIZE_EXT + 1) * 512B * 256 * (C_SIZE_MULT_EXT)
/*   C_SIZE_MULT_EXT = (EXT_CSD[223:222] & 0x03) | (EXT_CSD[221] & 0xC0) >> 6
   ERASE_GRP_DEF 决定这个 256/512 是写还是擦除单位,仅在超大容量时影响结果 */

调试时用这段算式和 lsblk 报出的容量对一下:对不上说明 EXT_CSD 读的不是 你期望的那张卡 (多卡场景)或 EXT_CSD_REV 解析路径不对。

1.6 上电时序与初始化状态机

eMMC 有自己的内部状态机。上电后卡处于 Pre-Idle 附近, 必须由主机发 CMD1 完成电压协商与初始化 ,才能进入 Stand-by 接受 CMD2 / CMD3。 驱动代码里 mmc_send_init_ocr() 就是这个循环。

Pre-IdleVCC 稳定≥300 msCLK 可提供Init OCR循环 CMD1直到 OCR.CCS = 1IdentificationCMD2 → CMD3→ CMD9 → CMD7Bus SetupCMD16 块长CMD6 总线宽度高速模式CMD6 切 HS200再切 HS400Trans(传输态)------ 一切数据命令的执行状态CMD17/18(单块读写)、CMD25/18(多块读写)在这里发出;写命令结束必等 R1b busyHS200 / HS400 切换在回到 Stand-by 之后做,切换完再回 Trans关键约束:CMD1 必须在 VCC 稳定且时钟可用后开始;busy 期间不得插入任何其它命令失败恢复:任一步骤超时则拉 RST_n 复位(低有效,保持不小于 1 个时钟周期)→ 重新走 Init OCR

图 4 · eMMC 上电初始化状态机与关键 CMD 顺序

RST_n 复位的两个常见误用

① 复位脉冲太短 :datasheet 要求 RST_n 保持低电平至少 1 个主机时钟周期。 如果 RST_n 直接接到 SoC 的 GPIO 且该 GPIO 上电默认状态不确定,首次上电可能出现 「器件没有真正复位」的偶发初始化失败。规范做法是用 regulator + reset-gpios, 并在 DT 里明确初始电平。

② 复位后立刻发命令 :RST_n 拉高后器件需要内部状态机复位时间(典型不小于 1 ms), 此时发出的 CMD 可能被吃掉。Linux 的做法是在 DT 里用 reset-delay-ms 预留延时, 或由 pwrseq 的 post-power-on-delay-ms 覆盖。

第 2 章 · 硬件设计规范(驱动视角)

本章只讲与驱动行为直接相关的硬件约束。 完整的电路方案、PCB 叠层、回流路径、焊接与生产细节见《eMCP 电路设计指南》, 本章的作用是解释「为什么硬件必须这样设计,否则驱动会怎么坏」。

2.1 引脚定义与连接框图

eMMC 的 153 个焊球里真正需要主控连接的只有 30 来个,其余是电源、地和保留球。 驱动开发者需要清楚每一根信号线的电气角色,因为信号完整性问题最终会以 -ETIMEDOUT 或 CRC 错误的形式暴露在 dmesg 里。

信号组 引脚 电气角色 驱动侧关联 设计红线
CLK 1 根 主控输出给卡的时钟,HS400 下 200 MHz mmc_set_ios 里配置输出时钟 50 欧姆阻抗;长度匹配 DAT 差分对在 5 mm 以内
CMD 1 根 双向命令与响应,串行 命令索引与 CRC 校验 必须上拉(卡侧或主控侧二选一),禁止浮空
DAT7:0 8 根 HS200 单线 / HS400 双线 DDR 总线宽度切换与采样点训练 HS400 必须按 2 组 4 根差分对布线,不要单端走 8 根
DS 1 根 卡输出数据选通(HS400EX) DT 属性 mmc-hs400-enhanced-strobe 需从 SoC 引到卡 DS 球;走线与 CLK 平行等长
RST_n 1 根 低有效复位,主控输出 reset-gpios 与 mmc_reset() 不能悬空;必须有确定的上电默认电平
CMD_DS 1 根 HS400 模式下的命令选通(部分器件) 由主机驱动,不由软件控制 按 datasheet 确认是否需要引出
VCC 多球 NAND 阵列与内核电源,典型 3.0~3.6 V 对应 regulator-supply 与 13.3 节门控 上电时序要求最严,见 2.2 节
VCCQ 多球 接口电源,跟随 SoC IO 电平 io-voltage 与 CMD11 电压切换 必须与 SoC 的 MMC IO 域电平一致,否则 CMD 通信直接失败
VDDIO 多球 部分器件的第二 IO 电源 同上 与 datasheet 的 BGA 球表逐球核对,不可想当然

SoC MMC 控制器CMD / CLK / DAT7:0DS / RST_neMMC 器件FTL + CacheNAND 阵列PMIC / LDOVCC / VCCQ / VDDIOGPIO 门控VCC_EN 开关控制低功耗CLK / CMD / DAT7:0 / DS(HS400 双 DDR 差分)VCC_EN(可由主控 GPIO 驱动)驱动视角的三条硬约束① VCCQ 必须与 SoC IO 域等电平,否则 CMD 通信在第一根命令就失败(不是软件能补的)② CMD 必须有确定上拉,浮空时上电瞬间的噪声会被误判为 R1 响应,导致 OCR 阶段假成功③ HS400 的 DAT 必须差分对布线;单端走线在 200 MHz 下几乎必然出现 CRC 错误图示为功能框图,实际 BGA 球位见器件 datasheet 的 Ball Assignment 表与 PCB 库文件

图 5 · eMMC 与 SoC 连接框图(驱动视角标注)

2.2 电源域与上电时序

eMMC 有三路电源,时序关系被 datasheet 明确规定。时序不满足的典型表现是 「冷启动偶尔不识别,热重启正常」------因为热重启时电源已经稳定,反而绕过了问题。

电源域 典型电压 作用 上电时序要求 关机时序要求
VCC 3.0 ~ 3.6 V NAND 阵列、FTL 内核、PLL 最先;稳定后至少等 300 ms 且 CLK 可提供才能发 CMD1 最后断;需等器件完成 Cache 落盘(见 10.3 节)
VCCQ 1.7 ~ 2.0 / 2.7 ~ 3.6 V 接口域(CMD / DAT / CLK) 与 VCC 无强制先后,但必须先于或同时于第一次命令有效 与 VCC 同步或先断
VDDIO 1.7 ~ 2.0 / 2.7 ~ 3.6 V 第二 IO 域(部分器件) 同 VCCQ 同 VCCQ

上电时序(VCC 领先,CMD1 延后)VCCVCC 上升后稳定不小于 300 ms 且 CLK 已提供VCCQVCCQ 上升后稳定(须早于首次命令)CMD1开始循环发 CMD1(OCR 握手)关机时序(与上电相反):先停止所有 I/O,发 Cache Flush 或 BKOPS_EN,等 R1b 完成再拉 RST_n,最后断 VCC,最后断 VCCQ。顺序错了就是「偶发丢数据」。时间刻度为示意;实际延时参数以器件 datasheet 的 Power-On / Power-Off Timing 章节为准

图 6 · eMMC 上电与关机时序波形(VCC 领先、CMD1 延后)

关机顺序错了,数据会「已经 fsync 了但还是丢」

Linux 里 sync() / fsync() 只保证数据交给了 块设备驱动,而 eMMC 驱动的完成回调可能在数据还留在器件 Cache 里时就返回了。 如果系统紧接着通过 PMIC 直接切 VCC,数据就没了。

两条解决路径:① 关闭 eMMC Cache (EXT_CSD246),写命令的 R1b 完成即代表落到 NAND, 代价是随机写性能下降;② 保留 Cache,在关机流程里显式 Flush, 并在 PMIC 断电前等待完成。第 10 章与第 13 章会详细展开。

2.3 VDD 门控与低功耗设计

eMMC 待机电流(mA 级)远高于主控的微安级,对电池供电的设备是可观的负担。 主流做法是用主控 GPIO 或 PMIC 的负载开关控制 VCC:不用时彻底断电, 用到时重新上电并重新枚举。这一节讲门控电路与软件配合的完整要求。

门控位置 控制对象 断电后可见性 重新上电步骤 适用场景
VCC 域 NAND 阵列 + 内核 卡彻底消失 ,/dev/mmcblk0 消失 上电 → 等 300 ms → 驱动重新 probe 枚举 深度睡眠、电池设备,推荐
VCCQ 域 仅接口域 卡仍在,但主机侧读不到 CID(CMD 通道断) 仅需重新握手,枚举结果可缓存 轻量省电,保留数据完整性
整机电源域 eMMC + 主控休眠域 全部掉电,等价于关机 完整冷启动 深度待机(RAM 保电)

运行态mmcblk0 已挂载,正常读写umountCache Flush + syncremove_hostGPIO 拉低断 VCC休眠态:VCC 已断,/dev/mmcblk0 不存在,驱动已 remove_host唤醒路径:GPIO 拉高给 VCC 上电,延时不小于 300 ms,驱动重新 probe,重新枚举,再 mount注意:重新枚举会消耗 200~600 ms(大容量卡读 EXT_CSD 更慢),因此「按一次唤醒」的用户体验明显变差省电收益对照:eMMC 待机约 2~5 mA,VCC 切断后仅剩 VCCQ 域约 100~300 微安(具体值查 datasheet)

图 7 · VDD 门控与 Linux 侧卸载、移除、重枚举的配合时序

门控方案的三个必做项

① 断电前必须让块设备彻底消失 :只 umount 不 remove_host, 内核里仍可能有 in-flight 请求,切电后会触发 -EIO 甚至 panic; ② 唤醒时预留足 300 ms 硬延时 :这个延时不能用 request_thread 塞进 probe 里 (会卡住设备注册),应在电源上电后由 GPIO 置位完成、中断里再触发 probe, 或干脆用 regulator 的 regulator-boot-on 语义管理; ③ 不要在有 I/O 的路径上反复门控:每次门控等于一次完整重枚举, 频繁门控的用户体验比省下的那点功耗更糟。

2.4 时钟与复位

时钟与复位是最容易被忽略但最影响稳定性的两根线。Linux 侧的表现是 「mmc0: Timeout waiting for hardware interrupt 偶发出现」。

信号 要求 满足后的 Linux 表现 违反时的症状 验证方法
CLK 源 由 SoC 提供,不能来自外部晶振分频后再分频(抖动叠加) 命令与数据传输稳定 偶发 CRC 错误与超时,频率越高越明显 示波器看抖动与占空比
CLK 最大频率 不超 datasheet 上限(HS400 为 200 MHz) DT 的 max-frequency 生效 超频后错误率飙升但不一定报错,可能静默丢数据 逐步加压测,测到错误就退档
CLK 门控 空闲时可门控,恢复后第一个命令要留额外延时 低功耗与性能兼得 唤醒后第一条命令超时,重试后正常 反复 suspend/resume 压测
RST_n 极性 低有效;上电默认应为低(器件保持复位态) 开机时序干净 上电瞬间器件状态不确定,冷启动偶发失败 上电时示波器抓 RST_n
RST_n 复位脉宽 低电平至少 1 个主机时钟周期 复位可靠 复位无效,旧状态残留 同上,抓脉宽
复位后延时 RST_n 拉高后等不小于 1 ms 再发命令 初始化成功 CMD1 被忽略,OCR 阶段超时 在 reset-gpios 后加 reset-delay-ms

2.5 信号完整性与 HS400 差分

HS400 是双数据线 DDR、200 MHz,等效每周期传 2 bit × 200 MHz × 8 根线。 这个速率下布线规则不是「能连通就行」,而是必须满足严格的等长与阻抗要求。 驱动侧能做的补救非常有限------训练只能纠正采样点,不能修正过大的 skew。

项目 HS200 要求 HS400 / HS400EX 要求 违反后果
DAT 布线 8 根单端,走线等长 2 组 4 根差分对(DAT3:0 一组、DAT7:4 一组) HS400 下 CRC 错误率超过 1%,写数据静默损坏
差分阻抗 --- 100 欧姆差分(正负 10%),需与 SoC 及 PCB 厂叠层匹配计算 眼图闭合不良,误码高
组内 skew 组内 5 mm 以内 同组 4 根 skew 在 1 mm 以内 训练窗口塌缩,无法找到稳定采样点
CLK 与 DAT 关系 CLK 与 DAT 长度差 5 mm 以内 CLK 与 DAT 差分对长度差在 2 mm 以内 读写采样点漂移,两向性能都掉
DS 走线 不适用 DS 与 CLK 平行等长(差 1 mm 以内) HS400EX 退化为 HS400,甚至训练失败
过孔与换层 尽量少 差分对内禁止中途换层;换层需对称过孔 阻抗不连续,插损增加
回流路径 有连续参考平面 差分对下方必须有完整地平面,不得跨分割 串扰,误码率随温度上升

HS400 降速的正确姿势:按顺序退,不要随机改 max-frequency

发现 HS400 误码时的退档顺序是:HS400 → HS200(同样 200 MHz 但单线、时序余量大得多) → HS 52 MHz(1/4/8 bit) 。注意不要从 HS400 直接跳到「HS200 加高频率」, HS200 的价值就是时序余量,混着用等于既没有 DDR 的速度也没有 SDR 的稳。

DT 侧改法:删掉 cap-hs400,保留 cap-hs200,把 max-frequency 设为 200000000;若 HS200 也不稳,把 max-frequency 降到 52000000 并 加 cap-high-speed。每一次退档都要重跑第 15 章的测试规范,不要只看能否识别。

2.6 必留测试点

eMMC 的问题几乎都需要波形才能定位到底。这些测试点不是为了调试方便, 而是量产阶段判断板子好坏、区分「芯片不良」与「板子不良」的唯一手段。

测试点 位置 测量方式 判据 主要能定位的问题
VCC 纹波 PMIC 输出端,近卡侧 示波器 AC 耦合,20 MHz 带宽限制 峰峰值小于 50 mV(按 datasheet) 供电不足导致的 CRC 错误与初始化失败
VCC 上电斜率 VCC 引脚 示波器单次触发 单调上升,斜率在 datasheet 范围内 上电时序违规、器件内部上电失败
VCCQ 电平 SoC IO 域侧 万用表或示波器 DC 档 与 SoC IO 域完全一致 电平不匹配,CMD 通信全错
CLK 串联电阻后引脚 示波器,探头地线要短 频率、占空比 45~55%、抖动在允许值内 时钟质量问题、源分频链路问题
CMD 串联电阻后 逻辑分析仪加示波器 上升沿单调、无过冲回卷 CMD 浮空、上拉缺失、信号完整性
DAT7:0 每根就近 逻辑分析仪(8 通道) 无过冲、上升沿单调 数据线断路或虚接、串扰
RST_n 卡侧引脚 示波器抓上电与复位过程 低有效、脉宽足够、复位时序正确 复位无效、初始电平不确定
DS 卡侧引脚 示波器(HS400EX 必测) 与 CLK 平行、延迟稳定 HS400EX 训练失败

强烈建议:CMD / CLK / DAT 各串 22~33 欧姆电阻

串联电阻的价值:① 调试时可用示波器在卡侧就近取样,不干扰信号; ② 与器件及主控的输出阻抗共同形成 RC 边沿,控制过冲; ③ HS400 训练失败时,把电阻从 22 欧姆调到 33 欧姆有时能直接救回来 (相当于加 RC 滤波、降低边沿速率的思路)。位置放在靠近信号源一侧(主控侧), 在靠近主控的 1~2 mm 内。

2.7 BOM 选型与分区容量约束

选型阶段的很多决定会锁死后面的软件方案。软件能补救的越来越少, 所以这一节要把「哪些选型决定不可逆」讲清楚。

选型维度 可选范围 对软件的不可逆影响 建议
eMMC 版本 4.41 / 4.51 / 5.0 / 5.1 5.0+ 才能 HS400;5.1 才能 HS400EX 新项目直接上 5.1,成本差极小但留足裕量
接口电压 VCCQ 1.8 V 或 3.3 V 与 SoC 绑死,改电压等于改硬件 先定 SoC 的 IO 域电平,再选器件
Boot 分区策略 Boot0 / Boot1 / 双 Boot 决定 Bootloader 放哪、能否做 A/B 选「双 Boot」器件,A/B OTA 才有可能
寿命等级 量产级 / 工业级 / 车载级 决定 Pre-EOL 口径与耐久测试目标 按实际写入量估算后再定,不要盲目选高
厂商 三星 / 铠侠 / 西部数据 / 镁光 / 长江存储等 EXT_CSD 私有行为、训练块格式、时序参数有差异 同一产品尽量锁定单一厂商加单一料号,来料切换需重测
容量 16 GB ~ 256 GB 分区规划需在产线固化 留 20% 余量给 OTA 双备份与日志

第 2 章自检

  • VCCQ 电平与 SoC IO 域已逐路核对(万用表实测,不是看 datasheet)
  • RST_n 有确定的上电默认电平,且 DT 里有 reset-delay-ms
  • CMD 上拉已确认;HS400 的 DAT 已按 2 组 4 根差分对布线,CLK 与 DAT 长度差 2 mm 以内
  • VCC 纹波实测峰峰值满足判据;上电斜率单调
  • 若用 VDD 门控:断电前 Flush 路径已实现,唤醒延时 300 ms 已落实
  • CMD / CLK / DAT / RST_n 均有就近测试点;VCC 纹波测试点已留
  • 料号已锁定,EXT_CSD 差异与训练块格式已记录到第 16 章适配表

第 3 章 · Linux MMC 子系统架构

MMC 子系统是内核里结构最规整的子系统之一:核心层(core)稳定且几乎不用改, 所有适配工作都落在 host/ 目录下的控制器驱动里。 先把这层结构看明白,才知道要改哪个文件。

3.1 整体分层总览

用户态:文件系统与应用ext4 / f2fs、mmc-utils、fio块设备层:mmcblkmmcblk.c 与 mmc_ioctl.c ------ 队列、bio、flush、discard核心层:corecore/ 目录 ------ 协议栈:命令组装、状态机解析、EXT_CSD、模式切换、电压切换控制器驱动:hosthost/ 目录 ------ 寄存器读写、时钟与中断、命令下发、卡检测硬件:SoC MMC 控制器SDHCI 或厂商控制器加 eMMC 器件core 与 host 的接口就是 struct mmc_host_ops 加 struct mmc_card 加一组 mmc_request改动原则:能在 core 层通用化就不要在 host 里打补丁;实在要打补丁,必须写清为什么不能通用

图 8 · Linux MMC 子系统分层架构(以 eMMC 为例)

层 关键文件 关键符号 读代码时关注什么
块设备 drivers/mmc/block/mmcblk.c mmcblk_submit_bio()、mmcblk_data_cmd() bio 如何转成 ADTC 命令;flush 与 discard 如何映射
块设备 ioctl drivers/mmc/block/mmc_ioctl.c mmc_blk_ioctl()、mmc_erase()、mmc_ffs() CMD38 erase 是无响应命令,靠状态查询等完成
核心 drivers/mmc/core/core.c mmc_send_init_ocr()、mmc_set_ios() OCR 循环、IO 设置如何下发给 host
核心 drivers/mmc/core/mmc_ops.c mmc_erase()、mmc_set_erase_size() 模式切换辅助函数怎么拼 CMD6 参数
核心 drivers/mmc/core/mmc_ext_csd.c mmc_read_ext_csd()、mmc_select_timing() 模式切换与卡状态判定的绝大部分逻辑在这里
核心 drivers/mmc/core/mmc_card.c mmc_init_card()、mmc_manage_cache() 整卡初始化顺序、Cache 开关时机
主机接口 include/linux/mmc/card.h mmc_host、mmc_card、mmc_ext_csd 所有可定制字段都在这里

3.2 源码目录与文件职责

内核 drivers/mmc/ 的实际目录结构如下。定位问题时先确定问题在哪一层, 能省掉大量无效的搜索。

复制代码
drivers/mmc/
├── core/                       # 协议栈:与具体控制器无关
│   ├── core.c                  # MMC 设备模型、总线、电源管理、init/ocr
│   ├── mmc_card.c              # 整卡初始化、Cache 管理、ext_csd 解析
│   ├── mmc_ops.c               # 命令构造:erase / sanitize / hp / ext_csd
│   ├── mmc_ext_csd.c           # 模式切换、电压切换、卡状态位定义
│   ├── mmc_ioctl.c             # ioctl 转命令
│   ├── debugfs.c               # /sys/kernel/debug/mmc0 全部节点
│   ├── mmc-bus.c               # 虚拟 MMC 总线(给 SDIO 用)
│   ├── sd.c / sd_io.c          # SD 协议(本项目一般不用)
│   ├── block.c / block/*       # bus 无关的块设备辅助
│   └── of.c                    # 设备树解析:mmc_of_parse()
├── block/
│   ├── mmcblk.c                # /dev/mmcblk0 的实现
│   └── mmc_ioctl.c             # ioctl 实现
├── host/                       # 控制器驱动:所有适配工作在这里
│   ├── sdhci.c                 # SDHCI 通用框架
│   ├── sdhci-pci.c             # PCIe 枚举的 SDHCI
│   ├── sdhci-pltfrm.c          # platform 总线包装(含 DT 解析)
│   ├── sdhci-of-dwcmsh.c       # Synopsys DesignWare 核心(Rockchip/ZynqMP/紫光等)
│   ├── sdhci-of-arasan.c       # Arasan 系列 IP
│   ├── mmc_dw.c / mmc_dw_mmc.c # Synopsys DesignWare 核心(旧路径)
│   ├── mmc-sunxi.c             # 全志
│   ├── mtk-sd.c                # 联发科
│   ├── sdhci-msm.c             # 高通
│   ├── sdhci-sprd.c            # 展锐
│   ├── sdhci-tec.c / -nxp.c    # T-Engine / NXP
│   ├── uniphier-sd.c           # 松下
│   ├── cavium-mmc.c            # Marvell
│   ├── meson-gx-mmc.c          # Amlogic
│   ├── samsung-sdhci.c         # 三星 Exynos
│   ├── au1xmmc.c               # MIPS
│   └── mmc-slot-gpio.c         # 卡检测与写保护 GPIO(通用)

各个 common.h(如 drivers/mmc/host/sdhci.h、 drivers/mmc/host/mmc-sdhci-core.h)里定义了控制器寄存器与 quirks, 排查「为什么这个平台的 SDHCI 行为不一样」时,第一个要看的就是它对 quirks 加了什么。

文件名里的 -of- 是什么意思

Linux 驱动命名约定:xxx-of-yyy 表示「yyy 厂商基于 OF(设备树) 实现的 xxx 控制器驱动」。所以看到 sdhci-of-dwcmsh.c 就知道它是 「用设备树描述的 Synopsys DesignWare SDHCI」,Rockchip、ZynqMP、紫光等都用这个文件。

对应地,如果某个 SoC 的支持被拆到单独文件(如 sdhci-of-arasan.c), 说明通用代码不够用,需要厂商私有时序处理。这时要去看内核 Documentation/devicetree/bindings/mmc/ 目录下的 yaml , 所有 *-of- 驱动支持的 DT 属性都在那里列出。

3.3 关键数据结构

MMC 子系统的核心是三个结构体:mmc_host(控制器)、 mmc_card(卡)、mmc_ext_csd(扩展寄存器)。 驱动代码里出现的绝大多数问题都能归结为「这三个结构体的某个字段填错了」。

struct mmc_hostops 指向 mmc_host_ops(驱动填)f_min / f_max / f_max_hwmax_freq / mode / ioscqe_on / cqe / cmdqpwrseq / mmc_pwrseqstruct mmc_cardcid / csd / rcaext_csd(内嵌结构)type / rca / statehost(反向指针)part(硬件分区链表)struct mmc_ext_csdrev / hs200 / hs400cache_size / cache_ctrlpartition_configlife_time / pre_eol_infoenhanced_strobemmc_card 结构体把 ext_csd 直接内嵌因此驱动里的访问方式是 card->ext_csd.xxx而不是 card->ext_csd_ptr->xxx三个结构体的完整定义见 include/linux/mmc/card.h;字段随内核版本增减,改代码前先看本项目内核的版本

图 9 · mmc_host / mmc_card / mmc_ext_csd 关系图

结构体 / 字段 作用 配错的典型症状
mmc_host.ops 控制器操作函数表,必须全部提供 probe 成功但任何命令都失败,dmesg 报 -EINVAL
mmc_host.f_max 软件能设的最高频率 设得超过硬件能力则偶发 CRC 错误
mmc_host.f_max_hw 硬件实际能力,不得大于 f_max 填错会掩盖板级问题,调优时无法判断瓶颈
mmc_host.max_freq 当前生效频率(由 DT 初始化) DT 的 max-frequency 生效值,日志里能看到
mmc_host.mode 当前总线模式 枚举完成后应为目标模式,HS400 时看 debugfs 的 timing
mmc_host.caps 能力位(MMC_CAP_HS200 等) 不填 MMC_CAP_HS400 则永远停在 HS200
mmc_host.caps2 第二组能力位(Cache / CQE / 3D 等) Cache 相关能力没置位则 Cache 完全不生效
mmc_host.cqe_on 命令队列是否使能 使能但 CQE 寄存器未配好则提交即挂死
mmc_card.type 卡类型(MMC_TYPE_MMC 或 MMC_TYPE_SD) eMMC 误判为 SD 会走完全不同的初始化路径
mmc_card.ext_csd.rev EXT_CSD 版本 版本判断错则用 5.1 才有的字段,切 5.0 器件时失败
mmc_card.ext_csd.hs400 5.0+ 的 HS400 能力 未读对则切 HS400 失败,表现为超时

3.4 mmc_request 与命令流

struct mmc_request 是驱动与 core 之间的「工单」: core 填好命令、参数、数据缓冲,调用 host->ops->request(), host 把它翻译成寄存器操作,命令完成后回调 req->complete 通知 core。

复制代码
struct mmc_request {
    struct mmc_card *card;          /* 关联的卡,驱动据此选 RCA 与时序 */
    struct mmc_host *host;
    struct mmc_command cmd;         /* 命令:opcode / arg / flags / resp */
    struct mmc_data  *data;         /* 数据:buf / blocks / blksz / dma */
    struct mmc_req_cookie *cookie;  /* CQE 时代的 cookie */
    struct mmc_req_complete complete;/* 完成回调(core 提供) */
    int (*error_check)(struct mmc_card *, struct mmc_request *);
};

struct mmc_command {
    u32 opcode;                     /* 命令索引,如 MMC_SEND_EXT_CSD */
    u32 arg;                        /* 32 位参数 */
    unsigned int flags;             /* MMC_RSP_PRESENT / MMC_RSP_R1B / ... */
    unsigned int *resp;             /* 响应数组(指向 host->resp) */
    unsigned int resp_len;          /* 1 / 2 / 4 / 6 个 word */
    void *data;                     /* 单块数据指针 */
    unsigned int datalen;           /* 单块数据长度 */
};

struct mmc_data {
    unsigned int blksz;             /* 块大小(字节),eMMC 常为 512 */
    unsigned int blocks;            /* 块数 */
    unsigned int flags;             /* MMC_DATA_PRELATCH / MMC_DATA_WRITE */
    void *buf;                      /* 数据缓冲 */
    unsigned int timeout_ns;        /* 数据超时(纳秒)*/
    unsigned int xfer_len;          /* 总传输长度 */
    dma_addr_t dma_addr;            /* DMA 地址(若用 DMA)*/
    unsigned int sg_len;            /* sg 表长度 */
    struct scatterlist *sg;         /* sg 表(多段传输)*/
};

关于 timeout_ns:它是纳秒不是毫秒

这是 eMMC 驱动里最容易被忽略的单位错误。 mmc_data.timeout_ns 与命令等待用的 timeout_ms 是两个不同字段、两种不同单位: 数据阶段用纳秒(由 core 按传输量算出默认值),命令等待用毫秒(由驱动在特定命令里手动设置)。

单位写错的后果很隐蔽:把 100 ms 写成 timeout_ns = 100(实际 100 ns), 表现是「大传输偶发超时,小传输正常」,非常难查。

3.5 probe 执行流程

从 module_init() 到 /dev/mmcblk0 出现,完整链路如下。 理解这条链路,才能判断「设备节点没出来」时该查哪一步。

① 模块加载或内置启动module_init 或 subsys_initcall② 平台 probe 入口platform_driver.probe():读 DT、映射寄存器③ MMC 核心 probemmc_of_parse(),分配 mmc_host,填 ops④ pwrseq 执行若 DT 有 mmc-pwrseq:上电、延时、pwrseq 的 pow_on⑤ 分配请求队列blk_alloc_queue() 并初始化 bio 结构⑥ 注册 hostmmc_add_host(),内核匹配 mmc_bus,调 host 的 probe⑦ 扫描与枚举mmc_scan() 到 mmc_rescan(),识别卡并读 EXT_CSD⑧ 注册块设备mmc_blk_alloc() 到 mmc_blk_alloc_req() 再 add_disk()断点排查判据:dmesg 里能看到「mmc0: new SD card」说明第 7 步成功;能看到 mmcblk0 说明第 8 步成功pwrseq 失败(第 4 步)是最常见的整条链断点,典型日志是 pwrseq 的 gpio 申请失败

图 10 · eMMC 驱动 probe 到 /dev/mmcblk0 出现的完整流程

「dmesg 里 mmc0 但没有 mmcblk0」的判定

这是最常见的「驱动没起来」误判。实际上有五种情况都能产生这个现象,按可能性排序:

  1. 探测到的是 SD 卡不是 eMMC :mmc0: new SD card at addr 0x1 说明 SoC 的 SD 控制器被先枚举了。查 ls /sys/bus/mmc/devices/,确认 eMMC 挂在哪个 host 上。
  2. 硬件分区节点没使能 :只有 mmcblk0 没有 mmcblk0boot0, 说明 EXT_CSD[224] HESW 里 boot 分区使能位没置上(见 2.2 与 8.4 节)。
  3. EXT_CSD 读取失败 :dmesg 会有 error -84 之类,容量识别为 0 或异常值。
  4. 块设备注册被 max_part 或 partscan 影响 : 若 EXT_CSD 容量为 0,mmc_blk_alloc_req() 可能只注册出空设备。
  5. 真的没注册 :dmesg | grep -i mmc 里若完全没有 mmc_blk_alloc 相关行,回到第 2 步检查 probe 是否被调用(-EPROBE_DEFER 被吞)。

3.6 mmcblk 块设备层

mmcblk 把「协议栈的一次 ADTC 命令」抽象成「一次块设备 bio」。 它的设计是直通式 的:mmcblk_submit_bio() 直接调 mmc_blk_data_cmd() 发命令,没有额外的中间层。因此 eMMC 的性能特征基本等于器件特征, 这也是为什么「eMMC 随机写慢」这件事在软件层几乎无法优化。

bio 特征 映射的 eMMC 命令 耗时特征(HS400 典型量级) 备注
顺序大块读(1 MB 以上) CMD18 多块读 1.5 ~ 3 GB/s 量级,接近总线上限 受 CPU 拷贝与文件系统影响明显
顺序大块写(1 MB 以上) CMD25 多块写 600 MB/s ~ 1.2 GB/s,取决于是否回写 受 Cache 策略影响极大
4K 随机读 CMD17 单块读 与顺序读接近(器件内部并行) eMMC 的随机读延迟优势明显
4K 随机写 CMD24 单块写 最差场景,可能与顺序写差一到两个数量级 FTL 需读改写,落入写放大
sync / fsync Flush 之前的等待加写命令 R1b 与 Cache 开关强相关 见第 10 章
discard(fstrim) CMD38 TRIM(无响应) 异步,需轮询状态 对 eMMC 收益取决于 FTL 实现

为什么 eMMC 上的「写缓存」会毁掉 fsync 的意义

在 eMMC 上启用 Cache 后,write() 返回只代表数据进了器件 Cache。 fsync() 会通过 mmc_blk_flush() 触发 Cache Flush 语义, 但不同厂商对「Cache Flush」的实现质量差别很大:有的只是把 Cache 标记为无效 (回写放到后台做),有的才真正同步落到 NAND。

因此本文的建议是:写关键数据的分区(数据库、配置、计费数据)所在 eMMC, 在产品定义上就要明确是否开启 Cache;不确定时先关 Cache 跑通功能, 再在测试充分后开 Cache 做性能优化。

3.7 队列、中断与 DMA

eMMC 走的是「中断 + DMA」路径。理解这条路径对定位「偶发卡死」「高负载丢数据」很关键。

环节 机制 排查手段 常见问题
命令提交 驱动写寄存器并拉 CMD 起始脉冲 逻辑分析仪抓 CMD 线 命令索引被篡改说明信号完整性或时序有问题
数据传输 DMA 搬数据(多为 SDMA / ADMA / IDMA) 看 /proc/interrupts 里 MMC 中断计数 DMA 描述符配错会导致数据错乱但无 CRC 错误
完成中断 控制器置位 INT,驱动在中断里处理 dmesg 出现 Timeout waiting for hardware interrupt 中断没来通常是时钟门控没开或共享中断没配好
超时恢复 core 层 mmc_reset() 复位 host dmesg 出现 timeout 与 trying to reset card 连续超时是硬件或时序问题,不是软件问题
CQE(可选) 硬件命令队列,控制器批量取命令 debugfs caps 里的 CMDQ 位 使能后不稳定就关掉,见 9.5 节

3.8 编码规范与 remove 路径

eMMC 控制器的 remove 路径比多数块设备复杂:要先停队列、flush 块设备、 移除 pwrseq、释放 GPIO 与 regulator,顺序错了就会在 suspend 或模块卸载时 panic。

复制代码
static int xxx_mmc_remove(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct mmc_host *mmc = dev_get_drvdata(dev);
    struct xxx_host *host = mmc_get_drvdata(mmc);

    /* 1. 先关卡:停止新的请求,等待 in-flight 完成 */
    mmc_remove_host(mmc);

    /* 2. 再拆块设备:flush 掉所有脏数据并注销 /dev/mmcblkN */
    mmc_blk_remove(mmc);
    mmc->card = NULL;

    /* 3. 关电源时序(若用了 pwrseq) */
    if (host->pwrseq)
        mmc_pwrseq_power_off(&host->pwrseq);

    /* 4. 释放资源:DMA 通道、中断、regulator、GPIO、寄存器映射 */
    dma_release_channel(host->dma);
    free_irq(host->irq, dev);
    if (host->vq)
        regulator_disable(host->vq);
    gpiod_put(host->reset_gpiod);
    devm_clk_put(dev, host->clocks[0]);

    /* 5. 最后销毁 mmc_host 本身 */
    mmc_free_host(mmc);
    dev_set_drvdata(dev, NULL);
    return 0;
}

/* 关键:remove 与 suspend 共用同一套「优雅下线」逻辑,不要重复写两份 */
static void xxx_mmc_shutdown(struct device *dev)
{
    struct mmc_host *mmc = dev_get_drvdata(dev);
    /* 无响应命令(例如在写卡时突然关电源)无法 abort,这里只能等超时 */
    if (mmc->card)
        mmc_shutdown(mmc);
}

写卡过程中直接断电,会留下「未完成命令」状态

eMMC 的写命令(CMD24 / CMD25)一旦发出,主机无法中途取消 ------ 设备树里 cd-gpios 那类「拔卡即中断」的机制在 eMMC 上不存在(不可移除器件)。所以:

① xxx_remove() 里不能假设命令能 abort,只能等超时;

② 产品必须明确定义「写操作过程中断电」的用户数据预期 ,并在文件系统上做保护 (ext4 的 journal 提交间隔、f2fs 的 checkpoint 策略);

③ 关键数据结构需要应用层双写加校验,不能只依赖文件系统。

第 4 章 · 控制器驱动:SDHCI 与厂商实现

绝大多数 SoC 的 eMMC 控制器都兼容 SDHCI 规范,Linux 用一套通用框架覆盖了它们。 本章先讲 SDHCI 通用部分,再按厂商拆解差异,最后给出「厂商私有补丁该怎么写」的范式。

4.1 SDHCI 寄存器组

SDHCI 寄存器按「能力寄存器 / 状态寄存器 / 寄存器组 / 时钟与复位 / 中断」五类划分。 驱动开发者需要记住的只有下面这十几个,其余可查 drivers/mmc/host/sdhci.h。

寄存器 偏移 作用 要点
SDHCI_CAPABILITIES 0x40 能力:支持的电压、总线宽度、模式、时钟频率 只读;它决定了这个控制器能做什么 ,与 DT 的 cap-* 属性取交集
SDHCI_CAPABILITIES_1 0x44 能力扩展:HS200 / HS400 / CQE 支持 5.0 与 5.1 器件要用 HS400 就必须看这里
SDHCI_ADMA_CAP 0x58 ADMA(增强 DMA)描述符能力 决定走 ADMA 还是 SDMA;不支持时性能降一档
SDHCI_PRESENT_STATE 0x24 当前状态:卡在位、busy、数据线状态 调试 HS400 训练失败必看:DAT 线状态不对说明时序没对齐
SDHCI_CLOCK_CONTROL 0x2C 时钟使能、频率分频、稳态与突发时钟 SDHCI_CLOCK_INT_STABLE 位要等置位
SDHCI_SOFTWARE_RESET 0x2F 命令线、数据线、全部复位 分级复位顺序:先 CMD,再 DAT,再 ALL
SDHCI_ARGUMENT 与 CMD 0x08 与 0x0C 命令参数与命令寄存器 写 ARGUMENT 后必须立刻写 CMD,否则会被清零
SDHCI_TRANSFER 0x0E 传输方式:单块或多块、读写、DMA 使能 多块加写加使能 DMA 三个位必须同时对
SDHCI_BLOCK_SIZE 与 BLOCK_COUNT 0x10 与 0x0F 块大小(12 bit,0 表示 4096)与块数 块大小寄存器写 0 表示 4096,这是 4K 扇区必须注意的地方
SDHCI_INT_STATUS 与 INT_ENABLE 0x30 与 0x34 中断标志与使能 命令完成、数据完成、busy 完成要分别使能
SDHCI_SDHC_CONTROL 0x2D 1.8 V 电压切换控制 与 CMD11 配合,控制器侧完成 IO 电平切换
SDHCI_CMD_ARGS 与 XFER_PARAM 0x28 与 0x0D 64 bit 命令参数(CMD23 与 CMD18 的起始地址) 仅 MMC_CAP_CMDQ 与部分控制器使用

SDHCI 能力寄存器的经验判读

SDHCI_CAPABILITIES 是纯硬件事实,DT 里写的 cap-* 是软件诉求, 实际生效的是两者交集。所以:

  • DT 写了 cap-hs400 但能力寄存器没置位,实际停在 HS200,日志里能看到;
  • 能力寄存器置位但 DT 没写,同样停在 HS200,因为没人申请;
  • 排查「为什么没到 HS400」时,两个地方都要看,只查 DT 是最常见的疏漏。

4.2 SDHCI 通用驱动家族

Linux 里 SDHCI 通用框架的入口是 sdhci.c,往上分三路总线:

入口文件 总线 典型平台 驱动方需提供
sdhci.c 与 sdhci.h 核心框架 全部 什么都不提供,只是能力与流程
sdhci-pci.c PCIe x86、AMD、Intel SoC PCI 枚举与电源管理
sdhci-pltfrm.c platform(含 DT) 绝大多数 ARM SoC DT 属性 :bus-width、cap-*、max-frequency、quirks
sdhci-of-dwcmsh.c platform 加 OF Rockchip、ZynqMP、紫光展锐部分 DSR(驱动强度与采样点)、频率表、quirks
sdhci-of-arasan.c platform 加 OF Arasan 系列 IP 时序参数、延迟调整

SoC 硬件 MMC 控制器SDHCI 寄存器组(能力见 0x40 与 0x44)host 目录下的厂商驱动quirks、DSR、频率表、pinctrl 申请sdhci.c 与 sdhci-pltfrm.c(通用)统一 probe、命令下发、中断处理、PMcore 目录与 block/mmcblk.c协议栈与块设备层,不随控制器变排查顺序:先看厂商驱动的 quirks 打印(dmesg 里的 quirks 列表),再往上查 core

图 11 · SDHCI 驱动家族的调用链:厂商驱动在上、通用框架在下

quirks 打印是 eMMC 调试的第一个必看点

每次 SDHCI 控制器 probe 成功,dmesg 里都会打印一行类似:

复制代码
mmc0: SDHCI controller [d4200000.mmc] at ... 200 MHz, 4-bit, ADMA
mmc0: new SDHC card at address 59b1c74d  [quirks=0x00008001: ...]

这个 quirks 列表说明 SoC 厂商主动声明了自己与规范不一致的地方 。 看到陌生的 quirk 位就去 include/linux/mmc/sdhci.h 查它的含义------ 这行日志能直接省掉大量「为什么这个平台的 eMMC 表现和别的平台不一样」的排查时间。

4.3 Rockchip dwcmsd

Rockchip 的 SDHCI 控制器在主线里是 sdhci-of-dwcmsh.c, 最需要注意的是它把 HS200 与 HS400 训练及 DSR(驱动强度、采样点)绑定在一起, 参数缺失会导致「能识别但切不到高速」。

关键 DT 属性或代码字段 作用 缺失或错误的后果
rockchip,grf-cmds-sent-delay? 调整 GRF 时序延迟 偶发识别失败,表现为开机偶尔不识别
ROCKCHIP_SDHCI_CAPABILITIES1 覆写 厂商代码里手动改能力寄存器 HS400 使能后仍停在 HS200
rockchip,sd-freq-dn-msm 等 quirk 按 SoC 版本微调频率 高频下 CRC 错误
dsn-in-t 等 DSN 延时字段 训练后的采样点延时 HS200 训练成功但 HS400 失败
mmc-hs200-postpone-clk 训练时先关时钟 训练期间偶发超时
mmc-hs400-enhanced-strobe 启用 HS400EX 的 DS 支持 DS 走线已留但仍走普通 HS400

4.4 Allwinner sunxi-mmc

全志(Allwinner)平台的 mmc-sunxi.c 是自研控制器,不走 SDHCI 框架。 它有两个特别容易踩的点:相位配置 与电源门控的时序要求。

项 说明 注意点
相位调整 mmc-sunxi 用 DTB 的相位参数调整采样点 换料或换温度后偶发错误,需要重新扫相位,见第 7 章
时钟源切换 有些型号在 100 MHz 以上需要切到 PLL 时钟 不切则高频下大幅误码
电压域门控 支持的型号有独立的 VCCQ 门控 门控后必须先恢复电压再释放时钟
驱动私有 quirk controller.h 里的 SUNXI_MMC_QUIRK_XXX 不同 SoC 型号位定义不同,换型号必须重查

4.5 联发科 MSDC

MTK 的 mtk-sd.c 是自研控制器,复杂度主要来自上电相位校准 与电压切换时序。它也是「驱动自己不发 CMD1,而是复用 SD 路径再切回 eMMC」的典型实现。

  • CMD1 由谁发 :MTK 驱动在 mtk_sd_probe_hw() 里 用 mmc_send_init_ocr() 发完成,才注册块设备;自定义 pwrseq 时要注意 不要重复发 CMD1。
  • 相位校准 :MSDC 在 HS200 与 HS400 前会跑一轮相位校准(msdc_calibrate_phase), 耗时几十毫秒;若校准失败会降档,日志里会打印校准结果。
  • 电压切换 :MTK 需要配合相应的 DT 属性;eMMC 场景关注的是 mmc-hs200-postpone-clk 与 VCCQ 电压确认。

4.6 高通 sdhci-msm

高通平台的 SDHCI 增强版。实际项目中要关注两点:

  • 电压缩放器(PMIC vdd-io) :eMMC 用 1.8 V IO 时需要 PMIC 支持 qcom,vdd-io-voltages 属性列出可切电压,驱动据此切 GPIO 控制 vreg; 配置不对会在 CMD11 时切电压失败。
  • 总线时钟来源 :部分 SoC 上 eMMC 总线时钟是独立的,需要在 sdhci-msm.c 里确认 clk_get 拿到的是哪一路。

4.7 NXP USDHC 与 i.MX USDHC

NXP 的 USDHC 支持 eMMC 4.5 及以上,实际项目常见两个坑:

  • USDHC 的总线宽度属性名与通用 DT 不同 ; 8 bit 支持需要 SoC 引脚配置正确,否则 PRESENT_STATE 的 DAT 线数读出来是 4。
  • HS400 支持的型号有限 :并非所有 i.MX 型号都有 USDHC3 与增强选通; DT 里写 cap-hs400 也可能停在 HS200。查 SoC 手册确认增强选通引脚是否存在。

4.8 ZynqMP 与其它 SoC

ZynqMP 用 sdhci-of-dwcmsh.c,有个特殊点:SD 控制器与 eMMC 控制器 共用 zynqmp_sdhci_probe_hw() 的部分逻辑 ,改代码时要区分。 其余平台(紫光展锐、兆易创新、龙、全志、瑞芯微、晶晨等)多数已有主线支持, 少数仍需厂商 out-of-tree 驱动,此时先查 drivers/mmc/host/ 下 有没有同名文件(可能已经进主线了),再考虑移植 out-of-tree。

4.9 厂商私有补丁范式

当通用框架确实不够用(私有寄存器、私有时序、私有 quirk),需要在厂商驱动里加代码。 关键是把这些代码写成「可上游化」的形式,否则每次内核升级都要重打一遍。

复制代码
/* 范式:所有平台私有逻辑用 quirk 位控制,主线逻辑保持通用 */
static const unsigned int xxx_mmc_of_quirks[] = {
    /* 该 SoC 的 SDHCI 需要在切 HS400 前额外写 DSN 延时,否则训练失败 */
    XXX_QUIRK_NEED_DSN_DELAY      | (3 << XXX_QUIRK_DSN_SHIFT),
    /* CMD6 busy 需要更长的超时时间 */
    XXX_QUIRK_LONG_SWITCH_TIMEOUT | (2000 << XXX_QUIRK_TMO_SHIFT),
    /* 硬件只在 3.3 V 下工作,不能切 1.8 V */
    XXX_QUIRK_NO_1V8_SWITCH       | 0,
};

static const struct mmc_of_quirk xxx_mmc_of_quirks[] = {
    { .compatible = "rockchip,rk3399-dwcmshc",
      .quirks = xxx_mmc_of_quirks },
    { /* 新的 SoC 复用同一套 quirks,加 compatible 即可,不要复制代码 */
      .compatible = "rockchip,rk3588-dwcmshc",
      .quirks = xxx_mmc_of_quirks },
    {}
};

/* 在 xxx_mmc_probe() 末尾按 quirk 生效,而不是在 DT 解析里硬编码 */
static void xxx_apply_quirks(struct mmc_host *mmc)
{
    struct device *dev = mmc->dev;
    int quirks = of_property_read_int(dev, "vendor,quirks", 0);

    if (quirks & XXX_QUIRK_NEED_DSN_DELAY)
        xxx_write_dsn_delay(mmc, (quirks >> XXX_QUIRK_DSN_SHIFT) & 0xff);
    if (quirks & XXX_QUIRK_LONG_SWITCH_TIMEOUT)
        mmc->max_switch_timeout = (quirks >> XXX_QUIRK_TMO_SHIFT) & 0xfff;
}

/* 写日志,方便现场定位「到底哪个 quirk 在起作用」 */
    dev_info(dev, "quirks=0x%08x: %s%s%s\n", quirks,
             (quirks & XXX_QUIRK_NEED_DSN_DELAY)     ? "NEED_DSN_DELAY "     : "",
             (quirks & XXX_QUIRK_LONG_SWITCH_TIMEOUT) ? "LONG_SWITCH_TIMEOUT " : "",
             (quirks & XXX_QUIRK_NO_1V8_SWITCH)       ? "NO_1V8_SWITCH "       : "");

判断「能不能上游化」的四条标准

① 是否只针对一个 SoC :针对单平台的逻辑用 quirk 位隔离,不写死在通用路径;

② 是否有 DT 表达 :能写成 DT 属性的就不要用 DT 里的私有 compatible 硬编码;

③ 是否有确定的判据 :加上 of_property_read_bool 判断, 这样其他平台完全不受影响;

④ 是否有日志:quirks 生效必须打日志,否则现场无法确认补丁是否起作用。

四条都满足的补丁,上游 review 通过率很高,后续内核升级基本无成本。

第 4 章自检

  • dmesg 已记录 quirks= 完整列表,陌生 quirk 位已查明含义
  • 已核对 SDHCI_CAPABILITIES 与 DT 声明的交集,确认目标模式在此范围内
  • 若用 out-of-tree 驱动:已确认主线 drivers/mmc/host/ 中没有替代实现
  • 私有逻辑已全部收敛到 quirk 位,且每个 quirk 都有日志输出
  • 已记录当前内核版本下该平台的 quirks 基线,升级内核后回归对比

第 5 章 · 设备树与分区设计细则

设备树是「一张板」与「一套软件」之间唯一的正式契约。 写错一个属性,症状可能是「不识别」「容量少一半」「掉电丢数据」这类看似无关的问题。

5.1 mmc 节点完整字段

下面是一个面向 eMMC 的完整 mmc 节点,逐项注释见下表。 实际项目里按需裁剪,不要照抄全量属性------多余的属性也会带来维护成本。

复制代码
/* ---------- SoC 侧的 MMC 控制器节点(在 SoC 的 dtsi 里) ---------- */
&mmc0 {
    status = "okay";
    pinctrl-names = "default", "sleep";
    pinctrl-0 = <&mmc0_default>;
    pinctrl-1 = <&mmc0_sleep>;
    clocks = <&sdhci_in_clk>, <&sdhci_ahb_clk>;
    clock-names = "in_clk", "ahb_clk";
    clock-frequency = <200000000>;
    max-frequency = <200000000>;       /* 驱动用 mmc_of_parse 转成 host->max_freq */
    bus-width = <8>;                    /* eMMC 常见 8 bit;1 与 4 也可 */
    cd-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>;   /* 卡在位检测;eMMC 可省略 */
    /* 复位若由 GPIO 承担则打开;由控制器自身复位则不需要 */
    reset-gpios = <&gpio1 11 GPIO_ACTIVE_LOW>;
    reset-delay-ms = <20>;
    /* 能力声明:与硬件能力寄存器取交集 */
    cap-mmc-highspeed;                     /* 52 MHz */
    cap-hs200;                             /* 200 MHz 单线 */
    cap-hs400;                             /* 200 MHz 双线 DDR */
    cap-hs400-enhanced-strobe;             /* 5.1 的 DS 增强选通 */
    non-removable;                         /* eMMC 必备:没有插拔动作 */
    mmc-hs200-postpone-clk;                /* 训练时先关时钟(部分控制器需要)*/
    mmc-hs400-postpone-clk;
    vmmc-supply = <&vmmc_supply>;       /* 3.3 V 主电源 */
};

/* ---------- 板级:eMMC 设备节点 ---------- */
&mmc0 {
    status = "okay";
    cap-sd-highspeed;                      /* 兼容 SD 卡时需要 */
    sd-max-frequency = <200000000>;
    mmc-pwrseq = <&mmc_pwrseq>;
    keep-power-in-suspend;                 /* suspend 时不掉 VCC,见 13.2 节 */
    /delete-property/ cd-gpios;            /* eMMC 不可移除,删掉卡检测 */
};

/* ---------- 上电时序:电源时序 pwrseq ---------- */
mmc_pwrseq: mmc-pwrseq {
    compatible = "mmc-pwrseq-simple";
    reset-gpios = <&gpio3 5 GPIO_ACTIVE_LOW>,      /* PMIC 的 VCC_EN,排第一位 */
                 <&gpio1 11 GPIO_ACTIVE_LOW>;     /* eMMC RST_n */
    post-power-on-delay-ms = <200>;      /* reset 拉高后的延时 */
};

/* ---------- 分区:GPT 固定分区(子节点) ---------- */
&mmc0 {
    partitions {
        compatible = "fixed-partitions";
        #size-cells = <1>;
        #address-cells = <1>;
        boot partition@0 {
            label = "boot";
            reg = <0x0 0x4000000>;               /* 64 MB */
        };
        rootfs partition@4000000 {
            label = "rootfs";
            reg = <0x4000000 0x20000000>;         /* 512 MB */
        };
        data partition@24000000 {
            label = "data";
            reg = <0x24000000 0x0>;               /* 余量 */
        };
    };
};
属性 作用域 必填 作用与填写要点
status = "okay" SoC 与板级 是 板级必须覆盖 SoC 的 disabled;两处都写时板级优先
non-removable 板级 eMMC 必备 告诉 core 不做插拔检测与卡拔出处理
bus-width SoC 是 1 / 4 / 8;写错时 PRESENT_STATE 读出的 DAT 线数不符,表现为读写错误
max-frequency SoC 是 单位 Hz;不要超过 clock-frequency 与能力寄存器上限
cap-mmc-highspeed SoC 建议 52 MHz 模式;即使只用 HS400 也建议打开,作为降级路径
cap-hs200 / cap-hs400 SoC HS200 与 HS400 必备 与能力寄存器取交集;不写则到不了该模式
cap-hs400-enhanced-strobe SoC HS400EX 必备 需要硬件真的接了 DS;接错会导致训练失败
mmc-hs200-postpone-clk SoC 视平台 训练时先关时钟,规避部分控制器训练期间的竞争
cd-gpios SoC SD 必备,eMMC 省略 eMMC 保留会让部分平台反复尝试卡切换,浪费启动时间
reset-gpios 与 reset-delay-ms SoC 视设计 复位脉宽与复位后延时;reset-delay-ms 缺失是冷启动偶发失败的常见原因
pinctrl-0 / pinctrl-1 SoC 与板级 是 default 跑高频;sleep 用于 suspend 时把 IO 拉回安全电平
vmmc-supply SoC 建议 对应 3.3 V 主电源;配合 regulator 门控方案
mmc-pwrseq 板级 门控方案必备 见 5.6 节
keep-power-in-suspend 板级 视设计 suspend 时不给 VCC 断电;门控方案不要设这个,否则与门控逻辑冲突
partitions 子节点 板级 可选 见 5.2 节;不写则设备上没有分区节点

5.2 分区方案:GPT / MBR / eMMC 硬件分区

eMMC 有两层分区概念,混用是最常见的规划错误:

  1. 硬件分区 (Boot0 / Boot1 / RPMB):器件内部固化,绕过分区表, 由 EXT_CSD[224] 使能与否决定是否上报;
  2. 软件分区 (用户区内的 GPT 或 MBR 分区):落在 /dev/mmcblk0 上, 由分区表描述。
方案 优点 缺点 适用场景
GPT 加 PARTUUID 引用 支持大于 2 TB;分区 UUID 稳定,设备顺序变化不影响 需要 parted 或 sgdisk;部分老 bootloader 不认 推荐。所有现代量产项目
MBR 加 /dev/mmcblk0pN 引用 兼容性最好,U-Boot 老版本也认 无 UUID,多卡场景下 mmc0 与 mmc1 顺序会错 只有一个 eMMC 且 bootloader 很老的项目
GPT 加 /dev/mmcblk0pN 容量大且引用直观 多卡场景高危:SD 卡插上后枚举成 mmc1,eMMC 变 mmc0,rootfs 挂错 不推荐
单分区(无分区表) 简单,整个卡当 rootfs 无法 OTA、无法 A/B、无独立数据区 仅数据采集类终端
eMMC 硬件分区装 Bootloader 独立于用户区,用户区损坏不影响启动 容量固定(各约 4 MB),需 mmc bootpart 写入 推荐:Boot0 与 Boot1 做 A/B

「/dev/mmcblk0p2 挂不上」的根因几乎都是多卡顺序

Linux 按探测顺序分配 host 编号:先探测到的拿 mmc0 。 如果板子上有 SD 卡槽,插入 SD 卡时 eMMC 可能是 mmc1、SD 卡是 mmc0, 于是 root=/dev/mmcblk0p2 就指向了错误的设备。

两种正确解法:

① 设备树用 PARTUUID 引用 :root=PARTUUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, U-Boot 会把它解析成正确的 /dev/mmcblkNpM;

② 在 U-Boot 里固定 host 编号 ,并用 bootpart 显式指定分区号。

本文推荐第一种,唯一需要注意的是重刷分区表时 PARTUUID 会变, 产线流程必须把 PARTUUID 作为唯一标识记录在案。

5.3 U-Boot 与内核分区一致性

U-Boot 和内核共享同一份设备树(通常 U-Boot 拿到的是编译期传入的 dtb)。 一旦分区表定义不一致,典型症状是「U-Boot 能启动、内核挂不上 rootfs」。

一致性项 U-Boot 侧 内核侧 不一致的表现
分区表 读同一份 dtb 的 partitions 子节点 同一份 U-Boot printenv 显示分区大小对但 bootcmd 读不出来
bootargs 里的 root U-Boot 拼接并传给内核 内核按此挂载 内核 panic:VFS 无法挂载 root fs
存储设备号 mmc_set_devno 或 mmc dev 按探测顺序 U-Boot 从 mmc1 读,内核当 mmc0 用
硬件分区使能 U-Boot 阶段已用 mmc bootpart 0 1 写入 内核读 EXT_CSD[224] U-Boot 能从 boot0 启动,内核看不到 mmcblk0boot0
Cache 模式 U-Boot 传给内核的 cache mode 内核据此初始化 性能异常或掉电丢数据,见第 10 章

5.4 overlay 与多板型

多板型项目(同一 SoC 不同 PCB 载板)建议用编译期按板型选 dts, 而不是运行时 overlay:eMMC 涉及 pinctrl、regulator、reset 引脚, overlay 覆盖容易出现「属性没被完全替换」的隐性 bug。

做法 适用 风险 推荐度
按板型编译不同 dts 板型在编译期已知 构建产物变多 推荐
单 dts 加多板型节点,用 dtsi include 板型差异小 属性覆盖需用 /delete-property/,易漏 可用
运行时 overlay 板型运行时才知道(如外接模组) eMMC 相关属性覆盖后时序类属性可能不生效 不推荐用于 eMMC
/delete-node/ 加重新定义 需要「取消继承」的属性 漏写会导致属性叠加(如 SoC 定义 4 bit,板级又写 8 bit) 需谨慎

SoC 层:mmc 控制器节点status / clocks / clock-namespinctrl-0 / pinctrl-1max-frequency / bus-widthcap-* 能力声明reset-gpios / reset-delay-msvmmc-supply板级:覆盖与补充/delete-property/ 取消不 Needed 属性mmc-pwrseq 引用上电时序keep-power-in-suspendpartitions 子节点(GPT)boot / rootfs / data#address-cells 与 #size-cells属性合并的三条规则① 同名属性:板级覆盖 SoC(后定义生效)② 不同名属性:同时生效(cap-sd-highspeed 与 cap-mmc-highspeed 会共存)③ 需要「取消继承」时必须显式 /delete-property/ 或 /delete-node/,否则会残留叠加验证手段:dtc -I dtb -O dts 后直接看最终值,或启动后读 /sys/firmware/devicetree/base/

图 12 · eMMC 设备树节点结构与属性合并规则

属性叠加是 DT 最隐蔽的 bug 类型

DT 的属性定义不是覆盖而是合并 : SoC 的 dtsi 里写了 bus-width = <4>,板级 dts 里写 bus-width = <8>, 最终生效的是 8 (同名词后定义覆盖),这是对的。 但如果 SoC 里写的是 cap-sd-highspeed、板级又写了 cap-mmc-highspeed, 两者是不同属性名,会同时生效,导致 core 认为这张卡同时具备 SD 与 MMC 能力, 在某些版本上会走错初始化路径。

排查手段:dtc -I dts -O dtb 编译后 dtc -I dtb -O dts dtb.dtb | grep -A5 mmc@ 直接看最终值, 比反复读源码可靠得多。启动后也可以 cat /sys/firmware/devicetree/base/.../bus-width 复核。

5.5 pinctrl / regulator / reset / cd-gpios

这四类是「硬件与驱动握手」的部分,每个都有明确的时序要求。

资源 DT 写法要点 时序要求 配错的表现
pinctrl pinctrl-0 跑高频、pinctrl-1 为 sleep;两组都要配,只配一组会在 suspend 时 IO 悬空 default 组在 probe 时应用;sleep 组在 suspend 时应用 suspend 唤醒后首次访问失败(IO 电平不对)
regulator vmmc-supply 指向固定 regulator;regulator-boot-on 让 suspend 保持供电 必须先有稳定供电才 probe;门控方案要配合 regulator-allow-set-load 电压掉到 0(门控后没恢复),或 regulator_is_enabled 报错
reset GPIO reset-gpios 加 reset-delay-ms;pwrseq 的 post-power-on-delay-ms 也管延时 RST_n 拉低保持不小于 1 个时钟周期;拉高后不小于 1 ms 再发 CMD 冷启动偶发不识别(热重启正常)
cd-gpios eMMC 建议删除;SD 卡必须配对且极性要与卡座一致 探测阶段先于 CMD1 eMMC 上留着会让驱动反复尝试切换,浪费启动时间
WP(写保护)GPIO wp-gpios(部分控制器支持);eMMC 写保护一般由厂商硬件决定 无 写入无报错但不落盘

5.6 mmc-pwrseq 与上电延时

当 eMMC 的上电需要外部协助(多个 GPIO 依次拉起、电源需要先升到某个电压档)时, 就要用 mmc-pwrseq。它是内核提供的标准机制(drivers/mmc/core/mmc-pwrseq.c)。

复制代码
/* 标准写法:所有引脚一起拉起,post-power-on-delay-ms 统一延时 */
mmc_pwrseq: mmc-pwrseq {
    compatible = "mmc-pwrseq-simple";
    reset-gpios = <&gpio1 11 GPIO_ACTIVE_LOW>,      /* eMMC RST_n */
                 <&gpio3 5  GPIO_ACTIVE_LOW>;      /* PMIC 的 VCC_EN */
    post-power-on-delay-ms = <200>;
};

/* 带 GPIO 范围控制的写法(部分 SoC 需要 pinctrl 切换到 3.3 V 档)*/
mmc_pwrseq_x: mmc-pwrseq {
    compatible = "mmc-pwrseq-simple";
    reset-gpios = <&gpio1 11 GPIO_ACTIVE_LOW>;
    post-power-on-delay-ms = <300>;
    pinctrl-0 = <&mmc_vddq_3v3>;
    pinctrl-1 = <&mmc_vddq_3v3>;
};

硬件分区(器件内部固化,绕过分区表)Boot0 约 4 MB Boot1 约 4 MB RPMB 约 128 KB用户区(/dev/mmcblk0,GPT 或 MBR 分区表管理)p1 boot 64 MB p2 rootfs 512 MB p3 data 余量 p4 userdataU-Boot 引用方式load mmc 0:1 ${loadaddr} Imagemmc bootpart 0 1bootcmd 用 PARTUUID 指定 root高危写法(多卡场景)root=/dev/mmcblk0p2插 SD 卡后 mmc0 变成 SD 卡结果:挂错设备或直接起不来分区表操作全部通过 sgdisk 或 parted 完成,产线必须记录 PARTUUID 作为唯一标识重刷分区表会生成新的 PARTUUID,U-Boot 环境与设备树需同步更新

图 13 · eMMC 分区规划与 U-Boot 引用关系

pwrseq 配错是「开机偶发不识别」的头号原因

  • 延时太短 :post-power-on-delay-ms 只写 10 ms, 而 PMIC 的 3.3 V 轨实际建立需要 100 ms 以上,器件在 VCC 未稳时收到 CMD1,OCR 阶段失败。 建议不小于 200 ms,冷启动问题优先怀疑这里。
  • 引脚顺序没管 :reset-gpios 是数组, 内核按数组顺序依次拉起。如果 VCC_EN 必须在 RST_n 之前,就把它放第一位。
  • 与 SoC 节点的 reset-gpios 冲突 :同一个 GPIO 在 mmc 节点和 pwrseq 里都被申请,第二个会失败(-EBUSY)。 二选一,不要都写。
  • procfs 里看不到 pwrseq :成功时 /sys/kernel/debug/mmc0/pwrseq 有内容, 没有就是 pwrseq 根本没被执行,去 dmesg 里找 pwrseq 相关的错误。

本文档为 《原理方案设计》 系列文档之一,与《eMCP_电路设计指南》《eMMC 软件设计规范》 《Linux_SPI_NAND_驱动开发指南》配套使用。文中涉及的寄存器偏移、命令常量与内核接口 以 Linux mainline 为准;厂商私有部分请以对应 BSP 的手册与源码为准。 代码片段仅用于说明设计意图,实际移植时需按目标内核版本调整 API。

LINUX-EMMC-DRV-001 · V1.0 · 嵌入式文件系统系列 · 共 20 章 + 附录 A~F · 27 图

后续章节请下载「芯参谋」查看

......(篇幅原因, 还有22章· 详见芯参谋→方案设计)

打开路径:🔧解决方案&应用市场分析 -> 原理方案设计 -> 嵌入式驱动&系统开发 -> Linux_eMMC_驱动开发指南

📥 下载芯参谋,免费查看完整报告

相关报告 · 1

相关报告 · 2

相关推荐
SEO-狼术1 小时前
Aspose.Email for .NET 26 Crack
运维·服务器
vortex51 小时前
Windows Server IIS 运维配置指南:架构、身份、安全与排障
运维·windows·架构
无足鸟ICT10 小时前
【RHCA+】vmstat命令(监控系统资源)
linux·服务器
闪亮的路灯10 小时前
Linux 安装mqtt服务mosquitto
linux·运维·服务器
HideF10 小时前
nmcli
linux·服务器·网络
qetfw11 小时前
UOS DHCP Relay:跨网段分配与 IP、DNS、网关验证
linux·服务器·网络·tcp/ip·uos
szxinmai主板定制专家13 小时前
X86 工控机 vs RK3588 异构主控:AMR 控制器 BOM、散热、长期运维对比
运维·stm32·单片机·嵌入式硬件·fpga开发·am62x
叼烟扛炮13 小时前
Linux 学习笔记(四十一):TCP 并发演进——从单进程到多线程服务器
linux·运维·服务器·网络·笔记·学习·tcp/ip
ynchyong13 小时前
Linux 有两个 “版本“
linux·发行版·内核版本