Linux驱动之设备树(华清创客学院)

Open Firmware Device Tree – 开发固件设备树

  1. 描述包括CPU的数量、类别、内存基地址和大小、总线和桥、外设连接、中断控制器、GPIO控制器、Clock等。
  2. ARM Linux中,一个.dts文件对应一个ARMmachine,放置在内核的arch/arm/boot/dts/
  3. Device Tree由一系列节点(node)和其属性(property)组成,而节点本身可以包含子节点,属性是由成对的(key-value)构成。
  4. DTS源文件由DTC编译为DTB文件,由Bootloader传递给内核,对其进行解析展开(Flattened),从而产生硬件的拓扑图,在编程的过程中,可以直接通过系统提供的接口,获取设备树中的节点和属性信息。

Device Tree解决的问题

在Linux 2.6中arch/arm/plat-xxxarch/arm/mach-xxx中充斥着大量垃圾代码。如,板上的platform设备,resource i2c_board_info spi_board_info 以及各种硬件platform_data。这些描述板级细节的代码对内核而言都是垃圾。

编译设备树

  1. DTC是编译设备树源文件.dts编译为.dtb的工具。

  2. DTC的源码位于scripts/dtc/,在Linux内核使能了Device Tree的情况下编译内核时,dtc会被编译出来。

  3. arch/arm/boot/dts/Makefile中,描述了当某种SOC被选中后,哪些.dtb文件会被编译出来。

    dtb-$(CONFIG_ARCH_EXYNOS)+=exynos4412-smdk4412.dtb exynos4412-tiny4412.dtb

  4. make dtbs

缩写名词解释

DT : Device Tree

FDT : Flattened Device Tree

OF : Open Firmware

DTS : Device Tree Source

DTSI : Device Tree Source Include

DTB : Device Tree Blob

DTC : Device Tree Compiler

设备树语法

  • 节点

  • 属性

  • 根节点

  • compatible属性

    指定了系统的名称,是一个字符串列表,实际在代码中可以用于进行匹配,至少包含一个<制造商>,<型号>形式的字符串,重要的是要指定一个确切的设备,并且包括制造商的名字,以避免命名空间冲突。例如:compatible= "acme, coyotes-revenge;"

  • reg属性

    reg的组织形式为 reg = <address1 length1 [address2 length2] ... >其中每一组address length表明了设备使用的一个地址范围。

  • #address-cells和#size-cells属性

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    #address-cells = <1>; 表示address字段的长度为1
    #size-cells = <1>; 表示length字段的长度为1
    
    external-bus{
        #address-cells = <2> 
    	#size-cells = <1>;
        ethernet@0,0{
            compatible = "smc, smc91c111";
            reg = <0 0 0x1000>; // address占两个cells, length占1个cells
        };
    }
    
  • 模板

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    /{
        node1{
            a-string-property = "A String";
            a-string-list-property = "first string", "second string";
            a-byte-data-property = [0x01 0x02 0x03 0x04];
            child-node1 {
                dirst-child-property;
                second-child-property=<1>;
                a-string-property="Hello, world";
            };
            child-node2{
            };
        };
        node2{
            an-empty-property;
            a-cell-property=<1 2 3 4>; /* each number (cell) is a uint32 */
            child-node1{
                mixed-property = "a string", [01 23 45 67], <0x12345678>;
            };
        };
    }
    

中断信息

1
2
3
4
interrupt-controller : 一个空的属性,定义该节点作为一个接受中断信号的设备
#interrupt-cells : 中断控制器的属性,声明该控制器下中断的cell个数
interrupt-parent : 设备节点属性,包含一个中断控制器的phandle,没有该属性的节点从父节点继承。
interrupts : 设备节点属性,包含一个中断指示符列表,对应每个中断信号。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/{
    compatible = "acme, coyotes-revenge";
    #address-cells = <1>;
    #size-cells = <1>;
    interrupt-parent=<&intc>; /* 声明此节点以及所有子节点的中断控制器节点 */
    
    serial@101f0000{
        compatible = "arm, pl011";
        reg = <0x101f0000 0x1000>;
        interrupts = <1 0>; 
        /* 对于arm架构,标志位具体含义Documentation/devicetree/bindings/arm/gic.txt */
    };
    intc interrupt-controller@10140000{
        compatible = "arm, pl190";
        reg = <0x10140000 0x1000>;
        interrupt-controller;   /* 声明此节点是一个interrupt-controller */
        #interrupt-cells = <2>;
    };
}

对于arm架构,标志位具体含义/Documentation/devicetree/bindings/interrupt-controller/arm,gic.txt,例如

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- interrupt-controller : Identifies the node as an interrupt controller
- #interrupt-cells : Specifies the number of cells needed to encode an
  interrupt source.  The type shall be a <u32> and the value shall be 3.

  The 1st cell is the interrupt type; 0 for SPI interrupts, 1 for PPI
  interrupts.

  The 2nd cell contains the interrupt number for the interrupt type.
  SPI interrupts are in the range [0-987].  PPI interrupts are in the
  range [0-15].

  The 3rd cell is the flags, encoded as follows:
        bits[3:0] trigger type and level flags.
                1 = low-to-high edge triggered
                2 = high-to-low edge triggered (invalid for SPIs)
                4 = active high level-sensitive
                8 = active low level-sensitive (invalid for SPIs).
        bits[15:8] PPI interrupt cpu mask.  Each bit corresponds to each of
        the 8 possible cpus attached to the GIC.  A bit set to '1' indicated
        the interrupt is wired to that CPU.  Only valid for PPI interrupts.
        Also note that the configurability of PPI interrupts is IMPLEMENTATION
        DEFINED and as such not guaranteed to be present (most SoC available
        in 2014 seem to ignore the setting of this flag and use the hardware
        default value).

常用OF API

OF提供的函数主要集中在drivers/of/目录下,有address.c base.c device.c fdt.c irq.c platform.c等等

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <of.h>
/*根据device_node结构的full_name参数,在全局链表of_allnodes中,查找合适的device_node*/
struct device_node *of_find_node_by_path(const char *path);

/*根据property结构的name参数,在指定的device_node中查找合适的property*/
struct property *of_find_property(const struct device_node *np, const char *name, int *lenp);

/*根据compat参数与device node的compatible匹配,返回匹配度*/
int of_device_is_compatible(const struct device_node *device, const char *compat)
    
/*获得父节点的device node*/
struct device_node *of_get_parent(const struct device_node *node);

/*根据属性名propname, 读出该属性的数组中sz个属性值给out_values*/
int of_property_read_u32_arry(const struct device_node *np, const char *propname, u8 *out_values, size_t sz);

/*读取该设备的第index个irq号*/
#include <linux/of_irq.h>
unsigned int irq_of_parse_and_map(struct device_node *dev, int index);

设备树实战

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 将以下内容插入到dts文件中,比如 arch/arm/boot/dts/exynos4412-fs4412.dts
test_node@12345678 {
	compatible = "test, farsight";
    reg = <0xA2345678 0x23
          0xB3456780 0x24>;
    testprop, mytest; // 空属性
    test_list_string = "red fish", "bule fish";
    interrupt-parent = <&gpx1>;
    interrupts = <1 2>;
};
// 编译dts
$ make dtbs
// 拷贝到开发板,并由uboot加载后
$ cd /proc/device-tree
$ ls test_node@12345678 //可以看到设备树中的每一个属性都是一个文件, 属性值是文件内容

通过代码获得节点的信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
static irqreturn_t key_irq_handler(int irqno, void * data)
{
    return IRQ_HANDLED;
}

# include <linux/of.h>
static int __init led_drv_init(void)
{
    /*获取节点的所有信息*/
    
    /*a.获取节点*/
    struct devoce_node *np = NULL;
    np = of_find_node_by_path("/test_node@12345678");
    if (np) {
        printk("find test node ok\n");
    } else {
        printk("find test node failed!\n");
    }
    /* 还可以查看node的其他信息
        struct device_node {
            const char *name;
            const char *type;
            phandle phandle;
            char	*full_name;

            struct	property *properties;
            struct	property *deadprops;	/* removed properties */
            struct	device_node *parent;
            struct	device_node *child;
            struct	device_node *sibling;
            struct	device_node *next;	/* next device of same type */
            struct	device_node *allnext;	/* next in list of all nodes */
            struct	proc_dir_entry *pde;	/* this node's proc directory */
            struct	kref kref;
            unsigned long _flags;
            void	*data;
        #if defined(CONFIG_SPARC)
            char	*path_component_name;
            unsigned int unique_id;
            struct of_irq_controller *irq_trans;
        #endif
    	};
    */
    /*b.获取节点的属性*/ 
    struct property *prop = NULL;
    prop = of_find_property(np, "compatible", NULL);
	if (prop) {
        printk("find compatible ok : %s\n", prop->value);
    } else {
        printk("find test node failed!\n");
    }
	/*c.读取属性中的整数数组*/
	#define U32_DATA_LEN 4
	u32 regdata[U32_DATA_LEN] = {0};
	int ret = 0;
	ret = of_property_read_u32_array(np, "reg", regdata, U32_DATA_LEN);
	if (!ret) {
        int i;
        for (i=0; i<U32_DATA_LEN; i++) {
            printk("regdata[%d] : %x\n", i, regdata[i]);
        }
    } else {
        printk("get data of reg failed!\n");
    }

	/*c.读取属性中的字符串数组*/
	const char *pstr[3];
    int i;
    for (i=0; i<3; i++) {
        ret = of_property_read_string_index(np, "test_list_string", i, pstr[i]);  
        if (!ret) {
            printk("regdata[%d] : %s\n", i, pstr[i]);
        } else {
            printk("get data of reg failed!\n");
        }
    }
	/*d.通过查找某个属性是否存在,设置一些全局标志*/
	if (of_find_property(np, "testprop,mytest", NULL)) {
        is_good = 1;
        printk("is good = %d\n", is_good);
    }

	/*e.获取中断的号码*/
	#include <linux/of_irq.h>
	int irqno = irq_of_parse_and_map(np, 0);
	request_irq(irqno, key_irq_handler, IRQF_TRIGGER_FALLING, "key_irq", NULL);
}
module_init(led_drv_init);

设备树匹配相关代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct of_device_id of_match_id[] = {
    {.compatible = "test, farsight"}     // 和设备树中节点的compatible保持一致
};

struct platform_driver led_pdrv = {
    .probe = led_pdrv_probe,
    .remove = led_pdrv_remove,
    .driver = {
        .name = "samsung led_drv",
        .of_match_table = of_match_id,// 设备树用此方法匹配设备和驱动,不再用.id_table
    },
    // .id_table = led_id_table, 
};

韦东山讲设备树

此部分是对前边华清创客学院所讲内容的一个补充,重点内容有

  1. 内核启动时对设备树的解析;
  2. dtb文件转换为device_node以及device_node转换为 platform_device的过程;
  3. 平台设备和平台驱动的匹配在其他文章中有总结过,这里主要需要注意platform_match函数中关于设备树匹配的方法;

目前只是简单听了一遍视频课,后续抽时间对照代码再仔细研究以上内容。

韦东山设备树的第四课是u-boot对设备树的支持,留到研究u-boot时再细究;第五课是中断系统中的设备树这一部分涉及到内核处理中断的方式,比较复杂,也留到以后再研究。

第二课 设备树的规范【dts和dtb】

DTS文件布局

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/ {
	[property devinitions]
	[child nodes]    
};
Property格式[label:] property_name = property_value;  //也可以没有值,为空属性

 value 有三种类型
<1 0x3 0x123>   32位的数组
hello : 字符串
[0x11 0x22 0x33] : byte string, 十六进制表示一个或者多个byte,一个byte用2位十六进制表示byte之间的空格可以省略
三种类型可以随意组合为属性的值,不同类型之间用逗号隔开
    
Devicetree node格式
[label:] node-name[@unit-address] {
    [properties definitions]
    [chile nodes]
}    

特殊的、默认的属性:

a. 根节点

​ #address-cells : 在它的子节点的reg属性中,使用多少个u32整数来描述地址(address)

​ #size-cells :在它的子节点的reg属性中,使用多少个u32整数来描述地址长度(length)

​ compatible : 定义一些列的字符串,用来指定内核中哪个machine_desc可以支持本设备,即这个板子兼容哪些平台

​ model : 如果两款板子配置基本一致,他们的compatible是一样的,那么就通过model来分辨这两块板子

b. /memory

​ device_type=“memory”;

​ reg : 用来指定内存的

c. /chosen

​ bootargs 内核command line参数,跟u-boot中设置的bootargs作用一样。

d. /cpu/cpu*

​ device_type=“cpu”

​ reg : 表明自己是哪一个cpu

引用其他节点

a. phandle: // 节点的phandle属性,它的取值必须是唯一的(不能和其他phandle值一样)

​ pic@10000000 {

​ phandle = <1>;

​ interrupt-controller; // 声明自己是一个中断控制器

​ };

​ another_device_node{

​ interrupt-parent = <1>; //使用phandler值为1来引用上述节点

​ };

b. label:

​ PIC: pic@10000000 {

​ phandle = <1>;

​ interrupt-controller; // 声明自己是一个中断控制器

​ };

​ another_device_node{

​ interrupt-parent = <&PIC>; //引用lable为PIC的节点作为中断控制器

​ };

如果dtsi中声明的某节点属性不满足需求,可以直接在dts文件中覆盖声明

如在 dtsi文件中声明了LED节点,但是管角和实际使用不同,可以直接引用LED的label并覆盖声明

1
2
3
4
5
6
7
8
9
&LED { // LED为dtsi文件中的一个节点label
    pin = <S3C2410_GPF(7)>;
}; //这种方法不能写在根节点下
第二种覆盖声明的方法是直接在/根节点下重新覆盖声明节点
/{
      led{
       	pin=  <S3C2410_GPF(7)>;
      };  
};

DTB格式

内核文档:/Documentation/devicetree/booting-without-of.txt

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/*           ------------------------------
---- base -> |  struct boot_param_header  |  // 指示各个段的偏移地址
             ------------------------------
             |      (alignment gap) (*)   |
             ------------------------------
             |      memory reserve map    |  //
             ------------------------------
             |      (alignment gap)       |
             ------------------------------
             |                            |
             |    device-tree structure   |
             |                            |
             ------------------------------
             |      (alignment gap)       |
             ------------------------------
             |                            |
             |     device-tree strings    |
             |                            |
      -----> ------------------------------
      |
      |
      --- (base + totalsize)
*/ 
struct boot_param_header { 
		u32     magic;                  /* magic word OF_DT_HEADER  0xd00dfeed */
        u32     totalsize;              /* total size of DT block */
        u32     off_dt_struct;          /* offset to structure */
        u32     off_dt_strings;         /* offset to strings */
        u32     off_mem_rsvmap;         /* offset to memory reserve map*/ 
    	u32     version;                /* format version */
        u32     last_comp_version;      /* last compatible version */
        /* version 2 fields below */
        u32     boot_cpuid_phys;        /* Which physical CPU id we're booting on */ 
    	/* version 3 fields below */
        u32     size_dt_strings;        /* size of the strings block */
        /* version 17 fields below */
        u32	size_dt_struct;		/* size of the DT structure block */ 
};
   Along with the constants:
/* Definitions used by the flattened device tree */ 
#define OF_DT_HEADER            0xd00dfeed      /* 4: version, 4: total size */ 
#define OF_DT_BEGIN_NODE        0x1             /* Start node: full name*/ 
#define OF_DT_END_NODE          0x2             /* End node */ 
#define OF_DT_PROP              0x3             /* Property: name off, size, content */ #define OF_DT_END               0x9             /* 整个structure block结束*/

All values in this header are in big endian format(大端方式), the various fields in this header are defined more precisely below. All “offset” values are in bytes from the start of the header; that is from the physical base address of the device tree block.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Here's the basic structure of a single node:
     * token OF_DT_BEGIN_NODE (that is 0x00000001) // + 节点名字,表示节点开始
     * for version 1 to 3, this is the node full path as a zero terminated string, starting with "/". For version 16 and later,
       this is the node unit name only (or an empty string for the
       root node)
     * [align gap to next 4 bytes boundary]
     * for each property:
        * token OF_DT_PROP (that is 0x00000003) //属性开始
        * 32-bit value of property value size in bytes (or 0 if no value) // value长度,字节
        * 32-bit value of offset in string block of property name // 属性名字在string block中的偏移
        * property value data if any // 属性的值
        * [align gap to next 4 bytes boundary] //4字节对齐
     * [child nodes if any]
     * token OF_DT_END_NODE (that is 0x00000002) 

​ Device tree “strings” block ​ In order to save space, property names, which are generally redundant, are stored separately in the “strings” block. This block is simply the whole bunch of zero terminated strings for all property names concatenated together. The device-tree property definitions in the structure block will contain offset values from the beginning of the strings block.

尝试自己分析dtb文件。

第三课 内核对设备树的处理

1. 从源头分析:内核head.S文件对dtb文件的简单处理

bootloader启动内核时,会设置r0, r1, r2三个寄存器

  • r0 设置为0
  • r1 设置machine_id , 使用设备树时该参数没有被使用, 传递给了__machine_arch_type
  • r3 设置ATAGS或者DTB的启始地址 , 传递给了__atags_pointer

a. __lookup_process_type : 使用汇编指令读取CPUID根据该ID找到对应的proc_info_list结构体,该结构体种含有这类CPU的初始化函数和信息;

b. __vet_atags : 判断是否存在可用的ATAGS或者DTB;

c. __create_page_tables : 创建页表,即创建虚拟地址和物理地址的映射关系;

d. __enable_mmu : 使能MMU

e. __mmap_switched :切换物理地址为虚拟地址

f. 把bootloader传入的r2寄存器中的参数,保存到变量__atags_pointer

g. 调用C函数start_kernel

2. 对设备树中平台信息的处理

dts文件中“根节点”会声明兼容什么样的machine_desc

1
2
3
4
/{
    model = "SMDK2440";
    compatible = "samsung, smdk2440", "samsung, smdk2410" //声明一系列兼容的单板,依兼容性高低排列。
}

内核中的每一个machine_desc,同样也用字符串表明自己支持哪些单板

1
2
3
4
5
6
7
8
9
static const char *const smdk2440_dt_compt[] __initconst = {
    "samsung, smdk2440",
    NULL
};
MACHINE_START(S3C2440, "smdk2440")
    ...
    .dt_compat = smdk2440_dt_compt, //这也是一个字符串数组
    ...
MACHINE_END

假设有多个machine_descdts符合,该选择哪一个? 看看代码执行过程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
start_kernel // init/main.c
    setup_arch(&command_line); // arch/arm/kernel/setup.c
		mdesc = setup_machine_fdt(__atags_pointer); // arch/arm/kernel/devtree.c
			early_init_dt_verify(phys_to_virt(dt_phys)) //此函数检查__atags_pointer是否有DTB头
                init_boot_params = params; //如果是DTB,则将DTB虚拟地址保存为全局变量
				mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach); //找到最匹配的mdesc, arch_get_next_mach是一个函数指针,此函数会依次取出mdesc并返回m->dt_compat
                    while((data==get_next_compat())) {
                        score = of_flat_dt_match();
                            if (score>0 && score<best_score) {
                                best_data = data;
                                best_score = score;
                            }
                    }
		machine_desc = mdesc;

3. 对设备树运行时配置信息的处理

在上一节中找到最匹配的mdescearly_init_dt_scan_nodes()函数中,处理以下内容。

a. /chosen中的bootargs属性值,存入全局变量boot_command_line :

b. 确定根节点的#address-cells#size-cells存入全局变量dt_root_addr_cellsdt_root_size_cells;

c. 解析/memory中的reg属性,提取出base,size最终调用memblock_add(base,size);

4. dtb文件转换为device_node

内存中如何保留DTB区域

1
2
3
4
5
6
7
8
start_kernel();
    setup_arch();
    	arm_memblock_init();
    		early_init_fdt_reserve_self();
                /* 把DTB所占区域保留下来,即调用 memblock_reserve() */
                early_init_dt_reserve_memory_arch(); 
			/* 根据dtb中的memreserve信息调用memblock_reserve() */
    		early_init_fdt_scan_reserved_mem(); 

设备树节点展开的样子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
struct device_node {
    const char *name; // 来自节点中的name属性,如果没有该属性,则设为NULL
    const char *type; // 来自节点中的device_type属性,如果没有该属性,则设为NULL
};

struct property{
    char *name;
    int length;
    value;
    next; 
};
// 画一下 结构之间的关系
1
unflatten_device_tree(void)  // arch/arm/kernel/setup.c

a. 在DTB文件中

​ 每一个节点都以TAG(FDT_BEGIN_NODE, 0x00000001)开始,节点内部可以嵌套其他节点;

​ 每一个属性都以TAG(FDT_PROP, 0x00000003)开始;

b. 每一个节点都转换为struct device_node结构体, 每一个属性都转换为struct property结构体;

函数调用过程

5. device_node转换为platform_device

dts->dtb->device_node->platform_device

两个问题:

a. 哪些device_node可以转换成platform_device

​ 必须包含compatible属性

b. 怎么转换?

platform_device结构体中的resource数组,表示设备所需的资源,它来自于device_nodereginterrupts属性。 platform_device.dev.of_node指向device_node可以通过它来获取其他属性。

​ 函数调用过程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
of_platform_default_populate_init
    此函数被编译到initcall3段,在内核启动时被调用

start_kernel
    reset_init();
		pid = kernel_thread(kernel_init, NULL, CLONEFS)
            
kernel_init
     kernel_init_freeable();
		do_basic_setup();
			do_initcalls();
				for (level=0; level<ARRAY_SIZE(initcall_levels)-1; level++)
                    do_initcall_level(level)
                    
                    
of_platform_default_populate_init生成platform_device的过程
	of_platform_default_populate_init
        of_platform_default_populate(NULL, NULL, NULL);
			of_platform_populate(NULL, of_dafault_bus_match_table, NULL, NULL)
                for_each_child_of_node(root, child) {
                	rc = of_platform_bus_create(child, matches, lookup, parent, true);// 调用过程在后边
                    	dev = of_device_alloc(np, bus_id, parent); //根据device_node节点的属性设置platform_device的resource
                   if (rc) {
                       of_node_put(child);
                       break;
                   }
            	}

6. 平台设备和平台驱动的匹配

匹配函数是platform_bus_type.matchplatform_match

a. 比较platform_devive.driver_overrideplatform_driver.drv->name

b. 比较platform_device.dev.of_nodecompatible属性和platform_driver.drv->of_match_table

c. 比较platform_device.nameplatform_driver.id_table

d. 比较platform_device.nameplatform_driver.drv->name

有一个匹配成功,则匹配成功。