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_ios和request这两个回调调通。这两个是基础,其他的都是锦上添花。
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-cd、non-removable这些属性,很多时候能帮你省掉不少硬件适配的麻烦。我一般会在设备树里把这些属性都配好,驱动里直接读就行。
嗯,说到设备树,还有一个容易踩的坑——时钟频率的配置。有些平台默认把MMC时钟配得很高,但你的卡可能不支持。我曾经遇到过一块eMMC,标称支持HS200模式,但实际跑在200MHz时就是不稳定。后来我把时钟降到100MHz,问题就解决了。所以,设备树里的max-frequency属性,该设就设,别偷懒。
总结一下今天的内容:MMC子系统的分层架构,说白了就是「各层干各层的活」。核心层管调度,主机控制器层管硬件,协议层管差异,块设备层管存储。设备树绑定则是把硬件描述和驱动代码解耦。你把这些理清楚了,写MMC驱动就不会觉得无从下手了。