esp32-rust-embedded について
ESP-RS と Embassy を使った ESP32 向け no-std Rust ファームウェア開発のガイドです。組み込み開発者向けに、ビルド、ペリフェラル、非同期タスク、無線通信、省電力機能を説明し、依存関係の変更点も扱います。
主な機能
- 01ESP-RS と Embassy の依存関係設定を紹介
- 02ビルド、書き込み、よくあるエラーへの対処
- 03GPIO、ADC、センサーの実装パターン
- 04非同期タスクとタスク間通信を解説
- 05WiFi、ディスプレイ、ディープスリープを扱う
活用シーン
- 01ESP32 の Rust ファームウェアプロジェクトを構成
- 02非同期タスクでセンサー計測を実装
- 03WiFi、ビルド、スリープ復帰の問題を調査
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ psytraxx/ esp32-homecontrol-no-std-rs --skill esp32-rust-embeddedインストール後、Claude Code で/esp32-rust-embeddedを入力して読み込まれたか確認します。
詳細
ESP32 組み込み Rust 専門ガイド
ESP-RS エコシステムと Embassy 非同期フレームワークを使用した、ESP32 マイクロコントローラー向け no-std Rust 開発の専門的なガイダンスです。
ESP-RS エコシステムのスタック
コア依存関係
esp-hal = { version = "~1.1.0", features = ["esp32s3", "log-04", "unstable"] }
esp-rtos = { version = "0.3.0", features = ["embassy", "esp-alloc", "esp-radio", "esp32s3", "log-04"] }
esp-radio = { version = "0.18.0", features = ["esp-alloc", "esp32s3", "log-04", "unstable", "wifi"] }
esp-bootloader-esp-idf = { version = "0.5.0", features = ["esp32s3", "log-04"] }
esp-alloc = "0.10.0"
esp-println = { version = "0.17.0", features = ["esp32s3", "log-04"] }
# smoltcp is now a DIRECT dependency (no longer a feature of esp-radio)
smoltcp = { version = "0.13.0", default-features = false, features = [
"log", "medium-ethernet", "multicast",
"proto-dhcpv4", "proto-dns", "proto-ipv4",
"socket-dns", "socket-icmp", "socket-raw", "socket-tcp", "socket-udp",
] }
Embassy フレームワーク
embassy-executor = { version = "0.10.0", features = ["log"] }
embassy-time = { version = "0.5.0", features = ["log"] }
embassy-net = { version = "0.9.1", features = ["dhcpv4", "log", "medium-ethernet", "tcp", "udp"] }
embassy-sync = { version = "0.7.2" }
依存関係の階層
esp-radio (WiFi) -> esp-rtos (scheduler) -> esp-hal (HAL) -> esp-phy (PHY)
embassy-executor -> embassy-time -> embassy-sync -> embassy-net
Rust エディションと MSRV
- エディション:2024(Cargo.toml の
edition = "2024") - MSRV:1.88(
rust-version = "1.88") - バイナリの配置場所:
src/main.rsではなくsrc/bin/main.rs— Cargo.toml に[[bin]]エントリーが必要です
ビルドとフラッシュ
環境のセットアップ
# Install ESP toolchain (one-time)
espup install
source $HOME/export-esp.sh
# Configure credentials (.env file)
cp .env.dist .env
# Edit: WIFI_SSID, WIFI_PSK, MQTT_HOSTNAME, MQTT_USERNAME, MQTT_PASSWORD
ビルドコマンド
# Quick build and flash
./run.sh
# Manual release build (recommended)
cargo run --release
# Debug build (slower on device)
cargo run
Cargo プロファイルの最適化
[profile.dev]
opt-level = "s" # Rust debug too slow for ESP32
[profile.release]
lto = 'fat'
opt-level = 's'
codegen-units = 1
よくあるビルドエラー
リンカーエラー:未定義シンボル _stack_start
build.rsに linkall.x の設定があることを確認してください- esp-hal のバージョン互換性を確認してください
未定義シンボル:esp_rtos_*(「esp-radio でスケジューラーが有効になっていません」)
- esp-rtos がタイマーとソフトウェア割り込みの両方で起動されていることを確認してください(0.3.0 で変更):
let timg0 = TimerGroup::new(peripherals.TIMG0);
let sw_interrupt = esp_hal::interrupt::software::SoftwareInterruptControl::new(
peripherals.SW_INTERRUPT
);
esp_rtos::start(timg0.timer0, sw_interrupt.software_interrupt0);
環境変数のエラー
- 変数は
env!()マクロを介してコンパイル時に取得されます - 変更を反映するには、完全な再ビルドが必要です
No-Std パターン
アプリケーションのエントリーポイント
#![no_std]
#![no_main]
// Recommended lints (now standard in esp-generate templates)
#![deny(clippy::mem_forget)] // esp-hal types must not be mem::forgotten
#![deny(clippy::large_stack_frames)]
use embassy_executor::Spawner;
use esp_hal::{clock::CpuClock, timer::timg::TimerGroup};
esp_bootloader_esp_idf::esp_app_desc!();
#[esp_rtos::main]
async fn main(spawner: Spawner) -> ! {
// Initialize logger (reads RUST_LOG env var at compile time via esp-config)
esp_println::logger::init_logger_from_env();
// Initialize HAL with max CPU clock
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals = esp_hal::init(config);
// Setup heap allocator using reclaimed memory (replaces dram2_uninit approach)
esp_alloc::heap_allocator!(#[esp_hal::ram(reclaimed)] size: 73744);
// Start RTOS scheduler — now requires BOTH a timer AND a software interrupt
let timg0 = TimerGroup::new(peripherals.TIMG0);
let sw_interrupt = esp_hal::interrupt::software::SoftwareInterruptControl::new(
peripherals.SW_INTERRUPT
);
esp_rtos::start(timg0.timer0, sw_interrupt.software_interrupt0);
// Spawn tasks...
let _ = spawner;
loop {}
}
メモリ管理
- 動的割り当てには
esp-allocを使用します - コンパイル時に容量を指定できる
heaplessコレクションを優先します 'staticライフタイムが必要な場合はstatic_cell::StaticCellを使用します
文字列の扱い
use alloc::string::String; // Dynamic strings (heap)
use heapless::String; // Bounded strings (stack)
let s: heapless::String<64> = heapless::String::new();
可能な場合はクローンを避けてください。
StaticCell パターン
static CHANNEL: StaticCell<Channel<NoopRawMutex, Data, 3>> = StaticCell::new();
// In async function
let channel: &'static mut _ = CHANNEL.init(Channel::new());
let (sender, receiver) = (channel.sender(), channel.receiver());
ハードウェアのパターン
GPIO の設定
use esp_hal::gpio::{Level, Output, OutputConfig, Pull, DriveMode};
// Standard output
let pin = Output::new(peripherals.GPIO2, Level::Low, OutputConfig::default());
// Open-drain for sensors like DHT11
let pin = Output::new(
peripherals.GPIO1,
Level::High,
OutputConfig::default()
.with_drive_mode(DriveMode::OpenDrain)
.with_pull(Pull::None),
).into_flex();
キャリブレーションを使用した ADC 読み取り
use esp_hal::analog::adc::{Adc, AdcConfig, AdcCalCurve, Attenuation};
let mut adc_config = AdcConfig::new();
let pin = adc_config.enable_pin_with_cal::<_, AdcCalCurve<ADC2>>(
peripherals.GPIO11,
Attenuation::_11dB // 0-3.3V range
);
let adc = Adc::new(peripherals.ADC2, adc_config);
// Read with nb::block!
let value = nb::block!(adc.read_oneshot(&mut pin))?;
ペリフェラルバンドルパターン
pub struct SensorPeripherals {
pub dht11_pin: GPIO1<'static>,
pub moisture_pin: GPIO11<'static>,
pub power_pin: GPIO16<'static>,
pub adc2: ADC2<'static>,
}
非同期タスクのアーキテクチャ
タスクの定義
#[embassy_executor::task]
pub async fn my_task(sender: Sender<'static, NoopRawMutex, Data, 3>) {
loop {
// Do work
sender.send(data).await;
Timer::after(Duration::from_secs(5)).await;
}
}
タスクのスポーン
spawner.spawn(sensor_task(sender, peripherals)).ok();
spawner.spawn(update_task(stack, display, receiver)).ok();
タスク間通信
チャネル(複数の値)
use embassy_sync::{blocking_mutex::raw::NoopRawMutex, channel::Channel};
static CHANNEL: StaticCell<Channel<NoopRawMutex, Data, 3>> = StaticCell::new();
// sender.send(data).await / receiver.receive().await
シグナル(単一の通知)
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal};
static SIGNAL: Signal<CriticalSectionRawMutex, ()> = Signal::new();
// SIGNAL.signal(()) / SIGNAL.wait().await
再接続ループパターン
'reconnect: loop {
let mut client = initialize_client().await?;
loop {
match client.process().await {
Ok(_) => { /* handle messages */ }
Err(e) => {
println!("Error: {:?}", e);
continue 'reconnect; // Reconnect on error
}
}
}
}
電源管理
ディープスリープの設定
use esp_hal::rtc_cntl::{Rtc, sleep::{RtcSleepConfig, TimerWakeupSource, RtcioWakeupSource, WakeupLevel}};
pub fn enter_deep(wakeup_pin: &mut dyn RtcPin, rtc_cntl: LPWR, duration: Duration) -> ! {
// GPIO wake source
let wakeup_pins: &mut [(&mut dyn RtcPin, WakeupLevel)] = &mut [(wakeup_pin, WakeupLevel::Low)];
let ext0 = RtcioWakeupSource::new(wakeup_pins);
// Timer wake source
let timer = TimerWakeupSource::new(duration.into());
let mut rtc = Rtc::new(rtc_cntl);
let mut config = RtcSleepConfig::deep();
config.set_rtc_fastmem_pd_en(false); // Keep RTC fast memory powered
rtc.sleep(&config, &[&ext0, &timer]);
unreachable!();
}
RTC Fast Memory の永続化
use esp_hal::ram;
#[ram(unstable(rtc_fast))]
pub static BOOT_COUNT: RtcCell<u32> = RtcCell::new(0);
// Survives deep sleep - read/write with .get()/.set()
let count = BOOT_COUNT.get();
BOOT_COUNT.set(count + 1);
電力の最適化
- センサーの読み取り時のみ、センサーの電源ピンを切り替えます
- ディスプレイでは省電力モードを使用します
- スリープ前に WiFi を適切に切断します
- 起動状態の継続時間を最小限に抑えます
WiFi ネットワーク
接続のセットアップ
// esp_radio::init() is GONE — wifi::new() is called directly (as of esp-radio 0.18+)
use esp_radio::wifi::{self, ClientConfig, ModeConfig, WifiController};
let (mut controller, interfaces) =
esp_radio::wifi::new(peripherals.WIFI, Default::default())
.expect("Failed to initialize Wi-Fi controller");
let client_config = ModeConfig::Client(
ClientConfig::default()
.with_ssid(env!("WIFI_SSID").try_into().unwrap())
.with_password(env!("WIFI_PSK").try_into().unwrap()),
);
controller.set_config(&client_config)?;
controller.start_async().await?;
controller.connect_async().await?;
Embassy-Net スタック
use embassy_net::{Config, Stack, StackResources};
let config = Config::dhcpv4(DhcpConfig::default());
let (stack, runner) = embassy_net::new(wifi_interface, config, stack_resources, seed);
// Wait for link and IP
loop {
if stack.is_link_up() { break; }
Timer::after(Duration::from_millis(500)).await;
}
loop {
if let Some(config) = stack.config_v4() {
println!("IP: {}", config.address);
break;
}
Timer::after(Duration::from_millis(500)).await;
}
WiFi の適切なシャットダウン
pub static WIFI_SIGNAL: Signal<CriticalSectionRawMutex, ()> = Signal::new();
// In connection task
WIFI_SIGNAL.wait().await;
controller.stop_async().await?;
// Before deep sleep
WIFI_SIGNAL.signal(());
センサーパターン
ウォームアップを伴う ADC サンプリング
async fn sample_adc_with_warmup<PIN, ADC>(
adc: &mut Adc<ADC, Blocking>,
pin: &mut AdcPin<PIN, ADC>,
warmup_ms: u64,
) -> Option<u16> {
Timer::after(Duration::from_millis(warmup_ms)).await;
nb::block!(adc.read_oneshot(pin)).ok()
}
電源を制御したセンサー読み取り
async fn read_sensor(adc: &mut Adc, pin: &mut AdcPin, power: &mut Output) -> Option<u16> {
power.set_high();
let result = sample_adc_with_warmup(adc, pin, 50).await;
power.set_low();
result
}
外れ値に強い平均化
fn calculate_average<T: Copy + Ord + Into<u32>>(samples: &mut [T]) -> Option<T> {
if samples.len() <= 2 { return None; }
samples.sort_unstable();
let trimmed = &samples[1..samples.len() - 1]; // Remove min/max
let sum: u32 = trimmed.iter().map(|&x| x.into()).sum();
(sum / trimmed.len() as u32).try_into().ok()
}
ディスプレイの統合
ST7789 パラレルインターフェース
use mipidsi::{Builder, options::ColorInversion};
let di = display_interface_parallel_gpio::Generic8BitBus::new(/*pins*/);
let mut display = Builder::new(ST7789, di)
.display_size(320, 170)
.invert_colors(ColorInversion::Inverted)
.init(&mut delay)?;
省電力モード
display.set_display_on(false)?; // Enter power save
// Before deep sleep
power_pin.set_low();
エラー処理
モジュールのエラーパターン
#[derive(Debug)]
pub enum Error {
Wifi(WifiError),
Display(display::Error),
Mqtt(MqttError),
}
impl From<WifiError> for Error {
fn from(e: WifiError) -> Self { Self::Wifi(e) }
}
エラーを返すメイン関数のパターン
// Note: #[esp_rtos::main] (not use esp_rtos::main; #[main])
// Note: main now returns `-> !` (diverging)
#[esp_rtos::main]
async fn main(spawner: Spawner) -> ! {
if let Err(error) = main_fallible(spawner).await {
log::error!("Fatal: {:?}", error);
esp_hal::system::software_reset();
}
unreachable!()
}
async fn main_fallible(spawner: Spawner) -> Result<(), Error> {
// Application logic with ? operator
}
依存関係の更新
安全な更新手順
cargo outdated
cargo update -p esp-hal
cargo build --release
cargo clippy -- -D warnings
破壊的変更のパターン
- GPIO API は頻繁に変更されます(OutputConfig)
- タイマーの初期化方法の変更
- フィーチャーフラグの名前変更
- esp-rtos 0.3.0:
esp_rtos::start()で引数が 2 つ(タイマーとソフトウェア割り込み)必要になりました - esp-radio 0.18.0:
esp_radio::init()が削除されました。代わりにesp_radio::wifi::new()を直接呼び出してください - smoltcp:esp-radio のフィーチャーではなく、直接の依存関係になりました
- heap_allocator!:
#[unsafe(link_section = ".dram2_uninit")]の代わりに#[esp_hal::ram(reclaimed)]を使用してください - esp-hal のリリースノートを必ず確認し、参考として esp-generate が生成するテンプレートを使用して移行してください
バージョンの整合
Embassy のクレートをまとめて更新します:
cargo update -p embassy-executor -p embassy-time -p embassy-sync -p embassy-net
esp-generate(プロジェクトのひな形作成ツール)
# Install
cargo install esp-generate --locked
# Generate ESP32-S3 project with Embassy + WiFi + alloc
esp-generate --chip esp32s3 -o unstable-hal -o embassy -o alloc -o wifi -o log my-project
# Available options: unstable-hal, alloc, wifi, ble-trouble, embassy,
# probe-rs, defmt, log, esp-backtrace, embedded-test, wokwi, ci,
# vscode, neovim, helix, zed
注:wifi オプションには、unstable-hal と alloc の両方が必要になりました。
注:embassy オプションには unstable-hal が必要です。
デバッグ
シリアルログ
use esp_println::println;
// New: reads RUST_LOG at compile time via esp-config (.cargo/esp-config.toml)
esp_println::logger::init_logger_from_env();
// Or explicitly set a level:
// esp_println::logger::init_logger(log::LevelFilter::Info);
log::info!("Debug: value = {}", value);
println!("Debug: value = {}", value); // also works
よくある実行時の問題
- WiFi が動作しない:2.4GHz ネットワークと信号強度を確認してください
- MQTT が動作しない:DNS の名前解決とブローカーの認証情報を確認してください
- センサーが動作しない:ウォームアップの遅延と電源ピンの切り替えを確認してください
- ディスプレイに何も表示されない:GPIO15 が HIGH(電源有効)になっていることを確認してください
- スリープからの復帰に失敗する:RTC Fast Memory の設定を確認してください
ソフトウェアリセット
use esp_hal::system::software_reset;
software_reset(); // Clean restart on unrecoverable error
コメント
Markdown 対応