Linux 內核:設備樹(2)dtb轉換成device_node
背景
前面我們了解到dtb的內存分布以后(dtb格式),接下來就來看看內核是如何把設備樹解析成所需的device_node
。
原文(有刪改):https://www.cnblogs.com/downey-blog/p/10485596.html
基於arm平台,Linux 4.14
設備樹的執行入口setup_arch
linux最底層的初始化部分在HEAD.s中,這是匯編代碼,我們暫且不作過多討論。
在head.s完成部分初始化之后,就開始調用C語言函數,而被調用的第一個C語言函數就是start_kernel
:
asmlinkage __visible void __init start_kernel(void)
{
//...
setup_arch(&command_line);
//...
}
而對於設備樹的處理,基本上就在setup_arch()
這個函數中。
可以看到,在start_kernel()
中調用了setup_arch(&command_line);
void __init setup_arch(char **cmdline_p)
{
const struct machine_desc *mdesc;
// 根據傳入的設備樹dtb的首地址完成一些初始化操作
mdesc = setup_machine_fdt(__atags_pointer);
// ...
// 保證設備樹dtb本身存在於內存中而不被覆蓋
arm_memblock_init(mdesc);
// ...
// 對設備樹具體的解析
unflatten_device_tree();
// ...
}
這三個被調用的函數就是主要的設備樹處理函數:
setup_machine_fdt()
:根據傳入的設備樹dtb的首地址完成一些初始化操作。arm_memblock_init()
:主要是內存相關函數,為設備樹保留相應的內存空間,保證設備樹dtb本身存在於內存中而不被覆蓋。用戶可以在設備樹中設置保留內存,這一部分同時作了保留指定內存的工作。unflatten_device_tree()
:對設備樹具體的解析,事實上在這個函數中所做的工作就是將設備樹各節點轉換成相應的struct device_node
結構體。
下面我們再來通過代碼跟蹤仔細分析。
setup_machine_fdt
const struct machine_desc *mdesc;
// 根據傳入的設備樹dtb的首地址完成一些初始化操作
mdesc = setup_machine_fdt(__atags_pointer);
__atags_pointer
這個全局變量存儲的就是r2的寄存器值,是設備樹在內存中的起始地址,將設備樹起始地址傳遞給setup_machine_fdt
,對設備樹進行解析。
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)))
return NULL;
// 讀取 compatible 屬性
mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);
// 掃描各個子節點
early_init_dt_scan_nodes();
// ...
}
setup_machine_fdt
主要是獲取了一些設備樹提供的總覽信息。
內存地址檢查
先將設備樹在內存中的物理地址轉換為虛擬地址
然后再檢查該地址上是否有設備樹的魔數(magic),魔數就是一串用於識別的字節碼:
- 如果沒有或者魔數不匹配,表明該地址沒有設備樹文件,函數返回失敗
- 否則驗證成功,將設備樹地址賦值給全局變量
initial_boot_params
。
讀取compatible屬性
逐一讀取設備樹根目錄下的compatible
屬性。
/**
* of_flat_dt_match_machine - Iterate match tables to find matching machine.
*
* @default_match: A machine specific ptr to return in case of no match.
* @get_next_compat: callback function to return next compatible match table.
*
* Iterate through machine match tables to find the best match for the machine
* compatible string in the FDT.
*/
const void * __init of_flat_dt_match_machine(const void *default_match,
const void * (*get_next_compat)(const char * const**))
{
const void *data = NULL;
const void *best_data = default_match;
const char *const *compat;
unsigned long dt_root;
unsigned int best_score = ~1, score = 0;
// 獲取首地址
dt_root = of_get_flat_dt_root();
// 遍歷
while ((data = get_next_compat(&compat))) {
// 將compatible中的屬性一一與內核中支持的硬件單板相對比,
// 匹配成功后返回相應的machine_desc結構體指針。
score = of_flat_dt_match(dt_root, compat);
if (score > 0 && score < best_score) {
best_data = data;
best_score = score;
}
}
// ...
pr_info("Machine model: %s\n", of_flat_dt_get_machine_name());
return best_data;
}
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
首先of_scan_flat_dt()
這個函數接收兩個參數,一個是函數指針it,一個為相當於函數it執行時的參數。
/**
* of_scan_flat_dt - scan flattened tree blob and call callback on each.
* @it: callback function
* @data: context data pointer
*
* This function is used to scan the flattened device-tree, it is
* used to extract the memory information at boot before we can
* unflatten the tree
*/
int __init of_scan_flat_dt(int (*it)(unsigned long node,
const char *uname, int depth,
void *data),
void *data)
{
unsigned long p = ((unsigned long)initial_boot_params) +
be32_to_cpu(initial_boot_params->off_dt_struct);
int rc = 0;
int depth = -1;
do {
u32 tag = be32_to_cpup((__be32 *)p);
const char *pathp;
p += 4;
if (tag == OF_DT_END_NODE) {
depth--;
continue;
}
if (tag == OF_DT_NOP)
continue;
if (tag == OF_DT_END)
break;
if (tag == OF_DT_PROP) {
u32 sz = be32_to_cpup((__be32 *)p);
p += 8;
if (be32_to_cpu(initial_boot_params->version) < 0x10)
p = ALIGN(p, sz >= 8 ? 8 : 4);
p += sz;
p = ALIGN(p, 4);
continue;
}
if (tag != OF_DT_BEGIN_NODE) {
pr_err("Invalid tag %x in flat device tree!\n", tag);
return -EINVAL;
}
depth++;
pathp = (char *)p;
p = ALIGN(p + strlen(pathp) + 1, 4);
if (*pathp == '/')
pathp = kbasename(pathp);
rc = it(p, pathp, depth, data);
if (rc != 0)
break;
} while (1);
return rc;
}
結論:of_scan_flat_dt()
函數的作用就是掃描設備樹中的節點,然后對各節點分別調用傳入的回調函數。
那么重點關注函數指針,在上述代碼中,傳入的參數分別為
early_init_dt_scan_chosen
- ``early_init_dt_scan_root`
early_init_dt_scan_memory
從名稱可以猜測,這三個函數分別是處理chosen節點、root節點中除子節點外的屬性信息、memory節點。
early_init_dt_scan_chosen
of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
boot_command_line
,boot_command_line
是一個靜態數組,存放着啟動參數,
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));
// ...
}
int __init early_init_dt_scan_chosen(unsigned long node, const char *uname,
int depth, void *data)
{
unsigned long l;
char *p;
pr_debug("search \"chosen\", depth: %d, uname: %s\n", depth, uname);
if (depth != 1 || !data ||
(strcmp(uname, "chosen") != 0 && strcmp(uname, "chosen@0") != 0))
return 0;
early_init_dt_check_for_initrd(node);
/* Retrieve command line */
// 找到設備樹中的的chosen節點中的bootargs,並作為cmd_line
p = of_get_flat_dt_prop(node, "bootargs", &l);
if (p != NULL && l > 0)
strlcpy(data, p, min((int)l, COMMAND_LINE_SIZE));
// ...
pr_debug("Command line is: %s\n", (char*)data);
/* break now */
return 1;
}
經過代碼分析,early_init_dt_scan_chosen
的作用是獲取從chosen節點
中獲取bootargs
,然后將bootargs
放入boot_command_line
中,作為啟動參數。
而非字面意思的處理整個
chosen
。
以我之前調過的zynq平台為例:
/ {
model = "ZynqMP ZCU104 RevA";
compatible = "xlnx,zynqmp-zcu104-revA", "xlnx,zynqmp-zcu104", "xlnx,zynqmp";
aliases {
ethernet0 = &gem3;
gpio0 = &gpio;
i2c0 = &i2c1;
mmc0 = &sdhci1;
rtc0 = &rtc;
serial0 = &uart0;
serial1 = &uart1;
serial2 = &dcc;
spi0 = &qspi;
usb0 = &usb0;
};
chosen {
bootargs = "earlycon";
stdout-path = "serial0:115200n8";
};
memory@0 {
device_type = "memory";
reg = <0x0 0x0 0x0 0x80000000>;
};
};
在支持設備樹的嵌入式系統中,實際上:
- uboot基本上可以不通過顯式的
bootargs=xxx
來傳遞給內核,而是在env
拿出,並存放進設備樹中的chosen
節點中 - Linux也開始在設備樹中的
chosen
節點中獲取出來,
這樣子就可以做到針對uboot與Linux在bootargs
傳遞上的統一。
early_init_dt_scan_root
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);
// ...
}
通過進一步代碼分析,early_init_dt_scan_root
為了將root節點中的#size-cells
和#address-cells
屬性提取出來,並非獲取root節點中所有的屬性,放到全局變量dt_root_size_cells
和dt_root_addr_cells
中。
size-cells和address-cells表示對一個屬性(通常是reg屬性)的地址需要多少個四字節描述,而地址的長度需要多少個四字節描述,數據長度基本單位為4。
// 表示數據大小為一個4字節描述,32位
#size-cells = 1
// 表示地址由一個四字節描述
#address-cells = 1
// 而reg屬性由四個四字節組成,所以存在兩組地址描述,
// 第一組是起始地址為0x12345678,長度為0x100,
// 第二組起始地址為0x22,長度為0x4,
// 因為在<>中,所有數據都是默認為32位。
reg = <0x12345678 0x100 0x22 0x4>
early_init_dt_scan_memory
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, ®);
size = dt_mem_next_cell(dt_root_size_cells, ®);
early_init_dt_add_memory_arch(base, size);
}
}
函數先判斷節點的unit name是memory@0,如果不是,則返回。然后將所有memory相關的reg屬性取出來,根據address-cell和size-cell的值進行解析,然后調用early_init_dt_add_memory_arch()
來申請相應的內存空間。
memory@0 {
device_type = "memory";
reg = <0x0 0x0 0x0 0x80000000>, <0x8 0x00000000 0x0 0x80000000>;
};
到這里,setup_machine_fdt()函數對於設備樹的第一次掃描解析就完成了,主要是獲取了一些設備樹提供的總覽信息。
arm_memblock_init
// arch/arm/mm/init.c
void __init arm_memblock_init(const struct machine_desc *mdesc)
{
// ...
early_init_fdt_reserve_self();
early_init_fdt_scan_reserved_mem();
// ...
}
對於設備樹的初始化而言,主要做了兩件事:
- 調用
early_init_fdt_reserve_self
,根據設備樹的大小為設備樹分配空間,設備樹的totalsize在dtb頭部中有指明,因此當系統啟動之后,設備樹就一直存在在系統中。 - 掃描設備樹節點中的"
reserved-memory
"節點,為其分配保留空間。
memblock_init
對於設備樹的部分解析就完成了,主要是為設備樹指定保留內存空間。
unflatten_device_tree
這一部分就進入了設備樹的解析部分:
注意of_root
這個對象,我們后續文章中會提到它。實際上,解析以后的數據都是放在了這個對象里面。
void __init unflatten_device_tree(void)
{
// 展開設備樹
__unflatten_device_tree(initial_boot_params, NULL, &of_root,early_init_dt_alloc_memory_arch, false);
// 掃描設備樹
of_alias_scan(early_init_dt_alloc_memory_arch);
// ...
}
展開設備樹
property原型
struct property {
char *name;
int length;
void *value;
struct property *next;
// ...
};
在設備樹中,對於屬性的描述是key = value
,這個結構體中的name和value分別對應key和value,而length表示value的長度;
next指針指向下一個struct property結構體(用於構成單鏈表)。
__unflatten_device_tree
__unflatten_device_tree(initial_boot_params, NULL, &of_root,early_init_dt_alloc_memory_arch, false);
我們再來看最主要的設備樹解析函數:
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);
unflatten_dt_nodes
第一個參數是設備樹存放首地址,第二個參數是申請的內存空間,第三個參數為父節點,初始值為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()
,從函數命名上來看,這個函數就是填充節點,為節點分配內存。
device_node原型
// include/linux/of.h
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目錄下生成相應用戶文件。
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;
// 申請內存
// 注,allocl是節點的unit_name長度(類似於chosen、memory這類子節點描述開頭時的名字,並非.name成員)
np = unflatten_dt_alloc(mem, sizeof(struct device_node) + allocl,__alignof__(struct device_node));
// 初始化node(設置kobj,接着設置node的fwnode.ops。)
of_node_init(np);
// 將device_node的full_name指向結構體結尾處,
// 即,將一個節點的unit name放置在一個struct device_node的結尾處。
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);
// 獲取,並設置device_node節點的name和type屬性
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>";
// ...
}
一個設備樹中節點轉換成一個struct device_node
結構的過程漸漸就清晰起來,現在我們接着看看populate_properties()
這個函數,看看屬性是怎么解析的,
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
字段,不放在屬性鏈表中。
掃描節點:of_alias_scan
從名字來看,這個函數的作用是解析根目錄下的alias
struct device_node *of_chosen;
struct device_node *of_aliases;
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內容放置在aliases_lookup
鏈表中。
轉換過程總結
此后,內核就可以根據device_node
來創建設備。