Linux eMMC 驱动开发指南
编制日期 2026-10-05
面向嵌入式 Linux 平台的 eMMC 存储器件 驱动开发、集成、调试与量产指南 —— 覆盖 SDHCI / 厂商控制器、MMC 子系统、设备树、速度模式切换、块设备 I/O 路径、Cache 一致性 与 RPMB 全链路。
| 文档编号 | LINUX-EMMC-DRV-001 |
|---|---|
| 版本 | V1.0 |
| 适用器件 | eMMC 4.41 / 4.51 / 5.0 / 5.1(153-ball BGA,含 eMCP / uMCP 合封形态) |
| 适用内核 | Linux 4.19 / 5.4 / 5.10 / 5.15 / 6.1 / 6.6(主线 MMC 子系统) |
| 适用平台 | ARM32 / ARM64 / RISC-V 嵌入式 SoC,SDHCI 兼容控制器与厂商私有 MMC 控制器 |
| 配套文档 | 《eMCP_电路设计指南》《eMCP_软件设计规范》《Linux_SPI_NAND_驱动开发指南》 |
| 读者对象 | 嵌入式 Linux BSP / 驱动工程师、存储产品经理、硬件与 PCB 工程师、产线与测试工程师 |
0.1 编写目的与读者对象
本文要解决的是「一颗 eMMC 焊到板子上之后,从硬件上电、内核枚举、模式切换、 块设备挂载、一直到产线烧录与 OTA 的整条链路,怎么做才不会踩坑」。
很多团队在 eMMC 上的问题并不是「不会写驱动」,而是分散在多个环节的细节没有对齐: 硬件上电顺序没按规范做、EXT_CSD 没读全就切了 HS400、Cache 开着但掉电策略没配套、 U-Boot 与内核分区表不一致、产线第一次烧录用的工具跟量产工具不是同一个…… 这些坑每一个单独查资料都能解决,但串起来看才能真正省时间。
- 驱动开发者:需要理解 MMC 子系统的分层、控制器驱动怎么接、模式切换怎么调、设备树怎么写。
- BSP / 内核移植工程师:需要 config 清单、DTS 样例、U-Boot 配套、与启动链路的衔接。
- 硬件 / PCB 工程师:需要知道驱动行为对硬件的约束(电源时序、门控、复位、差分走线、测试点)。
- 产线与测试工程师:需要首件烧录 SOP、掉电测试方法、寿命与健康度采集手段。
- 存储产品经理:需要容量/分区规划依据、寿命指标口径、模式与性能的取舍关系。
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_版本号 Rev 1.4 >= 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、缓存、调度)+ 主机接口」三层叠在一起的东西。 理解这三层的分工,才能明白哪些行为是主机能控制的、哪些只能「请求并等待」。
这张图解释了三个常见现象的成因:
- 为什么 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 改写(只有「是否使能」可配)。理解这一点,才能解释分区表里那些「凭空多出来」的设备节点。
| 设备节点 | 所属分区 | 可否被分区表引用 | 典型用途 |
|---|---|---|---|
| /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 的训练结果。
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 |
场景:切 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() 就是这个循环。
① 复位脉冲太短: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 校验 | 必须上拉(卡侧或主控侧二选一),禁止浮空 |
| DAT[7: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 球表逐球核对,不可想当然 |
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 |
Linux 里 sync() / fsync() 只保证数据交给了
块设备驱动,而 eMMC 驱动的完成回调可能在数据还留在器件 Cache 里时就返回了。
如果系统紧接着通过 PMIC 直接切 VCC,数据就没了。
两条解决路径:① 关闭 eMMC Cache(EXT_CSD[246]),写命令的 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 保电) |
① 断电前必须让块设备彻底消失:只 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 根差分对(DAT[3:0] 一组、DAT[7: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 误码时的退档顺序是: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 浮空、上拉缺失、信号完整性 |
| DAT[7:0] | 每根就近 | 逻辑分析仪(8 通道) | 无过冲、上升沿单调 | 数据线断路或虚接、串扰 |
| RST_n | 卡侧引脚 | 示波器抓上电与复位过程 | 低有效、脉宽足够、复位时序正确 | 复位无效、初始电平不确定 |
| DS | 卡侧引脚 | 示波器(HS400EX 必测) | 与 CLK 平行、延迟稳定 | HS400EX 训练失败 |
串联电阻的价值:① 调试时可用示波器在卡侧就近取样,不干扰信号; ② 与器件及主控的输出阻抗共同形成 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 整体分层总览
| 层 | 关键文件 | 关键符号 | 读代码时关注什么 |
|---|---|---|---|
| 块设备 | 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 加了什么。
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(扩展寄存器)。
驱动代码里出现的绝大多数问题都能归结为「这三个结构体的某个字段填错了」。
| 结构体 / 字段 | 作用 | 配错的典型症状 |
|---|---|---|
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 表(多段传输)*/
};这是 eMMC 驱动里最容易被忽略的单位错误。
mmc_data.timeout_ns 与命令等待用的 timeout_ms 是两个不同字段、两种不同单位:
数据阶段用纳秒(由 core 按传输量算出默认值),命令等待用毫秒(由驱动在特定命令里手动设置)。
单位写错的后果很隐蔽:把 100 ms 写成 timeout_ns = 100(实际 100 ns),
表现是「大传输偶发超时,小传输正常」,非常难查。
3.5 probe 执行流程
从 module_init() 到 /dev/mmcblk0 出现,完整链路如下。
理解这条链路,才能判断「设备节点没出来」时该查哪一步。
这是最常见的「驱动没起来」误判。实际上有五种情况都能产生这个现象,按可能性排序:
- 探测到的是 SD 卡不是 eMMC:
mmc0: new SD card at addr 0x1说明 SoC 的 SD 控制器被先枚举了。查ls /sys/bus/mmc/devices/,确认 eMMC 挂在哪个 host 上。 - 硬件分区节点没使能:只有
mmcblk0没有mmcblk0boot0, 说明EXT_CSD[224] HESW里 boot 分区使能位没置上(见 2.2 与 8.4 节)。 - EXT_CSD 读取失败:dmesg 会有
error -84之类,容量识别为 0 或异常值。 - 块设备注册被
max_part或partscan影响: 若 EXT_CSD 容量为 0,mmc_blk_alloc_req()可能只注册出空设备。 - 真的没注册:
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 上启用 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_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 | 时序参数、延迟调整 |
每次 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
全志平台的 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基线,升级内核后回归对比
本文档为 《原理方案设计》 系列文档之一,与《eMCP_电路设计指南》《eMMC 软件设计规范》
《Linux_SPI_NAND_驱动开发指南》配套使用。文中涉及的寄存器偏移、命令常量与内核接口
以 Linux mainline 为准;厂商私有部分请以对应 BSP 的手册与源码为准。
代码片段仅用于说明设计意图,实际移植时需按目标内核版本调整 API。
LINUX-EMMC-DRV-001 · V1.0 · 嵌入式文件系统系列 · 共 20 章 + 附录 A~F · 27 图