☰
Linux内核MMC/SDIO驱动三层架构解析:从SD卡到设备适配
2026/10/17 1:30:17 网站建设 项目流程

1. 从一张SD卡看Linux驱动分层:为什么是三层架构

很多刚接触嵌入式Linux驱动开发的同学,第一次打开内核里的drivers/mmc目录时都会愣一下:明明是一张小小的SD卡,内核为什么要拆出core、host、card三个子目录?这三者之间到底怎么配合?

这个困惑我当年也有过。我第一次接到SDIO WiFi模组的驱动适配任务时,拿着芯片厂商的驱动代码,对着内核源码翻了三天,才勉强搞清楚mmc_alloc_host、mmc_add_host、mmc_register_host_class这一串函数调用背后到底发生了什么。后来我意识到一个关键点:搞懂SD/MMC驱动,核心不在于背函数,而在于理解内核为什么会把这条驱动链拆成三个core。这篇文章我会从内核源码的视角,把mmc core、host core、card core这三层架构的职责边界、交互流程和开发实操完整过一遍,并结合一个简化版的SDIO WiFi驱动适配过程,给出具体的代码走读和调试思路。无论你是要写host控制器驱动、适配SDIO外设驱动,还是纯粹想把内核的MMC子系统吃透,这篇内容都能给你一条清晰的路径。

先说我的结论:三层架构不是凭空设计的,它对应的是协议、硬件、设备这三种完全不同的变化维度。协议层要处理SD/MMC/SDIO三类卡片的规范差异,硬件层要屏蔽不同SoC上host控制器的寄存器差异,设备层要对接具体的SDIO功能设备(WiFi、蓝牙、GPS模块等)。三层各管一摊,各自独立演进,这就是Linux内核把系统复杂度的惯用解法——分层,然后给每一层定义清晰的接口契约。

1.1 一张SD卡从插入到挂载,到底经过哪些环节

在展开代码之前,先用大白话把整条链路捋一遍。你把一张SD卡插入开发板的卡座,内核里大概会发生这么几件事:

  1. host控制器驱动检测到卡检测引脚(CD,Card Detect)电平变化,触发中断或者轮询。
  2. host core层发起上电时序,给卡供电、拉高时钟,让卡进入工作状态。
  3. host core层通过host控制器发送CMD0(复位)、CMD8(电压检测)、ACMD41(OCR协商)等一系列命令,跟卡完成握手。
  4. card core层根据卡返回的响应数据,判断卡的类型——是普通SD卡、MMC卡还是SDIO卡。
  5. 如果是SD卡,继续发送CMD2、CMD3、CMD9等命令,读取CID(卡识别信息)、CSD(卡描述信息),确定卡容量和速度等级。
  6. card core层注册一个mmc_card设备,然后交由块设备驱动层(drivers/mmc/core/block.c)创建一个磁盘设备,也就是你看到的/dev/mmcblk0。
  7. 分区、文件系统挂载这些事,就属于内核VFS层的工作了,跟MMC子系统再无关系。

这条链路里,第1步是host控制器驱动(板级相关),第2~4步主要落在mmc core,第5~6步涉及card core和块设备层。三个core的边界,从这条链路里就已经能看出个大概。

1.2 host core、mmc core、card core各自的职责边界

直接看内核源码目录结构可能更直观。打开drivers/mmc/你看到的是这样三个子目录,外加一个Kconfig和Makefile:

drivers/mmc/ ├── core/ # mmc core:协议核心、总线管理、命令与数据流 ├── host/ # host控制器驱动:面向具体SoC硬件的适配层 ├── card/ # card core:块设备驱动、SDIO功能驱动注册

这三个目录对应内核里常说的三个core,但严格讲,“core”这个词在不同语境下指的东西略有差别。内核源码里mmc/core/core.c是协议核心,mmc/host/是控制器驱动,mmc/card/是设备驱动。按功能划分更准确的说法是:

  • host控制器驱动:它面向具体的硬件寄存器。负责实现struct mmc_host_ops里那些底层操作——request(发送命令/数据传输)、set_ios(设置时钟、电压、总线宽度)、get_ro(获取写保护状态)、enable/disable(电源管理)等。每个SoC的SD/MMC控制器寄存器都不一样,这部分就是用来抹平硬件差异的。
  • mmc core(协议核心):这是整个子系统的“交通警察”。它定义了struct mmc_host这个核心数据结构,管理总线上所有命令的发送时序、状态机流转、卡的上电初始化流程,以及mmc bus的总线注册和匹配逻辑。
  • card core(设备层):SD卡在这层被抽象成块设备,SDIO卡在这层被抽象成功能设备。它负责向上层(块设备层、网络驱动、字符设备驱动)提供统一的服务接口。

三层的关系用一句话概括:host驱动描述“我有多少个控制器、底层怎么收发数据”,mmc core负责“这些控制器如何跟卡打交道”,card core决定“卡最终以什么形态呈献给用户空间”。

我当年调试经常犯一个错误:某个功能异常,总想着在host驱动里加打印去看命令时序,后来才发现问题出在card core的探测逻辑上,白白浪费了好几天。搞清楚每一层的职责边界,能帮你快速定位问题到底出在哪一层——这是这个架构给开发者最大的价值。

接下来我按开发的实际顺序,把三个core的关键源码结构和配置流程逐一拆开讲。

2. host core实战:从设备树到mmc_host注册

实际开发中,绝大多数工程师接触的第一站就是host控制器驱动。因为无论你用什么SoC,只要板子上有SD卡座或者SDIO WiFi,你就得先确认host控制器驱动是否正常工作。这一节我重点讲host core的初始化路径和关键数据结构,这些都是写板级适配代码的必修课。

2.1 struct mmc_host是怎么一步步被创建和注册的

先从最核心的数据结构说起。内核里用struct mmc_host代表一个SD/MMC控制器实例。这个结构体很庞大,但你真正要关注的字段没那么夸张,我按优先级帮你圈几个重点:

struct mmc_host { const struct mmc_host_ops *ops; // 控制器的底层操作函数集 unsigned int f_min; // 最低时钟频率 unsigned int f_max; // 最高时钟频率 unsigned int actual_clock; // 当前实际时钟频率 unsigned int caps; // 控制器能力位掩码 u32 ocr_avail; // 支持的电压范围 struct device *parent; // 父设备,通常是指platform_device struct mmc_card *card; // 当前总线上的卡 ... };

驱动的注册路径通常是这样的:platform驱动在probe回调函数里做三件事——分配mmc_host、设置ops和各项参数、注册到内核。这是完整的主干流程:

static int my_sdio_host_probe(struct platform_device *pdev) { struct mmc_host *mmc; struct my_host_priv *priv; int ret; /* 1. 分配私有数据和mmc_host结构体 */ priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; mmc = mmc_alloc_host(sizeof(struct my_host_priv), &pdev->dev); if (!mmc) return -ENOMEM; /* 2. 把私有数据绑定到mmc_host */ priv = mmc_priv(mmc); priv->base = devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(priv->base)) return PTR_ERR(priv->base); /* 3. 填充host控制器的参数 */ mmc->ops = &my_sdio_host_ops; mmc->f_min = 400000; // 400kHz,协议要求的初始化频率 mmc->f_max = 50000000; // 50MHz,根据SoC实际能力定 mmc->ocr_avail = MMC_VDD_32_33 | MMC_VDD_33_34; mmc->caps |= MMC_CAP_SDIO | MMC_CAP_4_BIT_DATA | MMC_CAP_SD_HIGHSPEED; /* 4. 注册到MMC子系统 */ ret = mmc_add_host(mmc); if (ret) goto err_add_host; dev_info(&pdev->dev, "SDIO host registered\n"); return 0; err_add_host: mmc_free_host(mmc); return ret; }

mmc_alloc_host做的事情比较有意思:它不只是分配一块内存,还会初始化这个host的定时器、任务队列、电源管理相关字段,创建一个mmc_host_class设备类下的设备节点。你会在/sys/class/mmc_host/mmcX/看到它,每个mmcX对应一个host控制器。mmc_add_host则是把host挂到mmc_bus总线上,并触发一次扫描——如果板子上已经有卡,在这里就会开始识别流程。

注意:mmc_alloc_host的size参数是你的私有数据大小,内核会用mmc_priv(mmc)返回这个私有数据区的指针。很多驱动把寄存器基地址、中断号、DMA描述符都放在这里,这是个很实用的惯用法。

2.2 mmc_host_ops:你到底要填哪些回调函数

struct mmc_host_ops是host驱动和mmc core之间的接口契约。有点像出租车公司和司机之间的协议:mmc core是叫车的人,你只需要告诉他“我要从A到B”,而司机(host驱动)负责具体怎么踩油门、打方向盘。来看这个结构体里的关键成员:

struct mmc_host_ops { /* 关键的请求入口:发送命令、传输数据 */ void (*request)(struct mmc_host *host, struct mmc_request *mrq); /* 设置host的IO状态:时钟、电压、总线宽度 */ 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); /* 电源管理 */ int (*enable)(struct mmc_host *host); int (*disable)(struct mmc_host *host); ... };

request是整个host驱动里最核心、最难写的回调,没有之一。所有命令和数据的收发最终都会汇聚到这个函数里。它的入参struct mmc_request里面包含了命令(cmd)、数据(data)和停止命令(stop)三部分。

struct mmc_request { struct mmc_command *cmd; // 命令阶段 struct mmc_data *data; // 数据传输阶段 struct mmc_command *stop; // 可选:停止命令 ... int error; // 返回结果 struct completion completion; // 用于等待完成 };

一个典型的request回调流程是这样的:把mrq里的命令和数据翻译成硬件寄存器操作序列,启动DMA传输,然后等待中断。中断处理函数里判断传输是否完成,设置mrq->error,最后调用mmc_request_done通知mmc core可以继续下一个请求了。

这里有个新手容易掉进去的坑:request回调可能被并发调用,而上层(文件系统、协议栈)同一时间会发起多个请求。mmc core内部有排队机制,它会确保同一时刻只有一个request在host驱动里执行。但host驱动自己必须处理好中断上下文和进程上下文的同步问题,否则很容易出现资源竞争导致数据传输错乱。

set_ios这个回调也很重要。它接收一个struct mmc_ios结构体,里面带了当前期望的时钟频率、总线宽度、电压、上电状态等信息。host驱动的职责就是把这些“期望”翻译成寄存器的实际配置。通俗点讲:set_ios就是给控制器发指令——“把车速调整到60公里每小时,走四车道”。

实操建议:调试底层问题时,在set_ios里把ios->clock、ios->bus_width、ios->power_mode挨个打印出来,能很直观地看到卡的初始化流程走到哪一步了。这个技巧在内核社区也经常有人用。

3. mmc core解析:总线上的一切秩序都由它维护

如果说host驱动是具体干活的,那mmc core就是制定流程和规则的。它不直接操作硬件,但所有命令的发送时序、卡的状态迁移、总线上设备的生命周期管理,都由这一层统一调度。理解这一层,你才能真正看懂SD卡为什么会“识别失败”、SDIO设备为什么“枚举不过去”。

3.1 命令的完整生命周期:从mmc_wait_for_cmd到mmc_request_done

mmc core里最基础的函数就是mmc_wait_for_cmd,它负责把一个命令发给host驱动并等待完成。这个函数虽然短,但它揭示了mmc core和host驱动之间最重要的交互模式:

int mmc_wait_for_cmd(struct mmc_host *host, struct mmc_command *cmd, int retries) { struct mmc_request mrq = {}; cmd->retries = retries; mrq.cmd = cmd; cmd->data = NULL; mmc_wait_for_req(host, &mrq); return cmd->error; }

mmc_wait_for_req内部做的事情是:把mrq挂到host->mrq上,调用host->ops->request(host, mrq),然后等待completion。等到host驱动在中断里调用了mmc_request_done,这个等待才会被唤醒。整个过程是典型的“请求-响应”异步模型——进程上下文发起请求后挂起,中断上下文完成后唤醒。

这里有一个值得注意的设计:一个host同一时刻只能有一个在飞的mmc_request。这个约束是整个MMC子系统的基本前提。内核在mmc_wait_for_req里有相应逻辑来确保不会同时有两个request插入,这个机制保证了一个host控制器的硬件资源不会被多个请求并发踩踏,但同时也意味着host驱动的request回调实现必须快,长延时操作会让整个存储/网络链路卡顿。

完整命令生命周期大致如下:

  1. card驱动决定发送某个命令,构造struct mmc_command,填入命令号、参数、期望的响应类型。
  2. mmc core把命令包装成mmc_request,检查host状态、电源状态。
  3. 调用host->ops->request,host驱动把命令写入硬件寄存器,触发控制器发送。
  4. 硬件把命令发到卡上,卡返回响应,触发中断(或DMA完成中断)。
  5. host驱动在中断上下文读取响应数据、检查错误状态,填充mrq的返回信息。
  6. 调用mmc_request_done,唤醒等待的进程。
  7. mmc core检查命令的返回响应,更新卡状态。

你看,整个过程中mmc core像是个包工头,真正的体力活都由host驱动干了。也正因为如此,如果host驱动的request实现有问题,那mmc core上层能拿到的就只有各种错误码——这也是为什么调试SD/SDIO驱动问题,第一步永远是定位是不是host底层就没把命令正确发出去。

3.2 卡检测机制:轮询、中断和mmc_rescan

mmc core里有一个后台线程叫mmc_rescan,它负责周期性地扫描host总线上有没有新卡插入或者旧卡被移除。扫描机制大致是:

  • 定时器周期触发mmc_rescan。
  • mmc_rescan调用host->ops->get_cd检查卡是否在位。
  • 如果在位且之前没有卡,就执行完整的初始化流程。
  • 如果不在位但之前有卡,就执行移除流程。

get_cd两种实现方式最常见:一种是GPIO中断触发检测,另一种是host控制器内部寄存器自带CD状态位。前者需要驱动里主动注册中断,后者只需要在get_cd回调里读取状态寄存器即可。会做GPIO中断触发的场景,通常是为了省电——卡插入瞬间才唤醒系统执行扫描,而不是一直轮询。

mmc_rescan的初始化流程里有个关键顺序:先以最低频率(400kHz)发送广播命令,检测卡的响应,然后根据响应类型判断卡类型。这一步就是所谓的“卡识别模式”,命令顺序大致是:

  1. CMD0:让所有卡进入空闲态。
  2. CMD8:发送接口条件检测,区分SD V2.0以上卡和旧卡/MMC卡。
  3. ACMD41:请求卡返回OCR寄存器,协商工作电压。
  4. 若ACMD41响应表明是SDIO卡,则走SDIO初始化流程;否则继续走SD/MMC流程。
  5. CMD2:获取CID(卡唯一标识)。
  6. CMD3:分配相对卡地址(RCA)。
  7. CMD9:读取CSD,获知卡的容量、速度等能力参数。

每一步都对应mmc core源码里的一个函数,从mmc_sd_init_card、mmc_sdio_init_card到mmc_init_card,你可以顺着mmc_rescan的代码一路跟下来。对驱动开发者来说,这里的核心启示是:卡识别流程是一条严格有序的状态机,任何一步失败都会导致整个识别流程终止。在实际调试中,识别失败问题最经典的排查手段就是:开着CONFIG_MMC_DEBUG编译内核,打开mmc core的调试日志,看它卡在哪一条命令上。

提示:调试SDIO设备时尤其要注意CMD52(IO_RW_DIRECT)和CMD53(IO_RW_EXTENDED)这两个命令。很多SDIO功能设备的初始化依赖这两类命令读写其内部的CIS(Card Information Structure)和FBR(Function Basic Registers)。如果mmc core连CMD52都发不过去,问题基本可以锁定在host底层。

3.3 电压协商和时钟频率的演进逻辑

SD卡初始化过程里有个容易让人忽略但极其关键的细节:卡识别阶段只能跑400kHz,初始化完成后才能切换到高速时钟。这是SD物理层协议的强制要求。原因是卡刚上电时状态不确定,只有低速时钟下才能稳定通信;等双方通过ACMD41协商好了工作电压、通过CMD6切换了功能模式,才能逐步提高时钟频率。

set_ios回调在这个过程里会被反复调用,每一次都是不同的ios->power_mode和ios->clock组合。一个典型的时序演进是这样的:

  1. power off:host不给卡供电。
  2. power up:卡上电,时钟400kHz,总线宽度1-bit。
  3. power on:卡已完成上电复位,继续400kHz,发送识别命令。
  4. 识别完成后,切换到数据传输模式,时钟升高到25MHz/50MHz,总线宽度切换到4-bit。

这个演进过程全部由mmc core控制,host驱动只是被动执行set_ios。如果你在host驱动里擅自改了时序,比如提前拉高时钟,很可能会出现一种诡异现象:大部分卡能正常识别,某一张卡却偶发失败。这基本就是时序违反了协议规范。

4. card core解析:从协议设备到用户空间服务

前两层搞清楚了,第三层相对轻松一些,但它是SDIO外设驱动开发的主战场。因为你最终要打交道的SDIO WiFi、蓝牙模块,它们在MMC子系统里的身份就是card core层注册的SDIO功能设备。

4.1 mmc_attach_sdio如何把一个SDIO卡变成一组功能设备

SDIO协议里,一张物理卡可以同时包含最多7个功能(Function),每个功能对应一个独立的设备。比如一个Combo芯片可以同时提供WiFi和蓝牙两个SDIO功能,它们在逻辑上是独立的设备,共享同一条SDIO总线。

mmc_attach_sdio的流程大致是这样的:

  • 通过CMD5(IO_SEND_OP_COND)获取卡的IO能力信息,判断是否真的是SDIO卡。
  • 读取CIS(Card Information Structure),获取卡的厂商信息、设备信息、功能的注册地址等。
  • 为每个存在的功能创建一个struct sdio_func设备,挂到mmc bus上。
  • 触发总线匹配,让对应功能的驱动与设备绑定。

在/sys/bus/sdio/devices/目录下,你会看到类似sio1:0001、sio2:0001这样的设备节点,sio1代表这是SDIO总线,冒号后是厂商ID和设备ID。你的SDIO功能驱动就是通过sdio_device_id匹配表跟这些设备节点绑定的。

4.2 sdio_func结构体和驱动绑定机制

SDIO功能设备的驱动模型跟platform驱动很像,核心数据结构是struct sdio_func和struct sdio_driver。我摘一段SDIO功能驱动的骨架代码,这个模式在WiFi、蓝牙、GNSS模组驱动里几乎是通用模板:

static const struct sdio_device_id my_sdio_devices[] = { { SDIO_DEVICE(0x1234, 0x5678) }, {} }; MODULE_DEVICE_TABLE(sdio, my_sdio_devices); static int my_sdio_func_probe(struct sdio_func *func, const struct sdio_device_id *id) { struct my_dev *dev; int ret; func->card->quirks |= MMC_QUIRK_LENIENT_FN0; dev = kzalloc(sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; sdio_claim_host(func); ret = sdio_enable_func(func); if (ret) goto err_enable; ret = sdio_set_block_size(func, 64); if (ret) goto err_set_blksz; sdio_release_host(func); dev->func = func; sdio_set_drvdata(func, dev); /* 注册此函数对应的子系统设备,例如网络接口或字符设备 */ return 0; err_set_blksz: sdio_disable_func(func); err_enable: sdio_release_host(func); kfree(dev); return ret; } static void my_sdio_func_remove(struct sdio_func *func) { struct my_dev *dev = sdio_get_drvdata(func); /* 反注册子系统设备,释放资源 */ kfree(dev); } static struct sdio_driver my_sdio_driver = { .name = "my_sdio_func", .id_table = my_sdio_devices, .probe = my_sdio_func_probe, .remove = my_sdio_func_remove, }; module_sdio_driver(my_sdio_driver);

这段代码里有几个点值得说道说道:

  • sdio_claim_host非常关键。前文讲过,一个host同一时刻只能有一个请求在飞。SDIO多函数设备共用一条总线,两个function同时发CMD52/CMD53就会冲突。sdio_claim_host/release_host就是用来做这个互斥的。所有SDIO读写操作之前,都必须先claim host。

  • sdio_enable_func内部会给这个功能设置上电状态和中断使能。如果这个函数被漏掉,后续的IO操作会直接失败。

  • sdio_set_block_size设置为64字节是一个常见的经验值。很多SDIO外设默认支持的最大块大小并不是协议里理论上的最大值,设置过大的块会导致数据丢失、死机等诡异问题。一般只要不是特殊需求,64字节是各厂商兼容性最好的选择。

  • func->card->quirks |= MMC_QUIRK_LENIENT_FN0是给特定芯片打补丁的惯用手法。SDIO协议规定Function 0只能用CMD52做单字节读写,但有些芯片的寄存器访问需要CMD53多次读写。这种非标行为就得靠quirk标志来放宽限制。

从probe函数开始,你的驱动就正式接管了这个SDIO功能设备。后面的事情,比如注册一个net_device、一个input_dev或者一个tty设备,就完全取决于这个模组外设的功能了。

4.3 CMD52和CMD53:SDIO外设驱动最常用的两条命令

SDIO驱动开发绕不开CMD52和CMD53这两条命令,它的读写方式和普通SD卡的块读写完全不同。简单区分:

  • CMD52:IO_RW_DIRECT,直接读写单个寄存器。类似I2C的单字节读写字操作。SDIO的CIS、FBR、CCCR寄存器都是用CMD52来访问的。
  • CMD53:IO_RW_EXTENDED,一次读写一块数据。类似I2C的页读写,通常用于DMA批量数据传输。SDIO WiFi的数据包收发就是通过CMD53完成的。

内核里对应的封装函数是sdio_readb、sdio_writeb、sdio_readsb、sdio_writesb,以及类似sdio_readw、sdio_readl的多字节版本。这些封装函数会自动处理sdio_claim_host/sdio_release_host,不需要你手动调用。但有一点要特别注意:sdio_readb等函数在失败时会返回一个错误值(通常是-EIO),你需要结合sdio_f0_readb这类函数来读取Function 0的寄存器,因为普通的sdio_readb无法访问Function 0地址空间。

踩坑记录:曾经有个案子,WiFi模组的寄存器配置一直写不进去,实测总线波形都正常,命令也发出去了,但响应数据就是不对。后来发现是块大小设置成了512字节,而模组的内部FIFO最大支持256字节块,CMD53传输出错被控制器直接丢弃了。改回128字节块后问题立刻消失。所以遇到数据通路异常,先查块大小和FIFO深度的匹配关系。

5. 实操:开发一个模拟SDIO WiFi模组的完整流程

前面讲了大量原理,这一节我用一个实际开发场景来串一遍。假设我们要在一个新的SoC平台上,把一款SDIO接口的WiFi模组(以下简称为“某WiFi模组”)驱动起来。平台host控制器的驱动是现成的,但我们需要在设备树里使能SDIO节点、确认host初始化正确,然后完成SDIO功能驱动的适配。

5.1 第一步:设备树配置与host控制器验证

设备树里要配置的是host控制器的IO属性:时钟、电源、CD引脚、数据线引脚。一个典型的配置长这样:

&sdio0 { status = "okay"; /* 原来默认是disabled,需要打开 */ bus-width = <4>; /* 使用4-bit模式 */ max-frequency = <50000000>; /* 最大50MHz */ sdio = <&sdio0>; /* 标记为SDIO控制器(具体写法因平台而异) */ cap-sdio-irq; /* 声明支持SDIO设备中断 */ keep-power-in-suspend; /* 挂在WiFi时,休眠要保持供电 */ non-removable; /* WiFi模组不是可插拔卡,焊死在板上的 */ vmmc-supply = <&vmmc_regulator>; /* 卡供电稳压器 */ pinctrl-0 = <&sdio0_clk &sdio0_cmd &sdio0_data0 &sdio0_data1 &sdio0_data2 &sdio0_data3>; pinctrl-names = "default"; };

这段配置里几个属性的取舍值得注意:

  • non-removable在WiFi/蓝牙模组场景下几乎是必加的。它告诉mmc core这个设备不会热插拔,省掉了CD引脚的检测逻辑,还能避免某些平台的CD检测误触发导致设备反复掉线。
  • keep-power-in-suspend也很关键。WiFi模组需要做WOW(Wake-on-WLAN)等低功耗唤醒功能,系统休眠时不能把卡的电给断了,否则唤醒后设备状态全部丢失,只能重新枚举。
  • enforce-sdio-irq或者cap-sdio-irq这类属性是告诉host控制器:SDIO外设会通过DAT1引脚向host发送中断。这种“带内中断”机制是SDIO的一大特色,WiFi模组的数据包到达通知就是靠它实现的。

设备树配好后,启动系统,可以先看/sys/class/mmc_host/下是否出现了新的host节点,再通过dmesg确认host驱动的probe是否成功。如果host驱动本身没跑起来,后面的一切都无从谈起。

5.2 第二步:利用mmc core完成SDIO卡枚举

只要设备树正确、host驱动正常,mmc core会在host注册后自动发起一次扫描。SDIO卡枚举成功的日志大致长这样:

mmc2: new SDIO card at address 0001 mmc2: SDIO device [vendor:0x1234, device:0x5678] mmc2: new SDIO function device(s): 1 function

看到new SDIO card这行日志,说明卡识别流程已经走通了。这时候去/sys/bus/sdio/devices/下会看到类似sio2:0001的目录。如果你的模组被识别出来了,下一步才是SDIO功能驱动的工作。

如果卡识别失败,常见的错误日志是:

mmc2: error -110 whilst initialising SDIO card

-110对应的errno是ETIMEDOUT,也就是说某条命令超时了。这类问题优先查硬件上电、时钟、IO电压。我遇到过一次-110,最后定位到是vmmc稳压器的最大驱动电流不够,卡瞬间把电压拉垮了。这属于硬件问题,不是软件代码问题,但从排查顺序上讲,dmesg日志能帮你快速锁定怀疑方向。

5.3 第三步:对接SDIO功能驱动并验证数据通路

SDIO功能驱动的写法在前面已经给出了骨架代码。真正对接时还要做这些事:

  • 确认厂商ID/设备ID匹配:从dmesg日志或者/sys/bus/sdio/devices/sio2:0001/vendor、device文件读到ID,填到sdio_device_id表里。
  • 在probe里读取FBR寄存器,确认该功能设备支持多大的块、有多少个IO区域。这块信息可以通过sdio_readb读取寄存器获取。
  • 注册子系统的设备接口。比如WiFi模块,通常注册一个ieee80211_hw交给mac80211子系统管理;蓝牙模块则注册一个hci_dev。不管注册到哪个子系统,核心的数据通路都是通过CMD53批量收发数据包。

数据通路的验证方法,最早期我习惯用内存映射IO测试:直接通过sdio_readsb/sdio_writesb读写一块已知内容的缓冲区,连续循环几千次,确认数据不出错。这一步能排除DMA配置、FIFO深度、中断处理路径上的潜在问题。数据通路稳定后,再往上层暴露业务接口,会省很多调试时间。

5.4 中断处理:SDIO带内中断的注册与使用

SDIO设备不像普通PCIe设备那样有独立的中断引脚,它的中断信号是通过DAT1引脚在特定时间片里插入的。mmc core在初始化时会配置卡的中断使能,host控制器在DAT1上检测到中断信号后,会调用sdio_irq_thread在进程上下文里轮询每个function的中断状态寄存器,然后回调对应sdio_driver注册的irq_handler。

如果你的SDIO外设需要中断通知,可以通过sdio_claim_irq、sdio_release_irq来注册回调:

static irqreturn_t my_sdio_irq_handler(int irq, void *dev_id) { struct my_dev *dev = dev_id; /* 清除设备侧中断状态,读取FIFO数据等 */ schedule_work(&dev->work); return IRQ_HANDLED; } ret = sdio_claim_irq(func, my_sdio_irq_handler); if (ret) dev_err(&func->dev, "failed to claim sdio irq\n");

这个回调运行在sdio_irq_thread的进程上下文里,而不是硬中断上下文,所以可以做一些相对耗时的操作。但要注意sdio_irq_thread的轮询间隔受主机控制器时钟和各功能驱动处理速度影响,如果你要极致性能,可以考虑在host驱动层实现更高效的中断处理路径,这属于高级优化了。

提示:很多SDIO WiFi驱动初始化模组固件时,需要先下载几百KB的固件到模组的RAM中,这个过程就是通过CMD53批量写。如果固件下载失败,模组不会进入工作状态,中断也不会正常产生。遇到WiFi模块扫描不到网络,千万别一头扎进无线协议栈的调试里,先确认SDIO固件有没有下成功。

6. 调试技巧与问题排查实录

驱动开发没有哪次是一把过的。最后这一部分我把自己在SD/SDIO驱动开发过程中积累的一些高频问题、排查思路和工具心得整理一下,这些内容在很多文档里不会写。

6.1 内核配置项:MMC_DEBUG和动态调试怎么开

drivers/mmc/core和drivers/mmc/host里有大量dev_dbg级别的日志,默认不打出来。调试时一定把这两个Kconfig配置打开:

CONFIG_MMC=y CONFIG_MMC_DEBUG=y CONFIG_DYNAMIC_DEBUG=y

CONFIG_MMC_DEBUG会把pr_debug全部启用,配合CONFIG_DYNAMIC_DEBUG,运行期可以通过debugfs精准控制某个文件的日志输出级别,不用每次改代码重新编内核。这在dmesg日志已经被刷屏、需要看特定函数细节的时候特别管用。

查看动态调试的使用方法:

# 查看mmc core相关调试点的当前状态 cat /sys/kernel/debug/dynamic_debug/control | grep drivers/mmc # 打开某个源文件的全部调试输出 echo 'file drivers/mmc/core/sdio.c +p' > /sys/kernel/debug/dynamic_debug/control

开了调试后,dmesg能看到的日志包括每个命令的发送情况、响应状态、超时信息。这些日志在定位“卡在哪条命令”上价值巨大。

6.2 常见问题的现象、根因和排查方向

我整理了一张速查表,覆盖了SD/SDIO调试里最常碰到的几类问题,按“现象 → 优先排查方向 → 典型根因”来组织:

现象优先排查方向典型根因
卡完全没反应,dmesg无任何日志电源、复位引脚、CD引脚状态host驱动probe失败,设备树节点没打开,或稳压器输出异常
识别过程报-110超时时钟配置、命令时序、电压稳定性电压跌落、时钟频率过高、卡座接触不良
识别成功但块读写失败set_ios总线宽度配置、DMA配置4-bit模式配置不对,或DMA描述符写错
SDIO设备枚举失败CMD52能否成功、CIS数据是否可读FIFO深度/块大小不匹配,ENABLE位没设置
偶发掉卡、读写不稳定信号完整性、电源纹波、时钟抖动差分线长不匹配、地线回流路径太长、驱动能力不足
休眠唤醒后设备无法工作休眠时的电压与时钟处理未配置keep-power-in-suspend,或SDIO中断唤醒没有正确注册

每个方向展开都能写很长,但上面的表格提纲挈领,大家遇到问题时可以先按图索骥缩小范围。

6.3 逻辑分析仪是我排查命令时序的最后底牌

软件手段能定位80%的问题,但剩下20%的硬件协议问题,只能靠逻辑分析仪。SDIO总线协议比较简单,CLK、CMD、DAT0-DAT3这几根线拉出来,用逻辑分析仪抓一次初始化时序,对照SD spec的命令索引和响应内容,就能非常快地发现问题。

有个实际案列:某平台在低温环境下偶发SD卡识别失败,软件层打印显示CMD8发送超时。逻辑分析仪一抓,发现CMD线的上升沿变缓,正是低温下上拉电阻驱动能力衰减导致的信号质量问题。这类型的问题,软件怎么调参都治标不治本,确认根因后直接在硬件上调整上拉电阻阻值才彻底解决。内核里mmc_host_ops提供的调参能力再强,也弥补不了硬件设计缺陷。

实操心得:如果逻辑分析仪抓到的波形显示命令正常、响应正常,但dmesg还是报错误,那八成是host驱动解析响应时出了问题。重点检查struct mmc_command里的响应类型resp_type是不是填对了。响应类型填错,mmc core会按错误的字段偏移解析,明明收到一个合法的R6响应,硬是解析成了R1,结果当然报错。这类问题通常表现为“用SDIO卡时好时坏,用普通SD卡完全正常”的奇怪现象。

6.4 电源管理排查:一个隐蔽的卡掉线问题

再讲一个我印象深刻的排查过程。某平台的SDIO WiFi在深度休眠唤醒后,经常出现WiFi无法联网的故障。从dmesg看,SDIO设备还在,但功能驱动的寄存器读写全部返回错误。

排查过程回顾一遍:

  1. 先在probe里加打印,确认唤醒后probe不再被调用,说明设备枚举状态还在。
  2. 用sdio_readb读取CCCR寄存器,发现返回0xFF,这通常是总线无响应的标志。
  3. 抓逻辑分析仪,发现唤醒后CMD线根本没有任何活动——host控制器没发命令。
  4. 查看host驱动的休眠回调,发现休眠时把控制器时钟关了,但唤醒后没有恢复f_min初始频率,导致后续命令无法正常送出。
  5. 修复休眠回调,确保唤醒后先重新初始化控制器时钟,再恢复设备状态。

这个例子说明SDIO调试的问题往往跨层交叉:不是单纯的host问题,也不是单纯的设备问题,而是电源管理路径上的时序缺口。排查这类问题,关键是脑子里始终保有一条完整的状态链路图——从电源、时钟到总线、设备、驱动,一层一层顺着查。

6.5 别忘了内核社区和现有驱动里的现成答案

最后说点经验层面的东西。SDIO驱动开发不太需要从零开始写,因为内核里已经有大量现成的高质量驱动可以参考。比如各种SDIO WiFi、蓝牙、GPS模组的驱动代码,里面包含了完整的probe/中断/数据通路/电源管理实现。你做新平台适配时,先找内核里功能最接近的驱动改,比自己重新发明轮子效率高得多。硬要手撕全部逻辑,反而容易在缓冲管理、并发访问、电源状态机这些小细节上漏掉一堆坑。

还有一个容易被忽视的渠道:SoC厂商提供的内核分支里,往往带有本平台host控制器的完整驱动和板级配置示例。虽然厂商代码质量参差不齐,但作为新平台第一个能跑起来的参考,价值很大。比如很多厂商的SDIO驱动里就带了对自家WiFi模组的quirk处理,这些兼容性细节自己踩坑可能要花上几周时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询