デバイストリートの理解

エムベデッド分野において、ハードウェアの多様性が課題となるが、多くのインターフェースは標準化されている。以前は各ベンダーが他社のコードをコピーすることでドライバ開発を行っていたが、大量の冗長コードを生み出していた。Linuxカーネルではこの問題を解決するために、共通ライブラリとして抽象化し、デバイスが「何のデバイスか」を指定するだけで、カーネルが適切なドライバを自動選択できるようにした。これにより品質の均一性と管理の容易さが向上した。

デバイストリートに関与する主なファイルはDTS(Device Tree Source)、DTSI(Device Tree Include)、DTB(Device Tree Blob)である。基本的なフローは、DTSを編集し(DTSIは共通データを含むヘッダーファイルとして利用される)、DTC(Device Tree Compiler)でDTBに変換し、ブートローダー経由でカーネルに渡すというものだ。

DTSは特定のハードウェアを記述し、DTSIは共有可能なハードウェア情報を含む。DTBはコンパイル後のバイナリファイルである。以下はDTSの例:

/dts-v1/;

/ {
    compatible = "custom_board";
    model = "Custom Board Design";

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;
        cpu@0 {
            device_type = "cpu";
            compatible = "arm,cortex-a9";
            reg = <0x0>;
        };
    };

    memory {
        device_type = "memory";
        reg = <0x90000000 0x8000000>;
    };

    serial@0 {
        compatible = "st,stm32-usart";
        reg = <0x40011000 0x400>;
        interrupt-parent = <&gic>;
        interrupts = <123 1>;
    };
};

この例では、compatibleフィールドがデバイスの識別子として使用され、ドライバとのマッチングに使われる。regはレジスタのベースアドレスとサイズを示し、interruptsは割り込み番号を、interrupt-parentは割り込みコントローラーを指定する。#address-cells#size-cellsはサブノードのアドレスとサイズの表現形式を定義する。

ドライバ側では以下のようにマッチングが行われる:

static const struct of_device_id uart_match[] = {
    { .compatible = "st,stm32-usart" },
    { }
};
MODULE_DEVICE_TABLE(of, uart_match);

static int uart_probe(struct platform_device *pdev) {
    struct resource *res;
    void __iomem *base;

    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res)
        return -ENODEV;

    base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(base))
        return PTR_ERR(base);

    // UART初期化処理とドライバ登録

    return 0;
}

static struct platform_driver uart_driver = {
    .probe = uart_probe,
    .driver = {
        .name = "stm32-uart",
        .of_match_table = uart_match,
    },
};

module_platform_driver(uart_driver);

このように、DTSに記載されたcompatibleフィールドとドライバコードのof_match_tableが一致すれば、デバイスの初期化が開始される。

デバイストリートの構文では、ノードとプロパティの2種類がある。以下はノードの例:

uart1: serial@40020000 {
    compatible = "mychip,uart-v2";
    reg = <0x40020000 0x2000>;
    interrupts = <6>;
    status = "disabled";
};

ノードの形式は[ラベル]: ノード名[@ユニットアドレス] { ... }となる。ラベルはグローバルユニークでなければならないが、ノード名は重複可能である。例えば、&uart1 { status = "enabled"; };のように、既存ノードのプロパティを変更できる。

プロパティの例として、compatible = "mychip,uart-v2";があり、文字列は""、数値は<>、バイト列は[]で表現される。これらの型はカンマで区切って複数指定できる。

デバイストリートにはオーバーレイ(overlay)という機能があり、既存のDTBに追加情報を合成する。以下はオーバーレイの作成手順:

dtc -I dts -O dtb -o base.dtb base.dts
dtc -I dts -O dtb -o overlay.dtb overlay.dts
fdtoverlay -i base.dtb -o merged.dtb overlay.dtb

よくある質問への回答:

  1. DTSは単なる設定ファイルであり、実際のドライバはカーネルまたはベンダー固有のコードに存在する。
  2. ブートローダーとカーネルは同じDTBを共有するが、ブートローダーがDTBをメモリに読み込み、カーネルにアドレスを渡すことで実現される。
  3. DTSの記述規則は一貫しておらず、各ドライバが必要とする情報に依存する。
  4. 既存デバイスを追加するには、DTSファイルを編集するだけで済む。
  5. 新規デバイスを追加するには、ドライバの実装とDTSの更新が必須となる。

参考資料:

  • Linuxカーネルにおけるデバイストリートの仕組み
  • DTSによるデバイス記述の詳細(CSDNブログ)

タグ: linux Device Tree Embedded Systems DTS DTB

7月25日 20:42 投稿