7. Linux平台设备驱动

在之前的字符设备程序中驱动程序,我们只要调用open()函数打开了相应的设备文件,就可以使用read()/write()函数, 通过file_operations这个文件操作接口来进行硬件的控制。这种驱动开发方式简单直观,但是从软件设计的角度看,却是一种十分糟糕的方式。

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

为了解决这种驱动代码和设备信息耦合的问题,linux对这些设备进行了更进一步的封装,抽象出设备驱动模型。 前面章节我们已经对设备驱动模型进行了深入剖析,在设备驱动模型中,引入总线的概念可以对驱动代码和设备信息进行分离。 但是驱动中总线的概念是软件层面的一种抽象,与我们SOC中物理总线的概念并不严格相等:

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

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

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

平台总线用于管理、挂载那些没有相应物理总线的设备,这些设备被称为平台设备,对应的设备驱动则被称为平台驱动。 平台设备驱动的核心依然是Linux设备驱动模型,平台设备使用platform_device结构体来进行表示,其继承了设备驱动模型中的device结构体。 而平台驱动使用platform_driver结构体来进行表示,其则是继承了设备驱动模型中的device_driver结构体。

重点学习:总线的匹配机制、如何填充设备驱动和设备信息、理解平台设备驱动和字符设备的关系。

7.1. 平台设备

7.1.1. platform_device结构体

内核使用platform_device结构体来描述平台设备,结构体原型如下:

platform_device结构体(内核源码/include/linux/platform_device.h)
1
2
3
4
5
6
7
8
9
 struct platform_device {
     const char *name;
     int id;
     struct device dev;
     u32 num_resources;
     struct resource *resource;
     const struct platform_device_id *id_entry;
     /* 省略部分成员 */
 };
  • name: 设备名称,总线进行匹配时,会比较设备和驱动的名称是否一致;

  • id: 指定设备的编号,Linux支持同名的设备,而同名设备之间则是通过该编号进行区分;

  • dev: Linux设备模型中的device结构体,linux内核大量使用了面向对象思想,platform_device通过继承该结构体可复用它的相关代码,方便内核管理平台设备;

  • num_resources: 记录资源的个数,当结构体成员resource存放的是数组时,需要记录resource数组的个数,内核提供了宏定义ARRAY_SIZE用于计算数组的个数;

  • resource: 平台设备提供给驱动的资源,如irq,dma,内存等等。该结构体会在接下来的内容进行讲解;

  • id_entry: 平台总线提供的另一种匹配方式,原理依然是通过比较字符串,这部分内容会在平台总线小节中讲,这里的id_entry用于保存匹配的结果;

7.1.2. 何为设备信息?

平台设备的工作是为驱动程序提供设备信息,设备信息包括硬件信息和软件信息两部分。

  • 硬件信息:驱动程序需要使用到什么寄存器,占用哪些中断号、内存资源、IO口等等;

  • 软件信息:以太网卡设备中的MAC地址、I2C设备中的设备地址、SPI设备的片选信号线等等。

对于硬件信息,使用结构体struct resource来保存设备所提供的资源,比如设备使用的中断编号,寄存器物理地址等,结构体原型如下:

resource结构体(内核源码/include/linux/ioport.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/*
* Resources are tree-like, allowing
* nesting etc..
*/

struct resource {
    resource_size_t start;
    resource_size_t end;
    const char *name;
    unsigned long flags;
    /* 省略部分成员 */
};
  • name: 指定资源的名字,可以设置为NULL;

  • start、end: 指定资源的起始地址以及结束地址;

  • flags: 用于指定该资源的类型,在Linux中,资源包括I/O、Memory、Register、IRQ、DMA、Bus等多种类型,最常见的有以下几种:

资源宏定义

描述

IORESOURCE_IO

用于IO地址空间,对应于IO端口映射方式

IORESOURCE_MEM

用于外设的可直接寻址的地址空间

IORESOURCE_IRQ

用于指定该设备使用某个中断

IORESOURCE_DMA

用于指定使用的DMA通道

设备驱动程序的主要目的是操作设备的寄存器。不同架构的计算机提供不同的操作接口,主要有IO端口映射和IO內存映射两种方式。 对应于IO端口映射方式,只能通过专门的接口函数(如inb、outb)才能访问; 采用IO内存映射的方式,可以像访问内存一样,去读写寄存器。在嵌入式中,基本上没有IO地址空间,所以通常使用IORESOURCE_MEM。

在资源的起始地址和结束地址中,对于IORESOURCE_IO或者是IORESOURCE_MEM,他们表示要使用的内存的起始位置以及结束位置; 若是只用一个中断引脚或者是一个通道,则它们的start和end成员值必须是相等的。

而对于软件信息,这种特殊信息需要我们以私有数据的形式进行封装保存,我们注意到platform_device结构体中, 有个device结构体类型的成员dev。在前面章节,我们提到过Linux设备模型使用device结构体来抽象物理设备, 该结构体的成员platform_data可用于保存设备的私有数据。platform_data是void *类型的万能指针, 无论你想要提供的是什么内容,只需要把数据的地址赋值给platform_data即可, 还是以GPIO引脚号为例,示例代码如下:

示例代码
1
2
3
4
5
6
7
unsigned int pin = 10;

struct platform_device pdev = {
    .dev = {
        .platform_data = &pin;
    }
}

将保存了GPIO引脚号的变量pin地址赋值给platform_data指针,在驱动程序中通过调用平台设备总线中的核心函数,可以获取到我们需要的引脚号。

重要

对于硬件信息的描述,新方法都是通过设备树来描述,具体可以参考后面Linux设备树和设备树插件章节。

7.1.3. 注册/注销平台设备

当我们定义并初始化好platform_device结构体后,需要把它注册、挂载到平台设备总线上。注册平台设备需要使用platform_device_register()函数,该函数原型如下:

platform_device_register函数(内核源码/drivers/base/platform.c)
1
int platform_device_register(struct platform_device *pdev)

函数参数和返回值如下:

参数: pdev: platform_device类型结构体指针

返回值:

  • 成功: 0

  • 失败: 负数

同样,当需要注销、移除某个平台设备时,我们需要使用platform_device_unregister函数,来通知平台设备总线去移除该设备。

platform_device_unregister函数(内核源码/drivers/base/platform.c)
1
void platform_device_unregister(struct platform_device *pdev)

函数参数和返回值如下:

参数: pdev: platform_device类型结构体指针

返回值:

到这里,平台设备的知识已经讲解完毕,平台设备的主要内容是将硬件部分的代码与驱动部分的代码分开,注册到平台设备总线中。平台设备总线为设备和驱动之间搭建 了一座桥——统一的数据结构以及函数接口,设备和驱动的数据交互直接在“这座桥上”进行。

7.2. 平台设备驱动

平台设备驱动是专用于平台总线的设备,而传统的总线设备,比如I2C或者SPI设备是平台设备,依赖的是I2C或者SPI总线。

7.2.1. platform_driver结构体

内核中使用platform_driver结构体来描述平台驱动,结构体原型如下所示:

platform_driver结构体(内核源码/include/platform_device.h)
1
2
3
4
5
6
7
8
struct platform_driver {

    int (*probe)(struct platform_device *);
    int (*remove)(struct platform_device *);
    struct device_driver driver;
    const struct platform_device_id *id_table;
    .......
};
  • probe: 函数指针,驱动开发人员需要在驱动程序中初始化该函数指针,当总线为设备和驱动匹配上之后,会回调执行该函数。我们一般通过该函数,对设备进行一系列的初始化。

  • remove: 函数指针,驱动开发人员需要在驱动程序中初始化该函数指针,当我们移除某个平台设备时,会回调执行该函数指针,该函数实现的操作,通常是probe函数实现操作的逆过程。

  • driver: Linux设备模型中用于抽象驱动的device_driver结构体,platform_driver继承该结构体,也就获取了设备模型驱动对象的特性;

  • id_table: 表示该驱动能够兼容的设备类型。

platform_device_id结构体原型如下所示:

id_table结构体(内核源码/include/linux/mod_devicetable.h)
1
2
3
4
5
struct platform_device_id {
    char name[PLATFORM_NAME_SIZE];
    kernel_ulong_t driver_data;

};

在platform_device_id这个结构体中,有两个成员,第一个是数组用于指定驱动的名称,总线进行匹配时,会依据该结构体的name成员与platform_device中的变量name进行比较匹配, 另一个成员变量driver_data,则是用于来保存设备的配置。我们知道在同系列的设备中,往往只是某些寄存器的配置不一样,为了减少代码的冗余, 尽量做到一个驱动可以匹配多个设备的目的。接下来以imx芯片的串口为例,具体看下这个结构体的作用:

示例代码(内核源码/drivers/tty/serial/imx.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
static struct imx_uart_data imx_uart_devdata[] = {

    [IMX1_UART] = {
        .uts_reg = IMX1_UTS,
        .devtype = IMX1_UART,
    },

    [IMX21_UART] = {
        .uts_reg = IMX21_UTS,
        .devtype = IMX21_UART,
    },

    [IMX6Q_UART] = {
        .uts_reg = IMX21_UTS,
        .devtype = IMX6Q_UART,
    },

};

static struct platform_device_id imx_uart_devtype[] = {

    {
        .name = "imx1-uart",
        .driver_data = (kernel_ulong_t) &imx_uart_devdata[IMX1_UART],
    },

    {
        .name = "imx21-uart",
        .driver_data = (kernel_ulong_t) &imx_uart_devdata[IMX21_UART],
    },

    {
        .name = "imx6q-uart",
        .driver_data = (kernel_ulong_t) &imx_uart_devdata[IMX6Q_UART],

    },

    {
        /* sentinel */

    }

};
  • 第1-18行:声明了一个结构体数组,用来表示不同平台的串口类型。

  • 第20-42行:使用platform_device_id结构体中的driver_data成员来储存上面的串口信息

在上面的代码中,支持三种设备的串口,支持imx1、imx21、imx6q三种不同系列芯片,他们之间区别在于串口的test寄存器地址不同。 当总线成功配对平台驱动以及平台设备时,会将对应的id_table条目赋值给平台设备的id_entry成员,而平台驱动的probe函数是以平台设备为参数, 这样的话,就可以拿到当前设备串口的test寄存器地址了。

7.2.2. 注册/注销平台驱动

当我们初始化了platform_driver之后,通过platform_driver_register()函数来注册我们的平台驱动,该函数原型如下:

platform_driver_register函数
1
int platform_driver_register(struct platform_driver *drv);

函数参数和返回值如下:

参数: drv: platform_driver类型结构体指针

返回值:

  • 成功: 0

  • 失败: 负数

由于platform_driver继承了driver结构体,结合Linux设备模型的知识, 当成功注册了一个平台驱动后,就会在/sys/bus/platform/driver目录下生成一个新的目录项。

当卸载的驱动模块时,需要注销掉已注册的平台驱动,platform_driver_unregister()函数用于注销已注册的平台驱动,该函数原型如下:

platform_driver_unregister函数(内核源码/drivers/base/platform.c)
1
void platform_driver_unregister(struct platform_driver *drv);

参数: drv: platform_driver类型结构体指针

返回值:

上面所讲的内容是最基本的平台驱动框架,只需要实现probe函数、remove函数,初始化platform_driver结构体,并调用platform_driver_register进行注册即可。

7.2.3. 平台驱动获取设备信息

在学习平台设备的时候,我们知道平台设备使用结构体resource来抽象表示硬件信息,而软件信息则可以利用设备结构体device中的成员platform_data来保存。 先看一下如何获取平台设备中结构体resource提供的资源。

platform_get_resource()函数通常会在驱动的probe函数中执行,用于获取平台设备提供的资源结构体,最终会返回一个struct resource类型的指针,该函数原型如下:

platform_get_resource函数
1
struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num);

参数:

  • dev: 指定要获取哪个平台设备的资源;

  • type: 指定获取资源的类型,如IORESOURCE_MEM、IORESOURCE_IO等;

  • num: 指定要获取的资源编号。每个设备所需要资源的个数是不一定的,为此内核对这些资源进行了编号,对于不同的资源,编号之间是相互独立的。

返回值:

  • 成功: struct resource结构体类型指针

  • 失败: NULL

假若资源类型为IORESOURCE_IRQ,平台设备驱动还提供以下函数接口,来获取中断引脚,

platform_get_irq函数
1
int platform_get_irq(struct platform_device *pdev, unsigned int num)

参数:

  • pdev: 指定要获取哪个平台设备的资源;

  • num: 指定要获取的资源编号。

返回值:

  • 成功: 可用的中断号

  • 失败: 负数

对于存放在device结构体中成员platform_data的软件信息,我们可以使用dev_get_platdata函数来获取,函数原型如下所示:

dev_get_platdata函数
1
2
3
4
static inline void *dev_get_platdata(const struct device *dev)
{
    return dev->platform_data;
}

参数:

  • dev: struct device结构体类型指针

返回值: device结构体中成员platform_data指针

以上几个函数接口就是如何从平台设备中获取资源的常用的几个函数接口,到这里平台驱动部分差不多就结束了。总结一下平台驱动需要 实现probe函数,当平台总线成功匹配驱动和设备时,则会调用驱动的probe函数,在该函数中使用上述的函数接口来获取资源, 以初始化设备,最后填充结构体platform_driver,调用platform_driver_register进行注册。

7.3. 平台总线

7.3.1. 平台总线注册和匹配方式

在Linux的设备驱动模型中,总线是最重要的一环。上一章中,我们提到过总线是负责匹配设备和驱动, 它维护着两个链表,里面记录着各个已经注册的平台设备和平台驱动。每当有新的设备或者是新的驱动加入到总线时, 总线便会调用platform_match函数对新增的设备或驱动,进行配对。内核中使用bus_type来抽象描述系统中的总线,平台总线结构体原型如下所示:

platform_bus_type结构体(内核源码/driver/base/platform.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct bus_type platform_bus_type = {

    .name           = "platform",
    .dev_groups     = platform_dev_groups,
    .match          = platform_match,
    .uevent         = platform_uevent,
    .pm             = &platform_dev_pm_ops,

};

EXPORT_SYMBOL_GPL(platform_bus_type);

内核用platform_bus_type来描述平台总线,该总线在linux内核启动的时候自动进行注册。

platform_bus_init函数(内核源码/driver/base/platform.c)
1
2
3
4
5
6
7
8
int __init platform_bus_init(void)
{
    int error;
    ...
    error =  bus_register(&platform_bus_type);
    ...
    return error;
}

第5行:向linux内核注册platform平台总线

这里重点是platform总线的match函数指针,该函数指针指向的函数将负责实现平台总线和平台设备的匹配过程。对于每个驱动总线, 它都必须实例化该函数指针。platform_match的函数原型如下:

platform_match函数(内核源码/driver/base/platform.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static int platform_match(struct device *dev, struct device_driver *drv)
{

    struct platform_device *pdev = to_platform_device(dev);
    struct platform_driver *pdrv = to_platform_driver(drv);

    /* When driver_override is set, only bind to the matching driver */
    if (pdev->driver_override)
        return !strcmp(pdev->driver_override, drv->name);

    /* Attempt an OF style match first */
    if (of_driver_match_device(dev, drv))
        return 1;

    /* Then try ACPI style match */
    if (acpi_driver_match_device(dev, drv))
        return 1;

    /* Then try to match against the id table */
    if (pdrv->id_table)
        return platform_match_id(pdrv->id_table, pdev) != NULL;

    /* fall-back to driver name match */
    return (strcmp(pdev->name, drv->name) == 0);

}
  • 第4-5行:这里调用了to_platform_device()和to_platform_driver()宏。这两个宏定义的原型如下:

to_platform_xxx宏定义(内核源码/include/linux/platform_device.h)
1
2
#define to_platform_device(x)     (container_of((x), struct platform_device, dev)
#define to_platform_driver(drv)   (container_of((drv), struct platform_driver, driver))

其中,to_platform_device和to_platform_driver实现了对container_of的封装, dev、driver分别作为platform_device、platform_driver的成员变量, 通过container_of宏可以获取到正在进行匹配的platform_driver和platform_device。

  • 第8-21行:platform总线提供了四种匹配方式,并且这四种方式存在着优先级:设备树机制>ACPI匹配模式>id_table方式>字符串比较。 虽然匹配方式五花八门,但是并没有涉及到任何复杂的算法,都只是在匹配的过程中,比较一下设备和驱动提供的某个成员的字符串是否相同。 设备树是一种描述硬件的数据结构,它用一个非C语言的脚本来描述这些硬件设备的信息。驱动和设备之间的匹配时通过比较compatible的值。 acpi主要是用于电源管理,基本上用不到,这里就并不进行讲解了。关于设备树的匹配机制,会在设备树章节进行详细分析。

7.3.2. id_table匹配方式

在这一章节,我们先来分析平台总线id_table匹配方式,在定义结构体platform_driver时,我们需要提供一个id_table的数组,该数组说明了当前的驱动能够支持的设备。当加载该驱动时,总线的match函数发现id_table非空, 则会比较id_table中的name成员和平台设备的name成员,若相同,则会返回匹配的条目,具体的实现过程如下:

platform_match_id函数(内核源码/drivers/base/platform.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
static const struct platform_device_id *platform_match_id(
            const struct platform_device_id *id,
            struct platform_device *pdev)

{
    while (id->name[0]) {
        if (strcmp(pdev->name, id->name) == 0) {
            pdev->id_entry = id;
            return id;
        }
        id++;
    }
    return NULL;
}

大家可以看到这里的代码实现并不复杂,只是通过字符串进行配对。每当有新的驱动或者设备添加到总线时,总线便会调用match函数对新的设备或者驱动进行配对。 platform_match_id函数中第一个参数为驱动提供的id_table,第二个参数则是待匹配的平台设备。当待匹配的平台设备的name字段的值等于驱动提供的id_table中的值时, 会将当前匹配的项赋值给platform_device中的id_entry,返回一个非空指针。若没有成功匹配,则返回空指针。

驱动和设备匹配过程

倘若我们的驱动没有提供前三种方式的其中一种,那么总线进行匹配时,只能比较platform_device中的name字段以及嵌在platform_driver中的device_driver的name字段。

名称匹配方式

如果匹配驱动成功,则将调用已匹配驱动程序的probe函数,并传递struct platform_device参数。

7.4. 平台设备实验代码讲解

本节实验使用Lubancat_RK板卡进行操作,本节将会把平台设备驱动应用到LED字符设备驱动的代码中,实现硬件与软件代码相分离,巩固平台设备驱动的学习,驱动led相关,详细参考”字符设备驱动–点亮LED灯”章节。

示例代码目录为: linux_driver/07_platform_driver

7.4.1. 编程思路

  1. 编写第一个内核模块pdev_led.c;

  2. 在内核模块中定义一个平台设备,并填充LED灯相关设备信息;

  3. 在该模块入口函数,注册/挂载这个平台设备;

  4. 编写第二个内核模块pdrv_led.c;

  5. 在内核模块中定义一个平台驱动,在probe函数中完成字符设备驱动的创建;

  6. 在该模块入口函数,注册/挂载这个平台驱动。

在平台设备总线上,注册/挂载平台设备和平台驱动时,会自动进行配对。配对成功后,回调执行平台驱动的probe函数, 从而完成字符设备驱动的创建。

7.4.2. 代码分析

7.4.2.1. 定义平台设备

我们需要将字符设备中的硬件信息提取出来,独立成一份代码,将其作为平台设备,注册到内核中。点亮LED灯,需要控制与LED灯相关的寄存器, 包括GPIO时钟寄存器,IO配置寄存器,IO数据寄存器等,这里的资源,实际上就是寄存器地址,可以使用IORESOURCE_MEM进行处理; 除了这些之外,还需要提供一些寄存器的偏移量,我们可以利用平台设备的私有数据进行管理。

寄存器宏定义(位于linux_driver/07_platform_driver/pdev_led.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//参考字符设备led驱动,GPIO0_C7引脚
/* 定义 GPIO 寄存器的基地址 */
#define GPIO_BASE (0xfdd60000)
/* 定义 GPIO 数据寄存器低 16 位的地址 */
#define GPIO_DR_L (GPIO_BASE + 0x0000)
/* 定义 GPIO 数据寄存器高 16 位的地址 */
#define GPIO_DR_H (GPIO_BASE + 0x0004)
/* 定义 GPIO 数据方向寄存器低 16 位的地址 */
#define GPIO_DDR_L (GPIO_BASE + 0x0008)
/* 定义 GPIO 数据方向寄存器高 16 位的地址 */
#define GPIO_DDR_H (GPIO_BASE + 0x000C)
  • 使用宏定义来对GPIO引脚的寄存器进行封装。

定义一个resource结构体,用于存放上述的寄存器地址,提供给驱动使用,如下所示:

定义资源数组(位于linux_driver/07_platform_driver/pdev_led.c)
1
2
3
4
5
6
static struct resource pdev_led_resource[] = {
    /* 定义 GPIO 数据寄存器高 16 位的内存资源,起始地址为 GPIO_DR_H,长度为 4 字节 */
    [0] = DEFINE_RES_MEM(GPIO_DR_H, 4),
    /* 定义 GPIO 数据方向寄存器高 16 位的内存资源,起始地址为 GPIO_DDR_H,长度为 4 字节 */
    [1] = DEFINE_RES_MEM(GPIO_DDR_H, 4),
};
  • 在内核源码/include/linux/ioport.h中,提供了宏定义DEFINE_RES_MEM、DEFINE_RES_IO、DEFINE_RES_IRQ和DEFINE_RES_DMA,用来定义所需要的资源类型。 DEFINE_RES_MEM用于定义IORESOURCE_MEM类型的资源,我们只需要传入两个参数,一个是寄存器地址,另一个是大小。从手册上看,可以得知一个寄存器都是32位的,因此, 这里我们选择需要4个字节大小的空间。pdev_led_resource资源数组中,我们将所有的MEM资源进行了编号,0对应了GPIO_DR_H,1对应了GPIO_DDR_H,驱动到时候就可以根据这些编号获得对应的寄存器地址。

定义平台设备的私有数据(位于linux_driver/07_platform_driver/pdev_led.c)
1
unsigned int pdev_led_hwinfo[1] = { 7 };  /* 偏移,高 16 引脚,GPIO0_C7 */
  • 使用一个数组pdev_led_hwinfo,来记录寄存器的偏移量,填充平台私有数据时,只需要把数组的首地址赋给platform_data即可。

关于设备的硬件信息,我们已经全部完成了,接下来只需要定义一个platform_device类型的变量,填充相关信息。

定义平台设备(位于linux_driver/07_platform_driver/pdev_led.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static void pdev_led_release(struct device *dev)
{
    /* 打印平台设备释放信息 */
    printk("pdev release\r\n");
}

/* 定义平台设备结构体 */
static struct platform_device pdev_led = {
    /* 平台设备的名称,用于和平台驱动进行匹配 */
    .name = "pdev_led",
    /* 平台设备的 ID */
    .id = 0,
    /* 平台设备的资源数量,通过 ARRAY_SIZE 宏计算 pdev_led_resource 数组的大小 */
    .num_resources = ARRAY_SIZE(pdev_led_resource),
    /* 指向平台设备的资源数组 */
    .resource = pdev_led_resource,
    /* 平台设备的设备结构体 */
    .dev = {
        /* 设备释放函数指针,指向 pdev_led_release 函数 */
        .release = pdev_led_release,
        /* 平台设备的私有数据,指向 pdev_led_hwinfo 数组 */
        .platform_data = pdev_led_hwinfo,
    },
};
  • 第10-12行:定义了一个设备名为“pdev_led”的设备,这里的名字确保要和驱动的名称保持一致,否则就会导致匹配失败。id编号设置为0,驱动会利用该编号来注册设备。

  • 第14-16行:将上面实现好的pdev_led_resource数组赋值给resource成员,同时,我们还需要指定资源的数量,内核提供了宏定义ARRAY_SIZE,用于计算数组长度, 因此,num_resources直接赋值为ARRAY_SIZE(rled_resource)。

  • 第18-23行:对dev中的成员进行赋值,将pdev_led_hwinfo存储到platform_data中。

最后,只需要在模块加载的函数中调用platform_device_register函数,这样,当加载该内核模块时,新的平台设备就会被注册到内核中去,实现方式如下:

模块初始化(位于linux_driver/07_platform_driver/pdev_led.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
static __init int pdev_led_init(void)
{
    /* 打印平台设备初始化信息 */
    printk("pdev init\n");
    /* 注册平台设备,将 pdev_led 平台设备注册到内核中 */
    platform_device_register(&pdev_led);
    return 0;
}

static __exit void pdev_led_exit(void)
{
    /* 打印平台设备退出信息 */
    printk("pdev exit\n");
    /* 注销平台设备,将 pdev_led 平台设备从内核中注销 */
    platform_device_unregister(&pdev_led);
}

module_init(pdev_led_init);
module_exit(pdev_led_exit);
  • 第1-8行:实现模块的入口函数,打印信息并注册平台设备

  • 第10-16行:实现模块的出口函数,打印信息并注销设备

这样,我们就实现了一个新的设备,只需要在开发板上加载该模块,平台总线下就会挂载我们LED灯的平台设备。

7.4.2.2. 定义平台驱动

我们已经注册了一个新的平台设备,驱动只需要提取该设备提供的资源,并提供相应的操作方式即可。这里我们仍然采用字符设备来控制我们的LED灯, 想必大家对于LED灯字符设备的代码已经很熟悉了,对于这块的代码就不做详细介绍了,让我们把重点放在平台驱动上。

我们驱动提供id_table的方式,来匹配设备。我们定义一个platform_device_id类型的变量led_pdev_ids,说明驱动支持哪些设备, 这里我们只支持一个设备,名称为led_pdev,要与平台设备提供的名称保持一致。

id_table(位于linux_driver/07_platform_driver/pdrv_led.c)
1
2
3
4
5
6
7
8
/* 定义平台设备 ID 表,用于平台总线的匹配 */
static struct platform_device_id pdev_led_ids[] = {
    {.name = "pdev_led"},
    {}
};

/* 声明平台设备 ID 表,供内核使用 */
MODULE_DEVICE_TABLE(platform, pdev_led_ids);
  • 这块代码提供了驱动支持哪些设备;

  • MODULE_DEVICE_TABLE()这个宏让驱动程序公开其ID表,该表描述它可以支持哪些设备,用于匹配设备。

这仅仅完成了第一个内容,这是总线进行匹配时所需要的内容。而在匹配成功之后,驱动需要去提取设备的资源, 这部分工作都是在probe函数中完成。由于我们采用字符设备的框架,因此,在probe过程,还需要完成字符设备的注册等工作,具体实现的代码如下:

pdrv_led_probe函数(位于linux_driver/07_platform_driver/pdrv_led.c)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
/* 定义字符设备的名称 */
#define DEV_NAME    "pdrv_led"
/* 定义字符设备的数量 */
#define DEV_CNT     1

/* 定义设备号变量 */
static dev_t devno;
/* 定义设备类指针 */
static struct class *class;
/* 定义设备指针 */
static struct device *device;

/* 定义 LED 字符设备结构体 */
struct led_chrdev {
    /* 字符设备结构体 */
    struct cdev dev;
    /* 数据寄存器的虚拟地址,用于设置输出的电压 */
    unsigned int __iomem *va_dr;
    /* 数据方向寄存器的虚拟地址,用于设置输入或者输出 */
    unsigned int __iomem *va_ddr;
    /* 引脚偏移量 */
    unsigned int led_pin;
};
/* 定义 led_chrdev 结构体指针,用于动态分配内存管理 LED 硬件信息 */
static struct led_chrdev *led_cdev;

static int pdrv_led_probe(struct platform_device *pdev)
{
    /* 定义引脚偏移变量 */
    unsigned int *pdev_led_hwinfo;
    /* 定义数据寄存器的虚拟地址变量*/
    struct resource *mem_dr;
    /* 定义数据方向寄存器的虚拟地址变量 */
    struct resource *mem_ddr;
    /* 定义返回值变量 */
    int ret = 0;
    /* 定义主设备号变量 */
    int major;
    /* 定义次设备号变量 */
    int minor;

    /* 定义临时变量,用于存储寄存器的值 */
    unsigned int val = 0;

    /* 打印平台驱动探测信息 */
    printk("led platform driver probe\n");

    /* 第一步:提取平台设备提供的资源 */
    /* 使用 devm_kzalloc 函数为 led_chrdev 结构体分配内存并清零 */
    led_cdev = devm_kzalloc(&pdev->dev, sizeof(struct led_chrdev), GFP_KERNEL);
    if (!led_cdev)
        return -ENOMEM;

    /* 获取平台设备的私有数据,得到 LED 灯的引脚偏移量 */
    pdev_led_hwinfo = dev_get_platdata(&pdev->dev);
    if (!pdev_led_hwinfo) {
        return -ENOMEM;
    }
    led_cdev->led_pin = pdev_led_hwinfo[0];

    /* 获取平台设备的资源,得到 GPIO 数据寄存器和数据方向寄存器的资源结构体 */
    mem_dr = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    mem_ddr = platform_get_resource(pdev, IORESOURCE_MEM, 1);

    /* 使用 devm_ioremap 将物理地址映射为虚拟地址,注意该函数分配的内存会自动释放,不需要手动调用iounmap */
    led_cdev->va_dr = devm_ioremap(&pdev->dev, mem_dr->start, resource_size(mem_dr));
    led_cdev->va_ddr = devm_ioremap(&pdev->dev, mem_ddr->start, resource_size(mem_ddr));
    if (!led_cdev->va_dr || !led_cdev->va_ddr) {
        /* 打印映射失败信息 */
        printk("fail to ioremap GPIO registers\n");
        ret = -ENOMEM;
        /* 跳转到错误处理标签 */
        goto ioremap_err;
    }

    /* 设置输出模式 */
    /* 读取数据方向寄存器的值 */
    val = ioread32(led_cdev->va_ddr);
    /* 设置高 16 位的使能位 */
    val |= ((unsigned int)0x1 << (led_cdev->led_pin + 16));
    /* 设置低 16 位的对应引脚为输出模式 */
    val |= ((unsigned int)0x1 << (led_cdev->led_pin));
    /* 将修改后的值写回到数据方向寄存器 */
    iowrite32(val, led_cdev->va_ddr);

    /* 输出高电平 */
    /* 读取数据寄存器的值 */
    val = ioread32(led_cdev->va_dr);
    /* 设置高 16 位的使能位 */
    val |= ((unsigned int)0x1 << (led_cdev->led_pin + 16));
    /* 设置低 16 位的对应引脚输出高电平 */
    val |= ((unsigned int)0x1 << (led_cdev->led_pin));
    /* 将修改后的值写回到数据寄存器 */
    iowrite32(val, led_cdev->va_dr);

    /* 第二步:初始化注册字符设备 */
    /* 分配设备号 */
    ret = alloc_chrdev_region(&devno, 0, DEV_CNT, DEV_NAME);
    if (ret < 0) {
        /* 打印设备号分配失败信息 */
        printk("fail to alloc devno\n");
        /* 跳转到错误处理标签 */
        goto ioremap_err;
    }
    /* 获取主设备号 */
    major = MAJOR(devno);
    /* 获取次设备号 */
    minor = MINOR(devno);
    /* 打印主设备号和次设备号 */
    printk("major=%d, minor=%d\n", major, minor);

    /* 初始化字符设备 */
    cdev_init(&led_cdev->dev, &pdrv_led_fops);
    led_cdev->dev.owner = THIS_MODULE;

    /* 添加字符设备 */
    ret = cdev_add(&led_cdev->dev, devno, DEV_CNT);
    if (ret < 0) {
        /* 打印字符设备添加失败信息 */
        printk("fail to add cdev\n");
        /* 跳转到错误处理标签 */
        goto add_err;
    }

    /* 创建设备类 */
    class = class_create(THIS_MODULE, DEV_NAME);
    if (IS_ERR(class)) {
        /* 打印设备类创建失败信息 */
        printk("fail to create class\n");
        ret = PTR_ERR(class);
        /* 跳转到错误处理标签 */
        goto class_err;
    }

    /* 创建设备节点 */
    device = device_create(class, NULL, devno, NULL, DEV_NAME);
    if (IS_ERR(device)) {
        /* 打印设备节点创建失败信息 */
        printk("fail to create device\n");
        ret = PTR_ERR(device);
        /* 跳转到错误处理标签 */
        goto device_err;
    }

    /* 保存驱动数据 */
    platform_set_drvdata(pdev, led_cdev);

    return 0;

device_err:
    /* 销毁设备类 */
    class_destroy(class);

class_err:
    /* 删除字符设备 */
    cdev_del(&led_cdev->dev);

add_err:
    /* 释放设备号 */
    unregister_chrdev_region(devno, DEV_CNT);

ioremap_err:
    return ret;
}
  • 第14-25行:仍然使用结构体led_chrdev来管理我们LED灯的硬件信息,定义时钟寄存器的虚拟地址变量。

  • 第50-52行:使用devm_kzalloc函数申请cur_led和led_hwinfo结构体内存大小。

  • 第55行:使用dev_get_platdata函数获取私有数据。

  • 第62-63行:利用函数platform_get_resource可以获取到各个寄存器的地址。

  • 第66-67行:在内核中,这些地址并不能够直接使用,使用devm_ioremap将获取到的寄存器地址转化为虚拟地址,到这里我们就完成了提取资源的工作了。

  • 第98-143行:就需要注册一个LED字符设备了。开发板上板载了LED灯,使用MKDEV宏定义来创建一个设备编号,再调用alloc_chrdev_region、cdev_init、cdev_add等函数来注册字符设备。

  • 第146行:使用platform_set_drvdata函数,将LED数据信息存入在平台驱动结构体中。

当驱动的内核模块被卸载时,我们需要将注册的驱动注销,相应的字符设备也同样需要注销,具体的实现代码如下:

pdrv_led_remove函数(位于linux_driver/07_platform_driver/pdrv_led.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
static int pdrv_led_remove(struct platform_device *pdev)
{
    struct led_chrdev *led_cdev = platform_get_drvdata(pdev);

    /* 打印平台驱动移除信息 */
    printk("pdrv_led remove\n");

    /* 销毁设备节点 */
    device_destroy(class, devno);

    /* 删除字符设备 */
    cdev_del(&led_cdev->dev);

    /* 释放设备号 */
    unregister_chrdev_region(devno, DEV_CNT);

    /* 销毁设备类 */
    class_destroy(class);

    return 0;
}
  • 第3行:在probe函数中调用了platform_set_drvdata,将当前的LED灯数据结构体保存到pdev的driver_data成员中, 在这里调用platform_get_drvdata,获取当前LED灯对应的结构体,该结构体中包含了字符设备。

关于操作LED灯字符设备的方式,实现方式如下,这里只做简单介绍,具体介绍可以参阅LED灯字符设备章节的内容。

led灯的字符设备框架(位于linux_driver/07_platform_driver/pdrv_led.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
static int pdrv_led_open(struct inode *inode, struct file *filp)
{
    /* 通过 inode 中的 i_cdev 成员找到对应的 led_chrdev 结构体 */
    struct led_chrdev *led_cdev = container_of(inode->i_cdev, struct led_chrdev, dev);
    /* 将 led_chrdev 结构体指针存储到文件的私有数据中 */
    filp->private_data = led_cdev;

    /* 打印设备打开信息 */
    printk("pdrv_led open\n");

    return 0;
}

static int pdrv_led_release(struct inode *inode, struct file *filp)
{
    printk("pdrv_led release\r\n");
    return 0;
}

static ssize_t pdrv_led_write(struct file *filp, const char __user * buf,
                            size_t count, loff_t * ppos)
{
    unsigned long val = 0;
    unsigned long ret = 0;
    /* 从文件结构体的私有数据中获取 led_chrdev 结构体指针 */
    struct led_chrdev *led_cdev = filp->private_data;

    /* 打印设备写操作信息 */
    printk("pdrv_led write \r\n");

    /* 从用户空间读取一个字符 */
    get_user(ret, buf);
    /* 读取数据寄存器的值 */
    val = ioread32(led_cdev->va_dr);
    if (ret == '0') {
        /* 设置高 16 位的使能位 */
        val |= ((unsigned int)0x1 << (led_cdev->led_pin + 16));
        /* 设置低 16 位的对应引脚输出低电平 */
        val &= ~((unsigned int)0x01 << (led_cdev->led_pin));
    } else {
        /* 设置高 16 位的使能位 */
        val |= ((unsigned int)0x1 << (led_cdev->led_pin + 16));
        /* 设置低 16 位的对应引脚输出高电平 */
        val |= ((unsigned int)0x01 << (led_cdev->led_pin));
    }
    /* 将修改后的值写回到数据寄存器 */
    iowrite32(val, led_cdev->va_dr);
    return count;
}

static ssize_t pdrv_led_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)
{
    return 0;
}

static int pdrv_led_release(struct inode *inode, struct file *filp)
{
    printk("pdrv_led release\r\n");
    return 0;
}

/* 定义字符设备的文件操作结构体 */
static struct file_operations pdrv_led_fops = {
    .owner = THIS_MODULE,
    .open = pdrv_led_open,
    .release = pdrv_led_release,
    .write = pdrv_led_write,
    .read = pdrv_led_read,
};

编写led字符设备的操作之后,我们只需要将我们实现好的内容,填充到platform_driver类型的结构体,并使用platform_driver_register函数注册即可。

注册平台驱动(位于linux_driver/07_platform_driver/pdrv_led.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* 定义平台驱动结构体,包含探测、移除函数,驱动名称和设备 ID 表 */
static struct platform_driver pdrv_led = {
    .probe = pdrv_led_probe,
    .remove = pdrv_led_remove,
    .driver.name = "pdev_led",
    .id_table = pdev_led_ids,
};

static __init int pdrv_led_init(void)
{
    /* 打印平台驱动初始化信息 */
    printk("led platform driver init\n");
    /* 注册平台驱动 */
    platform_driver_register(&pdrv_led);

    return 0;
}

static __exit void pdrv_led_exit(void)
{
    /* 打印平台驱动退出信息 */
    printk("led platform driver exit\n");
    /* 注销平台驱动 */
    platform_driver_unregister(&pdrv_led);
}

module_init(pdrv_led_init);
module_exit(pdrv_led_exit);
  • 第2-7行,在pdrv_led中定义了两种匹配模式,在平台总线匹配过程中,只会根据id_table中的name值进行匹配,若和平台设备的name值相等,则表示匹配成功; 反之,则匹配不成功,表明当前内核没有该驱动能够支持的设备。

  • 第14行,调用函数platform_driver_register注册我们的平台驱动结构体,这样当加载该内核模块时就会有新的平台驱动加入到内核中。

  • 第24行,调用函数platform_driver_unregister注销平台驱动。

7.5. 实验准备

如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。

7.5.1. 编译驱动程序

7.5.1.1. makefile修改说明

本节实验使用的Makefile如下所示,编写该Makefile时,只需要根据实际情况修改变量KERNEL_DIR和obj-m即可。

Makefile(位于linux_driver/07_platform_driver/Makefile)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 #指定内核路径,可以是相对路径或绝对路径
 KERNEL_DIR=../../kernel/
 #KERNEL_DIR=/home/guest/LubanCat_Linux_rk356x_SDK/kernel/

 #指定目标架构为arm64
 ARCH=arm64

 #指定交叉编译工具链的前缀
 CROSS_COMPILE=aarch64-linux-gnu-

 #导出为环境变量
 export  ARCH  CROSS_COMPILE

 #指定要编译的内核模块目标文件
 obj-m := pdrv_led.o pdev_led.o

 #all :默认目标,执行时会编译驱动模块
 #$(MAKE) :调用make工具
 #-C $(KERNEL_DIR) :指定的内核源码目录
 #M=$(CURDIR) :模块的源码位于当前目录
 #modules :编译模块
 all:
     $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules

 .PHONE:clean

 #清理编译生成的文件
 clean:
     $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean

7.5.1.2. 编译命令说明

在实验目录下输入如下命令来编译驱动模块:

1
2
3
4
5
#进入platform_driver例程源码目录
cd linux_driver/07_platform_driver

#编译驱动模块
make

编译成功后,实验目录下会生成两个名为“pdev_led.ko”、”pdrv_led.ko”的驱动模块文件

7.6. 程序运行结果

在本节实验中,鲁班猫系列板卡系统设备树中均默认使能了 LED 的设备功能,需要关闭设备树的leds节点, 可以修改leds节点的 status = "okay";status = "disabled";, 然后编译设备树进行替换,也可以在板卡中直接使用以下命令关闭系统leds驱动对LED的控制:

1
2
3
4
5
6
7
8
#心跳灯命名可能为sys_status_led或sys_led,需先确认
ls /sys/class/leds/

#如果为sys_status_led
sudo sh -c 'echo 0 > /sys/class/leds/sys_status_led/brightness'

#如果为sys_led
sudo sh -c 'echo 0 > /sys/class/leds/sys_led/brightness'

将led的亮度调为0,与此同时led的触发条件自动变为none,从而取消leds驱动对LED的控制。

将pdev_led.ko文件拷贝到开发板中,然后执行命令加载,如果是在板卡上编译内核模块,直接执行命令加载驱动:

1
2
3
4
5
6
7
8
#加载驱动
sudo insmod pdev_led.ko

#查看注册的led设备
ls /sys/bus/platform/devices/pdev_led.0

#信息输出如下
driver_override  modalias  power  subsystem  uevent

可以在/sys/bus/platform/devices下看到我们注册的LED灯设备 pdev_led.0,后面的数字0对应了平台设备结构体的id编号。

将pdrv_led.ko文件拷贝到开发板中,然后执行命令加载,如果是在板卡上编译内核模块,直接执行命令加载驱动:

1
2
3
4
5
6
7
#加载驱动
sudo insmod pdev_led.ko

#信息输出如下
[  148.930454] led platform driver init
[  148.931340] led platform driver probe
[  148.931464] major=236, minor=0

可以看到驱动加载时打印了一次led platform driver probe,说明匹配成功。

通过驱动代码,最后会在/dev下创建led设备,可以使用echo命令来测试我们的led驱动是否正常。 我们使用以下命令控制灯的亮灭:

1
2
3
4
5
#控制灯亮
sudo sh -c "echo 0 > /dev/pdrv_led"

#控制灯灭
sudo sh -c "echo 1 > /dev/pdrv_led"