STM32F4 + Embassy(Rust) + Docker で組み込みRust開発環境

By demiplus (@demiplus.com)
Published:

English

概要

STM32F4マイコンでEmbassy(Rust非同期組み込みフレームワーク)を使った開発環境をDocker上に構築してみました。環境構築からLED点滅までの手順を紹介します。

動作確認環境はUbuntu 24.04です。

動作中のSTM32F411 Black Pill


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 ハードウェア

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 今回のホスト側環境

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のポイント

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のポイント

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

各依存クレートの役割:

[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

[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 コード全文

<!-- 参考: 公式サンプル https://github.com/embassy-rs/embassy/blob/main/examples/stm32f4/src/bin/blinky.rs -->

#![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 コードのポイント

6.4 picoprobeの配線

picoprobeは SWD 3本 だけで動作します。Black PillをUSBで給電する場合は電源線不要です。

picoprobe - Black Pill 配線図

| 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クローンを使うことを想定していました。 しかし、二回目以降のダウンロードで失敗する現象が発生。

症状:

原因:

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を使う場合)

書き込みのたびに以下の手順が必要:

リカバリーモードでの復旧

何らかの原因で JtagNoDeviceConnected で繋がらなくなったときの復旧方法(上記のBOOT0操作と同じ手順):

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. まとめ


参考にしたサイト