目录
一、设备树
对于点灯字符设备驱动程序可以有三种写法,首先是传统方法,这种方式直接在程序中写死,其次是利用总线设备驱动模型,利用分离的思想,将程序规划为两部分,对于驱动部分一般是稳定的,在设备部分更改资源,最后是利用设备树方法,可以使用设备树直接指明引脚,而驱动写法的核心不变,差别就在于如何指定硬件资源,这三种方法有各自的优缺点
优点 | 缺点 | |
传统方法 | 简单 | 不易扩展,需重新编译 |
总线设备驱动模型 | 易扩展,只需要修改资源 | 稍微复杂 ,冗余代码多(如果有多个版本的硬件就会有多个版本的程序),需重新编译 |
设备树 | 易扩展,无冗余代码,不需重新编译内核或者驱动 ,只需要提供不一样的设备树文件 | 复杂 |
对于设备树,通过dts文件来指定资源,内核会根据.dts文件分配设置注册platform_device(平台设备),当更改单板时,只需要重新定义dts文件就可以了,dts文件最终会编译为dtb文件,启动单板的时候,既要启动内核,也要传入dtb文件,因此更改单板时,不需要重新编译驱动程序,只需要提供一个不一样的dtb文件
二、DTS格式
写设备树我们是写出dts文件,文件写出来之后根据编译器dtc编译为二进制dtb文件,DTS文件布局(layout)如下,第一行表示版本,第二行表示保留的的内存区域,比如板子有64M内存,里面想留下4M给自己使用,不想让内核使用,就可以定义这个选项,如果想让内核使用全部内存就可以省略这个选项
/dts-v1/;
[memory reservations] /* 格式为:/memreserve/ <address> <length>; */
/{ /* /表示根,设备树的起点 */
[property definitions] /* 首先有属性来描述硬件 */
[child nodes] /* 子节点,在子节点中还可以有子节点 */
};
2.1 属性
对于属性"[property definitions]"的格式有两种,即无值和有值两种方式,属性后需要分号
Property格式1:
[label:] property-name = value;
Property格式2(没有值):
[label:] property-name = value;
如下led节点,可以知道属性中的value值的形式有多种
led {
compatible = "jz2440_led";
pin = <S3C2410_GPF>; //S3C2410_GPF为一个宏
};
对于value的值有三种表示:arrays of cells(1个或多个32位数据, 64位数据使用2个32位数据表示)、string(字符串)、bytestring(1个或多个字节),这些值什么含义,完全取决于驱动程序
示例:
interrupts = <17 0xc>;(里面每一个数据都是32位数据)
clock-frequency = <0x00000001 0x00000000>; (64bit数据使用2个cell来表示 )
compatible = "simple-bus"; (A null-terminated string有结束符的字符串)
A bytestring(字节序列) : local-mac-address = [00 00 12 34 56 78]; 或者local-mac-address = [000012345678]; (每个byte使用2个16进制数来表示 )
可以是各种值的组合, 用逗号隔开: compatible = "ns16550", "ns8250";example = <0xf00f0000 19>, "a strange property format";
特殊、默认的属性:
在根节点中
- #address-cells 在它的子节点的reg属性中, 使用多少个u32整数来描述地址(address)
- #size-cells 在它的子节点的reg属性中, 使用多少个u32整数来描述大小(size)
如下,若CPU为64位的,则需要两个32位来表示reg,因此可以写为#address-cells = <1>; 对于reg中还可以写多个内存,如reg = <0x30000000 0x40000000 0 4096>; 第一个是地址单元 第二个表示大小,可以写多个address size
compatible:定义一系列的字符串, 用来指定内核中哪个machine_desc可以支持本设备,即这个板子兼容哪些平台, uImage是用来选择单板的,uImage支持很多单板每个单板都会有一个machine_desc结构体 ,如下设备树优先去寻找 “samsung,smdk2440”的machine_desc,如果找不到的话再来找 "samsung,s3c24xx"
chosen节点可以设置内核command line参数, 跟u-boot中设置的bootargs作用一样
cpus节点代表有多个CPU,对于个人的单板只有一个CPU,就可以只写一个子节点,在子节点中需要写device_type = "cpu";上面的例子没有引用cpus节点,cpu子节点中用reg属性用来标明自己是哪一个cpu, 所以 /cpus 中有以下2个属性: #address-cells:在它的子节点的reg属性中, 使用多少个u32整数来描述地址(address),#size-cells:在它的子节点的reg属性中, 使用多少个u32整数来描述大小(size),必须设置为0
/ {
model = "SMDK24440";
compatible = "samsung,smdk2440";
#address-cells = <1>;
#size-cells = <1>;
memory {
device_type = "memory"; //这是大家约定的,必须写这个
reg = <0x30000000 0x4000000>; //reg用来表示内存的起始地址和大小
};
/*
cpus {
cpu {
compatible = "arm,arm926ej-s";
};
};
*/
chosen {
bootargs = "noinitrd root=/dev/mtdblock4 rw init=/linuxrc console=ttySAC0,115200";
};
led {
compatible = "jz2440_led";
pin = <S3C2410_GPF(5)>;
};
};
2.2 节点
对于节点"[child nodes]"的格式如下,可以看出节点可以有自己的子节点,节点结束后需要分号
[label:] node-name[@unit-address] { //label可以写也可以不写,写的时候别人引用该节点就比较方便,而node-name比如上面写的led
[properties definitions]
[child nodes]
};
对于node-name后面可以加上地址,组合起来就是节点的名字,假设有两个memory(内存)节点在同一级别就可以来用区别,用@加上内存的首地址来表示名字的不同,在memory的子目录下可以有同名的memory目录,当然没有这么古怪的节点,这里只是举例
memory@30000000{
device_type = "memory";
reg = <0x30000000 0x4000000>;
memory{
device_type = "memory";
reg = <0 4096>;
};
};
memory@0{
device_type = "memory";
reg = <0 4096>;
};
2.3 引用其他节点
利用节点中的phandle属性, 它的取值必须是唯一的(不要跟其他的phandle值一样)
pic@10000000 {
phandle = <1>;
interrupt-controller;
};
another-device-node {
interrupt-parent = <1>; // 使用phandle值为1来引用上述节点
};
还有另外一种方式,实质也是利用了phandle属性,使用label来引用节点
PIC: pic@10000000 { //编译器dtc会在父节点插入一个 phandle = <xxx>; 然后会把下面的引用<&PIC>替换为<xxx>
interrupt-controller;
};
another-device-node {
interrupt-parent = <&PIC>; // 使用label来引用上述节点,
// 使用lable时实际上也是使用phandle来引用,
// 在编译dts文件为dtb文件时, 编译器dtc会在dtb中插入phandle属性
};
举例,在dts中会把共同的地方抽出来然后写为dtsi文件
- dtsi文件
// 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 { /* /memory */
device_type = "memory";
reg = <0x30000000 0x4000000 0 4096>;
};
/*
cpus {
cpu {
compatible = "arm,arm926ej-s";
};
};
*/
chosen {
bootargs = "noinitrd root=/dev/mtdblock4 rw init=/linuxrc console=ttySAC0,115200";
};
led {
compatible = "jz2440_led";
pin = <S3C2410_GPF(5)>;
};
};
- dts文件
// 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
*/
/dts-v1/;
#include "jz2440.dtsi"
/ {
led {
pin = <S3C2410_GPF(6)>;
};
};
这样后面写的属性会覆盖前面的属性,反汇编"./scripts/dtc/dtc -I dtb -O dts -o tmp.dts arch/arm/boot/dts/jz2440.dtb ",查看tmp.dts可以看到led节点中的pin引脚就被覆盖了
/dts-v1/;
/ {
model = "SMDK24440";
compatible = "samsung,smdk2440";
#address-cells = <0x1>;
#size-cells = <0x1>;
memory {
device_type = "memory";
reg = <0x30000000 0x4000000 0x0 0x1000>;
};
chosen {
bootargs = "noinitrd root=/dev/mtdblock4 rw init=/linuxrc console=ttySAC0,115200";
};
led {
compatible = "jz2440_led";
pin = <0x50006>;
};
};
上面这种方式比较麻烦,若不想重头写led节点可以修改dtsi中的led节点加上label
LED: led {
compatible = "jz2440_led";
pin = <S3C2410_GPF(5)>;
};
修改dts中的led节点,反汇编之后一样会看到节点的变化
/dts-v1/;
#include "jz2440.dtsi"
&LED {
pin = <S3C2410_GPF(7)>;
};
2.4 小总结
在内核文档中Documentation\devicetree\usage-model.txt,其中对设备树进行了三部分的总结,首先是平台识别信息,其次是运行时的配置,最后是设备枚举
Linux uses DT data for three major purposes:
1) platform identification,
2) runtime configuration, and
3) device population.
举例
- 平台信息
model = "SMDK24440";
compatible = "samsung,smdk2440";
#address-cells = <1>;
#size-cells = <1>;
- 运行时的配置,设置bootargs传给内核,还有/memreserve/ 0x33000000 0x10000;
chosen {
bootargs = "noinitrd root=/dev/mtdblock4 rw init=/linuxrc console=ttySAC0,115200";
};
- 设备枚举
LED: led {
compatible = "jz2440_led";
pin = <S3C2410_GPF(5)>;
};
三、DTB格式
用dtc编译器把dts文件转换为dtb文件,首先会将宏展开,其次还有dtsi和dts编译为一个dtb文件,可以根据可读性强的代码进行帮我们找到错误,对于DTB格式,可以阅读官方文档:https://www.devicetree.org/specifications/ ,也可以阅读内核文档:Documentation/devicetree/booting-without-of.txt
3.1 结构
dtb文件分为四部分,首先是头部信息,其次是内存保留区、结构区和字符串区,对于保留区:例/memreserve/ 0x33f00000 0x100000;放在了memory reserve map,其中都是64位表示的,在dtb文件中数据存放的格式是以大端保存的,对于小端高位存放在高地址,对于大端低位存放在低地址
在官方文件中头部header的结构体,在文档描述了magic必须是D00DFEED,totalsize为文件大小,查看文件属性占用空间是4096而实际的大小是465个字节,465的16进制是1D1,因此是"01 D1 00 00",而off_dt_struct是structure block在文件中的偏移地址,off_dt_strings、off_mem_rsvmap同理,在strings block存放属性的名称,每一个名字有0字符结尾,属性的名字例如compatible用了多次为了节约空间,把属性的名字单独作为字符串保留在strings block里面
struct fdt__header {
uint32_t magic;
uint32_t totalsize;
uint32_t off_dt_struct ;
uint32_t off_dt_strings ;
uint32_t off_mem_rsvmap;
uint32_t version;
uint32_t last_comp_version;
uint32_t boot_cpuid_phys;
uint32_t size_dt_strings;
uint32_t size_dt_struct;
};
对于内存保留区在文档中也有个结构体用来表示 ,并且都是64位的表示地址和大小,在下面例子中"/memreserve/ 0x33f00000 0x100000;"在dts文件中在第四行和五行标红的地方
struct fdt_reserve_entry {
uint64_t address;
uint64_t size;
};
3.2 分析
- dtb文件
// 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/;
/memreserve/ 0x33f00000 0x100000;
/ {
model = "SMDK24440";
compatible = "samsung,smdk2440";
#address-cells = <1>;
#size-cells = <1>;
memory { /* /memory */
device_type = "memory";
reg = <0x30000000 0x4000000 0 4096>;
};
/*
cpus {
cpu {
compatible = "arm,arm926ej-s";
};
};
*/
chosen {
bootargs = "noinitrd root=/dev/mtdblock4 rw init=/linuxrc console=ttySAC0,115200";
};
led {
compatible = "jz2440_led";
pin = <S3C2410_GPF(5)>;
};
};
- dts文件
分析device-tree structure区域,是最复杂的区,用0x00000001表示根节点和子节点的开始,从0x00000002结束,整个根节点结束 0x00000009
而节点的名字并不是放在strings block里面 而是紧跟着0x00000001后面加上节点名字,根节点没有名字,因此后面都0,直到0x00000003表示属性开始,那怎么表示名字和值呢,在属性后面有一个结构体,然后再加上val 如果val不是向4取整的话就会自动向4取整,对于第二个属性也一致
struct{
uint32_t len; //val长度
uint32_t nameoff; //表示属性名字在strings blosk的偏移
}
对于字符串的偏移值为188h, 第一个属性为model,大小为0x0A,在字符串区的偏移值为0,值为"53 4D 44 4B 32 34 34 34 30 00 00 00",自动向4取整,紧接着从第二属性开始即"00 00 00 03"