没有Device Tree的ARM linux是如何运转的?
1、自己撰写一个bootloader并传递适当的参数给kernel。除了传统的command line以及tag list之类的,最重要的是申请一个machine type,当拿到属于自己项目的machine type ID的时候。
2、在内核的arch/arm目录下建立mach-xxx目录,这个目录下,放入该SOC的相关代码,例如中断controller的代码,时间相关的代码,内存映射,睡眠相关的代码等等。
此外,内核提供了一个重要的结构体struct machine_desc ,内核通过machine_desc结构体来控制系统体系架构相关部分的初始化。machine_desc结构体的成员包含了体系架构相关部分的几个最重要的初始化函数。
同时一个内核可以支持多块单本,所以一个zImage里可以包含多个machine_desc结构体。他们通常被组织成一个特殊的数据段。
早期machine_desc结构体通过MACHINE_START宏来初始化,在代码中, 通过在start_kernel->setup_arch中调用setup_machine_tags来获取。
#define MACHINE_START(_type,_name) \
static const struct machine_desc __mach_desc_##_type \
__used \
__attribute__((__section__(".arch.info.init"))) = { \ //这个结构体被编译成一个特殊的段
.nr = MACH_TYPE_##_type, \
.name = _name,
#define MACHINE_END \
};
/* 所有用MACHINE_START定义的结构体都被组织到一个.arch.info.init段中 */
.init.arch.info : {
__arch_info_begin = .;
*(.arch.info.init)
__arch_info_end = .;
}
MACHINE_START(project name, "xxx公司的xxx硬件平台")
.phys_io = 0x40000000,
.boot_params = 0xa0000100,
.io_pg_offst = (io_p2v(0x40000000) >> 18) & 0xfffc,
.map_io = xxx_map_io,
.init_irq = xxx_init_irq,
.timer = &xxx_timer,
.init_machine = xxx_init,
MACHINE_END
在xxx_init函数中,一般会加入很多的platform device。因此,伴随这个board specific文件中是大量的静态table,描述了各种硬件设备信息。
3、调通了system level的driver(timer,中断处理,clock等)以及串口terminal之后,linux kernel基本是可以起来了,后续各种driver不断的添加,直到系统软件支持所有的硬件。
设备树特点
a、对于传统字符驱动的编写有两种方式:
一是在驱动程序中,直接写死硬件资源,如:GPIO、寄存器地址、中断号等,使得硬件改动时,必须修改驱动程序。
二是采用总线驱动platform模型,将硬件资源与驱动软件分离,在platform_device中描述硬件资源,arch/arm/mach-xxx对应的文件,便是以platform_device描述各自CPU对应的硬件资源;
在platform_driver中分配/设置/注册 file_operations结构体, 并从platform_device获得硬件资源。这种编写方式使得驱动易于扩展,硬件改动时只需修改platform_device或者platform_driver,这就导致linux内核产生大量的冗余代码。
b、 使用设备树的特点在于,在设备树dts文件指定硬件资源,dts被编译为dtb文件, 在启动单板时,U-boot会将dtb文件传给内核,使得驱动程序与硬件分离,我们只需要修改dts文件,便能实现需求。这就是设备树易于扩展,硬件有变动时不需要重新编译内核或驱动程序,只需要提供不一样的dtb文件。原来通过tag list传递的一些linux kernel的运行时参数可以通过Device Tree传递。例如command line可以通过bootargs这个property这个属性传递;initrd的开始地址也可以通过linux,initrd-start这个property这个属性传递。在本例中,chosen节点包含了bootargsbootargs的属性。
chosen {
bootargs = "console=ttySAC2,115200n8 root=/dev/nfs nfsroot=192.168.0.101:/home/run/work/rootfs/rootfs_3.16.57
ip=192.168.0.20 init=/linuxrci earlyprintk mem=512M@30000000";
};
编译设备树
设备树文件的格式为dts,包含的头文件格式为dtsi,dts文件是一种人可以看懂的编码格式。但是uboot和linux不能直接识别,他们只能识别二进制文件,所以需要把dts文件编译成dtb文件。dtb文件是一种可以被kernel和uboot识别的二进制文件。把dts编译成dtb文件的工具是dtc。
Linux源码目录下scripts/dtc目录包含dtc工具的源码。在Linux的scripts/dtc目录下除了提供dtc工具外,也可以自己安装dtc工具,linux下执行:sudo apt-get install device-tree-compiler安装dtc工具。其中还提供了一个fdtdump的工具,可以反编译dtb文件。dts和dtb文件的转换如图所示。
dtc工具的使用方法是:dtc –I dts –O dtb –o xxx.dtb xxx.dts,即可生成dts文件对应的dtb文件了。
编译
make dtbs CROSS_COMPILE=arm-none-linux-gnueabi-
反编译
dtc -I dtb -O dts -o tmp.dts s5pv210-x210.dtb
在编译linux内核时。也可以直接make dtbs生成dtb文件。
DTS和DTSI
*.dts文件是一种ASCII文本对Device Tree的描述,放置在内核的/arch/arm/boot/dts目录。一般而言,一个*.dts文件对应一个ARM的machine。
*.dtsi文件作用:由于一个SOC可能有多个不同的电路板,而每个电路板拥有一个 *.dts。这些dts势必会存在许多共同部分,为了减少代码的冗余,设备树将这些共同部分提炼保存在*.dtsi文件中,供不同的dts共同使用。
*.dtsi的使用方法,类似于C语言的头文件,在dts文件中需要进行include *.dtsi文件。当然,dtsi本身也支持include 另一个dtsi文件。
在内核的arch/arm/boot/dts/Makefile中,若选中某种SOC,则与其对应相关的所有dtb文件都将编译出来。在linux下,make dtbs可单独编译dtb。以下截取了S5PV210平台的一部分。
888行是我自己添加的一个设备树文件
DTC编译*.dts生成的二进制文件(*.dtb),bootloader在引导内核时,会预先读取*.dtb到内存,进而由内核解析。
bootloader需要将设备树在内存中的地址传给内核。在ARM中通过bootm或bootz命令来进行传递。bootm [kernel_addr] [initrd_address] [dtb_address],其中kernel_addr为内核镜像的地址,initrd为initrd的地址,dtb_address为dtb所在的地址。若initrd_address为空,则用“-”来代替。
设备树中dts、dtsi文件的基本语法
DTS的基本语法范例,如图所示。
/dts-v1/;
#include <dt-bindings/input/input.h>
#include "s5pv210.dtsi"
/ {
model = "YIC System SMDKV210 based on S5PV210";
compatible = "yic,smdkv210", "samsung,s5pv210";
chosen {
bootargs = "console=ttySAC2,115200n8 root=/dev/nfs nfsroot=192.168.0.101:/home/run/work/rootfs/rootfs_3.16.57 ip=192.1 68.0.20 init=/linuxrci earlyprintk";
};
memory@30000000 {
device_type = "memory";
reg = <0x30000000 0x20000000>;
};
ethernet@88000000 {
compatible = "davicom,dm9000";
reg = <0x88000000 0x2 0x88000004 0x2>;
interrupt-parent = <&gph1>;
interrupts = <2 4>;
local-mac-address = [00 00 de ad be ef];
davicom,no-eeprom;
clocks = <&clocks CLK_SROMC>;
clock-names = "sromc";
};
key {
empty_property;
}
};
它包括一系列节点,以及描述节点的属性。
“/”为root节点。在一个.dts文件中,有且仅有一个root节点;在root节点下有“node1”,“node2”子节点,称root为“node1”和“node2”的parent节点,除了root节点外,每个节点有且仅有一个parent;其中子节点node1下还存在子节点“child-nodel1”和“child-node2”。
注:如果看过内核/arch/arm/boot/dts目录的读者看到这可能有一个疑问。在每个.dsti和.dts中都会存在一个“/”根节点,那么如果在一个设备树文件中include一个.dtsi文件,那么岂不是存在多个“/”根节点了么。
其实不然,编译器DTC在对.dts进行编译生成dtb时,会对node进行合并操作,最终生成的dtb只有一个root node。
在节点的{ }里面是描述该节点的属性(property),即设备的特性。它的值是多样化的:
1.它可以是字符串string,如model = "YIC System SMDKV210 based on S5PV210";
也可能是字符串数组string-list,如compatible = "yic,smdkv210", "samsung,s5pv210";
2.它也可以是32 bit unsigned integers,整形用<>表示, reg = <0x30000000 0x20000000>;
3.它也可以是binary data,十六进制用[]表示,local-mac-address = [00 00 de ad be ef];
4.它也可能是空,empty_property;
struct device_node {
const char *name; //节点的名字
const char *type; //device_type属性的值
phandle phandle; //对应该节点的phandle属性
const char *full_name; //节点的名字, node-name[@unit-address]从“/”开始的,表示该node的full path
struct fwnode_handle fwnode;
struct property *properties; // 节点的属性
struct property *deadprops; /* removed properties 如果需要删除某些属性,kernel并非真的删除,而是挂入到deadprops的列表 */
struct device_node *parent; // 节点的父亲
struct device_node *child; // 节点的孩子(子节点)
struct device_node *sibling; // 节点的兄弟(同级节点)
#if defined(CONFIG_OF_KOBJ) // 在sys文件系统表示
struct kobject kobj;
#endif
unsigned long _flags;
void *data;
#if defined(CONFIG_SPARC)
const char *path_component_name;
unsigned int unique_id;
struct of_irq_controller *irq_trans;
#endif
};
child node的格式和node是完全一样的,因此,一个dts文件中就是若干嵌套组成的node,property以及child note、child note property描述。
dts文件中,对于properties,有一些常用的、默认的、特殊的属性,定义如下:
model
设备制造商的描述,如果有2款板子配置基本一致, 它们的compatible是一样的那么就通过model来分辨这2款板子
compatible
定义一系列的字符串, 用来指定内核中哪个machine_desc可以支持本设备,即这个板子兼容哪些平台 。一般"供应商,产品"。
reg
描述设备资源在其父总线定义的地址空间中的地址。通常这意味着内存映射IO寄存器块的偏移量和长度,但在某些总线类型上可能有不同的含义。根节点定义的地址空间中的地址是CPU实际地址。
#address-cells
在它的子节点的reg属性中, 使用多少个u32整数来描述地址(address)
#size-cells
在它的子节点的reg属性中, 使用多少个u32整数来描述地址长度(size)
phandle
节点中的phandle属性, 它的取值必须是唯一的(不要跟其他的phandle值一样),使用phandle值来引用节点
bootargs
内核command line参数, 跟u-boot中设置的bootargs作用一样
cpus
/cpus节点下面有1个或多个cpu子节点,cpu子节点用reg属性来表明自己是那个cpu。
一、节点删除
应用条件:通常DTS中包含了多个平台的描述文件,且多个平台会共享一些通用的dtsi。这些dtsi的节点对于指定的平台来说,其节点未必全部需要,因此就需要将不需要的节点进行裁剪或者DISABLE。节点删除就是实现这个作用。
例如:
&soc {
/delete-node/ ssusb@a800000;
/delete-node/ qusb@88e3000;
/delete-node/ ssphy@88eb000;
/delete-node/ usb_audio_qmi_dev;
......
二、属性删除
类似于节点删除,当然还是按照DISABLE来理解,更贴近一些,例如系统已经默认启用了一个panel, OEM需要重新指定一个新的panel时,可以将默认panel的active属性DISABLE
/delete-property/ xxxx,dsi-display-active;