分类: LINUX
2011-10-18 17:56:43
本文先介绍了一下MMC 的基本框架结构,然后采用自底向上的方法来分析整个MMC 层是如何共同作用的。阅读时请结合参考资料1和2.
参考资料:
1.SD Memory Card Specifications / Part 1. Physical Layer Specification; Version 1.0
2.LDD3 CHAPTER-16 BLOCK DEVICE
3.
1.硬件基础:
http://blog.ednchina.com/yelov/198217/message.aspx
2.MMC 子系统的基本框架结构:
很遗憾,内核没有为我们提供关于MMC 子系统的文档,在谷歌上搜索了很多,也没有找到相关文章。只能自己看代码分析了,可能有很多理解不对的地方,希望研究过这方面的朋友多邮件交流一下。
MMC 子系统的代码在kernel/driver/MMC下,目前的MMC 子系统支持一些形式的记忆卡:SD,SDIO,MMC.由于笔者对SDIO的规范不是很清楚,后面的分析中不会涉及。MMC 子系统范围三个部分:
HOST部分是针对不同主机的驱动程序,这一部是驱动程序工程师需要根据自己的特点平台来完成的。
CORE部分:这是整个MMC 的核心存,这部分完成了不同协议和规范的实现,并为HOST层的驱动提供了接口函数。
CARD部分:因为这些记忆卡都是块设备,当然需要提供块设备的驱动程序,这部分就是实现了将你的SD卡如何实现为块设备的。
3.HOST层分析:
HOST层实现的就是我们针对特定主机的驱动程序,这里以mini2440的s3cmci.c为例子进行分析,我们先采用 platform_driver_register(&s3cmci_2440_driver)注册了一个平台设备,接下来重点关注probe函数。在这个函数总,我们与CORE的联系是通过下面三句实现的。首先分配一个mmc_host结构体,注意sizeof(struct s3cmci_host),这样就能在mmc_host中找到了s3cmci_host,嵌入结构和被嵌入的结构体能够找到对方在Linux内核代码中的常用技术了。接下来为mmc->pos赋值,s3cmci_ops结构实现了几个很重要的函数,待会我一一介绍。中间还对mmc 结构的很多成员进行了赋值,最后将mmc 结构加入到MMC 子系统,mmc_alloc_host,以及mmc_add_host 的具体做了什么事情,我们在下节再分析,这三句是些MMC 层驱动必须包含的。
mmc = mmc_alloc_host(sizeof(struct s3cmci_host), &pdev->dev);
mmc->ops = &s3cmci_ops;
……………
s3cmci_ops 中包含了四个函数:
static struct mmc_host_ops s3cmci_ops = {
.request = s3cmci_request,
.set_ios = s3cmci_set_ios,
.get_ro = s3cmci_get_ro,
.get_cd = s3cmci_card_present,
};
我们从简单的开始分析,这些函数都会在core部分被调用:
s3cmci_get_ro:这个函数通过从GPIO读取,来判断我们的卡是否是写保护的
s3cmci_card_present:这个函数通过从GPIO读取来判断卡是否存在
s3cmci_set_ios:s3cmci_set_ios(struct mmc_host *mmc, struct mmc_ios *ios)
依据核心层传递过来的ios,来设置硬件IO,包括引脚配置,使能时钟,和配置总线带宽。
s3cmci_request:这个函数是最主要,也最复杂的函数,实现了命令和数据的发送和接收,
当CORE部分需要发送命令或者传输数据时,都会调用这个函数,并传递mrq 请求。
static void s3cmci_request(struct mmc_host *mmc, struct mmc_request *mrq)
{
struct s3cmci_host *host = mmc_priv(mmc );
host->status = "mmc request";
host->cmd_is_stop = 0;
host->mrq = mrq;
if (s3cmci_card_present(mmc ) == 0) {
dbg(host, dbg_err, "%s: no medium present/n", __func__);
host->mrq->cmd->error = -ENOMEDIUM;
mmc_request_done(mmc , mrq);//如果卡不存在,就终止请求
} else
s3cmci_send_request(mmc );
}
接下来看s3cmci_send_request(mmc ):
这个函数先判断一下请求时传输数据还是命令,如果是数据的话:
先调用 s3cmci_setup_data来对S3C2410_SDIDCON寄存器进行设置,然后设置SDITIMER寄存器这就设置好了总线宽度,是否使用 DMA,,并启动了数据传输模式,并且使能了下面这些中断:
imsk = S3C2410_SDIIMSK_FIFOFAIL | S3C2410_SDIIMSK_DATACRC |
S3C2410_SDIIMSK_DATATIMEOUT | S3C2410_SDIIMSK_DATAFINISH;
解析来判断是否是采用DMA进行数据传输还是采用FIFO进行数据传输
if (host->dodma)
/ because host->dodma = 0,so we don't use it
res = s3cmci_prepare_dma(host, cmd->data);//准备DMA传输,
else
res = s3cmci_prepare_pio(host, cmd->data);.//准备FIFO传输
如果是命令的话:则调用 s3cmci_send_command()这个函数是命令发送的函数,和datesheet上描述的过程差不多,关于SD规范中命令的格式,请参考参考资料1.
writel(cmd->arg, host->base + S3C2410_SDICMDARG);/*先写参数寄存器
ccon = cmd->opcode & S3C2410_SDICMDCON_INDEX;//确定命令种类
ccon |= S3C2410_SDICMDCON_SENDERHOST | S3C2410_SDICMDCON_CMDSTART;
/*with start 2bits*/
if (cmd->flags & MMC_RSP_PRESENT)
ccon |= S3C2410_SDICMDCON_WAITRSP;
/*wait rsp*/
if (cmd->flags & MMC_RSP_136)
ccon |= S3C2410_SDICMDCON_LONGRSP;
//确定 respose的种类
writel(ccon, host->base + S3C2410_SDICMDCON);
命令通道分析完了,我们分析数据通道,先分析采用FIFO方式传输是怎么样实现的。
先分析s3cmci_prepare_pio(host, cmd->data)
根据rw来判断是读还是写
if (rw) {
do_pio_write(host);
/* Determines SDI generate an interrupt if Tx FIFO fills half*/
enable_imask(host, S3C2410_SDIIMSK_TXFIFOHALF);
} else {
enable_imask(host, S3C2410_SDIIMSK_RXFIFOHALF
| S3C2410_SDIIMSK_RXFIFOLAST);
}
如果是写数据到SD的话,会调用do_pio_write,往 FIFO中填充数据。当64字节的FIFO少于33字节时就会产生中断。如果是从SD读数据,则先使能中断,当FIFO多于31字节时时,则会调用中断服务程序,中断服务程序中将会调用do_pio_read FIFO的数据读出。
接下来分析do_pio_write:
to_ptr = host->base + host->sdidata;
fifo_free(host)用来检测fifo剩余空间
while ((fifo = fifo_free(host)) > 3) {
if (!host->pio_bytes) {
res = get_data_buffer(host, &host->pio_bytes,
/* If we have reached the end of the block, we have to
* write exactly the remaining number of bytes. If we
* in the middle of the block, we have to write full
* words, so round down to an even multiple of 4. */
if (fifo >= host->pio_bytes)//fifo的空间比pio_bytes大,表明这是读这个块的最后一次
fifo = host->pio_bytes;
/* because the volume of FIFO can contain the remaning block*/
else
fifo -= fifo & 3;/*round down to an even multiple of 4*/
host->pio_bytes -= fifo;//更新还剩余的没有写完的字
host->pio_count += fifo;/*chang the value of pio_bytes*/
fifo = (fifo + 3) >> 2;//将字节数转化为字数
/*how many words fifo contain,every time we just writ one word*/
ptr = host->pio_ptr;
while (fifo--)
writel(*ptr++, to_ptr);//写往FIFO.
host->pio_ptr = ptr;
}
注释一:注意,MMC 核心为 mrq->data成员分配了一个struct scatterlist的表,用来支持分散聚集,使用这种方法,这样使物理上不一致的内存页,被组装成一个连续的数组,避免了分配大的缓冲区的问题
我们看代码
if (host->pio_sgptr >= host->mrq->data->sg_len) {
dbg(host, dbg_debug, "no more buffers (%i/%i)/n",
host->pio_sgptr, host->mrq->data->sg_len);
return -EBUSY;
}
sg = &host->mrq->data->sg[host->pio_sgptr];
*bytes = sg->length;//页缓冲区中的长度
*pointer = sg_virt(sg);将页地址映射为虚拟地址
host->pio_sgptr++; 这里表明我们的程序又完成了一次映射
这样,每一个mmc 请求,我们只能处理 scatterlist表中的一个页(块)。因此,完成一次完整的请求需要映射sg_len次
再来总结一下一个mmc 写设备请求的过程:
在s3cmci_prepare_pio中我们第一次先调用 do_pio_write,如果FIFO空间大于3,且能够获取到scatterlist,则我们就开始往FIFO写数据,当FIFO空间小于3,则使能 TXFIFOHALF中断,在中断服务程序中,如果检测到TFDET表明又有FIFO空间了,则关闭TXFIFOHALF中断,并调用 do_pio_write进行写。
数据流向如下:scatterlist-------->fifo---------->sdcard
一个mmc 读设备请求的过程数据流向如下:sdcard --------> fifo ---------->scatterlist,
????关于读数据的过程,中断的触发不是很清楚,s3cmci_prepare_pio 中enable_imask(host, S3C2410_SDIIMSK_RXFIFOHALF,S3C2410_SDIIMSK_RXFIFOLAST);但如果没从SD卡中读数据,怎么会引发这个中断呢?是由S3C2410_SDIIMSK_RXFIFOLAST引起的吗
接下来我们分析一下中断服务程序:
static irqreturn_t s3cmci_irq(int irq, void *dev_id)
该程序先获取所有的状态寄存器:
mci_csta = readl(host->base + S3C2410_SDICMDSTAT);
mci_dsta = readl(host->base + S3C2410_SDIDSTA);
mci_dcnt = readl(host->base + S3C2410_SDIDCNT);
mci_fsta = readl(host->base + S3C2410_SDIFSTA);
mci_imsk = readl(host->base + host->sdiimsk);
这些将作为中断处理的依据。
如果不是DMA模式,则处理数据的收发
if (!host->dodma) {
if ((host->pio_active == XFER_WRITE) &&
(mci_fsta & S3C2410_SDIFSTA_TFDET)) {
/*This bit indicates that FIFO data is available for transmit when
DatMode is data transmit mode. If DMA mode is enable, sd
host requests DMA operation.*/
disable_imask(host, S3C2410_SDIIMSK_TXFIFOHALF);
tasklet_schedule(&host->pio_tasklet);
注意我们采用tasklet这种延时机制来减少中断服务的时间,延时函数pio_tasklet中调用了do_pio_write和了 do_pio_read
host->status = "pio tx";
}
if ((host->pio_active == XFER_READ) &&
(mci_fsta & S3C2410_SDIFSTA_RFDET)) {
disable_imask(host,
S3C2410_SDIIMSK_RXFIFOHALF |
S3C2410_SDIIMSK_RXFIFOLAST);
tasklet_schedule(&host->pio_tasklet);
host->status = "pio rx";
}
接下来的很多代码是对其他的一些类型中断的处理。
最后来分析DMA模式:这种模式下不需要CPU的干预。S3C2440的DMA有4个通道,我们选择了通道0
static int s3cmci_prepare_dma(struct s3cmci_host *host, struct mmc_data *data)
{
int dma_len, i;
int rw = (data->flags & MMC_DATA_WRITE) ? 1 : 0;
BUG_ON((data->flags & BOTH_DIR) == BOTH_DIR);
s3cmci_dma_setup(host, rw ? S3C2410_DMASRC_MEM : S3C2410_DMASRC_HW);//注一
s3c2410_dma_ctrl(host->dma, S3C2410_DMAOP_FLUSH);
dma_len = dma_map_sg(mmc_dev(host->mmc), data->sg, data->sg_len,
(rw) ? DMA_TO_DEVICE : DMA_FROM_DEVICE);//注二
if (dma_len == 0)
return -ENOMEM;
host->dma_complete = 0;
host->dmatogo = dma_len;
for (i = 0; i < dma_len; i++) {
int res;
dbg(host, dbg_dma, "enqueue %i:%u@%u/n", i,
sg_dma_address(&data->sg),
sg_dma_len(&data->sg));
res = s3c2410_dma_enqueue(host->dma, (void *) host,
sg_dma_address(&data->sg),
sg_dma_len(&data->sg));
if (res) {
s3c2410_dma_ctrl(host->dma, S3C2410_DMAOP_FLUSH);
return -EBUSY;
}
}
s3c2410_dma_ctrl(host->dma, S3C2410_DMAOP_START);
return 0;
}
注一:这个函数先调用 s3c2410_dma_devconfig来配置DMA 源/目的的意见类型和地址,注意我们这里的设备地址host->mem->start + host->sdidata实际上就是SDIDATA寄存器的地址值,如果是写SD卡,则为目的地址,否则为源地址。然后调用 s3c2410_dma_set_buffdone_fn(host->dma, s3cmci_dma_done_callback);
设置dma通道0的回调函数。
注二:
dma_len = dma_map_sg(mmc_dev(host->mmc), data->sg, data->sg_len,
(rw) ? DMA_TO_DEVICE : DMA_FROM_DEVICE);
这里进行分散/聚集映射(P444,LDD3),返回值是传送的DMA缓冲区数,可能会小于sg_len,也就是说sg_len与dma_len可能是不同的。
sg_dma_address(&data->sg),返回的是总线(DMA)地址
sg_dma_len(&data->sg)); 返回的是缓冲区的长度。
最后调用 s3c2410_dma_enqueue(host->dma, (void *) host,
sg_dma_address(&data->sg),
sg_dma_len(&data->sg));
对每个DMA缓冲区进行排队,等待处理。
s3c2410_dma_ctrl(host->dma, S3C2410_DMAOP_START);启动DMA
这样DMA缓冲区就和scatterlist联系起来,当写数据时,scatterlist中的数据由于上面的映射关系会直接“拷贝”到DMA缓冲区,当读数据时则反之。整个过程不需要CPU干预,自动完成。