
2026/09/03 3:32
組み込みRust RTOS対C RTOS
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
主な発見は、協業型マルチタスクを使用した場合、Rust 非同期フレームワークである Embassy が STM32 マイクロコントローラー上で FreeRTOS よりもメモリフットプリント、コードサイズ、割り込み遅延の面で優れているということです。比較は 180MHz で動作する STM32F446ZET6 上で行われ、プログラムサイズ、静的 RAM 使用量、プログラミングの容易さを含む指標のために標準アプリケーション(LED ブlinking、除振なしのボタン割り込み(遅延測定に影響を与えないように)、UART 送信)が評価されました。Embassy はコンパイル時に既知の静的タスク割当を必要とする協業型マルチタスク非同期エグゼキューターを利用するのに対し、FreeRTOS は完全なプロセッサコンテキストスィッチを行う独立したスレッドを管理する優先制御マルチスレディングを採用しています。結果はすべての初期指標において Embassy が勝利しました:平均割り込み時間は 51.0%、プログラムサイズは 31.0%、静的メモリ(精度のために C ヒープの 15kb を除外)は 84.1% 低減されました。重要なトレードオフは、スレッドの目覚め時のコンテキストスィッチ遅延において FreeRTOS が Embassy の状態機械再開に比べてより高速であることです。ただし、Embassy の割り込みハンドラマクロは複数のエグゼキューターを通じて高優先度コンテキストが低優先度の非同期タスクを効果的に優先制御することを可能にします。結局のところ、Rust ベースの非同期ソリューションである Embassy は最小電力消費と小さなバイナリサイズを重視するプロジェクトに対して優れており、FreeRTOS はスレッド目覚め時間の急速さが重要である場合に依然として関連性を持ちます。将来の分析では効率化指標をさらに改善し、Embassy と比較して平均割り込み時間を 52.8% 削減し、スレッド時間を 49.1% 高速化するというベンチマーク結果を示す RTIC フレームワークを検討できるかもしれません。開発者はリソース制約のある組み込みデバイスで低割り込み遅延を必要とする場合 Embassy を選択し、即応するマルチスレッド応答性を求めるアプリケーションには FreeRTOS を保留すべきです。
本文
STM32F446 上での Async Rust (Embassy) と FreeRTOS (C) 対決:パフォーマンスと実装性の検証
STM32F446 マイコンにおいて、非同期 Rust フレームワーク Embassy とリアルタイムオペレーティングシステム **FreeRTOS **(C) を対決させました。両者は同一の動作を行うアプリケーションを実行し、以下の 4 つの指標で比較を行いました。
- 割り込み遅延
- プログラムサイズ (Flash)
- RAM 使用量 (静的メモリ)
- **プログラミングのしやすさ **(Ergonomics)
既存の Rust vs C の議論とは異なり、今回は「通常的な」アプリケーション構成(移植性、シンプルさ、標準最適化)を前提とした比較です。
環境と対戦カード
使用環境
- マイコン: STM32F446ZET6 (180MHz)
- 計測機器: Rigol DS1054Z オシロスコープ
- C プロジェクト: STMCube 1.8 を使用
- Rust プロジェクト: 標準的な
バイナリを使用cargo
アプリケーションのガイドライン
双方のプロジェクトは以下の条件で構築されています。
- 移植性: HAL に依存しない部分は、他チップやアーキテクチャへの移植が可能 (Portable-ish)。
- シンプルさ: コードが直感的で理解しやすい構成。
- 最適化: コンパイルオプション、RTOS 設定、スレッド優先度などは通常の設定。
アプリケーション機能 (3 つのタスク)
- LED タスク: 200ms に 1 回点灯し、その間に 100ms 間隔で実行エンジン遅延関数を使用(ボタンのみ)。
- ボタン入力処理:
- ユーザーボタンが押された場合、別のスレッドから通知される(レジスター直接アクセスなし)。
- GPIO 割り込みで状態変化を検知。
- 共有原子変数でボタン High/Low を通信し、メッセージキューへ
を登録。Button is <0/1> (N)\n
- UART 送信タスク: メッセージキューの文字列をシリアルポートに出力。
アーキテクチャ解説
非同期 Rust (Future & Task)
Rust の非同期関数は
Future (未来) を返す構文糖衣です。
- 状態機械: ポーリング可能で、
点を跨ぐ変数を追跡し、以前の状態から再開できる。await - **怠惰性 **(Lazy): 実行はポーリングが開始されるまで待機する。
- **Waker **(ワーカー): 「未来を再びポーリングすべき」と実行エンジンにシグナルを送る機構。
- タスク: トップレベルの未来。
Embassy の制約と特徴
- タスクは静的アロケーションのみ(動的アロケータ非依存)。
- コンパイル時に既知のタスク構成。
- Rust Nightly (
機能) 必須。type_alias_impl_trait - **協調的多任務 **(Cooperative Multitasking): アクティブなタスクが
中のみ、より重要なタスクへの切り替えが可能(プリエンプション不可)。await
RTOS (FreeRTOS)
- スレッドベース: 状態機械ではなく通常のコードを実行。
- コンテキストスイッチ: スレッド停止時にプロセッサ全体の状態を保存・復元。
- プリエンプティブ: カーネルが強制終了可能な優先度制御と公平な実行時間確保。
対決結果の測定項目
| 項目 | 詳細 |
|---|---|
| パフォーマンス | ボタン GPIO 割り込み処理速度、スレッド再開までの時間。 |
| 割り込み遅延 | 割り込み開始からボタンスレッドの再開(ピン立ち上がり)までの時間差。 |
| プログラムサイズ | で測定したテキストセクションサイズ (Flash)。動的メモリは除外。 |
| プログラミングのしやすさ | 主観的評価。最適化競争ではなく、比較的大きなプログラムの構築効率を重視。 |
コード実装の詳細
1. ボタン割り込みの通知
- **Rust **(Embassy): ハードウェア割り込み処理が内部で管理されており、追加作業不要。
- **C **(FreeRTOS): 割り込みハンドラ内で明示的にスレッドにフラグを設定 (
) する必要がある。osThreadFlagsSet
// C: FreeRTOS void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == USER_Btn_Pin) { osThreadFlagsSet(buttonWaiterHandle, 1); } }
2. LED の点滅処理
- Rust:
注釈付きの静的アロケーション。原子変数による同期。#[embassy::task] - C: グローバル変数を介した同期。C11 アトミック型を使用。
// Rust: Embassy #[embassy::task] async fn blink_led(mut led: Output<'static, PB0>, button_high: &'static AtomicBool) { loop { Timer::after(Duration::from_millis(100)).await; if !button_high.load(Ordering::SeqCst) { led.set_high().unwrap(); } Timer::after(Duration::from_millis(100)).await; led.set_low().unwrap(); } } // C: FreeRTOS void StartBlinkLedTask(void *argument) { for (;;) { osDelay(100); if (atomic_load(&buttonPressed) == GPIO_PIN_RESET) { HAL_GPIO_WritePin(LD1_GPIO_Port, LD1_Pin, GPIO_PIN_SET); } osDelay(100); HAL_GPIO_WritePin(LD1_GPIO_Port, LD1_Pin, GPIO_PIN_RESET); } }
3. メッセージキューの UART 送信
- Rust:
とスタック割当済みの型を使用。シンプル。ArrayVec - C: 構造体定義 (
) を作成し、メッセージ長を管理。UartMessage
// C: FreeRTOS typedef struct { char data[32]; } UartMessage; void StartUartWriter(void *argument) { for (;;) { UartMessage message; CheckStatus( osMessageQueueGet(uartQueueHandle, &message, NULL, osWaitForever) ); size_t len = strnlen(message.data, sizeof(message.data)); CheckStatus( HAL_UART_Transmit(&huart3, (uint8_t*)&message.data, (uint16_t)len, 1000) ); } }
4. ボタンの待機・デバウンシング処理
- Rust:
を使用。ExtiInput
/wait_for_rising_edge
。falling_edge - C: EXTI 設定 (
/RTSR
) をコード内で記述。ピン状態を直接制御。FTSR
// Rust: Embassy #[embassy::task] async fn button_waiter( mut button: ExtiInput<'static, PC13>, button_pressed: &'static AtomicBool, sender: Sender<'static, Noop, ArrayString<32>, 8>, mut button_processed: Output<'static, PG1>, ) { let mut trigger_count = 0; loop { // Rising edge detection... button.wait_for_rising_edge().await; // Message send & state update... // Falling edge detection... button.wait_for_falling_edge().await; // Message send & state update... } } // C: FreeRTOS void StartButtonWaiterTask(void *argument) { for (;;) { EXTI->RTSR |= USER_Btn_Pin; // Rising edge setup osThreadFlagsWait(...); // Wait for flag // Process message & toggle pin... EXTI->FTSR &= ~USER_Btn_Pin; // Falling edge setup (Reset FTSR to ignore falling) // Logic repeats... } }
割り込みハンドラの調整 (Rust Embassy)
Embassy の標準ではピンを即座に High にできないため、以下のマクロを追加。
macro_rules! impl_irq { ($e:ident) => { #[interrupt] unsafe fn $e() { pac::gpio::Gpio(0x40021800 as *mut u8).odr().modify(|odr| odr.set_odr(0, stm32_metapac::gpio::vals::Odr::HIGH)); let x = on_irq(); pac::gpio::Gpio(0x40021800 as *mut u8).odr().modify(|odr| odr.set_odr(0, stm32_metapac::gpio::vals::Odr::LOW)); x } }; }
メasurement Results (結果)
ボタンを 100 回押し、200 サンプルを測定・平均化および標準偏差を算出しました。
比較表:Embassy vs FreeRTOS
| 項目 | Rust (Embassy) | C (FreeRTOS) | 差 (絶対値) | 差 (%) |
|---|---|---|---|---|
| **割り込み時間 **(平均) | 2.962 µs | 1.450 µs | -1.512 µs | -51.0% (Rust 遅い) |
| **割り込み時間 **(SD) | 124.8 ns | 4.96 ns | -119.84 ns | -96.0% (Rust 変動大) |
| **スレッド時間 **(平均) | 16.19 µs | 11.64 µs | -4.55 µs | -28.1% (Rust 遅い) |
| **スレッド時間 **(SD) | 248.2 ns | 103.0 ns | -145.2 ns | -56.2% (Rust 変動大) |
| **割り込み遅延 **(平均) | 4.973 µs | 3.738 µs | -1.235 µs | -24.8% (Rust 遅い) |
| プログラムサイズ | 20,676 B | 14,272 B | -6,404 B | -31.0% (Rust 大きい) |
| 静的メモリ使用量 | 5,480 B | 872 B | -4,608 B | -84.1% (Rust 大きい*) |
*注:編集により、C プロジェクトでの未検出のヘッパ (Heap) 使用が 15KB 分加算されたことを考慮し、静的メモリサイズから差し引いた数値です。修正前の比較では C がさらに不利でした。
深層分析
- コンテキストスイッチ速度: RTOS は速い
。RTOS の方がスレッド再開自体は優れています。(4.973 - 2.962 = 2.011us) vs (3.738 - 1.450 = 2.288us) - ボトルネック: Rust (Embassy) が全項目で遅いのは、割り込み時間の 2 倍程度に起因しています。正確な原因(HAL、RTOS 非効率、シグナリングモデルなど)は特定されていません。
対戦結果:勝者は?
**Embassy / Rust **(勝利)
- プログラミング体験が大幅に優れています。
- 全項目で負けていた割には、コードの綺麗さと統合感において圧倒的な利点があります。
- FreeRTOS/C の自由度は高まりますが、実装コスト(通知処理、ピン制御など)が増大します。
追加評価:RTIC (2022/2/17 追記)
Rust エンベデッドエコシステムで広く使われている完全な割り込み駆動ランタイム RTIC を追加比較しました。
- RTIC の特徴: マクロ内で割り込みハンドラ定義。即座にピンを High にできるなど、ユーザー制御が容易。
| 項目 | RTIC (Rust) | Embassy (Rust) | 差 (%) |
|---|---|---|---|
| **割り込み時間 **(平均) | 0.651 µs | 1.450 µs | +122.8% (RTIC は約 2 倍速い) |
| **スレッド時間 **(平均) | 7.807 µs | 11.64 µs | -49.1% (RTIC が速い) |
| **割り込み遅延 **(平均) | 1.184 µs | 3.738 µs | +215.7% (RTIC の遅延は半分以下) |
| プログラムサイズ | 8,888 B | 14,272 B | -60.0% (RTIC が小さい) |
| 静的メモリ使用量 | 392 B | 872 B | -122.4% (RTIC が少ない) |
RTIC の結果について
- RTIC は圧倒的なパフォーマンスを発揮しています。ランタイムが最小限であるためです。
- 「割り込み上で機能を実装すると、結局 RTIC の劣化版になってしまい、そのまま RTIC を使うのが良い」という格言通りです。
まとめと考察
- **RTOS **(FreeRTOS): 実際のリアルタイムアプリケーションで使用可能ですが、プリエンプト不可な非同期タスクは常に時間内に完了を保証できない点に注意が必要です(ただし Embassy は割り込みコンテキスト内で追加の実行エンジンを使用することで対応可能です)。
- Waker の仕組み: 実行エンジンが割り込みコンテキストにある場合、Waker は割り込みペンドインフラグも設定します。これにより、より優先度の高い割り込みを持つ実行エンジンが低いものをプリエンプトできます。
- 技術的結論:
- Embassy/Rust: コード記述の簡潔さ、統合感、安全性において優れています。性能面では RTIC を除き C 製と同等か劣りますが、開発効率の向上を考慮すれば勝者です。
- RTIC: パフォーマンスとサイズ面での最強の結果を示しました。
謝辞: Rust Embedded Working Group ロゴデザイン (Erin Power 氏) と、記事へのフィードバック (Dario 氏, Sjors 氏) を提供した皆様に感謝します。
議論は /r/rust および /r/embedded でも行われています。