はじめに
マイコンのプログラミングといえば、C/C++が主流です。しかし、メモリ安全性の問題や、インテリセンスのエラー検出の弱さから、開発中に予期せぬバグと遭遇することも少なくありません。
Embassyは、Rust言語でマイコンを扱うための非同期ランタイムおよびHAL(Hardware Abstraction Layer)クレートです。Rustの強力な型システムと所有権・トレイト境界・ライフタイムの仕組みを活用することで、従来の組み込み開発にはない安全性を実現できます。
この記事では、Embassyの特徴と環境構築、そして基本的なLチカのプログラムを通じて、初心者でも理解できるよう、なるべく分かりやすくEmbassy-rsに入門します。
使用したマイコンはNucleo STM32 F446REです。
今回使用したコードはembassy-introductionリポジトリにまとめておきました。
また、Embassyに関する情報がまだ少ないためか、AIに聞いても間違った解答が返ってくることが多いです。その点には注意してください。
参考資料
The Rust Programming Language 日本語版
Rustの日本語翻訳版ドキュメントです。一連の記事では3, 4, 5, 6, 9, 10, 18章の内容を扱います。
記事内でも少しは解説しますが、ドキュメントの完成度が非常に高いので読むことをおすすめします。
Embassy Book
全部英語で読むのが辛いですが、From bare metal to async Rust、System description
は読んでみることをおすすめします。
embassy
EmbassyのGitHubリポジトリ。examplesが非常に役に立ちました。
NUCLEO-F446RE
MbedによるNucleo F446REのページです。
STM32F446xC/E DataSheet
STMicroelectronicsによるNucleo F446xC/Eのデータシートです。
Embassyの特徴
コンパイル時のピン衝突検出
Rustの概念である所有権により、コンパイル時にピンの重複を防ぐことができます。
ピンは一度使用すると所有権が移動するため、再度使おうとすればコンパイルエラーが発生します。
let led = Output::new(p.PA5, Level::Low, Speed::Low);
// ここまではOK
// ❌ コンパイルエラー
let led2 = Output::new(p.PA5, Level::Low, Speed::Low);
// error[E0382]: use of moved value: `p.PA5`
ハードウェアの制約を型で表現
タイマーのチャンネルとピンの対応関係など、ハードウェア固有の制約が型システムで表現されています。
ピン配置の正当性をコンパイラが保証してくれるため、認知資源に余裕ができます。
// TIM1のChannel 1はPA8でしか使えない
let pwm = SimplePwm::new(
p.TIM1,
Some(PwmPin::new(p.PA8, OutputType::PushPull)), // ✅ OK
Some(PwmPin::new(p.PA9, OutputType::PushPull)), // ✅ OK
None,
None,
khz(10),
Default::default(),
);
// 間違ったタイマー・チャンネルのピンを指定するとコンパイルエラー
let pwm = SimplePwm::new(
p.TIM1,
// ❌ コンパイルエラー
// 要求: タイマー1のチャンネル1
// 実際: タイマー2のチャンネル1
Some(PwmPin::new(p.PA0, OutputType::PushPull)),
// ❌ コンパイルエラー
// 要求: タイマー1のチャンネル2
// 実際: タイマー1のチャンネル3
Some(PwmPin::new(p.PA10, OutputType::PushPull)),
...
);
まあ、STM32 HALにはCube IDEがあるのですが。
Channelによるタスク間通信
EmbassyではChannelを使うことで、複雑な排他制御を意識することなく、データを安全に、かつCPUをブロックせずにタスク間でやり取りできます。
#[embassy_executor::task]
async fn update_sensor(
mut sensor: Sensor<'static>, // Sensorは架空の型
sender: Sender<'static, CriticalSectionRawMutex, i64, 1>,
) {
loop {
sensor.update();
sender.send(sensor.get_value()).await;
Timer::after_millis(5).await;
}
}
#[embassy_executor::task]
async fn print_sensor(receiver: Receiver<'static, CriticalSectionRawMutex, i64, 1>) {
loop {
info!("{}", receiver.receive().await);
}
}
ゼロコスト抽象化
「高機能なライブラリを使うと、実行速度が遅くなったり、メモリを余計に消費したりするのでは?」と心配になるかもしれません。
Rustの大きな特徴の1つとして、「ゼロコスト抽象化」があります。上記のような便利な機能を使っても、コンパイラが最終的に生成するコードは、C言語で書いたコードと同等かそれ以上の速度・メモリ効率になります。
実際、Async Rust vs RTOS showdown!というブログ記事で行われた計測では、C言語 + FreeRTOS + STM32 HALの実装と比較して、割り込みレイテンシで約25%、プログラムサイズで約31%、メモリ使用量で約84%も優れた結果を出しました。
優れた開発体験
Rustではランゲージサーバー、パッケージマネージャー、ビルドシステムなどが丁寧に整備されています。
さらに他の言語にはない安全性を持ちつつ、ガベージコレクション不使用、ゼロコスト抽象化によりC/C++と同等の速度を実現しています。
- rust-analyzer: 自動補完、型ヒント、エラー表示など、IDEのサポートが充実
- Cargo: 依存関係の管理、ビルド、テストが統一されたコマンドで可能
私は以前PlatformIOとmbedを使って開発をしていましたが、Embassy-rsのコーディング体験は誇張無しで別次元でした。
環境構築
まず、どこかにembassyなどの名前を付けてディレクトリを作ってください。
Ubuntu(Docker使用)
Embassy-rsで使用するツールprobe-rsは、glibcを必要としますが、そのバージョンの関係でUbuntu 24.04以降の環境が推奨されます。古いUbuntuを使っている場合は、Dockerコンテナを利用すると良いでしょう。
dockerについてもっと知りたい方は実践 Docker - ソフトウェアエンジニアの「Docker よくわからない」を終わりにする本を読んでみてください。
これだけでdockerの基本的な操作を習得できる、素晴らしい本です。
Dockerのインストール
sudo apt update
sudo apt install ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo groupadd docker
sudo usermod -aG docker $USER
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
次に、以下の3つのファイルを.devcontainerディレクトリに入れてください。
Dockerfile
執筆時点で、使用できるRustのバージョンは1.90でした。(要検証)
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
# change time zone
RUN apt update && \
apt install -y tzdata && \
ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
# instal Rust
RUN apt update && \
apt install -y curl && \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
ENV PATH=/root/.cargo/bin:$PATH
# install probe-rs
RUN apt update && \
apt install -y gcc xz-utils && \
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/probe-rs/probe-rs/releases/latest/download/probe-rs-tools-installer.sh | sh
# change Rust version
RUN rustup toolchain install 1.90 && \
rustup default 1.90 && \
rustup toolchain uninstall stable
# install utilities
RUN apt update && \
apt install -y udev git
# clean cache
RUN apt clean && \
rm -rf /var/lib/apt/lists/*
docker-compose.yml
services:
embassy:
# USBデバイスへのアクセスに必要
privileged: true
tty: true
build:
dockerfile: ./Dockerfile
context: .
volumes:
# プロジェクトディレクトリ
- type: bind
source: ../
target: /embassy
# Git
- type: bind
source: ~/.ssh
target: /root/.ssh
# USBデバイスへのアクセス
- type: bind
source: /dev/
target: /dev/
command: ["bash"]
devcontainer.json
{
"name": "embassy",
"dockerComposeFile": "./docker-compose.yml",
"service": "embassy",
"workspaceFolder": "/embassy",
"customizations": {
"vscode": {
"extensions": [
// 必須
"rust-lang.rust-analyzer",
// 任意
"leodevbro.blockman",
"streetsidesoftware.code-spell-checker",
"usernamehw.errorlens",
"tamasfe.even-better-toml",
"mhutchie.git-graph",
"donjayamanne.githistory",
"eamodio.gitlens",
"tal7aouy.indent-colorizer",
"MS-CEINTL.vscode-language-pack-ja",
"PKief.material-icon-theme",
"christian-kohler.path-intellisense",
"shardulm94.trailing-spaces",
// Vim拡張機能(おすすめ)
// "vscodevim.vim",
]
}
}
}
VSCodeにDev Containers拡張機能を入れてください。
そしてCtrl + Shift + Pから「Dev Containers: Reopen in Container」を実行すれば、開発環境が整います。
Windows
未検証です。Embassy公式ドキュメントのGetting startedを参照してください。
プロジェクトの作成と設定
参考にすべき資料
Embassyの設定は使用するマイコンによって異なります。embassy/examplesディレクトリには、各マイコン向けの設定済みプロジェクトがあるので、これを参考にするのが確実です。
私はNucleo STM32 F446REを使用しているので、embassy/examples/stm32f4から設定ファイルを引っ張ってきて少し編集しました。
まずはcargo initを実行してカレントディレクトリでRustのプロジェクトの初期化をします。Gitが必要ない方はcargo init --vcs noneです。
生成されたものをベースに、ファイルを追加・編集していきます。
Cargo.toml - 依存関係の管理
[dependencies]にあるembassy-stm32のfeaturesには、使用するマイコンの型番を指定してください。
そして、[package.metadata.embassy]にあるtargetには、使用するマイコンのCPUに合わせたものを指定してください。
The rustc bookには、CPUに対応するターゲットの記述があったので参考にしてください。
Nucleo STM32 F446REの場合、CPUはArm Cortex-M4 CPU with FPUであるため、thumbv7em-none-eabihfを指定しました。
CPUがwith FPUであれば最後にhfの付くtargetで良いらしいです。
[package]
edition = "2024"
name = "embassy"
[dependencies]
# Change stm32f429zi to your chip name, if necessary.
# embassy-stm32 = { version = "0.4.0", features = ["defmt", "stm32f429zi", "unstable-pac", "memory-x", "time-driver-tim4", "exti", "chrono"] }
embassy-stm32 = { version = "0.4.0", features = ["defmt", "stm32f446re", "unstable-pac", "memory-x", "time-driver-tim4", "exti", "chrono"] }
embassy-sync = { version = "0.7.2", features = ["defmt"] }
embassy-executor = { version = "0.9.0", features = ["arch-cortex-m", "executor-thread", "executor-interrupt", "defmt"] }
embassy-time = { version = "0.5.0", features = ["defmt", "defmt-timestamp-uptime", "tick-hz-32_768"] }
embassy-usb = { version = "0.5.1", features = ["defmt" ] }
embassy-net = { version = "0.7.1", features = ["defmt", "tcp", "dhcpv4", "medium-ethernet", ] }
embassy-net-wiznet = { version = "0.2.1", features = ["defmt"] }
embassy-futures = "0.1.2"
defmt = "1.0.1"
defmt-rtt = "1.0.0"
cortex-m = { version = "0.7.6", features = ["inline-asm", "critical-section-single-core"] }
cortex-m-rt = "0.7.0"
embedded-hal = "1.0.0"
embedded-hal-bus = { version = "0.3.0", features = ["async"] }
embedded-io = "0.7.1"
embedded-io-async = "0.7.0"
panic-probe = { version = "1.0.0", features = ["print-defmt"] }
futures-util = { version = "0.3.30", default-features = false }
heapless = { version = "0.9.1", default-features = false }
critical-section = "1.1"
nb = "1.1.0"
embedded-storage = "0.3.1"
micromath = "2.0.0"
usbd-hid = "0.9.0"
static_cell = "2"
chrono = { version = "^0.4", default-features = false}
[profile.release]
debug = 2
[package.metadata.embassy]
build = [
{ target = "thumbv7em-none-eabihf", artifact-dir = "out/practice" }
]
.cargo/config.toml - ビルドとデバッグの設定
probe-rsを使用してマイコンに書き込む際のコマンドを指定しています。runnerの--chipオプションには使用するマイコンの名前を指定してください。
一応、probe-rsのリポジトリで使用するマイコンの名前を検索してみてください。
私の場合はprobe-rs/probe-rs/targets/STM32F4_Series.yamlのファイルにSTM32F446REと書かれていました。そのため、これを指定すれば多分いけるだろう、とやったらいけました。
オプション--connect-under-resetを付けると、書き込み時に前のプログラムが消去されます。私の場合、付けないと書き込むことができなかったため、付けることを推奨します。
また、[build]のtargetにもCargo.tomlで指定したものと同じターゲットを指定してください。
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "probe-rs run --chip STM32F446RE --connect-under-reset"
[build]
target = "thumbv7em-none-eabihf"
[env]
DEFMT_LOG = "info"
build.rs - ビルドスクリプト
リンカースクリプトを指定するためのファイルらしいです。通常はこのままで問題ありません。
fn main() {
println!("cargo:rustc-link-arg-bins=--nmagic");
println!("cargo:rustc-link-arg-bins=-Tlink.x");
println!("cargo:rustc-link-arg-bins=-Tdefmt.x");
}
rust-toolchain.toml - コンパイラの設定
リポジトリのルートにあったファイルを編集したものです。
指定することでプロジェクトごとにRustのバージョンを固定できるので、チーム開発でも安心です。
ここでも同様に、targetsへCargo.tomlで指定したものと同じターゲットを指定してください。
[toolchain]
channel = "1.90"
components = ["rust-src", "rustfmt", "llvm-tools"]
targets = [
"thumbv7em-none-eabihf",
]
.vscode/settings.json - VSCodeの設定
rust-analyzerやcargoの設定を行います。このファイルもリポジトリのルートにあったファイルを編集しました。
{
"rust-analyzer.check.allTargets": false,
"rust-analyzer.check.noDefaultFeatures": true,
"rust-analyzer.cargo.noDefaultFeatures": true,
"rust-analyzer.cargo.target": "thumbv7em-none-eabihf",
}
最終的に、ディレクトリ構成は以下のようになります。
.
|-- .cargo
| `-- config.toml
|-- .devcontainer
| |-- Dockerfile
| |-- devcontainer.json
| `-- docker-compose.yml
|-- .vscode
| `-- settings.json
|-- src
| `-- main.rs
|-- build.rs
|-- Cargo.toml
`-- rust-toolchain.toml
Lチカ
それでは、組み込みにおける「Hello, World!」とも言える、Lチカのプログラムを書いてみましょう。
コード全体
#![no_std]
#![no_main]
use defmt::*;
use embassy_executor::Spawner;
use embassy_stm32::gpio::{Level, Output, Speed};
use embassy_time::Timer;
// Panic handler. Don't remove.
use {defmt_rtt as _, panic_probe as _};
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
// マイコンの初期化
let p = embassy_stm32::init(Default::default());
// LED用ピンの初期化(PA5を出力モードで使用)
let mut led = Output::new(p.PA5, Level::Low, Speed::Low);
loop {
info!("LED ON");
led.set_high();
Timer::after_millis(500).await;
info!("LED OFF");
led.set_low();
Timer::after_millis(500).await;
}
}
ビルドと実行
rust-analyzerが有効になっていれば、main関数の上にRunというボタンがあるのでそれを押すか、以下のコマンドを実行してください。
cargo run
初めて実行する場合は自動でコンポーネント、ターゲットのダウンロード・インストールが始まります。
そして自動で以前のプログラムが削除され、新しいプログラムがビルド・書き込みされます。
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.16s
Running `probe-rs run --chip STM32F446RE --connect-under-reset target/thumbv7em-none-eabihf/debug/blink`
Erasing ✔ 100% [####################] 128.00 KiB @ 51.47 KiB/s (took 2s)
Programming ✔ 100% [####################] 79.00 KiB @ 38.62 KiB/s (took 2s) Finished in 4.54s
0.000000 [TRACE] BDCR ok: 00008200 (embassy_stm32 src/rcc/bd.rs:221)
0.000000 [DEBUG] flash: latency=0 (embassy_stm32 src/rcc/f247.rs:264)
0.000000 [DEBUG] rcc: Clocks { hclk1: MaybeHertz(16000000), hclk2: MaybeHertz(16000000), hclk3: MaybeHertz(16000000), hse: MaybeHertz(0), hsi: MaybeHertz(16000000), lse: MaybeHertz(0), lsi: MaybeHertz(0), pclk1: MaybeHertz(16000000), pclk1_tim: MaybeHertz(16000000), pclk2: MaybeHertz(16000000), pclk2_tim: MaybeHertz(16000000), pll1_q: MaybeHertz(0), pll1_r: MaybeHertz(0), plli2s1_p: MaybeHertz(0), plli2s1_q: MaybeHertz(0), plli2s1_r: MaybeHertz(0), pllsai1_q: MaybeHertz(0), rtc: MaybeHertz(32000), sys: MaybeHertz(16000000) } (embassy_stm32 src/rcc/mod.rs:71)
0.000335 [INFO ] LED ON (blink src/bin/blink.rs:22)
0.501434 [INFO ] LED OFF (blink src/bin/blink.rs:26)
1.002593 [INFO ] LED ON (blink src/bin/blink.rs:22)
1.503753 [INFO ] LED OFF (blink src/bin/blink.rs:26)
2.004913 [INFO ] LED ON (blink src/bin/blink.rs:22)
2.506072 [INFO ] LED OFF (blink src/bin/blink.rs:26)
3.007232 [INFO ] LED ON (blink src/bin/blink.rs:22)
3.508331 [INFO ] LED OFF (blink src/bin/blink.rs:26)
4.009490 [INFO ] LED ON (blink src/bin/blink.rs:22)
4.510650 [INFO ] LED OFF (blink src/bin/blink.rs:26)
5.011810 [INFO ] LED ON (blink src/bin/blink.rs:22)
このように出力され、LEDが0.5秒間隔で点滅すれば成功です。
コードの解説
Rust初学者向け解説
変数定義
Rustの変数はデフォルトで不変(immutable)です。これはC++で全ての変数にデフォルトでconstが付いているようなものです。mutを付けて定義することで可変(mutable)にできます。
// C++: const int a = 10;
let a = 10;
// a = 20; // ❌ コンパイルエラー
// C++: int b = 10;
let mut b = 10;
b = 20; // ✅ OK(mutなら変更可能)
Lチカのコードでlet mut led = Output::new(...)としているのは、後でled.set_high()のように状態を変更するメソッドを呼ぶ必要があるためです。
マクロ
Rustでは、コードの末尾に!がついているものは、マクロと呼ばれます。これらはコンパイル時にコードを生成・変換する強力な機能です。
C++のプリプロセッサ#defineとは異なり、型チェックが行われる安全な仕組みです。コンパイル時にフォーマット文字列を解析し、コードへ展開されます。
let x = 5;
let y = 10;
info!("x: {}, y: {}", x, y); // {秒数} [INFO ] x: 5, y: 10 ({バイナリ名} {ファイルパス}:{行数})
println!("x: {}, y: {}", x, y); // x: 5, y: 10
他にもwarn!()、todo!()、unimplemented!()、unreachable!()などがあります。
defmtで定義されているので見てみると楽しいですよ。
::(パス)と.(メソッド)
Output::new(...)のような記述に出てくる::は、C++のスコープ解決演算子とよく似ています。
-
::(関連関数呼び出し)
型そのものに紐付いた関数を呼び出します。C++でいう静的メンバー関数(staticメソッド)に相当します。
例:Output::new(...)は「Outputクラスのnewという静的関数を呼ぶ」という意味です。 -
.(メソッド呼び出し)
インスタンスに対して操作します。
例:led.set_high()は「ledというインスタンスのset_highメソッドを呼ぶ」という意味です。
Rust言語には、C++のような特別な「コンストラクタ」という機能はありません。
その代わり、「初期化してインスタンスを返す関連関数をnewという名前にする」という慣習があります。
非同期処理と.await
Timer::after_millis(500).await;
Mbedのwait_us(500000)は、500ms経過するまでCPUをその場で待機させ、他の処理をブロックしてしまいます。
一方、EmbassyのTimer::after_millis(500)は「500ms後に起こしてね」というタイマーをセットするだけです。そして.awaitで「完了するまで、CPUを他のタスクに譲ります」と宣言します。
これにより、待ち時間の間に裏で他のタスク(例えばセンサーのデータ取得や通信)を並行して実行できるのです。
必須の宣言
#![no_std]
#![no_main]
#![no_std]は、Rustの標準ライブラリ(std)を使わないことを指定しています。stdはOSに依存しているため、マイコンのようなOSの無い環境では使うことができません。
OSへ依存せずにstdの一部の機能を提供しているcoreというライブラリも存在します。
use std::sync::atomic::AtomicUsize; // ❌ コンパイルエラー
use core::sync::atomic::AtomicUsize; // ✅ OK
#![no_main]は、通常のmain関数ではなく、独自のエントリポイントを使うことを指定しています。
通常のRustではmain関数がエントリポイント、つまりプログラムの開始場所となります。
一方Embassy-rsでは#[embassy_executor::main]アトリビュートの付いた関数がエントリポイントとなります。
パニックハンドラ
use {defmt_rtt as _, panic_probe as _};
プログラムがパニック(回復不能なエラー)を起こした時の処理を定義します。
-
defmt_rtt: デバッグメッセージをRTT経由でPCに送信 -
panic_probe: パニック時の情報をprobe-rsで表示
no_std環境ではパニックハンドラすら定義されていないため、Embassy-rsから提供されているものを使用します。
as _を消すとunused import: ...と警告が出ますが、実際には使用しているので消去しないでください。
main関数
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
// ...
}
#[embassy_executor::main]アトリビュートにより、非同期ランタイム(非同期処理を行うための仕組み)が自動的に構築されます。この関数の中ではasync/awaitを使った非同期処理を書けます。
_spawnerは複数のタスクを並行実行するために使いますが、今回は使わないので先頭に_を付けて未使用であることを示します。(先程のas _と同様に、付けないと警告が出ます)
ペリフェラル(周辺機器)の初期化
let p = embassy_stm32::init(Default::default());
マイコンのすべてのペリフェラル(GPIO、タイマー、UARTなど)を提供する変数pを取得します。
先述したとおり、p.PA5のようなピンを一度使うと所有権がOutputへ移動するため、同じピンを2回使うミスを防げます。
let led1 = Output::new(p.PA5, Level::Low, Speed::Low);
let led2 = Output::new(p.PA5, Level::High, Speed::Low);
// ^^^^^^ error: use of moved value: `p.PA5`
Default::default()は、RustのDefaultトレイトを使った初期化です。
embassy_stm32::init()は設定を表すConfig構造体を引数に取りますが、すべての設定項目を手動で指定するのは大変です。しかし、Config構造体はDefaultトレイトを実装しているため、Default::default()を呼び出すだけでデフォルト値を簡単に取得できます。
// デフォルト設定で初期化
let config: embassy_stm32::Config = Default::default();
// 一部だけ変更したい場合
let mut config: embassy_stm32::Config = Default::default();
config.enable_debug_during_sleep = false;
GPIOの設定
let mut led = Output::new(p.PA5, Level::Low, Speed::Low);
-
p.PA5: 使用するピン -
Level::Low: 初期状態はLow(消灯) -
Speed::Low: GPIO速度(LED点滅程度ならLowで十分)
LEDの点滅ループ
loop {
info!("LED ON");
led.set_high();
Timer::after_millis(500).await;
info!("LED OFF");
led.set_low();
Timer::after_millis(500).await;
}
-
info!(): デバッグメッセージを出力(時間、行数も表示される) -
Timer::after_millis(500).await: 500ms待機(ノンブロッキング)
awaitを使うことで、待機中に他のタスクを実行(並行処理)できます。mbedのwait_us()のようにCPUをブロックしません。
これが非同期ランタイムの強みです。次は実際に並行処理を行ってみましょう。
並行処理
単純なLチカはできましたが、Embassyの真価は並行処理にあります。次は並行処理を行いつつ、ボタンとLEDを同時に扱ってみましょう。
コード全体
#![no_std]
#![no_main]
use defmt::*;
use embassy_executor::Spawner;
use embassy_stm32::{
exti::ExtiInput,
gpio::{Level, Output, Pull, Speed},
};
use {defmt_rtt as _, panic_probe as _};
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = embassy_stm32::init(Default::default());
// LED(PA5)とボタン(PC13)の設定
let led = Output::new(p.PA5, Level::Low, Speed::Low);
let button = ExtiInput::new(p.PC13, p.EXTI13, Pull::Up);
// 別タスクとして起動
spawner.spawn(button_blink(led, button)).unwrap();
loop {
println!("Hello, world!");
Timer::after_secs(1).await;
}
}
#[embassy_executor::task]
async fn button_blink(mut led: Output<'static>, mut button: ExtiInput<'static>) {
loop {
// ボタンが押された(立ち下がり)を待つ
button.wait_for_falling_edge().await;
led.set_high();
info!("LED turned on");
// ボタンが離された(立ち上がり)を待つ
button.wait_for_rising_edge().await;
led.set_low();
info!("LED turned off");
}
}
ビルドと実行
先ほどと同じようにして実行してみてください。
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.14s
Running `probe-rs run --chip STM32F446RE --connect-under-reset target/thumbv7em-none-eabihf/debug/button_blink`
Erasing ✔ 100% [####################] 128.00 KiB @ 51.97 KiB/s (took 2s)
Finished in 4.66s
0.000000 [TRACE] BDCR ok: 00008200 (embassy_stm32 src/rcc/bd.rs:221)
0.000000 [DEBUG] flash: latency=0 (embassy_stm32 src/rcc/f247.rs:264)
0.000000 [DEBUG] rcc: Clocks { hclk1: MaybeHertz(16000000), hclk2: MaybeHertz(16000000), hclk3: MaybeHertz(16000000), hse: MaybeHertz(0), hsi: MaybeHertz(16000000), lse: MaybeHertz(0), lsi: MaybeHertz(0), pclk1: MaybeHertz(16000000), pclk1_tim: MaybeHertz(16000000), pclk2: MaybeHertz(16000000), pclk2_tim: MaybeHertz(16000000), pll1_q: MaybeHertz(0), pll1_r: MaybeHertz(0), plli2s1_p: MaybeHertz(0), plli2s1_q: MaybeHertz(0), plli2s1_r: MaybeHertz(0), pllsai1_q: MaybeHertz(0), rtc: MaybeHertz(32000), sys: MaybeHertz(16000000) } (embassy_stm32 src/rcc/mod.rs:71)
Hello, world!
Hello, world!
1.737030 [INFO ] led turned on (button_blink embassy/src/button_blink.rs:34)
Hello, world!
Hello, world!
3.563323 [INFO ] led turned off (button_blink embassy/src/button_blink.rs:38)
Hello, world!
Hello, world!
5.370361 [INFO ] led turned on (button_blink embassy/src/button_blink.rs:34)
5.517913 [INFO ] led turned off (button_blink embassy/src/button_blink.rs:38)
Hello, world!
Hello, world!が1秒ごとに表示されます。
それと同時に、ボタンを押すとled turned on表示されLEDが点灯し、離すとled turned offと表示されLEDが消灯すれば成功です。
コードの解説
Rust初学者向け解説
unwrapとエラー処理
spawner.spawn(button_blink(led, button)).unwrap();
Rustでは失敗する可能性のある処理はResult<T, E>型を返します。unwrap()は以下の動作をします。
- 成功(Ok(T))なら: 中身の値(T型)を取り出して返す
- 失敗(Err(E))なら: パニック(プログラムを停止)
// unwrapを使わず丁寧に書くとこうなる
match spawner.spawn(button_blink(led, button)) {
Ok(t) => { /* 成功 */ },
Err(e) => defmt::panic!("Failed: {:?}", e), // パニック!
}
組み込み開発ではタスク起動に失敗したら動作できないため、unwrap()でパニックさせる方がシンプルで実用的です。エラーが起こりうるかどうかが型で表現されるのは、Rustの大きな特徴です。
また、値が存在するか(Some(T))、存在しないか(None)を表すOption<T>という型もあります。詳しくは次の記事で。
ライフタイムと'static
C++では、ポインタや参照が指し示す先のメモリがいつ解放されるか、プログラマが責任を持って管理する必要がありました。解放済みのメモリを参照してしまうバグ(ダングリングポインタ)は、非常に危険で発見が困難です。
Rustは、この問題をコンパイル時に解決するため、ライフタイムという概念を導入しました。これは「データがメモリ上で有効な期間」のことです。
-
通常のライフタイム(
'aなど): あるスコープの中だけで有効な、一時的なデータ -
'staticライフタイム: プログラムが実行されている間、ずっと有効なデータ
Embassyのタスクは、起動した関数(main)とは独立して実行され、いつ終了するか分かりません。もしタスクが一時的なデータを参照してしまうと、タスク実行時にはそのデータが既に消えているかもしれません。
そのため、タスクの引数には'staticが要求されます。「このタスクに渡すデータは、プログラムが動いている限り絶対に消えない安全なものですよ」と保証する必要があるのです。
外部割り込み(EXTI)入力
let button = ExtiInput::new(p.PC13, p.EXTI13, Pull::Up);
-
p.PC13: ボタンのピン -
p.EXTI13: 外部割り込みライン(ピン番号と対応) -
Pull::Up: 内部プルアップ抵抗を有効化
F446REにおいて、ユーザーボタンはプルアップで接続されているようです。
そのためボタンが押されていない時はHigh、押された時はLowになります。
この変動を後述のbutton_blink関数で検知します。
タスクの定義と起動
#[embassy_executor::task]
async fn button_blink(mut led: Output<'static>, mut button: ExtiInput<'static>) {
// ...
}
#[embassy_executor::task]を付けることで、並行実行可能なタスクになります。
spawner.spawn(button_blink(led, button)).unwrap();
spawnerを使ってタスクを起動します。この時点で、ledとbuttonの所有権はbutton_blinkタスクに移動します。
また、#[embassy_executor::task]アトリビュートが付いている関数の引数は全て'staticライフタイムを持つことが要求されます。button_blinkタスクはmainが終了したかどうかに関わらず無期限に実行されるため、引数は'staticライフタイムを持つ必要があるのです。
イベント駆動の処理
button.wait_for_falling_edge().await;
// ボタンが押されるまで待機(CPUを占有しない)
button.wait_for_rising_edge().await;
// ボタンが離されるまで待機(CPUを占有しない)
awaitにより、イベントが発生するまで他のタスクを実行できます。CPUリソースを無駄にしません。
まとめ
この記事では、Embassyの基礎を紹介しました。
Embassyに限らずRustは良い言語です。学習コストは確かに大きいですが、みなさんに一度でも試してみてほしいです。
次回はPWM制御について解説します。