“嵌入式Linux中的LED驅動控制(裝置樹方式)”一文透過裝置樹方式實現了在野火STM32MP157開發板上對三個LED燈的控制,這裡來討論一下裝置樹的原理。
裝置樹用於描述一個硬體平臺的硬體資源,它經由bootloader傳遞到核心,核心就可從裝置樹中獲取到硬體資訊。裝置樹描述硬體資源時有兩個特點,其一,裝置樹以“樹狀”結構來描述硬體資源。其二,裝置樹可以像標頭檔案那樣,一個裝置樹檔案可以包含(引用)另外一個裝置樹檔案,以實現裝置樹內容的共享重用。
裝置樹的程式碼是一種基於文字描述的形式,其原始碼檔案以.dts為副檔名,一般一個.dts檔案對應一個硬體平臺。原始碼檔案一般位於Linux原始碼的“/arch/arm/boot/dts”目錄下。裝置樹目標檔案則以.dtb為副檔名,是裝置樹原始碼經過編譯後生成的二進位制檔案,它可以直接被核心獲取。另外,DTC是裝置樹原始碼的編譯工具,一般需要手動安裝這個編譯工具。
裝置樹的原始碼一般分為三個部分,即包含標頭檔案部分,裝置樹節點部分和裝置樹節點追加內容部分。裝置樹由一個根節點和多個子節點組成,子節點還可以繼續包含其他子節點。每個節點由一對大括號“{}”表示,而“/ {⋯};”表示“根節點”(注意最後以分號結尾),一個裝置樹只允許有一個根節點,不同檔案中的根節點最終會合併為一個根節點。在根節點內部會形成多個子節點,如“aliases {⋯}”、“chosen{⋯}”、“memory {⋯}”等。
裝置樹中的每個節點都按照如下的形式約定命名。
node-name@unit-address{ 屬性1 = ⋯ 屬性2 = ⋯ 屬性3 = ⋯ 子節點⋯ };
node-name用於指定節點的名稱,它的長度為1至31個字元。節點名應當使用大寫或小寫字母開頭,並且能夠描述裝置類別。根節點沒有節點名,它直接使用“/”指代根節點。unit-address用於指定“單元地址”,它的值要和節點“reg”屬性的第一個地址一致。如果節點沒有“reg”屬性值,可以直接省略“@unit-address”部分。
節點標籤:節點名的簡寫,作用是當其它位置需要引用時可以使用節點標籤來向該節點中追加內容。
節點路徑:指從根節點到所需節點的完整路徑,可以唯一地標識裝置樹中的節點,不同層次的裝置樹節點名稱可以相同,同層次的裝置樹節點名稱要唯一。
節點屬性:指在節點“{}”之間包含的內容,通常情況下一個節點有多個屬性,這些屬性資訊是要傳遞到核心的“板級硬體描述資訊”,在驅動程式中會透過一些API函式來獲取這些資訊。編寫裝置樹最主要的內容就是編寫這些節點屬性,通常情況下一個節點代表一個裝置。有些節點屬性是所有節點共有的,而有些屬性只作用於特定的節點。節點屬性分為標準屬性和自定義屬性,標準屬性的屬性名是固定的,自定義屬性的屬性名可按照要求自行定義。
以下是一些重要的節點屬性:
compatible屬性,其值由一個或多個字串組成,有多個字串時使用“,”分隔開。裝置樹中每一個表示裝置的節點都要有一個compatible屬性,compatible是系統用來決定繫結裝置驅動的關鍵。compatible屬性也是用來查詢節點的方法之一(另外還可以透過節點名或節點路徑查詢指定節點)。例如,在系統初始化platform匯流排上的裝置時,會根據裝置節點的”compatible”屬性與驅動of_match_table中對應的compatible值進行匹配,匹配成功就載入對應的驅動。
status屬性,用於指示裝置的“操作狀態”,透過status可以禁止裝置(disabled)或啟用裝置(okay),預設情況下status屬性裝置是使能的。
reg屬性,描述裝置資源在其父匯流排定義的地址空間內的地址,通常情況下用於表示一塊暫存器的起始地址(偏移地址)和長度,在特定情況下也會有不同的含義。reg屬性值由一串數字組成,ret屬性的書寫格式為reg = < cells cells cells cells cells cells⋯>,長度根據實際情況而定,這些資料分為地址資料(地址欄位),長度資料(大小欄位),例如reg = <0x900000 0x4000>,其中0x9000000表示的是地址,0x4000表示的是地址長度,這裡的reg屬性指定了起始地址為0x9000000,長度為0x4000 的一塊地址空間。
#address-cells和#size-cells屬性要同時存在,它們用在有子節點的裝置節點,用於設定子節點的“reg”屬性的“格式”。#address-cells用於指定子節點reg屬性“地址欄位”所佔的長度(單元格cells 的個數)。#size-cells用於指定子節點reg 屬性“大小欄位”所佔的長度(單元格cells 的個數)。例如像reg = <0x9000000 x4000>這樣的形式,應該設定成#address-cells = <1>,#address-cells = <1>。
model屬性,用於指定裝置的製造商和型號,推薦使用“製造商, 型號”的格式,也可以自定義。該屬性為非必要屬性。
ranges屬性,提供了子節點地址空間和父地址空間的對映(轉換)方法,常見格式是ranges = < 子地址, 父地址, 轉換長度>。如果父地址空間和子地址空間相同則無需轉換,可以讓renges的內容為空。
name屬性用於指定節點名,在舊的裝置樹中它用於確定節點名,現在的裝置樹已經不用了。
device_type屬性也是一個很少用的屬性,只用在CPU和記憶體的節點上。
下面來看兩類特殊的子節點。
1、aliases子節點,它的作用就是為其他節點起一個別名,如下面的示例。
aliases { ethernet0 = ðernet0; serial0 = &uart4; serial1 = &usart1; serial2 = &usart2; serial3 = &usart3; }
以上面的“serial0 = &uart4;”為例,“serial0”是一個節點的名字,設定別名後可以使用“serial0”來指代uart4節點,這與節點標籤有點類似。在裝置樹中更多的是為節點新增標籤,而不太使用節點別名,別名的作用是為了“快速找到裝置樹節點”。在驅動中如果要查詢一個節點,通常情況下我們可以使用“節點路徑”一步步找到節點。也可以使用別名“一步到位”找到節點。
2、chosen子節點,它位於根節點下,它不代表實際硬體,主要用於給核心傳遞引數,如下面的示例。
chosen { stdout-path = "serial0:115200n8"; };
上面只設定了“stdout-path =”serial0:115200n8”;”一條屬性,表示系統標準輸出stdout使用串列埠serial0,並指定了波特率、校驗、位數等引數。此外,chosen節點還可做為uboot向Linux核心傳遞配置引數的“通道”。
子節點名稱前如果多加了一個“&”符號,表示該節點是向已經存在的子節點追加資料,如“&cpu0 {⋯}”表示向已經存在的cpu0這個子節點中追加資訊。這些原始碼並不一定包含在根節點“/{⋯}”內,它們本身並不是一個新的節點,只是向原有的節點追加內容,被追加的節點可能定義在當前檔案中,也可能定義在當前檔案所包含的其他裝置樹檔案中。
以上是裝置樹相關內容的討論,接下來再討論一下與裝置樹配套的平臺驅動的相關內容。
在驅動中,用一個名為device_node的結構體來描述裝置樹中的資訊,該結構體的形式如下。
struct device_node { const char *name; const char *type; phandle phandle; const char *full_name; struct fwnode_handle fwnode; struct property *properties; struct property *deadprops; /* removed properties */ struct device_node *parent; struct device_node *child; struct device_node *sibling; #if defined(CONFIG_OF_KOBJ) struct kobject kobj; #endif 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 };
上述中,name為節點中屬性為name的值。type為節點中屬性為device_type的值。full_name為節點的名字,在device_node結構體後面放一個字串,full_name指向它。properties為連結串列,連線該節點的所有屬性。parent指向父節點。child指向子節點。sibling指向兄弟節點。
驅動如何從裝置樹的裝置節點獲取需要的資料?其實Linux核心提供了一組用於從裝置節點獲取資源(裝置節點中定義的屬性)的函式,這些函式均以of_ 開頭,一般稱為OF操作函式。
1、根據節點路徑尋找節點函式:struct device_node *of_find_node_by_path(const char *path),引數path指定節點在裝置樹中的路徑。如果查詢失敗則返回NULL,否則返回device_node型別的結構體指標,它儲存著裝置節點的資訊。
2、根據節點名字尋找節點函式:struct device_node *of_find_node_by_name(struct device_node *from, const char *name),引數from指定從哪個節點開始查詢,它本身並不在查詢行列中,只查詢它後面的節點,如果設定為NULL表示從根節點開始查詢。引數 name為要尋找的節點名。如果查詢失敗則返回NULL,否則返回device_node型別的結構體指標,它儲存著裝置節點的資訊。
3、根據節點型別尋找節點函式:struct device_node *of_find_node_by_type(struct device_node *from, const char *type),引數from指定從哪個節點開始查詢,它本身並不在查詢行列中,只查詢它後面的節點,如果設定為NULL表示從根節點開始查詢。引數type為要查詢節點的型別,這個型別就是device_node-> type。返回值為device_node型別的結構體指標,儲存獲取得到的節點。同樣,如果失敗返回NULL。
4、根據節點型別及compatible屬性尋找節點函式:struct device_node *of_find_compatible_node(struct device_node from, const char *type, const char *compatible),引數from指定從哪個節點開始查詢,它本身並不在查詢行列中,只查詢它後面的節點,如果設定為NULL表示從根節點開始查詢。引數type為要查詢節點的型別,這個型別就是device_node-> type。引數compatible為要查詢節點的compatible屬性,相比of_find_node_by_type函式增加了一個compatible屬性作為篩選條件。返回值為device_node型別的結構體指標,儲存獲取得到的節點。同樣,如果失敗返回NULL。
5、根據匹配表尋找節點函式:static inline struct device_node *of_find_matching_node_and_match(struct device_node *from, const struct of_device_id *matches, const struct of_device_id **match),引數from指定從哪個節點開始查詢,它本身並不在查詢行列中,只查詢它後面的節點,如果設定為NULL表示從根節點開始查詢。引數matches為源匹配表,查詢與該匹配表想匹配的裝置節點。引數of_device_id是一個結構體,原型如下。
struct of_device_id { char name[32]; char type[32]; char compatible[128]; const void *data; };
上述中,name為節點中屬性為name的值。type為節點中屬性為device_type的值。compatible為節點的名字,在device_node結構體後面放一個字串,full_name指向它。data為連結串列,連線該節點的所有屬性該函式的返回值為device_node型別的結構體指標,儲存獲取得到的節點。同樣,如果失敗返回NULL。
6、查詢父節點函式:struct device_node *of_get_parent(const struct device_node *node),引數node指定要查詢哪個節點的父節點。返回值為device_node型別的結構體指標,儲存獲取得到的節點。同樣,如果失敗返回NULL。
7、查詢子節點函式:struct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev),引數node指定要查詢哪個節點的子節點。引數prev為前一個子節點,查詢的是prev節點之後的節點。這是一個迭代查尋過程,例如尋找第二個子節點,這裡就要填第一個子節點。引數為NULL表示尋找第一個子節點。返回值為device_node型別的結構體指標,儲存獲取得到的節點。同樣,如果失敗返回NULL。
以是7個函式有一個共同特點,即返回值型別相同。只要找到了節點就會返回節點對應的device_node結構體,在驅動程式中透過這個device_node來獲取裝置節點的屬性資訊,並查詢它的父、子節點等等。函式of_find_node_by_path與後面六個不同,它是透過節點路徑尋找節點的,而“節點路徑”是從裝置樹原始檔(.dts) 中的到的。中間四個函式是根據節點屬性在某一個節點之後查詢符合要求的裝置節點,這個“某一個節點”是裝置節點結構體(device_node),也就是說這個節點是已經找到的。最後兩個函式與中間四個類似,只不過最後兩個沒有使用節點屬性而是根據父、子關係查詢。
下面來看節點屬性結構體,名稱為property,該結構體的形式如下。
struct property { char *name; int length; void *value; struct property *next; #if defined(CONFIG_OF_DYNAMIC) || defined(CONFIG_SPARC) unsigned long _flags; #endif #if defined(CONFIG_OF_PROMTREE) unsigned int unique_id; #endif #if defined(CONFIG_OF_KOBJ) struct bin_attribute attr; #endif };
上述中,name為屬性名,length為屬性長度,value為屬性值,next為下一個屬性。
1、查詢節點屬性函式:struct property *of_find_property(const struct device_node *np, const char *name, int *lenp),引數np指定要獲取那個裝置節點的屬性資訊,引數name為屬性名,引數lenp為獲取得到的屬性值的大小,這個指標作為輸出引數,這個引數被填充的值是實際獲取得到的屬性大小。返回值為property型別的結構體指標,失敗返回NULL。從這個結構體中可以得到想要的屬性值。
2、讀取整型屬性函式:int of_property_read_uX_array(const struct device_node *np, const char *propname, uX *out_values, size_t sz),注意這種函式一共有4個型別,以X值(8、16、32、64)不同來區分,其他都一樣。對數np指定要讀取那個裝置節點結構體,也就是說讀取那個裝置節點的資料。引數propname指定要獲取裝置節點的哪個屬性。引數out_values是一個輸出引數,是函式的“返回值”,儲存讀取得到的資料。引數sz是一個輸入引數,它用於設定讀取的長度。返回值,成功返回0,錯誤返回錯誤狀態碼(非零值),EINVAL(屬性不存在),-ENODATA(沒有要讀取的資料),-EOVERFLOW(屬性值列表太小)。
3、簡化後的讀取整型屬性函式:int of_property_read_uX (const struct device_node *np, const char *propname, uX *out_values),這種函式也有4個型別,以X值(8、16、32、64)不同來區分,其他都一樣。
4、讀取字串屬性函式1:int of_property_read_string(const struct device_node *np, const char *propname, const char **out_string),引數np指定要獲取那個裝置節點的屬性資訊,引數propname為屬性名,引數out_string獲取得到字串指標,這是一個“輸出”引數,帶回一個字串指標。也就是字串屬性值的首地址。這個地址是“屬性值”在記憶體中的真實位置,也就是說我們可以透過對地址操作獲取整個字串屬性(一個字串屬性可能包含多個字串,這些字串在記憶體中連續儲存,使用’0’分隔)。該函式成功時返回0,失敗返回時錯誤狀態碼。
5、讀取字串屬性函式2:int of_property_read_string_index(const struct device_node *np, const char *propname, int index, const char **out_string),相比前面的函式增加了引數index,它用於指定讀取屬性值中第幾個字串,index從零開始計數。第一個函式只能得到屬性值所在地址,也就是第一個字串的地址,其他字串需要我們手動修改移動地址,非常麻煩,推薦使用第2個函式。
6、記憶體對映相關of函式:void __iomem *of_iomap(struct device_node *np, int index),引數np指定要獲取那個裝置節點的屬性資訊。引數index用於指定對映哪一段(reg屬性包含多段),標號從0開始。返回值,若成功得到轉換得到的地址,失敗則返回NULL。
7、獲取地址的of函式:int of_address_to_resource(struct device_node *dev, int index, struct resource *r),引數np指定要獲取那個裝置節點的屬性資訊。引數index用於指定對映哪一段(reg屬性包含多段),標號從0開始。引數r是一個resource結構體指標,是“輸出引數”用於返回得到的地址資訊。該函式成功返回0,失敗返回錯誤狀態碼。
以上就是裝置樹配套的平臺驅動的相關內容。下面來看一下“嵌入式Linux中的LED驅動控制(裝置樹方式)”一文中的具體實現過程。
先是在裝置樹中加入了三個LED的節點,如下。
rgb_led{ #address-cells = <1>; #size-cells = <1>; compatible = "fire,rgb_led"; ranges; //紅色LED節點 led_red@0x50002000{ compatible = "fire,led_red"; reg = < 0x50002000 0x00000004 0x50002004 0x00000004 0x50002008 0x00000004 0x5000200C 0x00000004 0x50002018 0x00000004 0x50000A28 0x00000004 >; status = "okay"; }; //綠色LED節點 led_green@0x50008000{ compatible = "fire,led_green"; reg = < 0x50008000 0x00000004 0x50008004 0x00000004 0x50008008 0x00000004 0x5000800C 0x00000004 0x50008018 0x00000004 >; status = "okay"; }; //藍色LED節點 led_blue@0x50003000{ compatible = "fire,led_blue"; reg = < 0x50003000 0x00000004 0x50003004 0x00000004 0x50003008 0x00000004 0x5000300C 0x00000004 0x50003018 0x00000004 >; status = "okay"; }; };
以上在裝置樹的根下,新建了一個名為rgb_led的節點,然後在該節點內定義了#address-cells和#size-cells屬性,即指定在後面的reg屬性中,“地址欄位”和“大小欄位”所佔的長度均為1個字(32位)。然後定義一個compatible,用於和平臺驅動匹配。隨後指定一個空的ranges屬性(不能省略)。最後定了紅、綠、藍3個子節點,並給出了連線三個LED引腳的實體地址。
在子節點內部,最重要的是reg屬性。以紅色LED為例,它接在PA引腳上,其基址為0x50002000,每個暫存器的偏移量為4位元組(32位)。所以,從0x50002000到0x50002018,分別表示了PA埠MODER、OTYPER、OSPEEDR、PUPDR、BSRR四個暫存器的地址(暫存器的具體內容可參看“嵌入式Linux中的LED驅動控制(續)”一文) 。最後一個地址0x50000A28則表示埠時鐘管理暫存器RCC_MP_AHB4ENSETR,它只用設定一次,所以在後面的綠、藍子節點中並沒有設這個地址。每個子節點的地址要與reg屬性中的第一個值一致。最後,三個子節點中,都設定了status狀態為啟用。
接下來看平臺驅動,在驅動程式的入口函式中,向核心註冊了一個platform_driver結構體,名稱為led_platform_driver(有關platform平臺的討論可參看“嵌入式Linux中platform平臺裝置模型的框架(實現LED驅動)”一文)。在該結構體的driver成員中,指定了匹配表of_match_table為一個of_device_id型結構體,名為rgb_led[],在rgb_led[]中定義了一個compatible成員,它的值與裝置樹中rgb_led節點下的compatible屬性值要一致,匹配成功會觸發led_pdrv_probe函式執行。
在led_pdrv_probe函式中,先呼叫of_find_node_by_path("/rgb_led")在裝置樹中查詢rgb_led節點,找到後把該節點資訊賦值給rgb_led_device_node。然後透過rgb_led_device_node節點,再繼續呼叫of_find_node_by_name函式查詢紅、綠、藍3個子節點,找到後賦值給相應的device_node變數。然後透過呼叫of_iomap函式把找到的裝置子節點中的地址與相應的暫存器變數對映起來。接下來的做法就和“嵌入式Linux中platform平臺裝置模型的框架(實現LED驅動)”一文中基本一樣了,只不過解除對映放在了led_pdrv_remove函式中來進行。