Linux MMC Subsystem
Table of Contents
简介
本文参考linux-4.2内核源代码进行讲述,侧重介绍软件结构,简要介绍协议。在介绍MMC子系统之前,先看一下MMC子系统在内核中的位置。 Linux内核系统的分层结构如下图所示。
进一步的讲,MMC也是分为三个层次的,MMC的分层结构如下图所示。
从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,分别表征了控制器,卡对象和块设备数据。请求由通用块层将用户的操作传递下来,向卡对象发送出去,最后由控制器来完成实际的硬件控制。
- 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, \ }
总线操作
设备和驱动的匹配由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卡。
如果运气好认卡成功了,就会添加卡设备,有mmc_add_card
来添加。
SDIO初始化
如果插入的是单纯的SDIO卡,主要初始化流程如下图所示。
- 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初始化。
SD初始化
SD初始化的主要工作由mmc_sd_init_card
来完成,如下图所示。
- 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_request_fn的主要任务就是唤醒线程mmc_queue_thread,而线程负责从请求队列提取一个块设备请求,通过issue_fn发送出去。而issue_fn则相当于一个分支器。
MMC请求处理
命令传输
数据传输
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