简介

本文参考linux-4.2内核源代码进行讲述,侧重介绍软件结构,简要介绍协议。在介绍MMC子系统之前,先看一下MMC子系统在内核中的位置。 Linux内核系统的分层结构如下图所示。

mmc-in-linux.png

进一步的讲,MMC也是分为三个层次的,MMC的分层结构如下图所示。

mmc-driver-frame.png

从MMC的Makefile可以看到,MMC将三个层次分别放到三个目录中。

obj-$(CONFIG_MMC)               += core/
obj-$(CONFIG_MMC)               += card/
obj-$(subst m,y,$(CONFIG_MMC))  += host/
core
核心层完成协议部分功能
card
卡层提供块设备驱动
host
主机层由各控制器厂商提供特定代码

核心层主要包括如下一些文件,每个目标文件对应一个源文件。

obj-$(CONFIG_MMC)       += mmc_core.o
mmc_core-y := core.o bus.o host.o mmc.o mmc_ops.o sd.o  \
           sd_ops.o sdio.o sdio_ops.o sdio_bus.o        \
           sdio_cis.o sdio_io.o sdio_irq.o quirks.o     \
           slot-gpio.o
mmc_core
最终生成mmc_core.ko模块
core.c
传输中间件,上传下达
bus.c
注册mmc_bus_type
host.c
通用host接口,供厂商使用

卡层主要包括如下一些文件,从中可以看出主要还是块设备驱动,关键任务是完成请求队列的处理和请求的发送。

obj-$(CONFIG_MMC_BLOCK)         += mmc_block.o
mmc_block-objs                  := block.o queue.o
obj-$(CONFIG_MMC_TEST)          += mmc_test.o

obj-$(CONFIG_SDIO_UART)         += sdio_uart.o
mmc_block
最终生成mmc_block.ko模块
block.c
块设备驱动
queue.c
块设备请求队列

主机层包括许多厂商的主机控制器驱动,基本上一个模块就对应一个厂商的控制器。

obj-$(CONFIG_MMC_SDHCI)         += sdhci.o
obj-$(CONFIG_MMC_SDHCI_PCI)     += sdhci-pci.o
...
sdhci.c
SDHCI(SD Host Controller Interface)

数据结构

最关键的数据结构是mmc_host、mmc_card和mmc_blk_data,分别表征了控制器,卡对象和块设备数据。请求由通用块层将用户的操作传递下来,向卡对象发送出去,最后由控制器来完成实际的硬件控制。

data-struct.png

mmc_host_ops
由各厂商提供的操作,如设置MMC总线参数、命令/数据请求处理、电压切换等等。这部分参数和厂商控制器相关,因此由各厂商的驱动来设定。
mmc_ios
负责MMC总线上的参数设定,如时钟、模式、电源开关、时序、电压等。这部份参数有核心层来控制,通过调用mmc_set_ios来设定参数。
mmc_bus_ops
MMC总线状态控制,主要包括电源控制、移除卡和探测卡的动作。这部分参数由具体卡的驱动来设定,如MMC、SD和SDIO各有一套设定。
mmc_host
表示一个MMC控制器的数据结构,MMC控制器相关的操作,围绕该结构展开。
mmc_card
描述卡的信息,除了这里提到的分区信息,还包括卡的特征参数,如CID、CSD、SCR、SSR、CCCR等等,特征参数相关的参数最好参考规范去读。
mmc_part
需要注意的是这个地方的part是指物理分区,只有MMC卡才有这个概念。
mmc_blk_data
描述磁盘设备的块信息,每个卡槽对应一个mmc_blk_data,这是描述卡数据传输的核心数据结构。
mmc_queue
MMC管理的请求队列,基于request_queue实现了一个异步传输机制。
mmc_queue_req
mmc_queue借助mmc_queue_req来实现异步传输,两个请求,一个表示前一个请求,一个表示当前请求。发送当前请求的时候必须要等待上一个请求完成。
mmc_async_req
本质上它不过是一组指针,由mmc_request来封装,更方便访问请求中的一些数据。也就是说借助于mmc_request,我们可以方便的获取mmc_command和mmc_data。
mmc_blk_request
这才是真正用于存放数据的数据结构,里面进一步封装了mmc_request、mmc_command和mmc_data,命令包括三个类型,SBC、CMD和STOP。

MMC结构视图

MMC总线

MMC总线类型

static struct bus_type mmc_bus_type = {
    .name           = "mmc",
    .dev_groups     = mmc_dev_groups,
    .match          = mmc_bus_match,
    .uevent         = mmc_bus_uevent,
    .probe          = mmc_bus_probe,
    .remove         = mmc_bus_remove,
    .shutdown       = mmc_bus_shutdown,
    .pm             = &mmc_bus_pm_ops,
};

一旦注册就能看到目录/sys/bus/mmc,也就是说mmc表示的是总线的名字。比较有趣的是probe和remove两个函数,最早是有的,后来在Linux内核中对总线和驱动的概念做了一些优化,总线上的回调和驱动的回调不能同时指定,否则内核会提出警告。再后来又被加上去了,并且将mmc_driver也改成了device_driver。

设备属性(组)

static ssize_t type_show(struct device *dev,
                         struct device_attribute *attr,
                         char *buf);
static DEVICE_ATTR_RO(type);            // 创建dev_attr_type属性

#define DEVICE_ATTR_RO(_name)                                   \
    struct device_attribute dev_attr_##_name = __ATTR_RO(_name)
#define __ATTR_RO(_name) {                                    \
    .attr = { .name = __stringify(_name), .mode = S_IRUGO },  \
    .show = _name##_show,                                     \
}
static struct attribute *mmc_dev_attrs[] = {
    &dev_attr_type.attr,                // 创建属性数组
    NULL,
};
ATTRIBUTE_GROUPS(mmc_dev);

#define ATTRIBUTE_GROUPS(_name)                                 \
    static const struct attribute_group _name##_group = {       \
        .attrs = _name##_attrs,                                 \
    };                                                          \
    __ATTRIBUTE_GROUPS(_name)
#define __ATTRIBUTE_GROUPS(_name)                               \
    static const struct attribute_group *_name##_groups[] = {   \
        &_name##_group,                                         \
        NULL,                                                   \
    }

attr-groups.png

总线操作

设备和驱动的匹配由mmc_bus_match来完成,该函数始终返回1,因此一个driver匹配所有的mmc设备。

uevent操作主要用于添加环境变量,包括MMC_TYPE、MMC_NAME、MODALIAS。该回调函数在添加卡的时候会调用。

关机动作包含了两个部分的工作,一个是driver->shutdown用于停止块设备请求队列,一个是host->bus_type->shutdown,由具体类型的卡驱动来指定如何动作,一般来说就是suspend卡,而suspend卡就是给卡断电。

电源操作包括mmc总线suspend/resume,以及RPM的suspend/resume。它们和shutdown一样都是转交给MMC驱动来处理。

MMC设备

假定主机控制器已经准备好,那么插入卡的时候就会添加一个卡设备。添加卡的基本思路是:卡插到控制器,控制器产生一个中断,控制器驱动处理中断,调用mmc_rescan初始化卡,添加卡设备,扫描会尝试多个频率,由400kHz降低到100kHz,而对应卡的类型则是先尝试认SDIO,再认SD、MMC卡。

card-insert-irq.png

如果运气好认卡成功了,就会添加卡设备,有mmc_add_card来添加。

SDIO初始化

如果插入的是单纯的SDIO卡,主要初始化流程如下图所示。

sdio-init.png

CMD5(OCR)
IO_SEND_OP_COND,发送CMD5就是为了获取OCR,用于确定这张卡是不是SDIO卡,如果不是就退出。
OCR
如果在认识到SDIO之后会在发一次CMD5,这一次是为了设置OCR,也就是让卡和控制器的OCR匹配起来,找到其公共区间来设定。
18V
检查OCR是否支持R4_18V_PRESENT,如果支持就会切电压。
RCA
获取相对地址。
SELECT
发送CMD7选中卡。
CCCR
Card Common Control Registers,通过CMD52来传输。
CIS
Card Information Structure,提供卡信息。
UHS
检查是否支持UHS主要是看是否可以切到18V电压。如果是UHS卡主要需要进行TUNING过程,不论是UHS还是HS都是要设置MMC总线模式为4bits。

实际上有可能插入的是单纯的SDIO卡,而是COMBO卡,也就是包含存储功能。这样就会在初始化SDIO的过程中插入SD部分的初始化。下图中粉红色的部分就是SD部分的初始化,详细解释请参考SD初始化。

sdio-combo-init.png

SD初始化

SD初始化的主要工作由mmc_sd_init_card来完成,如下图所示。

sd-init.png

RESET
即CMD0,发送复位命令。
OCR
即CMD8,设置检查操作电压。
18V
即ACMD41,如果支持切换到18V就会去切电压。
CID
Card IDentification,获取卡ID信息。
RCA
卡相对地址。
CSD
Card Specification Data,卡规范相关的数据,CMD9。
DSR
Driver Stage Register,驱动级别寄存器,即CMD4。
SELECT
选卡,即CMD7。
SCR
SD Configuration Register,SD卡配置寄存器,即ACMD51。
SSR
SD Status Register,卡状态寄存器,即ACMD13。
SWITCH
读取switch信息。
RO
判断卡是否为只读卡。
UHS
判别是不是UHS卡,主要是看是否支持18V电压。如果支持就需要TUNING。不论是UHS模式还是HS模式都要设置MMC总线为4bits。
mmc_sd_init_card
初始化SD卡,包含从RESET到UHS部分的代码。
mmc_sd_get_cid
包含从RESET到CID部分的流程。
mmc_sd_setup_card
包含从SCR到RO部分的流程。

初始化的过程中会分配一个mmc_card结构来表示卡设备,其中会加入很多属性,这里以SD卡为例:

MMC_DEV_ATTR(scr, "%08x%08x\n", card->raw_scr[0], card->raw_scr[1]);
MMC_DEV_ATTR(date, "%02d/%04d\n", card->cid.month, card->cid.year);
...

static struct attribute *sd_std_attrs[] = {
    &dev_attr_csd.attr,
    &dev_attr_scr.attr,
    &dev_attr_date.attr,
    ...
    NULL,
};
ATTRIBUTE_GROUPS(sd_std);

struct device_type sd_type = {
    .groups = sd_std_groups,
};

设备初始化的时候设定:card->dev.type = sd_type

MMC驱动

块设备驱动

static struct device_driver mmc_driver = {
    .name           = "mmcblk",
    .pm             = &mmc_blk_pm_ops,
    .probe          = mmc_blk_probe,
    .remove         = mmc_blk_remove,
    .shutdown       = mmc_blk_shutdown,
};

电源管理

mmc_blk_pm_ops仅包括两个电源管理功能,即suspend/resume, suspend就是停止块设备请求队列。 shutdown和suspend的功能相同。

块设备驱动探测

很显然在添加好MMC设备时,就会进行设备和驱动匹配,此时就会执行probe动作。 probe会初始化disk、queue,注册块设备请求队列主函数mmc_request_fn,注册内核线程mmc_queue_thread,队列回调issue_fn(mmc_blk_issue_rq),最后添加disk。

mmc-blk-probe.png

mmc_request_fn的主要任务就是唤醒线程mmc_queue_thread,而线程负责从请求队列提取一个块设备请求,通过issue_fn发送出去。而issue_fn则相当于一个分支器。

mmc-request-fn.png

mmc-issue-rq.png

MMC请求处理

命令传输

mmc-send-cmd.png

数据传输

mmc-send-data.png

MMC主机控制器

主机控制器本身需要一个driver来控制,至于是什么类型的driver由具体的平台而定,可以是PCI驱动,也可以是平台驱动等等。在probe主机的时候就需要添加mmc host,添加的方法比较简单,直接利用core/host.c中提供的接口即可。

要能够实现一个完整的MMC/SD卡控制器,需要去设定mmc_host_ops中的回调函数,包括请求处理,MMC总线参数设定,切电压、tuning等。

要实现对主机控制器控制,最基本的需要能够读写寄存器,DMA数据传输。寄存器的控制通过ioremap来映射总线地址,之后即可借助ioread/iowrite来操作。

DMA传输则是通过dma_alloc_coherent来分配一致映射,有DMA描述表来控制DMA传输,因此驱动的主要工作就是创建描述表,把描述表地址写入到控制寄存器。

MMC测试驱动

测试驱动和块设备驱动二者只会有一个可以可卡设备匹配,因此如果要编译到内核,只能选择一个。挂载好测试驱动之后,如果插卡就可以通过如下命令查看可以测试的用例。

cd /sys/kernel/debug/mmc0/mmc0:1234
cat testlist
1:      Basic write (no data verification)
2:      Basic read (no data verification)
3:      Basic write (with data verification)
4:      Basic read (with data verification)
5:      Multi-block write
6:      Multi-block read
7:      Power of two block writes
8:      Power of two block reads
9:      Weird sized block writes
10:     Weird sized block reads
11:     Badly aligned write
12:     Badly aligned read
13:     Badly aligned multi-block write
14:     Badly aligned multi-block read
15:     Correct xfer_size at write (start failure)
16:     Correct xfer_size at read (start failure)
17:     Correct xfer_size at write (midway failure)
18:     Correct xfer_size at read (midway failure)
......

可以通过执行如下命令来启动测试用例。

echo 1 > test                           # test case 1
i=0; while [ $i -lt 45 ]; do echo $i > test; let i+=1; done # test 1..45