3、Linux SDIO子系统框架:MMC/SDIO子系统架构、核心层与主机控制器层、设备驱动与总线驱动模型

好,咱们今天聊聊Linux SDIO子系统的整体框架。说实话,我第一次接触这块代码时,也被它绕得有点晕。MMC、SD、SDIO,这三个东西搅在一起,代码路径又长又杂。但后来我摸清了门道——其实它就是一个典型的分层架构,搞懂了每一层干什么,你就抓住了主线。

3.1 MMC/SDIO子系统架构概览

Linux内核把MMC、SD卡、SDIO设备统一管理在一个子系统里。为什么?因为它们共用同一套物理总线协议,电气特性也差不多。你想想看,SD卡和SDIO WiFi模块,插在同一个卡槽里,底层通信方式是一样的。

整个架构分三层:

  • 设备驱动层:你写的SDIO功能驱动,比如WiFi、蓝牙、GPS模块驱动
  • 核心层:MMC/SD/SDIO共用的协议处理、总线管理、设备枚举
  • 主机控制器层:直接操作硬件寄存器,处理DMA、中断这些底层活

我个人习惯把这三层想象成「老板-经理-员工」的关系。设备驱动是老板,只管发指令;核心层是经理,负责翻译和调度;主机控制器是员工,埋头干活。

设备驱动层 sdio_func_driver / mmc_driver WiFi驱动 · 蓝牙驱动 · GPS驱动 · 自定义SDIO外设驱动 核心层 mmc_core / mmc_block / sdio_bus 协议解析 · 命令封装 · 设备枚举 · 总线管理 · 电源管理 主机控制器层 mmc_host_ops / sdhci_ops 寄存器操作 · DMA传输 · 中断处理 · 时钟/电压控制 请求下发 请求下发 事件上报 → 中断/完成 →

核心要点:SDIO子系统不是独立存在的,它寄生在MMC子系统中。你在代码里看到的 mmc_ 前缀函数,很多也服务于SDIO设备。这一点经常让新手困惑——明明我写的是SDIO驱动,怎么调用的都是mmc_xxx函数?

3.2 核心层——承上启下的枢纽

核心层在 drivers/mmc/core/ 目录下。我当初啃这部分代码时,发现它主要干三件事:

  1. 设备枚举:上电后发CMD0、CMD2、CMD3、CMD5、CMD7这些命令,把卡或设备认出来
  2. 命令封装:把上层发来的读写请求,组装成SDIO协议规定的命令格式
  3. 总线管理:维护设备状态、电源管理、时钟频率切换

核心层暴露了两个关键结构体给你:

// 这是SDIO设备的核心描述结构
struct sdio_func {
    struct device       dev;        // 内核设备模型
    struct mmc_card     *card;      // 所属的MMC卡
    unsigned char       num;        // 功能编号 (1~7)
    unsigned short      vendor;     // 厂商ID
    unsigned short      device;     // 设备ID
    unsigned int        max_blksize; // 最大块大小
    unsigned int        cur_blksize; // 当前块大小
    unsigned char       class;      // 设备类别
};

// 这是SDIO功能驱动的结构
struct sdio_driver {
    char *name;
    int  (*probe)(struct sdio_func *, const struct sdio_device_id *);
    void (*remove)(struct sdio_func *);
    const struct sdio_device_id *id_table;
};

我的经验:写SDIO驱动时,sdio_func 就是你的「设备句柄」。所有读写操作都通过它来完成。我曾经犯过一个低级错误——在probe函数里直接访问 func->card 的寄存器,结果内核直接panic。后来才明白,设备刚probe时,有些资源还没准备好。

3.3 主机控制器层——跟硬件打交道的地方

主机控制器层,说白了就是芯片厂商要实现的代码。不同的SoC,SDIO主机的寄存器布局、DMA控制器、时钟管理都不一样。内核通过 mmc_host_ops 这个结构体来抽象这些差异:

struct mmc_host_ops {
    // 发送命令
    void (*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);
    // 使能/禁能中断
    void (*enable_sdio_irq)(struct mmc_host *host, int enable);
    // ... 还有不少
};

嗯,这里要注意:request 函数是核心。所有数据收发最终都会走到这里。你在调试时,如果发现数据传不出去,第一个断点就打在 request 里。

回调函数 作用 调试要点
request 执行MMC请求(命令+数据) 检查命令索引、参数、数据方向
set_ios 设置时钟、电压、总线宽度 时钟频率是否超限?电压是否匹配?
get_ro 获取写保护状态 SDIO设备一般返回0
enable_sdio_irq 使能SDIO中断线 中断触发方式是否正确?

避坑指南:我曾经在移植SDIO WiFi驱动时,WiFi模块死活枚举不出来。折腾了两天,最后发现是 set_ios 里时钟频率设错了——模块要求最高50MHz,我设成了100MHz。硬件工程师看了直摇头:「你这是在超频啊!」所以,时钟和电压的配置一定要看芯片手册,别想当然。

3.4 设备驱动与总线驱动模型

Linux的设备驱动模型,在SDIO子系统里体现得淋漓尽致。核心是 sdio_bus_type:

static struct bus_type sdio_bus_type = {
    .name       = "sdio",
    .match      = sdio_bus_match,
    .probe      = sdio_bus_probe,
    .remove     = sdio_bus_remove,
    .pm         = &sdio_bus_pm_ops,
};

匹配规则很简单——比较 sdio_driver->id_table 和 sdio_func 的vendor/device ID。匹配上了,就调用你的probe函数。

你写驱动时,只需要做三件事:

  1. 定义一个 sdio_driver 结构体,填好probe、remove、id_table
  2. 在probe里初始化设备、注册中断、创建接口
  3. 调用 sdio_register_driver() 注册到内核

举个例子,一个最简单的SDIO驱动骨架:

static const struct sdio_device_id my_id_table[] = {
    { SDIO_DEVICE(0x1234, 0x5678) },
    { /* end: all zero */ }
};

static int my_probe(struct sdio_func *func, 
                    const struct sdio_device_id *id)
{
    dev_info(&func->dev, "My SDIO device found!\n");
    // 使能功能
    sdio_claim_host(func);
    sdio_enable_func(func);
    sdio_release_host(func);
    return 0;
}

static void my_remove(struct sdio_func *func)
{
    sdio_claim_host(func);
    sdio_disable_func(func);
    sdio_release_host(func);
}

static struct sdio_driver my_driver = {
    .name       = "my_sdio_drv",
    .probe      = my_probe,
    .remove     = my_remove,
    .id_table   = my_id_table,
};

module_sdio_driver(my_driver);

关键提醒:sdio_claim_host() 和 sdio_release_host() 这对函数,是SDIO驱动的「锁」。所有对SDIO总线的访问,都要包在这对函数中间。我见过不少新手驱动崩溃,就是因为忘了claim host就直接读写寄存器。记住:SDIO总线是共享资源,不锁就会出乱子。

说到总线驱动模型,其实内核帮我们做了大量工作。你注册驱动后,内核自动帮你做设备匹配、电源管理、热插拔处理。你只需要关注业务逻辑——怎么跟你的SDIO外设通信。

我个人觉得,理解这个模型最好的方法,就是去看 drivers/mmc/core/sdio_bus.c 这个文件。代码不长,300行左右,把总线模型的核心逻辑讲得清清楚楚。我当年就是反复读这个文件,才真正搞懂了Linux的设备模型是怎么运作的。

好了,这一章的内容就到这里。框架搭起来了,下一章咱们就深入代码,看看SDIO驱动到底怎么写、怎么调。