Linux内核之mmc子系统-sdio
现在的Linux内核中,mmc不仅是一个驱动,而是一个子系统。这里通过分析Linux3.2.0内核,结合TI的arm335x平台及omap_hsmmcd host分析下mmc子系统,重点关注sdio及架构在其上的具体sdio IP驱动实现。
Linux kernel把mmc,sd以及sdio三者的驱动代码整合在一起,俗称mmc子系统。源码位于drivers/mmc下。其下有三个子目录,分别是:
其中,card用于构建一个块设备作为上层与mmc子系统沟通的桥梁;core抽象了mmc,sd,sdio三者的通用操作;host则是各类平台上的host驱动代码,包括如TI Omap的omap_hsmmc,三星的s3cmci等。
即,cpu要访问slave必须通过host进行,包括slave的中断。omap host的具体组成及其与slave之间的连线如图1.1所示:
图1.1
和别的中断控制器一样,host的MPU中断子系统有一个仲裁机制,分别使用IE和ISE控制是否向CPU报中断以及是否care slave报出来的中断。
Host在数据传输时支持8bit模式,但slave是sdio时仅支持1-bit & 4-bit模式,当支持4-bit模式时,data1和IRQ线复用。
注意:MMCHS的smart-idle wake up line(SWAKEUP)没有连出去,在别的一些host中,它是和PRCM子系统连接,用于在MMCHS处于suspend时自动提醒PRCM给MMCHS提供clock。
Linux下的任何驱动在内核中最终都抽象为bus, driver以及device三者间的相互作用。
Mmc子系统涉及到三条总线。
Host驱动相应的driver和device挂载在Linux内核内置的虚拟抽象总线platform_bus_type。两者的匹配采用名称匹配的方式,即driver和device两者的name一样则认为该device对应该driver,这里是”omap_hsmmc”。
Card驱动相应的driver和device挂载在mmc自己创建的虚拟总线mmc_bus_type下,直接匹配。
Sdio驱动相应的driver和device挂载在mmc自己创建的虚拟总线sdio_bus_type下,ID匹配。
注意:Linux内核中,匹配函数默认使用bus注册的匹配函数,如果bus没有注册则使用driver注册的匹配函数。所以,一般自己创建虚拟总线时,其匹配函数和driver的匹配函数都是一致的。
2.2.1 Host device对象
Host device对象首先被初始化并挂载到platform_bus_type,但是这个过程不在mmc子系统源码下,它在平台初始化过程中init了,具体的流程参考5.1。
2.2.2 mmc_bus,sdio_bus对象
core初始化时,件core.c中的subsys_initcall(mmc_init)创建这两条mmc自己的虚拟总线。
2.2.3 card driver对象
Card初始化时,文件block.c中module_init(mmc_blk_init);函数创建mmcblk driver对象,并将之挂载到mmc_bus_type总线上。
2.2.4 host driver对象
Host初始化时,文件omap_hsmmc.c中module_init(omap_hsmmc_init);函数创建”omap_hsmmc”driver对象。
2.2.5 card device对象
到2.2.4为止,host的device和driver都已经创建,匹配后调用host对应的probe函数:omap_hsmmc_probe()。该函数将进行检测slave、初始化slave等操作。
检测sdio并创建card device对象。换言之,这个device对象并没有对应的硬件设备,它只是抽象出来用于和他人交互的一个虚拟device。具体的流程参考5.1。
2.2.6 sdio device对象
紧接着2.2.5后面,在函数sdio_init_func()函数中将创建一个device对象并挂载到sdio_bus_type上。具体的流程参考5.1。
2.2.7 小结
到此为止,mmc下三条总线、六个device/driver对象只差一个sdio driver了。这个driver对象对应着具体的sdio IP驱动,比如sdio_wifi, sdio_uart等。这样来看sdio IP驱动其实是构建在mmc子系统之上的。
从软件层面看mmc内部模块交互以及外部访问mmc的方式,如图3.1所示,。
图3.1
如之前所言,mmc内部的core模块是各类host、sd及sdio操作的抽象,访问host的通用行为都放在其中,而各类host将其各自独特的行为注册到host规定的接口上。Mmc提供了两种方式供外部访问,mmc子系统本身对外呈现的是一个块设备,应用层通过VFS到具体的FS再到块设备逐层访问,块设备通过core让host和slave进行通信,sd卡的访问就是这种典型方式。另外一种方式是不通过块设备访问core,而是在驱动层新建一个驱动,这个驱动构建在mmc之上,让它有权限访问core,具有sdio接口的IP驱动就可以这么做。至于这个sdio的IP驱动对外以何种方式呈现由它自己决定,抽象为一个块设备或者字符设备都可以。
需要注意的是:core内部访问host的操作有睡眠动作,一般的访问行为是这样的:
1) 获取host资源(mmc_claim_host),可能sleep;
2) 发送访问请求(如读/写);
3) 等待(block)本次访问结果(无论slave是否response,host都会通过中断给出返回值);
4) 释放host资源(mmc_release_host)。
sdio事件通过card interrupt通知host,在host已有的中断处理过程中进行,如图4.1所示。
图4.1
但是sdio的中断事件(以CIRQ bit位表示)的处理和别的事件有点差异。Sdio中断事件的处理可能需要sleep。
比如读操作,它的流程是:1)来一个CIRQ表明sdio中的FIFO已经达到阈值;2)host告诉上层这个事件;3)sdio中断处理例程将去sdio FIFO中读取数据,它是通过host的标准操作进行的,如之前所言,这个过程会阻塞。而同样的读操作对sd卡则不需要,它的流程是:1)host发出读操作;2)slave把准备好的数据放到host的FIFO中并assert中断;3)host处理自己的FIFO并通知上层读操作结束。
所以对于sdio而言,host只是充当sdio和上层的沟通媒介,它不是sdio slave的controller,sdio IP有自己的controller,有自己的FIFO。
截至3.12内核版本,Sdio在TI的Omap Soc下仍然没有实现以中断方式处理sdio事件。具体点说,即mmc子系统软件框架已经考虑了sdio中断,但是针对omap平台的host驱动(omap_hsmmc.c)没有支持,它直接忽略了中断寄存器SD_STAT中的CIRQ。
4.2.1为什么host不支持sdio中断?
究其原因,应该是因为某些平台的host没有swakeup line(smart-idle-wakeup),比如arm335x。看一下图4.1所示的mmc子系统的大致硬件架构和host的硬件组成就大致明白了。
Host包括hsmmc和PRCM等子模块,其中PRCM提供clock,电源管理时host可能会进入suspend模式,该模式下PRCM不提供clock,所以此时host将无法处理CIRQ中断,除非上层强制host wake up,否则host不会智能恢复正常工作。而如果有swakeup line的时候,当有CIRQ时,通过swakeup line,PRCM将自动供clock。这一点在图1.1中可以看的更清楚。
4.2.2 mmc子系统如何实现sdio中断方式
假设不考虑上述没有swakeup line,进入suspend状态的hsmmc无法在有卡中断时自动开始恢复工作的情况,或者说我们已经找到了某种解决办法后,考虑下host,sdio等mmc子系统模块如何配合实现使用中断方式处理sdio事件。
图4.2表明了在当前mmc子系统软件架构下可行的中断处理方式。
图4.2
请注意图中给出的注意事项。
5.1.1 Linux内核平台初始化
Linux内核将平台相关的具体数据和模块的特定初始化函数分别放在两个平台相关的文件中。内核提供一个框架,并调用各平台提供的注册函数。
以omap arm33xx为例,板级初始化相关函数放在文件board-am335xevm.c中,由kernel框架调用;板级各模块特定信息放在omap_hwmod_33xx_data.c中,并由omap_hwmod.c中提供的函数进行调用。
注意:现在Linux下已经不用这种方式存储板级信息,取而代之的是device tree,它用一种更简洁的方式来描述这些特定平台信息,从而让kernel代码更加清晰,具体的可以参考http://blog.csdn.net/21cnbao/article/details/8457546。
5.1.2 平台环境文件
平台环境文件board-am335xevm.c定义了各模块的初始化函数,以及提供内核初始化必要的平台初始化函数:
1 MACHINE_START(AM335XEVM, "xxxx") 2 3 /* Maintainer: Texas Instruments */ 4 5 .atag_offset = 0x100, 6 7 .map_io = am335x_evm_map_io, 8 9 .init_early = am33xx_init_early, 10 11 .init_irq = ti81xx_init_irq, 12 13 .handle_irq = omap3_intc_handle_irq, 14 15 .timer = &omap3_am33xx_timer, 16 17 .init_machine = am335x_evm_init, 18 19 MACHINE_END
上面这段代码注册了内核在start_kernel()中会调用的函数,如map_io提供将各IP寄存器地址到内核虚拟地址的转换,init_early()函数由start_kernel()--->setup_arch()--->mdesc->init_early()调用,其余的init_irq,handle_irq()等会在start_kernel()中分别调用。其中init_machine()函数用于初始化各IP模块的device抽象对象,熟悉Linux驱动架构的应该就清楚了,这就是bus,driver,device三剑客之一的device初始化的地方。
举例来说,init_machine()这个函数被调用流程是:start_kernel()--->rest_init()--->kernel_init()-àdo_basic_setup()-àdo_initcalls()--->customize_machine()--->init_machine()。
5.1.3 平台数据文件及其调用者
可以看到init_machine()函数在do_initcalls()中处于第三优先级,第一优先级的是core_init(),它会使用omap_hwmod.c提供的相关函数在device初始化前作一些操作。
omap_hwmod_33xx_data.c大部分内容是定义板子各模块的资源信息如clock、irq号、模块寄存器相应的物理地址等。
do_initcalls()时,core_initcall(omap_hwmod_setup_all)---> _setup()(对每个模块依次作此操作)。
所以平台初始化操作除MACHINE_START提供的几个函数外,还有一个地方就是这里的omap_hwmod_setup_all。
以各模块的sysconfig寄存器初始配置为例:
core_initcall(omap_hwmod_setup_all);
omap_hwmod_for_each(_setup, NULL);//枚举平台omap_hwmod_33xx_data.c中定义的omap_hwmod(每个模块的寄存器设定值)
_setup
++++++++++++ 以下是clock相关 +++++++++
_enable
_enable_sysc
_set_clockactivity
_write_sysconfig
omap_hwmod_write(v, oh, oh->class->sysc->sysc_offs);
注意:omap相关模块寄存器默认值一般情况下并非都为0,以mmc的sysconfig为例,其默认值是0x2015。
如2.2.5所言,host的device和driver创建并匹配后调用host对应的probe函数:omap_hsmmc_probe()。其中一个重要的步骤是启用一个工作队列进行slave检测并创建后续相关device/driver对象。
Host驱动为了兼容多种host controller,有些操作对于SDIO是不需要的(下面用删除线mark了),但是给SDIO这样的操作,SDIO不应报错。具体流程如下所示:
mmc_alloc_host
INIT_DELAYED_WORK(&host->detect, mmc_rescan);
mmc_add_host(mmc);
mmc_start_host(host);
mmc_power_off(host);
mmc_detect_change(host, 0);
mmc_schedule_delayed_work(&host->detect, delay);
mmc_rescan
mmc_rescan_try_freq
sdio_reset(host); //使用CMD52设置slave寄存器6(CCCR)的RES bit
mmc_go_idle(host);
mmc_send_if_cond(host, host->ocr_avail);
mmc_attach_sdio(host)
mmc_send_io_op_cond(host, 0, &ocr);//使用CMD5命令,获取OCR值(slave支持的电压范围)
mmc_attach_bus(host, &mmc_sdio_ops);//设置host的bus操作函数是mmc_sdio_ops
mmc_select_voltage(host, ocr);//设置电压为选取电压(slave本身支持的电压范围 & 平台环境选取的电压ocr_avail==ocr_mask)的最小值
mmc_sdio_init_card(host, host->ocr, NULL, 0);
mmc_alloc_card(host, NULL);//device_init,创建card device,指定为mmc_bus_type
host->ops->init_card == omap_hsmmc_init_card //没设置,为空
mmc_send_relative_addr(host, &card->rca);//CMD3,获取slave的rca
mmc_select_card(card);//CMD7,选中rca对应的slave
sdio_read_cccr(card);//read CCCR
sdio_read_common_cis(card);// read CIS
// set high speed, set bus width and so on...
sdio_init_func
sdio_alloc_func//创建device,指定为sdio_bus_type
sdio_read_fbr
sdio_read_func_cis
mmc_add_card//把card device挂载到mmc_bus_type总线
sdio_add_func//把sdio device挂载到sdio_bus_type总线
- Linux kernel source code(version3.2.0)
- Linux kernel 最新patch
- AM335x ARM® Cortex™-A8 Microprocessors(MPUs) Technical Reference Manual
- SDIO Simplified Specification
- SD Host Controller Simplified Specification