简介

这一篇文章的主要目的是为了理清U-boot和设备树之间的关系。我以前查到的资料一直以为U-boot会传递参数确定内核使用哪一个设备树。有一次我去一家公司面试的时候,被指导说U-boot似乎是不能这么操作。因此想要求证一下。首先需要确定一件事,就是U-boot也在使用设备树。我之前以为设备树是只有Linux会使用的,但是今天面试的时候,经过大佬提醒才反应过来。U-boot也已经有那么庞大了,开始使用设备树是很正常的事。

  1. Uboot-2017.03
  2. Linux-版本
  3. VERSION = 4
  4. PATCHLEVEL = 0
  5. SUBLEVEL = 0

u-boot 启动内核

首先是u-boot初始化的操作,我们这里首先不做分析。直接分析void main_loop(void) 。因为这个函数是u-boot初始化好了以后,一直循环这个函数来等待命令。直到等待超时,来启动内核。我的目的是分析这个自动启动内核所使用的命令以及参数。

  1. main_loop
  2. s = bootdelay_process();
  3. s = getenv("bootcmd"); # 返回得到的bootcmd 命令
  4. # 前面讲到u-boot也是用了设备树,这个参数一般会设置到设备树里面
  5. # 查看设备树里面的
  6. # "bootm ${tee_addr} ${initrd_addr} ${fdt_addr}; "
  7. # 在这里我们就可以确定使用的是 bootm 确实是指定了 设备树地址
  8. autoboot_command(s);
  9. run_command_list(s, -1, 0);

上面确定了bootm将会传递三个参数,然后会根据启动内核,调用对应的os启动函数,那么怎么知道是调用的哪一个函数了?

boot_os_fn 函数

  1. /*
  2. * Continue booting an OS image; caller already has:
  3. * - copied image header to global variable `header'
  4. * - checked header magic number, checksums (both header & image),
  5. * - verified image architecture (PPC) and type (KERNEL or MULTI),
  6. * - loaded (first part of) image to header load address,
  7. * - disabled interrupts.
  8. *
  9. * @flag: Flags indicating what to do (BOOTM_STATE_...)
  10. * @argc: Number of arguments. Note that the arguments are shifted down
  11. * so that 0 is the first argument not processed by U-Boot, and
  12. * argc is adjusted accordingly. This avoids confusion as to how
  13. * many arguments are available for the OS.
  14. * @images: Pointers to os/initrd/fdt
  15. * @return 1 on error. On success the OS boots so this function does
  16. * not return.
  17. */
  18. typedef int boot_os_fn(int flag, int argc, char * const argv[],
  19. bootm_headers_t *images);

在u-boot的bootm_os.c 文件中定义以下几个函数。

  1. static boot_os_fn *boot_os[] = {
  2. [IH_OS_U_BOOT] = do_bootm_standalone,
  3. #ifdef CONFIG_BOOTM_LINUX
  4. [IH_OS_LINUX] = do_bootm_linux,
  5. #endif
  6. #ifdef CONFIG_BOOTM_NETBSD
  7. [IH_OS_NETBSD] = do_bootm_netbsd,
  8. #endif
  9. ...
  10. }

显然我们只需要分析do_bootm_linux 函数就可以了。此函数位于arch\arm\lib\bootm.c

  1. /* Main Entry point for arm bootm implementation
  2. *
  3. * Modeled after the powerpc implementation
  4. * DIFFERENCE: Instead of calling prep and go at the end
  5. * they are called if subcommand is equal 0.
  6. */
  7. int do_bootm_linux(int flag, int argc, char * const argv[],
  8. bootm_headers_t *images)
  9. boot_jump_linux(images, flag);
  10. /* Subcommand: GO */
  11. static void boot_jump_linux(bootm_headers_t *images, int flag)
  12. #ifdef CONFIG_ARM64
  13. void (*kernel_entry)(void *fdt_addr, void *res0, void *res1,
  14. void *res2);
  15. #else
  16. /*原来arm64arm32启动参数多了一个*/
  17. void (*kernel_entry)(int zero, int arch, uint params);
  18. /*最重要的在这里*/
  19. kernel_entry = (void (*)(int, int, uint))images->ep;
  20. /*r2有两种赋赋值方式*/
  21. if (IMAGE_ENABLE_OF_LIBFDT && images->ft_len)
  22. /*根据镜像里面指定的地址*/
  23. r2 = (unsigned long)images->ft_addr; # 设备树的地址
  24. else
  25. /***********************************/
  26. /*根据启动命令参数的地址*/
  27. r2 = gd->bd->bi_boot_params;
  28. # 参考AAPCS规则
  29. # r0 = 0
  30. # r1 = machid
  31. # r2 = r2
  32. # 现在我们就进入内核了,这一步和内联汇编操作没什么区别
  33. kernel_entry(0, machid, r2);

上面最重要的就是kernel_entry的处理。其中r2 = gd->bd->bi_boot_params; 就表示我们u-boot启动的时候可以命令指定设备树。

内核对设备树的处理

  • 首先确定内核入口 ```bash

    arm\kernel\head-nommu.S

    我们分析 head-nommu.S 这个汇编文件可以确定一件事,那就是r0,r1,r2三个寄存器都没有被使用到。

    /*
    • Kernel startup entry point.

    *
    • This is normally called from the decompressor code. The requirements
    • are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,
    • r1 = machine nr. *
    • See linux/arch/arm/tools/mach-types for the complete list of machine
    • numbers for r1. /

ifdef CONFIG_CPU_THUMBONLY

  1. .thumb

ENTRY(stext)

else

  1. .arm

ENTRY(stext) … /记住r9是在内核启动时修改的,并非是u-boot传递进来的/ ldr r9, =CONFIG_PROCESSOR_ID … 1: bl after_proc_init b mmap_switched

  1. u-boot启动内核时,首先判断是否开启mmu。否则将会调用__mmap_switched。此函数在head-common.S中。再调用__mmap_switched。需要注意此时R0R1R2寄存器都没有被修改,还是u-boot传入进来的value
  2. <a name="AjBQo"></a>
  3. ## head-common.S 对dtb的简单处理
  4. __mmap_switched入手分析。
  5. ```bash
  6. /*
  7. * The following fragment of code is executed with the MMU on in MMU mode,
  8. * and uses absolute addresses; this is not position independent.
  9. *
  10. * r0 = cp#15 control register
  11. * r1 = machine ID
  12. * r2 = atags/dtb pointer
  13. * r9 = processor ID
  14. */
  15. __INIT
  16. # 内核启动以后判断到没有开启MUU,调用这个函数
  17. __mmap_switched:
  18. adr r3, __mmap_switched_data
  19. # 将地址上的值加载到寄存器上
  20. # r3的value会递增的,先存r4
  21. ldmia r3!, {r4, r5, r6, r7}
  22. # 这里bss段之类的鬼东西
  23. # 还有代码的重定位
  24. cmp r4, r5 @ Copy data segment if needed
  25. 1: cmpne r5, r6
  26. ldrne fp, [r4], #4
  27. strne fp, [r5], #4
  28. bne 1b
  29. mov fp, #0 @ Clear BSS (and zero fp)
  30. 1: cmp r6, r7
  31. strcc fp, [r6],#4
  32. bcc 1b
  33. # 前面提到了 r3 地址每一次会增加
  34. # 在这里再一次读取数据,
  35. # r5 = &__machine_arch_type
  36. # r6 = &__atags_pointer
  37. ARM( ldmia r3, {r4, r5, r6, r7, sp})
  38. THUMB( ldmia r3, {r4, r5, r6, r7} )
  39. THUMB( ldr sp, [r3, #16] )
  40. # 执行到这里的时候
  41. str r9, [r4] @ Save processor ID
  42. str r1, [r5] @ Save machine type
  43. str r2, [r6] @ Save atags pointer
  44. cmp r7, #0
  45. strne r0, [r7] @ Save control register values
  46. # 执行到这里我们就可以确定
  47. # __machine_arch_type 保存了R1寄存器的数据,也就是机器ID
  48. # __atags_pointer 保存了R2寄存器的数据,早期版本(没有设备树以前)这里存放Atags,现在存放设备树地址
  49. b start_kernel
  50. ENDPROC(__mmap_switched)
  51. .align 2
  52. .type __mmap_switched_data, %object
  53. __mmap_switched_data:
  54. .long __data_loc @ r4
  55. .long _sdata @ r5
  56. .long __bss_start @ r6
  57. .long _end @ r7
  58. .long processor_id @ r4
  59. .long __machine_arch_type @ r5
  60. .long __atags_pointer @ r6
  61. #ifdef CONFIG_CPU_CP15
  62. .long cr_alignment @ r7

分析到这里,我认为结论已经相当清晰了,u-boot通过r2寄传递设备树地址,
上面的代码可以分析到,在调用start_kernel以前。机器ID和设存器备树地址已经存放在下面两个地址了。以后就可以通过这两个地址访问。

  1. # __machine_arch_type 保存了R1寄存器的数据,也就是机器ID
  2. # __atags_pointer 保存了R2寄存器的数据,早期版本(没有设备树以前)这里存放Atags,现在存放设备树地址

对设备树平台信息的处理

设备树信息,参考Documentation\devicetree\usage-model.txt文档

  1. Linux uses DT data for three major purposes:
  2. 1) platform identification,
  3. 2) runtime configuration, and
  4. 3) device population.

这一节的目的主要是为了讲述匹配machine_desc 结构体的过程。这个结构体描述了系统体系架构相关部分的内容。

  1. arch\arm\include\asm\mach
  2. struct machine_desc {
  3. unsigned int nr; /* architecture number */
  4. const char *name; /* architecture name */
  5. unsigned long atag_offset; /* tagged list (relative) */
  6. ...
  7. }

我们编译的一个UImage很有可能支持多种板子,我们可以通过compatible 来匹配对应的machine_desc
首先查看我们的设备树文件,确定compatible

  1. compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";

搜索上面的compatible然后我们就可以确定,匹配的数据了。

  1. arch\arm\mach-imx\mach-imx6ul.c
  2. DT_MACHINE_START(IMX6UL, "Freescale i.MX6 UltraLite (Device Tree)")
  3. .map_io = imx6ul_map_io,
  4. .init_irq = imx6ul_init_irq,
  5. .init_machine = imx6ul_init_machine,
  6. .init_late = imx6ul_init_late,
  7. .dt_compat = imx6ul_dt_compat,
  8. MACHINE_END
  9. static const char * const imx6ul_dt_compat[] __initconst = {
  10. "fsl,imx6ul",
  11. "fsl,imx6ull",
  12. NULL,

DT_MACHINE_START 定义了一个machine_desc结构体。其中的imx6ul_dt_compat 用来指定匹配的dts。那么内核是怎么找到这一个结构体的了?这一切一切的根源又要从start_kernel说起。

start_kernel 匹配machine_desc

  1. init\main.c
  2. start_kernel
  3. setup_arch(&command_line);
  4. # __atags_pointer 就是保存的设备树的地址
  5. mdesc = setup_machine_fdt(__atags_pointer);
  6. early_init_dt_verify(phys_to_virt(dt_phys)
  7. # 检测设备树有效性
  8. # params 就是虚拟地址
  9. fdt_check_header(params)
  10. /* Setup flat device-tree pointer */
  11. # initial_boot_params 也是一个全局的空指针函数
  12. initial_boot_params = params;
  13. # 比较重要的就是这一步
  14. # 在这里找到了匹配的match_machine
  15. mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);
  16. # machine_desc 是全局的指针函数,
  17. machine_desc = mdesc;

在这里将全局变量 initial_boot_params初始化了。此变量保存的是 设备树的虚拟地址。
同时还保存了machine_desc指针。这个指针保存了匹配到的machine_desc结构体。

of_flat_dt_match_machine

查找machine_desc结构体最重要的of_flat_dt_match_machine 函数。此函数将会找到最合适的machine_desc结构体。

  1. /**
  2. * of_flat_dt_match_machine - Iterate match tables to find matching machine.
  3. *
  4. * @default_match: A machine specific ptr to return in case of no match.
  5. * @get_next_compat: callback function to return next compatible match table.
  6. *
  7. * Iterate through machine match tables to find the best match for the machine
  8. * compatible string in the FDT.
  9. */
  10. const void * __init of_flat_dt_match_machine(const void *default_match,
  11. const void * (*get_next_compat)(const char * const**))
  12. dt_root = of_get_flat_dt_root();
  13. # 以 score 为依据,
  14. while ((data = get_next_compat(&compat))) {
  15. score = of_flat_dt_match(dt_root, compat);
  16. if (score > 0 && score < best_score) {
  17. /*保存最匹配的数据*/
  18. best_data = data;
  19. # 最后返回的score 越小越好。最小就是为1
  20. best_score = score;
  21. }
  22. }
  23. return best_data;

of_flat_dt_match

这个函数一直深入下去会匹配compatible 参数。然后作比较返回匹配值。

  1. of_flat_dt_match -> of_fdt_match -> of_fdt_is_compatible
  2. int of_fdt_is_compatible(const void *blob,
  3. unsigned long node, const char *compat)
  4. cp = fdt_getprop(blob, node, "compatible", &cplen);
  5. while (cplen > 0) {
  6. score++;
  7. if (of_compat_cmp(cp, compat, strlen(compat)) == 0)
  8. /*匹配 compatible 里面的每一个value score 1 肯定是最匹配的, 也就是第一个compatible里面的value*/
  9. return score;
  10. l = strlen(cp) + 1;
  11. cp += l;
  12. cplen -= l;
  13. }

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

上面我们已经知道内核匹配到了machine_desc结构体。然后将其保存在了machine_desc指针中。下面我们来查查看内核是怎么来操作这些信息的。

  1. init\main.c
  2. start_kernel
  3. setup_arch(&command_line);
  4. # __atags_pointer 就是保存的设备树的地址
  5. mdesc = setup_machine_fdt(__atags_pointer);
  6. mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);
  7. early_init_dt_scan_nodes();
  8. void __init early_init_dt_scan_nodes(void)
  9. /* Retrieve various information from the /chosen node */
  10. of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
  11. /* Initialize {size,address}-cells info */
  12. of_scan_flat_dt(early_init_dt_scan_root, NULL);
  13. /* Setup memory, calling early_init_dt_add_memory_arch */
  14. of_scan_flat_dt(early_init_dt_scan_memory, NULL);
  • of_scan_flat_dt 函数的处理方式

    1. of_scan_flat_dt
    2. const void *blob = initial_boot_params; // 设备树的虚拟地址
    3. /*遍历寻找所有节点,每找到一个节点,就调用一下回调函数,直到返回1*/
    4. for (offset = fdt_next_node(blob, -1, &depth);
    5. offset >= 0 && depth >= 0 && !rc;
    6. offset = fdt_next_node(blob, offset, &depth)) {
    7. pathp = fdt_get_name(blob, offset, NULL);
    8. if (*pathp == '/')
    9. pathp = kbasename(pathp);
    10. rc = it(offset, pathp, depth, data);
    11. }
  • chosen 节点的处理

/chosen 节点中的bootargs 属性中的value存入全局变量boot_command_line char数组中。

  • 确定根节点的下面的两个属性值

    • address-cells

    • size-cells

      1. early_init_dt_scan_root
      2. prop = of_get_flat_dt_prop(node, "#size-cells", NULL);
      3. if (prop)
      4. dt_root_size_cells = be32_to_cpup(prop);
      5. prop = of_get_flat_dt_prop(node, "#address-cells", NULL);
      6. if (prop)
      7. dt_root_addr_cells = be32_to_cpup(prop);
      保存到了dt_root_size_cells以及dt_root_addr_cells
  • 解析/memory节点中的reg属性

    1. early_init_dt_scan_memory
    2. const char *type = of_get_flat_dt_prop(node, "device_type", NULL);
    3. ...
    4. /*运行到这里的时候,已经能确定是处于memory节点下面*/
    5. /*获得reg参数,l就是参数长度*/
    6. reg = of_get_flat_dt_prop(node, "reg", &l);
    7. /*获取这节点数据的结束地址*/
    8. endp = reg + (l / sizeof(__be32));
    9. /*得到base 以及size*/
    10. base = dt_mem_next_cell(dt_root_addr_cells, &reg);
    11. size = dt_mem_next_cell(dt_root_size_cells, &reg);
    12. /*早期初始化memory*/
    13. early_init_dt_add_memory_arch(base, size);
    14. ...
    15. memblock_add(base, size);

    这个函数最终调用了memblock_add函数来添加内存块。

    dtb转化为device_node

    函数调用过程 ```c init\main.c start_kernel setup_arch(&command_line);

    1. /* Register the kernel text, kernel data and initrd with memblock. */
    2. arm_memblock_init(mdesc); //arch\arm\kernel\devtree.c
    3. /*保留设备树的这一块内存*/
    4. early_init_fdt_reserve_self();//drivers\of\fdt.c
    5. /*保留设备树自身的这一块区域*/
    6. # 使得系统运行中可以随时访问这一块区域
    7. early_init_dt_reserve_memory_arch(__pa(initial_boot_params),
    8. fdt_totalsize(initial_boot_params),0);
    9. # 根据dtb中的memreserver中的信息调用memblock_reserve
    10. early_init_fdt_scan_reserved_mem();

    create tree of device_nodes from flat blob

    在这里创建 device_nodes

    dtb 里面包含了各个设备节点,现在要将他设置为一棵树

    unflatten_device_tree();

    1. # early_init_dt_alloc_memory_arch 是一个回调函数
    2. __unflatten_device_tree(initial_boot_params, NULL, &of_root,
    3. early_init_dt_alloc_memory_arch, false);
    4. # 获取大小
    5. size = unflatten_dt_nodes(blob, NULL, dad, NULL);
    6. /* Allocate memory for the expanded device tree */
    7. # 分配空间,
    8. # 但是为什么这里还需要加上4了?是为了加上魔数占用的空间
    9. mem = dt_alloc(size + 4, __alignof__(struct device_node));
    10. # 填充对应的魔数
    11. #
    12. *(__be32 *)(mem + size) = cpu_to_be32(0xdeadbeef);
    13. /* Second pass, do actual unflattening */
    14. # 在这里就是真正的处理设备树节点了
    15. unflatten_dt_nodes(blob, mem, dad, mynodes);

遍历每一个节点,填充struct device_node 结构体

unflatten_dt_nodes

  1. # 遍历每一个节点,然后调用下面的函数,
  2. # 构造 struct device_node 以及 struct property 的属性
  3. populate_node(blob, offset, &mem,
  4. nps[depth],
  5. fpsizes[depth],
  6. &nps[depth+1], dryrun);

构造每一个device_node 节点,填充 struct device_node 结构体

populate_node

  1. # 获取节点 name属性
  2. pathp = fdt_get_name(blob, offset, &l);
  3. # 分配空间 sizeof(struct device_node) + allocl
  4. # allocl 大小是 fpsize + l
  5. np = unflatten_dt_alloc(mem, sizeof(struct device_node) + allocl,
  6. __alignof__(struct device_node));
  7. # 现在这里可以看到 struct device_node 是怎么填充的了
  8. np->full_name = fn = ((char *)np) + sizeof(*np);
  9. np->name = of_get_property(np, "name", NULL);
  10. np->type = of_get_property(np, "device_type", NULL);
  1. 这里主要讲述了struct device_node 的分配以及填充
  2. <a name="j08qn"></a>
  3. ### struct device_node
  4. 在设备树文件里面,每一个节点对应下面的一个`struct device_node ` 。每一个节点都可能包含子节点和父节点以及兄弟节点。当然,除了根节点没有父节点。在这里,树的概念就形成了。
  5. ```c
  6. struct device_node {
  7. const char *name; /*来源于节点的name属性,并非节点的名称*/
  8. const char *type; /*来源于节点的device_type属性*/
  9. phandle phandle;
  10. const char *full_name; /*这个才是节点的名字*/
  11. struct fwnode_handle fwnode;
  12. struct property *properties;
  13. struct property *deadprops; /* removed properties */
  14. struct device_node *parent;
  15. struct device_node *child;
  16. struct device_node *sibling;
  17. struct kobject kobj;
  18. unsigned long _flags;
  19. void *data;/*数据都放放在这个地方的,比如 name 以及 type的内容*/
  20. };

我们还可以看到两个struct property 结构体指针。

  1. struct property {
  2. char *name; /*属性名称*/
  3. int length; /*表示value的长度。(1个字节为单位)*/
  4. void *value;
  5. struct property *next; # 组成了链表
  6. unsigned long _flags;
  7. unsigned int unique_id;
  8. struct bin_attribute attr;
  9. };

device_node转化为platform_device

上面我们讲述了dtb怎么转化为device_node节点。现在我们需要来分析device_node节点转化为platform_device。
有两个问题,

  1. 1. 那些device_node可以转化成为platform_device
  2. 2. 转化这个过程是怎样的?
  • 首先分析第一个问题。 ```c
  1. 节点必须含有 compatible 属性(不包含根节点)
  2. 属于根节点的子节点(必须含有compatible 属性)

I2C以及SPI等子节点应该交给对应的总线驱动来处理,不应该被转化为platform_device。

  1. - 转化这个过程是怎样的?
  2. ```c
  3. platform_device 中包含有 resource 数组。里面包含了 IO 资源,内存资源。中断资源,
  4. platform_device.dev 中包含了 of_node 结构体,这里包含了设备树的属性

platform_device

  1. struct platform_device {
  2. const char *name;
  3. int id;
  4. bool id_auto;
  5. # struct device 结构体里面 包含了一个
  6. # struct device_node *of_node;
  7. # 这个结构体里面都是包含了读取设备树节点的属性值
  8. struct device dev;
  9. u32 num_resources; # resource 数组的长度
  10. # 资源包含了 IO 资源,内存资源。中断资源, 这些资源都是从device node里面得到
  11. struct resource *resource; # 指向了一个数组,这些资源来自设备树中的reg属性
  12. const struct platform_device_id *id_entry;
  13. char *driver_override; /* Driver name to force a match */
  14. /* MFD cell pointer */
  15. struct mfd_cell *mfd_cell;
  16. /* arch specific additions */
  17. struct pdev_archdata archdata;
  18. };

初始化步骤

  1. arch_initcall_sync(of_platform_default_populate_init);
  2. #define arch_initcall_sync(fn) __define_initcall(fn, 3s)
  3. #define __define_initcall(fn, id) \
  4. static initcall_t __initcall_##fn##id __used \
  5. __attribute__((__section__(".initcall" #id ".init"))) = fn;
  6. static initcall_t __initcall_of_platform_default_populate_init3s
  7. __used __attribute__((__section__(".initcall3s.init"))) =
  8. of_platform_default_populate_init;;

我们可以看到 of_platform_default_populate_init 函数被放到了.initcall3s.init段落中。此时我们查看链接脚本

  1. # arch/arm/kernel/vmlinux.lds
  2. __initcall3_start = .; KEEP(*(.initcall3.init)) KEEP(*(.initcall3s.init)) __initcall4_start = .;

也就是说所有的.initcall3s.init中的数据都是放在initcall3_start以后的内存里面。直到initcall4_start。
下面我们来跟踪这个的调用过程

  1. start_kernel
  2. /* Do the rest non-__init'ed, we're now alive */
  3. rest_init();
  4. # 新建了一个线程
  5. kernel_thread(kernel_init, NULL, CLONE_FS);
  6. kernel_init
  7. kernel_init_freeable
  8. do_basic_setup();
  9. do_initcalls();
  10. for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
  11. do_initcall_level(level);
  12. # 我们需要分析的是level 3
  13. do_initcall_level(int level)
  14. for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
  15. do_one_initcall(*fn);

上面我们确定了 of_platform_default_populate_init 函数将会被调用。那么现在就分析此函数

of_platform_default_populate_init

此函数的大致作用是遍历整个设备树节点分别为每一个可以转化为platform_device的device_node创建平台设备。

  1. of_platform_default_populate_init
  2. /* Populate everything else. */
  3. of_platform_default_populate(NULL, NULL, NULL);
  4. of_platform_populate(root, of_default_bus_match_table, lookup,
  5. parent);
  6. for_each_child_of_node(root, child)
  7. # 默认根节点下面的子节点都是总线节点,比如 i2C以及SPI
  8. of_platform_bus_create(child, matches, lookup, parent, true);
  9. of_platform_bus_create
  10. /* Make sure it has a compatible property */
  11. # 印证了我们的第一个问题,必须要有 compatible 属性
  12. of_get_property(bus, "compatible", NULL)
  13. # 创建平台设备数据
  14. dev = of_platform_device_create_pdata(bus, bus_id, platform_data, parent);
  15. for_each_child_of_node(bus, child)
  16. # 递归循环下去
  17. of_platform_bus_create(child, matches, lookup, &dev->dev, strict);


platform_device和platform_driver的匹配

上面我们讨论了platform_device的被创建的过程。那么现在我们需要讨论一下platform_device和platform_driver匹配过程。
内核探查到了设备,但是设备对应的驱动还需要匹配才行。因此我们需要来分析这个匹配过程。
大致可以分为两个主题,

  1. platform_driver 的注册过程
  2. platform_device 的注册过程
  3. 换句话说就是 驱动的注册过程,以及设备的注册过程。

首先明确一点,设备就是一个又一个struct platform_device结构体,设备驱动就是一个又一个
struct platform_driver结构体。

platform_driver 的注册过程

  1. platform_driver_register
  2. __platform_driver_register
  3. # 比较重要的就是 probe函数
  4. drv->driver.probe = platform_drv_probe;
  5. driver_register
  6. bus_add_driver(drv);
  7. # 很重要的一步,将platform_driver 放入 platform_bus_driver 链表中去
  8. klist_add_tail(&priv->knode_bus, &bus->p->klist_drivers);
  9. # 尝试绑定驱动到设备上面
  10. # try to bind driver to devices.
  11. driver_attach(drv);
  12. bus_for_each_dev # 包含一个回调函数 __driver_attach
  13. static int __driver_attach(struct device *dev, void *data)
  14. # 就是这个,相当重要的函数,驱动和设备的匹配
  15. ret = driver_match_device(drv, dev);

上面大致分析了 platform_driver_register函数的浅层调用过程。直到driver_match_device函数的调用。

三个平台设备相关的结构体

  1. 总线类型 bus_type platform_bus_type
  2. 设备 platform_driver
  3. 驱动 platform_device

首先我们查看设备树文件中的

  1. gpio_spi: gpio_spi@0 {
  2. compatible = "fairchild,74hc595";
  3. gpio-controller;
  4. #gpio-cells = <2>;
  5. reg = <0>;
  6. registers-number = <1>;
  7. registers-default = /bits/ 8 <0x57>;
  8. spi-max-frequency = <10000>;
  9. };

搜索对应的”fairchild,74hc595”

  1. # drivers\gpio\gpio-74x164.c
  2. static const struct of_device_id gen_74x164_dt_ids[] = {
  3. { .compatible = "fairchild,74hc595" },
  4. { .compatible = "nxp,74lvc594" },
  5. {},
  6. };
  7. MODULE_DEVICE_TABLE(of, gen_74x164_dt_ids);
  8. # 从这里我们可以观察到
  9. static struct spi_driver gen_74x164_driver = {
  10. .driver = {
  11. .name = "74x164",
  12. .of_match_table = gen_74x164_dt_ids,
  13. },
  14. .probe = gen_74x164_probe,
  15. .remove = gen_74x164_remove,
  16. };

以gen_74x164_probe为入口来分析数据,待续

内核中的设备树文件以及函数

内核中与设备树有关的文件

  1. include/linux/of_fdt.h # 设备树的一般处理文件
  2. include/linux/of_irq.h # 地址相关文件
  3. include/linux/of_net.h #
  4. include/linux/of_gpio.h # GPIO 相关函数
  5. include/linux/of_platform.h # 平台相关函数
  6. # 将device_node 转化为 platform_device的函数
  7. include/linux/of_dma.h # 设备树中dma相关函数
  8. include/linux/of_pdt.h
  9. include/linux/of_pci.h
  10. include/linux/of_device.h
  11. include/linux/of_address.h
  12. include/linux/of_graph.h
  13. include/linux/of_iommu.h
  14. include/linux/of_mdio.h
  15. include/linux/of_reserved_mem.h
  16. include/video/of_videomode.h
  17. include/video/of_display_timing.h

总结

  1. - bootloader 启动内核的时候将会设置三个寄存器
  2. - r0 一般设置为0
  3. - r1 一般设置为机器ID
  4. - r2 设置为设备树地址或者是早期的ATAGS(没有设备树以前使用,现在已经丢弃了)
  5. - 按照以下的
  6. dtb -> device_node -> platform_device (最后匹配platform_driver)

有关于 platform device 和 platform_driver 之间的匹配可以参考 驱动匹配过程

参考资料

第01节_传递dtb给内核
What does __init mean in the Linux kernel code?
Linux启动过程分析(head.s)