プログラミング
組み込みRust RTOS vs C RTOS
Embedded Rust RTOS vs. C RTOS (tweedegolf.nl)
要約
この記事では、組み込みシステムにおける非同期Rust(Embassy)と、従来のC言語ベースのリアルタイムオペレーティングシステム(FreeRTOS)をSTM32F446マイクロコントローラー上で比較します。両者を、割り込みレイテンシ、プログラムサイズ、RAM使用量、プログラミングの容易さといった観点から評価し、それぞれのアーキテクチャと利点を解説します。
全文翻訳
2022年1月31日 非同期Rust vs RTOS対決!
Dion
組み込みソフトウェアエンジニア
組み込み
iot
rust
今回は組み込みにおける非同期Rustに関する技術ブログ記事です。今回はEmbassy/RustとFreeRTOS/CをSTM32F446マイクロコントローラーで対決させます。
両者は同じアクションを実行するアプリケーションを実行します。その後、割り込みレイテンシ、プログラムサイズ、RAM使用量、プログラミングの容易さに基づいて評価します。CとRustを比較する記事はすでにたくさんあるので、今日はそれに焦点を当てません。
私が示そうとしているのは、2つの「通常の」アプリケーションです。どちらのプロジェクトも、多くの作業を行えばパフォーマンスを向上させるように調整できます。そうすることはほぼ終わりのないタスクになり得ます。したがって、ガイドラインとして、アプリケーションは次のようになります。
ポータブル(あるいはそれに近い)他のチップやアーキテクチャへの移植性(HALへの依存を除く)
わかりやすい
コンパイラの最適化、RTOS設定、スレッド優先度などの通常のオプションと設定で調整済み
最終的には、RTOSと非同期エグゼキューターがどのように機能するか(または機能できるか)についての基本的な理解を得られるはずです。
私は個人的な意見を持っていますが、この記事が公正な比較を提供することを願っています。提案があれば、お知らせください!
180MHzで動作するSTM32F446ZET6マイクロコントローラーと、Rigol DS1054Zオシロスコープを使用して一部の測定を行います。
非同期Rust
Rustの非同期関数は、Futureを返す関数のシンタックスシュガーです。
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
関数はステートマシンオブジェクトに変換され、ポーリング可能になります。ステートマシンにより、コードは関数にジャンプし、以前停止した場所から再開できます。また、awaitポイントをまたいで保持されるすべての変数を追跡します。
RustのFutureは遅延実行であり、ポーリングされるまで実行されません。Futureを完了まで実行するには、Pending状態を返さなくなりReady(Output)状態を返すまで、ポーリング関数を継続的に呼び出すだけです。
これはわかりやすいですが、あまり効率的ではありません。
それを修正するために、Wakerもあります。Wakerは、Futureを再度ポーリングする必要があることをエグゼキューターに通知できます。このWakerはFuture自体から呼び出されるか、Futureが依存する別のプロセス/スレッドに渡すことができます。一般的に、エグゼキューターはポーリング関数を一度呼び出し、Wakerがトリガーされた場合にのみ再度呼び出します。
Futureは他のFutureを呼び出し、それらを自身に組み込むことができます。エグゼキューターにとって、ポーリングするトップレベルのFutureは通常タスクと呼ばれます。
他にも多くのことを言うことができます。幸いなことに、私は言う必要はありません。なぜなら、そこには非常に優れたリソースがあるからです。
Under the Hood: Executing Futures and Tasks
How Rust optimizes async/await
Understanding Rust futures by going way too deep
Embassyにおいて
Embassyもこのメカニズムを使用しますが、いくつかの制約が追加されます。
タスクは静的に割り当てられる必要があります
Embassyはアロケーターに依存したくありません
すべてのタスクはコンパイル時に知られている必要があります
ナイトリーコンパイラが必要です
type_alias_impl_traitプレビュー機能が必要です
これは、アロケーターがないため、ボックス化されたトレイトオブジェクトを使用できないためです。
多くのペリフェラルに対して、Embassyは非同期インターフェースを作成しました。これにより、次のようなコードが可能になります。
#[embassy::task]
async fn my_task(mut button: ExtiInput<'static, PC13>) {
loop {
button.wait_for_rising_edge().await;
info!("Pressed!");
button.wait_for_falling_edge().await;
info!("Released!");
}
}
ここでいくつかのことが起こっています。
wait_for_rising_edgeは新しいFutureを作成して返します。Futureのコンストラクタはピンの割り込みを設定します。最初のポーリングで、FutureはそのWakerをグローバルなEXTI Waker配列に配置します。EXTI割り込みが発生すると、その配列内の適切なWakerが正しいタスクをウェイクアップするために使用されます。
したがって、割り込みが終了すると、エグゼキューターはタスクを再度ポーリングし、wait_for_rising_edge Futureはその割り込みが発生したことに気づき、準備ができたことを返します。そしてプログラムは続行されます。
Embassyが行わないことの1つはプリエンプションです。これは、アクティブなタスクが、何かにawaitするまで、より重要なタスクに切り替えられないことを意味します。これは協調型マルチタスクと呼ばれます。しかし、Embassyには、この欠けている機能が問題にならないようにする他の機能がいくつかあり、それはこの記事の後半でカバーされます。
RTOS
リアルタイムオペレーティングシステムは、すべてを独立したスレッドに分割します。タスクとは異なり、スレッドはステートマシンを実行するのではなく、通常のコードを実行します。これは、コードを特別な方法でプログラムする必要がないことを意味します。古い関数はRTOSで実行できます。
スレッドの実行を一時停止して別のスレッドに切り替える必要がある場合、スレッドは通常のコードを実行しているため、プロセッサコンテキスト全体をキャプチャして保存する必要があります。そのコードが再開するときは、プロセッサコンテキストが再び同じである必要があります。
このマルチスレッド設計は、プリエンプティブスレッドに適しています。これは、カーネルがすべてのスレッドに公平な実行時間を提供でき、ユーザーが優先度を指定でき、カーネルが予測可能な時間でイベントや割り込みに応答できることを意味します。
この説明はRTOSの表面をかすめただけです。より深く理解するために、興味のある方は以下の記事をご覧ください。
How to build a Real-Time Operating System
FreeRTOS Kernel Developer Docs
対決開始!
両方のモデルについて少し理解したので、同じプログラムを両方で実装して対決させます。
プログラム
完全に現実的なプログラムを作成することはできません。なぜなら、作成に時間がかかりすぎるからです。しかし、あまり単純すぎないものを持ってみましょう。
現実性に近づくと主張するために必要なことがいくつかあります。
複数のタスク
タスク間のデータ共有
割り込みに応答する
そこで、私たちのプログラムは次の3つの(文字通りの)タスクを実行します。
200ミリ秒ごとに100ミリ秒間LEDを点滅させる
エグゼキューターの遅延機能を使用してループする
ユーザーボタンが押された場合、LEDはオンにしない(別のスレッドから通信される。レジスタを自分でチェックしない)
ユーザーボタンの状態を追跡する
GPIO割り込みを設定して、信号の変化を検出できるようにする
ボタンが高かろうが低かろうが、共有(アトミック)ブール値で通信する
ボタンの状態が変化したら、メッセージキューに "Button is <0/1> (N)\n" というテキスト(<0/1>はボタンがローなら0、ハイなら1、Nはトリガー数)を置く
メッセージキューの内容をシリアルに出力する
文字列を含むメッセージキューを待機する
それをシリアルに出力する
測定するもの
この対決は、以下の点で勝敗が決まります。
パフォーマンス
ボタンGPIO割り込みはどのくらい時間がかかりますか?
割り込みが発生したら、ピンをハイにします
割り込みが終了したら、ピンをローにします
その間の時間はオシロスコープで測定します
ボタンのスレッドは、再び待機するまでどのくらい時間がかかりますか?
スレッドが待機を停止したら、ピンをハイにします
スレッドが再び待機を開始したら、ピンをローにします
その間の時間はオシロスコープで測定します
割り込み(処理)レイテンシ
ボタンGPIO割り込みの開始からボタンスレッドが再開するまでの時間はどのくらいですか?
割り込みピンの上昇からスレッドピンの上昇までの時間はオシロスコープで測定します
プログラムサイズ。
arm-none-eabi-sizeによって報告される.textセクション
静的メモリ使用量。arm-none-eabi-sizeによって報告される.data + .bssセクション
すべてのタスクとスレッドは静的に割り当てられます
動的なメモリ使用量は測定が難しいため、静的なメモリ使用量のみを見ています。静的に多くのメモリを割り当てるプログラムは、そうしない類似のプログラムよりもスタックメモリの使用量が少なくなる可能性が高いです。しかし、RTOSがこれに苦労する可能性があるため、比較する上で関連性の高いメトリックだと思います。
プログラミングの容易さ
非常に主観的であることは承知しています
最初から繰り返しますが、最も最適化されたソリューションを探しているわけではありません。目標は、比較的通常のプログラムを持つことです。
Exp