第一部分:Platform 虚拟总线的引入与基础架构

一、总线设备驱动模型的演进

在Linux内核的早期版本中,驱动开发常采用一种“硬编码”的方式。开发者需要将具体的硬件资源(如寄存器地址、中断号、引脚信息)直接写在驱动代码中。这种方式虽然简单直接,能够快速实现功能,但存在明显的弊端:当硬件平台或引脚发生变化时,驱动代码需要重新修改、编译和加载,缺乏灵活性和可扩展性

为了解耦“驱动逻辑”与“硬件资源”,Linux内核引入了总线设备驱动模型。该模型的核心思想是:

  • 设备:只负责描述硬件资源(如地址、中断)。
  • 驱动:只负责实现硬件控制逻辑。
  • 总线:负责匹配设备和驱动,当匹配成功时,由驱动接管设备。

随着嵌入式技术的发展,设备树(Device Tree)的引入进一步优化了这一模型。硬件资源不再由C代码定义,而是通过配置文件(.dts文件)来描述,实现了驱动代码与硬件配置的完全分离。修改硬件引脚时,只需修改设备树文件并重新编译生成设备树二进制文件(.dtb),无需改动驱动源码。

二、虚拟总线的概念:为什么需要 Platform 总线?

在复杂的片上系统(SoC)中,集成了大量的独立外设控制器(如I2C控制器、SPI控制器、GPIO控制器、时钟等)。这些外设直接挂在SoC的内存总线上,并没有依附于像PCI、USB、I2C这样的物理总线

为了让这些“无总线依附”的设备也能享受“总线-设备-驱动”模型的便利,Linux内核发明了一种虚拟总线——Platform总线。

Platform总线的作用可以总结为:

  1. 抽象连接:将SoC内部和外围直接挂接在内存空间的设备统一视为“平台设备”(platform_device)。
  2. 管理资源:统一管理这些设备占用的内存、中断和I/O资源。
  3. 提供匹配机制:作为设备与驱动的桥梁,完成配对并触发驱动的探测(probe)函数。

三、核心数据结构概览

Platform总线的实现主要依赖三个核心数据结构,它们定义了总线、设备和驱动的属性和行为。

1. platform_bus_type (总线)

这是Platform总线的“代言人”,内核通过它来定义总线的名称、匹配规则和行为。

定义位置drivers/base/platform.c

struct bus_type platform_bus_type = {
    .name           = "platform",        // 总线名称,在/sys/bus下可见
    .dev_groups     = platform_dev_groups,
    .match          = platform_match,    // 匹配函数:核心配对逻辑
    .uevent         = platform_uevent,
    .dma_configure  = platform_dma_configure,
    .pm             = &platform_dev_pm_ops,
};
2. platform_device (设备)

用于描述一个平台设备,包含了设备名称、ID、硬件资源以及与设备模型相关的通用结构体。

定义位置include/linux/platform_device.h

struct platform_device {
    const char  *name;          // 设备名称,用于匹配
    int         id;             // 设备ID,用于区分同名设备
    bool        id_auto;
    struct device dev;          // 通用设备结构体(父类)
    u32         num_resources;  // 资源数量
    struct resource *resource;  // 指向资源数组的指针
    const struct platform_device_id *id_entry;
    // ...
};
3. platform_driver (驱动)

用于描述一个平台驱动,包含了驱动需要实现的核心回调函数(probe, remove)以及通用的驱动结构体。

定义位置include/linux/platform_device.h

struct platform_driver {
    int (*probe)(struct platform_device *);     // 匹配成功时调用
    int (*remove)(struct platform_device *);    // 设备移除时调用
    void (*shutdown)(struct platform_device *);
    int (*suspend)(struct platform_device *, pm_message_t state);
    int (*resume)(struct platform_device *);
    struct device_driver driver;                // 通用驱动结构体
    const struct platform_device_id *id_table;  // 支持的设备ID表
    bool prevent_deferred_probe;
};

三者关系

  • platform_bus_type 是总线基座。
  • platform_deviceplatform_driver 分别挂在总线的设备链表和驱动链表上。
  • platform_match 函数负责检查设备链表上的某个设备和驱动链表上的某个驱动是否匹配。

四、总线注册底层流程

内核在启动的早期阶段,需要完成Platform总线的初始化。这个过程的核心是调用 platform_bus_init 函数。

1. 调用路径

在内核初始化过程中,platform_bus_init 的调用链路大致如下:

start_kernel()
  └── rest_init()
      └── kernel_init()
          └── do_basic_setup()
              └── driver_init()
                  └── platform_bus_init()  // 在这里完成 Platform 总线的初始化
2. platform_bus_init 函数分析

platform_bus_init 函数的实现位于 drivers/base/platform.c 中,它主要做了两件事:注册平台总线设备(platform_bus)和注册平台总线类型(platform_bus_type)。

int __init platform_bus_init(void)
{
    int error;

    // 清空早期平台设备链表,避免干扰
    early_platform_cleanup();

    // 1. 注册平台总线设备
    // 将 platform_bus 作为一个设备注册到内核设备模型中
    error = device_register(&platform_bus);
    if (error) {
        put_device(&platform_bus);
        return error;
    }

    // 2. 注册平台总线类型
    // 将 platform_bus_type 总线类型注册到内核总线模型中
    error = bus_register(&platform_bus_type);
    if (error)
        device_unregister(&platform_bus);

    // 注册设备树平台设备的通知链(用于动态加载设备树节点)
    of_platform_register_reconfig_notifier();
    return error;
}

关键操作解读

  • device_register(&platform_bus)
    • platform_bus 设备注册到内核。
    • 这会在 /sys/devices 目录下创建 platform 目录(/sys/devices/platform)。这个目录是所有平台设备(platform_device)的父目录。你可以把它想象成所有平台设备在sysfs文件系统中的根文件夹。
  • bus_register(&platform_bus_type)
    • platform_bus_type 总线类型注册到内核。
    • 这会在 /sys/bus 目录下创建 platform 目录(/sys/bus/platform),并在其下建立 devicesdrivers 子目录,用于管理挂载在该总线上的所有设备和驱动。
    • 此步骤至关重要,它建立了总线的匹配和管理机制。
3. 注册效果图

执行完 platform_bus_init 后,内核中会建立如下结构:
在这里插入图片描述

4. 设备添加时的总线挂载

在后续注册具体的平台设备(platform_device)时,platform_device_add 函数会利用已经注册好的 platform_bus_typeplatform_bus 设备:

int platform_device_add(struct platform_device *pdev)
{
    // 如果设备没有指定父设备,则默认挂在 platform_bus 下
    if (!pdev->dev.parent)
        pdev->dev.parent = &platform_bus;   // 父设备是 platform_bus

    // 将该设备的总线设置为 platform_bus_type
    pdev->dev.bus = &platform_bus_type;     // 挂在 platform 总线上

    // 设置设备名称,如 "led_pdev.0"
    dev_set_name(&pdev->dev, "%s.%d", pdev->name, pdev->id);

    // 最终将设备添加到内核设备模型中
    return device_add(&pdev->dev);
}

第二部分:平台设备 (Device) 与硬件资源 (Resource)

一、平台设备抽象:platform_device 结构体

平台设备是 Platform 总线下挂载的设备实体,它通过 struct platform_device 来描述。该结构体定义在 include/linux/platform_device.h 中,其核心成员如下:

struct platform_device {
    const char  *name;          // 设备名称,用于匹配驱动
    int         id;             // 设备 ID,区分同名的不同实例(如 "uart.0", "uart.1")
    bool        id_auto;        // ID 是否自动生成
    struct device dev;          // 通用设备结构体,嵌入内核设备模型
    u32         num_resources;  // 资源数量
    struct resource *resource;  // 指向资源数组的指针
    const struct platform_device_id *id_entry; // 设备 ID 表项(匹配用)
    char *driver_override;      // 强制绑定指定驱动
    // ... 其他成员(MFD、架构特定数据等)
};
重要成员解析
  1. name
    设备的名称,是匹配驱动的关键标识之一(当不使用设备树或 ID 表时,驱动通过 .driver.name 与它比较)。必须是唯一标识。
  2. id
    用于区分同名的多个设备。例如,一个 SoC 可能有多个 UART 控制器,它们的 name 都是 "uart",但通过 id 来区分(如 0、1)。若只有一个设备,通常设置为 -1PLATFORM_DEVID_NONE,此时在 sysfs 中显示为 name;若需要自动分配 ID,可设为 PLATFORM_DEVID_AUTO
  3. dev
    内嵌的通用设备结构体,由内核设备模型管理。重要:该结构体中的 release 方法必须实现,否则卸载设备时内核会报错。通常在定义设备时提供 dev.release 函数。
  4. num_resourcesresource
    • num_resources:资源的数量,即资源数组的元素个数。
    • resource:指向 struct resource 数组的指针,描述设备占用的硬件资源(内存区域、中断、I/O 端口等)。
      资源通过 DEFINE_RES_MEMDEFINE_RES_IRQ 等宏定义。

二、硬件资源描述:resource 结构体

struct resource 定义在 include/linux/ioport.h 中,用于描述一段硬件资源(如寄存器地址范围、中断号)。其核心成员如下:

struct resource {
    resource_size_t start;      // 资源起始地址(物理地址或中断号)
    resource_size_t end;        // 资源结束地址(如果是中断,则与 start 相同)
    const char *name;           // 资源名称(便于调试)
    unsigned long flags;        // 资源类型和属性标志
    struct resource *parent, *sibling, *child; // 用于资源树管理
};
关键标志位 flags
1. 资源类型标志(必选其一)
宏定义 含义
IORESOURCE_MEM 内存映射寄存器区域
IORESOURCE_IO I/O 端口资源(x86 常用)
IORESOURCE_IRQ 中断资源
IORESOURCE_DMA DMA 通道资源
IORESOURCE_REG 寄存器偏移量(较少用)
2. 资源属性标志(可选)
宏定义 含义
IORESOURCE_PREFETCH 可预取的内存
IORESOURCE_READONLY 只读
IORESOURCE_CACHEABLE 可缓存
IORESOURCE_MEM_64 64 位内存资源
3. 状态标志
宏定义 含义
IORESOURCE_DISABLED 资源当前未使用
IORESOURCE_UNSET 地址尚未分配
IORESOURCE_AUTO 地址由系统自动分配
资源定义示例

在代码中,可以使用内核提供的辅助宏快速定义资源:

// 定义内存资源:起始地址 0xfd60004,大小 4 字节
#define GPIO0_DR  0xfd60004
DEFINE_RES_MEM(GPIO0_DR, 4)

// 定义中断资源:中断号 23
DEFINE_RES_IRQ(23)

三、设备的注册与注销机制

平台设备的注册通过 platform_device_register() 完成,注销则使用 platform_device_unregister()。其核心流程如下:

1. 注册流程:platform_device_register()
int platform_device_register(struct platform_device *pdev)
{
    device_initialize(&pdev->dev);          // 初始化通用设备结构体
    arch_setup_pdev_archdata(pdev);         // 架构相关的设置(如 DMA 掩码)
    return platform_device_add(pdev);       // 实际添加设备
}
platform_device_add() 的关键步骤
int platform_device_add(struct platform_device *pdev)
{
    // 1. 设置父设备:若未指定,则默认挂在 platform_bus 下
    if (!pdev->dev.parent)
        pdev->dev.parent = &platform_bus;   // platform_bus 是全局设备

    // 2. 设置总线类型:挂载到 platform 总线上
    pdev->dev.bus = &platform_bus_type;

    // 3. 根据 id 设置设备名称(决定 sysfs 中的名称)
    switch (pdev->id) {
    case PLATFORM_DEVID_NONE:
        dev_set_name(&pdev->dev, "%s", pdev->name);
        break;
    case PLATFORM_DEVID_AUTO:
        // 自动分配 ID,生成 "name.autoid"
        dev_set_name(&pdev->dev, "%s.%d", pdev->name, id);
        break;
    default:
        dev_set_name(&pdev->dev, "%s.%d", pdev->name, pdev->id);
        break;
    }

    // 4. 将资源插入全局资源树(request_resource)
    for (i = 0; i < pdev->num_resources; i++) {
        struct resource *r = &pdev->resource[i];
        // ... 检查并插入资源
    }

    // 5. 将设备添加到内核设备模型(关键一步)
    ret = device_add(&pdev->dev);
    // ... 错误处理
}

流程图

在这里插入图片描述

2. 注销流程:platform_device_unregister()
void platform_device_unregister(struct platform_device *pdev)
{
    platform_device_del(pdev);   // 从设备模型中移除
    platform_device_put(pdev);   // 减少引用计数,可能释放内存
}
  • platform_device_del:调用 device_del() 从内核设备模型中移除设备,同时释放占用的资源(从资源树中移除)。
  • platform_device_put:递减引用计数,当计数归零时调用 dev->release 释放设备结构体。

四、资源的提取与获取

在驱动开发中,驱动需要获取设备描述的资源(如寄存器物理地址、中断号)。Platform 总线提供了标准 API 来获取这些资源。

1. platform_get_resource() 函数
struct resource *platform_get_resource(struct platform_device *pdev,
                                       unsigned int type, unsigned int num);

参数

  • pdev:平台设备指针。
  • type:资源类型(IORESOURCE_MEMIORESOURCE_IRQ 等)。
  • num:同类型资源中的索引(从 0 开始)。

返回值:指向 resource 结构的指针,若未找到则返回 NULL

实现原理:该函数会遍历 pdev->resource 数组,依次比较 r->flagstype 是否匹配,并累加匹配次数,直到索引为 num 时返回该资源。

2. platform_get_irq() 快捷函数
int platform_get_irq(struct platform_device *pdev, unsigned int num);

该函数是 platform_get_resource(pdev, IORESOURCE_IRQ, num) 的封装,直接返回中断号(resource->start),若获取失败则返回负数。

3. 使用示例
static int my_probe(struct platform_device *pdev)
{
    struct resource *mem_res;
    struct resource *irq_res;
    void __iomem *reg_base;
    int irq;

    // 获取第一个内存资源
    mem_res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!mem_res) {
        dev_err(&pdev->dev, "failed to get memory resource\n");
        return -ENXIO;
    }

    // 映射物理地址到虚拟地址
    reg_base = devm_ioremap(&pdev->dev, mem_res->start,
                            resource_size(mem_res));
    if (!reg_base) {
        dev_err(&pdev->dev, "failed to ioremap\n");
        return -ENOMEM;
    }

    // 获取第一个中断资源
    irq = platform_get_irq(pdev, 0);
    if (irq < 0) {
        dev_err(&pdev->dev, "failed to get irq\n");
        return irq;
    }

    // 请求中断...
    dev_info(&pdev->dev, "IRQ %d, registers at 0x%08llx\n",
             irq, (unsigned long long)mem_res->start);

    return 0;
}
4. 资源获取的底层遍历

platform_get_resource() 的实现核心:

struct resource *platform_get_resource(struct platform_device *dev,
                                       unsigned int type, unsigned int num)
{
    u32 i;

    for (i = 0; i < dev->num_resources; i++) {
        struct resource *r = &dev->resource[i];
        if (type == (r->flags & (IORESOURCE_IO | IORESOURCE_MEM |
                                  IORESOURCE_IRQ | IORESOURCE_DMA |
                                  IORESOURCE_REG))) {
            if (num-- == 0)
                return r;
        }
    }
    return NULL;
}

注意:该函数仅匹配类型,不校验 num 是否超过实际数量。

5. 设备树节点的资源获取

当设备通过设备树(Device Tree)描述时,设备树节点会被转换为 platform_device,其 resource 数组由设备树的 reginterrupts 等属性自动填充。驱动中除了使用 platform_get_resource,还可以直接使用 OF 函数(如 of_iomap)来获取资源,但通常更推荐使用统一的 platform_get_resource 以保持代码无关性。

第三部分:平台驱动 (Driver) 与设备树 (OF) 解析协同

在前两部分中,我们了解了 Platform 总线的基础架构以及平台设备的描述与注册。现在,我们将目光转向平台驱动(platform_driver)。平台驱动是真正实现硬件控制逻辑的代码单元,它通过总线与设备配对,并在配对成功后执行初始化(probe)。随着设备树的广泛应用,驱动开发中越来越多地使用 OF(Open Firmware)函数来解析设备树中描述的硬件资源,从而实现与硬件无关的驱动程序。

本部分将深入剖析平台驱动的结构、注册流程,以及设备树驱动的核心 OF 函数。

一、平台驱动抽象:platform_driver 结构体

平台驱动通过 struct platform_driver 结构体来描述,它定义了驱动与设备交互的核心回调函数。该结构体定义在 include/linux/platform_device.h 中:

struct platform_driver {
    int (*probe)(struct platform_device *);         // 探测函数,设备匹配成功后调用
    int (*remove)(struct platform_device *);        // 移除函数,设备从系统移除时调用
    void (*shutdown)(struct platform_device *);     // 关机函数,系统关机时调用
    int (*suspend)(struct platform_device *, pm_message_t state); // 挂起函数
    int (*resume)(struct platform_device *);        // 恢复函数
    struct device_driver driver;                    // 通用驱动结构体
    const struct platform_device_id *id_table;      // 支持的设备 ID 表
    bool prevent_deferred_probe;                    // 是否阻止延迟探测
};
关键成员详解
成员 描述
probe 必须实现。当总线匹配到设备后,内核会调用该函数。驱动在此函数中完成硬件初始化、资源获取、设备注册(如字符设备、网络设备)等工作。
remove 必须实现。当设备从系统中移除时(如模块卸载),内核调用该函数,驱动需释放 probe 中申请的资源。
shutdown 系统关机时调用,用于执行设备特定的关闭操作。
suspend / resume 系统电源管理相关的挂起/恢复函数,用于实现设备的低功耗管理。
driver 内嵌的通用设备驱动结构体,包含驱动的名称、所属总线、设备树匹配表等。.name 成员是早期平台驱动匹配的重要依据
id_table 指向 struct platform_device_id 数组的指针,用于指定驱动支持的设备名称列表,可实现一个驱动匹配多个设备。当设备树的兼容性匹配不存在时,内核会回退到此表进行匹配。
prevent_deferred_probe 若设置为 true,则禁止该驱动的延迟探测(通常用于解决依赖问题)。
平台驱动定义示例
static int my_probe(struct platform_device *pdev)
{
    // 初始化硬件、注册设备等
    return 0;
}

static int my_remove(struct platform_device *pdev)
{
    // 释放资源
    return 0;
}

static const struct platform_device_id my_ids[] = {
    { .name = "my_device_v1" },
    { .name = "my_device_v2" },
    { },
};
MODULE_DEVICE_TABLE(platform, my_ids);

static struct platform_driver my_driver = {
    .probe      = my_probe,
    .remove     = my_remove,
    .driver = {
        .name   = "my_driver",               // 用于名称匹配
        .owner  = THIS_MODULE,
        .of_match_table = of_match_ptr(my_of_ids), // 设备树匹配表
    },
    .id_table   = my_ids,                   // 可选 ID 表
};

二、驱动的注册流程:platform_driver_register

平台驱动通过 platform_driver_register 函数注册到内核。该函数实际上是一个宏,展开后调用 __platform_driver_register,并传入当前模块指针。

#define platform_driver_register(drv) \
    __platform_driver_register(drv, THIS_MODULE)

__platform_driver_register 定义在 drivers/base/platform.c 中:

int __platform_driver_register(struct platform_driver *drv, struct module *owner)
{
    drv->driver.owner = owner;
    drv->driver.bus = &platform_bus_type;       // 绑定到平台总线
    drv->driver.probe = platform_drv_probe;     // 包装后的 probe
    drv->driver.remove = platform_drv_remove;   // 包装后的 remove
    drv->driver.shutdown = platform_drv_shutdown;

    return driver_register(&drv->driver);       // 注册到内核设备驱动模型
}
关键操作解读
  1. 绑定总线:将驱动的 bus 成员设置为 &platform_bus_type,这样驱动就隶属于平台总线,内核在匹配时会遍历该总线上的设备。
  2. 设置通用 probe/remove:内核的设备驱动模型要求 struct device_driver 提供 proberemove 回调。这里用 platform_drv_probeplatform_drv_remove 作为包装函数,它们内部会调用驱动开发者定义的 drv->probedrv->remove,并完成一些公共操作(如电源域管理、时钟设置)。
  3. 调用 driver_register:将驱动插入内核的驱动链表,并触发与现有设备的匹配过程。
platform_drv_probe 包装函数
static int platform_drv_probe(struct device *_dev)
{
    struct platform_driver *drv = to_platform_driver(_dev->driver);
    struct platform_device *dev = to_platform_device(_dev);
    int ret;

    // 设置设备树中指定的默认时钟
    ret = of_clk_set_defaults(_dev->of_node, false);
    if (ret < 0)
        return ret;

    // 将设备附加到电源域
    ret = dev_pm_domain_attach(_dev, true);
    if (ret)
        goto out;

    // 调用驱动开发者定义的 probe 函数
    if (drv->probe) {
        ret = drv->probe(dev);
        if (ret)
            dev_pm_domain_detach(_dev, true);
    }

out:
    // 处理延迟探测的情况
    if (drv->prevent_deferred_probe && ret == -EPROBE_DEFER) {
        dev_warn(_dev, "probe deferral not supported\n");
        ret = -ENXIO;
    }
    return ret;
}
注册流程图

在这里插入图片描述

三、设备树 (DTS) 与 OF 函数的引入

设备树是一种描述硬件配置的数据结构,它以节点(node)和属性(property)的形式向内核传递硬件信息。在驱动开发中,设备树极大地提高了代码的可移植性:同一份驱动可以支持多个硬件平台,只需修改设备树文件即可。

当设备树中的节点被解析后,会自动创建对应的 platform_device,其资源(如 reginterrupts)被转换为 struct resource。驱动中则通过 OF 函数来获取设备树节点中的特定信息。

常用 OF 函数简介
函数 功能
of_find_node_by_name 根据节点名称查找设备树节点
of_find_node_by_path 根据绝对路径查找设备树节点
of_property_read_u32 / of_property_read_string 读取节点属性值
of_iomap 映射节点 reg 属性指定的内存区域
irq_of_parse_and_map 解析节点中的中断信息并映射为 Linux 中断号
of_irq_get 获取节点的中断号
设备树节点示例
myirq {
    compatible = "my_devicetree_irq";
    interrupt-parent = <&gpio3>;
    interrupts = <RK_PA5 IRQ_TYPE_LEVEL_LOW>;
};

四、核心 OF 函数应用

在实际驱动开发中,probe 函数通常需要从设备树节点中提取硬件信息。以下结合 05、of函数.pdf 中的代码片段,详细讲解几个核心 OF 函数的使用。

1. 查找设备树节点:of_find_node_by_name
struct device_node *of_find_node_by_name(struct device_node *from,
                                         const char *name);
  • 功能:根据节点名称查找设备树节点。
  • 参数:
    • from:起始节点,通常设为 NULL 表示从根节点开始查找。
    • name:要查找的节点名称。
  • 返回值:找到的节点指针,未找到则返回 NULL

示例:

struct device_node *mydevice_node;
mydevice_node = of_find_node_by_name(NULL, "myirq");
if (!mydevice_node) {
    pr_err("Failed to find node myirq\n");
    return -ENODEV;
}
2. 解析中断并映射:irq_of_parse_and_map
unsigned int irq_of_parse_and_map(struct device_node *dev, int index);
  • 功能:解析设备树节点的 interrupts 属性,将其转换为 Linux 内核的中断号(虚拟 IRQ 号)。
  • 参数:
    • dev:设备树节点指针。
    • index:中断索引(一个节点可能有多个中断,0 表示第一个)。
  • 返回值:中断号,失败返回 0。

示例:

int irq = irq_of_parse_and_map(mydevice_node, 0);
if (!irq) {
    pr_err("Failed to parse interrupt\n");
    return -EINVAL;
}
pr_info("irq is %d\n", irq);
3. 获取中断数据结构:irq_get_irq_datairqd_get_trigger_type

这两个函数用于获取中断的详细信息,如触发类型(上升沿、下降沿、高电平、低电平)。

struct irq_data *irq_get_irq_data(unsigned int irq);
unsigned int irqd_get_trigger_type(struct irq_data *d);

示例:

struct irq_data *my_irq_data = irq_get_irq_data(irq);
if (my_irq_data) {
    unsigned int trigger_type = irqd_get_trigger_type(my_irq_data);
    pr_info("trigger type is 0x%x\n", trigger_type);
}
4. 从设备节点获取中断号:of_irq_get
int of_irq_get(struct device_node *dev, int index);
  • 功能:与 irq_of_parse_and_map 类似,也是获取中断号,但返回负错误码用于错误处理。
  • 返回值:中断号(正数),失败返回负的错误码。

示例:

int irq = of_irq_get(mydevice_node, 0);
if (irq < 0) {
    pr_err("Failed to get irq: %d\n", irq);
    return irq;
}
pr_info("irq is %d\n", irq);
5. 通过平台设备直接获取中断号:platform_get_irq

在实际平台驱动中,最常用的还是 platform_get_irq,它封装了 OF 解析过程,使用更简单。

int platform_get_irq(struct platform_device *pdev, unsigned int num);

示例:

int irq = platform_get_irq(pdev, 0);
if (irq < 0) {
    dev_err(&pdev->dev, "Failed to get irq\n");
    return irq;
}
6. 获取设备树中的其他属性

除了中断,设备树节点中还可能包含自定义属性,可以通过 of_property_read_u32 等函数读取。

int ret;
u32 my_value;
ret = of_property_read_u32(mydevice_node, "my-property", &my_value);
if (ret) {
    // 属性不存在或读取失败
}

五、完整的平台驱动与 OF 协同示例

下面是一个完整的平台驱动示例,它利用设备树节点描述中断信息,并在 probe 中获取该中断号:

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_irq.h>

static int my_platform_probe(struct platform_device *pdev)
{
    struct device_node *np = pdev->dev.of_node;
    int irq;

    // 方式1:通过 of_irq_get 获取中断
    irq = of_irq_get(np, 0);
    if (irq < 0) {
        dev_err(&pdev->dev, "of_irq_get failed: %d\n", irq);
        return irq;
    }
    dev_info(&pdev->dev, "of_irq_get: irq = %d\n", irq);

    // 方式2:通过 platform_get_irq 获取(更推荐)
    irq = platform_get_irq(pdev, 0);
    if (irq < 0) {
        dev_err(&pdev->dev, "platform_get_irq failed\n");
        return irq;
    }
    dev_info(&pdev->dev, "platform_get_irq: irq = %d\n", irq);

    // 其他初始化...

    return 0;
}

static int my_platform_remove(struct platform_device *pdev)
{
    // 清理资源
    return 0;
}

static const struct of_device_id my_of_match[] = {
    { .compatible = "my_devicetree_irq" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_of_match);

static struct platform_driver my_platform_driver = {
    .probe  = my_platform_probe,
    .remove = my_platform_remove,
    .driver = {
        .name = "my_platform_device",
        .of_match_table = my_of_match,
    },
};

static int __init my_driver_init(void)
{
    return platform_driver_register(&my_platform_driver);
}
module_init(my_driver_init);

static void __exit my_driver_exit(void)
{
    platform_driver_unregister(&my_platform_driver);
}
module_exit(my_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");

第四部分:核心枢纽 —— 设备与驱动的配对机制 (Match)

设备与驱动的配对是 Platform 总线模型的核心。当系统添加一个新设备或一个新驱动时,总线都会尝试将两者匹配起来。匹配成功后,驱动中的 probe 函数将被调用,驱动开始接管设备。本部分将深入剖析配对机制的触发时机、匹配函数的实现逻辑、五级匹配优先级,并梳理从注册到 probe 的完整调用链路。


一、匹配触发时机:双向枚举机制

在内核中,设备与驱动的配对是双向的

  1. 设备注册时:当通过 platform_device_register 添加一个新设备时,内核会检查总线上已有的驱动,看是否有驱动与该设备匹配。
  2. 驱动注册时:当通过 platform_driver_register 添加一个新驱动时,内核会检查总线上已有的设备,看是否有设备与该驱动匹配。

无论是哪一方先注册,最终都会触发匹配过程,确保已存在的设备和驱动能够及时配对。

设备注册时的触发路径
platform_device_register
  └── platform_device_add
      └── device_add
          └── bus_probe_device
              └── device_initial_probe
                  └── __device_attach
                      └── bus_for_each_drv(..., __device_attach_driver, ...)
                          └── __device_attach_driver
                              ├── driver_match_device  // 判断是否匹配
                              └── driver_probe_device  // 匹配则探测
驱动注册时的触发路径
platform_driver_register
  └── __platform_driver_register
      └── driver_register
          └── bus_add_driver
              └── driver_attach
                  └── bus_for_each_dev(..., __driver_attach, ...)
                      └── __driver_attach
                          ├── driver_match_device  // 判断是否匹配
                          └── driver_probe_device  // 匹配则探测

二、匹配函数解析:platform_match 的底层实现

Platform 总线的匹配函数是 platform_match,它定义在 drivers/base/platform.c 中。该函数实现了五级匹配优先级,按顺序依次尝试,只要其中一级匹配成功,就返回 1(匹配),否则返回 0。

static int platform_match(struct device *dev, struct device_driver *drv)
{
    struct platform_device *pdev = to_platform_device(dev);
    struct platform_driver *pdrv = to_platform_driver(drv);

    // 1. 强制匹配(driver_override)
    if (pdev->driver_override)
        return !strcmp(pdev->driver_override, drv->name);

    // 2. 设备树匹配(OF style)
    if (of_driver_match_device(dev, drv))
        return 1;

    // 3. ACPI 匹配(ACPI style)
    if (acpi_driver_match_device(dev, drv))
        return 1;

    // 4. ID 表匹配(id_table)
    if (pdrv->id_table)
        return platform_match_id(pdrv->id_table, pdev) != NULL;

    // 5. 名称匹配(name)
    return (strcmp(pdev->name, drv->name) == 0);
}
优先级详解
  1. 强制匹配(driver_override)
    • 最高优先级。如果设备设置了 driver_override 字段(例如通过 sysfs 写入驱动名称),则只与该名称的驱动匹配。
    • 用途:强制设备绑定到某个特定驱动,忽略其他匹配规则。
  2. 设备树匹配(OF style)
    • 如果驱动提供了 of_match_table(通常通过 MODULE_DEVICE_TABLE(of, ...) 定义),则调用 of_driver_match_device
    • 该函数会遍历驱动的 of_match_table,与设备树节点的 compatible 属性进行比较,匹配成功则返回 1。
    • 设备树匹配是目前最常用的方式。
  3. ACPI 匹配(ACPI style)
    • 如果系统启用了 ACPI,则尝试 ACPI 匹配规则,适用于 x86 等平台。
  4. ID 表匹配(id_table)
    • 如果驱动定义了 id_tablestruct platform_device_id 数组),则调用 platform_match_id 函数。
    • 该函数遍历 id_table,检查 id_table[i].name 是否与设备的 name 匹配。如果匹配成功,驱动可以通过 id_table[i].driver_data 获取设备相关的私有数据。
    • 这种机制允许一个驱动支持多个不同名称的设备(一对多)。
  5. 名称匹配(name)
    • 最传统的匹配方式。如果以上都没有匹配成功,则比较设备的 name 和驱动的 driver.name 是否相同。
    • 这是早期 Platform 驱动的主要匹配方式,现在仍被广泛支持。
设备树匹配的细节

of_driver_match_device 最终会调用 of_match_device,该函数遍历驱动的 of_match_table,与设备节点的 compatible 字符串进行比对。每个 compatible 字符串对应一个优先级分数,匹配分数最高的条目将被选中。

static inline int of_driver_match_device(struct device *dev,
                                         const struct device_driver *drv)
{
    return of_match_device(drv->of_match_table, dev) != NULL;
}

三、五重匹配优先级总结

优先级 匹配方式 触发条件 典型应用场景
1 driver_override 设备设置了 driver_override 强制绑定,调试或特殊需求
2 OF style (compatible) 驱动提供 of_match_table,设备有设备树节点 现代嵌入式开发,设备树描述硬件
3 ACPI style 系统启用 ACPI,设备有 ACPI 描述 x86 等服务器/PC 平台
4 ID table (id_table) 驱动定义了 id_table 一个驱动支持多个不同名称的设备
5 Name (name) 以上均未匹配 传统平台驱动,向后兼容

四、函数调用图谱:从注册到 probe 的完整链路

下面以设备注册为例,绘制完整的调用流程图,展示从 platform_device_register 到驱动 probe 的路径。

在这里插入图片描述

关键节点说明
  • __device_attach_driver:这是设备侧遍历驱动的回调函数。它先调用 driver_match_device(即 platform_match)判断是否匹配,若匹配则调用 driver_probe_device
  • driver_probe_device:负责调用驱动的 probe 函数。它会进行一些准备工作(如增加引用计数),然后调用 really_probe
  • really_probe:真正执行探测。它会优先调用设备所属总线的 probe 函数(即 platform_bus_typeprobe 成员),该函数实际上是 platform_drv_probe
  • platform_drv_probe:平台总线包装的探测函数。它内部会调用驱动开发者定义的 probe 函数,并处理电源域、时钟等公共操作。

五、匹配示例:设备树匹配 vs 名称匹配

示例 1:设备树匹配

设备树节点:

led_test {
    compatible = "fire,led_test";
    reg = <0xfd60004 0x4 0xfd6000c 0x4>;
};

驱动定义:

static const struct of_device_id led_ids[] = {
    { .compatible = "fire,led_test" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, led_ids);

static struct platform_driver led_driver = {
    .probe  = led_probe,
    .driver = {
        .name = "led_platform",
        .of_match_table = led_ids,
    },
};

匹配过程:platform_match 调用 of_driver_match_device,比较 led_ids 中的 compatible 与设备树节点中的 compatible,匹配成功。

示例 2:名称匹配(无设备树)

设备定义:

static struct platform_device led_pdev = {
    .name = "led_pdev",
    .id   = -1,
    .resource = led_resource,
    .num_resources = ARRAY_SIZE(led_resource),
};

驱动定义:

static struct platform_driver led_driver = {
    .probe  = led_probe,
    .driver = {
        .name = "led_pdev",   // 与设备 name 相同
    },
};

匹配过程:由于没有 driver_override、设备树匹配、ACPI、id_table,最后走到名称匹配,strcmp(pdev->name, drv->name) 返回 0,匹配成功。

示例 3:ID 表匹配(一对多)

驱动支持两个设备:

static const struct platform_device_id led_ids[] = {
    { .name = "led_v1", .driver_data = 1 },
    { .name = "led_v2", .driver_data = 2 },
    { },
};
MODULE_DEVICE_TABLE(platform, led_ids);

static struct platform_driver led_driver = {
    .probe    = led_probe,
    .id_table = led_ids,
    .driver   = {
        .name = "led",
    },
};

匹配过程:platform_match 发现驱动有 id_table,调用 platform_match_id 遍历 led_ids,若设备 name"led_v1""led_v2" 则匹配成功,并将 driver_data 存入 platform_device->id_entry,供 probe 使用。

第五部分:综合实战 —— Platform 驱动点亮 LED 完整流程

经过前面四个部分的理论学习,我们已经掌握了 Platform 总线的架构、设备与资源、驱动与设备树、配对机制。本部分将通过一个完整的 LED 控制实验,将理论付诸实践。我们将分别采用传统硬编码方式(无设备树)和现代化设备树方式两种方法,实现一个可通过用户空间程序控制的 LED 设备。通过对比,可以更深刻地理解 Platform 模型的灵活性和设备树的优势。


一、硬件背景与引脚分析

以某款开发板为例,点亮一个连接在 GPIO0_C7 引脚的 LED。该 LED 的阳极通过限流电阻接 3.3V,阴极接 GPIO0_C7。因此,GPIO 输出低电平时 LED 亮,输出高电平时 LED 灭。

根据芯片手册,GPIO0 的寄存器基地址为 0xFDD60000,寄存器偏移如下:

寄存器 偏移 作用
GPIO_SWPORTA_DR (数据寄存器) 0x0004 读写引脚电平(高16位为置位/清除控制)
GPIO_SWPORTA_DDR (方向寄存器) 0x000C 配置引脚方向(0=输入,1=输出)

GPIO0_C7 对应位 7(即引脚 7),同时需要设置对应的高 16 位(位 23)来置位或清除输出。


二、传统硬编码实战(无设备树)

在这种方式中,所有硬件信息(寄存器地址、引脚编号)都直接写在设备端 C 代码中。驱动端通过标准 API 获取这些资源。

2.1 定义设备资源 (platform_device)

首先创建设备端文件 led_pdev.c,在其中定义资源数组和平台设备结构体。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>

// 寄存器物理地址
#define GPIO0_BASE   0xFDD60000
#define GPIO0_DR     (GPIO0_BASE + 0x0004)   // 数据寄存器
#define GPIO0_DDR    (GPIO0_BASE + 0x000C)   // 方向寄存器

// 资源数组:分别描述数据寄存器和方向寄存器
static struct resource led_resource[] = {
    [0] = DEFINE_RES_MEM(GPIO0_DR, 4),   // 数据寄存器,大小4字节
    [1] = DEFINE_RES_MEM(GPIO0_DDR, 4),  // 方向寄存器,大小4字节
};

// 平台私有数据:存放引脚偏移(这里是7)
static unsigned int led_hwinfo[] = { 7 };

// 设备释放回调(必须实现)
static void led_release(struct device *dev)
{
    // 通常为空,仅用于满足内核要求
}

// 平台设备结构体
static struct platform_device led_pdev = {
    .name          = "led_pdev",          // 设备名称,用于匹配
    .id            = -1,                  // 无 ID
    .num_resources = ARRAY_SIZE(led_resource),
    .resource      = led_resource,
    .dev = {
        .release    = led_release,
        .platform_data = led_hwinfo,      // 私有数据:引脚偏移
    },
};

static __init int led_pdev_init(void)
{
    printk("led_pdev: registering device\n");
    return platform_device_register(&led_pdev);
}

static __exit void led_pdev_exit(void)
{
    printk("led_pdev: unregistering device\n");
    platform_device_unregister(&led_pdev);
}

module_init(led_pdev_init);
module_exit(led_pdev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedfire");
2.2 定义平台驱动 (platform_driver)

驱动端文件 led_pdrv.c,实现驱动结构体和回调函数。

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/io.h>
#include <linux/slab.h>

#define DEV_MAJOR   243
#define DEV_NAME    "led"

static struct class *led_class;

// 设备私有数据结构,保存映射后的虚拟地址和引脚信息
struct led_data {
    unsigned int led_pin;               // 引脚编号(如7)
    void __iomem *va_DDR;               // 方向寄存器虚拟地址
    void __iomem *va_DR;                // 数据寄存器虚拟地址
    struct cdev led_cdev;
};

// 字符设备操作函数
static int led_open(struct inode *inode, struct file *filp)
{
    struct led_data *cur = container_of(inode->i_cdev, struct led_data, led_cdev);
    unsigned int val;

    // 配置为输出模式
    val = readl(cur->va_DDR);
    val |= (1 << (cur->led_pin + 16));   // 高16位控制置位
    val |= (1 << cur->led_pin);          // 输出模式
    writel(val, cur->va_DDR);

    // 默认输出高电平(LED灭)
    val = readl(cur->va_DR);
    val |= (1 << (cur->led_pin + 16));   // 置位
    val |= (1 << cur->led_pin);
    writel(val, cur->va_DR);

    filp->private_data = cur;
    return 0;
}

static int led_release(struct inode *inode, struct file *filp)
{
    return 0;
}

static ssize_t led_write(struct file *filp, const char __user *buf,
                         size_t count, loff_t *ppos)
{
    struct led_data *cur = filp->private_data;
    unsigned char cmd;
    unsigned int val;

    if (copy_from_user(&cmd, buf, 1))
        return -EFAULT;

    val = readl(cur->va_DR);
    if (cmd == 0) {
        // 输出低电平:清除 bit7(引脚输出低,LED亮)
        val |= (1 << (cur->led_pin + 16));  // 高16位置位
        val &= ~(1 << cur->led_pin);        // 清除低16位
    } else {
        // 输出高电平:置位 bit7(LED灭)
        val |= (1 << (cur->led_pin + 16));
        val |= (1 << cur->led_pin);
    }
    writel(val, cur->va_DR);

    *ppos += count;
    return count;
}

static struct file_operations led_fops = {
    .owner   = THIS_MODULE,
    .open    = led_open,
    .release = led_release,
    .write   = led_write,
};

// 平台驱动 probe 函数
static int led_probe(struct platform_device *pdev)
{
    struct led_data *cur;
    struct resource *res_dr, *res_ddr;
    unsigned int *hw_info;
    dev_t devno;
    int ret;

    printk("led_probe: device matched\n");

    // 分配私有数据
    cur = devm_kzalloc(&pdev->dev, sizeof(*cur), GFP_KERNEL);
    if (!cur)
        return -ENOMEM;

    // 获取平台数据(引脚偏移)
    hw_info = dev_get_platdata(&pdev->dev);
    cur->led_pin = hw_info[0];

    // 获取资源
    res_dr = platform_get_resource(pdev, IORESOURCE_MEM, 0);  // 数据寄存器
    res_ddr = platform_get_resource(pdev, IORESOURCE_MEM, 1); // 方向寄存器

    // 映射物理地址到虚拟地址
    cur->va_DR = devm_ioremap(&pdev->dev, res_dr->start, resource_size(res_dr));
    cur->va_DDR = devm_ioremap(&pdev->dev, res_ddr->start, resource_size(res_ddr));
    if (!cur->va_DR || !cur->va_DDR) {
        dev_err(&pdev->dev, "ioremap failed\n");
        return -ENOMEM;
    }

    // 注册字符设备
    devno = MKDEV(DEV_MAJOR, pdev->id);
    ret = register_chrdev_region(devno, 1, "led_cdev");
    if (ret < 0) {
        dev_err(&pdev->dev, "register_chrdev_region failed\n");
        return ret;
    }

    cdev_init(&cur->led_cdev, &led_fops);
    ret = cdev_add(&cur->led_cdev, devno, 1);
    if (ret < 0) {
        unregister_chrdev_region(devno, 1);
        return ret;
    }

    // 创建设备节点 /dev/led
    device_create(led_class, NULL, devno, NULL, DEV_NAME);

    // 保存私有数据,供 remove 使用
    platform_set_drvdata(pdev, cur);

    return 0;
}

// 平台驱动 remove 函数
static int led_remove(struct platform_device *pdev)
{
    struct led_data *cur = platform_get_drvdata(pdev);
    dev_t devno = MKDEV(DEV_MAJOR, pdev->id);

    device_destroy(led_class, devno);
    cdev_del(&cur->led_cdev);
    unregister_chrdev_region(devno, 1);

    return 0;
}

// ID 表:支持一个设备
static struct platform_device_id led_ids[] = {
    { .name = "led_pdev" },   // 必须与设备 name 一致
    { }
};
MODULE_DEVICE_TABLE(platform, led_ids);

// 平台驱动结构体
static struct platform_driver led_driver = {
    .probe    = led_probe,
    .remove   = led_remove,
    .driver   = {
        .name = "led_pdev",          // 传统名称匹配
    },
    .id_table = led_ids,             // ID 表匹配
};

static __init int led_driver_init(void)
{
    led_class = class_create(THIS_MODULE, "test_leds");
    if (IS_ERR(led_class))
        return PTR_ERR(led_class);

    return platform_driver_register(&led_driver);
}

static __exit void led_driver_exit(void)
{
    platform_driver_unregister(&led_driver);
    class_destroy(led_class);
}

module_init(led_driver_init);
module_exit(led_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedfire");
2.3 测试应用程序
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    int fd;
    unsigned char cmd;

    if (argc != 2) {
        printf("Usage: %s <0|1>\n", argv[0]);
        printf("  0: LED on, 1: LED off\n");
        return -1;
    }

    fd = open("/dev/led", O_RDWR);
    if (fd < 0) {
        perror("open");
        return -1;
    }

    cmd = atoi(argv[1]);
    write(fd, &cmd, 1);
    close(fd);
    return 0;
}
2.4 传统方式的特点
  • 优点:无需设备树,简单直接,适合硬件资源固定的场景。
  • 缺点:硬件信息固化在 C 代码中,更换引脚或寄存器地址需要重新编译驱动,扩展性差。

三、现代化实战(基于设备树 DTS)

在设备树方式中,硬件资源(寄存器地址、引脚信息)在设备树文件中描述,驱动只需解析这些信息,实现与硬件无关的代码。

3.1 设备树节点编写

在开发板的设备树文件中添加如下节点:

/ {
    led_test: led_test {
        compatible = "fire,led_test";
        reg = <0x0 0xfdd60004 0x0 0x4>,   // 数据寄存器
              <0x0 0xfdd6000c 0x0 0x4>;   // 方向寄存器
        led-pin = <7>;                     // 自定义属性,引脚偏移
        status = "okay";
    };
};
3.2 平台驱动实现

驱动文件 led_driver.c,使用设备树匹配。

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/io.h>

#define DEV_MAJOR   243
#define DEV_NAME    "led"

static struct class *led_class;

struct led_data {
    unsigned int led_pin;
    void __iomem *va_DDR;
    void __iomem *va_DR;
    struct cdev led_cdev;
};

static int led_open(struct inode *inode, struct file *filp)
{
    struct led_data *cur = container_of(inode->i_cdev, struct led_data, led_cdev);
    unsigned int val;

    // 配置为输出模式
    val = readl(cur->va_DDR);
    val |= (1 << (cur->led_pin + 16));
    val |= (1 << cur->led_pin);
    writel(val, cur->va_DDR);

    // 默认输出高电平(LED灭)
    val = readl(cur->va_DR);
    val |= (1 << (cur->led_pin + 16));
    val |= (1 << cur->led_pin);
    writel(val, cur->va_DR);

    filp->private_data = cur;
    return 0;
}

static int led_release(struct inode *inode, struct file *filp)
{
    return 0;
}

static ssize_t led_write(struct file *filp, const char __user *buf,
                         size_t count, loff_t *ppos)
{
    struct led_data *cur = filp->private_data;
    unsigned char cmd;
    unsigned int val;

    if (copy_from_user(&cmd, buf, 1))
        return -EFAULT;

    val = readl(cur->va_DR);
    if (cmd == 0) {
        val |= (1 << (cur->led_pin + 16));
        val &= ~(1 << cur->led_pin);
    } else {
        val |= (1 << (cur->led_pin + 16));
        val |= (1 << cur->led_pin);
    }
    writel(val, cur->va_DR);

    *ppos += count;
    return count;
}

static struct file_operations led_fops = {
    .owner   = THIS_MODULE,
    .open    = led_open,
    .release = led_release,
    .write   = led_write,
};

static int led_probe(struct platform_device *pdev)
{
    struct device_node *np = pdev->dev.of_node;
    struct led_data *cur;
    struct resource *res;
    int ret;
    u32 pin;

    printk("led_probe: device matched\n");

    // 读取自定义属性 led-pin
    if (of_property_read_u32(np, "led-pin", &pin)) {
        dev_err(&pdev->dev, "failed to get led-pin property\n");
        return -EINVAL;
    }

    cur = devm_kzalloc(&pdev->dev, sizeof(*cur), GFP_KERNEL);
    if (!cur)
        return -ENOMEM;
    cur->led_pin = pin;

    // 获取数据寄存器资源(索引0)
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    cur->va_DR = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(cur->va_DR))
        return PTR_ERR(cur->va_DR);

    // 获取方向寄存器资源(索引1)
    res = platform_get_resource(pdev, IORESOURCE_MEM, 1);
    cur->va_DDR = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(cur->va_DDR))
        return PTR_ERR(cur->va_DDR);

    // 注册字符设备
    dev_t devno = MKDEV(DEV_MAJOR, pdev->id);
    ret = register_chrdev_region(devno, 1, "led_cdev");
    if (ret < 0) {
        dev_err(&pdev->dev, "register_chrdev_region failed\n");
        return ret;
    }

    cdev_init(&cur->led_cdev, &led_fops);
    ret = cdev_add(&cur->led_cdev, devno, 1);
    if (ret < 0) {
        unregister_chrdev_region(devno, 1);
        return ret;
    }

    device_create(led_class, NULL, devno, NULL, DEV_NAME);

    platform_set_drvdata(pdev, cur);
    return 0;
}

static int led_remove(struct platform_device *pdev)
{
    struct led_data *cur = platform_get_drvdata(pdev);
    dev_t devno = MKDEV(DEV_MAJOR, pdev->id);

    device_destroy(led_class, devno);
    cdev_del(&cur->led_cdev);
    unregister_chrdev_region(devno, 1);

    return 0;
}

static const struct of_device_id led_of_match[] = {
    { .compatible = "fire,led_test" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, led_of_match);

static struct platform_driver led_driver = {
    .probe    = led_probe,
    .remove   = led_remove,
    .driver   = {
        .name = "led_platform",
        .of_match_table = led_of_match,
    },
};

static __init int led_driver_init(void)
{
    led_class = class_create(THIS_MODULE, "test_leds");
    if (IS_ERR(led_class))
        return PTR_ERR(led_class);

    return platform_driver_register(&led_driver);
}

static __exit void led_driver_exit(void)
{
    platform_driver_unregister(&led_driver);
    class_destroy(led_class);
}

module_init(led_driver_init);
module_exit(led_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedfire");
3.3 编译与测试
  • 编译内核,确保设备树节点被包含。
  • 加载驱动模块(insmod led_driver.ko)。
  • /dev/ 下应出现 led 设备节点。
  • 运行测试程序:./test_app 0(LED 亮),./test_app 1(LED 灭)。
3.4 设备树方式的优势
  • 硬件描述与驱动分离:修改引脚或寄存器地址只需修改设备树,无需重新编译驱动。
  • 可移植性强:同一份驱动可适用于多个硬件平台。
  • 内核自动管理:平台设备由内核根据设备树自动创建,减少了手动编写设备端代码的工作。

四、两种方式对比与总结

方面 传统硬编码 设备树方式
硬件信息位置 设备端 C 代码 设备树 (.dts) 文件
匹配方式 ID 表或名称匹配 OF 匹配 (compatible)
资源获取 手动定义 resource 内核自动转换,驱动用 platform_get_resource
扩展性 差,需改代码 好,改 dts 即可
内核版本要求 任意 支持设备树的内核(ARM/ARM64 等)
开发复杂度 设备驱动两端代码 仅驱动端 + dts 修改

无论哪种方式,驱动中 probe 的核心任务是一致的:

  1. 获取硬件资源(地址、中断、私有数据等)。
  2. 将物理地址映射为虚拟地址(ioremapdevm_ioremap)。
  3. 初始化硬件(设置 GPIO 方向、默认状态等)。
  4. 注册字符设备(或其它类型设备),创建设备节点,供用户空间访问。

通过本实验,我们完整地走通了 Platform 总线模型下的驱动开发流程,从设备注册到驱动匹配,再到用户空间控制硬件,实现了理论与实践的统一。

附录:常见问题与调试技巧

  • 模块加载后 /dev 下没有设备节点:检查 class_createdevice_create 是否成功;查看 dmesg 输出。
  • GPIO 控制无效:检查寄存器地址是否正确,引脚位是否正确,方向寄存器是否已配置为输出。
  • 设备树节点未生效:确保 dts 文件已编译并替换;检查 compatible 字符串与驱动一致;使用 ls /sys/firmware/devicetree/base/ 查看节点是否存在。
  • 匹配失败:在驱动中打印 pdev->name 和驱动匹配条件,对照 platform_match 的优先级检查。

第六部分:全文总结

通过以上五个部分的学习,我们从理论到实践,完整地剖析了 Linux 内核中 Platform 总线的设计思想、核心机制以及具体应用。下面将各部分的重点进行汇总,形成一个系统的知识回顾。

一、Platform 虚拟总线的引入与基础架构

  • 总线设备驱动模型的演进:传统的驱动开发将硬件资源(寄存器地址、中断号等)硬编码在驱动代码中,导致代码难以扩展和维护。“总线-设备-驱动”模型将硬件描述与驱动逻辑分离,通过总线作为桥梁,实现了设备与驱动的解耦。
  • 虚拟总线的概念:嵌入式 SoC 内部集成了大量外设(如 GPIO、I2C 控制器等),它们没有依附于物理总线(如 PCI、USB)。Platform 总线作为虚拟总线,将这些外设统一管理,让它们也能享受设备驱动模型的便利。
  • 核心数据结构
    • platform_bus_type:定义总线的名称、匹配函数、电源管理操作等。
    • platform_device:描述平台设备,包含设备名、ID、资源数组、通用 device 结构体等。
    • platform_driver:描述平台驱动,包含 probe、remove 等核心回调,以及通用 driver 结构体。
  • 总线注册流程:内核启动时调用 platform_bus_init,通过 device_register 注册 platform_bus 设备(在 /sys/devices/platform 下可见),通过 bus_register 注册 platform_bus_type 总线类型(在 /sys/bus/platform 下可见)。后续的 platform_device 默认挂载在此总线之下,其父设备为 platform_bus

二、平台设备与硬件资源

  • platform_device 结构体:核心成员包括 name(匹配名称)、id(区分同类型设备)、dev(通用设备,必须实现 release)、num_resourcesresource(硬件资源描述)。
  • resource 结构体:通过 startendnameflags 描述一段资源(内存区域、中断等)。flags 分为资源类型(IORESOURCE_MEMIORESOURCE_IRQ 等)和属性标志(IORESOURCE_PREFETCH 等)。
  • 设备注册与注销
    • platform_device_register 最终调用 platform_device_add,设置父设备为 platform_bus、总线为 platform_bus_type,根据 id 生成设备名,插入资源到全局树,最后调用 device_add 触发匹配。
    • 注销时使用 platform_device_unregister,依次调用 platform_device_del(移除设备和资源)和 platform_device_put(释放引用)。
  • 资源获取 API
    • platform_get_resource 遍历 platform_device.resource 数组,按类型和索引返回资源指针。
    • platform_get_irq 是获取中断资源的快捷方式,直接返回中断号。

三、平台驱动与设备树协同

  • platform_driver 结构体:关键成员包括 probe(设备匹配后执行)、remove(设备移除时执行)、driver(通用驱动,包含 nameof_match_table)、id_table(ID 表,支持一对多匹配)。
  • 驱动注册流程
    • platform_driver_register 宏调用 __platform_driver_register,设置驱动的总线为 platform_bus_type,并用 platform_drv_probe 包装用户定义的 probe。
    • 最终调用 driver_register 将驱动加入内核,触发与已有设备的匹配。
  • 设备树(OF)函数
    • of_find_node_by_name / of_find_node_by_path:查找设备树节点。
    • irq_of_parse_and_map:解析节点中断信息并映射为 Linux 中断号。
    • of_irq_get:更安全的中断获取方式。
    • platform_get_irq:平台层封装,推荐使用。
  • 设备树匹配优势:驱动通过 of_match_table 中的 compatible 字符串与设备树节点匹配,实现了硬件描述与驱动逻辑的彻底分离,极大地提高了可移植性。

四、设备与驱动的配对机制

  • 双向枚举机制
    • 设备注册时,调用 bus_probe_device 遍历已有驱动。
    • 驱动注册时,调用 driver_attach 遍历已有设备。
    • 最终都通过 driver_match_device 调用总线的 match 函数进行匹配。
  • platform_match 五级优先级(由高到低):
    1. 强制匹配:设备 driver_override 字段与驱动名称比较。
    2. 设备树匹配:驱动 of_match_table 与设备树节点的 compatible 属性比较。
    3. ACPI 匹配:适用于 ACPI 平台。
    4. ID 表匹配:驱动 id_table 中的 name 与设备 name 比较(一对多)。
    5. 名称匹配:设备 name 与驱动 driver.name 比较(传统方式)。
  • 调用链路:从 device_add / driver_register 开始,经过 bus_for_each_* 遍历,最终通过 __device_attach_driver / __driver_attach 判断匹配,匹配成功后调用 driver_probe_devicereally_probeplatform_drv_probe → 驱动自定义的 probe

五、综合实战:Platform 驱动点亮 LED

  • 传统硬编码方式
    • 在设备端定义 resource 数组(用 DEFINE_RES_MEM 描述寄存器地址),并构造 platform_device(指定 name、资源、私有数据等)。
    • 在驱动端实现 probe,使用 platform_get_resource 获取资源,映射虚拟地址,初始化 GPIO,注册字符设备。
    • 匹配方式采用 id_table 或传统名称匹配。
    • 缺点:硬件信息固化,修改引脚或地址需重新编译。
  • 现代化设备树方式
    • 在设备树中添加节点,用 reg 属性描述寄存器地址,用自定义属性(如 led-pin)描述引脚偏移。
    • 驱动通过 of_match_table 指定 compatible 字符串,内核自动创建设备。
    • probe 中使用 of_property_read_u32 读取自定义属性,用 platform_get_resource 获取寄存器资源,其余流程与传统方式相同。
    • 优点:硬件描述与驱动分离,可移植性强。
  • 用户空间控制:通过字符设备驱动,实现 openwrite 等操作,用户程序通过写入 0 或 1 来控制 LED 亮灭。

六、整体知识体系

层次 核心内容 关键函数/结构体
总线层 Platform 总线抽象与注册 platform_bus_typeplatform_bus_initbus_register
设备层 平台设备与硬件资源描述 platform_deviceresourceplatform_device_registerplatform_get_resource
驱动层 平台驱动与设备树解析 platform_driverplatform_driver_registerof_match_table、OF 函数
匹配层 五级匹配优先级与 probe 触发 platform_matchdriver_match_devicedriver_probe_device
应用层 用户空间控制硬件 字符设备框架(cdevclassdevice_create)、file_operations
  1. ID 表匹配:驱动 id_table 中的 name 与设备 name 比较(一对多)。
  2. 名称匹配:设备 name 与驱动 driver.name 比较(传统方式)。
  • 调用链路:从 device_add / driver_register 开始,经过 bus_for_each_* 遍历,最终通过 __device_attach_driver / __driver_attach 判断匹配,匹配成功后调用 driver_probe_devicereally_probeplatform_drv_probe → 驱动自定义的 probe

五、综合实战:Platform 驱动点亮 LED

  • 传统硬编码方式
    • 在设备端定义 resource 数组(用 DEFINE_RES_MEM 描述寄存器地址),并构造 platform_device(指定 name、资源、私有数据等)。
    • 在驱动端实现 probe,使用 platform_get_resource 获取资源,映射虚拟地址,初始化 GPIO,注册字符设备。
    • 匹配方式采用 id_table 或传统名称匹配。
    • 缺点:硬件信息固化,修改引脚或地址需重新编译。
  • 现代化设备树方式
    • 在设备树中添加节点,用 reg 属性描述寄存器地址,用自定义属性(如 led-pin)描述引脚偏移。
    • 驱动通过 of_match_table 指定 compatible 字符串,内核自动创建设备。
    • probe 中使用 of_property_read_u32 读取自定义属性,用 platform_get_resource 获取寄存器资源,其余流程与传统方式相同。
    • 优点:硬件描述与驱动分离,可移植性强。
  • 用户空间控制:通过字符设备驱动,实现 openwrite 等操作,用户程序通过写入 0 或 1 来控制 LED 亮灭。

六、整体知识体系

层次 核心内容 关键函数/结构体
总线层 Platform 总线抽象与注册 platform_bus_typeplatform_bus_initbus_register
设备层 平台设备与硬件资源描述 platform_deviceresourceplatform_device_registerplatform_get_resource
驱动层 平台驱动与设备树解析 platform_driverplatform_driver_registerof_match_table、OF 函数
匹配层 五级匹配优先级与 probe 触发 platform_matchdriver_match_devicedriver_probe_device
应用层 用户空间控制硬件 字符设备框架(cdevclassdevice_create)、file_operations

通过这套知识体系,开发者可以清晰地理解 Platform 总线模型的工作机制,并能够独立完成从硬件描述到驱动编写、再到用户空间应用的全流程开发。无论是传统硬编码方式还是现代化的设备树方式,核心思想都是分离硬件描述与驱动逻辑,这正是 Linux 内核设备驱动模型的核心价值所在。

Logo

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

更多推荐