3、Linux MMC子系统框架:MMC子系统分层架构、核心层与主机控制器层、块设备层与协议层、设备树绑定与平台驱动

好,咱们今天来聊聊Linux MMC子系统的框架。说实话,这个框架我第一次接触的时候,也觉得挺绕的。但后来我慢慢发现,它其实是一个很优雅的分层设计。你想想看,从最底层的硬件寄存器,到最上层的文件系统,中间隔了多少层抽象?MMC子系统把这些都安排得明明白白的。

3.1 MMC子系统分层架构

MMC子系统的分层,说白了就是「各司其职」。我习惯把它分成四层来看:

  • 主机控制器层(Host Controller Layer):直接跟硬件打交道,操作SD/MMC控制器的寄存器
  • 核心层(Core Layer):承上启下,管理协议、总线、设备枚举
  • 协议层(Protocol Layer):处理MMC、SD、SDIO这些不同协议的细节
  • 块设备层(Block Device Layer):把存储设备抽象成块设备,给文件系统用

嗯,这里要注意,每一层都有自己明确的职责。我在项目中见过有人把协议层的代码塞到主机控制器层里,结果后面维护起来那叫一个痛苦。

核心要点:分层架构的核心思想是「上层不关心下层的实现细节,下层不依赖上层的业务逻辑」。这样你换一个主机控制器,上层代码基本不用动。

3.2 核心层与主机控制器层

核心层是MMC子系统的「大脑」。它负责的事情包括:

  • 管理MMC总线(mmc_bus)
  • 处理设备的热插拔
  • 执行设备初始化序列
  • 管理电源状态

主机控制器层则是「手脚」。它实现了具体的硬件操作接口。核心层定义了一套标准接口,主机控制器层去实现它们。我举个例子:

// 核心层定义的结构体
struct mmc_host_ops {
    int (*request)(struct mmc_host *host, struct mmc_request *mrq);
    void (*set_ios)(struct mmc_host *host, struct mmc_ios *ios);
    int (*get_ro)(struct mmc_host *host);
    int (*get_cd)(struct mmc_host *host);
    // ... 其他回调
};

你看,核心层只关心「我要发一个请求」,至于这个请求怎么通过寄存器发出去,那是主机控制器层的事。我在调试一个SDIO WiFi驱动时,就发现主机控制器层的set_ios实现有问题,导致时钟切换失败。嗯,这种问题排查起来确实费劲。

个人经验:写主机控制器驱动时,我建议先把set_iosrequest这两个回调调通。这两个是基础,其他的都是锦上添花。

3.3 块设备层与协议层

块设备层,说白了就是把MMC卡变成一个/dev/mmcblk0这样的设备。它注册到Linux的块设备子系统,然后文件系统就可以在上面跑了。这一层做的事情其实挺固定的:

  • 分配请求队列
  • 处理bio请求
  • 管理分区

协议层则负责处理不同卡类型的差异。MMC卡、SD卡、SDIO卡,它们的初始化流程、命令集、数据传输方式都有区别。协议层把这些差异封装起来,给上层提供统一的接口。

我记得有一次,一个SDIO设备在初始化时总是超时。排查了半天,发现是协议层在处理SDIO的CCCR(Card Common Control Register)时,读写的顺序搞反了。你看,这种细节问题,如果不是对协议层足够熟悉,真的很难发现。

避坑指南:我曾经在移植一个SDIO驱动时,忽略了协议层的版本检查。结果新卡用老协议跑,速度上不去不说,还时不时丢数据。所以,协议层的版本兼容性一定要处理好。

3.4 设备树绑定与平台驱动

现在大部分嵌入式平台都用设备树来描述硬件。MMC主机控制器的设备树绑定,通常长这样:

sdhci@fe330000 {
    compatible = "rockchip,rk3399-sdhci-5.1";
    reg = <0x0 0xfe330000 0x0 0x10000>;
    interrupts = <GIC_SPI 13 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&cru SCLK_EMMC>, <&cru PCLK_EMMC>;
    clock-names = "clk_emmc", "pclk_emmc";
    bus-width = <8>;
    status = "disabled";
};

设备树绑定的关键点在于:

  • compatible:匹配对应的驱动
  • reg:寄存器地址范围
  • interrupts:中断号
  • clocks:时钟配置
  • bus-width:数据总线宽度

平台驱动这边,核心是注册一个platform_driver,然后在probe函数里解析设备树、初始化MMC主机。我习惯在probe里先打印一下设备树解析出来的参数,方便调试:

static int my_mmc_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct mmc_host *mmc;
    struct resource *res;
    int ret;

    // 解析设备树
    ret = device_property_read_u32(dev, "bus-width", &bus_width);
    if (ret)
        bus_width = 4; // 默认4位

    dev_info(dev, "bus-width: %d\n", bus_width);

    // 分配MMC主机
    mmc = mmc_alloc_host(sizeof(struct my_mmc_host), dev);
    if (!mmc)
        return -ENOMEM;

    // ... 初始化硬件、注册主机
}

小技巧:设备树里的broken-cdnon-removable这些属性,很多时候能帮你省掉不少硬件适配的麻烦。我一般会在设备树里把这些属性都配好,驱动里直接读就行。

嗯,说到设备树,还有一个容易踩的坑——时钟频率的配置。有些平台默认把MMC时钟配得很高,但你的卡可能不支持。我曾经遇到过一块eMMC,标称支持HS200模式,但实际跑在200MHz时就是不稳定。后来我把时钟降到100MHz,问题就解决了。所以,设备树里的max-frequency属性,该设就设,别偷懒。

总结一下今天的内容:MMC子系统的分层架构,说白了就是「各层干各层的活」。核心层管调度,主机控制器层管硬件,协议层管差异,块设备层管存储。设备树绑定则是把硬件描述和驱动代码解耦。你把这些理清楚了,写MMC驱动就不会觉得无从下手了。