奇小葩讲设备树(4/5)-- Linux设备树详解(四)kernel的解析
uboot將一些參數,設備樹文件傳給內核,那么內核如何處理這些設備樹文件呢?本章就kernel解析設備樹的過程和原理,本章的主要內容以Device Tree相關的數據流分析為索引,對ARM linux kernel的代碼進行解析。主要的數據流包括:
1. 設備樹的作用
由前面幾章已經大致可以得出設備樹對于內核的作用
| 平臺標識 | 告訴內核dtb支持哪些平臺 ; 用DT 來標識特定的machine ; root 節點的compatible 字段,匹配machine_desc的dt_compat |
| 運行時配置 | chosen節點的屬性 |
| 設備信息集合 | 傳遞各種設備信息 |
2. 初始化流程
從上一章我們已經知道fdt的地址是作為參數傳遞到kernel。下面看一下kernel階段怎么獲取這個地址值的。bootloader啟動內核時,會設置r0,r1,r2三個寄存器,
r0一般設置為0;
r1一般設置為machine id (在使用設備樹時該參數沒有被使用);
r2一般設置ATAGS或DTB的開始地址;
對于啟動的流程代碼如下:
ENTRY(stext)ARM_BE8(setend be ) @ ensure we are in BE8 modeTHUMB( adr r9, BSYM(1f) ) @ Kernel is always entered in ARM.THUMB( bx r9 ) @ If this is a Thumb-2 kernel,THUMB( .thumb ) @ switch to Thumb now.THUMB(1: )#ifdef CONFIG_ARM_VIRT_EXTbl __hyp_stub_install #endif@ ensure svc mode and all interrupts maskedsafe_svcmode_maskall r9mrc p15, 0, r9, c0, c0 @ get processor idbl __lookup_processor_type @ r5=procinfo r9=cpuidmovs r10, r5 @ invalid processor (r5=0)?THUMB( it eq ) @ force fixup-able long branch encodingbeq __error_p @ yes, error 'p'#ifdef CONFIG_ARM_LPAEmrc p15, 0, r3, c0, c1, 4 @ read ID_MMFR0and r3, r3, #0xf @ extract VMSA supportcmp r3, #5 @ long-descriptor translation table format?THUMB( it lo ) @ force fixup-able long branch encodingblo __error_lpae @ only classic page table format #endif#ifndef CONFIG_XIP_KERNELadr r3, 2fldmia r3, {r4, r8}sub r4, r3, r4 @ (PHYS_OFFSET - PAGE_OFFSET)add r8, r8, r4 @ PHYS_OFFSET #elseldr r8, =PLAT_PHYS_OFFSET @ always constant in this case #endif/** r1 = machine no, r2 = atags or dtb,* r8 = phys_offset, r9 = cpuid, r10 = procinfo*/bl __vet_atags #ifdef CONFIG_SMP_ON_UPbl __fixup_smp #endif #ifdef CONFIG_ARM_PATCH_PHYS_VIRTbl __fixup_pv_table #endifbl __create_page_tables/** The following calls CPU specific code in a position independent* manner. See arch/arm/mm/proc-*.S for details. r10 = base of* xxx_proc_info structure selected by __lookup_processor_type* above. On return, the CPU will be ready for the MMU to be* turned on, and r0 will hold the CPU control register value.*/ldr r13, =__mmap_switched @ address to jump to after@ mmu has been enabledadr lr, BSYM(1f) @ return (PIC) addressmov r8, r4 @ set TTBR1 to swapper_pg_dirARM( add pc, r10, #PROCINFO_INITFUNC )THUMB( add r12, r10, #PROCINFO_INITFUNC )THUMB( ret r12 ) 1: b __enable_mmu ENDPROC(stext).ltorg #ifndef CONFIG_XIP_KERNEL 2: .long ..long PAGE_OFFSET #endif在匯編的階段,大概可以看出來用變量__atags_pointer指向FDT的首地址,執行完匯編的階段就會調到C代碼的流程里面了
3. 平臺信息的處理(machine_desc)
進入到start_kernel的處理流程中
asmlinkage void __init start_kernel(void) {...setup_arch(&command_line); //設置架構相關的內容... }由于涉及的知識點內容實在太多,那么我們只是重點的關注fdt的處理,直接進到setup_ arch()函數。
void __init setup_arch(char **cmdline_p) { ...mdesc = setup_machine_fdt(__atags_pointer);if (!mdesc)mdesc = setup_machine_tags(__atags_pointer, __machine_arch_type);machine_desc = mdesc;machine_name = mdesc->name; ... }首先通過set_machine_fdt來set_machine描述符,如果返回值是NULL,那么就采用傳統的方式,如果u-boot傳遞了,就采用設備樹方式
首先我們來看看struct machine_desc的定義方式:
struct machine_desc { ...unsigned int nr; /* architecture number */const char *name; /* architecture name */unsigned long atag_offset; /* tagged list (relative) */const char *const *dt_compat; /* array of device tree ... }nr成員就是過去使用的machine type ID。內核machine描述符的table有若干個entry,每個都有自己的ID。bootloader傳遞了machine type ID,指明使用哪一個machine描述符。而dtb方式中目前匹配machine描述符使用compatible strings,也就是dt_compat成員,這是一個string list,定義了這個machine所支持的列表。
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;mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);if (!mdesc) {const char *prop;int size;unsigned long dt_root;early_print("\nError: unrecognized/unsupported ""device tree compatible list:\n[ ");dt_root = of_get_flat_dt_root();prop = of_get_flat_dt_prop(dt_root, "compatible", &size);while (size > 0) {early_print("'%s' ", prop);size -= strlen(prop) + 1;prop += strlen(prop) + 1;}early_print("]\n\n");dump_machine_table(); /* does not return */}/* We really don't want to do this, but sometimes firmware provides buggy data */if (mdesc->dt_fixup)mdesc->dt_fixup();early_init_dt_scan_nodes();/* Change machine number to match the mdesc we're using */__machine_arch_type = mdesc->nr;return mdesc; }setup_machine_fdt函數的功能就是根據Device Tree的信息,找到最適合的machine描述符。其主要做了下面幾件事情
成績為"吻合的compatile屬性值的位置",成績越低越匹配, 對應的machine_desc即被選中
_arch_info_begin指向machine描述符列表第一個entry。通過mdesc++不斷的移動machine描述符指針(Note:mdesc是static的)。match返回了該machine描述符的compatible string list。具體匹配的算法倒是很簡單,就是比較字符串而已,最終找到對應的machine type。 從該流程可以知道,內核是可以支持很多種不同類型的設備,只要在bootloader傳遞的時候,傳遞對應不同的dtb表即可。
4. 運行時參數傳遞
設備樹只是起一個信息傳遞的作用,對這些信息配置的處理,也比較簡單,即從設備樹的DTB文件中,把這些設備信息提取出來賦給內核中的某個變量即可。那么在系統初始化的過程中,我們需要將DTB轉換成節點是device_node的樹狀結構,以便后續方便操作。緊接上章的函數繼續分析
void __init early_init_dt_scan_nodes(void) {/* Retrieve various information from the /chosen node */of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);/* Initialize {size,address}-cells info */of_scan_flat_dt(early_init_dt_scan_root, NULL);/* Setup memory, calling early_init_dt_add_memory_arch */of_scan_flat_dt(early_init_dt_scan_memory, NULL); }該函數主要完成3個工作:
- 掃描 /chosen node,保存運行時參數(bootargs)到boot_command_line,此外,還通過early_init_dt_check_for_initrd處理initrd相關的property,并保存在initrd_start和initrd_end這兩個全局變量中 。其中主要是解析dts的配置為
bootargs屬性就是內核啟動的命令行參數,它里面可以指定根文件系統在哪里,第一個運行的應用程序是哪一個,指定內核的打印信息從哪個設備里打印出來
- 掃描根節點,獲取 {size,address}-cells信息,并保存在dt_root_size_cells和dt_root_addr_cells全局變量中 ,memory中的reg屬性的地址是32位還是64位,大小是用一個32位表示,還是兩個32位表示
- 掃描DTB中的memory node,并把相關信息保存在meminfo中,全局變量meminfo通過memblock_add保存了系統內存相關的信息
5. dtb解析成device node
uboot把設備樹DTB文件隨便放到內存的某一個地方就可以使用,為什么內核運行中,他不會去覆蓋DTB所占用的那塊內存呢?在設備樹文件中,可以使用/memreserve/指定一塊內存,這塊內存就是保留的內存,內核不會占用它。即使你沒有指定這塊內存,當我們內核啟動時,他也會把設備樹所占用的區域保留下來。內核在arm_memblock_init中會使用early_init_fdt_scan_reserved_mem來配置fdt的內存,通知也回對memreserve指定內存進行保留操作。
void __init early_init_fdt_scan_reserved_mem(void) {int n;u64 base, size;if (!initial_boot_params)return;/* Reserve the dtb region */early_init_dt_reserve_memory_arch(__pa(initial_boot_params),fdt_totalsize(initial_boot_params),0);/* Process header /memreserve/ fields */for (n = 0; ; n++) {fdt_get_mem_rsv(initial_boot_params, n, &base, &size);if (!size)break;early_init_dt_reserve_memory_arch(base, size, 0);}of_scan_flat_dt(__fdt_scan_reserved_mem, NULL);fdt_init_reserved_mem(); }- initial_boot_params實際上是dtb的虛擬地址,在early_init_dt_verify初始化的時候設定,首先進來就判斷dtb是否存在,如果存在就將dtb的空間進行保留
- 對fdt中的每一個節點調用__fdt_scan_reserved_mem函數,進行reserved-memory節點的掃描,之后調用fdt_init_reserved_mem函數進行內存預留的動作
- ?
說完了dtb對于內存的流程,那么來到這節的重點,dtb解析成device node,首先來看看下面的代碼
void __init unflatten_device_tree(void) {__unflatten_device_tree(initial_boot_params, &of_allnodes,early_init_dt_alloc_memory_arch);/* Get pointer to "/chosen" and "/aliases" nodes for use everywhere */of_alias_scan(early_init_dt_alloc_memory_arch); }分析以上代碼,在unflatten_device_tree()中,調用函數__unflatten_device_tree(),參數initial_boot_params指向Device Tree在內存中的首地址,of_root在經過該函數處理之后,會指向根節點,early_init_dt_alloc_memory_arch是一個函數指針,為struct device_node和struct property結構體分配內存的回調函數(callback)。在__unflatten_device_tree()函數中,兩次調用unflatten_dt_node()函數,第一次是為了得到Device Tree轉換成struct device_node和struct property結構體需要分配的內存大小,第二次調用才是具體填充每一個struct device_node和struct property結構體。那么Device Tree中的每一個node節點經過kernel處理都會生成一個struct device_node的結構體,struct device_node最終一般會被掛接到具體的struct device結構體。struct device_node結構體描述如下:
struct device_node {const char *name; //device node name const char *type; //對應device_type的屬性 phandle phandle; //對應該節點的phandle屬性const char *full_name; //從“/”開始的,表示該node的full pathstruct property *properties; //該節點的屬性列表 struct property *deadprops; //如果需要刪除某些屬性,kernel并非真的刪除,而是掛入到deadprops的列表 struct device_node *parent; //parent、child以及sibling將所有的device node連接起來struct device_node *child; struct device_node *sibling;struct device_node *next; //通過該指針可以獲取相同類型的下一個node struct device_node *allnext; //通過該指針可以獲取node global list下一個nodestruct kobject kobj;unsigned long _flags;void *data; #if defined(CONFIG_SPARC)const char *path_component_name;unsigned int unique_id;struct of_irq_controller *irq_trans; #endif }對于dtb最后都存儲在,其結構圖如下
6. linux kernel的設備驅動模型
在linux kernel引入統一設備模型之后,bus、driver和device形成了設備模型中的鐵三角。在驅動初始化的時候會將代表該driver的一個數據結構掛入bus上的driver鏈表,device的數據結構掛入bus上的devie鏈表,那么如何讓device遇到“對”的那個driver呢?那么就要靠緣分了,也就是bus的match函數來完成。在傳統的方式中,代碼中會定義一個static struct platform_device *xxx_devices的靜態數組,在初始化的時候調用platform_add_devices。這些靜態定義的platform_device往往又需要靜態定義各種resource,那么對于設備樹,也就是需要根據device_node的樹狀結構(root是of_allnodes)將一個個的device node掛入到相應的總線device鏈表中即可。
static int __init customize_machine(void) {/** customizes platform devices, or adds new ones* On DT based machines, we fall back to populating the* machine from the device tree, if no callback is provided,* otherwise we would always need an init_machine callback.*/if (machine_desc->init_machine)machine_desc->init_machine(); #ifdef CONFIG_OFelseof_platform_populate(NULL, of_default_bus_match_table,NULL, NULL); #endifreturn 0; }那么Linux系統是怎么知道哪些device node要注冊為platform_device,哪些是用于i2c_client,哪些是用于spi_device?不知道你有沒有注意到調用of_platform_populate的時候給它傳遞了一個參數of_default_bus_match_table
const struct of_device_id of_default_bus_match_table[] = {{ .compatible = "simple-bus", }, #ifdef CONFIG_ARM_AMBA{ .compatible = "arm,amba-bus", }, #endif /* CONFIG_ARM_AMBA */{} /* Empty terminated list */ };那么在dts文件會也會對對應的驅動進行配置
ap-apb {compatible = "simple-bus";#address-cells = <1>;#size-cells = <1>;ranges;uart0: serial@70000000 {compatible = "sprd,sc9836-uart";reg = <0x70000000 0x100>;interrupts = <GIC_SPI 2 IRQ_TYPE_LEVEL_HIGH>;clock-names = "uart", "source","enable";clocks = <&clk_uart0>, <&ext_26m>,<&clk_ap_apb_gates 13>;status = "disabled";}; }如果某個device node的compatible屬性的值與數組of_default_bus_match_table中的任意一個元素的compatible的值match,那么這個device node的child device node(device_node的child成員變量指向的是這個device node的子節點,也是一個鏈表)仍舊會被注冊為platform_device。下面來看看重點的解析過程
int of_platform_populate(struct device_node *root,const struct of_device_id *matches,const struct of_dev_auxdata *lookup,struct device *parent) {struct device_node *child;int rc = 0;root = root ? of_node_get(root) : of_find_node_by_path("/");if (!root)return -EINVAL;for_each_child_of_node(root, child) {rc = of_platform_bus_create(child, matches, lookup, parent, true);if (rc)break;}of_node_put(root);return rc; }該函數主要完成完成:
也就是說當of_platform_populate()函數執行完畢,kernel就為DTB中所有包含compatible屬性名的第一級node創建platform_device結構體,并向平臺設備總線注冊設備信息。如果第一級node的compatible屬性值等于“simple-bus”、“simple-mfd”或者"arm,amba-bus"的話,kernel會繼續為當前node的第二級包含compatible屬性的node創建platform_device結構體,并注冊設備。Linux系統下的設備大多都是掛載在平臺總線下的,因此在平臺總線被注冊后,會根據of_root節點的樹結構,去尋找該總線的子節點,所有的子節點將被作為設備注冊到該總線上。
7 參考文檔
http://www.wowotech.net/device_model/dt-code-analysis.html
https://blog.csdn.net/thisway_diy/article/details/84336817
總結
以上是生活随笔為你收集整理的奇小葩讲设备树(4/5)-- Linux设备树详解(四)kernel的解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 奇小葩讲设备树(3/5)-- Linux
- 下一篇: 奇小葩讲设备树(5/5)-- Linux