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类型,表示资源的终止信息,对于内存类的资源,就表示内存终止地址。namechar *类型,表示资源名字。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