一、前言
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的約會。