参考:

Linux的platform设备驱动框架-CSDN博客

不要觉得Linux下所有的驱动都使用platform总线架构!!!

物理总线决定驱动模型

Linux 下优先使用与硬件物理连接相匹配的总线驱动模型,只有当设备不依附于任何现有标准总线时,才退而使用 platform 模型。

核心原则:物理总线决定驱动模型

Linux 内核的驱动模型是高度结构化的,其根本原则是“设备连接在哪条总线上,驱动就应该基于那条总线的框架”。内核为不同总线提供了不同的驱动模型,例如:

总线类型 驱动模型/核心结构 设备枚举/匹配方式
USB struct usb_driver 基于 id_table (VID/PID) 自动匹配
PCIe struct pci_driver 基于 id_table (VID/DID) 自动匹配
SDIO struct sdio_driver 基于 id_table (Manufacturer/Device ID) 自动匹配
I2C struct i2c_driver 通常需要设备树或 ACPI 辅助枚举
SPI struct spi_driver 通常需要设备树或 ACPI 辅助枚举
Platform struct platform_driver 软件定义的虚拟总线,需要手动匹配 (设备树/ACPI/固定名称)

从表中可以看出,USB、PCIe、SDIO 这些热插拔总线,设备本身有硬件标识符 (ID),内核可以自动完成驱动和设备的匹配。因此,一个挂在 USB 上的 Wi-Fi 网卡,其驱动必须是 usb_driver

而 Platform 总线是一条虚拟总线,用于那些不是挂在标准物理总线上的设备,比如芯片内部的 UART、I2C 控制器、以及一些通过内存映射连接的外部设备。这类设备没有标准的硬件枚举机制,所以需要软件来告知内核它们的存在和资源(如内存地址、中断号等)。


举例:挂在 SPI 总线上的无线网卡

以 cc2520 这款 SPI 接口的无线芯片为例。它的驱动代码中,不会出现 platform_driver,而是 spi_driver

// drivers/net/ieee802154/cc2520.c

static const struct spi_device_id cc2520_ids[] = {
    { "cc2520", 0 },
    { },
};

static struct spi_driver cc2520_driver = {
    .driver = {
        .name   = "cc2520",
        .of_match_table = cc2520_of_match,
    },
    .id_table   = cc2520_ids,
    .probe      = cc2520_probe,
    .remove     = cc2520_remove,
};

module_spi_driver(cc2520_driver);

这个芯片是通过 SPI 总线连接的,因此它必须使用 spi_driver 模型。内核的 SPI 核心会负责枚举 SPI 总线上的设备,当它发现一个 ID 为 "cc2520" 的 SPI 设备时,就会调用此驱动的 probe 函数。


Platform 模型的核心用途

Platform 模型主要用在以下场景:

1. 芯片内部的“片上设备” (SoC integrated peripherals)

这是 platform 模型最经典的应用。例如,一块 SoC 芯片内部集成的 I2C 控制器、UART、USB PHY、以太网 MAC、随机数生成器等。

  • 它们没有 USB/PCI 那样的硬件枚举机制。

  • 系统需要通过设备树 (Device Tree) 或 ACPI 表来告诉内核:“在这个内存地址,有一个 UART 设备,使用这个中断号。”

例如,drivers/tty/serial/imx.c (i.MX系列SoC的UART驱动) 就是一个标准的 platform_driver。驱动代码中通过 struct platform_driver 结构体定义,并使用 module_platform_driver 宏注册。其 probe 函数从 platform_device 中获取内存地址和中断号等关键资源。

2. 不依附于现有总线的“离散设备”

例如,通过 GPIO 模拟并行总线连接的 LCD 屏幕,或者通过特定内存映射连接的 FPGA 设备。它们没有挂在 I2C/SPI 等标准总线上,因此适合用 platform 模型来描述。

3. “粘合”逻辑

有时,一个复杂的硬件功能可能由多个不同的模块组成,它们需要协同工作。平台驱动可以作为一个“管理者”,将它们组织在一起。


总结

回到你之前的疑问:“如果有具体的总线接口,就不使用 platform 模型了么?”

场景 使用的驱动模型 结论
设备通过 USB/PCIe/SDIO 连接 usb_driver / pci_driver / sdio_driver 不使用 platform 模型
设备通过 I2C/SPI 连接 i2c_driver / spi_driver 不使用 platform 模型 (但匹配通常依赖设备树)
设备是 SoC 片内集成的外设 (如 UART) platform_driver 使用 platform 模型
设备是 内存映射、无标准总线的外设 platform_driver 使用 platform 模型

所以,结论是:对于通过具体物理总线(如USB、PCIe、SDIO、I2C、SPI)连接的设备,驱动模型由该总线决定,不直接使用platform模型。 Platform 模型是作为一种“后备”或“基本”机制,用于那些没有标准总线的设备。它不是一个“通用”模型,而是专门针对“平台设备”这一特定类别设计的。

捋一捋“总线”的概念

不是所有的外部硬件连线都叫总线,而且“总线”这个概念源自CPU内部和主板级互联,但它已经被引申到了更广泛的外部设备连接中。

为了彻底厘清这个问题,我们需要从计算机体系结构(狭义)嵌入式工程(广义)两个维度来看。

1. 狭义的总线(计算机体系结构视角)

在经典的计算机组成原理教材(如《计算机组成与设计》)中,总线被严格定义为一组由多个设备共享的、用于传输数据、地址和控制信号的公共通信路径

它有几个核心特征:

  1. 共享性: 多个设备挂载在同一条总线上(如CPU、内存、PCIe设备)。

  2. 标准协议: 必须遵循标准的通信协议(如仲裁、握手、时序)。

  3. 分时复用: 同一时刻只能有一个设备使用总线传输数据。

  4. 三组信号线: 通常分为数据总线(DB)、地址总线(AB)、控制总线(CB)。

典型例子: AMBA总线(ARM内部)、PCIe总线、前端总线(FSB)。

从这个角度看,简单的点对点连接确实不能叫总线。比如:

  • 两个芯片之间的一根GPIO连接: 这只是一个“信号线”,不是总线。

  • I2C: 这是总线(共享时钟和数据线,多设备挂载)。

  • UART(串口): 严格来说,这不是总线,是“点对点通信链路”。

2. 广义的总线(嵌入式/Linux驱动视角)

在Linux内核文档、驱动工程师的日常交流以及芯片数据手册中,“总线”这个词被极大地泛化了

这里的“总线”,指的是任何连接主机(Host)与外设(Device)的、有明确协议规范和驱动模型的物理接口。它不再强调“共享性”和“多设备挂载”。

为什么会被泛化?

因为Linux内核的驱动模型抽象出了一个概念——“Bus”。内核认为,只要某种物理接口能连接设备,并且内核为该接口实现了一套标准的注册、匹配、电源管理框架,这个接口就可以被称为一种总线类型。

所以,在Linux内核代码里,你会看到:

  • usb_bus_type

  • pci_bus_type

  • i2c_bus_type

  • spi_bus_type

  • platform_bus_type (虚拟的)

在这种语境下,即使是点对点的SPI或USB连接,也被称为总线

3. 关键区分:哪些常见“接口”其实是总线,哪些不是?

为了让你在实际开发中不再困惑,这里有一个明确的分类:

接口/连线名称 严格计算机体系结构视角 Linux驱动/嵌入式工程视角 理由
AMBA/AXI ✅ 是总线 ✅ 是总线 标准、共享、多主从、片内互联
PCI/PCIe ✅ 是总线 ✅ 是总线 多设备共享(交换结构)、标准协议、地址空间
I2C ✅ 总线 ✅ 是总线 双线共享,多设备挂载,需要地址寻址
SPI ❌ 不是总线 ✅ 是总线 主从点对点(或菊花链),但Linux框架将其视为总线
SDIO ❌ 不是总线 ✅ 是总线 类似SPI,但Linux框架支持多Function设备
USB ❌ 不是总线 ✅ 是总线 树形拓扑(Hub),不是传统共享总线,但Linux称为USB总线
UART (串口) ❌ 不是总线 ❌ 不是总线 点对点异步通信,Linux中通常作为TTY设备,没有"总线驱动模型"
GPIO 模拟并行 ❌ 不是总线 ❌ 不是总线 无协议,无标准框架,通常是platform驱动直接操作

4. 为什么你会感觉“总线是CPU里的概念”?

这很可能来源于你学习嵌入式开发时的“内存总线”概念。

  • CPU内部 通过 指令总线、数据总线 连接内核和缓存。

  • CPU外部通过 系统总线(如AXI)连接 DDR控制器

  • 然后通过 外部总线接口 连接 Nor Flash、SDRAM 等。

在那个语境下,总线是指地址空间统一编址、CPU直接通过地址访问的并行总线。

而像 USB、I2C 这类设备,CPU不能直接访问其内部寄存器,必须通过 Host Controller 发送命令包来间接通信。所以,在体系结构教材中,它们常被称为 “通信接口” 或 “IO接口” ,而不是严格意义上的总线。

总结

回到你的问题:

所有的外部硬件连线,都能叫总线吗?

不能。 只有具备共享性、多设备挂载、标准协议寻址的才能叫严格意义上的总线(如I2C)。点对点协议(如UART、简单的GPIO)不是总线。

总线不是CPU里面的概念吗?

原来是这样。在计算机体系结构中,总线特指CPU访问内存和高速外设的并行共享通道。但在嵌入式Linux开发领域,这个概念的外延被扩大了,通常用来泛指任何具有标准Linux驱动框架的物理通信接口(包括I2C、SPI、USB)。

对驱动开发者来说: 不必纠结于学术定义。只要记住:如果内核里有对应的 struct bus_type 和注册函数(usb_registeri2c_add_driverplatform_driver_register),就可以称之为“总线驱动模型”。这也就是为什么我们在之前讨论中,把USB、SDIO、I2C都归为“具体总线”的原因。

SDIO总线驱动模型

SDIO总线驱动模型是Linux内核中用于支持SDIO接口设备(如Wi-Fi/蓝牙二合一模块、GPS、摄像头等)的驱动框架。它建立在MMC(MultiMediaCard)子系统之上,与SD卡驱动共用物理层,但通过特定的命令和CCCR(Card Common Control Registers)来识别和操作功能设备。

下面从分层架构、核心数据结构、设备匹配流程和编程实践四个维度来拆解。


🧱 一、分层架构:从硬件到应用

SDIO驱动模型在Linux内核中大致分为三层:

┌─────────────────────────────────────────┐
│           用户空间 (iw, wpa_s, 应用)       │
└─────────────────────────────────────────┘
                    │ netlink / ioctl
┌─────────────────────────────────────────┐
│          网络子系统 / TTY层 / 其他        │
└─────────────────────────────────────────┘
                    │
┌─────────────────────────────────────────┐
│      SDIO 功能驱动 (如 aic8800_sdio)      │  ← 驱动层(驱动开发者重点关注)
│   - 实现 probe / disconnect              │
│   - 注册网络设备 / 字符设备               │
│   - 通过 sdio_* 函数读写卡                │
└─────────────────────────────────────────┘
                    │ sdio_register_driver(内核提供的注册接口)
┌─────────────────────────────────────────┐
│            MMC/SDIO 核心层               │
│   - 管理 SDIO 总线                        │
│   - 枚举设备、解析 CIS、分配 Function 号  │
│   - 提供 sdio_read/write 等 API          │
└─────────────────────────────────────────┘
                    │
┌─────────────────────────────────────────┐
│        主机控制器驱动 (如 dw_mmc, sdhci)  │
│   - 操作具体硬件控制器 (时钟、命令、数据)  │
└─────────────────────────────────────────┘
                    │ 物理总线
┌─────────────────────────────────────────┐
│         SDIO 物理设备 (AIC8800 等)        │
└─────────────────────────────────────────┘

关键点

  • MMC核心层负责与硬件控制器交互,处理SDIO协议细节。

  • SDIO功能驱动只需要调用sdio_*系列函数读写卡,不需要关心CMD/数据包如何发送。


🧬 二、核心数据结构

1. struct sdio_driver - SDIO功能驱动

每个SDIO设备驱动需要实例化这个结构体:

struct sdio_driver {
    char *name;
    const struct sdio_device_id *id_table;  // 匹配表
    int (*probe)(struct sdio_func *func, const struct sdio_device_id *id);
    void (*remove)(struct sdio_func *func);
    struct device_driver drv;
};

使用示例(简化自AIC8800风格):

static const struct sdio_device_id aic8800_sdio_ids[] = {
    { SDIO_DEVICE(SDIO_VENDOR_ID_AIC, SDIO_DEVICE_ID_AIC8800) },
    { }
};

static struct sdio_driver aic8800_sdio_driver = {
    .name = "aic8800_sdio",
    .id_table = aic8800_sdio_ids,
    .probe = aic8800_sdio_probe,
    .remove = aic8800_sdio_remove,
};

2. struct sdio_func - 代表一个SDIO功能设备

一个物理SDIO卡可以有最多7个功能(Function 0是公共控制区,Function 1~7是实际功能)。例如,一个Wi-Fi+蓝牙二合一模块:

  • Function 1:Wi-Fi

  • Function 2:蓝牙

struct sdio_func {
    struct device dev;
    unsigned int card;      // 所属的SD卡结构
    unsigned char num;      // 功能号 (1~7)
    unsigned short vendor;  // 制造商ID
    unsigned short device;  // 设备ID
    unsigned int max_blksize; // 最大块大小
    unsigned int cur_blksize;
    // ...
};

probe函数中,驱动会获得这个func指针,后续所有读写操作都需要它。


🔌 三、设备匹配与探测流程

当SDIO卡(或芯片)插入时,MMC核心层会自动完成:

主机检测到卡插入
    ↓
发送 CMD5 查询是否支持SDIO
    ↓
读取 CCCR (地址 0x000000~0x0000FF) 获取功能数量
    ↓
为每个功能 (Function 1..N) 创建 struct sdio_func
    ↓
读取每个功能的 CIS (Card Information Structure) 获取制造商ID/设备ID
    ↓
遍历所有已注册的 sdio_driver,比对 id_table
    ↓
匹配成功 → 调用驱动的 probe(func)

匹配表示例

static const struct sdio_device_id aic8800_ids[] = {
    { SDIO_DEVICE(0x1234, 0x8800) },   // VID=0x1234, PID=0x8800
    { },
};

SDIO_DEVICE宏展开后就是一个填充好vendordevice的结构体。


📡 四、核心API:如何读写SDIO卡

probe获得struct sdio_func *func后,驱动可以使用以下函数与硬件通信:

单字节读写(最常用)

u8 sdio_readb(struct sdio_func *func, unsigned int addr, int *err_ret);
void sdio_writeb(struct sdio_func *func, u8 b, unsigned int addr, int *err_ret);

多字节读写(块传输)

int sdio_memcpy_fromio(struct sdio_func *func, void *dst, unsigned int addr, int count);
int sdio_memcpy_toio(struct sdio_func *func, void *src, unsigned int addr, int count);

设置块大小

int sdio_set_block_size(struct sdio_func *func, unsigned int blksize);

中断注册(异步事件)

SDIO设备可以拉低中断线通知主机。驱动可以注册中断处理函数:

int sdio_claim_irq(struct sdio_func *func, sdio_irq_handler_t *handler);
void sdio_release_irq(struct sdio_func *func);

💻 五、编程实践:一个极简SDIO驱动骨架

#include <linux/module.h>
#include <linux/slab.h>
#include <linux/mmc/sdio.h>
#include <linux/mmc/sdio_func.h>

static int my_sdio_probe(struct sdio_func *func, const struct sdio_device_id *id)
{
    int ret;
    u8 val;

    /* 1. 申请中断(如果设备支持) */
    ret = sdio_claim_irq(func, my_sdio_irq_handler);
    if (ret) {
        dev_err(&func->dev, "Failed to claim IRQ\n");
        return ret;
    }

    /* 2. 设置块大小(通常为512) */
    sdio_set_block_size(func, 512);

    /* 3. 读取设备寄存器,验证通信 */
    val = sdio_readb(func, 0x1000, &ret);
    if (ret) {
        dev_err(&func->dev, "Failed to read register\n");
        goto err_release_irq;
    }
    dev_info(&func->dev, "Chip version: 0x%02x\n", val);

    /* 4. 注册上层接口(如net_device) */
    my_netdev = alloc_etherdev(sizeof(struct my_priv));
    if (!my_netdev) {
        ret = -ENOMEM;
        goto err_release_irq;
    }
    SET_NETDEV_DEV(my_netdev, &func->dev);
    register_netdev(my_netdev);

    /* 5. 保存func指针到私有数据,供后续使用 */
    struct my_priv *priv = netdev_priv(my_netdev);
    priv->func = func;
    sdio_set_drvdata(func, my_netdev);

    return 0;

err_release_irq:
    sdio_release_irq(func);
    return ret;
}

static void my_sdio_remove(struct sdio_func *func)
{
    struct net_device *ndev = sdio_get_drvdata(func);
    unregister_netdev(ndev);
    free_netdev(ndev);
    sdio_release_irq(func);
}

static const struct sdio_device_id my_sdio_ids[] = {
    { SDIO_DEVICE(0x02D0, 0xA880) },   // 示例VID/PID
    { },
};
MODULE_DEVICE_TABLE(sdio, my_sdio_ids);

static struct sdio_driver my_sdio_driver = {
    .name = "my_sdio_driver",
    .id_table = my_sdio_ids,
    .probe = my_sdio_probe,
    .remove = my_sdio_remove,
};

module_sdio_driver(my_sdio_driver);
MODULE_LICENSE("GPL");

🎯 六、与其他驱动模型的对比

特性 SDIO驱动 USB驱动 Platform驱动
匹配依据 VID+PID (通过CIS) VID+PID 设备树/compatible
设备数量 最多7个func 任意数量 1个设备对应1个驱动
数据传输 sdio_read/write 族 usb_control_msg 等 ioread/iowrite (MMIO)
中断处理 共享物理中断线 USB中断端点 独立IRQ号
典型场景 Wi-Fi/蓝牙模块 外设 SoC片内集成外设

📁 七、源码位置速查

组件 路径
SDIO核心层 drivers/mmc/core/sdio.cdrivers/mmc/core/sdio_ops.c
SDIO总线定义 drivers/mmc/core/bus.c (sdio_bus_type)
头文件 (API) include/linux/mmc/sdio.hinclude/linux/mmc/sdio_func.h
主机控制器示例 drivers/mmc/host/sdhci.cdrivers/mmc/host/dw_mmc.c
功能驱动示例 drivers/net/wireless/realtek/rtl818x/rtl8187/rtl8187.hdrivers/bluetooth/btmrvl_sdio.c

⚠️ 八、常见注意事项

  1. Function 0只能读CCCR,不能用于数据通信。你的驱动匹配的是Function 1~7。

  2. 必须调用sdio_claim_host/sdio_release_host保护多线程访问

    sdio_claim_host(func);
    val = sdio_readb(func, addr, NULL);
    sdio_release_host(func);
  3. 块传输注意对齐。有些SDIO控制器要求地址和大小按块大小对齐。

  4. 电源管理sdio_set_host_pm_flags用于控制睡眠时的行为。

以上就是SDIO总线驱动模型的完整概览。

SDIO自动发现与匹配过程

SDIO采用了一种纯硬件自动探测的机制,通过SDIO协议直接在总线上询问设备:“你是谁?”

简单来说,设备信息是Linux内核通过SDIO总线协议,在运行时向硬件设备“实时询问”出来的,而不是从设备树“静态读取”的。

整个自动发现与匹配过程分为以下四步:

1. 🕵️‍♂️ 硬件枚举:协议命令获取ID

当SDIO设备插入或系统上电时,内核的MMC核心层会主动发起通信。它不依赖任何外部配置文件,而是直接通过硬件协议与设备交互。

这个过程的核心是一系列标准命令(CMD),用于获取设备的“身份证”:

  • CMD5 (IO_SEND_OP_COND):主机发送此命令以查询总线上是否存在SDIO设备,并协商工作电压。设备会响应,表明自己的身份。

  • CMD3:主机为设备分配一个唯一的相对地址(RCA),用于后续通信。

  • 读取CIS (Card Information Structure):最关键的一步。主机通过CMD52CMD53命令,读取设备内部固化的一块特定存储区域——CIS。这块区域里包含了设备的制造商标识(Manufacturer ID)和设备标识(Product ID),格式为SD\VID_v(4)&PID_p(4)

2. 📋 构造标准ID:生成设备标识符

主机在通过CMD5确认设备是SDIO类型后,会从设备的CIS区域中提取出制造商ID(VID)设备ID(PID)

提取完成后,内核中的MMC/SDIO核心层会根据这些原始信息,按照标准规范动态生成一个唯一的设备标识符字符串。这个字符串的格式通常为:

SDIOVID_vvvv PID_pppp

  • vvvv:4位十六进制制造商代码。

  • pppp:4位十六进制产品代码。

3. 🔗 驱动匹配:内核总线的“红娘”机制

内核中有一个虚拟的SDIO总线sdio_bus_type),它负责管理所有SDIO设备和驱动。当上述设备标识符被生成后,总线就会执行标准的设备-驱动匹配流程。

  • 设备方:将刚刚生成的标识符SDIO VID_xxxx PID_yyyy加入到自己的设备表中。

  • 驱动方:SDIO驱动(例如你的AIC8800驱动)在初始化时,会向内核注册一个sdio_driver结构体。这个结构体里包含一个id_table,表中列出了该驱动能支持的所有设备ID。

匹配逻辑:总线会拿着设备的ID,去遍历所有已注册驱动的id_table。一旦发现某个驱动的id_table中声明支持这个SDIO VID_xxxx PID_yyyy,它就立即“撮合”双方,调用驱动的.probe函数。

4. 🛠️ 驱动接管:Probe函数执行

驱动中的.probe函数被调用,这意味着设备已经被成功发现并匹配

.probe函数里,驱动作者会做真正的硬件初始化工作:

  • 设置块大小,使能设备功能。

  • 注册网络设备(如果是Wi-Fi卡,则生成wlan0)。

  • 注册中断处理函数等。

SDIO总线下挂的是什么设备

看到这里,就要进一步理解SDIO总线驱动和其连接的外设之间的关系!!!

如果SDIO连接的是一个wifi芯片,当SDIO被匹配,然后就会执行probe,这个是关键,你可以在probe里初始化并配置WIFI相关功能!!!!

probe 函数就是 SDIO 驱动和 Wi-Fi 功能之间的桥梁。一旦 SDIO 核心层完成了匹配,它就会调用你在 sdio_driver 结构体中指定的 probe 函数。在这个函数里,你就可以放手去做所有让 Wi-Fi 芯片真正“跑起来”的事情了。

🔑 Probe:驱动初始化的总入口

一个典型的 aic8800_sdio_probe 函数内部,通常会按顺序做这几件大事:

  1. 基础通信验证:通过 sdio_readb / sdio_writeb 等函数,先跟芯片“打个招呼”,比如读取芯片ID寄存器,确认硬件已经活着并且通信正常。

  2. 硬件初始化:下载固件、配置MAC地址、设置工作模式等。

  3. 注册网络设备:调用 alloc_etherdev 和 register_netdev,向Linux网络子系统注册一个网络接口,也就是生成我们熟悉的 wlan0

  4. 中断处理设置:通过 sdio_claim_irq 注册中断处理函数,这样当Wi-Fi芯片有数据或事件时,可以异步通知驱动。

  5. 保存私有数据:把关键的 sdio_func 等指针保存到驱动的私有数据区,供后续的数据发送、状态查询等操作使用。

🎯 这个机制的好处

这种设计带来的最大好处是职责分明

  • SDIO 核心层:负责总线的枚举、电源管理、数据传输等底层细节。它不关心挂在上面的具体是什么设备。

  • Wi-Fi 驱动:只需要专注于 Wi-Fi 协议和芯片特有的逻辑。它不需要知道SDIO命令是如何通过硬件发送出去的,调用 sdio_writeb 就行了。

这就把驱动开发者从繁琐的总线协议中解放了出来。你只需要在 probe 里完成你自己的初始化,剩下的数据收发,通过SDIO提供的API完成即可。

也就是说,你并不用再去加载一个什么wifi驱动了,在这种场景下,wifi芯片外设属于一种SDIO设备!!!!!

SDIO设备树配置

关于SDIO设备树的配置,需要明确一些问题,要不然容易搞混!

两类设备信息

区分这两类设备信息

  1. 一类是外设设备本身的信息,比如外设的id、mac地址等等,这些信息外设自己知道;
  2. 还有一种是外设连接到主控上的信息,比如连到主控的哪些引脚,是否需要复用功能,是否需要启用中断等等,这些信息只有主控知道,外设是不知道的;

可上报信息和不可上报信息设备

SDIO是一种可以上报自身信息的总线!!!

设备有没有“主动说话”的能力,取决于它挂在什么类型的总线上。

简单来说:有标准枚举协议的总线,设备就能“自报家门”;没有这个协议的总线,就得靠设备树之类的静态配置来告诉内核“你是谁、在哪”。

📢 有“自报家门”能力的总线(枚举型总线)

这类总线的设备有标准的硬件 ID(如 VID/PID),连接后主动或被动回答主机询问,无需静态配置。

  • USB:设备插入时主机通过控制传输读取设备描述符,获得 VID/PID 等信息。

  • PCI / PCIe:每个设备都有配置空间,包含 Vendor ID / Device ID 等信息,系统启动时固件会枚举。

  • SDIO:通过 CMD5 确认设备类型,再读取 CIS 获取制造商 ID 和设备 ID。

  • Thunderbolt:基于 PCIe,同样支持枚举。

🔇 没有“自报家门”能力的总线(非枚举型)

这类总线的设备通常只是简单的从设备(Slave),没有 ID 寄存器,或者有 ID 但无法用统一标准协议自动上报,必须在板级描述文件中写明“在哪个地址上连接了什么设备”。

  • I2C:设备只响应特定的地址,但没有标准指令集告诉主机“我是谁”。驱动必须在设备树里写明 compatible 属性来匹配。

  • SPI:类似 I2C,设备没有标准 ID 机制,同样依赖设备树的 compatible 字符串来匹配驱动。

  • 1-Wire:虽每个设备有唯一 ROM ID,但该 ID 通常只标识物理存在,不代表设备类型,一般仍需辅助信息。

  • Parallel Bus(如 SRAM、LCD 并口):纯内存映射,无 ID 概念,完全依赖设备树描述。

  • GPIO / 模拟信号输入:没有任何数字 ID 机制,驱动层必须通过设备树知道哪个 GPIO 对应什么功能。

⚖️ 对比总结

总线类型 是否有上报能力 匹配依据 典型场景
USB / PCIe / SDIO ✅ 有 硬件 VID/PID Wi-Fi、蓝牙、显卡、声卡
I2C / SPI ❌ 没有 compatible 字符串 (设备树) 传感器、触摸屏、显示驱动
1-Wire △ 部分 ROM ID + 辅助信息 电池电量计、温度传感器

💡 一个有趣的例外:I2C 设备也能有 ID 吗?

有些 I2C 设备(如特定型号的温湿度传感器)内部确实有制造商 ID 或设备 ID 寄存器。问题是:

  • 没有统一标准:每个厂商的 ID 寄存器地址和读取方式可能都不一样。

  • 还是需要事先知道地址:主机连设备地址都不知道,根本没法去读那个寄存器。

I2C 设备连“通信地址”都需要在设备树里写明,因为硬件上没有地址发现机制。这跟 USB 的 VID/PID 在本质上完全不同。

🧠 记忆窍门

  • “热插拔总线”(USB、PCIe、SDIO):必须能自报家门,否则用户体验会很差。

  • “板载嵌入式总线”(I2C、SPI):通常不需要热插拔,设备是固定的,所以可以依赖静态配置。

设备树到底是用来干嘛的

设备树描述的是板级硬件拓扑和配置,比如“这个SDIO控制器用哪个时钟、哪个GPIO来唤醒”。它描述的是板级连接信息,是设备自己不知道的事:

信息类型 例子 为什么设备自己不知道 是否该写进设备树
VID/PID 0x1234, 0x8800 设备知道,会主动报告 ❌ 不该
MAC地址 00:11:22:33:44:55 有时烧录在设备中,有时由bootloader传递 ❌ 不该(除非没有OTP)
中断引脚 GPIO 123 芯片不知道主控把它的中断线接到了哪个GPIO ✅ 
电源使能 GPIO 456 芯片不知道谁给它供电 ✅ 
时钟频率 50 MHz 芯片不知道主控给它配了多快的时钟 ✅ 
是否使用SDIO的某功能 keep-power-in-suspend 这是板级电源策略,设备无法知道 ✅ 

一句话:设备自己能说清楚的事,别写进设备树;设备自己不知道、只有板子设计者知道的事,才写进设备树。

总结一下就是:

问题 回答
能把VID/PID写进设备树吗? 技术上可以,但这是设计错误。
为什么没人这么做? 因为违反了可枚举总线的设计哲学,导致驱动和板卡强绑定,失去自动发现能力。
那设备树到底管什么? 描述板级连接信息(中断引脚、电源控制等),这些是设备自己无法报告、但驱动运行时必须知道的。
有没有例外? 对于非枚举总线(I2C, SPI),设备没有自报家门的能力,设备树(或ACPI)就是必须的。这正是SDIO比I2C/SPI先进的地方。

所以,你的AIC8800驱动走的是标准SDIO路径:匹配靠VID/PID自动完成,板级配置(如果有特殊GPIO控制)才可能依赖设备树。这种分层设计,保证了驱动可以在任何板子的SDIO接口上无缝工作。

SDIO应该配置哪些设备树信息

SDIO设备自身的识别信息(VID/PID)不需要设备树,但SDIO主机控制器本身以及它与板级的连接信息,是需要写进设备树的。

简单来说:设备树告诉内核“SDIO控制器在哪、怎么工作”,而控制器通过协议去问设备“你是谁”。

✅ 必须写在设备树里的(主机控制器相关)

这部分描述的是SoC内部的SDIO主机控制器(Host Controller)及其板级配置:

  • 控制器节点:声明存在一个SDIO主机,以及它在内存中的地址、中断号等。

  • 时钟配置:控制器工作频率上限(如50MHz、100MHz)、时钟源等。

  • 引脚/电压:SDIO数据线(4根)、时钟、命令线;是否支持1.8V或3.3V信号。

  • 电源管理:如keep-power-in-suspend(挂起时保持供电)、cap-sdio-irq(支持SDIO中断)等。

  • 总线宽度:默认4位模式还是1位模式。

这是一个典型的设备树节点示例:

&sdmmc1 {  // 对应某个SDIO主机控制器
    pinctrl-names = "default";
    pinctrl-0 = <&sdio_pins>;        // 引脚配置
    vmmc-supply = <&wifi_reg>;       // Wi-Fi模块的电源
    bus-width = <4>;                 // 4位数据总线
    max-frequency = <50000000>;      // 50MHz
    non-removable;                   // 设备不可移除
    cap-power-off-card;              // 支持断电
    keep-power-in-suspend;           // 睡眠不断电
    status = "okay";

    // 这里放的是SDIO设备自身的板级信息,不是VID/PID
    brcmf: bcrmf@1 {
        reg = <1>;                   // SDIO function 1
        compatible = "brcm,bcm4329-fmac";
        interrupt-parent = <&gpio>;
        interrupts = <23 1>;         // 中断引脚(板级)
    };
};

❌ 不应该写在设备树里的(设备自身属性)

下面这些是SDIO设备自己的“身份证”,属于设备内部信息,走协议自动上报:

  • VID/PID(通过CIS读取)

  • 设备功能号(Function 0/1/2...,协议自动发现)

  • 设备需要的固件版本(驱动通过sdio_readb去问设备寄存器)

  • MAC地址(存储在设备OTP或由驱动运行时读取)

🔍 特殊情况:什么时候需要在设备树里声明SDIO设备?

有两种情况例外,需要在设备树里写明:

  1. 需要传递板级信息:比如设备有一个中断引脚接在了GPIO上(而不是走SDIO中断线),就必须在设备树子节点里用interrupts属性指明。设备自己不知道接的是哪个GPIO。

  2. 非标准SDIO设备:少数老旧的SDIO设备可能不遵循标准CIS,内核枚举不到ID。这时候只能在设备树里用compatible硬编码强制匹配——但这属于workaround,新设备不应该这样设计。

📊 对比总结

信息类型 是否写在设备树 谁提供 用途
SDIO控制器地址/中断 ✅ 是 SoC厂商/硬件设计 驱动找到硬件寄存器
控制器时钟/电压/引脚 ✅ 是 板级开发者 配置物理信号
设备中断引脚(板级) ✅ 是 板级开发者 路由中断(如果走GPIO)
VID/PID ❌ 否 设备自身 驱动匹配
功能号 ❌ 否 设备自身 定位具体的Function
MAC地址/固件版本 ❌ 否 设备自身/驱动 运行时读取

💡 总结一句话

SDIO控制器本身是平台设备,它的资源必须由设备树描述;而挂在上面的SDIO卡是自描述设备,不需要设备树去告诉内核“这是一块什么卡”。

这就解释了为什么你写AIC8800驱动时:在设备树里只要配好控制器(&sdmmc1节点)和电源、中断这些板级信息即可,不需要写compatible = "aic,aic8800"。驱动匹配走的是SDIO总线自动上报的VID/PID路径。

SDIO驱动开发一般流程

在我们上面的内容里,已经把 probe 函数定位为驱动初始化的核心了。现在,基于这个共识,我们把 SDIO 驱动开发的完整流程和核心接口系统地梳理一遍,形成一个清晰的开发路线图。

一个完整的 SDIO 驱动开发工作,主要分为下面这张图里的几个层次和步骤。对于 WiFi 驱动开发者来说,我们的主战场在设备驱动层,但需要对下面的层次有清晰的认识。

⚙️ 第一层:主机驱动层 (Host Driver)

这是最底层,直接和芯片的 SDIO 控制器硬件打交道。它负责实现最基础的硬件操作,比如发送命令、处理中断、DMA传输等。这部分工作基本不需要 Wi-Fi 驱动工程师操心,主要由 SoC 厂商(如瑞芯微、联发科)提供,通常是 dw_mmcsdhci 这样的标准驱动。

我们只需要通过设备树 (DTS) 告诉这个驱动一些板级信息即可,例如:

  • 总线宽度 (bus-width = <4>): 使用 4 位数据线。

  • 最高频率 (max-frequency = <50000000>): 时钟跑多快。

  • 工作电压 (vmmc-supply): 哪个稳压器给它供电。

  • 卡检测 (non-removable): 设备是否焊死在板子上,不可移除。

🔌 第二层:MMC核心层 (MMC Core)

这是内核里 MMC/SD/SDIO 子系统的中枢,位于 drivers/mmc/core/-2-8。它像一个大管家,向上给设备驱动提供统一的 API,向下管理主机驱动。它会自动处理 SDIO 卡的枚举流程:发送 CMD5 询问设备类型,读取配置信息(CIS)获得设备的 VID/PID,然后为每个功能创建一个 struct sdio_func

理解这个层的作用主要是让我们知道,设备是如何被发现的,以及驱动和设备是如何匹配上的。

🎯 第三层:设备驱动层 (Device Driver)

这就是我们编写 SDIO WiFi 驱动的核心工作了。我们的驱动,本质上是一个挂载在 SDIO 总线上的标准驱动

1. 驱动注册与匹配

我们用 sdio_driver 结构体来代表这个驱动,其中最关键的就是 .id_table,它像一张“身份证”,声明自己能支持哪些 VID/PID 的设备。

当 MMC 核心层枚举出一个设备时,就会拿它的 VID/PID 和这张表比对,一旦匹配,就会自动调用我们指定的 .probe 函数。

// 驱动定义示例
static const struct sdio_device_id aic8800_ids[] = {
    { SDIO_DEVICE(0xAAAA, 0xBBBB) }, // 假设的AIC8800 VID/PID
    { },
};

static struct sdio_driver aic8800_driver = {
    .name = "aic8800_sdio",
    .id_table = aic8800_ids,
    .probe = aic8800_probe,    // <-- 匹配成功后执行
    .remove = aic8800_remove,
};

module_sdio_driver(aic8800_driver);

2. Probe函数:一切的起点

.probe 函数是整个驱动的发动机,在这里我们完成设备的初始化:

第一步:获取控制权并建立通信
首先调用 sdio_claim_host 获得对该SDIO功能设备的独占访问权。这类似于获取一个锁,防止其他驱动干扰。然后就可以通过读写操作检查设备是否存活,比如读取芯片的ID寄存器-4-7

第二步:硬件初始化
通过 sdio_set_block_size 设置传输的块大小(通常是 512 字节)。接着将固件通过 sdio_memcpy_toio 下载到芯片中。最后通过 sdio_writeb 写寄存器,让芯片启动。

第三步:注册网络设备
调用 alloc_etherdev 创建一个网络设备结构,然后填充 net_device_ops 结构体,把我们自己的 openstophard_start_xmit 等函数注册进去。最后调用 register_netdev,就能在系统中生成 wlan0 接口了。

第四步:设置中断
通过 sdio_claim_irq 注册中断处理函数。这样,当 WiFi 芯片有数据要接收或事件要上报时(通过 DAT1 引脚),我们的驱动就能第一时间响应。

3. 核心数据读写:SDIO接口API

这是驱动中最重要的“工匠活”,所有通信都通过下面这些接口完成:

分类 函数 作用 备注
控制访问 sdio_claim_host / sdio_release_host 获取/释放对SDIO设备的独占访问 每次操作前后必须成对调用,避免冲突
单字节读写 sdio_readb / sdio_writeb 读写芯片内部的寄存器 主要用于配置、控制、查询状态
多字节读写 sdio_memcpy_fromio / sdio_memcpy_toio 从/向芯片内存拷贝一块数据 用于下载固件和收发网络数据包,效率高
块传输 sdio_readsb / sdio_writesb 读写多个块的数据 数据量大时的高效传输方式
中断管理 sdio_claim_irq / sdio_release_irq 申请/释放SDIO中断 处理设备主动上报的事件
功能控制 sdio_enable_func / sdio_disable_func 启用/禁用SDIO功能 激活一个Function,使其可以工作
块大小 sdio_set_block_size 设置数据传输的块大小 根据设备规格设置,常见512或1024字节

4. 数据收发流程示例

WiFi 驱动工作时,上层协议栈要发一个数据包,流程大致是这样的:

  1. 上层调用:内核调用我们注册的 ndo_start_xmit 函数。

  2. 驱动准备:驱动将数据包从 sk_buff 中取出,放入 DMA 缓冲区。

  3. 申请总线:调用 sdio_claim_host(func)

  4. 发送数据:调用 sdio_memcpy_toio(func, buffer, chip_addr, size),将数据发往芯片。

  5. 释放总线:调用 sdio_release_host(func)

  6. 通知完成:发送完成后,通常通过写寄存器或等待中断来告知上层。

接收数据则相反,当芯片收到数据后,会通过中断线(DAT1)通知主机。我们在中断服务程序中,调用 sdio_claim_host 和 sdio_memcpy_fromio 将数据读回。

🚀 从启动到联网:完整流程回顾

把上面的内容串起来,一个 SDIO WiFi 设备从上电到工作的完整旅程是:

  1. 系统启动:SoC 厂商的主机驱动初始化,在设备树中配置好 SDIO 控制器。

  2. 模块加载:执行 insmod aic8800.ko,我们的设备驱动向 MMC 核心层注册自己。

  3. 硬件枚举:MMC 核心层通过总线发现设备,读取其 VID/PID,并创建 sdio_func

  4. 驱动匹配:MMC 核心层发现我们驱动的 ID 表与设备 VID/PID 匹配,调用 aic8800_probe

  5. 设备初始化:在 probe 函数中,我们下载固件、注册 net_device,生成 wlan0

  6. 建立连接:用户空间程序(如 wpa_supplicant)通过 socket 或 netlink 配置 wlan0,连接到 AP。

  7. 数据传输:应用程序的数据包,经过网络协议栈,最终通过我们驱动的 ndo_start_xmit 函数,调用 sdio_memcpy_toio 发送给硬件。

SDIO驱动简单示例

一个实际的代码示例确实能帮我们把之前聊的那些概念都串起来。我们正好可以以 Linux 内核源码中的一个真实驱动——Libertas SDIO WiFi 驱动为例,来看看一个 SDIO 驱动到底是怎么写的。

这个驱动代码位于 drivers/net/wireless/libertas/if_sdio.c,是一个相当标准的参考实现。


🎯 第一步:定义匹配表 (id_table)

这是驱动用来“认领”设备的凭证。它定义了这个驱动能支持哪些 SDIO 设备,通过 VID (Vendor ID) 和 PID (Product ID) 来区分。

// 代码位置: drivers/net/wireless/libertas/if_sdio.c

static const struct sdio_device_id if_sdio_ids[] = {
    { SDIO_DEVICE(SDIO_VENDOR_ID_MARVELL, SDIO_DEVICE_ID_MARVELL_8686) },
    { SDIO_DEVICE(SDIO_VENDOR_ID_MARVELL, SDIO_DEVICE_ID_MARVELL_8688) },
    { /* 结尾的空条目 */ },
};
MODULE_DEVICE_TABLE(sdio, if_sdio_ids);
  • 当插入一个 VID=0x02DF (Marvell)、PID=0x9103 的 SDIO 卡时,系统就会知道要找 if_sdio 这个驱动。

  • MODULE_DEVICE_TABLE 宏会把这个表编译进模块信息里,实现自动加载-5

🧱 第二步:定义并注册 sdio_driver

有了匹配表,我们就可以定义真正的驱动结构体了。

// 代码位置: drivers/net/wireless/libertas/if_sdio.c

static struct sdio_driver if_sdio_driver = {
    .name = "libertas_sdio",       // 驱动名称,会在 /sys/bus/sdio/drivers/ 下看到它
    .id_table = if_sdio_ids,       // 第一步定义的匹配表
    .probe = if_sdio_probe,        // ★ 匹配成功后,核心函数!
    .remove = if_sdio_remove,      // 设备移除时的清理函数
};

// 模块加载入口
static int __init if_sdio_init_module(void)
{
    return sdio_register_driver(&if_sdio_driver);
}
module_init(if_sdio_init_module);

驱动加载时,sdio_register_driver 会把这个结构体注册到内核的 SDIO 总线上。

🚀 第三步:编写 probe 函数——核心工作在这里

probe 是驱动最关键的初始化函数。匹配成功后的所有硬件初始化和接口注册,都在这发生。

// 代码位置: drivers/net/wireless/libertas/if_sdio.c
static int if_sdio_probe(struct sdio_func *func, const struct sdio_device_id *id)
{
    struct lbs_private *priv;   // 私有数据结构,存放驱动关键信息
    struct if_sdio_card *card;
    int ret;

    // 1. 申请硬件资源,获取总线的控制权
    sdio_claim_host(func);

    // 2. 启用SDIO功能。只有启用了,后续的读写操作才有效。
    ret = sdio_enable_func(func);
    if (ret)
        goto release;

    // 3. 读取设备的寄存器,确认芯片的存在和版本
    //    sdio_readb 是通过 CMD52 实现的单字节读写[citation:1]
    ret = sdio_readb(func, IF_SDIO_REG_HW_VERSION, &ret);
    if (ret)
        goto disable;

    // 4. 设置块大小,为批量数据传输做准备[citation:5]
    sdio_set_block_size(func, IF_SDIO_BLOCK_SIZE);

    // 5. 分配并初始化网络设备(net_device)
    priv = lbs_add_card(/* ... */);
    card = priv->card;
    card->func = func;  // 保存 sdio_func 指针,供后续发送数据使用
    sdio_set_drvdata(func, card);  // 将 card 结构体与 func 绑定

    // 6. 申请SDIO中断。当WiFi芯片有数据要发送时,会通过中断线通知主机。
    ret = sdio_claim_irq(func, if_sdio_interrupt);
    if (ret)
        goto free;

    // 7. 最后,注册网络设备,系统里就会出现 wlan0 了[citation:3]
    ret = lbs_start_card(priv);
    if (ret)
        goto release_irq;

    sdio_release_host(func);
    return 0;

// ... 错误处理的跳转标签 ...
}

📡 实际的数据收发操作

probe 之后,驱动就准备就绪了。当上层要发送网络数据包时,驱动会调用 sdio_memcpy_toio 进行批量传输。

  • 发送数据sdio_memcpy_toio:将 skb 中的数据通过 CMD53 命令发送给 SDIO 设备,这种方式支持块传输,效率很高。

  • 接收数据sdio_memcpy_fromio:当收到硬件中断后,通过 CMD53 命令从设备 FIFO 中读取数据。


💎 总结:驱动生命周期全回顾

结合这个例子,我们可以完整地回顾一个 SDIO WiFi 驱动的生命周期:

  1. 模块加载insmod libertas_sdio.ko -> 执行 if_sdio_init_module -> 调用 sdio_register_driver

  2. 驱动匹配:SDIO 核心层比对 if_sdio_ids 和设备的 VID/PID。匹配成功,则调用 if_sdio_probe

  3. 设备初始化probe 函数中,通过 sdio_enable_func 启用设备,sdio_claim_irq 注册中断,最后 lbs_start_card 注册 net_device,生成 wlan0

  4. 正常运行:用户通过 wlan0 收发数据包,驱动层使用 sdio_memcpy_toio/fromio 与硬件交互。

  5. 模块卸载rmmod libertas_sdio -> 调用 if_sdio_remove -> 释放中断、注销网络设备、释放资源。


💡 与你的 AIC8800 对照

你在接触的 AIC8800 驱动,也必然遵循同样的模式:

  • 入口函数module_init 会调用其自身的 sdio_register_driver

  • 核心结构:其中必然有一个包含 .id_table 和 .probe 的 sdio_driver 结构体。

  • 关键回调.probe 函数中会有固件下载 (sdio_memcpy_toio) 和网络设备注册 (register_netdev) 的过程。

  • 硬件操作:所有对硬件的读写,底层最终都会通过 sdio_readb/sdio_writeb (CMD52) 和 sdio_memcpy_toio/fromio (CMD53) 来实现。

这个例子的代码框架非常清晰,建议你可以结合 AIC8800 的驱动源码对照查看,理解起来会快很多。

sdio_register_driver

sdio_register_driver() 是 Linux 内核中用于注册 SDIO 功能驱动的核心接口。简单来说,它就是你的 WiFi 驱动向内核“报到”的入口,作用和我们之前聊到的 module_sdio_driver 宏是一样的。

它之所以如此关键,是因为它完成了驱动与 SDIO 总线绑定的最后一步。

⚙️ 接口做了什么?

它的内部逻辑其实很清晰,可以概括为三步:

  1. 设置总线类型:将驱动的 .bus 成员设置为 &sdio_bus_type。这一步非常关键,它告诉内核:“我是 SDIO 总线下的一个驱动。”

  2. 填充驱动名:将驱动名称传递给底层结构。

  3. 注册到内核:调用通用的 driver_register() 函数,将驱动正式纳入内核的设备驱动模型中进行管理。

用代码来表示,这个函数的核心逻辑就是:

int sdio_register_driver(struct sdio_driver *drv)
{
    drv->drv.name = drv->name;          // 设置驱动名称
    drv->drv.bus = &sdio_bus_type;      // ★ 关键:绑定SDIO总线
    return driver_register(&drv->drv);  // 向内核通用驱动模型注册
}

(注:最新内核中该函数已通过宏实现,主要逻辑与此一致)

🔗 绑定之后:匹配与回调

一旦驱动通过 sdio_register_driver() 注册成功,内核的总线逻辑就会自动接管接下来的工作:

  1. 触发匹配:内核的 SDIO 总线(sdio_bus_type)会拿着你驱动中定义的 id_table(VID/PID 列表),去和系统中已存在的 SDIO 设备进行对比。

  2. 执行回调:当 id_table 中的某个 ID 与一个未被驱动的 SDIO 设备匹配上时,内核就会立刻调用你驱动里指定的 probe 函数。

  3. 驱动工作:正如我们之前详细讨论过的,在 probe 函数中,你才真正开始初始化硬件、注册网络设备(wlan0),让 WiFi 卡开始工作。

📍 它应该被放在哪?

在你的 WiFi 驱动代码中,sdio_register_driver() 必须在驱动的初始化函数中被调用。

通常,这个初始化函数会通过 module_init() 宏来声明。结构如下:

static int __init my_wifi_init(void)
{
    // 做一些其他准备工作...
    return sdio_register_driver(&my_sdio_driver); // 在这里注册
}

static void __exit my_wifi_exit(void)
{
    sdio_unregister_driver(&my_sdio_driver); // 别忘了卸载
}

module_init(my_wifi_init);
module_exit(my_wifi_exit);

简单来说,sdio_register_driver() 就是你 WiFi 驱动向内核正式宣告存在、并等待匹配硬件的“敲门砖”。 它之后的 .probe 函数,才是我们之前讨论的、真正的初始化大本营。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐