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