Bootstrap

Android Kernel DTS设备树学习

资料参考来源于以下链接中的大佬分享
https://blog.csdn.net/wdjjwb/article/details/77297372
https://blog.csdn.net/weixin_38019025/article/details/104034539
https://blog.csdn.net/qq_34598667/article/details/83827876
https://www.cnblogs.com/tureno/articles/6403946.html
https://blog.csdn.net/qq_28992301/article/details/53321610
https://blog.csdn.net/John_chaos/article/details/106676490
https://blog.csdn.net/ruanjianruanjianruan/article/details/61622053

Kernel_DTS学习

       Linux3.x版本后,arch/arm/plat-xxx和arch/arm/mach-xxx中,描述板级细节的代码(比如platform_device、i2c_board_info等)被大量取消,取而代之的是设备树,其目录位于arch/arm/boot/dts。

       设备树由1个dts文件+n个dtsi文件,编译后的dtb二进制可执行文件是真正设备树文件。

.dts文件:
  是一种ASCII文本格式的Device Tree描述。在ARM Linux中,一个.dts文件对应一个ARM的machine,一般放置在内核arch/arm/boot/dts/目录。
.dtsi文件:
  soc厂商会把soc公共的特性和多块开发板公用的特性提炼为dtsi,而dts则负责描述某个具体的产品(开发板)的特性。dts直接或间接的包含多个dtsi(使用include包含)。
dtb文件:
  Device Tree Blob,dts经过dtc(Device Tree Compiler类似于gcc的命令)编译后得到dtb文件,dtb通过BootLoader引导程序加载到内核。所以Bootloader和kernel需要支持设备树。
  具体要生成所需要的的dts,在arch/arm/dts/Makefile中添加对应的目标dtb

DTS的加载过程:
  将硬件配置和系统运行参数等组织成Device Tree source file,通过dts编译后成dtb,在系统启动的过程:
  一是在引导kernel的同时,将dtb载入内存,前提是定义#define CONFIG_OF_LIBFDT支持设备树,tftp 0x8000 zlmage ,tftpboot 0x100 test-ek.dtb将kernel和dtb下载到flash中,fdt addr 0x100将dtb载入内存,最终通过bootz 0x8000 – 0x100启动系统。
  二是讲dts和kernel打包成plmage,这种方式无需将dtb的地址写到uboot中(但是uboot中要实现读plmage头部的功能),uboot从plmage的头部信息处去读取dtb的地址,然后传递给kernel。
  三是启动kernel中的”ARM_APPENDED_DTB”选项,该选项将dtb和kernel打包在一起,在kernel启动时会去紧挨着它的地方寻找dtb,这样就不需要uboot来传递dtb地址了。
DTS加载过程
DTS的结构:
  Device Tree由一系列被命名的结点(node)和属性(property)组成,而结点本身可包含子结点。所谓属性,其实就是成对出现的name和value。在Device Tree中,可描述的信息包括(原先这些信息大多被hard code到kernel中):
  CPU的数量和类别
  内存基地址和大小
  总线和桥
  外设连接
  中断控制器和中断使用情况
  GPIO控制器和GPIO使用情况
  Clock控制器和Clock使用情况
  最终生成的dtb中只有一个root node,除了root node,每个node都只有一个parent。每个node中包含property/value来描述特性。root node 的node name是确定的,必须是”/”

<硬编码和软编码的区别>
  硬编码:就是将数据直接写入到代码中进行编译开发,如果要发生更改的问题,就需要更改源代码,如果是C/S开发,就客户端的软件需要重新下载安装,非常不合理。
  软编码:则是将数据与源代码解耦,比如mybatis的配置文件,将sql于底层代码分离,就只对外暴露SQL语句供程序员编写。

实际例子分析:
  1个双核ARM Cortex-A9 32位处理器;
  ARM的local bus上的内存映射区域分布了2个串口(分别位于0x101F1000 和 0x101F2000)、GPIO控制器(位于0x101F3000)、SPI控制器(位于0x10170000)、中断控制器(位于0x10140000)和一个external bus桥;
  External bus桥上又连接了SMC SMC91111 Ethernet(位于0x10100000)、I2C控制器(位于0x10160000)、64MB NOR Flash(位于0x30000000);
  External bus桥上连接的I2C控制器所对应的I2C总线上又连接了Maxim DS1338实时钟(I2C地址为0x58)。
  其对应的.dts文件为:

/ {                                     
    compatible = "acme,coyotes-revenge";   
    #address-cells = <1>;             
    #size-cells = <1>;                 
    interrupt-parent = <&intc>;  
/* 
	/ 代表是root节点,内核通过 /的compatible判断启动的machine,每个node都有compatible属性,
通过字符串列表,在,之前的代表确切的设备,后面的代表可兼容的其他设备,
例如:compatible = "arm,vexpress-flash", "cfi-flash";
	#address-cells #size-cells 代表使用多少个u32来代表address 和 size
	interrput-parent 用于描述interrput request line 连接到哪个controller上,
&代表引用,可以通过&来覆写或增加节点属性,比如
    xusbxti: oscillator@1 {
        compatible = "fixed-clock";
        reg = <1>;
        clock-frequency = <0>;
        clock-output-names = "xusbxti";
        #clock-cells = <0>;
    };
    &xusbxti{
	    clock-frequency = <24000000>;
    }
*/
    cpus {  
        #address-cells = <1>;  
        #size-cells = <0>;  
        cpu@0 {  
            compatible = "arm,cortex-a9";  
            reg = <0>;  
        };  
        cpu@1 {  
            compatible = "arm,cortex-a9";  
            reg = <1>;  
        };  
    };  
/*
	根节点下的cpus子节点又包含2个cpu子节点,命名方式遵循:<name>[@<unit-address>] 
第一个<>是必须项,多个相同类型的设备节点name可以一样,使用不同unit-address。
设备中的unit-address地址也经常在其对应节点的reg属性中给出。
*/
    serial@101f0000 {  
        compatible = "arm,pl011";  
        reg = <0x101f0000 0x1000 >;  
        interrupts = < 1 0 >;  
    };  
  
    serial@101f2000 {  
        compatible = "arm,pl011";  
        reg = <0x101f2000 0x1000 >;  
        interrupts = < 2 0 >;  
    };  
/*
	reg的组织形式为 reg = <address1 length1 [address2 length2] [address3 length3]]>;
父节点的#address和#size-cells分别决定了子节点的reg属性的address和length字段的长度;
	interrupts = <2 0>cell的大小由intc:interrupt controller中的#interrupt-cells = <2>决定
第一个代表中断号,第二个代表触发类型
*/
    gpio@101f3000 {  
        compatible = "arm,pl061";  
        reg = <0x101f3000 0x1000  
             	0x101f4000 0x0010>;  
        interrupts = < 3 0 >;  
    };  
  
    intc: interrupt-controller@10140000 {  
        compatible = "arm,pl190";  
        reg = <0x10140000 0x1000 >;  
        interrupt-controller;  
        #interrupt-cells = <2>;  
};  
/*
	interrupt-controller属性代表这是一个接受中断的设备,节点前加上intc标签,
用于给根节点interrupt-parent属性分配一个phandle。这个interrupt-parrent将成为本系统的默认值,
所有子节点都将继承它,除非覆写这个属性。
	intc作为一个标签,指在节点名称前加上标号,这样设备树的其他位置就能够通过&符号来调用,
引用的模块在该模块之前无需加&,若在其后面或外面声明的,则需要加&
#interrput-cells第一个cell代表中断控制器的索引(中断号),第二个cell代表触发的type,
常见的有1 = low-to-high edge triggered 2 = high-to-low edge triggered (边缘触发) 
4 = active high level-sensitive 8 = active low level-sensitive(高低电平触发) 
(如果有第三个cell,则代表优先级,0级为最高,7级最低)
*/
    spi@10115000 {  
        compatible = "arm,pl022";  
        reg = <0x10115000 0x1000 >;  
        interrupts = < 4 0 >;  
    };  
  
    external-bus {  
        #address-cells = <2>  
        #size-cells = <1>;  
        ranges = <0 0  0x10100000   0x10000     // Chipselect 1, Ethernet  
                	1 0  0x10160000   0x10000     // Chipselect 2, i2c controller  
                	2 0  0x30000000   0x1000000>; 	// Chipselect 3, NOR Flash  
/*
	externel-bus节点的#address-cells = <2>和 #size-cells = <1>代表其子节点ethernet、i2c、
flash的reg字段形式为<0 0 0x1000>;注意第一个元素是对应片选,第二个元素是相对该片选的基地址
(相对偏移量),第三个元素为length。
	root子结点的address区域直接位于CPU的memory区域。但是,经过总线桥后的address往往需要
经过转换才能对应的CPU的memory映射。external-bus的ranges属性定义了经过external-bus桥后的
地址范围如何映射到CPU的memory区域。
	ranges属性则为一个地址转换表。表中的每一行都包含了子地址、父地址、在自地址空间内的区域大小,
分别由子节点的#address-cells、父节点的#address-cells和子节点的size-cells决定,
比如0 0  0x10100000  0x10000; 0 0是由于子节点的#address-cells = <2>,
 0x10100000是由于父节点的#address-cells = <1>, 0x10000是由于子节点的#size-cells = <1>;
 最终解释为片选0,偏移0,被映射到了0x10100000~0x1011000中,地址长度则为0x10000。
*/
        ethernet@0,0 {  
            compatible = "smc,smc91c111";  
            reg = <0 0 0x1000>;  
            interrupts = < 5 2 >;  
        };  
         i2c@1,0 {  
            compatible = "acme,a1234-i2c-bus";  
            #address-cells = <1>;  
            #size-cells = <0>;  
            reg = <1 0 0x1000>;  
            interrupts = < 6 2 >;  
            rtc@58 {  
                compatible = "maxim,ds1338";  
                reg = <58>;  
                interrupts = < 7 3 >;  
            };  
        };  
          flash@2,0 {  
            compatible = "samsung,k8f1315ebm", "cfi-flash";  
            reg = <2 0 0x4000000>;  
        };  
    };  
};  

操作节点:
  在Android中一般使用API来操作DTS中的硬件资源;对于经典的SOC,可以在比如arch/arm/mach-s5pv210/include/mach/gpio.h中的宏来获得gpio号,而对于比较新的soc,通常是在设备树中以reset-gpios = <&gpio1 15 1>;来表示gpio,可以通过of_get_named_gpio(xxxnode,”reset-gpios”,0);返回所需要的gpio号
  Linux内核中目前DTS相关的函数都是以of_前缀开头,实现位于内核源码的drivers/of下面.
  区别于无Device Tree驱动的例子:
  首先在dts中加入GPIO资源:

gpio_demo:gpio_demo{
    states = “okay”;       
    compatible = “rk3328,gpio_demo”;
    firefly-gpio = <&gpio0 12 GPIO_ACTIVE_HIGH>;           //GPIO0_B4
	firefly-irq-qpio = <&gpio2 29 IRQ_TYPE_EDGE_RISING>     //GPIO2_D5
}
/*
	GPIO_ACTIVE_HIGH:引脚状态为常高
	IRQ_TYPE_EDGE_RISING:上升沿触发
	RK系列GPIO分为GPIO0,GPIO1,GPIO2…等组,每组GPIO下分为A,B,C,D等小组,而每个A下有0-7个引脚,
B,C,D同A。于是A=0,B与A相差8个引脚,C与B相差8个引脚,以此类推GPIO0_B4 属于B组,基数为8,
加4即为引脚序列号12。
*/

        在驱动中,首选你需要一个device_id结构体用于匹配设备数资源中的属性

static const struct of_device_id gpio_demo_dt_ids[] = {
	{ .compatible = “rk3328 , gpio_demo”, },
    {},
};

        在驱动结构体中加上.of_match_table属性,of_mathc_ptr去匹配设备树的函数

static struct platform_driver int_demo_driver = {
	driver = {
	    .name = “gpio_demo”,
	    .of_match_table = of_match_ptr(gpio_demo_dt_ids),
    }
        .probe = firefly_gpio_probe,
        .remove = firefly_gpio_remove,
};
enum of_gpio_flags{
	OF_GPIO_ACTIVE_LOW = 0x1,
};
//probe函数在安装驱动的时候会被调用
static int firefly_gpio_probe(struct platform_device *pdev){
	int ret;
	int gpio;
	enum of_gpio_flags flag;
	struct firefly_gpio_info *gpio_info;
	struct device_node *firefly_gpio_node = pdev->dev.of_node//设备树节点
	printk(“Firefly GPIO Test Program Probe\n”);

	//1申请空间
	gpio_info = devm_kzalloc(&pdev->dev,sizeof(structfirefly_gpio_info *),GFP_KERNEL);
	if(!gpio_info){
	return –ENOMEM;
}
	//2获取gpio信息
    gpio = of_get_name_flags(firefly_gpio_node, “firefly-gpio”, 0, &flag);
    if(!gpio_is_valid(gpio)){
	    printk(“firefly-gpio: %d is invalid\n”, gpio);
	    return –ENODEV;
    }
	//3请求控制获取的gpio
    if(gpio_request(gpio, “firefly-gpio”)){
	    printk(“gpio %d request failed!\n”, gpio);
	    gpio_free(gpio);
	    return –NODEV;
    }
	//设置操作的gpio
    gpio_info->firefly_gpio = gpio;
	//根据flag判断使能值
    gpio_info->gpio_enable_value = (flag == OF_GPIO_ACTIVE_LOW) ? 0 : 1;
	//设置输出
    gpio_direction_output(gpio_info->firefly_gpio, gpio_info->gpio_enable_value);
    printk(“Firefly gpio putout\n”);
}

DTS下的平台驱动device与driver的匹配

static int platform_match(struct device *dev, struct device_driver *drv){
    struct platform_device *pdev = to_platform_device(dev);
    struct platform_driver *pdrv = to_platform_driver(drv);
    /* Attempt an OF style match first */
    if (of_driver_match_device(dev, drv))
        return 1;
    /* Then try ACPI style match */
    if (acpi_driver_match_device(dev, drv))
        return 1;
    /* Then try to match against the id table */
    if (pdrv->id_table)
        return platform_match_id(pdrv->id_table, pdev) != NULL;
    /* fall-back to driver name match */
    return (strcmp(pdev->name, drv->name) == 0);
}

        of_driver_match_device这个函数最终调用到__of_match_node()函数,在这个函数里,通过把device_driver的of_match_table(of_device_id结构体的数组)和device里的of_node(device_node结构体)进行匹配,匹配方式是分别比较两者的name、type、和compatible字符串,三者要同时相同(一般name、和type为空,只比较compatible字符串)。
        在head.S完成部分初始化后,开始调用start_kernel,其中setup_arch中调用的setup_machine_fdt将设备树dtb文件中的各个节点转换成device_node,就是驱动probe中调用struct device_node *firefly_gpio_node = pdev->dev.of_node。

;