Raspberry PiでDevice Treeを読んでみる


Raspberry Pi 4を使って、Device Treeを読んでみます。

このページではRaspberry Pi 4を題材に、Device Treeの読み方を3種類紹介します。1つ目はLinux kernelのソースツリー上で読む方法、2つ目はビルドしたバイナリー (Device Tree blob) から読む方法、3つ目はOS起動後のランタイムで読む方法です。2つ目と3つ目の方法はどちらも一旦Device Treeソースに戻して読む方法を紹介します。

なお、筆者が使用している環境はRaspberry Pi 4 Model BのRAM 8GBモデル、Raspberry Pi OSの64-bit版です。Raspberry Pi OSで使用しているLinux kernelのソースコードは https://github.com/raspberrypi/linux からダウンロードできます。

参考:
Device Tree (eLinux Wiki)
Linux and the Deivce Tree (The Linux Kernel Documentation)

ソースツリー上で読んでみる

Device Treeは、システムに搭載されたデバイスのうち、OSが検出する仕組みを持っていないデバイスを記述しておく手段の一つです。主に組込み向けのLinuxで採用されていて、Raspberry Pi OSでもDevice Treeを採用しています。

Linux kernelのソースツリー上の、Device Treeの格納場所は arch/<各アーキテクチャ名>/boot/dts/ の下です。Raspberry Pi OSは正式版の32-bitと、評価版の64-bitのものがありますが、それぞれ'arm'と'arm64'でアーキテクチャのディレクトリが別になっています。

まずは64-bit版のDevice Treeソースを見てみます。ファイルはRaspberry Piが採用しているチップのベンダーであるBroadcom用のディレクトリ arch/arm64/boot/dts/broadcom ディレクトリの下にあります。ここには、Coretex-a53を採用して64-bit動作可能になったRasberry Pi 2-B (v1.2)、3-A+、3-Bおよび3-B+、Coretex-a57を採用した4-Bや、新たに発表されているRaspberry Pi 400のものが格納されています。
ちなみに、32-bit版の arch/arm/boot/dts の下はチップベンダー毎のディレクトリ階層になっておらず、ちょっとカオスな状態になっています。ファイル名の接頭辞 (broadcomなら'bcmxxx')を見ると判別できます。

Raspberry Pi 4用のarch/arm64/boot/dts/broadcom/bcm2711-rpi-4.dtsの中を見てみます。

#include "../../../../arm/boot/dts/bcm2711-rpi-4-b.dts"

1行だけです。LinuxのDevice Treeソースでは、Cソースと同様に#includeを使って他のDevice Treeソースやヘッダファイルをインクルードしたり、マクロの定数を使用したりできます。ここでは32-bit版の"bcm2711-rpi-4-b.dts"をインクルードしているだけですが、これは32-bit版のソースと共通化するための施策だと思います。

ということで、64-bit版向けにビルドする場合であっても、Device Treeのソースは32-bit版の物だけを見ればよさそうです。arch/arm/boot/dts/bcm2711-rpi-4-b.dtsを見ていきます。

// SPDX-License-Identifier: GPL-2.0
/dts-v1/;
#include "bcm2711.dtsi"
#include "bcm2835-rpi.dtsi"

#include <dt-bindings/reset/raspberrypi,firmware-reset.h>

/ {
    compatible = "raspberrypi,4-model-b", "brcm,bcm2711";
    model = "Raspberry Pi 4 Model B";
...

このファイルでも、別のファイルをインクルードしています。

Device Treeの基本的な書式

Device Treeの書式について、詳しくはDevice Tree Usage (eLinux Wiki)などを参考にしてください。ここではざっくりと眺める上で必要な最低限のことだけを記載します。

Device Treeの基本的な構造は単純です。ノードと呼ぶ中括弧{}で囲われた中に、キー・バリュー型のプロパティが列挙されています。

ノード名 {
    プロパティ名 = プロパティの値;
    ...
};

ノードは階層構造を持たせることもできます。一番外側のノードはルートノード/と呼びます。

/ {
  親ノード {
    子ノード1 {
    };
    子ノード2 {
    };
  };
};

オーバーライド

また、ルートノード/の外側に、&で始まるノードが書かれている場合があります。これは既に定義済みのノードへの参照です。これまでに見た通り、LinuxのDevice Treeソースでは他ファイルをインクルードすることができますが、インクルードされたファイルを追っていくと、同じノード名・同じプロパティ名が定義されていることに気がつくかと思います。このとき、そのノードのプロパティは後から記述された値によってオーバーライドされます。

/{
    あるノード {
        プロパティ名 = プロパティ値;
    };
    ...
};
...
&あるノード {
        プロパティ名 = オーバーライドする値; 
};

例として、LEDのノードを見てみます。ノード名はledsです。
ledsノードはbcm2711-rpi-4-b.dtsの2箇所 (ここここ)、bcm2711-rpi-4-b.dtsがインクルードしているbcm2835-rpi.dtsiに1箇所 (ここ) 記述があります。インクルードの順序関係から、最初に解釈されるノード記述はbcm2835-rpi.dtsi、それをbcm2711-rpi-4-b.dtsの記述でオーバーライドした後、さらに同ファイルの&leds {...};の値がオーバーライドしています。

bcm2835-rpi.dtsi

    leds {
        compatible = "gpio-leds";

        act {
            label = "ACT";
            default-state = "keep";
            linux,default-trigger = "heartbeat";
        };
    };

bcm2711-rpi-4-b.dts

    leds {
        act {
            gpios = <&gpio 42 GPIO_ACTIVE_HIGH>;
        };

        pwr {
            label = "PWR";
            gpios = <&expgpio 2 GPIO_ACTIVE_LOW>;
            default-state = "keep";
            linux,default-trigger = "default-on";
        };
    };
...
&leds {
    act_led: act {
        label = "led0";
        linux,default-trigger = "mmc0";
        gpios = <&gpio 42 GPIO_ACTIVE_HIGH>;
    };

    pwr_led: pwr {
        label = "led1";
        linux,default-trigger = "default-on";
        gpios = <&expgpio 2 GPIO_ACTIVE_LOW>;
    };
};

ビルドされたときには、ledsの子ノードであるactノードのプロパティlabelの値は"led0"、pwrノードのプロパティlabelの値は"led1"になります。

Note: 同じノード名のを記述した場合、後から書いたものでプロパティがオーバーライドされる。例えば同系統のボードで搭載デバイスに共通するものがある場合はdtsiにし、各ボードのdtsからインクルードしたうえで細かい差異はオーバーライドすることでDevice Treeを構造化できる。

バイナリーから読んでみる

Raspberry Pi上でLinux kernelをビルドする手順や必要なパッケージについては、公式ページのビルド手順を参考にしてください。Linuxソースツリー上でDevice Treeのみをコンパイルするには、ソースツリーのトップディレクトリ下でmake dtbsを叩きます。

$ export ARCH=arm64       # クロス環境・Raspberry Pi OSの32-bit版で行う場合は実行する
$ make bcm2711_defconfig  # このコマンドはまだ実行していなければ実行する
$ make dtbs

arch/arm64/boot/dts/broadcom/ ディレクトリおよびarch/arm64/boot/dts/overlays/ ディレクトリのソースがビルドされ、Device Treeのバイナリー (Device Tree blob)が生成されます。このうち、Raspberry Pi 4で使用するのはarch/arm64/boot/dts/broadcom/bcm2711-rpi-4-b.dtbです。これを/bootの下におけばLinux起動時にブートローダがロードしてくれるのですが、今回はこれをソースに戻して読めるようにしてみます。

Device Treeをソースに戻すにはDevice Treeコンパイラdtcを使います。dtcはソースからバイナリー、バイナリーからソースのどちらの変換もできるツールです。詳しい使い方はman dtcを参照してください。主に使うオプションは下記です。

使い方: dtc <オプション> <入力ソース>
-I <入力形式>
入力ソースの形式を指定する。下記3つが選択可能。
dts -- Device Treeソース形式 (テキスト形式)
dtb -- Device Tree blob 形式 (バイナリー形式)
fs -- /proc/device-tree スタイルのディレクトリ (※後述)
-O <出力形式>
コンパイル後の形式を指定する。下記3つが選択可能。
dts, dtb, asm (アセンブラ命令の形式)
-s
ノード、プロパティ名をソートして出力する

なお、入力ソースは引数で指定するか、パイプを使って標準入力で入力できます。出力は-oでファイル名を指定しない限りは標準出力になります。

Device Treeバイナリーからソースへと変換する例:

$ dtc -I dtb -O dts -s -q arch/arm64/boot/dts/broadcom/bcm2711-rpi-4-b.dtb | head
/dts-v1/;

/memreserve/    0x0000000000000000 0x0000000000001000;
/ {
    #address-cells = < 0x02 >;
    #size-cells = < 0x01 >;
    compatible = "raspberrypi,4-model-b\0brcm,bcm2711";
    interrupt-parent = < 0x01 >;
    model = "Raspberry Pi 4 Model B";

…私の環境だとなんかWarningがいっぱい出ましたが、とりあえず無視します。-qオプションをつけるとWarningが抑止されます。

ランタイムで読んでみる

OS起動後に、どのようなDevice Treeで起動したのかを確認します。
dtcのオプションにある通り、/proc/device-treeなどのファイルシステムを入力としてDevice Treeソースへ変換することができます。/proc/device-treeは、OSが読み込んだDevice Treeの情報を確認できるようにしているAPIです。

/proc/device-treeの実体は /sys/firmware/devicetree/base です。

$ ls -l /proc/device-tree
lrwxrwxrwx 1 root root 29 Mar 13 02:49 /proc/device-tree -> /sys/firmware/devicetree/base

ディレクトリの構造を見てみると、Device Treeのノードとプロパティの構造に対応した構造になっているのがわかるかと思います。例として、ledsディレクトリの下はこのようになっています。

$ cd /sys/firmware/devicetree/base/leds/
$ tree
.
├── act
│   ├── default-state
│   ├── gpios
│   ├── label
│   ├── linux,default-trigger
│   ├── name
│   └── phandle
├── compatible
├── name
├── phandle
└── pwr
    ├── default-state
    ├── gpios
    ├── label
    ├── linux,default-trigger
    ├── name
    └── phandle

/sys/firmware/devicetree/base/leds/act/labelの値を見てみると、Device Treeソースので設定されたlabelプロパティの値"led0"が読めることがわかります。

$ cat act/label
led0

上記の例は文字列のプロパティ値のためcatコマンドで読むことができますが、整数のプロパティはhexdumpを使えば16進の値として読むことができます。

dtcで/proc/device-treeをDevice Treeソースに戻してみます。-I fsオプションを付け、引数に/proc/device-treeを指定して実行します。コンパイルに成功すると、バイナリーからコンパイルしたときと同様に、Device Treeソースとして読めるようになります。

/proc/device-treeをDevice Treeソースに戻す例

$ dtc -I fs -O dts -s -q /proc/device-tree | head
/dts-v1/;

/ {
    #address-cells = < 0x02 >;
    #size-cells = < 0x01 >;
    compatible = "raspberrypi,4-model-b\0brcm,bcm2711";
    interrupt-parent = < 0x01 >;
    memreserve = < 0x3b400000 0x4c00000 >;
    model = "Raspberry Pi 4 Model B Rev 1.4";
    serial-number = "10000000ff852c78";

例えば組込み機器開発中のデバッグ中に「あれ、なんか挙動がおかしいけどDevice Tree間違えたんだろか?」という状況で、起動に実際に使われたプロパティを確認したいこともあるかと思います。そんな場合でも、ランタイムでDevice Treeを読む方法を知っていると役に立つかと思います。

余談

ランタイムで読まないといけないケース、というのがあり、例えばRaspberry Pi 4の一部のプロパティ値は、ブートローダによって書き換えられてしまうものがあるようです。Device Treeソースを見てみると、memoryノードに下記のようなコメントが付いています。

    /* Will be filled by the bootloader */
    memory@0 {
        device_type = "memory";
        reg = <0 0 0>;
    };

memoryノードは搭載されているRAMのアドレスやサイズを記載しておくノードですが、上記の通り中身が全て0です。Raspberry Pi 4はRAM 2GB/4GB/8GBのモデルがあり、1つのDevice Treeで全てのモデルをカバーすることができません。Raspberry Piのブートローダはクローズドソースなので中で何をやっているのかは確認できませんが、上記コメントから察するに、おそらくブートローダが起動時にモデルの判別を行い、memoryノードのアドレスやサイズを書き換えるのでしょう。

実際に読み込まれたmemoryノードのプロパティregの値をコンパイルして確かめてみます。

$ dtc -I fs -O dts -q /proc/device-tree | grep "memory@0" -A 3
    memory@0 {
        device_type = "memory";
        reg = < 0x00 0x00 0x3b400000 0x00 0x40000000 0xbc000000 0x01 0x00 0x80000000 0x01 0x80000000 0x80000000 >;
    };

regはメモリマップドデバイスのアドレスとサイズを設定するためのプロパティで、書式はreg = <address1 length1 [address2 length2] [address3 length3] ... >のように、アドレスとサイズが一対となったタプルのリストとして表現されます (書式の詳細はこちらを参照してください)。
タプルの解釈に必要な#address-cells#size-cellsの値はルートノード/をみると、

    #address-cells = <2>;
    #size-cells = <1>;

となっているため、上記regの値より、下表のようにアドレス・サイズが設定されていることがわかります。

address length
0x00000000_00000000 0x3b400000
0x00000000_40000000 0xbc000000
0x00000001_00000000 0x80000000
0x00000001_80000000 0x80000000

設定されているRAM領域は4つあり、サイズ合計 0x1F7400000(16) Byte = 8443133952(10) Byte = 8052 MBとなり、約8GB分が設定されているようです。

他にもDevice Tree Overlayという仕組みで書き換えられるケースがあります。ブートローダによって書き換えられてしまうケースはRaspberry Piが特殊なだけな気もしますが、使用している環境によっては実際に設定される値をあらかじめ知る手段が無い場合があるので、そんなときにもランタイムでDevice Treeを読む方法が役に立つでしょう。