SDIO技术概述

SDIO的基本概念与技术背景
SDIO(Secure Digital Input Output)是一种基于SD(Secure Digital)存储卡标准扩展的输入输出接口标准。它在物理和电气特性上与SD卡兼容,但功能上有了极大的扩展。SDIO最初的设计目的是为了让SD插槽不再局限于存储功能,而是能够连接各种外设,为移动设备提供了一种标准化的扩展接口。从技术本质上看,SD、MMC和SDIO都属于同一技术体系,都是从MMC规范演化而来,其中MMC强调多媒体存储,SD强调安全和数据保护,而SDIO则专注于输入输出功能。

在物理接口层面,SDIO使用了与SD卡相同的9针接口设计,包括时钟线(CLK)、命令线(CMD)、数据线(DAT0-DAT3)和电源线等。SDIO设备支持两种总线模式:1位模式(只使用DAT0)和4位模式(使用DAT0-DAT3)。与SD存储卡主要进行大数据块传输不同,SDIO设备通常需要进行多次小数据量的传输,同时还需要中断机制来处理异步事件。

从技术架构角度来看,SDIO规范在SD基础上增加了以下关键特性:I/O模式中断机制字节传输模式多功能设备支持。这使得SDIO可以连接网络适配器、蓝牙模块、数码相机、GPS接收器等多样化的功能设备,大大扩展了SD接口的应用范围。

Linux SDIO代码框架与核心数据结构

1.MMC子系统模块划分与关系

Linux内核中的MMC子系统代码主要位于drivers/mmc目录下,包含三个核心子目录:card/、core/和host/。这种目录结构直观地反映了子系统的分层架构。

  • host目录:包含各种平台相关的SD/MMC主机控制器驱动,如STM32、i.MX、OMAP等芯片的驱动实现。每个主机控制器驱动都通过struct mmc_host_ops中定义的回调函数与核心层交互。

  • core目录:实现了MMC、SD和SDIO的协议栈,提供公共接口和数据结构,负责总线的注册、设备的探测和驱动匹配等核心功能。

  • card目录:主要处理存储卡相关的逻辑,包括块设备请求的处理、分区解析等。对于SDIO设备,此部分负责其作为存储功能的相关处理。

从软件架构看,MMC子系统涉及三条虚拟总线:platform_bus_type(用于主机控制器)、mmc_bus_type(用于存储卡)和sdio_bus_type(用于SDIO功能设备)。这种多总线模型使得子系统能够灵活地管理不同类型的设备和驱动。

2.核心数据结构及其关系

SDIO子系统的核心数据结构包括struct mmc_host、struct mmc_card、struct sdio_func和struct sdio_driver,它们共同描述了系统中SDIO的各个组件及其相互关系。
在这里插入图片描述
数据结构间的关系:
在这里插入图片描述

3.主机控制器操作与SDIO总线交互

主机控制器驱动通过实现struct mmc_host_ops中定义的回调函数来与核心层交互。其中与SDIO密切相关的关键操作包括:

  • request():处理MMC请求的核心方法,负责发送命令和数据传输
  • set_ios():配置总线的电气特性,如时钟频率、总线宽度和电压
  • enable_sdio_irq():启用或禁用SDIO中断,这对于SDIO设备的异步事件处理至关重要

SDIO总线使用struct sdio_device_id来进行驱动和设备之间的匹配。该结构包含厂商ID、设备ID和功能号等信息。当插入SDIO设备时,内核会遍历已注册的SDIO驱动,并比较id_table中的信息,如果匹配则调用驱动的probe函数。

设备匹配与生命周期管理

sdio_bus.c中定义了SDIO总线类型的具体行为,这是驱动模型得以运行的关键。它确保了设备与驱动能够自动匹配,并管理其整个生命周期。

Sysfs属性自动化

通过sdio_dev_attrs和sdio_drv_attrs等属性组,内核自动为每个SDIO功能设备在**/sys/bus/sdio/devices/**下创建属性文件,如vendor、device、class等。其中,modalias属性(格式如sdio:c00v02D0d4324)尤为重要,它是udev或modprobe自动加载对应驱动模块的依据。

核心注册匹配逻辑

SDIO驱动通过**sdio_register_driver()**函数向系统注册,该函数实际上是对driver_register()的封装,将驱动添加到SDIO总线驱动列表中。

int sdio_register_driver(struct sdio_driver *drv)
{
    drv->drv.name = drv->name;
    drv->drv.bus = &sdio_bus_type;
    return driver_register(&drv->drv);
}

当驱动注册后,总线会遍历所有已连接的设备,尝试将设备与驱动进行匹配。匹配过程由sdio_bus_match()函数完成,它通过比较sdio_device_id结构中的厂商ID、设备ID和功能号来确定是否匹配。

对于主机控制器驱动,其注册过程略有不同。主机控制器驱动作为平台设备驱动注册到platform_bus_type上,使用平台特定的设备名称进行匹配(如"omap_hsmmc")。一旦匹配成功,主机控制器的probe函数会被调用,初始化硬件并注册mmc_host对象。

设备发现与初始化过程:

SDIO设备的发现过程始于主机控制器的探测。当主机控制器驱动加载后,它会初始化硬件并启动设备检测机制。在Linux内核中,这一过程通常通过延迟工作队列(delayed workqueue)实现,定期检查总线上是否有设备插入或移除。

当检测到设备插入时,主机控制器会启动SDIO初始化序列,这一过程包括多个步骤:

  • 1.电源上电与时钟设置:主机控制器为设备提供电源并设置初始时钟频率(通常为400kHz)
  • 2.CMD5命令发送:查询设备支持的电压范围
  • 3.CMD52/CMD53命令:读取设备的CCCR(Card Common Control Register)和FBR(Function Basic Register)寄存器
  • 4.功能枚举:识别设备中的各个功能并分配资源

设备初始化过程中不涉及数据线路的使用,仅使用时钟和命令线。但如果SDIO DAT0线路被意外置为有效(设置为低电平),初始化过程可能会暂停并失败,因为这会被主机解读为BUSY信号。

多总线模型与驱动绑定

Linux MMC子系统采用多总线模型来管理不同类型的设备。如下图所示,子系统涉及三条总线:platform_bus_type、mmc_bus_type和sdio_bus_type,它们各自负责不同层次的驱动和设备匹配。

drivers/mmc/core/sdio_bus.c实现了一个二级总线模型,专门用于管理SDIO卡上的多个独立功能。这是解决“一张物理卡,多个逻辑设备”问题的核心设计。

诞生背景与核心问题:标准的MMC总线将整张存储卡视为单一设备,但这无法满足SDIO Combo卡(如同时包含Wi-Fi和蓝牙)的需求。SDIO总线驱动的诞生,就是为了在MMC总线之上,为每个I/O功能(Function)提供独立的设备与驱动绑定模型。

工作原理(层次化流程)

  • 1.MMC总线发现:MMC核心识别到SDIO卡,并与通用SDIO卡驱动绑定。
  • 2.功能枚举:通用驱动读取卡的CIS(卡信息结构),了解卡上所有功能。
  • 3.设备注册:为每个功能创建一个sdio_func设备,并注册到SDIO总线上。
  • 4.驱动匹配:具体的功能驱动(如Wi-Fi驱动)通过ID表与设备匹配,触发其probe函数完成初始化。

这种设计的主要优势在于高度的模块化和精细的粒度。Wi-Fi驱动和蓝牙驱动可以像独立的JavaScript模块一样开发和维护,互不干扰,仅通过标准的SDIO总线接口交互。

总线管理还负责协调多个SDIO功能设备。一个物理SDIO卡可以包含多个功能设备(sdio_func)(如WiFi、蓝牙和GPS集成在一个卡上),每个功能都有独立的功能号(Function Number,通常为1-7)和独立的配置空间(Configuration Space)。每个功能的功能号在卡出厂时即被固化,用于在总线上唯一标识该功能。其配置空间则包含了该功能的CIS(Card Information Structure),其中详细定义了功能类型(如Wi-Fi、蓝牙、GPS)、制造商信息、支持的SDIO标准版本、最大块大小、中断能力以及电源管理需求等关键参数。这种设计使得主机能够像访问PCI设备配置空间一样,精确地查询和配置每个SDIO功能。SDIO总线负责在这些功能之间共享主机控制器资源,确保它们能够协同工作。
在这里插入图片描述
这种多总线模型使得子系统能够灵活地支持各种类型的设备,同时保持各层的独立性和可扩展性。例如,SDIO WiFi驱动只需要关注sdio_bus_type上的注册和匹配,而不需要了解底层主机控制器的具体实现细节。

SDIO初始化和通信过程深度剖析

设备初始化流程与命令交互

SDIO设备的初始化是一个复杂的过程,涉及多个标准命令的交换和寄存器配置。当主机检测到设备插入后,会执行以下初始化序列:

1.基础初始化阶段:

  • 主机发送CMD0(GO_IDLE_STATE)使设备进入空闲状态
  • 主机发送CMD5(IO_SEND_OP_COND)查询设备支持的电压范围和IO能力
  • 设备响应其OCR(Operating Conditions Register)内容

2.设备识别阶段:

  • 主机发送CMD3(SEND_RELATIVE_ADDR)为设备分配相对地址(RCA)
  • 主机发送CMD7(SELECT_CARD)选择特定地址的设备,将其置于传输状态

3.SDIO特定初始化:

  • 主机使用CMD52读取CCCR(Card Common Control Register)了解设备公共能力
  • 主机读取FBR(Function Basic Register)获取每个功能的配置信息
  • 主机使用CMD52配置设备的工作参数,如总线宽度和中断模式

4.功能特定配置:

  • 对于每个SDIO功能,主机读取其CIS(Card Information Structure)获取厂商ID、设备ID等信息
  • 主机配置每个功能的工作参数,如块大小和传输模式

在整个初始化过程中,CMD52(IO_RW_DIRECT)命令起着关键作用,它用于读写单个SDIO设备的寄存器,是配置设备的主要手段。而CMD53(IO_RW_EXTENDED)则用于大数据量的传输,支持字节模式和块模式两种传输方式。

数据传输机制与状态管理

SDIO设备支持两种数据传输模式:字节模式和块模式。字节模式适用于小数据量的寄存器访问,而块模式则适用于大数据量的传输,如网络数据包或文件内容。

数据传输涉及三个关键状态机的协同工作:命令状态机、响应状态机和数据状态机。以下Mermaid状态图展示了SDIO数据读写的典型流程:
在这里插入图片描述
在实际的数据传输过程中,主机控制器负责协调命令、响应和数据的流动。对于读操作,主机先发送读命令,然后设备返回响应和数据;对于写操作,主机发送写命令后接着发送数据,然后设备返回操作结果。

中断处理与电源管理

SDIO设备的中断机制是其区别于普通SD存储卡的重要特性之一。SDIO设备通过将DAT1线拉低来向主机发送中断请求,这使得设备能够在发生特定事件时(如数据就绪、状态改变)主动通知主机,而不需要主机轮询设备状态

主机控制器驱动通过实现enable_sdio_irq()回调函数来控制系统对SDIO中断的响应。当该函数参数enable为true时,主机应配置硬件监听DAT1线的中断信号;当为false时,则忽略该信号。

在Linux内核中,SDIO中断处理通常分为两个部分:

  • 硬件中断处理:在主机控制器的中断服务例程中检测到SDIO中断,禁用进一步的中断,并调度下半部(此时cpu开中断,可被自身或其他中断打断)处理
  • 功能驱动处理:底半部通知具体的SDIO功能驱动(如WiFi驱动),由驱动处理中断原因并采取相应行动

电源管理是SDIO子系统另一个重要方面。SDIO设备支持多种电源状态,包括活动状态、睡眠状态和关机状态。主机可以通过CMD52修改设备的电源管理寄存器来控制设备的电源状态,从而实现功耗的动态管理。

简单SDIO设备驱动实例实现

驱动框架与注册代码

#include <linux/module.h>
#include <linux/slab.h>
#include <linux/sdio.h>
#include <linux/device.h>

/* 定义设备ID表,用于驱动匹配,这里的ID是整张SDIO Card的硬件标识符 */
static const struct sdio_device_id simple_sdio_ids[] = {
    { SDIO_DEVICE(0x1234, 0x5678) }, /* 厂商ID:0x1234, 设备ID:0x5678 */
    { /* 结束 */ }
};

MODULE_DEVICE_TABLE(sdio, simple_sdio_ids);

/* 驱动probe函数,当设备匹配时调用 */
static int simple_sdio_probe(struct sdio_func *func,
                 const struct sdio_device_id *id)
{
    int ret;
    struct device *dev = &func->dev;
    
    dev_info(dev, "SDIO设备已检测 (厂商:%04x, 设备:%04x)\n",
             func->vendor, func->device);
    
    /* 启用SDIO功能 */
    ret = sdio_enable_func(func);
    if (ret) {
        dev_err(dev, "无法启用SDIO功能: %d\n", ret);
        return ret;
    }
    
    /* 设置块大小 */
    ret = sdio_set_block_size(func, 64);
    if (ret) {
        dev_err(dev, "无法设置块大小: %d\n", ret);
        sdio_disable_func(func);
        return ret;
    }
    
    /* 注册中断处理程序 */
    ret = sdio_claim_irq(func, simple_sdio_irq);
    if (ret) {
        dev_err(dev, "无法注册中断处理程序: %d\n", ret);
        sdio_disable_func(func);
        return ret;
    }
    
    /* 初始化设备特定数据结构 */
    // init_device_data(dev);
    
    dev_info(dev, "SDIO设备驱动加载成功\n");
    return 0;
}

/* 驱动remove函数,当设备移除或驱动卸载时调用 */
static void simple_sdio_remove(struct sdio_func *func)
{
    struct device *dev = &func->dev;
    
    /* 释放中断 */
    sdio_release_irq(func);
    
    /* 禁用SDIO功能 */
    sdio_disable_func(func);
    
    dev_info(dev, "SDIO设备驱动已卸载\n");
}

/* 中断处理函数 */
static void simple_sdio_irq(struct sdio_func *func)
{
    /* 处理设备中断 */
    // handle_interrupts(func);
}

/* 定义SDIO驱动结构 */
static struct sdio_driver simple_sdio_driver = {
    .name = "simple_sdio",
    .id_table = simple_sdio_ids,
    .probe = simple_sdio_probe,
    .remove = simple_sdio_remove,
};

/* 模块初始化函数 */
static int __init simple_sdio_init(void)
{
    int ret;
    
    ret = sdio_register_driver(&simple_sdio_driver);
    if (ret)
        pr_err("无法注册SDIO驱动: %d\n", ret);
    else
        pr_info("SDIO驱动注册成功\n");
    
    return ret;
}

/* 模块退出函数 */
static void __exit simple_sdio_exit(void)
{
    sdio_unregister_driver(&simple_sdio_driver);
    pr_info("SDIO驱动已注销\n");
}

module_init(simple_sdio_init);
module_exit(simple_sdio_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple SDIO Device Driver");

设备操作与数据传输

实现基本的设备注册后,我们需要添加设备操作和数据传输功能。下面的代码展示了如何实现SDIO设备的读写操作:

/* 从设备读取数据 */
static int simple_sdio_read(struct sdio_func *func, unsigned addr, 
                           void *buf, unsigned size)
{
    int ret;
    
    /* 获取主机控制器 */
    ret = sdio_claim_host(func);
    if (ret)
        return ret;
    
    /* 执行读取操作 */
    if (size == 1) {
        /* 单字节读取 */
        *(u8 *)buf = sdio_readb(func, addr, &ret);
    } else {
        /* 多字节读取 */
        ret = sdio_readsb(func, buf, addr, size);
    }
    
    /* 释放主机控制器 */
    sdio_release_host(func);
    
    return ret;
}

/* 向设备写入数据 */
static int simple_sdio_write(struct sdio_func *func, unsigned addr,
                            void *buf, unsigned size)
{
    int ret;
    
    /* 获取主机控制器 */
    ret = sdio_claim_host(func);
    if (ret)
        return ret;
    
    /* 执行写入操作 */
    if (size == 1) {
        /* 单字节写入 */
        sdio_writeb(func, *(u8 *)buf, addr, &ret);
    } else {
        /* 多字节写入 */
        ret = sdio_writesb(func, addr, buf, size);
    }
    
    /* 释放主机控制器 */
    sdio_release_host(func);
    
    return ret;
}

/* 实现文件操作接口 */
static ssize_t simple_sdio_fops_read(struct file *file, char __user *buf,
                                    size_t count, loff_t *ppos)
{
    struct sdio_func *func = file->private_data;
    u8 *kernel_buf;
    int ret;
    
    /* 分配内核缓冲区 */
    kernel_buf = kmalloc(count, GFP_KERNEL);
    if (!kernel_buf)
        return -ENOMEM;
    
    /* 从设备读取数据 */
    ret = simple_sdio_read(func, *ppos, kernel_buf, count);
    if (ret < 0)
        goto out;
    
    /* 将数据复制到用户空间 */
    if (copy_to_user(buf, kernel_buf, count)) {
        ret = -EFAULT;
        goto out;
    }
    
    *ppos += count;
    ret = count;
    
out:
    kfree(kernel_buf);
    return ret;
}

应用场景、局限性与总结

主流应用场景:

  • Wi-Fi/蓝牙Combo卡:最经典用例,一张卡在系统中生成独立的网络接口和蓝牙设备。
  • 嵌入式外设:如SDIO接口的GPS、NFC、Zigbee模块,常用于物联网设备和单板计算机。
    局限性:
  • 性能瓶颈:SDIO协议本身的带宽(通常最高约50-100 MB/s)和延迟不及PCIe或USB 3.0,限制了高性能应用。
  • 协议复杂性:初始化、中断协商等机制比纯存储卡复杂,增加了驱动调试难度。
  • 适用性:仅适用于SDIO设备,不用于SD存储卡(用MMC总线)或其他总线设备。

学习内容来源:
https://blog.csdn.net/qq_38061020/article/details/154073165
https://www.cnblogs.com/yangykaifa/p/19675859

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐