没有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文件。

Android 设备设备树 gpio 编写安卓设备树_子节点

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平台的一部分。

Android 设备设备树 gpio 编写安卓设备树_设备树_02

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;