<kernel v5.10>
디바이스 트리(FDT) -> Expanded 포맷으로 변환
- device_node와 property 구조체를 사용하여 트리 구조로 각 노드와 속성을 연결한다.
- 기존에 사용하던 DTB 바이너리들도 문자열등을 그대로 사용하므로 삭제되지 않고 유지된다.
노드 명
다음 디바이스 트리를 보고 3 가지 노드 명 분류를 알아본다.
- Full path 노드명
- Compact 노드명
- Alias 명
arch/arm64/boot/dts/broadcom/northstar2/ns2.dtsi
/ { compatible = "brcm,ns2"; interrupt-parent = <&gic>; #address-cells = <2>; #size-cells = <2>; cpus { #address-cells = <2>; #size-cells = <0>; A57_0: cpu@0 { device_type = "cpu"; compatible = "arm,cortex-a57", "arm,armv8"; reg = <0 0>; enable-method = "psci"; next-level-cache = <&CLUSTER0_L2>; };
Full path 노드명
- 디렉토리 구조로 표현한다.
- /cpus/cpu@0
Compact 노드명
- Device Tree 버전 0x10이되면서 full path 노드명 대신 compact 노드명을 널리 사용한다.
- cpu@0
- 참고: Device trees I: Are we having fun yet? | LWN.net
Alias 명
- Compact 노드명 앞에 alias 명을 둘 수 있다.
- A57_0
FDT -> Expanded Format으로 변환 수행
워드(4바이트) 단위로 정렬된 DTB를 unflatten_device_tree( ) 함수를 통해 unflatten 과정으로 변환하면 각 노드는 device_node 구조체로 변환된다. 전역 of_root 노드가 루트 노드를 가리킨다. 각 노드에 있는 속성들도 property 구조체로 변환되어 해당 노드에 등록된다. 이렇게 바이너리 형태로 존재하다가 device_node 구조체와 property 구조체를 할당받아 트리 형태로 구성된 것을 확장 포맷(expanded format)이라고 부른다.
노드와 속성은 of_로 시작되는 API에 의해 관리되어 사용한다. unflatten된 구조체들은 슬랩 캐시 할당자에서 할당받아 만들어진다.
unflatten_device_tree()
drivers/of/fdt.c
/** * unflatten_device_tree - create tree of device_nodes from flat blob * * unflattens the device-tree passed by the firmware, creating the * tree of struct device_node. It also fills the "name" and "type" * pointers of the nodes so the normal device-tree walking functions * can be used. */
void __init unflatten_device_tree(void) { __unflatten_device_tree(initial_boot_params, NULL, &of_root, early_init_dt_alloc_memory_arch, false); /* Get pointer to "/chosen" and "/aliases" nodes for use everywhere */ of_alias_scan(early_init_dt_alloc_memory_arch); unittest_unflatten_overlay_base(); }
DTB를 확장 포맷으로 변환하고 관련 전역 변수가 적절한 노드를 가리키도록 초기화한다.
- 코드 라인 3~4에서 4바이트 단위의 바이너리로 구성된 DTB를 파싱하여 확장 포맷으로 변환한 후 of_root 전역 변수가 가리키게 한다.
- 코드 라인 7에서 전역 aliases_lookup 리스트에 alias_prop들을 추가한다.
- 전역 변수 of_aliases가 “/aliases” 노드를 가리키도록 설정한다.
- 전역 변수 of_chosen이 “/chosen” 노드를 가리키도록 설정한다.
- 전역 변수 of_stdout을 “/chosen” 노드의 “stdout-path” 속성 값에 대응하는 노드로 설정한다.
다음 그림과 같이 주요 노드들은 of_로 시작되는 전역 변수가 가리키는 것을 보여준다.
- of_stdout
- /soc/uart@7e201000 노드를 가리킨다.
- /chosen 노드의 stdout-path가 가리키는 노드가 출력 디바이스로 사용된다.
- of_aliases
- /aliases 노드를 가리킨다.
- aliases_lookup
- /aliases 노드에 담겨있는 모든 속성들을 alias_prop 구조체 형태로 변환한 후 그 들이 연결되어 있다.
- of_root
- 루트 노드를 가리킨다.
- of_chosen
- /chosen 노드를 가리킨다.
- of_stdout_option
- 출력 노드명에 사용된 옵션(‘:’문자로 시작하는) 문자열이 저장된다.
__unflatten_device_tree()
drivers/of/fdt.c
/** * __unflatten_device_tree - create tree of device_nodes from flat blob * * unflattens a device-tree, creating the * tree of struct device_node. It also fills the "name" and "type" * pointers of the nodes so the normal device-tree walking functions * can be used. * @blob: The blob to expand * @dad: Parent device node * @mynodes: The device_node tree created by the call * @dt_alloc: An allocator that provides a virtual address to memory * for the resulting tree * @detached: if true set OF_DETACHED on @mynodes * * Returns NULL on failure or the memory chunk containing the unflattened * device tree on success. */
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; void *mem; pr_debug(" -> unflatten_device_tree()\n"); if (!blob) { pr_debug("No device tree pointer\n"); return NULL; } pr_debug("Unflattening device tree:\n"); pr_debug("magic: %08x\n", fdt_magic(blob)); pr_debug("size: %08x\n", fdt_totalsize(blob)); pr_debug("version: %08x\n", fdt_version(blob)); if (fdt_check_header(blob)) { pr_err("Invalid device tree blob header\n"); return NULL; } /* First pass, scan for size */ size = unflatten_dt_nodes(blob, NULL, dad, NULL); if (size < 0) return NULL; size = ALIGN(size, 4); pr_debug(" size is %d, allocating...\n", size); /* Allocate memory for the expanded device tree */ mem = dt_alloc(size + 4, __alignof__(struct device_node)); if (!mem) return NULL; memset(mem, 0, size); *(__be32 *)(mem + size) = cpu_to_be32(0xdeadbeef); pr_debug(" unflattening %p...\n", mem); /* Second pass, do actual unflattening */ unflatten_dt_nodes(blob, mem, dad, mynodes); if (be32_to_cpup(mem + size) != 0xdeadbeef) pr_warn("End of tree marker overwritten: %08x\n", be32_to_cpup(mem + size)); if (detached && mynodes) { of_node_set_flag(*mynodes, OF_DETACHED); pr_debug("unflattened tree is detached\n"); } pr_debug(" <- unflatten_device_tree()\n"); return mem; }
DTB를 파싱하여 확장 포맷으로 변환한 후 of_root 전역 변수가 가리키게 한다.
- 코드 라인 22~25에서 DTB의 첫 부분에 위치한 헤더에서 첫 워드를 통해 DTB 데이터 여부를 체크한다. 추가로 지원 가능한 DTB 버전이 0x02 ~ 0x11인지 확인하여 체크하고, 다른 경우 에러를 출력하고 처리를 하지 않는다.
- 코드 라인 28~30에서 가장 마지막 인자 dryrun을 true로 전달하여 실제 컨버팅 동작을 하지 않고 DTB를 unflatten할 때 만들어질 device_node 구조체들과 properties 구조체들의 구성에 필요한 전체 크기의 크기만을 구한다. 그리고 최종 산출된 크기를 워드(4바이트) 단위로 정렬한다.
- 코드 라인 36~38에서 인자로 전달받은 (*dt_alloc) 함수를 통해 메모리를 할당받는다. 할당 시의 크기로 위에서 산출한 크기에 추가로 끝부분을 나타내기 위한 4바이트만큼을 추가한다. 또한 정렬 단위는 시스템의 최소 정렬 단위가 주어지는데 ARM, ARM64는 4바이트다.
- 코드 라인 42에서 할당된 메모리의 마지막 4바이트에 0xdeadbeef를 저장한다. 이 값은 경계 침범을 모니터링하기 위해 사용한다.
- 코드 라인 47에서 DTB를 파싱하여 device_node, property 구조체 배열로 변환한다.
- 코드 라인 48~50에서 할당된 메모리의 끝에 설치한 경계 침범 값이 오염되었는지 확인하여 경고 출력을 한다.
다음 그림은 바이너리 형태의 DTB를 unflatten하여 확장 포맷으로 변환하는 모습을 보여준다.
unflatten_dt_node()
drivers/of/fdt.c
/** * unflatten_dt_nodes - Alloc and populate a device_node from the flat tree * @blob: The parent device tree blob * @mem: Memory chunk to use for allocating device nodes and properties * @dad: Parent struct device_node * @nodepp: The device_node tree created by the call * * It returns the size of unflattened device tree or error code */
static int unflatten_dt_nodes(const void *blob, void *mem, struct device_node *dad, struct device_node **nodepp) { struct device_node *root; int offset = 0, depth = 0, initial_depth = 0; #define FDT_MAX_DEPTH 64 struct device_node *nps[FDT_MAX_DEPTH]; void *base = mem; bool dryrun = !base; if (nodepp) *nodepp = NULL; /* * We're unflattening device sub-tree if @dad is valid. There are * possibly multiple nodes in the first level of depth. We need * set @depth to 1 to make fdt_next_node() happy as it bails * immediately when negative @depth is found. Otherwise, the device * nodes except the first one won't be unflattened successfully. */ if (dad) depth = initial_depth = 1; root = dad; nps[depth] = dad; for (offset = 0; offset >= 0 && depth >= initial_depth; offset = fdt_next_node(blob, offset, &depth)) { if (WARN_ON_ONCE(depth >= FDT_MAX_DEPTH)) continue; if (!IS_ENABLED(CONFIG_OF_KOBJ) && !of_fdt_device_is_available(blob, offset)) continue; if (!populate_node(blob, offset, &mem, nps[depth], &nps[depth+1], dryrun)) return mem - base; if (!dryrun && nodepp && !*nodepp) *nodepp = nps[depth+1]; if (!dryrun && !root) root = nps[depth+1]; } if (offset < 0 && offset != -FDT_ERR_NOTFOUND) { pr_err("Error %d processing FDT\n", offset); return -EINVAL; } /* * Reverse the child list. Some drivers assumes node order matches .dts * node order */ if (!dryrun) reverse_nodes(root); return mem - base; }
FDT 형태의 디바이스 트리를 파싱하고 확장(expand)하여 디바이스 노드로 변환한다. @blob에 디바이스 트리(FDT)의 시작 주소를 지정하고, 확장된 디바이스 노드가 저장될 @mem을 지정한다. @dad에는 부모 디바이스 노드를 지정하고, 출력 인자 @mynode는 이 함수가 호출되어 생성될 디바이스 노드이다. 처음 호출될 때 @dad에는 null, @mynode에는 루트 디바이스 노드를 가리키는 &of_root가 주어진다.
- 코드 라인 13~14에서 먼저 출력 인자 @nodepp에 null을 대입한다.
- 코드 라인 23~24에서 @dad가 지정된 경우에 한해 depth와 초기 depth를 1부터 시작한다.
- 코드 라인 29~33에서 다음(next) 노드를 읽고 offset을 알아온다. 이 때 읽은 노드의 depth도 알아온다.
- 코드 라인 35~37에서 노드가 enable 또는 ok 상태가 아닌 경우는 skip 한다.
- 코드 라인 39~41에서 노드를 활성화한다. 지금까지 변환한 사이즈를 반환한다.
- 코드 라인 43~44에서 2nd pass에서 @nodepp에 현재 노드를 지정한다. 단 한 번만 지정한다.
- 코드 라인 45~46에서 2nd pass의 루트가 아니고 아직 루트가 지정되지 않은 경우 현재 노드를 루트로 지정한다.
- 코드 라인 49~52에서 노드 파싱에 문제가 있는 경우 에러를 반환한다.
- 코드 라인 58~59에서 2nd pass인 경우 노드를 reverse 한다.
- 코드 라인 61에서 지금까지 변환한 사이즈를 반환한다.
디바이스 노드와 속성 활성화
populate_node()
drivers/of/fdt.c
static bool populate_node(const void *blob, int offset, void **mem, struct device_node *dad, struct device_node **pnp, bool dryrun) { struct device_node *np; const char *pathp; unsigned int l, allocl; pathp = fdt_get_name(blob, offset, &l); if (!pathp) { *pnp = NULL; return false; } allocl = ++l; np = unflatten_dt_alloc(mem, sizeof(struct device_node) + allocl, __alignof__(struct device_node)); if (!dryrun) { char *fn; of_node_init(np); np->full_name = fn = ((char *)np) + sizeof(*np); memcpy(fn, pathp, l); if (dad != NULL) { np->parent = dad; np->sibling = dad->child; dad->child = np; } } populate_properties(blob, offset, mem, np, pathp, dryrun); if (!dryrun) { np->name = of_get_property(np, "name", NULL); if (!np->name) np->name = "<NULL>"; } *pnp = np; return true; }
노드를 파싱하여 디바이스 노드로 변환한다. 성공 시 true를 반환한다.
- 코드 라인 12~16에서 노드 명이 null인 경우 출력 인자 @pnp에 null을 대입한 후 더 이상 처리하지 않고 false를 반환한다.
- 코드 라인 20~21에서 @mem에서 노드가 저장될 영역을 확보한다.
- 코드 라인 22~34에서 2nd pass인 경우 노드를 초기화 하고, 노드 명을 지정한 후 노드 간의 관계를 연결한다.
- 코드 라인 36~41에서 속성을 파싱하여 속성 정보로 변환한다. 속성 이름이 없는 경우 “<NULL>” 문자열을 이름으로 지정한다.
- 코드 라인 43~44에서 출력 인자 @pnp에 디바이스 노드를 지정하고, true를 반환한다.
populate_properties()
drivers/of/fdt.c -1/2-
static void populate_properties(const void *blob, int offset, void **mem, struct device_node *np, const char *nodename, bool dryrun) { struct property *pp, **pprev = NULL; int cur; bool has_name = false; pprev = &np->properties; for (cur = fdt_first_property_offset(blob, offset); cur >= 0; cur = fdt_next_property_offset(blob, cur)) { const __be32 *val; const char *pname; u32 sz; val = fdt_getprop_by_offset(blob, cur, &pname, &sz); if (!val) { pr_warn("Cannot locate property at 0x%x\n", cur); continue; } if (!pname) { pr_warn("Cannot find property name at 0x%x\n", cur); continue; } if (!strcmp(pname, "name")) has_name = true; pp = unflatten_dt_alloc(mem, sizeof(struct property), __alignof__(struct property)); if (dryrun) continue; /* We accept flattened tree phandles either in * ePAPR-style "phandle" properties, or the * legacy "linux,phandle" properties. If both * appear and have different values, things * will get weird. Don't do that. */ if (!strcmp(pname, "phandle") || !strcmp(pname, "linux,phandle")) { if (!np->phandle) np->phandle = be32_to_cpup(val); } /* And we process the "ibm,phandle" property * used in pSeries dynamic device tree * stuff */ if (!strcmp(pname, "ibm,phandle")) np->phandle = be32_to_cpup(val); pp->name = (char *)pname; pp->length = sz; pp->value = (__be32 *)val; *pprev = pp; pprev = &pp->next; }
drivers/of/fdt.c -2/2-
/* With version 0x10 we may not have the name property, * recreate it here from the unit name if absent */ if (!has_name) { const char *p = nodename, *ps = p, *pa = NULL; int len; while (*p) { if ((*p) == '@') pa = p; else if ((*p) == '/') ps = p + 1; p++; } if (pa < ps) pa = p; len = (pa - ps) + 1; pp = unflatten_dt_alloc(mem, sizeof(struct property) + len, __alignof__(struct property)); if (!dryrun) { pp->name = "name"; pp->length = len; pp->value = pp + 1; *pprev = pp; pprev = &pp->next; memcpy(pp->value, ps, len - 1); ((char *)pp->value)[len - 1] = 0; pr_debug("fixed up name for %s -> %s\n", nodename, (char *)pp->value); } } if (!dryrun) *pprev = NULL; }
아래 그림은 노드명이 full path name으로 바뀌어 저장되는 과정을 설명하였다.
아래 그림은 a@1000 노드의 서브 노드로 a2 노드가 추가될 때의 상황이다.
아래 그림은 cpu@0 노드에 속한 속성들이 연결된 모습을 보여준다.
다음 그림은 속성명에 name이 없는 경우 마지막에 추가되는 모습을 보여준다.
child 노드가 있는 경우 DTB 순서대로 만들기 위해 각 child 노드를 reverse 한다.
unflatten_dt_alloc()
drivers/of/fdt.c
static void *unflatten_dt_alloc(void **mem, unsigned long size, unsigned long align) { void *res; *mem = PTR_ALIGN(*mem, align); res = *mem; *mem += size; return res; }
mem 값을 align 단위로 round up 하고 리턴하며 입출력 인수 mem 값은 size만큼 증가시킨다.
early_init_dt_alloc_memory_arch()
drivers/of/fdt.c
static void * __init early_init_dt_alloc_memory_arch(u64 size, u64 align) { void *ptr = memblock_alloc(size, align); if (!ptr) panic("%s: Failed to allocate %llu bytes align=0x%llx\n", __func__, size, align); return ptr; }
align 단위로 size 만큼의 공간을 memblock으로 부터 할당 받고 그 가상 주소를 리턴한다.
alias 노드 스캔
of_alias_scan()
drivers/of/base.c
/** * of_alias_scan - Scan all properties of the 'aliases' node * * The function scans all the properties of the 'aliases' node and populates * the global lookup table with the properties. It returns the * number of alias properties found, or an error code in case of failure. * * @dt_alloc: An allocator that provides a virtual address to memory * for storing the resulting tree */
void of_alias_scan(void * (*dt_alloc)(u64 size, u64 align)) { struct property *pp; of_aliases = of_find_node_by_path("/aliases"); of_chosen = of_find_node_by_path("/chosen"); if (of_chosen == NULL) of_chosen = of_find_node_by_path("/chosen@0"); if (of_chosen) { /* linux,stdout-path and /aliases/stdout are for legacy compatibility */ const char *name = NULL; 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); } if (!of_aliases) return; for_each_property_of_node(of_aliases, pp) { const char *start = pp->name; const char *end = start + strlen(start); struct device_node *np; struct alias_prop *ap; int id, len; /* Skip those we do not want to proceed */ if (!strcmp(pp->name, "name") || !strcmp(pp->name, "phandle") || !strcmp(pp->name, "linux,phandle")) continue; np = of_find_node_by_path(pp->value); if (!np) continue; /* walk the alias backwards to extract the id and work out * the 'stem' string */ while (isdigit(*(end-1)) && end > start) end--; len = end - start; if (kstrtoint(end, 10, &id) < 0) continue; /* Allocate an alias_prop with enough space for the stem */ 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); } }
“/chosen” 노드를 검색하여 전역 변수 of_stdout을 “stdout-path”에 연결된 노드로 설정한다. 그리고 “/aliases” 노드의 속성 중 “name” 및 “phandle”을 찾아 aliases_prop 구조체로 구성하여 리스트 aliases_lookup에 추가한다.
- 코드 라인 5에서 “/aliases” 노드를 찾아 전역 of_aliases에 설정한다.
- 코드 라인 6~8에서 “/chosen” 노드를 찾아 전역 of_chosen에 설정한다. 만일 of_chosen 노드가 발견되지 않으면 “/chosen@0”으로 다시 한번 검색한다.
- 코드 라인 10~18에서 “/chosen” 노드가 발견된 경우 of_chosen 노드에 있는 속성들에서 “stdout-path” 속성명으로 검색하여 찾은 속성 value 값을 name에 저장한다. 검색 결과가 없으면 레거시 호환을 위해 “linux,stdout-path” 및 “stdout” 속성명으로도 검색한다.
- 코드 라인 19~20에서 name(of_chosen 노드에서 검색한 “stdout-path” 속성의 value 값)으로 노드를 검색하고 전역 of_stdout_options에는 name 문자열에 옵션(‘:’ 문자로 시작하는 문자열) 값이 있다면 저장한다.
- 코드 라인 23~24에서 등록된 aliases가 없다면 함수를 빠져나간다.
- 코드 라인 26~31에서 of_aliases에 속한 모든 속성에 대해 루프를 돈다.
- 코드 라인 34~37에서 속성명이 “name”, “phandle”, “linux,phandle”인 경우에는 스킵한다.
- 코드 라인 39~41에서 속성의 value 값으로 노드를 검색하고 노드가 발견되지 않으면 스킵한다.
- 코드 라인 45~46에서 속성 값에 있는 노드명이 숫자가 있다면 숫자가 시작되는 위치를 end에 설정한다.
- 예 “/abc/def@1000”
- end = ‘1’ 문자를 가리킴
- 예 “/abc/def@1000”
- 코드 라인 47에서 len에 노드명의 마지막에 있는 주소를 제외한 ‘@’ 문자까지의 노드명 길이가 담긴다.
- 예) “/abc/def@1000”
- len = 9
- 예) “/abc”
- len = 4
- 예) “/abc/def@1000”
- 코드 라인 49~50에서 주소에 대한 문자열을 10진수로 변환하여 id 변수에 저장을 하는데, 에러인 경우에는 스킵한다.
- 코드 라인 53~55에서 alias_prop 구조체 크기 + len + 1 크기만큼 memblock을 할당한다. 만일 할당이 실패하면 스킵한다.
- 코드 라인 56에서 할당받은 메모리를 0으로 초기화한다.
- 코드 라인 57에서 alias가 속성명을 가리키게 한다. /alias 노드의 각 속성명은 ‘ / ’ 문자로 시작하지 않는다.
- 예 name = “uart0”
- 코드 라인 58에서 np, id, stem 값을 저장하고 전역 aliases_lookup 리스트에 추가한다. stem 문자열은 속성명에서 주소 부분을 제외했다.
- 예) 속성 데이터 = “/soc/uart@7e201000”
- stem = “/soc/uart@”
- 예) 속성 데이터 = “/soc/uart@7e201000”
구조체
device_node 구조체
include/linux/of.h
struct device_node { const char *name; phandle phandle; const char *full_name; 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) unsigned int unique_id; struct of_irq_controller *irq_trans; #endif };
property 구조체
include/linux/of.h
struct property { char *name; int length; void *value; 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 };
alias_prop 구조체
drivers/of/of_private.h
/** * struct alias_prop - Alias property in 'aliases' node * @link: List node to link the structure in aliases_lookup list * @alias: Alias property name * @np: Pointer to device_node that the alias stands for * @id: Index value from end of alias name * @stem: Alias string without the index * * The structure represents one alias property of 'aliases' node as * an entry in aliases_lookup list. */
struct alias_prop { struct list_head link; const char *alias; struct device_node *np; int id; char stem[]; };
- link
- 링크드 리스트
- alias
- alias 속성명
- 예) “chosen”, “uart0”
- alias 속성명
- np
- 노드(device_node)를 가리킨다.
- id
- 노드의 메모리 또는 포트가 사용하는 주소
- 예) 노드명이 serial@12000 인 경우 id=12000
- 노드의 메모리 또는 포트가 사용하는 주소
- stem
- index(id)를 제외한 full path 노드명
- 예) “/soc/uart@”
- index(id)를 제외한 full path 노드명
각 구조체의 Memblock 할당 시 사이즈
- device_node
- 생성할 때 마다 full path 노드명 공간이 추가 할당된다.
- name, type, data 등은 기존 DTB에 있는 문자열이나 값을 가리킨다.
- property
- 생성할 때 마다 property 사이즈만큼 공간이 할당된다. 그러나 DTB에 없는 name 속성을 추가 생성해야 하는 경우에는 property 속성 이외에도 주소제외 노드명 공간을 추가 할당한다.
- alias_prop
- 생성할 때 마다 주소제외 노드명 공간이 추가 할당되고 이 공간은 stem 문자열이 사용하는 공간이다.
참고
- DTB – 구조 | 문c
- DTB – 라즈베리파이2 샘플 소스 | 문c
- DTB (fdt API) | 문c
- DTB (of API) | 문c