【linux】驅動-8-一文解決裝置樹

李柱明 發表於 2021-04-06


前言

8. Linux裝置樹

Linux3.x 以後引入了裝置樹,用於描述一個硬體平臺的板級細節。

8.1 裝置樹簡介

裝置樹可以被 bootloader(uboot)傳遞到核心,核心從中獲取裝置樹中的硬體資訊。

裝置樹的兩個特點

  • :以 樹狀結構 描述硬體資源。
  • :裝置樹可以像標頭檔案使用,一個裝置樹檔案引用另外一個裝置樹檔案。

幾個常用的縮寫

  • DTS:是指 .dts 格式的檔案,是一種 ASII 文字格式的裝置樹描述,也是我們要編寫的裝置樹原始碼,一般一個 .dts 檔案對應一個硬體平臺,位於 Linux 原始碼的 /arch/arm/boot/dts 目錄下。
  • DTC:是指編譯裝置樹原始碼的工具,一般情況下,需要手動安裝這個編譯工具。
  • DTB:是裝置樹原始碼編譯生成的檔案。
  • .dts:裝置樹原始檔。
  • .dtsi:裝置樹標頭檔案。
  • .dtb:裝置樹可執行檔案。

8.2 裝置樹框架

裝置樹是由 一個根節點 和 多個子節點 組成。

8.2.1 裝置樹格式

8.2.1.1 DTS 檔案佈局
/dts-v1/; // 表示版本
[memory reservations] // 格式為: /memreserve/ <address> <length>;
/ {
[property definitions]
[child nodes]
};
8.2.1.2 node 格式

node為裝置樹中的基本單元。格式為:

[label:] node-name[@unit-address] {
[properties definitions]
[child nodes]
};
  • label:是節點標籤。可以省略,方便地引用 node。通常,節點標籤通常為基點名稱的縮寫,一般用於追加內容時使用。
  • node-name:節點名稱。長度為1-31個字元。可由 0-9 a-z A-Z , . _ + - 組成,且開頭只能是大小寫字母。
    • :根節點沒有節點名,使用 / 來表示。
  • @unit-address:是單元地址。@ 為分隔符。
    • :節點中 reg 屬性的第一個地址要和這個 單元地址 一致。
    • :如果節點中沒有 reg 屬性值,則可以省略該 單元地址,但此時必須保證同級別的子節點節點名唯一。反之,若同級別的子節點節點名相同,則單元地址要求不一樣。就是說 [email protected] 整體同級唯一
8.2.1.3 properties 格式

就是 naem = value

  • 格式1:(沒有值
[label:] property-name;
  • 格式2:(支援三種取值
    • arrays of cell:一個或多個 32 位資料,64 位資料使用 2 個 32 位資料表示。
    • string:字串。
    • bytestring:一個或多個字串。
[label:] property-name = value;
  • 例子1:64bit 用兩個 cell 表示,使用 尖括號
clock-frequency = <0x00000001 0x00000000>;
  • 例子2:字串,用 雙引號
compatible = "lzm-bus";
  • 例子3:位元組序列,用 中括號
local-mac-address = [00 00 12 34 56 78]; // 每個 byte 使用 2 個 16 進位制數來表示
local-mac-address = [000012345678]; // 每個 byte 使用 2 個 16 進位制數來表示
  • 例子4:各種組合,用 逗號 隔開。
example = <0x84218421 23>, "hello world";
8.2.1.4 包含 dtsi

一般裝置樹都不需要從零開始寫,只需要包含晶片廠商提供的裝置樹模板,然後再新增,修改即可。
dts 可以包含 dtsi 檔案,也可以包含 .h 檔案。.h 檔案可以定義一些巨集。

/dts-v1/;

#include <dt-bindings/input/input.h>
#include "imx6ull.dtsi"

/ {
……
};

8.2.2 修改、追加裝置樹節點

修改、追加裝置樹節點都可在檔案末尾或新檔案修改或追加。

而修改節點,可參考以下兩種方法:標籤法全路徑法
標籤法

// 方法一:在根節點之外使用標籤引用節點
&red_led
{
    status = "okay";
}

// 方法二:使用全路徑引用節點
&{/[email protected]}
{
    status = "okay";
}

全路徑法

  • 追加節點,類似新建一個簡易的裝置樹一樣。包含根節點到需要新建節點的全路徑。

8.2.3 常用屬性

在節點 {} 中包含的內容時節點屬性。這些屬性資訊就是板級硬體描述的資訊,驅動會通過核心提供的 API 去獲取這些資源資訊。

注意:節點屬性可分為 標準屬性自定義屬性,即是可以自行新增屬性。

8.2.3.1 常用標準屬性

compatible 屬性:

  • 屬性值型別:字串。雙引號。
  • compatible 表示相容。
  • 每一個代表裝置的節點都必須有一個 compatible 屬性值。
  • 由一個或多個字串組成,使用 "," 分隔開即可。
  • 如:compatible = "A", "B", "C";
  • 核心啟動時,會按順序 A、B、C 找到對應的驅動程式,與驅動中 of_match_table 中的值進行匹配,然後載入對應的驅動。
  • compatible 是查詢節點的方法之一,還可以通過 節點名節點路徑 找到指定節點。
  • compatible 建議格式:"manufacturer,model" ,即是 "廠家名,模組名"。

model 屬性:

  • 屬性值型別:字串。雙引號。
  • model 定義硬體是什麼。
    • 推薦指定裝置的製造商和型號,推薦格式 "製造商,型號"。
  • 如:model = "lzm com,IMX6U-V1";

status 屬性:

  • 屬性值型別:字串。雙引號。
  • status 表示當前裝置節點狀態,用於禁止和啟動裝置。
  • 有如下值可選:
value description
okay 裝置正常
disabled 裝置不可操作,但後面可恢復正常
fail 發生嚴重錯誤,需要修復
fail-sss 發生嚴重錯誤,需要修復。sss 表示錯誤資訊

#address-cells、#size-cells 屬性:

  • 屬性值型別:u32。尖括號。
  • #address-cells、#size-cells 是同時出現的。
    • #address-cells:表示 address 要用多少個 32 位數來表示。
    • #size-cells:表示 size 要用多少個 32 位數來表示。
  • 用於設定子節點 reg、ranges 等地址相關屬性的書寫格式。

reg 屬性:

  • 屬性值型別:地址、長度資料對。尖括號。
  • reg 就是 register,用於表示暫存器地址。
  • 用於描述一段記憶體空間。
  • reg 屬性的值是一些列的
    • 用多少個 32 位的數來表示是由其父節點的 #address-cells、#size-cells 決定的。
  • 如:
/dts-v1/; 
/ { 
    #address-cells = <1>; 
    #size-cells = <1>;  
    memory { 
        reg = <0x80000000 0x20000000>; 
    }; 
}; 

ranges 屬性:

  • 屬性值型別:任意數量的 <子地址、父地址、地址長度>編碼。尖括號。
  • 該屬性提供了子節點地址空間和父地址空間的對映(轉換)方法。
  • 如:ranges=<0x05 0x10 0x20>

name、ldevice_type 屬性:

  • 屬性值型別:字串。雙引號。
  • 過時,不建議使用
8.2.3.2 自定義屬性

名稱及內容可自定義,但是名稱不能與標準屬性重名。獲取方式,後述。

8.2.4 常用節點

根節點

  • dts 檔案中必須有一個 根節點
  • 根節點 必須有以下屬性:
    • #address-cells
    • #size-cells
    • compatible:定義一些列的字串,用於指定核心中哪個 machine_desc 可以支援本裝置。即是相容性。
    • model:表示本硬體型號。

CPU

  • 一般都在 dtsi 檔案中定義好了,不需要我們設定。

memory

  • 這個是表示板子記憶體大小,一般由開發板開發者定義的。

chosen

  • 該節點主要作用於向核心傳遞引數。如:
chosen 
{ 
    bootargs = "noinitrd root=/dev/mtdblock4 rw init=/linuxrc console=ttySAC0,115200"; 
};

aliases

  • aliases 是為了給其它節點起個別名。如:
aliases {
    can0 = &flexcan1;
    gpio0 = &gpio1;
}
  • can0 就是 flexcan1 的別名。

8.3 編譯、更換裝置樹

一般的程式猿會修改裝置樹即可,不必從零開始。

8.3.1 在核心中編譯裝置樹(推薦)

編譯時需要設定一下三個環境變數 ARCH、CROSS_COMPILE、PATH

在開發環境中進入板子對應的核心原始碼目錄,使用核心中的 makefile 即可,執行如下命令來編譯 dtb 檔案:

make dtbs V=1

上述命令是單獨編譯裝置樹。
會編譯以下裝置樹
arch/arm/Makefile 或 arch/arm/boot/Makefile 或 arch/arm/boot/dts/Makefile 等相關 Makefile 中找到 dtb-$(xxx) ,該值包含的就是要編譯的 dtb
如該檔案中巨集 dtb-$(CONFIG_SOC_XXX) 包含的 .dtb 就會被編譯出來。
如果想編譯自己的裝置樹,新增該值內容,並把自己的裝置樹放在 arch/arm/boot/dts 下即可。
具體檢視該 arch/arm/boot/Makefile 內容

8.3.2 人工編譯(不推薦)

意思是手工使用 dtc 工具直接編譯。

dtc 工具存放於核心目錄 scripts/dtc 下。
若直接使用 dtc 工具手工編譯的話,包含其它檔案時不能使用 #include,而必須使用 /include

  • 因為核心中 make dtb 時能使用 #include 是因為使用了 交叉編譯鏈

編譯、反編譯的示例命令如下,-I 指定輸入格式,-O 指定輸出格式,-o 指定輸出檔案:

./scripts/dtc/dtc -I dts -O dtb -o tmp.dtb arch/arm/boot/dts/xxx.dts  // 編譯 dts 為 dtb 
./scripts/dtc/dtc -I dtb -O dts -o tmp.dts arch/arm/boot/dts/xxx.dtb  // 反編譯 dtb 為 dts

8.3.3 更換裝置樹

一般步驟:

  • 確保好三個環境變數。
  • 在核心原始碼目錄中執行 make dtbs
  • 生成的裝置樹檔案一般儲存在核心目錄 arch/arm/boot/dts/ 下。
  • 把生成的裝置樹檔案替換到板子上。開發板使用的裝置樹一般放在 /boot/ 目錄下。
  • 若需要自定義新的裝置樹檔名稱,則修改 /boot/ 目錄下的 uEnv.txt 檔案內容。

8.3.4 檢視裝置樹

目錄 /sys/firmware/devicetree 下以目錄結構呈現的 dtb 檔案。

  • 根節點對應 base 目錄。
  • 每一個節點對應一個目錄。
  • 每一個屬性對應一個檔案。
    • 若屬性值為字串,則可以使用 cat 命令列印出來。
    • 若屬性值為數值,則可以使用 hexdump 命令列印出來。

目錄 /sys/firmware/fdt 檔案,就是 dtb 格式的裝置樹檔案。

  • 可以將其賦值出來,反編譯。

8.4 核心處理裝置樹

8.4.1 裝置樹過程

裝置樹生命過程
DTS --(PC)--> DTB --(核心)--> device_node -·(核心)·-> platform_device

流程

  1. dts 在 PC 機上被編譯為 dtb 檔案。
  2. u-bootdtb 檔案傳給核心。
  3. 核心解析 dtb 檔案,把每一個節點都轉換為 device_node 結構體。
  4. 對於某些 device_node 結構體,會被轉換為 platform_device 結構體。

對於 device_node 和 platform_device,建議去核心原始碼看看它們的成員。

8.4.2 轉換為 platform_device 的條件

  • 根節點下有 compatile 屬性的子節點。
  • 含有特定 compatile 屬性的節點的子節點。
    • 如果一個節點的 compatile 屬性是以下 4 個值之一,那麼該節點含有 compatile 屬性的 子節點也可以轉換為 platform_device
      • simple-bus
      • simple-mfd;
      • isa;
      • arm,amba-bus
  • 匯流排 I2C、SPI 節點下的子節點 不轉換platform_device
    • 某個匯流排下的子節點,不應該被轉換為 platform_device。而應該交給對應的匯流排驅動來處理。

8.5 獲取節點函式

在驅動程式中,核心載入裝置樹後。可以通過以下函式獲取到裝置樹節點中的資源資訊。
獲取節點函式及獲取節點內容函式稱為 of 函式。

8.5.1 重要結構體內容

8.5.1.1 device_node

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:節點的名字。
  • properties:連結串列,連線該節點的所有屬性。
  • parent:指向父節點。
  • child:指向子節點。
  • sibling:指向兄弟節點。
8.5.1.2 of_device_id

of_device_id 結構體如下:

/* Struct used for matching a device  */
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:連結串列,連線該節點的所有屬性。

8.5.2 據節點路徑尋找節點

of_find_node_by_path()

  • 函式原型:struct device_node *of_find_node_by_path(const char *path)
  • 原始碼路徑:核心原始碼/include/linux/of.h
  • path:節點在裝置樹中的路徑。
  • 返回值:
    • 成功:返回 device_node 結構體指標。
    • 失敗:NULL。

8.5.3 據節點型別尋找節點

of_find_node_by_type()

  • 函式原型:struct device_node *of_find_node_by_type(struct device_node *from, const char *type)
  • 原始碼路徑:核心原始碼/include/linux/of.h
  • from:指定從哪裡開始找(不包含本身),若要從根節點開始找,且包含根節點,則該值未 NULL
  • type: 要查詢節點的型別,這個型別就是 device_node->type
  • 返回值:
    • 成功:返回 device_node 結構體指標。
    • 失敗:NULL。

8.5.4 據節點型別和compatible屬性尋找節點

of_find_compatible_node()

  • 函式原型:struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compatible)
  • 原始碼路徑:核心原始碼/include/linux/of.h
  • from:指定從哪裡開始找(不包含本身),若要從根節點開始找,且包含根節點,則該值未 NULL
  • type: 要查詢節點的型別,這個型別就是 device_node->type
  • compatible:需要查詢的節點的 compatible 屬性。
  • 返回值:
    • 成功:返回 device_node 結構體指標。
    • 失敗:NULL。

8.5.5 據匹配表尋找節點

of_find_matching_node_and_match()

  • 函式原型:struct inline device_node *of_find_matching_node_and_match(struct device_node *from, const struct of_device_id *matches, const struct of_device_id **match)
  • 原始碼路徑:核心原始碼/include/linux/of.h
  • from:指定從哪裡開始找(不包含本身),若要從根節點開始找,且包含根節點,則該值未 NULL
  • matchesof_device_id 匹配表,也就是在此匹配表裡面查詢節點。
  • match:找到的匹配的 of_device_id
  • 返回值:
    • 成功:返回 device_node 結構體指標。
    • 失敗:NULL。

8.5.6 尋找父節點

of_get_parent()

  • 函式原型:struct device_node *of_get_parent(const struct device_node *node)
  • 原始碼路徑:核心原始碼/include/linux/of.h
  • node:需要查詢要查詢父節點的節點。
  • 返回值:
    • 成功:返回 device_node 結構體指標。
    • 失敗:NULL。

8.5.7 尋找子節點

of_get_child()

  • 函式原型:struct device_node *of_get_child(const struct device_node *node, struct device_node *prev)
  • 原始碼路徑:核心原始碼/include/linux/of.h
  • node:需要查詢要查詢子節點的節點。
  • prev:需要尋找的節點的前一個節點,即是本函式需要尋找 prev 節點的後一個節點。
  • 返回值:
    • 成功:返回 device_node 結構體指標。
    • 失敗:NULL。

8.6 提取節點中的屬性值

8.6.1 重要結構體內容

8.6.1.1 property 結構體

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:屬性名。
  • lenght:屬性長度。
  • value:屬性值。
  • next:下一個屬性。
8.6.1.2 resource 結構體

resource 結構體:

struct resource 
{
    resource_size_t start;
    resource_size_t end;
    const char *name;
    unsigned long flags;
    unsigned long desc;
    struct resource *parent, *sibling, *child;
};

8.6.2 查詢節點屬性值

of_find_property()

  • 函式原型:struct property *of_find_property(const struct device_node *np,const char *name,int *lenp)

  • 原始碼路徑:核心原始碼/include/linux/of.h

  • np:裝置節點。

  • name:屬性名稱。

  • lenp:實際獲得屬性值的長度(函式輸出引數)。

  • 返回值:

    • 成功:返回 property 結構體,獲取得到的屬性。
    • 失敗:返回 NULL。
  • 可以瞭解下 獲取屬性值函式 of_get_property() ,與 of_find_property() 的區別是一個返回屬性值,一個返回屬性結構體。

8.6.3 獲取整型屬性

of_property_read_u8_array

  • 以下函式分別讀取 8、16、32、64 位資料:

//8位整數讀取函式
int of_property_read_u8_array(const struct device_node *np, const char *propname, u8 *out_values, size_t sz)

//16位整數讀取函式
int of_property_read_u16_array(const struct device_node *np, const char *propname, u16 *out_values, size_t sz)

//32位整數讀取函式
int of_property_read_u32_array(const struct device_node *np, const char *propname, u32 *out_values, size_t sz)

//64位整數讀取函式
int of_property_read_u64_array(const struct device_node *np, const char *propname, u64 *out_values, size_t sz)
  • 原始碼路徑:核心原始碼/include/linux/of.h
  • np:指定裝置節點。
  • propname:哪個屬性。
  • out_values:儲存讀取到的資料(函式輸出引數)。
  • sz:設定讀取的長度。
  • 返回值:
    • 成功:0.
    • 失敗:非零值
      • -EINVAL:屬性不存在。
      • -ENODATA:沒有要讀取的資料。
      • -EOVERFLOW:屬性值列表太小。

8.6.4 簡化後的讀取整型屬性函式

of_property_read_u8

  • 其讀取長度為 1。
//8位整數讀取函式
int of_property_read_u8 (const struct device_node *np, const char *propname,u8 *out_values)

//16位整數讀取函式
int of_property_read_u16 (const struct device_node *np, const char *propname,u16 *out_values)

//32位整數讀取函式
int of_property_read_u32 (const struct device_node *np, const char *propname,u32 *out_values)

//64位整數讀取函式
int of_property_read_u64 (const struct device_node *np, const char *propname,u64 *out_values)

8.6.5 讀取字串屬性

of_property_read_string_index:(推薦

  • 函式原型:int of_property_read_string_index(const struct device_node *np,const char *propname, int index, const char **out_string)
  • 原始碼路徑:核心原始碼/include/linux/of.h
  • np:指定裝置節點。
  • propname:哪個屬性。
  • index:指定要讀取該屬性值得第幾個字串。index 從 0 開始。
  • out_string:獲取到的字串的指標(函式輸出引數)。
  • 返回:
    • 成功:0;
    • 失敗:失敗錯誤碼。

of_property_read_string:(不推薦

  • 函式原型:int of_property_read_string(const struct device_node *np,const char *propname,const char **out_string)
  • 原始碼路徑:核心原始碼/include/linux/of.h
  • 引數同上。

8.6.6 讀取 bool 型屬性函式

of_property_read_bool()

  • 函式原型:static inline bool of_property_read_bool(const struct device_node *np, const char *propname)
  • np:裝置節點。
  • propname:屬性名稱。
  • 返回值:只返回該屬性存不存在。
  • 若要讀取該屬性值,需要用到函式 of_find_property

8.6.7 記憶體對映相關 of 函式

裝置樹提供暫存器的地址段,但是一般情況下都會使用 ioremap 對映為虛擬地址使用。
of_address_to_resource 只是獲取 reg 的值,也就是暫存器值。
of_iomap 函式就是獲取 reg 屬性值&指定哪一段記憶體&對映為虛擬地址。

of_address_to_resource

  • 函式原型:int of_address_to_resource(struct device_node *dev, int index, struct resource *r)
  • 原始碼路徑:核心原始碼/drivers/of/address.c
  • np:裝置節點。
  • index:指定對映那一段記憶體。通常情況下,reg 屬性包含多段。標號從 0 開始。
  • rresource 結構體,得到的地址資訊(函式輸出引數)。
  • 返回:
    • 成功:0;
    • 失敗:失敗錯誤碼。

of_iomap

  • 函式原型:void __iomem *of_iomap(struct device_node *np, int index)
  • 原始碼路徑:核心原始碼/include/linux/of.h
  • np:裝置節點。
  • index:指定對映那一段記憶體。通常情況下,reg 屬性包含多段。標號從 0 開始。
  • 返回:
    • 成功:轉換後的地址。
    • 失敗:NULL。

出處