#1.はじめに
ARM CPUを内蔵するFPGAをLinuxで動作させているうちに、デバイスツリー(devicetree)というものを修正する必要が出てきました。そこで、Kernel sourceに入っているデバイスツリーソース(.dts)の内容がおおよそ理解できるよう、調べてみた(自分なりに理解した)ことについて書かせていただきます。
Kernel sourceに入っている .dts ファイルを見て「なんじゃこりゃ」と思った人が、なんとなく判った気になって、追加修正できそうな気分になる、内容になればと思います。
#2.デバイツリーって何者
##2.1 成り立ち
デバイスツリーというのは、OS(CPU)からアクセスできるハードウェア部品たちをsoftware(デバイスドライバ)目線で記述したデータ構造(ファイルフォーマット)と言って良いかと思います。このフォーマットは、Open Firmware というグループ(Linux開発のグループとは別の方々)により策定されました。
##2.2 Linuxでの使われ方
Linux でもこのフォーマットを使わせてもらい、Kernelコードとボード毎の情報(すなわちデバイスツリー)を分離して持つ運用になっています。これにより、デバイスツリーをボード用に用意すれば、Kernelバイナリは共通のまま、異なるボードでの動作も可能とすることができるようになりました。
Kernelソースには、サポートされているメジャーなボード用にデバイスツリーソース(.dts)ファイルが存在しており、Kernelバイナリのビルド(例: make zImage
)と同様にデバイスツリーバイナリも Makefile システムで生成可能となっています(例: make dtbs
)。ちなみに、この記事を書いている時点(2020年5月)での本家 Kernel.org の Linux kernel source tree では、ARMの32bit系のCPU用だけで1200を超える.dtsファイルが登録されてました。
Kernelコード(各デバイスドライバ)は、初期化時に自分が必要とする情報をデバイスツリーから検索しその情報に従いデバイス登録・初期化を完了する流れになっています。
そのため、Kernelコードが必要とする情報がデバイスツリー内に記述されていないとKernelが起動しない、といった状況も出てきますので、Kernelバイナリとデバイスツリーは同じバージョンのKernel sourceから作るというのが原則となります。
2.3 デバイスツリーファイルの種類
人間が読めるテキスト形式で記述したものはデバイツリーソースと呼ばれ、.dts の拡張子を持ちます。これをバイナリ形式に変換したファイルはデバイスツリーブロブと呼ばれ、.dtb の拡張子を持ちます。Kernel は、バイナリ形式のデバイスツリーブロブ(.dtb)を読み込みます。デバイツリーソースからデバイスツリーブロブへの変換はデバイスツリーコンパイラ(dtc コマンド) で行います。デバイスツリーコンパイラは、.dts -> .dtb だけでなく、逆変換 .dtb -> .dts を行うオプションもあり、バイナリ.dtbの内容を見てみたいというときにも使うことができます。
3. デバイツリーの理解
(前置きが長くなりましたが、)Linuxでのデバイスツリーを理解するにあたっては、
- デバイスツリーのフォーマット(書き方)
- デバイスツリーにどんな情報を書いておく必要があるのか(Linux デバイスドライバがどんな情報をデバイスツリーから読み取るのか)
の2つに分けて理解するのが宜しいかと思います。
ここでは、デバイスツリーの書き方(フォーマット)
を中心に書きます。
デバイスツリーにどんな情報を書いておく必要があるのか
は、Linuxのデバイスドライバがどんな情報を必要としているのか次第となります。この記事の最後のところでそのあたりのところをちょっとご紹介します。
3.1 dts ファイルのフォーマット概要
dts ファイルについてざくっと言うと、
- CPUにアクセスさせたいハードウェア(device node)のリストをツリー状のデータベースとして記述したもの
- 構成要素は node と property の2種類のみ
- nodeは、各種の属性情報propertyと、子ノード(child node)を持つことができる
- propertyは、属性名nameとその値valueの組み合わせで表現され、アドレス情報や割り込み番号等ハードウェア情報を記述するためのもの
です。より詳細は下記のリンクを見ていただくのが良いかと思います。
https://elinux.org/Device_Tree_Usage
3.2 Node
[label:] node-name[@unit-address] {
[properties definitions]
[child nodes]
}
-
deviceは、nodeとして表現される
-
node は、 property と node(子ノード) を持つ
- nodeの中にnodeを持たせることでnodeのツリー構造が作られることになります。
-
node名 @ unit-address の形式で名前を定義し、続く中括弧内 { } にそのnode内容を定義する
-
unit-address の部分はreg propertyで指定されるアドレスを16進で記述。node名が同じデバイスが複数あってもunit-addresで区別できるように。 reg propertyを持たないnodeではunit-addressは必要なし。Linuxのdtcでは、unit-addressが無くてもコンパイル可能(Warningが出るが)
-
node名は、単純なASCII文字列で、長さは最大31文字。
- そのデバイスの機能を表す名前(ethernet, serial, gpio 等)を付けることが推奨されている
-
最上位(ルート) nodeは、node名は無し "/" のみ。
/ { ... };
-
ノードのツリー構造(親node - 子node の関係)は、CPU から見たデバイスの接続状態を示すように記述する(例えば Bus bridgeを親とし、その子ノードとしてBus bridgeに接続されたSlaveDeviceを定義していく)
-
ラベル(label:)を付与しておくと、そのnodeのphandleを
&label
という書き方で指定可能。"-@" オプション付きでコンパイルされていれば、別ファイル(オーバレイファイル)からの参照も可能。(下の phandle property の説明も見て下さい)
3.3 Property
- propertyは、property-name = value の組み合わせ
- value が無い場合もあり
[label:] property-name = value;
[label:] property-name ;
3.3.1 Property で使う value の種類
- Text String: 二重引用符で囲む compatible =
"arm,cortex-a9";
- Array/Cell-list : 山括弧<>で区切られた32ビットの符号なし整数の集合
reg =<0xffd04000 0x1000>;
- Binary Data :角括弧[]で区切る mac-address =
[12 34 56 ab cd ef];
- 複数データ: コンマを使用して連結可能 reg =
<0xff900000 0x100000>, <0xffb80000 0x10000>;
mixed-property ="a string", [0x01 0x23 0x45 0x67], <0x12345678>;
3.3.2 多くのnodeで共通して良く使われる標準 Property
-
compatible
-
デバイスドライバとの結び付けに使われる最重要Property。
- デバイスドライバを必要とするデバイスには、このpropertyが必要
-
このvalueで指定されている文字列に一致するcompatible-stringを持つデバイスドライバの初期化ルーチンが呼び出されデバイス登録が行われる。
-
Text String のリストで "manufacturer,model" という形式。
- 例:
compatible = "altr,socfpga-stmmac", "snps,dwmac-3.70a", "snps,dwmac";
- 例:
-
status
- そのデバイスを有効にするかしないか。
- Value は "okay" or "disabled“
-
phandle
- node を示す id 番号をCell-listとして設定
- 通常は(人間が書く.dtsでは)省略されてる
- phandle が省略された node には、dtc コンパイラが自動的に固有のidを生成し、phandleを追加し.dtb を生成
- '<&XXX>' という書式で、label “XXX” を持つ node の phandle を指定できる。interrupt property の記述でよく使用されている。
-
reg
- デバイスに対するアドレス情報。 (address-cells, size-cells) のペアで指定。
- address-cells でベースアドレスを、size-cellsでメモリサイズ(byteサイズ)を指定
- address-cells, size-cells の数は、 親 node の #address-cells, #size-cells の値により決められる
- デバイスに対するアドレス情報。 (address-cells, size-cells) のペアで指定。
-
#address-cells, #size-cells
- child node を持つ node で定義。どの様に child node がアドレッシングされるかを定義。その階層の ranges および子階層のreg property でのアドレス指定方法がこれで決まる
- #address-cells
- child node の reg property 中の addres cell の数、通常は 1。 複数 bus master を持つ場合だと 2(bus 番号と addr で指定)。64bit address だと 2。
- #size-cells
- child node の reg property 中の size cell の数(通常は 1, memory mapped でない場合 0)
-
ranges
- Bus を表す node で定義、自分と子nodeとのアドレス変換方法を指定する。
- <子のaddress-cells値 親のaddress-cells値 子のsize-cells値> を組みとする
- value を持たない場合は、親node と 子node は同一のアドレス空間であることを意味する
/{
....
sopc0: sopc@0 {
ranges;
/*
sopc@0 (親)と bridge@0xc0000000(子)の関係
sopc@0とbridge@0xc0000000 は、同一のメモリ空間を持つ(アドレス変換は行わない)。
*/
#address-cells = <1>;
#size-cells = <1>;
....
hps_0_bridges: bridge@0xc0000000 {
reg = <0xc0000000 0x20000000>,
<0xff200000 0x00200000>;
/*
bridge@0xc0000000は、0xc0000000からサイズ0x20000000 のメモリ空間と、
0xff200000から0x00200000 のメモリ空間の2つのメモリ領域を持つ
*/
#address-cells = <2>;
#size-cells = <1>;
/*
bridge@0xc0000000の子ノードは、address-cells は2つの32bit数値、size-cell は1つの32bit数値で表現される。
*/
ranges = < 0x00000000 0x00000000 0xc0000000 0x00010000 >,
< 0x00000001 0x00020000 0xff220000 0x00000008 >,
< 0x00000001 0x00010040 0xff210040 0x00000020 >;
/*
0xc0000000からサイズ0x00010000 のメモリ領域は、子ノードのアドレス<0x00000000 0x00000000>からサイズ0x00010000 に割り当て、
0xff220000からサイズ0x00000008 のメモリ領域は、子ノードのアドレス<0x00000001 0x00020000>からサイズ0x00000008 に割り当て、
0xff210040 からサイズ0x00000020 のメモリ領域は、子ノードのアドレス<0x00000001 0x00010040>からサイズ0x00000020 に割り当てる
(それ以外の領域は子ノードには割り当てない)。
*/
....
led_pio: gpio@0x100010040 {
compatible = "altr,pio-16.0", "altr,pio-1.0";
reg = <0x00000001 0x00010040 0x00000020>;
/*
gpio@0x10001004 は、<0x00000001 0x00010040>からサイズ0x00000020の
メモリ領域を持つが、これは親(上位)nodeの 0xff210040 からサイズ
0x00000020のメモリ領域にマップされる(CPUからも0xff210040-60 の範囲でアクセスできる)。
*/
....
}; //end gpio@0x100010040 (led_pio)
....
}; //end bridge@0xc0000000 (hps_0_bridges)
};
};
-
interrupt-controller
- value を持たない. interrupt を受けるデバイス(=割り込みコントローラ)であることを意味する。
-
#interrupt-cells
- 割り込みコントローラ node の property。interrupt property の値のcell数。割り込みコントローラの device driver によって規定されている(割り込み番号、active-high/active-low/posedge/negedge など)。コントローラのドキュメントを参照。下の"4. DeviceTreeにどんな情報を書くのか"の項でもう少し説明します。
-
interrupt-parent
- 割り込みを出力する node の property。接続される割り込みコントローラ node の phandle を設定。この property を持たない場合、親 node の値を使う。
-
interrupts
- 割り込みを出力する node の property。
- 出力する割り込み信号の属性。接続している interrupt-controller の#interrupt-cellsで定義されている値を設定する。
3.5 その他補足
- 最初の行は、 /dts-v1/;
- コメントは C style (/* ... */) および C++ style (// ...) が使える.
- 同じ階層で同じ node名 に対する定義が出てきた場合は、同じnodeとみなし、そのnodeに対する追加定義となる。propertyの再定義も可能(後からの定義が使われる)
- Kernel Makefile でコンパイルする場合、まず gcc の preprocessor で処理されるので
- #include ... , #define ... が使える
4. DeviceTreeにどんな情報を書くのか
この点に関しては、Kernel Sourceのドキュメントを読むのが一番です。
例として下記の割り込みを持つnodeの記述内容について見てみましょう。
intc: intc@fffed000 {
compatible = "arm,cortex-a9-gic";
#interrupt-cells = <3>;
interrupt-controller;
....
};
soc {
interrupt-parent = <&intc>;
....
i2c0: i2c@ffc04000 {
....
interrupts = <0 158 0x4>;
....
};
};
上記のコードで、i2c@ffc04000のinterrupts propertyは、3つのcellで構成されていますが、この3つの数値に何を書けばよいのかについての探し方です。interrupt-controllerのcompatible stringである "arm,cortex-a9-gic" をKernel Source の Documentation/devicetree/bindings ディレクトリ下のファイルで、 検索します(例: find Documentation/devicetree/bindings -name "*.txt" |xargs grep "arm,cortex-a9-gic"
, git で Kernel Sourceを持ってきているのなら git grep "arm,cortex-a9-gic" -- Documentation/devicetree/bindings
など)。すると、Documentation/devicetree/bindings/interrupt-controller/arm,gic.txt がヒットします。このファイルの内容を見てみると、#interrupt-cellsは3とし、それぞれの内容は、
- 1番目は interrupt type: 0は Shared Peripheral Interrupt (SPI), 1 は Private Peripheral Interrupt (PPI)
- 2番目は 割り込み番号: SPIなら[0-987], PPIなら[0-5] のレンジ
- 3番目は 以下の意味を持つフラグ:
- 1 = low-to-high edge triggered
- 2 = high-to-low edge triggered (invalid for SPIs)
- 4 = active high level-sensitive
- 8 = active low level-sensitive (invalid for SPIs).
とするように記載されています。上記コードの <0 158 4> は、
- Shared Peripheral Interrupt割り込み,
- SPIの158番目の割り込み番号、
- active high level 割り込み
を意味します。ちなみに、この.dtsが記述しているSoCの仕様書を確認すると、Shared Peripheral Interrupt割り込み番号は、GIC割り込みコントローラの32番から開始されているので、SoCの仕様書のGIC割り込み番号から32を引いた値を2番目に書くことになります(SoCの仕様書では、i2c@ffc04000は190番の割り込みとなっている)。多分 ARM GIC を使っているSoCでは、共通して32を引いた割り込み番号を.dtsに記述することになると思います。
あと、さらに.dtsの書き方全体についてより詳しく知りたい人は、Linux Sourceの Documentation フォルダ下にある、Documentation/devicetree/booting-without-of.txt を読むのが良いかと思います。
必須のノードとして
- root node
- /cpus node
- /cpus/* nodes
- /memory node(s)
- /chosen node
- /soc node
がある、等々 色々とたっぷり記載されています。
以上、これにて。
5. 参考資料
https://elinux.org/Device_Tree_Usage
https://elinux.org/Device_Tree_presentations_papers_articles
https://devicetree-specification.readthedocs.io/
https://git.kernel.org/?p=/linux/kernel/git/torvalds/linux.git;a=blob_plain;f=Documentation/devicetree
https://www.nds-osk.co.jp/forum/freedownload/08/casestudy2/DeviceTree2.pdf