Linux Device Tree(三):代碼分析(1)

2021-02-17 嵌入式Linux中文站


一、前言

Device Tree總共有三篇,分別是:

1、為何要引入Device Tree,這個機制是用來解決什麼問題的?(請參考引入Device Tree的原因)

2、Device Tree的基礎概念(請參考DT基礎概念)

3、ARM linux中和Device Tree相關的代碼分析(這是本文的主題)

本文主要內容是:以Device Tree相關的數據流分析為索引,對ARM linux kernel的代碼進行解析。主要的數據流包括:

1、初始化流程。也就是掃描dtb並將其轉換成Device Tree Structure。

2、傳遞運行時參數傳遞以及platform的識別流程分析

3、如何將Device Tree Structure併入linux kernel的設備驅動模型。

註:本文中的linux kernel使用的是3.14版本。

 

二、如何通過Device Tree完成運行時參數傳遞以及platform的識別功能?

1、彙編部分的代碼分析

linux/arch/arm/kernel/head.S文件定義了bootloader和kernel的參數傳遞要求:

MMU = off, D-cache = off, I-cache = dont care, r0 = 0, r1 = machine nr, r2 = atags or dtb pointer.

目前的kernel支持舊的tag list的方式,同時也支持device tree的方式。r2可能是device tree binary file的指針(bootloader要傳遞給內核之前要copy到memory中),也可以能是tag list的指針。在ARM的彙編部分的啟動代碼中(主要是head.S和head-common.S),machine type ID和指向DTB或者atags的指針被保存在變量__machine_arch_type和__atags_pointer中,這麼做是為了後續c代碼進行處理。

2、和device tree相關的setup_arch代碼分析

具體的c代碼都是在setup_arch中處理,這個函數是一個總的入口點。具體代碼如下(刪除了部分無關代碼):

void __init setup_arch(char **cmdline_p) 

    const struct machine_desc *mdesc;

……

    mdesc = setup_machine_fdt(__atags_pointer); 
    if (!mdesc) 
        mdesc = setup_machine_tags(__atags_pointer, __machine_arch_type); 
    machine_desc = mdesc; 
    machine_name = mdesc->name;

…… 
}

對於如何確定HW platform這個問題,舊的方法是靜態定義若干的machine描述符(struct machine_desc ),在啟動過程中,通過machine type ID作為索引,在這些靜態定義的machine描述符中掃描,找到那個ID匹配的描述符。在新的內核中,首先使用setup_machine_fdt來setup machine描述符,如果返回NULL,才使用傳統的方法setup_machine_tags來setup machine描述符。傳統的方法需要給出__machine_arch_type(bootloader通過r1寄存器傳遞給kernel的)和tag list的地址(用來進行tag parse)。__machine_arch_type用來尋找machine描述符;tag list用於運行時參數的傳遞。隨著內核的不斷發展,相信有一天linux kernel會完全拋棄tag list的機制。

3、匹配platform(machine描述符)

setup_machine_fdt函數的功能就是根據Device Tree的信息,找到最適合的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_scan(phys_to_virt(dt_phys))) 
        return NULL;

    mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);

    if (!mdesc) {  
        出錯處理 
    }

    /* Change machine number to match the mdesc we're using */ 
    __machine_arch_type = mdesc->nr;

    return mdesc; 
}

early_init_dt_scan函數有兩個功能,一個是為後續的DTB scan進行準備工作,另外一個是運行時參數傳遞。具體請參考下面一個section的描述。

of_flat_dt_match_machine是在machine描述符的列表中scan,找到最合適的那個machine描述符。我們首先看如何組成machine描述符的列表。和傳統的方法類似,也是靜態定義的。DT_MACHINE_START和MACHINE_END用來定義一個machine描述符。編譯的時候,compiler會把這些machine descriptor放到一個特殊的段中(.arch.info.init),形成machine描述符的列表。machine描述符用下面的數據結構來標識(刪除了不相關的member):

struct machine_desc { 
    unsigned int        nr;        /* architecture number    */ 
    const char *const     *dt_compat;    /* array of device tree 'compatible' strings    */

……

   };

nr成員就是過去使用的machine type ID。內核machine描述符的table有若干個entry,每個都有自己的ID。bootloader傳遞了machine type ID,指明使用哪一個machine描述符。目前匹配machine描述符使用compatible strings,也就是dt_compat成員,這是一個string list,定義了這個machine所支持的列表。在掃描machine描述符列表的時候需要不斷的獲取下一個machine描述符的compatible字符串的信息,具體的代碼如下:

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。具體匹配的算法倒是很簡單,就是比較字符串而已,一個是root node的compatible字符串列表,一個是machine描述符的compatible字符串列表,得分最低的(最匹配的)就是我們最終選定的machine type。

4、運行時參數傳遞

運行時參數是在掃描DTB的chosen node時候完成的,具體的動作就是獲取chosen node的bootargs、initrd等屬性的value,並將其保存在全局變量(boot_command_line,initrd_start、initrd_end)中。使用tag list方法是類似的,通過分析tag list,獲取相關信息,保存在同樣的全局變量中。具體代碼位於early_init_dt_scan函數中:

bool __init early_init_dt_scan(void *params) 

    if (!params) 
        return false;

    /* 全局變量initial_boot_params指向了DTB的header*/ 
    initial_boot_params = params;

    /* 檢查DTB的magic,確認是一個有效的DTB */ 
    if (be32_to_cpu(initial_boot_params->magic) != OF_DT_HEADER) { 
        initial_boot_params = NULL; 
        return false; 
    }

    /* 掃描 /chosen node,保存運行時參數(bootargs)到boot_command_line,此外,還處理initrd相關的property,並保存在initrd_start和initrd_end這兩個全局變量中 */ 
    of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);

    /* 掃描根節點,獲取 {size,address}-cells信息,並保存在dt_root_size_cells和dt_root_addr_cells全局變量中 */ 
    of_scan_flat_dt(early_init_dt_scan_root, NULL);

    /* 掃描DTB中的memory node,並把相關信息保存在meminfo中,全局變量meminfo保存了系統內存相關的信息。*/ 
    of_scan_flat_dt(early_init_dt_scan_memory, NULL);

    return true; 
}

設定meminfo(該全局變量確定了物理內存的布局)有若干種途徑:

1、通過tag list(tag是ATAG_MEM)傳遞memory bank的信息。

2、通過command line(可以用tag list,也可以通過DTB)傳遞memory bank的信息。

3、通過DTB的memory node傳遞memory bank的信息。

目前當然是推薦使用Device Tree的方式來傳遞物理內存布局信息。

 

三、初始化流程

在系統初始化的過程中,我們需要將DTB轉換成節點是device_node的樹狀結構,以便後續方便操作。具體的代碼位於setup_arch->unflatten_device_tree中。

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

我們用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 path

    struct    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下一個node 
    struct    proc_dir_entry *pde;--------開放到userspace的proc接口信息 
    struct    kref kref;-------------該node的reference count 
    unsigned long _flags; 
    void    *data; 
};

unflatten_device_tree函數的主要功能就是掃描DTB,將device node被組織成:

1、global list。全局變量struct device_node *of_allnodes就是指向設備樹的global list

2、tree。

這些功能主要是在__unflatten_device_tree函數中實現,具體代碼如下(去掉一些無關緊要的代碼):

static void __unflatten_device_tree(struct boot_param_header *blob,---需要掃描的DTB 
                 struct device_node **mynodes,---------global list指針 
                 void * (*dt_alloc)(u64 size, u64 align))------內存分配函數 

    unsigned long size; 
    void *start, *mem; 
    struct device_node **allnextp = mynodes;

    此處刪除了health check代碼,例如檢查DTB header的magic,確認blob的確指向一個DTB。

    /* scan過程分成兩輪,第一輪主要是確定device-tree structure的長度,保存在size變量中 */ 
    start = ((void *)blob) + be32_to_cpu(blob->off_dt_struct); 
    size = (unsigned long)unflatten_dt_node(blob, 0, &start, NULL, NULL, 0); 
    size = ALIGN(size, 4);

    /* 初始化的時候,並不是掃描到一個node或者property就分配相應的內存,實際上內核是一次性的分配了一大片內存,這些內存包括了所有的struct device_node、node name、struct property所需要的內存。*/ 
    mem = dt_alloc(size + 4, __alignof__(struct device_node)); 
    memset(mem, 0, size);

    *(__be32 *)(mem + size) = cpu_to_be32(0xdeadbeef);   //用來檢驗後面unflattening是否溢出

    /* 這是第二輪的scan,第一次scan是為了得到保存所有node和property所需要的內存size,第二次就是實打實的要構建device node tree了 */ 
    start = ((void *)blob) + be32_to_cpu(blob->off_dt_struct); 
    unflatten_dt_node(blob, mem, &start, NULL, &allnextp, 0);  
   

    此處略去校驗溢出和校驗OF_DT_END。 
}

具體的scan是在unflatten_dt_node函數中,如果已經清楚地了解DTB的結構,其實代碼很簡單,這裡就不再細述了。

四、如何併入linux kernel的設備驅動模型

在linux kernel引入統一設備模型之後,bus、driver和device形成了設備模型中的鐵三角。在驅動初始化的時候會將代表該driver的一個數據結構(一般是xxx_driver)掛入bus上的driver鍊表。device掛入鍊表分成兩種情況,一種是即插即用類型的bus,在插入一個設備後,總線可以檢測到這個行為並動態分配一個device數據結構(一般是xxx_device,例如usb_device),之後,將該數據結構掛入bus上的device鍊表。bus上掛滿了driver和device,那麼如何讓device遇到「對」的那個driver呢?那麼就要靠緣分了,也就是bus的match函數。

上面是一段導論,我們還是回到Device Tree。導致Device Tree的引入ARM體系結構的代碼其中一個最重要的原因的太多的靜態定義的表格。例如:一般代碼中會定義一個static struct platform_device *xxx_devices的靜態數組,在初始化的時候調用platform_add_devices。這些靜態定義的platform_device往往又需要靜態定義各種resource,這導致靜態表格進一步增大。如果ARM linux中不再定義這些表格,那麼一定需要一個轉換的過程,也就是說,系統應該會根據Device tree來動態的增加系統中的platform_device。當然,這個過程並非只是發生在platform bus上(具體可以參考「Platform Device」的設備),也可能發生在其他的非即插即用的bus上,例如AMBA總線、PCI總線。一言以蔽之,如果要併入linux kernel的設備驅動模型,那麼就需要根據device_node的樹狀結構(root是of_allnodes)將一個個的device node掛入到相應的總線device鍊表中。只要做到這一點,總線機制就會安排device和driver的約會。

相關焦點

  • ARM Linux 3.x的設備樹(Device Tree)
    譬如,對於ARM GIC中斷控制器而言,#interrupt-cells為3,它3個cell的具體含義Documentation/devicetree/bindings/arm/gic.txt就有如下文字說明:01The1stcellistheinterrupttype;0forSPIinterrupts,1forPPI02interrupts
  • 淺談分析Arm linux 內核移植及系統初始化的過程二
    4.1. 處理器、設備4.2. 描述設備描述主要兩個結構體完成:structresource和structplatform_device。操作(1)intplatform_device_register(structplatform_device*pdev);註冊設備(2)voidplatform_device_unregister(structplatform_device*pdev);註銷設備(3)intplatform_add_devices(structplatform_device**devs,intnum
  • 「正點原子Linux連載」第五十八章Linux INPUT子系統實驗
    58.1.3.2可以看出,tv_sec和tv_usec這兩個成員變量都為long類型,也就是32位,這個一定要記住,後面我們分析event事件上報數據的時候要用到。工程創建好以後新建keyinput.c文件,在keyinput.c裡面輸入如下內容:示例代碼58.3.2.1 keyinput.c文件代碼段1 #include <linux/types.
  • Linux 內核通知鏈和例程代碼
    代碼位置include/linux/notifier.h kernel/notifier.c 代碼不超過 1000 行,但是也是因為代碼少,才顯現出大神的厲害之處。NOTIFY_STOP_MASK) break; nb = next_nb; nr_to_call--; } return ret;}參數nl是通知鏈的頭部,val表示事件類型,v用來指向通知鏈上的函數執行時需要用到的參數,一般不同的通知鏈,參數類型也不一樣,例如當通知一個網卡被註冊時,v就指向net_device
  • 使用Treetime分析tMRCA和進化速率
    其中通過軟體Beast分析tMRCA和進化速率在本人之前的《Beast v1.8使用手冊》中已經提到,這裡重點介紹Regression of root-to-tip distances(RTT)裡的treetime軟體包分析tMRCA和進化速率。
  • 乙太網驅動的流程淺析(三)-ifconfig的-19錯誤最底層分析
    【硬體環境】         Imx6ul【Linux kernel版本】   Linux4.1.15【乙太網phy】        Realtek8201f1.1 ifconfig的-19錯誤最底層分析首先我們來看nxp的乙太網驅動代碼 路徑:drivers/net/ethernet/freescale
  • Linux 4.4.220 PCI總線驅動分析
    同時強烈推薦「Linux內核原始碼情景分析」這本書,它基於2.4的內核代碼對PCI的一些難以弄懂的片段做了分析,同時也推薦先讀2.4內核相關代碼,因為4.4比2.4複雜很多,先閱讀2.4更有助於弄清PCI驅動的邏輯。
  • Linux下使用tar命令
    範例:範例一:將整個 /etc 目錄下的文件全部打包成為 /tmp/etc.tar[root@linux ~][root@linux ~][root@linux ~]特別注意:在參數範例三:將 /tmp/etc.tar.gz 文件解壓縮在 /usr/local/src 底下[root@linux ~][root@linux src]在預設的情況下,我們可以將壓縮檔在任何地方解開的,以這個範例來說我先將工作目錄變換到 /usr/local/src 底下,並且解開 /tmp/etc.tar.gz
  • Linux SD/MMC/SDIO驅動分析
    在linux系統中,將每個host設備封裝成platform_device來逐一進行註冊。的註冊分析,發現s3c_device_mmc3.name也剛好是「s3c-sdhci」,所以他倆剛好可以配對,探測成功,同時當大家查閱s3c_device_hsmmc,s3c_device_hsmmc1以及s3c_device_hsmmc2的時候發現他們的name成員變量都是「s3c-sdhci」,,所以會有四次成功的探測,每一次探測成功,就會調用sdhci_s3c_driver.probe函數--
  • Linux input子系統編程、分析與模板
    由於每種輸入的設備上報的事件都各有不同,所以為了應用層能夠很好識別上報的事件,內核中也為應用層封裝了標準的接口來描述一個事件,這些接口在"/include/upai/linux/input"中。對象  void input_free_device(struct input_dev *dev);  初始化 初始化一個input對象是使用input子系統編寫驅動的主要工作,內核在頭文件"include/uapi/linux/input.h"中規定了一些常見輸入設備的常見的輸入事件,這些宏和數組就是我們初始化input對象的工具。
  • 「正點原子Linux連載」第六十二章Linux SPI驅動實驗
    Linux內核使用spi_master表示SPI主機驅動,spi_master是個結構體,定義在include/linux/spi/spi.h文件中,內容如下(有縮減):示例代碼62.1.1.1 spi_master結構體315struct spi_master {316struct device dev;317318 struct
  • Linux2.6內核驅動移植參考
    1、 使用新的入口 必須包含 linux/init.h> module_init(your_init_func); module_exit(your_exit_func); 老版本:int init_module(void); void cleanup_module(voi); 2.4中兩種都可以用
  • 乾坤合一~Linux SD/MMC/SDIO驅動分析
    在linux系統中,將每個host設備封裝成platform_device來逐一進行註冊。的註冊分析,發現s3c_device_mmc3.name也剛好是「s3c-sdhci」,所以他倆剛好可以配對,探測成功,同時當大家查閱s3c_device_hsmmc,s3c_device_hsmmc1以及s3c_device_hsmmc2的時候發現他們的name成員變量都是「s3c-sdhci」,,所以會有四次成功的探測,每一次探測成功,就會調用sdhci_s3c_driver.probe函數--
  • Linux 自旋鎖spinlock,教你如何把ubuntu弄死鎖
    為了解決這樣的問題,linux kernel採用了這樣的辦法:如果涉及到中斷上下文的訪問,spin lock需要和禁止本 CPU 上的中斷聯合使用。三、再考慮下面的場景(底半部場景)linux kernel中提供了豐富的bottom half的機制,雖然同屬中斷上下文,不過還是稍有不同。
  • 從串口驅動到Linux驅動模型,想轉Linux的必會!
    可以直接閱讀後面的代碼分析:1、什麼是Linux作業系統 ?Linux是一套免費使用和自由傳播的類Unix作業系統,是一個基於POSIX和UNIX的多用戶、多任務、支持多線程和多CPU的作業系統。它能運行主要的UNIX工具軟體、應用程式和網絡協議。它支持32位和64位硬體。Linux繼承了Unix以網絡為核心的設計思想,是一個性能穩定的多用戶網絡作業系統。
  • Linux原始碼分析工具鏈
    上面我們講的是用vim來查看原始碼,但是面對幾十萬代碼的時候,想要看清楚各個結構體之間的關係就不是vim能夠做到的了。這時候我們就需要doxygen來幫手了。分析源碼執行流程的最好方式的是運行它,然後一步步執行。用來觀察它最好的工具當然是gdb了(針對C/C++)。gdb的使用我也不打算造輪子,直接參考用GDB調試程序,都通俗易懂。以上就是我在閱讀源碼的時候使用的Linux工具,三劍客vim+ctags+cscope,兩板斧doxygen gdb,足以馳騁原始碼的江湖。
  • Linux嵌入式驅動開發——ioctl接口
    文章目錄我們從平臺總線模型,然後到pinctrl和gpio子系統,會發現步驟逐漸的規範,代碼也逐漸的簡單,也越來越能體會到linux屏蔽底層硬體的優勢
  • linux內核移植-移植2.6.35.4內核到s3c2440
    本來是想移植最新的內核2.6.39但是總是在編譯快完成的時候報錯,有人說是新的內核對arm平臺的支持不好,所以就降低了一下版本,這裡移植2.6.35.4內核一、準備工作1、下載 解壓內核從官網上下載linux-2.6.35的內核, ftp://ftp.kernel.org/pub/linux/kernel/v2.6/ ,文件不大,約85M。
  • linux字符設備驅動基本框架
    1.linux函數調用過程1.1 系統函數調用的意義在Linux的中,有一個思想比較重要:一切皆文件。也就是說,在應用程式中,可以通過open,write,read等函數來操作底層的驅動。字符設備與塊設備驅動程序的區別與聯繫1.字符設備的最小訪問單元是字節,塊設備是塊字節512或者512位元組為單位2.訪問順序上面,字符設備是順序訪問的,而塊設備是隨機訪問的3.在linux中,字符設備和塊設備訪問字節沒有本質區別網絡設備驅動程序的本質提供了協議與設備驅動通信的通用接口
  • linux靜態庫和動態庫分析
    1.什麼是庫本文引用地址:http://www.eepw.com.cn/article/257989.htm  在windows平臺和linux平臺下都大量存在著庫。  本質上來說庫是一種可執行代碼的二進位形式,可以被作業系統載入內存執行。