2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

nRF Connect SDKによるBLEアプリケーション開発1:デバイスツリーの作成

Last updated at Posted at 2024-07-21

記事の概要

Nordic社製のBLEモジュール nRF53 を用いて、nRF Connect SDKによりBLEアプリケーションを開発する方法を紹介します

今回はボードの設定として用いるデバイスツリーを作成します

デバイスツリーについて

マイコンのアプリケーションには、マイコンが実装された基板(以降、ボード)の設定を与えないといけません。

ボードの設定とは、「出力ピンがポート0の5番で、I2CのSCLがポート1の20番と1の23番だ」というIF設定や「ROMのアドレスが0x10000で、SRAMのアドレスが0x22000、周辺機能Aのレジスタは開始アドレスが080000から0x9FFFFまで」であるというメモリマップ設定などのことです。

多くのマイコン開発環境ではボード設定用のヘッダーファイルを作成し、そこにピン番号などを定義していますが、nRF Connect SDKではデバイスツリーを使用します。
デバイスツリーとはハードウェアの構成を記述するためのフォーマットです。ファイル拡張子は dts もしくは dtsi になります。

デバイスツリーの仕様は以下のサイトのリンクからGithubに飛んで入手できます

仕様を要約してくれている記事もありました

また、Nordicの演習記事も参考になります

市販されている様々な Nordic の開発ボードに対応したデバイスツリーは nRF Connect SDK と一緒にインストールされるサンプル、もしくは以下から見ることができます

デバイスツリーの構造

デバイスツリーは、ノードとプロパティからできています。
デバイスツリーを作成するというのは、ボードの構成をノード構造に置き換え、そのノードにプロパティを記述する作業になります。

https://docs.zephyrproject.org/latest/build/dts/intro-syntax-structure.html では、DTSファイルの一例として以下が挙げられています。

/ {
        a-node {
                subnode_label: a-sub-node {
                        foo = <3>;
                };
        };
};

/はルートノードと呼ばれるツリーの根本になるノードです。デバイスツリーは、ルートノードから無数のノードが分岐する構造になります。
上記の例では、ルートノードの下にノードa-node、ノードa-nodeの下にサブノードa-sub-nodeがあります。

ノードとサブノードは以下の規則で記述されます。

ラベル: ノード名@ユニットアドレス {
    プロパティ

    ラベル: サブノード名@ユニットアドレス {
        プロパティ
    };
};

ラベルとユニットアドレスは省略可能です。
最初の例ではサブノードa-sub-nodeにラベル subnode_labelがつけられています。

ユニットアドレスは、ノードのメモリ空間内のアドレスを示しています。

例えば、以下ではCPUノードがアドレス2に位置することを示しています。

zephyr\dts\common\nordic\nrf54h20_enga.dtsi
cpuapp: cpu@2 {

以下ではSPIノードがアドレス8e6000から始まることを示しています。

zephyr\dts\common\nordic\nrf54h20_enga.dtsi
spi120: spi@8e6000 {

また、ノードの詳細な情報はプロパティで与えます。
最初の例では、サブノードa-sub-node{}内に、プロパティfoo = <3>が記述されています。

DTSバージョンのタグ

DTSファイルについては、最初の行に以下のタグがあります。

/dts-v1/;

このタグは、DTSファイルがDTSのバージョン1に従うことを示しています。
このタグがないDTSファイルはバージョン0として認識されてしまいます。

プロパティ

プロパティはプロパティ名とプロパティ値から構成されます。

プロパティ名 = プロパティ値;

以下では様々なプロパティの中身と実際の使われ方を確認したいと思います

compatible

compatibleプロパティは、ノードが、何のハードウェアについてのノードなのかを表しています。

アプリケーションは、compatibleプロパティを参照することで、ボードが使用しているデバイスに対応するデバイスドライバーを選択します。

プロパティ値は""で囲った文字列になります。,で区切って複数の文字列を与えることもできます。
典型的なプロパティ値は"製造元,モデル名"となります。

以下にサンプルから具体例を示します。

zephyr\dts\common\nordic\nrf54h20_enga.dtsi
cpuapp: cpu@2 {
    compatible = "arm,cortex-m33";

この compatible は、ノードcpuが ARM社の Cortex-M33 についてのノードであることを示しています。

zephyr\boards\arm\nrf54h20pdk_nrf54h20\nrf54h20pdk_nrf54h20_cpuapp.dts
/ {
	compatible = "nordic,nrf54h20pdk_nrf54h20-cpuapp";

この compatible はルートノードのプロパティであり、 このファイルが記述するデバイスそのものを示しており、それがNordic社のモデル「nrf54h20pdk_nrf54h20-cpuapp」 であると分かります

zephyr\boards\arm\nrf5340dk_nrf5340\nrf5340_cpuapp_common.dtsi
	leds {
		compatible = "gpio-leds";

一般的なハードウェアについては製造元を記述しなくても問題ありません。
上記の例では、GPIOによるLEDの仕組みは製造元に依存しないので、単にgpio-ledsとしています。

Nordic 製品 Compatible の固有プロパティ

プロパティには Compatibleで設定した製品に固有のプロパティがあります。
Nordic 製品に対応した固有プロパティは以下で確認できます

例えば compatible = "nordic,nrf-spim"ならばdtbinding-nordic-nrf-spimに固有プロパティが列挙されています。

これらの固有プロパティの中で、SoCレベルのDTSファイルでの設定が必須なのは以下になります。

  • max-frequency
    • SPI通信のスレーブ側を動作させられる最大周波数[Hz]
  • easydma-maxcnt-bits
    • EasyDMA MAXCNTレジスタの最大値

以下にサンプルから具体例を示します。

zephyr\dts\common\nordic\nrf54h20_enga.dtsi
spi120: spi@8e6000 {
    compatible = "nordic,nrf-spim";
    reg = <0x8e6000 0x1000>;
    status = "disabled";
    easydma-maxcnt-bits = <15>;
    interrupts = <230 NRF_DEFAULT_IRQ_PRIORITY>;
    max-frequency = <DT_FREQ_M(32)>;
    #address-cells = <1>;
    #size-cells = <0>;
};

compatible = "nordic,nrf-spim"の必須固有プロパティである最大周波数が32MHzで、EasyDMA MAXCNTレジスタの最大値が15bit(32,767)に設定されています。

model

modelプロパティは、デバイスの製造元モデルを表しています。
主にルートノードのプロパティとして使用します。

プロパティ値は""で囲った文字列になります。,で区切って複数の文字列を与えることもできます。

以下にサンプルから具体例を示します。

zephyr\boards\arm\nrf5340dk_nrf5340\nrf5340dk_nrf5340_cpuapp_ns.dts
/ {
	model = "Nordic NRF5340 DK NRF5340 Application";
	compatible = "nordic,nrf5340-dk-nrf5340-cpuapp";

ルートノードのmodeleプロパティは、このデバイスが Nordic社のnRF5340のDKボード(開発ボード)であることを示しています。

status

statusプロパティは、ノードの記述するデバイスが使用可能かどうかを示しています。

プロパティ値は""で囲った以下の文字列のどれかになります。
ただし、 Zephyr ではokaydisabled以外は使用しません。

  • okay
    • デバイスは使用可能
  • disabled
    • デバイスは使用不可、ただし使用可能に遷移することができる
  • reserved
    • デバイスは使用不可、かつ使用可能に遷移することもできない
  • fail
    • デバイスは深刻なエラーにより使用不可
  • fail-sss
    • デバイスは深刻なエラーにより使用不可、sssには検知したエラーを示す値が表示される

ノードにstatusプロパティを設定しない場合は、自動でokayに設定されます。

以下にサンプルから具体例を示します。

zephyr\boards\arm\nrf5340dk_nrf5340\nrf5340_cpuapp_common.dtsi
&i2c1 {
	compatible = "nordic,nrf-twim";
	status = "okay";

上記は、nRF5340のI2Cモジュールが使用可能であることを示しています。

zephyr\dts\common\nordic\nrf54h20_enga.dtsi
timer021: timer@29000 {
   compatible = "nordic,nrf-timer";
   reg = <0x29000 0x1000>;
   status = "disabled";

上記は、タイマーが使用不可であることを示しています。

#address-cells および #size-cells

#address-cellsプロパティは、同じノードの ranges および 子ノードの regプロパティのアドレスフィールドのセル数を設定します。

#size-cellsプロパティは、同じノードの ranges および 子ノードの regプロパティのサイズフィールドのセル数を設定します。

どちらのプロパティ値も<>で囲ったu32型の数値になります。

このプロパティを設定しないと、自動で#address-cellsプロパティ値は2に、#size-cellsプロパティ値は1に設定されます。

reg

regプロパティは、アドレスフィールドとサイズフィールドから構成されます。

プロパティ値は<>で囲ったu32型の数値になります。

#address-cells と #size-cellsの設定値を見ないとフィールドがアドレスフィールドなのか、それともサイズフィールドなのかが分かりません。
例えば、regプロパティの設定が

reg = <1000 100>

となっていても、#address-cells が1で #size-cells も1ならば、アドレスフィールドが1000で、サイズフィールドが100になりますが、#address-cells が2で #size-cells が0ならば、アドレスフィールドが1000<<32 + 100で、サイズフィールドはなしになります。
同じ設定値でも意味がまるで異なります。

また、その際に確認するのは自ノードの#address-cells と #size-cellsではなく、親ノードの#address-cells と #size-cellsであることに注意します。

つまり reg の設定は以下の組み合わせになります

reg = <親ノードの#address-cells 親ノードの#size-cells>

例えば、親ノードの #address-cells と #size-cells の設定値が 1として

reg = <0x100 0x10>;

と書けば、アドレス範囲は 0x100 から 0x1ff になります。

複数のアドレス範囲を同時に指定することもできます。
例えば、#address-cells と #size-cells の設定値が 1として

reg = <0x100 0x10 0x2000 0x100>;

と書けば、アドレス範囲は 0x100 から 0x1ff および 0x1000 から 0x1fffとなります。

64bit のノードの場合は以下のように記述します。

parent_node {
    #address-cells = <2>;
    #size-cells = <2>;
    
    child_node@100000000  {
        reg = <0x000000001 0x00000000 0x00000000 0x80000000>;
    };
};

上記の例では、reg = <64bitのアドレス 64bitのサイズ>であり、それぞれに2個ずつ32bitの値が並んでいます。
child_nodeは開始アドレスが0x100000000 で、そのサイズは0x80000000であることを示しています。

ranges

ranges プロパティは、自ノードのバスのアドレス空間と親ノードのアドレス空間の対応関係を設定します。
プロパティ値は<>で囲ったu32型の数値になります。

ranges の設定は以下の組み合わせになります

ranges = <自ノードの#address-cells 親ノードの#address-cells 自ノードの#size-cells>

以下にサンプルから具体例を示します。

zephyr\dts\common\nordic\nrf54h20_enga.dtsi
global_peripherals: peripheral@5f000000 {
		#address-cells = <1>;
		#size-cells = <1>;
    ...(略)...
    cpuppr_vpr: vpr@908000 {
        compatible = "nordic,nrf-vpr-coprocessor";
        reg = <0x908000 0x4000>;
        ...(略)...
        #address-cells = <1>;
        #size-cells = <1>;
        ranges = <0x0 0x908000 0x4000>;

rangesプロパティは、子ノードvprのアドレス0は、親ノードperipheralのアドレス0x908000に対応しており、vprのサイズは0x4000であることを示しています。

reg と ranges と #address-cells および #size-cells の使用例

#address-cells と #size-cells の組み合わせで様々なデバイスを表現できます。
具体的に幾つかの例を見てみます。

CPUの場合

32bit のCPUノードは#address-cells を1に、#size-cells を0に設定します。
64bit のCPUノードは#address-cells を2に、#size-cells を0に設定します。

よって reg プロパティはアドレスフィールドのみを持ちます。
#size-cellsが0になっている理由は、各CPUは1つのアドレスしか割り当たらないので、アドレス範囲を指定する必要がないからです。
(次に紹介するメモリマップドデバイスにはアドレス範囲のサイズ設定が必要になります。)

以下にサンプルから具体例を示します。

zephyr\dts\common\nordic\nrf54h20_enga.dtsi
cpus {
    #address-cells = <1>;
    #size-cells = <0>;

    cpuapp: cpu@2 {
        compatible = "arm,cortex-m33";
        reg = <2>;
        device_type = "cpu";
        clock-frequency = <DT_FREQ_M(320)>;
    };

    cpurad: cpu@3 {
        compatible = "arm,cortex-m33";
        reg = <3>;
        device_type = "cpu";
        clock-frequency = <DT_FREQ_M(256)>;
    };

cpusノードの下にCPUノードが2つあります。
これはnRF54H20が2つの32bit CPUを持っていることを示しています。
そして regプロパティを見ると、1つにはアドレス 2 が、もう1つにはアドレス 3 が割り当てられていることが分かります。

周辺機能の場合

マイコンの周辺機能はメモリマップドデバイスであり、ある一定のアドレス範囲を与えられます。
例えばGPIO機能はアドレス0x2900から0x31FFまで(これらのアドレスはレジスタのアドレスになります。例えばP0DDRレジスタは0x2900、P8DRレジスタは0x2C80などです)とか、UART機能は0x3200から0x4AFFまでといったようなアドレス範囲を持ちます。

周辺機能アドレスについては以下もご参照ください

32bit の周辺機能ノードは#address-cells と #size-cells を1に設定します。
64bit の周辺機能ノードは#address-cells と #size-cells を2に設定します。

以下にサンプルから具体例を示します。

zephyr\dts\common\nordic\nrf54h20_enga.dtsi
global_peripherals: peripheral@5f000000 {
		#address-cells = <1>;
		#size-cells = <1>;

        timer120: timer@8e2000 {
            compatible = "nordic,nrf-timer";
            reg = <0x8e2000 0x1000>;
            ...(略)...
        };
        
        spi120: spi@8e6000 {
            compatible = "nordic,nrf-spim";
            reg = <0x8e6000 0x1000>;
            ...(略)...
            #address-cells = <1>;
            #size-cells = <0>;
        };

        cpuppr_vpr: vpr@908000 {
            compatible = "nordic,nrf-vpr-coprocessor";
            reg = <0x908000 0x4000>;
            ...(略)...
            #address-cells = <1>;
            #size-cells = <1>;
            ranges = <0x0 0x908000 0x4000>;

				cpuppr_vevif_remote: mailbox@0 {
					compatible = "nordic,nrf-vevif-remote";
					reg = <0x0 0x1000>;
                ...(略)...
				};

				cpuppr_clic: interrupt-controller@1000 {
					compatible = "nordic,nrf-clic";
					reg = <0x1000 0x3000>;
                ...(略)...
				};
        };

上記の例では、ノードperipheralでは#address-cells が1 で、#size-cells も1に設定されています。
よって、子ノードtimerspivprregのアドレスフィールドには1個のアドレス、サイズフィールドには1個のサイズが設定されています。
例えば timerのアドレス範囲は 0x8e2000 から 0x8e2fff までということが分かります。

ここで親ノードと自ノードの#address-cellsプロパティや#size-cells プロパティを混同しないように注意します。

ノードspiに設定された#address-cellsプロパティ は1 で、#size-cellsプロパティは0になっていますが、これはノードspiに対する子ノードの設定なので、spiの reg プロパティは親ノードの#address-cells と #size-cells の設定値である 1 に従います。

#size-cellsが0でないのはメモリマップドデバイスではないことを意味するので、spiから分岐する子ノードはアドレス範囲を持ちません。
つまり、spiとその子ノードは同一のアドレス空間にあります。

一方、vprに設定された#address-cellsプロパティ は1 で、#size-cellsプロパティは1です。
これはvprから分岐する子ノードもメモリマップドデバイスであり、アドレス範囲を持つことを示しています。

ranges で説明したようにvprのアドレス0は、親ノードperipheralのアドレス0x908000に対応しており、mailboxのアドレス範囲は0x908000から0x909000、interrupt-controllerのアドレス範囲は0x909000から0x90c000になります。

外部バスに接続されたデバイスの場合

例えば、マイコンにFLASHメモリを接続した場合、マイコンのCPUは外部バスを介してFLASHメモリのアドレスにアクセスします。
具体例は以下を参照してください

FAQ 1000518 : V850マイコンの外部バス・インターフェースとは、何ですか?

外部バスに接続されたデバイスは#address-cells を2に、#size-cells を1に設定します。
具体例として以下のサイトの例を引用します。

/dts-v1/;

/ {
    compatible = "acme,coyotes-revenge";
    #address-cells = <1>;
    #size-cells = <1>;
    ...
    external-bus {
        #address-cells = <2>;
        #size-cells = <1>;
        ranges = <0 0  0x10100000   0x10000     // Chipselect 1, Ethernet
                  1 0  0x10160000   0x10000     // Chipselect 2, i2c controller
                  2 0  0x30000000   0x4000000>; // Chipselect 3, NOR Flash

        ethernet@0,0 {
            compatible = "smc,smc91c111";
            reg = <0 0 0x1000>;
        };

        i2c@1,0 {
            compatible = "acme,a1234-i2c-bus";
            #address-cells = <1>;
            #size-cells = <0>;
            reg = <1 0 0x1000>;
            rtc@58 {
                compatible = "maxim,ds1338";
                reg = <58>;
            };
        };

        flash@2,0 {
            compatible = "samsung,k8f1315ebm", "cfi-flash";
            reg = <2 0 0x4000000>;
        };
    };
};

external-busは親ノードであるルートノードの#address-cellsが1で、自ノードの#address-cellsが2で、#size-cellsが1なので rangesプロパティは

<2個のアドレスセル 1個のアドレスセル 1個のサイズセル>

となります。

外部バスに接続されたデバイスノードにおける ranges の意味は、

<チップセレクト番号  チップセレクトのベースアドレスからのオフセット 親ノードが外部デバイスにアクセスする為のアドレス 親アドレス空間における外部デバイスのサイズ>

となります。
例えば、FLASHはregプロパティのアドレスフィールドは2 0であり、rangesプロパティの<2 0 0x30000000 0x4000000> がアドレス空間の対応関係になります。
よってFLASHは外部バスのチップセレクト番号2番に接続され、アドレスは0x4000000にまで拡張され、これは親ノード(CPU)のアドレス空間 0x30000000から0x34000000に対応することが分かります。

device_type

device_typeプロパティはIEEE 1275 で定められた FCcode プログラミングモデルを設定します。
プロパティ値は""で囲った文字列になります。

IEEE 1275 に基づくデバイスツリーとの互換性の為に必要な設定で、CPUノードとmemoryノードでのみ用います。

phandle

phandleプロパティは、デバイスツリー内でノードを一意的に特定する為に割り当てるID番号です。

プロパティ値は<>で囲ったu32型の数値になります。

多くのDTSファイルではphandleプロパティは省略されています。
phandleプロパティがないノードについては、自動でID番号が割り当てられます。

実際、nRF Connect SDKのサンプルのdtsファイルでphandleをわざわざ設定しているものは見つかりませんでした。

その他

プロパティは他にも以下がありますが、今後の記事では使用しないので解説を省略します。

  • virtual-reg
  • dma-ranges
  • dma-coherent
  • dma-noncoherent
  • name

割り込み設定のプロパティ

割り込みは、GPIOの入力割り込みやタイマーカウンター割り込み、SPIデータ受信割り込みなど、周辺機能で主に使用されます。

以下では割り込みに関するプロパティを紹介します

interrupt-controller

interrupt-controllerプロパティは、ノードが割り込みコントローラであることを示しています。
プロパティ値はありません。

#interrupt-cells

#interrupt-cellsプロパティは、子ノードの interrupts プロパティが幾つのセル数を持つかを設定します。

プロパティ値は<>で囲ったu32型の数値になります。
このプロパティを設定しない場合は、自動で2を設定したのと同じことになります。

interrupt-parent

interrupt-parentプロパティは、自ノードに対する割り込み出力先がどのノードなのかを、ノードのphandle番号で指定します。

このプロパティを設定しない場合、自動で親ノードが選択されます。
デバイスツリーが複雑になると、必ずしも親ノードが割り込み出力先ではないので、直接にノードを指定できるようになっています。

プロパティ値は<>で囲ったphandleの値になります。

phandle番号は自動で設定されるので、番号が分からないという場合は、ラベル名を用いてinterrupt-parent = <&対象ノードのラベル名>のように設定します。

以下にサンプルから具体例を示します。

C:zephyr\dts\common\nordic\nrf54h20_enga.dtsi
cpuppr_clic: interrupt-controller@1000 {
    ...(略)...
    #interrupt-cells = <2>;
    interrupt-controller;
    ...(略)...
};

...

cpuppr_vevif_local: mailbox {
    ...(略)...
    interrupt-parent = <&cpuppr_clic>;

ノードmailboxは、割り込みコントローラにノードcpuppr_clicを指定しています

interrupts

interruptsプロパティはノードの割り込みを設定します。
プロパティ値は<>で囲ったu32型の数値になります。

以下にサンプルから具体例を示します。

zephyr\dts\common\nordic\nrf54h20_enga.dtsi
spi121: spi@8e7000 {
    compatible = "nordic,nrf-spim";
    ...(略)...
    interrupts = <231 NRF_DEFAULT_IRQ_PRIORITY>;

interrupt-parentは設定されていないので、親ノードperipheralが割り込み出力先になります。

#interrupt-cellsプロパティを設定していないのでセル数は自動で2個になり、プロパティ値はinterrupts = <割り込み番号 割り込み優先度>となります。
周辺機能の割り込み番号はマイコンのマニュアルに記載されている値を用います。

SPIの割り込み番号は231で、割り込み優先度は NRF_DEFAULT_IRQ_PRIORITY で定義された定数値を用いています。

その他

割り込みのプロパティは他にも以下がありますが、今後の記事では使用しないので解説を省略します。

  • interrupts-extended
  • interrupt-map
  • interrupt-map-mask

特別なノード

ノードは自由な名前を設定できますが、特定の役割を担うノードの名前は決まっており、その役割以外のノードがその名前を使用することはできません。

例えば、デバイスツリーは必ずルートノード/から始まり、1つのcpusノードと1つ以上のmemoryノードを持たないといけません。
これらのノード名は、他のノードでは使用できません。

以下ではそれらの特別なノードを紹介します。

ルートノード

デバイスツリーはルートノード/から始まります。
設定できるプロパティは以下になります

  • #address-cells
  • #size-cells
  • model
  • compatible
  • serial-number
  • chassis-type

aliasesノード

aliasesノードはエイリアス(コマンドや機能の別名)を定義します。
aliasesノードは必ずルートノードの子ノードにしないといけません。

aliasesノードは以下の構造になります。

/ {
        aliases {
                エイリアス名 = &ノードラベル;
        };
};

具体例を以下に示します。

   / {
   	aliases {
   		led0 = &myled0;
   	};

   	leds {
   		compatible = "gpio-leds";
   		myled0: led_0 {
   			gpios = <&gpio0 13 GPIO_ACTIVE_LOW>;
                };
   	};
   };

led0をノードled_0のラベル名myled0の参照として定義しています。

ボードが変更になった時、エイリアスの参照先を変更すればいいので、プログラムの修正が楽になります。

chosenノード

chosenノードはパラメータを定義します。
chosenノードは必ずルートノードの子ノードにしないといけません。

chosenノードでは、以下のZephyr固有ノードを定義することもあります。

以下に具体例を示します。

zephyr\boards\arm\nrf5340dk_nrf5340\nrf5340dk_nrf5340_cpuapp_ns.dts
chosen {
    zephyr,sram = &sram0_ns;
    zephyr,flash = &flash0;
    zephyr,code-partition = &slot0_ns_partition;
};

cpusノード

cpusノードはシステムのCPUをまとめたノードです。
デバイスツリーは必ず1個のcpusノードを持たないといけません。
そして、cpusノードは必ずルートノードの子ノードになり、cpusの子ノードはcpuノードになります。

設定できるプロパティは以下になります

  • #address-cells
  • #size-cells

cpuノード

cpuノードは必ずcpusノードの子ノードになります。
設定できるプロパティは多いので、ここでは省略します。

プロパティの説明でも引用しましたが、具体例を以下に示します

zephyr\dts\common\nordic\nrf54h20_enga.dtsi
	cpus {
		#address-cells = <1>;
		#size-cells = <0>;

		cpuapp: cpu@2 {
			compatible = "arm,cortex-m33";
			reg = <2>;
			device_type = "cpu";
			clock-frequency = <DT_FREQ_M(320)>;
		};

		cpurad: cpu@3 {
			compatible = "arm,cortex-m33";
			reg = <3>;
			device_type = "cpu";
			clock-frequency = <DT_FREQ_M(256)>;
		};

cpusノードの子ノードとして2つの ARM Cortex-m33 のcpuノードが設定されています。

memoryノード

メモリ空間の設定を行うノードです。
説明は省略します。

reserved-memoryノード

あらかじめシステムが予約しており、ユーザーは使用できないメモリ空間の設定を行うノードです。
見れば内容が分かると思いますので、説明は省略して具体例だけを以下に示します

zephyr\dts\common\nordic\nrf54h20_enga.dtsi
reserved-memory {
    #address-cells = <1>;
    #size-cells = <1>;

    cpurad_uicr_ext: memory@e1ff000 {
        reg = <0xe1ff000 DT_SIZE_K(2)>;
    };

    cpuapp_uicr_ext: memory@e1ff800 {
        reg = <0xe1ff800 DT_SIZE_K(2)>;
    };
};

デバイスツリーファイルの種類

サンプルで使用されているマイコンのデバイスツリーは、1つのファイルに全てが記述されてはいません。
役割に応じて、幾つかの種類のデバイスツリーファイルに分けられています。

以下ではそれらのデバイスツリーファイルの種類について説明します

ベース SoC デバイスツリー

CPU、メモリ、周辺機能などのSoCレベルのハードウェアについて記述した SoC デバイスツリーが、nRF52832、nRF52840、nRF5340などのnRマイコンの各シリーズごとに存在します。

SoC デバイスツリーには、以下のようにcpusノード、socノード、そして周辺機能や割り込み、クロックに関する設定がされています。

zephyr\dts\arm\nordic\nrf5340_cpuappns.dtsi
/ {
	cpus {
		#address-cells = <1>;
		#size-cells = <0>;

		cpu@0 {
			device_type = "cpu";
			compatible = "arm,cortex-m33f";
           ...(略)...
		};
	};
 
	soc {
		sram0: memory@20000000 {
			compatible = "mmio-sram";
		};

		peripheral@40000000 {

SoC 差分デバイスツリー

同じ製品シリーズでもSoCのバージョンの異なる場合があります。
例えば、nRF52340にはQIAAとCKAAという異なるバージョンがあります。

それらの差分にについて記述したSoC 差分デバイスツリーがあります。

例えば、nRF5340のバージョン QKAA については、FLASHとSRAMなどを設定した以下のデバイスツリーファイルが存在します

zephyr\dts\arm\nordic\nrf5340_cpuappns_qkaa.dtsi
&flash0 {
	reg = <0x00000000 DT_SIZE_K(1024)>;
};

&sram0 {
	reg = <0x20000000 DT_SIZE_K(512)>;
};

周辺機能の端子設定デバイスツリー

マイコンの周辺機能には端子が割り当てられています。
多くのマイコンでは端子機能が決まっています。
例えば、P1.10はSPI通信のSCL端子もしくはI2C通信のSDA端子もしくは入出力ポートになれて、P0.23はUART通信のTX端子もしくは出力ポートになれる、といったように与えられた選択肢の中から1つの機能を設定します。

ですが、nRF5xシリーズは、ほとんどの端子に自由に周辺機能を対応させることができます。
そこで、そのような端子設定を行うデバイスツリーが必要になります。
このタイプのファイル名の末尾には-pinctrlがついています。

例えば、以下のデバイスツリーにおいては、I2C1モジュールのSDA端子はP1.02に割り当て、SCL端子はP1.03に割り当てられ、UART0モジュールのTX端子はP0.20に割り当て、RX端子はP0.19に割り当てられています。

zephyr\boards\arm\nrf5340dk_nrf5340\nrf5340_cpuapp_common-pinctrl.dtsi
&pinctrl {
	i2c1_default: i2c1_default {
		group1 {
			psels = <NRF_PSEL(TWIM_SDA, 1, 2)>,
				<NRF_PSEL(TWIM_SCL, 1, 3)>;
		};
	};

	uart0_default: uart0_default {
		group1 {
			psels = <NRF_PSEL(UART_TX, 0, 20)>,
				<NRF_PSEL(UART_RTS, 0, 19)>;
		};
	};

psel以外にも、周辺機能の設定に関わるpinctrl特有のプロパティがあります。
それらのプロパティについては以下を参照ください

ボード固有のデバイスツリー

ボードにはLEDやスイッチ、加速度センサーやFLASHメモリなどの部品が実装されています。
そのようなボード固有の情報を表すデバイスツリーが必要になります。

このデバイスツリーでは、主にそれらの部品がマイコンのどの周辺端子機能を用いているかを設定します。
例えば、LEDならば、どのGPIOモジュールのどの端子を使用し、端子出力がHighとLowのどちらの時に点灯するかを以下のように記述します

zephyr\boards\arm\nrf5340dk_nrf5340\nrf5340_cpuapp_common.dtsi
leds {
    compatible = "gpio-leds";
    led0: led_0 {
        gpios = <&gpio0 28 GPIO_ACTIVE_LOW>;
        label = "Green LED 0";
    };
    led1: led_1 {
        gpios = <&gpio0 29 GPIO_ACTIVE_LOW>;
        label = "Green LED 1";
    };
    led2: led_2 {
        gpios = <&gpio0 30 GPIO_ACTIVE_LOW>;
        label = "Green LED 2";
    };
    led3: led_3 {
        gpios = <&gpio0 31 GPIO_ACTIVE_LOW>;
        label = "Green LED 3";
    };
};

開発ボードについては、これもメーカーの提供するデバイスツリーファイルを使用できますが、オリジナルのボードならば自分で作成しないといけません。

デバイスツリー作成

以上でデバイスツリーの仕様について解説したので、次は具体例にオリジナルボードのデバイスツリーを作成します。

今回は、以下のボードについてのデバイスツリーを作成します

createDevicetree0b.png

スイッチサイエンスの nRF5340ボードに9軸センサ BNO055 と OLED を接続したものになります。

デバイスツリー作成方法については、以下の動画で詳しく説明されていますが、1時間15分もある動画を見るのが面倒な方は本記事をご参照ください

フォルダ作成

ボードの設定を格納する boardsフォルダを作成します。既存のboardsフォルダがある場合は、それを利用します。

自分でboardsフォルダ作成した場合は、そのフォルダを Board Root に登録します。
nRF Connect SDKインストール済みの VS Code を起動させ、File -> Preferences -> Setting を選択します。

createDevicetree0a.png

起動したUser Setting画面において、Extensions -> nRF Connect -> Board Root の Add Item をクリックし、作成した boards フォルダのある階層を入力します。
例えば、C:\ncs\v2.6.1\boards フォルダがあるとしたら、上記の設定には C:\ncs\v2.6.1 と入力します。

createDevicetree0b.png

これで準備ができたので、次からデバイスツリーを作成開始します。
nRF Connect を選択し、メニューから Create a new board をクリックします。

createDevicetree1.png

すると Create New Board の入力欄が表示されるので、オリジナルのボード名を入力します。
今回は trial_nrf5340 という名称にしました。

createDevicetree2.png

次はSoC一覧が表示されるので、その中からボードで使用している製品と一致するものをクリックします。
今回は、 nRF5340_cpuapp_QKAA(Non-secure)を選択します。

createDevicetree3.png

次は boards フォルダのある階層を選択します。
今回は C:\ncs\v2.6.1\zephyr の階層にある boards フォルダを利用するので、その階層を入力します。

createDevicetree4.png

最後に所属するチームや会社名を入力します。何も入力したくなければ Escape キーをクリックします。

createDevicetree5.png

以上の設定を行うと、/boards/arm/trial_nrf534 というフォルダが作成されているはずです。

boardsフォルダおよび、その階層のアーキテクチャのフォルダが自動で選択されます。
nRF53の場合、アーキテクチャのフォルダは arm フォルダになります。

そして、アーキテクチャのフォルダの階層にカスタムボード名 trial_nrf5340 のフォルダが自動で作成されます。

必須ファイルの確認

次に Explore から Open Folder をクリックして、先ほどに作成した trial_nrf5340 を入力します。

createDevicetree6.png

するとフォルダの中身が表示されます。

createDevicetree7.png

これらはボード設定において必須となるファイルです。

  • Kconfig.board
    • ボードの Boolean Kconfig Symbol を作成する
      • 例)bool "trial_nrf5340"
    • ボートの SoC の種類を定義する
  • Kconfig.defconfig
    • Kconfig オプションの初期値を設定する
  • trial_nrf5340_deconfig
    • ボードの有効にする機能を設定する
  • trial_nrf5340.dts
    • ボードのデバイスツリー
  • trial_nrf5340-pinctrl.dtsi
    • ボードの端子設定
    • 自動生成されないので自分で追加しないといけない

必須ではありませんが、必要に応じて以下のファイルも追加します。
今回のツールによる自動生成では、board.cmake、trial_nrf5340.yaml が最初から用意されています。

  • board.cmake
    • ビルドとデバッグの設定
  • CMakeLists.txt
  • c_files.c
  • Kconfig
  • trial_nrf5340-pinctrl.dtsi
  • trial_nrf5340.yaml
  • trial_nrf5340_<revision>.conf
    • <revision>を有効にした場合、適用される設定
  • trial_nrf5340_<revision>.overlay
    • Kconfig で<revision>を有効にした場合、上書きして適用される設定
  • revision.cmake
    • どの<revision>を使用するか設定する

defconfig ファイル設定

今回、Kconfig.boardとKconfig.defconfigは初期設定のままでよく、修正は必要ありません。
trial_nrf5340_deconfig には修正が必要になります。

trial_nrf5340_deconfig では有効にする機能を設定します。
必ず有効化しないといけないのは以下の設定になります。

  • SoC Series ハードウェア設定
  • SoC ハードウェア設定
  • ボードの Kconfig Symbol
zephyr\boards\arm\trial_nrf5340\trial_nrf5340_defconfig
CONFIG_SOC_SERIES_NRF53X=y
CONFIG_SOC_NRF5340_CPUAPP_QKAA=y
CONFIG_BOARD_TRIAL_NRF5340=y

ファイルでは、最初から幾つかの機能が有効化されているのがを確認できます。
今回は、これに GPIO の機能の有効化を追加します。

zephyr\boards\arm\trial_nrf5340\trial_nrf5340_defconfig
# enable GPIO
CONFIG_GPIO=y

Configファイルの設定方は他にも色々とあるので、以下の解説もご参照ください。
VS Code の nRF Kconfig GUI で設定する方法も説明されています。

任意のアプリケーション作成

これから先の設定でビジュアルエディターを使用する為には、何でもいいのでアプリケーションを作成してビルドしておく必要があります。

nRF Connect から create a new application をクリックして、サンプルから hello world もしくは blinky を選択します。

createDevicetree8.png

作成したアプリケーションで Add build configuration をクリックして、Board に 先ほど作成したBoard名 trial_nrf5340 を入力してから、画面下の Build Configuration をクリックします。

createDevicetree9.png

ビルド後、ACTIONSメニューの Devicetree Board file をクリックします。

createDevicetree10.png

すると以下のようなビジュアルエディターが表示されます。これはdtsファイルを視覚的に表示したものです。
画面右上のアイコンをクリックすると、テキストタイプのdtsファイルに切り替えることができます。

createDevicetree11.png

LEDとスイッチの設定

まずはLEDの設定を行います。
NODESの SoC にある gpiote、gpio0、gpio1 にチェックを入れます。

createDevicetree12.png

チェックを入れると右のピン配置図に対応するピンが表示されます。右下のアイコンをクリックするとピン配置図とピン設定画面を入れ替えることができます。

チェックを入れた後、テキストタイプの trial_nrf5340.dts を見ると、以下の設定が追加されているのが確認できます。

zephyr\boards\arm\trial_nrf5340\trial_nrf5340.dts
&gpio0 {
	status = "okay";
};

&gpio1 {
	status = "okay";
};

&gpiote {
	status = "okay";
};

次にLEDの端子設定を行います。
以下の回路図を見ると、LEDは P1.11に接続され、3.3V でプルアップされているので、LOW出力で点灯することが分かります。

NODESの + アイコンをクリックして新規にNODEを追加します。

createDevicetree13.png

I/O pins を選択します。

createDevicetree14.png

LED を選択します。

createDevicetree15.png

追加する NODE 名を入力します。今回は led_0 としました。

createDevicetree16.png

端子一覧から P1.11 を選択します。

createDevicetree17.png

するとグラフィカルエディターに leds と led_0 が追加されます。

createDevicetree18.png

labelに Board Red LED と入力し、GPIO を ACTIVE LOW に設定し、ノード名に対するラベルの名称を led0 とします。

createDevicetree19.png

設定後、テキストタイプの trial_nrf5340.dts を見ると、以下の設定が追加されているのが確認できます。

zephyr\boards\arm\trial_nrf5340\trial_nrf5340.dts
leds {
    compatible = "gpio-leds";
    led0: led_0 {
        label = "Board Red LED";
        gpios = <&gpio1 11 GPIO_ACTIVE_LOW>;
    };
};

次にスイッチの設定を行います。
先ほどの回路図を見ると、スイッチは P1.10に接続され、3.3V でプルアップされているので、スイッチ押下すると端子にLOW入力されることが分かります。

LEDと同様に + アイコンをクリックし、I/O pins を選択し、Button を選択します。

createDevicetree20.png

追加する NODE 名を入力します。今回は button_0 としました。

createDevicetree21.png

するとグラフィカルエディターに buttons と button_0 が追加されます。

createDevicetree22.png

labelに Board SW1 と入力し、GPIO を ACTIVE LOW に設定し、ノード名に対するラベルの名称を sw0 とします。

createDevicetree23.png
zephyr\boards\arm\trial_nrf5340\trial_nrf5340.dts
buttons {
    compatible = "gpio-keys";
    sw0: button_0 {
        label = "Board_SW1";
        gpios = <&gpio1 10 GPIO_ACTIVE_LOW>;
    };
};

周辺機能の設定

次に周辺機能を設定します。
今回のボードで使用する部品である9軸センサとOLEDはI2Cで接続されるので、I2Cの設定を行います。

まずはgpioと同様にI2Cモジュールにチェックを入れます。
1つの I2Cモジュールに複数の部品を接続することもできますが、今回は2つのI2Cモジュールをそれぞれの部品に接続させます。

createDevicetree24.png

設定後、テキストタイプの trial_nrf5340.dts を見ると、以下の設定が追加されているのが確認できます。

zephyr\boards\arm\trial_nrf5340\trial_nrf5340.dts
&i2c0 {
	status = "okay";
};

&i2c1 {
	status = "okay";
};

端子配置はグラフィカルエディターからは設定できないので、端子設定のデバイスツリーファイルを作成します。
trial_nrf5340 フォルダに trial_nrf5340-pinctrl.dtsi ファイルを追加し、以下の端子配置を設定します。
端子は空いている中から好きな端子を選びます。今回は P0.02からP0.05までを使うことにしました。

端子設定は、ディフォルトモードである default と低消費電力モードである sleep の2通りが必要になります。

zephyr\boards\arm\trial_nrf5340\trial_nrf5340-pinctrl.dtsi
&pinctrl {
	i2c0_default: i2c0_default {
		group1 {
			psels = <NRF_PSEL(TWIM_SDA, 0, 3)>,
				<NRF_PSEL(TWIM_SCL, 0, 2)>;
		};
	};

	i2c0_sleep: i2c0_sleep {
		group1 {
			psels = <NRF_PSEL(TWIM_SDA, 0, 3)>,
				<NRF_PSEL(TWIM_SCL, 0, 2)>;
			low-power-enable;
		};
	};

	i2c1_default: i2c1_default {
		group1 {
			psels = <NRF_PSEL(TWIM_SDA, 0, 5)>,
				<NRF_PSEL(TWIM_SCL, 0, 4)>;
		};
	};

	i2c1_sleep: i2c1_sleep {
		group1 {
			psels = <NRF_PSEL(TWIM_SDA, 0, 5)>,
				<NRF_PSEL(TWIM_SCL, 0, 4)>;
			low-power-enable;
		};
	};
};

trial_nrf5340.dts において、作成した端子設定ファイルを include ファイルに設定します。

zephyr\boards\arm\trial_nrf5340\trial_nrf5340.dts
#include "trial_nrf5340-pinctrl.dtsi"

また、 I2C のプロパティに pinctrl-0pinctrl-1pinctrl-namesを追加し、以下のように設定します。

zephyr\boards\arm\trial_nrf5340\trial_nrf5340.dts
&i2c0 {
	status = "okay";
	pinctrl-0 = <&i2c0_default>;
	pinctrl-1 = <&i2c0_sleep>;
    pinctrl-names = "default", "sleep";
};

&i2c1 {
	status = "okay";
	pinctrl-0 = <&i2c1_default>;
	pinctrl-1 = <&i2c1_sleep>;
    pinctrl-names = "default", "sleep";
};

エイリアス設定

相互性を担保するためにエイリアスも設定しておきます。
今回は以下を設定しておきます

zephyr\boards\arm\trial_nrf5340\trial_nrf5340.dts
aliases {
    led0 = &led0;
    sw0 = &sw0;
};

その他の設定

UART、タイマー、PWM、SPIなどの設定も同様に行います。
それらについては、今後の記事で実際にそれらの機能を使用する際に設定していきたいと思います

boardsフォルダのパス設定

上記で作成した設定は boards フォルダに格納されていますが、アプリケーションの設定でパスを通していないと使用できません。

File -> Preferences -> Setting をクリックして設定を確認します。

パスの確認1.png

Extensions -> nRF Connect -> Board Roots に boards フォルダの置いてある階層が設定されていれば問題ありません。
もし boards フォルダの置いていないパスが設定されていれば、Add Item をクリックして新しくboards フォルダの置いてあるパスを追加してください。

パスの確認2.png

関連記事

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?