linux內核設備樹及編譯

1、設備樹的概念

        在內核源碼中,存在大量對板級細節信息描述的代碼。這些代碼充斥在/arch/arm/plat-xxx和/arch/arm/mach-xxx目錄,對內核而言這些platform設備、resource、i2c_board_info、spi_board_info以及各種硬件的platform_data絕大多數純屬垃圾冗餘代碼。爲了解決這一問題,ARM內核版本3.x之後引入了原先在Power PC等其他體系架構已經使用的Flattened Device Tree。

        開源文檔中對設備樹的描述是,一種描述硬件資源的數據結構,它通過bootloader將硬件資源傳給內核,使得內核和硬件資源描述相對獨立。

         Device Tree可以描述的信息包括CPU的數量和類別、內存基地址和大小、總線和橋、外設連接、中斷控制器和中斷使用情況、GPIO控制器和GPIO使用情況、Clock控制器和Clock使用情況

         另外,設備樹對於可熱插拔的設備不進行具體描述,它只描述用於控制該熱插拔設備的控制器

         設備樹的主要優勢:對於同一SOC的不同主板,只需更換設備樹文件.dtb即可實現不同主板的無差異支持,而無需更換內核文件

注:要使得3.x之後的內核支持使用設備樹,除了內核編譯時需要打開相對應的選項外,bootloader也需要支持將設備樹的數據結構傳給內核

2、設備樹的組成和使用

        設備樹包含DTC(device tree compiler),DTS(device tree source和DTB(device tree blob)。其對應關係如下圖所示:

                                                               

2.1 DTS和DTSI(源文件)

        .dts文件是一種ASCII文本對Device Tree的描述,放置在內核的/arch/arm/boot/dts目錄。一般而言,一個.dts文件對應一個ARM的machine。

         由於一個SOC可能有多個不同的電路板(  .dts文件爲板級定義, .dtsi文件爲SoC級定義),而每個電路板擁有一個 .dts。這些dts勢必會存在許多共同部分,爲了減少代碼的冗餘,設備樹將這些共同部分提煉保存在.dtsi文件中,供不同的dts共同使用。.dtsi的使用方法,類似於C語言的頭文件,在dts文件中需要進行include .dtsi文件。當然,dtsi本身也支持include 另一個dtsi文件。

2.2 DTC (編譯工具)

        DTC爲編譯工具,dtc編譯器可以把dts文件編譯成爲dtb,也可把dtb編譯成爲dts文件。在3.x內核版本中,DTC的源碼位於內核的scripts/dtc目錄,內核選中CONFIG_OF,編譯內核的時候,主機可執行程序DTC就會被編譯出來。 即scripts/dtc/Makefile中

  1. hostprogs-:= dtc
  2. always := $(hostprogs-y) 

        在內核的arch/arm/boot/dts/Makefile中,若選中某種SOC,則與其對應相關的所有dtb文件都將編譯出來。在linux下,make dtbs可單獨編譯dtb。以下截取了TEGRA平臺的一部分。

  1. ifeq ($(CONFIG_OF),y)
  2. dtb-$(CONFIG_ARCH_TEGRA) += tegra20-harmony.dtb \
  3. tegra30-beaver.dtb \
  4. tegra114-dalmore.dtb \
  5. tegra124-ardbeg.dtb 
        在2.6.x版本內核中,只在powerpc架構下使用了設備樹,DTC的源碼位於內核的arch/powerpc/boot/dtc-src目錄,編譯內核後,可將DTC編譯出來,DTC編譯工具位於arch/powerpc/boot目錄下。

2.3 DTB (二進制文件)

       DTC編譯.dts生成的二進制文件(.dtb),bootloader在引導內核時,會預先讀取.dtb到內存,進而由內核解析

        在2.6.x版本內核中,在powerpc架構下,dtb文件可以單獨進行編譯,編譯命令格式如下:

dtc [-I input-format] [-O output-format][-o output-filename] [-V output_version] input_filename

參數說明

input-format:

- “dtb”: “blob” format

- “dts”: “source” format.

- “fs” format.

output-format:

- “dtb”: “blob” format

- “dts”: “source” format

- “asm”: assembly language file

output_version:

定義”blob”的版本,在dtb文件的字段中有表示,支持1 2 3和16,默認是3,在16版本上有許多特性改變

(1)  Dts編譯生成dtb

./dtc -I dts -O dtb -o B_dtb.dtb A_dts.dts

把A_dts.dts編譯生成B_dtb.dtb

(2)  Dtb編譯生成dts

./dtc -I dtb -O dts -o A_dts.dts A_dtb.dtb

把A_dtb.dtb反編譯生成爲A_dts.dts

        在linux 3.x內核中,可以使用make的方式進行編譯。

2.4 Bootloader(boottloader支持)

    Bootloader需要將設備樹在內存中的地址傳給內核。在ARM中通過bootm或bootz命令來進行傳遞。    

    bootm [kernel_addr] [initrd_address] [dtb_address],其中kernel_addr爲內核鏡像的地址,initrd爲initrd的地址dtb_address爲dtb所在的地址。若initrd_address爲空,則用“-”來代替。


3、linux內核對硬件的描述方式

       在以前的內核版本中:
1)內核包含了對硬件的全部描述;
2)bootloader會加載一個二進制的內核鏡像,並執行它,比如uImage或者zImage;
3)bootloader會提供一些額外的信息,成爲ATAGS,它的地址會通過r2寄存器傳給內核;
    ATAGS包含了內存大小和地址,kernel command line等等;
4)bootloader會告訴內核加載哪一款board,通過r1寄存器存放的machine type integer;
5)U-Boot的內核啓動命令:bootm <kernel img addr>
6)Barebox變量:bootm.image (?)


現今的內核版本使用了Device Tree:
1)內核不再包含對硬件的描述,它以二進制的形式單獨存儲在另外的位置:the device tree blob
2)bootloader需要加載兩個二進制文件:內核鏡像和DTB
    內核鏡像仍然是uImage或者zImage;
    DTB文件在arch/arm/boot/dts中,每一個board對應一個dts文件;
3)bootloader通過r2寄存器來傳遞DTB地址,通過修改DTB可以修改內存信息,kernel command line,以及潛在的其它信息;
4)不再有machine type;
5)U-Boot的內核啓動命令:bootm <kernel img addr> - <dtb addr>
6)Barebox變量:bootm.image,bootm.oftree


        有些bootloader不支持Device Tree,或者有些專門給特定設備寫的版本太老了,也不包含。爲了解決這個問題,CONFIG_ARM_APPENDED_DTB被引進。 
    它告訴內核,在緊跟着內核的地址裏查找DTB文件;
    由於沒有built-in Makefile rule來產生這樣的內核,因此需要手動操作:
        cat arch/arm/boot/zImage arch/arm/boot/dts/myboard.dtb > my-zImage
        mkimage ... -d my-zImage my-uImage
    (cat這個命令,還能夠直接合並兩個mp3文件哦!so easy!)
另外,CONFIG_ARM_ATAG_DTB_COMPAT選項告訴內核去bootloader裏面讀取ATAGS,並使用它們升級DT。


4、DTB加載及解析過程

    先從uboot裏的do_bootm出發,根據之前描述,DTB在內存中的地址通過bootm命令進行傳遞。在bootm中,它會根據所傳進來的DTB地址,對DTB所在內存做一系列操作,爲內核解析DTB提供保證。上圖爲對應的函數調用關係圖。

    在do_bootm中,主要調用函數爲do_bootm_states,第四個參數爲bootm所要處理的階段和狀態。 

    在do_bootm_states中,bootm_start會對lmb進行初始化操作,lmb所管理的物理內存塊有三種方式獲取。起始地址,優先級從上往下:

  1.  環境變量“bootm_low”
  2.  宏CONFIG_SYS_SDRAM_BASE(在tegra124中爲0x80000000)
  3.  gd->bd->bi_dram[0].start

大小:

  1.  環境變量“bootm_size”
  2.  gd->bd->bi_dram[0].size

    經過初始化之後,這塊內存就歸lmb所管轄。接着,調用bootm_find_os進行kernel鏡像的相關操作,這裏不具體闡述。

    還記得之前講過bootm的三個參數麼,第一個參數內核地址已經被bootm_find_os處理,而接下來的兩個參數會在bootm_find_other中執行操作。

    首先,bootm_find_other根據第二個參數找到ramdisk的地址,得到ramdisk的鏡像;然後根據第三個參數得到DTB鏡像,同檢查kernel和ramdisk鏡像一樣,檢查DTB鏡像也會進行一系列的校驗工作,如果校驗錯誤,將無法正常啓動內核。另外,uboot在確認DTB鏡像無誤之後,會將該地址保存在環境變量“fdtaddr”中。

    接着,uboot會把DTB鏡像reload一次,使得DTB鏡像所在的物理內存歸lmb所管理:    

  • ①boot_fdt_add_mem_rsv_regions會將原先的內存DTB鏡像所在的內存置爲reserve,保證該段內存不會被其他非法使用,保證接下來的reload數據是正確的;
  • ②boot_relocate_fdt會在bootmap區域中申請一塊未被使用的內存,接着將DTB鏡像內容複製到這塊區域(即歸lmb所管理的區域)

注:若環境變量中,指定“fdt_high”參數,則會根據該值,調用lmb_alloc_base函數來分配DTB鏡像reload的地址空間。若分配失敗,則會停止bootm操作。因而,不建議設置fdt_high參數。

    接下來,do_bootm會根據內核的類型調用對應的啓動函數。與linux對應的是do_bootm_linux。

  • ① boot_prep_linux

        爲啓動後的kernel準備參數

  • ② boot_jump_linux

    以上是boot_jump_linux的片段代碼,可以看出:若使用DTB,則原先用來存儲ATAG的寄存器R2,將會用來存儲.dtb鏡像地址。

    boot_jump_linux最後將調用kernel_entry,將.dtb鏡像地址傳給內核。

 

    下面我們來看下內核的處理部分:

    在arch/arm/kernel/head.S中,有這樣一段:

    _vet_atags定義在/arch/arm/kernel/head-common.S中,它主要對DTB鏡像做了一個簡單的校驗。

    真正解析處理dbt的開始部分,是setup_arch->setup_machine_fdt。這部分的處理在第五部分的machine_mdesc中有提及。

 

    如圖,是setup_machine_fdt中的解析過程。

  •     解析chosen節點將對boot_command_line進行初始化。
  •     解析根節點的{size,address}將對dt_root_size_cells,dt_root_addr_cells進行初始化。爲之後解析memory等其他節點提供依據。
  •     解析memory節點,將會把節點中描述的內存,加入memory的bank。爲之後的內存初始化提供條件。

 

  •     解析設備樹在函數unflatten_device_tree中完成,它將.dtb解析成device_node結構(第五部分有其定義),並構成單項鍊表,以供OF的API接口使用。

下面主要結合代碼分析:/drivers/of/fdt.c

 

 

 

總的歸納爲:

    ① kernel入口處獲取到uboot傳過來的.dtb鏡像的基地址

    ② 通過early_init_dt_scan()函數來獲取kernel初始化時需要的bootargs和cmd_line等系統引導參數。

    ③ 調用unflatten_device_tree函數來解析dtb文件,構建一個由device_node結構連接而成的單向鏈表,並使用全局變量of_allnodes保存這個鏈表的頭指針。

    ④ 內核調用OF的API接口,獲取of_allnodes鏈表信息來初始化內核其他子系統、設備等。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章