- Raspberry Pi Picoベースのビデオ機器コントローラーで同時実行タスクの要求が増え、pico-sdkとデュアルコア分離だけではソフトウェア構造で扱いきれなくなった
- コントローラーはRS-485 VISCA、Ethernet、9個のRGBボタン、ジョイスティック、ディスプレイを同時に扱い、DHCP、mDNS、UDPベースのATEMプロトコル処理まで必要になる
- FreeRTOSはスケジューラとタスク間通信で一部実装できたが、
printf停止問題とハードウェア抽象化不足のため、デバッグとコード再利用が難しかった
- Apache NuttXはシェル、ファイルシステム、
/dev/i2c0のようなデバイス抽象化、Kconfig設定を提供したが、I2C問題は後に設定ミスだったと訂正され、当時の評価のかなりの部分は無効かもしれない
- Zephyrは5GB規模のリポジトリとSDK、ボード定義、ビルドの複雑さが障壁となり、最終的には既存環境に最も単純に組み込めるFreeRTOSを引き続き試す方向に傾いた
なぜRTOSが必要になったのか
- 複数の小型マイクロコントローラープロジェクトがRaspberry Pi Picoボードを中心に作られている
- Picoは適切なSDK、安価なハードウェア、gdb/openocdデバッグ対応のおかげでIDE統合がしやすい
- 現在のプロジェクトは、複数のビデオ機器を制御するハードウェアコントローラーである
- 制御対象はモーター付きPTZカメラ2台、固定カメラ1台、接続されたビデオスイッチング機器
- 既存のPTZカメラ制御器はMarshall VS-PTC-200に似た無名のパネルである
- 数年前の価格は€650だったが、ボタンの感触とアナログジョイスティックの品質がよくない
- 多くのボタンは現在使用中のカメラでは動作せず、監視カメラ向けに最適化されているように見える
- カメラとはRS-485バスで接続される
- ATEMビデオスイッチャーの制御は、現在はコンピューター上のソフトウェアパネルでしか行えない
- Blackmagic Designのハードウェアパネルは非常に高価である
コントローラーハードウェア構成
- 最小設計は9個のボタン、ジョイスティック、ユーザーインターフェース用ディスプレイとした
- 1年間にわたり何度もハードウェア設計を繰り返した後、PCBを確保した
- 9個のRGBボタン
- Marshallクローンパネルにも入っていた$10のジョイスティック
- PTZカメラとRS-485で通信するためのTP8485E
- Ethernetでビデオスイッチャーと通信するためのWiznet W5500モジュール
- ボード修正の末にハードウェア部品はすべて動作するようになったが、より難しかったのはソフトウェアだった
pico-sdkだけでは苦しくなったソフトウェア
- 既存のRP2040プロジェクトと同様に、cmakeプロジェクトでpico-sdkを取り込む方式で始めた
- 動作するレベルにするため、Picoの第2コアをWiznetモジュール処理に割り当て、第1コアでユーザーインターフェースI/Oを処理するようにした
- LED点滅と、第2コアで動くDHCPクライアントの実装までは可能だった
- 残りのシステム実装ははるかに複雑になった
- 同時に処理すべき作業が急速に増えていった
- ディスプレイにある程度滑らかなユーザーインターフェースを描画すること
- RS-485インターフェースでVISCAコマンドを送ること
- ボタン入力に反応すること
- 複数接続を含むネットワークスタックを維持すること
- ネットワーク側は別のバックグラウンド作業を必要とした
- 標準準拠のDHCP対応では期限切れ時刻を追跡し、リース状態維持のためにDHCPサーバーと時々通信する必要がある
- mDNSはATEMビデオスイッチャーのIP自動検出に必要であり、コントロールパネルの存在も通知できると望ましい
- ATEMプロトコルは単純だが、Wiznetモジュールのバッファサイズを超えるデータを受信することがあり、UDPデータグラム送信が止まると接続切断タイムアウトが非常に短い
- こうした条件では、自前のループを積み増すよりRTOSで作業を分割した方が適切に見えた
FreeRTOS: シンプルだが抽象化が足りない
- FreeRTOSは技術的にはpico-sdkに含まれているが、チュートリアルでは新しいコピーをダウンロードする方法が使われており、それに従った
- 検討したRTOSの中では最もシンプルに見え、主にスケジューラとタスク間通信を提供する
xTaskCreateでタスクを作成し、vTaskStartSchedulerでスケジューラを開始する構造である
- キューを使ってボタン状態をLEDタスクへ渡す形のIPCを利用できる
- 数日使った結果、コードベースはまだ実機能が少ないにもかかわらず複数タスクに分かれた
buttonsTask: I2C GPIOエクスパンダーをポーリングしてボタン入力を確認し、ボタンキューにメッセージを入れる
ledTask: ledQueueメッセージで特定ボタンのRGB色を設定する
mainTask: ボタン入力に応じてプロジェクト状態を更新するメインループを実行する
networkTask: Wiznetモジュールと通信する
dhcpTask: ネットワークケーブル接続時にnetworkTaskが生成する
mdnsTask: IPアドレス取得後にdhcpTaskが生成する
atemTask: mDNSがATEMデバイスからの応答を受け取ると生成される
viscaTask: まだ何もしないが、RS-485ポートへデータを送る必要がある
- ハードウェアはまだネットワーク上に現れる以外ほとんど何もしていないのに、タスク数が多くなった
- 最も不便な問題は、printfが毎回停止を引き起こす点だった
- gdbデバッガは動作するが、DHCPトラフィックを出力して確認する用途には向いていない
- FreeRTOSはハードウェア抽象化を提供しないため、複数チップと通信するために書いたコードを簡単に再利用しにくい
- その後、クリーンなFreeRTOSプロジェクトを新しく作って機能を移そうとしたが、シリアル出力なしで手探りのデバッグをしなければならない不便さから、他の選択肢も調べた
Apache NuttX: Unix風の構造と設定ミス
- Apache NuttXは一般的なOSにより近く見え、マイクロコントローラーをUnixシステムのように扱う
- チュートリアルではpico-sdkを取り込み、環境変数を設定するよう案内される
- すでに
/usr/shareにSDKがあり環境変数も設定済みだったが、NuttXがpico-sdkのversion.hファイルを上書きしようとして権限問題でビルドが失敗した
- 最小のNuttXファームウェアをビルドしてシリアルポートに接続すると、実際のシェルが現れた
uptime、uname、uname -aのようなコマンドが動作する
- 表示されたバージョンは
NuttX 12.5.1で、対象はarm raspberrypi-picoだった
- Unix方式に従うため、アプリケーションを書いて起動時に自動実行できそうに見える
- ファイルシステムがあり、ハードウェアは
/dev/i2c0、/dev/adc0のような抽象化で公開される
- 気に入った点はmenuconfig/Kconfigベースの構成である
- Linux開発で慣れた方式である
- 実際のハードウェアドライバーシステムがあり、ボタンに使ったGPIOエクスパンダーチップのドライバーもすでに存在する
- RP2040のピンmux設定もmenuconfigで指定できるため、ピン番号の定数を別途維持したり、I2Cバス初期化コードを大量に書いたりしなくて済む
- I2Cテストユーティリティもファームウェアに含められる
- 当初はI2Cの基本動作ができていないように見えた
- その後の更新で実際には問題なく動作しており、設定ミスでI2Cバスを壊していたと訂正された
- そのNuttX節の残りの評価の大半は無効である可能性があると明記されている
- 当時は、GPIOボタンがGPIOエクスパンダーの背後にあることをNuttXにどう伝えるか、GPIOエクスパンダーをI2Cバスにどう接続するかも把握できなかった
configure.sh失敗後にリポジトリが不整合な状態になり、NuttXリポジトリを何度も再クローンしなければならなかった
distcleanも同じ理由で正しく動かない状況があった
- Unixライクなアプローチは最初は良さそうに見えたが、マイクロコントローラーで擬似的なファイルシステムパスを扱いたいわけではない
Zephyr: 大きなSDKとボード定義の壁
- 次の選択肢はZephyrで、プロジェクト設定用のPythonユーティリティを提供している
- 最初の段階で約5GBのGitリポジトリを取得した
- そこには多数のチップ向けHALライブラリが含まれている
- ユーザー全体のcmake設定にも影響する
- チュートリアルではZephyr SDKのインストールを要求する
- Zephyr SDKには、対応アーキテクチャごとのツールチェーン、コンパイラ、アセンブラ、リンカ、ビルドツールが含まれる
- QEMUやOpenOCDのようなホストツールも含まれる
- すでに複数のARMツールチェーンがあるため、全アーキテクチャ向けコンパイラをビルドしたり、事前ビルド済みのものを取得したりはしたくなかった
- Zephyr SDKなしでビルドするため、クロスコンパイルオプションを設定した
ZEPHYR_TOOLCHAIN_VARIANT=cross-compile
CROSS_COMPILE=/usr/bin/arm-none-eabi-
west build -p always -b sparkfun_pro_micro_rp2040 samples/basic/blinky
- Raspberry Pi Picoは実際にはサポートされておらず、同じSoCを使う別のボードだけがサポートされていた
- 同じSoCなので実質的に似たボードだと見なして進めた
- すぐに
blinkyデモがビルドできない問題にぶつかった
- デモは点滅対象である
led0定義を要求する
- Sparkfun Pro Micro RP2040には単純なGPIO LEDではなく、WS2812Bアドレス指定LEDがある
- カスタムボードのマニュアルに従って他のボード定義をコピーする方法で進めたが、ビルドエラーと警告を修正した後も対象ボードのビルドには成功しなかった
結局またFreeRTOSへ
- 3つのRTOSのうち、実際のアプリケーションの一部をビルドできたのはFreeRTOSだけだった
printf問題は、オンラインの案内どおり別のprintf実装に置き換え、必要な箇所で別の関数を呼ぶ形で解決する可能性がある
- 今後はFreeRTOSを引き続き試す予定である
- 他のRTOSのように開発環境をRTOSに合わせるのではなく、既存環境に比較的単純に統合できる唯一の選択肢だったためである
1件のコメント
Hacker Newsのコメント
この筆者は、RTOSをArduino環境のようなもの、あるいは適当に触っていても動くことを期待してよい対象だと見ているようだ。たいていはそうではない
最近のArduinoの多くは内部にmbedやFreeRTOSが入っていて、それを露出させる方法もあるので、筆者のスタイルにはそちらのほうが向いていたかもしれない
Zephyrは使いやすく、CLionのサポートも良いが、ツールチェーンをインストールせずに全部動くことを期待はできない。Pi Picoも確実にサポートしており、実際に使った範囲では問題なかった
簡単にまとめると、FreeRTOSはほぼどこでもサポートされているが、ドライバはたいていSoC/デバイスごとなので面倒で、APIも親切ではないが慣れることはできる。Bluetoothを使うならスタックを自分で探す必要がある
Zephyrは実際のハードウェア抽象化をサポートし、ほとんどのSoCをサポートしているが、ボード作業を多少やる必要があるかもしれない。Bluetoothスタックは提供されており、HCIサポートを少し追加する必要がある場合がある
NuttXはサポートが抜群というわけではないが、動くようにできるならかなりクールな選択肢だ。まだ業界でのサポートは強くない。mbedもあるが、ここでは省略する
実際のRTOSの世界では、普通はSoCベンダーがサポートしているものを選ぶ。NordicならZephyr、NXPならFreeRTOSといった具合で、そのほうがしっかりしたサポートを受けられる
OEMデバイス向けファームウェアのプロジェクトで、開発者たちがZephyrで苦労しなかった例を一度も見たことがない。出荷製品の実際のファームウェアを作っている開発者の中で、Zephyrのハードウェア抽象化が役に立つと考えている人にもまだ会ったことがない
そういう人がいないという意味ではないが、ここ5年ほどでは見かけていない
私から見ると、Zephyrはマーケティング資料がとてもきれいだ。しかしその華やかさの裏には、過度な肥大化、非常に遅いコンパイル、始めにくい環境がある
伝統的なUNIX流のようにツールチェーンをシステム全体にインストールするのは苦痛で、正直あまり賢いやり方でもない
一人で使っていて問題なく回るならよいが、複数の開発者が対象の異なる複数のプロジェクトを扱うと、ビルドや設定の問題を把握するのに多くの時間を使うことになる
ツールにPythonを使い続けるのも助けにならない。独自のバージョン問題を持ち込み、開発者のマシンごとに違う動きをする言語を、なぜわざわざ使おうとするのかわからない
組み込み開発を趣味と仕事で10年ほどやってきたが、プロジェクトで全員の環境を同じにそろえるのに1週間使っても、それを問題だと思わない態度が理解できない
これは現実の問題で、いら立たしく、時間を浪費し、不要だ
ツールは静的リンクされたバイナリであるべきだ。Rust、Go、C、C++のどれで書かれていても構わないが、その場しのぎの開発より、コンピュータに何がインストールされているかに関係なく同じように動くと信頼できる堅牢なツールを優先してほしい
Pythonはそれを実現してくれないし、この問題をもっと真剣に受け止める代わりに怒って防御的になるのも役に立たない
それでもPlatformIOのようなものは正しい方向だ。Pythonプロジェクトだということはわかっており、ときどき問題にもなるが、他のツールよりはましで、発想は正しい
ツールチェーン、SDK、ライブラリは管理されるべきで、プロジェクト設定は単純であるべきで、ビルドはどこでもいつでも再現可能であるべきだ
組み込み業界が、共通の構造化された取り組みの価値にもっと目を向けてほしい。主要MCUメーカーで働く人を多く知っているが、たいていは目先の自分の問題解決に追われていて、開発者の要求に対して視野が狭く見えるので、いつもがっかりする
RP2040で動くキーボードプロジェクトがあり、ファームウェアはRustで書かれている。リポジトリだけがあり、Rustツールチェーンがない状態からフラッシュする手順は次の通りだ
(rustupをインストール)
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh$ rustup target add thumbv6m-none-eabi$ cargo install elf2uf2-rs* キーボードをブートローダーモードに切り替える *$ cargo run --releaseここではrustupとCargoがツールチェーン管理とビルドの大部分を処理しており、この2つは本当に素晴らしい。Pythonプロジェクトは近くにも及ばない
コマンドを1つ実行するだけで、ローカルでもCIでも同じバージョンのビルドツールとライブラリで、完全に同じように設定されて動作する
だからCMakeやMakeをまた扱ってライブラリのインストールに苦労するたびにつらい。
compile 'library-name-here'のようなやり方とはあまりにも違うむしろライブラリエコシステムがあるから選ばれているのだ。データ分析、可視化、科学計算では、ほかの言語に太刀打ちできる選択肢がない
こうしたものはゼロから再実装したい領域ではない。数値安定性のような部分でバグを入れやすく、99%は正しい結果を出しても、1%ではもっともらしいが完全に間違った結果を出すことがある
個人的には、RP2040 プロジェクトを Rust と Embassy に移し始めている。
Rust は少し慣れが必要だったが、かなり気に入っている。RTOS ではないものの、RTOS を必要にさせる多くの要件を満たしてくれる
Rust と Cargo は、RP2040 や STM32 のビルドとフラッシュの苦労を取り除いてくれる。これまで使った組み込み環境の中でいちばん快適だった
多くのアプリケーションでは MPU を使わない。そこに Rust のメモリ安全性と、RTOS なしで減るファームウェア全体の複雑さを考えればよい
Embassy executor の非同期処理も非常によく動き、RTOS 設計プロセスの苦労をかなり軽減してくれる
RP2040 用の PIO インターフェース まで加えると、コードは非常にシンプルで美しくなり、他のプロセッサでは実現しにくい構成になる
Pi Pico は Zephyr で 100% サポートされている。https://github.com/zephyrproject-rtos/zephyr/tree/main/board... ドキュメントを確認しなかったのだろうか? https://docs.zephyrproject.org/latest/boards/raspberrypi/rpi...
さらに、多くのケースでは 1 つの「main」Zephyr インストールを使うのではなく、プロジェクトの
west.ymlに必要な外部モジュールを含めるのが想定された使い方だ。複数のプロジェクトで同じ Zephyr インストールを共有することは別の議論だが、ありとあらゆるツールチェーンや HAL をインストールすることだけが唯一の方法ではない
最近ではオープンソースの ThreadX が言及されていなかった。
https://github.com/eclipse-threadx/threadx/
https://github.com/RIOT-OS/RIOT
RTOS の選択肢比較はよかった。
個人的には MicroPython のほうがより簡単な道だと思う。
async/awaitベースの協調的マルチタスクが自分には合っている。最近のプロジェクトでは、ステッピングモーター 6 台、複数の LED、ボタンのスキャンをすべて処理したが、ユーザーにはリアルタイムのように見えていた
当時使えたリソースがどれほど少なかったか、それでも高水準言語を使っていたという事実を、いまだに多くの人がきちんと理解していないのは驚きだ
ESP32 で MicroPython を使ったとき、割り込みハンドラーを Python らしくサポートしている点が気に入った。RP2040 でも同様にサポートされていると思う
私の顕微鏡では、ほぼリアルタイムに近いステッピングモーター制御を FluidNC ボードが担当し、私は軽量なシリアルプロトコルで制御している。
ただし https://pypi.org/project/micropython-stepper/ を見ているが、これはハードウェアタイマーと Python コードを使っているように見える
ちゃんとしたプロジェクトで Hubris を一度使ってみたい(https://hubris.oxide.computer/reference/)。
そのアーキテクチャ上のアプローチは、私が組み込み分野で目指しているものとかなりよく合っている。ただ、私はそれを C でもっと苦労しながらやっている。
ホスティング環境で Erlang/Elixir を使うやり方とも大きくは違わない。
メモリがさらに限られていて複数のスタックを抱えられない状況では、Embassy がよい選択肢に見える
新しい組み込みプロジェクトは常に、本当に常に、仮想マシン で始めるべきだ。同じシステムにツールを混在させてはいけない。
私の商用プロジェクトでは、品質問題の最大の原因がまさにこれだった。
新しいチップセット、新しいベンダーでプロジェクトを始めるなら、新しい VM を作って、その VM にベンダーツールだけをインストールし、そこでビルドすべきだ。
実験的な開発は自分のローカルの非 VM マシンでやっても構わない。しかしリリースは必ず VM 上で行うべきだ。そして頼むから VM と開発ワークステーションを同期 しておいてほしい。
今、元の開発者が休暇中の状態で、どうしても修正しなければならないファームウェアのビルド対応をしていて、とてつもない苦痛を味わっている。誰もその人のワークステーションにアクセスできず、用意されていた VM は 6 か月も遅れており、顧客はなぜ地球上でたった 1 人の特別なプログラマーしかできない仕事のためにチーム全体のコストを払わなければならないのかと尋ねている
リリースは CI システムがビルドすべきだと思う。git でリリースタグを打てば、テスト通過後にバイナリが出てくる形であるべきだ
FreeRTOS を選んでおけば、大きく外すことはまずない。現時点では事実上の業界標準だ
printf()を動作させる問題には、どんな解決策があるのだろうか?かなり厄介そうに見える
元の記事と似たような経験があった
そこでシンプルな グリーンスレッドタイマー を自作した
本物のカーネルのように実際のプロセス管理をサポートするわけでもなく、何の保証もないが、ベアメタルのスケジューリングより先まで進めたし、RTOS という混沌を避けることもできた
C でオプションのコンテキスト構造体を受け取る JavaScript のタイマーコールバックのようなものと考えればよい
さまざまなセンサーへの問い合わせ、入力信号の処理、制御判断、コマンドの送信を、それぞれ異なる周期で処理できた
こうした遅くて抽象的なアーキテクチャで人生を台無しにする前に、まずはこのやり方を試してみることを強く勧める