i.MX8MM处理器采用了先进的14LPCFinFET工艺,提供更快的速度和更高的电源效率;四核Cortex-A53,单核Cortex-M4,多达五个内核 ,主频高达1.8GHz,2G DDR4内存、8G EMMC存储。千兆工业级以太网、MIPI-DSI、USB HOST、WIFI/BT、4G模块、CAN、RS485等接口一应俱全。H264、VP8视频硬编码,H.264、H.265、VP8、VP9视频硬解码,并提供相关历程,支持8路PDM接口、5路SAI接口、2路Speaker。系统支持Android9.0(支持获取root限)Linux4.14.78+Qt5.10.1、Yocto、Ubuntu20、Debian9系统。适用于智能充电桩,物联网,工业控制,医疗,智能交通等,可用于任何通用工业和物联网应用、
【公众号】迅为电子
【粉丝群】258811263(加群获取驱动文档+例程)
第四十八章 Platform 设备驱动
本章导读
在前面的章节中我们了解了蜂鸣器等字符设备的驱动,但是当我们参考 Linux 内核源码中其他设备比较复杂的驱动时,并没有找到如此简单形式的驱动,在 Linux 内核中,提出了驱动的分离和分层这样的软件思路,在本章我们先来看一下最常用的 platform 设备驱动框架。
48.1章节讲解了Linux设备驱动分层和分离的思想
48.2章节讲解了Platform设备驱动模型,其下的三个小节分别讲解了Platform 设备,Platform 驱动,Platform总线
48.3章节我们完整的写了一个以蜂鸣器为代表的平台总线模型的驱动。其分为三个小章节,分别编写了device.c driver.c probe编写。并且测试可以控制蜂鸣器响灭。
本章内容对应视频讲解链接(在线观看):
平台总线模型介绍 → https://www.bilibili.com/video/BV1Vy4y1B7ta?p=19
注册Platform设备 → https://www.bilibili.com/video/BV1Vy4y1B7ta?p=20
注册platform驱动 → https://www.bilibili.com/video/BV1Vy4y1B7ta?p=21
平台总线probe函数编写 → https://www.bilibili.com/video/BV1Vy4y1B7ta?p=22
程序源码在网盘资料“iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\2.驱动程序例程\09-Platform设备驱动”路径下。
48.1 Linux设备驱动分层和分离
48.1.1 设备驱动的分层思想
在面向对象的程序设计中,可以为某一类相似的事物定义一个基类,而具体的事物可以继承这个基类
中的函数。如果对于继承的这个事物而言,其某函数的实现与基类一致,那它就可以直接继承基类的函数;相反,它可以重载之。这种面向对象的设计思想极大地提高了代码的可重用能力,是对现实世界事物间关
系的一种良好呈现。
Linux 内核完全由 C 语言和汇编语言写成,但是却频繁用到了面向对象的设计思想。在设备驱动方面,
往往为同类的设备设计了一个框架,而框架中的核心层则实现了该设备通用的一些功能。同样的,如果具
体的设备不想使用核心层的函数,它可以重载之。举个例子:
return_type core_funca(xxx_device * bottom_dev, param1_type param1, param1_type param2)
{
if (bottom_dev->funca)
return bottom_dev->funca(param1, param2);
/* 核心层通用的 funca 代码 */
...
}
上述 core_funca 的实现中,会检查底层设备是否重载了 funca(),如果重载了,就调用底层的代码,否
则,直接使用通用层的。这样做的好处是,核心层的代码可以处理绝大多数该类设备的 funca()对应的功能,只有少数特殊设备需要重新实现 funca()。
再看一个例子:
copyreturn_type core_funca(xxx_device * bottom_dev, param1_type param1, param1_type param2)
{
/*通用的步骤代码 A */
...
bottom_dev->funca_ops1();
/*通用的步骤代码 B */
...
bottom_dev->funca_ops2();
/*通用的步骤代码 C */
...
bottom_dev->funca_ops3();
}
上述代码假定为了实现 funca(),对于同类设备而言,操作流程一致,都要经过“通用代码 A、底层 ops1、
通用代码 B、底层 ops2、通用代码 C、底层 ops3”这几步,分层设计明显带来的好处是,对于通用代码 A、
B、C,具体的底层驱动不需要再实现,而仅仅只关心其底层的操作 ops1、ops2、ops3。这样的分层化设计
在 Linux 的 input、RTC、MTD、I2C、SPI、TTY、USB 等诸多设备驱动类型中屡见不鲜。
48.1.2 主机驱动和外设驱动分离思想
在 Linux 设备驱动框架的设计中,除了有分层设计实现以外,还有分隔的思想。举一个简单的例子,假设我们要通过 SPI 总线访问某外设,在这个访问过程中,要通过操作 CPU XXX 上的 SPI 控制器的寄存器来达到访问 SPI 外设 YYY 的目的,最简单的方法是:
copyreturn_type xxx_write_spi_yyy(...)
{
xxx_write_spi_host_ctrl_reg(ctrl);
xxx_ write_spi_host_data_reg(buf);
while(!(xxx_spi_host_status_reg()&SPI_DATA_TRANSFER_DONE));
...
}
如果按照这种方式来设计驱动,结果是对于任何一个 SPI 外设来讲,它的驱动代码都是 CPU 相关的。
也就是说,当然用在 CPU XXX 上的时候,它访问 XXX 的 SPI 主机控制寄存器,当用在 XXX1 的时候,它访问XXX1 的 SPI 主机控制寄存器:
return_type xxx1_write_spi_yyy(...)
{
xxx1_write_spi_host_ctrl_reg(ctrl);
xxx1_ write_spi_host_data_reg(buf);
while(!(xxx1_spi_host_status_reg()&SPI_DATA_TRANSFER_DONE));
...
}
这显然是不能接受的,因为这意味着外设 YYY 用在不同的 CPU XXX 和 XXX1 上的时候需要不同的驱动。那么,我们可以用如图的思想对主机控制器驱动和外设驱动进行分离。这样的结构是,外设 a、b、c 的驱动与主机控制器 A、B、C 的驱动不相关,主机控制器驱动不关心外设,而外设驱动也不关心主机,外设只是访问核心层的通用的 API 进行数据传输,主机和外设之间可以进行任意的组合。
如果我们不进行上图的主机和外设分离,外设 a、b、c 和主机 A、B、C 进行组合的时候,需要 9 个不同的驱动。设想一共有 m 个主机控制器,n 个外设,分离的结果是需要 m+n 个驱动,不分离则需要 m*n 个驱动。Linux SPI、I2C、USB、ASoC(ALSA SoC)等子系统都典型地利用了这种分离的设计思想。
48.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里面。
我们前几章内容给大家讲了杂项设备和字符设备驱动文件,这俩种驱动文件都有一个特点,它把驱动和设备都写在一个驱动文件里面了,但是如果相同的设备很多,就会出现一系列的问题。比如说我有一个硬件平台,这个硬件平台上有很多的模块,比如说有500个模块,这500个模块上都用到了led灯,如果说我用杂项设备来写,虽然用杂项设备比用字符设备的代码量要少,那么我是不是也要写500份这样的代码然后生成设备节点,供我们上层应用控制在不同模块上的led灯呢?那么写500份代码就带来两个问题,第一个是你写了大量重复性的代码。第二个是代码的重用性不是很好,这些驱动我从NXP的平台上移植到三星的平台上,那么要一个个改我们的驱动,但是实际上每个驱动你改的东西并不多,只改了相应的和硬件相关的部分。平台总线模型就很好地解决了这俩个问题。
(1)可以提高代码的重用性
(2)减少重复性代码。
如下图所示,平台总线模型将设备代码和驱动代码分离,将和硬件设备相关的都放到device.c文件里面,驱动部分代码都放到driver.c文件里面,那么500个led的驱动有重复的代码只要写一遍就可以了。
设备,平台总线,驱动的关系如下图所示:
48.2.1 Platform 设备
首先来看一下在 platform 平台模型下硬件设备信息如何表示和注册。
在 platform 平台下用 platform_device 这个结构体表示 platform 设备,如果内核支持设备树的话就不用使用 platform_device 来描述设备了,因为改用设备树去描述了 platform_device。具体定义在内核源码/include/linux/platform_device.h里面,结构体内容如下:
22 struct platform_device {
23 const char *name;
24 int id;
25 bool id_auto;
26 struct device dev;
27 u32 num_resources;
28 struct resource *resource;
29
30 const struct platform_device_id *id_entry;
31 char *driver_override; /* Driver name to force a match */
32
33 /* MFD cell pointer */
34 struct mfd_cell *mfd_cell;
35
36 /* arch specific additions */
37 struct pdev_archdata archdata;
38 };
第 23 行,platform 设备的名字,用来和 platform 驱动相匹配。名字相同才能匹配成功。
第 24 行,ID 是用来区分如果设备名字相同的时候(通过在后面添加一个数字来代表不同的设备,因为
有时候有这种需求)
第 25 行,内置的 device 结构体。
第 27 行,资源结构体数量。
第 28 行,指向一个资源结构体数组。一般包含设备信息。Linux 内核使用 resource
结构体表示资源,resource 结构体内容如下:
18 struct resource {
19 resource_size_t start;
20 resource_size_t end;
21 const char *name;
22 unsigned long flags;
23 struct resource *parent, *sibling, *child;
24 };
start 和 end 分别表示资源的起始和终止信息,对于内存类的资源,就表示内存起始和终止地址,name
表示资源名字,flags 表示资源类型,可选的资源类型都定义在了文件 include/linux/ioport.h 里面,如下所示:
29 #define IORESOURCE_BITS 0x000000ff /* Bus-specific bits */
30
31 #define IORESOURCE_TYPE_BITS 0x00001f00 /* Resource type */
32 #define IORESOURCE_IO 0x00000100 /* PCI/ISA I/O ports */
33 #define IORESOURCE_MEM 0x00000200
34 #define IORESOURCE_REG 0x00000300 /* Register offsets */
35 #define IORESOURCE_IRQ 0x00000400
36 #define IORESOURCE_DMA 0x00000800
37 #define IORESOURCE_BUS 0x00001000
......
104 /* PCI control bits. Shares IORESOURCE_BITS with above PCI ROM. */
105 #define IORESOURCE_PCI_FIXED (1<<4) /* Do not move resource */
第 30 行,用来进行与设备驱动匹配用的 id_table 表
第 37 行,添加自己的私有数据。
常用flags宏定义如下所示:
#define IORESOURCE_IO IO的内存
#define IORESOURCE_MEM 表述一段物理内存
#define IORESOURCE_IRQ 表示中断
在不支持设备树的 Linux 内核版本中需要在通过 platform_device 结构体来描述设备信息,然后使用
platform_device_register 函数将设备信息注册到 Linux 内核中,此函数原型如下所示:
int platform_device_register(struct platform_device *pdev)
如果内核支持设备树的话就不用使用 platform_device 来描述设备了,因为改用设备树去描述了 platform_device。
48.2.2 Platform 驱动
在 Linux 内核中,用 platform_driver 结构体表示platform 驱动,platform_driver结构体定义指定名称的平台设备驱动注册函数和平台设备驱动注销函数,此结构体定义在文件include/linux/platform_device.h 中,内容如下:
struct platform_driver {
/*当driver和device匹配成功的时候,就会执行probe函数*/
int (*probe)(struct platform_device *);
/*当driver和device任意一个remove的时候,就会执行这个函数*/
int (*remove)(struct platform_device *);
/*当设备收到shutdown命令的时候,就会执行这个函数*/
void (*shutdown)(struct platform_device *);
/*当设备收到suspend命令的时候,就会执行这个函数*/
int (*suspend)(struct platform_device *, pm_message_t state);
/*当设备收到resume命令的时候,就会执行这个函数*/
int (*resume)(struct platform_device *);
// 内置的device_driver 结构体
struct device_driver driver;
// 该设备驱动支持的设备的列表 他是通过这个指针去指向 platform_device_id 类型的数组
const struct platform_device_id *id_table;
bool prevent_deferred_probe;
};
id_table 表保存了很多 id 信息。这些 id 信息存放着这个 platform驱动所支持的驱动类型。id_table 是个表(也就是数组),每个元素的类型为 platform_device_id,platform_device_id 结构体内容如下:
struct platform_device_id {
char name[PLATFORM_NAME_SIZE];
kernel_ulong_t driver_data;
};
device_driver 结构体定义在 include/linux/device.h,device_driver 结构体内容如下:
1 struct device_driver {
2 const char *name;
3 struct bus_type *bus;
4
5 struct module *owner;
6 const char *mod_name; /* used for built-in modules */
7
8 bool suppress_bind_attrs; /* disables bind/unbind via sysfs */
9
10 const struct of_device_id *of_match_table;
11 const struct acpi_device_id *acpi_match_table;
12
13 int (*probe) (struct device *dev);
14 int (*remove) (struct device *dev);
15 void (*shutdown) (struct device *dev);
16 int (*suspend) (struct device *dev, pm_message_t state);
17 int (*resume) (struct device *dev);
18 const struct attribute_group **groups;
19
20 const struct dev_pm_ops *pm;
21
22 struct driver_private *p;
23 };
0 行,of_match_table 就是采用设备树的时候驱动使用的匹配表,同样是数组,每个匹配项都为
of_device_id 结构体类型,此结构体定义在文件 include/linux/mod_devicetable.h 中,内容如下:
struct of_device_id {
char name[32];
char type[32];
char compatible[128];
const void *data;
};
compatible 成员,在支持设备树的内核中,就是通过设备节点的 compatible 属性值和of_match_table 中每个项目的 compatible 成员变量进行比较,如果有相等的就表示设备和此驱动匹配成功。
在编写 platform 驱动的时候,首先定义一个 platform_driver 结构体变量,然后实现结构体中的各个
成员变量,重点是实现匹配方法以及 probe 函数。当驱动和设备匹配成功以后 probe 函数就会执行,驱动
程序具体功能的实现在 probe 函数里面编写。
platform 驱动的注册使用 platform_driver_register 函数来实现,函数原型如下:
int platform_driver_register (struct platform_driver *driver)
参数 driver 为预先创建的 platform_driver 结构体。
通过 platform_driver_unregister 函数来卸载 platform 驱动,函数原型如下:
void platform_driver_unregister(struct platform_driver *drv)
48.2.3 Platform 总线
48.2.3.1 Platform总线简介
前面讲了 platform 设备和 platform 驱动,这就相当于把设备和驱动分离了,那么他们要如何进行匹配呢,这就需要 platform 总线,前面的 platform 设备和 platform 驱动进行内核注册时,也都是注册到总线上。打个比方,就好比相亲,总线是红娘,设备是男方,驱动是女方。
a -- 红娘(总线)负责男方(设备)和女方(驱动)的撮合;
b -- 男方(女方)找到红娘,说我来登记一下,看有没有合适的姑娘(汉子)—— 设备或驱动的注册;
c -- 红娘这时候就需要看看有没有八字(二者的name 字段)匹配的姑娘(汉子)——match 函数进行匹配,看name是否相同;
d -- 如果八字不合,就告诉男方(女方)没有合适的对象,先等着 —— 设备和驱动会等待,直到匹配成功;
e -- 终于遇到八字匹配的了,那结婚呗,结完婚,男方就向女方交代,我有多少存款,我的房子在哪,钱放在哪等等( struct resource *resource),女方说好啊,于是去房子里拿钱,去给男方买菜啦,给自己买衣服、化妆品、首饰啊等等(int (*probe)(struct platform_device *) 匹配成功后驱动执行的第一个函数),当然如果男的跟小三跑了(设备卸载),女方也不会继续待下去的( int (*remove)(struct platform_device *))。
platform总线模型如下图所示:
当内核中有驱动注册时,总线就会在右侧的设备中查找,是否有匹配的设备,同样的,当有设备注册
到内核中时,也会在总线左侧查找是否有匹配的驱动。我们来看一下 platform 总线是如何定义的。
在 Linux 内核中使用 bus_type 结构体表示总线,此结构体定义在文件 include/linux/device.h,bus_type 结构体内容如下:
1 struct bus_type {
2 const char *name; /* 总线名字 */
3 const char *dev_name;
4 struct device *dev_root;
5 struct device_attribute *dev_attrs;
6 const struct attribute_group **bus_groups; /* 总线属性 */
7 const struct attribute_group **dev_groups; /* 设备属性 */
8 const struct attribute_group **drv_groups; /* 驱动属性 */
9
10 int (*match)(struct device *dev, struct device_driver *drv);
11 int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
12 int (*probe)(struct device *dev);
13 int (*remove)(struct device *dev);
14 void (*shutdown)(struct device *dev);
15
16 int (*online)(struct device *dev);
17 int (*offline)(struct device *dev);
18 int (*suspend)(struct device *dev, pm_message_t state);
19 int (*resume)(struct device *dev);
20 const struct dev_pm_ops *pm;
21 const struct iommu_ops *iommu_ops;
22 struct subsys_private *p;
23 struct lock_class_key lock_key;
24}
第 10 行,match 函数,此函数就是完成设备和驱动之间的匹配任务,,因此每一条总线都必须实现此函数。match 函数有两个参数:dev 和 drv,这两个参数分别为 device 和 device_driver 类型,也就是设备和驱动。
platform 总线是 bus_type 的一个具体实例,定义在文件 drivers/base/platform.c,platform 总线定义如下:
1 struct bus_type platform_bus_type = {
2 .name = "platform",
3 .dev_groups = platform_dev_groups,
4 .match = platform_match,
5 .uevent = platform_uevent,
6 .pm = &platform_dev_pm_ops,
7 };
其中 platform_bus_type 结构体实例就表示 platform 总线,其中重要的就是 platform_match 匹配函数,用来匹配注册到 platform 总线的设备和驱动。我们来看一下 platform 总线上的设备和驱动是如何匹配的,platform_match 函数定义在文件 drivers/base/platform.c 中,函数内容如下所示:
1 static int platform_match(struct device *dev, struct device_driver *drv)
2 {
3 struct platform_device *pdev = to_platform_device(dev);
4 struct platform_driver *pdrv = to_platform_driver(drv);
5
6 /*When driver_override is set,only bind to the matching driver*/
7 if (pdev->driver_override)
8 return !strcmp(pdev->driver_override, drv->name);
9
10 /* Attempt an OF style match first */
11 if (of_driver_match_device(dev, drv))
12 return 1;
13
14 /* Then try ACPI style match */
15 if (acpi_driver_match_device(dev, drv))
16 return 1;
17
18 /* Then try to match against the id table */
19 if (pdrv->id_table)
20 return platform_match_id(pdrv->id_table, pdev) != NULL;
21
22 /* fall-back to driver name match */
23 return (strcmp(pdev->name, drv->name) == 0);
24 }
从上面代码可以看出,platform 总线上设备和驱动的匹配方法一共有四种。
第 11、12 行,OF 类型的匹配,也是设备树采用的匹配方式,of_driver_match_device 函数定义在文件
include/linux/of_device.h 中。device_driver 结构体(表示设备驱动)中有个名为 of_match_table 的成员变量,
此成员变量保存着驱动的 compatible 匹配表,设备树中的每个设备节点的 compatible 属性会和
of_match_table 表中的所有成员比较,查看是否有相同的条目,如果有的话就表示设备和此驱动匹配,设
备和驱动匹配成功以后 probe 函数就会执行。第 15、16 行,ACPI 匹配方式。第 19、20 行,id_table 匹配方式,每个 platform_driver 结构体有一个 id_table 成员变量,顾名思义,保存了很多 id 信息。这些 id 信息存放着这个 platformd 驱动所支持的驱动类型。在上一小节 platform 驱动中也注意到了,在驱动程序中需要创建 platform_device_id 结构体,并指定驱动名称信息。第 23 行,如果 id_table 不存在的话,就直接比较驱动和设备的 name 字段,是不是相等,如果相等的话就匹配成功。
48.2.3.2 编写probe函数的思路
(1)从device.c里面获得硬件资源,因为我们的平台总线将驱动拆成了俩部分,第一部分是device.c,另一部分是driver.c。那么匹配成功了之后,driver.c要从device.c中获得硬件资源,那么driver.c就是在probe函数中获得的。
(2)获得硬件资源之后,就可以在probe函数中注册杂项/字符设备,完善file_operation结构体,并生成设备节点。
获得硬件资源有两种方法:
方法一: 直接获取,不推荐
int beep_probe(struct platform_device *pdev){
printk("beep_probe\n");
return 0;
}
beep_probe函数里面有一个形参pdev,他指向了platform_device,那么我们可以直接通过指针访问结构体platform_device的成员变量。比如说我要访问beep_device中的beep_res中的name,参考如下的代码
struct resource beep_res[] = {
[0] ={
.start = 0x020AC000,
.end = 0x020AC003,
.flags = IORESOURCE_MEM,
.name = "GPIO5_DR",
}
};
struct platform_device beep_device = {
.name = "beep_test",
.id = -1,
.resource=beep_res,
.num_resources =ARRAY_SIZE(beep_res),
.dev = {
.release = beep_release
}
};
那么probe函数可以直接获取,如下所示:
int beep_probe(struct platform_device *pdev){
printk("beep_probe\n");
printk("beep_res is %s\n",pdev->resource[0].name);
return 0;
}
方法二:使用函数来获取资源
我们也可以使用函数来获取资源,函数如下表所示:
函数 | extern struct resource *platform_get_resource(struct platform_device *,unsigned int, unsigned int); |
第一个 参数 | 平台设备名 |
第二个 参数 | 资源类型 |
第三个 参数 | 索引号,资源处在同类资源的哪个位置上,大家注意理解同类资源是指flags一模一样。 |
48.2.3.3 申请I/O内存
申请I/O内存使用request_region函数,其定义在内核源码/include/linux/ioport.h里面,如下图所示:
#define request_mem_region(start,n,name) __request_region(&iomem_resource, (start), (n), (name), 0)
第一个参数:起始地址,第二个参数:长度,第三个参数:名字
Linux把基于I/O映射方式的I/O端口和基于内存映射方式的I/O端口资源统称为“I/O区域”(I/O Region)。I/O Region仍然是一种I/O资源,因此它仍然可以用resource结构类型来描述。
Linux是以一种倒置的树形结构来管理每一类I/O资源(如:I/O端口、外设内存、DMA和IRQ)的。每一类I/O资源都对应有一颗倒置的资源树,树中的每一个节点都是一个resource结构,而树的根结点root则描述了该类资源的整个资源空间。其实说白了,request_mem_region函数并没有做实际性的映射工作,只是告诉内核要使用一块内存地址,声明占有,也方便内核管理这些资源。
48.3 实验程序
平台总线模型就是将我们之前写的驱动分成了两个部分,第一个部分就是我们device部分,还有一个部分是driver部分。device.c里面写的是硬件资源,这里硬件资源是指寄存器的地址,中断号,时钟等硬件资源。但是硬件资源不是指具体的硬件资源。比如说我要描述led的硬件资源,他描述的不是led灯,他描述的led灯使用了哪些管脚,这些管脚又涉及到了哪些寄存器。我们以iTOP-iMX8MM开发板为例,写一个基于platform平台驱动模型的蜂鸣器设备的驱动。
48.3.1 编写device.c
iTOP-i.MX8MM开发板是底板加核心板的结构,底板原理图在“8MM开发板\iTOP-i.MX8MM开发板\01-i.MX8MM开发板光盘资料\20210830\01-硬件资料\02-原理图\底板原理图”下载。iTOP-i.MX8MM开发板底板上默认的LED灯被使用了,所以我们可以找个空闲的gpio引脚连接led灯来进行实验。打开底板原理图找到U60 插槽,如下图所示:
我们将led小灯正极插在GPIO_IO13上,负极插在2号引脚接地,硬件连接如下图所示:
程序源码在网盘资料“iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\2.驱动程序例程\09-Platform设备驱动\09\001”路径下。
通过前面的学习,我们已经把基本概念搞懂了。我们在ubuntu 的/home/topeet/imx8mm/09/001目录下新建device.c文件。这里我们以iTOP-IMX8MM开发板为例,写一个LED设备的驱动,修改代码为如下所示:
/*
* @Author: topeet
* @Description: 基于平台设备模型的device.c
*/
#include <linux/init.h> //初始化头文件
#include <linux/module.h> //最基本的文件,支持动态添加和卸载模块。
#include <linux/platform_device.h> //平台设备所需要的头文件
/**
* @description: 释放 flatform 设备模块的时候此函数会执行
* @param {structdevice} *dev:要释放的设备
* @return {*}
*/
void led_release(struct device *dev)
{
printk("led_release \n");
}
// 设备资源信息,也就是LED所使用的所有寄存器
struct resource led_res[] = {
[0] = {
.start = 0x30200000,
.end = 0x30200003,
.flags = IORESOURCE_MEM,
.name = "GPIO1_IO13",
},
[1] = {
.start = 0x30200004,
.end = 0x30200007,
.flags = IORESOURCE_MEM,
.name = "GPIO1_IO13_GDIR",
},
};
// platform 设备结构体
struct platform_device led_device = {
.name = "led_test",
.id = -1,
.resource = led_res,
.num_resources = ARRAY_SIZE(led_res),
.dev = {
.release = led_release}};
/**
* @description: 设备模块加载
* @param {*}无
* @return {*}无
*/
static int device_init(void)
{
// 设备信息注册到 Linux 内核
platform_device_register(&led_device);
printk("platform_device_register ok \n");
return 0;
}
/**
* @description: 设备模块注销
* @param {*}无
* @return {*}无
*/
static void device_exit(void)
{
// 设备信息卸载
platform_device_unregister(&led_device);
printk("gooodbye! \n");
}
module_init(device_init);
module_exit(device_exit);
MODULE_LICENSE("GPL");
接下来将编写的device.c编译为驱动模块。我们将device.c文件拷贝到Ubuntu的/home/topeet/imx8mm/09/001目录下。将上次编译helloworld的Makefile文件和build.sh文件拷贝到device.c同级目录下,修改Makefile为:
obj-m += device.o
KDIR:=/home/topeet/linux/linux-imx
PWD?=$(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules ARCH=arm64
clean:
make -C $(KDIR) M=$(PWD) clean
文件如下图所示:
驱动编译成功如下图所示:
驱动编译完,我们通过nfs将编译好的驱动程序加载模块。
我们首先查看下开发板/sys/bus/platform/devices/下是否有平台设备led_test,输入以下命令:
ls /sys/bus/platform/devices/
如下图所示,没有平台设备led_test
再次输入以下命令查看,如下图所示,发现有平台设备led_test
ls /sys/bus/platform/devices/
我们可以输入以下命令卸载驱动,如下图所示:
rmmod device
48.3.2 编写 driver.c
程序源码在网盘资料“iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\2.驱动程序例程\09-Platform设备驱动\002”路径下。
通过48.2.2章节的学习,我们已经把基本概念搞懂了。我们在ubuntu 的/home/topeet/imx8mm/09/002目录下新建driver.c文件。我们可以将第一次编写的helloworld.c里面的代码拷贝到driver.c文件。这里我们以iTOP-iMX8MM开发板为例,写一个设备的驱动,修改代码为如下所示:
/*
* @Author:topeet
* @Description: 基于平台设备模型的driver.c
*/
#include <linux/init.h> //初始化头文件
#include <linux/module.h> //最基本的文件,支持动态添加和卸载模块。
#include <linux/platform_device.h> //平台设备所需要的头文件
/**
* @description: led_probe,驱动和设备匹配成功会进入此函数
* @param {structplatform_device} *pdev
* @return {*}
*/
int led_probe(struct platform_device *pdev)
{
printk("led_probe\n");
return 0;
}
/**
* @description: led_remove,当driver和device任意一个remove的时候,就会执行这个函数
* @param {structplatform_device} *pdev
* @return {*}
*/
int led_remove(struct platform_device *pdev)
{
printk("led_remove\n");
return 0;
}
// platform 驱动结构体
struct platform_driver led_driver = {
.probe = led_probe,
.remove = led_remove,
.driver = {
.owner = THIS_MODULE,
.name = "led_test"},
};
/**
* @description: 设备模块加载
* @param {*}无
* @return {*}无
*/
static int led_driver_init(void)
{
int ret = 0;
// platform驱动注册到 Linux 内核
ret = platform_driver_register(&led_driver);
if (ret < 0)
{
printk("platform_driver_register error \n");
}
printk("platform_driver_register ok \n");
return 0;
}
/**
* @description: 设备模块注销
* @param {*}无
* @return {*}无
*/
static void led_driver_exit(void)
{
// platform驱动卸载
platform_driver_unregister(&led_driver);
printk("goodbye! \n");
}
module_init(led_driver_init);
module_exit(led_driver_exit);
MODULE_LICENSE("GPL");
接下来我们将driver.c驱动编译成模块。将上次编译helloworld的Makefile文件和build.sh拷贝到driver.c同级目录下,修改Makefile为:
obj-m += driver.o
KDIR:=/home/topeet/linux/linux-imx
PWD?=$(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules ARCH=arm64
clean:
make -C $(KDIR) M=$(PWD) clean
驱动编译成功如下图所示:
驱动编译完,我们通过nfs将编译好的驱动程序加载模块。
我们进入共享目录,加载第48.3.2章节编译好的device.ko和本章节编译好的driver.ko,如下图所示:
insmod driver.ko
insmod device.ko
由上图可知打印信息可知,设备和驱动匹配成功进入probe函数。
我们将driver驱动模块卸载掉,修改一下driver.c代码, platform_driver结构体中const struct platform_device_id *id_table比device_driver结构体中的name的优先级要高,优先和id_table进行匹配,我们修改driver.c的代码来验证一下,修改后的driver.c如下所示,程序源码在网盘资料“iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\2.驱动程序例程\09-Platform设备驱动\09\003”路径下。
/*
* @Author:topeet
* @Description: 基于平台设备模型的driver.c
*/
#include <linux/init.h> //初始化头文件
#include <linux/module.h> //最基本的文件,支持动态添加和卸载模块。
#include <linux/platform_device.h> //平台设备所需要的头文件
/**
* @description: led_probe,驱动和设备匹配成功会进入此函数
* @param {structplatform_device} *pdev
* @return {*}
*/
int led_probe(struct platform_device *pdev)
{
printk("led_probe\n");
return 0;
}
/**
* @description: led_remove,当driver和device任意一个remove的时候,就会执行这个函数
* @param {structplatform_device} *pdev
* @return {*}
*/
int led_remove(struct platform_device *pdev)
{
printk("led_remove\n");
return 0;
}
//该设备驱动支持的设备的列表 ,他是通过这个指针去指向 platform_device_id 类型的数组
const struct platform_device_id led_idtable = {
.name = "123", //设备名字叫“123”
};
// platform 驱动结构体
struct platform_driver led_driver = {
.probe = led_probe,
.remove = led_remove,
.driver = {
.owner = THIS_MODULE,
.name = "led_test"
},
.id_table=&led_idtable};
/**
* @description: 设备模块加载
* @param {*}无
* @return {*}无
*/
static int led_driver_init(void)
{
int ret = 0;
// platform驱动注册到 Linux 内核
ret = platform_driver_register(&led_driver);
if (ret < 0)
{
printk("platform_driver_register error \n");
}
printk("platform_driver_register ok \n");
return 0;
}
/**
* @description: 设备模块注销
* @param {*}无
* @return {*}无
*/
static void led_driver_exit(void)
{
// platform驱动卸载
platform_driver_unregister(&led_driver);
printk("goodbye! \n");
}
module_init(led_driver_init);
module_exit(led_driver_exit);
MODULE_LICENSE("GPL");
修改完driver.c文件,编译通过后,加载驱动模块,如下图所示:
所以得出结论,设置了id_table后,优先和id_table进行匹配,因为id_table中name属性为“123”,和device.c中的name匹配不上,所以不会进入probe函数。那么如果我们将id_table中name属性设为和device.c中的name一样呢?我们修改driver.c文件试试看。程序源码在网盘资料“iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\2.驱动程序例程\09-Platform设备驱动\09\004”路径下。
/*
* @Author:topeet
* @Description: 基于平台设备模型的driver.c
*/
#include <linux/init.h> //初始化头文件
#include <linux/module.h> //最基本的文件,支持动态添加和卸载模块。
#include <linux/platform_device.h> //平台设备所需要的头文件
/**
* @description: led_probe,驱动和设备匹配成功会进入此函数
* @param {structplatform_device} *pdev
* @return {*}
*/
int led_probe(struct platform_device *pdev)
{
printk("led_probe\n");
return 0;
}
/**
* @description: led_remove,当driver和device任意一个remove的时候,就会执行这个函数
* @param {structplatform_device} *pdev
* @return {*}
*/
int led_remove(struct platform_device *pdev)
{
printk("led_remove\n");
return 0;
}
//该设备驱动支持的设备的列表 ,他是通过这个指针去指向 platform_device_id 类型的数组
const struct platform_device_id led_idtable = {
.name = "led_test", //设备名字叫“123”
};
// platform 驱动结构体
struct platform_driver led_driver = {
.probe = led_probe,
.remove = led_remove,
.driver = {
.owner = THIS_MODULE,
.name = "led_test"
},
.id_table=&led_idtable};
/**
* @description: 设备模块加载
* @param {*}无
* @return {*}无
*/
static int led_driver_init(void)
{
int ret = 0;
// platform驱动注册到 Linux 内核
ret = platform_driver_register(&led_driver);
if (ret < 0)
{
printk("platform_driver_register error \n");
}
printk("platform_driver_register ok \n");
return 0;
}
/**
* @description: 设备模块注销
* @param {*}无
* @return {*}无
*/
static void led_driver_exit(void)
{
// platform驱动卸载
platform_driver_unregister(&led_driver);
printk("goodbye! \n");
}
module_init(led_driver_init);
module_exit(led_driver_exit);
MODULE_LICENSE("GPL");
修改完driver.c文件,编译通过后,加载驱动模块,如下图所示:
由上图可知,driver.c中id_table中name属性设为和device.c中的name一样的话,就可以匹配进入probe函数了。不论我们先加载driver.ko还是device.ko都是一样的。
48.3.3 编写probe
程序源码在网盘资料“iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\2.驱动程序例程\09-Platform设备驱动\09\005”路径下。
通过48.2.3章节编写probe函数理论基础的学习,我们已经把基本概念搞懂了。我们在第48.3.2章driver.c的基础上继续编写probe函数。
/*
* @Author: topeet
* @Description: 基于平台设备模型的driver.c,在probe函数中获取硬件资源
*/
//初始化头文件
#include <linux/init.h>
//最基本的文件,支持动态添加和卸载模块。
#include <linux/module.h>
//平台设备所需要的头文件
#include <linux/platform_device.h>
#include <linux/ioport.h>
/*注册杂项设备头文件*/
#include <linux/miscdevice.h>
//文件系统头文件,定义文件表结构(file,buffer_head,m_inode等)
#include <linux/fs.h>
//包含了copy_to_user、copy_from_user等内核访问用户进程内存地址的函数定义。
#include <linux/uaccess.h>
//包含了ioremap、iowrite等内核访问IO内存等函数的定义。
#include <linux/io.h>
#define GPIO1_IO13 0x30200000 //led数据寄存器物理地址
#define GPIO1_IO13_GDIR 0x30200004 //led数据寄存器方向寄存器物理地址
unsigned int *vir_gpio1_io13; //存放映射完的虚拟地址的首地址
unsigned int *vir_gpio1_io13_gdir; //存放映射完的虚拟地址的首地址
int ret; //返回值
struct resource *led_mem;
struct resource *led_mem_gdir;
struct resource *led_mem_tmp;
/****************************************************************************************
* @brief led_probe : 与设备信息层(device.c)匹配成功后自动执行此函数,
* @param inode : 文件索引
* @param file : 文件
* @return 成功返回 0
****************************************************************************************/
int led_probe(struct platform_device *pdev)
{
printk("led_probe\n");
/*获取硬件资源方法一: 不推荐*/
//printk("led_res is %s\n",pdev->resource[0].name);
//return 0;
/*获取硬件资源方法二: 推荐*/
led_mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
led_mem_gdir = platform_get_resource(pdev, IORESOURCE_MEM, 1);
if (led_mem == NULL||led_mem_gdir == NULL)
{
printk("platform_get_resource is error\n");
return -EBUSY;
}
printk("led_res start is 0x%llx \n", led_mem->start);
printk("led_res end is 0x%llx \n", led_mem->end);
printk("led_res start is 0x%llx \n", led_mem_gdir->start);
printk("led_res end is 0x%llx \n", led_mem_gdir->end);
return 0;
}
int led_remove(struct platform_device *pdev)
{
printk("led_remove\n");
return 0;
}
const struct platform_device_id led_idtable = {
.name = "led_test",
};
// platform 驱动结构体
struct platform_driver led_driver = {
.probe = led_probe,
.remove = led_remove,
.driver = {
.owner = THIS_MODULE,
.name = "123"},
.id_table = &led_idtable
};
/**
* @description: 设备模块加载
* @param {*}无
* @return {*}无
*/
static int led_driver_init(void)
{
int ret = 0;
// platform驱动注册到 Linux 内核
ret = platform_driver_register(&led_driver);
if (ret < 0)
{
printk("platform_driver_register error \n");
}
printk("platform_driver_register ok \n");
return 0;
}
/**
* @description: 设备模块注销
* @param {*}无
* @return {*}无
*/
static void led_driver_exit(void)
{
platform_driver_unregister(&led_driver);
printk("gooodbye! \n");
}
module_init(led_driver_init);
module_exit(led_driver_exit);
MODULE_LICENSE("GPL");
接下来我们将driver.c驱动编译成模块,我们将driver.c文件拷贝到Ubuntu的/home/topeet/imx8mm/09/005目录下。将上次编译使用的Makefile文件和build.sh文件拷贝到driver.c同级目录下,修改Makefile为:
obj-m += driver.o
KDIR:=/home/topeet/linux/linux-imx
PWD?=$(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules ARCH=arm64
clean:
make -C $(KDIR) M=$(PWD) clean
我们通过nfs将编译好的驱动程序加载模块,我们进入共享目录,加载第48.3.1章编译好的device.ko和本章节编译好的driver.ko,如下图所示:
从上图可以看到,有打印led_res的内容,已经获取到了硬件资源。那么是不是就可以直接操作相关的寄存器地址了呢?是不是直接ioremap,像我们注册杂项设备那样来操作相关的寄存器呢?
接下来我们需要注册一个杂项设备,完整的驱动代码如下所示:
程序源码在网盘资料“iTOP-i.MX8MM开发板\02-i.MX8MM开发板网盘资料汇总(不含光盘内容)\嵌入式Linux开发指南(iTOP-i.MX8MM)手册配套资料\2.驱动程序例程\09-Platform设备驱动\09\006”路径下。
/*
* @Author: topeet
* @Description: 基于平台设备模型的driver.c,在probe函数中获取硬件资源后,注册一个杂项设备
*/
//初始化头文件
#include <linux/init.h>
//最基本的文件,支持动态添加和卸载模块。
#include <linux/module.h>
//平台设备所需要的头文件
#include <linux/platform_device.h>
#include <linux/ioport.h>
/*注册杂项设备头文件*/
#include <linux/miscdevice.h>
//文件系统头文件,定义文件表结构(file,buffer_head,m_inode等)
#include <linux/fs.h>
//包含了copy_to_user、copy_from_user等内核访问用户进程内存地址的函数定义。
#include <linux/uaccess.h>
//包含了ioremap、iowrite等内核访问IO内存等函数的定义。
#include <linux/io.h>
#define GPIO1_IO13 0x30200000 //led数据寄存器物理地址
#define GPIO1_IO13_GDIR 0x30200004 //led数据寄存器方向寄存器物理地址
unsigned int *vir_gpio1_io13; //存放映射完的虚拟地址的首地址
unsigned int *vir_gpio1_io13_gdir; //存放映射完的虚拟地址的首地址
int ret; //返回值
struct resource *led_mem;
struct resource *led_mem_gdir;
struct resource *led_mem_tmp;
/****************************************************************************************
* @brief misc_read : 用户空间向设备写入数据时执行此函数
* @param file : 文件
* @param ubuf : 指向用户空间数据缓冲区
* @return 成功返回 0
****************************************************************************************/
ssize_t misc_read(struct file *file, char __user *ubuf, size_t size, loff_t *loff_t)
{
printk("misc_read\n ");
return 0;
}
/****************************************************************************************
* @brief misc_write : 用户空间向驱动模块写入数据时执行此函数,对数据进行判断,控制LED亮灭
* @param file : 文件
* @param ubuf : 指向用户空间数据缓冲区
* @return 成功返回 0 ,失败返回 -1
****************************************************************************************/
ssize_t misc_write(struct file *file, const char __user *ubuf, size_t size, loff_t *loff_t)
{
char kbuf[64] = {0}; //保存的是从应用层读取到的数据
if (copy_from_user(kbuf, ubuf, size) != 0)
{
printk("copy_from_user error \n ");
return -1;
}
printk("kbuf is %d\n ", kbuf[0]);
*vir_gpio1_io13_gdir |= (1 << 13);
if (kbuf[0] == 1) //如果传递进来数据为1,则打开LED
{
*vir_gpio1_io13 |= (1 << 13);
}
else if (kbuf[0] == 0) //如果传递进来数据为0,关闭LED
*vir_gpio1_io13 &= ~(1 << 13);
return 0;
}
/****************************************************************************************
* @description:misc_release 释放 platform 设备模块的时候此函数会执行
* @param {structinode} *inode
* @param {structfile} *file
* @return {*}
****************************************************************************************/
int misc_release(struct inode *inode, struct file *file)
{
printk("hello misc_relaease bye bye \n ");
return 0;
}
/****************************************************************************************
* @brief misc_open : 打开设备节点时执行此函数,并初始化GPIO
* @param inode : 文件索引
* @param file : 文件
* @return 成功返回 0
****************************************************************************************/
int misc_open(struct inode *inode, struct file *file)
{
printk("hello misc_open\n ");
return 0;
}
struct file_operations misc_fops = {
.owner = THIS_MODULE,
.open = misc_open,
.release = misc_release,
.read = misc_read,
.write = misc_write,
};
struct miscdevice misc_dev = {
.minor = MISC_DYNAMIC_MINOR,
.name = "hello_misc",
.fops = &misc_fops,
};
/****************************************************************************************
* @brief led_probe : 与设备信息层(device.c)匹配成功后自动执行此函数,
* @param inode : 文件索引
* @param file : 文件
* @return 成功返回 0
****************************************************************************************/
int led_probe(struct platform_device *pdev)
{
printk("led_probe\n");
/*获取硬件资源方法一: 不推荐*/
//printk("led_res is %s\n",pdev->resource[0].name);
//return 0;
/*获取硬件资源方法二: 推荐*/
led_mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
led_mem_gdir = platform_get_resource(pdev, IORESOURCE_MEM, 1);
if (led_mem == NULL || led_mem_gdir == NULL)
{
printk("platform_get_resource is error\n");
return -EBUSY;
}
printk("led_res start is 0x%llx \n", led_mem->start);
printk("led_res end is 0x%llx \n", led_mem->end);
printk("led_res start is 0x%llx \n", led_mem_gdir->start);
printk("led_res end is 0x%llx \n", led_mem_gdir->end);
/*led_mem_tmp = request_mem_region(led_mem->start,led_mem->end-led_mem->start+1,"led");
if(led_mem_tmp == NULL)
{
printk(" request_mem_region is error\n");
goto err_region;
err_region:
release_mem_region(led_mem->start,led_mem->end-led_mem->start+1);
return -EBUSY;
}*/
/*****************************************************************/
//映射GPIO资源
vir_gpio1_io13 = ioremap(led_mem->start, 4);
if (vir_gpio1_io13 == NULL)
{
printk("GPIO1_IO13 ioremap is error \n");
return EBUSY;
}
printk("GPIO1_IO13 ioremap is ok \n");
vir_gpio1_io13_gdir = ioremap(led_mem_gdir->start, 4);
if (vir_gpio1_io13_gdir == NULL)
{
printk("GPIO1_IO13_GDIR ioremap is error \n");
return EBUSY;
}
printk("GPIO1_IO13_GDIR ioremap is ok \n");
//注册杂项设备
ret = misc_register(&misc_dev);
if (ret < 0)
{
printk("misc registe is error \n");
}
printk("misc registe is succeed \n");
return 0;
}
int led_remove(struct platform_device *pdev)
{
printk("led_remove\n");
return 0;
}
const struct platform_device_id led_idtable = {
.name = "led_test",
};
// platform 驱动结构体
struct platform_driver led_driver = {
.probe = led_probe,
.remove = led_remove,
.driver = {
.owner = THIS_MODULE,
.name = "led_test"},
.id_table = &led_idtable
};
/**
* @description: 设备模块加载
* @param {*}无
* @return {*}无
*/
static int led_driver_init(void)
{
int ret = 0;
// platform驱动注册到 Linux 内核
ret = platform_driver_register(&led_driver);
if (ret < 0)
{
printk("platform_driver_register error \n");
}
printk("platform_driver_register ok \n");
return 0;
}
/**
* @description: 设备模块注销
* @param {*}无
* @return {*}无
*/
static void led_driver_exit(void)
{
platform_driver_unregister(&led_driver);
misc_deregister(&misc_dev);
iounmap(vir_gpio1_io13);
iounmap(vir_gpio1_io13_gdir);
printk("gooodbye! \n");
}
module_init(led_driver_init);
module_exit(led_driver_exit);
MODULE_LICENSE("GPL");
编译driver.c文件为驱动模块,如下图所示:
加载driver.ko,如下图所示:
输入命令查看是否生成设备节点
ls /dev/hello_misc
编写应用程序app.c,如下所示:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
int fd;
char buf[64] = {0};
fd = open("/dev/hello_misc",O_RDWR);
if(fd < 0)
{
perror("open error \n");
return fd;
}
buf[0] = atoi(argv[1]);//atoi()
//read(fd,buf,sizeof(buf));
write(fd,buf,sizeof(buf));
//printf("buf is %s\n",buf);
close(fd);
return 0;
}
我们将app.c拷贝到Ubuntu的/home/topeet/imx8mm/09/006目录下,并输入命令编译为app
我们在开发板上面运行应用程序,如下图所示:
./app 1
./app 0
当我们输入命令“./app 1”,可以看到led亮了,当我们输入命令“./app 0”,led灭了,如下图所示。到目前为止48.3章节我们完整的写了一个以LED为代表的平台总线模型的驱动。