[toc]

drivers/mmc 多媒体卡(MultiMediaCard)子系统 SD/eMMC/SDIO设备驱动框架

历史与背景

这项技术是为了解决什么特定问题而诞生的?

drivers/mmc 子系统是为了给Linux内核提供一个统一的、可扩展的框架来支持一整类基于**多媒体卡(MultiMediaCard)**规范及其衍生协议的设备而诞生的。这些设备包括:

  • MMC (MultiMediaCard):一种早期的闪存卡标准。
  • SD (Secure Digital) Card:在MMC基础上发展而来的、目前最普及的移动存储卡,增加了安全特性和更高的性能。
  • eMMC (embedded MMC):将MMC控制器和NAND闪存封装在同一个BGA芯片中,作为一种“嵌入式”存储解决方案,被广泛用作智能手机、平板电脑和许多嵌入式设备的“硬盘”。
  • SDIO (Secure Digital Input/Output):一种扩展规范,允许SD插槽除了支持存储卡外,还能连接I/O设备,如Wi-Fi模块、蓝牙模块、GPS接收器等。

在没有这个统一框架之前,对这些设备的支持是零散的。该框架的诞生解决了以下关键问题:

  • 代码复用:MMC、SD、SDIO、eMMC协议有大量共通之处。一个统一的框架可以抽象出公共的协议处理、命令收发、数据传输逻辑,避免为每种卡和每种主机控制器都重写一遍。
  • 硬件抽象:市面上有各种各样的SD/MMC主机控制器(Host Controller),它们是SoC上负责与SD卡进行物理通信的IP核。该框架需要将这些具体硬件的差异隔离开,为上层提供一个标准接口。
  • 功能分离:需要将对“卡的功能”(是存储设备还是I/O设备)的驱动与底层的“总线协议”驱动分离开。

它的发展经历了哪些重要的里程碑或版本迭代?

  • 框架的建立:最重要的里程碑是将分散的驱动重构为一个分层的、模块化的MMC子系统。这确立了Host驱动 - Core - Card驱动的核心架构。
  • SDHCI规范的支持:SD主机控制器接口(SD Host Controller Interface)是一个标准化的硬件寄存器规范。sdhci.c驱动的出现是一个巨大的进步,因为它提供了一个通用的Host驱动,可以支持任何符合SDHCI规范的硬件控制器,大大减少了为新SoC编写Host驱动的工作量。
  • 高性能模式的支持:随着SD卡和eMMC标准的演进,框架不断增加对新功能和更高速度模式的支持,如DDR模式、HS200(200MB/s)、HS400(400MB/s)等。
  • eMMC高级功能:为eMMC增加了对TRIM/Discard(提升闪存寿命和性能)、安全擦除、分区管理等高级功能的支持。

目前该技术的社区活跃度和主流应用情况如何?

MMC子系统是Linux内核中最为成熟和活跃的核心子系统之一。由于几乎所有的移动和嵌入式设备都依赖它,因此它得到了持续的维护和功能增强。

  • 主流应用
    • 智能手机/平板电脑:eMMC曾是安卓设备内部存储的主流标准(现在正逐渐被UFS取代)。
    • 单板计算机(SBC):如树莓派,使用SD卡作为其主启动和存储设备。
    • 物联网(IoT)设备:广泛使用SDIO接口的Wi-Fi/蓝牙模块。
    • 数码相机、无人机等消费电子产品:使用SD卡作为主要存储介质。

核心原理与设计

它的核心工作原理是什么?

drivers/mmc 的核心是一个清晰的三层架构

  1. 主机控制器驱动 (Host Driver) - (drivers/mmc/host/)

    • 职责:这是最底层,直接与硬件打交道。它负责控制具体的SD/MMC控制器IP核,包括管理时钟、电源、引脚、执行DMA传输、处理硬件中断等。
    • 实现:每个Host驱动(如sdhci.c for SDHCI, dw_mmc.c for Synopsys DesignWare a core)都会实现一套标准的操作函数集 struct mmc_host_ops
    • 抽象:它将底层硬件的复杂性抽象为一个标准的MMC主机对象 (struct mmc_host),并将其注册到MMC核心。
  2. MMC核心 (Core) - (drivers/mmc/core/)

    • 职责:这是整个子系统的大脑和调度中心。它实现了MMC/SD/SDIO协议栈,负责卡的上电、初始化、识别过程,以及命令(CMD)和数据(DAT)的收发逻辑。
    • 实现:核心层代码不关心具体的Host硬件是什么样的。当它需要发送一个命令时,它会通过Host驱动注册的mmc_host_ops中的函数(如 .request(), .set_ios())来间接地控制硬件。
    • 角色:它扮演了总线驱动的角色,扫描总线上(插槽里)的设备,并为识别出的设备创建对应的逻辑设备。
  3. 设备驱动 (Card Driver) - (drivers/mmc/card/)

    • 职责:这是最上层,负责驱动卡片本身的功能。
    • 实现
      • block.c:这是一个通用的Card驱动。当MMC核心识别出一张存储卡(SD或eMMC)时,block.c会绑定到该设备,并创建一个块设备节点(如 /dev/mmcblk0),使其可以被文件系统挂载。
      • sdio.c:当MMC核心识别出是一张SDIO卡时,它会进一步解析卡上的功能(Function),并为每个功能(如WLAN, Bluetooth)创建SDIO设备。然后,相应的SDIO功能驱动(如位于drivers/net/wireless/下的Wi-Fi驱动)会绑定到这些设备上。

它的主要优势体现在哪些方面?

  • 高度模块化:三层架构使得各部分可以独立开发和维护。添加对新Host控制器的支持只需编写一个新的Host驱动;支持一种新的SDIO设备只需编写一个新的Card驱动,核心层代码保持不变。
  • 代码复用率高:协议逻辑和公共流程都集中在核心层,被所有驱动共享。
  • 标准化:紧密遵循MMC/SD/SDIO的官方规范,保证了良好的兼容性。SDHCI的通用驱动更是标准化的典范。
  • 灵活性:同一个框架无缝支持存储、I/O等多种类型的设备。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 性能瓶颈:与现代的NVMe或UFS接口相比,MMC/SD协议(即使是最高速的模式)在吞吐量、延迟和IOPS方面都存在性能上限。它是一个半双工的总线,命令和数据传输不能完全并行。
  • 协议复杂性:MMC/SD的初始化过程和命令集相当复杂,给调试带来了一定的挑战。
  • 功耗:虽然SDIO功耗不高,但相比专用的低功耗总线(如I2C/SPI),在某些极低功耗场景下可能不是最优选择。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。

  • 可移动存储:SD卡是事实上的标准。所有需要可插拔、小型化存储卡的场景,如数码相机、运动相机、无人机、便携式游戏机,都依赖MMC子系统。
  • 高性价比的嵌入式系统存储:eMMC在成本、性能和封装尺寸之间取得了很好的平衡,因此在对成本敏感的中低端智能手机、平板电脑、车载信息娱乐系统、智能电视中,是内部存储的首选方案。
  • 无线模块连接:SDIO是连接Wi-Fi/蓝牙组合芯片的一种非常流行和成熟的接口,特别是在各种物联网网关和嵌入式设备中。

是否有不推荐使用该技术的场景?为什么?

  • 高性能计算与存储:在需要极高I/O性能的场景,如企业级服务器、高端PC、高端智能手机,应使用NVMe SSD或UFS闪存。这些技术基于PCIe或M-PHY,提供多通道、全双工、更高效的命令队列机制,性能远超eMMC。
  • 简单的低速外设:对于只需要传输少量控制数据的简单传感器或外设,使用SDIO是“杀鸡用牛刀”。更简单、引脚更少的总线,如I2C或SPI,是更合适的选择。

对比分析

请将其 与 其他相似技术 进行详细对比。

特性 MMC/SD/eMMC USB (Mass Storage Class) UFS (Universal Flash Storage) SATA / NVMe
主要用途 可移动存储、嵌入式内部存储、SDIO外设 通用的外部设备连接(存储、外设) 高性能嵌入式内部存储(eMMC的替代者) 高性能内部/外部存储
总线协议 并行、半双工、命令-响应式协议。 差分串行、包交换、主从协议。 差分串行、全双工、基于SCSI命令集。 差分串行、全双工、基于ATA或PCIe命令集。
性能 中等。eMMC最高约400MB/s,SD卡速度不等。延迟较高。 高。USB 3.x/4可达数GB/s。 高。UFS 3.x/4.0可达数GB/s,延迟低,支持命令队列。 非常高。SATA 6Gbps,NVMe可达数十GB/s,延迟极低。
物理接口 并行数据线+命令线+时钟线 (1/4/8位)。 串行差分对 (D+/D-)。 高速差分串行对 (M-PHY)。 高速差分串行对。
应用场景 成本敏感的移动/嵌入式设备,SD卡生态。 PC、移动设备的外设连接,U盘。 中高端智能手机、平板电脑、汽车。 PC、工作站、服务器。
内核子系统 drivers/mmc drivers/usb drivers/ufs (或 drivers/scsi) drivers/ata, drivers/pci

include/linux/mmc/mmc.h

MMC协议核心定义:命令、响应及寄存器规范

本文件是Linux内核中用于支持MMC(MultiMediaCard)和eMMC(embedded MMC)存储卡的核心协议头文件。与sdio.h专注于I/O功能不同,此文件定义了MMC存储卡相关的命令集、响应格式、寄存器(CSD, EXT_CSD)布局以及各种状态和能力标志。它将JEDEC发布的MMC物理规范中的数值和位域,转化为内核代码可以使用的符号常量和宏,是所有MMC/eMMC驱动程序与卡进行交互的基础语言。

实现原理分析

该文件的实现是典型的将硬件规范文档化的编码实践,其核心是利用C预处理器将协议的细节抽象化:

  1. 命令符号化: 每一个MMC命令(如CMD0, CMD1, …)都被#define为一个唯一的整数值,并赋予一个描述性的名称,例如MMC_GO_IDLE_STATE。这使得驱动代码的逻辑(如状态机转换)清晰易懂。
  2. 响应格式位域化: 卡片对命令的响应(特别是R1响应)是一个包含多个状态和错误标志的32位字。文件通过定义位掩码(如R1_OUT_OF_RANGE (1 << 31))和提取宏(如R1_CURRENT_STATE(x)),将解析这个状态字的过程符号化,使得错误处理和状态判断逻辑标准化。
  3. 寄存器映射: MMC卡拥有复杂的内部寄存器,如CSD(Card-Specific Data)和至关重要的EXT_CSD(Extended CSD)。该文件将EXT_CSD这个长达512字节的寄存器中的每一个字节偏移量(offset)都定义为一个宏,如EXT_CSD_BUS_WIDTH (183)。同时,寄存器内特定位或位域的含义也被定义为宏,如EXT_CSD_BUS_WIDTH_8 (2)。
  4. 内联辅助函数: 文件提供了一些简单的static inline函数,如mmc_op_multi,用于快速判断一个命令的类型。这些函数在编译时会被内联,没有函数调用的开销,提供了类型安全的便捷检查。

代码分析

MMC命令定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* 标准MMC命令 (根据JEDEC 4.1规范) */
/* Class 1: 基础命令 */
#define MMC_GO_IDLE_STATE 0 // CMD0: 使卡进入空闲状态 (复位)
#define MMC_SEND_OP_COND 1 // CMD1: 发送操作条件,用于初始化和识别卡
#define MMC_ALL_SEND_CID 2 // CMD2: 请求所有卡发送其CID(卡识别)号
#define MMC_SET_RELATIVE_ADDR 3 // CMD3: 设置卡的相对地址 (RCA)
#define MMC_SWITCH 6 // CMD6: 切换卡的功能 (例如, 速度模式, 总线宽度)
#define MMC_SELECT_CARD 7 // CMD7: 选择/取消选择一张卡
#define MMC_SEND_EXT_CSD 8 // CMD8: 读取扩展CSD寄存器 (对eMMC至关重要)
#define MMC_SEND_CSD 9 // CMD9: 读取CSD寄存器
#define MMC_STOP_TRANSMISSION 12 // CMD12: 停止多块读/写操作
#define MMC_SEND_STATUS 13 // CMD13: 获取卡的状态

/* Class 2 & 4: 块读写命令 */
#define MMC_SET_BLOCKLEN 16 // CMD16: 设置块长度
#define MMC_READ_SINGLE_BLOCK 17 // CMD17: 读单个块
#define MMC_READ_MULTIPLE_BLOCK 18 // CMD18: 读多个块
#define MMC_SET_BLOCK_COUNT 23 // CMD23: 设置后续多块传输的块数量 (可靠写入)
#define MMC_WRITE_BLOCK 24 // CMD24: 写单个块
#define MMC_WRITE_MULTIPLE_BLOCK 25 // CMD25: 写多个块

/* Class 5: 擦除命令 */
#define MMC_ERASE_GROUP_START 35 // CMD35: 定义擦除组的起始地址
#define MMC_ERASE_GROUP_END 36 // CMD36: 定义擦除组的结束地址
#define MMC_ERASE 38 // CMD38: 执行擦除

/* Class 8: 应用特定命令 */
#define MMC_APP_CMD 55 // CMD55: 指示下一条命令是应用特定命令 (SD卡用)
#define MMC_GEN_CMD 56 // CMD56: 通用命令读/写

R1响应格式与状态定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
MMC R1响应中的状态位定义 (原生模式)
*/
#define R1_OUT_OF_RANGE (1 << 31) // 参数超出范围错误
#define R1_ADDRESS_ERROR (1 << 30) // 地址错误
#define R1_BLOCK_LEN_ERROR (1 << 29) // 块长度错误
#define R1_COM_CRC_ERROR (1 << 23) // 命令CRC校验错误
#define R1_ILLEGAL_COMMAND (1 << 22) // 非法命令
#define R1_ERROR (1 << 19) // 通用错误
#define R1_STATUS(x) (x & 0xFFF9A000) // 提取R1响应中所有错误位的掩码
#define R1_CURRENT_STATE(x) ((x & 0x00001E00) >> 9) // 提取当前卡状态 (4位)
#define R1_READY_FOR_DATA (1 << 8) // 卡已准备好接收数据

/* R1_CURRENT_STATE(x) 的可能值 */
#define R1_STATE_IDLE 0 // 空闲状态
#define R1_STATE_READY 1 // 准备状态
#define R1_STATE_IDENT 2 // 识别状态
#define R1_STATE_STBY 3 // 待命状态
#define R1_STATE_TRAN 4 // 传输状态
#define R1_STATE_DATA 5 // 发送数据状态
#define R1_STATE_RCV 6 // 接收数据状态
#define R1_STATE_PRG 7 // 编程状态
#define R1_STATE_DIS 8 // 断开连接状态

扩展CSD (EXT_CSD) 寄存器关键字段定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/*
* EXT_CSD 寄存器字段的字节偏移量定义
*/

#define EXT_CSD_BKOPS_EN 163 // 后台操作使能
#define EXT_CSD_BKOPS_START 164 // 启动后台操作
#define EXT_CSD_ERASE_GROUP_DEF 175 // 擦除组定义
#define EXT_CSD_PART_CONFIG 179 // 分区配置
#define EXT_CSD_BUS_WIDTH 183 // 总线宽度模式
#define EXT_CSD_HS_TIMING 185 // 高速时序接口
#define EXT_CSD_CARD_TYPE 196 // 卡类型 (支持的速度模式)
#define EXT_CSD_REV 192 // EXT_CSD 结构版本
#define EXT_CSD_SEC_CNT 212 // 设备扇区计数 (4字节)

/* EXT_CSD_CARD_TYPE 字段的位域定义 */
#define EXT_CSD_CARD_TYPE_HS_26 (1<<0) // 卡支持 26MHz
#define EXT_CSD_CARD_TYPE_HS_52 (1<<1) // 卡支持 52MHz
#define EXT_CSD_CARD_TYPE_DDR_52 (1<<2 | 1<<3) // 卡支持 52MHz DDR模式
#define EXT_CSD_CARD_TYPE_HS200 (1<<4 | 1<<5) // 卡支持 200MHz HS200模式
#define EXT_CSD_CARD_TYPE_HS400 (1<<6 | 1<<7) // 卡支持 200MHz DDR HS400模式

/* EXT_CSD_BUS_WIDTH 字段的值定义 */
#define EXT_CSD_BUS_WIDTH_1 0 // 1位总线模式
#define EXT_CSD_BUS_WIDTH_4 1 // 4位总线模式
#define EXT_CSD_BUS_WIDTH_8 2 // 8位总线模式
#define EXT_CSD_DDR_BUS_WIDTH_4 5 // 4位DDR总线模式
#define EXT_CSD_DDR_BUS_WIDTH_8 6 // 8位DDR总线模式

/* EXT_CSD_HS_TIMING 字段的值定义 */
#define EXT_CSD_TIMING_BC 0 // 向后兼容时序
#define EXT_CSD_TIMING_HS 1 // 高速时序 (HS)
#define EXT_CSD_TIMING_HS200 2 // HS200时序
#define EXT_CSD_TIMING_HS400 3 // HS400时序

MMC_SWITCH (CMD6) 命令相关定义

1
2
3
4
5
6
7
/*
* MMC_SWITCH (CMD6) 的 "access" 模式
*/
#define MMC_SWITCH_MODE_CMD_SET 0x00 // 改变命令集
#define MMC_SWITCH_MODE_SET_BITS 0x01 // 置位 (将'value'中为1的位设置为1)
#define MMC_SWITCH_MODE_CLEAR_BITS 0x02 // 清位 (将'value'中为1的位清零)
#define MMC_SWITCH_MODE_WRITE_BYTE 0x03 // 写字节 (将目标字节直接设置为'value')

drivers/mmc/core/card.h MMC/SD/eMMC 卡功能驱动集合

历史与背景

这项技术是为了解决什么特定问题而诞生的?

drivers/mmc/core/card 目录及其下的文件是为了在MMC子系统的三层架构(Host - Core - Card)中扮演最顶层——“卡功能驱动”的角色。当MMC核心层(Core)完成对物理卡片的识别和初始化后,需要有一个驱动来解释这张卡的功能,并向Linux内核的其他子系统(如块设备层、输入子系统等)提供这些功能。

card目录下的驱动解决了以下核心问题:

  • 功能实现:将一张原始的、遵循MMC/SD/eMMC协议的卡片,转换成一个内核和用户空间可以理解和使用的标准设备。
  • 块设备抽象:对于SD卡和eMMC这类存储卡,block.c驱动负责将其抽象为一个标准的块设备(如/dev/mmcblk0),使其可以被分区、格式化并挂载文件系统。
  • 设备特定功能:eMMC标准定义了许多超越简单块存储的高级功能,如分区管理(Boot partitions, RPMB partition)、TRIM/Discard命令(用于SSD性能优化)、安全擦除等。mmc/card/block.c和相关文件负责实现对这些高级功能的支持。
  • SDIO功能枚举:对于SDIO卡,sdio.c驱动扮演一个特殊的“枚举器”角色。它本身不驱动具体功能,而是负责解析SDIO卡的CIS(卡信息结构),找出卡上有多少个I/O功能,并为每个功能在sdio_bus上注册一个设备,等待真正的功能驱动来绑定。

它的发展经历了哪些重要的里程碑或版本迭代?

card目录下的驱动发展与MMC/SD/eMMC规范的演进紧密相关。

  • 基础块设备支持 (block.c):最初的核心功能是提供对MMC和SD卡的读/写/擦除操作,并将其封装成一个块设备。
  • eMMC的兴起:随着eMMC成为嵌入式系统的主流存储,block.c被大幅扩展以支持eMMC的各种新特性:
    • 高速模式支持(HS200, HS400)。
    • 分区支持,能够访问eMMC的Boot和RPMB(Replay Protected Memory Block)分区,并为它们创建独立的块设备节点。
    • TRIM/Discard和Sanitize命令的支持,这对于保持闪存性能和寿命至关重要。
  • SDIO的成熟 (sdio.c)sdio.c的完善是另一个里程碑,它与sdio_bus.c共同构成了Linux强大的SDIO支持框架,能够处理复杂的多功能combo卡。
  • SD卡新规范支持:不断增加对新SD规范(如SDHC, SDXC, SDUC)和速度等级(UHS-I, UHS-II)的支持。

目前该技术的社区活跃度和主流应用情况如何?

card目录下的代码是MMC子系统中非常活跃的部分,因为硬件规范在不断演进。

  • block.c:由于eMMC和SD卡技术仍在发展(例如,引入新的缓存或健康报告功能),block.c会持续更新以支持这些新特性。它是所有使用SD卡或eMMC存储的Linux系统的核心。
  • sdio.c:相对稳定,但随着新的SDIO规范或特性出现,也可能需要更新。

这些驱动的应用无处不在,覆盖了从树莓派(SD卡启动)到绝大多数安卓手机(eMMC作为主存储)的广阔领域。

核心原理与设计

它的核心工作原理是什么?

card目录下的驱动作为**MMC总线(mmc_bus_type上的驱动程序(struct mmc_driver)**来工作。

  1. 驱动注册:在模块初始化时,block.c会调用mmc_register_driver(&mmc_block_driver)将自己注册到MMC总线上。mmc_block_driver这个结构体声明了它是一个MMC驱动,并提供了.probe.remove等回调函数。
  2. 设备匹配:当MMC核心层在插槽中发现一张卡并成功初始化后,它会将这张卡注册为一个mmc总线设备。MMC总线核心(bus.c)会尝试将这个新设备与所有已注册的mmc_driver进行匹配。mmc_block_driver的匹配规则很简单:它几乎能匹配所有类型的存储卡(SD, MMC, eMMC)。
  3. 探测(Probe):一旦匹配成功,mmc_block_driver.probe函数(mmc_block_probe)就会被调用。这是功能实现的核心所在:
    • 分配块设备:它会分配一个gendisk结构体和一个请求队列(request_queue),这是块设备层的核心数据结构。
    • 设置请求队列:它会配置请求队列,将队列的“请求处理函数”指向mmc_queue_rq。这意味着,当文件系统层发出一个读/写请求(struct bio)时,最终会由mmc_queue_rq函数来处理。
    • mmc_queue_rq的逻辑:这个函数是连接块设备层和MMC核心层的桥梁。它会将一个来自块设备层的request转换成一个或多个发往MMC核心层的mmc_request(包含CMD17/18/24/25等读写命令)。
    • 添加磁盘:最后,调用add_disk,向内核宣告一个新的块设备诞生了,此时/dev/mmcblkX设备节点就会出现。
  4. SDIO的特殊流程sdio.c的流程类似,但它的.probe函数(sdio_probe)不做块设备相关的工作。它的任务是解析CIS,然后为每个发现的功能调用sdio_add_func(),在sdio_bus上创建设备。

它的主要优势体现在哪些方面?

  • 关注点分离card驱动完美地体现了分层设计的思想。它不关心底层Host控制器是如何操作硬件的,也不关心MMC协议的握手细节。它只关注于如何将一个已初始化的mmc_card设备的功能暴露给上层。
  • 标准化接口:通过实现标准的块设备或SDIO设备接口,它使得上层应用(如文件系统、网络栈)可以无缝地使用MMC设备,而无需知道其底层技术。
  • 可扩展性:支持新的eMMC功能或SD卡类型,通常只需要修改block.c,而不会影响到核心层或主机驱动。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 单一模型block.c是一个通用的块设备驱动,它试图以一种统一的方式来处理所有存储卡。对于某些有非常特殊功能或性能特性的eMMC芯片,这种通用模型可能无法发挥其全部潜力。
  • 块设备层的开销:对于某些嵌入式场景,如果只是想对闪存进行裸读写,经过完整的块设备层(包括I/O调度器等)可能会带来不必要的开销。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

它是Linux内核中处理MMC/SD/eMMC存储和SDIO功能的唯一且标准的解决方案。

  • 挂载文件系统:当你在Linux系统中将一张SD卡格式化为ext4并挂载时,所有对该文件系统的读写操作最终都会通过VFS -> 文件系统 -> 块设备层,最终由mmc_block_driver转换成发往卡片的物理命令。
  • eMMC分区:在嵌入式设备或手机上,系统访问eMMC的不同物理分区(如userdata, system, boot)时,依赖于block.c为这些分区创建的独立块设备节点(/dev/mmcblk0p1, /dev/mmcblk0p2等)。
  • 使用SDIO Wi-Fi模块:当一个SDIO Wi-Fi模块被识别时,sdio.c负责枚举其功能,然后真正的Wi-Fi驱动会绑定到sdio_bus上的功能设备,从而提供网络服务。

是否有不推荐使用该技术的场景?为什么?

不存在不推荐使用该技术的场景。只要一个设备是标准的MMC/SD/eMMC/SDIO卡,就必须由card目录下的相应驱动来处理,这是MMC子系统设计的核心部分。

对比分析

请将其 与 其他总线的顶层功能驱动(如USB Storage, NVMe)进行详细对比。

特性 MMC Card Driver (block.c) USB Storage Driver (usb-storage) NVMe Driver (nvme/host.c)
所属总线 mmc_bus_type usb_bus_type pci_bus_type
功能抽象 将MMC/SD卡抽象为Linux块设备 将USB Mass Storage设备抽象为SCSI磁盘设备,然后再由SCSI中间层转换为块设备。 将NVMe SSD抽象为专有的**nvme_ns块设备**。
交互协议 直接构建和发送MMC协议命令(CMD17, CMD18等)。 将读/写请求转换为SCSI命令(CDBs),再封装成USB批量传输(Bulk Transfers)。 将请求转换为NVMe命令,并将其放入提交队列(Submission Queue),通过写门铃寄存器(doorbell)来通知硬件。
性能模型 命令-响应式,半双工。 命令-响应式,但USB协议本身支持一定程度的流水线。 队列化、异步模型。支持多个队列和极高的队列深度(QD),性能极高。
驱动角色 直接的块设备驱动。 协议转换驱动。它本身是一个USB接口驱动,同时又是一个SCSI低层驱动(LLD)。 全功能的块设备驱动。它实现了从PCIe交互到块设备接口的全套逻辑。
复杂性 中等。需要理解MMC读写和eMMC高级功能。 较高。需要理解USB传输和SCSI命令集。 非常高。需要理解PCIe、NVMe协议、队列管理、中断处理等。

总结drivers/mmc/core/card下的驱动与其他总线的功能驱动一样,都扮演着“翻译官”的角色,将上层子系统(如块设备层)的通用请求,翻译成底层总线和设备能听懂的特定协议。它们的实现细节和复杂性,完全由其所服务的硬件协议的特性所决定。MMC协议相对直接,而USB Storage和NVMe则分别引入了SCSI协议层和高性能队列模型,因此其驱动实现也更为复杂。

MMC卡状态与属性宏定义:卡片核心状态的管理接口

本代码片段并非可执行函数,而是一系列C预处理器宏的定义,通常位于头文件(如include/linux/mmc/card.h)中。其核心功能是为MMC/SD卡(由struct mmc_card表示)提供一个标准化的、抽象的接口,用于定义、查询和修改卡片的核心状态(如是否存在、是否只读),以及便捷地访问其关键属性(如设备名称)。

实现原理分析

这组宏的实现原理是C语言中一种常见且高效的设计模式,即利用位掩码(Bitmask)来管理一组布尔状态,并通过宏来封装其操作,以提高代码的可读性和可维护性。

  1. 位掩码(Bitmask): struct mmc_card 结构体中有一个名为 state 的整型成员。每一个独立的状态(如PRESENT, READONLY)都被定义为一个唯一的2的幂次值(例如 (1<<0), (1<<1)),这意味着在二进制表示中,每个状态都对应一个独立的位置为’1’。通过这种方式,一个32位的整型变量理论上可以同时存储32个不同的布尔状态,极大地节省了内存空间。
  2. 状态查询: 查询宏(如 mmc_card_present(c))利用按位与(&)操作符。将 state 变量与代表特定状态的位掩码相与,如果结果不为零,则说明该状态位被置位(为’1’),表示状态为真。
  3. 状态设置: 设置宏(如 mmc_card_set_present(c))利用按位或赋值(|=)操作符。将 state 变量与特定状态的位掩码相或,这会将对应的状态位置为’1’,而不会影响其他位的状态。
  4. 状态清除: 清除宏(如 mmc_card_clr_suspended(c))则使用按位与赋值(&=)和按位非(~)的组合。~MMC_STATE_SUSPENDED 会产生一个除了暂停状态位为’0’、其他所有位都为’1’的掩码。将其与 state 变量相与,就能精确地将暂停状态位清零,同时保持其他位不变。
  5. 访问器宏: 像 mmc_card_name(c)container_of 这样的宏提供了对结构体成员的便捷访问,隐藏了内部数据结构的细节,使得上层代码更加简洁。container_of 是一个内核中的标准宏,它能根据结构体成员的地址反向计算出整个结构体的起始地址,是设备模型中实现对象间关联的关键技术。

代码分析

访问器宏

1
2
3
4
5
6
// mmc_card_name(c): 获取卡的产品名称。
#define mmc_card_name(c) ((c)->cid.prod_name)
// mmc_card_id(c): 获取卡的设备名称(例如 "mmcblk0")。
#define mmc_card_id(c) (dev_name(&(c)->dev))
// mmc_dev_to_card(d): 从一个device结构体指针获取其所属的mmc_card结构体指针。
#define mmc_dev_to_card(d) container_of(d, struct mmc_card, dev)

卡状态位定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 卡状态定义 */
// MMC_STATE_PRESENT: 卡片存在于sysfs中,表示逻辑上已插入并被识别。
#define MMC_STATE_PRESENT (1<<0)
// MMC_STATE_READONLY: 卡片是只读的(可能由硬件写保护开关或内部状态决定)。
#define MMC_STATE_READONLY (1<<1)
// MMC_STATE_BLOCKADDR: 卡片使用块寻址(用于SDHC/SDXC/SDUC等高容量卡)。
#define MMC_STATE_BLOCKADDR (1<<2)
// MMC_CARD_SDXC: 卡片是SDXC类型。
#define MMC_CARD_SDXC (1<<3)
// MMC_CARD_REMOVED: 卡片已被移除。这是一个重要的状态,用于快速失败后续的I/O操作。
#define MMC_CARD_REMOVED (1<<4)
// MMC_STATE_SUSPENDED: 卡片处于挂起状态(低功耗模式)。
#define MMC_STATE_SUSPENDED (1<<5)
// MMC_CARD_SDUC: 卡片是SDUC类型。
#define MMC_CARD_SDUC (1<<6)

状态查询宏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// mmc_card_present(c): 检查卡片是否在位。
#define mmc_card_present(c) ((c)->state & MMC_STATE_PRESENT)
// mmc_card_readonly(c): 检查卡片是否为只读。
#define mmc_card_readonly(c) ((c)->state & MMC_STATE_READONLY)
// mmc_card_blockaddr(c): 检查卡片是否使用块寻址。
#define mmc_card_blockaddr(c) ((c)->state & MMC_STATE_BLOCKADDR)
// mmc_card_ext_capacity(c): 检查卡片是否为SDXC类型。
#define mmc_card_ext_capacity(c) ((c)->state & MMC_CARD_SDXC)
// mmc_card_removed(c): 检查卡片是否已被移除。
#define mmc_card_removed(c) ((c) && ((c)->state & MMC_CARD_REMOVED))
// mmc_card_suspended(c): 检查卡片是否已挂起。
#define mmc_card_suspended(c) ((c)->state & MMC_STATE_SUSPENDED)
// mmc_card_ult_capacity(c): 检查卡片是否为SDUC类型。
#define mmc_card_ult_capacity(c) ((c)->state & MMC_CARD_SDUC)

状态修改宏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// mmc_card_set_present(c): 设置卡片为存在状态。
#define mmc_card_set_present(c) ((c)->state |= MMC_STATE_PRESENT)
// mmc_card_set_readonly(c): 设置卡片为只读状态。
#define mmc_card_set_readonly(c) ((c)->state |= MMC_STATE_READONLY)
// mmc_card_set_blockaddr(c): 设置卡片为块寻址模式。
#define mmc_card_set_blockaddr(c) ((c)->state |= MMC_STATE_BLOCKADDR)
// mmc_card_set_ext_capacity(c): 设置卡片为SDXC类型。
#define mmc_card_set_ext_capacity(c) ((c)->state |= MMC_CARD_SDXC)
// mmc_card_set_ult_capacity(c): 设置卡片为SDUC类型。
#define mmc_card_set_ult_capacity(c) ((c)->state |= MMC_CARD_SDUC)
// mmc_card_set_removed(c): 设置卡片为已移除状态。
#define mmc_card_set_removed(c) ((c)->state |= MMC_CARD_REMOVED)
// mmc_card_set_suspended(c): 设置卡片为已挂起状态。
#define mmc_card_set_suspended(c) ((c)->state |= MMC_STATE_SUSPENDED)
// mmc_card_clr_suspended(c): 清除卡片的挂起状态。
#define mmc_card_clr_suspended(c) ((c)->state &= ~MMC_STATE_SUSPENDED)

drivers/mmc/core/core.c

MMC/SD/SDIO核心子系统初始化与卸载

本代码片段是Linux内核MMC/SD/SDIO核心子系统的模块入口和出口。其核心功能是在内核启动时,注册和初始化MMC、SDIO总线类型以及MMC主机控制器类,为所有具体的MMC主机控制器驱动(如针对STM32H750的SDMMC外设驱动)和MMC/SD/SDIO设备驱动(如SD卡块设备驱动、SDIO WiFi驱动)提供必要的基础设施。在模块卸载时,它负责按相反的顺序清理这些注册的组件。

实现原理分析

该代码遵循标准的Linux内核模块初始化和退出模式,利用subsys_initcall确保在系统早期阶段执行初始化。

  1. 初始化流程 (mmc_init):

    • 该函数按严格的依赖顺序执行初始化步骤。首先调用mmc_register_bus(),这会向内核设备模型注册一个名为mmcbus_type。这个总线类型是连接MMC卡设备和MMC卡驱动的桥梁。
    • 成功后,调用mmc_register_host_class(),它会注册一个名为mmc_host的设备类(class)。这会在sysfs中创建/sys/class/mmc_host目录,为所有MMC主机控制器提供一个统一的视图(如mmc0, mmc1)。
    • 接着,调用sdio_register_bus()注册一个独立的、名为sdiobus_type。虽然SDIO卡通过MMC总线进行通信,但其上的功能设备(Functions)具有不同的寻址和驱动匹配逻辑,因此需要一个专门的SDIO总线来管理SDIO功能驱动和设备。
    • 函数使用了goto语句进行错误处理。如果在任何一步注册失败,程序会跳转到相应的标签,执行已经成功注册部分的反向注销操作,确保系统状态的一致性。这是一种在内核中常见的、高效的错误回滚(rollback)模式。
  2. 退出流程 (mmc_exit):

    • 该函数在模块卸载时被调用,执行与mmc_init完全相反的操作。
    • 它严格按照“后进先出”(LIFO)的原则进行清理:首先注销SDIO总线,然后是MMC主机类,最后是MMC总线。这种相反的顺序是至关重要的,可以避免因依赖关系导致的错误(例如,在仍有设备注册在总线上时就注销了总线类型)。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// mmc_init: MMC核心子系统的模块初始化函数。
static int __init mmc_init(void)
{
int ret;

// 注册MMC总线类型。
ret = mmc_register_bus();
if (ret)
return ret;

// 注册MMC主机控制器设备类。
ret = mmc_register_host_class();
if (ret)
goto unregister_bus; // 如果失败,跳转到unregister_bus标签进行清理。

// 注册SDIO总线类型。
ret = sdio_register_bus();
if (ret)
goto unregister_host_class; // 如果失败,跳转进行清理。

return 0; // 所有初始化成功。

// 错误处理标签,用于回滚注册操作。
unregister_host_class:
mmc_unregister_host_class(); // 注销MMC主机类。
unregister_bus:
mmc_unregister_bus(); // 注销MMC总线。
return ret; // 返回错误码。
}

// mmc_exit: MMC核心子系统的模块退出函数。
static void __exit mmc_exit(void)
{
// 以与初始化相反的顺序注销所有组件。
sdio_unregister_bus();
mmc_unregister_host_class();
mmc_unregister_bus();
}

// 使用subsys_initcall宏,确保该初始化函数在内核子系统初始化阶段被调用。
subsys_initcall(mmc_init);
// 注册模块退出函数。
module_exit(mmc_exit);

// 模块元信息。
MODULE_DESCRIPTION("MMC core driver");
MODULE_LICENSE("GPL");

MMC主机控制器独占声明:实现总线访问的互斥与电源管理

本代码片段展示了MMC核心子系统中至关重要的主机控制器锁定机制。其核心功能是通过mmc_claim_host(及其实现__mmc_claim_host)提供一个可重入的、可中断的、且与电源管理集成的互斥锁。任何需要访问MMC/SD/SDIO总线硬件的驱动程序(如SD卡块设备驱动、SDIO WiFi驱动)在执行I/O操作前都必须调用此函数。它确保了在任何时刻,只有一个执行上下文可以控制MMC主机硬件,同时保证了硬件在被访问前处于上电状态。

实现原理分析

此函数并未直接使用标准的mutex_lock,而是实现了一个更复杂的、定制化的阻塞锁。这是因为MMC主机的锁定需要满足几个特殊需求:可重入性、可被外部事件中止、以及与运行时电源管理(Runtime PM)的紧密集成。

  1. 自定义阻塞锁:

    • 函数的核心是一个while(1)循环,它在一个自旋锁(host->lock)的保护下检查锁定条件。自旋锁用于保护host->claimed标志位和host->wq等待队列等共享状态,防止在检查和修改这些状态时发生竞态。
    • 等待条件: 循环的退出条件是 !host->claimed || mmc_ctx_matches(...)。这意味着,如果主机当前未被声明(claimed为0),或者声明主机的上下文与当前请求的上下文匹配(mmc_ctx_matches返回真),那么当前任务就可以获得锁。后者实现了锁的可重入性,允许同一个任务或上下文多次声明主机而不会死锁。
    • 睡眠与唤醒: 如果条件不满足(即其他上下文已持有锁),当前任务会调用schedule()放弃CPU,进入TASK_UNINTERRUPTIBLE睡眠状态。它通过add_wait_queue将自己挂载在host->wq等待队列上。当锁的持有者释放锁时,会唤醒等待队列上的所有任务,它们将重新进入循环进行条件检查。
  2. 中止机制: abort参数允许锁的获取过程被外部事件中断。例如,当一个卡被物理拔出时,另一个内核线程可以设置abort标志。在等待循环中,每次都会检查atomic_read(abort),如果发现非零值,将立即放弃获取锁并返回,避免了在设备已不存在的情况下无限期等待。

  3. 电源管理集成:

    • host->claim_cnt是一个引用计数器,用于支持可重入锁。
    • 关键逻辑在于if (host->claim_cnt == 1)。只有在主机从未被声明(计数从0变为1)的第一次成功获取锁时,才会执行pm_runtime_get_sync(mmc_dev(host))
    • pm_runtime_get_sync会同步地唤醒并恢复设备的电源。这确保了硬件只在上层驱动真正需要访问它时才上电,并在第一次claim时完成。对于后续的重入claim(计数从1变为2等),则不再需要重复上电操作,提高了效率。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// mmc_claim_host: 独占性地声明一个主机(简化接口)。
// @host: 需要声明的mmc主机。
// 这是一个内联函数,作为对__mmc_claim_host的简单封装,
// 提供了最常用情况下的接口(没有特殊的上下文或中止条件)。
static inline void mmc_claim_host(struct mmc_host *host)
{
__mmc_claim_host(host, NULL, NULL);
}

// __mmc_claim_host: 独占性地声明一个主机(完整实现)。
// @host: 需要声明的mmc主机。
// @ctx: 声明主机的上下文,为NULL则使用默认上下文。
// @abort: 一个原子变量指针,用于从外部中止锁的获取过程。
// 返回值: 成功获取锁则返回0,被中止则返回非零值。
int __mmc_claim_host(struct mmc_host *host, struct mmc_ctx *ctx,
atomic_t *abort)
{
// 如果没有提供特定上下文,则使用当前任务作为上下文。
struct task_struct *task = ctx ? NULL : current;
// 声明一个等待队列节点,并将其与当前任务关联。
DECLARE_WAITQUEUE(wait, current);
unsigned long flags;
int stop;
bool pm = false; // 标志是否需要执行电源管理操作。

// 告知内核锁检查器(lockdep)此函数可能会睡眠。
might_sleep();

// 将当前任务的等待节点加入到主机的等待队列中。
add_wait_queue(&host->wq, &wait);
// 获取主机的自旋锁,并保存当前中断状态、禁用本地中断。
spin_lock_irqsave(&host->lock, flags);
while (1) {
// 将当前任务状态设置为不可中断睡眠。
set_current_state(TASK_UNINTERRUPTIBLE);
// 检查是否被外部请求中止。
stop = abort ? atomic_read(abort) : 0;
// 退出循环的条件:1.被中止;2.主机未被声明;3.是同一个上下文的可重入声明。
if (stop || !host->claimed || mmc_ctx_matches(host, ctx, task))
break;
// 条件不满足,临时释放自旋锁并允许中断。
spin_unlock_irqrestore(&host->lock, flags);
// 放弃CPU,进行任务调度,任务在此处睡眠。
schedule();
// 任务被唤醒后,重新获取自旋锁并禁用中断,再次进入循环检查条件。
spin_lock_irqsave(&host->lock, flags);
}
// 退出循环后,将任务状态恢复为运行态。
set_current_state(TASK_RUNNING);
if (!stop) {
// 成功获取锁(未被中止)。
host->claimed = 1; // 标记主机已被声明。
mmc_ctx_set_claimer(host, ctx, task); // 记录声明者。
host->claim_cnt += 1; // 递增声明计数器(支持重入)。
// 如果是第一次声明(计数从0到1),则标记需要进行PM操作。
if (host->claim_cnt == 1)
pm = true;
} else
// 如果是被中止的,唤醒等待队列中的其他任务,以防它们也需要中止。
wake_up(&host->wq);
// 释放自旋锁,并恢复之前的中断状态。
spin_unlock_irqrestore(&host->lock, flags);
// 将当前任务的等待节点从主机的等待队列中移除。
remove_wait_queue(&host->wq, &wait);

// 如果标记了需要PM操作。
if (pm)
// 同步地恢复设备的运行时电源,确保硬件已上电可用。
pm_runtime_get_sync(mmc_dev(host));

return stop; // 返回中止状态。
}
// 导出符号,使得内核其他模块可以调用此函数。
EXPORT_SYMBOL(__mmc_claim_host);

MMC请求完成与调谐管理:请求生命周期的终点与信号完整性维护

本代码片段展示了MMC子系统中的两个关键功能:mmc_request_done 函数,它是所有MMC请求处理流程的终点站;以及mmc_retune_hold/release 函数对,它们实现了一个引用计数机制来管理和推迟信号调谐(retuning)过程。mmc_request_done 负责在底层硬件操作完成后,进行错误分析、状态更新,并最终通知上层调用者,而调谐管理则是在高速模式下维持总线信号完整性的重要保障。

实现原理分析

  1. 请求完成 (mmc_request_done): 这是一个由底层主机控制器驱动(Host Driver)调用的核心回调函数。其原理如下:

    • 错误检测与响应: 函数首先检查请求中各个命令(主命令、数据、停止命令)的错误码。它特别关注CRC校验错误(-EILSEQ),因为这通常是信号完整性问题的标志。如果检测到CRC错误,并且当前操作本身不是调谐命令,它会调用mmc_retune_needed()来设置一个标志,表明在处理下一个请求之前需要进行一次信号调谐。
    • 状态清理: 它负责清理MMC核心层的状态,例如将host->ongoing_mrq指针置为NULL,表示当前没有正在进行中的请求。
    • 调试与追踪: 在请求被确认为最终完成(即没有错误、不需要重试或卡已被拔出)后,函数会打印详细的调试日志,并关闭LED指示灯,为开发者提供详尽的状态信息。
    • 异步通知: 最关键的一步是调用mrq->done(mrq)回调函数(如果存在)。这是实现异步操作的核心机制。上层(如块设备层)在提交请求时可以提供这个回调函数,当硬件操作完成时,该回调被执行,从而唤醒等待的进程或触发下一个操作,而不需要上层进行阻塞轮询。
  2. 调谐保持机制 (mmc_retune_hold/release):

    • 这是一个基于引用计数的锁机制,用于控制何时可以安全地执行信号调谐。
    • mmc_retune_hold会增加host->hold_retune计数器。这通常在一个复杂操作(如多块读写)开始时调用。
    • mmc_retune_release则减少该计数器,在操作结束时调用。
    • MMC核心在准备分发新请求时,会检查host->hold_retune计数器。只要该计数器大于0,即使retune_needed标志被设置,实际的调谐操作也会被推迟。这可以防止在一次连续的数据传输过程中间插入耗时的调谐操作,保证了数据流的连续性。

代码分析

调谐保持与释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// mmc_retune_hold: 增加调谐保持计数,推迟调谐操作。
void mmc_retune_hold(struct mmc_host *host)
{
// 如果这是第一次保持(计数器为0),则设置retune_now标志。
// 这可能用于在释放后立即触发一次被挂起的调谐。
if (!host->hold_retune)
host->retune_now = 1;
// 增加保持计数器。只要此计数器>0,调谐就不会在请求之间自动执行。
host->hold_retune += 1;
}

// mmc_retune_release: 减少调谐保持计数,允许调谐操作。
void mmc_retune_release(struct mmc_host *host)
{
if (host->hold_retune)
// 减少保持计数器。
host->hold_retune -= 1;
else
// 如果计数器已经为0,则说明发生了保持/释放不匹配,这是一个内核错误。
WARN_ON(1);
}
EXPORT_SYMBOL(mmc_retune_release);

static inline void mmc_retune_needed(struct mmc_host *host)
{
if (host->can_retune)
host->need_retune = 1;
}

请求完成处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// mmc_request_done: 完成一个MMC请求的处理。
// @host: 完成请求的MMC主机。
// @mrq: 已完成的MMC请求。
// 描述: MMC驱动在完成对一个请求的处理后,应调用此函数。
void mmc_request_done(struct mmc_host *host, struct mmc_request *mrq)
{
struct mmc_command *cmd = mrq->cmd;
int err = cmd->error;

/* 在出现CRC错误时,标记需要重新调谐 */
if (!mmc_op_tuning(cmd->opcode) && // 如果当前命令本身不是调谐命令
!host->retune_crc_disable && // 并且主机没有禁用CRC错误触发的调谐
(err == -EILSEQ || (mrq->sbc && mrq->sbc->error == -EILSEQ) || // 检查命令、SBC、数据或停止命令是否有CRC错误
(mrq->data && mrq->data->error == -EILSEQ) ||
(mrq->stop && mrq->stop->error == -EILSEQ)))
mmc_retune_needed(host); // 设置需要调谐的标志

// 对于SPI模式,如果命令是非法的,则不再进行重试。
if (err && cmd->retries && mmc_host_is_spi(host)) {
if (cmd->resp[0] & R1_SPI_ILLEGAL_COMMAND)
cmd->retries = 0;
}

// 如果当前正在进行的请求是本请求,则将其清空。
if (host->ongoing_mrq == mrq)
host->ongoing_mrq = NULL;

// 调用内部函数完成命令,这可能会唤醒等待此特定命令完成的进程。
mmc_complete_cmd(mrq);

// 记录请求完成的跟踪点,用于调试和性能分析。
trace_mmc_request_done(host, mrq);

/* 仅在请求明确完成(无错误、不重试或卡已移除)时,才进行调试打印和关闭LED等操作 */
if (!err || !cmd->retries || mmc_card_removed(host->card)) {
mmc_should_fail_request(host, mrq);

// 如果没有正在进行的请求,则关闭LED。
if (!host->ongoing_mrq)
led_trigger_event(host->led, LED_OFF);

// 打印详细的调试信息,包括命令、响应和数据传输情况。
if (mrq->sbc) {
pr_debug("%s: req done <CMD%u>: %d: %08x %08x %08x %08x\n",
mmc_hostname(host), mrq->sbc->opcode,
mrq->sbc->error,
mrq->sbc->resp[0], mrq->sbc->resp[1],
mrq->sbc->resp[2], mrq->sbc->resp[3]);
}

pr_debug("%s: req done (CMD%u): %d: %08x %08x %08x %08x\n",
mmc_hostname(host), cmd->opcode, err,
cmd->resp[0], cmd->resp[1],
cmd->resp[2], cmd->resp[3]);

if (mrq->data) {
pr_debug("%s: %d bytes transferred: %d\n",
mmc_hostname(host),
mrq->data->bytes_xfered, mrq->data->error);
}

if (mrq->stop) {
pr_debug("%s: (CMD%u): %d: %08x %08x %08x %08x\n",
mmc_hostname(host), mrq->stop->opcode,
mrq->stop->error,
mrq->stop->resp[0], mrq->stop->resp[1],
mrq->stop->resp[2], mrq->stop->resp[3]);
}
}
/*
* 请求的发起者必须处理重试。
* 此处调用完成回调函数,通知上层调用者整个请求(mrq)已结束。
*/
if (mrq->done)
mrq->done(mrq);
}

EXPORT_SYMBOL(mmc_request_done);

MMC请求处理核心:命令的准备、分发与完成机制

本代码片段是Linux内核MMC子系统的核心部分,负责处理一个MMC/SD/SDIO请求(mmc_request,简称mrq)从提交到完成的整个生命周期。它定义了MMC核心层(总线无关的逻辑)与底层主机控制器驱动(硬件相关的实现)之间的标准交互接口。其主要功能包括:对请求进行合法性检查与准备、将请求分发给主机控制器驱动执行,以及在请求完成后进行状态更新、错误处理和完成通知。

实现原理分析

该代码的执行流程体现了典型的分层驱动模型,通过函数指针结构(mmc_host_ops)实现了上层逻辑与底层硬件的解耦。

  1. 请求的准备 (mmc_mrq_prep): 在一个请求被发送到硬件之前,此函数作为“守门员”,负责进行一系列的验证和初始化。它确保请求中的命令(mmc_command)、数据(mmc_data)等部分的内部指针和状态被正确设置。最重要的是,它会根据主机控制器(mmc_host)的能力(如最大块大小max_blk_size、最大请求大小max_req_size)来校验请求参数的合法性,防止向硬件提交无法处理的请求。

  2. 请求的启动与分发 (mmc_start_request, __mmc_start_request):

    • mmc_start_request是上层(如块设备层)发起一个请求的标准入口。它是一个协调者,负责调用准备函数mmc_mrq_prep,并设置用于同步的完成量(completion)。
    • 核心的分发任务由__mmc_start_request完成。它会处理一些协议细节,如在必要时执行mmc_retune以保证信号完整性,或为SDIO设备等待card_busy状态。
    • 最终,它通过调用host->ops->request(host, mrq),将请求的控制权移交给底层的主机控制器驱动。这个ops->request函数指针是关键的抽象层,其具体实现由硬件驱动(如针对STM32的SDMMC驱动)提供。
  3. 请求的完成 (mmc_request_done):

    • 这是一个回调函数。当主机控制器驱动完成了硬件操作(无论是成功、失败还是超时),它必须调用mmc_request_done来通知核心层。
    • 此函数是请求处理的终点。它负责分析命令执行后的错误码,并根据错误类型采取相应措施,例如,在发生CRC错误(-EILSEQ)时,通过mmc_retune_needed标记需要进行重新调谐。
    • 它会更新主机的状态,例如将ongoing_mrq指针清空。
    • 最关键的一步是,如果请求中设置了done回调函数(mrq->done),它会调用该函数。这构成了异步通知机制,允许最初发起请求的调用者得知操作已完成。对于同步请求,这个done回调通常会唤醒一个正在等待的完成量。

代码分析

完成命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static inline void mmc_complete_cmd(struct mmc_request *mrq)
{
if (mrq->cap_cmd_during_tfr && !completion_done(&mrq->cmd_completion))
complete_all(&mrq->cmd_completion);
}

void mmc_command_done(struct mmc_host *host, struct mmc_request *mrq)
{
if (!mrq->cap_cmd_during_tfr)
return;

mmc_complete_cmd(mrq);

pr_debug("%s: cmd done, tfr ongoing (CMD%u)\n",
mmc_hostname(host), mrq->cmd->opcode);
}
EXPORT_SYMBOL(mmc_command_done);

请求启动与分发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
static inline void mmc_delay(unsigned int ms)
{
if (ms <= 20)
usleep_range(ms * 1000, ms * 1250);
else
msleep(ms);
}

// __mmc_start_request: 内部函数,实际启动一个请求。
static void __mmc_start_request(struct mmc_host *host, struct mmc_request *mrq)
{
int err;

/* 假设主机控制器已由 mmc_claim_host 恢复 */
// 首先尝试重新调谐,如果失败则直接完成请求并返回错误。
err = mmc_retune(host);
if (err) {
mrq->cmd->error = err;
mmc_request_done(host, mrq);
return;
}

/* 对于某些SDIO读写命令,必须等待卡不处于繁忙状态 */
if (sdio_is_io_busy(mrq->cmd->opcode, mrq->cmd->arg) &&
host->ops->card_busy) {
int tries = 500; /* 最多等待约500ms */

// 调用底层驱动提供的card_busy函数检查状态。
while (host->ops->card_busy(host) && --tries)
mmc_delay(1);

// 如果等待超时,则以-EBUSY错误完成请求。
if (tries == 0) {
mrq->cmd->error = -EBUSY;
mmc_request_done(host, mrq);
return;
}
}

if (mrq->cap_cmd_during_tfr) {
host->ongoing_mrq = mrq;
// 确保重试路径也能重新初始化完成量。
reinit_completion(&mrq->cmd_completion);
}

trace_mmc_request_start(host, mrq);

if (host->cqe_on)
host->cqe_ops->cqe_off(host);

// 调用底层主机控制器驱动提供的request函数,将请求分发到硬件。
host->ops->request(host, mrq);
}

// mmc_start_request: 启动一个MMC请求(上层API)。
int mmc_start_request(struct mmc_host *host, struct mmc_request *mrq)
{
int err;

// ... (处理扩展地址)
if (mrq->cmd->has_ext_addr)
mmc_send_ext_addr(host, mrq->cmd->ext_addr);

// 初始化用于命令完成的完成量。
init_completion(&mrq->cmd_completion);

// 保持调谐状态。
mmc_retune_hold(host);

// 如果卡已被移除,返回错误。
if (mmc_card_removed(host->card))
return -ENOMEDIUM;

// 打印调试信息。
mmc_mrq_pr_debug(host, mrq, false);

// 确保主机已被声明(claimed)。
WARN_ON(!host->claimed);

// 准备请求,进行合法性校验。
err = mmc_mrq_prep(host, mrq);
if (err)
return err;

// ... (UHS-II 准备)
if (host->uhs2_sd_tran)
mmc_uhs2_prepare_cmd(host, mrq);

// 点亮LED,表示活动状态。
led_trigger_event(host->led, LED_FULL);
// 调用内部函数分发请求。
__mmc_start_request(host, mrq);

return 0;
}
EXPORT_SYMBOL(mmc_start_request);

请求准备与同步封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// mmc_mrq_prep: 准备一个MMC请求,进行校验和初始化。
static int mmc_mrq_prep(struct mmc_host *host, struct mmc_request *mrq)
{
// ... (初始化命令、数据等结构中的error和mrq指针)
if (mrq->cmd) { //...
mrq->cmd->error = 0;
mrq->cmd->mrq = mrq;
mrq->cmd->data = mrq->data;
}
if (mrq->sbc) { //...
mrq->sbc->error = 0;
mrq->sbc->mrq = mrq;
}
if (mrq->data) {
// 校验请求的数据大小是否在主机能力范围之内。
if (mrq->data->blksz > host->max_blk_size ||
mrq->data->blocks > host->max_blk_count ||
mrq->data->blocks * mrq->data->blksz > host->max_req_size)
return -EINVAL;

// ... (校验scatterlist的总长度是否与预期相符)
for_each_sg(mrq->data->sg, sg, mrq->data->sg_len, i)
sz += sg->length;
if (sz != mrq->data->blocks * mrq->data->blksz)
return -EINVAL;

// 初始化数据传输相关的状态。
mrq->data->error = 0;
mrq->data->mrq = mrq;
if (mrq->stop) { //...
mrq->data->stop = mrq->stop;
mrq->stop->error = 0;
mrq->stop->mrq = mrq;
}
}

return 0;
}

// mmc_wait_done: 一个通用的完成回调函数,用于同步等待。
static void mmc_wait_done(struct mmc_request *mrq)
{
// 唤醒在mrq->completion上等待的进程。
complete(&mrq->completion);
}

static inline void mmc_wait_ongoing_tfr_cmd(struct mmc_host *host)
{
struct mmc_request *ongoing_mrq = READ_ONCE(host->ongoing_mrq);

/*
* If there is an ongoing transfer, wait for the command line to become
* available.
*/
if (ongoing_mrq && !completion_done(&ongoing_mrq->cmd_completion))
wait_for_completion(&ongoing_mrq->cmd_completion);
}

// __mmc_start_req: 封装了启动一个同步请求的逻辑。
static int __mmc_start_req(struct mmc_host *host, struct mmc_request *mrq)
{
int err;

// ... (等待可能正在进行的传输命令)
mmc_wait_ongoing_tfr_cmd(host);

// 初始化用于同步等待的完成量。
init_completion(&mrq->completion);
// 将请求的完成回调设置为mmc_wait_done。
mrq->done = mmc_wait_done;

// 启动请求。
err = mmc_start_request(host, mrq);
if (err) {
// 如果启动失败,则手动完成请求流程以唤醒等待者。
mrq->cmd->error = err;
mmc_complete_cmd(mrq);
complete(&mrq->completion);
}

return err;
}

MMC命令发送与阻塞等待

本代码片段定义了MMC核心子系统中用于发送命令并同步等待其完成的高级接口:mmc_wait_for_cmd 及其依赖的 mmc_wait_for_req。其核心功能是为上层驱动提供一个简化的、阻塞式的编程模型。驱动程序只需构建一个命令,调用这些函数,函数将在命令完成后才返回。这极大地简化了驱动的编写,隐藏了底层请求提交、中断处理和任务睡眠/唤醒的复杂细节。

实现原理分析

此功能的实现建立在MMC核心的异步请求机制之上,并将其封装为同步调用。它体现了清晰的层次结构:一个命令(mmc_command)被包装在一个请求(mmc_request)中进行处理。

  1. 请求(Request)与命令(Command)的抽象:

    • mmc_command (cmd): 代表一个单独的MMC命令,包含操作码、参数、期望的响应类型等。
    • mmc_request (mrq): 代表一个完整的MMC操作,它通常包含一个命令(mrq.cmd),并且可能还包含一个数据传输(mrq.data)和一个停止命令(mrq.stop)。本代码片段处理的是最简单的情况:一个只包含单个命令的请求。
  2. 同步封装 (mmc_wait_for_req):

    • 此函数首先调用__mmc_start_req(host, mrq)。这个内部函数负责将请求mrq提交给底层的主机控制器驱动,并启动硬件操作。这个调用是非阻塞的,它会立即返回。
    • 关键在于后续的 if (!mrq->cap_cmd_during_tfr) 判断。在普通命令中,这个标志位为假,因此会立即调用mmc_wait_for_req_done(host, mrq)
    • mmc_wait_for_req_done 是一个阻塞函数。它内部会使用等待队列(wait queue)机制,使当前任务进入睡眠状态,直到硬件完成命令并产生中断。中断服务程序会标记请求已完成并唤醒等待的任务。
  3. 便利的命令封装 (mmc_wait_for_cmd):

    • 这是一个更高级的便利函数,专门用于发送不涉及数据传输的单个命令(例如,SDIO CMD52)。
    • 它在函数栈上创建一个临时的mmc_request结构体mrq
    • WARN_ON(!host->claimed) 是一个重要的断言,确保上层驱动在发送命令前已经通过mmc_claim_host获取了总线锁。
    • 它将用户传入的cmd指针放入mrq.cmd中,并将cmd->data明确设置为NULL,表示这是一个没有数据阶段的请求。
    • 最后,它调用mmc_wait_for_req来同步地执行这个只包含命令的请求。
    • 函数返回cmd->error,这个字段会在底层请求处理过程中,根据命令的执行结果(如超时、CRC错误等)被填充。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// mmc_wait_for_req: 启动一个请求并等待其完成。
// @host: MMC主机控制器。
// @mrq: 要启动的MMC请求。
// 描述:
// 为一个主机启动一个新的MMC请求,并等待其完成。
// 对于支持 'cap_cmd_during_tfr' 的特殊请求,此函数仅启动传输,
// 调用者可以继续发送不使用数据线的命令,然后通过调用mmc_wait_for_req_done()来等待。
void mmc_wait_for_req(struct mmc_host *host, struct mmc_request *mrq)
{
// 调用内部函数 __mmc_start_req 来提交请求并启动硬件。
// 这是一个非阻塞调用。
__mmc_start_req(host, mrq);

// 如果请求不是 "传输期间可发命令" 的特殊类型。
if (!mrq->cap_cmd_during_tfr)
// 则立即调用 mmc_wait_for_req_done 来阻塞等待请求完成。
mmc_wait_for_req_done(host, mrq);
}
// 导出符号,供内核其他模块使用。
EXPORT_SYMBOL(mmc_wait_for_req);

// mmc_wait_for_cmd: 启动一个命令并等待其完成。
// @host: MMC主机控制器。
// @cmd: 要启动的MMC命令。
// @retries: 最大重试次数。
// 描述:
// 这是一个便利函数,用于同步发送单个不带数据的命令。
// 它返回命令执行期间发生的任何错误。
int mmc_wait_for_cmd(struct mmc_host *host, struct mmc_command *cmd, int retries)
{
struct mmc_request mrq = {}; // 在函数栈上创建一个MMC请求结构体。

// 警告:确保在发送命令前,主机已经被声明(加锁)。
// 这是一个重要的正确性检查。
WARN_ON(!host->claimed);

// 清空命令的响应缓冲区,以防含有旧数据。
memset(cmd->resp, 0, sizeof(cmd->resp));
// 设置命令的重试次数。
cmd->retries = retries;

// 将命令包装在请求中。
mrq.cmd = cmd;
// 明确指出此命令没有关联的数据传输。
cmd->data = NULL;

// 调用mmc_wait_for_req来同步执行这个只包含命令的请求。
mmc_wait_for_req(host, &mrq);

// 请求完成后,命令结构体中的error字段已被底层驱动填充。
// 将此错误码返回给调用者。
return cmd->error;
}
// 导出符号,供内核其他模块使用。
EXPORT_SYMBOL(mmc_wait_for_cmd);

drivers/mmc/core/bus.c MMC总线驱动(MMC Bus Driver) 连接MMC/SD卡设备与功能驱动的桥梁

历史与背景

这项技术是为了解决什么特定问题而诞生的?

drivers/mmc/core/bus.c 的诞生是为了在MMC子系统中实现Linux驱动模型的核心思想:将设备(Device)的发现与驱动(Driver)的绑定过程分离开来

在MMC子系统的三层架构(Host -> Core -> Card)中,MMC核心(Core)负责与卡片通信并识别其类型(是SD存储卡、eMMC还是SDIO Wi-Fi卡)。但识别出卡片后,需要一个机制来为这张卡找到并加载正确的功能驱动(例如,为存储卡加载块设备驱动,为Wi-Fi卡加载网络驱动)。

bus.c 就是这个机制的实现。它创建了一个名为 mmc 的新总线类型(Bus Type),将MMC卡抽象为挂载在这条总线上的“设备”,并将功能驱动抽象为这条总线上的“驱动”,然后负责将两者“匹配”(match)起来。这解决了以下问题:

  • 关注点分离:让MMC核心专注于协议和卡片识别,而让功能驱动(如card/block.c)专注于实现其特定功能(如块设备I/O),两者通过bus.c这个中间层解耦。
  • 标准化:为所有类型的MMC/SD/SDIO卡提供了一个统一的驱动注册和设备绑定模型,遵循了Linux设备模型的标准范式。
  • 可扩展性:当需要支持一种新型的SDIO设备时,开发者只需编写一个遵循MMC总线规范的新功能驱动,而无需改动核心代码。

它的发展经历了哪些重要的里程碑或版本迭代?

bus.c 本身是MMC子系统核心框架的一部分,其发展与MMC核心的演进紧密相连。

  • 框架确立:其最重要的里程碑是作为MMC核心的一部分被创建出来,正式将MMC子系统纳入了Linux的标准化总线模型。
  • SDIO支持的完善:随着SDIO变得越来越重要,bus.c 的逻辑需要支持更复杂的场景。一张SDIO卡可以有多个功能(Function),例如一个Wi-Fi加蓝牙的“combo”卡。MMC总线需要能正确地为每个功能创建设备,并将其与对应的功能驱动匹配。这通常涉及到与 drivers/mmc/core/sdio_bus.c 的协作,后者在MMC总线之上建立了一个专门的SDIO功能总线。
  • 电源管理集成:为mmc_bus_type添加了电源管理回调函数(.suspend, .resume),使得MMC设备可以参与到整个系统的休眠与唤醒流程中。

目前该技术的社区活跃度和主流应用情况如何?

bus.c 是MMC子系统的基石,其代码非常稳定,不会频繁变动。它的正确运行是所有使用SD卡、eMMC、SDIO设备的Linux系统的基础。社区的开发活动更多地集中在Host驱动和具体的Card功能驱动上,而bus.c作为底层的粘合剂,默默地支撑着整个体系的运转。

核心原理与设计

它的核心工作原理是什么?

bus.c 的核心工作是定义并实现 mmc_bus_type,这是一个 struct bus_type 实例,它告诉内核如何管理MMC总线上的设备和驱动。

其工作流程如下:

  1. 总线注册:在模块初始化时,bus.c 调用 bus_register(&mmc_bus_type),在内核中注册“mmc”这条总线。这会在sysfs中创建 /sys/bus/mmc 目录。
  2. 设备发现与注册(由Core触发):这个过程不由bus.c发起。当MMC核心(core.c)通过 mmc_rescan() 函数检测到一张卡并成功初始化后,它会为一个 struct mmc_card 分配内存,并调用 mmc_add_card()mmc_add_card() 会调用 device_add(),将这张卡作为一个 struct device 注册到内核,并挂在 mmc_bus_type 上。
  3. 驱动注册(由功能驱动触发):一个功能驱动,比如块设备驱动(card/block.c),会在其初始化时调用 mmc_register_driver(),将自己(一个struct mmc_driver)注册到 mmc_bus_type 上。
  4. 匹配过程(Matchmaking):每当有新的设备或新的驱动注册到 mmc 总线上时,总线核心就会尝试进行匹配。它会调用 mmc_bus_type 中指定的 .match 函数,即 mmc_bus_match()
  5. mmc_bus_match() 的逻辑:这个函数非常核心。它会比较驱动程序所支持的设备ID列表(driver->id_table)和当前设备的ID(card->cidcard->type等信息)。如果找到匹配项,函数返回true
  6. 探测(Probing):一旦 .match() 返回成功,总线核心就会调用匹配到的驱动的 .probe 函数(例如 mmc_block_probe())。.probe 函数负责执行所有功能相关的初始化,比如为存储卡创建块设备节点 /dev/mmcblkX
  7. 移除过程:当卡被拔出或驱动被卸载时,会触发相反的过程:调用驱动的 .remove 函数,然后设备和驱动会从总线上解除绑定并注销。

它的主要优势体现在哪些方面?

  • 完全遵循驱动模型:完美地将设备和驱动解耦,是Linux驱动模型的一个经典实现。
  • 自动化绑定:内核可以自动完成设备和驱动的匹配与加载,支持热插拔。
  • 清晰的层次结构:明确了MMC核心、总线和功能驱动之间的界限和交互方式。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 抽象带来的复杂性:对于初学者来说,这种通过总线进行的间接调用(device_add() -> bus_match() -> driver->probe())比直接函数调用更难跟踪和理解。
  • 非通用性bus.c 的逻辑是为MMC/SD/SDIO协议量身定做的,其匹配规则(基于CID等卡信息)对其他类型的总线没有意义。它本身没有劣势,只是其设计具有高度的特异性。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。

bus.c 是MMC子系统内部的强制性组件,而不是一个可选项。它的“使用场景”就是MMC子系统工作的每一个瞬间。

  • 场景一:插入一张SD卡
    1. Host驱动检测到卡插入,通知MMC核心。
    2. MMC核心(core.c)执行卡初始化序列,识别出这是一张SD存储卡。
    3. 核心调用 mmc_add_card(),将卡注册为一个mmc总线设备。
    4. bus.cmmc_bus_match() 被调用,它发现这张SD卡与 mmc_block_driver 兼容。
    5. mmc_block_driver.probe 函数被调用,创建 /dev/mmcblk0
  • 场景二:系统启动时识别eMMC
    1. 系统启动时,eMMC的Host驱动被加载。
    2. 它调用MMC核心的扫描函数。
    3. 核心识别出焊接在主板上的eMMC芯片。
    4. 后续流程与场景一完全相同,bus.c 负责将其与块设备驱动绑定,最终创建出代表eMMC分区的设备节点,如 /dev/mmcblk1p1

是否有不推荐使用该技术的场景?为什么?

不存在不推荐使用bus.c的场景。只要一个设备使用MMC/SD/SDIO协议栈,它就必须通过bus.c所定义的mmc_bus_type来与功能驱动进行交互。绕过这个机制就等于破坏了整个MMC子系统的设计。

对比分析

请将其 与 其他总线实现(如PCI, USB)进行详细对比。

mmc/core/bus.cdrivers/pci/pci-driver.cdrivers/usb/core/driver.c目标上是完全一致的:实现一个标准化的总线模型。但它们在实现细节,特别是设备枚举和匹配方式上,反映了各自总线的物理和协议特性。

特性 MMC Bus (bus.c) PCI Bus USB Bus
总线拓扑 简单,通常是点对点(一个Host控制器一个插槽)。没有复杂的树状或网状结构。 树状层次结构。有根总线(Root Complex)、桥(Bridge)和端点设备(Endpoint)。 树状层次结构。有根集线器(Root Hub)、外部集线器(Hub)和功能设备。
设备枚举方式 软件协议驱动。由MMC核心发送一系列标准命令(如CMD2, CMD3)与卡片进行交互,卡片会回复其CID/CSD等身份信息。 硬件扫描。由内核读取标准化的PCI配置空间(Configuration Space)。每个设备都有唯一的Vendor ID和Device ID。 软件协议驱动。设备插入后,由Host Controller进行端口复位和地址分配,然后内核读取设备返回的一系列标准描述符(Device, Configuration, Interface Descriptors)。
匹配规则 (.match) 基于MMC/SD卡内部寄存器信息(如CID中的制造商ID、产品名)和卡类型(SD, MMC, SDIO)。 主要基于PCI配置空间中的Vendor ID, Device ID, Class Code等。 主要基于USB描述符中的Vendor ID, Product ID, Class/SubClass/Protocol等。
设备地址 由MMC核心在逻辑上管理,没有固定的硬件地址。 由总线号(Bus)、设备号(Device)、功能号(Function)组成的唯一硬件地址。 由总线分配的动态设备地址(1-127)。

MMC总线类型定义:连接卡设备与驱动的桥梁

本代码片段定义了Linux内核中mmc总线的核心行为。它通过填充一个bus_type结构体(mmc_bus_type),实现了MMC设备(如SD卡、eMMC芯片)在内核设备模型中的探测(probe)、移除(remove)、关机(shutdown)和电源管理等关键回调函数。此外,它还负责通过uevent机制向用户空间(userspace)通告设备信息,并创建了sysfs属性文件(如type),以展示卡的类型。这部分代码是MMC核心的基础,它使得具体的MMC卡驱动(如mmc_block)能够与具体的MMC卡设备进行匹配和绑定。

实现原理分析

此代码的核心是mmc_bus_type结构体的实例化。这个结构体是Linux设备模型中用于描述一种总线的标准方式,它通过函数指针将总线级别的通用操作与设备模型核心连接起来。

  1. Probe/Remove/Shutdown 代理: mmc_bus_probemmc_bus_removemmc_bus_shutdown函数扮演了“代理”或“中间人”的角色。当设备模型核心决定要为一个MMC卡设备probe一个MMC驱动时,它会调用mmc_bus_probe。这个函数并不执行设备特定的初始化,而是简单地从通用device结构中提取出mmc_cardmmc_driver结构,然后调用mmc_driver结构中真正的probe函数指针。这种设计将总线的通用逻辑与驱动的具体实现解耦。

  2. Uevent事件生成: mmc_bus_uevent函数是实现设备自动配置的关键。当一个MMC卡被识别并注册到总线上时,内核会调用此函数来生成一个uevent事件。该函数将卡的详细信息(如类型、名称、SDIO ID等)打包成环境变量,发送给用户空间的udevdmdev等守护进程。其中最关键的一行是add_uevent_var(env, "MODALIAS=mmc:block"),它生成了一个标准的模块别名(MODALIAS)。用户空间守护进程看到这个别名后,会执行modprobe mmc:block,从而自动加载处理MMC块设备的内核模块(mmc_block.ko)。

  3. Sysfs属性: type_show函数和DEVICE_ATTR_RO(type)宏一起工作,在sysfs中为每个MMC设备创建一个名为type的只读文件。当用户通过cat /sys/bus/mmc/devices/mmc0:xxxx/type读取该文件时,type_show函数会被调用,它会根据卡的类型返回一个可读的字符串(如”SD”, “MMC”)。

  4. 总线注册: mmc_register_bus函数非常简单,它只是调用了设备模型核心提供的bus_register函数,将mmc_bus_type结构体注册到内核中,从而正式“激活”了MMC总线。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
// type_show: sysfs属性 "type" 的读取回调函数。
// @dev: 指向设备结构体的指针。
// @attr: 指向设备属性结构体的指针。
// @buf: 用于存放输出字符串的缓冲区。
// 返回值: 成功则为写入缓冲区的字节数,失败则为负数错误码。
static ssize_t type_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct mmc_card *card = mmc_dev_to_card(dev);

// 根据卡的类型,向sysfs缓冲区输出对应的字符串。
switch (card->type) {
case MMC_TYPE_MMC:
return sysfs_emit(buf, "MMC\n");
case MMC_TYPE_SD:
return sysfs_emit(buf, "SD\n");
case MMC_TYPE_SDIO:
return sysfs_emit(buf, "SDIO\n");
case MMC_TYPE_SD_COMBO:
return sysfs_emit(buf, "SDcombo\n");
default:
return -EFAULT;
}
}
// 使用宏定义一个名为 "type" 的只读设备属性。
static DEVICE_ATTR_RO(type);

// 定义一个包含所有MMC设备属性的数组。
static struct attribute *mmc_dev_attrs[] = {
&dev_attr_type.attr,
NULL, // 数组以NULL结尾。
};
// 使用宏将属性数组定义为一个属性组。
ATTRIBUTE_GROUPS(mmc_dev);

// mmc_bus_uevent: MMC总线的uevent事件生成函数。
// @dev: 产生事件的设备。
// @env: uevent环境变量。
// 返回值: 成功则为0,失败则为负数错误码。
static int
mmc_bus_uevent(const struct device *dev, struct kobj_uevent_env *env)
{
const struct mmc_card *card = mmc_dev_to_card(dev);
const char *type;
unsigned int i;
int retval = 0;

// 根据卡类型设置字符串。
switch (card->type) {
// ... (类型转换) ...
}

// 添加 MMC_TYPE 环境变量。
if (type) {
retval = add_uevent_var(env, "MMC_TYPE=%s", type);
if (retval)
return retval;
}

// 如果是SDIO或SD Combo卡,添加SDIO相关的ID信息。
if (mmc_card_sdio(card) || mmc_card_sd_combo(card)) {
retval = add_uevent_var(env, "SDIO_ID=%04X:%04X",
card->cis.vendor, card->cis.device);
// ... (添加其他SDIO信息) ...
}

/* 纯SDIO卡不由mmc_block驱动处理,因此uevent信息到此为止。 */
if (mmc_card_sdio(card))
return 0;

// 添加 MMC_NAME 环境变量,即卡的名称。
retval = add_uevent_var(env, "MMC_NAME=%s", mmc_card_name(card));
if (retval)
return retval;

/*
* 请求加载mmc_block模块。
* 这会生成一个 MODALIAS=mmc:block 的uevent变量,
* 用户空间的udev会响应该事件并尝试加载mmc_block.ko模块。
*/
retval = add_uevent_var(env, "MODALIAS=mmc:block");

return retval;
}

// mmc_bus_probe: MMC总线的probe回调函数。
// @dev: 需要进行probe的设备。
// 返回值: 来自具体驱动probe函数的返回值。
static int mmc_bus_probe(struct device *dev)
{
struct mmc_driver *drv = to_mmc_driver(dev->driver);
struct mmc_card *card = mmc_dev_to_card(dev);

// 直接调用具体MMC驱动的probe函数。
return drv->probe(card);
}

// mmc_bus_remove: MMC总线的remove回调函数。
// @dev: 需要移除的设备。
static void mmc_bus_remove(struct device *dev)
{
struct mmc_driver *drv = to_mmc_driver(dev->driver);
struct mmc_card *card = mmc_dev_to_card(dev);

// 调用具体MMC驱动的remove函数。
drv->remove(card);
}

// mmc_bus_shutdown: MMC总线的shutdown回调函数。
// @dev: 需要关闭的设备。
static void mmc_bus_shutdown(struct device *dev)
{
struct mmc_driver *drv = to_mmc_driver(dev->driver);
struct mmc_card *card = mmc_dev_to_card(dev);
struct mmc_host *host = card->host;
int ret;

// 如果驱动实现了shutdown,则调用它。
if (dev->driver && drv->shutdown)
drv->shutdown(card);

// 停止MMC主机。
__mmc_stop_host(host);

// 如果主机控制器驱动实现了shutdown,则调用它。
if (host->bus_ops->shutdown) {
ret = host->bus_ops->shutdown(host);
if (ret)
pr_warn("%s: error %d during shutdown\n",
mmc_hostname(host), ret);
}
}

// 定义MMC总线的电源管理操作集。
static const struct dev_pm_ops mmc_bus_pm_ops = {
SET_RUNTIME_PM_OPS(mmc_runtime_suspend, mmc_runtime_resume, NULL)
SET_SYSTEM_SLEEP_PM_OPS(mmc_bus_suspend, mmc_bus_resume)
};

// 定义MMC总线类型结构体。
static const struct bus_type mmc_bus_type = {
.name = "mmc", // 总线名称,会出现在 /sys/bus/mmc
.dev_groups = mmc_dev_groups, // 设备的sysfs属性组
.uevent = mmc_bus_uevent, // uevent事件处理函数
.probe = mmc_bus_probe, // probe回调
.remove = mmc_bus_remove, // remove回调
.shutdown = mmc_bus_shutdown, // shutdown回调
.pm = &mmc_bus_pm_ops, // 电源管理回调
};

// mmc_register_bus: 注册MMC总线。
// 返回值: 成功则为0,失败则为负数错误码。
int mmc_register_bus(void)
{
// 调用设备模型核心函数来注册总线类型。
return bus_register(&mmc_bus_type);
}

drivers/mmc/core/host.c MMC主机控制器管理(MMC Host Controller Management) 所有主机驱动的注册与管理中心

历史与背景

这项技术是为了解决什么特定问题而诞生的?

drivers/mmc/core/host.c 是MMC子系统三层架构(Host - Core - Card)中承上启下的关键文件。它的存在是为了解决如何将种类繁多的物理MMC/SD主机控制器(Host Controller)硬件以一种标准化的方式接入到MMC核心协议层的问题。

host.c出现之前,或者说在没有这种抽象层的情况下,会遇到以下问题:

  • 缺乏统一接口:每种主机控制器(例如,由Synopsys、Cadence或特定SoC厂商设计的IP核)都有其独特的寄存器和工作方式。MMC核心协议层(core.c)如果需要直接与这些硬件交互,代码将会变得极其臃肿和混乱,充满了针对特定硬件的if/else分支。
  • 代码重复:每个主机控制器驱动都需要自己实现与MMC核心的对接逻辑,导致大量重复代码。
  • 开发困难:为一款新的主机控制器编写驱动将是一项非常复杂的工作,开发者不仅要理解硬件,还要深入了解MMC核心的内部工作机制。

host.c通过定义一个标准的“主机”抽象 (struct mmc_host) 和一套操作接口 (struct mmc_host_ops),完美地解决了这些问题。它充当了所有具体主机驱动和通用MMC核心协议层之间的适配器(Adapter)和管理中心

它的发展经历了哪些重要的里程碑或版本迭代?

作为MMC子系统的基础组件,host.c的演进与整个子系统的发展同步。

  • 框架的建立:最重要的里程碑是struct mmc_hostmmc_host_ops的定义,以及mmc_alloc_host/mmc_add_host等生命周期管理函数的创建。这确立了所有主机驱动必须遵循的开发范式。
  • 功能和能力的扩展:随着MMC/SD标准的发展,struct mmc_host中增加了越来越多的能力标志(capscaps2字段),以向核心层宣告其支持的特性,例如支持4/8位总线宽度、支持DDR模式、支持HS200/HS400高速模式、支持CMD23(SET_BLOCK_COUNT命令)等。mmc_host_ops也相应增加了新的回调函数。
  • devres集成:引入了devm_mmc_alloc_host(),利用内核的设备资源管理(devres)框架,使得主机驱动的资源管理(特别是内存分配与释放)变得更加简单和安全,减少了代码量并防止了内存泄漏。
  • 电源管理:为主机控制器自身增加了标准的电源管理支持,允许主机控制器在不使用时进入低功耗状态。

目前该技术的社区活跃度和主流应用情况如何?

host.c的代码非常稳定,是MMC子系统中不经常发生根本性改变的部分。然而,它又是绝对核心的,因为每一个MMC主机驱动(位于drivers/mmc/host/目录下)都必须依赖它提供的API来工作。社区的活跃度更多体现在基于这个框架开发新的主机驱动,或者为主机能力集添加新的特性支持。它是所有使用MMC/SD/eMMC/SDIO功能的Linux系统的基础。

核心原理与设计

它的核心工作原理是什么?

host.c的核心原理是接口与实现分离。它定义了“一个MMC主机应该是什么样子以及应该能做什么”(接口),而将“具体怎么做”(实现)的任务留给了具体的硬件驱动。

其工作流程和核心组件如下:

  1. 主机抽象 (struct mmc_host):这是描述一个MMC主机控制器的核心数据结构。它包含了主机的所有信息,如:

    • ops: 一个指向mmc_host_ops结构体的指针,包含了所有操作的回调函数。
    • caps, caps2: 描述硬件能力的位掩码。
    • ios: 描述当前总线状态(时钟、电压、总线宽度等)。
    • card: 指向当前插槽中被识别的卡的指针。
  2. 操作接口 (struct mmc_host_ops):这是一个函数指针的集合,是具体主机驱动必须实现的“合同”。关键操作包括:

    • .request(): 核心函数,MMC核心通过它向主机驱动提交一个数据传输请求(struct mmc_request)。
    • .set_ios(): MMC核心通过它来命令主机驱动改变总线状态(如调整时钟频率、设置总线宽度)。
    • .get_ro(): 用于检测卡是否处于只读状态(通过硬件引脚)。
    • .get_cd(): 用于检测卡是否存在(通过硬件引脚)。
  3. 生命周期管理host.c提供了一套标准的API来管理mmc_host对象的生命周期:

    • mmc_alloc_host(): 一个主机驱动在其.probe函数中首先调用此函数,来分配一个mmc_host结构体和驱动的私有数据区。
    • mmc_add_host(): 驱动在配置好mmc_host(特别是填充了opscaps)之后,调用此函数将主机注册到MMC核心。这是一个关键步骤,一旦调用成功,MMC核心就会接管这个主机,并立即触发一次总线扫描 (mmc_rescan) 来检测卡片。
    • mmc_remove_host(): 在驱动卸载时调用,将主机从核心中注销。
    • mmc_free_host(): 释放mmc_host结构体占用的内存。

它的主要优势体现在哪些方面?

  • 高度抽象和解耦:MMC核心协议层完全无需关心底层硬件的细节,它只与标准的mmc_host接口交互。
  • 简化驱动开发:为主机驱动开发者提供了一个清晰的、必须实现的接口(mmc_host_ops),极大地降低了开发门槛。
  • 代码复用:所有主机的公共管理逻辑都集中在host.c中,被所有驱动共享。
  • 可维护性:结构清晰,当需要为所有主机增加一个新功能时,可能只需要修改host.ccore.c,而无需改动成百上千个具体的主机驱动。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 接口的刚性mmc_host_ops定义了一套固定的接口,如果某个硬件有一些非常规的、无法通过现有opscaps描述的特殊功能,就很难优雅地将其集成进来。但这在实践中非常罕见。
  • 间接调用的复杂性:对于调试和代码跟踪来说,通过函数指针(host->ops->request())进行的间接调用比直接调用更难跟踪。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

host.c提供的框架是为MMC/SD/eMMC主机控制器编写Linux驱动的唯一且强制的解决方案。任何想要将其硬件集成到Linux MMC子系统的芯片厂商或开发者,都必须使用它。

一个典型的主机驱动开发流程就是其使用场景的完美展示:

  1. 在驱动的.probe函数中,首先调用mmc_alloc_host()
  2. 获取到mmc_host指针后,填充其成员:
    • 设置host->ops指向驱动自己实现的静态mmc_host_ops实例。
    • 根据硬件寄存器的值,设置host->capshost->caps2来宣告硬件能力。
    • 设置host->f_min, host->f_max等频率范围。
  3. 完成所有硬件相关的初始化(如申请中断、映射寄存器)。
  4. 最后,调用mmc_add_host(),将控制器“上线”并交由MMC核心管理。

是否有不推荐使用该技术的场景?为什么?

不存在。只要设备是一个MMC、SD、eMMC或SDIO主机控制器,并且需要在Linux下工作,就必须遵循host.c定义的这套框架。

对比分析

请将其 与 其他总线的主机控制器管理框架(如USB HCD, SCSI Host)进行详细对比。

mmc/core/host.c的角色与USB子系统中的主机控制器驱动(HCD)层和SCSI子系统中的**SCSI主机模板(SCSI Host Template)**非常相似。它们的目标都是一样的:抽象底层硬件,为上层协议提供标准接口。

特性 MMC Host (host.c) USB HCD (Host Controller Driver) SCSI Host Template
核心抽象 struct mmc_host struct usb_hcd struct Scsi_Host
操作接口 struct mmc_host_ops struct hc_driver struct scsi_host_template
协议复杂度 中等。处理命令-响应、数据传输、总线状态。拓扑简单(点对点)。 高。处理复杂的USB协议,包括控制、批量、中断、同步四种传输类型,以及树状的设备拓扑(Hubs和设备)。 非常高。处理SCSI命令集(CDBs),支持复杂的命令队列、错误处理和多种设备类型(磁盘、磁带、光驱等)。
交互单元 struct mmc_request (包含命令mmc_command和数据mmc_data) struct urb (USB Request Block) struct scsi_cmnd (SCSI Command)
注册/注销 mmc_add_host / mmc_remove_host usb_add_hcd / usb_remove_hcd scsi_add_host / scsi_remove_host
主要关注点 总线时序、电压、速度模式和卡状态机。 端点(Endpoint)、带宽管理、帧调度和拓扑管理。 命令队列、LUN(逻辑单元)管理、错误恢复。

总结:这三者在设计哲学上是相通的,都采用了“框架 + 驱动实现”的模式。它们的区别主要源于所服务的总线协议的内在差异。MMC总线相对简单,所以mmc_host的抽象也相对直接;而USB和SCSI的协议和设备模型要复杂得多,因此它们的HCD和Host Template框架也更为庞大和复杂。

MMC主机控制器类:sysfs中的统一视图与生命周期管理

本代码片段定义并注册了一个名为mmc_host的设备类(Device Class)。其核心功能是在Linux的sysfs中创建/sys/class/mmc_host/目录,并为所有MMC主机控制器(如mmc0, mmc1等)提供一个统一的、与物理总线无关的视图。同时,它还定义了属于该类的所有设备的通用生命周期管理回调函数,特别是资源释放(release)和系统关机前(pre-shutdown)的处理逻辑。

实现原理分析

此代码是Linux设备模型中“类”概念的一个典型应用。struct class作为一种高层抽象,用于将功能相似的设备组织在一起。

  1. 类定义 (mmc_host_class):

    • 一个静态的struct class实例被定义,其.name字段被设置为"mmc_host",这决定了在sysfs中创建的目录名。
    • .dev_release回调: mmc_host_classdev_release函数被指定为该类的释放回调。当一个mmc_host设备(例如mmc0)的最后一个引用被释放时,设备模型核心会调用此函数。它的职责是完成最终的清理工作,包括注销唤醒源(wakeup source)、释放动态分配的主机索引号(ID),以及释放mmc_host结构体本身占用的内存。这是防止内存泄漏的关键步骤。
    • .shutdown_pre回调: mmc_host_classdev_shutdown函数被指定为系统关机前的回调。在系统关机过程中,设备模型会遍历该类的所有设备,并调用此函数。它的任务是调用__mmc_stop_host,将MMC主机控制器硬件置于一个安全、静默的状态,确保在系统断电前不会有意外的总线活动。
  2. 类注册 (mmc_register_host_class):

    • 这个函数非常简单,它只调用了设备模型提供的核心API class_register(),将mmc_host_class结构体注册到内核中。一旦注册成功,/sys/class/mmc_host目录就会被创建,并且具体的MMC主机控制器驱动就可以通过device_create()等函数将其设备注册到这个类中。
  3. 动态ID管理: mmc_host_classdev_release函数中的ida_free逻辑展示了对主机索引号的动态管理。当一个主机控制器驱动(如STM32的SDMMC驱动)注册时,它会通过ida_alloc从一个全局的ID分配器(mmc_host_ida)中获取一个唯一的索引号(如0, 1, 2…),除非设备树中通过别名(alias)指定了静态ID。在设备被释放时,此处的代码负责将动态分配的ID归还给ID池,以备后续使用。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// mmc_host_classdev_release: mmc_host类设备的释放回调函数。
// 当一个mmc_host设备的引用计数降为0时,此函数被调用。
// @dev: 指向被释放的设备结构体的指针。
static void mmc_host_classdev_release(struct device *dev)
{
struct mmc_host *host = cls_dev_to_mmc_host(dev);

// 注销与此主机关联的唤醒源。
wakeup_source_unregister(host->ws);

// 检查此主机的索引号是否是通过设备树别名静态分配的。
// 如果不是(of_alias_get_id返回负值),说明是动态分配的,需要释放。
if (of_alias_get_id(host->parent->of_node, "mmc") < 0)
// 使用IDA(ID Allocator)框架释放动态分配的主机索引号。
ida_free(&mmc_host_ida, host->index);

// 释放mmc_host结构体本身占用的内存。
kfree(host);
}

// mmc_host_classdev_shutdown: mmc_host类设备的关机前回调函数。
// 在系统关机流程中被调用。
// @dev: 指向要关闭的设备结构体的指针。
static int mmc_host_classdev_shutdown(struct device *dev)
{
struct mmc_host *host = cls_dev_to_mmc_host(dev);

// 调用核心函数停止MMC主机,使其进入安全状态。
__mmc_stop_host(host);
return 0;
}

// mmc_host_class: 定义mmc_host设备类。
static const struct class mmc_host_class = {
.name = "mmc_host", // 类名,对应/sys/class/mmc_host/
.dev_release = mmc_host_classdev_release, // 释放设备的回调函数
.shutdown_pre = mmc_host_classdev_shutdown, // 系统关机前的回调函数
.pm = MMC_HOST_CLASS_DEV_PM_OPS, // 电源管理操作
};

// mmc_register_host_class: 注册mmc_host设备类。
// 返回值: 成功则为0,失败则为负数错误码。
int mmc_register_host_class(void)
{
// 调用设备模型核心函数来注册此类。
return class_register(&mmc_host_class);
}

sd

drivers/mmc/core/sd_ops.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int mmc_send_ext_addr(struct mmc_host *host, u32 addr)
{
struct mmc_command cmd = {
.opcode = SD_ADDR_EXT,
.arg = addr,
.flags = MMC_RSP_R1 | MMC_CMD_AC,
};

if (!mmc_card_ult_capacity(host->card))
return 0;

return mmc_wait_for_cmd(host, &cmd, 0);
}

drivers/mmc/core/sd_uhs2.c SD UHS-II总线管理(SD UHS-II Bus Management) 实现UHS-II卡初始化与信号调整

历史与背景

这项技术是为了解决什么特定问题而诞生的?

这项技术是为了支持 SD UHS-II(Ultra High Speed II) 存储卡标准而诞生的。随着专业相机(4K/8K视频录制、RAW格式高速连拍)和高性能计算设备对存储速度要求的不断提升,传统的SD/UHS-I总线(最高104 MB/s)已成为严重的性能瓶颈。UHS-II标准的出现旨在解决此问题,它引入了一套全新的物理接口和协议,以实现更高的数据传输速率。

drivers/mmc/core/sd_uhs2.c 文件中的代码专门用于解决支持UHS-II所带来的新挑战:

  1. 全新的物理接口:UHS-II卡在传统SD卡引脚的基础上增加了第二排引脚,用于低电压差分信号(LVDS),以实现数百MB/s的高速传输。内核需要新的逻辑来管理和切换到这个新接口。
  2. 复杂的初始化序列:与传统SD卡简单的命令响应初始化方式不同,UHS-II卡的发现和初始化需要一个独特的、基于特定信号模式的握手过程。
  3. 专用寄存器访问:UHS-II卡的功能、速度模式和配置信息存储在一组扩展功能寄存器中,访问这些寄存器需要一套特殊的命令序列。
  4. 信号训练(Training):在如此高的频率下工作,为了保证信号的完整性和可靠的数据传输,主机控制器必须在切换到高速模式之前执行一个“训练”或“调谐(Tuning)”过程,以校准时钟和数据信号的相位。

它的发展经历了哪些重要的里程碑或版本迭代?

UHS-II标准本身是SD卡协会发布的一个重大技术飞跃。在Linux内核中,对其支持是逐步演进的:

  • 初期:内核的MMC/SD子系统只能将UHS-II卡识别为普通的UHS-I或更低速的卡,并以降级模式运行。
  • 引入专有逻辑:为了支持UHS-II的原生高速模式,社区将与UHS-II相关的、高度专业化的代码从通用的SD卡处理逻辑(sd.c)中分离出来,创建了sd_uhs2.c。这一步是关键的里程碑,它实现了UHS-II卡的发现、握手和初始化流程。
  • 完善与优化:后续的版本迭代主要集中在修复特定主机控制器或UHS-II卡上的兼容性问题,优化训练序列的可靠性,以及添加对UHS-III等更新标准的支持(如果它们共享相似的机制)。

目前该技术的社区活跃度和主流应用情况如何?

sd_uhs2.c是Linux内核MMC子系统中的一个核心且稳定的部分,由该子系统的维护者持续维护。这项技术是任何需要支持UHS-II高速读写的现代Linux系统的必备功能。

  • 主流应用:广泛应用于带有高性能SD读卡器的高端笔记本电脑、USB外置读卡器、以及需要高速数据采集的嵌入式系统(例如,专业无人机、高端便携式录音设备等)。

核心原理与设计

它的核心工作原理是什么?

sd_uhs2.c中的代码并不在内核启动时独立运行,而是在通用的SD卡初始化流程中,当检测到可能是UHS-II卡时被调用。其工作流程如下:

  1. 发现(Discovery):在标准的SD卡初始化流程中(位于sd.c),当主机发送ACMD41后,UHS-II卡会通过保持CMDDAT0-3线路为高电平来发出一个特殊信号。MMC核心检测到这个信号后,会中止传统流程,转而调用sd_uhs2_start_discovery()函数。
  2. 握手与初始化:该函数会请求底层的主机控制器驱动执行UHS-II的物理层发现序列。这是一个精确的时序操作,成功后,卡和主机就建立了一个基本的LVDS通信链路。
  3. 读取扩展寄存器:一旦链路建立,sd_uhs2.c中的代码(如sd_uhs2_read_ext_regs)会使用UHS-II专用的命令来读取卡的扩展功能寄存器。这些寄存器描述了卡支持的速度模式、总线宽度(Lane数)、全双工/半双工能力等关键信息。
  4. 模式选择:驱动会比较主机和卡共同支持的最佳模式,并通过sd_uhs2_write_ext_regs将选择的模式(例如,FD156 - 全双工156MB/s)写入卡的寄存器中。
  5. 训练(Training):在正式切换到所选的高速模式之前,必须执行信号训练。sd_uhs2_execute_tuning()函数会请求主机控制器进入训练模式,硬件会发送特定的数据模式,并根据接收到的响应来调整内部的采样点,以确保数据能被无误地读取。
  6. 切换总线模式:训练成功后,驱动会最后一次配置主机控制器,使其正式进入UHS-II LVDS模式进行数据传输。至此,sd_uhs2.c的任务完成,后续的数据传输由MMC核心和主机控制器驱动接管。

它的主要优势体现在哪些方面?

  • 极致性能:解锁了UHS-II卡的全部性能潜力,使数据传输速率从UHS-I的约100MB/s提升至300MB/s甚至更高。
  • 模块化设计:将复杂的UHS-II专用逻辑封装在一个独立的文件中,避免了对通用SD/MMC代码的侵入,使得代码结构更清晰,更易于维护。
  • 遵循标准:严格按照SD卡协会发布的官方规范实现,保证了与标准UHS-II卡和主机的兼容性。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 高度依赖主机驱动sd_uhs2.c本身只实现了协议层的逻辑。所有关键操作(如发现序列、训练)最终都必须由底层的主机控制器驱动程序来实现。如果主机驱动没有实现UHS-II所需的回调函数,或者实现有误,那么UHS-II模式将无法启用。
  • 调试困难:UHS-II的初始化过程涉及物理层的精确信号时序,一旦出现问题(如训练失败),很难单纯通过软件日志来判断是卡的问题、主机的问题,还是电路板信号完整性的问题。
  • 物理层敏感:由于工作频率极高,UHS-II对PCB板的布线质量和信号完整性要求非常苛刻,硬件设计上的瑕疵很容易导致软件层面上的功能不稳定。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

这是支持UHS-II原生速度的唯一标准解决方案

  • 专业媒体工作站:用于为摄影师、摄像师和视频剪辑师提供服务的Linux工作站或笔记本电脑,需要快速地从UHS-II卡中导入大量的RAW照片和高码率视频素材。
  • 高性能外置读卡器:基于Linux的嵌入式系统(例如,一个通过USB-C连接的高速多功能读卡器)需要使用此驱动来支持UHS-II卡。
  • 数据记录设备:在科学研究或工业监控领域,需要将高速传感器数据实时写入可移动存储介质的设备。

是否有不推荐使用该技术的场景?为什么?

不存在不推荐使用该技术的场景,只有硬件不支持的场景。如果一个系统的硬件(主机控制器和卡槽)支持UHS-II,那么启用sd_uhs2.c中的逻辑是发挥硬件性能的必要条件。如果硬件不支持,那么这部分代码自然不会被执行,系统会自动回退到UHS-I或更旧的模式。

对比分析

请将其 与 其他相似技术 进行详细对比。

最直接的对比对象是传统的SD/UHS-I协议,其逻辑主要实现在drivers/mmc/core/sd.c中。

特性 SD UHS-II (sd_uhs2.c驱动) 传统 SD / UHS-I (sd.c驱动)
物理接口 两排引脚,使用低电压差分信号(LVDS)进行高速传输。 单排引脚,使用单端CMOS信号。
初始化方式 复杂的物理层握手和发现序列,访问专用的UHS-II寄存器。 基于命令-响应的协议,访问标准的SD卡寄存器(CSD, CID等)。
总线模式 全双工或半双工,使用1或2对差分信号线(Lane)。 仅半双工,使用1位或4位数据线。
信号训练 强制要求,在进入高速模式前必须执行复杂的训练序列。 对SDR104等高速模式推荐执行,但过程相对简单(使用CMD19)。
理论最高速率 FD312模式下可达312 MB/s。 SDR104模式下最高104 MB/s。
实现复杂性 。协议层和物理层的交互非常紧密,对主机驱动要求高。 中等。是经过多年发展的成熟技术,逻辑相对直接。
向后兼容性 完全兼容。UHS-II卡和主机可以无缝回退到UHS-I或更低模式运行。 N/A

sdio

include/linux/mmc/sdio.h

SDIO协议核心定义:命令、响应及寄存器映射

本文件是Linux内核中用于支持SDIO(Secure Digital Input/Output)卡的核心头文件。它不包含可执行代码,而是完全由C预处理器宏(#define)构成。其核心功能是将SDIO物理规范中定义的命令码、寄存器地址、响应格式以及寄存器内的位域(bit fields)等数值,翻译成一组具有明确语义的符号常量。这为上层的SDIO核心逻辑和底层的主机控制器驱动提供了一个标准、可读且与具体数值无关的编程接口,是内核中所有SDIO相关操作的基础。

实现原理分析

该文件的实现原理是典型的“规范到代码”的直接映射,是硬件编程中的一种基础实践。

  1. 符号化常量: 文件使用 #define 将SDIO规范中定义的魔法数字(Magic Numbers)替换为人类可读的名称。例如,将SDIO的“读写直接命令”的操作码52定义为SD_IO_RW_DIRECT。这样做极大地提高了代码的可读性和可维护性,当规范更新时,只需修改一处定义即可。
  2. 位域抽象: SDIO的寄存器和命令参数通常由多个位域组成。该文件通过位移(<<)、掩码(&)和位或(|)操作,为这些位域提供了符号化的定义。例如,R5_COM_CRC_ERROR被定义为(1 << 15),代码中可以直接使用这个名称来检查R5响应中的CRC错误位,而无需关心它在16位响应中的具体位置是第15位。
  3. 结构化组织: 文件内容按照SDIO规范的逻辑层次进行组织,依次定义了命令(Commands)、响应格式(Responses)、公共控制寄存器(CCCR - Card Common Control Registers)和功能基本寄存器(FBR - Function Basic Registers)。这种结构使得开发者可以轻松地找到与规范特定章节对应的定义。

代码分析

SDIO命令定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* SDIO commands                         type  argument     response */
// SD_IO_SEND_OP_COND: CMD5,用于查询SDIO卡的操作条件寄存器(OCR)。
#define SD_IO_SEND_OP_COND 5 /* bcr [23:0] OCR R4 */
// SD_IO_RW_DIRECT: CMD52,用于单字节直接读写I/O寄存器。
#define SD_IO_RW_DIRECT 52 /* ac [31:0] See below R5 */
// SD_IO_RW_EXTENDED: CMD53,用于多字节块或字节流的扩展读写。
#define SD_IO_RW_EXTENDED 53 /* adtc [31:0] See below R5 */

/*
* CMD52 (SD_IO_RW_DIRECT) 参数格式:
* [31] 读/写标志 (1=读, 0=写)
* [30:28] 功能号 (0-7, 0代表公共I/O区)
* [27] RAW标志 (Read after Write),写操作后是否回读数据
* [25:9] 寄存器地址
* [7:0] 写入的数据 (读操作时忽略)
*/

/*
* CMD53 (SD_IO_RW_EXTENDED) 参数格式:
* [31] 读/写标志
* [30:28] 功能号
* [27] 块模式 (1=多块传输, 0=字节流传输)
* [26] 增量地址 (1=每次读写后地址递增, 0=固定地址)
* [25:9] 寄存器地址
* [8:0] 字节/块 计数
*/

R4与R5响应格式定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// R4_18V_PRESENT: R4响应中的位24,表示卡接受1.8V信号电压。
#define R4_18V_PRESENT (1<<24)
// R4_MEMORY_PRESENT: R4响应中的位27,表示该SDIO卡还包含一个存储器部分(SD Combo card)。
#define R4_MEMORY_PRESENT (1 << 27)

/*
SDIO R5响应中的状态位定义
*/
// R5_COM_CRC_ERROR: 位15,命令CRC校验错误。
#define R5_COM_CRC_ERROR (1 << 15)
// R5_ILLEGAL_COMMAND: 位14,非法命令。
#define R5_ILLEGAL_COMMAND (1 << 14)
// R5_ERROR: 位11,通用错误。
#define R5_ERROR (1 << 11)
// R5_FUNCTION_NUMBER: 位9,指定的功能号无效。
#define R5_FUNCTION_NUMBER (1 << 9)
// R5_OUT_OF_RANGE: 位8,参数超出范围。
#define R5_STATUS(x) (x & 0xCB00) // 提取R5响应中所有状态/错误位的掩码。
#define R5_IO_CURRENT_STATE(x) ((x & 0x3000) >> 12) // 提取I/O当前状态 (CMD, DAT, etc)。

卡公共控制寄存器 (CCCR) 地址与位域定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/*
* Card Common Control Registers (CCCR)
*/
// CCCR寄存器区域的各个寄存器地址定义
#define SDIO_CCCR_CCCR 0x00 // CCCR版本号
#define SDIO_CCCR_SD 0x01 // SD规范版本号
#define SDIO_CCCR_IOEx 0x02 // I/O功能使能
#define SDIO_CCCR_IORx 0x03 // I/O功能就绪
#define SDIO_CCCR_IENx 0x04 // 中断使能
#define SDIO_CCCR_INTx 0x05 // 中断挂起
#define SDIO_CCCR_ABORT 0x06 // 功能中止/卡复位
#define SDIO_CCCR_IF 0x07 // 总线接口控制

/* SDIO_CCCR_IF 寄存器内的位域定义 */
#define SDIO_BUS_WIDTH_MASK 0x03 // 总线宽度掩码
#define SDIO_BUS_WIDTH_1BIT 0x00 // 1位总线宽度
#define SDIO_BUS_WIDTH_4BIT 0x02 // 4位总线宽度
#define SDIO_BUS_CD_DISABLE 0x80 // 禁用DAT3上的内部上拉电阻

#define SDIO_CCCR_CAPS 0x08 // 卡能力寄存器

/* SDIO_CCCR_CAPS 寄存器内的位域定义 */
#define SDIO_CCCR_CAP_LSC 0x40 // 卡是低速卡
#define SDIO_CCCR_CAP_4BLS 0x80 // 卡支持4位低速模式

#define SDIO_CCCR_CIS 0x09 // 公共CIS指针 (3字节)
#define SDIO_CCCR_BLKSIZE 0x10 // I/O块大小 (2字节)
#define SDIO_CCCR_POWER 0x12 // 电源控制
#define SDIO_CCCR_SPEED 0x13 // 总线速度模式选择

/* SDIO_CCCR_SPEED 寄存器内的位域定义 */
#define SDIO_SPEED_SHS 0x01 // 支持高速模式 (Supports High-Speed)
#define SDIO_SPEED_EHS SDIO_SPEED_SDR25 // 使能高速模式 (Enable High-Speed)

功能基本寄存器 (FBR) 地址与位域定义

1
2
3
4
5
6
7
8
9
10
11
12
/*
* Function Basic Registers (FBR)
* 每个SDIO功能 (如WLAN, BT) 都有自己的一组FBR
*/
// SDIO_FBR_BASE(f): 计算功能f的FBR基地址。
#define SDIO_FBR_BASE(f) ((f) * 0x100)

// FBR寄存器区域的各个寄存器地址定义 (相对于FBR基地址)
#define SDIO_FBR_STD_IF 0x00 // 标准I/O功能接口代码
#define SDIO_FBR_POWER 0x02 // 功能电源控制
#define SDIO_FBR_CIS 0x09 // 功能CIS指针 (3字节)
#define SDIO_FBR_BLKSIZE 0x10 // 功能块大小 (2字节)

drivers/mmc/core/sdio_bus.c SDIO总线驱动(SDIO Bus Driver) 为SDIO多功能卡提供独立的设备与驱动绑定模型

历史与背景

这项技术是为了解决什么特定问题而诞生的?

drivers/mmc/core/sdio_bus.c 的诞生是为了解决一个在drivers/mmc/core/bus.c所建立的主MMC总线模型之上更为精细的问题:如何管理一张物理卡片上的多个独立I/O功能(Functions)

标准的MMC总线(mmc_bus)将一整张卡(如SD存储卡)视为一个单一的设备。但这对于SDIO(Secure Digital Input/Output)卡来说是不够的。一张SDIO卡本身不是一个功能设备,而是一个承载多个功能设备的容器。例如,一张典型的“combo”卡可能包含一个Wi-Fi功能和一个蓝牙功能。

如果只使用mmc_bus,我们就无法做到:

  • 将一个Wi-Fi驱动(如brcmfmac)只绑定到Wi-Fi功能上。
  • 同时将一个蓝牙驱动(如btbcm)绑定到蓝牙功能上。

sdio_bus.c通过创建另一个、更细粒度的**sdio_bus_type总线**,专门用来管理这些独立的SDIO功能,解决了这个问题。它允许内核将一张物理卡上的每个功能(Function 1, Function 2, …)都看作是一个独立的、可以与特定功能驱动绑定的逻辑设备。

它的发展经历了哪些重要的里程碑或版本迭代?

sdio_bus.c作为SDIO支持的核心部分,其发展与SDIO规范在Linux中的实现同步进行。

  • 基础框架的建立:最重要的里程碑是创建了sdio_bus_type,并定义了SDIO功能设备(struct sdio_func)和SDIO功能驱动(struct sdio_driver)这两个核心结构。这为SDIO的多功能特性建立了标准的驱动模型。
  • CIS解析:完善了对卡信息结构(Card Information Structure, CIS)的解析。CIS是SDIO卡内部的一块存储区域,它像一张“说明书”,描述了这张卡有哪些功能、每个功能的ID(Vendor/Device ID)以及其他属性。sdio_bus.c依赖这些信息来创建和识别sdio_func设备。
  • 中断处理:SDIO设备通过DAT1线来产生中断。sdio_bus.c参与管理SDIO中断的注册和分发,确保中断能被正确地路由到声明它的那个功能驱动。
  • 电源管理集成:为SDIO功能设备增加了电源管理支持,允许系统在不使用某个功能(如蓝牙)时,可以单独将其置于低功耗状态,而保持其他功能(如Wi-Fi)的运行。

目前该技术的社区活跃度和主流应用情况如何?

sdio_bus.c是MMC子系统中非常成熟和稳定的基础组件。其本身的代码改动很少,但它是所有SDIO功能驱动赖以生存的土壤。社区的活跃度主要体现在不断有新的SDIO功能驱动(特别是各种Wi-Fi和蓝牙芯片的驱动)被开发出来,并注册到sdio_bus上。
它被广泛应用于:

  • 物联网(IoT)设备和开发板。
  • 单板计算机(SBC),如早期的树莓派。
  • 一些特定的工业和嵌入式设备中,SDIO因其相对简单的硬件接口而被用作连接无线模块的方案。

核心原理与设计

它的核心工作原理是什么?

sdio_bus.c的工作原理是在主mmc_bus之上建立一个二级总线

工作流程(层次递进):

  1. 第一层:MMC总线发现SDIO卡

    • MMC核心(core.c)扫描插槽,发现了一张卡,并通过初始化序列识别出其类型为SDIO卡。
    • 主MMC总线(bus.c)将这张整卡与一个通用的SDIO卡驱动(drivers/mmc/core/sdio.c)进行绑定。
  2. 通用SDIO卡驱动进行功能枚举

    • sdio.c驱动的.probe函数被调用后,它的核心任务不是驱动某个具体功能,而是对这张SDIO卡进行“功能枚举”。
    • 它会读取卡的CIS信息,了解这张卡总共有多少个I/O功能(例如,Function 1是Wi-Fi,Function 2是蓝牙)。
  3. 第二层:SDIO总线注册功能设备

    • 对于枚举出的每一个功能,sdio.c会调用sdio_add_func()函数。
    • sdio_add_func()sdio_bus.c提供的核心API。它会创建一个struct sdio_func来代表这个单一功能,并将其作为一个设备注册到sdio_bus_type总线上。
  4. SDIO功能驱动与设备匹配

    • 一个具体的SDIO功能驱动(例如,drivers/net/wireless/broadcom/brcmfmac/sdio.c)会通过sdio_register_driver()将自己注册到sdio_bus上。它会提供一个设备ID表,声明它支持哪些厂商和设备。
    • 当新的sdio_func设备被注册时,sdio_bus.match函数会比较设备的ID(从CIS中读取)和驱动支持的ID表。
    • 如果匹配成功,该功能驱动的.probe函数就会被调用,从而真正地初始化和驱动这个Wi-Fi或蓝牙功能。

它的主要优势体现在哪些方面?

  • 精细的粒度:它将一个物理上的多功能设备,成功地解构成多个逻辑上独立的设备,使得驱动模型可以一一对应。
  • 高度模块化:Wi-Fi驱动和蓝牙驱动可以完全独立开发,它们都面向标准的sdio_func接口,彼此之间没有任何依赖,即使它们物理上在同一块芯片上。
  • 遵循标准:完全遵循了Linux设备模型的层次化思想,结构清晰。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 性能非最优:SDIO总线协议本身相较于现代的PCIe或USB,其带宽和延迟性能都较差,这限制了其上设备(特别是高性能Wi-Fi)的性能上限。这不是sdio_bus.c代码的劣势,而是其服务的协议本身的局限性。
  • 协议复杂性:SDIO的初始化、中断和多功能协商机制比简单的存储卡要复杂,这给驱动开发和调试带来了一定的挑战。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

它是Linux内核中处理SDIO卡的唯一且标准的解决方案。

  • Wi-Fi/蓝牙 Combo卡:这是最经典的使用场景。一张SDIO Combo卡插入后,sdio_bus会创建两个sdio_func设备。一个与Wi-Fi驱动匹配,在系统中生成一个wlan0网络接口;另一个与蓝牙驱动匹配,生成一个hci0蓝牙设备。
  • 独立的SDIO外设:例如SDIO接口的GPS模块、FM收音机模块、NFC模块等。每种模块都有其对应的功能驱动,通过sdio_bus进行绑定。

是否有不推荐使用该技术的场景?为什么?

此技术是高度特化的。

  • 不用于存储卡:对于纯粹的SD/eMMC存储卡,应该使用主mmc_buscard/block.c驱动,sdio_bus完全不参与。
  • 不用于其他总线设备:它只服务于SDIO协议,不能用于USB、PCIe等其他类型的设备。

对比分析

请将其 与 其他支持多功能设备的总线进行详细对比。

sdio_bus与USB总线是进行对比的绝佳范例,因为两者都原生支持复杂的、一个物理设备包含多个逻辑功能的场景。

特性 SDIO Bus (sdio_bus.c) USB Bus
物理层 基于SD卡的并行总线(CMD, CLK, DAT0-3)。 高速差分串行总线(D+/D-)。
逻辑设备抽象 功能 (Function),由struct sdio_func表示。 接口 (Interface),由struct usb_interface表示。一个设备可以有多个配置,每个配置可以有多个接口。
设备枚举方式 读取**CIS (Card Information Structure)**元组链来发现功能及其属性。 读取一系列标准化的描述符 (Descriptors),如设备、配置、接口、端点描述符。
驱动匹配依据 SDIO Vendor ID, Device ID (定义在SDIO规范中)。 USB Vendor ID, Product ID, 以及更通用的Class/SubClass/Protocol码。
驱动绑定对象 struct sdio_driver 绑定到 struct sdio_func struct usb_driver 绑定到 struct usb_interface
架构总结 mmc_bus之上的二级总线,专门处理多功能I/O。 一个统一的总线,其协议原生支持复杂的树状拓扑和多接口设备。

结论sdio_bus.c和USB总线核心在设计思想上殊途同归:它们都为多功能设备提供了一个“容器(设备)与内容(功能/接口)”分离的驱动模型。其根本区别源于底层物理总线和协议规范的巨大差异,但它们都成功地将复杂的物理设备分解为简单的、可独立驱动的逻辑单元,这是现代操作系统驱动框架的共同特征。

SDIO总线类型实现:SDIO功能设备与驱动的匹配与管理

本代码片段完整地定义了Linux内核中sdio总线的行为。其核心功能是为SDIO卡上的功能(Function,如WiFi、蓝牙、GPS等)提供一个独立的总线类型。它实现了SDIO功能设备与相应驱动程序的匹配逻辑、生命周期管理(probe/remove)、sysfs属性导出以及uevent事件生成,从而允许用户空间自动加载驱动模块。这是所有SDIO设备能在Linux下正常工作的基础。

实现原理分析

mmc总线类似,sdio总线也通过实例化一个bus_type结构体(sdio_bus_type)来融入Linux设备模型,但其行为针对SDIO协议的特点进行了定制。

  1. Sysfs属性自动化: 代码使用了sdio_config_attrsdio_info_attr两个宏来批量生成sysfs属性文件。这些宏极大地简化了代码,为每个SDIO功能设备在sysfs中自动创建了如class, vendor, device, revision等只读文件。其中,modalias属性尤为重要,它以标准格式(sdio:cXXvYYYYdZZZZ)输出了设备的ID信息,是udev进行模块自动加载的依据。

  2. 设备与驱动的匹配 (sdio_bus_match): 这是总线的核心逻辑之一。当一个新SDIO功能设备或新SDIO驱动被注册时,设备模型核心会调用sdio_bus_match

    • 该函数会获取驱动程序中定义的id_table,这是一个sdio_device_id数组。
    • 它会遍历这个ID表,并使用sdio_match_one函数将表中的每一项与SDIO功能设备的class, vendor, device ID进行比较。
    • id_table支持SDIO_ANY_ID作为通配符,增加了匹配的灵活性。
    • 只要找到一个匹配的ID,sdio_bus_match就返回1,表示匹配成功,设备模型将继续调用sdio_bus_probe
  3. Uevent事件生成 (sdio_bus_uevent): 当一个SDIO功能设备注册后,此函数被调用。它将SDIO功能的详细ID信息(SDIO_CLASS, SDIO_ID等)打包成环境变量,并通过uevent机制发送到用户空间。其中最关键的是MODALIAS变量,其格式与sysfs中的modalias属性完全一致。用户空间的udev守护进程会捕获此事件,并根据MODALIAS的值自动执行modprobe命令,加载正确的内核驱动模块。

  4. Probe/Remove流程:

    • sdio_bus_probe: 在匹配成功后被调用。它首先进行了一些必要的准备工作,包括处理电源管理(通过pm_runtime_get_sync确保设备上电)和设置一个默认的块大小。然后,它才调用具体SDIO驱动自己的probe函数,将控制权移交给驱动。该函数有完善的错误处理机制,如果在任何步骤失败,都会回滚之前的操作(如释放电源管理引用)。
    • sdio_bus_remove: 在设备移除或驱动卸载时被调用。它同样首先确保设备处于上电状态,以便驱动可以安全地访问硬件寄存器进行清理。然后调用具体驱动的remove函数,并执行与probe相反的清理操作,如释放电源管理引用。

代码分析

Sysfs属性定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/* 定义一个宏,用于快速生成读取SDIO配置字段的sysfs属性 */
#define sdio_config_attr(field, format_string, args...) \
static ssize_t \
field##_show(struct device *dev, struct device_attribute *attr, char *buf) \
{ \
struct sdio_func *func; \
\
func = dev_to_sdio_func (dev); \
/* 使用sysfs_emit格式化输出字段值 */ \
return sysfs_emit(buf, format_string, args); \
} \
/* 使用DEVICE_ATTR_RO宏创建只读的设备属性 */ \
static DEVICE_ATTR_RO(field)

/* 使用宏实例化class, vendor, device, revision和modalias属性 */
sdio_config_attr(class, "0x%02x\n", func->class);
sdio_config_attr(vendor, "0x%04x\n", func->vendor);
sdio_config_attr(device, "0x%04x\n", func->device);
sdio_config_attr(revision, "%u.%u\n", func->major_rev, func->minor_rev);
sdio_config_attr(modalias, "sdio:c%02Xv%04Xd%04X\n", func->class, func->vendor, func->device);

/* 定义一个宏,用于生成读取 CISTPL_FUNCE 信息字符串的属性 */
#define sdio_info_attr(num) \
static ssize_t info##num##_show(struct device *dev, struct device_attribute *attr, char *buf) \
{ \
struct sdio_func *func = dev_to_sdio_func(dev); \
\
if (num > func->num_info) \
return -ENODATA; \
if (!func->info[num - 1][0]) \
return 0; \
return sysfs_emit(buf, "%s\n", func->info[num - 1]); \
} \
static DEVICE_ATTR_RO(info##num)

/* 实例化4个info属性 */
sdio_info_attr(1);
sdio_info_attr(2);
sdio_info_attr(3);
sdio_info_attr(4);

/* 定义包含所有SDIO设备属性的数组 */
static struct attribute *sdio_dev_attrs[] = {
&dev_attr_class.attr,
&dev_attr_vendor.attr,
&dev_attr_device.attr,
/* ... 其他属性 ... */
&dev_attr_modalias.attr,
NULL,
};
ATTRIBUTE_GROUPS(sdio_dev);

设备与驱动匹配逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// sdio_match_one: 比较单个SDIO设备ID与一个SDIO功能设备。
// @func: SDIO功能设备。
// @id: 要比较的SDIO设备ID。
// 返回值: 匹配成功则返回id指针,否则返回NULL。
static const struct sdio_device_id *sdio_match_one(struct sdio_func *func,
const struct sdio_device_id *id)
{
// 依次比较class, vendor, device ID,支持通配符SDIO_ANY_ID。
if (id->class != (__u8)SDIO_ANY_ID && id->class != func->class)
return NULL;
if (id->vendor != (__u16)SDIO_ANY_ID && id->vendor != func->vendor)
return NULL;
if (id->device != (__u16)SDIO_ANY_ID && id->device != func->device)
return NULL;
return id;
}

// sdio_match_device: 遍历驱动的ID表,查找与设备匹配的条目。
// @func: SDIO功能设备。
// @sdrv: SDIO驱动。
// 返回值: 找到匹配项则返回对应的sdio_device_id指针,否则返回NULL。
static const struct sdio_device_id *sdio_match_device(struct sdio_func *func,
const struct sdio_driver *sdrv)
{
const struct sdio_device_id *ids;

ids = sdrv->id_table;

if (ids) {
// 遍历ID表,直到遇到全零的结束条目。
while (ids->class || ids->vendor || ids->device) {
if (sdio_match_one(func, ids))
return ids;
ids++;
}
}

return NULL;
}

// sdio_bus_match: SDIO总线的match回调函数。
// @dev: 设备。
// @drv: 驱动。
// 返回值: 1表示匹配成功,0表示不匹配。
static int sdio_bus_match(struct device *dev, const struct device_driver *drv)
{
struct sdio_func *func = dev_to_sdio_func(dev);
const struct sdio_driver *sdrv = to_sdio_driver(drv);

if (sdio_match_device(func, sdrv))
return 1;

return 0;
}

总线回调函数 (Uevent, Probe, Remove)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// sdio_bus_uevent: SDIO总线的uevent事件生成函数。
static int
sdio_bus_uevent(const struct device *dev, struct kobj_uevent_env *env)
{
const struct sdio_func *func = dev_to_sdio_func(dev);
/* ... 添加 SDIO_CLASS, SDIO_ID, SDIO_REVISION 等环境变量 ... */

// 添加MODALIAS环境变量,这是自动加载模块的关键。
if (add_uevent_var(env,
"MODALIAS=sdio:c%02Xv%04Xd%04X",
func->class, func->vendor, func->device))
return -ENOMEM;

return 0;
}

// sdio_bus_probe: SDIO总线的probe回调函数。
static int sdio_bus_probe(struct device *dev)
{
struct sdio_driver *drv = to_sdio_driver(dev->driver);
struct sdio_func *func = dev_to_sdio_func(dev);
const struct sdio_device_id *id;
int ret;

id = sdio_match_device(func, drv);
if (!id)
return -ENODEV;

/* ... 附加到电源管理域 ... */
ret = dev_pm_domain_attach(dev, 0);
if (ret)
return ret;

atomic_inc(&func->card->sdio_funcs_probed);

/* 确保设备上电,以便驱动可以访问它 */
if (func->card->host->caps & MMC_CAP_POWER_OFF_CARD) {
ret = pm_runtime_get_sync(dev);
if (ret < 0)
goto disable_runtimepm;
}

/* 设置一个默认的、合理的块大小 */
sdio_claim_host(func);
if (mmc_card_removed(func->card))
ret = -ENOMEDIUM;
else
ret = sdio_set_block_size(func, 0);
sdio_release_host(func);
if (ret)
goto disable_runtimepm;

// 调用具体SDIO驱动的probe函数。
ret = drv->probe(func, id);
if (ret)
goto disable_runtimepm;

return 0;

disable_runtimepm: // 错误处理路径
atomic_dec(&func->card->sdio_funcs_probed);
if (func->card->host->caps & MMC_CAP_POWER_OFF_CARD)
pm_runtime_put_noidle(dev);
dev_pm_domain_detach(dev, false);
return ret;
}

// sdio_bus_remove: SDIO总线的remove回调函数。
static void sdio_bus_remove(struct device *dev)
{
struct sdio_driver *drv = to_sdio_driver(dev->driver);
struct sdio_func *func = dev_to_sdio_func(dev);

/* 确保卡上电,以便驱动在移除前可以访问硬件 */
if (func->card->host->caps & MMC_CAP_POWER_OFF_CARD)
pm_runtime_get_sync(dev);

drv->remove(func);
atomic_dec(&func->card->sdio_funcs_probed);

/* ... 检查并清理遗漏的IRQ处理程序 ... */

/* ... 撤销在probe中进行的电源管理操作 ... */

dev_pm_domain_detach(dev, false);
}

总线类型定义与注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 定义SDIO总线的电源管理操作集。
static const struct dev_pm_ops sdio_bus_pm_ops = { /* ... */ };

// 定义SDIO总线类型结构体。
static const struct bus_type sdio_bus_type = {
.name = "sdio",
.dev_groups = sdio_dev_groups,
.match = sdio_bus_match,
.uevent = sdio_bus_uevent,
.probe = sdio_bus_probe,
.remove = sdio_bus_remove,
.pm = &sdio_bus_pm_ops,
};

// sdio_register_bus: 注册SDIO总线。
int sdio_register_bus(void)
{
return bus_register(&sdio_bus_type);
}

// sdio_unregister_bus: 注销SDIO总线。
void sdio_unregister_bus(void)
{
bus_unregister(&sdio_bus_type);
}

drivers/mmc/core/sdio_io.c SDIO I/O操作(SDIO I/O Operations) SDIO功能驱动的底层通信API

历史与背景

这项技术是为了解决什么特定问题而诞生的?

drivers/mmc/core/sdio_io.c 的存在是为了给SDIO功能驱动(Function Drivers)提供一个稳定、标准且抽象的I/O接口。当一个Wi-Fi或蓝牙驱动需要与物理SDIO芯片通信时,它不应该关心如何手动构建复杂的SDIO命令(如CMD52、CMD53),也不应该直接操作MMC核心层的数据结构。

sdio_io.c解决了以下核心问题:

  • 抽象协议复杂性:SDIO协议使用特定的命令(CMD52用于读写单个字节,CMD53用于读写数据块)来进行I/O操作。这些命令的参数和格式都比较复杂。sdio_io.c将这些复杂的命令封装成简单直观的API,如sdio_readb()(读字节)或sdio_memcpy_toio()(写数据块)。
  • 代码复用:如果没有这个文件,那么每一个SDIO功能驱动(如brcmfmac for Broadcom Wi-Fi, mwifiex for Marvell Wi-Fi等)都需要自己实现一遍发送CMD52和CMD53的逻辑。sdio_io.c将这些公共的、重复性的代码提取出来,形成一个共享库。
  • 提供原子操作和总线仲裁:SDIO总线是共享资源,尤其是在一张卡上有多个功能(如Wi-Fi和蓝牙)时。sdio_io.c与总线锁定机制(sdio_claim_host)紧密配合,确保一个功能驱动在执行一系列I/O操作时不会被另一个功能驱动打断,保证了操作的原子性。

简而言之,sdio_io.c是连接上层SDIO功能驱动下层MMC核心协议层之间的关键API实现层

它的发展经历了哪些重要的里程碑或版本迭代?

sdio_io.c作为SDIO子系统最基础的部分,其本身相对稳定。它的发展主要体现在功能的增强和性能的优化上:

  • 基础API的建立:定义了一整套核心的I/O函数,包括单字节、双字节、四字节的读写,以及块数据的读写。
  • 性能优化:针对CMD53的块模式(Block Mode)和字节模式(Byte Mode)进行了优化。对于支持块模式的卡和主机,使用块模式可以显著提高数据吞吐率。
  • 错误处理的完善:增强了对各种I/O错误的检测和处理,向上层驱动返回更明确的错误码。
  • 异步I/O的讨论:虽然当前主流API是同步阻塞的,但社区曾讨论过为SDIO引入异步I/O接口,以适应更高性能的需求,但这会显著增加驱动模型的复杂性。

目前该技术的社区活跃度和主流应用情况如何?

sdio_io.c是SDIO驱动栈的基石,代码非常成熟和稳定。它的“活跃度”体现在它被所有的SDIO功能驱动持续不断地调用。任何对SDIO的性能调优或bug修复都可能触及此文件。它是所有使用SDIO接口的无线模块、GPS模块等设备在Linux下正常工作的底层保障。

核心原理与设计

它的核心工作原理是什么?

sdio_io.c的核心原理是将高级的、面向功能的I/O请求转换成低级的、面向总线协议的MMC请求

工作流程:

  1. API调用:一个SDIO功能驱动(例如Wi-Fi驱动)想要读取芯片上的一个寄存器,它会调用sdio_readb(func, addr, &err_ret)
  2. 总线锁定:在功能驱动中,所有对sdio_io.c中函数的调用都必须被sdio_claim_host(func)sdio_release_host(func)包围。这确保了在执行I/O期间,该功能独占MMC主机控制器。
  3. 命令构建sdio_readb()函数内部会:
    • 创建一个struct mmc_command结构体。
    • 设置其操作码opcodeSD_IO_RW_DIRECT (CMD52)。
    • 根据函数参数(读/写方向、功能编号、寄存器地址、数据)填充mmc_commandarg字段。
  4. 请求提交:该函数会调用mmc_wait_for_cmd(),将构建好的命令传递给MMC核心层。
  5. 核心层处理:MMC核心层(core.c)接收到这个命令后,通过host.c定义的接口将其发送给具体的主机控制器驱动,由硬件执行。
  6. 结果返回:命令执行完毕后,结果(读取到的数据或错误码)会沿着调用链返回,最终sdio_readb()将读取到的字节返回给上层驱动。

对于块数据传输(如sdio_memcpy_toio),流程类似,但构建的是一个包含SD_IO_RW_EXTENDED (CMD53)命令和关联数据(struct mmc_data)的struct mmc_request,并通过mmc_wait_for_req()提交。

它的主要优势体現在哪些方面?

  • 简单易用:为驱动开发者提供了非常直观的API,隐藏了所有底层协议的细节。
  • 健壮性:集中的实现确保了对SDIO命令的构建和错误处理是正确和一致的。
  • 解耦:将功能驱动与MMC核心彻底分离开,功能驱动只需关心自己的设备逻辑,而无需了解MMC总线的工作方式。

它存在哪些已知的劣劣势、局限性或在特定场景下的不适用性?

  • 同步阻塞模型sdio_io.c提供的大部分API都是同步的,即调用会一直阻塞直到I/O操作完成。在高IOPS(每秒I/O操作次数)的场景下,这可能会成为性能瓶颈,因为驱动无法在等待I/O时执行其他任务。
  • 粗粒度锁定sdio_claim_host()的锁定粒度是整个MMC主机。如果一个功能(如Wi-Fi)正在进行一次耗时较长的块传输,另一个功能(如蓝牙)的紧急、低延迟的I/O请求也必须等待,可能会影响其实时性。这是SDIO协议本身的特性,而非sdio_io.c的缺陷。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

它是SDIO功能驱动进行I/O操作的唯一且强制的解决方案。

  • 读写控制寄存器:Wi-Fi驱动在初始化时,需要通过sdio_writeb() / sdio_writel()向芯片写入一系列配置值来启动固件、设置MAC地址等。
  • 收发数据:Wi-Fi驱动通过sdio_memcpy_toio()将一个网络数据包(sk_buff)写入芯片的发送FIFO;通过sdio_memcpy_fromio()从接收FIFO中读取数据包。
  • 中断处理:在中断处理函数中,驱动通常会调用sdio_readb()来读取中断状态寄存器,以确定中断发生的原因。

是否有不推荐使用该技术的场景?为什么?

不存在。任何在Linux内核中编写SDIO功能驱动的开发者都必须使用sdio_io.c提供的API。绕过它直接与MMC核心交互是违反驱动模型设计的,会导致代码不可移植且极不稳定。

对比分析

请将其 与 其他总线的底层I/O实现进行详细对比。

sdio_io.c的实现可以与USB、PCIe等总线的底层I/O机制进行对比,以突显其设计特点。

特性 SDIO I/O (sdio_io.c) USB I/O (Core & HCD) PCI/PCIe I/O (MMIO)
访问模型 命令-响应模型。所有I/O都通过发送特定命令(CMD52/53)来完成。 请求块模型。通过提交异步的USB请求块(URB)来描述传输,支持四种传输类型(控制、批量、中断、同步)。 内存映射模型。设备寄存器被映射到CPU的物理地址空间,驱动像读写内存一样通过ioread/iowrite系列函数来访问。
抽象层次 中等。API封装了命令,但开发者仍需了解寄存器地址和数据格式。 。URB模型完全抽象了底层的USB事务和包。 。驱动直接与“内存地址”交互,非常接近硬件。
同步/异步 主要是同步阻塞 主要是异步非阻塞(通过完成回调)。 同步阻塞ioread/iowrite是阻塞操作。
并发控制 粗粒度总线锁 (sdio_claim_host)。 细粒度端点队列。由主机控制器硬件和驱动软件调度多个端点的并发传输。 细粒度寄存器锁。驱动开发者需要使用自旋锁等机制来保护对共享寄存器或数据结构的并发访问。
数据单位 字节、字、数据块(可以是字节流或固定大小的块)。 端点(Endpoint),每个端点有其固定的传输类型和最大包大小。 字节、字、双字、四字等,与CPU的内存访问粒度一致。

总结sdio_io.c提供了一个介于高层抽象(如USB URB)和底层直接访问(如PCIe MMIO)之间的I/O模型。它通过命令封装提供了必要的抽象,简化了驱动开发,同时其同步模型和粗粒度锁也反映了SDIO总线本身相对简单的设计。

SDIO驱动核心API:总线访问与参数配置

本代码片段提供了SDIO设备驱动程序与硬件交互所必需的三个核心API函数:sdio_claim_hostsdio_release_hostsdio_set_block_size。它们共同构成了SDIO驱动进行I/O操作的基础:claim/release函数用于管理对底层物理总线的独占访问权,而set_block_size则用于配置数据传输的基本单元(块大小),这是优化性能和满足设备要求的关键步骤。

实现原理分析

这段代码体现了Linux内核驱动模型中的分层设计思想,将SDIO协议的特定逻辑建立在通用的MMC/SD总线核心之上。

  1. 总线访问的封装与委托: sdio_claim_hostsdio_release_host是典型的封装函数。SDIO协议是MMC/SD协议的扩展,它们共享同一条物理总线和同一个主机控制器。为了避免并发访问冲突(例如,文件系统正在读写SD卡内存部分,而SDIO驱动同时要访问WLAN功能),必须对主机控制器进行锁定。这两个函数将SDIO驱动开发者关心的“声明/释放SDIO功能总线”这一高级概念,直接委托给底层的MMC核心函数mmc_claim_hostmmc_release_host来执行。它们通过func->card->host这个指针链找到了代表物理控制器的mmc_host结构体,从而实现了逻辑上的分层和代码上的复用。

  2. 通过CMD52进行寄存器配置: sdio_set_block_size函数展示了如何通过SDIO协议配置功能参数。SDIO标准定义了一组通用的寄存器空间,称为功能基本寄存器(Function Basic Registers, FBR)。

    • 该函数的目标是向指定功能(func->num)的FBR中写入16位的块大小值。
    • 它没有使用复杂的块传输命令,而是调用了mmc_io_rw_direct。这个底层函数会生成SDIO的CMD52命令,这是一种专门用于读/写单个字节I/O寄存器的简单命令。
    • 函数分两次调用mmc_io_rw_direct,第一次写入块大小的低8位(LSB),第二次写入高8位(MSB),从而完成了对16位寄存器的设置。
    • 函数还包含了健全性检查(块大小不能超过主机能力)和默认值计算逻辑(在不指定大小时,选择一个对主机和设备都最优的尺寸,但不超过512字节)。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// sdio_claim_host: 为一个指定的SDIO功能设备独占性地声明总线。
// @func: 将要被访问的SDIO功能设备。
// 描述:
// 为一系列操作声明总线。给定的SDIO功能设备被用来确定
// 哪个总线是相关的。在任何I/O操作前都必须调用此函数。
void sdio_claim_host(struct sdio_func *func)
{
// 健壮性检查:如果传入的指针为空,则打印内核警告。
if (WARN_ON(!func))
return;

// 通过 func->card->host 导航到所属的MMC主机控制器,
// 并调用MMC核心的锁定函数来获取对总线的独占访问权。
mmc_claim_host(func->card->host);
}
// 导出符号,使模块化的SDIO驱动能链接到此函数。
EXPORT_SYMBOL_GPL(sdio_claim_host);

// sdio_release_host: 为一个指定的SDIO功能设备释放总线。
// @func: 之前被访问的SDIO功能设备。
// 描述:
// 释放总线,允许其他任务声明总线以执行它们的操作。
void sdio_release_host(struct sdio_func *func)
{
// 健壮性检查:如果传入的指针为空,则打印内核警告。
if (WARN_ON(!func))
return;

// 调用MMC核心的解锁函数,释放对总线的独占访问权。
mmc_release_host(func->card->host);
}
EXPORT_SYMBOL_GPL(sdio_release_host); // 导出符号

// sdio_set_block_size: 设置一个SDIO功能设备的块大小。
// @func: 需要更改的SDIO功能设备。
// @blksz: 新的块大小;如果为0,则使用默认值。
// 描述:
// 驱动可以调用此函数来覆盖核心设置的默认块大小。
// 返回0表示成功,返回负数错误码表示失败。
int sdio_set_block_size(struct sdio_func *func, unsigned blksz)
{
int ret;

// 检查请求的块大小是否超过主机控制器的最大能力。
if (blksz > func->card->host->max_blk_size)
return -EINVAL;

// 如果请求的块大小为0,则计算一个最优的默认值。
if (blksz == 0) {
// 默认值取“功能设备支持的最大值”和“主机控制器支持的最大值”中的较小者。
blksz = min(func->max_blksize, func->card->host->max_blk_size);
// 并且,这个值不能超过512字节,这是一个通用的高效尺寸。
blksz = min(blksz, 512u);
}

// 使用CMD52(通过mmc_io_rw_direct实现)向SDIO卡的FBR寄存器写入块大小的低8位。
// 1: 表示写操作, 0: 表示非读后写
ret = mmc_io_rw_direct(func->card, 1, 0,
SDIO_FBR_BASE(func->num) + SDIO_FBR_BLKSIZE, // 寄存器地址
blksz & 0xff, NULL); // 要写入的数据
if (ret)
return ret;

// 接着,写入块大小的高8位。
ret = mmc_io_rw_direct(func->card, 1, 0,
SDIO_FBR_BASE(func->num) + SDIO_FBR_BLKSIZE + 1, // 高位字节的寄存器地址
(blksz >> 8) & 0xff, NULL); // 要写入的数据
if (ret)
return ret;

// 硬件寄存器写入成功后,更新软件结构体中记录的当前块大小。
func->cur_blksize = blksz;
return 0;
}
// 导出符号,使模块化的SDIO驱动能链接到此函数。
EXPORT_SYMBOL_GPL(sdio_set_block_size);

drivers/mmc/core/sdio_ops.h

1
2
3
4
5
6
7
8
9
10
11
static inline bool sdio_is_io_busy(u32 opcode, u32 arg)
{
u32 addr;

addr = (arg >> 9) & 0x1FFFF;

return (opcode == SD_IO_RW_EXTENDED ||
(opcode == SD_IO_RW_DIRECT &&
!(addr == SDIO_CCCR_ABORT || addr == SDIO_CCCR_SUSPEND)));
}

drivers/mmc/core/sdio_ops.c

SDIO直接I/O命令(CMD52)的实现

本代码片段是Linux内核MMC/SDIO子系统中执行底层I/O操作的核心。它实现了mmc_io_rw_direct函数,该函数的功能是构建并发送SDIO协议中定义的CMD52命令。CMD52是一种基础命令,用于对SDIO卡上特定功能(Function)的I/O寄存器空间进行单字节的读或写操作。这是所有更高级SDIO驱动(如WiFi、蓝牙)配置和控制硬件的基础。

实现原理分析

该函数的实现遵循SDIO协议规范,将一个高级的读/写请求精确地转换为一个32位的命令参数,并通过底层的主机控制器驱动发送出去。

  1. 参数到命令的转换 (mmc_io_rw_direct_host):

    • 命令结构体: 函数首先创建一个mmc_command结构体,这是向底层主机驱动传递命令请求的标准格式。
    • 操作码: cmd.opcode被设置为SD_IO_RW_DIRECT,其标准值为52。
    • 参数构建: CMD52的所有信息都编码在32位的cmd.arg中。该函数按照SDIO规范逐位构建此参数:
      • Bit 31 (方向): write ? 0x80000000 : 0,写操作时置1,读操作时为0。
      • Bits 28-30 (功能号): fn << 28,指定要操作的功能(0-7)。
      • Bit 27 (读后写标志): (write && out) ? 0x08000000 : 0,用于写操作,表示写完后是否需要把寄存器的值读回来。
      • Bits 9-25 (寄存器地址): addr << 9,要访问的17位寄存器地址。
      • Bits 0-7 (写数据): in,对于写操作,这里是待写入的数据字节。
    • 响应类型: cmd.flags被设置为MMC_RSP_R5,表示期望卡返回一个R5类型的响应。R5是SDIO命令特有的响应格式。
  2. 命令执行与等待: mmc_wait_for_cmd(host, &cmd, 0)是将构建好的命令发送出去的关键。它会调用注册在mmc_host中的操作函数集(host->ops->request),将命令传递给具体的主机控制器驱动(如STM32的SDMMC驱动)。此函数会阻塞等待,直到命令完成(或超时)。

  3. 响应解析:

    • 命令完成后,硬件返回的R5响应被存放在cmd.resp[0]中。
    • 代码会解析R5响应中的状态位,如R5_ERROR(通用错误)、R5_FUNCTION_NUMBER(功能号错误)、R5_OUT_OF_RANGE(地址越界),以判断操作是否成功。
    • 数据提取: 对于读操作,R5响应的低8位(cmd.resp[0] & 0xFF)包含了从卡上读取到的数据字节。这个字节被赋给调用者提供的*out指针。
  4. 封装 (mmc_io_rw_direct): 这是一个简单的内联包裹函数,它从mmc_card结构体中提取出mmc_host,然后调用核心实现函数mmc_io_rw_direct_host,为上层驱动提供了更方便的接口。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// mmc_io_rw_direct_host: 执行SDIO CMD52的核心实现。
// @host: MMC主机控制器。
// @write: 0表示读,1表示写。
// @fn: SDIO功能号 (0-7)。
// @addr: 17位的I/O寄存器地址。
// @in: 写操作时待写入的字节。
// @out: 读操作时用于存放结果的字节指针。
static int mmc_io_rw_direct_host(struct mmc_host *host, int write, unsigned fn,
unsigned addr, u8 in, u8 *out)
{
struct mmc_command cmd = {}; // 在栈上初始化命令结构体。
int err;

// 功能号有效性检查。
if (fn > 7)
return -EINVAL;

/* 寄存器地址有效性检查,确保在17位范围内。 */
if (addr & ~0x1FFFF)
return -EINVAL;

// 设置命令码为SD_IO_RW_DIRECT (52)。
cmd.opcode = SD_IO_RW_DIRECT;
// 开始构建32位的命令参数。
// bit 31: 方向位,写操作为1。
cmd.arg = write ? 0x80000000 : 0x00000000;
// bits 30-28: 功能号。
cmd.arg |= fn << 28;
// bit 27: RAW (Read after Write)标志,写后读回数据。
cmd.arg |= (write && out) ? 0x08000000 : 0x00000000;
// bits 25-9: 寄存器地址。
cmd.arg |= addr << 9;
// bits 7-0: 写操作的数据。
cmd.arg |= in;
// 设置期望的响应类型为R5,命令类型为AC(地址命令)。
cmd.flags = MMC_RSP_SPI_R5 | MMC_RSP_R5 | MMC_CMD_AC;

// 调用MMC核心函数发送命令并等待其完成。
// 这会阻塞直到主机控制器驱动报告命令已发送并收到响应。
err = mmc_wait_for_cmd(host, &cmd, 0);
if (err)
return err;

// SPI模式下的响应处理是不同的,这里假设为SD模式。
if (mmc_host_is_spi(host)) {
/* SPI模式下,主机驱动已处理错误。 */
} else {
// 检查R5响应中的状态位。
if (cmd.resp[0] & R5_ERROR)
return -EIO;
if (cmd.resp[0] & R5_FUNCTION_NUMBER)
return -EINVAL;
if (cmd.resp[0] & R5_OUT_OF_RANGE)
return -ERANGE;
}

// 如果是读操作(或写后读),将响应中的数据提取出来。
if (out) {
if (mmc_host_is_spi(host))
// SPI模式下,数据在响应的不同位置。
*out = (cmd.resp[0] >> 8) & 0xFF;
else
// SD模式下,数据在R5响应的低8位。
*out = cmd.resp[0] & 0xFF;
}

return 0;
}

// mmc_io_rw_direct: 上层驱动调用的标准接口。
// @card: MMC/SDIO卡。
// (其他参数同上)
int mmc_io_rw_direct(struct mmc_card *card, int write, unsigned fn,
unsigned addr, u8 in, u8 *out)
{
// 这是一个简单的包裹函数,从card结构体中获取host,然后调用核心实现。
return mmc_io_rw_direct_host(card->host, write, fn, addr, in, out);
}