Skip to content

✍️ 필사 모드: 組み込み Rust 2026 深掘り — no_std・Embassy・esp-rs・RP2350・probe-rs で見るマイクロコントローラ上の Rust 実戦記

日本語
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

プロローグ — 「組み込み Rust はいつ使えるようになるのか」の問いは終わった

5 年前ならこの記事の最初の一文は「組み込み Rust はまだ実験だが」で始まっていただろう。2021 年の Embassy は alpha、RP2040 の HAL はまだ定着していなかった、ESP32 では Rust が LLVM フォーク上で動いていた、probe-rs は OpenOCD の補助ツールだった。

2026 年 5 月の風景はまったく違う。

  • Embassy 0.6 がマイクロコントローラ上 async ランタイムの事実上の標準として定着した。メインループ + 割り込みハンドラのパターンはもう新規プロジェクトであまり見ない。
  • RP2350(Raspberry Pi Pico 2 のデュアル Cortex-M33 + デュアル RISC-V Hazard3)の HAL が rp235x-hal / embassy-rp 上で stable に定着した。
  • esp-rs は Espressif の公式第一級 SDK に格上げされた。Xtensa(ESP32, ESP32-S3)は依然 LLVM フォークが必要だが、RISC-V ライン(ESP32-C3, C6, H2, P4)は upstream Rust でビルドできる。
  • probe-rs が OpenOCD を実質的に置き換えた。cargo run 一回で flash + RTT ログ + バックトレースが一つのターミナルから流れてくる。
  • defmt ロギングは printf より速く、フォーマッタはホスト側で動く — ファームウェアバイナリにフォーマット文字列が乗らない。
  • 非 async の組み込み Rust も健在。Embassy を使わないのは罪ではない。ただ新規プロジェクトはたいてい Embassy を選ぶというだけ。

要するに 組み込み Rust は 2026 年に production だ。 Apollo3・Arduino R4・バッジ・ホームオートメーション・アートインスタレーション — コードに困らない分野で実際に動いている。本稿はこの風景を頭から尻尾まで、no_std の基礎から Embassy の最初の blinky まで一気に押さえる。


1 章 · 風景 — なぜいま Rust がマイクロコントローラに入るのか

1.1 組み込みの 5 つの圧力

マイクロコントローラのファームウェアを書く人は同時に 5 つの圧力を受ける。

  1. メモリ — RAM 32KB、Flash 256KB の環境 で標準ライブラリが入らない。
  2. 決定性 — ISR の中で動的割り当てしてはいけない。 リアルタイム性が壊れる。
  3. ハードウェア抽象化 — GPIO・UART・SPI・I2C・タイマーをチップ固有のコードなしに扱いたい。
  4. 並行性 — センサポーリング + UART RX + ログ送信 + ディスプレイ更新を OS なしで同時にやらねばならない。
  5. デバッグ — JTAG/SWD・RTT・semihosting で印字が難しい。延々と LED 点滅デバッグ。

1.2 C/C++ が 60 年間答えだった理由

この 5 つの圧力に対して C/C++ は実は良い答えを持っていた。static 変数 + 割り込みハンドラ + RTOS あるいはスーパーループ、vendor SDK(ST の HAL、Espressif の ESP-IDF、Nordic の nRF Connect SDK)。組み込み産業全体が 60 年間この上で動いてきた。

問題は二つ。

  • メモリ安全性 — null ポインタ、不正なキャスト、バッファオーバーフロー。ファームウェアを 1 行間違えるとデバッグが無限 LED。
  • 並行性モデルの貧しさ — RTOS タスク + ミューテックス + キューが標準だが、優先度逆転・デッドロック・優先度継承の落とし穴が尽きない。

1.3 Rust の答え方

Rust はこれら一つ一つの圧力に対して 言語レベルの答え を持っている。

圧力Rust の答え
メモリ#![no_std] — 標準ライブラリなしでビルド。corealloc のみ使う
決定性所有権/借用 → 動的割り当てなしの状態機械を自然に表現
ハードウェア抽象化embedded-hal trait エコシステム
並行性async/await + Embassy ランタイム(または RTIC)
デバッグdefmt + probe-rs で RTT/SWD のセットアップが一行

5 年前と決定的に違うのは、これらの答え一つ一つに実働するコードと実チップのサポートがあることだ。本稿の残りはこの 5 つの答えを順に解いていく作業になる。


2 章 · no_std の本質 — core だけでどこまでいけるか

2.1 std が追いつけない環境

デスクトップ/サーバ Rust では std が OS の上で動く。std::fs はファイルシステム、std::net は TCP スタック、std::thread は OS スレッド、std::sync::Mutex は pthread の上のミューテックス。これらすべてがマイクロコントローラにはない。

#![no_std] 属性はコンパイラに「このクレートは std を使わない」と伝える。代わりに core というより小さな標準ライブラリを使う。

領域std にあるcore にある
プリミティブ型(u8, f32, …)yesyes
Option, Resultyesyes
Iterator traityesyes
Vec, String, HashMapyesno(alloc 別)
println!yesno
std::fs, std::netyesno
Box, Rc, Arcyesno(alloc 別)
スレッド/同期プリミティブyesno

core だけでもスライス・イテレータ・trait・マクロの豊かさはそのまま残る。動的割り当てが必要な型だけが落ちる。

2.2 alloc を入れる決断

マイクロコントローラにも RAM が十分あれば — RP2040 は 264KB SRAM、ESP32-S3 は 512KB — alloc クレートを追加して Box/Vec/String を取り戻せる。グローバル allocator を自分で指定する必要がある。

#![no_std]
#![no_main]

extern crate alloc;

use alloc::vec::Vec;
use embedded_alloc::Heap;

#[global_allocator]
static HEAP: Heap = Heap::empty();

#[entry]
fn main() -> ! {
    // 8KB の heap を静的に確保する
    {
        use core::mem::MaybeUninit;
        const HEAP_SIZE: usize = 8 * 1024;
        static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
        unsafe { HEAP.init(HEAP_MEM.as_ptr() as usize, HEAP_SIZE) }
    }

    let mut v: Vec<u32> = Vec::new();
    v.push(42);
    loop {}
}

ただ組み込み Rust コミュニティの一般的な助言は「ISR の中では絶対に割り当てるな」だ。メインループでは可、割り込みは事前確保したバッファのみ。

2.3 panic_handler という義務

no_std バイナリは panic が起きたとき何をするかを自分で指定しなければならない。std 環境なら自動で abort + バックトレースが出るが、マイクロコントローラにはそれがない。

use panic_halt as _;        // panic 時無限ループ
// または
use panic_probe as _;       // panic 時 RTT で報告して halt(probe-rs と組合せ)
// または
use panic_reset as _;       // panic 時ウォッチドッグでリセット

production ファームウェアならたいてい panic_reset + 直前 panic 情報を不揮発メモリに書いて再起動後に報告するパターン。

2.4 no_main とエントリポイント

no_std クレートはふつう no_main も付ける。標準の fn main エントリの代わりにチップ別ブートシーケンスが制御を握る必要があるからだ。

#![no_std]
#![no_main]

use cortex_m_rt::entry;
use panic_halt as _;

#[entry]
fn main() -> ! {
    // ! の戻り値 = 決して戻らない。無限ループまたは wfi
    loop {}
}

cortex_m_rt::entry マクロがリセットハンドラを作り、適切なブートシーケンス(スタックポインタ、BSS 領域の初期化、RAM 初期値コピー)を挿入する。RP2040 は rp2040-hal が、ESP32 は esp-hal が同じ役を担う。


3 章 · PAC・HAL・BSP — 三層の抽象化

組み込み Rust エコシステムは 3 層のクレートで成り立っている。上にいくほど抽象度が高く、下にいくほどチップ固有だ。

3.1 PAC(Peripheral Access Crate)

最下層。チップメーカーが配布する SVD(System View Description)XML ファイルから svd2rust が自動生成するクレート。レジスタのビットフィールドを型として露出する。

// rp2040-pac の使用例
use rp2040_pac as pac;

let peripherals = pac::Peripherals::take().unwrap();
let sio = peripherals.SIO;

// GPIO25 を出力にして High に
sio.gpio_oe_set.write(|w| unsafe { w.bits(1 << 25) });
sio.gpio_out_set.write(|w| unsafe { w.bits(1 << 25) });

PAC だけで書いたコードはチップデータシートそのままの見た目になる。速いが可読性が落ち、チップ間の移植性がない。

3.2 HAL(Hardware Abstraction Layer)

PAC の上に安全な抽象を載せたクレート。rp2040-halembassy-rpstm32f4xx-halesp-halnrf52840-hal など。

// rp2040-hal の使用例
use rp2040_hal::gpio::Pins;
use embedded_hal::digital::OutputPin;

let pins = Pins::new(pac.IO_BANK0, pac.PADS_BANK0, sio.gpio_bank0, &mut pac.RESETS);
let mut led = pins.gpio25.into_push_pull_output();

led.set_high().unwrap();

OutputPin trait のおかげで、チップが変わっても同じコードがほぼそのまま動く。

3.3 embedded-hal — 抽象化契約

HAL がみな同じ形の API を露出する理由は、embedded-hal という共通 trait クレートのおかげだ。2026 年時点で embedded-hal 1.0 が stable に定着し、async 版 embedded-hal-async 1.0 も一緒に出ている。

// embedded-hal 1.0 の核 trait
pub trait OutputPin {
    type Error;
    fn set_low(&mut self) -> Result<(), Self::Error>;
    fn set_high(&mut self) -> Result<(), Self::Error>;
}

pub trait SpiDevice<Word = u8>: ErrorType {
    fn transaction(&mut self, operations: &mut [Operation<'_, Word>]) -> Result<(), Self::Error>;
}

pub trait I2c<A = SevenBitAddress, Word = u8>: ErrorType {
    fn transaction(&mut self, address: A, operations: &mut [Operation<'_, Word>]) -> Result<(), Self::Error>;
}

センサドライバクレート(例えば bme280mpu6050ssd1306embedded-graphics)は、これらの trait に対して総称(ジェネリック)に書く。結果として、BME280 ドライバ 1 個が RP2040・STM32・ESP32・nRF52 すべてで動く。

3.4 BSP(Board Support Package)

特定ボード(Raspberry Pi Pico、Adafruit Feather、STM32 Nucleo など)に合わせてピンマップ・電源・LED マクロをあらかじめ整えたクレート。rp-picofeather_rp2040stm32f4-discovery など。

// rp-pico BSP の使用例 — LED ピンに既に名前がついている
use rp_pico::hal::Clock;
use rp_pico::hal::pac;
use rp_pico::hal::gpio::Pins;

let pac = pac::Peripherals::take().unwrap();
let sio = rp_pico::hal::Sio::new(pac.SIO);
let pins = rp_pico::Pins::new(pac.IO_BANK0, pac.PADS_BANK0, sio.gpio_bank0, &mut pac.RESETS);

let mut led = pins.led.into_push_pull_output();
led.set_high().unwrap();

pins.led という命名が意味深い — Pico では LED が GPIO25 に繋がっていることを BSP が知っているからだ。


4 章 · Embassy — async が当たり前の組み込み Rust

4.1 なぜ組み込みで async か

マイクロコントローラ上の並行性は伝統的に三つのパターンだった。

  • スーパーループloop { check_a(); check_b(); check_c(); }。単純だが一つの作業が長くなると他が飢える。
  • 割り込みハンドラ + メインループ — ISR が仕事を始め、メインループが結果を処理。共有状態が厄介。
  • RTOS スレッド — FreeRTOS、ChibiOS、Zephyr。コンテキストスイッチコスト、N 倍のスタックメモリ、同期プリミティブの学習コスト。

Embassy は 第四 の答えだ。async/await で協力的並行性を表現し、ランタイムが割り込みの上で目を覚ます。

4.2 Embassy の核心アイデア

// 二つのタスクが同時に動くコード。スレッドではなく — async タスク。
#[embassy_executor::task]
async fn blink(mut led: Output<'static>) {
    loop {
        led.set_high();
        Timer::after_millis(500).await;   // 500ms の間に他のタスクが動く
        led.set_low();
        Timer::after_millis(500).await;
    }
}

#[embassy_executor::task]
async fn echo(mut uart: BufferedUart<'static, UART0>) {
    let mut buf = [0u8; 64];
    loop {
        let n = uart.read(&mut buf).await.unwrap();
        uart.write_all(&buf[..n]).await.unwrap();
    }
}

肝は Timer::after_millis(500).await の一行。await 地点でタスクが譲り(yield)し、その間に別のタスクが目覚めて動ける。500ms 後にタイマー割り込みが入ると、Embassy ランタイムがこのタスクをまた起こす。

uart.read(&mut buf).await も同様 — UART RX 割り込みが来るまでタスクは眠る。ランタイムはその間に他タスクを回し、idle なら wfi(Wait For Interrupt)で眠る。

4.3 スタックメモリ — RTOS との決定的な違い

RTOS スレッドはそれぞれ独立したスタックを持つ。5 タスク = 5×4KB = 20KB 程度。小さなマイクロコントローラには負担だ。

Embassy の async タスクは 一つのスタックを共有する。タスクの状態は自動生成の状態機械構造体に圧縮される。結果として 10 個のタスクが 1-2KB のスタックで足りる場面が珍しくない。

RTOS:    [Stack T1: 4KB] [Stack T2: 4KB] [Stack T3: 4KB] [Stack T4: 4KB]
Embassy: [Shared Stack: 4KB] + [Task1 state: 64B] [Task2 state: 96B] [Task3 state: 32B]

4.4 Spawner と main

Embassy main の定型コード。

#![no_std]
#![no_main]

use embassy_executor::Spawner;
use embassy_rp::gpio::{Level, Output};
use embassy_time::Timer;
use {defmt_rtt as _, panic_probe as _};

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    let p = embassy_rp::init(Default::default());
    let led = Output::new(p.PIN_25, Level::Low);
    spawner.spawn(blink(led)).unwrap();
}

#[embassy_executor::task]
async fn blink(mut led: Output<'static>) {
    loop {
        led.toggle();
        Timer::after_millis(500).await;
    }
}

embassy_executor::main マクロが main を async にして Spawner を引数で渡す。そこに spawn(...) でタスクを登録。

4.5 RTIC vs Embassy — 二大選択肢

RTIC(Real-Time Interrupt-driven Concurrency)は Embassy 以前から存在するもう一つの組み込み並行性フレームワーク。優先度ベースの割り込みスケジューリングをコンパイル時にリソースロックまで静的検証する。

側面EmbassyRTIC
モデルasync/await + executorpriority-based interrupt scheduling
学習コストasync を知っていれば即マクロ + リソースモデルの学習が必要
優先度保証協力的 — 全タスクが等価優先度ベース、コンパイル時検証
メモリより小さい(共有スタック)優先度ごとのスタック
人気2024-2026 新規プロジェクトの主流安全クリティカル・実時間保証が必要なとき

新規プロジェクトはまず Embassy。ハードリアルタイム(自動車・航空) のように優先度検証が必須なら RTIC。


5 章 · ターゲットチップ別現状(2026 年 5 月)

5.1 RP2040 / RP2350 — Raspberry Pi Pico ライン

  • RP2040 — Cortex-M0+ デュアルコア、264KB SRAM。embassy-rp 安定。Pico 1・Pico W どちらもカバー。
  • RP2350 — Cortex-M33 デュアルコア + RISC-V Hazard3 デュアルコアを同時に持つ(ブート時にどちらかを選ぶ)。520KB SRAM、Secure Boot、ARM TrustZone。rp235x-hal 1.0、embassy-rp の rp235x 対応 stable。
  • Bluetooth/Wi-Fi(Pico W, Pico 2 W) — cyw43 クレート + embassy-net(smoltcp ベース TCP/IP)の組合せで動く。PIO アセンブリ + SPI で BLE まで全部いける。
  • PIO — RP シリーズのシグネチャ機能。軽量な stateful I/O マシン。pio-proc + embassy-rp::pio で Rust からアセンブリを書ける。
// Pico W で Wi-Fi に接続する断片(要約)
use cyw43_pio::PioSpi;
use embassy_net::Stack;

let mut control = cyw43::new(state, pwr, spi, fw).await;
control.init(clm).await;
control.join_wpa2("SSID", "PASSWORD").await.unwrap();

5.2 ESP32(esp-rs)

Espressif が 2023 年から Rust を公式第一級 SDK に格上げした。2026 年現在 esp-hal 1.0 が stable。

  • RISC-V チップ(ESP32-C3, C6, H2, P4) — upstream Rust toolchain でビルド。cargo install espup なしでも可能。
  • Xtensa チップ(ESP32, ESP32-S2, ESP32-S3) — 依然 LLVM フォークが必要。espup install で自動設定。
  • embassy-executor + esp-hal-embassy — Embassy を ESP32 でそのまま使える。
  • Wi-Fi/BLEesp-wifi クレート。Wi-Fi 6、BLE 5.3。embassy-net と統合。
# Cargo.toml — ESP32-C6 用
[dependencies]
esp-hal = { version = "1.0", features = ["esp32c6"] }
esp-hal-embassy = { version = "0.6", features = ["esp32c6"] }
embassy-executor = { version = "0.6", features = ["task-arena-size-20480"] }
esp-wifi = { version = "0.10", features = ["esp32c6","wifi","ble"] }

ESP32-C6 の RISC-V + Wi-Fi 6 + Thread/Zigbee ラジオの組合せが 2026 年組み込み Rust の hot spot。Matter デバイスを Rust で書く一番楽な道。

5.3 STM32(stm32-rs)

STMicroelectronics の STM32 シリーズ。組み込み産業のデフォルトチップセット。

  • stm32-rs — 全 STM32 チップの PAC を SVD から自動生成する共同プロジェクト。
  • stm32f4xx-hal, stm32h7xx-hal, stm32l4xx-hal — シリーズ別 HAL。
  • embassy-stm32 — Embassy の統合 HAL。全シリーズをカバー、features = ["stm32f407vg"] のようにチップを選ぶ。2026 年の新規プロジェクトのデフォルト。

STM32 で Embassy を選ぶ強い理由は、割り込み → async waker のマッピングがほぼ全ペリフェラルに敷かれていること。UART、SPI、I2C、ADC、DMA、USB すべて async。

5.4 nRF52 / nRF53 / nRF54 — Nordic Semiconductor

BLE デバイスの事実上の標準チップ。

  • embassy-nrf — nRF52832、52840、5340、54L すべて stable 対応。
  • nrf-softdevice — Nordic の SoftDevice(BLE スタック)を Rust async から呼ぶラッパ。peripheral・central 両方対応。
  • 2026 年の新状況trouble クレート(純 Rust BLE host)が SoftDevice 依存のない選択肢として成熟。nRF52・ESP32・CYW43 で動く。

5.5 その他 — 手短に

  • Ambiq Apollo3 / 4ambiq-hal。超低消費電力(uA)MCU。ウェアラブル・BLE で人気。
  • Arduino UNO R4(Renesas RA4M1)ra4m1-hal(コミュニティ)。Cortex-M4。Arduino 互換ピンマップ。
  • CH32V — WCH の超低価格 RISC-V。ch32-hal。20 セント台の値段。
  • SAMD(Microchip / Atmel)atsamd-hal。SAMD21(Adafruit Feather M0)、SAMD51 カバー。
  • 汎用 RISC-Vriscv-rt でブート、チップ固有 HAL は別途。

6 章 · ツールチェイン — probe-rscargo-embeddefmt

6.1 probe-rs — OpenOCD の後継

OpenOCD/GDB の組合せが組み込みデバッグの標準だった時代は終わった。probe-rs が事実上置き換えた。

probe-rs は Rust で書かれたデバッグプローブ(J-Link、ST-Link、CMSIS-DAP、Raspberry Pi Debug Probe、FTDI など)のドライバ + flash アルゴリズム + RTT/SWO ホスト。

# インストール
cargo install probe-rs-tools

# 接続確認
probe-rs list

# flash + run + RTT ログを同時に
cargo run --release  # .cargo/config.toml に runner = "probe-rs run" を設定して

6.2 .cargo/config.toml 標準セットアップ

RP2040 プロジェクトの標準セットアップ例。

[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "probe-rs run --chip RP2040 --protocol swd"

[build]
target = "thumbv6m-none-eabi"   # RP2040(Cortex-M0+)
# target = "thumbv8m.main-none-eabihf"  # RP2350(Cortex-M33)

[env]
DEFMT_LOG = "trace"

cargo run --release を打つだけでビルド → flash → 実行 → RTT ログ → バックトレースが一コマンドで出る。

6.3 defmt — printf より速いロギング

defmtprintln! のようなマクロだが、キモのトリックがある — フォーマット文字列をファームウェアバイナリに入れず、インデックスだけ送る。ホスト(probe-rs)が ELF の .defmt セクションを読んで文字列を復元する。

use defmt::{info, warn, error};

fn read_sensor(i: u32) {
    info!("sensor read iteration={}", i);

    let value = sensor.read();
    if value > 100 {
        warn!("sensor value high: {=u32}", value);
    } else if value == 0 {
        error!("sensor not responding");
    }
}

{=u32} のような型ヒントはシリアライズ効率のため。一般に 1 ログが 4-8 バイトにシリアライズされる。1MHz の UART でも秒 10 万ログをさばける。

比較printf(semihosting)RTT printfdefmt over RTT
1 ログのコスト数十 µs(ブロッキング)1-2 µs100-500 ns
ファームウェアサイズフォーマット文字列が全部入る入るインデックスのみ
コンパイル時検証nonoyes

6.4 cargo-embed — もう一つのランナー

probe-rs run 以外に cargo-embed もある。TUI ベースで RTT ログ + GDB デバッガを一画面に出す。

cargo install cargo-embed
cargo embed --release

ただ 2025-2026 のトレンドは probe-rs run がより軽くて新規プロジェクトのデフォルト。cargo-embed はダッシュボードが必要な場合に。

6.5 GDB も健在

複雑なメモリ破壊デバッグなど GDB の方が強い領域では、probe-rs gdb のサーバモードを立てて arm-none-eabi-gdb でアタッチ。

probe-rs gdb --chip RP2040
# 別のターミナルで
arm-none-eabi-gdb target/thumbv6m-none-eabi/release/myfirmware
(gdb) target remote :1337

7 章 · Embassy で最初の blinky — RP2040 walkthrough

ここから実際のコード。Raspberry Pi Pico のオンボード LED(GPIO25)を 0.5 秒周期で点滅させる最初のプロジェクト。

7.1 プロジェクト作成

cargo new --bin blinky
cd blinky
rustup target add thumbv6m-none-eabi

7.2 Cargo.toml

[package]
name = "blinky"
version = "0.1.0"
edition = "2021"

[dependencies]
embassy-executor = { version = "0.6", features = ["arch-cortex-m","executor-thread","integrated-timers"] }
embassy-time = { version = "0.4" }
embassy-rp = { version = "0.4", features = ["rp2040","time-driver"] }
defmt = "0.3"
defmt-rtt = "0.4"
panic-probe = { version = "0.3", features = ["print-defmt"] }
cortex-m-rt = "0.7"

[profile.release]
debug = 2          # defmt のためにデバッグシンボル保持
lto = true
codegen-units = 1
opt-level = "s"

7.3 .cargo/config.toml

[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "probe-rs run --chip RP2040 --protocol swd"
rustflags = [
  "-C", "link-arg=--nmagic",
  "-C", "link-arg=-Tlink.x",
  "-C", "link-arg=-Tdefmt.x",
]

[build]
target = "thumbv6m-none-eabi"

[env]
DEFMT_LOG = "trace"

7.4 src/main.rs — 最初の blinky

#![no_std]
#![no_main]

use defmt::info;
use embassy_executor::Spawner;
use embassy_rp::gpio::{Level, Output};
use embassy_time::Timer;
use {defmt_rtt as _, panic_probe as _};

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    let p = embassy_rp::init(Default::default());
    let mut led = Output::new(p.PIN_25, Level::Low);

    info!("blinky started");

    loop {
        led.set_high();
        Timer::after_millis(500).await;
        led.set_low();
        Timer::after_millis(500).await;
    }
}

7.5 ビルド + フラッシュ

Pico の BOOTSEL ボタンを押したまま USB を挿すと mass storage として認識される。その状態で probe-rs 互換のデバッグプローブが繋がっていれば(または別の Pico に Pico Probe ファームを焼いていれば)、一行で完了。

cargo run --release
# Compiling blinky ...
# Finished release [optimized + debuginfo] target(s)
# Running `probe-rs run --chip RP2040 --protocol swd target/thumbv6m-none-eabi/release/blinky`
# 0.000000 INFO  blinky started

LED が 1 秒周期で点滅する。一行のログが RTT 経由でホストのターミナルに流れてくる。

7.6 もう一段拡張 — UART echo + 2 タスク同時

#![no_std]
#![no_main]

use defmt::info;
use embassy_executor::Spawner;
use embassy_rp::gpio::{Level, Output};
use embassy_rp::peripherals::{PIN_25, UART0};
use embassy_rp::uart::{Async, BufferedUart, Config as UartConfig, InterruptHandler};
use embassy_rp::{bind_interrupts, uart};
use embassy_time::Timer;
use embedded_io_async::{Read, Write};
use {defmt_rtt as _, panic_probe as _};

bind_interrupts!(struct Irqs {
    UART0_IRQ => InterruptHandler<UART0>;
});

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    let p = embassy_rp::init(Default::default());

    // LED タスク
    let led = Output::new(p.PIN_25, Level::Low);
    spawner.spawn(blink(led)).unwrap();

    // UART echo タスク — GPIO0 TX、GPIO1 RX、115200 baud
    let mut cfg = UartConfig::default();
    cfg.baudrate = 115_200;
    let uart = BufferedUart::new(
        p.UART0, Irqs, p.PIN_0, p.PIN_1,
        cfg, &mut [0u8; 64], &mut [0u8; 64],
    );
    spawner.spawn(echo(uart)).unwrap();
}

#[embassy_executor::task]
async fn blink(mut led: Output<'static>) {
    loop {
        led.toggle();
        Timer::after_millis(500).await;
    }
}

#[embassy_executor::task]
async fn echo(mut uart: BufferedUart<'static, UART0>) {
    let mut buf = [0u8; 32];
    info!("echo task ready");
    loop {
        match uart.read(&mut buf).await {
            Ok(n) => {
                info!("rx {} bytes", n);
                let _ = uart.write_all(&buf[..n]).await;
            }
            Err(_) => Timer::after_millis(10).await,
        }
    }
}

肝 — blinkecho両方生きている。片方が Timer::after_millis(500).await で寝ている間に、もう片方が UART RX 割り込みで目を覚ます。1 コア、1 スタック、2 タスク。

7.7 defmt マクロ一行追加

info!("rx {} bytes, first={=u8:#04x}", n, buf[0]);

ホストターミナルでの出力:

0.001234 INFO  blinky started
0.001500 INFO  echo task ready
0.012345 INFO  rx 5 bytes, first=0x48

ここで 0x48 は ASCII の 'H'。それで全部だった。最初の組み込み Rust プロジェクト。


8 章 · 実プロジェクト — 何を作っているのか

8.1 ホームオートメーション / Matter デバイス

ESP32-C6 + esp-hal + rs-matter クレート。Matter(Thread/Wi-Fi 上の IoT 標準)の Rust 実装。2024 年から rs-matter が v0.5 に到達し、BLE コミッショニング → Wi-Fi または Thread → Matter クラスタ公開までフルコースを Rust で書ける。

[BLE commissioning] -> [Wi-Fi/Thread] -> [Matter cluster: OnOff, LevelControl, ...]
                       embassy-net          rs-matter

Apple Home、Google Home、Amazon Alexa が同じデバイスを見る。

8.2 カンファレンスバッジ

Hackaday Supercon、Defcon、EMF Camp などのカンファレンスバッジが徐々に Rust に移っている。EMF Camp 2024 のバッジは RP2040 + ePaper + LoRa、ファームウェアは最初から Embassy ベース。

理由: ePaper 更新 + ラジオ RX + ユーザ入力 + バッテリーモニター — 4 つを同時に扱う必要があり、async で表現するのが圧倒的に楽だから。

8.3 インタラクティブアートインスタレーション

LED マトリクス数千個 + センサアレイ + Wi-Fi/Art-Net を束ねるメディアアート作品。アメリカの Burning Man、日本の teamLab 系、欧州のフェスティバルで Rust ファームウェアが徐々に標準になってきた。C のボイラープレートとメモリ破壊デバッグから解放されるのが決定的な魅力。

8.4 産業 IoT — 振動/温度モニタ

STM32H7 + LoRa + 振動センサ(MEMS) — 振動データを FFT して閾値超えで無線送信。STM32H7 のようなチップの DSP 命令を cortex-m::asm::sev のような wrapper で呼んでもよく、cmsis-dsp-sys バインディングで CMSIS-DSP を呼ぶパターンもよくある。

8.5 不揮発メモリ Key/Value ストレージ(sequential-storage)

フラッシュメモリに key/value を永続保存。ファームウェアが再起動しても設定・キャリブレーションデータが残る。embedded-storage trait の上に sequential-storage クレートでウェアレベリングまで。RP2040 の最後の 4KB セクタ、STM32 の EEPROM 領域、ESP32 の NVS パーティションを同じ API で使える。


9 章 · C/C++・MicroPython・Zig embedded・Ada/SPARK との比較

9.1 C/C++ — 依然産業標準

側面C/C++Rust
コンパイラ全チップにあるupstream LLVM 対応チップのみ(ほぼ全部)
メモリ安全自分で保証コンパイラが検証
ベンダ SDK第一級(ST HAL、ESP-IDF、nRF Connect)2-3 年で追いつき中(esp-rs は第一級)
並行性RTOS + 割り込みEmbassy/RTIC
デバッグOpenOCD + GDBprobe-rs + defmt
人材プール巨大急成長中
自動車/航空認証MISRA C 標準Ferrocene(Ferrous Systems の認証 Rust コンパイラ)

要するに — レガシーチップ、人材プール、認証済みコンパイラが決め手なら C/C++。新チップ、新チーム、メモリ安全と並行性モデルが価値なら Rust。

9.2 MicroPython — 参入障壁と RAM

MicroPython は ESP32、RP2040、STM32 でインタープリタが動く。参入障壁が圧倒的に低い — 5 分で最初の LED 点滅。

側面MicroPythonRust
参入障壁非常に低い中〜高
実行速度インタープリタ(C の 1/100〜1/50)C 並み
RAM 使用インタープリタ + GC のオーバーヘッド(〜64KB スタート)ほぼ 0 オーバーヘッド
コンパイル時検証ほぼなし強い
向く場所プロトタイピング、教育、軽いロジックproduction、リアルタイム、リソース逼迫

MicroPython は決して死なない — 教育・プロトタイピング領域で強すぎる。ただ製品になるならたいてい Rust か C/C++ で書き直す。

9.3 Zig embedded

Zig は組み込みを最初から第一級市民として見ている。comptime がマクロより強力、メモリモデルが明示的、C ABI 相互運用が自然。

ただ 2026 年 5 月時点で — エコシステムの深さが Rust よりだいぶ浅い。Embassy のような統合 async ランタイム、RTIC のような静的検証、そして 100 を超えるセンサドライバクレートが Rust にはあって Zig にはまだない。小規模ファームウェアに Zig は魅力的だが、大規模プロジェクトは Rust が優位。

9.4 Ada / SPARK

自動車・航空・原子力で何十年も標準だった言語。SPARK サブセットによる静的検証が強力。

Rust との比較 — メモリ安全はどちらも、並行性安全は Rust が強い、産業認証は Ada/SPARK が深い、人材プールは Rust が急成長中。 Ferrocene が Rust コンパイラの ISO 26262 / IEC 61508 認証を取って(2023 年に ASIL D 対応)、Rust が安全クリティカル領域にも進出している。


10 章 · 難所 — 正直な落とし穴

10.1 'static ライフタイムの圧迫

Embassy タスクは 'static でなければならない — つまり関数スタックで借りた参照をタスクに渡せない。たいてい static_cell クレートで静的メモリに一度初期化するパターンを使う。

use static_cell::StaticCell;

static UART_BUF: StaticCell<[u8; 256]> = StaticCell::new();

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    let p = embassy_rp::init(Default::default());
    let buf: &'static mut [u8; 256] = UART_BUF.init([0; 256]);
    // これで buf は 'static、spawn に渡せる
}

最初の 1 ヶ月はこのパターンが不自然。2 ヶ月目には自然になる。

10.2 割り込みバインディングマクロ

ペリフェラル割り込みハンドラを Embassy ランタイムに繋ぐマクロ(bind_interrupts!) — 形がやや不格好で、チップごとに少し違う。

bind_interrupts!(struct Irqs {
    UART0_IRQ => embassy_rp::uart::InterruptHandler<UART0>;
    USBCTRL_IRQ => embassy_rp::usb::InterruptHandler<USB>;
});

最初は「なんでこんな形」だが、解きほぐすと「USB 割り込みが入ったら Embassy の USB ハンドラを呼べ」という安全な登録に過ぎない。

10.3 Xtensa Rust は依然 LLVM フォーク

ESP32(原作)と ESP32-S2/S3 は Xtensa コア。Xtensa は LLVM upstream ではなく Espressif が維持するフォークが必要。espup install で自動セットアップされるが、GitHub Actions のような CI で一段階増える。

RISC-V チップ(C3、C6、H2、P4)は upstream Rust でビルドできてこの問題がない。新 ESP プロジェクトは RISC-V ライン優先が 2026 年の常識。

10.4 デバッグビルドのサイズ問題

cargo build(デバッグ)は LTO・opt-level 0 なのでコードサイズが 4〜10 倍に膨らんで Flash に入らないことがある。組み込み Rust はほぼ常に cargo build --release で書く。ただし release でデバッグシンボルは debug = 2 でつけておく必要がある — そうしないと defmt/backtrace が動かない。

10.5 知られていないチップの PAC

エコシステムは活発だが全チップをカバーしているわけではない。SiPEED MAIX(Kendryte K210)、Bouffalo BL602 などは PAC があっても HAL は未完。メジャーライン(RP2xxx、ESP32、STM32、nRF5x、SAMD)の外なら、自分で PAC の上に HAL を書く必要があるかもしれない。


11 章 · 学習経路 — どこから始めるか

11.1 第 0 週 — Pico を買う

最速のスタート: Raspberry Pi Pico 2 W(〜7)+RaspberryPiDebugProbe(7)** + **Raspberry Pi Debug Probe(〜12) + USB-C ケーブル 2 本。合わせて $25 程度。デバッグプローブを別に買うのが肝 — flash + RTT デバッグが一コマンドで終わる違いになる。

ESP32 ライン派なら ESP32-C6 DevKit(〜$15)も良い。C6 は Wi-Fi 6 + BLE 5 + Thread すべて持っている。

11.2 第 1-2 週 — embedded-hal の trait たち

embedded-hal::digital::OutputPinembedded-hal::spi::SpiDeviceembedded-hal::i2c::I2cembedded-hal-async::*。最初の 1-2 週はこれら trait の形に慣れる。そのあと、BME280 ドライバ、SSD1306 OLED ドライバ、MPU6050 IMU ドライバが全部同じ形に見える。

11.3 第 3-4 週 — Embassy の async

Embassy 公式 examples フォルダが最速の学習教材。examples/rp または examples/esp32c6 または examples/stm32f4 から、自分のチップフォルダを開いて最初から最後まで読む。各サンプルが 100-200 行で短く、パターンが反復する。

11.4 1-2 ヶ月目 — 最初のプロジェクト

意味のある小さなプロジェクトを一つ。おすすめ — ePaper 時計 + Wi-Fi NTP 同期BME280 環境センサ → BLE/Wi-Fi 送信ロータリエンコーダ + OLED メニュー UI など。100-500 行のプロジェクトが学習効率が最高。

11.5 3-6 ヶ月目 — 深さ

ここまできたら次のどれかで深さを決める。

  • Matter デバイスrs-matter 使用。実際の Apple/Google Home に登録。
  • 自前 PCB + 自前ファーム — KiCad でボード設計 + Rust ファーム。
  • 低消費電力最適化 — uA 単位のスリープ + 割り込み起床 + バッテリ寿命計算。
  • DMA + 高速ペリフェラル — SPI DMA で 1Gbps 近いデータフロー。
  • PIO(Pico)または RMT(ESP32) — カスタムプロトコル(WS2812、IR、1-wire)。

エピローグ — Rust が組み込みをもっと正直にした

組み込み Rust の一言要約: コンパイラが「このメモリは誰の?」と一緒に問うてくれる。 割り込みとメインループが同じバッファを触るコードはコンパイルを通れない。async タスクが借りたペリフェラルを別タスクが同時に使えない。静的 RAM 領域が一箇所で初期化されているかが検証される。

小さなことのようだが — 組み込みファームウェアのデバッグ時間の半分以上が「なぜ LED が点滅しない」 → 実はメモリ破壊だったという経験を一度でもした人にとっては決定的だ。

C/C++ は消えない。60 年のコード、人材、ベンダ SDK、自動車認証体系がそこにある。新チップで Rust がデフォルトになっていくだけだ。そして一度デフォルトになれば — 次世代の開発者は決して C には戻らない。

Embassy の async は 5 年前「面白い実験」だった。2026 年には「他のモデルで書く理由は?」になる。RTIC も、スーパーループも、RTOS も生きているが — 新規プロジェクトのデフォルト選択は Embassy だ。

probe-rsdefmt は組み込みデバッグを 1990 年代から 2020 年代に引っ張ってきた。cargo run --release 一行でビルド・フラッシュ・ロギングが終わるワークフローを一度味わうと OpenOCD に戻れない。

大きな絵 — 組み込み Rust は 2026 年に「使えるか」の段階を過ぎて「なぜ使わない?」の段階に来ている。 次の 5 年は自動車・医療・航空のような認証領域に Ferrocene が浸透していく時間になるだろう。

14 項目チェックリスト

  1. #![no_std] + #![no_main] が有効になっているか?
  2. panic_handler が明示されているか(panic_halt/panic_probe/panic_reset)?
  3. Cargo.toml[profile.release]debug = 2 でデバッグシンボルがついているか?
  4. .cargo/config.tomlrunnerprobe-rs run --chip ... が設定されているか?
  5. PAC を直接触るコードの代わりに HAL/BSP を一次的に使っているか?
  6. センサ/ディスプレイのドライバを embedded-hal trait に対して総称的に使っているか?
  7. Embassy タスクが 'static を要求するとき static_cell/StaticCell で解決しているか?
  8. 割り込みハンドラの中で動的割り当てをしていないか?
  9. defmt::info! などの info/warn/error レベルが production ビルドで適切にフィルタされているか?
  10. Wi-Fi/BLE を使うなら embassy-net/trouble/esp-wifi のどれで行くか決めたか?
  11. 不揮発ストレージが必要なら sequential-storage/embedded-storage で抽象化したか?
  12. Xtensa(ESP32、S2、S3)の代わりに可能なら RISC-V(C3、C6、H2)を選んで toolchain の負担を減らしたか?
  13. CI で cargo build --release + cargo clippy --target ... が回っているか?
  14. デバッグプローブが別途あるか(Pico Probe / J-Link / ST-Link) — RTT デバッグの肝だ。

アンチパターン 10 個

  1. PAC だけ触ってビットシフトで GPIO を扱う — 可読性・移植性が両方損なわれる。
  2. Embassy タスクの中で大きな同期ブロッキング関数を呼ぶ — 他タスクが飢える。
  3. ISR の中で Vec::push のような動的割り当て — リアルタイム性が壊れる。
  4. panic_halt で production ファームウェアを置く — ウォッチドッグでリセットされるようにすべき。
  5. cargo build(デバッグ)ファームウェアをそのまま Flash — サイズ・速度ともに台無し。
  6. RTT ホストなしで defmt::info! ばかり大量に置く — ホストが繋がってないと RTT バッファが満杯になってファームが止まることがある。
  7. Xtensa(ESP32)一チップに全機能を詰め込む — RISC-V に行く方が toolchain 負担がはるかに少ない。
  8. 割り込みとメインループが同じ可変変数をミューテックスなしで共有 — Rust が止めてくれるとはいえ、unsafe で迂回したコードは危険。
  9. PCB 試作段階でデバッグプローブピンを引き出さない — flash を一度壊すと USB DFU で復旧できないことがある。
  10. 100% Rust 原理主義 — ベンダの BLE スタック(SoftDevice)や ESP-IDF Wi-Fi の方が安定しているならそれをラップして使うのが正しい。

次回予告

次回候補: Matter デバイスを Rust で — ESP32-C6 + rs-matter フルスタックEmbassy + LoRa(SX126x)で long-range センサネットワークprobe-rs deep dive — デバッグプローブ自体を書く

"組み込み Rust はコンパイラを味方にする仕事だ。メモリは誰のもので、割り込みは何を起こし、リソースはどこで解放されるか — コードが自分の口で言う。"

— 組み込み Rust 2026 深掘り、終わり。


参考 / References

현재 단락 (1/483)

5 年前ならこの記事の最初の一文は「組み込み Rust はまだ実験だが」で始まっていただろう。2021 年の Embassy は alpha、RP2040 の HAL はまだ定着していなかった、ESP...

작성 글자: 0원문 글자: 25,873작성 단락: 0/483