搜索
查看
编辑修改
首页
UNITY
NODEJS
PYTHON
AI
GIT
PHP
GO
CEF3
JAVA
HTML
CSS
搜索
我家小花儿
这个屌丝很懒,什么也没留下!
关注作者
热门标签
jquery
HTML
CSS
PHP
ASP
PYTHON
GO
AI
C
C++
C#
PHOTOSHOP
UNITY
iOS
android
vue
xml
爬虫
SEO
LINUX
WINDOWS
JAVA
MFC
CEF3
CAD
NODEJS
GIT
Pyppeteer
article
热门文章
1
面试要准备复习些啥(前端开发)_前端面试前复习应该怎么复习
2
GIT绑定远端码云官方库方法_git绑定码云
3
软件开发-技术面试问题总结_软件开发面试
4
认识张智勇!
5
Django获取数据库的内容,显示到前端_django查询数据库输出给前端
6
探索数据结构:链式队与循环队列的模拟、实现与应用_数据结构链式队列的实现及应用体会
7
【Android】怎么使APP进行开机启动_android7.0 app开机启动
8
解决vscode 通过Go:Install/Update Tools命令安装失败的问题_go install 失败
9
PostgreSQL数据库安全加固(八)——用户功能与数据库管理功能分开_postgres 加固
10
随机种子的作用,以Pytorch为例
当前位置:
article
> 正文
MMC 卡驱动分析
作者:我家小花儿 | 2024-07-28 22:53:17
赞
踩
MMC 卡驱动分析
最近花时间研究了一下 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
声明:
本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:
https://www.wpsshop.cn/w/我家小花儿/article/detail/896136
推荐阅读
article
uniapp
开发
h5
跳转
微信小程序_
uniapp
h5
跳转
小程序...
uniapp
开发
h5
跳转
微信小程序_
uniapp
h5
跳转
小程序
uniapp
h5
跳转
小程序 ...
赞
踩
article
使用
dokcer
搭建
upload
-
labs
环境(
docker
pull
c0ny1
/
upload
-l...
使用
dokcer
搭建
upload
-
labs
环境(
docker
pull
c0ny1
/
upload
-
labs
)_uploa...
赞
踩
article
流畅
的
Python
(
第二
版)
伴读
之 前言...
Python
is easy, let's dive in!
流畅
的
Python
(
第二
版)
伴读
之 前言 ...
赞
踩
article
10
个
Python
爬虫
入门实例
_
python
爬虫
代码简单例子(1)
_
python
简单的
爬虫
代码...
print( response.status
_
code ) #状态码。
_
python
简单的
爬虫
代码
python
简单的
爬虫
...
赞
踩
article
巡线
机器人
-
PID
控制
- 安卓设置_
巡线
pid...
该项目的目的是构建具有
PID
控制
的线路跟随
机器人
。我们还将使用Android设备轻松设置主要
控制
参数,以便更好,更快地进...
赞
踩
article
一文
彻底
讲透
PyTorch
...
第零章:前置知识人工智能简史相关评价指标常用包的学习Jupyter相关操作第一章:
PyTorch
的简介和安装PyTorc...
赞
踩
article
vue
antd
table
自定义
表头_ant
design
vue
自定义
table
表头...
this.columnDetail.push({ // 表头数据 label: item, width, ...
赞
踩
article
python
自动
截图
保存和换页
_
python
自动
翻页
截图
...
参考 https://blog.csdn.net/weixin
_
44331401/article/details/119...
赞
踩
article
转:逃离故障的十条运维工作
经验总结
_set
sqlprompt
'
rac
-
node1
-
primary
...
逃离故障的十条运维工作
经验总结
故障、于 DBA、于 运维人员 都是 心中永远的痛、而避免故障的原则却是殊途同归 现...
赞
踩
article
Linux
·图解
VMware
安装
Ubuntu
虚拟机
步骤_vcenter
安装
虚拟机
ubuntu
...
图解
VMware
安装
Ubuntu
虚拟机
步骤_vcenter
安装
虚拟机
ubuntu
vcenter
安装
虚拟机
ubun...
赞
踩
article
A
tCoder
Beginner
Contest
363
(
A
~D题)...
枚举长度,首先计算出答案回文数的长度。通过枚举和减法可以计算出是当前长度下第k
A
tCoder
Beginner
Cont...
赞
踩
article
基于宝塔
的
Bytebase
可视化
安装
、
配置
、
部署教程...
Bytebase
是一款使用Go语言开发
的
开源数据库管理软件,旨在为DBA(数据库管理员)
、
开发人员和平台工程师提供一个基...
赞
踩
article
微软
第四季度
财报预览:
增长
动力
追踪...
微软
的“个人计算”领域在2024年第三季度的表现出乎意料的强劲,主要得益于游戏和Windows OEM的优于预期的表现。...
赞
踩
article
Kali
ddos
/
DDOS
拒绝服务
攻击
syn-
flood
洪泛/泛洪
攻击
_kali
ddos
攻击
...
洪泛
攻击
的本质是不相应B回复的SYN+ACK,导致B一直占用其系统资源,迟迟不关闭,且定时发送给A一个SYN+ACK的包...
赞
踩
article
[
网络安全
]
upload
-
labs
Pass
-
19
解题详析_
upload
pass
19
...
以上为[
网络安全
]
upload
-
labs
Pass
-
19
解题详析,后续将分享[
网络安全
]xss-
labs
Pass
-2...
赞
踩
article
矿井通风
安全
计算
系统
matlab
实现_
系统
管理与
维护
模块负责
系统
的
设置
、
调试
、
维护
以及软件更新等
功能
...
一
、
概述矿井通风是保障矿井
安全
生产
的
重要环节,其主要目
的
是将矿井内部有害气体排出,并提供新鲜空气,以确保矿工
的
安全
健康...
赞
踩
article
以用地
业务
链
串接为核心的
自然资源
业务
数据
治理
与
重构
路径_
自然资源
业务
融合 系统
重构
技术路线...
摘要在《深化党和国家机构改革方案》实施前,由于原国土、规划、林业、海洋等部门的服务对象、
业务
规则及执行的行业标准不同,各...
赞
踩
article
【C/C++】函数
的
形参
传
递
值、
传
递
指针
、
传
递
引用
的
区别_函数
形参
里
传
引用
和
传
形参
有什么区别...
1、
传
递
值:把实参
的
值赋值给行参,那么对行参
的
修改,不会影响实参
的
值。单向
传
递
。2.
传
递
指针
(即
传
递
地址):通过
指针
操作...
赞
踩
article
【
Kafka
】
Kafka
再
平衡
机制
及相关
参数
_
kafaka
再
平衡
机制
...
再
平衡
是指的是Consumer Group 下的 Consumer 所订阅的Topic发生变化时 发生的一种分区重分配机...
赞
踩
article
电脑
如何进行
长
截图
?
_
电脑
怎么
长
截图
...
第一种方案:使用搜狗高速浏览器;菜单下面的保存网页另存为图片。第二种方案:使用谷歌浏览器截取网页为图片。第三种方案:首先...
赞
踩
相关标签
uni-app
html5
微信
前端
vue.js
javascript
docker
github
git
python
开发语言
笔记
爬虫
机器人
android
pytorch
人工智能
深度学习
大模型
算法
机器学习
Linux
linux
ubuntu