platform设备驱动实验

我们在前面几章编写的设备驱动都非常的简单,都是对IO进行最简单的读写操作。像I2C、

SPI、LCD等这些复杂外设的驱动就不能这么去写了,Linux系统要考虑到驱动的可重用性,因

此提出了驱动的分离与分层这样的软件思路,在这个思路下诞生了我们将来最常打交道的

platform设备驱动,也叫做平台设备驱动。本章我们就来学习一下Linux下的驱动分离与分层,

以及platform框架下的设备驱动该如何编写。

1 Linux驱动的分离与分层

1.1 驱动的分隔与分离

对于Linux这样一个成熟、庞大、复杂的操作系统,代码的重用性非常重要,否则的话就

会在Linux内核中存在大量无意义的重复代码。尤其是驱动程序,因为驱动程序占用了Linux

内核代码量的大头,如果不对驱动程序加以管理,任由重复的代码肆意增加,那么用不了多久

Linux内核的文件数量就庞大到无法接受的地步。

假如现在有三个平台A、B和C,这三个平台(这里的平台说的是SOC)上都有MPU6050这

个I2C接口的六轴传感器,按照我们写裸机I2C驱动的时候的思路,每个平台都有一个MPU6050

的驱动,因此编写出来的最简单的驱动框架如图所示:

从图可以看出,每种平台下都有一个主机驱动和设备驱动,主机驱动肯定是必须要的,毕竟不同的平台其I2C控制器不同。但是右侧的设备驱动就没必要每个平台都写一个,因为不管对于那个SOC来说,MPU6050都是一样,通过I2C接口读写数据就行了,只需要一个MPU6050的驱动程序即可。如果再来几个I2C设备,比如AT24C02、FT5206(电容触摸屏)等,如果按照图中的写法,那么设备端的驱动将会重复的编写好几次。显然在Linux驱动程序中这种写法是不推荐的,最好的做法就是每个平台的I2C控制器都提供一个统一的接口(也叫做主机驱动),每个设备的话也只提供一个驱动程序(设备驱动),每个设备通过统一的I2C接口驱动来访问,这样就可以大大简化驱动文件,三种平台下的MPU6050驱动框架就可以简化为图所示:

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

所示

这个就是驱动的分隔,也就是将主机驱动和设备驱动分隔开来,比如I2C、SPI等等都会采

用驱动分隔的方式来简化驱动的开发。在实际的驱动开发中,一般I2C主机控制器驱动已经由

半导体厂家编写好了,而设备驱动一般也由设备器件的厂家编写好了,我们只需要提供设备信

息即可,比如I2C设备的话提供设备连接到了哪个I2C接口上,I2C的速度是多少等等。相当

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

取到设备信息),然后根据获取到的设备信息来初始化设备。 这样就相当于驱动只负责驱动,

设备只负责设备,想办法将两者进行匹配即可。这个就是Linux中的总线(bus)、驱动(driver)和

设备(device)模型,也就是常说的驱动分离。总线就是驱动和设备信息的月老,负责给两者牵线

搭桥,如图所示:

当我们向系统注册一个驱动的时候,总线就会在右侧的设备中查找,看看有没有与之匹配

的设备,如果有的话就将两者联系起来。同样的,当向系统中注册一个设备的时候,总线就会

在左侧的驱动中查找看有没有与之匹配的设备,有的话也联系起来。Linux内核中大量的驱动

程序都采用总线、驱动和设备模式,我们一会要重点讲解的platform驱动就是这一思想下的产

物。

1.2 驱动的分层

上一小节讲了驱动的分隔与分离,本节我们来简单看一下驱动的分层,大家应该听说过网

络的7层模型,不同的层负责不同的内容。同样的,Linux下的驱动往往也是分层的,分层的目

的也是为了在不同的层处理不同的内容。以其他书籍或者资料常常使用到的input(输入子系统,

后面会有专门的章节详细的讲解)为例,简单介绍一下驱动的分层。input子系统负责管理所有

跟输入有关的驱动,包括键盘、鼠标、触摸等,最底层的就是设备原始驱动,负责获取输入设

备的原始值,获取到的输入事件上报给input核心层。input核心层会处理各种IO模型,并且提

供file_operations操作集合。我们在编写输入设备驱动的时候只需要处理好输入事件的上报即

可,至于如何处理这些上报的输入事件那是上层去考虑的,我们不用管。可以看出借助分层模

型可以极大的简化我们的驱动编写,对于驱动编写来说非常的友好。

2 platform平台驱动模型简介

前面我们讲了设备驱动的分离,并且引出了总线(bus)、驱动(driver)和设备(device)模型,比

如I2C、SPI、USB等总线。但是在SOC中有些外设是没有总线这个概念的,但是又要使用总

线、驱动和设备模型该怎么办呢?为了解决此问题,Linux提出了platform这个虚拟总线,相应

的就有platform_driver和platform_device。

2.1 platform总线

Linux系统内核使用bus_type结构体表示总线,此结构体定义在文件include/linux/device.h,

bus_type结构体内容如下:

cpp 复制代码
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的意思就是"匹配、相配",因此此函数

就是完成设备和驱动之间匹配的,总线就是使用match函数来根据注册的设备来查找对应的驱

动,或者根据注册的驱动来查找相应的设备,因此每一条总线都必须实现此函数。match函数有

两个参数:dev和drv,这两个参数分别为device和device_driver类型,也就是设备和驱动。

platform总线是bus_type的一个具体实例,定义在文件drivers/base/platform.c,platform总

线定义如下:

cpp 复制代码
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_match函数定义在文件drivers/base/platform.c中,函

数内容如下所示:

cpp 复制代码
1  static int platform_match(struct device *dev,
2  struct device_driver *drv)
3  {
4      struct platform_device *pdev = to_platform_device(dev);
5      struct platform_driver *pdrv = to_platform_driver(drv);
6  
7      /*When driver_override is set,only bind to the matching driver*/
8      if (pdev->driver_override)
9          return !strcmp(pdev->driver_override, drv->name);
10 
11     /* Attempt an OF style match first */
12     if (of_driver_match_device(dev, drv))
13         return 1;
14 
15     /* Then try ACPI style match */
16     if (acpi_driver_match_device(dev, drv))
17         return 1;
18 
19     /* Then try to match against the id table */
20     if (pdrv->id_table)
21         return platform_match_id(pdrv->id_table, pdev) != NULL;
22 
23     /* fall-back to driver name match */
24     return (strcmp(pdev->name, drv->name) == 0);
25 }

驱动和设备的匹配有四种方法,我们依次来看一下:

第 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驱动所支持的驱

动类型。

第23行,第四种匹配方式,如果第三种匹配方式的id_table不存在的话就直接比较驱动和

设备的name字段,看看是不是相等,如果相等的话就匹配成功。

对于支持设备树的Linux版本号,一般设备驱动为了兼容性都支持设备树和无设备树两种

匹配方式。也就是第一种匹配方式一般都会存在,第三种和第四种只要存在一种就可以,一般

用的最多的还是第四种,也就是直接比较驱动和设备的name字段,毕竟这种方式最简单了。

2.2 platform驱动

platform_driver 结 构 体 表 示platform 驱 动 , 此 结 构 体 定 义 在 文 件include/linux/platform_device.h中,内容如下:

cpp 复制代码
1  struct platform_driver { 
2      int (*probe)(struct platform_device *); 
3      int (*remove)(struct platform_device *); 
4      void (*shutdown)(struct platform_device *); 
5      int (*suspend)(struct platform_device *, pm_message_t state); 
6      int (*resume)(struct platform_device *); 
7      struct device_driver driver; 
8      const struct platform_device_id *id_table; 
9      bool prevent_deferred_probe; 
10 };

第2行,probe函数,当驱动与设备匹配成功以后probe函数就会执行,非常重要的函数!!

一般驱动的提供者会编写,如果自己要编写一个全新的驱动,那么probe就需要自行实现。

第7行,driver成员,为device_driver结构体变量,Linux内核里面大量使用到了面向对象

的思维,device_driver相当于基类,提供了最基础的驱动框架。plaform_driver继承了这个基类,

然后在此基础上又添加了一些特有的成员变量。

第8行,id_table表,也就是我们上一小节讲解platform总线匹配驱动和设备的时候采用的

第三种方法,id_table 是个表(也就是数组),每个元素的类型为 platform_device_id,

platform_device_id结构体内容如下:

cpp 复制代码
1 struct platform_device_id { 
2 char name[PLATFORM_NAME_SIZE]; 
3 kernel_ulong_t driver_data; 
4 }; 

device_driver结构体定义在include/linux/device.h,device_driver结构体内容如下:

cpp 复制代码
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  };

第10行,of_match_table就是采用设备树的时候驱动使用的匹配表,同样是数组,每个匹

配项都为of_device_id结构体类型,此结构体定义在文件include/linux/mod_devicetable.h中,内

容如下:

cpp 复制代码
1  struct of_device_id {
2      char        name[32];
3      char        type[32];
4      char        compatible[128];
5      const void  *data;
6  };

第4行的compatible非常重要,因为对于设备树而言,就是通过设备节点的compatible属

性值和of_match_table中每个项目的compatible成员变量进行比较,如果有相等的就表示设备

和此驱动匹配成功。

在编写platform驱动的时候,首先定义一个platform_driver结构体变量,然后实现结构体

中的各个成员变量,重点是实现匹配方法以及probe函数。当驱动和设备匹配成功以后probe

函数就会执行,具体的驱动程序在probe函数里面编写,比如字符设备驱动等等。

当我们定义并初始化好 platform_driver 结构体变量以后,需要在驱动入口函数里面调用

platform_driver_register函数向Linux内核注册一个platform驱动,platform_driver_register函数

原型如下所示:

cpp 复制代码
int platform_driver_register (struct platform_driver    *driver) 

还需要在驱动卸载函数中通过 platform_driver_unregister 函数卸载 platform 驱动,

platform_driver_unregister函数原型如下:

cpp 复制代码
void platform_driver_unregister(struct platform_driver *drv) 

platform驱动框架如下所示:

cpp 复制代码
/* 设备结构体 */
1   struct xxx_dev{
2       struct cdev cdev;
3       /* 设备结构体其他具体内容 */
4   };
5   
6   struct xxx_dev xxxdev;   /* 定义个设备结构体变量 */
7   
8   static int xxx_open(struct inode *inode, struct file *filp)
9   {
10      /* 函数具体内容 */
11      return 0;
12  }
13  
14  static ssize_t xxx_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
15  {
16      /* 函数具体内容 */
17      return 0;
18  }
19  
20  /*
21   * 字符设备驱动操作集
22   */
23  static struct file_operations xxx_fops = {
24      .owner = THIS_MODULE,
25      .open = xxx_open,
26      .write = xxx_write,
27  };
28  
29  /*
30   * platform驱动的probe函数
31   * 驱动与设备匹配成功以后此函数就会执行
32   */
33  static int xxx_probe(struct platform_device *dev)
34  {
35      ......
36      cdev_init(&xxxdev.cdev, &xxx_fops); /* 注册字符设备驱动 */
37      /* 函数具体内容 */
38      return 0;
39  }
40  
41  static int xxx_remove(struct platform_device *dev)
42  {
43      ......
44      cdev_del(&xxxdev.cdev);/*  删除cdev */
45      /* 函数具体内容 */
46      return 0;
47  }
48  
49  /* 匹配列表 */
50  static const struct of_device_id xxx_of_match[] = {
51      { .compatible = "xxx-gpio" },
52      { /* Sentinel */ }
53  };
54  
55  /*
56   * platform平台驱动结构体
57   */
58  static struct platform_driver xxx_driver = {
59      .driver = {
60          .name       = "xxx",
61          .of_match_table = xxx_of_match,
62      },
63      .probe      = xxx_probe,
64      .remove     = xxx_remove,
65  };
66  
67  /* 驱动模块加载 */
68  static int __init xxxdriver_init(void)
69  {
70      return platform_driver_register(&xxx_driver);
71  }
72  
73  /* 驱动模块卸载 */
74  static void __exit xxxdriver_exit(void)
75  {
76      platform_driver_unregister(&xxx_driver);
77  }
78  
79  module_init(xxxdriver_init);
80  module_exit(xxxdriver_exit);
81  MODULE_LICENSE("GPL");
82  MODULE_AUTHOR("zuozhongkai");

第1~27行,传统的字符设备驱动,所谓的platform驱动并不是独立于字符设备驱动、块设

备驱动和网络设备驱动之外的其他种类的驱动。platform只是为了驱动的分离与分层而提出来

的一种框架,其驱动的具体实现还是需要字符设备驱动、块设备驱动或网络设备驱动。

第33~39行,xxx_probe函数,当驱动和设备匹配成功以后此函数就会执行,以前在驱动

入口init函数里面编写的字符设备驱动程序就全部放到此probe函数里面。比如注册字符设备

驱动、添加cdev、创建类等等。

第41~47行,xxx_remove函数,platform_driver结构体中的remove成员变量,当关闭platfor

备驱动的时候此函数就会执行,以前在驱动卸载exit函数里面要做的事情就放到此函数中来。

比如,使用iounmap释放内存、删除cdev,注销设备号等等。

第50~53行,xxx_of_match匹配表,如果使用设备树的话将通过此匹配表进行驱动和设备

的匹配。第51行设置了一个匹配项,此匹配项的compatible值为"xxx-gpio",因此当设备树中

设备节点的compatible属性值为"xxx-gpio"的时候此设备就会与此驱动匹配。第52行是一个

标记,of_device_id表最后一个匹配项必须是空的。

第58~65行,定义一个platform_driver结构体变量xxx_driver,表示platform驱动,第59~62

行设置paltform_driver中的device_driver成员变量的name和of_match_table这两个属性。其中

name 属性用于传统的驱动与设备匹配,也就是检查驱动和设备的 name 字段是不是相同。

of_match_table属性就是用于设备树下的驱动与设备检查。对于一个完整的驱动程序,必须提供

有设备树和无设备树两种匹配方法。最后63和64这两行设置probe和remove这两成员变量。

第68~71行,驱动入口函数,调用platform_driver_register函数向Linux内核注册一个platform

驱动,也就是上面定义的xxx_driver结构体变量。

第74~77行,驱动出口函数,调用platform_driver_unregister函数卸载前面注册的platform

驱动。

总体来说,platform驱动还是传统的字符设备驱动、块设备驱动或网络设备驱动,只是套

上了一张"platform"的皮,目的是为了使用总线、驱动和设备这个驱动模型来实现驱动的分

离与分层。

2.3 platform设备

platform驱动已经准备好了,我们还需要platform设备,否则的话单单一个驱动也做不了

什么。platform_device这个结构体表示platform设备,这里我们要注意,如果内核支持设备树

的话就不要再使用platform_device来描述设备了,因为改用设备树去描述了。当然了,你如果

一定要用platform_device来描述设备信息的话也是可以的。platform_device结构体定义在文件

include/linux/platform_device.h中,结构体内容如下:

cpp 复制代码
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行,name表示设备名字,要和所使用的platform驱动的name字段相同,否则的话设

备就无法匹配到对应的驱动。比如对应的platform驱动的name字段为"xxx-gpio",那么此name

字段也要设置为"xxx-gpio"。

第27行,num_resources表示资源数量,一般为第28行resource资源的大小。

第28行,resource表示资源,也就是设备信息,比如外设寄存器等。Linux内核使用resource

结构体表示资源,resource结构体内容如下:

cpp 复制代码
8  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里面,如下所示:

cpp 复制代码
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 */

在以前不支持设备树的Linux版本中,用户需要编写platform_device变量来描述设备信息,

然后使用platform_device_register函数将设备信息注册到Linux内核中,此函数原型如下所示:

cpp 复制代码
int platform_device_register(struct platform_device *pdev) 

如果不再使用platform的话可以通过platform_device_unregister函数注销掉相应的platform

设备,platform_device_unregister函数原型如下:

cpp 复制代码
void platform_device_unregister(struct platform_device *pdev) 

platform设备信息框架如下所示:

cpp 复制代码
1  /* 寄存器地址定义*/ 
2  #define PERIPH1_REGISTER_BASE    (0X20000000) /* 外设1寄存器首地址 */     
3  #define PERIPH2_REGISTER_BASE    (0X020E0068) /* 外设2寄存器首地址 */ 
4  #define REGISTER_LENGTH           4 
5   
6  /* 资源 */ 
7  static struct resource xxx_resources[] = { 
8      [0] = { 
9         .start  = PERIPH1_REGISTER_BASE, 
10        .end    = (PERIPH1_REGISTER_BASE + REGISTER_LENGTH - 1), 
11        .flags  = IORESOURCE_MEM, 
12     },   
13     [1] = { 
14        .start  = PERIPH2_REGISTER_BASE, 
15        .end    = (PERIPH2_REGISTER_BASE + REGISTER_LENGTH - 1), 
16        .flags  = IORESOURCE_MEM, 
17     }, 
18  }; 
19  
20 /* platform设备结构体 */ 
21 static struct platform_device xxxdevice = { 
22    .name = "xxx-gpio", 
23    .id = -1, 
24    .num_resources = ARRAY_SIZE(xxx_resources), 
25    .resource = xxx_resources, 
26 }; 
27       
28 /* 设备模块加载 */ 
29 static int __init xxxdevice_init(void) 
30 { 
31    return platform_device_register(&xxxdevice); 
32 } 
33  
34 /* 设备模块注销 */ 
35 static void __exit xxx_resourcesdevice_exit(void) 
36 { 
37    platform_device_unregister(&xxxdevice); 
38 } 
39  
40 module_init(xxxdevice_init); 
41 module_exit(xxxdevice_exit); 
42 MODULE_LICENSE("GPL"); 
43 MODULE_AUTHOR("zuozhongkai");

第7~18行,数组xxx_resources表示设备资源,一共有两个资源,分别为设备外设1和外

设2的寄存器信息。因此flags都为IORESOURCE_MEM,表示资源为内存类型的。

第21~26行,platform设备结构体变量,注意name字段要和所使用的驱动中的name字段

一致,否则驱动和设备无法匹配成功。num_resources表示资源大小,其实就是数组xxx_resources

的元素数量,这里用ARRAY_SIZE来测量一个数组的元素个数。

第29~32行,设备模块加载函数,在此函数中调用platform_device_register向Linux内核注

册platform设备。

第35~38行,设备模块卸载函数,在此函数中调用platform_device_unregister从Linux内核

中卸载platform设备。

示例代码主要是在不支持设备树的Linux版本中使用的,当Linux内核支持了设备树以后就不需要用户手动去注册platform设备了。因为设备信息都放到了设备树中去描述,Linux内核启动的时候会从设备树中读取设备信息,然后将其组织成platform_device形式,至于设备树到platform_device的具体过程就不去详细的追究了,感兴趣的可以去看一下,网上也有很多博客详细的讲解了整个过程。

关于platform下的总线、驱动和设备就讲解到这里,我们接下来就使用platform驱动框架来编写一个 LED 灯驱动,本章我们不使用设备树来描述设备信息,我们采用自定义platform_device这种"古老"方式来编写LED的设备信息。下一章我们来编写设备树下的platform驱动,这样我们就掌握了无设备树和有设备树这两种platform驱动的开发方式。

3 试验程序编写

本实验对应的例程路径为:开发板光盘-> 2、Linux驱动例程-> 17_platform。

本章实验我们需要编写一个驱动模块和一个设备模块,其中驱动模块是platform驱动程序,

设备模块是platform的设备信息。当这两个模块都加载成功以后就会匹配成功,然后platform

驱动模块中的probe函数就会执行,probe函数中就是传统的字符设备驱动那一套。

3.1 platform设备与驱动程序编写

新建名为"17_platform"的文件夹,然后在17_platform文件夹里面创建vscode工程,工作

区命名为"platform"。新建名为leddevice.c和leddriver.c这两个文件,这两个文件分别为LED

灯的platform设备文件和LED灯的platform的驱动文件。在leddevice.c中输入如下所示内容:

cpp 复制代码
1   #include <linux/types.h> 
2   #include <linux/kernel.h> 
3   #include <linux/delay.h> 
4   #include <linux/ide.h> 
5   #include <linux/init.h> 
6   #include <linux/module.h> 
7   #include <linux/errno.h> 
8   #include <linux/gpio.h> 
9   #include <linux/cdev.h> 
10  #include <linux/device.h> 
11  #include <linux/of_gpio.h> 
12  #include <linux/semaphore.h> 
13  #include <linux/timer.h> 
14  #include <linux/irq.h> 
15  #include <linux/wait.h> 
16  #include <linux/poll.h> 
17  #include <linux/fs.h> 
18  #include <linux/fcntl.h> 
19  #include <linux/platform_device.h> 
20  #include <asm/mach/map.h> 
21  #include <asm/uaccess.h> 
22  #include <asm/io.h> 
23  /*************************************************************** 
24  Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved. 
25  文件名      : leddevice.c 
26  作者        : 左忠凯 
27  版本        : V1.0 
28  描述        : platform设备 
29  其他        : 无 
30  论坛        : www.openedv.com 
31  日志        : 初版V1.0 2019/8/13 左忠凯创建 
32  ***************************************************************/ 
33   
34  /*  
35   * 寄存器地址定义 
36   */ 
37  #define CCM_CCGR1_BASE              (0X020C406C)     
38  #define SW_MUX_GPIO1_IO03_BASE      (0X020E0068) 
39  #define SW_PAD_GPIO1_IO03_BASE      (0X020E02F4) 
40  #define GPIO1_DR_BASE                (0X0209C000) 
41  #define GPIO1_GDIR_BASE              (0X0209C004) 
42  #define REGISTER_LENGTH              4 
43   
44  /* @description   : 释放flatform设备模块的时候此函数会执行  
45   * @param - dev   : 要释放的设备  
46   * @return         : 无 
47   */ 
48  static void led_release(struct device *dev) 
49  { 
50      printk("led device released!\r\n");  
51  } 
52   
53  /*   
54   * 设备资源信息,也就是LED0所使用的所有寄存器 
55   */ 
56  static struct resource led_resources[] = { 
57      [0] = { 
58          .start  = CCM_CCGR1_BASE, 
59          .end    = (CCM_CCGR1_BASE + REGISTER_LENGTH - 1), 
60          .flags  = IORESOURCE_MEM, 
61      },   
62      [1] = { 
63          .start  = SW_MUX_GPIO1_IO03_BASE, 
64          .end    = (SW_MUX_GPIO1_IO03_BASE + REGISTER_LENGTH - 1), 
65          .flags  = IORESOURCE_MEM, 
66      }, 
67      [2] = { 
68          .start  = SW_PAD_GPIO1_IO03_BASE, 
69          .end    = (SW_PAD_GPIO1_IO03_BASE + REGISTER_LENGTH - 1), 
70          .flags  = IORESOURCE_MEM, 
71      }, 
72      [3] = { 
73          .start  = GPIO1_DR_BASE, 
74          .end    = (GPIO1_DR_BASE + REGISTER_LENGTH - 1), 
75          .flags  = IORESOURCE_MEM, 
76      }, 
77      [4] = { 
78          .start  = GPIO1_GDIR_BASE, 
79          .end    = (GPIO1_GDIR_BASE + REGISTER_LENGTH - 1), 
80          .flags  = IORESOURCE_MEM, 
81      }, 
82  }; 
83   
84   
85  /* 
86   * platform设备结构体  
87   */ 
88  static struct platform_device leddevice = { 
89      .name = "imx6ul-led", 
90      .id = -1, 
91      .dev = { 
92          .release = &led_release, 
93      }, 
94      .num_resources = ARRAY_SIZE(led_resources), 
95      .resource = led_resources, 
96  }; 
97           
98  /* 
99   * @description   : 设备模块加载  
100  * @param         : 无 
101  * @return        : 无 
102  */ 
103 static int __init leddevice_init(void) 
104 { 
105     return platform_device_register(&leddevice); 
106 } 
107  
108 /* 
109  * @description   : 设备模块注销 
110  * @param         : 无 
111  * @return        : 无 
112  */ 
113 static void __exit leddevice_exit(void) 
114 { 
115     platform_device_unregister(&leddevice); 
116 } 
117  
118 module_init(leddevice_init); 
119 module_exit(leddevice_exit); 
120 MODULE_LICENSE("GPL"); 
121 MODULE_AUTHOR("zuozhongkai"); 

leddevice.c文件内容就是按照示例代码的platform设备模板编写的。

第56~82行,led_resources数组,也就是设备资源,描述了LED所要使用到的寄存器信息,

也就是IORESOURCE_MEM资源。

第88~96,platform设备结构体变量leddevice,这里要注意name字段为"imx6ul-led",所

以稍后编写platform驱动中的name字段也要为"imx6ul-led",否则设备和驱动匹配失败。

第103~106行,设备模块加载函数,在此函数里面通过platform_device_register向Linux内

核注册leddevice这个platform设备。

第113~116行,设备模块卸载函数,在此函数里面通过platform_device_unregister从Linux

内核中删除掉leddevice这个platform设备。

leddevice.c文件编写完成以后就编写leddriver.c这个platform驱动文件,在leddriver.c里

面输入如下内容:

cpp 复制代码
1   #include <linux/types.h> 
2   #include <linux/kernel.h> 
3   #include <linux/delay.h> 
4   #include <linux/ide.h> 
5   #include <linux/init.h> 
6   #include <linux/module.h> 
7   #include <linux/errno.h> 
8   #include <linux/gpio.h> 
9   #include <linux/cdev.h> 
10  #include <linux/device.h> 
11  #include <linux/of_gpio.h> 
12  #include <linux/semaphore.h> 
13  #include <linux/timer.h> 
14  #include <linux/irq.h> 
15  #include <linux/wait.h> 
16  #include <linux/poll.h> 
17  #include <linux/fs.h> 
18  #include <linux/fcntl.h> 
19  #include <linux/platform_device.h> 
20  #include <asm/mach/map.h> 
21  #include <asm/uaccess.h> 
22  #include <asm/io.h> 
23  /*************************************************************** 
24  Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved. 
25  文件名     : leddriver.c 
26  作者        : 左忠凯 
27  版本        : V1.0 
28  描述        : platform驱动 
29  其他        : 无 
30  论坛        : www.openedv.com 
31  日志        : 初版V1.0 2019/8/13 左忠凯创建 
32  ***************************************************************/ 
33   
34  #define LEDDEV_CNT      1              /* 设备号长度   */ 
35  #define LEDDEV_NAME     "platled"     /* 设备名字     */ 
36  #define LEDOFF            0 
37  #define LEDON             1 
38   
39  /* 寄存器名 */ 
40  static void __iomem *IMX6U_CCM_CCGR1; 
41  static void __iomem *SW_MUX_GPIO1_IO03; 
42  static void __iomem *SW_PAD_GPIO1_IO03; 
43  static void __iomem *GPIO1_DR; 
44  static void __iomem *GPIO1_GDIR; 
45   
46  /* leddev设备结构体 */ 
47  struct leddev_dev{ 
48      dev_t devid;              /* 设备号      */ 
49      struct cdev cdev;         /* cdev       */ 
50      struct class *class;     /* 类         */ 
51      struct device *device;   /* 设备         */ 
52      int major;                /* 主设备号    */       
53  }; 
54   
55  struct leddev_dev leddev;    /* led设备   */ 
56   
57  /* 
58   * @description   : LED打开/关闭 
59   * @param - sta   : LEDON(0) 打开LED,LEDOFF(1) 关闭LED 
60   * @return         : 无 
61   */ 
62  void led0_switch(u8 sta) 
63  { 
64      u32 val = 0; 
65      if(sta == LEDON){ 
66          val = readl(GPIO1_DR); 
67          val &= ~(1 << 3);    
68          writel(val, GPIO1_DR); 
69      }else if(sta == LEDOFF){ 
70          val = readl(GPIO1_DR); 
71          val|= (1 << 3);  
72          writel(val, GPIO1_DR); 
73      }    
74  } 
75   
76  /* 
77   * @description   : 打开设备 
78   * @param -- inode : 传递给驱动的inode 
79   * @param - filp  : 设备文件,file结构体有个叫做private_data的成员变量 
80   *                    一般在open的时候将private_data指向设备结构体。 
81   * @return         : 0 成功;其他 失败 
82   */ 
83  static int led_open(struct inode *inode, struct file *filp) 
84  { 
85      filp->private_data = &leddev; /* 设置私有数据  */ 
86      return 0; 
87  } 
88   
89  /* 
90   * @description   : 向设备写数据  
91   * @param -- filp  : 设备文件,表示打开的文件描述符 
92   * @param - buf   : 要写给设备写入的数据 
93   * @param - cnt   : 要写入的数据长度 
94   * @param - offt  : 相对于文件首地址的偏移 
95   * @return         : 写入的字节数,如果为负值,表示写入失败 
96   */ 
97  static ssize_t led_write(struct file *filp, const char __user *buf,  
size_t cnt, loff_t *offt) 
98  { 
99      int retvalue; 
100     unsigned char databuf[1]; 
101     unsigned char ledstat; 
102  
103     retvalue = copy_from_user(databuf, buf, cnt); 
104     if(retvalue < 0) { 
105         return -EFAULT; 
106     } 
107  
108     ledstat = databuf[0];        /* 获取状态值   */ 
109     if(ledstat == LEDON) { 
110         led0_switch(LEDON);      /* 打开LED灯   */ 
111     }else if(ledstat == LEDOFF) { 
112         led0_switch(LEDOFF);     /* 关闭LED灯   */ 
113     } 
114     return 0; 
115 } 
116  
117 /* 设备操作函数 */ 
118 static struct file_operations led_fops = { 
119     .owner = THIS_MODULE, 
120     .open = led_open, 
121     .write = led_write, 
122 }; 
123  
124 /* 
125  * @description  : flatform驱动的probe函数,当驱动与设备 
126  *                    匹配以后此函数就会执行 
127  * @param - dev   : platform设备 
128  * @return         : 0,成功;其他负值,失败 
129  */ 
130 static int led_probe(struct platform_device *dev) 
131 {    
132     int i = 0; 
133     int ressize[5]; 
134     u32 val = 0; 
135     struct resource *ledsource[5]; 
136  
137     printk("led driver and device has matched!\r\n"); 
138     /* 1、获取资源 */ 
139     for (i = 0; i < 5; i++) { 
140         ledsource[i] = platform_get_resource(dev, IORESOURCE_MEM, i);  
141         if (!ledsource[i]) { 
142             dev_err(&dev->dev, "No MEM resource for always on\n"); 
143             return -ENXIO; 
144         } 
145         ressize[i] = resource_size(ledsource[i]);    
146     }    
147  
148     /* 2、初始化LED */ 
149     /* 寄存器地址映射 */ 
150     IMX6U_CCM_CCGR1 = ioremap(ledsource[0]->start, ressize[0]); 
151     SW_MUX_GPIO1_IO03 = ioremap(ledsource[1]->start, ressize[1]); 
152     SW_PAD_GPIO1_IO03 = ioremap(ledsource[2]->start, ressize[2]); 
153     GPIO1_DR = ioremap(ledsource[3]->start, ressize[3]); 
154     GPIO1_GDIR = ioremap(ledsource[4]->start, ressize[4]); 
155      
156     val = readl(IMX6U_CCM_CCGR1); 
157     val &= ~(3 << 26);                /* 清除以前的设置  */ 
158     val |= (3 << 26);                 /* 设置新值    */ 
159     writel(val, IMX6U_CCM_CCGR1); 
160  
161     /* 设置GPIO1_IO03复用功能,将其复用为GPIO1_IO03 */ 
162     writel(5, SW_MUX_GPIO1_IO03); 
163     writel(0x10B0, SW_PAD_GPIO1_IO03); 
164  
165     /* 设置GPIO1_IO03为输出功能 */ 
166     val = readl(GPIO1_GDIR); 
167     val &= ~(1 << 3);               /* 清除以前的设置   */ 
168     val |= (1 << 3);                /* 设置为输出     */ 
169     writel(val, GPIO1_GDIR); 
170  
171     /* 默认关闭LED1 */ 
172     val = readl(GPIO1_DR); 
173     val |= (1 << 3) ;    
174     writel(val, GPIO1_DR); 
175      
176     /* 注册字符设备驱动 */ 
177     /*1、创建设备号 */ 
178     if (leddev.major) {      /*  定义了设备号   */ 
179         leddev.devid = MKDEV(leddev.major, 0); 
180         register_chrdev_region(leddev.devid, LEDDEV_CNT, LEDDEV_NAME); 
181     } else {                  /* 没有定义设备号   */ 
182         alloc_chrdev_region(&leddev.devid, 0, LEDDEV_CNT,  LEDDEV_NAME);    
183         leddev.major = MAJOR(leddev.devid);  
184     } 
185      
186     /* 2、初始化cdev */ 
187     leddev.cdev.owner = THIS_MODULE; 
188     cdev_init(&leddev.cdev, &led_fops); 
189      
190     /* 3、添加一个cdev */ 
191     cdev_add(&leddev.cdev, leddev.devid, LEDDEV_CNT); 
192  
193     /* 4、创建类 */ 
194     leddev.class = class_create(THIS_MODULE, LEDDEV_NAME); 
195     if (IS_ERR(leddev.class)) { 
196         return PTR_ERR(leddev.class); 
197     } 
198  
199     /* 5、创建设备 */ 
200     leddev.device = device_create(leddev.class, NULL, leddev.devid,NULL,LEDDEV_NAME); 
201     if (IS_ERR(leddev.device)) { 
202         return PTR_ERR(leddev.device); 
203     } 
204  
205     return 0; 
206 } 
207  
208 /* 
209  * @description   :移除platform驱动的时候此函数会执行 
210  * @param - dev   : platform设备 
211  * @return         : 0,成功;其他负值,失败 
212  */ 
213 static int led_remove(struct platform_device *dev) 
214 { 
215     iounmap(IMX6U_CCM_CCGR1); 
216     iounmap(SW_MUX_GPIO1_IO03); 
217     iounmap(SW_PAD_GPIO1_IO03); 
218     iounmap(GPIO1_DR); 
219     iounmap(GPIO1_GDIR); 
220  
221     cdev_del(&leddev.cdev);   /*  删除cdev */ 
222     unregister_chrdev_region(leddev.devid, LEDDEV_CNT);  
223     device_destroy(leddev.class, leddev.devid); 
224     class_destroy(leddev.class); 
225     return 0; 
226 } 
227  
228 /* platform驱动结构体 */ 
229 static struct platform_driver led_driver = { 
230     .driver     = { 
231         .name   = "imx6ul-led",         /* 驱动名字,用于和设备匹配 */ 
232     }, 
233     .probe      = led_probe, 
234     .remove     = led_remove, 
235 }; 
236          
237 /* 
238  * @description   : 驱动模块加载函数 
239  * @param         : 无 
240  * @return        : 无 
241  */ 
242 static int __init leddriver_init(void) 
243 { 
244     return platform_driver_register(&led_driver); 
245 } 
246  
247 /* 
248  * @description   : 驱动模块卸载函数 
249  * @param        : 无 
250  * @return        : 无 
251  */ 
252 static void __exit leddriver_exit(void) 
253 { 
254     platform_driver_unregister(&led_driver); 
255 } 
256  
257 module_init(leddriver_init); 
258 module_exit(leddriver_exit); 
259 MODULE_LICENSE("GPL"); 
260 MODULE_AUTHOR("zuozhongkai");

leddriver.c文件内容就是按照示例代码的platform驱动模板编写的。

第34~122行,传统的字符设备驱动。

第130~206行,probe函数,当设备和驱动匹配以后此函数就会执行,当匹配成功以后会在

终端上输出"led driver and device has matched!"这样语句。在probe函数里面初始化LED、注

册字符设备驱动。也就是将原来在驱动加载函数里面做的工作全部放到probe函数里面完成。

第213~226行,remobe函数,当卸载platform驱动的时候此函数就会执行。在此函数里面

释放内存、注销字符设备等。也就是将原来驱动卸载函数里面的工作全部都放到remove函数中

完成。

第 229~235行,platform_driver驱动结构体,注意name字段为"imx6ul-led",和我们在

leddevice.c文件里面设置的设备name字段一致。

第242~245行,驱动模块加载函数,在此函数里面通过platform_driver_register向Linux内

核注册led_driver驱动。

第252~255行,驱动模块卸载函数,在此函数里面通过platform_driver_unregister从Linux

内核卸载led_driver驱动。

3.2 测试APP编写

测试APP的内容很简单,就是打开和关闭LED灯,新建ledApp.c这个文件,然后在里面

输入如下内容:

cpp 复制代码
1  #include "stdio.h" 
2  #include "unistd.h" 
3  #include "sys/types.h" 
4  #include "sys/stat.h" 
5  #include "fcntl.h" 
6  #include "stdlib.h" 
7  #include "string.h" 
8  /*************************************************************** 
9  Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved. 
10 文件名      : ledApp.c 
11 作者         : 左忠凯 
12 版本         : V1.0 
13 描述         : platform驱动驱测试APP。 
14 其他         : 无 
15 使用方法     :./ledApp /dev/platled  0 关闭LED 
16               ./ledApp /dev/platled  1 打开LED      
17 论坛         : www.openedv.com 
18 日志         : 初版V1.0 2019/8/16 左忠凯创建 
19 ***************************************************************/ 
20 #define LEDOFF   0 
21 #define LEDON    1 
 
22 /* 
23  * @description   : main主程序 
24  * @param - argc   : argv数组元素个数 
25  * @param - argv   : 具体参数 
26  * @return          : 0 成功;其他 失败 
27  */ 
28 int main(int argc, char *argv[]) 
29 { 
30     int fd, retvalue; 
31     char *filename; 
32     unsigned char databuf[2]; 
33 
34     if(argc != 3){ 
35         printf("Error Usage!\r\n"); 
36         return -1; 
37     } 
38 
39     filename = argv[1]; 
40     /* 打开led驱动 */ 
41     fd = open(filename, O_RDWR); 
42     if(fd < 0){ 
43         printf("file %s open failed!\r\n", argv[1]); 
44         return -1; 
45     } 
46 
47     databuf[0] = atoi(argv[2]); /* 要执行的操作:打开或关闭 */ 
48     retvalue = write(fd, databuf, sizeof(databuf)); 
49     if(retvalue < 0){ 
50         printf("LED Control Failed!\r\n"); 
51         close(fd); 
52         return -1; 
53     } 
54 
55     retvalue = close(fd); /* 关闭文件 */ 
56     if(retvalue < 0){ 
57         printf("file %s close failed!\r\n", argv[1]); 
58         return -1; 
59     } 
60 
61     return 0; 
62 }

输入如下命令加载leddevice.ko设备模块和leddriver.ko这个驱动模块。

cpp 复制代码
depmod  //第一次加载驱动的时候需要运行此命令 
modprobe leddevice.ko  //加载设备模块 
modprobe leddriver.ko  //加载驱动模块 

根文件系统中/sys/bus/platform/目录下保存着当前板子platform总线下的设备和驱动,其中

devices子目录为platform设备,drivers子目录为plartofm驱动。查看/sys/bus/platform/devices/

目录,看看我们的设备是否存在,我们在leddevice.c中设置leddevice(platform_device类型)的

name字段为"imx6ul-led",也就是设备名字为imx6ul-led,因此肯定在/sys/bus/platform/devices/

目录下存在一个名字"imx6ul-led"的文件,否则说明我们的设备模块加载失败,结果如图

同理,查看/sys/bus/platform/drivers/目录,看一下驱动是否存在,我们在leddriver.c中设置

led_driver (platform_driver类型)的name字段为"imx6ul-led",因此会在/sys/bus/platform/drivers/

目录下存在名为"imx6ul-led"这个文件,结果如图

驱动模块和设备模块加载成功以后platform总线就会进行匹配,当驱动和设备匹配成功以

后就会输出如图所示一行语句:

相关推荐
m0_619731192 小时前
linux配置数据库
linux·运维·数据库
duangww2 小时前
Linux设置定时作业执行node.js脚本
linux·node.js·sap fiori
scilwb2 小时前
Isaac Sim机械臂教程 - 阶段2:场景构建与物体创建
linux
Code Warrior3 小时前
【Linux】Ext系列文件(1)
linux
绛洞花主敏明3 小时前
全缓冲和行缓冲
linux·运维·服务器
文慧的科技江湖3 小时前
开源 | 充电桩 运维 管理平台(IoT+运维工单平台)功能清单 - 慧知开源充电桩平台
运维·分布式·物联网·机器人·开源·充电桩平台
xx.ii3 小时前
59.keepalived实现高可用
运维·nginx·负载均衡
路弥行至3 小时前
C语言入门教程 | 第四讲:深入理解数制与码制,掌握基本数据类型的奥秘
服务器·c语言·开发语言·经验分享·笔记·其他·入门教程
艾莉丝努力练剑3 小时前
【Linux指令 (一)】Linux 命令行入门:从零开始理解Linux系统理论核心概念与基础指令
linux·c++·经验分享·ubuntu·centos