2025年10月、ESP32向けRust HAL「esp-hal」がバージョン1.0.0に到達しました。これはマイコンベンダーが公式にサポートする初の安定版Rust HALであり、組込みRust界にとって画期的な出来事です。この記事では、esp-hal 1.0の意義から、実際にLチカを動かすコード、Embassyによる非同期化、よくあるハマりどころまでを、手を動かせるレベルで解説します。
esp-hal 1.0.0とは
esp-halはEspressifが公式に開発・メンテナンスしている、ESP32ファミリー向けのRustハードウェア抽象化レイヤー(HAL)です。2025年2月にベータ版、10月に正式リリースされました。「1.0」という数字が意味するのは、APIに対するセマンティックバージョニングの約束です。つまり、安定版として公開された型・関数のシグネチャは、1.x系の間は破壊的変更を受けません。これにより、業務プロダクトでも安心して依存できるようになりました。
1.0で安定化された機能:
- HAL初期化(
esp_hal::init)と設定 - GPIO、UART、SPI、I2Cドライバ
- 時間モジュール(Instant、Duration、Rate)
- async/blockingの両方のドライバモード
- Embassyなどの非同期ランタイムとの互換性
その他の機能はunstableフィーチャーフラグの下に配置され、段階的に安定化される予定です。unstableな機能を使うこと自体は可能ですが、その部分のAPIは将来のマイナーバージョンで変わる可能性がある、という位置づけです。次の大きな安定化ターゲットはWi-Fi/Bluetooth/ESP-NOWスタック「esp-radio」です。
2つの開発アプローチ:no_std と std
ESP32でRustを使う方法は大きく2つあります。どちらを選ぶかで、ツールチェーンも書き方も変わるため、最初の分岐点として最も重要です。
1. no_std + esp-hal(ベアメタル)
OSを使わず、ハードウェアを直接制御するアプローチです。バイナリサイズが小さく、起動が速く、リアルタイム性が必要な場面に適しています。esp-hal 1.0はこのパスの安定版です。標準ライブラリ(std)は使えず、ヒープ確保も自分でesp-allocなどを足して初めて使えます。
2. std + esp-idf-hal(IDF上で動作)
ESP-IDF(C言語のESP32公式フレームワーク)の上にRust標準ライブラリを載せるアプローチです。スレッド、ネットワーキング、ファイルシステムなど標準ライブラリの機能がフルに使えます。バイナリは大きくなりますが、Wi-Fi/BLEを含む既存のIDF資産をそのまま使えるのが強みです。
どちらを選ぶべきか
| 観点 | no_std + esp-hal | std + esp-idf-hal |
|---|---|---|
| バイナリサイズ | 小さい | 大きい |
| 起動速度 | 速い | やや遅い |
| Wi-Fi / BLE | esp-radio(安定化進行中) | IDF経由で安定利用可 |
| 標準ライブラリ | 使えない(no_std) | フル利用可 |
| 非同期 | Embassyと好相性 | スレッド/async両対応 |
| 向いている用途 | 低消費電力・リアルタイム制御 | ネットワーク機器・複雑なアプリ |
迷ったら、Wi-Fi/BLEを今すぐ本格的に使いたいなら std + esp-idf-hal、低レイヤを学びたい・省電力やリアルタイム制御が主目的なら no_std + esp-hal、と覚えておくとよいでしょう。
環境構築:始め方
ここからは no_std + esp-hal を例に、実際に手を動かす手順を示します。重要なポイントは、ターゲットのアーキテクチャによってツールチェーンが分かれることです。
- RISC-Vチップ(ESP32-C2/C3/C6、ESP32-H2):標準のRustツールチェーンでそのままコンパイル可能。
espupは不要。 - Xtensaチップ(ESP32、ESP32-S2、ESP32-S3):Espressif提供のカスタムツールチェーンが必要で、
espupで導入します。
RISC-V系(ESP32-C3など)の場合
# Rust本体(未導入なら)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# C3向けターゲットを追加(imc。C6/H2はimac)
rustup target add riscv32imc-unknown-none-elf
# フラッシュ&モニタツール
cargo install espflash
# プロジェクト雛形を生成(no_std用の新しいテンプレートツール)
cargo install esp-generate
esp-generate --chip esp32c3 my-project
Xtensa系(ESP32 / S3など)の場合
# ESP32用ツールチェーンマネージャ
cargo install espup
espup install
# 生成された環境変数読み込みスクリプトをシェルに反映
. $HOME/export-esp.sh
# あとはRISC-Vと同様に esp-generate でプロジェクト生成
cargo install esp-generate
esp-generate --chip esp32 my-project
std側(esp-idf-hal)を使いたい場合は、cargo install cargo-generate 後に cargo generate esp-rs/esp-idf-template でstd用テンプレートを生成します。
最初のコード:LチカをRustで
esp-hal 1.0でのGPIO制御の基本形です。esp-generate が生成する雛形がそのまま動くので、ここでは中身を理解するためにLチカの最小構成を示します。
Cargo.toml(依存関係の例)
[dependencies]
esp-hal = { version = "1.0.0", features = ["esp32c3"] }
esp-backtrace = { version = "0.15", features = ["esp32c3", "panic-handler", "println"] }
esp-println = { version = "0.13", features = ["esp32c3"] }
src/main.rs
#![no_std]
#![no_main]
use esp_hal::clock::CpuClock;
use esp_hal::delay::Delay;
use esp_hal::gpio::{Level, Output, OutputConfig};
use esp_hal::main;
use esp_println::println;
use esp_backtrace as _;
#[main]
fn main() -> ! {
// クロックを最大に設定してHALを初期化
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals = esp_hal::init(config);
// オンボードLED(基板により GPIO 番号は異なる)
let mut led = Output::new(peripherals.GPIO2, Level::Low, OutputConfig::default());
let delay = Delay::new();
println!("blink start");
loop {
led.toggle();
delay.delay_millis(500);
}
}
ビルドして実機に書き込むのは、テンプレートに設定済みのランナー経由で1コマンドです。
# ビルド・フラッシュ・シリアルモニタまで一気に
cargo run --release
※ GPIO番号・依存クレートのバージョンはチップや時期で変わります。最終的な正解はesp-generateが生成した雛形なので、APIの細部は生成物とesp-halのdocs.rsで確認してください。
非同期で書く:Embassyとの組み合わせ
esp-hal 1.0の大きな魅力が、非同期ランタイムEmbassyとの統合です。esp-hal-embassyを足すと、async/awaitで割り込み駆動の処理を、まるで普通の非同期Rustのように書けます。複数のセンサを並行してポーリングするような用途で、状態機械を手書きするより遥かに読みやすくなります。
#![no_std]
#![no_main]
use embassy_executor::Spawner;
use embassy_time::{Duration, Timer};
use esp_hal::clock::CpuClock;
use esp_hal::gpio::{Level, Output, OutputConfig};
use esp_hal::timer::systimer::SystemTimer;
use esp_backtrace as _;
#[esp_hal_embassy::main]
async fn main(_spawner: Spawner) {
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals = esp_hal::init(config);
// Embassyのタイムドライバを起動
let systimer = SystemTimer::new(peripherals.SYSTIMER);
esp_hal_embassy::init(systimer.alarm0);
let mut led = Output::new(peripherals.GPIO2, Level::Low, OutputConfig::default());
loop {
led.toggle();
// delayと違い、待っている間CPUはスリープ/他タスク実行できる
Timer::after(Duration::from_millis(500)).await;
}
}
ブロッキングのDelayはその間CPUを占有しますが、Timer::after().awaitは待機中に他のタスクへ実行を譲ります。これがasyncにする最大のメリットです。
LLVM Xtensaアップストリーム化
ESP32・ESP32-S2・ESP32-S3が採用するXtensaアーキテクチャのLLVMサポートが、本家LLVMへのアップストリーム化が進んでいます。基本ISAの大部分はすでにLLVM本体に取り込まれており、将来的にはEspressifのカスタムLLVMフォークなしで、標準のRustツールチェーンからXtensaターゲットにコンパイルできるようになります。そうなればespupすら不要になり、RISC-Vチップと同じ手軽さでESP32が扱えるようになります。
なお、RISC-VベースのESP32-C系/H系チップはすでに標準ツールチェーンでコンパイル可能です。これから新規に始めるなら、ツールチェーンのハマりが少ないESP32-C3やC6から入るのがおすすめです。
よくあるハマりどころ
- Xtensaターゲットがビルドできない:
. $HOME/export-esp.shをシェルに読み込み忘れているケースが大半。新しいターミナルを開くたびに必要です。 - フラッシュ時にポートが見つからない:Linuxでは
dialoutグループへの追加(sudo usermod -aG dialout $USER)と再ログインで解決することが多いです。 - unstableなAPIでコンパイルエラー:該当機能の
unstableフィーチャーをCargo.tomlで有効化する必要があります。エラーメッセージに必要なフィーチャー名が出ます。 - リンカエラー / メモリ不足:
--releaseでビルドするとサイズと最適化が改善します。開発中もサイズが厳しい場面ではopt-level = "s"を検討します。
よくある質問(FAQ)
Q. C/C++(ESP-IDF)から乗り換える価値はある?
メモリ安全性(バッファオーバーフローやuse-after-freeをコンパイル時に排除)と、強力な型・所有権による設計の堅牢さが得られます。一方で、Wi-Fi/BLEを多用する既存資産が多い現場では、まだIDF(C)側の方が情報も枯れています。新規・低レイヤ・安全性重視ならRust、という判断がバランス良いでしょう。
Q. どのチップから始めるべき?
ツールチェーンが標準のままで済むRISC-V系(ESP32-C3 / C6)が最も導入が楽です。Xtensa系のESP32/S3はespupが必要な分、最初の一歩がやや重くなります。
Q. esp-hal と esp-idf-hal、結局どっち?
上の比較表のとおりです。「OSなしで軽く速く」ならesp-hal(no_std)、「標準ライブラリとIDF資産をフルに使いたい」ならesp-idf-hal(std)です。
関連記事
- ESP32の最新動向まとめ(2025-2026):新チップ・Wi-Fi 6・AI対応まで完全解説
- ESP32-S3でTinyML入門:エッジAI推論を実装する方法
まとめ
esp-hal 1.0.0のリリースにより、ESP32でのRust開発は「実験的」から「プロダクション対応」へと段階が上がりました。RISC-V系チップなら標準ツールチェーンでLチカまで数分、Embassyを足せば非同期処理も自然に書けます。メモリ安全性とゼロコスト抽象化というRustの利点を、IoTデバイス開発で活かせる時代が来ています。C/C++に慣れた組込みエンジニアも、ぜひRust on ESP32を試してみてください。
コメントを残す