Linux驱动之platform 总线设备注册流程分析

平台总线platform 设备注册流程分析


前言

之前知识点:

  • 了解数据模型keyObject keyset;了解总线模型:设备、驱动、总线
  • 了解了 注册自己的总线并创建属性文件
  • 驱动总线bus 注册流程分析
  • platform平台总线注册流程分析
  • 在总线下注册设备及设备注册流程分析

这里 就开始分析platform 平台总线设备注册流程分析。 其实 就是以总线设备、总线驱动、总线为知识点展开理论知识,以platform 平台总线为例展开进一步讨论研究知识点,深化理解。

参考资料

设备模型基本框架-kobject-kset
驱动-设备模型kobject实现属性文件读写功能
驱动-设备模型kobject实现属性文件读写终篇
驱动-注册自己的总线并创建属性文件
驱动-总线bus注册流程分析
platform总线注册流程分析
在总线下注册设备及设备注册流程分析

Linux平台设备驱动

注册 platform 设备

以前platform 平台总线相关知识点:
驱动-平台总线-platform设备注册platform驱动注册篇
驱动-平台总线-probe

一、platform 总线设备注册

这个实验和基本知识点我们在 驱动-平台总线-platform设备注册platform驱动注册篇 了解过,这里我先把代码直接贴出来,之前知识点里面其实都有的。

platform 总线设备注册 实验

参考 驱动-平台总线-platform设备注册platform驱动注册篇 ,具体带阿米如下

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

#define MEM_START_ADDR 0xFDD60000
#define MEM_END_ADDR 0xFDD60004
#define IRQ_NUMBER 101

static struct resource my_resources[] = {
    {
        .start = MEM_START_ADDR, // 内存资源起始地址
        .end = MEM_END_ADDR,     // 内存资源结束地址
        .flags = IORESOURCE_MEM, // 标记为内存资源
    },
    {
        .start = IRQ_NUMBER,     // 中断资源号
        .end = IRQ_NUMBER,       // 中断资源号
        .flags = IORESOURCE_IRQ, // 标记为中断资源
    },
};

static void my_platform_device_release(struct device *dev)
{
    // 释放资源的回调函数
}

static struct platform_device my_platform_device = {
    .name = "my_platform_device",              // 设备名称
    .id = -1,                                  // 设备ID
    .num_resources = ARRAY_SIZE(my_resources), // 资源数量
    .resource = my_resources,                  // 资源数组
    .dev.release = my_platform_device_release, // 释放资源的回调函数
};

static int __init my_platform_device_init(void)
{
    int ret;

    ret = platform_device_register(&my_platform_device); // 注册平台设备
    if (ret)
    {
        printk(KERN_ERR "Failed to register platform device\n");
        return ret;
    }

    printk(KERN_INFO "Platform device registered\n");
    return 0;
}

static void __exit my_platform_device_exit(void)
{
    platform_device_unregister(&my_platform_device); // 注销平台设备
    printk(KERN_INFO "Platform device unregistered\n");
}

module_init(my_platform_device_init);
module_exit(my_platform_device_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("fangchen");

这里我们需要梳理的核心代码就是: ret = platform_device_register(&my_platform_device)

API分析- platform_device_register(&my_platform_device)

直接上源码,如下:

/**
 * platform_device_register - add a platform-level device
 * @pdev: platform device we're adding
 */
int platform_device_register(struct platform_device *pdev)
{
	device_initialize(&pdev->dev);
	arch_setup_pdev_archdata(pdev);
	return platform_device_add(pdev);
}
EXPORT_SYMBOL_GPL(platform_device_register);

如注释说明,这个方法就是把平台总线设备,添加到platform 总线上去

步骤 - device_initialize(&pdev->dev);

这个API 不就是 在总线下注册设备及设备注册流程分析 里面重点分析的 设备初始化部分内容嘛,这里暂不分析。

void device_initialize(struct device *dev)
{
    dev->kobj.kset = devices_kset;  // 设置设备所属的kset
    kobject_init(&dev->kobj, &device_ktype); // 初始化kobject
    INIT_LIST_HEAD(&dev->dma_pools); // 初始化DMA池链表
    mutex_init(&dev->mutex); // 初始化互斥锁
    lockdep_set_novalidate_class(&dev->mutex);
    spin_lock_init(&dev->devres_lock); // 初始化自旋锁
    INIT_LIST_HEAD(&dev->devres_head); // 初始化资源链表
    device_pm_init(dev); // 初始化电源管理
    set_dev_node(dev, -1); // 设置NUMA节点
}

二、核心步骤 platform_device_add

先上源码,供后面内容分析、理解:

// 参考 drivers/base/platform.c
int platform_device_add(struct platform_device *pdev)
{
    int i, ret = 0;
    
    // 1. 参数有效性检查
    if (!pdev)
        return -EINVAL;
    
    // 2. 设置设备的父设备(如果未指定,则默认为平台总线)
    if (!pdev->dev.parent)
        pdev->dev.parent = &platform_bus; // 默认父设备是平台总线:cite[3]:cite[5]
    
    // 3. 将设备绑定到平台总线类型
    pdev->dev.bus = &platform_bus_type; // 指定设备所属的总线类型:cite[3]:cite[5]
    
    // 4. 根据设备ID设置设备的全名
    if (pdev->id != -1)
        dev_set_name(&pdev->dev, "%s.%d", pdev->name, pdev->id); // 例如 "serial0"
    else
        dev_set_name(&pdev->dev, "%s", pdev->name); // 例如 "my-device":cite[3]:cite[5]
    
    // 5. 遍历并处理设备提供的所有资源(resources)
    for (i = 0; i < pdev->num_resources; i++) {
        struct resource *p, *r = &pdev->resource[i]; // 获取第i个资源
        
        // 如果资源没有名称,使用设备名作为资源名
        if (r->name == NULL)
            r->name = dev_name(&pdev->dev);
        
        // 为资源寻找合适的父资源树(iomem_resource 或 ioport_resource)
        p = r->parent;
        if (!p) {
            if (resource_type(r) == IORESOURCE_MEM)       // 内存资源
                p = &iomem_resource;
            else if (resource_type(r) == IORESOURCE_IO)   // I/O端口资源
                p = &ioport_resource;
            // 对于IRQ等资源,可能不需要父资源
        }
        
        // 将资源插入到全局资源树中,防止冲突
        if (p && insert_resource(p, r)) { // 插入资源失败
            printk(KERN_ERR "%s: failed to claim resource %d\n",
                   dev_name(&pdev->dev), i);
            ret = -EBUSY;
            goto failed; // 跳转到错误处理
        }
    }
    
    // 6. 最终通过设备模型的核心函数将设备添加到内核
    ret = device_add(&pdev->dev); // 这会创建sysfs条目,触发热插拔事件,并尝试绑定驱动:cite[3]:cite[4]:cite[5]
    if (ret == 0)
        return ret;
    
failed: // 错误处理路径
    // 逆向操作,释放之前已经申请的资源
    while (--i >= 0) {
        struct resource *r = &pdev->resource[i];
        unsigned long type = resource_type(r);
        
        if (type == IORESOURCE_MEM || type == IORESOURCE_IO)
            release_resource(r); // 释放资源
    }
    
    return ret;
}

核心功能

platform_device_add 主要完成以下工作:

  • 参数检查:确保传入的platform_device结构体有效。
  • 设置设备父总线和总线类型:若无指定父设备,则默认设置为平台总线(platform_bus),并将设备总线类型设置为platform_bus_type
  • 设备命名:根据设备ID(pdev->id)设置设备的名称。
  • 资源管理:将设备提供的资源(如内存、I/O、中断等)注册到内核全局资源树中,防止冲突并由内核统一管理。
  • 设备添加:最终通过device_add函数将设备添加到设备模型中,使其对用户空间和驱动程序可见。

函数执行流程

platform_device_add 函数的执行流程大致如下,你可以通过下面的流程图来快速了解其整体运作过程:
在这里插入图片描述
下面是流程中几个关键步骤的详细说明:
设置设备父总线和总线类型
    若设备的父设备(pdev->dev.parent)未指定,则将其父设备设置为全局变量platform_bus,并将设备的总线类型设置为platform_bus_type,这表明该设备挂载在平台总线上。

if (!pdev->dev.parent)
    pdev->dev.parent = &platform_bus;
pdev->dev.bus = &platform_bus_type;

我们看看platform_bus 是如何定义的,如下:

struct device platform_bus = {
	.init_name	= "platform",
};

哈哈,这说明,默认情况下就是绑定到:/system/bus/platorm 下面,当前文件作为父设备目录。

对于 platform_bus_type 理解,可以参考之前文章 驱动-注册自己的总线并创建属性文件
中的 bus_type 结构体详解 内容。

设备命名
根据设备ID(pdev->id)的不同,采用两种方式为设备命名:

  • 如果id不是-1,设备名称格式为"name.id"(例如"serial.0")。
  • 如果id是-1,则直接使用pdev->name作为设备名称(例如"my-device")。
    这个名称是驱动和设备匹配的关键依据之一。

这里首先看一下平台设备注册代码中的platform_device 结构体定义内容:

static struct platform_device my_platform_device = {
    .name = "my_platform_device",              // 设备名称
    .id = -1,                                  // 设备ID
    .num_resources = ARRAY_SIZE(my_resources), // 资源数量
    .resource = my_resources,                  // 资源数组
    .dev.release = my_platform_device_release, // 释放资源的回调函数
};

static int __init my_platform_device_init(void)
{
    int ret;

    ret = platform_device_register(&my_platform_device); // 注册平台设备

对于 platform_device 设备定义里面有个id ,设备id 定义。
源码里面是这样设计的,如下:

switch (pdev->id) {
	default:
		dev_set_name(&pdev->dev, "%s.%d", pdev->name,  pdev->id);
		break;
	case PLATFORM_DEVID_NONE:
		dev_set_name(&pdev->dev, "%s", pdev->name);
		break;
	case PLATFORM_DEVID_AUTO:
		/*
		 * Automatically allocated device ID. We mark it as such so
		 * that we remember it must be freed, and we append a suffix
		 * to avoid namespace collision with explicit IDs.
		 */
		ret = ida_simple_get(&platform_devid_ida, 0, 0, GFP_KERNEL);
		if (ret < 0)
			goto err_out;
		pdev->id = ret;
		pdev->id_auto = true;
		dev_set_name(&pdev->dev, "%s.%d.auto", pdev->name, pdev->id);
		break;
	}

那么实际结果,可以看看如下:
在这里插入图片描述

资源管理(核心步骤)

这是platform_device_add相较于通用device_add的特有且关键步骤3。

  • 函数遍历pdev->resource数组(资源数量由pdev->num_resources指定)。

  • 为每个资源(struct resource *r)设置名称(若未设置,则使用设备名称)。

  • 根据资源类型(flags,如IORESOURCE_MEM, IORESOURCE_IO, IORESOURCE_IRQ)为其指定父资源域(parent),例如iomem_resource用于IORESOURCE_MEM

  • 调用insert_resource(p, r)将资源插入内核的全局资源树。这一步确保了设备资源的唯一性,避免了资源冲突。如果插入失败(例如资源已被占用),则跳转到错误处理路径。

添加到设备模型
最终,通过调用device_add(&pdev->dev)将设备添加到设备模型层次结构中35。这会:

  • 将设备注册到内核的设备列表(kobject)。
  • sysfs中创建对应的设备入口点。
  • 触发内核尝试为该设备寻找并绑定匹配的驱动(如果驱动已注册,则会调用驱动的probe函数)。

对于添加逻辑可以参考:在总线下注册设备及设备注册流程分析 中的 device_add 添加设备到系统里面去 知识点

关键数据结构

要理解这些函数,需要了解它们操作的核心结构体: 之前 驱动-平台总线-platform设备注册platform驱动注册篇 里面描述过结构体:struct platform_device struct resource,这是核心结构体,方便理解api 和 代码业务分析的。
struct platform_device (在 include/linux/platform_device.h 中定义):

struct platform_device {
    const char      *name;       // 设备名称,用于与驱动匹配
    int             id;          // 实例ID(如同一类型设备多个实例),-1表示唯一
    struct device   dev;         // 内嵌的通用设备结构体,包含大量核心信息
    u32             num_resources; // 资源数量
    struct resource *resource;   // 指向资源数组的指针,描述设备使用的硬件资源(内存区域、中断号等):cite[1]:cite[2]
    // ... 其他字段如 archdata, of_node, driver_override 等
};

struct resource (在 include/linux/ioport.h 中定义):

struct resource {
    resource_size_t start;      // 资源的起始地址(物理地址、中断号)
    resource_size_t end;        // 资源的结束地址
    const char *name;           // 资源名称
    unsigned long flags;        // 资源类型和属性标志(如下)
    // ... 资源树链表指针
};

flags 的重要标志:

  • IORESOURCE_MEM:内存映射的资源。
  • IORESOURCE_IO:I/O端口映射的资源。
  • IORESOURCE_IRQ:中断资源。
  • IORESOURCE_DMA:DMA资源1。

要点与注意事项

设备与驱动的匹配:
     设备注册后,内核会在合适的时机(设备注册后或驱动注册后)通过平台总线 (platform_bus_type) 的匹配函数 (platform_match) 来尝试将设备与已注册的驱动进行匹配。匹配策略通常优先比较设备树兼容性 (of_driver_match_device),然后是ID表 (id_table),最后是设备名称和驱动名称的字符串比较 (strcmp(pdev->name, drv->name))。

注册顺序:
     虽然Linux设备模型在一定程度上能处理设备和驱动注册的先后顺序(后注册的一方会触发匹配),但通常推荐先注册设备 (platform_device_register),再注册驱动 (platform_driver_register)。这在某些初始化阶段或依赖资源的场景下更为可靠。

三、注册总线之前要先注册设备实例-platform_bus_init-device_register

之前我们了解 platform总线注册流程分析,里面为什么需要创建一个device,如下源码: 就是在注册平台总线的时候,为什么执行 device_register(&platform_bus); 创建设备一次。

int __init platform_bus_init(void)
{
	int error;

	early_platform_cleanup();

	error = device_register(&platform_bus);
	if (error) {
		put_device(&platform_bus);
		return error;
	}
	error =  bus_register(&platform_bus_type);
	if (error)
		device_unregister(&platform_bus);

	return error;
}

原因:
device_register(&platform_bus) 里面的 platform_bus 定义如下:

struct device platform_bus = {
	.init_name	= "platform",
};

那么执行的结果就是 创建了 /sys/devices/platform 目录,在 /sys/devices/ 目录下创建了platform目录。 继续看platform_device 注册流程源码:

// 参考 drivers/base/platform.c
int platform_device_add(struct platform_device *pdev)
{
    int i, ret = 0;
    
    // 1. 参数有效性检查
    if (!pdev)
        return -EINVAL;
    
    // 2. 设置设备的父设备(如果未指定,则默认为平台总线)
    if (!pdev->dev.parent)
        pdev->dev.parent = &platform_bus; // 默认父设备是平台总线    
这里要讲解的就是这个,如下:
pdev->dev.parent = &platform_bus; // 默认父设备是平台总线:cite[3]:cite[5]

所以所有的子设备都放在了 /sys/devices/platform 目录下作为子目录统一管理了。

总结

  • Linux驱动之platform 总线设备注册流程分析流程,对比总线下设备注册,思路其实都是一样的。 platform_device 只是作为一类来具体说明,典型应用而已。
  • 看数据结构,platform_device 里面有成员变量 struct device dev; platform_device_add 添加platform设备最终也是走到了device_add,所以平台平台总线设备其实就是总线设备的一次封装。无论结构体还是注册都是封装了一层而已。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

野火少年

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值