概要
STM32F4マイコンでEmbassy(Rust非同期組み込みフレームワーク)を使った開発環境をDocker上に構築してみました。環境構築からLED点滅までの手順を紹介します。
動作確認環境はUbuntu 24.04です。
1. Embassyについて
Rustで組み込みプログラミングができると知り、コンパイル時に潜在的なバグを徹底的につぶして安全なコードを書く、という考え方をハードウェアを操作するシステムに応用するのは面白そうだ、と思ったのが出発点です。
MCUによるハードウェア制御の実行環境としては FreeRTOS(C/C++)という有名なOSがあります。
一方、Embassy は FreeRTOS のような preemptive スケジューラではなく、タスクが .await ポイントで制御を手放す協調スケジューリングを採用しています。
「タスクが自発的に yield しないと他のタスクが動かない」という制約があります。
例えば、移動ロボットの制御ループを想定すると、センサー読み取り・モーター制御・コマンド待機はいずれも Timer::after_millis() などで定期的に .await する構造になります。
タスクが preemptive でない、ということが移動ロボットの制御ソフトウェアでは問題となるのではないかという先入観がありましたが、各タスクが適切に yield する設計であれば、Embassyでも問題なく制御ループが動作するだろうと判断しました。
補足
Embassy は割り込みベースの複数優先度エグゼキュータもサポートしており、異なる優先度レベル間では .await ポイントでの preemption が可能です。今回は単一優先度の構成で使用しています。
2. 使用した環境
2.1 ハードウェア
-
ターゲットボード: STM32F411CEU6 (Black Pill)
- スペック概要(100MHz Cortex-M4F, 512KB Flash, 128KB RAM)
-
デバッガ: Raspberry Pi Pico(picoprobeファームウェア) ※ CMSIS-DAP対応プローブ
- USB ID:
2e8a:000c - ST-Link V2クローンも試したが、後述の問題があるためpicoprobeを推奨
- USB ID:
2.2 ソフトウェア構成
| コンポーネント | バージョン | 用途 |
|---|---|---|
| Ubuntu | 24.04 | ホストOS |
| Rust | 1.92 | プログラミング言語 |
| embassy-executor | 0.9 | 非同期タスクエグゼキュータ |
| embassy-time | 0.5 | 時間管理 |
| embassy-stm32 | 0.5 | STM32用HAL |
| probe-rs-tools | 0.30 | フラッシュ/デバッグツール |
| Docker | 24+ | 開発環境コンテナ |
| defmt | 1.0 | 組み込みログ |
3. ホスト環境の準備
3.1 今回のホスト側環境
- Docker / Docker Compose がインストール済み
- Ubuntu 24.04
3.2 udevルールの設定
デバッガをroot権限なしで使うために、udevルールを設定します。 picoprobeとST-Link V2の両方を登録しておきます。 参照: Probe Setup | probe-rs
[/etc/udev/rules.d/99-stlink.rules]
# ST-Link V2
ATTRS{idVendor}=="0483", ATTRS{idProduct}=="3748", MODE="0666", GROUP="plugdev"
# ST-Link V2-1
ATTRS{idVendor}=="0483", ATTRS{idProduct}=="374b", MODE="0666", GROUP="plugdev"
# ST-Link V3
ATTRS{idVendor}=="0483", ATTRS{idProduct}=="374e", MODE="0666", GROUP="plugdev"
ATTRS{idVendor}=="0483", ATTRS{idProduct}=="374f", MODE="0666", GROUP="plugdev"
# Raspberry Pi Debugprobe / picoprobe (CMSIS-DAP)
ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="000c", MODE="0666", GROUP="plugdev"
# udevルールの更新
sudo udevadm control --reload-rules
sudo udevadm trigger
3.3 plugdevグループへの追加
# 現在のユーザーがplugdevに所属しているか確認
groups | grep plugdev
# 所属していない場合は追加(要ログアウト/ログイン)
sudo usermod -aG plugdev $USER
4. Docker開発環境の構築
4.1 ディレクトリ構成
以下のような構成にしました。
stm32-embassy/
├── Dockerfile
├── docker-compose.yml
└── projects/
└── led-blink/
├── Cargo.toml
├── rust-toolchain.toml
├── .cargo/
│ └── config.toml
└── src/
└── main.rs
4.2 Dockerfileのポイント
- ベースイメージは
rust:1.92-bookwormを使用 - ビルドに必要なツールをインストール(pkg-config, libusb等)
- Rustターゲット
thumbv7em-none-eabihfを追加 probe-rs-toolsとflip-linkをインストール- 非rootユーザーで実行するように設定
FROM rust:1.92-bookworm
ENV DEBIAN_FRONTEND=noninteractive
# Build tools and dependencies
RUN apt-get update && apt-get install -y \
pkg-config \
libusb-1.0-0-dev \
libudev-dev \
gdb-multiarch \
openocd \
&& rm -rf /var/lib/apt/lists/*
# Rust target for Cortex-M4F (STM32F4)
RUN rustup target add thumbv7em-none-eabihf
RUN rustup component add rust-src rust-analyzer
# Embedded Rust tools
RUN cargo install probe-rs-tools --locked
RUN cargo install flip-link --locked
# Create developer user
ARG UID=1000
ARG GID=1000
RUN groupadd -g ${GID} developer && \
useradd -m -u ${UID} -g ${GID} -G dialout,plugdev developer
# Copy cargo and rustup to developer's home
RUN cp -r /usr/local/cargo /home/developer/.cargo && \
cp -r /usr/local/rustup /home/developer/.rustup && \
chown -R developer:developer /home/developer/.cargo /home/developer/.rustup
USER developer
ENV CARGO_HOME=/home/developer/.cargo
ENV RUSTUP_HOME=/home/developer/.rustup
ENV PATH="${CARGO_HOME}/bin:${PATH}"
WORKDIR /projects
CMD ["/bin/bash"]
4.3 docker-compose.ymlのポイント
- ボリュームマウントの設計
./projectsをコンテナの/projectsにマウント- Cargoキャッシュを名前付きボリュームで永続化(これがないとビルドが毎回遅い)
- USBデバイス(デバッガ)のパススルー設定
volumesに/dev/bus/usbをバインドマウント: libusb がデバイスツリーを親方向に辿るために必要devicesに/dev/bus/usbを指定: Docker へのデバイスノード宣言- 両方必要。
devicesだけだと probe-rs が "Could not determine a suitable packet size" で失敗する
privileged: trueが必要
services:
embassy-dev:
build:
context: .
args:
UID: ${UID:-1000}
GID: ${GID:-1000}
image: stm32f4-embassy-dev
container_name: stm32-embassy-dev
volumes:
- ./projects:/projects
- cargo-cache:/home/developer/.cargo/registry
- cargo-git:/home/developer/.cargo/git
- rustup-toolchains:/home/developer/.rustup
- /dev/bus/usb:/dev/bus/usb # libusb のデバイスツリー探索に必要
devices:
- /dev/bus/usb:/dev/bus/usb
privileged: true
tty: true
stdin_open: true
working_dir: /projects
environment:
- TERM=xterm-256color
volumes:
cargo-cache:
cargo-git:
rustup-toolchains:
4.4 イメージのビルド
# 基本的なビルドコマンド
docker compose build
# UID/GIDをカスタマイズする場合
UID=$(id -u) GID=$(id -g) docker compose build
初回ビルドは5〜10分程度かかります。2回目以降はキャッシュが効いて速くなります。
5. プロジェクトの構成
5.1 Cargo.toml
各依存クレートの役割:
embassy-executor: 非同期タスクランナーembassy-time: 時間管理(Timer::after_millis等)embassy-stm32: STM32用HAL(featuresでターゲットチップを指定)defmt/defmt-rtt: RTT経由の軽量ログpanic-probe: パニック時のdefmt出力cortex-m/cortex-m-rt: Cortex-Mランタイム
[package]
name = "led-blink"
version = "0.1.0"
edition = "2021"
[dependencies]
embassy-executor = { version = "0.9", features = ["arch-cortex-m", "executor-thread"] }
embassy-time = { version = "0.5", features = ["tick-hz-32_768"] }
embassy-stm32 = { version = "0.5", features = [
"stm32f411ce",
"time-driver-any",
"memory-x",
]}
defmt = "1.0"
defmt-rtt = "1.0"
panic-probe = { version = "1.0", features = ["print-defmt"] }
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
cortex-m-rt = "0.7"
[profile.dev]
opt-level = 1
[profile.release]
debug = 2
lto = true
opt-level = "s"
codegen-units = 1 # single unit + LTO で最大限の最適化
5.2 .cargo/config.toml
- ビルドターゲットの指定
- probe-rsランナーの設定(picoprobeを優先)
- リンカの設定
flip-link: スタックオーバーフロー検出用リンカラッパー。静的変数をRAM先頭に配置しスタックを上位方向に伸ばすことで、オーバーフロー時に HardFault を即時発生させるlink.x/defmt.x: cortex-m-rt / defmt のリンカスクリプト
DEFMT_LOG環境変数でログレベルを設定
[target.thumbv7em-none-eabihf]
# picoprobe (CMSIS-DAP) - BOOT0操作不要
runner = "probe-rs run --chip STM32F411CEUx --probe 2e8a:000c"
# ST-Link V2 - 毎回BOOT0+RESET操作が必要
# runner = "probe-rs run --chip STM32F411CEUx --connect-under-reset"
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
rustflags = [
"-C", "linker=flip-link",
"-C", "link-arg=-Tlink.x",
"-C", "link-arg=-Tdefmt.x",
]
[build]
target = "thumbv7em-none-eabihf"
[env]
DEFMT_LOG = "debug"
5.3 rust-toolchain.toml
ツールチェインをピン留めしておくと、環境の一貫性が保てます。
[toolchain]
channel = "1.92"
components = ["rust-src", "rust-analyzer"]
targets = ["thumbv7em-none-eabihf"]
6. LED点滅サンプル (led-blink)
6.1 コード全文
#![no_std]
#![no_main]
use defmt::*;
use embassy_executor::Spawner;
use embassy_stm32::gpio::{Level, Output, Speed};
use embassy_time::Timer;
use {defmt_rtt as _, panic_probe as _};
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let config = embassy_stm32::Config::default();
let p = embassy_stm32::init(config);
info!("Embassy STM32F4 LED Blink started!");
// PC13: onboard LED (active low)
let mut led = Output::new(p.PC13, Level::High, Speed::Low);
loop {
led.toggle();
info!("LED toggled");
Timer::after_millis(1000).await;
}
}
6.2 公式サンプルとの相違点
公式の blinky.rs(STM32F4) をベースに、Black Pill 向けに調整しました。
| 項目 | 公式 blinky.rs | 今回の main.rs | 理由 |
|---|---|---|---|
| 初期化 | init(Default::default()) | Config::default() を変数に分けて init(config) | コメントを付けやすくするため |
| LED ピン | PB7 | PC13 | Black Pill のオンボード LED は PC13 |
| トグル方法 | set_high() / set_low() を交互に | toggle() | より簡潔 |
| 点滅間隔 | 300ms × 2(High/Low 個別) | 1000ms | 視認しやすい 1秒間隔 |
| ログ | "high" / "low" を個別出力 | "LED toggled" を1回 | シンプル化 |
構造(#![no_std]・#![no_main]・インポート・マクロ)は公式と同一で、ハードウェアの違いと可読性のための微調整のみ。
6.3 コードのポイント
#![no_std]/#![no_main]: OSのない環境向けの必須設定embassy_stm32::init(config)でペリフェラルを初期化し、ピンを所有権で管理enable_debug_during_sleepの設定は 不要(picoprobeなら問題ない)- ST-Link V2クローンを使う場合も効果がない(詳細はハマったところセクション)
Output::new(p.PC13, Level::High, Speed::Low): Active Lowなので初期値をHigh(消灯)にloop { ... Timer::after_millis(1000).await; }: 非同期スリープ。この間CPUはWFEでスリープinfo!("LED toggled"): RTT経由でdefmtログが出力される
6.4 picoprobeの配線
picoprobeは SWD 3本 だけで動作します。Black PillをUSBで給電する場合は電源線不要です。
| Pico GPIO | Pico ピン番号 | Black Pill | 説明 |
|---|---|---|---|
| GP2 | Pin 4 | PA14 (SWCLK) | SWDクロック |
| GP3 | Pin 5 | PA13 (SWDIO) | SWDデータ |
| GND | Pin 38 など | GND | グランド |
| 3V3 | Pin 36 | 3V3 | 電源 3.3V |
6.5 ビルドと実行
# コンテナ起動
docker compose run --rm embassy-dev
# プロジェクト led-blink のディレクトリに移動してビルド
cd led-blink
cargo build --release
# フラッシュ&実行(picoprobeが接続されていれば一発で完了)
cargo run --release
実行するとLEDが1秒間隔で点滅し、ターミナルにdefmtログが表示されます:
INFO Embassy STM32F4 LED Blink started!
INFO LED toggled
INFO LED toggled
...
7. デバッグとログ
7.1 defmtによるログ出力
defmt は RTT (Real-Time Transfer) 経由でホストにログを転送する軽量ログシステムです。
cargo run 実行中に probe-rs がRTTを自動的にホストに転送し、ターミナルに表示されます。
use defmt::*;
info!("Value: {}", some_value); // INFO レベル
warn!("Something happened"); // WARN レベル
error!("Error: {}", err_code); // ERROR レベル
ログレベルは .cargo/config.toml の DEFMT_LOG で設定:
[env]
DEFMT_LOG = "debug" # trace / debug / info / warn / error
7.2 probe-rsの基本操作
# 接続プローブの確認
probe-rs list
# バージョン確認(-v は verbose フラグなので --version を使う)
probe-rs --version
# ターゲットへの接続確認
probe-rs info --chip STM32F411CEUx --probe 2e8a:000c
8. ハマったところ
ST-Link V2クローン + embassy 0.9 で JtagNoDeviceConnected になる問題
当初、プログラマとしてST-Link V2クローンを使うことを想定していました。 しかし、二回目以降のダウンロードで失敗する現象が発生。
症状:
- 初回の
cargo run --releaseは成功する - 2回目以降(WFEスリープに入った後)で
JtagNoDeviceConnectedエラー
原因:
embassy-executor 0.9 でアイドル実装が WFI から WFE に変更されました。 STM32F411 では WFE スリープ中に AHB バスマトリクスが無効化 され、SWD接続が切断されます。
enable_debug_during_sleep = true(DBGMCU_CR = 0x7)を設定しても効果がなく、ST-Link V2クローンでは根本的な回避策がなさそうです。
解決策(推奨): picoprobeへの切り替え
Raspberry Pi Picoを使って"picoprobe"を作成し、プログラマとして使用することにしました。
picoprobe(CMSIS-DAP)では probe-rs が SWD のビットレベル操作を直接制御でき、 WFE スリープ中でも SWD ライン・リセットシーケンスで DP を再初期化して接続を確立できます。
.cargo/config.toml の runner に --probe 2e8a:000c を指定するだけで動作します。
一時対処: BOOT0ボタン(ST-Link V2を使う場合)
書き込みのたびに以下の手順が必要:
- Black PillのBOOT0ボタンを押しながら
- RESETボタンを押して離す
- BOOT0を離す
cargo run --releaseを実行
リカバリーモードでの復旧
何らかの原因で JtagNoDeviceConnected で繋がらなくなったときの復旧方法(上記のBOOT0操作と同じ同じ手順):
- BOOT0ピンをHIGHにしてリセット → システムメモリブートで起動
- この状態でプローブからFlashに書き込めるようになる
Docker内で probe-rs が "Could not determine a suitable packet size" になる問題
docker-compose.yml の volumes から /dev/bus/usb:/dev/bus/usb を削除すると発生する。
症状:
Error: Failed to open probe: Failed to open the debug probe.
Caused by:
Could not determine a suitable packet size for this probe.
原因:
probe-rs が内部で使用する libusb はデバイスファイルを開く際にUSBデバイスツリーを親方向に辿る。
devices エントリはデバイスノードを渡すだけで、ツリー全体は渡さないため、libusb がデバイス階層を参照できない。
解決策:
volumes と devices の両方に /dev/bus/usb:/dev/bus/usb を記述する(4.3節の設定参照)。
なお probe-rs list や probe-rs info 実行時に表示される Couldn't get parent device は libusb の非致命的な警告で、接続自体には影響しない。
picoprobe の USB を抜き差しすると直ることがある
cargo run --release が突然失敗するようになった場合、picoprobe が不安定な状態になっていることがある。
picoprobe の USB ケーブルをホスト側で抜き差しして再試行すると解決することが多い。
9. まとめ
- EmbassyとDockerを組み合わせた再現性のある組み込みRust開発環境を構築できた
- LED点滅で非同期プログラミング(async/await)の基礎を確認
- デバッガはpicoprobeを推奨: ST-Link V2クローンはembassy 0.9以降のWFEスリープと相性が悪い
- defmtのRTTログが非常に便利で、
printfデバッグと同様に使える
参考にしたサイト
- Embassy Book
- Embassy 公式ドキュメント
- embassy/examples/stm32f4/src/bin/blinky.rs(STM32F4 公式サンプル)
- Getting Started · embassy-rs/embassy Wiki
- The Embedded Rust Book
- Hardware - The Embedded Rust Book(ターゲットトリプル説明)
- Installation | probe-rs(pkg-config / libusb 依存関係)
- Probe Setup | probe-rs(udevルール設定)
- probe-rs
- defmt
- STM32F411 リファレンスマニュアル
- Jeff McBride: Errors using RTT and WFI (2025) — バスマトリクス無効化の仕組みの解説