原文链接:http://www.wowotech.net/linux_kenrel/dt-code-analysis.html
转自:蜗窝科技
前言
Device Tree总共有三篇,分别是:
1、为何要引入Device Tree,这个机制是用来解决什么问题的?(请参考引入Device Tree的原因)
2、Device Tree的基础概念(请参考DT基础概念)
3、ARM linux中和Device Tree相关的代码分析(这是本文的主题)
本文主要内容是:以Device Tree相关的数据流分析为索引,对ARM linux kernel的代码进行解析。主要的数据流包括:
1、初始化流程。也就是扫描dtb并将其转换成Device Tree Structure。
2、运行时参数传递以及platform的识别流程分析
3、如何将Device Tree Structure并入linux kernel的设备驱动模型。
如何通过Device Tree实现运行时参数传递以及platform的识别?
汇编部分的代码分析
linux/arch/arm/kernel/head.S文件定义了bootloader和kernel的参数传递要求:
目前的kernel支持旧的tag list的方式,同时也支持device tree的方式。r2
可能是device tree binary file的指针(bootloader在传递给内核之前要copy到memory中),也可以能是tag list的指针。在ARM的汇编部分的启动代码中(主要是head.S和head-common.S),machine type ID和指向DTB或者atags的指针被保存在变量__machine_arch_type
和__atags_pointer
中,这么做是为了后续c代码进行处理。
和device tree相关的setup_arch代码分析
具体的c代码都是在setup_arch
中处理,这个函数是一个总的入口点。具体代码如下(删除了部分无关代码):
对于如何确定HW platform这个问题,旧的方法是静态定义若干的machine描述符(struct machine_desc
),在启动过程中,通过machine type ID作为索引,在这些静态定义的machine描述符中扫描,找到那个ID匹配的描述符。在新的内核中,首先使用setup_machine_fdt
来setup machine描述符,如果返回NULL,才使用传统的方法setup_machine_tags
来setup machine描述符。传统的方法需要给出__machine_arch_type
(bootloader通过r1
寄存器传递给kernel)和tag list的地址(用来进行tag parse)。__machine_arch_type
用来寻找machine描述符;tag list用于运行时参数的传递。随着内核的不断发展,相信有一天linux kernel会完全抛弃tag list的机制。
匹配platform(machine描述符)
setup_machine_fdt
函数的功能就是根据Device Tree的信息,找到最适合的machine描述符。具体代码如下:
early_init_dt_scan
函数有两个功能,一个是为后续的DTB scan进行准备工作,另外一个是运行时参数传递。具体请参考下面一个section的描述。
of_flat_dt_match_machine
是在machine描述符的列表中scan,找到最合适的那个machine描述符。我们首先看如何组成machine描述符的列表。和传统的方法类似,也是静态定义的。DT_MACHINE_START
和MACHINE_END
用来定义一个machine描述符。编译的时候,compiler会把这些machine descriptor放到一个特殊的段中(.arch.info.init
),形成machine描述符的列表。machine描述符用下面的数据结构来标识(删除了不相关的member):
nr
成员就是过去使用的machine type ID。内核machine描述符的table有若干个entry,每个都有自己的ID。bootloader传递了machine type ID,指明使用哪一个machine描述符。目前匹配machine描述符使用compatible strings,也就是dt_compat
成员,这是一个string list,定义了这个machine所支持的列表。在扫描machine描述符列表的时候需要不断的获取下一个machine描述符的compatible字符串的信息,具体的代码如下:
__arch_info_begin
指向machine描述符列表第一个entry。通过mdesc++
不断的移动machine描述符指针(Note:mdesc
是static的)。match
返回了该machine描述符的compatible string list。具体匹配的算法倒是很简单,就是比较字符串而已,一个是root node的compatible字符串列表,一个是machine描述符的compatible字符串列表,得分最低的(最匹配的)就是我们最终选定的machine type。
运行时参数传递
运行时参数是在扫描DTB的chosen node
时候完成的,具体的动作就是获取chosen node
的bootargs
、initrd
等属性的value,并将其保存在全局变量(boot_command_line
、initrd_start
、initrd_end
)中。使用tag list的方法是类似的,通过分析tag list,获取相关信息,保存在同样的全局变量中。具体代码位于early_init_dt_scan
函数中:
设定meminfo(该全局变量确定了物理内存的布局)有若干种途径:
1、通过tag list(tag是ATAG_MEM)传递memory bank的信息。
2、通过command line(可以用tag list,也可以通过DTB)传递memory bank的信息。
3、通过DTB的memory node传递memory bank的信息。
目前当然是推荐使用Device Tree的方式来传递物理内存布局信息。
初始化流程
在系统初始化的过程中,我们需要将DTB转换成节点是device_node
的树状结构,以便后续方便操作。具体的代码位于setup_arch->unflatten_device_tree
中。
我们用struct device_node
来抽象设备树中的一个节点,具体解释如下:
unflatten_device_tree
函数的主要功能就是扫描DTB,将device node组织成:
1、global list。全局变量struct device_node *of_allnodes
就是指向设备树的global list
2、tree。
这些功能主要是在__unflatten_device_tree
函数中实现,具体代码如下(去掉一些无关紧要的代码):
具体的scan是在unflatten_dt_node
函数中,如果已经清楚地了解DTB的结构,其实代码很简单,这里就不再细述了。
如何并入linux kernel的设备驱动模型
在linux kernel引入统一设备模型之后,bus
、driver
和device
形成了设备模型中的铁三角。在驱动初始化的时候会将代表该driver的一个数据结构(一般是xxx_driver
)挂入bus上的driver链表。device挂入链表分成两种情况,一种是即插即用类型的bus,在插入一个设备后,总线可以检测到这个行为并动态分配一个device数据结构(一般是xxx_device
,例如usb_device),之后,将该数据结构挂入bus上的device链表。bus上挂满了driver和device,那么如何让device遇到“对”的那个driver呢?那么就要靠缘分了,也就是bus的match
函数。
上面是一段导论,我们还是回到Device Tree。导致Device Tree的引入ARM体系结构的代码其中一个最重要的原因的太多的静态定义的表格。例如:一般代码中会定义一个static struct platform_device *xxx_devices
的静态数组,在初始化的时候调用platform_add_devices
。这些静态定义的platform_device
往往又需要静态定义各种resource
,这导致静态表格进一步增大。如果ARM linux中不再定义这些表格,那么一定需要一个转换的过程,也就是说,系统应该会根据Device tree来动态的增加系统中的platform_device。当然,这个过程并非只是发生在platform bus上(具体可以参考“Platform Device”的设备),也可能发生在其他的非即插即用的bus上,例如AMBA总线、PCI总线。一言以蔽之,如果要并入linux kernel的设备驱动模型,那么就需要根据device_node
的树状结构(root是of_allnodes
)将一个个的device node挂入到相应的总线device链表中。只要做到这一点,总线机制就会安排device和driver的约会。
当然,也不是所有的device node都会挂入bus上的设备链表,比如cpus node,memory node,choose node等。
cpus node的处理
这部分的处理可以参考setup_arch->arm_dt_init_cpu_maps
中的代码,具体的代码如下:
要理解这部分的内容,需要理解ARM CUPs binding的概念,可以参考linux/Documentation/devicetree/bindings/arm目录下的CPU.txt文件的描述。
memory的处理
这部分的处理可以参考setup_arch->setup_machine_fdt->early_init_dt_scan->early_init_dt_scan_memory
中的代码。具体如下:
interrupt controller的处理
初始化是通过start_kernel->init_IRQ->machine_desc->init_irq()
实现的。我们用S3C2416为例来描述interrupt controller的处理过程。下面是machine描述符的定义。
在driver/irqchip/irq-s3c24xx.c文件中定义了两个interrupt controller,如下:
当然,系统中可以定义更多的irqchip,不过具体用哪一个是根据DTB中的interrupt controller node中的compatible属性确定的。在driver/irqchip/irqchip.c文件中定义了irqchip_init函数,如下:
__irqchip_begin
就是所有的irqchip的一个列表,of_irq_init
函数是遍历Device Tree,找到匹配的irqchip。具体的代码如下:
只有该node中有interrupt-controller
这个属性定义,那么linux kernel就会分配一个interrupt controller的描述符(struct intc_desc
)并挂入队列。通过interrupt-parent
属性,可以确定各个interrupt controller的层次关系。在scan了所有的Device Tree中的interrupt controller的定义之后,系统开始匹配过程。一旦匹配到了interrupt chip列表中的项次后,就会调用相应的初始化函数。如果CPU是S3C2416的话,匹配到的是irqchip的初始化函数是s3c2416_init_intc_of
。
OK,我们已经通过compatible
属性找到了适合的interrupt controller,那么如何解析reg属性呢?我们知道,对于s3c2416的interrupt controller而言,其#interrupt-cells
的属性值是4,定义为。每个域的解释如下:
(1)ctrl_num表示使用哪一种类型的interrupt controller,其值的解释如下:
(2)parent_irq。对于sub controller,parent_irq标识了其在main controller的bit position。
(3)ctrl_irq标识了在controller中的bit位置。
(4)type标识了该中断的trigger type,例如:上升沿触发还是电平触发。
为了更顺畅的描述后续的代码,我需要简单的介绍2416的中断控制器,其block diagram如下:
53个Samsung2416的中断源被分成两种类型,一种是需要sub寄存器进行控制的,例如DMA,系统中的8个DMA中断是通过两级识别的,先在SRCPND寄存器中得到是DMA中断的信息,具体是哪一个channel的DMA中断需要继续查询SUBSRC寄存器。那些不需要sub寄存器进行控制的,例如timer,5个timer的中断可以直接从SRCPND中得到。
中断MASK寄存器可以控制产生的中断是否要报告给CPU,当一个中断被mask的时候,虽然SRCPND寄存器中,硬件会set该bit,但是不会影响到INTPND寄存器,从而不会向CPU报告该中断。对于SUBMASK寄存器,如果该bit被set,也就是该sub中断被mask了,那么即便产生了对应的sub中断,也不会修改SRCPND寄存器的内容,只是修改SUBSRCPND中寄存器的内容。
不过随着硬件的演化,更多的HW block加入到SOC中,这使得中断源不够用了,因此中断寄存器又被分成两个group,一个是group 1(开始地址是0X4A000000
,也就是main controller了),另外一个是group2(开始地址是0X4A000040
,叫做second main controller)。group 1中的sub寄存器的起始地址是0X4A000018
(也就是sub controller)。
了解了上面的内容后,下面的定义就比较好理解了:
对于s3c2416而言,irqchip的初始化函数是s3c2416_init_intc_of
,s3c2416_ctrl
作为参数传递给了s3c_init_intc_of
,大部分的处理都是在s3c_init_intc_of
函数中完成的,由于这个函数和中断子系统非常相关,这里就不详述了,后续会有一份专门的文档描述之。
GPIO controller的处理
暂不描述,后续会有一份专门的文档描述GPIO sub system。
machine初始化
machine初始化的代码可以沿着start_kernel->rest_init->kernel_init->kernel_init_freeable->do_basic_setup->do_initcalls
路径寻找。在do_initcalls
函数中,kernel会依次执行各个initcall
函数,在这个过程中,会调用customize_machine
,具体如下:
在这个函数中,一般会调用machine描述符中的init_machine callback
函数来把各种Device Tree中定义各个设备节点加入到系统。如果machine描述符中没有定义init_machine
函数,那么直接调用of_platform_populate
把所有的platform device加入到kernel中。对于s3c2416,其machine描述符中的init_machine callback
函数就是s3c2416_dt_machine_init
,代码如下:
由此可见,最终生成platform device的代码来自of_platform_populate
函数。该函数的逻辑比较简单,遍历device node global list中所有的node,并调用of_platform_bus_create
处理,of_platform_bus_create
函数代码如下:
具体增加platform device的代码在of_platform_device_create_pdata
中,代码如下:
PS: 上述分析中的 of* 函数,其 of 前缀为Open Firmware的缩写?