LV10-10-platform-01-platform驱动基础
本文主要是platform——平台总线驱动基础知识的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
Windows | windows11 |
Ubuntu | Ubuntu16.04的64位版本 |
VMware® Workstation 16 Pro | 16.2.3 build-19376536 |
SecureCRT | Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日 |
Linux开发板 | 华清远见 底板: FS4412_DEV_V5 核心板: FS4412 V2 |
u-boot | 2013.01 |
点击查看本文参考资料
参考方向 | 参考原文 |
驱动开发指南 | i.MX6ULL Linux阿尔法开发板资料 |
华清远见课程 | 华清远见课程 |
platform驱动模型 | Linux platform驱动模型 - LinFeng-Learning - 博客园 (cnblogs.com) |
platform驱动模型详解 | platform驱动模型详解,以及基于platform驱动模型的led驱动 - biaohc - 博客园 (cnblogs.com) |
Linux Platform驱动模型(一) | Linux Platform驱动模型(一) _设备信息 - Abnor - 博客园 (cnblogs.com) |
Linux Platform驱动模型(二) | Linux Platform驱动模型(二) _驱动方法 - Abnor - 博客园 (cnblogs.com) |
点击查看相关文件下载
文件 | 下载链接 |
--- | --- |
一、platform
简介
在我们之前学习的字符设备驱动模型中,只要应用程序调用open()
打开了相应的设备文件,就可以使用ioctl
通过驱动程序来控制我们的硬件,这种模型很直观,但是从软件设计的角度看,却是一种十分糟糕的方式,它有一个致命的问题,就是设备信息和驱动代码冗余在一起,一旦硬件信息发生改变甚至设备已经不在了,就必须要修改驱动源码,非常的麻烦。
为了解决这种驱动代码和设备信息耦合的问题,Linux
提出了platform bus
(平台总线)的概念,即使用虚拟总线将设备信息和驱动程序进行分离,设备树的提出就是进一步深化这种思想,将设备信息进行更好的整理。
在Linux2.6
以后的设备驱动模型,Linux
内核中的总线主要负责管理挂接在该总线下的设备与驱动,平台总线会维护两条链表,分别管理设备和驱动,将设备信息与驱动程序分类管理,可以提高驱动程序的可移植性。在系统每注册一个设备的时候,会寻找与之匹配的驱动;相同地,在系统每注册一个驱动的时候,会寻找与之匹配的设备,而匹配由总线完成。 相对于USB
、PCI
、I2C
、SPI
等物理总线来说,platform
总线是一种虚拟、抽象出来的总线,实际中并不存在这样的总线。 这种虚拟的总线仅用来管理设备和驱动(最核心的作用之一就是完成设备和驱动的匹配)。
设备主要提供硬件资源,驱动主要气功驱动代码,而总线则是完成设备和驱动匹配的媒介。
二、platform
总线理解
上边仅仅是做了一个简介,我感觉并不能很好的帮助我理解platform
平台总线模型。这一部分笔记是来自于正点原子的驱动开发手册,详细的可以查看手册原文,这里做一个笔记,加深自己的理解。
1. 驱动的分隔与分离
对于 Linux
这样一个成熟、庞大、复杂的操作系统,代码的重用性非常重要,否则的话就会在 Linux
内核中存在大量无意义的重复代码。尤其是驱动程序,因为驱动程序占用了 Linux
内核代码量的大头,如果不对驱动程序加以管理,任由重复的代码肆意增加,那么用不了多久Linux
内核的文件数量就庞大到无法接受的地步。
假如现在有三个平台 A
、 B
和 C
,这三个平台(这里的平台说的是 SOC
)上都有MPU6050
这个I2C
接口的六轴传感器,按照我们写裸机I2C
驱动的时候的思路,每个平台都有一个MPU6050
的驱动,因此编写出来的最简单的驱动框架如下图:
可以看出,每种平台下都有一个主机驱动和设备驱动,主机驱动肯定是必须要的,毕竟不同的平台其 I2C
控制器不同。但是右侧的设备驱动就没必要每个平台都写一个,因为不管对于那个 SOC
来说, MPU6050
都是一样,通过 I2C
接口读写数据就行了,只需要一个MPU6050
的驱动程序即可。
如果再来几个 I2C
设备,比如 AT24C02
、 FT5206
(电容触摸屏)等,若都按照上图的写法,那么设备端的驱动将会重复的编写好几次。显然在 Linux
驱动程序中这种写法是不推荐的,最好的做法就是每个平台的 I2C
控制器都提供一个统一的接口(也叫做主机驱动),每个设备的话也只提供一个驱动程序(设备驱动),每个设备通过统一的 I2C
接口驱动来访问,这样就可以大大简化驱动文件,比如A
、B
和C
三种平台下的 MPU6050
驱动框架就可以简化为下图的形式:
实际的 I2C
驱动设备肯定有很多种,不止 MPU6050
这一个,那么实际的驱动架构就如下图所示:
这个就是驱动的分隔,也就是将主机驱动和设备驱动分隔开来,比如I2C
、SPI
等等都会采用驱动分隔的方式来简化驱动的开发。在实际的驱动开发中,一般I2C
主机控制器驱动已经由半导体厂家编写好了,而设备驱动一般也由设备器件的厂家编写好了,我们只需要提供设备信息即可,比如I2C
设备的话提供设备连接到了哪个 I2C
接口上, I2C
的速度是多少等等。相当于将设备信息从设备驱动中剥离开来,驱动使用标准方法去获取到设备信息(比如从设备树中获取到设备信息),然后根据获取到的设备信息来初始化设备。 这样就相当于驱动只负责驱动,设备只负责设备,想办法将两者进行匹配即可。
2. 设备、驱动和总线
驱动进行分隔和分离后,在linux
中就形成了总线(bus
)、驱动(driver
)和设备(device
)模型,也就是常说的驱动分离。总线就是驱动和设备信息的月老,负责给两者牵线搭桥。所以linux
中的设备、驱动和总线的模式如下图:
当我们向系统注册一个驱动的时候,总线就会在右侧的设备中查找,看看有没有与之匹配的设备,如果有的话就将两者联系起来。同样的,当向系统中注册一个设备的时候,总线就会在左侧的驱动中查找看有没有与之匹配的设备,有的话也联系起来。 Linux
内核中大量的驱动程序都采用总线、驱动和设备模式。
3. 驱动的分层
Linux
下的驱动往往也是分层的,分层的目的也是为了在不同的层处理不同的内容。以常常使用到的input
(输入子系统,后边会学习到)为例,简单介绍一下驱动的分层。
input
子系统负责管理所有跟输入有关的驱动,包括键盘、鼠标、触摸等,最底层的就是设备原始驱动,负责获取输入设备的原始值,获取到的输入事件上报给 input
核心层。 input
核心层会处理各种 IO
模型,并且提供 file_operations
操作集合。我们在编写输入设备驱动的时候只需要处理好输入事件的上报即可,至于如何处理这些上报的输入事件那是上层去考虑的,我们不用关心。
可以看出借助分层模型可以极大的简化我们的驱动编写,对于驱动编写来说非常的友好。
4. platform
平台驱动模型
我们上边学习了设备驱动的分离,并且引出了总线(bus
)、驱动(driver
)和设备(device
)模型,比如 I2C
、 SPI
、 USB
等总线。但是在 SOC
中有些外设是没有总线这个概念的,但是又要使用总线、驱动和设备模型该怎么办呢?
为了解决此问题, Linux
提出了 platform
这个虚拟总线,相应的就有 platform_driver
和 platform_device
。
三、platform
总线机制
1. 在系统中的体现
platform
总线在根文件系统下的目录为/sys/bus/platform
,该目录下有两个子目录和相关的platform
属性文件:
devices
目录下存放的是platform
总线下的所有设备。drivers
目录下存放的是platform
总线下的所有驱动程序。
1 | ls /sys/bus/platform/ -lh |
2. 管理与匹配机制
platform
总线的驱动与设备的管理与匹配机制如下图所示:
四、基本数据类型
上边描述platform
总线的管理与匹配机制的时候出现了几种数据结构,这一部分主要是介绍一下这些重要的数据结构。
1. struct bus_type
Linux
系统内核使用bus_type
结构体表示总线,这个结构体定义在linux
内核源码的下边这个文件中:
1 | include/linux/device.h |
我们打开这个文件,找到struct bus_type
结构体如下:
点击查看源码中对该结构体的注释说明
1 | /** |
1 | struct bus_type { |
成员中match
指向的函数很重要,此函数指针指向的函数就是完成设备和驱动之间匹配的,总线就是使用match
函数来根据注册的设备来查找对应的驱动,或者根据注册的驱动来查找相应的设备,因此每一条总线都必须实现此函数。 match
函数有两个参数: dev
和 drv
,这两个参数分别为 device
和 device_driver
类型,也就是设备和驱动。
2. platform
总线相关结构体
2.1 总线定义
platform
总线是 bus_type
的一个具体实例,定义在linux
内核源码中的这个文件中:
1 | drivers/base/platform.c |
我们打开这个文件,可以看到 platform
总线定义如下:
1 | struct bus_type platform_bus_type = { |
其中,platform_match
就是匹配函数。
2.2 匹配函数
platform
匹配函数定义在linux
内核源码的下边这个文件中:
1 | drivers/base/platform.c |
我们打开这文件可以看到这个函数定义如下:
1 | /** |
从函数视实现中,我们可以看到,驱动和设备的匹配有四种方法 :
OF
类型的匹配,也就是设备树采用的匹配方式
of_driver_match_device
函数定义在linux
内核源码的下边这个文件中:
1 | include/linux/of_device.h |
device_driver
结构体(表示设备驱动)中有个名为of_match_table
的成员变量,此成员变量保存着驱动的compatible
匹配表,设备树中的每个设备节点的compatible
属性会和of_match_table
表中的所有成员比较,查看是否有相同的条目,如果有的话就表示设备和此驱动匹配,设备和驱动匹配成功以后 probe
函数就会执行。
ACPI
匹配方式
Advanced Configuration and Power Management Interface
高级配置和电源管理接口,是PC
机平台采用的一种硬件配置接口没怎么使用过,后续使用了再补充笔记。
id_table
匹配
每个 platform_driver
结构体有一个id_table
成员变量,它里边保存了很多id
信息。这些id
信息存放着这个 platform
驱动所支持的驱动类型。
- 名称匹配
这是第四种匹配方式,如果第三种匹配方式的 id_table
不存在的话就直接比较驱动和设备的 name
字段,看看是不是相等,如果相等的话就匹配成功。
对于支持设备树的 Linux
版本号,一般设备驱动为了兼容性都支持设备树和无设备树两种匹配方式。也就是第一种匹配方式一般都会存在,第三种和第四种只要存在一种就可以,一般用的最多的还是第四种,毕竟名称匹配是最简单的。
【匹配优先级】
从匹配函数中可以看出,不考虑第二种
ACPI
匹配方式的情况下,匹配的优先级为:设备树匹配 > ID匹配 > 名称匹配
3. platform
驱动相关结构体
3.1 struct platform_driver
platform_driver
结构体表示platform
驱动,这个结构体定义在linux
内核源码的这个文件中:
1 | include/linux/platform_device.h |
我们打开这个文件,可以看到这个结构体及其成员定义如下:
1 | struct platform_driver { |
【成员说明】
probe
:函数指针,指向一个probe
函数,当驱动与设备匹配成功以后probe
函数就会执行,一般驱动的提供者会编写,如果自己要编写一个全新的驱动,那么probe
就需要自行实现。remove
:函数指针,指向一个remove
函数,设备卸载了的时候会调用该函数。driver
:struct device_driver
类型(后边会介绍该结构体),Linux
内核里面大量使用到了面向对象的思维,device_driver
相当于基类,提供了最基础的驱动框架。plaform_driver
继承了这个基类,然后在此基础上又添加了一些特有的成员变量。内核里所有的驱动必须包含该结构体。id_table
:struct platform_device_id *
类型(后便会介绍该结构体),指向id_table
表,platform
总线匹配驱动和设备的时候采用的第三种方法,id_table
是个表(也就是数组),每个元素的类型为platform_device_id
。
【定义实例】
1 | /** 定义platform平台驱动 */ |
3.2 struct device_driver
device_driver
结构体定义在linux
内核源码的这个文件中:
1 | include/linux/device.h |
我们打开这个文件,可以看到这个结构体定义如下:
1 | struct device_driver { |
【成员说明】
name
:char *
类型,驱动名称,使用名称匹配的时候匹配device
用,最后一个成员是必须要初始化的。bus
:struct bus_type *
类型,总线类型。owner
:struct module *
类型,一般填THIS_MODULE
。of_match_table
:struct of_device_id *
类型(后边会介绍),用于设备树匹配of_match_ptr
(某struct of_device_id
对象地址) 。of_match_table
就是采用设备树的时候驱动使用的匹配表,同样是数组,每个匹配项都为of_device_id
结构体类型。
3.3 struct platform_device_id
在使用ID
匹配的时候,这个结构体类型在设备和驱动中都会用到。在驱动中,需要用该结构体对象定义一个结构体数组,而在驱动中,只需要定义一个变量即可。这个结构体定义在linux
内核源码的下边这个文件中:
1 | include/linux/mod_devicetable.h |
我们打开这个文件,会看到这个结构体定义如下:
1 |
|
【成员说明】
name
:char
类型,表示匹配用的名称(按我自己的理解这个名称就是匹配的时候用的ID
)。driver_data
:kernel_ulong_t
类型,需要向驱动传输的其它数据。
【注意事项】一般是定义在驱动中,定义的是一个结构体数组,一般不指定大小,初始化时最后加{}
表示数组结束,在设备程序中只需要定义一个变量即可。
【定义实例】
1 | /** |
3.4 struct of_device_id
上边提到of_match_table
就是采用设备树的时候驱动使用的匹配表,同样是数组,每个匹配项都为of_device_id
结构体类型,此结构体定义在linux
内核源码的这个文件中:
1 | include/linux/mod_devicetable.h |
我们打开这个文件,可以看到这结构体定义如下:
1 | /* |
【成员说明】
name
:char
类型数组,设备名。type
:char
类型数组,设备类型。compatible
:char
类型数组,对于设备树而言,通过设备节点的compatible
属性值和of_match_table
中每个项目的compatible
成员变量进行比较,如果有相等的就表示设备和此驱动匹配成功。
【注意事项】一般是定义在驱动中,定义的是一个结构体数组,一般不指定大小,初始化时最后加{}
表示数组结束。
【定义实例】
1 | /** |
4. platform
设备相关结构体
4.1 struct platform_device
platform_device
这个结构体表示 platform
设备,要注意,如果内核支持设备树的话就不要再使用platform_device
来描述设备了,因为改用设备树去描述了。当然了,如果一定要用platform_device
来描述设备信息的话也是可以的。这个结构体定义在linux
内核源码的这个文件中:
1 | include/linux/platform_device.h |
我们打开这个文件,可以看到结构体定义如下:
1 | struct platform_device { |
【成员说明】
name
:char *
类型, 表示设备名字,要和所使用的platform
驱动的name
字段相同,否则的话设备就无法匹配到对应的驱动。比如对应的platform
驱动的name
字段为xxx-gpio
,那么此name
字段也要设置为xxx-gpio
。 必须要进行初始化。id
:int
类型,设备id
,用于在该总线上同名的设备进行编号,如果只有一个设备,则为-1
。dev
:struct device
类型(后边会说明),设备模块必须包含该结构体。num_resources
:u32
类型,表示资源数量,一般为resource
成员(一般是一个数组)资源的个数 。resource
:struct resource *
类型(后边会说明),指向资源数组,表示资源,也就是设备信息,比如外设寄存器等。
【定义实例】
1 | /** 定义platform平台设备 */ |
4.2 struct device
device
结构体定义在linux
内核源码的这个文件中:
1 | include/linux/device.h |
我们打开这个文件,可以看到这个结构体定义如下(成员很多,这里只写了几个可能用到的):
1 | struct device { |
【成员说明】
bus
:struct bus_type *
类型,总线类型。devt
:dev_t
类型,表示设备号。driver
:struct device_driver *
类型,设备驱动。of_node
:struct device_node *
类型,驱动和设备匹配成功后,这里将指向匹配成功的设备在设备树中的节点省去获取节点的过程。release
:函数指针,指向一个删除设备函数。
【使用实例】
1 | int hello_driver_probe(struct platform_device *p_pltdev) |
4.3 struct resource
Linux
内核使用 resource
结构体表示资源 ,该结构体定义在linux
内核源码的这个文件中:
1 | include/linux/ioport.h |
我们打开这个文件,可以看到结构体定义如下:
1 | /* |
【成员说明】
start
:resource_size_t
类型, 表示资源的起始信息,对于内存类的资源,就表示内存起始地址。end
:resource_size_t
类型,表示资源的终止信息,对于内存类的资源,就表示内存终止地址。name
char *
类型,表示资源名字。flags
:unsigned long
类型,表示资源类型,可选的资源类型都定义在了文件include/linux/ioport.h
里面。我们常用的是IORESOURCE_MEM
、IORESOURCE_IRQ
这两种。start 和 end 的含义会随着 flags而变更。
flags
为IORESOURCE_MEM
时,start
、end
分别表示该platform_device
占据的内存的开始地址和结束值;注意不同MEM
的地址值不能重叠,重叠的话可能会报下边的错误:
1 | failed to claim resource 2/3/4/5 |
flags
为 IORESOURCE_IRQ
时,start
、end
分别表示该platform_device
使用的中断号的开始地址和结束值。
【定义实例】
1 | /** |
4.4 struct platform_device_id
在使用ID
匹配的时候,这个结构体类型在设备和驱动中都会用到。在驱动中,需要用该结构体对象定义一个结构体数组,而在驱动中,只需要定义一个变量即可。这个结构体定义在linux
内核源码的下边这个文件中:
1 | include/linux/mod_devicetable.h |
我们打开这个文件,会看到这个结构体定义如下:
1 |
|
【成员说明】
name
:char
类型,表示匹配用的名称(按我自己的理解这个名称就是匹配的时候用的ID
)。driver_data
:kernel_ulong_t
类型,需要向驱动传输的其它数据。
【注意事项】一般是定义在驱动中,定义的是一个结构体数组,一般不指定大小,初始化时最后加{}
表示数组结束,在设备程序中只需要定义一个变量即可。
【定义实例】
1 | /** |
五、基本函数
1. platform
驱动相关函数
1.1 platform_driver_register()
我们使用以下命令查询一下函数所在头文件:
1 | grep platform_driver_register -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】该函数向 Linux
内核注册一个 platform
驱动。
【函数参数】
driver
:struct platform_driver *
类型,要注册的platform
驱动。
【返回值】int
类型,成功返回0
,失败返回 一个负数,负数的绝对值表示错误码。
【使用格式】none
【注意事项】none
1.2 platform_driver_unregister()
我们使用以下命令查询一下函数所在头文件:
1 | grep platform_driver_unregister -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】该函数从 Linux
内核卸载一个 platform
驱动。
【函数参数】
drv
:struct platform_driver *
类型,要卸载的platform
驱动。
【返回值】none
【使用格式】none
【注意事项】none
1.3 platform_get_resource()
我们使用以下命令查询一下函数所在头文件:
1 | grep platform_get_resource -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于获取设备资源,用在platform
驱动中。
【函数参数】
dev
:struct platform_device *
类型,资源所在的设备。type
:unsigned int
类型,获取的资源类型。num
:unsigned int
类型,对应类型资源的序号(如第0
个MEM
、第2
个IRQ
等,不是数组下标,而是同类资源的序号)。
【返回值】struct resource *
类型,成功则返回资源结构体首地址,失败则返回NULL
。
【使用格式】
1 | /* 设备树节点部分 */ |
【注意事项】none
1.4 of_match_ptr()
我们使用以下命令查询一下函数所在头文件:
1 | grep of_match_ptr -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】该函数是一个宏,表示当使用设备树时,使用_ptr
进行匹配,否则其为空。
点击查看说明
在内核还没有引进设备树的时候,设备和驱动的匹配工作是由driver.name
或者id_table
来完成的,自从引进设备树之后(就有了CONFIG_OF
)这个宏,匹配规则中就多了of_match_table
这种通过compatible
方式来匹配的方式。同时为了兼容老的匹配方式,定义了宏of_match_ptr
。
- 当有设备树的时候(定义了
CONFIG_OF
),那么of_match_table
就有值。
1 | .of_match_table = of_match_ptr(adc_driver_dt_ids) |
宏替换之后就是
1 | .of_match_table = (adc_driver_dt_ids) |
这样就会使用设备树的方式进行匹配。
- 当没有设备树的时候(没有定义
CONFIG_OF
),那么of_match_table
就没值。
1 | .of_match_table = of_match_ptr(adc_driver_dt_ids) |
宏替换之后就是
1 | .of_match_table = NULL |
这样就就会使用传统方式匹配。
【函数参数】
_ptr
:struct of_device_id
类型的一个数组,表示设备树匹配的compatible
表。
【返回值】none
【使用格式】
1 | /** 定义匹配设备树数组 |
【注意事项】其实原本没用过这个宏,直接使用数组名就也完成了匹配,但是后便在编写一个驱动的时候,使用platform_get_resource
获从设备树获取资源的话,这里不用这个宏的话,在加载模块的时候就会报错,目前还没有仔细研究,所以这里先注意一下,后边搞懂了再补充。
2. platform
设备相关函数
2.1 platform_device_register()
我们使用以下命令查询一下函数所在头文件:
1 | grep platform_device_register -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】该函数设备信息注册到 Linux
内核中,把指定设备添加到内核中平台总线的设备列表,等待匹配,匹配成功则回调驱动中probe
指向的函数。
【函数参数】
pdev
:struct platform_device *
类型,要注册的platform
设备。
【返回值】int
类型,成功返回0
,失败返回 一个负数,负数的绝对值表示错误码。
【使用格式】none
【注意事项】none
2.2 platform_device_unregister()
我们使用以下命令查询一下函数所在头文件:
1 | grep platform_device_unregister -r -n ~/5linux/linux-3.14/include |
经过查找,我们可以得到如下信息:
1 | /* 需包含的头文件 */ |
【函数说明】该函数从 Linux
内核注销一个 platform
设备,如果驱动已匹配则回调驱动方法和设备信息中的release
指向的函数 。
【函数参数】
pdev
:struct platform_device *
类型,要注销的platform
设备。
【返回值】none
【使用格式】none
【注意事项】none