linux設備驅動程序-設備樹(1)-dtb轉換成device_node

linux設備驅動程序-設備樹(1)-dtb轉換成device_node

本設備樹解析基於arm平臺html

從start_kernel開始

linux最底層的初始化部分在HEAD.s中,這是彙編代碼,咱們暫且不做過多討論,在head.s完成部分初始化以後,就開始調用C語言函數,而被調用的第一個C語言函數就是start_kernel,start kernel原型是這樣的: asmlinkage __visible void __init start_kernel(void) { ... setup_arch(&command_line); ... } 而對於設備樹的處理,基本上就在setup_arch()這個函數中。node

在這篇文章中,咱們分析的方法就是持續地跟蹤linux源代碼,可是鑑於linux源代碼的複雜性,只將程序中相關性較強的部分貼出來進行分析,由於若是去深究細節部分,那隻會自討苦吃。linux

博主爲整個函數調用流程畫了一張思惟導圖,結合思惟導圖閱讀更加清晰,點此下載,博主也將其貼在了文章最後,須要下載查看,網頁上查看可能不清晰。git

setup_arch

能夠看到,在start_kernel()中調用了setup_arch(&command_line);github

void __init setup_arch(char **cmdline_p)
{
    const struct machine_desc *mdesc;
    mdesc = setup_machine_fdt(__atags_pointer);
    ...
    arm_memblock_init(mdesc);
    ...
    unflatten_device_tree();
    ...
}

這三個被調用的函數就是主要的設備樹處理函數,setup_machine_fdt()函數根據傳入的設備樹dtb的首地址完成一些初始化操做。數組

arm_memblock_init()函數主要是內存相關,爲設備樹保留相應的內存空間,保證設備樹dtb自己存在於內存中而不被覆蓋。用戶能夠在設備樹中設置保留內存,這一部分同時做了保留指定內存的工做。函數

unflatten_device_tree()從命名能夠看出,這個函數就是對設備樹具體的解析,事實上在這個函數中所作的工做就是將設備樹各節點轉換成相應的struct device_node結構體。spa

下面咱們再來經過代碼跟蹤仔細分析,先從setup_machine_fdt()開始。指針

setup_machine_fdt(__atags_pointer)

__atags_pointer這個全局變量存儲的就是r2的寄存器值,是設備樹在內存中的起始地址,將設備樹起始地址傳遞給setup_machine_fdt,對設備樹進行解析。接着跟蹤setup_machine_fdt()函數:code

const struct machine_desc * __init setup_machine_fdt(unsigned int dt_phys)
{
    const struct machine_desc *mdesc, *mdesc_best = NULL;                    
    if (!dt_phys || !early_init_dt_verify(phys_to_virt(dt_phys)))           ——————part 1
	    return NULL;

    mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);       ——————part 2

    early_init_dt_scan_nodes();                                             ——————part 3
    ...
}

第一部分先將設備樹在內存中的物理地址轉換爲虛擬地址,而後再檢查該地址上是否有設備樹的魔數(magic),魔數就是一串用於識別的字節碼,若是沒有或者魔數不匹配,代表該地址沒有設備樹文件,函數返回,不然驗證成功,將設備樹地址賦值給全局變量initial_boot_params。

第二部分of_flat_dt_match_machine(mdesc_best, arch_get_next_mach),逐一讀取設備樹根目錄下的compatible屬性。

將compatible中的屬性一一與內核中支持的硬件單板相對比,匹配成功後返回相應的machine_desc結構體指針。

machine_desc結構體中描述了單板相關的一些硬件信息,這裏不過多描述。

主要的的行爲就是根據這個compatible屬性選取相應的硬件單板描述信息,通常compatible屬性名就是"廠商,芯片型號"。

第三部分就是掃描設備樹中的各節點,主要分析這部分代碼。

void __init early_init_dt_scan_nodes(void)
{
    of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
    of_scan_flat_dt(early_init_dt_scan_root, NULL);
    of_scan_flat_dt(early_init_dt_scan_memory, NULL);
}

出人意料的是,這個函數中只有一個函數的三個調用,直覺告訴我這三個函數調用並不簡單。

首先of_scan_flat_dt()這個函數接收兩個參數,一個是函數指針,一個爲boot_command_line,boot_command_line是一個靜態數組,存放着啓動參數,而of_scan_flat_dt()函數的做用就是掃描設備樹中的節點,而後對各節點分別調用傳入的回調函數。

在上述代碼中,傳入的參數分別爲early_init_dt_scan_chosen,early_init_dt_scan_root,early_init_dt_scan_memory這三個函數,從名稱能夠猜想,這三個函數分別是處理chosen節點、root節點中除子節點外的屬性信息、memory節點。

int __init early_init_dt_scan_chosen(unsigned long node, const char *uname,int depth, void *data){
    ...
    p = of_get_flat_dt_prop(node, "bootargs", &l);
    if (p != NULL && l > 0)
	    strlcpy(data, p, min((int)l, COMMAND_LINE_SIZE));
    ...
}

通過代碼分析,第一個被傳入的函數參數做用是獲取bootargs,而後將bootargs放入boot_command_line中,做爲啓動參數,而並不是處理整個chosen節點。
接下來再看第二個函數調用:

int __init early_init_dt_scan_root(unsigned long node, const char *uname,int depth, void *data)
{
    dt_root_size_cells = OF_ROOT_NODE_SIZE_CELLS_DEFAULT;
    dt_root_addr_cells = OF_ROOT_NODE_ADDR_CELLS_DEFAULT;

    prop = of_get_flat_dt_prop(node, "#size-cells", NULL);
    if (prop)
        dt_root_size_cells = be32_to_cpup(prop);
    prop = of_get_flat_dt_prop(node, "#address-cells", NULL);
    if (prop)
        dt_root_addr_cells = be32_to_cpup(prop);
    ...
}

經過進一步代碼分析,第二個函數執行是爲了將root節點中的#size-cells和#address-cells屬性提取出來,並不是獲取root節點中全部的屬性,放到全局變量dt_root_size_cells和dt_root_addr_cells中。

size-cells和address-cells表示對一個屬性(一般是reg屬性)的地址須要多少個四字節描述,而地址的長度須要多少個四字節描述,數據長度基本單位爲4。

#size-cells = 1
#address-cells = 1
reg = <0x12345678 0x100 0x22 0x4>

在上述示例中,size-cells爲1表示數據大小爲一個4字節描述,address-cells爲1表示地址由一個四字節描述。

而reg屬性由四個四字節組成,因此存在兩組地址描述,第一組是起始地址爲0x12345678,長度爲0x100,第二組起始地址爲0x22,長度爲0x4,由於在<>中,全部數據都是默認爲32位。

接下來看第三個函數調用:

int __init early_init_dt_scan_memory(unsigned long node, const char *uname,int depth, void *data){
    ...
    if (!IS_ENABLED(CONFIG_PPC32) || depth != 1 || strcmp(uname, "memory@0") != 0)
		return 0;
    reg = of_get_flat_dt_prop(node, "reg", &l);
    endp = reg + (l / sizeof(__be32));

    while ((endp - reg) >= (dt_root_addr_cells + dt_root_size_cells)) {
        base = dt_mem_next_cell(dt_root_addr_cells, &reg);
	    size = dt_mem_next_cell(dt_root_size_cells, &reg);
        early_init_dt_add_memory_arch(base, size);
    }
}

函數先判斷節點的unit name是memory@0,若是不是,則返回。而後將全部memory相關的reg屬性取出來,根據address-cell和size-cell的值進行解析,而後調用early_init_dt_add_memory_arch()來申請相應的內存空間。

到這裏,setup_machine_fdt()函數對於設備樹的第一次掃描解析就完成了,主要是獲取了一些設備樹提供的總覽信息。

*** 接下來繼續回到setup_arch()函數中,繼續向下跟蹤代碼。 ***

arm_memblock_init

void __init arm_memblock_init(const struct machine_desc *mdesc)
{
    ...
    early_init_fdt_reserve_self();
    early_init_fdt_scan_reserved_mem();
    ...
}

arm_memblock_init()對於設備樹的初始化而言,主要作了兩件事:

  • 調用early_init_fdt_reserve_self,根據設備樹的大小爲設備樹分配空間,設備樹的totalsize在dtb頭部中有指明,所以當系統啓動以後,設備樹就一直存在在系統中。
  • 掃描設備樹節點中的"reserved-memory"節點,爲其分配保留空間。

memblock_init對於設備樹的部分解析就完成了,主要是爲設備樹指定保留內存空間。

接下來繼續回到setup_arch()函數中,繼續向下跟蹤代碼。

unflatten_device_tree

這一部分就進入了設備樹的解析部分:

void __init unflatten_device_tree(void)
{
    __unflatten_device_tree(initial_boot_params, NULL, &of_root,early_init_dt_alloc_memory_arch, false);  —————— part1

    of_alias_scan(early_init_dt_alloc_memory_arch);                  —————— part2
    ...
}

of_alias_scan

爲了講解的方便,咱們先來看part2,從名字來看,這個函數的做用是解析根目錄下的alias,跟蹤代碼:

void of_alias_scan(void * (*dt_alloc)(u64 size, u64 align)){
    of_aliases = of_find_node_by_path("/aliases");
    of_chosen = of_find_node_by_path("/chosen");
    if (of_chosen) {
        if (of_property_read_string(of_chosen, "stdout-path", &name))
            of_property_read_string(of_chosen, "linux,stdout-path",
                        &name);
        if (IS_ENABLED(CONFIG_PPC) && !name)
            of_property_read_string(of_aliases, "stdout", &name);
        if (name)
            of_stdout = of_find_node_opts_by_path(name, &of_stdout_options);
    }
    for_each_property_of_node(of_aliases, pp) {
        ...
        ap = dt_alloc(sizeof(*ap) + len + 1, __alignof__(*ap));
        if (!ap)
            continue;
        memset(ap, 0, sizeof(*ap) + len + 1);
        ap->alias = start;
        of_alias_add(ap, np, id, start, len);
        ...
    }
}

從上文貼出的程序來看,of_alias_scan()函數先是處理設備樹chosen節點中的"stdout-path"或者"stdout"屬性(二者最多存在其一),而後將stdout指定的path賦值給全局變量of_stdout_options,並將返回的全局struct device_node類型數據賦值給of_stdout,指定系統啓動時的log輸出。

接下來爲aliases節點申請內存空間,若是一個節點中同時沒有name/phandle/linux,phandle,則被定義爲特殊節點,對於這些特殊節點將不會申請內存空間。

而後,使用of_alias_add()函數將全部的aliases內容放置在同一個鏈表中。

of_chosen和of_aliases都是struct device_node型的全局數據。

__unflatten_device_tree

咱們再來看最主要的設備樹解析函數:

void *__unflatten_device_tree(const void *blob,struct device_node *dad,struct device_node **mynodes,void *(*dt_alloc)(u64 size, u64 align),bool detached){
    int size;
    ...
    size = unflatten_dt_nodes(blob, NULL, dad, NULL);
    ...
    mem = dt_alloc(size + 4, __alignof__(struct device_node));
    ...
    unflatten_dt_nodes(blob, mem, dad, mynodes);
}

主要的解析函數爲unflatten_dt_nodes(),在__unflatten_device_tree()函數中,unflatten_dt_nodes()被調用兩次,第一次是掃描得出設備樹轉換成device node須要的空間,而後系統申請內存空間,第二次就進行真正的解析工做,咱們繼續看unflatten_dt_nodes()函數:

值得注意的是,在第二次調用unflatten_dt_nodes()時傳入的參數爲unflatten_dt_nodes(blob, mem, dad, mynodes);

第一個參數是設備樹存放首地址,第二個參數是申請的內存空間,第三個參數爲父節點,初始值爲NULL,第四個參數爲mynodes,初始值爲of_node.

static int unflatten_dt_nodes(const void *blob,void *mem,struct device_node *dad,struct device_node **nodepp)
{
    ...
    for (offset = 0;offset >= 0 && depth >= initial_depth;offset = fdt_next_node(blob, offset, &depth)) {
        populate_node(blob, offset, &mem,nps[depth],fpsizes[depth],&nps[depth+1], dryrun);
        ...
     }
}

這個函數中主要的做用就是從根節點開始,對子節點依次調用populate_node(),從函數命名上來看,這個函數就是填充節點,爲節點分配內存。

咱們繼續往下追蹤:

static unsigned int populate_node(const void *blob,int offset,void **mem,
			  struct device_node *dad,unsigned int fpsize,struct device_node **pnp,bool dryrun){
    struct device_node *np;
    ...
    np = unflatten_dt_alloc(mem, sizeof(struct device_node) + allocl,__alignof__(struct device_node));
    of_node_init(np);
    np->full_name = fn = ((char *)np) + sizeof(*np);
    if (dad != NULL) {
		np->parent = dad;
		np->sibling = dad->child;
		dad->child = np;
	}
    ...
    populate_properties(blob, offset, mem, np, pathp, dryrun);
    np->name = of_get_property(np, "name", NULL);
	np->type = of_get_property(np, "device_type", NULL);
    if (!np->name)
		np->name = "<NULL>";
	if (!np->type)
		np->type = "<NULL>";
    ...
}

經過跟蹤populate_node()函數,能夠看出,首先爲當前節點申請內存空間,使用of_node_init()函數對node進行初始化,of_node_init()函數也較爲簡單:

static inline void of_node_init(struct device_node *node)
{
    kobject_init(&node->kobj, &of_node_ktype);
    node->fwnode.ops = &of_fwnode_ops;
}

設置kobj,接着設置node的fwnode.ops。

而後再設置一些參數,須要特別注意的是:對於一個struct device_node結構體,申請的內存空間是sizeof(struct device_node)+allocl,這個allocl是節點的unit_name長度(相似於chosen、memory這類子節點描述開頭時的名字,並不是.name成員)。

而後經過np->full_name = fn = ((char *)np) + sizeof(*np);將device_node的full_name指向結構體結尾處,即將一個節點的unit name放置在一個struct device_node的結尾處。

同時,設置其parent和sibling節點。

接着,調用populate_properties()函數,從命名上來看,這個函數的做用是爲節點的各個屬性分配空間。

緊接着,設置device_node節點的name和type屬性,name由設備樹中.name屬性而來,type則由設備樹中.device_type而來。

一個設備樹中節點轉換成一個struct device_node結構的過程漸漸就清晰起來,如今咱們接着看看populate_properties()這個函數,看看屬性是怎麼解析的:

static void populate_properties(const void *blob,int offset,void **mem,struct device_node *np,const char *nodename,bool dryrun){
    ...
    for (cur = fdt_first_property_offset(blob, offset);
     cur >= 0;
     cur = fdt_next_property_offset(blob, cur)) 
     {
         fdt_getprop_by_offset(blob, cur, &pname, &sz);
         unflatten_dt_alloc(mem, sizeof(struct property),__alignof__(struct property));
         if (!strcmp(pname, "phandle") ||  !strcmp(pname, "linux,phandle")) {
		 if (!np->phandle)
			np->phandle = be32_to_cpup(val);

        pp->name   = (char *)pname;
        pp->length = sz;
        pp->value  = (__be32 *)val;
        *pprev     = pp;
        pprev      = &pp->next;
        ...
	}
    }
}

從屬性轉換部分的程序能夠看出,對於大部分的屬性,都是直接填充一個struct property屬性,而對於,"phandle"屬性和"linux,phandle"屬性,直接填充struct device_node 的phandle字段,不放在屬性鏈表中。

struct property結構體是這樣的:

struct property {
    char	*name;
    int	length;
    void	*value;
    struct property *next;
    ...
};

在設備樹中,對於屬性的描述是key = value,這個結構體中的name和value分別對應key和value,而length表示value的長度,next指針指向下一個struct property結構體。

struct device_node的生成

程序跟蹤到這裏,設備樹由dtb二進制文件通過解析爲每一個節點生成一個struct device_node結構體的過程基本上就清晰了,咱們再進行一下總結,首先看看struct device_node結構:

struct device_node {
    const char *name;
    const char *type;
    phandle phandle;
    const char *full_name;
    ...
    struct	property *properties;
    struct	property *deadprops;	/* removed properties */
    struct	device_node *parent;
    struct	device_node *child;
    struct	device_node *sibling;
    struct	kobject kobj;
    unsigned long _flags;
    void	*data;
    ...
};
  • .name屬性:設備節點中的name屬性轉換而來。
  • .type屬性:由設備節點中的device_type轉換而來。
  • .phandle屬性:有設備節點中的"phandle"和"linux,phandle"屬性轉換而來,特殊的還可能由"ibm,phandle"屬性轉換而來。
  • full_name:這個指針指向整個結構體的結尾位置,在結尾位置存儲着這個結構體對應設備樹節點的unit_name,意味着一個struct device_node結構體佔內存空間爲sizeof(struct device_node)+strlen(unit_name)+字節對齊。
  • .properties這是一個設備樹節點的屬性鏈表,屬性可能有不少種,好比:"interrupts","timer","hwmods"等等。
  • .parent,.child,.sibling:與當前屬性鏈表節點相關節點,因此相關鏈表節點構成整個device_node的屬性節點。
  • .kobj:用於在/sys目錄下生成相應用戶文件。

這就是設備樹子節點到struct device_node的轉換,爲了能更直觀地看出設備樹節點到struct device_node的轉換過程,博主特地製做了一張腦圖:

好了,關於linux設備樹節點轉換成device_node過程的討論就到此爲止啦,若是朋友們對於這個有什麼疑問或者發現有文章中有什麼錯誤,歡迎留言

設備樹的解析後續device_node轉換成platform_device能夠參考另外一篇博客:linux設備樹--device_node轉換成platform_device

原創博客,轉載請註明出處!

祝各位早日實現項目叢中過,bug不沾身.

相關文章
相關標籤/搜索