Wireless・のおと

Linux Device Treeのはなし

ブログ
Linux Device Tree

Device Treeは主にARMプロセッサのLinuxで用いられる、ハードウェア仕様を記述するメタ原語の一種です。今回は主にARMの組み込みLinuxのドライバ開発者向けの(ガチに技術解説的な)内容です。

Device Treeについて

Linuxはもともと、「いわゆるAT互換機」のパソコンを主なプラットホームとして発展してきました。しかし90年代末からARMプロセッサの躍進が始まり、いわゆる情報家電機器に「ARMプロセッサ+Linux」の組み合わせが多用されるようになってゆきました。ARMチップがどんどん増えるのに加え、同じチップを使っても基板の設計が違えば下回りも異なり、arch/arm/下に格納されるチップ別ディレクトリ(mach-XXXX)とドライバファイルもどんどん増えて収拾が付かなくなってゆきました。

2011年頃(Linux 2.6.38)にLinus Torvalds氏が「いい加減にしろ」と言い出したのがきっかけで「交通整理」が図られることになり、90年代にSun Microsystems社が自社製品(SPARC Station)用に開発し仕様公開していたメタ原語「Open Firmware」をベースにしたハードウェア記述仕様を「Device Tree(以下DT)」としてLinuxに取り込むことになりました。

DTは基本的に静的なデータベースで、各ペリフェラル(CPU・メモリ・I/Oデバイスなど)の初期化に必要となるパラメータ(アドレス・割り込み・DMA・クロックなど)を格納したものです。「DOS時代のパソコンのBIOS設定みたいなもの」とも言えますが、もはやこんな例えも通じなくなっているでしょうね(※註) 。

DTを用いることにより、ソースコードを編集してカーネルを丸ごと再コンパイルしなくても、ペリフェラルのON/OFFや設定の変更はDTの変更だけで対応することができます(という建前になっています)。

(※註) なおDTを適用するか否かはアーキテクチャによって異なり、intel系のプロセッサではACPIという設定システムを使うのがデフォルトになっています。私は本業が組み込みばっかりでPC-Linuxの開発経験は殆ど無いので、ACPI APIについてはよく知りません。

 

Linuxの場合、DTはDTS(Device Tree Source)というソースコードで記述され、それをDTC(Device Tree Compiler)にかけてDTB(Device Tree Binary(DTB)というバイナリファイルに変換し、これをカーネルから参照して使用します。DTSソースコードはカーネルディレクトリのarch/<アーキテクチャ>/boot/dts/下に入っています。64bit ARMではarch/arm64/boot/dts下にチップメーカー毎のサブディレクトリが分かれていますが、6.5より前のカーネルでは32bit ARMでは全チップのDTSがarch/arm/boot/dts下にごっちゃに入っていました(歴史的経緯というやつです)。

DTSソースディレクトリには*.dtsと*.dtsiファイルがあります。dtsiはCで言うところのヘッダファイル(※註)で、*.dtsまたは他の*.dtsiファイルからincludeして使うためのファイルです。dtsiとはさらに別に*.hファイルをincludeすることもあり、*.hファイルはdts/下に転がっている場合もあればinclude/dt-bindingsの下にある場合もあります。*.dtsiと*.hの使い分け基準は、DT文法で書かれているものが*.dtsi・Cと同じ文法(#define)で書かれているものが*.hになるようです。Cで書かれたドライバソースからDTと共通の定義を取り込んで使う意図があるようです。

(※註) 実は、#defineや#includeはDTS本来の文法には存在しません!Linuxのソースディレクトリにある*.dtsファイルをdtcでコンパイルしようとするといきなりエラーになります。Linuxのカーネルビルドシステムでは、cpp(Cのプリプロセッサ)を使って#ディレクティブを処理した中間ファイルを作ってからdtcコンパイラにかけています。手動でプリプロセッサを通す場合のコマンドは下記のようなものになります。

 

cpp -nostdinc -I include -I arch -undef -x assembler-with-cpp ./arch/arm/boot/dts/XXX.dts > /tmp/tmp.dts

 

カーネルをビルドすると、カーネル.configで選択されたアーキテクチャに対応するDTBファイルが作られ、arch/<アーキテクチャ>boot/image/dts下に格納されます。これはDTSディレクトリ下のMakefileに

 

dtb-$(CONFIG_SOC_IMX6UL) += \
        imx6ul-14x14-evk.dtb \
        imx6ul-ccimx6ulsbcexpress.dtb \
        imx6ul-ccimx6ulsbcpro.dtb \
        imx6ul-geam.dtb \
        imx6ul-isiot-emmc.dtb \
        imx6ul-isiot-nand.dtb \
        :

 

みたいなのが入っていて、カーネル.configファイルに"CONFIG_SOC_IMX6UL=y"が設定されているとシンボル"dtb-y"に対応する*.dtbファイル名の一覧が追加され、Makeによって処理される仕掛けになっています。

 

ビルドしたDTBをどうやってターゲットに書き込み・どうやってロードするかは実装によって異なります。Raspberry PiのようなSBCの場合、ブートメディア(uSDカード)のパーティション0がbootパーティション(FAT32フォーマット)で、そこにカーネルイメージと一緒にdtbを書き込むものが多いです。一方、組み込みシステムの専用基板ではNOR Flashの物理アドレスを切ってカーネルイメージとDTBを書き込み、物理アドレス名指しでロードしたりもします。

LinuxカーネルではDTBのことをFlattened Device Tree(FDT)ファイルと呼んでいるため、ブートローダーでもfdt_fileとかfdt_addressとかのシンボルを使うことが多いです。一方カーネルAPIではOpen Firmwareを略して「of_find_node_by_name」のようなof_というシンボルを用いています。「Device Tree(DTS/DTB)」「FDT」「OF」が原則として同じものを指しているわけで、DTがとっつきにくい理由のひとつかも知れません。

 

U-Bootの場合、bootiやbootzコマンドの1番目の引数にカーネルイメージ・3番目の引数にFDTをロードしたRAMの物理アドレスを指定します(2番目はRAMディスクがらみで、使わない場合は"-"を指定します)。例としてSX-590のU-Bootでは、こういうコマンドを使って起動しています。

 

fdt_addr=0x83000000 ★FDTのRAMアドレス
loadaddr=0x80800000 ★カーネルイメージのRAMアドレス
sf probe ★SPI Flashドライバの初期化
sf read ${fdt_addr} 0x00050000 0x10000; ★SPI Flash 0x50000からFDTのロード
sf read ${loadaddr} 0x00060000 0x300000; ★SPI Flash 0x60000からカーネルのロード
bootz ${loadaddr} - ${fdt_addr} ★カーネルとFDTアドレスを指定して起動

 

Linuxが起動すると、カーネルは指定されたアドレス上のFDTを構造解釈(パース)し、/sys/firmware/devicetree/base下に対応する仮想ファイルシステムを作ります(/proc/device-treeにはシンボリックリンクが作られます)。また、/sys/firmware/fdtには生のFDTが見えます。

 

 

DTを実際に見てみる

DTには「ノード」と「プロパティ」という2つの要素しかありません。ノードはディレクトリのようなもの、プロパティはファイルのようなもので、/sys/firmware/devicetree/baseの下でもノード=ディレクトリ・プロパティ=ファイルに対応しています。

例えばSX-590で使用しているNXP iMX6ULLのDTS(imx6ull.dtsi, Kernel 4.1.15)で定義されているESCPI4のDTSは

 

ecspi4: ecspi@02014000 {
        #address-cells = <1>;
        #size-cells = <0>;
        compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";
        reg = <0x02014000 0x4000>;
        interrupts = <GIC_SPI 34 IRQ_TYPE_LEVEL_HIGH>;
        clocks = <&clks IMX6UL_CLK_ECSPI4>,<&clks IMX6UL_CLK_ECSPI4>;
        clock-names = "ipg", "per";
        dmas = <&sdma 9 7 1>, <&sdma 10 7 2>;
        dma-names = "rx", "tx";
        status = "disabled";
};

 

これにSX-590のDTS(imx6ull-sx.dts)で追加/上書きするDTSが

 

&ecspi4 {
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_ecspi4>;
        fsl,spi-num-chipselects = <1>;
        cs-gpios = <&gpio2 15 GPIO_ACTIVE_HIGH>;
        status = "okay";
        spidev0: spidev@0 {
              compatible = "sx5x0,spidev";
              spi-max-frequency = <60000000>;
              reg = <0>;
        };
};

 

となっていて、実際に稼働しているシステムの上ではこのように見えます。

 

# ls -l /sys/firmware/devicetree/base/soc/aips-bus@02000000/spba-bus@02000000/ecspi@02014000/
-r--r--r--    1 root     root             4 #address-cells
-r--r--r--    1 root     root             4 #size-cells
-r--r--r--    1 root     root             8 clock-names
-r--r--r--    1 root     root            16 clocks
-r--r--r--    1 root     root            33 compatible
-r--r--r--    1 root     root            12 cs-gpios
-r--r--r--    1 root     root             6 dma-names
-r--r--r--    1 root     root            32 dmas
-r--r--r--    1 root     root             4 fsl,spi-num-chipselects
-r--r--r--    1 root     root            12 interrupts
-r--r--r--    1 root     root             6 name
-r--r--r--    1 root     root             4 pinctrl-0
-r--r--r--    1 root     root             8 pinctrl-names
-r--r--r--    1 root     root             8 reg
drwxr-xr-x    2 root     root             0 spidev@0
-r--r--r--    1 root     root             5 status
 
# cat /sys/firmware/devicetree/base/soc/aips-bus@02000000/spba-bus@02000000/ecspi@02014000/compatible
fsl,imx6ul-ecspifsl,imx51-ecspi#

 

"spi@02014000"というノード(ディレクトリ)が/socから下に多段の入れ子構造になっていること、spi@02014000/の下にはいくつものプロパティ(読み出し専用ファイル)と、"spidev@0"というサブノード(サブディレクトリ)があること、プロパティファイルの中身はDTS中で=の右辺に書かれた値がそのまま入っていること、などがわかります。DTS独特の文法や、ノードやプロパティの命名規則については後述します。

 

(※註) なお、カーネルバージョンが変わるとDTの命名規則が"aips-bus"だったものが"bus"になったり、"qspi"だったものが"spi"になったり、"@02000000"だったものが"@2000000"になったり微妙に変わったりします。カーネルをアップデートしたらいきなり起動途中でパニック吐いて止まる原因の1つで、エンジニアとしては「一度決めたんなら要らんとこ変えるなよ...」と思います。

 

 

DTを書いてみる

まず、もっとも単純なDTSは次のようになります。

 

test1.dts:
/dts-v1/;
/ {
        property-name1 = "property-value1";
        property-name2 = <0x1234567>;
        node-nameA {
                property-name3 = "property-value3";
        };
};

 

1行目の/dts-v1/;はバージョン表記で必須です。

 

/ {は「"/"という名前のノード定義のはじまり」を意味しています。ルートノードには"/"以外の名前は指定できません。

property-name1 = "property-value1";は、ルートノード/の中に"property-name1"という文字列型のプロパティを定義しています。文字列は必ず""で括ります。文字列のなかに"自身を含めたいときはC言語同様\"と書きます。\n, \012, \x0aなどの特殊エスケープ規則もC言語と同じです。

property-name2 = <0x1234567>;は、property-name2という名前の数値型プロパティを定義しています。数値は必ず<>で括ります。10進でも16進(0x)でも表記できますが、型は符号なし32bit整数なのでマイナス値表記はできません。

node-nameA {は、ルートノード/の中に"node-nameA"という名前のサブノード定義のはじまりを意味しています。DTSには「ノード定義はプロパティ定義の後に書かなければならない」という規則があり、ノード定義の後にプロパティ定義を書くとエラーになります。

property-name3 = "property-value3";は、node-nameAの中に"property-name3"という文字列型のプロパティを定義しています。

};はそれぞれサブノード・ルートノードの終わりを示します。Cの関数と違って必ず;で終端しなければなりません。

ノードやプロパティの名前にはアルファベット(大文字・小文字は区別)と数字のほか幾つかの記号(, . _ + - ? #)が使えます。Cの文法とは異なり先頭文字がアルファベットである必要はなく、「#address-cells」のように#で始まるプロパティ名や「fsl,spi-num-chipselects」のように,を含むプロパティ名も使われます。C言語頭だと何か特別な意味があるのかと思ってしまいますが、#も,も?もただの文字列で意味はありません。ただし、いくつかDTS記述上の暗黙の了解があります。

 

・アルファベット大文字は使わない。

・単語は-で区切り、_は使わない。

・#ではじまるプロパティは何らかの個数を示している。

・ベンダー固有のプロパティは「ベンダ名,プロパティ名」と表記することがある。

 

コンパイラdtcを使ってコンパイルするコマンドは次のようになります。

 

dtc test1.dts -o test1.dtb

 

これで209バイトのtest1.dtbができるはずです。dtcには逆コンパイルの機能もあり、

 

dtc -I dtb test1.dtb

 

とすることでソースコードが逆生成されます。このくらい単純なDTSだと、ほぼ原形どおりのものが出力されます。

 

 

ノードの重複宣言

node-nameAは/の下のサブノードとして作られます。しかしDTSの仕様上、入れ子関係は直接のネストとして書かなければなりません。つまり、ルートノードからフルパスを指定した

 

/dts-v1/;
/ {
        property-name1 = "property-value1";
        property-name2 = <0x1234567>;
};
 
/node-nameA {
        property-name3 = "property-value3";
};

 

のような書き方は受け付けません。その一方で、DTSは同じ名前のノードを分けて書くことが可能です。

 

test1a.dts:
/dts-v1/;
/ {
        property-name1 = "property-value1";
        property-name2 = <1234>;
};
 
/ {
        node-nameA {
                property-name3 = "property-value3";
        };
};

 

C言語の構造体ならば、同じ名前のstructを複数宣言すればエラーになりますが、DTSでは「以前に宣言されたノードに追加・上書きされる」かたちで処理されます。つまりtest1.dtsとtest1a.dtsのコンパイル結果は全く同じになります。

 

 

ラベルと参照

プロパティやノードには「ラベル」を付けることが可能です。

 

test1b.dts:
/dts-v1/;
/ {
        property-name1 = "property-value1";
        property-name2 = <1234>;
        label_nameA: node-nameA {
                property-name3 = "property-value3";
        };
};

 

ここではnode-nameAに"label_nameA"というラベルを付けています。コロン(:)の手前にスペースを入れてはいけませんが、後ろには入れても良いという妙な規則があります。Linuxのソースを見ると「ラベル名:とノード名のあいだには1文字のスペースを入れる」という慣習があるようです。また、ラベルに使える名前はプロパティやノードと異なり「英数字と_」だけです。

ラベルはあくまでDTCの内部で処理される便宜的なもので、コンパイルされたDTBには出力されません(※註)。下記のtest1b.dtsをコンパイルした結果はtest1.dts,test1a.dtsと全く同じになります。dtc -I dtbで逆コンパイルするとラベル情報は失われ、test1.dts相当のコードが出力されます。

(※註) ただし例外があって、dtcに-@オプションを付けてコンパイルすると__symbols__セクションが作られ、その中にラベル=パスの羅列が出力されます。後述するオーバーレイ機能を使うときに必要になります。

 

ラベルは主に「*.dtsiで定義された値を、*.dtsで上書き・追加する」目的で使われます。たとえばnode-nameAにproperty-name4を追加する場合、もとのノードに追加して

 

test2.dts:
/dts-v1/;
/ {
        property-name1 = "property-value1";
        property-name2 = <1234>;
        node-nameA {
                property-name3 = "property-value3";
                property-name4 = "property-value4";
        };
};

 

と書くことも、/からはじまるノード入れ子構造を再定義して

 

test2a.dts:
/dts-v1/;
/ {
        property-name1 = "property-value1";
        property-name2 = <1234>;
        node-nameA {
                property-name3 = "property-value3";
        };
};
 
/ {
        node-nameA {
                property-name4 = "property-value4";
        };
};

 

と書くこともできますが、ラベルを使うことにより

 

test2b.dts
/dts-v1/;
/ {
        property-name1 = "property-value1";
        property-name2 = <1234>;
        label_nameA: node-nameA {
                property-name3 = "property-value3";
        };
};
 
&label_nameA {
        property-name4 = "property-value4";
};

 

と書くこともできます。test2.dts, test2a.dts, test2b.dtsのコンパイル結果は全て同じになります。

&label_nameAは「参照(Reference)」と呼ばれる文法で、「そのラベルに対応するノードのフルパス名」と同じになります。参照はプロパティ値として使うこともでき、

 

property-name5 = &label_nameA;

 

のように書けば、これは

 

property-name5 = "/node-nameA";

 

と書いたのと同じになります。ただし前述のように、DTSに直接フルパスを指定したノードを宣言することはできず、同じネストになるまで{を重ねるか、ラベルを宣言して&参照する必要があります。なんだか不合理な気もしますが、メタ言語には妙な制限や制約は付き物なので諦めてください。

 

 

リソース宣言

ノード名に@数字を付けたものは「リソース情報を伴うノード」という意味になります。たとえばnode-nameAに「メモリアドレス0x3000から始まる32バイトのリソース」を与えると次のようになります。

 

test3.dts:
/dts-v1/;
/ {
        #address-cells = <1>;
        #size-cells = <1>;
        property-name1 = "property-value1";
        property-name2 = <1234>;
        node-nameA@3000 {
                reg = <0x3000 0x20>;
                property-name3 = "property-value3";
                property-name4 = "property-value4";
        };
};

 

ルートノード(/{})の#address-cellsと#size-cellsは、そのノード内で宣言されるアドレスつきサブノードが持つ(はず)のregプロパティのアドレスとサイズの個数を示しています。

 

node-nameA@3000 {
        reg = <0x3000 0x20>;

 

という記述は「node-nameA@3000は、#address-cellsと#size-cellsがともに1の場合「アドレス0x3000から32バイトのメモリリソースを占有する」という意味になります。リソースが不連続な空間を占有する場合、アドレス+サイズのリストを続けて書くことができます。たとえば

 

node-nameA@3000 {
        reg = <0x3000 0x20 0xfe00 256>;

 

と書けば、「0x3000から32バイトに加えて0xfe00から256バイトのリソースを占有する」、という意味になります。64bitアドレス空間なら#address-cells=<2>になり、上位32bitと下位32bitを並べて書きます。

 

#address-cells = <2>;
#size-cells = <1>;
node-nameA@3000 {
        reg = <0 0x3000 0x20>

 

なお@記号は必ずしもアドレスを示すとは限らず、マルチコアCPUの場合cpu@0, cpu@1...のようにコア番号を示すインデックスに使われたりもします。この場合は外側ノードで#address-cells=<1>; #size-cells = <0>;として、regプロパティには<0> <1>などインデックス値だけを記述します。

regプロパティに書くアドレスやインデックスの値と、ノード名の@の後ろに付ける値を一致させるのは「Linuxにおける慣習」であってDTとして必須の文法ではありません。なので

 

node-nameA@0 {
        reg = <0x3000 0x20>;

 

みたいなDTSを書いてもコンパイルは通りますが、無駄な混乱を招くだけなのでやめましょう。

 

 

compatibleとstatus

実際のLinux DTSでは、デバイスノードには必ず"compatible"と"status"のプロパティが付きます。"compatible"はDTノードとデバイスドライバを紐づけるキーワードで、"fsl,imx6ul-ecspi"のようにベンダ名,製品名をコンマで区切って書く慣習があります(※註)。複数のマッチングに対応する場合は

 

compatible = "vendor1,product1","vendor2,product2";

 

のように、コンマで区切って並べた列挙文字列型を使います。

(※註) compatible名にも歴史的経緯の盲腸みたいなのがあって、/dev/spidevX.Xで見えるユーザー空間SPIドライバのDTノードには、ハードウェアのベンダ名やデバイス名とほぼ無関係な「compatible="rohm,dh2228fv"」と書く慣習があります。上で挙げたSX-590ではcompatible="sx5x0,spidev"というプロパティを持たせていますが、これはdrivers/spi/spidev.cのspidev_dt_ids[]にも対応する文字列を追加するパッチを当てて成立させています。

 

statusはそのデバイスノードを使うか否かの選択で、"okay"または"disabled"のどちらかを使います。他にもreservedとかfailとかもありますが、滅多に使いません。

リソース宣言とcompatibleとstatusを付けたDTSのサンプルを下に示します。だいぶ「カーネルソースで見慣れた、一見わかりにくい」DTSに似てきましたね。デバイスドライバから"compatible"をどうやって参照し、プロパティをどうやって読み出すかについては後述します。

 

test4.dts:
/dts-v1/;
/ {
        #address-cells = <1>;
        #size-cells = <1>;
        property-name1 = "property-value1";
        property-name2 = <1234>;
        node-nameA@3000 {
                compatible = "silex,test-device";
                reg = <0x3000 0x20>;
                property-name3 = "property-value3";
                property-name4 = "property-value4";
                status = "okay";
        };
};

 

エイリアス

エイリアスはデバイスノードを統一的に扱うための仕組みです。ルートノードの直下にaliasesノードが作られ、そこに「そのチップに搭載されているデバイスノードへの参照(&ラベル)」が羅列されます。「ラベルはソース上の便宜でFDT/DTBには出力されない」規則どおり、flexcan1とかuart1とかのラベル名はLinux上では原則として参照されません。

 

/ {
        aliases {
                can0 = &flexcan1;
                can1 = &flexcan2;
                ethernet0 = &fec1;
                ethernet1 = &fec2;
                gpio0 = &gpio1;
                gpio1 = &gpio2;
                gpio2 = &gpio3;
                gpio3 = &gpio4;
                gpio4 = &gpio5;
                i2c0 = &i2c1;
                i2c1 = &i2c2;
                i2c2 = &i2c3;
                i2c3 = &i2c4;
                mmc0 = &usdhc1;
                mmc1 = &usdhc2;
                serial0 = &uart1;
                serial1 = &uart2;
                serial2 = &uart3;
                serial3 = &uart4;
                serial4 = &uart5;
                serial5 = &uart6;
                serial6 = &uart7;
                serial7 = &uart8;
                spi0 = &ecspi1;
                spi1 = &ecspi2;
                spi2 = &ecspi3;
                spi3 = &ecspi4;
                usbphy0 = &usbphy1;
                usbphy1 = &usbphy2;
        };

 

ラベル"uart1"はノード名"/soc/aips-bus@02000000/spba-bus@02000000/serial@02020000"で、エイリアス名は"serial0"になり、Linux上では"/dev/ttymxc0"および"/sys/devices/platform/soc/2000000.aips-bus/2000000.spba-bus/2020000.serial/tty/ttymxc0"として見えます。同じものを示しているのに場所によってナンバーが0から始まったり1から始まったりするのは紛らわしくてめんどくさいのですが、これはLinuxというよりコンピュータ業界の宿病なので「そういうものだ」と諦めてください。

エイリアスはデバイスドライバから他のデバイスドライバを参照するときに使われます。エイリアスはDT内で定義されていれば必ず作成されるので、エイリアスが存在するからといって対応するデバイスドライバが存在するとは限らないことに注意してください(後述します)。

 

 

IOMUXとpinctrl

pinctrlプロパティはDT仕様で定められている標準プロパティではありませんが、Linuxの実装では多用されており、トラブルの元になりやすいもののひとつです。

いまのARMプロセッサはCPU単独でチップ化されることは殆どなく、周辺デバイスを抱き込んだSoC(System on Chip)としてチップ化されます(※註)。そして半導体の集積度に対してパッケージの制約のほうが大きく、チップに搭載される機能に対していつもピンが不足します。そのためいまどきのSoCは1本のピンに複数の機能が割り当てられ切り替えて使うのが普通になっており、これをIOMUX(I/O Multiplexing)と呼んでいます。

IOMUXの実装はベンダーによって・チップによってまちまちですが、Linuxではこれを定式化するためにpinctrlプロパティを導入しています。

NXP i.MX6のSPIの場合、まずimx6ull.dtsiに/soc/aips-bus@02000000/iomuxc@020e0000というノードが定義され、これに"iomuxc"のラベルが付けられています。

 

/ {
        :
        soc {
               :
                aips1: aips-bus@02000000 {
                        :
                        iomuxc: iomuxc@020e0000 {
                                compatible = "fsl,imx6ul-iomuxc";
                                reg = <0x020e0000 0x4000>;
                        };

 

ボード固有の*.dtsファイルでは#include "imx6ull.dtsi"でSoCの基本定義を取り込んだあと、&文法を使ってpinctrlの定義を追加します。SPI4ポートを開くためには下記のようなpinctrlを定義します。

 

/dts-v1/;
#include <dt-bindings/input/input.h>
#include "imx6ull.dtsi"
:
&iomuxc {
        pinctrl-names = "default";
                :
                pinctrl_ecspi4: ecspi4grp {
                        fsl,pins = <
                                MX6UL_PAD_ENET2_TX_DATA1__ECSPI4_SCLK 0x000010B0
                                MX6UL_PAD_ENET2_TX_EN__ECSPI4_MOSI    0x000010B0
                                MX6UL_PAD_ENET2_TX_CLK__ECSPI4_MISO   0x000010B0
                                MX6UL_PAD_ENET2_RX_ER__GPIO2_IO15     0x000010B
                        >;
                };
                :

 

MX6UL_PAD_xxxxとかのピン番号は別のファイル(imx6ul-pinfunc.h)に#define定義されており、けっこうめんどくさいことになっています。

 

/*
 * The pin function ID is a tuple of
 * <mux_reg conf_reg input_reg mux_mode input_val>
 */
:
#define MX6UL_PAD_ENET2_TX_DATA1__ECSPI4_SCLK                     0x00F4 0x0380 0x0564 0x3 0x0
#define MX6UL_PAD_ENET2_TX_EN__ECSPI4_MOSI                        0x00F8 0x0384 0x056C 0x3 0x0
#define MX6UL_PAD_ENET2_TX_CLK__ECSPI4_MISO                       0x00FC 0x0388 0x0568 0x3 0x0
#define MX6UL_PAD_ENET2_RX_ER__GPIO2_IO15                         0x0100 0x038C 0x0000 0x5 0x0

 

16進数の羅列はコメントにあるように「MUXレジスタのオフセット」「CONFレジスタのオフセット」「INPUTレジスタのオフセット」「MUXモードの設定値」「INPUTレジスタの設定値」を示していますが、具体的にどういう意味があるかはチップのマニュアルを見なければわかりません。iMX6のリファレンスマニュアルを調べると次のようになっています。

00F4 = IOMUXC_SW_MUX_CTL_PAD_ENET2_TX_DATA1
  ピンの機能切り替えを担当。設定値3(ALT3)はECSPI4_CLKの選択を指定。
0380 = IOMUXC_SW_PAD_CTL_PAD_ENET2_TX_DATA1
  ピンの電気的仕様設定。

0564 = IOMUXC_ECSPI4_SCLK_SELECT_INPUT
  入力オプションの設定。設定値0はデイジーチェーン機能の非使用を指定。

 

imx6ul-pinfunc.hのdefineではアドレス3つ(mux, conf, input)に対して設定値2つ(mux, input)しか定義しておらず、*.dtsで指定している値はCONFレジスタへの設定値になります。IOMUXC_SW_PAD_CTL_PAD_ENET2_TX_DATA1の場合、設定値0x10B0は

 

Hysteresis=Disabled
Pull config=100Kohm Pull down
Pull/Keep select=Keeper
Pull/Keep enable=Enabled
Open Drain=Disabled
Speed select=100MHz
Drive strength=R0/6
Slew Rate=Slow

 

を示していますが、何のことだかわかりませんね(なお、0x10B0はチップのリセットデフォルト値でもあります)。

pinctrl_ecspi4は定義されているだけで、まだ活性化はされていません。これを活性化するために、ボード固有dtsファイルの下のほうにecspi4(=ノード/soc/bus@2000000/spba-bus@2000000/spi@2014000)の追加定義が書かれており、

 

&ecspi4 {
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_ecspi4>;
        fsl,spi-num-chipselects = <1>;
        cs-gpios = <&gpio2 15 GPIO_ACTIVE_HIGH>;
        status = "okay";
};

 

ここでstatus="okay"となっていることでノードが活性化され、Linuxカーネルの起動時にpinctrlドライバによってpinctrl-0で参照されているIOMUXレジスタへの設定が行われます。

pinctrlがトラブルの元になりやすいのは、いまどきのSoCのIOMUX設定は機能が豊富すぎてわかりにくいこともありますが、気づかないうちに複数のノードが同じピンを使うpinctrlを参照活性化していても、DTCコンパイラもLinuxのpinctrlドライバも衝突を検出してくれないこともあります。

使っていないはずのピンをGPIOとして使おうとして、/sys/class/gpioでexportして割り当ててもエラーも出ず正常に終了したように見えるのに、output設定して0/1を書き込んでも該当ピンは0Vのままでウンともスンとも応答しなくて首をかしげていたら、実はそのピンは他の機能(UART3とかI2C4とか)が共用していて、その機能をstatus="disabled"にしないとpinctrlが活性化されてGPIO以外のIOMUXに設定されていた、というのは割とよくあります。

 

PHANDLE

phandleはノードを一意に識別する32bitの整数値を持つプロパティです。Linux上での扱いは歴史的経緯があって少々めんどくさいです。ノードラベルの参照が文字列型ではなく数値型で行われた場合、すなわち

 

/dts-v1/;
/ {
        label_nameA: node-nameA {
        };
};
 
/ {
        referenceA = <&label_nameA>;
};

 

のように<>で括ったプロパティ値として参照されたとき、DTCによって自動的にphandleプロパティが(そのラベルに対応するノード)追加されてユニークな値(通常は1~)が割り当てられ、&label_nameはその値で置き換えられます。

なおDTSでノードの中にphandleプロパティを明示的に宣言して

 

phandle = <0x1234>;

 

のように値を指定して書くことも可能で、こういう指定があれば自動割当機能はバイパスされますが、普通はこういう使い方をしません。

 

デバイスドライバ、カーネルAPIとDTの関係

Linuxカーネルはデバイスドライバを初期化するとき、メモリ上にあるFDTの対応ノードの情報を構造体struct device_nodeポインタとして、struct platform_deviceのメンバdev.of_nodeとして渡します。ドライバはこのof_nodeからプロパティを参照することができます。たとえばdrivers/spi/spi-imx.cの場合、

 

static int spi_imx_probe(struct platform_device *pdev)
{
        struct device_node *np = pdev->dev.of_node;
       :
        ret = of_property_read_u32(np, "fsl,spi-num-chipselects", &num_cs);
        :

 

のようなコードで、プロパティ"fsl,spi-num-chipselects"の値をint値num_csとして取得しています。(ecspi4=ecspi@02014000では1が取得されるはずです)。

一方、レジスタアクセスのアドレス(0x02014000)の取得は定式化されていて、プロパティ値の直接読み出しではなく専用のAPIを呼び出しています。

 

        res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
        spi_imx->base = devm_ioremap_resource(&pdev->dev, res);

 

ノードのregプロパティで指定されたアドレス範囲が「リソース」として解釈されstruct platform_deviceに格納されており、API2つの呼び出しで該当範囲のカーネル空間への割り当てとポインタの取得ができる仕組みになっています。こういう「お約束」は定型的な処理を統一文法で書けて一括処理できるので便利な反面、「そういうお約束がある」ことを知らないとぱっと見では何が何だかわかりにくくなるので、功罪相半ばするところがあります(※註)。

 

(※註) 個人的には、DTSの「とっつきにくさ・わかりにくさ」の殆どはDTS文法そのものではなく、「LinuxにおけるDTSの書き方のお約束」だと感じています。

何かの事情でカーネル内から直接FDTを参照したい場合、幾つかのAPIが用意されています。

 

struct device_node *of_find_node_opts_by_path(const char *path, const char **opts)
 

もっとも基本的なAPIで、pathで指定されたFDTノードを検索し、あればstruct device_node*ポインタを返します。pathは「/」で始まっていればフルパスですが、そうでない場合は/aliases/の下から検索されます(わかりにくい)。optsはもっとわかりにくい引数で、pathに渡した文字列に含まれる":"直後のポインタを返します。DTSの文法でノード名に":"を含めることはできず、:から後ろは検索一致対象にもなりません。Linuxの内部で何かの目的に使っているようですが、正直よくわかりません。

返されたポインタはrefcountが増やされているので、使い終わったらof_node_put()を呼び出してrefcountを減らす必要があります(これを忘れると、ものすごく追いかけにくいカーネルパニックの原因になります)。

 

struct device_node *of_find_node_by_name(struct device_node *from, const char *name)
 

FDTのノード名ではなく、ノードのnameプロパティを検索します。DTSソースにはname=というプロパティは直接書かないのですが、Linuxが「ノード名の@以後を外した文字列」として自動的に生成します。つまりserial@02020000ならname="serial"、ecspi@02008000ならname="ecspi"が自動的に生成されるわけです。同じnameを持つノードは複数存在することになるので、fromは親ノードではなく「検索の開始点」を示すポインタで、最初の検索ではNULLを指定します。

返されたデバイスノードポインタのrefcountが増やされることは同じですが、返された値をそのままfromに渡して「数珠つなぎ探索」する場合は、of_node_put()を呼び出してはいけません!fromに対するof_node_put()は内部で行われているので、多重解放が起きてこれまたカーネルパニックが起きます。

 

struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compatible)
 

FDTノードのcompatibleプロパティに羅列された文字列リストから、compatbileで指定した文字列に一致するノードを検索します。typeはdevice_typeプロパティの一致を指定するものですが、device_typeは既に廃止された盲腸(※註)なので、通常はNULLを指定します。refcountに対する考慮や数珠つなぎ検索における扱いは上のAPIと同じです。

(※註) https://elinux.org/Device_Tree_Linux#device_type_property に一覧がありますが、殆ど使われていません。現用のLinux DTSにも"cpu" "memory" "pci"とかのdevice_type宣言が残っていますが、「消すと古いソフトが動かなくなるかも知れないから残しておこう」くらいの意味しか持っていません。

 

 

struct platform_device *of_find_device_by_node(struct device_node *np)
 

ノードnpに対応するデバイスドライバの実体(インスタンス)を検索し、あればstruct platform_device *として返します。

 

ちょっとめんどくさいのは、ドライバをカーネル組み込みではなく*.koとしてinsmodであとからロードする場合です。FDTのなかにstatus="okay"のノードが存在した場合、対応するstruct platform_deviceの実体はカーネルが先に確保しています。しかしcompatible=に合致するドライバがカーネル内に存在せずmodprobeで拾うこともできなないと、platform_device.devは未初期化のままになります(※註)。

(※註) devp = of_find_device_by_node(np);は非NULLが返されますが、platform_get_drvdata(devp)するとNULLが返ってくる状態になります。

 

あとから*.koを読み込んだとき、ドライバの初期化処理のなかでplatform_device_alloc()を使って自力でドライバ実体を確保すると、同じ名前のドライバ実体が2つ存在する状態になります。そしてFDTノードと「紐づけ」されているのは先に作られた方だけなので、of_find_device_by_node(np)で検索するとポインタは返ってくるものの、実際にロードされて動いているドライバとは別の(devが初期化されていない)実体を拾ってくる齟齬が生じます。

つまり、FDTに紐づける可能性があるドライバについては、of_find_compatible_node() -> of_find_device_by_node()で「対応するFDTと、それに紐づいたドライバ実体」を検索し、それが無かった場合のみplatform_device_alloc()で確保するようにしなければなりません。

 

int of_property_read_string(struct device_node *np, const char *propname, const char **out_string)
 

ノードnpからpropnameで指定したプロパティを検索し、あればout_stringにポインタを入れて0を返します。無ければ何らかのマイナス値(-EINVAL)が返ります。他にもof_property_read_u32とかof_property_read_boolとか幾つもの型に対応したAPIがあります。FDT APIは読み出しのみで、書き込むことはできません。ノードやプロパティを追加することもできません。プロパティは「読み出しのみ」なので排他制御は必要なく、ノードと違ってrefcountもありません。

 

 

オーバーレイ

Linux起動後にFDTを書き換えることはできませんが、Linux起動前にFDTを上書き変更することは可能で、この処理を「オーバーレイ」と呼びます。オーバーレイのソースコードもDTS文法で書きDTCでコンパイルしますが、拡張子は*.dtboにする慣習があります。

たとえばiMX6ULLのDTSに対し、uart3(ttymxc2)を禁止するオーバーレイを書いてみます。

 

disable-uart3.dts
/dts-v1/;
/plugin/;
/ {
        fragment@1 {
                target=<&uart3>;
                __overlay__ {
                        status = "disabled";
                };
        };
};

 

これをコンパイルしてdisable-uart3.dtboを作る手順は普通のDTSと同じです。

 

dtc disable-uart3.dts -o disable-uart3.dtbo

 

オーバーレイをどうやって適用するかはブートローダーによって異なります。Raspberry Piの場合は/boot/overlaysの下に*.dtboファイルが格納されており、/boot/config.txtの末尾に

 

[all]
dtoverlay=disable-bt

 

のような宣言を書くことで、次回起動時にdisable-bt.dtboが読み込まれて適用されます。

 

U-Bootの場合は、例えば

 

sf read ${fdt_addr} 0x00050000 0x10000;

 

のようにメモリ上に原型FDTがロードされた状態から、

 

tftp ${loadaddr} disable-uart3.dtbo
fdt addr ${fdt_addr}
fdt resize 65536
fdt apply ${loadaddr}

 

のように、何らかの方法でdtboをロードして(ここではTFTPを使用)、fdt applyコマンドにそのアドレスを渡すことでメモリ上のFDTを上書きします。

...しかし、SX-590ではU-Bootのバージョンが古すぎてfdt applyコマンドに対応していないので、実際にオーバーレイ動作を試すことはできません!また上で少し言及したように、オーバーレイを適用するためにはdtcに-@オプションを渡してシンボルテーブルつきのFDT/DTBを生成する必要がありますが、Linux標準のビルド手順ではこれも生成されません。

なのでオーバーレイ機能検証のために、U-Bootのバージョンが新しいiMX8M Plus EVKを使ってみました。これを選んだのはたまたま仕事で関わって手元に現物と環境があったからで、それ以上の理由はありません。

まずiMX8M Plusのビルド環境を作り、Yoctoのビルドディレクトリでimx8mp-evk.dtsをプリプロセッサにかけたあと、-@を付けてdtcでコンパイルします。生成されたDTBおよびDTBOファイルはあとでU-Bootに転送するため、TFTPサーバにアップロードします。

(※註) USBメモリを使う方法もありますが、解説は省略します。

 

# cd tmp/work-shared/imx8mpevk/kernel-source
 cpp -I include -I arch -undef -x assembler-with-cpp arch/arm64/boot/dts/freescale/imx8mp-evk.dts > /tmp/test.dts
# dtc -@ -i dts /tmp/test.dts -o /tmp/test.dtb
# dtc disable_uart3.dts -o /tmp/disable_uart3.dtbo

 

iMX8MのブートローダーでUARTコンソールからEnterを連打して自動ブートを止め、"u-boot=>" のプロンプトが出た状態から、マニュアルコマンドでTFTPサーバからDTBおよびDTBOをRAM上にロードし、

 

run mmcargs
setenv ipaddr <空いているIPアドレス>
setenv serverip <DTBとDTBOファイルを取得するTFTPサーバのIPアドレス>
tftp ${fdt_addr_r} test.dtb
fdt addr ${fdt_addr_r} 100000
tftp ${loadaddr} disable-uart3.dtbo

 

"fdt apply"コマンドでDTBにオーバーレイを適用したあと、カーネルをロードして起動します。

(※註) ブートモードはMMCを仮定しています。このへんの話を始めると長くなるので、これも省略します)。

 

fdt apply ${loadaddr}
run loadimage
booti ${loadaddr} - ${fdt_addr_r}

 

これでブートに成功すれば、cat /sys/firmware/devicetree/bas/soc@0/bus@30800000/serial@30880000/statusが"okay"から"disabled"に変わり、/dev/ttymxc2が消えているはずです。なんか微妙に不便でめんどくさい機能ですが、とにかくこういう方法でコンパイル済のDTBを部分的に変更するのがオーバーレイです。

なお、iMX8Mのブートパーティションに書かれている(および、Yoctoの標準ビルド手順でwork/imx8mpevk-poky-linux/linux-imx/<カーネルバージョン>/build/arch/arm64/boot/dts/下に作られる)デフォルトのDTBファイルは-@つきでコンパイルされていないので、ftd applyコマンドを実行すると

 

failed on fdt_overlay_apply(): FDT_ERR_NOTFOUND
base fdt does did not have a /__symbols__ node
make sure you've compiled with -@

 

というエラーが出て弾かれます。

 

 

まとめ

いろいろ書いてきましたが、ざっくりまとめると

 

・DTの文法は基本的に単純で、「ノード」と「プロパティ」しかない。
・ノード名やプロパティ名に付く#や,に特別な意味は無いが、&(参照)や@(リソース宣言)には意味がある。
・プロパティの=右辺に書かれる値は原則として<>で囲めば数値、それ以外は文字列になる。","で区切れば列挙型(配列)になる。&labelはそのまま書けば文字列、<>で囲えばphandle値になる。
・#address-cells, #size-cells, reg, phandle, compatible, statusなどのプロパティ名には「LinuxにおけるDTのお約束」として特別な意味が割り当てられている。
・パワー・クロック・PINMUXなどのプロパティにはチップ固有のお約束・流儀・慣習があり、DTSだけ見ても何が何だか訳がわからないことが多い。チップマニュアルを熟読し照らし合わせて理解する必要がある。
・DTSは同じパスのノード・プロパティが後から宣言されても多重定義エラーにはならず、既存宣言への追加・上書きとして処理される。
・ノード名やプロパティ名の手前に:で区切って「ラベル」を付けることができる。ラベルは原則としてコンパイラ内部で使う識別子で、コンパイルされたDTBには出力されない。
・既存ノードへの追加上書きを行う場合、{}の重ね書きではなく&ラベルを使った参照表記が用いられることが多い。
・ラベルの特例的な使用法として「オーバーレイ」によるDTBの上書き変更機能があり、この場合はDTCに-@オプションを渡して「シンボルテーブルつき」のDTBを生成しなければならない。

 

...といったところでしょうか。

 

DTについては体系的に仕様を理解しないまま、既存DTSの切った貼ったでビルドして「なんかよくわからんけど動いたからヨシ!」「ぜんぜんわからない、俺たちは雰囲気でDTSを書いている」という場合も少なくないのではないかと思います(他ならぬ私がそうでした)。今回は仕事でどっぷり関わってDTを1から勉強しなおす必要があったので、そのときの経験を元に記事を書いてみました。何かの役に立ったのなら幸いです。

 

関連製品

関連リンク

製品のご購入・サービスカスタマイズ・資料請求など
お気軽にお問い合わせください