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

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

9.1. 实验说明

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

9.2. 实验代码讲解

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

9.2.1. 编程思路

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

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

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

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

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

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

9.2.2. 代码分析

9.2.2.1. 添加led设备节点

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

lubancat2板卡led灯GPIO0_C7为例,设备树编写如下:

添加设备节点(kernel/arch/arm64/boot/dts/rockchip/rk3568-lubancat-2.dts)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/ {
    ....

    led_test: led_test{
        compatible = "fire,led_test";
        #address-cells = <1>;
        #size-cells = <1>;

        led@0xfdd60000{ //RK3568的GPIO0基地址
            reg = <0xfdd60000 0x00000004>;
            high-low-position = <1>;    //0表示是低16位引脚,1表示高16位引脚
            led-pin = <7>;              //引脚偏移
        };
    };

};

以lubancat4板卡led灯GPIO4_B5为例,设备树编写如下:

添加设备节点(kernel/arch/arm64/boot/dts/rockchip/rk3588s-lubancat-4.dts)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/ {
    ....

    led_test: led_test{
        compatible = "fire,led_test";
        #address-cells = <1>;
        #size-cells = <1>;

        led@0xfec50000{ //RK3588S的GPIO4基地址
            reg = <0xfec50000 0x00000004>;
            high-low-position = <0>;     //0表示是低16位引脚,1表示高16位引脚
            led-pin = <13>;              //引脚偏移
        };
    };

};

提示

其余系列板卡参考 驱动章节实验环境搭建 章节确定板卡使用的设备树文件,然后进行修改添加。参考 使用内核的构建脚本编译设备树 进行编译。

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

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

  • 第9-13行: led子节点,如上所示,它会用到寄存器,为方便确认引脚,用high-low-position值确认引脚是高16位还是低16位,用led-pin确认引脚偏移。

9.2.2.2. 编写驱动程序

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

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

驱动入口函数

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

驱动初始化函数(位于linux_driver/09_device_tree_led/dts_led.c)
1
2
3
4
5
6
7
8
9
static __init int pdrv_led_init(void)
{
    /* 打印平台驱动初始化信息 */
    printk("led platform driver init\n");
    /* 注册平台驱动 */
    platform_driver_register(&pdrv_led);

    return 0;
}

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

定义平台设备结构体

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

平台设备结构体(位于linux_driver/09_device_tree_led/dts_led.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
static const struct of_device_id dts_led_of_match[] = {
    { .compatible = "fire,led_test" },
    {}
};

/* 定义平台驱动结构体,包含探测、移除函数,驱动名称和设备 ID 表 */
static struct platform_driver pdrv_led = {
    .probe = pdrv_led_probe,
    .remove = pdrv_led_remove,
    .id_table = pdev_led_ids,
    .driver     = {
        .name    = "dts_led",
        .of_match_table = dts_led_of_match,
    },
};
  • 第1-4行: 定义匹配表

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

  • 第9-14行: “.driver = { …}”定义driver的一些属性,其中最需要注意的是“.of_match_table”属性,它指定这个驱动的匹配表。

实现.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
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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
/* 定义字符设备的名称 */
#define DEV_NAME    "dts_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 hl_pos;
    /* 引脚偏移量 */
    unsigned int led_pin;
    /* LED 的设备树子节点 */
    struct device_node *device_node;
};
/* 定义 led_chrdev 结构体指针,用于动态分配内存管理 LED 硬件信息 */
static struct led_chrdev *led_cdev;

/* LED 的设备树节点 */
struct device_node *led_device_node;

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;

    /* 用于存储从 DTS 中读取的 32 位无符号整数数组。 */
    unsigned int gpio_base_addr[1];
    /* 计算得到的 GPIO_DR 地址*/
    u32 gpio_dr_addr;
    /* 计算得到的 GPIO_DDR 地址*/
    u32 gpio_ddr_addr;

    /* 定义临时变量,用于存储寄存器的值 */
    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;

    if (pdev->dev.of_node) {
        /* 设备树匹配 */
        /* 获取led的设备树节点 */
        led_device_node = of_find_node_by_path("/led_test");
        if (led_device_node == NULL) {
            printk("get led_test failed!\n");
            return -ENOMEM;
        }

        /* 获取led_test节点的子节点 */
        led_cdev->device_node = of_find_node_by_name(led_device_node, "led");
        if (led_cdev->device_node == NULL) {
            printk("get led device node failed!\n");
            return -ENOMEM;
        }

        /* 获取 GPIO_BASE 地址 */
        ret = of_property_read_u32_array(led_cdev->device_node,"reg",gpio_base_addr, 1);
        if(ret != 0)
        {
            /* 如果读取失败,打印错误信息并返回 -1 */
            printk("get gpio_base_addr failed!\n");
            return -ENOMEM;
        }

        /* 打印 GPIO_BASE 地址 */
        printk("GPIO_BASE address: 0x%08X\n", gpio_base_addr[0]);

        /* 从设备树获取引脚是高位或低位 */
        ret = of_property_read_u32(led_cdev->device_node, "high-low-position", &led_cdev->hl_pos);
        if(ret < 0)
        {
            printk(KERN_ERR "Failed to get high-low-position from device tree\n");
            return -EINVAL;
        }

        /* 判断引脚是高位还是低位 */
        if (led_cdev->hl_pos == 0) {
            /* 计算 GPIO_DR 地址并映射,此处是GPIO_DR_L */
            gpio_dr_addr = gpio_base_addr[0] + 0x0000;
        } else {
            /* 计算 GPIO_DR 地址并映射,此处是GPIO_DR_H */
            gpio_dr_addr = gpio_base_addr[0] + 0x0004;
        }

        led_cdev->va_dr = devm_ioremap(&pdev->dev, gpio_dr_addr, 4);
        if (!led_cdev->va_dr) {
            printk(KERN_ERR "Failed to ioremap GPIO_DR\n");
            return -ENOMEM;
        }

        /* 判断引脚是高位还是低位 */
        if (led_cdev->hl_pos == 0) {
            /* 计算 GPIO_DDR 地址并映射,此处是GPIO_DDR_L */
            gpio_ddr_addr = gpio_base_addr[0] + 0x0008;
        } else {
            /* 计算 GPIO_DDR 地址并映射,此处是GPIO_DDR_H */
            gpio_ddr_addr = gpio_base_addr[0] + 0x000C;
        }

        led_cdev->va_ddr = devm_ioremap(&pdev->dev, gpio_ddr_addr, 4);
        if (!led_cdev->va_ddr) {
            printk(KERN_ERR "Failed to ioremap GPIO_DDR\n");
            return -ENOMEM;
        }

        /* 从设备树获取引脚偏移量 */
        if (of_property_read_u32(led_cdev->device_node, "led-pin", &led_cdev->led_pin) < 0) {
            printk(KERN_ERR "Failed to get led pin offset from device tree\n");
            return -EINVAL;
        }
    } else {
        /* 平台设备匹配 */
        /* 获取平台设备的私有数据,得到 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-27行: 自定义led资源结构体,用于保存获取得到的设备节点信息以及转换后的虚拟寄存器地址。

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

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

  • 第82-88行: 读取子节点reg属性(GPIO基地址),存入gpio_base_addr数组;

  • 第94-99行: 读取子节点high-low-position属性(引脚高低位标识),存入led_cdev->hl_pos,确认引脚是高位还是低位;

  • 第102-114行: 根据hl_pos计算DR/DDR寄存器物理地址,并通过devm_ioremap映射为内核虚拟地址;

  • 第132-135行: 读取子节点led-pin属性(引脚偏移量),存入led_cdev->led_pin。

其余部分和平台设备驱动章节实验代码是一样的,不做赘述。

9.3. 编译设备树和驱动

9.3.1. 编译设备树

将led_test节点添加到设备树中,并在内核源码目录执行如下命令。

1
2
3
#这里以rk356x系列4.19.232内核配置文件为例
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- lubancat2_defconfig
make ARCH=arm64 -j4 CROSS_COMPILE=aarch64-linux-gnu- dtbs

提示

其余系列板卡参考 驱动章节实验环境搭建 章节确定板卡使用的设备树文件,然后进行修改添加。参考 使用内核的构建脚本编译设备树 进行编译。

编译成功后生成的设备树文件(.dtb)位于源码目录下的arch/arm64/boot/dts/rockchip/,文件名为“rk3568-lubancat2.dtb”

9.3.2. 编译驱动程序

9.3.2.1. makefile修改说明

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

Makefile(位于linux_driver/09_device_tree_led/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 := dts_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

9.3.2.2. 编译命令说明

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

1
2
3
4
5
#进入device_tree_led例程源码目录
cd linux_driver/09_device_tree_led

#编译驱动模块
make

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

9.4. 程序运行结果

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

9.4.1. 实验操作

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

将设备树、驱动程序拷贝到开发板中,替换/boot/dtb/目录下原来的同名设备树文件,并重启开发板。

1
2
3
4
#先传输到板卡

#再覆盖/boot/dtb/目录下同名文件,根据实际修改
sudo cp -f rk3568-lubancat2.dtb /boot/dtb/

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

1
2
3
4
5
#查看设备树节点目录
ls /proc/device-tree/led_test

#信息输出如下
'#address-cells'  '#size-cells'   compatible   led@0xfdd60000   name   phandle

执行以下命令加载驱动:

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

#信息输出如下
[ 3402.518948] led platform driver init
[ 3402.520096] led platform driver probe
[ 3402.520563] GPIO_BASE address: 0xFDD60000
[ 3402.520697] major=236, minor=0

可以看到驱动加载时打印了一次led platform driver probe,说明匹配成功,打印了GPIO基地址为0xFDD60000,与设备树配置的一致。

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

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

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