在这里插入图片描述

[toc]

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);
}