本文主要介绍一下Linux
内核的mmc
子系统驱动的整体框架。
(作者对SDIO
设备不熟悉,所以不过多描述;鄙人才疏学浅,有不当之处,还请指教。)
大概包括以下几个部分:
mmc
子系统驱动简介mmc
设备的识别、初始化流程的大概介绍mmc
设备的读写流程大概介绍mmc
子系统驱动简介mmc
子系统驱动分为三层,分别为:
MMC
控制器,是比较底层的寄存器操作及中断操作。mmc
子系统实际包含三种设备的驱动:
mmc
设备SD
设备SDIO
设备MMC、SD、SDIO的技术本质是一样的(使用相同的总线规范,等等),都是从MMC规范演化而来;MMC强调的是多媒体存储(MM,MultiMedia),SD强调的是安全和数据保护(S,Secure);SDIO是从SD演化出来的,强调的是接口(IO,Input/Output),不再关注另一端的具体形态(可以
是WIFI设备、Bluetooth设备、GPS等等)。
虽然这三种设备共用这一套子系统,但对应到实际的设备时,这三种不同的设备有不同的物理层规范协议:
eMMC
:
https://www.jedec.org/
eMMC
物理层协议V5.1版本:
JESD84-B51.pdf
SD
:
https://www.sdcard.org/
SD
物理层协议V3.1版本:
Part_1_Physical_Layer_Simplified_Specification_Ver_3.01.pdf
SDIO
:
https://www.sdcard.org/
SDIO
物理层协议V2.0版本:
Part_E1_SDIO_Simplified_Specification_Ver2.00.pdf
mmc
设备的初始化、识别流程的大概介绍这里主要介绍代码中的初始化及识别流程,主要以hi3559av100
的代码为例。
在设备启动过程中,会执行sdhci_hisi_init
函数注册平台驱动设备,注册完后,系统总线会执行probe
进行探测设备,这里的probe
函数注册为sdhci_hisi_probe
,通过跟代码,可以看到其实这个的调用流程如下所示:
可以看到这里是初始化了一个延迟工作队列,任务是调用mmc_rescan
函数进行识别设备流程。
mmc_rescan
函数就是用来扫描识别eMMC
、SD
、SDIO
设备的,设备识别时会一个较低的频率去与设备交互,当设备识别成功后,可以将工作频率提高。这里会先以400KHz~100KHz
这四种频率去与设备交互(注1),每次都调用mmc_rescan_try_freq
去扫描设备。
注1:虽然代码上会以这四个频率去识别设备,但实际频率往往取决于芯片本身提供的频率。比如如果芯片的
MMC
控制器模块在初始化阶段只能提供300KHz
的频率,那么实际工作就是一直以300KHz
的频率去识别设备。
实际扫描时会先尝试识别SDIO
设备,如果成功则返回;否则,继续尝试识别SD
设备,如果成功则返回;否则,继续尝试识别MMC
设备,如果成功则返回;否则返回错误。当然,识别前会先对设备上电,硬件复位等,如果都没有识别到设备,就对设备下电。比如海思有专门的寄存器(PWR_CTRL_R
)来控制eMMC
或者SD
的电源引脚,来控制设备的上下电。
正常eMMC
设备或者SOC芯片上电时已经在卡槽的SD
设备,这里已经被识别成功了。而热插拔的设备还需要另外的工作。
这里大概分三个情况来描述热插拔识别流程:
海思的SOC
大部分目前都支持有专门的引脚用来卡插入拔出检测,通过读取PASTATE_R
寄存器或者配置中断的方式进行。这里是以中断检测的方式进行卡插入拔出检测:
可以看到,接收到插入或移除的中断后,中断处理中重新启动detect
工作任务去扫描设备。
中断处理的代码如下:
static irqreturn_t sdhci_thread_irq(int irq, void *dev_id)
{
...
if (isr & (SDHCI_INT_CARD_INSERT | SDHCI_INT_CARD_REMOVE)) {
struct mmc_host *mmc = host->mmc;
mmc->ops->card_event(mmc);
mmc_detect_change(mmc, msecs_to_jiffies(200));
}
...
return isr ? IRQ_HANDLED : IRQ_NONE;
}
有的芯片没有专门的检测引脚,需要用普通的GPIO引脚作为检测引脚。这样的话可以在设备树的节点里配置cd
的引脚号,并配置触发电平,比如cd-gpios = <&gpio6 27 GPIO_ACTIVE_LOW>
。而mmc驱动中会解析此节点的值,并申请gpio中断。
int mmc_of_parse(struct mmc_host *host)
{
...
ret = mmc_gpiod_request_cd(host, "cd", 0, true,
0, &cd_gpio_invert);
...
}
若卡槽没有专门的检测引脚或者SOC上的没有预留检测引脚,可以用轮询的方式来检测设备的热插拔。
在上面的mmc_rescan
函数中,可以看到执行到最后有一个条件判断:根据是否有MMC_CAP_NEEDS_POLL
能力,来再次调度host->detect
任务。而这个能力可以通过设备树节点赋值broken-cd
能力获得,或者直接在host
初始化时对他的caps
能力赋值。
void mmc_rescan(struct work_struct *work)
{
...
out:
if (host->caps & MMC_CAP_NEEDS_POLL)
mmc_schedule_delayed_work(&host->detect, HZ);
}
mmc
设备的读写流程大概介绍从开始的子系统简介里面可以看到上下层之间的交互大概流程,无论用户空间是通过文件系统操作mmc
设备,都会经过通用块层。这里就对上面的交互流程进行稍微的细化,主要以读写数据为主。
从上图可以看到,读写数据的流程主要经过以下几层次:(下面的内容也是围绕上面的图来进行)
无论是经过文件系统读写,还是直接用open(block_dev)
的方式读写mmc
设备,最后都是会调用submit_bio
函数提交IO请求,最后由内核提交到具体的驱动,这里是mmc
驱动:
如果submit_bio
是提交的读操作,那么此时的空间已经由内核分配好;如果提交的是写操作,那么用户空间的数据已经拷贝到内核空间。
图中绿色函数blk_queue_bio
,是通用块层提供的接口,最终会调用到红色的函数mmc_request_fn
,即到了具体的驱动:mmc
驱动层了。
这个mmc_request_fn
函数是在mmd驱动的block层初始化时调用通用块层的接口初始化时传入:
在通用块层调用mmc_request_fn
之后,接着便唤醒mmc
的读写线程mmcqd/%d%s
,线程执行mmc_queue_thread
函数,从request_queue
请求队列中获取单个请求request
,并将请求通过mmc_blk_issue_rq
发送出去,最后经过mmc_blk_issue_rw_rq
发送到mmc core层。
在这里,会将上层传下来的request
请求解析:确定请求的读写方向、单块读写还是多块读写、读写命令的起始地址(在mmc设备上的地址)和数据长度、确定DMA搬移地址、设置超时等工作。
mmc core层主要工作还是为协议服务。
mmc_start_req
函数发起一个非阻塞的请求,此时的请求已经不是上层传下来的request
请求,而是一个只在mmc驱动范围内作用的、异步的请求mmc_async_req
。如果发送成功这将这次发送的请求返回,否则返回空。
到了mmc host层,则主要是soc控制器的寄存器相以及中断等操作。在这里,DMA搬移的数据目的地址或者源地址都以及经过dma_map_sg
等相关函数映射到scatterlist
结构体里,DMA可以直接操作映射后的地址,并且可以避免申请连续大块内存的需求。
在eMMC/SD
协议中,发送读写数据时会预先发送命令告诉eMMC/SD
设备,命令中会带有将要读写数据的地址,如果设备响应OK,在进行数据交互。读写数据的量可以由两种方式决定,一个是通过CMD23
命令并带上要读写的数据量,提前告诉eMMC/SD
设备;第二个是通过CMD12
命令来直接告诉eMMC/SD
设备停止发送/接收数据。
控制器发送/接受完后会产生数据中断,在中断处理函数里,会调用mmc core层的接口,通知上层完成数据的传输。
前面mmc block层提到,mmc_queue_thread
函数,从request_queue
请求队列中获取单个请求request
并发送。如果request
为空,但前一个请求没有发送完,这时候会发送一个空的request
请求。当数据传输完成后,mmc host层调用mmc core层的mmc_request_done接口,标志数据传输结束,此时函数返回到mmc_blk_issue_rw_rq
,并调用通用块层的blk_end_request
接口,表示数据以及从设备返回,这时候就代表着一整个读写流程的结束。
在通用块层到mmc驱动层再到硬件DMA,经过了许多个数据结构:
通用块层使用submit_bio
函数提交IO请求时,使用的结构体是struct bio
,然后经过blk_queue_bio
函数的处理,将bio
里面IO请求合并到struct request_queue
请求队列里面。到mmc驱动层时,会从request_queue
请求队列里面将单个的struct request
请求通过blk_fetch_request
函数取出,然后request
指针会存放到struct mmc_request
,同时,在mmc的block层准备数据时,会将request
映射到struct scatterlist
,scatterlist
指针存放到struct mmc_data
中。在准备DMA传输时,再将scatterlist
使用dma_map_sg
进行映射,交给DMA传输。
每个struct request
都会有一个叫__sector
的变量,用来记录读写磁盘的位置的。
struct request
同时记录有struct hd_struct *part
,struct hd_struct
结构体里面也有一个关于位置的变量sector_t start_sect
,而这个位置是分区在磁盘上的起始位置。
在前面数据传输过程的了解以及相关数据结构体的大概关系理清楚后,可以做一个对磁盘数据解密的需求。
大概过程如下:
关键点:
做法:
request
都会记录到所属的分区hd_struct
,这个结构体中的partition_meta_info
里面记录了当前分区的名字,可以根据名字来判断是否需要解密。sector_t start_sect
和request
的请求位置即可换算出来。blk_end_request
之前执行解密即可。mmc_rescan_try_freq
扫描过程就是一个上电->复位->发送命令尝试->成功后按照协议继续发命令的一个流程;如果失败了就执行下电,再次尝试。
SD卡支持锁定功能,但目前的内核代码是没有支持的,可以通过修改内核代码来实现SD卡锁定功能。
更多细节在协议上的4.3.7 Card Lock/Unlock Operation
有描述,过程不复杂,但用户密码管理不友好。
当然,还有另外的SD卡安全协议内容:Part_3_Security_Specification_Ver3.00
不在本篇讨论范围内。
的实现
SD卡支持锁定功能,但目前的内核代码是没有支持的,可以通过修改内核代码来实现SD卡锁定功能。
更多细节在协议上的4.3.7 Card Lock/Unlock Operation
有描述,过程不复杂,但用户密码管理不友好。
当然,还有另外的SD卡安全协议内容:Part_3_Security_Specification_Ver3.00
不在本篇讨论范围内。