LV06-05-linux平台总线模型-01-平台总线简介

什么是平台总线模型?若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.2的64位版本
VMware® Workstation 17 Pro 17.6.0 build-24238078
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04
linux内核 linux-4.19.71(NXP官方提供)
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
nxp-imx/linux-imx/releases/tag/v4.19.71 NXP linux内核仓库tags中的v4.19.71
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)
Source Code https://elixir.bootlin.com/linux/latest/source linux kernel源码
kernel/git/stable/linux.git - Linux kernel stable tree linux kernel源码(官网,tag 4.19.71)
https://elixir.bootlin.com/u-boot/latest/source uboot源码

一、平台总线简介

其实吧前面学习总线的时候应该大概都了解了,平台总线其实就是前面总线中的一种虚拟总线,下面再来详细了解下吧。

1. 概述

还记得前面学习LED灯的时候的我们我们学习了如何将驱动进行分层和分离——《LV06-03-chrdev-09-点亮LED-02-LED驱动框架》。虽然对整个驱动进行了分层和分离,从一个文件拆成好几个文件,可以支持不同的板子,也可以写一些比较通用的代码,但是但是其实本质还是一样的,我们只要调用open()函数打开了相应的设备文件,就可以使用read()/write()函数, 通过 file_operations 这个文件操作接口来进行硬件的控制。这种驱动开发方式简单直观,但是从软件设计的角度看,却是一种十分糟糕的方式。

它有一个严重的问题,就是设备信息和驱动代码还是杂糅在一起,在我们驱动程序中各种硬件寄存器地址随处可见。 本质上,这种驱动开发方式与单片机的驱动开发并没有太大的区别,一旦硬件信息发生变化甚至设备已经不在了,就必须要修改驱动源码。 我们之前做的事情只不过是简单地给它套了一个文件操作接口的外壳。

Linux作为一个发展成熟、功能齐全、结构复杂的操作系统,它对于代码的可维护性、复用性非常看重。 如果它放任每位驱动开发人员任凭自己的个人喜好来进行驱动代码开发,最终必将导致内核充斥着大量冗余、无意义的驱动代码, 给linux内核的迭代开发带来巨大的维护成本。

2. 驱动的分层与分离

下面的这一小节笔记是参考正点原子的《I.MX6U 嵌入式 Linux 驱动开发指南 V1.6 —— 第五十四章 platform 设备驱动实验 》,个人觉得对理解并没有什么效果,反正我看完,画完图,写完笔记,反而还给我整懵了。感觉还是写的《LV06-03-chrdev-09-点亮LED-02-LED驱动框架》这篇笔记中驱动的分层和分离更容易理解点。

2.1 分离

再举个例子来帮助理解,假如现在有三个平台 A、B 和 C,这三个平台(这里的平台说的是 SOC)上都有 MPU6050 这 个 I2C 接口的六轴传感器,按照我们写裸机 I2C “驱动” 的时候的思路,先写好I2C通信协议的 “驱动”,就是下面的主机 ”驱动”,然后再编每个平台的 MPU6050 ”驱动”,这个MPU6050的 ”驱动” 调用I2C驱动来试下设备寄存器的读写。因此编写出来的最简单的驱动框架如图所示:

image-20250117155817698

可以看出,每种平台下都有一个主机驱动和设备驱动,主机驱动肯定是必须要的,毕竟不同的平台其 I2C 控制器不同。但是右侧的设备驱动就没必要每个平台都写一个, 因为不管对于哪个 SOC 来说,MPU6050 都是一样,通过 I2C 接口读写数据就行了,只需要一 个 MPU6050 的驱动程序即可。如果再来几个 I2C 设备,比如 AT24C02、FT5206(电容触摸屏) 等,如果按照图上图中的写法,那么设备端的驱动将会重复的编写好几次。

image-20250118092030824

显然在 Linux 驱 动程序中这种写法是不推荐的,最好的做法就是每个平台的 I2C 控制器都提供一个统一的接口 (也叫做主机驱动),每个设备的话也只提供一个驱动程序(设备驱动),每个设备通过统一的 I2C 接口驱动来访问,这样就可以大大简化驱动文件,比如上面中三种平台下的 MPU6050 驱动 框架就可以简化为下图所示:

image-20250118092559112

实际的 I2C 驱动设备肯定有很多种,不止 MPU6050 这一个,那么实际的驱动架构如图 :

image-20250118093120065

这个就是驱动的分隔,也就是将主机驱动和设备驱动分隔开来,比如 I2C、 SPI 等等都会采用驱动分隔的方式来简化驱动的开发。在实际的驱动开发中,一般 I2C 主机控制器驱动已经由半导体厂家编写好了,而设备驱动一般也由设备器件的厂家编写好了,我们只需要提供设备信息即可,比如 I2C 设备的话提供设备连接到了哪个 I2C 接口上, I2C 的速度是多少等等。相当于将设备信息从设备驱动中剥离开来,驱动使用标准方法去获取到设备信息(比如从设备树中获取到设备信息),然后根据获取到的设备信息来初始化设备。 这样就相当于驱动只负责驱动,设备只负责设备,想办法将两者进行匹配即可。这个就是 Linux 中的总线(bus)、驱动(driver)和设备(device)模型,也就是常说的驱动分离。总线就是驱动和设备信息的月老,负责给两者牵线搭桥 :

image-20250118093425670

当我们向系统注册一个驱动的时候,总线就会在右侧的设备中查找,看看有没有与之匹配的设备,如果有的话就将两者联系起来。同样的,当向系统中注册一个设备的时候,总线就会在左侧的驱动中查找看有没有与之匹配的设备,有的话也联系起来。

2.2 分层

应该听说过网络的 7 层模型,不同的层负责不同的内容。同样的, Linux 下的驱动往往也是分层的,分层的目的也是为了在不同的层处理不同的内容。以其他书籍或者资料常常使用到的input(输入子系统,后面会去学习),这里简单了解下驱动的分层。 input 子系统负责管理所有跟输入有关的驱动,包括键盘、鼠标、触摸等,最底层的就是设备原始驱动,负责获取输入设备的原始值,获取到的输入事件上报给 input 核心层。 input 核心层会处理各种 IO 模型,并且提供 file_operations 操作集合。我们在编写输入设备驱动的时候只需要处理好输入事件的上报即可,至于如何处理这些上报的输入事件那是上层去考虑的,我们不用管。可以看出借助分层模型可以极大的简化我们的驱动编写,对于驱动编写来说非常友好。

3. platform平台总线

物理总线:芯片与各个功能外设之间传送信息的公共通信干线,其中又包括数据总线、地址总线和控制总线,以此来传输各种通信时序。

驱动总线:负责管理设备和驱动。制定设备和驱动的匹配规则,一旦总线上注册了新的设备或者是新的驱动,总线将尝试为它们进行配对。

一般对于I2C、SPI、USB这些常见类型的物理总线来说,Linux内核会自动创建与之相应的驱动总线,因此I2C设备、SPI设备、 USB设备自然是注册挂载在相应的总线上。但是,实际项目开发中还有很多结构简单的设备,对它们进行控制并不需要特殊的时序。 它们也就没有相应的物理总线,比如led、rtc时钟、蜂鸣器、按键等等,Linux内核将不会为它们创建相应的驱动总线。

为了使这部分设备的驱动开发也能够遵循设备驱动模型,Linux内核引入了一种虚拟的总线——平台总线( platform bus )。 平台总线用于管理、挂载那些没有相应物理总线的设备,这些设备被称为平台设备( platform device ),对应的设备驱动则被称为平台驱动(platform driver) 。 平台设备驱动的核心依然是Linux设备驱动模型,平台设备使用platform_device结构体来进行表示,其继承了设备驱动模型中的device结构体。 而平台驱动使用platform_driver结构体来进行表示,其则是继承了设备驱动模型中的device_driver结构体。

设备、 平台总线、 驱动的关系如下图 :

image-20250118094019537

4. 平台总线的优势?

前面学习总线之前,编写的驱动程序将驱动和设备相关的内容放在一起, 但是当涉及到多个相同类型的设备时, 这种方法会引发一系列问题。

假设我们有一个硬件平台,该硬件平台上存在了 500 个模块, 这些模块都使用了 LED 灯。 如果我们使用杂项设备来编写驱动, 虽然相比字符设备, 杂项设备的代码量较少, 但我们仍旧需要编写 500 份类似的代码, 从而生成相应的设备节点, 以供上层应用在不同模块上控制 LED 灯。

编写 500 份重复的代码会带来两个问题。 首先, 会造成大量重复劳动。 其次, 代码的重用性较差。 如果我们需要将这些驱动从一个平台移植到另一个平台, 就需要逐个修改驱动代码,尽管只需修改与硬件相关的部分, 但仍旧是一个很大的工作量。而在引入了平台总线模型后, 这些问题就得到了很好地解决。 通过使用平台总线模型, 将设备驱动和平台设备进行了分离。 这样一来, 我们只需编写一份通用的驱动代码即可, 然后针对不同的平台设备进行配置, 这就大大减少了重复编写代码的工作量, 并提高了驱动代码的重用性。 当我们需要将驱动移植到不同的平台时, 只需对硬件相关的部分进行适配即可, 其他部分可以保持不变。

总的来说,优势如下:

(1) 设备与驱动的分离: 传统的设备驱动模型将设备和驱动代码合并在同一个文件中,导致代码冗余和可维护性差。 而平台总线模型将设备代码和驱动代码分离, 设备代码放在device.c 文件中, 驱动代码放在 driver.c 文件中。 这种分离使得设备和驱动的职责更加清晰, 提高了代码的可读性和可维护性。

(2) 提高代码的重用性: 平台总线模型使得相同类型的设备可以共享相同的驱动代码。例如, 在一个硬件平台上存在多个相同类型的设备, 传统的驱动模型需要为每个设备编写独立的驱动代码。 而使用平台总线模型, 只需编写一个通用的驱动代码, 然后为每个设备创建相应的 device.c 文件, 将设备特定的代码放在其中。 这样可以减少代码的重复性, 提高了代码的重用性和可维护性。

(3) 减少重复性代码: 在传统的设备驱动模型中, 如果有多个相同类型的设备存在, 就需要为每个设备编写独立的驱动代码。 而使用平台总线模型, 只需编写一个通用的驱动代码,然后为每个设备创建相应的 device.c 文件, 将设备特定的代码放在其中。 这样可以避免大量的重复性代码, 简化了驱动开发过程。

(4) 提高可移植性: 平台总线模型可以提高驱动的可移植性。 开发者可以编写适应平台总线的平台驱动程序, 从而支持特定的外设, 而无需依赖于特定的标准总线。 这使得驱动可以更容易地在不同的硬件平台之间进行移植和重用。

二、数据结构与API

这一部分我们来了解一下平台总线的数据结构和相关的一些API。platform总线也是总线,它是一种虚拟总线,和前面我们学习总线的时候自定义的总线是一样的,都是kobject、kset这些来组织的。

1. struct bus_type platform_bus_type

平台总线变量 platform_bus_type 的原型如下所示:

1
2
3
4
5
6
7
8
9
struct bus_type platform_bus_type = {
.name = "platform",
.dev_groups = platform_dev_groups,
.match = platform_match,
.uevent = platform_uevent,
.dma_configure = platform_dma_configure,
.pm = &platform_dev_pm_ops,
};
EXPORT_SYMBOL_GPL(platform_bus_type);

其中 platform_match 就是匹配函数。

2. platform_bus_init()

平台总线的驱动入口函数是 platform_bus_init()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
of_platform_register_reconfig_notifier();
return error;
}

可以看到里面就是调用了 bus_register() 函数来完成注册。

三、platform 总线注册流程

1.  platform_bus_init()函数分析

前面我们大概已经了解过总线注册的流程,接下来我们来看一下这个平台总线的注册流程。内核在初始化的过程中调用 platform_bus_init() 函数来初始化平台总线, 调用流程为 kernel_init_freeable()do_basic_setup()driver_init()platform_bus_init()。我们来看一下这个 platform_bus_init() 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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); // 注册失败, 注销平台总线设备
// 注册平台重新配置的通知器
of_platform_register_reconfig_notifier();
return error;
}

函数首先清空总线 early_platform_device_list()上的所有节点。 然后使用调用 device_register() 注册平台总线设备 platform_bus , 将platform_bus 结构体注册到设备子系统中。然后使用bus_register() 函数注册平台总线类型, 将 platform_bus_type 结构体注册到总线子系统中。

上面 device_register() 这一步操作有点没看懂,因为前面我们自定义总线的时候直接就注册总线了,这里后面有机会再深究原因吧。

2. 在/sys中的表现

platform作为一种虚拟总线,当然也就是存在于/sys/bus目录下了:

image-20250118102631085

platform目录中的文件如下:

image-20250118102543162

参考资料:

一张图掌握 Linux platform 平台设备驱动框架!【建议收藏】-CSDN博客