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通信协议

卡的状态与模式:

eMMC驱动分析_第1张图片

工作条件检测:

发送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的原理图部分:

eMMC驱动分析_第2张图片

现在的内核使用设备树文件,来描述ARM平台上的相关硬件设备资源。对于设备树(Device Tree)这个东西资料还不是很多,网上有宋宝华写的有关设备树的文章,挺值得参考的。

按我自己的理解,设备树文件就是用一种类似于C语言的东西来描述具体平台相关的硬件资源。这些文件使用特定的编译器编译为.dtb文件,在uboot加载内核的时候,将dtb文件提供给内核,内核便获知了具体的硬件设置;而不再是以前那样,各种各样的硬件信息统统编译进内核。

 

所使用的硬件资源,来自于设备树文件sama5d3.dtsi(arch/arm/boot/dts/sama5d3.dtsi)

eMMC驱动分析_第3张图片

与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下的三个文件夹。

eMMC驱动分析_第4张图片

 

 

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中。

eMMC驱动分析_第5张图片

在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设备驱动开发详解》我理解的没错只是因为知识不够,所以理解不透啦~!哈哈~我是新手啊~求别喷呀。

}

eMMC驱动分析_第6张图片

 

 

 

 

整个代码运行的大致流程如下:

从控制器的INIT到块设备的probe。橙色方框描述的是要依赖的一些功能。

eMMC驱动分析_第7张图片

下面开始一段一段的说了……

 

 

函数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

 

你可能感兴趣的:(瞎搞滴啦)