記事の概要
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に位置することを示しています。
cpuapp: cpu@2 {
以下ではSPIノードがアドレス8e6000から始まることを示しています。
spi120: spi@8e6000 {
また、ノードの詳細な情報はプロパティで与えます。
最初の例では、サブノードa-sub-node
の{}
内に、プロパティfoo = <3>
が記述されています。
DTSバージョンのタグ
DTSファイルについては、最初の行に以下のタグがあります。
/dts-v1/;
このタグは、DTSファイルがDTSのバージョン1に従うことを示しています。
このタグがないDTSファイルはバージョン0として認識されてしまいます。
プロパティ
プロパティはプロパティ名とプロパティ値から構成されます。
プロパティ名 = プロパティ値;
以下では様々なプロパティの中身と実際の使われ方を確認したいと思います
compatible
compatibleプロパティは、ノードが、何のハードウェアについてのノードなのかを表しています。
アプリケーションは、compatibleプロパティを参照することで、ボードが使用しているデバイスに対応するデバイスドライバーを選択します。
プロパティ値は""
で囲った文字列になります。,
で区切って複数の文字列を与えることもできます。
典型的なプロパティ値は"製造元,モデル名"
となります。
以下にサンプルから具体例を示します。
cpuapp: cpu@2 {
compatible = "arm,cortex-m33";
この compatible は、ノードcpu
が ARM社の Cortex-M33 についてのノードであることを示しています。
/ {
compatible = "nordic,nrf54h20pdk_nrf54h20-cpuapp";
この compatible はルートノードのプロパティであり、 このファイルが記述するデバイスそのものを示しており、それがNordic社のモデル「nrf54h20pdk_nrf54h20-cpuapp」 であると分かります
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レジスタの最大値
以下にサンプルから具体例を示します。
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プロパティは、デバイスの製造元モデルを表しています。
主にルートノードのプロパティとして使用します。
プロパティ値は""
で囲った文字列になります。,
で区切って複数の文字列を与えることもできます。
以下にサンプルから具体例を示します。
/ {
model = "Nordic NRF5340 DK NRF5340 Application";
compatible = "nordic,nrf5340-dk-nrf5340-cpuapp";
ルートノードのmodeleプロパティは、このデバイスが Nordic社のnRF5340のDKボード(開発ボード)であることを示しています。
status
statusプロパティは、ノードの記述するデバイスが使用可能かどうかを示しています。
プロパティ値は""
で囲った以下の文字列のどれかになります。
ただし、 Zephyr ではokay
とdisabled
以外は使用しません。
- okay
- デバイスは使用可能
- disabled
- デバイスは使用不可、ただし使用可能に遷移することができる
- reserved
- デバイスは使用不可、かつ使用可能に遷移することもできない
- fail
- デバイスは深刻なエラーにより使用不可
- fail-sss
- デバイスは深刻なエラーにより使用不可、sssには検知したエラーを示す値が表示される
ノードにstatusプロパティを設定しない場合は、自動でokay
に設定されます。
以下にサンプルから具体例を示します。
&i2c1 {
compatible = "nordic,nrf-twim";
status = "okay";
上記は、nRF5340のI2Cモジュールが使用可能であることを示しています。
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>
以下にサンプルから具体例を示します。
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つのアドレスしか割り当たらないので、アドレス範囲を指定する必要がないからです。
(次に紹介するメモリマップドデバイスにはアドレス範囲のサイズ設定が必要になります。)
以下にサンプルから具体例を示します。
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に設定します。
以下にサンプルから具体例を示します。
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に設定されています。
よって、子ノードtimer
とspi
、vpr
のreg
のアドレスフィールドには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 = <&対象ノードのラベル名>
のように設定します。
以下にサンプルから具体例を示します。
cpuppr_clic: interrupt-controller@1000 {
...(略)...
#interrupt-cells = <2>;
interrupt-controller;
...(略)...
};
...
cpuppr_vevif_local: mailbox {
...(略)...
interrupt-parent = <&cpuppr_clic>;
ノードmailbox
は、割り込みコントローラにノードcpuppr_clic
を指定しています
interrupts
interruptsプロパティはノードの割り込みを設定します。
プロパティ値は<>
で囲ったu32型の数値になります。
以下にサンプルから具体例を示します。
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固有ノードを定義することもあります。
以下に具体例を示します。
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ノードの子ノードになります。
設定できるプロパティは多いので、ここでは省略します。
プロパティの説明でも引用しましたが、具体例を以下に示します
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ノード
あらかじめシステムが予約しており、ユーザーは使用できないメモリ空間の設定を行うノードです。
見れば内容が分かると思いますので、説明は省略して具体例だけを以下に示します
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
ノード、そして周辺機能や割り込み、クロックに関する設定がされています。
/ {
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などを設定した以下のデバイスツリーファイルが存在します
&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に割り当てられています。
&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のどちらの時に点灯するかを以下のように記述します
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";
};
};
開発ボードについては、これもメーカーの提供するデバイスツリーファイルを使用できますが、オリジナルのボードならば自分で作成しないといけません。
デバイスツリー作成
以上でデバイスツリーの仕様について解説したので、次は具体例にオリジナルボードのデバイスツリーを作成します。
今回は、以下のボードについてのデバイスツリーを作成します
スイッチサイエンスの nRF5340ボードに9軸センサ BNO055 と OLED を接続したものになります。
デバイスツリー作成方法については、以下の動画で詳しく説明されていますが、1時間15分もある動画を見るのが面倒な方は本記事をご参照ください
フォルダ作成
ボードの設定を格納する boardsフォルダを作成します。既存のboardsフォルダがある場合は、それを利用します。
自分でboardsフォルダ作成した場合は、そのフォルダを Board Root に登録します。
nRF Connect SDKインストール済みの VS Code を起動させ、File -> Preferences -> Setting を選択します。
起動したUser Setting画面において、Extensions -> nRF Connect -> Board Root の Add Item をクリックし、作成した boards フォルダのある階層を入力します。
例えば、C:\ncs\v2.6.1\boards フォルダがあるとしたら、上記の設定には C:\ncs\v2.6.1 と入力します。
これで準備ができたので、次からデバイスツリーを作成開始します。
nRF Connect を選択し、メニューから Create a new board をクリックします。
すると Create New Board の入力欄が表示されるので、オリジナルのボード名を入力します。
今回は trial_nrf5340 という名称にしました。
次はSoC一覧が表示されるので、その中からボードで使用している製品と一致するものをクリックします。
今回は、 nRF5340_cpuapp_QKAA(Non-secure)を選択します。
次は boards フォルダのある階層を選択します。
今回は C:\ncs\v2.6.1\zephyr の階層にある boards フォルダを利用するので、その階層を入力します。
最後に所属するチームや会社名を入力します。何も入力したくなければ Escape キーをクリックします。
以上の設定を行うと、/boards/arm/trial_nrf534 というフォルダが作成されているはずです。
boardsフォルダおよび、その階層のアーキテクチャのフォルダが自動で選択されます。
nRF53の場合、アーキテクチャのフォルダは arm フォルダになります。
そして、アーキテクチャのフォルダの階層にカスタムボード名 trial_nrf5340 のフォルダが自動で作成されます。
必須ファイルの確認
次に Explore から Open Folder をクリックして、先ほどに作成した trial_nrf5340 を入力します。
するとフォルダの中身が表示されます。
これらはボード設定において必須となるファイルです。
- Kconfig.board
- ボードの Boolean Kconfig Symbol を作成する
- 例)
bool "trial_nrf5340"
- 例)
- ボートの SoC の種類を定義する
- ボードの Boolean Kconfig Symbol を作成する
- 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
CONFIG_SOC_SERIES_NRF53X=y
CONFIG_SOC_NRF5340_CPUAPP_QKAA=y
CONFIG_BOARD_TRIAL_NRF5340=y
ファイルでは、最初から幾つかの機能が有効化されているのがを確認できます。
今回は、これに GPIO の機能の有効化を追加します。
# enable GPIO
CONFIG_GPIO=y
Configファイルの設定方は他にも色々とあるので、以下の解説もご参照ください。
VS Code の nRF Kconfig GUI で設定する方法も説明されています。
任意のアプリケーション作成
これから先の設定でビジュアルエディターを使用する為には、何でもいいのでアプリケーションを作成してビルドしておく必要があります。
nRF Connect から create a new application をクリックして、サンプルから hello world もしくは blinky を選択します。
作成したアプリケーションで Add build configuration をクリックして、Board に 先ほど作成したBoard名 trial_nrf5340 を入力してから、画面下の Build Configuration をクリックします。
ビルド後、ACTIONSメニューの Devicetree Board file をクリックします。
すると以下のようなビジュアルエディターが表示されます。これはdtsファイルを視覚的に表示したものです。
画面右上のアイコンをクリックすると、テキストタイプのdtsファイルに切り替えることができます。
LEDとスイッチの設定
まずはLEDの設定を行います。
NODESの SoC にある gpiote、gpio0、gpio1 にチェックを入れます。
チェックを入れると右のピン配置図に対応するピンが表示されます。右下のアイコンをクリックするとピン配置図とピン設定画面を入れ替えることができます。
チェックを入れた後、テキストタイプの trial_nrf5340.dts を見ると、以下の設定が追加されているのが確認できます。
&gpio0 {
status = "okay";
};
&gpio1 {
status = "okay";
};
&gpiote {
status = "okay";
};
次にLEDの端子設定を行います。
以下の回路図を見ると、LEDは P1.11に接続され、3.3V でプルアップされているので、LOW出力で点灯することが分かります。
NODESの + アイコンをクリックして新規にNODEを追加します。
I/O pins を選択します。
LED を選択します。
追加する NODE 名を入力します。今回は led_0 としました。
端子一覧から P1.11 を選択します。
するとグラフィカルエディターに leds と led_0 が追加されます。
labelに Board Red LED と入力し、GPIO を ACTIVE LOW に設定し、ノード名に対するラベルの名称を led0 とします。
設定後、テキストタイプの 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 を選択します。
追加する NODE 名を入力します。今回は button_0 としました。
するとグラフィカルエディターに buttons と button_0 が追加されます。
labelに Board SW1 と入力し、GPIO を ACTIVE LOW に設定し、ノード名に対するラベルの名称を sw0 とします。
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モジュールをそれぞれの部品に接続させます。
設定後、テキストタイプの trial_nrf5340.dts を見ると、以下の設定が追加されているのが確認できます。
&i2c0 {
status = "okay";
};
&i2c1 {
status = "okay";
};
端子配置はグラフィカルエディターからは設定できないので、端子設定のデバイスツリーファイルを作成します。
trial_nrf5340 フォルダに trial_nrf5340-pinctrl.dtsi ファイルを追加し、以下の端子配置を設定します。
端子は空いている中から好きな端子を選びます。今回は P0.02からP0.05までを使うことにしました。
端子設定は、ディフォルトモードである default と低消費電力モードである sleep の2通りが必要になります。
&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 ファイルに設定します。
#include "trial_nrf5340-pinctrl.dtsi"
また、 I2C のプロパティに pinctrl-0
、pinctrl-1
、pinctrl-names
を追加し、以下のように設定します。
&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";
};
エイリアス設定
相互性を担保するためにエイリアスも設定しておきます。
今回は以下を設定しておきます
aliases {
led0 = &led0;
sw0 = &sw0;
};
その他の設定
UART、タイマー、PWM、SPIなどの設定も同様に行います。
それらについては、今後の記事で実際にそれらの機能を使用する際に設定していきたいと思います
boardsフォルダのパス設定
上記で作成した設定は boards フォルダに格納されていますが、アプリケーションの設定でパスを通していないと使用できません。
File -> Preferences -> Setting をクリックして設定を確認します。
Extensions -> nRF Connect -> Board Roots に boards フォルダの置いてある階層が設定されていれば問題ありません。
もし boards フォルダの置いていないパスが設定されていれば、Add Item をクリックして新しくboards フォルダの置いてあるパスを追加してください。