Linux 设备模型和platform平台

设备模型

一、为什么要有 Linux 设备模型?

早年 Linux 驱动:

  • 各自为政,字符设备、块设备、网卡各玩各的
  • 没有统一层级,没法统一电源管理、休眠唤醒、热插拔
  • 没法在 /sys 下面看到设备拓扑

设备模型解决三件事:

  1. 把所有总线、设备、驱动、类 组织成一棵内核树
  2. 自动完成 设备 ↔ 驱动 匹配
  3. 导出到 sysfs(/sys),用户空间能看、能配置、能热插拔

二、设备模型核心 5 大核心结构体(背住就懂了)

从上到下层级:

复制代码
bus_type 总线
  ↓
device_driver 驱动
  ↓
device 设备
  ↓
class 设备类
  ↓
kobject 最底层基类(所有对象的祖宗)

1. struct kobject 内核对象

所有设备模型结构体的基类

  • 内核所有总线、设备、类、驱动,都继承 kobject
  • 自带引用计数、父子链表、sysfs 文件节点
  • 实现:生命周期管理、树形结构、sysfs 目录

理解:kobject 是万物之父

2. struct bus_type 总线类型

代表一条总线,比如:

  • platform 平台总线
  • I2C 总线
  • SPI 总线
  • PCIe 总线
  • USB 总线

总线做两件核心事:

  1. 维护两条链表:设备链表驱动链表
  2. 提供 match 匹配函数:拿设备和驱动比对,匹配上就调用 probe

3. struct device_driver 驱动基类

所有驱动的父类,平台驱动、I2C 驱动都继承它核心成员:

  • name:驱动名字
  • bus:属于哪条总线
  • probe /remove/shutdown 等回调

4. struct device 设备基类

所有硬件设备的父类

  • 挂在总线上
  • 有父设备、子设备链表
  • 设备树 node 对应到这里
  • 电源管理、属性文件都挂在这

5. struct class 设备类

按功能分类,不按总线分类例子:

  • class /sys/class/tty 串口类
  • class /sys/class/input 输入设备类
  • class /sys/class/net 网卡类

作用:同一类设备放一起,方便用户空间统一管理、创建设备节点。


三、核心工作流程(最重要,驱动天天在跑)

1. 注册顺序

  1. 内核先注册 总线 bus_type
  2. 驱动端:注册 device_driver 到对应总线
  3. 设备端:注册 device 到同一条总线

2. 设备和驱动匹配过程

  1. 总线遍历自己的设备链表驱动链表
  2. 调用总线的 match 函数比对:
    • 传统:名字字符串匹配
    • 设备树:compatible 字符串匹配
  3. 匹配成功 → 调用驱动的 probe() 函数
  4. probe 里做硬件初始化、申请资源、创建设备节点

你写的平台驱动、设备树驱动,全是这个流程。


四、sysfs 和 设备模型的关系

/sys 目录就是 设备模型在用户态的镜像 每一个 kobject 对应 /sys 下一个目录每一个属性文件对应内核里的 attribute

常见目录对应:

  • /sys/bus → 所有总线
  • /sys/devices → 真实设备树拓扑
  • /sys/class → 设备功能类
  • /sys/drivers → 已注册驱动

你之前代码里:

复制代码
class_create();
device_create();

本质就是:在内核设备模型里创建 class、device 对象,并在 /sys 生成对应目录文件


五、你学驱动一定会碰到的三层架构

1. 传统字符设备(早期)

只做:主次设备号 + file_operations不依赖完整设备模型,简陋,无总线、无匹配

2. 平台总线驱动(标准 Linux 驱动写法)

基于设备模型:

  • 挂在 platform 总线
  • platform_driver → device_driver
  • platform_device → device走标准 总线匹配 → probe 初始化

3. 设备树 DTS 驱动

设备树节点自动生成 platform_device驱动只需要写 compatible 匹配字符串内核自动帮你完成设备注册、总线匹配、调用 probe


六、极简层级结构图(一眼看懂)

复制代码
kobject (基类)
 ├─ bus_type 总线
 ├─ device_driver 驱动
 ├─ device 设备
 └─ class 设备类

总线(bus)
  ├─ 挂着一堆驱动(driver)
  └─ 挂着一堆设备(device)
       ↓
设备归属到 class 类
       ↓
在 /sys 生成目录、属性文件
       ↓
device_create 在 /dev 生成设备节点

Platform 平台总线

1. 主机驱动和外设驱动分离思想

在 Linux 设备驱动框架的设计中, 除了有分层设计实现以外, 还有分隔的思想。

假如现在有三个平台 A、 B 和 C,这三个平台上都有MPU6050 ,I2C 接口的六轴传感器

每种平台下都有一个主机驱动和设备驱动,主机驱动是必须要的,不同的平台其 I2C 控制器不同。但是设备驱动没必要每个平台都写一

个,因为不管对于不同的SOC ,MPU6050是一样,都是通过 I2C 接口读写数据,只需要一个 MPU6050 的驱动程序即可。

所以,每个平台的 I2C 控制器都提供一个统一的接口(也叫做主机驱动),每个设备的话也只提供一个驱动程序(设备驱动),每个设备通过统

一的 I2C接口驱动来访问,这样就可以大大简化驱动文件。

以上就是驱动的分隔,也就是将主机驱动和设备驱动分隔开来。比如 I2C、SPI会采用驱动分隔的方式来简化驱动的开发。在实际的驱动开

发中,一般 I2C 主机控制器驱动由半导体厂家编写好了,设备驱动一般由设备器件的厂家编写好了,开发人员只需要提供设备信息即可。

将设备信息从设备驱动中剥离开来,驱动使用标准方法去获取到设备信息 (比如从设备树中获取到设备信息),根据获取到的设备信息来初

始化设备。

驱动只负责驱动,设备只负责设备,总线将两者进行匹配。这就是Linux中的总线(bus)、驱动(driver)和设备(device)模型,即驱动分离。

当向系统注册一个驱动的时,总线会在设备中查找与之匹配的设备,如果有就将两者联系起来。同样的,当向系统中注册一个设备的时

候,总线就会在驱动中查找与之匹配的驱动,如果有也联系起来。 Linux 内核中大量的驱动程序都采用总线、驱动和设备模式。

2. Platform平台驱动模型

在 Linux 2.6 以后的设备驱动模型中, 需关心总线、 设备和驱动这 3 个实体, 总线将设备和驱动绑定。在系统每注册一个设备的时候,会寻找与之匹配的驱动;同样的,在系统每注册一个驱动的时候, 会寻找与之匹配的设备, 而匹配由总线完成。

Linux 设备和驱动通常都需要挂接在一种总线上, 对于本身依附于 PCI、 USB、 I2C、 SPI 等的设备而言, 这自然不是问题。但是在嵌入式系统里面, 在 SoC 系统中集成的独立外设控制器、 挂接在 SoC内存空间的外设等却不依附于此类总线。

基于这一背景, Linux 发明了一种虚拟的总线, 称为 platform 总线, 相应的设备称为 platform_device, 驱动称为 platform_driver。

通过这种方式实现了设备和驱动的分离, 增强设备驱动的可移植性。平台总线模型也称为 platform 总线模型,是 Linux 内核虚拟出来的一条总线, 不是真实的导线。

平台总线模型就是把原来的驱动C文件给分成了两个 C 文件,一个是 device.c, 一个是 driver.c 。把稳定不变的放在 driver.c 里面, 需要改变的就放在device.c 里面。

平台总线模型将设备代码和驱动代码分离, 将与硬件设备相关的都放到 device.c 文件里面,驱动部分代码放到 driver.c 文件里面。所以对

于大量的同类设备而言,只需要修改设备文件信息,驱动文件不用修改,可以提高代码的重用性,减少重复性代码。


三、Platform 三大核心结构体

1. 总线本身:struct bus_type platform_bus_type

内核早已提前定义好 ,你不用自己创建。位置:/sys/bus/platform

作用:

  • 维护两条链表:platform 设备链表、platform 驱动链表
  • 提供 match 匹配函数,负责设备和驱动配对
  • 匹配成功自动调用驱动的 probe

2. 设备端:struct platform_device

代表一个物理设备

  • 寄存器物理地址
  • 中断号
  • 设备名字、compatible 匹配字符串
  • 资源(内存、中断)

设备树 DTS 节点,内核会自动解析、生成 platform_device,不用自己写代码创建设备。

3. 驱动端:struct platform_driver

你自己写的驱动就是这个:

  • 匹配表(compatible 字符串)
  • probe:匹配成功就执行,做硬件初始化
  • remove:设备卸载时释放资源

四、Platform 完整工作流程(背下来就能写驱动)

方式一:传统方式(不使用设备树)

  1. 内核已有 platform 总线
  2. 开发者注册 platform_device 设备(手动填资源、名字)
  3. 开发者注册 platform_driver 驱动
  4. platform 总线拿 设备名 和 驱动名 比对
  5. 匹配成功 → 调用 probe()
  6. probe 里:映射地址、申请中断、创建设备节点、注册字符设备

方式二:设备树 DTS 方式(现在主流)

  1. 写 DTS 设备树节点,写 compatible = "xxx,yyy"
  2. 内核启动自动解析 DTS,生成 platform_device
  3. 驱动里 of_match_table 写一样的 compatible 字符串
  4. platform 总线按 compatible 字符串匹配
  5. 匹配成功 → 执行 probe
  6. probe 中从设备树读取寄存器、中断、引脚等资源

现在 ARM/ARM64 驱动 全部用设备树 + platform 总线


五、Platform 驱动关键函数关系

cpp 复制代码
platform_driver_register()  注册驱动
        ↓
总线匹配 match
        ↓
匹配成功 → 执行 probe()
        ↓
probe 里做:
    ioremap 寄存器映射
    request_irq 申请中断
    cdev 注册字符设备
    device_create 创建设备节点

卸载:
platform_driver_unregister() → 调用 remove()

六、最简 Platform 驱动代码(可直接编译运行)

platform_demo.c

cpp 复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>

// 匹配表:对应设备树 compatible
static const struct of_device_id my_of_match[] = {
    { .compatible = "my,platform_demo" },
    { /* 结束 */ }
};
MODULE_DEVICE_TABLE(of, my_of_match);

// probe:匹配成功进入
static int my_probe(struct platform_device *pdev)
{
    pr_info("platform probe 匹配成功\n");
    return 0;
}

// remove:设备移除
static int my_remove(struct platform_device *pdev)
{
    pr_info("platform remove\n");
    return 0;
}

// 注册 platform 驱动
static struct platform_driver my_platform_driver = {
    .probe  = my_probe,
    .remove = my_remove,
    .driver = {
        .name = "my_platform_drv",
        .of_match_table = my_of_match,
    },
};

static struct platform_device *my_platform_dev;

static int __init my_platform_init(void)
{
    int ret;

    ret = platform_driver_register(&my_platform_driver);
    if (ret) {
        pr_err("platform_driver_register failed: %d\n", ret);
        return ret;
    }

    /*
     * 注册一个同名 platform_device,便于在无设备树 overlay 的环境中
     * 也能触发 probe。
     */
    my_platform_dev = platform_device_register_simple("my_platform_drv", -1, NULL, 0);
    if (IS_ERR(my_platform_dev)) {
        ret = PTR_ERR(my_platform_dev);
        pr_err("platform_device_register_simple failed: %d\n", ret);
        platform_driver_unregister(&my_platform_driver);
        return ret;
    }

    pr_info("platform demo init done\n");
    return 0;
}

static void __exit my_platform_exit(void)
{
    platform_device_unregister(my_platform_dev);
    platform_driver_unregister(&my_platform_driver);
    pr_info("platform demo exit\n");
}

module_init(my_platform_init);
module_exit(my_platform_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Driver Study");

运行示例

1) 模块加载入口

  • module_init(my_platform_init) 指定加载时先执行 my_platform_init()
  • my_platform_init() 里做两步:
    1. platform_driver_register(&my_platform_driver):先把驱动注册到 platform 总线。
    2. platform_device_register_simple("my_platform_drv", ...):再注册一个同名设备,方便在没有设备树 overlay 时也能触发匹配。

2) 驱动匹配规则

  • 驱动结构在 my_platform_driver
    • .driver.name = "my_platform_drv"
    • .of_match_table = my_of_match(设备树 compatible = "my,platform_demo"
  • 因为你手动注册的 device 名字就是 "my_platform_drv",会立刻和驱动匹配成功,于是调用 my_probe()
  • my_probe() 当前只打印:platform probe 匹配成功
相关推荐
lzh200409191 小时前
深入理解进程:从PCB内核结构到写时拷贝的底层实战
linux·c++
Data_Journal1 小时前
如何使用cURL更改User Agent
大数据·服务器·前端·javascript·数据库
日取其半万世不竭2 小时前
Minecraft Java版社区服务器搭建教程(Linux,适合新手)
java·linux·服务器
时空自由民.2 小时前
蓝牙协议之GAP协议
linux·服务器·网络
byoass2 小时前
企业云盘与设计软件深度集成:AutoCAD/Revit/SolidWorks插件开发与API集成实战
服务器·网络·数据库·安全·oracle·云计算
leaves falling2 小时前
Linux 基础指令完全指南 —— 从入门到熟练
linux·运维·服务器
千百元3 小时前
FreeMove 文件夹转移工具
服务器
charlie1145141913 小时前
嵌入式Linux驱动开发——新字符设备驱动 API 概览
linux·运维·驱动开发
♛识尔如昼♛3 小时前
C 进阶(2) - 文件I/O
linux·文件i/o