とあるエンジニアの備忘log
2014年5月28日水曜日
Device Tree 入門
Device Tree というのは、ハードウェアの詳細を記述したデータ構造体です。 元々は PowerPC Sybsystem から始まったようなのですが、すでに ARM Linux は DeviceTree 一色になってしまっています。 そのため Device Tree を知らないと、 SoC の移植はおろか、ドライバの開発もできない。 そこで、 Device Tree の初歩についてまとめてみることにします。 ただし、私自身が初心者ですので、難しいことは説明できませんし、間違っている部分もあるかもしれませんが、ご了承ください。 ARM のことしかわかりませんので、 ARM を対象として書くことにします。 ### 何故に DeviceTree か? ### より Generic な OS を記述するためです。 ハードウェアを差分を吸収するのがドライバの役目なのですが、勘違いしてはいけないのは、ドライバは「ハードウェアの制御の仕方」を記述するものであって、 デバイスのベースアドレスや、クロック、割り込み番号といった「ハードウェアのプロパティ」を記述するものではありません。 たとえば 8250 とレジスタ互換の UART はいろいろな SoC に搭載されていますが、ベースアドレスや割り込み番号は SoC ごとに違うでしょう。 1つの SoC に同じハードウェア IP が複数個載っていて、ベースアドレスだけ違う、ということもあります。 したがって、ドライバをより汎用的に記述するには、ハードウェアのプロパティは別の場所に記述しておいて、ドライバの初期化時に引き渡す必要があります。 以前は、ハードウェアのプロパティはボードファイル (ARM でいうと `arch/arm/mach-*/` 以下のファイル)に記述していて、 platform_driver を経由してドライバに渡すというのがよく行われていました。 現在では、新しいボードファイルの追加は推奨されず、代わりに Device Tree に記述します。 つまり、ハードウェアの細かい情報を Kernel 内にハードコーディングするのではなくて、 Device Tree という別のデータ構造に押し出すということです。 これによって Kernel がよい汎用的な記述になります。 さらに ARM Linux はより汎用的な方向に向かっています。マルチプラットフォームです。 (`CONFIG_ARCH_MULTIPLATFORM` を選択する) 例えば `arch/arm/configs/multi_v7_defconfig` でコンフィグレーションすると、さまざまな SoC で動く Kernel ができます。 再コンパイルすることなく、同一の Kernel イメージで OMAP や Tegra や Zynq が動くということです。 今後、新しい SoC に移植する場合には、専用のコンフィグレーションではなくて、マルチプラットフォームを使うべきです。 ### Device Tree の記述の仕方 ### Deviece Tree Source (以下 DTS) を記述し、Device Tree Compiler (以下 DTC) でコンパイルすると、 Device Tree Blob (以下 DTB) というバイナリーができます。 この DTB をブート時に Kernel に渡します。 DTS それ自体は非常にシンプルな言語で、以下のような感じで記述します。 /dts-v1/; / { node1 { a-string-property = "A string"; a-string-list-property = "first string", "second string"; a-byte-data-property = [01 23 34 56]; child-node1 { first-child-property; second-child-property = <1>; a-string-property = "Hello, world"; }; child-node2 { }; }; node2 { an-empty-property; a-cell-property = <1 2 3 4>; /* each number (cell) is a uint32 */ child-node1 { }; }; }; 階層的にノードがあって、各ノードにプロパティ(とその値)を記述します。 XML みたいな構造のものと思っていいでしょう。 コンパイルの仕方ですが、上記のようなソースを `foo.dts` に書いて $ dtc -O dtb -o foo.dtb foo.dts とすると、 `foo.dtb` (DTB) ができます。 また、以下のようにして DTB から DTS へ戻すこともできます。 $ fdtdump foo.dtb DTC の公式リポジトリは `git://git.kernel.org/pub/scm/utils/dtc/dtc.git` です。 (以前は `git://jdl.com/software/dtc.git` だったが、こちらはもう更新されていないので注意!) ただし、 Kernel のビルド時には `scripts/dtc` に入っているものが使われますので、なくてもできます。 DTS の書き方ですが、[Device Tree Usage](http://www.devicetree.org/Device_Tree_Usage) のサイトがわかりやすいと思いますので、そちらで基本的な書き方を勉強するとよいです。 ### Device Tree の渡し方 ### Device Tree 以前は、 ATAGS という構造体にいろんな情報を格納して、Kernel の起動時に渡していました。 今でも ATAGS はサポートされているものの、より細かい記述のできる Device Tree に置き換わっています。 Kernel のエントリーポイントにおける レジスタは以下のようになっている必要があります。 | ATAGS | Device Tree ---|---------------------------------|---------------------------- r0 | 0 | 0 r1 | Machine type number | don't care r2 | Physical address of ATAGS | Physical address of DTB ### DTB の構造 ### DTS の記述の仕方は上記のとおりなのですが、DTB がどんな構造になっているのかを見てみます。 DTB は大きく見ると、以下のような構造になっています。 |----------------------------| | device-tree header | |----------------------------| | memory reserve map | |----------------------------| | | | device-tree structure | | | |----------------------------| | | | device-tree strings | | | |----------------------------| 前述の `foo.dtb` をダンプしてみると以下のようになった。 00000000 d0 0d fe ed 00 00 01 df 00 00 00 38 00 00 01 54 |...........8...T| 00000010 00 00 00 28 00 00 00 11 00 00 00 10 00 00 00 00 |...(............| 00000020 00 00 00 8b 00 00 01 1c 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 |................| 00000040 00 00 00 01 6e 6f 64 65 31 00 00 00 00 00 00 03 |....node1.......| 00000050 00 00 00 09 00 00 00 00 41 20 73 74 72 69 6e 67 |........A string| 00000060 00 00 00 00 00 00 00 03 00 00 00 1b 00 00 00 12 |................| 00000070 66 69 72 73 74 20 73 74 72 69 6e 67 00 73 65 63 |first string.sec| 00000080 6f 6e 64 20 73 74 72 69 6e 67 00 00 00 00 00 03 |ond string......| 00000090 00 00 00 04 00 00 00 29 01 23 34 56 00 00 00 01 |.......).#4V....| 000000a0 63 68 69 6c 64 2d 6e 6f 64 65 31 00 00 00 00 03 |child-node1.....| 000000b0 00 00 00 00 00 00 00 3e 00 00 00 03 00 00 00 04 |.......>........| 000000c0 00 00 00 53 00 00 00 01 00 00 00 03 00 00 00 0d |...S............| 000000d0 00 00 00 00 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 |....Hello, world| 000000e0 00 00 00 00 00 00 00 02 00 00 00 01 63 68 69 6c |............chil| 000000f0 64 2d 6e 6f 64 65 32 00 00 00 00 02 00 00 00 02 |d-node2.........| 00000100 00 00 00 01 6e 6f 64 65 32 00 00 00 00 00 00 03 |....node2.......| 00000110 00 00 00 00 00 00 00 69 00 00 00 03 00 00 00 10 |.......i........| 00000120 00 00 00 7b 00 00 00 01 00 00 00 02 00 00 00 03 |...{............| 00000130 00 00 00 04 00 00 00 01 63 68 69 6c 64 2d 6e 6f |........child-no| 00000140 64 65 31 00 00 00 00 02 00 00 00 02 00 00 00 02 |de1.............| 00000150 00 00 00 09 61 2d 73 74 72 69 6e 67 2d 70 72 6f |....a-string-pro| 00000160 70 65 72 74 79 00 61 2d 73 74 72 69 6e 67 2d 6c |perty.a-string-l| 00000170 69 73 74 2d 70 72 6f 70 65 72 74 79 00 61 2d 62 |ist-property.a-b| 00000180 79 74 65 2d 64 61 74 61 2d 70 72 6f 70 65 72 74 |yte-data-propert| 00000190 79 00 66 69 72 73 74 2d 63 68 69 6c 64 2d 70 72 |y.first-child-pr| 000001a0 6f 70 65 72 74 79 00 73 65 63 6f 6e 64 2d 63 68 |operty.second-ch| 000001b0 69 6c 64 2d 70 72 6f 70 65 72 74 79 00 61 6e 2d |ild-property.an-| 000001c0 65 6d 70 74 79 2d 70 72 6f 70 65 72 74 79 00 61 |empty-property.a| 000001d0 2d 63 65 6c 6c 2d 70 72 6f 70 65 72 74 79 00 |-cell-property.| 000001df 以下では、各領域ごとに見ていこうと思う。 #### Header #### 先頭から 40 byte (0x28 byte) 分が Header 領域である。 各ワードの意味は以下の通りです。 +0 +4 +8 +C 00000000 magic totalsize off_dt_struct off_dt_string 00000010 off_mem_rsvmap version last_comp_version boot_cpuid_phys 00000020 size_dt_strings size_dt_struct - `magic`: 常に `FDT_MAGIC` (= `0xd00dfeed`) - `totalsize`: この DTB のサイズ - `off_dt_struct`: DT structure の開始位置 - `off_dt_strings`: DT strings の開始位置 - `offset_to`: Memory Reserve Map の開始位置 - `version`: Version。現在は 17 - `last_comp_version`: 互換性のある最後のバージョン - `boot_cpuid_phys`: ブートさせるCPUのID (使い方よくわかりませんが、たぶん気にしなくてOK) - `size_dt_strings`: DT strings のサイズ - `size_dt_struct`: DT structure のサイズ #### Memory Reserve Map #### Kernel が書き潰してはいけない、メモリの予約領域を表した配列。 struct fdt_reserve_entry { fdt64_t address; fdt64_t size; }; のように 64 bit 長の物理アドレスとサイズのペアで記述され、エントリーはいくつでも持つことができる。 `size` が 0 のエントリーが終了を意味する。 例えば、 DTB 自身や InitRamdisk が置かれているメモリ領域は Kernel に書き潰されないように、Memory Reserve Map に記述する必要がある。 上記の `foo.dtb` の例では 0x28 ~ 0x37 がこの領域に相当するが、単に 0 埋めされているだけである。(つまり予約領域なし) 実際のところ、 Memory Reserve Map は、DTC よりもブートローダーによって記録されることが多い。 コンパイル段階では DTB や InitRamdisk をメモリ上のどこに置くか決まっていないため。 通常はブートローダーが DTB や InitRamdisk を適切な物理メモリ上に置いたあと、 Memory Reserve Map を更新する。 #### DT structure #### ノードやプロパティの構造をバイナリ化した領域。 (ただしプロパティの名前だけは DT structure ではなくて、 DT strings に記録される) データ構造をパースできるように、ノードの開始、終了、プロパティなどタグが定義されている。 #define FDT_BEGIN_NODE 0x1 /* Start node: full name */ #define FDT_END_NODE 0x2 /* End node */ #define FDT_PROP 0x3 /* Property: name off, size, content */ #define FDT_NOP 0x4 /* nop */ #define FDT_END 0x9 上記の `foo.dtb` の例では、 `0x00000038` 移行が DT structure に相当します。 各アドレスのデータが何を意味しているのか、データ構造をインデントを入れてわかりやすく表現すると以下のようになる。 00000038 FDT_BEGIN_NODE 0000003c node_name = NULL (/) 00000040 FDT_BEGIN_NODE 00000044 node_name = "node1" 0000004c FDT_PROP 00000050 data_length = 9 00000054 nameoff = 0 00000058 data = "A string" 00000064 FDT_PROP 00000068 data_length = 0x1b 0000006c nameoff = 0x12 00000070 data = "first string", "second string" 0000008c FDT_PROP 00000090 data_length = 4 00000094 nameoff = 0x29 00000098 data = 0x01, 0x23, 0x23, 0x56 0000009c FDT_BEGIN_NODE 000000a0 node_name = "child-node1" 000000ac FDT_PROP 000000b0 data_length = 0 000000b4 nameoff = 3e 000000b8 FDT_PROP 000000bc data_length = 4 000000c0 nameoff = 0x53 000000c4 data = 0x00000001 000000c8 FDT_PROP 000000cc data_length = 0xd 000000d0 nameoff = 0 000000d4 data = "Hello,world" 000000e4 FDT_END_NODE 000000e8 FDT_BEGIN_NODE 000000ec node_name = "child-node2" 000000f8 FDT_END_NODE 000000fc FDT_END_NODE 00000100 FDT_BEGIN_NODE 00000104 node_name = "node2" ...... このように、DTS の構造をそっくりそのままバイナリに変換したものが DTB となっています。 ノードやプロパティの出現順もそのままで、非常に簡単な変換アルゴリズムになっていることがわかると思います。 その他、いくつかの特徴をまとめておきます。 - データの切れ目は常に 4バイト境界 データが 4バイトに満たない場合はパディングされる。 - ビッグエンディアン - 先頭からなめないと、構文解析できない 例えば、途中から読み始めて `0x00000001` というデータを見つけても、ノード先頭(`FDT_BEGIN_NODE`)を表しているのか、プロパティの値なのか判別できないため。 - データの頻繁な更新に向かない 隙間なくデータが配置されているため、途中に新しいノードやプロパティを挿入したり、より長いプロパティ値に書きかえるのが苦手。 DTB を更新する場合には、後続のデータを必要サイズ分後ろにずらしてから、該当箇所を書きかえる必要があるため、メモリコピーが発生する。 - データの型情報は記録されない `"Hello,world"` のような文字列や `<1>` のような 32 bit 値などはあくまで DTS 上での表現である。 DTB に変換されるときに、いずれも単なるバイトストリームとして記録されるので、 DTB だけからはそれが文字列を表しているのか、数字なのかはわからない。 どう解釈するかは、 Kernel 側が知っている。 プロパティ名が `compatible` なら値は文字列だろうし、 `reg` なら値はアドレス値だろうから、そもそも型を記録しなくても困らない。 #### DT strings #### プロパティ名の文字列を格納している領域。 どうして、ノード名は DT structure の方に埋め込まれているのに、 プロパティ名は DT strings に格納し、 DT structure にはプロパティ名へのポインタを格納しているのでしょうか? おそらく、プロパティ名には、`compatible`, `reg`, `interrupts` のように同じ名前のものが繰り返し出現するので、 同じ文字列は 1回だけ DT strings に記録し、それへのポインタを持つ方がサイズが小さくなるからでしょう。 ### phandle ### Device Tree Source は階層的な構造を取りますが、何に基づいて階層化するかというと、基本的に CPU から見たアドレス空間です。 もうちょっと具体的にいうと、バスの構造に基づいて階層化して記述するのが普通です。 ですので、バス構造に基づいた親子関係(例えば 「I2Cコントローラー」と「I2Cバスにぶら下がっている EEPROM」)は Device Tree の構造で表すことができます。 ところが、バスの構造に現れない親子関係はうまく表現できません。例えば、 - 割り込みコントローラ(親)とその割り込みを使うデバイス(子) - クロックを供給するブロック(親)とそのクロックを使うデバイス(子) といった関係です。 例として以下のようなソースを見てみます。 /dts-v1/; / { amba { Label0: interrupt-controller { }; Label1: pll { }; FooDevice { clocks = <&Label1 100 200>; interrupt-parent = <&Label0 10>; }; }; external_bus { BarDevice { interrupt-parent = <&Label0 5>; }; }; }; `amba` と `external_bus` はバスを、 `interrupt-controller`, `pll`, `FooDevice`, `BarDevice` はデバイスを表していると思って下さい。 ノードの階層構造から、 `interrupt-controller`, `pll`, `FooDevice` は AMBAバスに、 `BarDevice` は外部バスにぶら下がっている といったことは読み取れます。 割り込みの関係、例えば `FooDevice` と `BarDevice` は `interrupt-controller` という割り込みコントローラにつながっている、 といったことは phandle という仕組みを使って記述します。 割り込みコントローラーが複数あることもありうるので、ちゃんと指定してあげないと、どれに繋がっているかわからないのです。 まず、親となる `interrupt-controller` になんでもいいのでラベルをつける。 ここでは `Label0: `としたが、実際はもうちょっとわかりやすいラベルをつけます。 子である `FooDevice` と `BarDevice` からは `<&Label0>` といった感じで親を指定する。 これを phandle といいます。通常は、いくつかの引数も合わせて指定します。 例えば、割り込みの場合は、つながっている割り込みコントローラの他に、割り込み番号が何番か、 といった情報も必要なので、 `10` とか `5` とかはそういったパラメータのつもりです。 同様にクロックの関係も、 親である `pll` ノードに `Label1: `というラベルをつけ、クロックを使う `FooDevice` には `<&Label1>` で指定する。 こういう風に記述すると、いったい何が起きるのかを見るために、いったんコンパイルして、それをダンプします。 $ dtc -O dtb -o foo.dtb foo.dts && fdtdump foo.dtb 以下のように表示された。 /dts-v1/; // magic: 0xd00dfeed // totalsize: 0x16a (362) // off_dt_struct: 0x38 // off_dt_strings: 0x144 // off_mem_rsvmap: 0x28 // version: 17 // last_comp_version: 16 // boot_cpuid_phys: 0x0 // size_dt_strings: 0x26 // size_dt_struct: 0x10c / { amba { interrupt-controller { linux,phandle = <0x00000002>; phandle = <0x00000002>; }; pll { linux,phandle = <0x00000001>; phandle = <0x00000001>; }; FooDevice { clocks = <0x00000001 0x00000008 0x00000002>; interrupt-parent = <0x00000002 0x00000001>; }; }; external_bus { BarDevice { interrupt-parent = <0x00000002 0x00000002>; }; }; }; `interrupt-controller` と `pll` ノードに、勝手にプロパティが追加されているのに気づいたでしょうか? これが phandle の正体です。phandle というのは単なる整数なのです。 Device Tree Compiler は `<&Label>` といった記述を見つけると、指し示された親に `linux,phandle"` と `plandle` というプロパティを追加し、1 から始まる整数を割り当てます。 そして、子の `<&Label>` の記述をその整数に置き換えます。 `linux,phandle` または `phandle` の値を元に検索すれば、所望の親を見つけられるというわけです。 phandle は `1` から割り振るというのは味噌です。 `< >` という記号は 32bit幅の unsigned integer を表すのに使います。 phandle を探す関数は、 phandle を見つけられない場合は `0` を返すようになっています。 割り込みコントローラを指すのに `interrupt-parent` というプロパティ名を使いますよとか、クロックを指すのには `clocks` というプロパティ名を使いますよ、とかいうのはもう一段上のレベルの取り決めです。 それぞれのプロパティ名をどう解釈するかはデバイスドライバの実装の話ですが、いちいちソースコードを読むのも大変なので、 Linux の `Documentation/devicetree/bindings` の下のドキュメントを読むとたいてい書いてあります。 なんで `linux,plandle` と `plandle` と2つも追加するのかは、詳しくは知らないのですが、後方互換性とかの問題だと思います。 一応 Device Tree というのは Open Firmware という規格で決まっているらしく、Linux 独自のものではないです。 `linux,phandle` みたいに `linux,` で始まるのは Linux の拡張機能で、最初は `linux,plandle` を使っていたけど、 途中でそれが Open Firmware 本家に取り込まれて `plandle` を使うようになったという事情だと想像します。 (間違っていたらすいません。。)
0 件のコメント:
コメントを投稿
次の投稿
前の投稿
ホーム
登録:
コメントの投稿 (Atom)
0 件のコメント:
コメントを投稿