eMMC驱动分析
作者:Aningsk ,本作品采用知识共享署名-非商业性使用-相同方式共享 3.0 未本地化版本许可协议进行许可。
基于ATMEL-sama5d3芯片与Linux-3.6.9内核。
SD卡系列简介
这些都是网上找出来的,权当作为开场白了。
MMC卡全称Multi Media Card,由西门子公司和SanDisk公司1997年推出的多媒体记忆卡标准。MMC卡尺寸为32mm x24mm x 1.4mm,它将存贮单元和控制器一同做到了卡上,智能的控制器使得MMC保证兼容性和灵活性。MMC卡具有MMC和SPI两种工作模式,MMC模式是默认工作模式,具有MMC的全部特性。而SPI模式则是MMC协议的一个子集,主要用于低速系统。
SD卡全称Secure Digital Memory Card,由松下、东芝和SanDisk公司于1999年8月共同开发的新一代记忆卡标准,已完全兼容MMC标准。SD卡比MMC卡多了一个进行数据著作权保护的暗号认证功能,读写速度比MMC卡快4倍。尺寸为32mm x 24mm x2.1mm,长宽和MMC卡一样,只是比MMC卡厚了0.7mm,以容纳更大容量的存贮单元。SD卡与MMC卡保持向上兼容,也就是说,MMC卡可以被新的设有SD卡插槽的设备存取,但是SD卡却不可以被设有MMC插槽的设备存取。
SDIO全称Secure Digital Input and Output Card,SDIO是在SD标准上定义了一种外设接口,它使用SD的I/O接口来连接外围设备,并通过SD上的I/O数据接口与这些外围设备传输数据。现在已经有很多手持设备支持SDIO功能,而且许多SDIO外设也被开发出来,目前常见的SDIO外设有:WIFI Card、GPS Card、 Bluetooth Card等等。
eMMC全称Embedded Multi MediaCard,是MMC协会所制定的内嵌式存储器标准规格,主要应用于智能手机和移动嵌入式产品等。eMMC是一种嵌入式非易失性存储系统,由闪存和闪存控制器两部分组成,它的一个明显优势是在封装中集成了一个闪存控制器,它采用JEDEC标准BGA封装,并采用统一闪存接口管理闪存。eMMC结构由一个嵌入式存储解决方案组成,带有MMC接口、快闪存储设备及主控制器,所有这些由一个小型BGA封装。由于采用标准封装,eMMC也很容易升级,并不用改变硬件结构。
MMC/SD通信协议
卡的状态与模式:
工作条件检测:
发送CMD0(reset);
发送CMD8,用于取得SD卡支持的工作电压。
在版本2.0中,发送ACMD41,必须先发送CMD8,有应答则是高容量SD卡。
ACMD41是给卡的控制器一个识别卡是否能在给定电压下工作的机制。
卡根据CMD8的参数检测控制器的电压,如果电压不可以,卡处于Idle状态;如果电压可以,发送回执(check voltage, check pattern),控制器分析回传的CMD8参数校验是否可以在给定的电压下工作。
在ACMD41之后,控制器与卡之间的工作电压将确定。
初始化:
开始于收到ACMD41。
响应CMD8的卡为SDHC卡,ACMD41的HCS部分为1;不响应CMD8的则是普通SD卡;
作为ACMD41的回应,SDHC卡会带有CCS=1。
控制器发送CMD2,处于Ready状态的卡,发送自己的CID作为响应,然后卡进入Identification状态。
控制器发送CMD3,SD卡会发送一个相对地址(RCA)作为响应。卡进入等待状态。
数据传输模式:
CMD7:选择某个卡进入Transfer状态。
CMD9:获取卡的CSD(card specific data),包括块长度、卡的容量等。
CMD12:停止命令。
CMD17:块读命令。
CMD18:多块读命令。
ACMD51:发送scr(SD Configuration Register)
CMD24:块写命令。
CMD25:多块写命令。
CMD32:擦除的起始地址。
CMD33:擦除的截止地址。
CMD38:擦除命令。
仅仅看SPEC上的通信协议可能略有枯燥,结合代码就比较好理解了。关于SD卡的初始化过程参见:http://www.cnblogs.com/fengeryi/p/3469782.html
这个博客详细描述了SD卡的初始化(mmc_sd_init_card())。MMC卡的初始化是mmc_init_card(),大致过程相似,比SD卡的函数长不少,但实际的代码干的事情貌似比SD的那些简单一些。这里我们用到的是MMC卡,它的初始化后面会涉及到的。
关于eMMC的原理图部分:
现在的内核使用设备树文件,来描述ARM平台上的相关硬件设备资源。对于设备树(Device Tree)这个东西资料还不是很多,网上有宋宝华写的有关设备树的文章,挺值得参考的。
按我自己的理解,设备树文件就是用一种类似于C语言的东西来描述具体平台相关的硬件资源。这些文件使用特定的编译器编译为.dtb文件,在uboot加载内核的时候,将dtb文件提供给内核,内核便获知了具体的硬件设置;而不再是以前那样,各种各样的硬件信息统统编译进内核。
所使用的硬件资源,来自于设备树文件sama5d3.dtsi(arch/arm/boot/dts/sama5d3.dtsi)
与MMC设置有关的寄存器地址:0xf0000000;长度0x600。中断号21,触发模式:4(上升沿触发)。中断优先级为0 。拥有的DMA资源是dma0,寄存器的配置为0x10002200 。
关于设备树文件含义的一些说明,在源码目录中的Document/devicetree/bindings/下有TXT类型的描述文档。
MMC驱动的层次结构
CARD层 具体的块设备驱动
CORE层 为card层提供操作接口,为host层注册提供机制
HOST层 MMC/SD/SDIO的控制器驱动层
我觉得之所以将整个的驱动分为三层,是为了驱动能够更好的移植到其他的硬件平台上去。CARD层是通用的MMC块设备驱动,这些代码只要是Linux都可以用的;而HOST层里的代码则是与具体平台有关的,进去看文件与函数的名字就可以猜到——我们这里HOST层中的函数常常是"atmci"开头,意思是ATMEL的媒体控制接口,这是因为我们的平台是ATMEL的ARM芯片;而CORE层,则是提供了MMC/SD的核心机制,并且为CARD层屏蔽了HOST层中具体硬件的差异。基于这些有但不完全的原因,MMC/SD的驱动自然而然的分出了三层,也就是drivers/mmc下的三个文件夹。
Linux内核中的面向对象的思想与实现
在个人学习Linux驱动的过程中,感觉如果只是按照一个一个函数的调用,不断的来回跳,即便是能看懂单个函数的意思,对于整个体系依然是云里雾里,不知内核写了神马东西。若是按照面向对象的思想去理解这些代码,我发现内核的东西就比较容易懂了。
内核虽然是用面向过程的C语言写出来的,但处处体现着面向对象的思想,甚至可以称为一种编程哲学(PS.那些把内核写出来的牛人们,实在太牛了)。Linux不仅仅把设备全部抽象为文件,这种宏观的抽象对象的方法在内核代码里也是比比皆是。个人感觉看代码是在自己脑子里把程序要处理的东西,都抽象为一件件具体的物品,对理解内核代码的来龙去脉,很有帮助。
Linux内核中用面向过程的C语言实现了面向对象的程序架构。一种面向对象的语言,拥有三方面的特性:封装、继承、多态。Linux内核用C语言的基本特性实现了这三种面向对象的特性:使用结构体实现了封装;而某一结构体成员中含有其他结构体的实例,这样实现了一个结构体对另一个结构体的继承;多态的实现则是使用了成员是函数指针的结构体,通过对该结构体中同一个函数指针赋不同的值,调用同一个成员,实际上就会调用不同的函数。当然这些只是我个人的理解了,不一定完全正确。
这种编程思想在驱动中的实现,则主要体现在驱动中对"总线""驱动""设备"这三种结构体处理上:
可以看出来,在驱动的整体框架代码中都是在描述这三个结构体的关系,而功能的实现常常被封装起来。
各种的驱动(device_driver)继承于device_driver结构体;
各种的设备(device)继承于device结构体;
各种的总线(bus)都是bus_type的实例化。
而上述这些都是直接或间接继承于基类kobject。Linux内核通过对kobject的操作,实现了对各部分(包括设备、驱动等)的管理。
驱动与设备在内核中的管理通过总线来实现,对于内核来说,驱动和设备是平级的,因为他们在内核看来都是一个个的继承于kobject的结构体。这一点与我们平常感觉驱动是依附在设备上的直观感觉不同。因为这些都是在代码里体现的,都是虚拟的0与1,驱动和设备以及总线,在内核看来自然差不多了,都是kobject。
在驱动或设备进行probe时,都是沿着总线上的设备和驱动进行匹配的。Linux使用总线的概念来管理设备与驱动的功用。总线就是挂着一串有着相似功用的东西的一根线,同种类的设备与驱动是挂在同一根总线上的,就像拴在一根线上的蚂蚱。在内核看来:这一根上拴的都是绿蚂蚱,那一根上拴的都是黄蚂蚱……想找哪一种蚂蚱,就把哪根线扯出来,再具体看自己是想要上面具体的哪一只蚂蚱。
初始化过程中,添加一个设备主要的工作是初始化继承于device的设备结构体,并把它添加到对应的总线上去。在总线上将去匹配挂在该总线上的驱动。
添加一个驱动的过程与此类似。
MMC/SD驱动的流程
在Linux启动过程中,加载的有关MMC的模块有3个,分别来自:
/drivers/mmc/core/core.c中的subsys_initcall(优先级4)
/drivers/mmc/card/block.c中的module_init(宏定义来自device_initcall)(优先级6)
/drivers/mmc/host/atmel-mci.c中的late_initcall(优先级7)
优先级的数字越小越是优先。描述各种加载优先级的宏,定义在include/linux/init.h中。
在core.c的init中注册了mmc_bus_type总线。
在block.c的init中调用了mmc_blk_init() → mmc_bus_register_driver(),向mmc_bus_type总线注册了mmc的块设备驱动,并有去尝试probe的动作;但是驱动并没有匹配到设备,因为此时对应的设备还没有注册到总线上,Linux此时并不知道设备的存在,因为到此late_initcall还没有执行到,所以并没有进行probe。
在atmel-mci.c的init调用了platform_driver_register(),完成主机的驱动与设备的注册,之后的一系列操作调用到了mmc_attach_mmc() → mmc_add_card()向总线注册了卡设备,并匹配到了之前的卡驱动。后面会继续详细说的。
在代码里可以看到,整个MMC的驱动涉及了两个总线:platform_bus_type总线和mmc_bus_type总线。因为内核将MMC/SD卡控制器抽象为一个platform总线设备,将MMC/SD卡(块设备)抽象为mmc总线设备。
插曲:{
嗯,上面说到了好多次"总线",这里再补充说明一下:这"总线"并不是实实在在存在的物理上的电路线;当然对于I2C、SPI等类型的设备,它们有自己的i2c_bus_type和spi_bus_type,它们也的确有看得见摸得着的、用于连接控制器与物理设备的电线存在;但是内核是不理会任何电气特性的,内核认得的东西都是虚拟抽象出来的概念对象。I2C、SPI使用不同方式进行通信,内核对这些不同的方式(通信协议)有不同的处理与管理的方法,因此归纳为不同的"总线",这个"总线"的概念与物理的"电线"没有逻辑依赖关系。
我看宋宝华的《Linux设备驱动开发详解》有关于platform的讲解,但是我一些内容理解有偏差:"在S3C6410处理器中,把内部集成的I2C、RTC、SPI、LCD、看门狗等控制器都归纳为platform_device"。我当时看了,以为platform总线是比i2c总线更为广泛的概念:i2c总线来自于platform总线。但是当我接触的sama5d3的Linux代码时,发现我之前理解的那样是有问题的,platform总线与i2c总线都是struct bus_type类型的实例,是平级的。Linux引入platform总线是为了描述那些不属于常见总线(常见的,比如i2c、spi等等)的设备的一种虚拟总线。
不过,对于"平级"的说法,是我这个初学者的个人看法哈~我发现在Linux启动的时候对platform总线是有"特别照顾"的,Linux启动时,系统会加载属于platform总线的设备。是不是i2c这样平台上自带的设备也会搭载这个顺风车?我就不是很清楚了,因为我还没有研究过Linux的I2C驱动。个人感觉应该会,毕竟都是平台(platform)上自带的嘛~那么《Linux设备驱动开发详解》我理解的没错只是因为知识不够,所以理解不透啦~!哈哈~我是新手啊~求别喷呀。
}
整个代码运行的大致流程如下:
从控制器的INIT到块设备的probe。橙色方框描述的是要依赖的一些功能。
下面开始一段一段的说了……
函数atmci_init()
(drivers/mmc/host/atmel-mci.c)
在这里的初始化直接对platform_driver的控制器驱动进行了probe。
下面将调用到platfrom_driver_register()。向platform总线注册驱动。凡是在总线注册了的驱动或设备,才是内核所能操作的。这其实是依赖有关于kobject的k_list。
platform_driver_probe() 与 platform_driver_register()
一般来说设备是不能被热插拔的,所以可以将probe()函数放在init中,来节省driver运行时候的内存开销。
一个驱动注册用platform_driver_probe(),在功能和使用上与platform_driver_register()是一样的。唯一的区别是它不能被以后其他的设备probe。也就是说,这个驱动只能和一个设备绑定。而这些的实现就是上图529——534行的代码,可以看看上面的那段注释。
函数platform_driver_register()
(drivers/base/platform.c)
将struct platform_driver类型的drv中的driver(struct device_driver类型)的probe成员、remove成员、shutdown成员,填入platform驱动的函数;然后调用driver_register()函数,将驱动注册到总线。
就是将drv变成platform driver。(重载了platform的操作函数,之后的调用者只管使用,并不关心是哪种总线的驱动。)
函数driver_register()
将驱动添加到总线。
在driver_register()中主要的就是调用:
bus_add_driver(drv);
完成驱动向总线的添加。这方面的实现主要是:
driver_attach(drv);
在总线的设备列表(klist_devices)中遍历,尝试匹配这个驱动。
如果driver_probe_device()返回0,并且dev→driver被设置,就是找到了这个驱动匹配的设备。(这是我翻译的上图中的一句注释)
函数bus_for_each_dev()
可以看出该函数里面最终是调用了__driver_attach(dev,data)
dev来自于klist_devices的设备;
data就是drv。
那这里之前都是注册driver的,运行到这里要在总线上为driver匹配device了。这里是找到设备的,因为我当时打印的Log发现能够继续向下运行,没见有找不到设备的样子。那么这个dev是什么时候加到总线上的?我当时找了很久,没找到。也的确不是前面涉及的那些代码添加的设备,是在Linux系统加载的时候把platform设备加到对应的总线上的。
在init/main.c中
kernel_init()→do_basic_setup()→driver_init()→platform_bus_init()初始化platform总线。在这里目前只找到这些可以确定的信息了。
在这个函数中,将由__driver_attach(dev, drv)来具体地实现设备与驱动的匹配。
函数__driver_attach()
(drivers/base/dd.c)
driver_match_device(drv, dev)中调用了drv→bus→match(dev, drv)。
这个match是platform_match()进行设备名字与驱动名字的比较,相同或者drv→bus→match为空将返回1。
如果dev→driver之前没有被设置,将执行driver_probe_device()。
__driver_attach()
→ driver_probe_device()
→ really_probe()
{
…
if(dev→bus →probe){
ret = dev→bus→probe(dev);
…
} else if {
ret = drv→probe(dev);
…
}
…
}
在这里将会执行drv→probe(dev)(这个probe在platform_driver_register()中被赋值)即platform_drv_probe(),这个函数中调用的其实是drv→probe(dev)。在这里的probe就是atmci_probe()了。
以上调用过程的总结:
在最开始传入的是struct platform_driver *atmci_driver(代表了MMC/SD控制器驱动)。然后使用由Linux kernel提供的platform机制对改驱动进行注册。
完成系统内的注册后,将交给atmci_probe()来处理具体平台相关的工作。
这里对probe函数的处理,就类似于面向对象的"重载"。因为不管是什么样的platform驱动都会调用platform_drv_probe(),此处则是重载到了atmci_probe()。
函数atmci_probe()
这个函数的操作主要是围绕struct atmel_mci *host展开的。这个结构体的含义应该是对控制器状态的描述。
可以分为两段:
第一段是获取资源。第二段是初始化slot。
获取内存资源;
从设备树的节点里读取信息到platform_data;
设置引脚模式;
获取中断资源;
初始化struct atmel_mci host中的锁和队列;
设置中断处理函数、数据传输方式,等。
atmci_init_slot()函数:设置硬件的相关信息,最后用这些信息初始化struct mmc_host *mmc硬件(表示mmc的控制器)。
主要包含mmc_alloc_host()为控制器结构体分配空间,并初始化控制器结构体;
和mmc_add_host()初始化host硬件。
函数mmc_add_host()
在atmci_probe()中调用的atmci_init_slot(),其前半段是获取资源设置相关信息,即函数mmc_alloc_host()。后半段则是使用这些信息去初始化MMC控制器的硬件,使之能够工作,即mmc_add_host()函数。
在mmc_add_host()中主要起作用的是mmc_start_host()
函数mmc_detect_change()
(drivers/mmc/core/core.c)
当有卡插入或移除时,应调用这个函数,MMC层将确定现有的卡正常工作,并且初始化新插入的卡。
最主要的是mmc_schedule_delayed_work(&host→detect, delay)
→ queue_delayed_work(workqueue, work, delay)
→ queue_work(wq, &dwork→work)
mmc_schedule_delayed_work(&host→detect, delay)的意思是,把&host→detect作为一个work传给了queue_delayed_work()。
这个函数将一个工作(work)添加到workqueue这个工作队列中。
在工作队列中,不久将会调用host→detect描述的函数。
那这个detect指向的是哪一个函数?在mmc_alloc_host()中有:
INIT_DELAYED_WORK(&host→detect, mmc_rescan);
含义为:初始化一个工作mmc_rescan,由detect指向它。
所以在工作队列workqueue中mmc_rescan()函数将被执行。在这个函数中将调用mmc_rescan_try_freq(),接下来将会是卡的检测与初始化。
以上阶段的总结:
struct atmci_mci 代表了MMC控制器的接口状态。并非内核中MMC控制器本身,而是其parent device。
在内核看来每一个卡槽(slot)由一个MMC控制器(struct mmc_host)来代表。嗯嗯,我是这样理解的。
函数mmc_rescan()
调用函数mmc_rescan_try_freq() (drivers/mmc/core/core.c)
首先发送CMD0,即mmc_go_idle();
然后发送CMD8,即mmc_send_if_cond();(其实后面还会在发送这些命令的。)
接下来依次探测SDIO、SD、MMC。
我们这里是eMMC,所以进入mmc_attach_mmc():
函数mmc_attach_mmc()
(drivers/mmc/core/mmc.c)
mmc_init_card()对一个卡(struct mmc_card card)进行初始化。
mmc_add_card()将卡设备添加到内核。
函数mmc_init_card()
下面的两个图是极简版的代码(原版的代码好像有460多行,挺长的),有好多重要的东西省掉了,仅仅是为了结合代码标注一下CMD。
CMD0:让卡进入idle状态。
CMD1:让卡发送OCR。
CMD2:向卡询问CID。
CMD3:设置卡的相对地址。
CMD9:获取卡的CSD。
CMD7:选择一个卡。
CMD8:获取扩展的CSD。
主机与卡之间的协调设置。(如:分区、掉电通知、高速等等)
CMD6:为选定的卡修改模式或更改EXT_CSD。(这个命令在mmc_switch()中的,上面的图中被省略了。)
总之,mmc_init_card()完成了对卡的应有的全部初始化工作,包括传输速度、总线宽度等等,但是要更改这些设置是不在这个函数中改的,这里全都是MMC/SD卡协议的落实。真正要改的话是在HOST层中改一些数据,而不是在这里的CORE层中改。在后面会说到如何更改传输速度和总线宽度的。
函数mmc_add_card()
(drivers/mmc/core/bus.c)
mmc_init_card()之后,调用mmc_add_card(),作用是将这个card设备添加到内核,内部调用了device_add将card→dev添加到内核中。
device_add(&card→dev)
→ bus_probe_device(dev)
→ device_attach(dev)
→ bus_for_each_drv(dev→bus, NULL, dev, __device_attach)
__device_attach()与之前的__driver_attach()基本一样。
我们传入的dev是&card→dev,这个在mmc_alloc_card()中被赋值为mmc_bus_type。(mmc.c-866; bus.c-250)
如前面所说的那样,在上图中的driver_match_device()将调用dev的match函数(mmc_bus_match())而这个函数始终返回1(里面什么都没写,除了一句"return 1;")。
所以执行driver_probe_device()。这里将调用really_probe()执行mmc_bus_type结构体中的probe成员(mmc_bus_probe())。
函数mmc_bus_probe()
(drivers/mmc/core/bus.c)
在to_mmc_driver(),将返回含有dev→driver的mmc_driver的指针。这个结构体中的probe()则是mmc_blk_probe()函数。
在mmc_dev_to_card(),将返回含有dev的mmc_card的指针。
在这个始终返回1的match,可以看出:挂在这条总线上的driver都会如此运行,也就是说都会来执行mmc_blk_probe()函数。(之所以要始终返回1,是为了,全部的设备都去匹配MMC的块设备驱动。)
现在,则是由CORE层转入了CARD层了。 struct mmc_driver, mmc_blk_probe()都是在 drivers/mmc/card/block.c中定义的。
函数mmc_blk_probe()
(drivers/mmc/card/block.c)
mmc_blk_alloc()为块设备分配空间,并初始化请求队列。
mmc_add_disk()中调用了add_disk()l函数,说明之前一定有alloc_disk和初始化队列的动作。
其中因为调用了add_disk(),磁盘设备将被"**",并随时会调用它提供的方法。
请求队列就是在mmc_blk_alloc()函数完成的。而其中的主要成分就是mmc_blk_alloc_req()。
函数mmc_blk_alloc_req()
(drivers/mmc/card/block.c)
下图红线标注的依次是:
allco_disk()分配gendisk结构体;
初始化请求队列;
绑定请求函数。
下面是初始化major、fops、queue等。
MMC卡驱动走的块设备驱动的套路:
1.alloc_disk()分配了gendisk结构体。并初始化了major,fops,queue。
2.mmc_init_queue()初始化了队列,并将mmc_blk_issue_rq()绑定为请求函数。
3.调用add_disk()将块设备加到内核。
函数add_disk()
由mmc_add_disk()调用的内核函数。
其完成的任务:
1.验证设备号。 blk_alloc_devt(&disk→part0, &devt);
2.注册bolck_device register_disk(disk);
3.注册请求队列 blk_register_queue(disk);
mmc_blk_probe()
→ mmc_blk_alloc()
→ mmc_blk_alloc_req()
→ alloc_disk()
& mmc_init_queue()
& mmc_add_disk()
→ add_disk()
add_disk()的调用标志着一个块设备驱动将被**。
MMC驱动的加载过程也到此结束。
以上阶段的总结:
对卡插入的支持
函数atmci_init_slot() (drivers/mmc/host/atmel-mci.c)
在中断处理函数atmci_detect_interrupt()中,调用了mod_timer(),这个函数会重新注册定时器到内核,而不管定时器函数是否被运行过。在注册定时器到内核的函数setup_timer()中的回调函数(atmci_detect_change())中,最终调用了mmc_detect_change()。如前所述,此函数会引起函数mmc_rescan()的调用,这个函数就是检测卡是否插入的。
也就是说,detect引脚有中断,就会重新注册定时器到内核,经过一段时间,将运行定时器的回调函数,最终进行了插卡的检测。
不过我们这里是eMMC,是直接焊在板子上的,不会有什么插卡的动作;在这里只是提一下。
MMC/SD卡读写
读写在内核中的处理层次
过程概述:
Linux系统调用(SCI)的实现机制,实际上是多路汇聚以及分解。汇聚点是0x80中断入口,所有的系统调用从用户空间汇聚到0x80中断,中断处理程序运行时,将根据不同的系统调用号分别处理。
例如,当调用read时,库函数在保存read的SCI号及参数后,陷入0x80中断。这时库函数的工作结束,从用户空间进入到内核空间。
0x80中断处理程序,根据系统调用号查询系统调用表。
以read为例,read对应的是sys_read,传递参数并运行sys_read。
最终将一个请求(request)传递给块设备驱动处理。
对应的块设备处理函数为mmc_blk_issue_rq()(drivers/mmc/card/block.c 在上文提到过,被绑定的请求函数)
请求的调用层次,见下图(图中第一项多写了一个"e"):
驱动层上的读写
(将会对上图进行一些说明)
在函数mmc_blk_alloc_req中,调用的mmc_init_queue拉起了一个内核线程,这个线程主要的作用是把上层IO的request一个个地向具体的driver发送。
这个线程叫做"mmcdq",函数mmc_queue_thread是实际做事情的。
线程mmcqd的工作非常简单,在blk_fetch_request(q)获取一个request后,最终通过调用mq→issue_fn(mq, req)向底层发送request。
这个函数便是mmc_blk_issue_rq。大部分request通过
ret = mmc_blk_issue_rw_rq(mq, req);
来发送。
之后会调用到mmc的core部分。
mmc_blk_issue_rw_rq()
→ mmc_start_req()
→ __mmc_start_req()
→ mmc_start_request()
→ host→ops→request(host, mrq)
== atmci_request()
→ atmci_queue_request(host, slot, mrq)
→ atmci_start_request(host, slot)
函数atmci_request() (drivers/mmc/host/atmel-mci.c)
atmci_request()
→ atmci_queue_request()
→ atmci_start_request()
如上图所示在atmel-mci.c, atmci_probe()中就对DMA操作函数进行准备了。
对atmci_start_request()的过程画了一个流程图:
以上过程的总结:
修改MMC的传输速度
函数atmci_init_slot() (drivers/mmc/host/atmel-mci.c)
关于时钟 55MHz与44MHz
(因为发现软件上设置的是55MHz,实际测出来是44MHz)
mmc_set_clock()→atmci_set_ios()。52MHz最终由atmci_set_ios()处理。
这是cpu的spec上的相关说明,其中:
{CLKDIV, CLKODD} + 2 == (CLKDIV * 2 + CLKODD) + 2
drivers/mmc/host/atmel-mci.c
atmci_set_ios()
上图红线标出的三个宏定义为:
#define DIV_ROUND_UP(n, d) (((n) + (d) - 1) / (d))
#define ATMCI_MR_CLKDIV(x) ((x) << 0)
#define ATMCI_MR_CLKODD(x) ((x) << 16)
那么,接下来我们按照代码中的描述,自己计算一下:
(132M + 52M - 1) / 52M = 3
clkdiv = 3 - 2 = 1
1 >> 1 = 0
1 & 1 = 1
mode_reg = (0 << 0) | (1 << 16)
所以写入HSMCI_MR寄存器的是:CLKODD置1;CLKDIV是00000000。
根据cpu的spec,得:
CLKDIV * 2 + CLKODD + 2
= 0 * 2 + 1 + 2
= 3 (3分频)
132MHz / 3 = 44MHz
所以传入的52MHz,实际获得的是44MHz。之前低速的26MHz,实际获得的是22MHz。
在atmci_set_ios()中的这些处理,是因为bus_hz具体是多少,写程序的人是不知道的,这个是由具体的硬件来决定的(在这里就是132MHz)。所以,不能保证设置52MHz,硬件就能真的分频出52MHz,于是设计了上面的运算:根据实际的总线时钟大小(132MHz),和软件中设定的时钟(52/26MHz);计算出一个接近设定的时钟,而且能由总线时钟分频出来的一个值(44/22MHz)。
我在更改过程中遇到的一些错误
错误1
drivers/mmc/core/mmc.c
mmc_init_card()
导致不能启动。
我刚开始改的时候,直接去找时钟在哪里设定的,然后去改那个值。就做出了上图的更改,这样是片面的;因为52MHz同时需要对高速的支持,控制器与卡之间也要保持协调。
错误2
高速8bit模式下,数据传输可能出错(windows循环冗余检查出错)
出现条件:线路与示波器测试头接触时,很可能出现。这个错误只会出现在硬件测量的时候,如果安静地让它去复制,就不会出错,一切正常。
在4bit模式下的数据传输过程中没有出现这个情况。
解决方法:CPU的HSMCI_CFG寄存器HSMODE置位。
嗯嗯,这个驱动就这样粗略地整理完了……
Aningsk
2014-5-13