本节简单介绍怎么使用设备树,并别写对应的驱动程序,后面将会再深入学习设备树。
上节使用总线设备模型,它与设备树的不同在于平台设备的构建。
使用总线设备模型,驱动程序被分为两部分:
- dev:主要是分配/设置/注册platform_device;
- drv:主要是分配/设置/注册platform_driver;
dev和drv通过bus进行匹配,当bus检测到匹配的dev和drv,将会调用drv->probe函数,在probe函数中将会分配/设置/注册file_operations结构体。
使用设备树,驱动程序同样被分为两部分:
- dts文件:在dts文件中构造节点,节点中含有资源;
- drv:主要是分配/设置/注册platform_driver;
其中drv->probe函数,分配/设置/注册file_operations结构体。
在总线设备模型中,platform_device在dev.c中,dev是.c文件,每次修改需要重新编译。
在设备树中,dts文件被编译成dtb文件,然后传给内核,内核会来处理解析dtb文件,得到一个一个的device_node结构体,这个device_node结构体会转换成platform_device资源。然后这个platform_device就会和drv链表中的platform_driver进行匹配,匹配成功就会调用对应的drv->probe函数来分配/设置/注册file_operations结构体。
dts->dtb->device_node->platform_device
所以,总线设备驱动模型和设备树的区别在于,之前的platform_device资源在dev.c文件中配置,而使用设备树,platform_device资源则来自于dts文件。
可以这么说,设备树是针对总线驱动模型的一种改进,使用设备树可以更方便的修改platform_device。
下面是百问网提供的设备树文件。
// SPDX-License-Identifier: GPL-2.0
/*
* SAMSUNG SMDK2440 board device tree source
*
* Copyright (c) 2018 weidongshan@qq.com
* dtc -I dtb -O dts -o jz2440.dts jz2440.dtb
*/
#define S3C2410_GPA(_nr) ((0<<16) + (_nr))
#define S3C2410_GPB(_nr) ((1<<16) + (_nr))
#define S3C2410_GPC(_nr) ((2<<16) + (_nr))
#define S3C2410_GPD(_nr) ((3<<16) + (_nr))
#define S3C2410_GPE(_nr) ((4<<16) + (_nr))
#define S3C2410_GPF(_nr) ((5<<16) + (_nr))
#define S3C2410_GPG(_nr) ((6<<16) + (_nr))
#define S3C2410_GPH(_nr) ((7<<16) + (_nr))
#define S3C2410_GPJ(_nr) ((8<<16) + (_nr))
#define S3C2410_GPK(_nr) ((9<<16) + (_nr))
#define S3C2410_GPL(_nr) ((10<<16) + (_nr))
#define S3C2410_GPM(_nr) ((11<<16) + (_nr))
/dts-v1/;
/ {
model = "SMDK24440";
compatible = "samsung,smdk2440";
#address-cells = <1>;
#size-cells = <1>;
memory@30000000 {
device_type = "memory";
reg = <0x30000000 0x4000000>;
};
/*
cpus {
cpu {
compatible = "arm,arm926ej-s";
};
};
*/
chosen {
bootargs = "noinitrd root=/dev/mtdblock4 rw init=/linuxrc console=ttySAC0,115200";
};
led {
compatible = "jz2440_led";
reg = <S3C2410_GPF(5) 1>;
};
};
可以看到,它可以使用一些C语言的语法,使用 /* */ 和 // 来添加注释,使用#define来设置宏。
这个节点设置了内核的命令行参数。
查看led节点,它有两个属性,一个compatible,后面就使用它在内核中找到能够支持这个节点的驱动程序,能支持这个节点的platform_driver;还有一个reg,本意是register,寄存器,在ARM系统中,寄存器和内存是同样对待的,因为寄存器的访问空间和内存的访问空间没什么差别。
在上节中,我们曾经使用一个platform_device,在resource中将flags设置成了mem,但实际上它并不是一个mem资源,我们也没有将它作为mem资源使用,而是将它转为一个led_pin来使用。
在这个设备树文件中,也是同样的做法,reg本来是寄存器的地址,但是在这个设备树文件中,将它设置为了某个引脚(S3C2410_GPF(5))。
在驱动程序中要将这个值/引脚读出来,将它作为一个引脚。
后面还有一个1,这是size,这次并没有使用,但是也需要提供一个size(对应内存的起始地址和大小,只是我们把起始地址当做引脚,不需要大小)。
介绍完dts文件,将这个文件传入/home/book/code/linux-4.19-rc3/arch/arm/boot/dts目录下,然后重新编译设备树文件。
编译之前需要先设置工具链。
export PATH=/home/book/code/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabi/bin/:$PATH
然后在内核目录下执行make dtbs编译设备树文件。
其中arch/arm/boot/dts/jz2440.dtb就是编译出来的dtb文件,将dtb文件拷贝到nfs挂载的路径下。
重启开发板,在uboot中执行命令,下载dtb文件,从虚拟机中下载dtb到0x32000000位置。
nfs 32000000 192.168.0.103:/work/nfs_root/003_led_device_tree/jz2440.dtb
使用nfs挂载需要先连上虚拟机,ping虚拟机失败。
将ipaddr设为192.168.0.108,确保虚拟机和开发板处于同一个网段,再ping一次可以检测到虚拟机。
再次下载,下载成功。
然后将device_tree分区擦除,将刚刚下载的dtb文件载入。
分区信息如下图所示,擦除的是第1个分区,device_tree分区。
然后重启开发板,进入/sys/devices/platform目录,执行ls命令,可以看到有一个50005.led目录。
进入该目录,执行ls。
进入of_node(open firmware node)文件夹,可以看到里面有三个文件,compatible,name和reg。
正好对应dts文件中的name=led,compatible=jz2440_led,reg=<0050 00 0 1>。
也就是说,设备树文件中的节点node确实被转换成了platform_device,那么要怎么去写对应的platform_driver。
在之前的总线设备模型中说过,有一个平台总线,里面有一个match函数,用来匹配dev和drv,所以我们需要查看一下这个match函数,看看它是怎么去匹配平台设备和平台驱动的。
传统的方法是比较name,但是对于从设备树构造的平台设备时怎么匹配的呢?
这个问题需要看源码分析,使用设备树时,在match函数中是调用of_driver_match_device函数来匹配dev和drv的。
点击进入of_driver_match_device函数,可以发现该函数直接return了of_match_device函数。
其中drv指向了结构体成员of_match_table,该变量包含了name,type,compatible。
可以猜测一下,这个compatible就会和从dts中得到的compatible属性进行比较,一样的话,就匹配成功。
了解了这些就可以开始写代码了,具体的研究下一节再展开。
在002的基础上进行修改,去掉dev相关的代码。
修改makefile,屏蔽led_dev.o。
然后修改led_drv.c,在led_drv.c中有一个led_drv结构体。
struct platform_driver led_drv = {
.probe = led_probe,
.remove = led_remove,
.driver = {
.name = "myled",
}
};
在led_drv.driver.name中添加.of_match_table = of_match_leds。
struct platform_driver led_drv = {
.probe = led_probe,
.remove = led_remove,
.driver = {
.name = "myled",
.of_match_table = of_match_leds, /* 能支持哪些来自dts的platform_device */
}
};
其中 of_match_leds 如下,.compatible = "jz2440_led"对应dts文件中led节点的compatible属性的值,data没有用上,设置为NULL。
static const struct of_device_id of_match_leds[] = {
{ .compatible = "jz2440_led", .data = NULL },
{ }
};
.compatible = "jz2440_led"的值设置得并不符合规范,正常应该为.compatible = "jz2440,led",表示MPU为jz2440,控制的外设为led,但是目前先这样写,匹配dts文件。
代码修改完成,编译实验一下。
首先,设置环境变量。
export PATH=/home/book/code/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabi/bin/:$PATH
然后编译将编译生成的led_drv.ko文件传到nfs挂载的路径下。
在开发板上insmod这个ko文件,然后lsmod可以看到,出现了一个led_drv模块,在dev目录下也出现了一个led设备。
此时执行测试程序,./ledtest on和off,可以看到led随着指令的控制亮灭。
如果要改变使用的引脚,只需要将dts文件中led节点的reg属性的值进行修改,然后重新编译下载即可,而不用编译任何.c文件。
问:在dts文件中,使用的是reg来指定led引脚,但是reg本意是寄存器,是否有更直接的表示方法?
答:有,可以将reg改为pin,在drv代码中获取这个pin值即可。
修改dts,重新编译下载到开发板上,然后还需要修改一下drv代码。
以前是获取IORESOURCE_MEM类型的资源,现在则是要获取名为pin的属性,把这个属性转换为led引脚。
为了兼容以前的程序,可以先获取IORESOURCE_MEM资源,获取不到的时候再获取pin属性。
问:怎么获取pin属性呢?
答:这些设备节点相关的函数,可以在of.h文件中查看。
使用of_property_read_s32函数就可以从dtb中获取某个指定的属性。
static inline int of_property_read_s32(const struct device_node *np,
const char *propname,
s32 *out_value)
{
return of_property_read_u32(np, propname, (u32*) out_value);
}
修改后的led_probe函数如下。
static int led_probe(struct platform_device *pdev)
{
struct resource *res;
/* 根据platform_device的资源进行ioremap */
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (res) {
led_pin = res->start;
} else {
/* 获得pin属性 */
of_property_read_s32(pdev->dev.of_node, "pin", &led_pin);
}
/* 如果都没有获取到led_pin值,返回错误 */
if (!led_pin) {
printk("can not get pin for led!\n");
return -EINVAL;
}
major = register_chrdev(0, "myled", &myled_oprs);
led_class = class_create(THIS_MODULE, "myled");
device_create(led_class, NULL, MKDEV(major, 0), NULL, "led"); /* /dev/led */
return 0;
}
修改编译后试验,可以看到,小灯同样可以根据输入的指令亮灭,说明成功使用pin属性设置了led_pin。
最后,在总线设备模型中,有一个platform_device结构体,当使用设备树时,platform_device结构体的dev成员,它有一个成员叫of_node,of_node中含有属性,含有的属性则取决于设备树,如compatible和pin属性,compatible属性会最先被用来匹配对应的drv程序。