大分前にZephyr RTOSのサンプルをビルドした後進捗なく過ごしておりましたのは他でもありません、Device Treeの抽象度の高さにGPIOの端子をトグルさせるのもどうしてよいか分からない状態だったからです。その辺FreeRTOSもArm MbedOSも分かり易いのでZephyr避けてました。GWだし再開。
※「モダンOSのお砂場」投稿順Indexはこちら
Zephyr RTOSのホームページの在り処が以下に。
当然、上記ページにはZephyr RTOSのインストールやらビルド方法やら「公式」の方法が書かれておりますが、当方は、例のごとく以下の環境です。
VSCode + PlatformIO
こちらの「ルート」では公式と違うところもあるので、以下のPlatformIOのページを参照した方が良いかもしれません。
くり返しになりますが PlatformIO の良いところは、ボード毎、OS毎に必要なライブラリとかSDK,ツールチェーンといったもろもろのものをほぼほぼ自動でインストールしてくれる点です。またプロジェクトの設定やビルドも自動化してくれるので、お忘れの面倒くさがりにはありがたい限りです。
通例、PlatformIOでサンプルプログラムをビルドできれば、その先は「淡々」と具体的にやりたいことをコードしていく感じじゃないかと思います。しかし、Zephyr、抽象度高いデス。サンプルプログラムでオンボードのLEDのLチカをやった後、適当な端子をトグルさせようとして詰まりました。
デバイスツリー
という壁が立ちはだかっておるのです。デバイスツリーは、ハードウエアにアクセスするためのハード構成を記したデータ構造といった感じですかね。これ無にはZephyr RTOSのお作法にしたがってペリフェラル回路を動作させられません(ベアメタル派としては、IOアドレスに直接読み書きしてしまえば動作させることはできますが、「モダンOS」になりませぬ。)
デバイスツリー、Linuxをビルドしたり、デバドラ書いたりしている人にはお馴染みなのかもしれませんが、しょぼくれたマイコン相手の「ベアメタル派」にとっては抽象度の高すぎる存在です。普通のマイコンなら簡単にできる PB_3 端子をペコペコさせる方法すら直観的には分かりません。デバイスツリーを見て初めてどんなペリフェラルがどんな「お名前」で定義されているのかが理解できます。そして「お名前」が分かれば後は何とかなる、と。ホントか?
PlatformIOでのデバイスツリーの定義のありか
PlatformIOをWindowsPC上にインストールした場合、ユーザーフォルダの中に
.platformio
なるフォルダが生成され、その中にPlatformIOが自動でダウンロードしてきたライブラリとかツールチェーンが格納されています。Zephyrのデバイスツリーの定義ファイルはその中の数か所にまたがって格納されていました。それらは packages¥framework-zephyrというパスの下にありますが、ザックリ書くと、
-
- boards フォルダの下にボード毎の定義がある
- dtsフォルダの下にマイコン・デバイス毎の定義がある
という構造です。今回ターゲットボードとしている ST Microelectronics社のNucleo F401REボードの場合、以下のフォルダ内にまずボード固有定義のファイルが置かれています。
boards\arm\nucleo_f401re
ボード固有の定義の中にはボードに関する定義のみ存在しています。マイコン内部のペリフェラル回路自体は以下のマイコン固有の定義を参照することになります。
dts\arm\st\f4\stm32f401Xe.dtsi
STM32F401でも複数定義があり、上記はそのうちの1種をさしています。実際には共通部分が多いみたいで、さらにF401のベースとなる以下の定義を参照しています。
dts\arm\st\f4\stm32f401.dtsi
さらに F401 はSTM32F4シリーズの一機種です。シリーズ共通部分が以下に。
dts\arm\st\f4\stm32f4.dtsi
とここまで参照するとようやくNucelo F401RE上でZephyr RTOSがサポートしている「デバイス」の全貌が明らかとなります。システマティック!しかし、メンドイ。マイコン1機種1ヘッダファイルで済ませていたような「ベアメタルな」しょぼくれマイコンに慣れ親しんできたものとしては、どんだけ~
ボードフォルダの様子が以下に。
その上のフレームワーク全体のフォルダです。
以下は機種固有の定義類のフォルダ。見えているのはごく一部です。F4シリーズだけで大量の機種があり、それぞれネストを重ねています。
プロジェクトの作成と実行
さて、デバイスツリーの定義の在り処が分かったので、VSCode上のPlatformIO機能を使ってプロジェクトを作成してみます。ターゲットボードをST Nucleo F401RE、フレームワークを Zephyr RTOSとすれば自動生成してくれます。
さて今回書いてみたのは以下のような「ダブルLチカ」プログラムです。
-
- サンプルのBlinkのソースそのままにオンボードのLEDを光らせる
- PB_3端子に外部接続したLEDを自力で光らせる
どちらも物理的にやっていることは同じなのですが、サンプルから自力への1歩が長かったです。似た処理を並べて書いてあるので比べると分かり易いんじゃないかと思います。
デバイスを制御する上で一番肝心なのは、
struct device
として定義されているデバイス構造体をGETすることじゃないかと思います。この構造体の中に実際にデバイスを操作するためのAPI関数へのポインタなども含まれてます。デバイスドライバの関数などは皆この構造体を当てにしているようです。
一方デバイスツリーを操作していろいろな情報を引き出すためにDTナンタラいうようなマクロがマクロを呼んで大量に定義されています。こいつらはデバイスツリーを抽象化して、機種依存性を無くするためのお作法とみました。マクロなのでコンパイル時には解決されており、とくに実行上のオーバヘッドはないのですが、PB_3端子の端子番号が3だとかまでマクロを重ねて引き出したくはないな、と思いました(個人の感想です。)
Blinkのサンプルプログラムが分かり難いのは、以下のコードではコメントアウトしてある #define LED0_NOE DT_ALIAS(led0)から始まる一連のマクロ部分がサッパリなためじゃないかと思っています(今回はエイリアスではなくデバイスツリーの実体の御名前で呼ぶようにしてみました。)これによって led0 という端子が抽象化され、各社各様のマイコンボード上で同じ名前で共通にアクセスできるようになると。仕組みは立派ですが、正直組み込みマイコン相手にそこまでやるか?と思ってしまいます(個人の感想です。)
デバイスツリーからデバイスのノードラベルを見つけてしまえば以下の3ステップで出力が可能です。2,3のステップは普通だし。
-
- device_get_binding()関数にノードラベルを与えてデバイス構造体を得る
- gpio_pin_configure()関数にデバイス構造体と端子番号、制御フラグを与えて端子を出力に設定する
- gpio_pin_set()関数にデバイス構造体と端子番号、値を与えて出力させる
作製した2方法併記の「冗長な」コードが以下に。
#include <zephyr.h> #include <device.h> #include <devicetree.h> #include <drivers/gpio.h> #include <sys/printk.h> #include <string.h> #define SLEEP_TIME_MS 5000 #define GPIO_DRV_NAME DT_LABEL(DT_NODELABEL(gpiob)) #define GPIO_PB3_D3 (3) #define FLAGSB DT_GPIO_FLAGS(DT_NODELABEL(gpiob), gpiob) //#define LED0_NODE DT_ALIAS(led0) #define LED0_NODE DT_NODELABEL(green_led_2) #if DT_NODE_HAS_STATUS(LED0_NODE, okay) #define LED0 DT_GPIO_LABEL(LED0_NODE, gpios) #define PIN DT_GPIO_PIN(LED0_NODE, gpios) #if DT_PHA_HAS_CELL(LED0_NODE, gpios, flags) #define FLAGS DT_GPIO_FLAGS(LED0_NODE, gpios) #endif #endif void main(void) { struct device *dev; struct device *devB; bool led_is_on = true; bool led_is_onB = true; int ret = 0; dev = device_get_binding(LED0); if (dev == NULL) { return; } devB = device_get_binding(GPIO_DRV_NAME); if (devB == NULL) { return; } ret = gpio_pin_configure(dev, PIN, GPIO_OUTPUT_ACTIVE | FLAGS); if (ret < 0) { return; } ret = gpio_pin_configure(devB, GPIO_PB3_D3, GPIO_OUTPUT_ACTIVE | FLAGSB); if (ret < 0) { return; } while (1) { gpio_pin_set(dev, PIN, (int)led_is_on); gpio_pin_set(devB, GPIO_PB3_D3, (int)led_is_onB); led_is_on = !led_is_on; led_is_onB = !led_is_onB; k_msleep(SLEEP_TIME_MS); } }
これでビルド成功。F401REボードに書き込んだところ所望の動作をいたしました。
ちょいとTIPS
上記のコードでは、オンボードのLEDがポートAに接続しており、外部LED動作させているのはポートBの端子に接続したものです。最初同じポートAの別端子を上記のコードで制御しようとしてうまく行きませんでした。どうも同じデバイス(ペリフェラル回路)を重ねてGETすることができないような気がします。知らんけど。
また上記コードのデバッグ中、printk()関数(Linuxなどでカーネルデバッグ等で使うやつ)にお世話になりました。PlatformIO環境でprintk()使う場合、platformio.ini ファイルに以下のように monitor_speed 定義しておいた方がよいかもです。PlatformIO内蔵のシリアルモニタ接続で使われます。
[env:nucleo_f401re] platform = ststm32 board = nucleo_f401re framework = zephyr monitor_speed = 115200
なんとかZephyr、動かせる気がしてきましたぜ。ホントか?