OS・実行環境

デバイスツリー

ハード構成をOSに伝える記述ファイル。

概要

デバイスツリー(Device Tree)は、組み込みLinuxシステムでボードのハードウェア構成をOSに記述・伝えるためのデータ構造とファイル形式です。どのペリフェラルがどのアドレスにあるか、クロック周波数・割り込み番号・GPIO番号・I2Cアドレスなどをテキスト形式(DTS: Device Tree Source)で記述し、コンパイルしたバイナリ(DTB: Device Tree Blob)をブートローダーからカーネルに渡します。

デバイスツリーが登場する以前、組み込みLinuxではボード固有の情報がカーネルのC言語ソースコード(board file)にハードコードされていました。ARM Linuxのboard fileが爆発的に増え「ARM mess問題」と呼ばれた状況を解決するため、2011年にLinus Torvaldsの要請のもとデバイスツリーへの移行が本格化しました。現在ではARMベースの組み込みLinuxで標準的に使われており、Zephyrもデバイスツリーを採用しています。

歴史・背景

デバイスツリーの起源はIBMとAppleが共同開発した「Open Firmware」(IEEE 1275)です。PowerPCマシン向けに1990年代から使われており、ハードウェア構成をOFW(Open Firmware)ツリーとして記述する仕組みが原型です。

Linuxカーネルでは2005年頃からPowerPC向けにデバイスツリー記述(.dts)が採用されていました。2011年にARMアーキテクチャでもデバイスツリーへの移行が決まり、2012〜2013年頃のカーネルではほぼ全ての主要ARMボードがデバイスツリーに対応しました。

2016年以降、Zephyr RTOSもデバイスツリーをLinuxから輸入して採用し、RTOSの世界にも広まりました。デバイスツリー仕様(Device Tree Specification)はlinuxfoundation.orgのワーキンググループが策定・管理しています。

技術仕様

ファイルの種類

拡張子説明
.dtsDevice Tree Source:ルートボード定義ファイル
.dtsiDevice Tree Source Include:共通部分を定義して複数から#includeする
.dtbDevice Tree Blob:.dtsをコンパイルしたバイナリ
.dtboDevice Tree Blob Overlay:ベースDTBに追加する差分DTB
.overlayZephyrで使うDTSオーバーレイファイル

DTS構文

/dts-v1/;                    /* DTSバージョン宣言 */

/* インクルード(SoC定義ファイル) */
#include "bcm2711.dtsi"
#include "bcm2711-rpi.dtsi"
#include <dt-bindings/gpio/gpio.h>

/* ルートノード */
/ {
    /* ボード名と互換性(文字列リスト) */
    model = "Raspberry Pi 4 Model B";
    compatible = "raspberrypi,4-model-b", "brcm,bcm2711";

    /* メモリ定義(4GB版) */
    memory@0 {
        device_type = "memory";
        reg = <0x0 0x0 0x0 0x80000000>,   /* 2GB */
              <0x1 0x80000000 0x0 0x80000000>; /* 追加2GB(アドレス拡張) */
    };

    /* I2Cに接続したBME280センサーの定義 */
    &i2c1 {
        status = "okay";
        clock-frequency = <400000>;  /* 400kHz(ファストモード) */

        bme280: bme280@76 {
            compatible = "bosch,bme280";
            reg = <0x76>;            /* I2Cアドレス */
        };
    };

    /* GPIOキー定義 */
    gpio-keys {
        compatible = "gpio-keys";

        button0: button0 {
            label = "USER_BUTTON";
            gpios = <&gpio 17 GPIO_ACTIVE_LOW>; /* GPIO17, Lowアクティブ */
            linux,code = <11>;                  /* KEY_0 */
        };
    };

    /* PWM制御のLED */
    pwm-leds {
        compatible = "pwm-leds";

        led0 {
            label = "green:act";
            pwms = <&pwm0 0 500000 0>;  /* PWM ch0, 500us周期 */
            max-brightness = <255>;
        };
    };
};

プロパティの型

node@address {
    /* 文字列 */
    compatible = "vendor,my-chip";   /* ドライバマッチングに使う */
    label = "my-uart";

    /* 整数(32bit) */
    reg = <0x40000000 0x1000>;       /* アドレス・サイズ(16進数) */
    clock-frequency = <115200>;      /* 10進数でも可 */

    /* バイト列 */
    mac-address = [01 02 03 04 05 06];

    /* ブール(存在=true) */
    interrupt-controller;

    /* phandle参照(別ノードへの参照) */
    clocks = <&rcc STM32_CLOCK_BUS_APB1 0x00200000>;

    /* ステータス */
    status = "okay";     /* 有効 */
    /* status = "disabled"; 無効 */
};

interrupt指定

uart0: uart@40011000 {
    compatible = "arm,pl011";
    reg = <0x40011000 0x1000>;
    
    /* インタラプトコントローラへの参照 */
    interrupt-parent = <&gic>;
    /* GIC SPI割り込み番号37、レベルトリガ(高) */
    interrupts = <GIC_SPI 37 IRQ_TYPE_LEVEL_HIGH>;
    
    clocks = <&rcc CLK_UART0>;
    status = "okay";
};

動作原理

DTSからDTBへのコンパイル

.dtsファイルはDTCコンパイラ(Device Tree Compiler)で.dtb(バイナリ)に変換されます:

# DTSをDTBにコンパイル
dtc -I dts -O dtb -o myboard.dtb myboard.dts

# DTBをDTSに逆コンパイル(解析用)
dtc -I dtb -O dts -o myboard.dts myboard.dtb

# Linuxカーネルビルド時に自動でDTBが生成される
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- dtbs
# → arch/arm64/boot/dts/broadcom/bcm2711-rpi-4-b.dtb 等が生成

ブートローダーからカーネルへの受け渡し

U-Bootは起動時にDTBのアドレスをカーネルに渡します(ARM64の場合、レジスタx0=DTBアドレス):

# U-Boot シェルでの手動起動例
fatload mmc 0:1 0x80000000 Image           # カーネル
fatload mmc 0:1 0x83000000 bcm2711-rpi-4-b.dtb  # DTB
booti 0x80000000 - 0x83000000              # カーネル起動("-"はinitramfs省略)

カーネルでのDTB解析

Linuxカーネルは起動時にDTBを解析してデバイスを認識します。compatibleプロパティを使ってドライバとデバイスをマッチングします:

/* ドライバのマッチングテーブル(カーネル内のI2Cドライバ例) */
static const struct of_device_id bme280_of_match[] = {
    { .compatible = "bosch,bme280", },
    { .compatible = "bosch,bmp280", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, bme280_of_match);

/* プローブ関数(デバイスが認識されたときに呼ばれる) */
static int bme280_probe(struct i2c_client *client,
                        const struct i2c_device_id *id) {
    /* DTから設定値を取得 */
    int irq = of_irq_get(client->dev.of_node, 0);
    /* ... */
}

DTBオーバーレイ(Raspberry Piのoverlayシステム)

Raspberry Piではベースのシステム設定(bcm2711-rpi-4-b.dtb)に対して、追加の.dtboオーバーレイを適用することで設定を拡張できます:

# /boot/config.txt(Raspberry Pi のブートローダー設定)
dtparam=i2c_arm=on       # I2C有効化
dtoverlay=i2c-rtc,ds3231 # RTCモジュール追加
dtoverlay=dwc2           # USB OTGモード

用途・ユースケース

組み込みLinuxのSBC・カスタムボード

Raspberry Pi・BeagleBone・ODROID・産業用SBCなど、ARMベースのボードでは必ずDTBが使われます。カスタム設計のボードではボード固有の.dtsを作成してLinuxをポーティングします。

Zephyrでのハードウェア定義

ZephyrはデバイスツリーをLinuxから採用し、MCUのペリフェラル構成を定義します。アプリケーションは.overlayファイルでベースボード定義をカスタマイズします。

Yocto / BuildrootでのBSP管理

YoctoBuildrootのBSPレイヤーにはボードのDTSファイルが含まれており、製品カスタマイズのDTSオーバーレイを追加できます。

実装・開発のポイント

よくある間違いと確認方法

# 起動中にDTBが読まれているか確認
ls /sys/firmware/devicetree/base/

# 特定ノードのプロパティ確認
cat /sys/firmware/devicetree/base/i2c@7e804000/status
# → okay

# ドライバがデバイスを認識しているか確認
ls /sys/bus/i2c/devices/
# → 1-0076(I2Cバス1のアドレス0x76)

# DTBの内容を全展開
dtc -I fs /sys/firmware/devicetree/base/ 2>/dev/null | less

カスタムDTSの作成

/* my-product.dts */
/dts-v1/;

/* ベースボードのDTSをインクルード */
#include "stm32mp157c-dk2.dts"

/* カスタム追加 */
&i2c4 {
    my-sensor@48 {
        compatible = "myvendor,my-sensor";
        reg = <0x48>;
        vdd-supply = <&v3v3>;
    };
};

/* 使用しないデバイスを無効化 */
&sdmmc2 {
    status = "disabled";
};

Zephyrでのオーバーレイ活用

/* app.overlay(Zephyrアプリ用カスタマイズ) */
&i2c0 {
    bme280@76 {
        compatible = "bosch,bme280";
        reg = <0x76>;
    };
};

&uart0 {
    current-speed = <9600>;  /* デフォルトの115200から変更 */
};

他技術との比較

デバイスツリー vs ACPIテーブル(x86系)

x86系PCでは、デバイスツリーの代わりにACPI(Advanced Configuration and Power Interface)テーブルが使われます。ACPIはより高機能(AMLという独自言語でドライバロジックも記述可能)ですが、組み込みシステムには過剰です。ARM ServerではACPIとDTBの両方が使えます。

デバイスツリー vs Board File

デバイスツリー登場以前のLinuxは、ボード固有の情報をC言語で記述した「board file」(arch/arm/mach-*/board-*.c)に書いていました。board fileはカーネルソースに取り込む必要があったため、変更のたびにカーネルの再ビルドが必要でした。デバイスツリーによってカーネルとボード設定が分離され、同じカーネルイメージを複数のボードで共有できるようになりました。

関連用語

参考リンク