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子系统不是独立存在的,它寄生在MMC子系统中。你在代码里看到的 mmc_ 前缀函数,很多也服务于SDIO设备。这一点经常让新手困惑——明明我写的是SDIO驱动,怎么调用的都是mmc_xxx函数?
3.2 核心层——承上启下的枢纽
核心层在 drivers/mmc/core/ 目录下。我当初啃这部分代码时,发现它主要干三件事:
- 设备枚举:上电后发CMD0、CMD2、CMD3、CMD5、CMD7这些命令,把卡或设备认出来
- 命令封装:把上层发来的读写请求,组装成SDIO协议规定的命令格式
- 总线管理:维护设备状态、电源管理、时钟频率切换
核心层暴露了两个关键结构体给你:
// 这是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函数。
你写驱动时,只需要做三件事:
- 定义一个
sdio_driver结构体,填好probe、remove、id_table - 在probe里初始化设备、注册中断、创建接口
- 调用
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驱动到底怎么写、怎么调。