Linux下物理总线驱动模型之SDIO驱动框架

参考:

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.c, drivers/mmc/core/sdio_ops.c
SDIO总线定义 drivers/mmc/core/bus.c (sdio_bus_type)
头文件 (API) include/linux/mmc/sdio.h, include/linux/mmc/sdio_func.h
主机控制器示例 drivers/mmc/host/sdhci.c, drivers/mmc/host/dw_mmc.c
功能驱动示例 drivers/net/wireless/realtek/rtl818x/rtl8187/rtl8187.h, drivers/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核心层会根据这些原始信息,按照标准规范动态生成一个唯一的设备标识符字符串。这个字符串的格式通常为:

SDIO VID_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_etherdevregister_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路径。

相关推荐
深圳市九鼎创展科技6 小时前
九鼎创展 X7110 开发板(JH7110):国产 RISC-V 多媒体平台全解析
大数据·linux·人工智能·嵌入式硬件·ubuntu·risc-v
流浪0016 小时前
Linux篇(八) Make 与 Makefile 超详细入门教程|从零基础到手写自动化编译
linux·运维·自动化
爱莉希雅&&&6 小时前
Redis哨兵模式和主从复制和集群模式搭建与扩容缩容
linux·redis·缓存·集群·哨兵·数据库同步
j_xxx404_6 小时前
Linux线程:从内存分页机制(Page Table/TLB/Page Fault)彻底读懂 Linux 线程本质
linux·运维·服务器·开发语言·c++·人工智能·ai
不做无法实现的梦~6 小时前
怎么实现codex控制嘉立创EDA绘制原理图
linux
青梅橘子皮7 小时前
Linux---冯诺伊曼体系结构,操作系统概况
java·linux·运维
鹏大师运维7 小时前
不用装远程桌面!统信UOS通过SSH直接调用麒麟图形界面程序
linux·运维·网络·ssh·麒麟·x11·统信v25
Jason_zhao_MR7 小时前
RK3506工业网关:如何打通现场采集、无线传输与行业规约接入?
linux·嵌入式硬件·物联网·系统架构·嵌入式
lingx_gps7 小时前
领新北斗(TracSeek)车辆动态监控系统 - Linux(Ubuntu) 安装部署完整指南
linux·运维·ubuntu·jt808·车辆监控·jt1078·北斗定位