日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > linux >内容正文

linux

奇小葩讲设备树(4/5)-- Linux设备树详解(四)kernel的解析

發布時間:2023/12/15 linux 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 奇小葩讲设备树(4/5)-- Linux设备树详解(四)kernel的解析 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

uboot將一些參數,設備樹文件傳給內核,那么內核如何處理這些設備樹文件呢?本章就kernel解析設備樹的過程和原理,本章的主要內容以Device Tree相關的數據流分析為索引,對ARM linux kernel的代碼進行解析。主要的數據流包括:

  • 設備樹對于內核的意義
  • 從u-boot傳遞dtb開始,kernel初始化流程,如何將dtb并將其轉換成Device Tree Structure
  • 傳遞運行時參數傳遞以及platform的識別流程分析
  • 如何將Device Tree Structure并入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
  • __lookup_processor_type : 使用匯編指令讀取CPU ID, 根據該ID找到對應的proc_info_list結構體(里面含有這類CPU的初始化函數、信息)
  • __vet_atags : 判斷是否存在可用的ATAGS或DTB
  • 在匯編的階段,大概可以看出來用變量__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傳遞了,就采用設備樹方式

  • 傳統方式:對于如何確定mdesc,舊的方法是靜態定義若干的machine描述符(struct machine_desc),在系統啟動的時候,通過machine type ID作為索引,在這些靜態定義的machine描述符中,找到對應哪個ID匹配的描述符。
  • 設備樹:通過__atags_pointer來找到對應的machine_desc設備描述符
  • 首先我們來看看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描述符。其主要做了下面幾件事情

  • 傳進來的fdt地址是物理地址,所以用phys_to_virt()函數轉換為虛擬地址,同時進行合法檢測
  • 在machine描述符的列表中scan,找到最合適的那個machine描述符。和傳統的方法類似,也是靜態定義的。DT_MACHINE_START和MACHINE_END用來定義一個machine描述符。編譯的時候,compiler會把這些machine descriptor放到一個特殊的段中(.arch.info.init),形成machine描述符的列表。
  • of_get_flat_dt_prop(dt_root, “compatible”, &size)使用compatile屬性的值, 跟’’‘每一個machine_desc.dt_compat’’'比較,
    成績為"吻合的compatile屬性值的位置",成績越低越匹配, 對應的machine_desc即被選中
  • static const void * __init arch_get_next_mach(const char *const **match) {static const struct machine_desc *mdesc = __arch_info_begin;const struct machine_desc *m = mdesc;if (m >= __arch_info_end)return NULL;mdesc++;*match = m->dt_compat;return m; }

    _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的配置為
    chosen {bootargs = "earlycon=sprd_serial,0x70100000,115200n8 loglevel=8 console=ttyS1,115200n8 init=/init root=/dev/ram0 rw androidboot.hardware=sc9830";linux,initrd-start = <0x85500000>;linux,initrd-end = <0x855a3212>;};

    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; }

    該函數主要完成完成:

  • 獲取根節點,如果傳遞進來的參數root為NULL,那么需要通過of_find_node_by_path函數找到device tree中的根節點。
  • 得到根節點之后,就可以通過這個根節點來遍歷device tree中的節點了。得到一個子節點之后,調用of_platform_bus_create函數為每一個節點創建platform_device結構體
  • static int of_platform_bus_create(struct device_node *bus,const struct of_device_id *matches,const struct of_dev_auxdata *lookup,struct device *parent, bool strict) {const struct of_dev_auxdata *auxdata;struct device_node *child;struct platform_device *dev;const char *bus_id = NULL;void *platform_data = NULL;int rc = 0;/* Make sure it has a compatible property */if (strict && (!of_get_property(bus, "compatible", NULL))) {pr_debug("%s() - skipping %s, no compatible prop\n",__func__, bus->full_name);return 0;}auxdata = of_dev_lookup(lookup, bus);if (auxdata) {bus_id = auxdata->name;platform_data = auxdata->platform_data;}if (of_device_is_compatible(bus, "arm,primecell")) {/** Don't return an error here to keep compatibility with older* device tree files.*/of_amba_device_create(bus, bus_id, platform_data, parent);return 0;}dev = of_platform_device_create_pdata(bus, bus_id, platform_data, parent);if (!dev || !of_match_node(matches, bus))return 0;for_each_child_of_node(bus, child) {pr_debug(" create child: %s\n", child->full_name);rc = of_platform_bus_create(child, matches, lookup, &dev->dev, strict);if (rc) {of_node_put(child);break;}}of_node_set_flag(bus, OF_POPULATED_BUS);return rc; }
  • 需要確定節點是否有"compatible"屬性,如果沒有"compatible"屬性,則直接返回,即不會創建platform設備的。
  • 如果"compatible"屬性值有"arm,primecell",則會調用of_amba_device_create函數去創建amba_device,它設計了AMBA的總線來連接SOC內的各個block。符合這個總線標準的SOC上的外設叫做ARM Primecell Peripherals
  • 如果不是ARM Primecell Peripherals,那么我們就需要向platform bus上增加一個platform device了,of_platform_device_create_pdata才是真正的platform_device
  • 一個device node可能是一個橋設備,因此要重復調用of_platform_bus_create來把所有的device node處理掉
  • static struct platform_device *of_platform_device_create_pdata(struct device_node *np,const char *bus_id,void *platform_data,struct device *parent) {struct platform_device *dev;if (!of_device_is_available(np) ||of_node_test_and_set_flag(np, OF_POPULATED))return NULL;dev = of_device_alloc(np, bus_id, parent);if (!dev)goto err_clear_flag;of_dma_configure(&dev->dev);dev->dev.bus = &platform_bus_type;dev->dev.platform_data = platform_data;/* We do not fill the DMA ops for platform devices by default.* This is currently the responsibility of the platform code* to do such, possibly using a device notifier*/if (of_device_add(dev) != 0) {platform_device_put(dev);goto err_clear_flag;}return dev;err_clear_flag:of_node_clear_flag(np, OF_POPULATED);return NULL; }
  • of_device_is_available函數,這個函數主要是用于檢測"status"屬性的,如果沒有"status"屬性,那還好說直接返回true。如果有"status"屬性,而它的值又不是"okay"或"ok",那么不好意思,返回false,否則還是返回true。所以"status"屬性就是用來檢測是否可用,是否需要創建platform_node
  • of_device_alloc除了分配struct platform_device的內存,還分配了該platform device需要的resource的內存。當然,這就需要解析該device node的interrupt資源以及memory address資源。
  • 回到of_platform_device_create_pdata函數中,平臺設備已經申請好了,然后對平臺設備繼續進行賦值操作,例如平臺設備的總線賦值為平臺總線,平臺設備的私有數據賦值為platform_data,最終會調用of_device_add函數將平臺設備注冊到內核中。
  • 也就是說當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的解析的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。