MMC 卡驱动分析[通俗易懂]

MMC 卡驱动分析[通俗易懂]最近花时间研究了一下MMC卡驱动程序,开始在网上找了很多关于MMC卡驱动的分析文章,但大都是在描述各个层,这对于初学者来讲帮助并不大,所以我就打算把自己的理解写下来,希望对大家有用。个人觉得理解LINUX内核当中MMC/SD卡驱动程序构架是学习MMC卡驱动程序的重点,只有理解了它的基本框架或流程才能真正理解一个块设备驱动程序的写法,同时才能真正理解LINUX设备驱动模型是如

大家好,又见面了,我是你们的朋友全栈君。

最近花时间研究了一下 MMC 卡驱动程序,开始在网上找了很多关于 MMC 卡驱动的分析文章,但大都是在描述各个层,这对于初学者来讲帮助并不大,所以我就打算把自己的理解写下来,希望对大家有用。个人觉得理解 LINUX 内核当中 MMC/SD 卡驱动程序构架是学习 MMC 卡驱动程序的重点,只有理解了它的基本框架或流程才能真正理解一个块设备驱动程序的写法,同时才能真正理解 LINUX 设备驱动模型是如何发挥作用的。
 
一.需要的基础知识:
1.       LINUX 设备驱动的基本结构。
2.       块设备驱动程序的基本构架(相信研究过 LDD3 当中的 sbull 的人应该都不成问题,如果只是走马观花的话,那可得好好再补补了)
3.       LINUX 设备驱动模型。
 
二.驱动程序分析
       首先,来明确一下我们需要分析的文件。下面的文件均来自 linux-2.6.24 源码,我们重点是分析驱动程序的基本构架,所以不同内核版本的差异并不是很大。 MMC/SD 卡驱动程序位于 drivers/mmc 目录下,我们只列出我们分析过程涉及到的几个文件:
Card/
       block.c
       queue.c/queue.h
core/
       bus.c/bus.h
       core.c/core.h
       host.c/host.h
       mmc.c
       mmc_ops.c/mmc_ops.h 拿 MMC 卡来分析, SD 卡驱动程序流程类似。
host/
       s3cmci.c/s3cmci.h 以 S3C24XX 的 MMC/SD 卡控制器为例,其它类型的控制器类似。
LINUX 当中对目录的划分是很有讲究的,这些文件被分布在 3 个目录下,正好对应 MMC/SD 驱动程序的 3 个层次(关于层的划分这里浏览一下,有个概念即可,当我们分析完了后再回头来看,你会觉得很形象):
(1)       区块层
主要是按照 LINUX 块设备驱动程序的框架实现一个卡的块设备驱动,这 block.c 当中我们可以看到写一个块设备驱动程序时需要的 block_device_operations 结构体变量的定义,其中有 open/release/request 函数的实现,而 queue.c 则是对内核提供的请求队列的封装,我们暂时不用深入理解它,只需要知道一个块设备需要一个请求队列就可以了。
(2)       核心层
核心层封装了 MMC/SD 卡的命令,例如存储卡的识别,设置,读写。例如不管什么卡都应该有一些识别,设置,和读写的命令,这些流程都是必须要有的,只是具体对于不同的卡会有一些各自特有的操作。 Core.c 文件是由 sd.c 、 mmc.c 两个文件支撑的, core.c 把 MMC 卡、 SD 卡的共性抽象出来,它们的差别由 sd.c 和 sd_ops.c 、 mmc.c 和 mmc_ops.c 来完成。
(3)       主机控制器层
主机控制器则是依赖于不同的平台的,例如 s3c2410 的卡控制器和 atmel 的卡控制器必定是不一样的,所以要针对不同的控制器来实现。以 s3cmci.c 为例,它首先要进行一些设置,例如中断函数注册,全能控制器等等。然后它会向 core 层注册一个主机( host ),用结构 mmc_host_ops 描述,这样核心层就可以拿着这个 host 来操作 s3c24xx 的卡控制器了,而具体是 s3c24xx 的卡控制器还是 atmel 的卡控制器, core 层是不用知道的。
 
驱动程序层次图
 
       好了,对这几个目录有一个大概认识以后,我们来看几个重要的数据结构:
       struct mmc_host 用来描述卡控制器
struct mmc_card 用来描述卡
struct mmc_driver 用来描述 mmc 卡驱动
struct mmc_host_ops 用来描述卡控制器操作集,用于从主机控制器层向 core 层注册操作函数,从而将 core 层与具体的主机控制器隔离。也就是说 core 要操作主机控制器,就用这个 ops 当中给的函数指针操作,不能直接调用具体主控制器的函数。
      
第一阶段:
       从 s3cmci_init 开始往下看
static int __init s3cmci_init(void)
{
platform_driver_register(&s3cmci_driver_2410);
}
 
有 platform_driver_register 函数,根据设备模型的知识,我们知道那一定会有对应的 platform_device_register 函数的,可是在哪里呢?没有看到,那是不是这个 s3cmci_driver_2410 当中给的 probe 函数就不执行了???当然不是, mci 接口一般都是硬件做好的(我认为是这样),所以在系统启动时一定会有调用 platform_device­_register 对板上的资源进行注册,如果没有这个硬件资源,那我们这个驱动也就没有用了。好,我们就假定是有 mci 接口的,而且也有与 s3cmci_driver_2410 对应的硬件资源注册了,那自己就会去跑 probe 函数。来看一下 s3cmci_driver_2410:
static struct platform_driver s3cmci_driver_2410 = {
       .driver.name    = “s3c2410-sdi”,
       .probe            = s3cmci_probe_2410,
       .remove          = s3cmci_remove,
       .suspend  = s3cmci_suspend,
       .resume          = s3cmci_resume,
};
 
我们到 s3cmci_probe_2410 函数中看,还是干脆直接看 s3cmci_probe 算了:
static int s3cmci_probe(struct platform_device *pdev, int is2440) // 来自 /host/s3cmci.c
{
       struct mmc_host   *mmc;
       struct s3cmci_host       *host;
 
       int ret;
……
       mmc = mmc_alloc_host (sizeof(struct s3cmci_host), &pdev->dev);
       if (!mmc) {
              ret = -ENOMEM;
              goto probe_out;
       }
……
       mmc->ops     = &s3cmci_ops;
……
       ret = mmc_add_host (mmc);
       if (ret) {
              dev_err(&pdev->dev, “failed to add mmc host.\n”);
              goto free_dmabuf;
       }
……
       platform_set_drvdata(pdev, mmc);
       return 0;
……
}
 
这个函数很长,做的事件也很多,但我们关心的整个驱动的构架 / 流程,所以过滤掉一些细节的东西,只看 2 个最重要的函数: mmc_alloc_host 、 mmc_add_host 。函数命名已经很形象了,前者是申请一个 mmc_host ,而后者是添加一个 mmc_host 。中间还有一个操作,就是给 mmc 的 ops  成员赋上了 s3cmci_ops 这个值。申请 mmc_host 当然很简单,就是申请一个结构体(我们暂且这样认为,因为他里面还做的其它事情,后面会看到),而添加又是添加到哪里去呢?看 mmc_add_host 函数:
int mmc_add_host(struct mmc_host *host) // 来自 core/host.c
{
       int err;
……
       err = device_add(&host->class_dev);
       if (err)
              return err;
 
       mmc_start_host(host);
       return 0;
}
 
很简单,就是增加了一个 device ,然后就调用 mmc_start_host 了,那就先跳过 device_add 这个动作,来看 mmc_start_host:
void mmc_start_host(struct mmc_host *host) // 来自 /host/core.c
{
       mmc_power_off(host);                // 掉电一下
       mmc_detect_change(host, 0);              // ???
}
看上去只有两行代码,不过浓缩才是精华, mmc_power_off(host) 光看名子都知道是在干什么,先跳过,来看 mmc_detect_change ,那么它到底干了些什么呢?看一下就知道了:
void mmc_detect_change(struct mmc_host *host, unsigned long delay)    // core/core.c
{
       mmc_schedule_delayed_work(&host->detect, delay);
}
static int mmc_schedule_delayed_work(struct delayed_work *work, unsigned long delay)
{
       return queue_delayed_work(workqueue, work, delay);
}
 
mmc_detect_change 又跳了一下,最后调用了 queue_delayed_work ,不知道这个函数功能的去查一下〈〈 LDD3 〉〉和〈〈深入理解 LINUX 内核〉〉,这几个代码告诉我们在 workqueue 这个工作队列当中添加一个延迟的工作任务,而这个工作任务就是由 host->detect 来描述的,在随后的 delay 个 jiffies 后会有一个记录在 host->detect 里面的函数被执行,那么到这里 s3cmci_probe 这个函数算是结束了,但事情还没有完, workqueue 这个工作队列还在忙,不一会儿它就会调用 host->detect 里面那个函数,这个函数到底是哪个函数,到底是用来干什么的呢?好像没有看到, detect 包含在 host 里面,那估计是在刚才那个申请的地方设置的那个函数,回过头来看一下 mmc_alloc_host:
struct mmc_host *mmc_alloc_host(int extra, struct device *dev)  // 来自 core/host.c
{
       struct mmc_host *host;
 
       host = kzalloc(sizeof(struct mmc_host) + extra, GFP_KERNEL);
       if (!host)
              return NULL;
 
       INIT_DELAYED_WORK(&host->detect, mmc_rescan);
       return host;
}
如果你看了 queue_delayed_work 这个函数功能介绍,相信对 INIT_DELAYED_WORK 也不会陌生了吧。不废话了,来看 mmc_rescan :
// 来自 core/host.c
void mmc_rescan(struct work_struct *work)   // // 来自 core/host.c
{
       struct mmc_host *host =      container_of(work, struct mmc_host, detect.work);
       u32 ocr;
       int err;
……
       /* detect a newly inserted card */
……
       /*
         * First we search for SDIO…
         */
       err = mmc_send_io_op_cond(host, 0, &ocr);
       if (!err) {
              if (mmc_attach_sdio(host, ocr))
                     mmc_power_off(host);
              goto out;
       }
 
       /*
         * …then normal SD…
         */
       err = mmc_send_app_op_cond(host, 0, &ocr);
       if (!err) {
              if (mmc_attach_sd(host, ocr))
                     mmc_power_off(host);
              goto out;
       }
 
       /*
         * …and finally MMC.
         */
       err = mmc_send_op_cond(host, 0, &ocr);
       if (!err) {
              if (mmc_attach_mmc(host, ocr))
                     mmc_power_off(host);
              goto out;
       }
 
       mmc_release_host(host);
       mmc_power_off(host);
 
out:
       if (host->caps & MMC_CAP_NEEDS_POLL)
              mmc_schedule_delayed_work(&host->detect, HZ);
}
 
浏览一个这个函数,看看函数名,再看看注释,知道什么了吗?它是在检测是不是有卡插入了卡控制器,如果有卡挺入就要采取相应的行动了。这里要明白一点,我们平时用的 SD/MMC 卡就是一个卡,如果要操作它得用 SD/MMC 卡控制器才行,所以可以看到有 struct mmc_card,struct mmc_host 的区分。
       到这里了,来回忆一下 s3cmci_probe 这个函数做的事情,大概就是准备一个 mmc_host 结构,然后添加一个主控制器设备到内核,最后又调用了一下 mmc_rescan 来检测是不是有卡插入了。
       如果有卡插入了还好,可以去操作卡了,那如果没有卡插入呢? mmc_rescan 不是白调用了一次吗?是啊,的确是白调用了一次。可是卡插入时为什么 PC 还是能检测到呢?看来卡检测的动作不光是在 probe 的最后一步做了一次,其它地方也有做。卡插入一般都是人为地随时去插入的,像这种情况一般都是会用中断机制去提供系统有外来侵入,然后再去采取行动。 SD/MMC 卡也的确是这样做的,找来找去,发现在 s3cmci_probe 里面注册了一个中断函数 s3cmci_irq_cd( 函数名的意思应该是 irq card detect) ,就是这个了,看看这个函数先:
static irqreturn_t s3cmci_irq_cd(int irq, void *dev_id)   // host/s3cmci.c
{
       struct s3cmci_host *host = (struct s3cmci_host *)dev_id;
 
       mmc_detect_change(host->mmc, msecs_to_jiffies(500));
 
       return IRQ_HANDLED;
}
看到这个函数想都不用想,直接跳到 mmc_rescan 里面去看就行了。前面已经知道了 mmc_rescan 里面就是在检测卡是不是插入了,既然卡随时插入我们都能检测到了,那就来看卡插入后都做了些什么动作吧。
 
第二阶段:
       mmc_rescan 里面既要检测 sd 卡,又要检测 mmc 卡的,我们就照着一个往下走,假定有个人插入了 MMC 卡,那就应该走下面这几行:
       err = mmc_send_op_cond(host, 0, &ocr);
       if (!err) {
              if (mmc_attach_mmc(host, ocr))
                     mmc_power_off(host);
              goto out;
       }
mmc_send_op_cond 这个函数据说是读了一下卡的什么值,这个值是什么意义我也不清楚,这就像检测 FLASH 时读 FLASH 的 ID 一样,网卡也是这样的,不用管这个值的意义了,只要知道它能标识是一个 MMC 卡插入就行了。如果取这个值没有错误的话就得进 mmc_attach_mmc 了:
/*
  * Starting point for MMC card init.
  */
int mmc_attach_mmc(struct mmc_host *host, u32 ocr)  // core/mmc.c
{
       int err;
……
       mmc_attach_bus_ops(host);         // 这个与总线的电源管理有关,暂时跳过
……
       /*
         * Detect and init the card.
         */
       err = mmc_init_card(host, host->ocr, NULL);
       if (err)
              goto err;
……
       mmc_release_host(host);
 
       err = mmc_add_card(host->card);
       if (err)
              goto remove_card;
 
       return 0;
 
remove_card:
……
err:
……
       return err;
}
 
还是找几个关键函数来看 mmc_init_card 从函数名来看就是初始化一个 card ,这个 card 就用 struct mmc_card 结构来描述,然后又调用 mmc_add_card 将卡设备添加到了内核,先来看 mmc_init_card 都做了些什么事情:
static int mmc_init_card(struct mmc_host *host, u32 ocr,
       struct mmc_card *oldcard)
{
       struct mmc_card *card;
       int err;
       u32 cid[4];
       unsigned int max_dtr;
……
              /*
                * Allocate card structure.
                */
              card = mmc_alloc_card(host, &mmc_type);
              if (IS_ERR(card)) {
                     err = PTR_ERR(card);
                     goto err;
              }
 
              card->type = MMC_TYPE_MMC;
              card->rca = 1;
              memcpy(card->raw_cid, cid, sizeof(card->raw_cid));
……
              host->card = card;
 
       return 0;
 
free_card:
……
err:
……
       return err;
}
将与硬件操作相关的全部删掉,最后对我们有用的也就这几行了 mmc_alloc_card 申请了一个 struct mmc_card 结构,然后给 card->type 赋上 MMC_TYPE_MMC ,最后将 card 又赋给了 host->card ,这和具体硬件还是挺像的,因为一个主控制器一般就插一个卡,有卡时 host->card 有值,没有卡时 host->card 自己就是 NULL 了。
       钻进 mmc_alloc_card 里面来看看:
/*
  * Allocate and initialise a new MMC card structure.
  */
struct mmc_card *mmc_alloc_card(struct mmc_host *host, struct device_type *type)
{
       struct mmc_card *card;
 
       card = kzalloc(sizeof(struct mmc_card), GFP_KERNEL);
       if (!card)
              return ERR_PTR(-ENOMEM);
 
       card->host = host;
 
       device_initialize(&card->dev);
 
       card->dev.parent = mmc_classdev(host);
       card->dev.bus = &mmc_bus_type;
       card->dev.release = mmc_release_card;
       card->dev.type = type;
 
       return card;
}
Struct mmc_card 结构里面包含了一个 struct device 结构, mmc_alloc_card 不但申请了内存,而且还填充了 struct device 中的几个成员,尤其 card->dev.bus = &mmc_bus_type; 这一句要重点对待。
       申请一个 mmc_card 结构,并简单初始化后, mmc_init_card 的使命就完成了,然后再调用 mmc_add_card 将这个 card 设备添加到内核。 mmc_add_card 其实很简单,就是调用 device_add 将 card->dev 添加到内核当中去。
       知道总线模型这个东西的人都明白,理到 device_add 里面总线就应该有动作了,具体是哪个总线呢?那就得看你调用 device_add 时送的那个 dev 里面指定的是哪个总线了,我们送的 card->dev ,那么 card->dev.bus 具体指向什么呢?很明现是那个 mmc_bus_type :
static struct bus_type mmc_bus_type = {
       .name             = “mmc”,
       .dev_attrs       = mmc_dev_attrs,
       .match           = mmc_bus_match,
       .uevent           = mmc_bus_uevent,
       .probe            = mmc_bus_probe,
       .remove          = mmc_bus_remove,
       .suspend  = mmc_bus_suspend,
       .resume          = mmc_bus_resume,
};
在 device_add 里面,设备对应的总线会拿着你这个设备和挂在这个总线上的所有驱动程序去匹配( match ),此时会调用 match 函数,如果匹配到了就会调用总线的 probe 函数或驱动的 probe 函数,那我们看一下这里的 mmc_bus_match 是如何进行匹配的:
static int mmc_bus_match(struct device *dev, struct device_driver *drv)
{
       return 1;
}
看来 match 永远都能成功,那就去执行 probe 吧:
static int mmc_bus_probe(struct device *dev)
{
       struct mmc_driver *drv = to_mmc_driver(dev->driver);
       struct mmc_card *card = dev_to_mmc_card(dev);
 
       return drv->probe(card);
}
这里就有点麻烦了,在这个函数里面又调用了一下 drv->probe() ,那这个 drv 是什么呢?上面有: struct mmc_driver *drv = to_mmc_driver(dev->driver);
match 函数总是返回 1 ,那看来只要是挂在这条总线上的 driver 都有可能跑到这里来了,事实的确也是这样的,不过好在挂在这条总线上的 driver 只有一个,它是这样定义的:
static struct mmc_driver mmc_driver = {
       .drv        = {
              .name      = “mmcblk”,
       },
       .probe            = mmc_blk_probe,
       .remove          = mmc_blk_remove,
       .suspend      = mmc_blk_suspend,
       .resume          = mmc_blk_resume,
};
看到这里时, card/core/host 几个已经全部被扯进来了,边看 mmc_driver 中的几个函数,他们几个如何联系起来也就慢慢明白了。那我们继续吧。
 
第三阶段:
前面已经看到了,在总线的 probe 里面调用了 drv->probe, 而这个函数就对应的是 mmc_blk_probe ,具体这个 mmc_driver 是怎么挂到 mmc_bus 上的,自己去看 mmc_blk_init() ,就几行代码,应该不难。
static int mmc_blk_probe(struct mmc_card *card) // 来自 card/block.c
{
       struct mmc_blk_data *md;
       int err;
……
       md = mmc_blk_alloc(card);
       if (IS_ERR(md))
              return PTR_ERR(md);
……
       add_disk(md->disk);
       return 0;
 
  out:
       mmc_blk_put(md);
 
       return err;
}
还是捡重要的函数看,一看到这个函数最后调用了 add_disk ,你应该可以想到些什么吧?如果你不知道我在说些什么,那我估计你没有看过 LDD3 ,或者看了也是走马观花了。我来告诉你:如果看到 add_disk ,那说明前面一定会有 alloc_disk 和初始化队列的动作,在 mmc_blk_probe 时面没有体现出来,那就看 mmc_blk_alloc(card) 那一行:
static struct mmc_blk_data *mmc_blk_alloc(struct mmc_card *card)
{
       struct mmc_blk_data *md;
       int devidx, ret;
 
       devidx = find_first_zero_bit(dev_use, MMC_NUM_MINORS);
       if (devidx >= MMC_NUM_MINORS)
              return ERR_PTR(-ENOSPC);
       __set_bit(devidx, dev_use);
 
       md = kzalloc(sizeof(struct mmc_blk_data), GFP_KERNEL);
       if (!md) {
              ret = -ENOMEM;
              goto out;
       }
 
 
       /*
         * Set the read-only status based on the supported commands
         * and the write protect switch.
         */
       md->read_only = mmc_blk_readonly(card);
 
       md->disk = alloc_disk(1 << MMC_SHIFT);
       if (md->disk == NULL) {
              ret = -ENOMEM;
              goto err_kfree;
       }
 
       spin_lock_init(&md->lock);
       md->usage = 1;
 
       ret = mmc_init_queue(&md->queue, card, &md->lock);
       if (ret)
              goto err_putdisk;
 
       md->queue.issue_fn = mmc_blk_issue_rq;
       md->queue.data = md;
 
       md->disk->major   = MMC_BLOCK_MAJOR;
       md->disk->first_minor = devidx << MMC_SHIFT;
       md->disk->fops = &mmc_bdops;
       md->disk->private_data = md;
       md->disk->queue = md->queue.queue;
       md->disk->driverfs_dev = &card->dev;
 
       /*
         * As discussed on lkml, GENHD_FL_REMOVABLE should:
         *
         * – be set for removable media with permanent block devices
         * – be unset for removable block devices with permanent media
         *
         * Since MMC block devices clearly fall under the second
         * case, we do not set GENHD_FL_REMOVABLE.  Userspace
         * should use the block device creation/destruction hotplug
         * messages to tell when the card is present.
         */
 
       sprintf(md->disk->disk_name, “mmcblk%d”, devidx);
 
       blk_queue_logical_block_size(md->queue.queue, 512);
 
       if (!mmc_card_sd(card) && mmc_card_blockaddr(card)) {
              /*
                * The EXT_CSD sector count is in number or 512 byte
                * sectors.
                */
              set_capacity(md->disk, card->ext_csd.sectors);
       } else {
              /*
                * The CSD capacity field is in units of read_blkbits.
                * set_capacity takes units of 512 bytes.
                */
              set_capacity(md->disk,
                     card->csd.capacity << (card->csd.read_blkbits – 9));
       }
       return md;
 
  err_putdisk:
       put_disk(md->disk);
  err_kfree:
       kfree(md);
  out:
       return ERR_PTR(ret);
}
看到这个函数的代码,我们自然就回忆起了块设备驱动的整个套路了:
1.       分配、初始化请求队列,并绑定请求队列和请求函数。
2.       分配,初始化 gendisk ,给 gendisk 的 major , fops , queue 等成员赋值,最后添加 gendisk 。
3.       注册块设备驱动。
我们看看 MMC 卡驱动程序有没有按这个套路走,
1 、 mmc_init_queue 初始了队列,并将 mmc_blk_issue_rq; 函数绑定成请求函数;
2 、 alloc_disk 分配了 gendisk 结构,并初始化了 major , fops ,和 queue ;
3 、最后调用 add_disk 将块设备加到 KERNEL 中去。
到这里虽然 mmc_blk_probe 已经结束了,但我们别停下来。记得 LDD3 上在讲 sbull 实例时说过, add_disk 的调用标志着一个块设备驱动将被激活,所以在这之前必须把其它所有准备工作全部做好,作者为什么会这样说是有理由的,因为在 add_disk 里面 kernel 会去调用你绑定到队列中的请求函数,目的是去你的块设备上读分区表。而且是在 add_disk 内部就要做的,而不是 add_disk 返回后再做,具体为什么会这样,去看 add_disk 的代码实现就知道了。
既然要调用请求函数去读,那我们就来看看请求函数: mmc_blk_issue_rq
static int mmc_blk_issue_rq(struct mmc_queue *mq, struct request *req)
{
       struct mmc_blk_data *md = mq->data;
       struct mmc_card *card = md->queue.card;
       struct mmc_blk_request brq;
       int ret = 1, disable_multi = 0;
 
       do {
 
              mmc_wait_for_req(card->host, &brq.mrq);
              /*
                * A block was successfully transferred.
                */
              spin_lock_irq(&md->lock);
              ret = __blk_end_request(req, 0, brq.data.bytes_xfered);
              spin_unlock_irq(&md->lock);
       } while (ret);
 
       return 1;
}
这个函数实在太长了,好在我们不用全部看,大部分读数据的准备代码和出错处理的代码已经被我删掉了,只要知道读数据都是在这里完成的就够了。看不懂这个函数的,拿上 LDD3 找个人少的地方,将 sbull 研究透了也就明白这个函数了。不过这个函数里涉及的东西还挺不少,“散列表”,“回弹”都在这里出现了,有时间慢慢去研究吧。
       在块设备驱动当中你只需要抓住请求队列和请求函数就可以了,具体那些 block_device_operations 里面赋值的函数可不像字符设备驱动里面那么受关注了。
 
       分析到这里, MMC/SD 卡的驱动整个构架基本也就很明析了,说简单了就是做了两件事:
1.       卡的检测;
2.       卡数据的读取。
最后再将这两个过程大概串一下:
1.       卡的检测:
S3cmci_probe(host/s3cmci.c)
       Mmc_alloc_host(core/core.c)
              Mmc_rescan(core/core.c)
                     Mmc_attach_mmc(core/mmc.c)
                            Mmc_init_card(core/mmc.c)
                            mmc_add_card(core/bus.c)
                                   device_add
                                          mmc_bus_match(core/bus.c)
                                          mmc_bus_probe(core/bus.c)
                                                 mmc_blk_probe(card/block.c)
                                                        alloc_disk/add_disk
2.       读写数据:
mmc_blk_issue_rq ( card/block.c )
       mmc_wait_for_req(core/core.c)
              mmc_start_request(core/core.c)
                     host->ops->request(host, mrq)   // s3cmci 中 s3cmci_request
 
MMC/SD 卡的驱动分析完了,是不是有些复杂,不过这样设计的目的是为了分层,让具体平台的驱动编写更加省事。
本文来自CSDN博客,转载请标明出处:
http://blog.csdn.net/mmmmpl/archive/2010/09/22/5900760.aspx
 
本文来自CSDN博客,转载请标明出处:
http://blog.csdn.net/xuehua_1008/archive/2011/01/15/6141661.aspx






版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/130444.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • idea创建springboot父子工程_Springboot框架

    idea创建springboot父子工程_Springboot框架在本系列第一篇文章,我们讲解了如何在IDEA中搭建第一个SpringBoot项目:【SpringBoot】一、创建第一个SpringBoot项目,本篇文章,我们讲解如何在IDEA中搭建SpringBoot的父子Module工程项目1、Module工程项目简介多模块项目,适用于一些比较大的项目,通过合理的模块拆分,实现代码的复用,便于维护和管理。尤其是一些开源框架,也是采用多模块的方式,提供插件集成,用户可以根据需要配置指定的模块。2、创建一个SpringBoot项目就是创

    2022年10月13日
  • Python3字符串替换replace(),translate(),re.sub()

    Python3字符串替换replace(),translate(),re.sub()Python3的字符串替换,这里总结了三个函数,和`translate()re.sub()`replace()python中的方法把字符串中的old(旧字符串)替换成new(新字符串

  • 解决VS2005打开js,css,asp.php等文件,中文都是乱码的问题

    解决VS2005打开js,css,asp.php等文件,中文都是乱码的问题

    2021年11月17日
  • 百度爬虫robots.txt文件规范[通俗易懂]

    百度爬虫robots.txt文件规范[通俗易懂]robots.txt文件的格式 robots文件往往放置于根目录下,包含一条或更多的记录,这些记录通过空行分开(以CR,CR/NL, or NL作为结束符),每一条记录的格式如下所示:    “:” 在该文件中可以使用#进行注解,具体使用方法和UNIX中的惯例一样。该文件中的记录通常以一行或多行User-agent开始,后面加上若干Disallow和Allow行,详细情

  • Android 代码混淆规则

    Android 代码混淆规则1.Proguard介绍AndroidSDK自带了混淆工具Proguard。它位于SDK根目录\tools\proguard下面。ProGuard是一个免费的Java类文件收缩,优化,混淆和预校验器。它可以检测并删除未使用的类,字段,方法和属性。它可以优化字节码,并删除未使用的指令。它可以将类、字段和方法使用短无意义的名称进行重命名。最后,预校验的Java6或针对JavaMicroEdi…

  • AE图床-图床聚合源码

    AE图床-图床聚合源码介绍:一个图床聚合程序,自带鉴黄功能,违规图片智能拦截可以直接复制上传以后的链接以及预览支持5个接口上传,图片都支持https永久免费图床,无需注册,批量上传,即时预览,无限流量,无限外链,永久保存,微博服务器,全网CDN,高速稳定,网页上传,无需插件。支持JPG,GIF,PNG等文件格式。支持远程图片上传。微博图床,围脖是个好图床。网盘下载地址:https://zijiewangpan.com/cH4upvpuqQw图片:…

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号