デバイスツリーとは何か
August 7, 2026 · – 訪問者
組み込みボードで Linux を立ち上げようとしたことがあれば、遅かれ早かれ .dts で終わるファイルに出会う。たいていは最悪のタイミングだ。ボードが起動しない、画面には何も出ない、そして誰かに「デバイスツリーを見てみろ」と言われる。この記事は、そのファイルが何なのか、なぜ存在するのか、そして実際そこで何を探しているのかについてのものだ。
何を解決するための仕組みか
デスクトップでは、カーネルはハードウェアをほぼ自力で見つけられる。PCI と USB は正式に仕様化された、列挙可能なバスだからだ(1)。デバイスはベンダ ID とプロダクト ID で自分を名乗り、ドライバは対応する ID をテーブルに並べ、カーネルはその項目に一致したデバイスごとにそのドライバの probe() を呼ぶ(2)。USB 側も同じ仕組みが独自の ID テーブルで動いている(3)。誰も事前に誰かへ何かを伝える必要がない。
組み込みボードにその贅沢はない。カーネルのドキュメントはこの線引きをはっきり書いている。SoC に統合された周辺機能は最小限のインフラしか持たないバスにぶら下がっており、「PCI や USB のような、大規模で正式に仕様化されたものとは対照的に」、それらのデバイスに共通するのは CPU バスから直接アドレスされることだ(1)。I²C センサや SPI にぶら下がったディスプレイコントローラは自分を名乗らない。ただそこに、そのアドレスに、その割り込み線に配線された状態で座っているだけだ。カーネルがそれを知る唯一の方法は、誰かが教えることである。
長いあいだ、その「誰か」はコードそのものだった。ボードごとにカーネルツリー内にファイルがあり、どのデバイスがどこに配線されているかを C で記述していた——カーネルは今もこの経路を文書化している。ボード固有の初期化コードがデバイスを一つずつ登録するやり方だ(1)。ARM ではこの種のファイルが増えすぎ、2011 年に Linus Torvalds がメールで状況を厳しく批判し、ARM のコードは立ち行かない方向へ向かっていると述べた。採用された出口がデバイスツリーだった。
考え方
デバイスツリーは、ハードウェアがどう配線されているかを記述したデータファイルである。コードではない——実行されないし、カーネルにコンパイルされて入るわけでもない。記述だ。このアドレスにコントローラがあり、この割り込み線を使い、このクロックから供給を受け、その下にこれらのデバイスがぶら下がっている、という。
フォーマット自体は正式な仕様に基づいている(4)。ツリーと呼ばれるのは、本当にツリーだからだ。ルートノードから始まり、そこからバスが枝分かれし、そのバスに接続されたデバイスがさらに枝分かれする——ハードウェアの物理的な接続をそのまま映している。
作り物の例より、本物を見よう。以下のノードは TI の J722S/AM67A ファミリ向け U-Boot ツリーにある k3-am62p-main.dtsi からのもので、AM67A ボードが起動時に実際に読む記述そのものだ。
main_i2c0: i2c@20000000 {
compatible = "ti,am64-i2c", "ti,omap4-i2c";
reg = <0x00 0x20000000 0x00 0x100>;
interrupts = <GIC_SPI 161 IRQ_TYPE_LEVEL_HIGH>;
#address-cells = <1>;
#size-cells = <0>;
power-domains = <&k3_pds 102 TI_SCI_PD_EXCLUSIVE>;
clocks = <&k3_clks 102 2>;
clock-names = "fck";
status = "disabled";
};
ここでは一行ごとに意味がある。reg はメモリマップ上のコントローラのアドレスを与える——0x20000000、長さ 0x100。アドレスが数値のペアで書かれているのはアドレス空間が 64 ビットだからで、最初の数値が上位 32 ビットを担う。interrupts はどの割り込み線を使うかを示す。GIC_SPI や IRQ_TYPE_LEVEL_HIGH といった名前はインクルードされたヘッダから来ている。つまりデバイスツリーのソースは素のデータでありながら、C プリプロセッサを通っているということだ。
注目に値するのは compatible が二つの値を持っていることだ。"ti,am64-i2c" と "ti,omap4-i2c"。このリストは具体的なものから一般的なものへと読まれる。カーネルはまず am64 専用のドライバを探し、なければより古く一般的な omap4 ドライバへフォールバックする(5)。おかげで新しいチップも、専用ドライバを誰かが書かなくても動きうる。
clocks と clock-names は、デバイスがどこからクロックを得るかを示す。&k3_clks のデバイス 102 の出力 2 であり、ドライバは "fck" という名前でそれを見つける。power-domains も同じ論理で電源ドメインを結びつける。これらは周波数の設定ではない——配線の記述、どのクロックがどこへ行くかである。
最後の行が重要だ。status = "disabled"。チップベンダは周辺機能を記述しつつ、無効のままにしておく。その I²C バスがあなたのボードで使われるかどうかを知りようがないからだ。有効にするのはボード自身の .dts の仕事である。J722S EVM のボードファイルではこう書かれている。
&main_i2c0 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&main_i2c0_pins_default>;
clock-frequency = <400000>;
exp1: gpio@23 {
compatible = "ti,tca6424";
reg = <0x23>;
gpio-controller;
#gpio-cells = <2>;
};
};
先頭の &main_i2c0 は上のノードへの参照だ——新しいノードを作るのではなく、既存のものに上書きする。status が有効化され、ピンが割り当てられ、速度が 400 kHz に設定され、その下に実際のデバイスが追加される。I²C バス上のアドレス 0x23 に座る TCA6424 GPIO エキスパンダだ。コントローラが #address-cells = <1> を宣言しているため、このアドレスは単一の数値で書かれる。コントローラ自身の reg とは違う点である。
これが実践におけるデバイスツリーの働き方だ。ベンダが SoC を一度記述し、あなたが自分のボードに何が繋がっているかを述べる。
本当に重要なプロパティは compatible である。カーネルがドライバとハードウェアを結びつけるために見るのがここだ。ドライバが「私は ti,tca6424 と互換だ」と言い、デバイスツリーのノードが同じ文字列を持っていれば、カーネルは両者を結びつける。だからタイプミスは静かに失敗する。文字列が一致しなければエラーは出ない——デバイスがただ現れないだけだ。
留意しておくべきこと。デバイスツリーが記述するのはハードウェアであって、ドライバの設定ではない。カーネルのバインディング記述ガイドはこの境界をとりわけ強調している(6)。実務ではこの線が絶えず曖昧になり、カーネルのメーリングリストで最も議論を呼ぶ話題でもある——「それは本当にハードウェアの性質か、それともあなたの好みか」。
ファイルたち
三つの拡張子を目にすることになり、これが実に紛らわしい(11)。
.dts はあなたが書くソース、人間が読める形式だ。.dtsi は他のファイルがインクルードできる断片で、「i」は include の i である。チップベンダは通常 SoC 全体を .dtsi に記述する。あなたは自分のボード用に短い .dts を書き、それをインクルードし、自分のボードに固有のものだけを足す。
.dtb はコンパイル後の形式——デバイスツリーブロブだ。カーネルが実際に読むのはこれである。コンパイラは dtc(7)。
dtc -I dts -O dtb -o my-board.dtb my-board.dts
逆方向にも動く。問題を追うときにこれが実に有難い。動作する .dtb があれば、開いて中を覗ける。
dtc -I dtb -O dts -o decoded.dts my-board.dtb
稼働中のシステムなら、ファイルシステムから直接デバイスツリーを読むこともできる。カーネルは実際に使っているツリーをここに晒している。
ls /proc/device-tree/
「自分が編集したファイルは、本当に読み込まれたファイルなのか」という問いへの、これが最速の答えだ。驚くほどの割合で、答えは「いいえ」である。
起動時に何が起きるか
順序はこうだ。ブートローダ——たいていは U-Boot(8)——が .dtb をメモリに読み込み、カーネルも読み込み、カーネルを起動する際にブロブのメモリアドレスをレジスタで渡す。ARM ではそのレジスタと、ブートローダが満たすべき条件がカーネルの起動ドキュメントに定められている(9)。カーネルはドライバを一つも立ち上げる前にこのツリーを読み、そこからデバイスを取り出し、compatible 文字列に従ってドライバを対応づける。
実際的な帰結として、カーネルとデバイスツリーは別々に更新できる。センサを一つ足すのにカーネルを作り直す必要はまずなく、たいていは .dtb の更新で足りる。ただしこれは両刃だ——独立しているがゆえに、ずれうる。新しいカーネルを古い .dtb で起動すれば、ボードは静かに不完全な状態で立ち上がり、その理由に辿り着くまで時間がかかる。
オーバーレイという考え方もある。既存のツリーの上に実行時に適用される断片だ(10)。Raspberry Pi に HAT を挿したときに起きているのがこれで、メインのツリーはそのままに、小さな断片が上から重ねられる。ベースボードが常に同じで、挿すモジュールだけが変わる場合に有用である。
何かが動かないとき
デバイスツリーの不具合には独特の厄介さがある。たいていエラーを出さないからだ。デバイスがただ存在しない。この順で辿ると時間の節約になる。
まずデバイスがそもそも作られたかを確認する。/proc/device-tree の下にそのノードはあるか。なければ問題はあなたが編集したファイルにはない——おそらく違う .dtb が読み込まれているか、ノードが一階層上で status = "disabled" により無効化されている。後者は日常的に起きる。ベンダの .dtsi では周辺機能はたいてい無効で届き、自分のボードの .dts で status = "okay" により一つずつ有効にすることが期待されている。
ノードはあるのにドライバが付いていないなら、compatible 文字列に目を凝らす。ドライバのソースで of_device_id テーブルを見つけ、文字単位で比較する。カンマ一つ、ハイフン一つで十分だ。
もう一つ。dtc の警告は多くのプロジェクトで無効のまま届くが、本当に有用なことを言ってくれる。アドレスと reg の食い違い、#address-cells の欠落——まさに起動時の静かな故障になる類の不具合だ。ビルド時に警告を有効にしておく価値はある。
おわりに
デバイスツリーが掴みづらいのは、それが実際何なのかを最初に誰も言わないからだ。設定ファイルのように見えるが違う。ドライバのように見えるが、それでもない。最も役に立つ捉え方は、ハードウェアとソフトウェアのあいだの契約と考えることだ。ボードがどう配線されているかを、カーネルの理解する言語で書き下したもの。
その枠に収まってしまえば、あとは自ずと続く。なぜ compatible がこれほど致命的なのか、なぜ .dtb がカーネルと別に存在するのか、なぜ何かが足りないとき最初に見るべきが /proc/device-tree なのか——すべて同じ考えから出てくる。
関連記事
- 組み込みLinuxのブートチェーン — ブートローダがカーネルへ制御を渡す仕組み
- DFUとは何か — 他に手がないときの USB 経由のファームウェア書き込み
参考文献
- Platform Devices and Drivers — SoC の周辺機能が PCI や USB のように列挙できない理由と、ボード固有コードによる登録。
- How To Write Linux PCI Drivers —
pci_device_idテーブルと、一致したデバイスに対するprobe()の呼び出し。 - USB Device Drivers — USB 側の ID に基づくドライバ対応づけ。
- Devicetree Specification — フォーマットの正式な定義。
- Linux and the Devicetree — カーネルがツリーを読み、ドライバを対応づける仕組み。
- Writing Devicetree Bindings — ハードウェア記述とドライバ設定の境界。
- dtc — Device Tree Compiler — コンパイラのリポジトリと使い方。
- U-Boot: Devicetree Control — U-Boot がデバイスツリーをどう扱うか。
- Booting ARM Linux — ブートローダがどのレジスタでブロブをカーネルに渡すか。
- Devicetree Overlay Notes — オーバーレイが実行時にどう適用されるか。
- Device Tree Reference (eLinux) — 実例を伴うコミュニティのリファレンス。
コメント