10. 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结构体。

重要

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

10.1. 平台设备

10.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;
     ......
 };
platform_device结构体成员

成员

描述

name

设备名称,总线进行匹配时,会比较设备和驱动的名称是否一致

id

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

dev

Linux设备模型中的device结构体

num_resources

记录资源的个数,当结构体成员resource存放的是数组时,需要记录resource数组的个数,

内核提供了宏定义ARRAY_SIZE用于计算数组的个数

resource

平台设备提供给驱动的资源,如irq,dma,内存等等。

id_entry

平台总线提供的另一种匹配方式,原理依然是通过比较字符串,这里的id_entry用于保存匹配的结果

10.1.2. 何为设备信息?

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

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

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

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

resource结构体(内核源码/include/linux/ioport.h)
1
2
3
4
5
6
7
struct resource {
    resource_size_t start;
    resource_size_t end;
    const char *name;
    unsigned long flags;
    ......
};
resource结构体成员

成员

描述

start

开始的位置,如果是IO地址资源,就是起始物理地址,如果是中断资源,就是中断号

end

结束的位置,如果是IO地址地址,就是映射的最后一个物理地址,如果是中断资源,就不用填

name

描述这个平台设备(资源)的名称,可以设置为NULL

flags

用于指定该资源的类型,资源包括I/O、Memory、Register、IRQ、DMA、Bus等多种类型

常见的flags类型有:

资源宏定义

描述

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指针,在驱动程序中通过调用平台设备总线中的核心函数,可以获取到我们需要的引脚号。

重要

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

10.1.3. 注册/注销平台设备

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

platform_device_register函数(内核源码/drivers/base/platform.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/*******************************************************************************
*  @brief     platform_device_register
*  @note      注册平台设备
*  @param     pdev        platform_device类型结构体指针
*  @return    返回0表示注册成功,返回负数表示注册失败
*******************************************************************************/
int platform_device_register(struct platform_device *pdev)
{
    int ret;
#ifdef CONFIG_SUNXI_BOOTEVENT
    unsigned long long ts = 0;
#endif
    TIME_LOG_START();
    device_initialize(&pdev->dev);
    setup_pdev_dma_masks(pdev);
    ret = platform_device_add(pdev);
    TIME_LOG_END();
    bootevent_pdev_register(ts, pdev);
    return ret;
}

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

platform_device_unregister函数(内核源码/drivers/base/platform.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/*******************************************************************************
*  @brief     platform_device_unregister
*  @note      注销平台设备
*  @param     pdev        platform_device类型结构体指针
*  @return    无
*******************************************************************************/
void platform_device_unregister(struct platform_device *pdev)
{
    platform_device_del(pdev);
    platform_device_put(pdev);
}

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

10.2. 平台设备驱动

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

10.2.1. platform_driver结构体

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

platform_driver结构体(内核源码/include/platform_device.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct platform_driver {
    int (*probe)(struct platform_device *);
    int (*remove)(struct platform_device *);
    void (*shutdown)(struct platform_device *);
    int (*suspend)(struct platform_device *, pm_message_t state);
    int (*resume)(struct platform_device *);
    struct device_driver driver;
    const struct platform_device_id *id_table;
    ......
};
resource结构体成员

成员

描述

probe

当驱动和硬件信息匹配成功之后,就会调用probe函数,驱动所有的资源的注册和初始化全部放在probe函数中

remove

硬件信息被移除了,或者驱动被卸载了,全部要释放,释放资源的操作就放在该函数中

shutdown,suspend,resume

代表设备驱动在调用管理的时候的回调函数。

driver

内核维护的所有的驱动必须包含该成员,通常driver->name用于和设备进行匹配

id_table

表示该驱动能够兼容的设备类型,往往一个驱动能同时支持多个硬件,这些硬件的名字都放在该结构体数组中

platform_device_id结构体原型如下所示:

platform_device_id结构体(内核源码/include/linux/mod_devicetable.h)
1
2
3
4
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
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,
    },
    [IMX53_UART] = {
        .uts_reg = IMX21_UTS,
        .devtype = IMX53_UART,
    },
    [IMX6Q_UART] = {
        .uts_reg = IMX21_UTS,
        .devtype = IMX6Q_UART,
    },
};

static const 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 = "imx53-uart",
        .driver_data = (kernel_ulong_t) &imx_uart_devdata[IMX53_UART],
    }, {
        .name = "imx6q-uart",
        .driver_data = (kernel_ulong_t) &imx_uart_devdata[IMX6Q_UART],
    }, {
        /* sentinel */
    }
};
  • 第1-18行:声明了一个结构体数组,用来表示不同平台的串口类型。

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

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

10.2.2. 注册/注销平台驱动

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

platform_driver_register函数
 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
// 以下部分内容位于kernel/include/linux/platform_device.h
// 将__platform_driver_register(drv, THIS_MODULE)重定义
#define platform_driver_register(drv) \
    __platform_driver_register(drv, THIS_MODULE)
// 外部声明
extern int __platform_driver_register(struct platform_driver *,
                    struct module *);

// 以下部分内容位于kernel/drivers/base/platform.c
/*******************************************************************************
*  @brief     __platform_driver_register
*  @note      注册平台驱动
*  @param     drv         platform_driver类型结构体指针
*  @param     owner       设备驱动的owner,通常为THIS_MODULE(实现设备驱动程序的模块)
*  @return    返回0表示注册成功,返回负数表示注册失败
*******************************************************************************/
int __platform_driver_register(struct platform_driver *drv,
                struct module *owner)
{
    drv->driver.owner = owner;
    drv->driver.bus = &platform_bus_type;
    drv->driver.probe = platform_drv_probe;
    drv->driver.remove = platform_drv_remove;
    drv->driver.shutdown = platform_drv_shutdown;

    return driver_register(&drv->driver);
}

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

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

platform_driver_unregister函数(内核源码/drivers/base/platform.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/*******************************************************************************
*  @brief     platform_driver_unregister
*  @note      卸载平台驱动
*  @param     drv         platform_driver类型结构体指针
*  @return    无
*******************************************************************************/
void platform_driver_unregister(struct platform_driver *drv)
{
    driver_unregister(&drv->driver);
}

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

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

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

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

platform_get_resource函数(内核源码/drivers/base/platform.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/*******************************************************************************
*  @brief     platform_get_resource
*  @note      获取平台设备提供的资源结构体
*  @param     dev         指定要获取哪个平台设备的资源
*  @param     type        指定获取资源的类型,如IORESOURCE_MEM、IORESOURCE_IO等
*  @param     num         指定要获取的资源编号
*  @return    注册成功返回struct resource结构体类型指针,注册失败返回NULL
*******************************************************************************/
struct resource *platform_get_resource(struct platform_device *dev,
                    unsigned int type, unsigned int num)
{
    u32 i;

    for (i = 0; i < dev->num_resources; i++) {
        struct resource *r = &dev->resource[i];

        if (type == resource_type(r) && num-- == 0)
            return r;
    }
    return NULL;
}

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

platform_get_irq函数(内核源码/drivers/base/platform.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/*******************************************************************************
*  @brief     platform_get_irq
*  @note      获取中断引脚
*  @param     dev         指定要获取哪个平台设备的资源
*  @param     num         指定要获取的资源编号
*  @return    注册成功返回可用的中断号,注册失败返回负数
*******************************************************************************/
int platform_get_irq(struct platform_device *dev, unsigned int num)
{
    int ret;

    ret = __platform_get_irq(dev, num);
    if (ret < 0 && ret != -EPROBE_DEFER)
        dev_err(&dev->dev, "IRQ index %u not found\n", num);

    return ret;
}

对于存放在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进行注册。

10.3. 平台总线

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

在Linux的设备驱动模型中,总线是最重要的一环。上一章中,我们提到过总线是负责匹配设备和驱动,它维护着两个链表,里面记录着各个已经注册的平台设备和平台驱动。

每当有新的设备或者是新的驱动加入到总线时,总线便会调用platform_match函数对新增的设备或驱动,进行配对。

内核中使用platform_bus_type来抽象描述系统中的总线,平台总线结构体原型如下所示:

platform_bus_type结构体(内核源码/driver/base/platform.c)
1
2
3
4
5
6
7
8
struct bus_type platform_bus_type = {
    .name           = "platform",
    .dev_groups     = platform_dev_groups,
    .match          = platform_match,
    .uevent         = platform_uevent,
    .dma_configure  = platform_dma_configure,
    .pm             = &platform_dev_pm_ops,
};

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

platform_bus_init函数(内核源码/driver/base/platform.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
int __init platform_bus_init(void)
{
    int error;

    early_platform_cleanup();

    error = device_register(&platform_bus);
    if (error) {
        put_device(&platform_bus);
        return error;
    }
    error =  bus_register(&platform_bus_type);
    if (error)
        device_unregister(&platform_bus);
    of_platform_register_reconfig_notifier();
    return error;
}
  • 第7行:注册设备

  • 第12行:向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
27
28
29
30
31
/*******************************************************************************
*  @brief     platform_match
*  @note      实现平台总线和平台设备的匹配过程
*  @param     dev         指定平台设备
*  @param     drv         指定平台驱动
*  @return    注册成功返回可用的中断号,注册失败返回负数
*******************************************************************************/
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);
}

第10-11行:这里调用了to_platform_device()和to_platform_driver()宏。其原型如下:

to_platform_xxx宏定义(内核源码/include/linux/platform_device.h)
1
2
3
#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。

  • 第13-27行:platform总线提供了四种匹配方式,并且这四种方式存在着优先级:设备树机制>ACPI匹配模式>id_table方式>字符串比较。

注解

虽然匹配方式五花八门,但是并没有涉及到任何复杂的算法,都只是在匹配的过程中,比较一下设备和驱动提供的某个成员的字符串是否相同。

设备树是一种描述硬件的数据结构,它用一个非C语言的脚本来描述这些硬件设备的信息。驱动和设备之间的匹配时通过比较compatible的值。acpi主要是用于电源管理,基本上用不到,这里就并不进行讲解了。关于设备树的匹配机制,会在设备树章节进行详细分析。

10.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
15
16
17
18
19
20
/*******************************************************************************************
*  @brief     platform_match_id
*  @note      用于在平台驱动中匹配平台设备的设备ID
*  @param     id      指向platform_device_id结构体数组的指针,该数组包含了驱动支持的所有设备ID
*  @param     pdev    指向 platform_device 结构体的指针,代表了当前正在探测的平台设备
*  @return    匹配成功返回指向该 ID 的指针,匹配失败返回 NULL
*******************************************************************************************/
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参数。

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

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

10.4.1. 编程思路

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

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

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

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

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

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

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

10.4.2. 代码分析

10.4.2.1. 定义平台设备

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

寄存器宏定义(位于../linux_driver/platform_driver/led_pdev.c)
1
2
3
4
//参考字符设备led驱动
#define GPIO_BASE (0x0300B000) //GPIO(PC,PF,PG,PH,PI)组基地址
#define GPIO_PF_CFG (GPIO_BASE + 0x00B4) //PF Configure Register,设置输入或者输出
#define GPIO_PF_DAT (GPIO_BASE + 0x00C4) //PF Data Register,设置输出的电压
  • 使用宏定义来对GPIO引脚的寄存器进行封装。

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

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

定义平台设备的私有数据(位于../linux_driver/platform_driver/led_pdev.c)
1
unsigned int led_hwinfo[1] = { 6 };  //偏移,PF6
  • 使用一个数组led_hwinfo,来记录寄存器的偏移量,填充平台私有数据时,只需要把数组的首地址赋给platform_data即可。

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

定义平台设备(位于../linux_driver/platform_driver/led_pdev.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
static int led_cdev_release(struct inode *inode, struct file *filp)
{
    return 0;
}

/* red led device */
static struct platform_device rled_pdev = {
    .name = "led_pdev",
    .id = 0,
    .num_resources = ARRAY_SIZE(led_resource),
    .resource = led_resource,
    .dev = {
        .release = led_release,
        .platform_data = led_hwinfo,
        },

};
  • 第1-4行:声明了led_cdev_release函数,目的为了防止卸载模块,内核提示报错。

  • 第7-9行:定义了一个设备名为“led_pdev”的设备,这里的名字确保要和驱动的名称保持一致,否则就会导致匹配失败。id编号设置为0,驱动会利用该编号来注册设备。

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

  • 第12-15行:对dev中的成员进行赋值,将rled_hwinfo存储到platform_data中。

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

模块初始化(位于../linux_driver/platform_driver/led_pdev.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
static __init int led_pdev_init(void)
{
    printk("pdev init\n");
    platform_device_register(&rled_pdev);
    return 0;

}
module_init(led_pdev_init);


static __exit void led_pdev_exit(void)
{
    printk("pdev exit\n");
    platform_device_unregister(&rled_pdev);

}
module_exit(led_pdev_exit);


MODULE_AUTHOR("Embedfire");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("the example for platform driver");
  • 第1-8行:实现模块的入口函数,打印信息并注册平台设备

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

  • 第18-20行:模块遵守协议以及一些模块信息

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

10.4.2.2. 定义平台驱动

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

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

id_table(位于../linux_driver/platform_driver/led_pdrv.c)
1
2
3
4
5
static struct platform_device_id led_pdev_ids[] = {
    {.name = "led_pdev"},
    {}
};
MODULE_DEVICE_TABLE(platform, led_pdev_ids);
  • 这块代码提供了驱动支持哪些设备

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

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

led_pdrv_probe函数(位于../linux_driver/platform_driver/led_pdrv.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
struct led_data {
    unsigned int led_pin;
    unsigned int __iomem *va_MODER;
    unsigned int __iomem *va_OTYPER;

    struct cdev led_cdev;
};
/* 省略部分代码 */
static int led_pdrv_probe(struct platform_device *pdev)
{
    struct led_data *cur_led;
    unsigned int *led_hwinfo;

    struct resource *mem_DR;
    struct resource *mem_DDR;

    dev_t cur_dev;

    int ret = 0;

    printk("led platform driver probe\n");

    //第一步:提取平台设备提供的资源
    //devm_kzalloc函数申请cur_led和led_hwinfo结构体内存大小
    cur_led = devm_kzalloc(&pdev->dev, sizeof(struct led_data), GFP_KERNEL);
    if(!cur_led)
        return -ENOMEM;
    led_hwinfo = devm_kzalloc(&pdev->dev, sizeof(unsigned int), GFP_KERNEL);
    if(!led_hwinfo)
        return -ENOMEM;

    /* get the pin for led and the reg's shift */
    //dev_get_platdata函数获取私有数据,得到LED灯的寄存器偏移量,并赋值给cur_led->led_pin
    led_hwinfo = dev_get_platdata(&pdev->dev);

    cur_led->led_pin = led_hwinfo[0];
    /* get platform resource */
    //利用函数platform_get_resource可以获取到各个寄存器的地址
    mem_DR = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    mem_DDR = platform_get_resource(pdev, IORESOURCE_MEM, 1);

    //使用devm_ioremap将获取到的寄存器地址转化为虚拟地址
    cur_led->va_DR = devm_ioremap(&pdev->dev, mem_DR->start, resource_size(mem_DR));
    cur_led->va_DDR = devm_ioremap(&pdev->dev, mem_DDR->start, resource_size(mem_DDR));

    //第二步:注册字符设备
    cur_dev = MKDEV(DEV_MAJOR, pdev->id);

    register_chrdev_region(cur_dev, 1, "led_cdev");

    cdev_init(&cur_led->led_cdev, &led_cdev_fops);

    ret = cdev_add(&cur_led->led_cdev, cur_dev, 1);
    if(ret < 0)
    {
        printk("fail to add cdev\n");
        goto add_err;
    }

    device_create(led_test_class, NULL, cur_dev, NULL, DEV_NAME "%d", pdev->id);

    /* save as drvdata */
    //platform_set_drvdata函数,将LED数据信息存入在平台驱动结构体中pdev->dev->driver_data中
    platform_set_drvdata(pdev, cur_led);

    return 0;

    add_err:
        unregister_chrdev_region(cur_dev, 1);
        return ret;
}
  • 第1-7行:仍然使用结构体led_data来管理我们LED灯的硬件信息,定义时钟寄存器的虚拟地址变量。

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

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

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

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

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

  • 第64行:使用platform_set_drvdata函数,将LED数据信息存入在平台驱动结构体中pdev->dev->driver_data中。

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

led_pdrv_remove函数(位于../linux_driver/platform_driver/led_pdrv.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
static int led_pdrv_remove(struct platform_device *pdev)
{
    dev_t cur_dev;
    //platform_get_drvdata,获取当前LED灯对应的结构体
    struct led_data *cur_data = platform_get_drvdata(pdev);

    printk("led platform driver remove\n");

    cur_dev = MKDEV(DEV_MAJOR, pdev->id);

    //cdev_del删除对应的字符设备
    cdev_del(&cur_data->led_cdev);

    //删除/dev目录下的设备
    device_destroy(led_test_class, cur_dev);

    //unregister_chrdev_region, 注销掉当前的字符设备编号
    unregister_chrdev_region(cur_dev, 1);

    return 0;

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

  • 第12-20行:,调用cdev_del删除对应的字符设备,删除/dev目录下的设备,则调用函数device_destroy,最后使用函数unregister_chrdev_region, 注销掉当前的字符设备编号。

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

led灯的字符设备框架(位于../linux_driver/platform_driver/led_pdrv.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
static int led_cdev_open(struct inode *inode, struct file *filp)
{
    unsigned int val = 0;
    struct led_data *cur_led = container_of(inode->i_cdev, struct led_data, led_cdev);

    printk("led_cdev_open() \n");

    // 设置引脚输出
    val = readl(cur_led->va_DDR);
    val |= ((unsigned int)0x001 << (cur_led->led_pin*4)); //一个引脚中用3位进行配置,保留1位,因此4位一个引脚

    writel(val,cur_led->va_DDR);

    //设置默认输出高电平
    val = readl(cur_led->va_DR);
    val |= ((unsigned int)0x1 << (cur_led->led_pin));//一个引脚中用1位进行配置,因此1位一个引脚

    writel(val, cur_led->va_DR);

    filp->private_data = cur_led;

    return 0;
}

static int led_cdev_release(struct inode *inode, struct file *filp)
{
    return 0;
}

static ssize_t led_cdev_write(struct file *filp, const char __user * buf,
                size_t count, loff_t * ppos)
{
    unsigned long val = 0;
    unsigned long ret = 0;

    int tmp = count;

    struct led_data *cur_led = (struct led_data *)filp->private_data;

    val = kstrtoul_from_user(buf, tmp, 10, &ret);

    val = readl(cur_led->va_DR);
    if (ret == 0)
    {
        val &= ~((unsigned int)0x1 << (cur_led->led_pin));   /*设置GPIO引脚输出低电平*/

    }
    else
    {
        val |= ((unsigned int)0x1 << (cur_led->led_pin));    /*设置GPIO引脚输出高电平*/

    }
    writel(val, cur_led->va_DR);

    *ppos += tmp;

    return tmp;
}

static struct file_operations led_cdev_fops = {
    .open = led_cdev_open,
    .release = led_cdev_release,
    .write = led_cdev_write,

};
  • 第1-23行是led_cdev_open函数的内容主要就是对硬件进行初始化。

  • 第25-28行的led_cdev_release函数的作用是为了防止警告产生。

  • 第38-53行对GPIO引脚进行置位控制

  • 第60-65行,对file_operations结构体进行填充

关于kstrtoul_from_user()函数,函数定义如下:

kstrtoul_from_user()函数 (内核源码/include/linux/kernel.h)
1
int __must_check kstrtoul_from_user(const char __user *s, size_t count, unsigned int base, unsigned long *res);

函数参数和返回值如下:

参数:

  • s: 字符串的起始地址,该字符串必须以空字符结尾;

  • count: count为要转换数据的大小;

  • base: 转换基数,如果base=0,则函数会自动判断字符串的类型,且按十进制输出,比如“0xa”就会被当做十进制处理(大小写都一样),输出为10。如果是以0开头则会被解析为八进制数,否则将会被解析成小数;

  • res: 一个指向被转换成功后的结果的地址。

返回值:

该函数相比kstrtoul()多了一个参数count,因为用户空间是不可以直接访问内核空间的,所以内核提供了kstrtoul_from_user()函数以实现用户缓冲区到内核缓冲区的拷贝,与之相似的还有copy_to_user(),copy_to_user() 完成的是内核空间缓冲区到用户空io间的拷贝。如果你使用的内存类型没那么复杂,便可以选择使用put_user()或者get_user()函数。

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

注册平台驱动(位于../linux_driver/platform_driver/led_pdrv.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
static struct platform_driver led_pdrv = {

    .probe = led_pdrv_probe,
    .remove = led_pdrv_remove,
    .driver.name = "led_pdev",
    .id_table = led_pdev_ids,
};

static __init int led_pdrv_init(void)
{
    printk("led platform driver init\n");
    //class_create,来创建一个led类
    led_test_class = class_create(THIS_MODULE, "test_leds");
    //调用函数platform_driver_register,注册我们的平台驱动结构体,这样当加载该内核模块时, 就会有新的平台驱动加入到内核中。 第20-27行,注销
    platform_driver_register(&led_pdrv);

    return 0;
}
module_init(led_pdrv_init);

static __exit void led_pdrv_exit(void)
{
    printk("led platform driver exit\n");
    platform_driver_unregister(&led_pdrv);
    class_destroy(led_test_class);
}
module_exit(led_pdrv_exit);

MODULE_AUTHOR("Embedfire");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("the example for platform driver");
  • 第1-6行,在led_pdrv中定义了两种匹配模式,在平台总线匹配过程中,只会根据id_table中的name值进行匹配,若和平台设备的name值相等,则表示匹配成功; 反之,则匹配不成功,表明当前内核没有该驱动能够支持的设备。

  • 第13行,调用函数class_create,来创建一个led类,并且调用函数platform_driver_register,注册我们的平台驱动结构体,这样当加载该内核模块时, 就会有新的平台驱动加入到内核中。

  • 第21-27行,注销函数led_pdrv_exit,则是初始化函数的逆过程。

10.5. 实验准备

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

10.5.1. 编译驱动程序

10.5.1.1. makefile修改说明

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

Makefile(位于../linux_driver/platform_driver/Makefile)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
KERNEL_DIR=../../kernel/

ARCH=arm64
CROSS_COMPILE=aarch64-linux-gnu-
export  ARCH  CROSS_COMPILE

obj-m := led_pdev.o led_pdrv.o

all:
    $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
modules clean:
    $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean

10.5.1.2. 编译命令说明

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

sudo sh -c 'echo 0 > /sys/class/leds/led_sys/brightness'

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

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

make

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

10.5.2. 编译应用程序

本节实验使用linux系统自带的”echo”应用程序进行测试,无需额外编译应用程序

10.5.3. 拷贝驱动程序到板卡

此处根据实际情况调整,可以使用ssh、scp、nfs等方法,如果是直接在板卡上编译的就直接加载驱动。

#或者通过scp命令直接传输到板卡,
scp *.ko cat@192.168.103.2:/home/cat/

10.6. 程序运行结果

10.6.1. 开发板加载第一个驱动模块

教程中为了节省篇幅,只列举了一个led灯,当我们运行命令 sudo  insmod led_pdev.ko 后, 可以在/sys/bus/platform/devices下看到我们注册的LED灯设备 led_pdev.0,后面的数字0对应了平台设备结构体的id编号。

led灯设备

10.6.2. 开发板加载第二个驱动模块

执行命令 sudo insmod led_pdrv.ko,加载LED的平台驱动。在运行命令“dmesg|tail”来查看内核打印信息,可以看到驱动加载时打印了一次led platform driver probe,说明匹配成功。

led灯设备

10.6.3. 开发板运行应用程序

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

#控制灯亮
sudo sh -c "echo 0 > /dev/led0"

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