6. 内核对设备树的处理

一、设备树的描述

对于设备树,其描述的信息可以分成三部分;在内核中,对设备树的处理也会分成三部分:

Linux uses DT data for three major purposes:
1) platform identification,   (平台识别信息)
2) runtime configuration, and (运行时配置信息)
3) device population.         (设备信息)

二、内核head.S对dtb的简单处理

u-boot把一些参数,把设备树文件传给内核,那么内核如何处理设备树文件呢?从内核的第一个启动文件head.S着手,分析其对dtb的处理。(详细的head.s的分析可参见:嵌入式Linux完全开发手册 - 移植Linux内核 - Linux内核启动概述)
bootloader启动内核时,会设置r0,r1,r2三个寄存器,由这三个寄存器将参数传给内核:

  • r0一般设置为0;
  • r1一般设置为machine id (在使用设备树时该参数没有被使用);
  • r2一般设置ATAGS或DTB的开始地址;
    这里额外介绍下machine id的作用。假设一个内核镜像uImage可以支持多种单板:

head.S分析如下:

  • a. [ __lookup_processor_type ] 使用汇编指令读取CPU ID, 根据该ID找到对应的proc_info_list结构体(里面含有这类CPU的初始化函数、信息)
  • b. [ __vet_atags ] 判断是否存在可用的ATAGS或DTB;
  • c. [ __create_page_tables ] 创建页表, 即创建虚拟地址和物理地址的映射关系;
  • d. [ __enable_mmu ] 使能MMU, 以后就要使用虚拟地址了
  • e. [ __mmap_switched ] 上述函数里将会调用__mmap_switched
  • f. 把bootloader传入的r2参数, 保存到变量__atags_pointer中
  • g. 调用C函数start_kernel
head.S/head-common.S  : 
把bootloader传来的r1值, 赋给了C变量: __machine_arch_type
把bootloader传来的r2值, 赋给了C变量: __atags_pointer     // dtb首地址

由此可见,head.S对dtb的处理还是比较简单的,只是将u-boot传来的R2寄存器里的值,也就是DTB的首地址赋给了内核中的C变量:__atags_pointer 。

三、内核对设备树中平台信息的处理(选择machine_desc)

  1. platform identification, (平台识别信息)

上面的介绍中,已经知道在以前u-boot的ATAG传参中,会传入machine id 由此来匹配machine desc;当使用dtb传参时,u-boot并不传递machine id了,内核machine desc 的依赖于dtb 根节点下的compatible属性,内核根据该属性来找到合适的machine desc。

作为内核的使用者,我们已经列出清单,期望在内核中找到支持某个单板的machine desc 。
对于内核,每个machine desc需表明能支持哪些单板;

compatible属性对machine desc的查找
  • a. 设备树根节点的compatible属性列出了一系列的字符串,表示它兼容的单板名,从"最兼容"到次之。

  • b. 内核中有多个machine_desc, 其中有dt_compat成员, 它指向一个字符串数组, 里面表示该machine_desc支持哪些单板

  • c. 使用compatile属性的值, 跟每一个machine_desc.dt_compat 比较,成绩为"吻合的compatile属性值的位置",成绩越低越匹配, 对应的machine_desc即被选中

分析下代码的调用流程:
从上面的分析已经知道,内核在启动后,将dtb文件的首地址保存到变量__atags_pointer中,然后调用start_kernel,接下来从start_kernel开始分析:
函数调用过程:

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
                   // 判断是否有效的dtb, drivers/of/ftd.c
                    early_init_dt_verify(phys_to_virt(dt_phys) 
                                    initial_boot_params = params; //将dtb的地址保存在全局变量里
                    // 找到最匹配的machine_desc, drivers/of/ftd.c
                    mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);  
                                    while ((data = get_next_compat(&compat))) {
                                        score = of_flat_dt_match(dt_root, compat);
                                        if (score > 0 && score < best_score) {
                                            best_data = data;
                                            best_score = score;
                                        }
                                    }
                    
        machine_desc = mdesc;

四、内核对设备树中运行时配置信息的处理

  1. runtime configuration, (运行时配置信息)

运行时的三种配置信息总结如下:

  • a. /chosen节点中bootargs属性的值, 存入全局变量: boot_command_line
  • b. 确定根节点的这2个属性的值: #address-cells, #size-cells
    存入全局变量: dt_root_addr_cells, dt_root_size_cells
  • c. 解析/memory中的reg属性, 提取出"base, size", 最终调用memblock_add(base, size);

函数调用过程:

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_scan_nodes();      // drivers/of/ftd.c
                        /* Retrieve various information from the /chosen node */
                        of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);

                        /* Initialize {size,address}-cells info */
                        of_scan_flat_dt(early_init_dt_scan_root, NULL);

                        /* Setup memory, calling early_init_dt_add_memory_arch */
                        of_scan_flat_dt(early_init_dt_scan_memory, NULL);
5

五、内核对设备树中设备信息的处理

  1. device population. (设备信息)

内核启动后,会将dtb中的所有节点转换为device_node,了解了device_node结构体和properties结构体,就可知道大致情况。

➢每一个节点都转换为一个device_node结构体:

struct device_node {
    const char *name;  // 来自节点中的name属性, 如果没有该属性, 则设为"NULL"
    const char *type;  // 来自节点中的device_type属性, 如果没有该属性, 则设为"NULL"
    phandle phandle;
    const char *full_name;  // 节点的名字, node-name[@unit-address]
    struct fwnode_handle fwnode;

    struct  property *properties;  // 节点的属性
    struct  property *deadprops;    /* removed properties */
    struct  device_node *parent;   // 节点的父亲
    struct  device_node *child;    // 节点的孩子(子节点)
    struct  device_node *sibling;  // 节点的兄弟(同级节点)
#if defined(CONFIG_OF_KOBJ)
    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
};

➢device_node结构体中有properties, 用来表示该节点的属性,每一个属性对应一个property结构体:

struct property {
    char    *name;    // 属性名字, 指向dtb文件中的字符串
    int length;       // 属性值的长度
    void    *value;   // 属性值, 指向dtb文件中value所在位置, 数据仍以big endian存储
    struct property *next;
#if defined(CONFIG_OF_DYNAMIC) || defined(CONFIG_SPARC)
    unsigned long _flags;
#endif
#if defined(CONFIG_OF_PROMTREE)
    unsigned int unique_id;
#endif
#if defined(CONFIG_OF_KOBJ)
    struct bin_attribute attr;
#endif
};
  • a. 在DTB文件中,
    每一个节点都以TAG(FDT_BEGIN_NODE, 0x00000001)开始, 节点内部可以嵌套其他节点,
    每一个属性都以TAG(FDT_PROP, 0x00000003)开始
  • b. 这些device_node构成一棵树, 根节点为: of_root
    跟踪下代码:
函数调用过程:
start_kernel // init/main.c
    setup_arch(&command_line);  // arch/arm/kernel/setup.c
        arm_memblock_init(mdesc);   // arch/arm/kernel/setup.c
            early_init_fdt_reserve_self();
                    /* Reserve the dtb region */
                    // 把DTB所占区域保留下来, 即调用: memblock_reserve
                    early_init_dt_reserve_memory_arch(__pa(initial_boot_params),
                                      fdt_totalsize(initial_boot_params),
                                      0);           
            early_init_fdt_scan_reserved_mem();  // 根据dtb中的memreserve信息, 调用memblock_reserve
            
        unflatten_device_tree();    // arch/arm/kernel/setup.c
            __unflatten_device_tree(initial_boot_params, NULL, &of_root,
                        early_init_dt_alloc_memory_arch, false);            // drivers/of/fdt.c
                
                /* First pass, scan for size */
                size = unflatten_dt_nodes(blob, NULL, dad, NULL);
                
                /* Allocate memory for the expanded device tree */
                mem = dt_alloc(size + 4, __alignof__(struct device_node));
                
                /* Second pass, do actual unflattening */
                unflatten_dt_nodes(blob, mem, dad, mynodes);
                    populate_node
                        np = unflatten_dt_alloc(mem, sizeof(struct device_node) + allocl,
                                    __alignof__(struct device_node));
                        
                        np->full_name = fn = ((char *)np) + sizeof(*np);
                        
                        populate_properties
                                pp = unflatten_dt_alloc(mem, sizeof(struct property),
                                            __alignof__(struct property));
                            
                                pp->name   = (char *)pname;
                                pp->length = sz;
                                pp->value  = (__be32 *)val;

对于驱动开发者,设备树中还有一个备受关注的信息:platform_device

dts -> dtb -> device_node -> platform_device

在上述,我们已经知道dts中的节点信息终将被转换为 device_node ,但并不是所有device_node都会被转换成platform_device,需满足:

  • 根节点下含有compatile属性的子节点
  • 如果一个结点的compatile属性含有这些特殊的值("simple-bus","simple-mfd","isa","arm,amba-bus")之一, 那么它的子结点(需含compatile属性)也可以转换为platform_device
    i2c, spi等总线节点下的子节点, 应该交给对应的总线驱动程序来处理, 它们不应该被转换为platform_device
转换规则:
  • platform_device中含有resource数组, 它来自device_node的reg, interrupts属性;
  • platform_device.dev.of_node指向device_node, 可以通过它获得其他属性;

总结下:

  • a. 内核函数of_platform_default_populate_init, 遍历device_node树, 生成platform_device
  • b. 并非所有的device_node都会转换为platform_device
    只有以下的device_node会转换:
    • b.1 该节点必须含有compatible属性
    • b.2 根节点的子节点(节点必须含有compatible属性)
    • b.3 含有特殊compatible属性的节点的子节点(子节点必须含有compatible属性):
      这些特殊的compatilbe属性为: "simple-bus","simple-mfd","isa","arm,amba-bus"

示例:

比如以下的节点, 
/mytest会被转换为platform_device, 
因为它兼容"simple-bus", 它的子节点/mytest/mytest@0 也会被转换为platform_device

/i2c节点一般表示i2c控制器, 它会被转换为platform_device, 在内核中有对应的platform_driver;
/i2c/at24c02节点不会被转换为platform_device, 它被如何处理完全由父节点的platform_driver决定, 一般是被创建为一个i2c_client。

类似的也有/spi节点, 它一般也是用来表示SPI控制器, 它会被转换为platform_device, 在内核中有对应的platform_driver;
/spi/flash@0节点不会被转换为platform_device, 它被如何处理完全由父节点的platform_driver决定, 一般是被创建为一个spi_device。

/ {
      mytest {
          compatile = "mytest", "simple-bus";
          mytest@0 {
                compatile = "mytest_0";
          };
      };
      
      i2c {
          compatile = "samsung,i2c";
          at24c02 {
                compatile = "at24c02";                      
          };
      };

      spi {
          compatile = "samsung,spi";              
          flash@0 {
                compatible = "winbond,w25q32dw";
                spi-max-frequency = <25000000>;
                reg = <0>;
              };
      };
};

函数调用过程:

a. of_platform_default_populate_init (drivers/of/platform.c) 被调用到过程:
start_kernel     // init/main.c
    rest_init();
        pid = kernel_thread(kernel_init, NULL, CLONE_FS);
                    kernel_init
                        kernel_init_freeable();
                            do_basic_setup();
                                do_initcalls();
                                    for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
                                        do_initcall_level(level);  // 比如 do_initcall_level(3)
                                                                               for (fn = initcall_levels[3]; fn < initcall_levels[3+1]; fn++)
                                                                                    do_one_initcall(initcall_from_entry(fn));  // 就是调用"arch_initcall_sync(fn)"中定义的fn函数

b. of_platform_default_populate_init  (drivers/of/platform.c) 生成platform_device的过程:
of_platform_default_populate_init
    of_platform_default_populate(NULL, NULL, NULL);
        of_platform_populate(NULL, of_default_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;
                }
            }
            
c. of_platform_bus_create(bus, matches, ...)的调用过程(处理bus节点生成platform_devie, 并决定是否处理它的子节点):
        dev = of_platform_device_create_pdata(bus, bus_id, platform_data, parent);  // 生成bus节点的platform_device结构体
        if (!dev || !of_match_node(matches, bus))  // 如果bus节点的compatile属性不吻合matches成表, 就不处理它的子节点
            return 0;

        for_each_child_of_node(bus, child) {    // 取出每一个子节点
            pr_debug("   create child: %pOF\n", child);
            rc = of_platform_bus_create(child, matches, lookup, &dev->dev, strict);   // 处理它的子节点, of_platform_bus_create是一个递归调用
            if (rc) {
                of_node_put(child);
                break;
            }
        }
        
d. I2C总线节点的处理过程:
   /i2c节点一般表示i2c控制器, 它会被转换为platform_device, 在内核中有对应的platform_driver;
   platform_driver的probe函数中会调用i2c_add_numbered_adapter:
   
   i2c_add_numbered_adapter   // drivers/i2c/i2c-core-base.c
        __i2c_add_numbered_adapter
            i2c_register_adapter
                of_i2c_register_devices(adap);   // drivers/i2c/i2c-core-of.c
                    for_each_available_child_of_node(bus, node) {
                        client = of_i2c_register_device(adap, node);
                                        client = i2c_new_device(adap, &info);   // 设备树中的i2c子节点被转换为i2c_client
                    }
                    
e. SPI总线节点的处理过程:
   /spi节点一般表示spi控制器, 它会被转换为platform_device, 在内核中有对应的platform_driver;
   platform_driver的probe函数中会调用spi_register_master, 即spi_register_controller:
   
   spi_register_controller        // drivers/spi/spi.c
        of_register_spi_devices   // drivers/spi/spi.c
            for_each_available_child_of_node(ctlr->dev.of_node, nc) {
                spi = of_register_spi_device(ctlr, nc);  // 设备树中的spi子节点被转换为spi_device
                                spi = spi_alloc_device(ctlr);
                                rc = of_spi_parse_dt(ctlr, spi, nc);
                                rc = spi_add_device(spi);
            }

最后,便是platform_device跟platform_driver的匹配过程了,具体的过程分析可移步:
1. 驱动程序分层分离概念-总线设备驱动模型。
2. 字符设备驱动-总线设备驱动模型写法。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容