三种Linux字符设备驱动写法-3:设备树

本文主要带新手体会使用设备树,与之前讲的总线设别驱动模型和基本框架驱动之间的区别。

三种Linux字符设备驱动写法-1:最简单的基本框架

三种Linux字符设备驱动写法-2:总线设备驱动框架
 

1. 什么是设备树

还是以 LED 驱动为例,如果你要更换 LED 所用的 GPIO 引脚,该怎么办?

在我们学了第一篇基本框架,应该知道就是把对应引脚值修改,然后整个.c文件重新编译,但缺点就是修改麻烦;

学了第二篇总线设备驱动模型后,我们把设备(资源)驱动分离开,dev.c和driver.c分别编译加载进内核,那么修改引脚只需要修改设备(资源)部分。

但是问题来了,Linux内核支持的板子太多了,每一个设计都有不同,以led为例,在A板子上用GPF4,在B板子上用GPF5......那样在内核文件中为了匹配板子,就会有大量.c文件。

linux 内核 arm 架构下添加了很多开发板的适配文件,打开内核源码,和arm开发板相关的文件放在arch/arm/mxch-xxx目录下,这些 c 文件仅仅用来适配某款开发板,对于 Linux 内核来说并没有提交什么新功能,但是每适配一款新的开发板就需要一堆文件,导致 Linux 内核越来越臃肿

Linux的创始人Linus 大发雷霆: "this whole ARM thing is a f*cking pain in theass"。

于是, Linux 内核开始引入设备树(device tree)

我们编写一个设备树文件(dts: device tree source),然后编译为dtb(device tree blob)文件,内核解析dtb,dts的本质用来描述板级设备信息的一种数据结构。内核使用一个dtb 文件,就可以代替一堆.c文件。

设备树的结构是基于第二篇中所讲的总线设备驱动模型的,区别只是在于对设备(资源)表示方式的不同。

如图,系统总线为树干,各个设备控制器为节点(也可以是设备),各设备为子节点,是不是特别像树。

其实dts的编写结构和这张图也有神似。

2. dts编写语法

先放一个简单的dts,然后一点点介绍:

/dts-v1/;

/ {
	model = "SMDK24440";
	compatible = "samsung,smdk2440";
	#address-cells = <0x1>;
	#size-cells = <0x1>;

	memory@30000000 {
		device_type = "memory";
		reg = <0x30000000 0x4000000>;
	};

	chosen {
		bootargs = "noinitrd root=/dev/mtdblock4 rw init=/linuxrc console=ttySAC0,115200";
	};

	led {
		compatible = "jz2440_led";
		pin = <0x50006>;
	};
};

2.1  设备树版本与保留内存

设备树版本

/dts-v1/

保留内存

/memreserve/<address><length>;

设备树版本的下一行可以加保留内存选项,让内核保留一段内存不用,如果让内核使用全部内存可省略,上文中的例子就没有该项。

2.2 设备树节点

设备树是由一个个节点组成的,每个节点相当于树上的一片叶子。

2.2.1 节点

/ {
...
};

 最外面的叫根节点。

memory@30000000 {
...
};

chosen {
...
};

led {
...
};

里面的是节点,用{};分隔,这里就表示了三个节点,节点中还可以有节点。

节点语法:

[label:] node-name[@unit-address] {    
    [properties definitions];    
    [child nodes];
};

1)label: 节点别名(标签),可写可不写,用:隔开,为了方便访问节点,可用直接通过&lable来访问节点

2)node-name: 节点名

3)unit-address: 设备地址,可写可不写。因为同级别节点名字不能一样,用设备地址可区分。例如:

memory@30000000{
    device_type="memory";
    reg=<0x30000000 0x4000000>;
}
memory@0{
    device_type="memory";
    reg=<0 4096>;
}

4)properties definitions:属性定义

5)child nodes:子节点

2.2.2 节点属性

[label:] property-name = value;
[label:] property-name;

属性分为有属性值无属性值

属性值有三种取值

1.arrays of cells(1个或多个32位数据, 64位数据使用2个32位数据表示),用尖括号表示< >,如:

example=<0x11223344 123>;

2.string(字符串), 用双引号表示" ",如:

example="hello";

3.bytestring(1个或多个字节),用方括号表示[ ],一个字节必须用两位16进制数表示,比如0必须写成[00],例如[00 11 22],空格可以省略,如[001122]

三种可以组合,用,号隔开,如:

example=<0x10101010 11>,"hello",[001122];

一般不会这么做。

2.2.2.1 compatible

compatible 属性值由 string list (字符串列表)组成,以"",符号分隔几个字符串,定义了设备的兼容性,推荐格式为manufacturer,model,manufacturer 描述了生产商,model 描述了型号。

compatible = "samsung,smdk2440","samsung,smdk2410";

这句表示兼容2440和2410两个板子。compatible属性在根节点下就是用来寻找对应的machine_decs,执行对应的初始化函数,在节点下就是用来寻找对应的驱动程序,归根结底就是匹配作用。

驱动程序先使用第一个兼容值在 Linux 内核中查找,看看能不能找到对应的驱动文件;如果没有找到的话,就使用第二个兼容值查找。

一般驱动程序文件都会有一个 OF 匹配表,此 OF 匹配表保存着一些 compatible 值,如果设备节点的 compatible 属性值核 OF 匹配表中的任何一个值相等,那么就表示这个设备可以使用这个驱动。

2.2.2.2 model

model 属性值是一个 string,指明了设备的厂商和型号,推荐格式为manufacturer,model

model = "samsung smdk2440";

 compatible用来指明兼容哪些,model表示他到底是什么东西。

2.2.2.4 reg

reg 的本意是 register,用来表示寄存器地址。但是在设备树里,它可以用来描述一段空间。反正对于 ARM 系统,寄存器和内存是统一编址的,即访问寄存器时用某块地址,访问内存时用某块地址,在访问方法上没有区别。

reg 属性值用来描述设备地址空间资源信息,一般是某个外设的寄存器地址范围信息,包括起始地址和地址长度。

reg = <address1 length1 address2 length2 address3 length3……>

如示例中:

reg = <0x30000000 0x4000000>;

代表了0x30000000地址开始的0x4000000字节内存。再如:

reg = <0x30000000 0x4000000 0 4096>;

代表了两段内存:代表了0x30000000地址开始的0x4000000字节内存,和0地址开始的4096字节内存。

如果是64位地址呢?那就需要2个32位数字才能表示一个地址了,怎么分辨?用到下文的#address-cells 和 #size-cells。

2.2.2.3 #address-cells 和 #size-cells

#address-cells and #size-cells 属性值是一个 u32,可以用在任何拥有子节点的设备中,并描述子设备节点应该如何寻址

#address-cells属性定义子节点 reg 属性地址字段所占用的字长,也就是占用 u32 单元格的数量。

#size-cells属性定义子节点 reg 属性值长度所占用的 u32 单元格的数量。

#address-cells = <0x1>;
#size-cells = <0x1>;

表示用1个32位数表示地址,用1个32位数表示长度。

2.2.2.5 chosen

chosen 节点是为了uboot 向 Linux 内核传递数据,重点是 bootargs 参数,这个涉及内核启动,不在本文讲。

2.2.3  引用其他节点

(1) phandle属性引用

节点中的phandle属性, 它的取值必须是唯一的(不要跟其他的phandle值一样)

pic@10000000 {
    phandle = <1>;
    interrupt-controller;
};

another-device-node {
    interrupt-parent = <1>;   // 使用phandle值为1来引用上述节点
};

pic代表中断控制器,interrupt-parent指名节点another-device-node的中断父亲是pic@100000000

(2)使用别名label(本质还是phandle)

PIC: pic@10000000 {
    interrupt-controller;
};

another-device-node {
    interrupt-parent = <&PIC>;   // 使用label来引用上述节点, 
                                 // 使用lable时实际上也是使用phandle来引用, 
                                 // 在编译dts文件为dtb文件时, 编译器dtc会在dtb中插入phandle属性
};

 2.2.4 包含dtsi文件

#include "2440.dtsi"

公共部分可以写成dtsi文件,语法和dts一模一样。

在每个.dsti和.dts中都会存在一个“/”根节点,那么如果在一个设备树文件中include一个.dtsi文件,那么岂不是存在多个“/”根节点了么?编译器DTC在对.dts进行编译生成dtb时,会对node进行合并操作,最终生成的dtb只有一个root node。

注意dts中同名的节点中的属性可以覆盖dtsi中的属性。

如:

dts:

/dts-v1/;
#include<2440.dtsi>
/{
    model = "SMDK2440";
    compatible = "samsung,smdk2440";
    #address-cells = <0x1>;
    #size-cells = <0x1>;
/{
    led{
        pin=<3>;    
    };
}

2440.dtsi:

/dts-v1/;
/{
    model = "SMDK2440";
    compatible = "samsung,smdk2440";
    #address-cells = <0x1>;
    #size-cells = <0x1>;
    led {
        pin=<6>;
    };
};

最后led的pin属性是3,正是这个属性可以使公共部分写在dtsi,在dts中写一些各板子的差异。

2.2.5 查看设备树

板子启动后查看设备树,板子启动后执行下面的命令:

# ls /sys/firmware/

得到:devicetree fdt

/sys/firmware/devicetree 目录下是以目录结构呈现的 dtb 文件, 根节点对应 base 目录, 每一个节点对应一个目录, 每一个属性对应一个文件。

这些属性的值如果是字符串,可以使用 cat 命令把它打印出来;对于数值,可以用 hexdump 把它打印出来。还可以看到/sys/firmware/fdt 文件,它就是 dtb 格式的设备树文件,可以把它复制出来放到 ubuntu 上,执行下面的命令反编译出来(-I dtb:输入格式是 dtb, -O dts:输出格式是 dts):cd 板子所用的内核源码目录

./scripts/dtc/dtc -I dtb -O dts /从板子上/复制出来的/fdt -o tmp

3. 设备树驱动

3.1 内核对设备树的处理

内核解析 dtb 文件,把每一个节点都转换为 device_node 结构体;对于部分device_node 结构体,会被转换为 platform_device 结构体

哪些设备树节点会被转换为 platform_device?

1)根节点下含有 compatile 属性的子节点

2)含有特定 compatile 属性的节点的子节点

如果一个节点的 compatile 属性,它的值是这 4 者之一:

"simplebus","simplemfd","isa","arm,amba-bus", 那 么 它 的 子 结 点 ( 需 含compatile 属性)也可以转换为 platform_device。

3)总线 I2C、 SPI 节点下的子节点: 不转换为 platform_device

某个总线下到子节点, 应该交给对应的总线驱动程序来处理, 它们不应该被转换为platform_device。

比如以下的节点中:

{
    mytest {
        compatile = "mytest", "simple-bus";
        mytest@0 {
            compatile = "mytest_0";
        };
    };
    i2c {
        compatile = "samsung,i2c";
        at24c02 {
            compatile = "at24c02";
        };
    };
    spi {
        compatile = "samsung,spi";
        flash@0 {
            compatible = "winbond,w25q32dw";
            spi-max-frequency = <25000000>;
            reg = <0>;
        };
    };
};
  • /mytest 会被转换为 platform_device, 因为它兼容"simple-bus";它的子节点/mytest/mytest@0 也会被转换为 platform_device
  •  /i2c 节点一般表示 i2c 控制器, 它会被转换为 platform_device, 在内核中有对应的platform_driver;
  •  /i2c/at24c02 节点不会被转换为 platform_device, 它被如何处理完全由父节点platform_driver 决定, 一般是被创建为一个 i2c_client。
  •  类似的也有/spi 节点, 它一般也是用来表示 SPI 控制器, 它会被转换为platform_device, 在内核中有对应的 platform_driver;
  •  /spi/flash@0 节点不会被转换为 platform_device, 它被如何处理完全由父节点的platform_driver 决定, 一般是被创建为一个 spi_device。

platform_device 中含有 resource 数组, 它来自 device_node 的 reg,interrupts 属性;

platform_device.dev.of_node 指向 device_node, 可以通过它获得其他属性

platform_dev和platform_drv如何匹配这里不讲,直接去源码搜索platform_match函数,特别简单肯定能看明白。

匹配过程按优先顺序如下:

a. 比较 platform_dev.driver_override 和 platform_driver.drv->name
b. 比较 platform_dev.dev.of_node的compatible属性 和 platform_driver.drv->of_match_table
c. 比较 platform_dev.name 和 platform_driver.id_table
d. 比较 platform_dev.name 和 platform_driver.drv->name

platform_get_resource函数

设 备 树 中 的 节 点 被 转 换 为platform_device 后,设备树中的 reg 属性、 interrupts 属性也会被转换为“ resource”。这时,你可以使用这个函数取出这些资源。
函数原型为:

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

对于设备树节点中的 reg 属性,它对应 IORESOURCE_MEM 类型的资源;对于设备树节点中的 interrupts 属性,它对应 IORESOURCE_IRQ 类型的资源。

of_property_read_u32函数

从device_node节点熟悉中读取u32数,np为节点指针,propname为属性名字

static inline int of_property_read_u32(const struct device_node *np,
				       const char *propname,
				       u32 *out_value)
{
	return of_property_read_u32_array(np, propname, out_value, 1);
}

3.2 修改设备树

直接反编译原来的设备树文件,在内核目录下执行:

./scripts/dtc/dtc -I dtb -O dts /从板子上/复制出来的/fdt -o tmp

然后加上myled0和myled1节点。

	myled0 {
		compatible = "my_led";
		pin = <5>;
	};
	myled1 {
		compatible = "my_led";
		pin = <6>;
	};

3.3 修改驱动

和第二篇一样要定义platform结构体,区别是要加上of_match_table,匹配列表,用来和节点的compatible属性匹配。

static struct platform_driver myled_drv = {
	.probe  = myled_probe,
	.remove = myled_remove,
	.driver = {
		.name = "myled",
		.of_match_table = of_match_leds,
	}
};
static const struct of_device_id of_match_leds[] = {
	{ .compatible = "my_led", .data = NULL },
};

在probe函数中register_chrdev ,构造类,构造设备

static int myled_probe(struct platform_device * pdev)
{
	struct device_node *np;
    int err = 0;

    np = pdev->dev.of_node;
    if (!np)
        return -1;
	
	if(major == 0){	
		gpio_con = ioremap(0x56000050, 8);
		gpio_dat = gpio_con + 1;
		major=register_chrdev(0,"myled",&myled_opr);
		myled_class = class_create(THIS_MODULE, "myled_class");
		if(myled_class==NULL){
				printk("class_create error \n");
				return -1;
		}
	}
	
	err = of_property_read_u32(np, "pin", &led_pin[led_num]);
	device_create(myled_class,NULL, MKDEV(major, led_num), NULL,"myled%d",led_num);
    led_num++; 
	
    return 0;
	
}

of_property_read_u32(np, "pin", &led_pin[led_num]);这就是读取pin属性函数,读出来放到led_pin数组里。

设备树里每有一个节点和驱动匹配上就会调用一次probe函数,就会在/dev下创建一个设备,

但是register_chrdev和class_create只需要一次,所以用major来判断一下是不是第一次进入probe。

完整代码:https://download.csdn.net/download/freestep96/86743909

显然使用了设备树后极其方便,甚至可以使用uboot直接修改设备树文件。

当然这里的代码为了新手可以明白,写得非常简略,学会后可以去看看内核源码其他的驱动是怎么写得,从成熟的代码中学习是十分好的方式,一个优秀的驱动,设计了分层、分离,面向对象的设计思想,不过越容易移植,就越复杂,还需要在工作中权衡。

猜你喜欢

转载自blog.csdn.net/freestep96/article/details/127213211