12. Linux设备树——LED灯实验

通过上一小节的学习,我们已经能够编写简单的设备树节点,并且使用常用的of函数从设备树中获取我们想要的节点资源。 这一小节我们带领大家使用设备树编写一个简单的LED灯驱动程序,加深对设备树的理解。

12.1. 实验说明

本节实验使用到Lubancat_AW开发板上的LED灯(使用的是系统灯,也可以自行换引脚,使用引出的GPIO接LED),以LubanCat-A1为例,可以参考”字符设备驱动实验–点亮LED灯”章节。

12.2. 实验代码讲解

本章的示例代码目录为:linux_driver/device_tree_led

12.2.1. 编程思路

程序编写的主要内容为添加LED灯的设备树节点、在驱动程序中使用of函数获取设备节点中的属性,编写测试应用程序。

  • 首先向设备树添加LED设备节点。

  • 其次编写平台设备驱动框架,主要包驱动入口函数、驱动注销函数、平台设备结构体定义三部分内容。

  • 实现.probe函数,对LED进行设备注册和初始化。

  • 实现字符设备操作函数集,这里主要实现.write操作。

  • 编写测试应用程序,对于输入不同的值控制LED亮灭。

12.2.2. 代码分析

12.2.2.1. 添加RGB设备节点

注意

本实验需要使用SDK源码,不能只是内核源码,可以参考快速使用手册 《aw-image-build介绍》 章节获取SDK源码。

RGB灯实际使用的是一个IO口,控制它所需要的资源几个控制寄存器,所以它的设备树节点,也非常简单。

以LubanCat-A1板卡sys_led灯PF6为例,设备树编写如下:

添加子节点led_test(在aw-image-build/source/kernel/linux-5.4-h618/arch/arm64/boot/dts/sunxi/sun50iw9-lubancat-a1.dts)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/*添加led_test节点,*/
led_test{
    #address-cells = <1>;
    #size-cells = <0>;
    compatible = "fire,led_test";

    led@0x0300b000{   //GPIO(PC,PF,PG,PH,PI)寄存器基地址
        reg = <0x0300b000>;
        status = "okay";
    };
};

LED灯的设备节点添加到了根节点下,添加示例请参考本章配套代码linux_driver/device_tree_led/sun50iw9-lubancat-a1.dts。

上面添加的设备树中,代码里包含了控制LED灯所使用的的寄存器,如有疑问可以参考 字符设备驱动实验——点亮LED灯 章节。

  • 第2行: 这里就是LED灯的设备树节点,节点名“led_test”由于在根节点下,很明显它的设备树路径为“/led_test”,在驱动程序中我们会用到这“cells”定义了它的子节点的reg属性样式。“compatible”属性用于匹配驱动,在驱动我们会配置一个和“compatible”一样的参数,这样加载驱动是就可以自动和这个设备树节点匹配了。

  • 第7-10行: led子节点,如上所示,它会用到寄存器,为方便管理,“status = “okay”定义子节点的状态,我们要用这个子节点所以设置为“okay”。

12.2.2.2. 编写驱动程序

基于设备树的驱动程序与平台总线驱动非常相似,差别是平台总线驱动中的平台驱动要和平台设备进行匹配, 使用设备树后设备树取代“平台设备”的作用,平台驱动只需要和与之对应的设备树节点匹配即可。

驱动程序主要内容包括编写平台设备驱动框架、编写.prob函数、实现字符设备操作函数集、驱动注销四部分内容。 源码linux_driver/device_tree_led/led_test.c。

驱动入口函数

驱动入口函数仅仅注册一个平台驱动,如下所示

驱动初始化函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/*
*驱动初始化函数
*/
static int __init led_platform_driver_init(void)
{
    int DriverState;
    DriverState = platform_driver_register(&led_platform_driver);
    printk(KERN_EMERG "\tDriverState is %d\n", DriverState);
    return 0;
}

在整个入口函数中仅仅调用了“platform_driver_register”函数注册了一个平台驱动。参数是传入一个平台设备结构体。

定义平台设备结构体

注册平台驱动时会用到平台设备结构体,在平台设备结构体主要作用是指定平台驱动的.probe函数、指定与平台驱动匹配的平台设备, 使用了设备树后就是指定与平台驱动匹配的设备树节点。

平台设备结构体
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
static const struct of_device_id led_ids[] = {
    {.compatible = "fire,led_test"},
    {/* sentinel */}
    };

/*定义平台设备结构体*/
struct platform_driver led_platform_driver = {
    .probe = led_probe,
    .driver = {
            .name = "leds-platform",
            .owner = THIS_MODULE,
            .of_match_table = led_ids,
    }
    };
  • 第1-4行: 定义匹配表

  • 第8行: 就是我们定义的平台设备结构体。其中“.probe =led_probe,”指定.probe函数。.probe函数比较特殊,当平台驱动和设备树节点匹配后会自动执行.probe函数,后面的RGB灯的初始化以及字符设备的注册都在这个函数中实现(当然也可以在其他函数中实现)。

  • 第9-13行: “.driver = { …}”定义driver的一些属性,包括名字、所有者等等,其中最需要注意的是“.of_match_table ”属性,它指定这个驱动的匹配表。这里只定义了一个匹配值“.compatible = “fire,led_test”,这个驱动将会和设备树中“compatible =“fire,led_test”的节点匹配”,准确的说是和““compatible = “fire,led_test””的相对根节点的子节点匹配。我们在根节点下定义了led_test子节点,并且设置“compatible = “fire,led_test”;所以正常情况下,驱动会和这个子节点匹配。

实现.probe函数

之前说过,当驱动和设备树节点匹配成功后会自动执行.probe函数,所以我们在.probe函数中实现一些初始化工作。 本实验将RGB初始化以及字符设备的初始化全部放到.probe函数中实现,.probe函数较长,但包含大量的简单、重复性的初始化代码,非常容易理解。

.probe函数
  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
/*定义 led 资源结构体,保存获取得到的节点信息以及转换后的虚拟寄存器地址*/
struct led_resource
{
    struct device_node *device_node;    //led的设备树节点
    void __iomem *va_DR;
    void __iomem *va_DDR;
};

static int led_probe(struct platform_device *pdv)
{

    int ret = -1; //保存错误状态码
    unsigned int register_data = 0;
    u32 gpio_base_addr;

    printk(KERN_EMERG "\t  match successed  \n");

    /*获取led_test的设备树节点*/
    led_test_device_node = of_find_node_by_path("/led_test");
    if (led_test_device_node == NULL)
    {
        printk(KERN_ERR "\t  get led_test failed!  \n");
        return -1;
    }
    /*获取led节点*/
    led_res.device_node = of_find_node_by_name(led_test_device_node, "led");
    if (led_res.device_node == NULL)
    {
        printk(KERN_ERR "\n get led_device_node failed ! \n");
        return -1;
    }

    /* 获取物理地址 */
    if (of_property_read_u32(led_res.device_node, "reg", &gpio_base_addr) < 0) {
        printk(KERN_ERR "Failed to read 'reg' property\n");
        return -1;
    }

    /* 进行内存映射 */
    led_res.va_DR = ioremap(gpio_base_addr + 0x00C4, 4);  // 内存映射,GPIO_PF_DAT = GPIO_BASE + 0x00C4
    if (!led_res.va_DR) {
        printk(KERN_ERR "ioremap for va_DR failed\n");
        return -1;
    }

    led_res.va_DDR = ioremap(gpio_base_addr + 0x00B4, 4);  // 内存映射,GPIO_PF_CFG = GPIO_BASE + 0x00B4
    if (!led_res.va_DDR) {
        printk(KERN_ERR "ioremap for va_DDR failed\n");
        return -1;
    }

    // 设置模式寄存器:输出模式
    register_data = readl(led_res.va_DDR);
    register_data |= ((unsigned int)0x1 << (6*4)); //PF6,一个引脚中用3位进行配置,保留1位,因此4位一个引脚

    writel(register_data,led_res.va_DDR);

    // 设置置位寄存器:默认输出高电平
    register_data = readl(led_res.va_DR);
    register_data |= ((unsigned int)0x1 << (6)); //PF6,一个引脚中用1位进行配置,因此1位一个引脚

    writel(register_data, led_res.va_DR);

    /*---------------------注册 字符设备部分-----------------*/

    //第一步
    //采用动态分配的方式,获取设备编号,次设备号为0,
    //DEV_CNT为1,当前只申请一个设备编号
    ret = alloc_chrdev_region(&led_devno, 0, DEV_CNT, DEV_NAME);
    if (ret < 0)
    {
        printk("fail to alloc led_devno\n");
        goto alloc_err;
    }
    //第二步
    //关联字符设备结构体cdev与文件操作结构体file_operations
    led_chr_dev.owner = THIS_MODULE;
    cdev_init(&led_chr_dev, &led_chr_dev_fops);

    //第三步
    //添加设备至cdev_map散列表中
    ret = cdev_add(&led_chr_dev, led_devno, DEV_CNT);
    if (ret < 0)
    {
        printk("fail to add cdev\n");
        goto add_err;
    }

    //第四步
    /*创建类 */
    class_led = class_create(THIS_MODULE, DEV_NAME);

    /*创建设备*/
    device = device_create(class_led, NULL, led_devno, NULL, DEV_NAME);

    return 0;

add_err:
    //添加设备失败时,需要注销设备号
    unregister_chrdev_region(led_devno, DEV_CNT);
    printk("\n error! \n");
alloc_err:

    return -1;
}
  • 第2-7行: 自定义led资源结构体,用于保存获取得到的设备节点信息以及转换后的虚拟寄存器地址。

  • 第19-24行: 使用of_find_node_by_path函数获取设备树节点“/led_test”,获取成功后会返回“/led_test”节点的“设备节点结构体”后面的代码我们就可以根据这个“设备节点结构体”访问它的子节点。

  • 第19-62行: 初始化LED灯,初始化过程如下:

  • 第19-31行:获取LED灯子节点,这里使用函数“of_find_node_by_name”,参数rgb_led_device_node指定从led_test节点开始搜索,参数“led”指定要获取那个节点,这里是led_test节点下的led子节点。

  • 第34-50行:获取并转换reg属性,我们知道reg属性保存的就是寄存器地址(物理地址),这里使用“of_property_read_u32”函数获取并通过ioremap完成物理地址到虚拟地址的转换。

  • 第53-62行:初始化寄存器,至于如何将初始化GPIO在字符设备-点亮LED章节已经详细介绍这里不再赘述,需要注意的是这里只能用系统提供的API(例如这里读写的是32位数据,使用writel和readl),不能像裸机那样直接使用“=”、“&=”、“|=”等等那样直接修改寄存器。

  • 第64-94行: 注册一个字符设备。字符设备的注册过程与之前讲解的字符设备驱动非常相似,这部分代码就是从字符设备驱动拷贝得到的。

注册字符设备使用到的结构体
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
static dev_t led_devno;                                      //定义字符设备的设备号
static struct cdev led_chr_dev;                      //定义字符设备结构体chr_dev
struct class *class_led;                             //保存创建的类
struct device *device;                                       // 保存创建的设备

static struct file_operations led_chr_dev_fops ={
            .owner = THIS_MODULE,
            .open = led_chr_dev_open,
            .write = led_chr_dev_write,
};

如果驱动和设备树节点完成匹配,系统会自动执行.probe函数,从上方代码可知,.probe函数完成了RGB灯的初始化和字符设备的创建。 下一步我们只需要在字符设备的操作函数集中控制RGB灯即可。

实现字符设备操作函数集

为简化程序设计这里仅仅实现了字符设备操作函数集中的.write函数,.write函数根据收到的信息控制LED灯的亮、灭,结合代码介绍如下:

.write函数实现
 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
/*字符设备操作函数集,open函数*/
static int led_chr_dev_open(struct inode *inode, struct file *filp)
{
        printk("\n led_chr_dev_open \n");
        return 0;
}

/*字符设备操作函数集,write函数*/
static ssize_t led_chr_dev_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{

    unsigned int register_data = 0; //暂存读取得到的寄存器数据
    unsigned char write_data; //用于保存接收到的数据

    int error = copy_from_user(&write_data, buf, cnt);
    if (error < 0)
    {
        return -1;
    }
    /*设置led引脚 输出电平*/
    if (write_data)
    {
        register_data |= ((unsigned int)0x1 << (6));

        writel(register_data, led_res.va_DR); // PF6引脚输出高电平,灯灭
    }
    else
    {
        register_data &= ~ ((unsigned int)0x1 << (6));

        writel(register_data, led_res.va_DR); // PF6引脚输出低电平,灯亮
    }

    return 0;

}

/*字符设备操作函数集*/
static struct file_operations led_chr_dev_fops =
    {
        .owner = THIS_MODULE,
        .open = led_chr_dev_open,
        .write = led_chr_dev_write,
};

我们仅实现了两个字符设备操作函数,open 对应led_chr_dev_open函数这是一个空函数。 .write对应led_chr_dev_write函数,这个函数接收应用程序传回的命令,根据命令控制LED灯的亮、灭。

  • 第15-19行: 使用copy_from_user函数将用户空间的数据拷贝到内核空间。这里传递的数据是一个无符号整型数据。

  • 第21-32行: 解析获取的值,控制led亮灭,这里led的引脚是PF6。

12.2.2.3. 编写测试应用程序

在驱动程序中我采用自动创建设备节点的方式创建了字符设备的设备节点文件,文件名可自定义,写测试应用程序时记得文件名即可。本例程设备节点名为“rgb_led”。测试程序很简单,源码如下所示。

测试应用程序
 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
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main(int argc, char *argv[])
{
    printf("led test_app\n");
    /*判断输入的命令是否合法*/
    if(argc != 2)
    {
        printf(" command error ! \n");
        printf(" usage : sudo test_app num [num can be 0 or 1]\n");
        return -1;
    }

    /*打开文件*/
    int fd = open("/dev/led_test", O_RDWR);
    if(fd < 0)
    {
                printf("open file : %s failed !\n", argv[0]);
                return -1;
    }

    unsigned char commend = atoi(argv[1]);  //将受到的命令值转化为数字;

    /*写入命令*/
    int error = write(fd,&commend,sizeof(commend));
    if(error < 0)
    {
        printf("write file error! \n");
        close(fd);
        /*判断是否关闭成功*/
    }

    /*关闭文件*/
    error = close(fd);
    if(error < 0)
    {
        printf("close file error! \n");
    }

    return 0;
}
  • 第7-14行: 简单判断输入是否合法,运行本测试应用程序时argc应该为2。它由应用程序文件名和命令组成例如“./test_app <命令值>”。

  • 第17-22行: 打开设备文件。

  • 第27行: 将终端输入的命令值转化为数字最终使用write函数

  • 第35-39行: 关闭设备文件。

12.3. 编译驱动程序

12.3.1. 编译设备树

参考: 如何编译和加载设备树 章节。

需要使用SDK进行编译,步骤如下:

  • 先单独编译设备树

  • 再通过./build.sh编译uboot deb包

  • 将uboot deb包传到板卡使用dpkg进行安装

  • 最后使用nand-sata-install工具更新uboot然后重启,从而更新设备树。

12.3.2. 编译驱动和应用程序

执行make命令,Makefile和前面章节大致相同,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
KERNEL_DIR=../../kernel/
ARCH=arm64
CROSS_COMPILE=aarch64-linux-gnu-
export  ARCH  CROSS_COMPILE

obj-m := led_test.o
CFLAGS_led_test.o := -fno-stack-protector
out = test_app


all:
        $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
        $(CROSS_COMPILE)gcc -o $(out) test_app.c

.PHONE:clean

clean:
        $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
        rm $(out)

最终会生成led_test.ko和test_app应用程序

12.4. 程序运行结果

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

12.4.1. 实验操作

在本节实验中,鲁班猫系列板卡,系统设备树中均默认使能了 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的控制。

更新设备树重启后在目录/proc/device-tree/下,可以找到led_test,如下所示:

led_test设备节点

执行如下命令加载驱动:

sudo insmod led_test.ko
加载led_test驱动

驱动加载成功后直接运行应用程序如下所示。

命令:./test_app <命令>

命令是一个“unsigned char”型数据,输入1表示灭,0表示亮。

执行结果如下:

应用程序测试效果

与此同时,观察板卡心跳灯可以看到LED亮或者灭。