Linux MMC 驱动子系统详解

文章详细介绍了Linux内核中的MMC驱动子系统,包括硬件关联、逻辑架构、设备-总线-驱动模型。重点讲解了MMC、SDIO驱动的抽象模型,以及MMC设备控制器(mmchost)的结构体定义和注册过程。内容涵盖mmc、sd、sdio的协议操作,DTS配置以及mmc_card的注册与探测机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Linux MMC 驱动子系统

SD/SDIO/MMC 驱动是一种基于 SDMMC 和 SD SPI 主机驱动的协议级驱动程序,目前已支持 SD 存储器、SDIO 卡和 eMMC 芯片。

因为linux内核mmc子系统里面已经实现了这些协议,我们以后并不需要重新实现这些,只需要对协议有个简单的了解。

mmc是比较老的存储卡了,sd是mmc的替代者,sdio是基于sd而额外开发出的一种io接口卡。

硬件关联

CPU、MMC controller、存储设备之间的关联如下图所示,主要包括了MMC controller、总线、存储卡等内容的连接,针对控制器与设备的总线连接,主要包括时钟、数据、命令三种类型的引脚,而这些引脚中的cd引脚主要用于卡的在位检测,当mmc controller检测到该位的变化后,则会进行mmc card的注册或注销操作。

cpu mmc硬件关联图

目录说明

针对mmc子系统,在代码实现上主要包括mmc core、mmc block、 mmc host这三个模块

  • mmc card:衔接最上层应用,主要用于实现mmc block驱动以及mmc driver即mmc层驱动(实际上我研究的源代码并没有这个目录(5.15.0-52-generic),猜测是合并到了core目录下);
  • 而mmc core:实现mmc/sd/sdio协议,主要包括mmc 总线、sdio总线的实现、mmc device、mmc driver的注册接口、mmc host与mmc card的注册与注销接口等内容。
  • mmc host:存放各个mmc/sd/sdio控制器的驱动代码,最终操作mmc/sd/sdio卡的部分;

mmc子系统的逻辑架构

MMC子系统从上到下分为3层

  • 块设备层(MMC card):与Linux的块设备子系统对接,实现块设备驱动以及完成请求,如sys_open调用;通过调用core接口函数(具体如host->ops->rquest),驱动MMC core抽象出来的虚拟的card设备,如mmc、sd、tf卡,实现读写数据。

  • 核心层(MMC core):是不同协议和规范的实现,为MMC控制器层和块设备驱动层提供接口函数。

    核心层封装了 MMC/SD 卡的命令(CMD),例如存储卡的识别、设置、读写、识别、设置等命令。

    MMC核心层由三个部分组成:MMC,SD和SDIO,分别为三类设备驱动提供接口函数;

    core.c 把 MMC 卡、 SD 卡的共性抽象出来,它们的差别由 sd.c 和 sd_ops.c 、 mmc.c 和 mmc_ops.c 来完成。

  • 控制器层(MMC host):主机端MMC controller的驱动,依赖于平台,由struct mmc_host描述。

    围绕此结构设计了struct mmc_host_ops(访问方法)、struct mmc_ios(相关参数)、struct mmc_bus_ops(电源管理和在位检测方法)

针对不同芯片,实现不同控制器对应的驱动代码。

mmc子系统框架结构图

块设备层与Linux的块设备子系统对接,实现块设备驱动以及完成请求,具体协议经过核心层的接口,最终通过控制器层完成传输,对MMC设备进行实际的操作。

更详细的结构图如下,指明了个部分的相关实现文件:

在这里插入图片描述

mmc core指的是mmc 子系统的核心,这里的mmc表示的是mmc总线、结构、设备相关的统称,而下方文件名的mmc单指mmc卡,区别于sd卡和sdio卡。

drivers/mmc/core/mmc.c(提供接口),
drivers/mmc/core/mmc-ops.c(提供和mmc type card协议相关的操作)

在mmc core层中的bus指的是由core抽象出来的虚拟总线,而与物理卡连接的MMC bus是物理的实际总线,是和host controller直接关联的。

设备-总线-驱动模型

针对MMC子系统而言,主要使用了系统中的两个模型:设备-总线-驱动模型块设备驱动模型。

在Linux驱动模型框架下,三者对应结构体以及MMC驱动子系统对应的实现关系如下:

  • 总线 (struct bus_type) —— MMC总线( mmc_bus )
  • 设备(struct device) —— 被封装在platform_device下的主设备 host
  • 驱动 (struct device_driver) —— 依附于MMC总线的MMC驱动( mmc_driver )

三者之间的关联图如下,每一个具体的总线均包括设备与驱动两部分,而每一个具体总线的所有添加的设备均链接至device下,每一个总线的所有注册的驱动均链接至drivers,而bus接口所有实现的功能也可以大致分为总线的注册、设备的注册、驱动的注册这三个部分。

设备-总线-驱动关联图

设备和对应的驱动必须依附于同一种总线

一、MMC驱动抽象模型

MMC驱动模型也是基于实际的硬件连接进行抽象的

  • 针对通信总线,抽象出mmc_bus
  • 针对mmc controller,该子系统抽象为mmc_host,用于描述一个进行设备通信的控制器,提供了相应的访问接口(记为mmc_host->request);
  • 针对mmc、sd、tf卡具体设备,该子系统抽象为mmc_card,用于描述卡信息。mmc子系统提供年rescan接口用于mmc card的注册;
  • 针对mmc、sd、tf,mmc子系统完成了统一的mmc driver,针对mmc总线规范以及SD规范,其已经详细的定义了一个存储卡的通信方式、通信命令,因此LINUXmmc子系统定义了mmc driver,用于和mmc、sd、tf等卡的通信,而不需要驱动开发人员来开发卡驱动。

bus driver host card 关联图

特点:

  1. mmc总线模型仅注册一个驱动类型,即mmc driver
  2. 一个mmc host与一个mmc card绑定
  3. mmc card属于热插拔的设备,而mmc card的创建主要由mmc host负责探测与创建,mmc host根据卡在位检测引脚,当检测到mmc card的存在后,即创建mmc card,同时注册至mmc bus上,并完成与mmc driver的绑定操作。
  4. host和card可以分别理解为 MMC device的两个子设备:MMC主设备和MMC从设备,其中host为集成于MMC设备内部的MMC controller,card为MMC设备内部实际的存储设备。

二、SDIO驱动抽象模型

sdio总线驱动模型和mmc类似,结构体上的区别为其driver类型为sdio_driver,并增加了sdio_func结构体变量(该结构体包含了该sdio设备相关的厂商id、设备id,同时包含了mmc_card)

因sdio主要突出接口概念,其设备端可以连接wifi、gps等设备,因此其外设备驱动需要由驱动工程师自己实现,sdio驱动模块不提供对应的驱动。

三、MMC/SDIO总线

总线接口实现的功能可分为总线的注册、设备的注册、驱动的注册这三个部分。

1. 总线结构体定义

结构体定义位于core\bus.c

static struct bus_type mmc_bus_type = {
  // 总线名称
    .name        = "mmc",
    .dev_groups    = mmc_dev_groups,
    // match接口用于实现mmc card与mmc driver的匹配检测,返回值均为1;
    .match        = mmc_bus_match,  
    // 应用层通知接口,用于添加该mmc bus的uevent参数(在调用device_add时,会调用kobject_uevent向应用层发送设备添加相关的事件,而kobject_uevent会调用该device所属bus和class的uevent接口,添加需要发送到应用的event参数
    .uevent        = mmc_bus_uevent,
    // probe接口主要用于mmc card与mmc driver匹配成功后,则会调用该mmc bus的probe接口实现探测操作;
    .probe        = mmc_bus_probe,
    // remove接口主要用于mmc card与mmc driver解绑时,调用该接口,进行remove操作(对于mmc drivemmc_ops
    .shutdown    = mmc_bus_shutdown,
    // pm是电源管理相关的接口。
    .pm        = &mmc_bus_pm_ops,
};

总线匹配接口 .mmc_bus_match
当向linux系统总线添加设备或驱动时,总是会调用各总线对应的match匹配函数来判断驱动和设备是否匹配。

此处的mmc_bus_match并没有进行匹配检测,直接返回1,表示mmc子系统实现的mmc driver可匹配所有注册至mmc bus上的mmc card

*sdio总线结构体
位于sdio_bus.c

static struct bus_type sdio_bus_type = {
    .name        = "sdio",
    .dev_groups    = sdio_dev_groups,mmc_ops
    .match        = sdio_bus_match,  // 根据id_table来匹配
    .uevent        = sdio_bus_uevent,
    .probe        = sdio_bus_probe,
    .remove        = sdio_bus_remove,
    .pm        = &sdio_bus_pm_ops,
};
2. 总线注册

调用入口位于core/core.c,通过mmc_init()实现。

core/core.c

subsys_initcall(mmc_init);

static int __init mmc_init(void)
{
    int ret;
    // 将mmc总线注册到linux的总线系统中,管理块设备
    ret = mmc_register_bus();

    // 注册mmc_host_class
    ret = mmc_register_host_class();

    // 注册sido总线到linux的总线系统中,管理sdio接口类型的设备
    ret = sdio_register_bus();

    return 0;
}

主要工作是:

a. mmc_register_bus注册mmc总线,这个总线主要是为card目录里实现的mmc设备驱动层和mmc控制器实例化一个mmc(包括sd/sdio)设备对象建立的。

b. sdio_register_bus这是sdio的部分,它比较特殊,需要额外的一条总线

具体包括两个方面:

  • 利用 bus_register() 注册 mmc_bus,包括mmc总线和sdio总线。对应sysfs下的 /sys/bus/mmc/ 目录。
  • 利用 class_register() 注册 mmc_host_class 。对应sysfs下的 /sys/class/mmc_host 目录。

core/bus.c

int mmc_register_bus(void)
{
    // 实际调用内核接口,注册总线
    return bus_register(&mmc_bus_type);
}

core/sdio_bus.c

int sdio_register_bus(void)
{
    return bus_register(&sdio_bus_type);
}
3. 驱动注册

mmc_dirver的注册、注销接口是对内核函数的封装。实现将mmc_driver注册到mmc_bus总线中。

调用入口位于core/block.c,通过mmc_blk_init()实现,先给出mmc设备结构体的定义:。

static struct mmc_driver mmc_driver = {
    .drv        = {device_register
        .name    = "mmcblk",
        .pm    = &mmc_blk_pm_ops,
    },
    .probe        = mmc_blk_probe,  // probe回调函数
    .remove        = mmc_blk_remove,
    .shutdown    = mmc_blk_shutdown,
};

入口函数:

core/block.c

module_init(mmc_blk_init);

static int __init mmc_blk_init(void)
{
    int res;

    // 注册mmc_rpmb_bus总线
    res  = bus_register(&mmc_rpmb_bus_type);

    res = alloc_chrdev_region(&mmc_rpmb_devt, 0, MAX_DEVICES, "rpmb");

    // 注册块设备,申请块设备号
    res = register_blkdev(MMC_BLOCK_MAJOR, "mmc");

    // 将mmc_driver注册到mmc_bus总线系统中
    res = mmc_register_driver(&mmc_driver);

    return 0;
}

core/bus.c

int mmc_register_driver(struct mmc_driver *drv)
{
    drv->drv.bus = &mmc_bus_type;
    // 实际调用内核接口,注册设备到总线系统
    return driver_register(&drv->drv);
}

// 使用EXPORT_SYMBOL将函数以符号的方式导出给其他模块使用。
EXPORT_SYMBOL(mmc_register_driver);

主要步骤包括:

a. 通过 register_blkdev() 向内核注册块设备。(仅注册,初始化的其他操作在mmc_driver结构体的prob接口中完成)

借助该块设备驱动模型,将mmc card与vfs(虚拟文件系统)完成了关联,即可通过系统调用借助VFS模型实现对块设备的读写访问操作。

b. 调用 mmc_register_driver() 将 mmc_driver 注册到 mmc_bus 总线系统。简单封装,和大部分驱动注册方式一致。

*sdio驱动注册
这两个接口的实现与mmc_driver的实现类似,均是简单的对driver_register/driver_unregister的封装(还有设置driver需要绑定的bus_type)

sdio_uart.c

module_init(sdio_uart_init);

static int __init sdio_uart_init(void)
{
    // ……
    ret = tty_register_driver(tty_drv);

    ret = sdio_register_driver(&sdio_uart_driver);
    // ……
}

sdio_bus.c

int sdio_register_driver(struct sdio_driver *drv)
{
    drv->drv.name = drv->name;
    drv->drv.bus = &sdio_bus_type;
    return driver_register(&drv->drv);
}
EXPORT_SYMBOL_GPL(sdio_register_driver);
4. 设备注册

主要包括mmc card内存的申请、mmc card的注册、mmc card的注销等接口。

调用入口位于实际host设备的驱动文件中,通过xxx_driver实现。下面以mvsdio驱动为例分析。

host/mvsdio.c

module_platform_driver(mvsd_driver);

static struct platform_driver mvsd_driver = device_register{
    .probe        = mvsd_probe,
    .remove        = mvsd_remove,
    .driver        = {
        .name    = DRIVER_NAME,
        .probe_type = PROBE_PREFER_ASYNCHRONOUS,
        .of_match_table = mvsdio_dt_ids,
    },
};

// 在probe回调中调用
static int mvsd_probe(struct platform_device *pdev)
{
    // ……
    // 实例化一个控制器对象
    mmc = mmc_alloc_host(sizeof(struct mvsd_host), &pdev->dev);
    // ……
    mmc->ops = &mvsd_ops;  // 控制器操作集
    // ……(一系列对控制器对象的初始化工作)
    ret = mmc_add_host(mmc);
    // ……
}

// 控制器操作集,编写控制器驱动的一个主要任务就是实现这个操作集
static const struct mmc_host_ops mvsd_ops = {
    .request        = mvsd_request,  // 最终执行硬件操作的函数,参数由核心层提供,由核心层更上一层的card设备驱动层向下调用
    .get_ro            = mmc_gpio_get_ro,  // 判断是否写保护
    .set_ios        = mvsd_set_ios,  // 配置控制器的函数
    .enable_sdio_irq    = mvsd_enable_sdio_irq,  // 与sdio相关
};

host.c

mmc host子系统提供了延迟队列机制,在执行mmc_alloc_host、mmc_add_host后,则完成了mmc card rescan延迟工作队列及其处理接口的创建INIT_DELAYED_WORK

若要触发mmc card rescan(即调度工作队列),则调用mmc_detect_change接口,即可触发mmc card rescan(即完成mmc_host->detect队列的调度);

struct mmc_host *mmc_alloc_host(int extra, struct device *dev)
{
    // ……
    // 将mmc_rescan指定为延时工作队列的工作函数
    INIT_DELAYED_WORK(&host->detect, mmc_rescan);
    // ……
}

EXPORT_SYMBOL(mmc_alloc_host);

mmc_rescan函数的大致调用流程如下,由mmc子系统通过mmc card的rescan机制,实现mmc card的自动检测及注册机制,依次完成了对sdio、sd和mmc设备的添加与移除操作。

换句话说,是使用事件的触发监控机制完成了卡(mmc,sd,sdio)的热插拔处理。

mmc_rescan[core.c]-->
    mmc_rescan_try_freq[core.c]-->
        mmc_attach_sdio[sdio.c]-->
            mmc_attach_bus[core.c]
            mmc_sdio_init_card[sdio.c]-->
                mmc_alloc_card[bus.c]
            sdio_init_func[sdio.c]-->
                sdio_alloc_func[sdio_bus.c]
            mmc_add_card[bus.c]
            sdio_add_func[sdio_bus.c]
        mmc_attach_sd[sd.c]-->
            mmc_attach_bus[core.c]
            mmc_sd_init_card[sd.c]-->
                mmc_alloc_card[bus.c]
            mmc_add_card[bus.c]
        mmc_attach_mmc[mmc.c]-->
            mmc_attach_bus[core.c]
            mmc_init_card[mmc.c]-->
                mmc_alloc_card[bus.c]
            mmc_add_card[bus.c]

从mmc_rescan调用关系中可以看出,mmc设备注册的过程依次完成了sdio设备、sd卡和mmc卡设备的初始化。

A. mmc_attach_sdio()
SDIO卡初始化的入口

a. 向卡发送CMD5命令,该命令有两个作用:

第一,通过判断卡是否有反馈信息来判断是否为SDIO设备mmc_send_io_op_cond()

  1. 如果有响应,并且响应中的MP位为0,说明对应卡槽中的卡为SDIO卡,进而开始SDIO卡的初始化流程
  2. 如果命令没有响应,则说明对应卡槽的卡为SD或MMC卡,进而开始SD/MMC卡的初始化流程(sdio卡时有魄力sdio协议,sd卡使用sd协议)
  3. 如果有响应,且响应中的MP位为说明这个卡mmc_alloc_card不但是SDIO卡,同时也时SD卡,也就是所谓的combo卡,则进行combo卡的初始化流程mmc_sdio_ops)

第二,如果是SDIO设备,就会给host反馈电压信息,就是说告诉host,本卡所能支持的电压是多少多少。

b. 设置sdio卡的总线操作集mmc_attach_bus(),传入struct mmc_bus_ops类型的实现mmc_sdio_ops。

void mmc_attach_bus(struct mmc_host *host, const struct mmc_bus_ops *ops)
{
    host->bus_ops = ops;
}

c. host根据SDIO卡反馈回来的电压要求,给其提供合适的电压mmc_select_voltage()

d. 对sdio卡进行探测和初始化mmc_sdio_init_card()

e. 注册SDIO的各个功能模块sdio_init_func()

f. 注册SDIO卡mmc_add_card()

g. 将所有SDIO功能添加到device架构中sdio_add_func()

mmc_alloc_card():
调用device模型对应的接口完成device类型变量的初始化,并完成mmc_card与mmc_host的绑定。

mmc_add_card():

  1. 调用device_add,完成将该mmc_card注册至mmc bus上;
  2. 设置mmc_card的状态为在位状态。

sdio func
sdio_func的注册与注销接口对应于mmc_card的注册与注销接口。主要函数有sdio_alloc_func、sdio_add_func、sdio_remove_func、sdio_release_func(相比mmc card,多了针对acpi的配置调用)

B. mmc_attach_sd()
SD卡初始化的入口

a. 发送CMD41指令,(sd卡支持该指令,但mmc卡不支持,所以可以以此区分)mmc_send_app_op_cond()

b. 设置sdio卡的总线操作集mmc_attach_bus(),传入struct mmc_bus_ops类型的实现mmc_sd_ops。

c. 设置合适的电压mmc_select_voltage()

d. 调用mmc_sd_init_card()(探测和初始化),获取mmc card的csd、cid,并创建mmc_card,并对mmc card进行初始化(如是否只读等信息)

e.调用mmc_add_card(),将该mmc_card注册至mmc_bus中,该接口会调用device_register将mmc_card注册至mmc_bus上,而这即触发mmc_driver与mmc_card的绑定流程,从而调用mmc_driver->probe接口,即执行mmc block device的注册操作(待解决,没有找到device_register相关代码)。

c. mmc_attach_mmc()
mmc卡初始化入口

a. 发送CMD1指令mmc_send_op_cond()

b. 设置mmc卡的总线操作集mmc_attach_bus(),传入struct mmc_bus_ops类型的实现mmc_ops。

c. 选择一个card和host都支持的最低工作电压mmc_select_voltage()

d. 初始化card使其进入工作状态mmc_init_card()

e. 为card构造对应的mmc_card并且注册到mmc_bus中mmc_add_card(),之后mmc_card就挂在了mmc_bus上,会和mmc_bus上的block(mmc_driver)匹配起来。相应block(mmc_driver)就会进行probe,驱动card,实现card的实际功能(也就是存储设备的功能)。会对接到块设备子系统中。

上面多次提到了mmc_bus_ops结构体,这是一个定义在core/core.h中的,用于表示总线操作的结构体。

struct mmc_bus_ops {
    void (*remove)(struct mmc_host *);
    void (*detect)(struct mmc_host *);
    int (*pre_suspend)(struct mmc_host *);
    int (*suspend)(struct mmc_host *);
    int (*resume)(struct mmc_host *);
    int (*runtime_suspend)(struct mmc_host *);
    int (*runtime_resume)(struct mmc_host *);
    int (*alive)(struct mmc_host *);
    int (*shutdown)(struct mmc_host *);
    int (*hw_reset)(struct mmc_host *);
    int (*sw_reset)(struct mmc_host *);
    bool (*cache_enabled)(struct mmc_host *);
    int (*flush_cache)(struct mmc_host *);
};

四、MMC设备控制器(mmc host)

1. 控制器结构体定义

该模块最重要的数据结构为mmc_host,位于core/host.h文件,用于描述一个mmc controller

而围绕着mmc controller又定义了相应的数据结构,用于描述mmc controller的各种行为(包括针对该mmc controller的访问方法抽象而来的数据结构mmc_host_ops、该mmc controller相关的参数抽象而来的数据结构体mmc_ios、针对mmc card相关的电源管理及在位检测方法抽象而来的数据结构mmc_bus_ops

部分成员如下:

  • struct device class_dev:
    host对应的device,用于借助设备模型与系统中注册的设备、class完成关联。
  • const struct mc_host_ops *ops:
    定义本mmc_host的操作接口,包括与mmccard通信的接口request、卡检测相关的接口等。
  • 块设备相关参数,包括段大小、块大小等。
  • u32 caps u32 caps2:
    指示host支持的功能特性,包括4bit数据模式、mmc high speed mode、sd high speed mode、spi mode、8bit data、noneremovable mode(emmc mode)、cd与wp引脚的active high/low等等。

MMC驱动注册

调用入口位于mmc/core/block.c,将mmc_driver注册到mmc_bus总线中.

主要步骤包括:

  • 通过 register_blkdev() 向内核注册块设备。

    借助该块设备驱动模型,将mmc card与vfs完成了关联,即可通过系统调用借助VFS模型实现对块设备的读写访问操作。

  • 调用 driver_register() 将 mmc_driver 注册到 mmc_bus 总线系统。和其他驱动注册方式一致。

关键函数为:platform_driver_register() --> driver_register() --> bus_add_driver()

MMC设备注册

每个host均有调用入口,使用moduSD/SDIO/MMC 驱动是一种基于 SDMMC 和 SD SPI 主机驱动的协议级驱动程序,目前已支持 SD 存储器、SDIO 卡和 eMMC 芯片。

le_platform_driver()宏实现。

驱动入口函数中将注册 platform_driver 和 platform_device , name 均定义为 xxx_mmc 。根据驱动模型,最终会回调 xxx_mmc_driver 中的 probe() 函数: xxx_mmc_probe() 。

关键函数为: platform_device_add() --> device_add() --> bus_add_device() / bus_probe_device()

注册过程(瑞芯微MMC驱动源码)

设备启动时,首先向linux系统注册mmc_bus和sdio_bus两条总线,用来管理块设备和sdio接口类型的设备。同时注册mmc_host_class类

# core.c
subsys_initcall(mmc_init);

接着调用module_init向系统注册一条mmc_rpmb_bus总线、一个mmc块设备和mmc driver。

# block.c
module_init(mmc_blk_init);

最后调用module_platform_driver,把mmc controler注册到platform总线,同时扫描一次挂载到mmc控制器上的设备。

# meson-mx-sdio.c
module_platform_driver(mmc_pwrseq_emmc_driver);

mmc卡(mmc type card)协议相关操作

mmc_ops提供了部分和mmc卡协议相关的操作。

mmc_go_idle
发送CMD0指令,GO_IDLE_STATE
使mmc card进入idle state。
虽然进入到了Idle State,但是上电复位过程并不一定完成了,这主要靠读取OCR的busy位来判断,而流程归结为下一步。

mmc_send_op_cond
发送CMD1指令,SEND_OP_COND
这里会设置card的工作电压寄存器OCR,并且通过busy位(bit31)来判断card的上电复位过程是否完成,如果没有完成的话需要重复发送。
完成之后,mmc card进入ready state。

mmc_all_send_cid
这里会发送CMD2指令,ALL_SEND_CID
广播指令,使card回复对应的CID寄存器的值。在这里就相应获得了CID寄存器的值了,存储在cid中。
完成之后,MMC card会进入Identification State。

mmc_set_relative_addr
发送CMD3指令,SET_RELATIVE_ADDR
设置该mmc card的关联地址为card->rca,也就是0x0001
完成之后,该MMC card进入standby模式。

mmc_send_csd
发送CMD9指令,MMC_SEND_CSD
要求mmc card发送csd寄存器,存储到card->raw_csd中,也就是原始的csd寄存器的值。
此时mmc card还是处于standby state

mmc_select_card & mmc_deselect_cards
发送CMD7指令,SELECT/DESELECT CARD
选择或者断开指定的car成员或者sending-data state依次来进行数据的传输

mmc_get_ext_csd
发送CMD8指令,SEND_EXT_CSD
这里要求处于transfer state的card发送ext_csd寄存器,这里获取之后存放在ext_csd寄存器中
这里会使card进入sending-data state,完成之后又退出到transfer state。

mmc_switch
发送CMD6命令,MMC_SWITCH
用于设置ext_csd寄存器的某些bit

mmc_send_status
发送CMD13命令,MMC_SEND_STATUS
要求card发送自己当前的状态寄存器

mmc_send_cid
发送CMD10命令,MMC_SEND_CID
要求mmc card回复cid寄存器

mmc_card_sleepawake
发送CMD5命令,MMC_SLEEP_AWAKE
使card进入或者退出sleep state,由参数决定。关于sleep state是指card的一种状态,具体参考emmc 5.1协议。

DTS配置

参考瑞芯微RK3568,配置文件位于

ARM:arch/arm/boot/dts/  // 主要是芯片级的配置,无需修改
ARM64:arch/arm64/boot/dts/rockchip  // 主要是板级相关的配置,可以根据实际需求修改

对应文件为

arch\arm64\boot\dts\rockchip\rk3568.dtsi
arch\arm68\boot\dts\rockchip\rk3568-pinctrl.dsti
arch\arm64\boot\dts\rockchip\rk3568-evb1-v10.dts

rk3568.dtsi

    sdmmc0: mmc@fe2b0000 {
        compatible = "rockchip,rk3568-dw-mshc", "rockchip,rk3288-dw-mshc";
        reg = <0x0 0xfe2b0000 0x0 0x4000>;
        interrupts = <GIC_SPI 98 IRQ_TYPE_LEVEL_HIGH>;
        clocks = <&cru HCLK_SDMMC0>, <&cru CLK_SDMMC0>,
             <&cru SCLK_SDMMC0_DRV>, <&cru SCLK_SDMMC0_SAMPLE>;
        clock-names = "biu", "ciu", "ciu-drive", "ciu-sample";
        fifo-depth = <0x100>;
        max-frequency = <150000000>;
        resets = <&cru SRST_SDMMC0>;
        reset-names = "reset";
        status = "disabled";
    };

参考资料

  1. Linux MMC 驱动子系统
  2. Linux设备驱动模型和sysfs文件系统
  3. LINUX MMC子系统分析之一 概述
  4. LINUX设备驱动模型分析之一 总体概念说明
  5. LINUX MMC子系统分析之二 MMC子系统驱动模型分析(包括总线、设备、驱动)
  6. LINUX设备驱动模型分析之二 总线(BUS)接口分析
  7. LINUX MMC子系统分析之三 MMC/SDIO总线接口分析
  8. LINUX设备驱动模型分析之三 驱动(DRIVER)接口分析
  9. LINUX MMC 子系统分析之六 MMC card添加流程分析
  10. Linux内核4.14版本——mmc core(4)——card相关模块(mmc type card)
  11. Rockchip RK3588 kernel dts解析之MMC_loitawu的博客-优快云博客
<think>嗯,用户想深入了解Linux驱动开发中的pinctrl子系统,这属于嵌入式Linux的核心内容。从用户提问的精准度看,应该是有一定驱动开发基础的技术人员,可能正在调试板级引脚配置。 用户提供的引用材料很全面,覆盖了pinctrl的架构设计、设备树集成、GPIO子系统联动等关键点。特别注意到引用[3]详细描述了pinctrl_dev结构体,这是理解子系统内部机制的重要突破口。用户可能最关心的是如何在实际驱动中正确应用pinctrl,而不仅是理论概念。 需要重点解释清楚三个层次:首先是子系统存在的必要性——为什么裸机寄存器操作被淘汰(如引用[5]对比的STM32/IMX6ULL案例);其次是设备树配置的实践细节(引用[1][2]强调的设备树依赖);最后是驱动代码中的API调用时机(引用[4]的LED控制示例很典型)。 用户可能隐含的需求是解决实际引脚冲突问题,比如某个GPIO无法正确复用为I2C功能。因此回复中要突出“状态切换”这个易错点,通过休眠/唤醒的状态机示例说明配置流程。对于结构体字段这类底层细节,采用表格对比更直观,但需控制深度避免信息过载。 最后生成的问题要引导用户向具体场景延伸:比如针对功耗优化场景询问pinctrl的省电配置,或者结合GPIO子系统讨论中断处理。这样既能检验理解程度,又切合实际开发需求。</think>好的,我们来详细解析 Linux 驱动开发中的 `pinctrl` 子系统。 ## 1. 为什么需要 `pinctrl` 子系统? 在现代复杂的系统级芯片(SoC)上,一个物理引脚(Pin)通常具有多种功能(复用功能)。例如,一个引脚可能可以被配置为: * 通用输入/输出引脚(GPIO) * I²C 总线的时钟线(SCL)或数据线(SDA) * SPI 总线的片选(CS)、时钟(SCK)、数据输入(MISO)或数据输出(MOSI) * UART 的发送(TX)或接收(RX) * PWM 输出 * 等等 同时,每个引脚可能还需要配置其电气特性,如上拉/下拉电阻、驱动强度(输出电流能力)、施密特触发器使能、输入/输出模式(开漏、推挽)等。 在 `pinctrl` 子系统出现之前,这些配置通常分散在板级支持包(BSP)代码、具体驱动代码或 `arch/` 目录下的平台相关代码中。这导致了: 1. **代码重复:** 不同驱动需要重复实现引脚配置逻辑。 2. **维护困难:** 引脚配置与具体驱动和平台代码耦合紧密,改动困难。 3. **缺乏抽象:** 没有统一的接口管理引脚复用和配置。 4. **板级定制麻烦:** 为不同开发板适配引脚配置需要修改多处代码。 `pinctrl` 子系统的引入就是为了**统一管理 SoC 引脚的复用(Multiplexing)和配置(Configuration)**,提供一套标准的、与具体硬件无关的接口给内核其他部分(尤其是设备驱动)使用,并将引脚配置信息从驱动代码中剥离出来,放入设备树(Device Tree),极大地提高了代码的可重用性和可维护性[^1][^2][^5]。 ## 2. `pinctrl` 子系统核心概念 理解 `pinctrl` 子系统需要掌握几个关键概念: 1. **Pin Controller Device (引脚控制器设备):** * 这是物理上控制 SoC 上引脚复用和配置的硬件模块(通常是一个寄存器块)。 * 在设备树中,它表现为一个节点(例如 `&iomuxc` 或 `&pinctrl`),描述了这个控制器所能控制的所有引脚以及这些引脚可用的功能组(Pin Groups)和状态(Pin States)。 * 一个 SoC 可能包含多个 Pin Controller(例如,一个负责 Bank A,一个负责 Bank B)。 2. **Client Device (客户端设备):** * 这是需要使用引脚的设备,如 I2C 控制器、SPI 控制器、UART 控制器、MMC/SD 控制器、GPIO 控制器本身,甚至是一个简单的 LED 设备。 * 在设备树中,它表现为一个设备节点(例如 `&i2c1`, `&usdhc1`)。 * 它通过 `pinctrl-` 属性(如 `pinctrl-0`, `pinctrl-1`, ... `pinctrl-names`)来引用 Pin Controller 中定义好的引脚状态(Pin States),告诉内核它需要哪种引脚配置。 3. **Pin State (引脚状态):** * 这是 `pinctrl` 子系统的核心抽象。它代表了一组引脚在某个特定时刻的配置集合。 * 在 Pin Controller 的设备树节点中,使用子节点来定义不同的状态。常见的预定义状态名包括: * `default`: 设备正常操作时需要的状态。 * `init`: 设备初始化时可能需要的特殊状态(有时与 `default` 合并)。 * `sleep`, `idle`: 设备进入低功耗状态(睡眠、空闲)时需要的状态。 * 驱动也可以定义和使用自定义状态名(如 `active`, `inactive`)。 * 每个状态节点内部使用 `pinctrl` 绑定规定的属性(如 `fsl,pins`,具体属性名由 SoC 厂商的 Pin Controller 驱动定义)来描述该状态下涉及的**引脚组(Pin Group)**及其**复用功能**和**电气属性配置**。 4. **Pin Group (引脚组):** * 指逻辑上属于同一功能的一组引脚。例如,一个 UART 的 TX 和 RX 引脚可以组成一个名为 `uart1grp` 的组;一个 SPI 接口的 4 个引脚(CS, SCK, MISO, MOSI)可以组成名为 `spi0grp` 的组。 * 在 Pin Controller 设备树节点中定义的状态节点里,配置信息通常就是按组(或单个引脚)来指定的。组名通常作为状态节点的属性值出现。 5. **Pin Bank:** * 指物理上属于同一个寄存器块控制的一组引脚。通常一个 Pin Controller 可能管理多个 Bank。例如,一个 32 位寄存器控制 Bank A 的 32 个引脚,另一个 32 位寄存器控制 Bank B 的 32 个引脚。 * 这个概念更多存在于 Pin Controller 驱动实现的底层,对使用 `pinctrl` 的设备驱动通常是透明的[^1][^3]。 ## 3. `pinctrl` 子系统的工作原理与流程 1. **设备树描述:** * **Pin Controller 节点:** SoC 厂商或板级支持包(BSP)开发者在设备树(`.dtsi` 或 `.dts` 文件)中定义 Pin Controller 节点。在这个节点下,定义各种**状态子节点**(如 `pinctrl_mydevice_default`),在每个状态子节点内,使用 SoC 特定的属性(如 `fsl,pins`, `rockchip,pins`)列出该状态需要配置的**引脚组**(或单个引脚),并指定每个引脚的**复用功能编号**和**电气属性位掩码**。 * **Client Device 节点:** 设备驱动开发者或板级开发者在需要使用引脚的设备节点(如 `&i2c1`)中添加 `pinctrl-names` 和 `pinctrl-` 属性。例如: ```dts &i2c1 { pinctrl-names = "default", "sleep"; pinctrl-0 = <&pinctrl_i2c1_default>; // 引用Pin Controller中定义的状态节点 pinctrl-1 = <&pinctrl_i2c1_sleep>; ... }; ``` 2. **内核启动/驱动加载:** * **Pin Controller 驱动注册:** 在内核启动过程中,特定 SoC 的 Pin Controller 驱动(通常是平台驱动)被加载。它解析设备树中对应的 Pin Controller 节点,创建并注册一个 `struct pinctrl_desc` 和 `struct pinctrl_dev` 结构体实例到 `pinctrl` 子系统核心[^3]。这个 `pinctrl_dev` 代表了物理的 Pin Controller 硬件。 * **状态解析与存储:** Pin Controller 驱动解析设备树中该 Pin Controller 节点下定义的所有状态子节点,将配置信息存储在其私有数据结构中。 * **Client Device 驱动加载:** 当 Client Device 的驱动(如 `i2c-imx.c`)被加载时,其 `probe` 函数被调用。 * **获取 `pinctrl` 句柄:** 在 `probe` 函数中,驱动通常调用 `devm_pinctrl_get(dev)` 或 `pinctrl_get(dev)`。这个函数: * 根据设备节点(`dev->of_node`)查找 `pinctrl-names` 和 `pinctrl-` 属性。 * 找到关联的 Pin Controller (`pinctrl_dev`)。 * 遍历 `pinctrl-` 属性引用的所有状态节点(在 Pin Controller 节点下定义),并请求对应的 Pin Controller 驱动解析这些状态(如果还没解析的话)。 * 创建一个 `struct pinctrl` 对象来管理这个设备的所有状态,并返回一个 `struct pinctrl` 的句柄。 3. **状态选择与应用:** * **查找状态:** 驱动使用 `pinctrl_lookup_state(pinctrl, name)` 根据状态名(如 `"default"`)查找对应的 `struct pinctrl_state` 对象。 * **应用状态:** 驱动调用 `pinctrl_select_state(pinctrl, state)` 来应用该状态。这个函数最终会调用到关联的 Pin Controller 驱动注册的 `->set_state()` 或 `->set_mux()` / `->config_set()` 等操作函数。 * **硬件配置:** Pin Controller 驱动根据 `pinctrl_state` 中存储的配置信息(复用功能ID、电气属性掩码),操作 SoC 上实际的配置寄存器(MUX 寄存器、PAD 控制寄存器等),完成引脚复用功能和电气属性的设置[^3][^5]。 4. **状态切换(如休眠唤醒):** * 当设备需要进入低功耗状态(如 `suspend`)时,驱动(或 PM 子系统)会调用 `pinctrl_select_state(pinctrl, sleep_state)` 切换到休眠状态(如 `"sleep"`)。 * 当设备从低功耗状态恢复(`resume`)时,驱动会再次调用 `pinctrl_select_state(pinctrl, default_state)` 切换回默认状态。 ## 4. `pinctrl` 子系统与 GPIO 子系统的关系 这两个子系统紧密协作,但职责分明: * **`pinctrl` 子系统:** 负责引脚的**复用(Muxing)** 和 **配置(Configuration)**。它决定一个引脚当前是作为 GPIO 功能,还是作为某个特定外设(如 I2C、SPI)的功能。如果配置为 GPIO,它还负责设置该引脚作为 GPIO 时的基本电气属性(如上下拉、驱动强度)[^4][^5]。 * **GPIO 子系统:** 当引脚通过 `pinctrl` **配置为 GPIO 功能**后,GPIO 子系统负责提供 API 来**操作**这个 GPIO 引脚:设置方向(输入/输出)、读取输入电平、设置输出电平、申请/释放 GPIO、设置中断(如果该 GPIO 支持中断)等[^2][^4]。 **简单来说:`pinctrl` 决定了“这个引脚现在是什么(功能)”,GPIO 子系统决定了“当它是 GPIO 时,怎么操作它”。** 一个引脚必须先由 `pinctrl` 配置为 GPIO 功能,然后才能通过 GPIO 子系统 API 进行操作[^4][^5]。 ## 5. 关键数据结构(驱动开发者视角) 虽然驱动开发者通常不需要直接操作这些结构体内部,但了解它们有助于理解流程: 1. **`struct pinctrl_dev`:** * 代表一个注册到内核的 Pin Controller 硬件实例。 * 包含指向 `pinctrl_desc` 的指针、管理的引脚描述树、GPIO 范围列表、私有数据指针等[^3]。 * 由 SoC 厂商的 Pin Controller 驱动创建和注册。 2. **`struct pinctrl_desc`:** * 描述 Pin Controller 的能力和操作函数集。由 Pin Controller 驱动填充并传递给 `pinctrl_register()`。 * 关键成员: * `name`: 控制器名称。 * `pins`: `struct pinctrl_pin_desc` 数组,描述该控制器管理的所有物理引脚(名称、编号)。 * `npins`: `pins` 数组的大小。 * `pctlops`: `struct pinctrl_ops`,提供引脚/组枚举等操作。 * `pmxops`: `struct pinmux_ops`,提供引脚复用操作(核心)。 * `confops`: `struct pinconf_ops`,提供引脚配置操作(核心)。 * `owner`: 通常是 `THIS_MODULE`。 3. **`struct pinctrl`:** * 代表一个 Client Device 与 `pinctrl` 子系统的关联。通过 `devm_pinctrl_get()` 或 `pinctrl_get()` 获得。 * 包含该设备在设备树中引用的所有状态。 4. **`struct pinctrl_state`:** * 代表一个引脚配置状态(如 `"default"`, `"sleep"`)。 * 包含该状态下需要应用的配置设置列表(指向底层 Pin Controller 驱动存储的实际配置数据)。 * 通过 `pinctrl_lookup_state()` 获得。 5. **`struct pinctrl_setting`:** * `pinctrl_state` 内部包含一个或多个 `pinctrl_setting`。 * 每个 `setting` 代表一个具体的操作:复用(`PIN_MAP_TYPE_MUX_GROUP`)或配置(`PIN_MAP_TYPE_CONFIGS_*`)一个引脚组。 ## 6. 在 Client Device 驱动中使用 `pinctrl`(典型代码片段) ```c #include <linux/pinctrl/consumer.h> // 必须包含 static int my_driver_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct pinctrl *pinctrl; struct pinctrl_state *default_state, *sleep_state; int ret; /* 1. 获取该设备的 pinctrl 句柄 */ pinctrl = devm_pinctrl_get(dev); if (IS_ERR(pinctrl)) { ret = PTR_ERR(pinctrl); dev_err(dev, "Failed to get pinctrl: %d\n", ret); return ret; // 或 probe deferral } /* 2. 查找需要的状态 */ default_state = pinctrl_lookup_state(pinctrl, PINCTRL_STATE_DEFAULT); // "default" if (IS_ERR(default_state)) { ret = PTR_ERR(default_state); dev_err(dev, "Failed to get default pinctrl state: %d\n", ret); goto err_pinctrl; } sleep_state = pinctrl_lookup_state(pinctrl, PINCTRL_STATE_SLEEP); // "sleep" if (IS_ERR(sleep_state)) { dev_warn(dev, "No sleep pinctrl state defined, ignoring\n"); sleep_state = NULL; // 不是所有设备都需要sleep状态 } /* 3. 应用默认状态 (通常在probe早期应用) */ ret = pinctrl_select_state(pinctrl, default_state); if (ret) { dev_err(dev, "Failed to apply default pinctrl state: %d\n", ret); goto err_pinctrl; } /* ... 驱动其他初始化代码 ... */ /* 存储 pinctrl 和 states 以备后用 (例如在 suspend/resume 中) */ my_priv_data->pinctrl = pinctrl; my_priv_data->default_state = default_state; my_priv_data->sleep_state = sleep_state; return 0; err_pinctrl: devm_pinctrl_put(pinctrl); // 如果使用 pinctrl_get() 则需要手动调用 return ret; } static int my_driver_suspend(struct device *dev) { struct my_priv_data *priv = dev_get_drvdata(dev); int ret = 0; if (priv->sleep_state) { ret = pinctrl_select_state(priv->pinctrl, priv->sleep_state); if (ret) dev_warn(dev, "Failed to apply sleep pinctrl state: %d\n", ret); } /* ... 其他 suspend 操作 ... */ return ret; } static int my_driver_resume(struct device *dev) { struct my_priv_data *priv = dev_get_drvdata(dev); int ret = 0; /* ... 其他 resume 操作 ... */ ret = pinctrl_select_state(priv->pinctrl, priv->default_state); if (ret) { dev_err(dev, "Failed to apply default pinctrl state on resume: %d\n", ret); return ret; } return 0; } static const struct dev_pm_ops my_driver_pm_ops = { .suspend = my_driver_suspend, .resume = my_driver_resume, // 可能还有 .suspend_noirq, .resume_noirq 等 }; ``` ## 7. 总结 * `pinctrl` 子系统Linux 内核中**统一管理 SoC 引脚复用和配置**的核心框架。 * 它通过**设备树**描述硬件引脚资源和设备所需的引脚状态(`default`, `sleep` 等),实现了**配置与驱动代码的分离**。 * 核心概念包括 **Pin Controller**、**Client Device**、**Pin State**、**Pin Group**。 * Client Device 驱动通过 `pinctrl_get()` / `devm_pinctrl_get()` 获取句柄,通过 `pinctrl_lookup_state()` 查找状态,通过 `pinctrl_select_state()` 应用状态。 * 状态切换(如休眠唤醒)是 `pinctrl` 的关键应用场景之一。 * `pinctrl` 负责**决定引脚功能(复用)和基本配置**,**GPIO 子系统**负责**操作配置为 GPIO 功能的引脚**,两者分工明确,协同工作[^4][^5]。 理解并正确使用 `pinctrl` 子系统是编写可移植、易维护的现代 Linux 设备驱动(尤其是涉及外设接口的驱动)的基础。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值