Linux eMMC子系统深度解析:从硬件协议到内核实现
1 eMMC基础概念与工作原理
1.1 eMMC物理结构与硬件接口
eMMC(Embedded Multi Media Card)是一种嵌入式存储解决方案,它将NAND Flash存储芯片、闪存控制器和标准MMC接口封装在同一块芯片中。这种高度集成的设计极大地简化了手机、平板电脑、嵌入式设备等产品的存储系统设计。从物理结构上看,eMMC可以划分为三个主要部分:NAND Flash存储阵列、闪存控制器和MMC接口控制器。
NAND Flash存储阵列 是实际数据存储的介质,由多个Block组成,每个Block又包含多个Page。这种结构特点使得NAND Flash适合大容量数据存储,但也带来了需要擦写均衡、坏块管理等挑战。闪存控制器 负责处理这些底层管理任务,包括ECC纠错、磨损均衡、坏块管理、垃圾回收等,从而让主机系统可以像访问普通存储设备一样访问eMMC,无需关心NAND Flash的复杂特性。MMC接口控制器则负责与主机通信,解析和执行主机发送的命令,并在内部闪存控制器和主机之间传输数据。
eMMC与主机的硬件连接主要通过以下信号线实现:
- CLK:时钟信号线,由主机提供,用于同步数据传输。不同的总线模式下时钟频率差异很大,从基础模式的0-26MHz到HS400模式的0-200MHz不等。
- CMD:双向命令/响应信号线,用于主机发送命令和设备返回响应。所有通信都由主机发起,设备通过CMD线回应。
- DAT0-DAT7:双向数据总线,用于实际数据传输。eMMC支持1-bit、4-bit和8-bit数据总线宽度,上电默认使用1-bit模式,初始化后可以配置为更宽的总线以提高传输效率。
- RST:复位信号线,用于硬件复位eMMC设备。
1.2 eMMC总线协议与通信原理
eMMC总线通信遵循严格的主从模式,主机始终发起所有通信,设备仅在响应主机命令时才会在CMD或DAT线上传输数据。每个通信事务都由命令(Command) 、响应(Response) 和可选的数据块(Data Block) 组成。
命令格式是所有eMMC通信的基础。每个命令都是固定的48位长度,包含以下字段:
- 起始位(1位):固定为0
- 传输位(1位):指示传输方向,主机到设备为1
- 命令索引(6位):表示命令类型,范围0-63
- 命令参数(32位):与具体命令相关的参数
- CRC7校验(7位):对前面40位数据的校验码
- 结束位(1位):固定为1
响应是设备对主机命令的回复,有6种不同的响应类型(R1、R1b、R2、R3、R4、R5),长度和内容各不相同。例如,R1响应为48位,包含命令索引和设备状态;R2响应为136位,用于传输CID或CSD寄存器的内容。
在实际通信中,eMMC支持单块读取 和多块读取。单块读取由CMD17命令发起,设备在接收到命令后,先发送一个响应,然后传输一个数据块,最后是CRC校验码。多块读取由CMD18命令发起,设备会连续传输多个数据块,直到主机发送CMD12停止命令。数据写入过程类似,只是数据传输方向相反。
表:eMMC主要命令类型及其功能
| 命令类型 | 命令索引 | 功能描述 | 响应类型 |
|---|---|---|---|
| 基本命令 | CMD0 | 设备复位 | 无 |
| 基本命令 | CMD1 | 发送操作条件 | R3 |
| 设备识别 | CMD2 | 获取CID号 | R2 |
| 设备识别 | CMD3 | 设置相对地址 | R1 |
| 数据读 | CMD17 | 读取单块数据 | R1 |
| 数据读 | CMD18 | 读取多块数据 | R1 |
| 数据写 | CMD24 | 写入单块数据 | R1 |
| 数据写 | CMD25 | 写入多块数据 | R1 |
| 应用命令 | CMD55 | 标识下一个为应用命令 | R1 |
| 应用命令 | ACMD41 | 发送操作条件 | R3 |
1.3 eMMC总线模式与性能演进
eMMC协议经历了多个版本的演进,每个版本都引入了新的总线模式以提高性能。从最初的兼容MMC模式(最高26MHz时钟)到HS400模式(200MHz DDR,等效400MHz),eMMC的数据传输速率得到了显著提升。
HS400模式是eMMC 5.0协议中引入的最高性能模式,它结合了8位数据总线宽度、双倍数据率(DDR)技术和200MHz时钟频率,理论传输速率可达400MB/s。为了实现这一高性能,HS400模式要求使用1.8V I/O电压,并且需要精确的时序控制和信号完整性保证。
eMMC设备在启动后通常处于最低性能的兼容模式,主机需要通过一系列初始化步骤来配置设备,逐步切换到更高性能的总线模式。这个过程包括:识别设备、配置总线宽度、提高时钟频率、切换到高速时序模式等。主机通过读取设备的CSD(Card Specific Data)、EXT_CSD等寄存器来了解设备支持的能力,然后通过相应的切换命令来配置设备的工作参数。
2 Linux eMMC子系统架构分析
2.1 MMC子系统三层架构概述
Linux内核中的MMC子系统采用典型的三层架构设计,清晰地将功能划分为主机层(Host Layer) 、核心层(Core Layer) 和块设备层(Block Device Layer)。这种分层设计实现了硬件相关代码与硬件无关代码的分离,提高了代码的可维护性和可移植性。
主机层 是与具体硬件平台相关的驱动层,负责管理与eMMC控制器相关的硬件资源,如时钟、GPIO、DMA等。在Linux内核中,不同的SoC厂商需要实现自己的主机驱动,例如Rockchip的dw_mmc_rockchip、Samsung的sdhci-s3c等。主机层通过struct mmc_host_ops结构体向核心层提供一组回调函数,这些函数包括硬件初始化、请求处理、电源管理等操作。
核心层 是MMC子系统的核心部分,它实现了eMMC协议栈、设备检测初始化、电源管理、请求队列调度等通用功能。核心层通过struct mmc_host结构体管理主机控制器信息,通过struct mmc_card结构体管理eMMC设备信息。核心层作为主机层和块设备层之间的桥梁,负责将上层的块设备请求转换为eMMC命令序列,并通过主机层与硬件交互。
块设备层 将eMMC设备呈现为标准的Linux块设备(如/dev/mmcblk0),使得上层文件系统(如ext4、FAT32)和应用程序可以通过标准的块设备接口访问eMMC存储。这一层实现了Linux块设备驱动程序的框架,包括请求队列管理、I/O调度、缓冲区和直接I/O等功能。
2.2 各层组件功能与交互机制
主机层 负责最底层的硬件交互,其核心是实现struct mmc_host_ops中定义的回调函数。主要函数包括:
request:处理MMC请求的主要函数,将MMC命令和数据传输转换为硬件操作set_ios:配置总线的时序参数,如时钟频率、总线宽度、电源模式等get_cd和get_ro:检测设备的插入状态和写保护状态enable_sdio_irq:启用或禁用SDIO中断(对于SDIO设备)
当系统启动时,主机驱动通过mmc_alloc_host和mmc_add_host函数向内核注册主机控制器,并准备好处理来自核心层的请求。
核心层的功能最为复杂,它主要包括以下几个模块:
-
设备生命周期管理 :负责eMMC设备的检测、初始化、识别和移除。当设备插入或系统启动时,核心层通过
mmc_rescan函数扫描总线,发现设备后执行初始化序列,包括获取CID、CSD、EXT_CSD等寄存器信息,分配相对地址(RCA),配置设备参数等。 -
协议命令处理 :将上层请求转换为符合eMMC协议的命令序列。核心层实现了各种eMMC标准命令的封装函数,如
mmc_go_idle(发送CMD0)、mmc_send_op_cond(发送CMD1)、mmc_all_send_cid(发送CMD2)等。 -
电源管理:管理eMMC设备和主机的电源状态,包括电源关闭、睡眠、唤醒等状态转换。eMMC协议定义了多种电源状态以降低功耗,核心层根据系统需求在适当的时候切换设备电源状态。
-
EXT_CSD寄存器管理 :EXT_CSD是eMMC设备的一个重要寄存器集合,包含设备特性、配置参数和性能相关的信息。核心层通过
mmc_get_ext_csd函数读取这些信息,并通过mmc_switch函数修改设备配置。
块设备层 将每个eMMC设备的分区呈现为/dev/mmcblk0p1、/dev/mmcblk0p2等块设备节点。它通过struct mmc_blk_data结构体管理块设备信息,并通过struct gendisk结构体与Linux块设备子系统集成。当文件系统发起I/O请求时,块设备层将请求封装为struct mmc_request,并通过核心层下发到主机层执行。
2.3 系统初始化与设备探测过程
eMMC子系统的初始化从主机驱动开始,当内核启动时,主机驱动通过模块初始化函数(如module_platform_driver)注册自己。在探测(probe)函数中,主机驱动执行以下操作:
- 分配
struct mmc_host结构体,并设置主机能力(caps)和操作函数集(ops) - 初始化硬件,包括时钟、中断、DMA、GPIO等资源
- 向核心层注册主机控制器(
mmc_add_host) - 启动扫描工作队列,检测连接的设备
设备探测过程由mmc_rescan函数实现,该函数在独立的线程中运行,避免阻塞系统启动。主要步骤包括:
- 检测设备是否存在(通过CD引脚或发送CMD1)
- 使能设备并发送CMD0进行复位
- 发送CMD1查询设备操作条件,获取OCR寄存器
- 发送CMD2获取设备的唯一标识符(CID)
- 发送CMD3设置设备相对地址(RCA)
- 发送CMD9获取设备特定数据(CSD)
- 发送CMD7选择设备,使其进入传输状态
- 读取EXT_CSD寄存器,获取扩展设备信息
- 配置设备参数,如总线宽度、高速模式等
- 注册块设备,使设备对上层可用
表:eMMC子系统关键数据结构与功能
| 数据结构 | 所在层级 | 主要功能 |
|---|---|---|
struct mmc_host |
核心层 | 描述MMC主机控制器,包含caps、ops、f_min、f_max等字段 |
struct mmc_card |
核心层 | 描述MMC设备,包含cid、csd、ext_csd、rca等设备信息 |
struct mmc_driver |
主机层 | MMC主机驱动,包含probe、remove、shutdown等函数 |
struct mmc_host_ops |
主机层 | 主机操作函数集,包含request、set_ios、get_cd等回调函数 |
struct mmc_request |
核心层 | 封装MMC请求,包含cmd、data、stop等子请求 |
struct mmc_command |
核心层 | 描述MMC命令,包含opcode、arg、resp等字段 |
struct mmc_data |
核心层 | 描述数据传输,包含blksz、blocks、sg、flags等字段 |
struct mmc_blk_data |
块设备层 | 描述MMC块设备,包含disk、read_only、part_curr等字段 |
下面的Mermaid图展示了Linux eMMC子系统的三层架构以及各层之间的交互关系:
设备树配置 emmc节点 clocks属性 bus-width属性 硬件层 eMMC设备 CLK/CMD/DATA信号 主机层 kdrv_emmc.ko 硬件抽象 寄存器操作 核心层 mmc_core.ko 协议处理 设备管理 电源管理 块设备层 mmc_block.ko 块设备请求队列 用户空间接口 /dev/mmcblk*
3 核心数据结构与代码分析
3.1 关键数据结构详解
struct mmc_host是描述MMC主机控制器的核心数据结构,每个eMMC控制器都对应一个该结构体的实例。其主要字段包括:
c
struct mmc_host {
const struct mmc_host_ops *ops; // 主机操作函数集
struct device *parent; // 父设备指针
struct device class_dev; // 类设备
int index; // 主机索引号
const struct mmc_bus_ops *bus_ops; // 总线操作函数集
unsigned int f_min, f_max; // 最小和最大时钟频率
u32 ocr_avail; // 可用电压范围
u32 caps; // 主机能力标志
u32 max_current_330, max_current_300, max_current_180; // 最大电流
unsigned int max_seg_size; // 最大段大小
unsigned short max_segs; // 最大段数
unsigned short max_req_size; // 最大请求大小
unsigned short max_blk_size; // 最大块大小
unsigned short max_blk_count; // 最大块计数
unsigned short max_busy_timeout; // 最大超时时间
spinlock_t lock; // 自旋锁
struct mmc_ios ios; // 当前IO设置
struct mmc_card *card; // 连接的设备
struct mmc_bus_ops *bus_ops; // 总线操作
struct dentry *debugfs_root; // debugfs根目录
// ... 其他字段
};
其中,caps字段表示主机控制器的能力,常用的标志位包括:
MMC_CAP_4_BIT_DATA:支持4位数据总线MMC_CAP_8_BIT_DATA:支持8位数据总线MMC_CAP_MMC_HIGHSPEED:支持MMC高速模式MMC_CAP_ERASE:支持擦除命令MMC_CAP_CMD23:支持CMD23包装命令
struct mmc_card描述已识别的eMMC设备,主要字段包括:
c
struct mmc_card {
struct mmc_host *host; // 所属主机
struct device dev; // 设备结构
unsigned int rca; // 相对设备地址
unsigned int type; // 设备类型
unsigned long state; // 设备状态
u32 ocr; // 操作条件寄存器
u32 cid[4]; // 设备标识符
u32 csd[4]; // 设备特定数据
u32 ext_csd[512]; // 扩展设备特定数据
u32 raw_cid[4], raw_csd[4], raw_scr[2], raw_ssr[16];
struct mmc_csd csd; // 解析后的CSD
struct mmc_ext_csd ext_csd; // 解析后的EXT_CSD
// ... 其他字段
};
struct mmc_request封装完整的MMC请求,可能包含命令、数据和停止命令:
c
struct mmc_request {
struct mmc_command *sbc; // 前置命令(如CMD23)
struct mmc_command *cmd; // 主命令
struct mmc_data *data; // 数据传输
struct mmc_command *stop; // 停止命令
struct completion completion; // 完成量
void *done_data; // 完成回调数据
void (*done)(struct mmc_request *); // 完成回调函数
// ... 其他字段
};
3.2 核心函数与工作流程
设备探测流程 从mmc_rescan函数开始,这是eMMC子系统中最关键的函数之一:
c
void mmc_rescan(struct work_struct *work)
{
struct mmc_host *host = container_of(work, struct mmc_host, detect.work);
int i;
// 检查主机是否被移除
if (mmc_host_is_removable(host)) {
// 检测设备是否存在
if (!mmc_try_claim_host(host))
return;
if (!mmc_get_cd(host)) {
mmc_release_host(host);
goto out; // 设备不存在
}
mmc_release_host(host);
}
// 尝试获取主机控制权
if (!mmc_try_claim_host(host))
return;
// 发送初始化序列
mmc_power_up(host, host->ocr_avail);
// 发送CMD0使设备进入空闲状态
mmc_go_idle(host);
// 发送CMD1查询设备操作条件
if (mmc_send_op_cond(host, 0, &ocr))
goto out;
// 发送CMD2获取CID
if (mmc_all_send_cid(host, cid))
goto out;
// 发送CMD3设置RCA
if (mmc_set_relative_addr(host))
goto out;
// 发送CMD9获取CSD
if (mmc_send_csd(host, card->raw_csd))
goto out;
// 发送CMD7选择设备
if (mmc_select_card(host, card))
goto out;
// 读取EXT_CSD
if (!mmc_card_sd(card) && host->ops->get_cd && mmc_card_highspeed(card)) {
err = mmc_read_ext_csd(host, ext_csd);
if (!err) {
// 解析EXT_CSD并配置设备
mmc_decode_ext_csd(host, card, ext_csd);
}
}
// 配置设备为高速模式(如支持)
if (card->ext_csd.hs_max_dtr && host->caps & MMC_CAP_MMC_HIGHSPEED) {
err = mmc_switch(card, EXT_CSD_CMD_SET_NORMAL,
EXT_CSD_HS_TIMING, 1, 100000);
if (!err) {
// 切换主机时序
mmc_set_timing(host, MMC_TIMING_MMC_HS);
}
}
// 配置总线宽度
if (host->caps & MMC_CAP_8_BIT_DATA) {
err = mmc_switch(card, EXT_CSD_CMD_SET_NORMAL,
EXT_CSD_BUS_WIDTH, EXT_CSD_BUS_WIDTH_8, 100000);
if (!err) {
// 设置主机总线宽度
host->ios.bus_width = MMC_BUS_WIDTH_8;
host->ops->set_ios(host, &host->ios);
}
}
// 注册块设备
mmc_add_card(host, card);
mmc_release_host(host);
out:
// 重新调度检测工作(用于可移动设备)
if (mmc_host_is_removable(host))
mmc_schedule_delayed_work(&host->detect, HZ);
}
请求处理流程 是eMMC子系统的另一个核心部分。当块设备层有I/O请求时,会调用mmc_blk_issue_rq函数:
c
static int mmc_blk_issue_rq(struct mmc_queue *mq, struct request *req)
{
struct mmc_blk_data *md = mq->blkdata;
struct mmc_card *card = md->queue.card;
struct mmc_blk_request *brq = &mq->brq;
int ret = 0;
// 准备MMC请求
mmc_blk_rw_rq_prep(mq, req, brq, 1);
// 设置完成回调
brq->mrq.done = mmc_blk_req_done;
// 下发请求到主机层
mmc_wait_for_req(card->host, &brq->mrq);
// 处理请求结果
ret = mmc_blk_err_check(brq);
if (ret)
goto out;
// 处理写入完成后的刷缓存操作(如需要)
if (brq->cmd.flags & MMC_RSP_BUSY)
mmc_blk_busy_wait(mq, req);
out:
// 完成请求
mmc_blk_rw_rq_post(mq, req, brq);
return ret;
}
下面的Mermaid时序图展示了eMMC设备探测和初始化的完整流程:
用户空间 块设备层 核心层 主机层 eMMC设备 设备探测与初始化时序 mmc_add_host()注册主机 启动mmc_rescan工作队列 mmc_power_up()上电 硬件上电 CMD0(GO_IDLE_STATE) 无响应 CMD1(SEND_OP_COND) R3响应(OCR) CMD2(ALL_SEND_CID) R2响应(CID) CMD3(SET_RELATIVE_ADDR) R1响应(RCA) CMD9(SEND_CSD) R2响应(CSD) CMD7(SELECT_CARD) R1响应 CMD8(SEND_EXT_CSD) R1响应 + 数据块 解析EXT_CSD CMD6(SWITCH)切换高速模式 R1响应 set_ios()配置主机时序 CMD6(SWITCH)切换总线宽度 R1响应 set_ios()配置总线宽度 mmc_add_card()注册块设备 设备节点/dev/mmcblk0可用 用户空间 块设备层 核心层 主机层 eMMC设备
3.3 设备树配置与硬件抽象
在嵌入式Linux系统中,eMMC主机控制器通常通过设备树进行配置。以下是一个典型的eMMC设备树节点示例:
dts
emmc {
compatible = "sstar_mci"; // 兼容性字符串,用于匹配驱动
clocks = <&CLK_sd>; // 时钟源
slot-num = <1>; // 使用的SD/SDIO IP个数
adma-mode = <1>,<1>,<1>; // data传输模式:0-dma, 1-adma
ip-select = <1>,<1>,<2>; // 对应卡槽的IP编号
pad-select = <0>,<0>,<0>; // 对应卡槽的padmux mode编号
bus-width = <8>,<4>,<4>; // 总线宽度:4-4bit, 8-8bit
max-clks = <48000000>,<48000000>,<48000000>; // 支持的最大时钟频率
status = "ok"; // 设备状态
};
设备树中的每个属性都有特定的作用:
compatible属性是驱动匹配的关键,必须与驱动程序中of_device_id表的兼容字符串一致。clocks属性指定主机控制器使用的时钟源。bus-width属性配置数据总线的宽度,eMMC支持1-bit、4-bit和8-bit模式。max-clks属性设置最大时钟频率,影响数据传输速率。adma-mode选择数据传输使用的DMA模式,ADMA(Advanced DMA)通常比标准DMA效率更高。
主机驱动在探测阶段会解析这些设备树属性,并据此配置硬件寄存器。例如,当解析bus-width = <8>时,驱动会设置主机控制器使用8位数据总线,并通过mmc_switch命令配置eMMC设备也使用8位模式。
4 简单实例与代码展示
4.1 简化版eMMC主机驱动实例
以下是一个简化版的eMMC主机驱动示例,展示了驱动的基本框架和关键操作:
c
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/mmc/host.h>
#include <linux/mmc/mmc.h>
// 主机驱动私有数据结构
struct my_emmc_host {
struct mmc_host *mmc; // 标准MMC主机结构
void __iomem *regbase; // 寄存器基地址
struct clk *clk; // 时钟
int irq; // 中断号
};
// 主机操作函数集
static const struct mmc_host_ops my_emmc_ops = {
.request = my_emmc_request, // 请求处理函数
.set_ios = my_emmc_set_ios, // 配置总线参数
.get_cd = my_emmc_get_cd, // 检测设备插入
.get_ro = my_emmc_get_ro, // 检测写保护
.enable_sdio_irq = my_emmc_enable_sdio_irq, // 使能SDIO中断
};
// 请求处理函数
static void my_emmc_request(struct mmc_host *mmc, struct mmc_request *mrq)
{
struct my_emmc_host *host = mmc_priv(mmc);
struct mmc_command *cmd = mrq->cmd;
struct mmc_data *data = mrq->data;
// 处理命令阶段
if (cmd) {
// 将命令写入命令寄存器
writel(cmd->opcode, host->regbase + COMMAND_REG);
writel(cmd->arg, host->regbase + ARGUMENT_REG);
// 等待命令完成中断或超时
// ...
// 读取响应
if (cmd->flags & MMC_RSP_PRESENT) {
if (cmd->flags & MMC_RSP_136) {
// 136位响应
cmd->resp[0] = readl(host->regbase + RESPONSE_0_REG);
cmd->resp[1] = readl(host->regbase + RESPONSE_1_REG);
cmd->resp[2] = readl(host->regbase + RESPONSE_2_REG);
cmd->resp[3] = readl(host->regbase + RESPONSE_3_REG);
} else {
// 48位响应
cmd->resp[0] = readl(host->regbase + RESPONSE_0_REG);
}
}
// 检查命令错误
if (cmd->error) {
mmc_request_done(host->mmc, mrq);
return;
}
}
// 处理数据传输阶段
if (data) {
if (data->flags & MMC_DATA_READ) {
// 读取数据
sg_miter_start(&host->sg_miter, data->sg, data->sg_len,
SG_MITER_ATOMIC | SG_MITER_TO_SG);
while (sg_miter_next(&host->sg_miter)) {
void *buf = host->sg_miter.addr;
size_t len = host->sg_miter.length;
// 从数据寄存器读取数据
readsb(host->regbase + DATA_REG, buf, len);
}
sg_miter_stop(&host->sg_miter);
} else {
// 写入数据
sg_miter_start(&host->sg_miter, data->sg, data->sg_len,
SG_MITER_ATOMIC | SG_MITER_FROM_SG);
while (sg_miter_next(&host->sg_miter)) {
void *buf = host->sg_miter.addr;
size_t len = host->sg_miter.length;
// 向数据寄存器写入数据
writesb(host->regbase + DATA_REG, buf, len);
}
sg_miter_stop(&host->sg_miter);
}
// 完成数据传输
data->bytes_xfered = data->blocks * data->blksz;
}
// 完成请求
mmc_request_done(host->mmc, mrq);
}
// 配置总线参数
static void my_emmc_set_ios(struct mmc_host *mmc, struct mmc_ios *ios)
{
struct my_emmc_host *host = mmc_priv(mmc);
u32 reg;
// 配置时钟频率
if (ios->clock) {
reg = readl(host->regbase + CLOCK_REG);
reg &= ~CLOCK_RATE_MASK;
reg |= ios->clock & CLOCK_RATE_MASK;
writel(reg, host->regbase + CLOCK_REG);
}
// 配置总线宽度
reg = readl(host->regbase + CONTROL_REG);
reg &= ~BUS_WIDTH_MASK;
switch (ios->bus_width) {
case MMC_BUS_WIDTH_1:
reg |= BUS_WIDTH_1BIT;
break;
case MMC_BUS_WIDTH_4:
reg |= BUS_WIDTH_4BIT;
break;
case MMC_BUS_WIDTH_8:
reg |= BUS_WIDTH_8BIT;
break;
}
writel(reg, host->regbase + CONTROL_REG);
// 配置电源模式
if (ios->power_mode == MMC_POWER_OFF) {
// 关闭电源
reg = readl(host->regbase + POWER_REG);
reg &= ~POWER_ON;
writel(reg, host->regbase + POWER_REG);
} else if (ios->power_mode == MMC_POWER_UP) {
// 开启电源
reg = readl(host->regbase + POWER_REG);
reg |= POWER_ON;
writel(reg, host->regbase + POWER_REG);
}
}
// 探测函数
static int my_emmc_probe(struct platform_device *pdev)
{
struct mmc_host *mmc;
struct my_emmc_host *host;
struct resource *res;
int ret;
// 分配MMC主机结构
mmc = mmc_alloc_host(sizeof(struct my_emmc_host), &pdev->dev);
if (!mmc) {
dev_err(&pdev->dev, "Failed to allocate MMC host\n");
return -ENOMEM;
}
host = mmc_priv(mmc);
host->mmc = mmc;
// 获取寄存器资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
host->regbase = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(host->regbase)) {
ret = PTR_ERR(host->regbase);
goto host_free;
}
// 获取时钟资源
host->clk = devm_clk_get(&pdev->dev, NULL);
if (IS_ERR(host->clk)) {
ret = PTR_ERR(host->clk);
goto host_free;
}
// 配置MMC主机能力
mmc->ops = &my_emmc_ops;
mmc->f_min = 400000; // 最小频率400KHz
mmc->f_max = 52000000; // 最大频率52MHz
mmc->ocr_avail = MMC_VDD_32_33 | MMC_VDD_33_34; // 可用电压
mmc->caps = MMC_CAP_4_BIT_DATA | MMC_CAP_8_BIT_DATA |
MMC_CAP_MMC_HIGHSPEED | MMC_CAP_ERASE;
mmc->max_segs = 64; // 最大段数
mmc->max_seg_size = 65536; // 最大段大小
mmc->max_req_size = 512 * 1024; // 最大请求大小
mmc->max_blk_size = 512; // 最大块大小
mmc->max_blk_count = 4096; // 最大块数
// 注册MMC主机
ret = mmc_add_host(mmc);
if (ret)
goto host_free;
platform_set_drvdata(pdev, host);
dev_info(&pdev->dev, "My eMMC host driver initialized\n");
return 0;
host_free:
mmc_free_host(mmc);
return ret;
}
4.2 用户空间eMMC设备使用示例
在用户空间,eMMC设备通常表现为块设备节点,可以通过标准文件操作或工具进行访问。以下示例展示了如何在用户空间与eMMC设备交互:
c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/fs.h>
// 读取eMMC设备信息
int read_emmc_info(const char *device_path) {
int fd;
unsigned long long size;
// 打开eMMC块设备
fd = open(device_path, O_RDONLY);
if (fd < 0) {
perror("Failed to open device");
return -1;
}
// 获取设备大小
if (ioctl(fd, BLKGETSIZE64, &size) == 0) {
printf("Device size: %llu bytes (%.2f GB)\n",
size, (double)size / (1024 * 1024 * 1024));
}
// 读取CSD寄存器信息(需要通过debugfs)
system("cat /sys/kernel/debug/mmc0/ios");
close(fd);
return 0;
}
// 简单的eMMC性能测试
void emmc_benchmark(const char *device_path) {
int fd;
char buffer[512 * 1024]; // 512KB缓冲区
struct timeval start, end;
double elapsed;
fd = open(device_path, O_RDWR);
if (fd < 0) {
perror("Failed to open device");
return;
}
// 写入性能测试
gettimeofday(&start, NULL);
for (int i = 0; i < 100; i++) {
if (write(fd, buffer, sizeof(buffer)) != sizeof(buffer)) {
perror("Write failed");
break;
}
}
gettimeofday(&end, NULL);
elapsed = (end.tv_sec - start.tv_sec) +
(end.tv_usec - start.tv_usec) / 1000000.0;
printf("Write performance: %.2f MB/s\n",
(100.0 * sizeof(buffer) / (1024 * 1024)) / elapsed);
close(fd);
}
int main() {
const char *emmc_device = "/dev/mmcblk0";
printf("=== eMMC Device Information ===\n");
read_emmc_info(emmc_device);
printf("\n=== eMMC Performance Benchmark ===\n");
emmc_benchmark(emmc_device);
return 0;
}
4.3 通过sysfs进行eMMC调试
Linux内核提供了丰富的sysfs接口用于eMMC设备的调试和监控。以下是一些常用的调试命令:
bash
# 进入eMMC设备sysfs目录
cd /sys/device/platform/soc/soc:emmc
# 开启读写监控
echo 1 > eMMC_monitor_count_enable
# 查看读写监控信息
cat eMMC_monitor_count_enable
# 开启写操作log打印
echo 1 > eMMC_write_log_enable
# 开启读操作log打印
echo 1 > eMMC_read_log_enable
# 查看eMMC的bootbus值
cat eMMC_bootbus
# 查看eMMC的工作时钟频率
cat eMMC_get_clock
# 启动eMMC IP验证测试
echo 1 > eMMC_run_ipverify
5 核心模型与框架剖析
5.1 eMMC协议状态机解析
eMMC设备在运行过程中会处于不同的状态,这些状态构成了一个复杂的状态机。理解这个状态机对于深入掌握eMMC工作原理至关重要。主要状态包括:
- 空闲状态(Idle State):设备上电或复位后的初始状态,在此状态下设备等待初始化命令。
- 就绪状态(Ready State):设备已响应CMD1命令,准备好进行识别过程。
- 识别状态(Identification State):主机正在通过CMD2和CMD3命令识别设备并分配地址。
- 待命状态(Stand-by State):设备已被识别,但未被选中进行数据传输。
- 传输状态(Transfer State):设备被选中(通过CMD7),可以进行数据传输。
- 发送数据状态(Sending-data State):设备正在向主机发送数据。
- 接收数据状态(Receive-data State):设备正在接收主机发送的数据。
- 编程状态(Programming State):设备正在将接收到的数据编程到闪存中。
- 断开连接状态(Disconnect State):设备处于高阻抗状态,用于SDIO操作。
下面的Mermaid状态图展示了eMMC设备的主要状态转换:
上电/复位 CMD1 CMD2 CMD3 CMD7 CMD7(其他RCA) 读命令 数据传输完成 写命令 CMD15 CMD7 数据传输完成 编程完成 IdleState ReadyState IdentificationState StandByState TransferState SendingDataState ReceiveDataState ProgrammingState 编程完成 Busy DisconnectState
状态转换由特定的命令触发,例如从Idle状态转换到Ready状态需要通过CMD1命令,而从Stand-by状态转换到Transfer状态需要通过CMD7命令选择设备。在Programming状态期间,设备会拉低DAT0线表示忙状态,直到内部编程操作完成。
5.2 数据传输的DMA描述符机制
为了提高数据传输效率,eMMC主机控制器通常使用DMA(直接内存访问)进行数据传输。ADMA(Advanced DMA)是eMMC规范中定义的一种更高效的DMA机制,它使用描述符表来管理传输过程。
ADMA描述符的数据结构如下:
c
struct adma_descriptor {
u16 attr; // 属性字段
u16 length; // 数据长度
u32 addr; // 数据地址
u32 next_desc; // 下一个描述符地址
};
// 属性字段定义
#define ADMA_ATTR_VALID (1 << 0) // 描述符有效
#define ADMA_ATTR_END (1 << 1) // 最后一个描述符
#define ADMA_ATTR_INT (1 << 2) // 传输完成产生中断
#define ADMA_ATTR_ACT_NOP (0 << 4) // 无操作
#define ADMA_ATTR_ACT_TRAN (2 << 4) // 传输数据
#define ADMA_ATTR_ACT_LINK (3 << 4) // 链接到下一个描述符
ADMA的工作流程如下:
- 驱动程序准备ADMA描述符表,每个描述符指向一个内存中的数据缓冲区
- 将描述符表的基地址写入主机的ADMA系统地址寄存器
- 启动DMA传输,主机控制器按顺序处理描述符表中的每个描述符
- 当处理到带有END标志的描述符时,传输完成,产生中断
与简单的DMA相比,ADMA的优势在于它可以处理分散/聚集(scatter-gather)I/O,无需物理上连续的内存缓冲区。当处理文件系统的分散读写请求时,ADMA可以高效地将多个不连续的数据块通过一次eMMC请求完成传输。
5.3 错误处理与恢复机制
eMMC子系统实现了完善的错误处理与恢复机制,确保在传输失败时能够恢复正常操作。主要的错误类型包括:
- 命令超时错误:设备在规定时间内没有响应命令
- CRC错误:命令、响应或数据传输的CRC校验失败
- 数据超时错误:数据传输未在规定时间内完成
- 对齐错误:数据缓冲区地址或长度不符合要求
当检测到错误时,eMMC子系统会采取以下恢复措施:
c
// 错误恢复函数示例
static int mmc_error_recovery(struct mmc_host *host, struct mmc_request *mrq)
{
struct mmc_card *card = host->card;
int err;
// 重置主机控制器
mmc_hw_reset(host);
// 重新初始化设备
err = mmc_init_card(host, card->ocr, card);
if (err) {
pr_err("Failed to reinit card after error: %d\n", err);
return err;
}
// 恢复之前的I/O设置
mmc_set_ios(host);
// 重新下发失败的请求(如果可能)
if (mrq && !mrq->error) {
pr_info("Retrying failed request\n");
mmc_wait_for_req(host, mrq);
}
return 0;
}
对于可恢复的错误,如偶尔的CRC错误或超时,子系统会自动重试操作。对于严重错误,如设备无响应或硬件故障,子系统会报告错误并向上层传递错误信息。
表:eMMC子系统错误类型与处理策略
| 错误类型 | 检测方式 | 恢复策略 | 严重程度 |
|---|---|---|---|
| 命令CRC错误 | 响应CRC校验 | 重试命令(最多3次) | 低 |
| 数据CRC错误 | 数据CRC校验 | 重试数据传输 | 中 |
| 命令超时 | 计时器超时 | 重置主机控制器并重试 | 高 |
| 数据超时 | 数据线无活动 | 重置数据线并重试 | 中 |
| 电压不匹配 | OCR寄存器检查 | 调整电压或放弃初始化 | 高 |
| 设备无响应 | 无CMD线响应 | 硬件复位或设备移除 | 严重 |
6 工具命令与调试手段
6.1 常用调试工具与命令
eMMC子系统的调试涉及多个层面,从硬件信号到上层文件系统。以下是一些常用的调试工具和命令:
基本状态检查命令:
bash
# 查看eMMC设备信息
cat /sys/kernel/debug/mmc0/mmc0\:0001/ext_csd
# 查看eMMC设备寄存器信息
cat /sys/kernel/debug/mmc0/ios
# 查看eMMC设备时钟频率
cat /sys/kernel/debug/mmc0/clock
# 查看eMMC磨损程度估计(需要解析EXT_CSD)
cat /sys/kernel/debug/mmc0/mmc0\:0001/ext_csd | python -c 'import binascii, sys; print "~%d%% wear" % (ord(binascii.unhexlify(sys.stdin.read().strip())[0x5e])*10)'
# 查看块设备信息
lsblk /dev/mmcblk0
fdisk -l /dev/mmcblk0
性能测试工具fio的使用:
bash
# 安装fio工具
apt-get install fio
# 顺序写测试(1MB块大小,1GB数据量)
fio -filename=/dev/mmcblk0p1 -direct=1 -thread -rw=write -ioengine=psync -bs=1M -size=1G -numjobs=1 -group_reporting -name=emmc_test
# 顺序读测试
fio -filename=/dev/mmcblk0p1 -direct=1 -thread -rw=read -ioengine=psync -bs=1M -size=1G -numjobs=1 -group_reporting -name=emmc_test
# 随机写测试(4KB块大小,4线程)
fio -filename=/dev/mmcblk0p1 -direct=1 -thread -rw=randwrite -ioengine=psync -bs=4k -size=1G -numjobs=4 -group_reporting -name=emmc_test
# 随机读测试
fio -filename=/dev/mmcblk0p1 -direct=1 -thread -rw=randread -ioengine=psync -bs=4k -size=1G -numjobs=4 -group_reporting -name=emmc_test
eMMC健康状态监测:
bash
# 挂载debugfs
mount -t debugfs none /sys/kernel/debug/
# 读取eMMC健康状态(磨损程度)
cat /sys/kernel/debug/mmc0/mmc0\:0001/ext_csd | python -c 'import binascii, sys; print "~%d%% wear" % (ord(binascii.unhexlify(sys.stdin.read().strip())[0x5e])*10)'
# 查看eMMC错误统计
cat /sys/kernel/debug/mmc0/err_stats
# 查看eMMC重试计数
cat /sys/kernel/debug/mmc0/retune_count
6.2 故障排查与性能优化
常见故障排查方法:
-
设备识别失败:
- 检查硬件连接和电源稳定性
- 验证设备树配置是否正确
- 检查时钟配置和信号完整性
- 使用示波器测量CMD和DAT信号质量
-
数据传输错误:
- 降低总线频率测试稳定性
- 检查DMA缓冲区对齐和大小
- 验证CRC错误计数
- 检查电源噪声和接地问题
-
性能优化建议:
- 使用8位总线宽度代替4位或1位
- 启用HS400或HS200高速模式
- 调整块大小和请求队列深度
- 使用ADMA代替标准DMA
- 启用命令队列(CQ)功能
设备树配置优化示例:
dts
&emmc {
compatible = "sstar_mci";
clocks = <&CLK_sd>;
slot-num = <1>;
/* 使用ADMA传输模式 */
adma-mode = <1>,<1>,<1>;
/* 配置为8位总线宽度 */
bus-width = <8>,<4>,<4>;
/* 提高最大时钟频率 */
max-clks = <52000000>,<52000000>,<52000000>;
/* 启用高速模式 */
mmc-hs200-1_8v;
/* 禁用1.8V电压(如不支持) */
/* no-1-8-v; */
status = "ok";
};
内核参数调整:
bash
# 提高读写超时时间(针对慢速设备)
echo 5000 > /sys/block/mmcblk0/device/timeout
# 调整调度器为deadline(针对实时性要求高的场景)
echo deadline > /sys/block/mmcblk0/queue/scheduler
# 增加队列深度
echo 128 > /sys/block/mmcblk0/queue/nr_requests
7 总结与梳理
7.1 eMMC子系统核心机制总结
通过对Linux eMMC子系统的深入分析,我们可以总结出以下几个核心机制:
-
分层架构设计:eMMC子系统采用清晰的三层架构 - 块设备层、核心层和主机层,这种设计实现了硬件无关性与硬件相关性的分离,提高了代码的可维护性和可移植性。
-
状态机管理:eMMC设备操作基于精细的状态机模型,从设备识别到数据传输,每个状态转换都由特定命令触发,确保了协议的正确实现。
-
请求处理流水线:I/O请求经过多级处理,从块设备层的请求队列到核心层的协议转换,再到主机层的硬件操作,形成了一个高效的处理流水线。
-
错误恢复机制:子系统实现了多级错误检测和恢复机制,包括命令重试、CRC错误处理、超时管理和硬件复位等,确保了系统的可靠性。
-
性能优化策略:通过总线宽度扩展、高速时序模式、DMA传输、命令队列等技术,充分发挥了eMMC设备的性能潜力。
7.2 关键知识点回顾
表:eMMC子系统关键知识点总结
| 知识点 | 核心内容 | 相关数据结构/函数 |
|---|---|---|
| 设备探测 | 上电、复位、识别、初始化流程 | mmc_rescan, mmc_init_card |
| 请求处理 | 命令构建、数据传输、完成回调 | mmc_request, mmc_host_ops.request |
| 电源管理 | 电压切换、时钟控制、电源状态 | mmc_set_ios, mmc_power_up |
| 错误处理 | CRC校验、超时管理、重试机制 | mmc_error_recovery, 错误统计 |
| 性能优化 | 总线宽度、时序模式、DMA | mmc_switch, ADMA描述符 |
| 调试手段 | sysfs接口、debugfs、性能测试 | fio工具、ext_csd解析 |