【ドライバ連携編】Linux/ZephyrのDevicetreeをC言語コードから読み解く&オーバーレイの仕組み

この記事は約7分で読めます。

これまでの記事を通して、Devicetree(DTS)の仕組みや、JSONに似たツリー構造の書き方、アドレスやピンマルチプレクス(pinctrl)といった実践的な構文について解説してきました。

しかし、DTSファイルを書いただけではハードウェアは動きません。OS(LinuxやZephyr)のカーネル内に存在する「デバイスドライバ(C言語コード)」が、Devicetreeの情報を読み取って初めて動作します。

今回は、DTSに書いた情報がドライバのCコードとどうやって紐付くのか、そして後付けでハードウェア構成を変更するデバイスツリーオーバーレイ(DTO)の仕組みについて解説します。

1. どうやってドライバとDTSが紐付くのか?(Probeの仕組み)

LinuxでもZephyrでも、Devicetreeに記述されたノードとデバイスドライバを紐付ける一番の鍵は compatible プロパティです。

OSの起動時、カーネルは読み込んだDevicetree(DTB)の各ノードをスキャンし、それに一致する compatible 文字列を持つドライバを探して、初期化関数(Probe関数)を呼び出します。

Linuxドライバでの紐付け例

LinuxカーネルのCソースコードでは、of_device_id 構造体の配列を使って、自分が対応する compatible 文字列をカーネルに登録します。

/* ドライバ側のCコード (Linux) */
#include <linux/of.h>
#include <linux/platform_device.h>

/* このドライバが対応するcompatible文字列のリスト */
static const struct of_device_id my_custom_of_match[] = {
    { .compatible = "vendor,my-custom-sensor", },
    { /* 終端記号 */ }
};
MODULE_DEVICE_TABLE(of, my_custom_of_match);

static struct platform_driver my_custom_driver = {
    .probe = my_custom_probe,  /* 紐付いた時に呼ばれる関数 */
    .driver = {
        .name = "my_custom_driver",
        .of_match_table = my_custom_of_match, /* ここでDTSとの照合ルールを渡す */
    },
};

Zephyr OSでの紐付け例

Zephyrでは、マクロを駆使してコンパイル時にドライバとノードを静的に紐付けます。メモリのオーバーヘッドを極限まで減らすためのRTOSならではのアプローチです。

/* ドライバ側のCコード (Zephyr) */
#define DT_DRV_COMPAT vendor_my_custom_sensor

#include <zephyr/device.h>
#include <zephyr/devicetree.h>

/* 初期化関数 */
static int my_custom_init(const struct device *dev) {
    /* ... 初期化処理 ... */
    return 0;
}

/* DT_DRV_COMPATに一致するノードごとに初期化関数を登録するマクロ */
DEVICE_DT_INST_DEFINE(0, my_custom_init, NULL, NULL, NULL,
                      POST_KERNEL, CONFIG_SENSOR_INIT_PRIORITY, NULL);

どちらのOSも、「DTS側の compatible」と「Cソースコード側の compatible」の完全一致をトリガーとして動作を開始していることが分かります。

2. C言語コードの中でプロパティの値を読み取る方法

初期化関数(Probe関数)が呼ばれた後、ドライバは自分が制御するデバイスのレジスタアドレス(reg)や割り込み番号(interrupts)を取得する必要があります。

Linuxの場合:of_* APIを使用

Linuxでは of_(Open Firmwareの略。Devicetreeの源流)から始まる関数群を使ってノードのプロパティを読み取ります。

static int my_custom_probe(struct platform_device *pdev) {
    struct device_node *np = pdev->dev.of_node;
    u32 clock_speed;

    /* 例: "clock-frequency" プロパティの数値を読み取る */
    if (of_property_read_u32(np, "clock-frequency", &clock_speed)) {
        dev_err(&pdev->dev, "クロック速度がDTSに定義されていません\n");
        return -EINVAL;
    }
    
    dev_info(&pdev->dev, "設定されたクロック: %d Hz\n", clock_speed);
    return 0;
}

Zephyrの場合:DT_* マクロを使用

Zephyrでは実行時のパース処理を省くため、Cのプリプロセッサマクロを使って、ビルド時にDTSの値をCの定数に変換します。

/* DTS内で label="my_sensor" となっているノードのプロパティを取得する例 */
#define MY_SENSOR_NODE DT_NODELABEL(my_sensor)

/* "clock-frequency"プロパティの値をCの定数として取得 */
#define CLOCK_FREQ DT_PROP(MY_SENSOR_NODE, clock_frequency)

static int my_custom_init(const struct device *dev) {
    printk("設定されたクロック: %d Hz\n", CLOCK_FREQ);
    return 0;
}

3. デバイスツリーオーバーレイ(DTO)とは?

現代の組み込み開発で非常に重要になる「デバイスツリーオーバーレイ(Device Tree Overlay = DTO)」について触れておきます。

拡張ボード(Raspberry PiのHATや、評価ボードのシールドなど)を接続した場合、ベースとなるSoCのハードウェア構成自体は変わりませんが、I2CやSPIの先に新しいセンサーが追加された状態になります。

このとき、大元の DTB 全体を再コンパイルするのではなく、差分情報だけを書いた .dtbo(Overlay)ファイルを実行時やブートローダで動的に後乗せ(パッチ当て)する仕組みがDTOです。

オーバーレイ(.overlay / .dtso)の記述例:

/dts-v1/ /plugin/;

/* 追加する内容(ターゲットとなるノードを指定して上書きする) */
&i2c1 {
    status = "okay";

    /* I2C1バスに新しいセンサーを後付けする */
    new_sensor@76 {
        compatible = "bosch,bme280";
        reg = <0x76>;
    };
};

Zephyr OSでもこのオーバーレイの仕組みは強力にサポートされており、プロジェクトディレクトリに app.overlay というファイルを置くだけで、大元のボード定義(.dts)を破壊することなく、アプリケーション固有のピン変更やセンサー追加を簡単に実現できます。

まとめ

今回は、DevicetreeがC言語のデバイスドライバとどのように連携するのか、そして動的に構成を変更するオーバーレイ(DTO)の仕組みについて解説しました。

  • ドライバとの紐付け: compatible 文字列の完全一致をトリガーに、OSが自動的にドライバの初期化(Probe)を実行する。
  • 値の読み取り: Linuxは of_ 関数群(実行時)、Zephyrは DT_ マクロ群(ビルド時)を使ってDTSのプロパティ値を取得する。
  • オーバーレイ(DTO): ベースのハードウェア定義を変更せず、拡張ボードなどの追加・変更差分だけを後乗せする強力な仕組み。

これで「なぜ必要なのか(仕組み)」「どう書くのか(構文)」「どう使われるのか(ドライバ連携)」という、Devicetreeの一連のフローが見えてきたかと思います。

Devicetreeは組み込みLinuxやZephyr開発における共通言語です。この強力なデータ構造を味方につけて、よりスムーズなハードウェア制御を目指しましょう!

コメント

タイトルとURLをコピーしました