Embassy Book へようこそ。Embassy Book は、Embassy を使いたい人や、Embassy がどのように動作するのかを理解したいすべての人のためのものです。

はじめに

Embassy は、組み込み開発において async/await を第一級の選択肢にするためのプロジェクトです。

async とは?

I/O を処理する際、ソフトウェアは、I/O 操作が完了するまでプログラムの実行をブロックする関数を呼び出さなければなりません。Linux のような OS 上で動作している場合、そのような関数は一般に制御をカーネルに移し、利用可能であれば別のタスク(「スレッド」と呼ばれます)を実行できるようにするか、別のタスクの準備が整うまで CPU をスリープ状態にします。

OS はスレッドが協調的に振る舞うと前提できないため、スレッドは比較的リソース消費が大きく、割り当てられた時間内に制御をカーネルへ戻さない場合は強制的に割り込まれることがあります。代わりに、タスクが協調的に振る舞うと前提できるのであれば、従来の OS スレッドと比べてほとんどコストのかからないタスクを作成できます。

他のプログラミング言語では、これらの軽量タスクは「coroutines」や「goroutines」として知られています。Rust では、これらは async で実装されます。async-await は、各 async 関数を future と呼ばれるオブジェクトに変換することで動作します。future が I/O でブロックされると、その future は実行を譲り、エグゼキュータと呼ばれるスケジューラが別の future を選んで実行できます。

RTOS などの代替手段と比べると、エグゼキュータが future の実行準備が整うタイミングを推測する必要がないため、async はより高い性能と低い消費電力を実現できる場合があります。ただし、プログラムサイズは他の代替手段より大きくなる可能性があり、メモリが非常に少ない容量制約の厳しいデバイスでは問題になることがあります。Embassy がサポートする STM32 や nRF などのデバイスでは、一般に、やや増加したプログラムサイズを収容するのに十分なメモリがあります。

Embassy とは?

Embassy プロジェクトは、組み合わせても個別でも使えるいくつかの crate で構成されています。

エグゼキュータ

embassy-executor は async/await エグゼキュータであり、通常は起動時に確保された固定数のタスクを実行しますが、後からさらに追加することもできます。また、エグゼキュータは async とブロッキングの両方の遅延に使えるシステムタイマーを提供する場合もあります。1 マイクロ秒未満では、コンテキストスイッチのコストが高すぎてエグゼキュータは正確なタイミングを提供できないため、ブロッキング遅延を使用すべきです。

ハードウェア抽象化レイヤー

HAL は安全な Rust API を実装しており、レジスタを直接操作しなくても USART、UART、I2C、SPI、CAN、USB などのペリフェラルを利用できます。

Embassy は、適切な場面では async API とブロッキング API の両方の実装を提供します。DMA(Direct Memory Access)は async と相性がよい例である一方、GPIO の状態操作はブロッキング API の方が適しています。

Embassy プロジェクトは一部のハードウェア向け HAL をメンテナンスしていますが、他のプロジェクトの HAL を Embassy と一緒に使うこともできます。

  • embassy-stm32、すべての STM32 マイクロコントローラファミリ向け。

  • embassy-nrf、Nordic Semiconductor の nRF52、nRF53、nRF54、nRF91 シリーズ向け。

  • embassy-rp、Raspberry Pi RP2040 および RP235x マイクロコントローラ向け。

  • embassy-mspm0、Texas Instruments MSPM0 マイクロコントローラ向け。

  • esp-hal、Espressif Systems の ESP32 シリーズチップ向け。

  • ch32-hal、WCH の 32 ビット RISC-V(CH32V)シリーズチップ向け。

  • mpfs-hal、Microchip PolarFire SoC 向け。

  • py32-hal、Puya Semiconductor の PY32 シリーズチップ向け。

Note
よくある質問として、Embassy の HAL を単体で使えるのかというものがあります。はい、可能です! HAL 内にエグゼキュータへの依存はありません。これらは Embedded HAL のブロッキングトレイトと async トレイトの両方を実装しているため、 async なしでも使用できます。

ネットワーキング

embassy-net ネットワークスタックは、Ethernet、IP、TCP、UDP、ICMP、DHCP を含む幅広いネットワーク機能を実装しています。async により、タイムアウト管理や複数接続の同時処理が大幅に簡単になります。WiFi および Ethernet チップ向けのドライバもいくつか用意されています。

Bluetooth

  • trouble crate は、bt-hci トレイトを実装する任意のマイクロコントローラ上で動作する Bluetooth Low Energy 4.x および 5.x のホストを提供します(現在は nRF52nrf54rp2040rp23xxesp32serial コントローラがサポートされています)。

  • nrf-softdevice crate は、nRF52 マイクロコントローラ向けの Bluetooth Low Energy 4.x および 5.x サポートを提供します。

LoRa

lora-rs は、Rust の LoRaWAN 実装と完全に統合された、幅広い LoRa 無線機器向けの LoRa ネットワーキングをサポートします。4 つの crate — lora-phy、lora-modulation、lorawan-encoding、lorawan-device — と、さまざまな開発ボード向けの基本的な例を提供します。STM32WL ワイヤレスマイクロコントローラや Semtech SX127x トランシーバなどをサポートしています。

USB

embassy-usb はデバイス側 USB スタックを実装しています。USB シリアル(CDC ACM)や USB HID などの一般的なクラスの実装が利用可能で、充実したビルダー API により独自のものを構築できます。

ブートローダーと DFU

embassy-boot は、試験起動とロールバックを備え、電源断時にも安全な方法でファームウェアアプリケーションの更新をサポートする軽量ブートローダーです。

DMA とは?

組み込みデバイスにおける大半の I/O では、CAN という顕著な例外を除き、ペリフェラルは複数バイトの同時送信を直接サポートしていません。その代わり、MCU は 1 バイトずつ書き込み、次を送信する準備がペリフェラルに整うまで待たなければなりません。I/O レートが高い場合、各バイトの処理に MCU がより多くの時間を割かなければならないと、これは問題になりえます。この問題の解決策が Direct Memory Access コントローラを使うことです。

Direct Memory Access コントローラ(DMA)は、STM32 や nRF を含む、Embassy がサポートする MCU に搭載されているコントローラです。DMA により、MCU は送信または受信の転送を設定し、その転送が完了するのを待つことができます。DMA では、一度開始すれば、転送が完了するまで MCU の介入は不要です。つまり、転送中に MCU は他の計算を実行したり、他の I/O を設定したりできます。I/O レートが高い場合、DMA によって MCU が I/O 処理に費やす時間を半分以上削減できます。ただし、DMA はセットアップがより複雑であるため、組み込みコミュニティではあまり広く使われていません。Embassy は、DMA を最後の選択肢ではなく第一の選択肢にすることで、この状況を変えることを目指しています。Embassy を使えば、I/O レートが増加しても、アプリケーションがすでにそれを処理できるようセットアップされているため、追加のチューニングは不要です。

Embassy は、サポートされているすべての HAL 向けの例を提供しています。これらは examples/ フォルダにあります。

メインループの例

use embassy_executor::Spawner;
use embassy_time::Timer;
use log::*;

#[embassy_executor::task]
async fn run() {
    loop {
        info!("tick");
        Timer::after_secs(1).await;
    }
}

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    env_logger::builder()
        .filter_level(log::LevelFilter::Debug)
        .format_timestamp_nanos()
        .init();

    spawner.spawn(run().unwrap());
}

実世界で使われている Embassy!

ここでは、Embassy を利用している実在のプロジェクトの既知の例を紹介します。ぜひ さらに追加 してください!

新しいエントリほど上にあります

  • ESPress WheeLE: Remote-Controlled Rover via Bluetooth LE

    • Bluetooth LE 経由で受信した命令を使用する、小規模なロボットプラットフォーム向けの、堅牢で省電力かつ安全性を重視したリモートコントロールソリューションです。

  • ProtoV MINI: A USB-C mini lab power supply

    • RP2040 ベースの、USB PD 駆動の 2 チャンネル・ブレッドボード用電源で、組み込みグラフィックスを実行します。回路図とファームウェアはオープンソースです。

  • Opentrig: A particle physics trigger and data acquisition system

    • しきい値付きデジタルイベントトリガーと、AIDA-2020 TLU システムとの接続向けに設計されたデータ収集システムで、DESY II Test Beam Facility でテスト済みです。RP2040 と Embassy の非同期イベント処理をベースにしています。

  • Air Quality Monitor

    • rp2350 ボード、ens160 と aht21 センサー、ssd1306 ディスプレイをベースにした空気品質モニターです。コードと 3D プリント可能な筐体が含まれています。

  • Embassy Clock: Layered, modular bare-metal clock with emulation

    • 多重化、点滅、UI ロジックを明確に分離するために、階層化された Embassy タスク (Display→Blinker→Clock) を示す no_std の Raspberry Pi Pico 時計です。単一ボタンによる HH:MM/MM:SS の時刻設定 UI、heapless データ構造、ハードウェアなしでテストできる Renode エミュレーターを備えています。詳細は この記事 を参照してください。

  • A simple tracked robot based on Raspberry Pi Pico 2

    • 基本的な自律走行モードと手動走行モードを備えた履帯式ロボットを作るホビープロジェクトです。

  • A Raspberry Pi Pico W Alarmclock

    • Pi Pico W を中心に、コード、部品表、筐体設計ファイルを含む目覚まし時計を作るホビープロジェクトです。

  • RMK: A feature-rich Rust keyboard firmware

    • RMK には、レイヤーの組み込みサポート、ワイヤレス (BLE) のサポート、vial を使ったリアルタイムのキー編集サポートなどがあります!

    • STM32、RP2040、nRF52、ESP32 MCU を対象としています

  • Printhor: The highly reliable but not necessarily functional 3D printer firmware

    • 一部の STM32 MCU を対象としています

  • Card/IO firmware - オープンソース ECG デバイス向けのファームウェア

    • ESP32-S3 または ESP32-C6 MCU を対象としています

  • The lora-rs project includes さまざまなスタンドアロンのサンプル for NRF52840, RP2040, STM32L0 and STM32WL

  • Air force one: A simple air quality monitoring system

    • nRF52 を対象とし、nrf-softdevice を使用しています

  • YLab Edge GoYLab Edge Pro プロジェクトでは、 行動科学研究における生理学的データを取得するためのファームウェア (RP2040, STM32) を開発しています。現在までに含まれているものは次のとおりです:

    • 生体電位 (アナログポート)

    • モーションキャプチャ (6 軸加速度計)

    • 空気品質 (CO2、温度、湿度)

    • データの取得と可視化のためのアプリも付属しています [Ystudio]

初心者向け

このセクションの記事は主に Embassy を初めて使うユーザーを対象としており、 使い始める方法、プロジェクトの構成方法、その他のベストプラクティスを紹介します。

はじめに

Embassy を試してみたいのですね。すばらしいです!始めるには、いくつかのツールをインストールする必要があります。

  • rustup - Rust コードをコンパイルするには Rust ツールチェーンが必要です。

  • probe-rs - デバイスにファームウェアを書き込むためのものです。OpenOCD のような別のツールがすでにセットアップ済みであれば、それを使うこともできます。

  • flip-link - 特定のサンプルバイナリをスタックオーバーフロー保護付きでリンクするためのものです。

対応しているボードを持っていなくても心配いりません。std のサンプルを使えば、PC 上で Embassy を実行することもできます。

サンプル付きのボードを入手する

Embassy は多くのマイクロコントローラファミリをサポートしていますが、最も手早く始めるには、Embassy に既存のサンプルコードがあるボードを使うことです。

この一覧は網羅的ではありません。お使いのボードがここに含まれていない場合は、examples フォルダー を確認して、そのボード向けのサンプルコードが書かれているか見てください。

nRF キット
RP2040 キット
ESP32

サンプルを実行する

まず、github リポジトリ をクローンする必要があります。

git clone https://github.com/embassy-rs/embassy.git
cd embassy

リポジトリのコピーを用意したら、お使いのボード向けの examples フォルダーを見つけて、サンプルプログラムをビルドします。blinky は良い選択です。LED を点滅させるだけなので、組み込みの世界における「Hello World」に相当します。

cd examples/nrf52840
cargo build --bin blinky --release

サンプルをビルドできることを確認したら、デバッグプローブでコンピューターとボードを接続し、実機で実行します。

cargo run --bin blinky --release

すべて正しく動作すれば、ボード上で LED が点滅し、コンピューターには次のようなデバッグ出力が表示されるはずです。

    Finished dev [unoptimized + debuginfo] target(s) in 1m 56s
     Running `probe-rs run --chip STM32F407VGTx target/thumbv7em-none-eabi/debug/blinky`
(HOST) INFO  flashing program (71.36 KiB)
(HOST) INFO  success!
────────────────────────────────────────────────────────────────────────────────
0 INFO  Hello World!
└─ blinky::__embassy_main::task::{generator#0} @ src/bin/blinky.rs:18
1 INFO  high
└─ blinky::__embassy_main::task::{generator#0} @ src/bin/blinky.rs:23
2 INFO  low
└─ blinky::__embassy_main::task::{generator#0} @ src/bin/blinky.rs:27
3 INFO  high
└─ blinky::__embassy_main::task::{generator#0} @ src/bin/blinky.rs:23
4 INFO  low
└─ blinky::__embassy_main::task::{generator#0} @ src/bin/blinky.rs:27
Note
cargo run コマンドは、どうやってボードへの接続方法とプログラムの書き込み方法を知っているのでしょうか?各 examples フォルダーには .cargo/config.toml ファイルがあり、そのフォルダー内の ARM バイナリに対して cargo が probe-rs をランナーとして使うよう指定しています。probe-rs はデバッグプローブと MCU の通信を処理します。これを機能させるには、probe-rs がどのチップを書き込むのかを知っている必要があるため、別のチップでサンプルを実行したい場合はこのファイルを編集する必要があります。
うまくいかなかった場合

cargo run --release を実行したときに問題が発生する場合は、以下を確認してください。

  • コマンドラインで正しい --chip を指定している、または

  • .cargo/config.toml の run 行を正しいチップに設定している、かつ

  • examples/Cargo.toml の HAL(例: embassy-stm32)依存関係の feature を、正しいチップを使うように変更している(既存の stm32xxxx feature を置き換える)

この時点で、プロジェクトは実行できるはずです。たとえば blinky で LED が点滅しない場合は、コードがボードの LED ピンをトグルしていることを必ず確認してください。

cargo run --release でサンプルを実行しようとしていて、次のような出力が表示される場合:

0.000000 INFO Hello World!
└─ <invalid location: defmt frame-index: 14>
0.000000 DEBUG rcc: Clocks { sys: Hertz(80000000), apb1: Hertz(80000000), apb1_tim: Hertz(80000000), apb2: Hertz(80000000), apb2_tim: Hertz(80000000), ahb1: Hertz(80000000), ahb2: Hertz(80000000), ahb3: Hertz(80000000) }
└─ <invalid location: defmt frame-index: 124>
0.000061 TRACE allocating type=Interrupt mps=8 interval_ms=255, dir=In
└─ <invalid location: defmt frame-index: 68>
0.000091 TRACE   index=1
└─ <invalid location: defmt frame-index: 72>

frame-index エラーを解消するには、次を Cargo.toml に追加してください。

[profile.release]
debug = 2

次のような内容を含む非常に長いエラーメッセージが表示される場合:

error[E0463]: can't find crate for `std`
  |
  = note: the `thumbv6m-none-eabi` target may not support the standard library
  = note: `std` is required by `stable_deref_trait` because it does not declare `#![no_std]`

probe-rs を正しくインストール する代わりに、誤って cargo add probe-rs(依存関係として追加してしまいます)を実行していないことを確認してください。

raspberry pi pico-w を使っている場合は、通常の blinky ではなく cargo run --bin wifi_blinky --release を実行していることを確認してください。pico-w のオンボード LED は WiFi チップに接続されているため、LED を点滅させる前にその初期化が必要です。

rp2040 デバッグプローブ(たとえば pico probe)を使用していて、probe-rs info の実行後に問題が発生している場合は、プローブの電源を入れ直すために、一度取り外してから再接続してください。probe-rs info を実行すると、pico probe が使用不能な状態になることが 知られています

それでも問題が解決しない場合は、https://embassy.dev/book/#_frequently_asked_questions[FAQ] を確認するか、https://matrix.to/#/#embassy-rs:matrix.org[Embassy チャットルーム] で助けを求めてください。

次は?

おめでとうございます。最初の Embassy アプリケーションが動作しました!次に進むための提案をいくつか挙げます。

基本的な Embassy アプリケーション

これで examples の 1 つは動かせるようになりましたが、その次はどうすればよいのでしょうか。理解を深めるために、nRF52 DK 向けのシンプルな Embassy アプリケーションを見ていきましょう。

Main

完全な example は こちら にあります。

Note
examples の表示や編集に VS Code と rust-analyzer を使っている場合は、どのプロジェクトを扱っているかを伝えるために .vscode/settings.json をいくつか変更する必要があるかもしれません。rust-analyzer が正しく動作するように、そのファイル内にコメントされている手順に従ってください。
ベアメタル

最初に気づくのは、ファイル先頭にある 2 つの属性です。これらはコンパイラに対して、このプログラムは std にアクセスできないこと、そして main 関数が存在しないこと(OS によって実行されるわけではないため)を伝えます。

#![no_std]
#![no_main]
エラーへの対処

続いて、panic や fault にどう対処するかに関するいくつかの宣言があります。開発中は、診断情報をターミナルに出力するために defmt-rttpanic-probe を利用するのが良い実践です。

use {defmt_rtt as _, panic_probe as _};
タスク宣言

少し import 宣言を行った後、アプリケーションによって実行されるタスクを宣言します。

#[embassy_executor::task]
async fn blink(pin: Peri<'static, AnyPin>) {
    let mut led = Output::new(pin, Level::Low, OutputDrive::Standard);

    loop {
        // Timekeeping is globally available, no need to mess with hardware timers.
        led.set_high();
        Timer::after_millis(150).await;
        led.set_low();
        Timer::after_millis(150).await;
    }
}

Embassy のタスクは async として宣言しなければならず、ジェネリック引数を取ることはできません。この場合、点滅させるべき LED と点滅間隔が渡されます。

Note
このタスクではビジーウェイトが一切行われていないことに注意してください。Embassy のタイマーを使って実行を yield しているため、マイクロコントローラは点滅の合間にスリープできます。
Main

Embassy アプリケーションの main エントリポイントは、#[embassy_executor::main] マクロを使って定義します。エントリポイントには Spawner が渡され、これを使って他のタスクを spawn できます。

次に、デフォルト設定で HAL を初期化します。これにより Peripherals 構造体が得られ、MCU のさまざまなペリフェラルにアクセスできます。この場合は、LED を駆動する GPIO 出力としてピンの 1 つを設定したいので、次のようにします。

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    // Initialize the embassy-nrf HAL.
    let p = embassy_nrf::init(Default::default());

    // Spawned tasks run in the background, concurrently.
    spawner.spawn(blink(p.P0_13.into()).unwrap());

    let mut button = Input::new(p.P0_11, Pull::Up);
    loop {
        // Asynchronously wait for GPIO events, allowing other tasks
        // to run, or the core to sleep.
        button.wait_for_low().await;
        info!("Button pressed!");
        button.wait_for_high().await;
        info!("Button released!");
    }
}

blinker タスクが spawn され、main が return したら何が起こるのでしょうか。実は main エントリポイントも、1 つしか持てず、いくつか特定の型引数を取る点を除けば、他のタスクとまったく同じです。肝となるのは #[embassy_executor::main] マクロの中です。このマクロは次のことを行います。

  1. Embassy Executor を作成する

  2. エントリポイント用の main タスクを定義する

  3. main タスクを spawn して executor を実行する

また、マクロを使わずに executor を実行する方法もあります。その場合は、自分で Executor インスタンスを作成しなければなりません。

Cargo.toml

プロジェクト定義には embassy の依存関係を含める必要があります。

embassy-executor = { version = "0.10.0", path = "../../../embassy-executor", features = ["defmt", "platform-cortex-m", "executor-thread"] }
embassy-time = { version = "0.5.1", path = "../../../embassy-time", features = ["defmt"] }
embassy-nrf = { version = "0.10.0", path = "../../../embassy-nrf", features = ["defmt", "nrf52840", "time-driver-rtc1", "gpiote"] }

使用するマイクロコントローラによっては、embassy-nrf を別のものに置き換える必要があるかもしれません(STM32 なら embassy-stm32)。feature flag も忘れずに更新してください。

この例では、nrf52840 チップが選択されており、RTC1 ペリフェラルが time driver として使われています。

プロジェクト構成

embassy とそのコンポーネントを、対象のアプリケーションに正確に合わせて構成する方法は数多くあります。各チップセット向けの examples ディレクトリには、プロジェクト構成がどのようになるべきかの例が示されています。では、これを分解して見ていきましょう。

プロジェクトのトップレベルのファイル構成は次のようになります:

{} = Maybe

my-project
|- .cargo
|  |- config.toml
|- src
|  |- main.rs
|- build.rs
|- Cargo.toml
|- {memory.x}
|- rust-toolchain.toml

.cargo/config.toml

このディレクトリ/ファイルでは、使用しているプラットフォームを記述し、デバイスにデプロイするための probe-rs を設定します。

最小限の例を以下に示します:

[target.thumbv6m-none-eabi] # <-使用するプラットフォームに合わせて変更
runner = 'probe-rs run --chip STM32F031K6Tx' # <- 使用するチップに合わせて変更

[build]
target = "thumbv6m-none-eabi" # <-使用するプラットフォームに合わせて変更

[env]
DEFMT_LOG = "trace" # <- info、warn、または error に変更可能

build.rs

これはプロジェクト用のビルドスクリプトです。必要に応じて defmt (defmt とは何でしょうか?) と memory.x ファイルをリンクします。このファイルはチップセットごとの依存がかなり強いため、対応する example からそのままコピー&ペーストしてください。

Cargo.toml

これはマニフェストファイルであり、必要な機能を使用できるように embassy の各コンポーネントを設定できます。

Features
Time
  • tick-hz-x: embassy-time のティックレートを設定します。ティックレートが高いほど精度は高くなりますが、CPU のウェイクアップ回数も増えます。

  • defmt-timestamp-uptime: defmt のログエントリに、秒単位のアップタイムが表示されます。

…​今後追加予定

memory.x

このファイルは、プログラムの flash/ram 使用量の概要を示します。nRF5x で nrf-softdevice を使用する場合に特に便利です。

nRF52840 で S140 を使用する場合の例を以下に示します:

MEMORY
{
  /* 注: 1 K = 1 KiBi = 1024 バイト */
  /* これらの値は Softdevices S140 7.0.1 を使用する NRF52840 に対応しています */
  FLASH : ORIGIN = 0x00027000, LENGTH = 868K
  RAM : ORIGIN = 0x20020000, LENGTH = 128K
}

rust-toolchain.toml

このファイルでは、使用する rust のバージョンと設定を構成します。

最小限の例:

[toolchain]
channel = "1.85" # <- 執筆時点では、これは embassy が使用している正確な rust バージョンです
components = [ "rust-src", "rustfmt" ] # <- "cargo size" などの追加機能のために、必要に応じて "llvm-tools-preview" を追加できます
targets = [
    "thumbv6m-none-eabi" # <- 使用するプラットフォームに合わせて変更
]

新しいプロジェクトを開始する

いくつかのサンプルプロジェクトを実行できたら、次のステップはスタンドアロンの Embassy プロジェクトを作成することです。

Embassy プロジェクトを生成するためのツール

CLI
cargo-generate
esp-generate

スクラッチからプロジェクトを開始する

例として、STM32G474 向けの新しい embassy プロジェクトをスクラッチから作成してみましょう。以下の手順は、いくつかの小さな変更を除けば、サポートされている任意のチップに適用できます。

実行します:

cargo new stm32g474-example
cd stm32g474-example

これで空の Rust プロジェクトが作成されます:

stm32g474-example
├── Cargo.toml
└── src
    └── main.rs

Embassy のサンプルを見ると、stm32g4 フォルダーがあることがわかります。src/blinky.rs を見つけて、その内容を src/main.rs にコピーします。

.cargo/config.toml

現時点では、cargo buildcargo run を実行するたびに Cargo にターゲットトリプルを指定する必要があります。その手間を省くために、examples/stm32g4 から .cargo/config.toml をプロジェクトにコピーしましょう。

stm32g474-example
├── .cargo
│   └── config.toml
├── Cargo.toml
└── src
    └── main.rs

ターゲットトリプルに加えて、.cargo/config.toml には runner キーも含まれており、probe-rs 経由で cargo run を使ってハードウェア上でプロジェクトを手軽に実行できます。これを動作させるには、正しいチップ ID を指定する必要があります。これは probe-rs chip list を確認することで行えます:

$ probe-rs chip list | grep -i stm32g474re
        STM32G474RETx

そして、次のように STM32G474RETx.cargo/config.toml にコピーします:

[target.'cfg(all(target_arch = "arm", target_os = "none"))']
# `probe-rs chip list` に表示されるチップに合わせて STM32G071C8Rx を置き換えてください
runner = "probe-rs run --chip STM32G474RETx"
Cargo.toml

これで Cargo はどのターゲット向けにコンパイルするかを認識し、probe-rs はどのチップ上で実行するかを認識したので、依存関係を追加する準備が整いました。

examples/stm32g4/Cargo.toml を見ると、サンプルではいくつかの embassy クレートが必要であることがわかります。blinky では、そのうち embassy-stm32embassy-executorembassy-time の 3 つだけが必要です。

執筆時点では、embassy はすでに crates.io に公開されています。そのため、依存関係は Cargo.toml に簡単に追加できます。

[dependencies]
embassy-stm32 = { version = "0.1.0", features =  ["defmt", "time-driver-any", "stm32g474re", "memory-x", "unstable-pac", "exti"] }
embassy-executor = { version = "0.6.3", features = ["nightly", "platform-cortex-m", "executor-thread", "defmt"] }
embassy-time = { version = "0.3.2", features = ["defmt", "defmt-timestamp-uptime", "tick-hz-32_768"] }

以前は、embassy を git リポジトリから直接インストールする必要がありました。まだ公開されていない embassy クレートの特定のリビジョンをチェックアウトしたい場合には、git からのインストールは今でも有用です。 推奨される方法は次のとおりです:

  • サンプルの Cargo.toml から必要な embassy-* の行をコピーする

  • embassy-stm32stm32g474re feature が必要な場合のように、必要に応じて features を変更する

  • embassy-* エントリーから path = "" キーを削除する

  • 必要な各 embassy クレートのエントリーを含む [patch.crates-io] セクションを作成する。これらにはすべて同じ値を指定する必要があります。つまり、git リポジトリへのリンクと、チェックアウトするコミットへの参照です。最新のコミットが必要であれば、git ls-remote https://github.com/embassy-rs/embassy.git HEAD を実行して確認できます

Note
この方法を使う場合、[dependencies] 内の version キーは、[patch.crates.io] の指定した rev における各クレートの Cargo.toml で定義されているバージョンと一致している必要があります。つまり、更新時には新しいリビジョンを選び、[patch.crates.io] 内のすべてをそれに合わせて変更し、そのうえで変更された version があれば [dependencies] 側でも修正しなければなりません。

以下に Cargo.toml ファイルの例を示します。version の値は、チェックアウトする git リビジョンにある embassy クレートで定義されているバージョンと一致している必要がある点に注意してください。これは、crates.io 上の最新公開バージョンとは異なる場合があります:

[dependencies]
embassy-stm32 = {version = "0.1.0", features =  ["defmt", "time-driver-any", "stm32g474re", "memory-x", "unstable-pac", "exti"]}
embassy-executor = { version = "0.6.3", features = ["platform-cortex-m", "executor-thread", "defmt"] }
embassy-time = { version = "0.3.2", features = ["defmt", "defmt-timestamp-uptime", "tick-hz-32_768"] }

[patch.crates-io]
embassy-time = { git = "https://github.com/embassy-rs/embassy", rev = "7703f47c1ecac029f603033b7977d9a2becef48c" }
embassy-executor = { git = "https://github.com/embassy-rs/embassy", rev = "7703f47c1ecac029f603033b7977d9a2becef48c" }
embassy-stm32 = { git = "https://github.com/embassy-rs/embassy", rev = "7703f47c1ecac029f603033b7977d9a2becef48c" }

プロジェクトをビルドするために必要な依存関係は他にもいくつかありますが、幸いそれらの導入はずっと簡単です。サンプルの Cargo.toml からそれらの行を新しい Cargo.toml[dependencies] セクションにコピーしてください:

defmt = "0.3.5"
defmt-rtt = "0.4.0"
cortex-m = {version = "0.7.7", features = ["critical-section-single-core"]}
cortex-m-rt = "0.7.3"
panic-probe = "0.3.1"

これらは blinky.rs を実行するために必要な最低限の依存関係ですが、サンプルの Cargo.toml に指定されているほかの依存関係にも目を通し、embassy で使うためにどの feature が必要かを確認しておく価値があります。たとえば futures = { version = "0.3.17", default-features = false, features = ["async-await"] } などです。

最後に、サンプルの Cargo.toml から [profile.release] セクションをこちらにもコピーします:

[profile.release]
debug = 2
rust-toolchain.toml

プロジェクトをビルドできるようにする前に、Cargo に使用するツールチェーンを伝えるための追加ファイルを 1 つ用意する必要があります。embassy リポジトリから rust-toolchain.toml をコピーし、ターゲット一覧をこのプロジェクトに必要なターゲットトリプルのみに絞り込みます。この場合は thumbv7em-none-eabi です:

stm32g474-example
├── .cargo
│   └── config.toml
├── Cargo.toml
├── rust-toolchain.toml
└── src
    └── main.rs
# アップグレードする前に、ここですべての tier1 ターゲットで利用可能か確認してください:
# https://rust-lang.github.io/rustup-components-history
[toolchain]
channel = "1.85"
components = [ "rust-src", "rustfmt", "llvm-tools", "miri" ]
targets = ["thumbv7em-none-eabi"]
build.rs

ターゲット向けに動作するバイナリを生成するには、cargo にはカスタムのビルドスクリプトが必要です。サンプルから build.rs を自分たちのプロジェクトにコピーしてください:

stm32g474-example
├── build.rs
├── .cargo
│   └── config.toml
├── Cargo.toml
├── rust-toolchain.toml
└── src
    └── main.rs
ビルドと実行

ここまで来れば、ようやくプロジェクトをビルドして実行する準備が整いました! ボードをデバッグプローブ経由で接続し、次を実行してください:

cargo run --release

すると、LED が点滅し(src/main.rs のピンに接続されている場合。接続されていない場合は変更してください!)、次のような出力が得られるはずです:

   Compiling stm32g474-example v0.1.0 (/home/you/stm32g474-example)
    Finished release [optimized + debuginfo] target(s) in 0.22s
     Running `probe-rs run --chip STM32G474RETx target/thumbv7em-none-eabi/release/stm32g474-example`
     Erasing sectors ✔ [00:00:00] [#########################################################] 18.00 KiB/18.00 KiB @ 54.09 KiB/s (eta 0s )
 Programming pages   ✔ [00:00:00] [#########################################################] 17.00 KiB/17.00 KiB @ 35.91 KiB/s (eta 0s )    Finished in 0.817s
0.000000 TRACE BDCR configured: 00008200
└─ embassy_stm32::rcc::bd::{impl#3}::init::{closure#4} @ /home/you/.cargo/git/checkouts/embassy-9312dcb0ed774b29/7703f47/embassy-stm32/src/fmt.rs:117
0.000000 DEBUG rcc: Clocks { sys: Hertz(16000000), pclk1: Hertz(16000000), pclk1_tim: Hertz(16000000), pclk2: Hertz(16000000), pclk2_tim: Hertz(16000000), hclk1: Hertz(16000000), hclk2: Hertz(16000000), pll1_p: None, adc: None, adc34: None, rtc: Some(Hertz(32000)) }
└─ embassy_stm32::rcc::set_freqs @ /home/you/.cargo/git/checkouts/embassy-9312dcb0ed774b29/7703f47/embassy-stm32/src/fmt.rs:130
0.000000 INFO  Hello World!
└─ embassy_stm32g474::____embassy_main_task::{async_fn#0} @ src/main.rs:14
0.000091 INFO  high
└─ embassy_stm32g474::____embassy_main_task::{async_fn#0} @ src/main.rs:19
0.300201 INFO  low
└─ embassy_stm32g474::____embassy_main_task::{async_fn#0} @ src/main.rs:23

ベストプラクティス

時間の経過とともに、いくつかのベストプラクティスが生まれてきました。以下のリストは、Rust で組み込みソフトウェアを書く開発者、特に Embassy フレームワークの文脈における開発者向けのガイドラインとして役立つはずです。

バッファを参照で渡す

heapless::Vec のような配列やラッパーを、 std::Vec で行うのと同じように関数に渡したり、戻り値として返したりしたくなるかもしれません。しかし、多くの組み込みアプリケーションでは、アロケータにリソースを費やしたくないため、結果としてバッファをスタックに配置することになります。ところが、注意しないと、これは簡単に スタックを圧迫してしまう可能性があります。

次の例を考えてみてください:

fn process_buffer(mut buf: [u8; 1024]) -> [u8; 1024] {
    // 処理を行い、新しいバッファを返す
    for elem in buf.iter_mut() {
        *elem = 0;
    }
    buf
}

pub fn main() -> () {
    let buf = [1u8; 1024];
    let buf_new = process_buffer(buf);
    // buf_new に対して何らかの処理を行う
    ()
}

プログラム内で process_buffer を呼び出すと、関数に渡したバッファのコピーが作成され、 さらに 1024 バイトを消費します。 処理の後、呼び出し元に返すために、さらに 1024 バイトのバッファがスタックに配置されます。 (アセンブリを確認できます。たとえば Cortex-M プロセッサ向けにコンパイルした場合、bl __aeabi_memcpy のような 2 つの memcopy 操作が存在するはずです。)

考えられる解決策:

入るときも出るときも、データは値ではなく参照で渡してください。 たとえば、出力として入力バッファのスライスを返すことができます。 入力スライスと出力スライスのライフタイムが同じであることを要求することで、この手順のメモリ安全性はコンパイラによって保証されます。

fn process_buffer<'a>(buf: &'a mut [u8]) -> &'a mut[u8] {
    for elem in buf.iter_mut() {
        *elem = 0;
    }
    buf
}

pub fn main() -> () {
    let mut buf = [1u8; 1024];
    let buf_new = process_buffer(&mut buf);
    // buf_new に対して何らかの処理を行う
    ()
}

ベアメタルから async Rust へ

Embassy を初めて使う場合、用語や概念の全体像をつかむのは難しく感じられるかもしれません。このガイドは、Embassy におけるさまざまなレイヤーと、それぞれのレイヤーがアプリケーション開発者にとってどの問題を解決するのかを明確にすることを目的としています。

このガイドでは STM32 IOT01A ボードを使用しますが、どの STM32 チップにも容易に移植できるはずです。nRF については、PAC 自体は Embassy プロジェクト内でメンテナンスされていませんが、概念やレイヤーは同様です。

これから作成するアプリケーションは、単純な「ボタンを押すと LED が点滅する」アプリケーションです。これは、この後に見ていく各例における入力と出力の処理を説明するのに適しています。Peripheral Access Crate (PAC) の例から始めて、async の例で終わります。

PAC バージョン

メモリアドレスへの直接の読み書きを除けば、PAC はペリフェラルやレジスタにアクセスするための最も低レベルな API です。ペリフェラルレジスタへのアクセスを容易にするための個別の型を提供しますが、それらのレジスタを誤って設定したり連携させたりすることを防ぐ仕組みはほとんどありません。

そのため、PAC を直接使ってアプリケーションを書くことは推奨されません。しかし、使いたい機能が上位レイヤーで公開されていない場合は、PAC を使う必要があります。

PAC を使った blinky アプリを以下に示します:

#![no_std]
#![no_main]

use pac::gpio::vals;
use {defmt_rtt as _, panic_probe as _, stm32_metapac as pac};

#[cortex_m_rt::entry]
fn main() -> ! {
    // Enable GPIO clock
    let rcc = pac::RCC;
    rcc.ahb2enr().modify(|w| {
        w.set_gpioben(true);
        w.set_gpiocen(true);
    });

    rcc.ahb2rstr().modify(|w| {
        w.set_gpiobrst(true);
        w.set_gpiocrst(true);
        w.set_gpiobrst(false);
        w.set_gpiocrst(false);
    });

    // Setup button
    let gpioc = pac::GPIOC;
    const BUTTON_PIN: usize = 13;
    gpioc.pupdr().modify(|w| w.set_pupdr(BUTTON_PIN, vals::Pupdr::PullUp));
    gpioc.otyper().modify(|w| w.set_ot(BUTTON_PIN, vals::Ot::PushPull));
    gpioc.moder().modify(|w| w.set_moder(BUTTON_PIN, vals::Moder::Input));

    // Setup LED
    let gpiob = pac::GPIOB;
    const LED_PIN: usize = 14;
    gpiob.pupdr().modify(|w| w.set_pupdr(LED_PIN, vals::Pupdr::Floating));
    gpiob.otyper().modify(|w| w.set_ot(LED_PIN, vals::Ot::PushPull));
    gpiob.moder().modify(|w| w.set_moder(LED_PIN, vals::Moder::Output));

    // Main loop
    loop {
        if gpioc.idr().read().idr(BUTTON_PIN) == vals::Idr::Low {
            gpiob.bsrr().write(|w| w.set_bs(LED_PIN, true));
        } else {
            gpiob.bsrr().write(|w| w.set_br(LED_PIN, true));
        }
    }
}

ご覧のとおり、ペリフェラルクロックを有効にし、アプリケーションの入力ピンと出力ピンを設定するには、多くのコードが必要です。

このアプリケーションのもう 1 つの欠点は、ボタンの状態をポーリングしながらビジーループしていることです。このため、マイクロコントローラは消費電力を節約するためのスリープモードを利用できません。

HAL バージョン

アプリケーションを簡単にするために、代わりに HAL を使うことができます。HAL は、次のような詳細を処理する、より高レベルな API を提供します:

  • ペリフェラルを使用するときに、ペリフェラルクロックを自動的に有効化する

  • より高レベルな型からレジスタ設定を導出して適用する

  • embedded-hal トレイトを実装し、サードパーティ製ドライバでペリフェラルを利用可能にする

HAL の例を以下に示します:

#![no_std]
#![no_main]

use cortex_m_rt::entry;
use embassy_stm32::gpio::{Input, Level, Output, Pull, Speed};
use {defmt_rtt as _, panic_probe as _};

#[entry]
fn main() -> ! {
    let p = embassy_stm32::init(Default::default());
    let mut led = Output::new(p.PB14, Level::High, Speed::VeryHigh);
    let button = Input::new(p.PC13, Pull::Up);

    loop {
        if button.is_low() {
            led.set_high();
        } else {
            led.set_low();
        }
    }
}

ご覧のとおり、async コードをまったく使わなくても、アプリケーションは大幅に単純になります。Input 型と Output 型は GPIO レジスタへのアクセスに関する詳細をすべて隠蔽し、ボタンの状態を問い合わせたり LED 出力を切り替えたりするための、はるかに単純な API を使えるようにします。

ただし、PAC の例と同じ欠点は依然として残っています。アプリケーションはビジーループしており、必要以上に電力を消費します。

割り込み駆動

消費電力を抑えるには、割り込みを使ってボタンが押されたことを通知できるようにアプリケーションを設定する必要があります。

割り込みを設定すると、アプリケーションはマイクロコントローラにスリープモードへ入るよう指示でき、消費電力を非常に低く抑えられます。

Embassy は async Rust に重点を置いているため(これについてはこの例の後で戻ります)、このサンプルアプリケーションでは割り込みを使うために HAL と PAC を組み合わせて使う必要があります。そのため、このアプリケーションには PAC にアクセスするためのいくつかのヘルパー関数も含まれています(以下には示していません)。

#![no_std]
#![no_main]

use core::cell::RefCell;

use cortex_m::interrupt::Mutex;
use cortex_m::peripheral::NVIC;
use cortex_m_rt::entry;
use embassy_stm32::gpio::{Input, Level, Output, Pull, Speed};
use embassy_stm32::{interrupt, pac};
use {defmt_rtt as _, panic_probe as _};

static BUTTON: Mutex<RefCell<Option<Input<'static>>>> = Mutex::new(RefCell::new(None));
static LED: Mutex<RefCell<Option<Output<'static>>>> = Mutex::new(RefCell::new(None));

#[entry]
fn main() -> ! {
    let p = embassy_stm32::init(Default::default());
    let led = Output::new(p.PB14, Level::Low, Speed::Low);
    let mut button = Input::new(p.PC13, Pull::Up);

    cortex_m::interrupt::free(|cs| {
        enable_interrupt(&mut button);

        LED.borrow(cs).borrow_mut().replace(led);
        BUTTON.borrow(cs).borrow_mut().replace(button);

        unsafe { NVIC::unmask(pac::Interrupt::EXTI15_10) };
    });

    loop {
        cortex_m::asm::wfe();
    }
}

#[interrupt]
fn EXTI15_10() {
    cortex_m::interrupt::free(|cs| {
        let mut button = BUTTON.borrow(cs).borrow_mut();
        let button = button.as_mut().unwrap();

        let mut led = LED.borrow(cs).borrow_mut();
        let led = led.as_mut().unwrap();
        if check_interrupt(button) {
            if button.is_low() {
                led.set_high();
            } else {
                led.set_low();
            }
        }
        clear_interrupt(button);
    });
}
//
//
//
//

この単純なアプリケーションは、再び複雑になっています。主な理由は、ボタンと LED の状態を、メインアプリケーションループと割り込みハンドラの両方からアクセスできるグローバルスコープに保持する必要があるためです。

そのためには、これらの型を mutex で保護する必要があり、ペリフェラルにアクセスするためにこのグローバル状態へアクセスする際は、常に割り込みを無効にしなければなりません。

幸い、Embassy を使うとこの問題には洗練された解決策があります。

Async バージョン

ここからは Embassy の機能を最大限に活用します。Embassy の中核には async executor、言い換えれば async タスクのランタイムがあります。executor は一連のタスク(コンパイル時に定義されます)をポーリングし、あるタスクが blocks すると、別のタスクを実行するか、マイクロコントローラをスリープさせます。

#![no_std]
#![no_main]

use embassy_executor::Spawner;
use embassy_stm32::exti::{self, ExtiInput};
use embassy_stm32::gpio::{Level, Output, Pull, Speed};
use embassy_stm32::{bind_interrupts, interrupt};
use {defmt_rtt as _, panic_probe as _};

bind_interrupts!(
    pub struct Irqs{
        EXTI15_10  => exti::InterruptHandler<interrupt::typelevel::EXTI15_10 >;
});

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    let p = embassy_stm32::init(Default::default());
    let mut led = Output::new(p.PB14, Level::Low, Speed::VeryHigh);
    let mut button = ExtiInput::new(p.PC13, p.EXTI13, Pull::Up, Irqs);

    loop {
        button.wait_for_any_edge().await;
        if button.is_low() {
            led.set_high();
        } else {
            led.set_low();
        }
    }
}

async バージョンは、いくつかの細かな違いを除けば HAL バージョンと非常によく似ています:

  • メインのエントリポイントには別のマクロが付与されており、async の型シグネチャを持っています。このマクロは Embassy ランタイムのインスタンスを生成して起動し、メインアプリケーションタスクを開始します。アプリケーションは Spawner インスタンスを使って他のタスクを spawn できます。

  • ペリフェラルの初期化はメインマクロによって行われ、その結果がメインタスクに渡されます。

  • ボタンの状態を確認する前に、アプリケーションはピン状態の遷移(low → high または high → low)を待機しています。

button.wait_for_any_edge().await が呼び出されると、実行可能な他のタスクがない限り、executor はメインタスクを一時停止し、マイクロコントローラをスリープモードに入れます。このチップでは、EXTI ライン 10-15 上の割り込み信号(EXTI ライン 13 上のボタンを含む)がハードウェア割り込み EXTI15_10 を発生させます。この割り込みハンドラは、exti モジュールが提供する InterruptHandler を呼び出すように bind_interrupts! を使ってバインドされているため、割り込みが発生するたびに、wait_for_any_edge() を通じてボタンを待っているタスクが起こされます。

executor のオーバーヘッドが最小限であること、複数のタスクを「並行して」実行できること、そしてアプリケーションを大幅に単純化できることを合わせると、async は組み込みに非常によく適しています。

まとめ

Embassy における異なる抽象化レベルで、同じアプリケーションをどのように書けるかを見てきました。まず PAC レベルから始め、次に HAL を使い、その後割り込みを使い、最後に async Rust を使って間接的に割り込みを利用しました。

次は?

基本に慣れたところで、システムの説明 セクションでは、executor、HAL、ブートローダー、その他の Embassy コンポーネントについてより詳しく説明します。

開発者向け

このセクションでは Embassy の内部について説明します。対象読者は、Embassy がどのように構築されているかを理解したい、または新しいハードウェアのサポートを追加したいコントリビューターや開発者です。

STM32

metapac を理解する

embassy-stm32 をインポートするプロジェクトがコンパイルされると、そのプロジェクトは使用しているチップに対応する feature を選択します。その feature に基づいて、embassy-stm32 はそのチップでサポートされる IP を選択し、対応する HAL 実装を有効にします。では、私たちがサポートしている何百ものチップの中から、そのチップにどの IP が含まれているのかを embassy-stm32 はどのように知るのでしょうか。これは stm32-data-sources から始まる長い話です。

stm32-data-sources

stm32-data-sources は、ほとんど何もないリポジトリです。README もドキュメントもなく、watcher もほとんどいません。しかし、これこそが embassy-stm32 を可能にしている中核です。私たちがサポートしている各チップのデータは、一部が STM32F051K4Ux.xml のような対応する XML ファイルから取得されています。そのファイルには、次のような行があります。

    <IP InstanceName="I2C1" Name="I2C" Version="i2c2_v1_1_Cube"/>
    <!-- 中略  -->
    <IP ConfigFile="TIM-STM32F0xx" InstanceName="TIM1" Name="TIM1_8F0" Version="gptimer2_v2_x_Cube"/>

これらの行は、このチップが i2c を持ち、そのバージョンが "v1_1" であることを示しています。また、バージョンが "v2_x" の汎用タイマーを持っていることも示しています。このデータから、embassy-stm32 にどの実装を含めるべきかを判断できます。しかし、それを実際に行うのはまた別の話です。

stm32-data

このプロジェクトのユーザーであれば embassy-stm32 は誰もが知っていますが、それを支えているプロジェクト stm32-data を知っている人はそれほど多くありません。このプロジェクトの目的は、embassy-stm32 向けのデータを生成することだけでなく、一般に機械処理で利用できるデータを生成することにもあります。これを実現するために、stm32-data-sources プロジェクト内の複数ファイルからの情報を結合して解析し、サポート対象の各 IP にレジスタブロック実装を割り当てます。この対応付けの中核は chips.rs にあります。

    (".*:I2C:i2c2_v1_1", ("i2c", "v2", "I2C")),
    // 中略
    (r".*TIM\d.*:gptimer.*", ("timer", "v1", "TIM_GP16")),

この場合、i2c のバージョンは私たちのいう "v2" に対応し、汎用タイマーのバージョンは私たちのいう "v1" に対応します。したがって、それぞれの IP には i2c_v2.yamltimer_v1.yaml のレジスタブロック実装が割り当てられます。その結果、STM32F051K4.json には次のような行が生成されます。

    {
        "name": "I2C1",
        "address": 1073763328,
        "registers": {
            "kind": "i2c",
            "version": "v2",
            "block": "I2C"
        },
        // 中略
    }
    // 中略
    {
        "name": "TIM1",
        "address": 1073818624,
        "registers": {
            "kind": "timer",
            "version": "v1",
            "block": "TIM_ADV"
        },
        // 中略
    }

レジスタブロックに加えて、ピンおよび RCC のマッピング用データも生成され、embassy-stm32 によって利用されます。stm32-metapac-gen は、このデータを crate としてパッケージ化して公開するために使われます。

embassy-stm32

embassy-stm32 のルートにある lib.rs ファイルには、次の行があります。

#[cfg(i2c)]
pub mod i2c;

そして、i2c モジュールの mod.rs には次の記述があります。

#[cfg_attr(i2c_v2, path = "v2.rs")]

STM32F051K4 では i2c がサポートされており、そのバージョンが私たちのいう "v2" に対応しているため、i2ci2c_v2 の設定ディレクティブが存在し、embassy-stm32 はそれぞれ対応するこれらのファイルを取り込みます。これらやその他の設定ディレクティブ、テーブルはチップのデータから生成されており、embassy-stm32 は各チップに必要な内容に応じて、ロジックや実装を明確かつ表現力高く適応させることができます。組み込みエコシステム全体の他のプロジェクトと比べても、embassy-stm32 は STM32 の全ラインアップにわたってコードを再利用し、実装が難しい unsafe なロジックを HAL に封じ込めることができる唯一のプロジェクトです。

システムの説明

このセクションでは、Embassy のさまざまな部分について、より詳しく説明します。

Embassy エグゼキューター

Embassy エグゼキューターは、割り込みやタイマーのサポート機能とともに、組み込み用途向けに設計された async/await エグゼキューターです。

機能

  • alloc は不要で、ヒープも必要ありません。タスクは静的に割り当てられます。

  • 「固定容量」のデータ構造は不要で、エグゼキューターは設定やチューニングを必要とせずに 1 個でも 1000 個でもタスクを扱えます。

  • タイマーキューを統合しています。スリープは簡単で、Timer::after_secs(1).await; とするだけです。

  • ビジーループによるポーリングは不要です。実行すべき処理がないとき、CPU は割り込みまたは WFE/SEV を使ってスリープします。

  • 効率的なポーリング: ウェイクによりポーリングされるのは、起こされたタスクだけであり、すべてのタスクではありません。

  • 公平性: タスクが常にウェイクされ続けていても CPU 時間を独占できません。あるタスクが 2 回目にポーリングされる前に、ほかのすべてのタスクに実行の機会が与えられます。

  • 複数のエグゼキューターインスタンスを作成して、異なる優先度レベルでタスクを実行できます。これにより、高優先度のタスクが低優先度のタスクをプリエンプトできます。

エグゼキューター

エグゼキューターは次のように動作します。

  1. タスクが作成されると、そのタスクはポーリングされます。

  2. タスクは、ブロックされる地点に到達するまで処理を進めようとします。これは、タスクが async 関数を .await しているときに発生することがあります。その場合、タスクは Poll::Pending を返すことで実行を譲ります。

  3. タスクが実行を譲ると、エグゼキューターはそのタスクを実行キューの末尾に入れ、キュー内の次のタスクのポーリングに進みます。

タスクが完了またはキャンセルされると、再びキューに入れられることはありません。

Important
エグゼキューターは、タスクが無期限にブロックしないことを前提としています。そうなると、エグゼキューターが制御を取り戻して別のタスクをスケジュールできなくなります。
エグゼキューターモデル

アプリケーションで #[embassy_executor::main] マクロを使用すると、Executor が自動的に作成され、メインエントリポイントが最初のタスクとしてスポーンされます。Executor は手動で作成することもでき、実際には複数の Executor を作成できます。

割り込み

割り込みは、ペリフェラルが何らかの操作の完了を通知する一般的な方法であり、async 実行モデルによく適合します。次の図は、典型的なアプリケーションフローを示しています。(1) タスクがポーリングされ、処理を進めようとします。次にタスクは (2) ペリフェラルに何らかの操作を実行するよう指示し、await します。しばらくすると、(3) 割り込みが発生し、その操作の完了が示されます。

続いてペリフェラル HAL は (4) 割り込み信号がそのペリフェラルにルーティングされることを保証し、操作結果でペリフェラルの状態を更新します。その後、エグゼキューターは (5) そのタスクをポーリングすべきことを通知され、実際にそうします。

割り込み処理
Note
InterruptExecutor という特別なエグゼキューターは、割り込みによって駆動できます。これは、複数の InterruptExecutor インスタンスを作成することで、異なる優先度レベルのタスクを実行するために使用できます。

時間

Embassy には、time フィーチャーフラグで有効化される内部タイマーキューがあります。有効にすると、Embassy はそのプラットフォームに時間用の Driver 実装が存在することを前提とします。Embassy は、nRF、STM32、RPi Pico、WASM、Std プラットフォーム向けの time ドライバーを提供しています。

Note
一部の組み込みプラットフォームのタイマードライバーは、サポートするアラーム数が限られている場合があります。タイマーを同時に使用するタスク数がこの制限を超えないようにしてください。

タイマー速度は、time-tick-<frequency> を使用してコンパイル時に設定できます。現在、タイマーは 1000 Hz、32768 Hz、または 1 MHz で動作するように設定できます。デフォルトを変更する前に、対象の HAL がその周波数設定をサポートしていることを確認してください。

Note
アプリケーションでタイマーが不要な場合は、time フィーチャーを有効にしないことで、CPU サイクルをいくらか節約し、消費電力を低減できます。

ブートローダー

embassy-boot は、試験的なブートとロールバックを備え、電源断に対して安全な方法でファームウェアアプリケーションのアップグレードをサポートする軽量ブートローダーです。

この更新方法は、A/B パーティション更新方式と呼ばれます。

汎用 OS では、A/B パーティション更新は、更新状態に応じて A または B パーティションのいずれかを直接ブートすることで実現されます。 同じ目標をすべてのマイクロコントローラーで移植可能な形で達成するために、embassy-boot は、ファームウェア更新がトリガーされたときに DFU パーティションと Active パーティションの間でページ単位にデータを入れ替えます(双方向に)。
この更新中に元の Active アプリケーションは DFU パーティションへ移動されるため、更新が中断された場合や、新しいファームウェアが正常に起動したことをフラグで示さなかった場合には、この操作を元に戻すことができます。
これがどのように実装されているかの詳細については、設計セクションを参照してください。

ブートローダーはライブラリとして使用することも、デフォルトの構成と機能で十分であれば直接フラッシュすることもできます。

設計上、このブートローダー自体はネットワーク機能を提供しません。新しいファームウェアを取得するためのネットワーク機能は、ファームウェア更新のためにブートローダーをライブラリとして利用するユーザーアプリケーション側で提供するか、またはブートローダーをライブラリとして使用して自分でこの機能を追加できます。

このブートローダーは、embedded-storage トレイトに依存することで、内蔵フラッシュと外部フラッシュの両方をサポートします。ブートローダーは、デジタル署名されたファームウェアの検証もオプションでサポートします(推奨)。

ハードウェアサポート

このブートローダーは以下をサポートします。

  • softdevice の有無にかかわらず nRF52

  • STM32 L4、WB、WL、L1、L0、F3、F7、H7

  • Raspberry Pi: RP2040

一般に、このブートローダーは、内部フラッシュ向けに embedded-storage トレイトを実装しているあらゆるプラットフォームで動作しますが、動作させるにはカスタム初期化コードが必要になる場合があります。

STM32L0x1 デバイスでは、flash-erase-zero 機能を有効にする必要があります。

設計

ブートローダーのフラッシュレイアウト

ブートローダーはストレージを 4 つの主要なパーティションに分割します。これらは、ブートローダーインスタンスの作成時、またはリンカスクリプトを介して設定できます。

  • BOOTLOADER - ブートローダーが配置される場所です。ブートローダー自体は約 8kB のフラッシュを消費しますが、デバッグする必要があり、かつ空き容量がある場合は、これを 24kB に増やすことで、probe-rs を使ってブートローダーを実行できるようになります。

  • ACTIVE - メインアプリケーションが配置される場所です。ブートローダーはこのパーティションの先頭からアプリケーションのロードを試みます。このパーティションはブートローダーによってのみ書き込まれます。このパーティションに必要なサイズは、アプリケーションのサイズに依存します。

  • DFU - スワップ対象となるアプリケーションが配置される場所です。このパーティションはアプリケーションによって書き込まれます。スワップアルゴリズムは、この余分な領域を使って電源断に対して安全なデータコピーを確保するため、このパーティションは ACTIVE パーティションより少なくとも 1 ページ大きくなければなりません。

    パーティションサイズdfu= パーティションサイズactive+ ページサイズactive

    すべての値はバイト単位で指定します。

  • BOOTLOADER STATE - Active パーティションと DFU パーティションをスワップする必要があるかどうかを示す現在の状態を、ブートローダーが保存する場所です。新しいファームウェアが DFU パーティションに書き込まれると、ブートローダーにパーティションをスワップすべきことを指示するため、マジックフィールドが書き込まれます。このパーティションは、マジックフィールドとパーティションスワップの進行状況の両方を保存できなければなりません。パーティションサイズは次式で与えられます。

    パーティションサイズstate = (2 × 書き込みサイズstate) + (4 × 書き込みサイズstate × パーティションサイズactive / ページサイズactive)

    すべての値はバイト単位で指定します。

ACTIVE(+BOOTLOADER)、DFU、BOOTLOADER_STATE の各パーティションは、別々のフラッシュに配置することもできます。ブートローダーが使用するページサイズは、ACTIVE と DFU のページサイズの最小公倍数によって決まります。 BOOTLOADER_STATE パーティションは、2 ワードに加えて、ACTIVE パーティションの各ページにつき 4 ワードを保存できる十分な大きさでなければなりません。

ブートローダーには、パーティションによって定められた境界内で電源断に対して安全なスワップアルゴリズムを実装する、プラットフォーム非依存の部分があります。プラットフォーム固有の部分は、ウォッチドッグや nRF52 softdevice のサポートなどの追加機能を提供する最小限のシムです。

Note
アプリケーション用とブートローダー用のリンカスクリプトは似ていますが、FLASH 領域は、ブートローダーでは BOOTLOADER パーティションを、アプリケーションでは ACTIVE パーティションを指している必要があります。
FirmwareUpdater

FirmwareUpdater は、DFU パーティションへファームウェアを簡単に書き込み、その後、次回のリセット時に Active パーティションとのスワップ準備完了としてマークするためのオブジェクトです。その主なメソッドは write_firmwaremark_updated です。write_firmware はフラッシュの「write block」サイズごと(通常は 4KiB)に 1 回呼び出され、mark_updated は最後に呼び出されます。

検証

このブートローダーは、DFU パーティションに書き込まれたファームウェアの検証をサポートします。検証を行うには、ファームウェアが ed25519 署名を使用してデジタル署名されている必要があります。検証を有効にした場合、mark_updated の代わりに FirmwareUpdater::verify_and_mark_updated メソッドを呼び出します。公開鍵と署名に加え、書き込まれたファームウェアの実際の長さが必要です。検証に失敗した場合、ファームウェアは更新済みとしてマークされず、そのため拒否されます。

署名は通常、更新対象のファームウェアとともに渡され、フラッシュには書き込まれません。署名をどのように提供するかは、ファームウェア側の責務です。

検証を有効にするには、embassy-boot クレートに依存する際に ed25519-dalek または ed25519-salty のいずれかの機能を使用してください。現時点では、サイズが小さいため ed25519-salty を推奨します。

ed25519 の鍵と署名に関するヒント

Ed25519 は公開鍵署名方式であり、秘密鍵を安全に保つ責任はあなたにあります。簡単に verify_and_mark_updated に渡せるよう、プログラムに 公開 鍵を埋め込むことを推奨します。ファームウェア内での公開鍵宣言の例を次に示します。

static PUBLIC_SIGNING_KEY: &[u8] = include_bytes!("key.pub");

署名は、追記する形でファームウェアと一緒に渡されることがよくあります。

Ed25519 の鍵はさまざまなツールで生成できます。OpenBSD ディストリビューションの署名および検証に広く使われており、使い方も簡単なため、signify を推奨します。

以下の Bash コマンド一式は、Unix プラットフォームで公開鍵と秘密鍵を生成し、さらに signify のファイルヘッダーを除去したローカルの key.pub ファイルを生成するために使用できます。安全な場所に SECRETS_DIR 環境変数を設定してください。

signify -G -n -p $SECRETS_DIR/key.pub -s $SECRETS_DIR/key.sec
tail -n1 $SECRETS_DIR/key.pub | base64 -d -i - | dd ibs=10 skip=1 > key.pub
chmod 700 $SECRETS_DIR/key.sec
export SECRET_SIGNING_KEY=$(tail -n1 $SECRETS_DIR/key.sec)

次に、FIRMWARE_DIR を宣言済みで、ファームウェアのファイル名が myfirmware であるとして、ファームウェアに署名するには次を実行します。

shasum -a 512 -b $FIRMWARE_DIR/myfirmware | head -c128 | xxd -p -r > $SECRETS_DIR/message.txt
signify -S -s $SECRETS_DIR/key.sec -m $SECRETS_DIR/message.txt -x $SECRETS_DIR/message.txt.sig
cp $FIRMWARE_DIR/myfirmware $FIRMWARE_DIR/myfirmware+signed
tail -n1 $SECRETS_DIR/message.txt.sig | base64 -d -i - | dd ibs=10 skip=1 >> $FIRMWARE_DIR/myfirmware+signed

なお、$SECRETS_DIR/key.sec キーは厳重に保護してください。これが侵害されると、第三者があなたのファームウェアに署名できてしまいます。

時間管理

組み込みプログラムでは、タスクを遅延させることは最も一般的に行われる操作の 1 つです。イベントループでは、他の I/O が実行されない場合、ループの次の反復が呼び出される前にほかのタスクにも実行の機会が与えられるよう、遅延を挿入する必要があります。Embassy は、現在のタスクを指定した時間間隔だけ遅延させるための抽象化を提供します。

Embassy における時間管理のインターフェースは、embassy-time クレートによって提供されます。これらの型は、embassy-executor の内部タイマーキュー、またはカスタムのタイマーキュー実装とともに使用できます。

Timer

embassy_time::Timer 型は 2 つのタイミングメソッドを提供します。

Timer::at は、システムの起動時刻を基準として、指定した Instant で完了する Future を作成します。 Timer::after は、Future が作成された時点を基準として、指定した Duration の後に完了する Future を作成します。

遅延の例を次に示します。

Tip
この例を実行するために必要な依存関係は こちら にあります。
use embassy_executor::task;
use embassy_time::{Duration, Timer};

#[task]
/// 定期的にティックするタスク
async fn tick_periodic() -> ! {
    loop {
        rprintln!("tick!");
        // 非同期スリーププリミティブ。タスクを 500ms の間停止します。
        Timer::after(Duration::from_millis(500)).await;
    }
}

Delay

embassy_time::Delay 型は、embedded-halembedded-hal-async トレイトの実装を提供します。これは、汎用的な遅延実装が提供されることを想定しているドライバーで使用できます。

これをどのように使用できるかの例を次に示します。

Tip
この例を実行するために必要な依存関係は こちら にあります。
use embassy_executor::task;
use embassy_time::Delay;

#[task]
/// 定期的にティックするタスク
async fn tick_periodic() -> ! {
    loop {
        rprintln!("tick!");
        // 非同期スリーププリミティブ。タスクを 500ms の間停止します。
        generic_delay(Delay).await
    }
}

async fn generic_delay<D: embedded_hal_async::delay::DelayNs>(delay: D) {
      delay.delay_ms(500).await;
}

ハードウェア抽象化レイヤー (HAL)

Embassy は、いくつかのマイクロコントローラファミリー向けに HAL を提供しています:

  • Nordic Semiconductor の nRF マイクロコントローラ向けの embassy-nrf

  • ST Microelectronics の STM32 マイクロコントローラ向けの embassy-stm32

  • Raspberry Pi の RP2040 および RP235x マイクロコントローラ向けの embassy-rp

これらの HAL は、ほとんどのペリフェラルに対して async/await 機能を実装すると同時に、embedded-hal および embedded-hal-async の async トレイトも実装しています。これらの HAL は、別のエグゼキューターと組み合わせて使用することもできます。

ESP32 シリーズについては、使用できる esp-hal があります。

WCH の 32 ビット RISC-V シリーズについては、使用できる ch32-hal があります。

Microchip PolarFire SoC については、mpfs-hal があります。

Puya Semiconductor の PY32 シリーズについては、py32-hal があります。

Embassy iMXRT HAL

Embassy iMXRT HAL は、以下のPAC(Peripheral Access Crate)に基づいています。

ペリフェラル

現在、以下のペリフェラルに対するHAL実装があります

  • CRC

  • DMA

  • GPIO

  • RNG

  • UART

Embassy MCX-A HAL

Embassy MCX-A HAL は、次の PAC(Peripheral Access Crate)に基づいています。

ペリフェラル

現時点で、以下のペリフェラルに HAL 実装があります。

  • クロック

  • GPIO

  • ADC

  • CLKOUT

  • I2C

  • I3C

  • LPUart

  • OSTimer

  • RTC

  • SPI コントローラー

  • TRNG

  • WWDT

  • CDOG

  • CTIMER

  • CRC

  • DMA

Embassy nRF HAL

Embassy nRF HAL は、nrf-rs の PAC(Peripheral Access Crate)に基づいています。

タイマードライバー

nRF タイマードライバーは、デフォルトで 32768 Hz で動作します。

ペリフェラル

現時点では、以下のペリフェラルに HAL 実装があります

  • PWM

  • SPIM

  • QSPI

  • NVMC

  • GPIOTE

  • RNG

  • TIMER

  • WDT

  • TEMP

  • PPI

  • UARTE

  • TWIM

  • SAADC

Bluetooth

Bluetooth には、nrf-softdevice クレートを使用できます。

Embassy STM32 HAL

Embassy STM32 HAL は、stm32-metapac プロジェクトに基づいています。

無限のバリアント問題

STM32 マイクロコントローラーには多くのファミリーやバリエーションがあり、それらすべてをサポートするのは大きな取り組みです。Embassy は、STM32 のペリフェラルのバージョンがチップファミリー間で共有されているという事実を活用しています。STM32 チップファミリーごとに SPI ペリフェラルを再実装する代わりに、embassy では単一の SPI 実装を用いており、これはコード生成されたレジスター型に依存しています。これらのレジスター型は、特定のペリフェラルのバージョンが同じ STM32 ファミリー間で共通です。

metapac

stm32-metapac モジュールは、STM32 チップファミリー向けに事前生成されたチップ定義とレジスター定義を使用して、レジスター型を生成します。これは Cargo の feature flag に基づいてコンパイル時に行われます。

チップ定義とレジスター定義は、別モジュールである stm32-data に配置されており、定義内でバグが見つかった場合や、新しいチップファミリーのサポートを追加する場合に更新されます。

HAL

embassy-stm32 モジュールには、すべての STM32 ファミリー向けの HAL 実装が含まれています。この実装では、自動的に導出された feature flag を使用して、特定のチップファミリーに対して特定のペリフェラルの正しいバージョンをサポートします。

タイマードライバー

STM32 タイマードライバーは、デフォルトで 32768 Hz で動作します。

タスク間で周辺機器を共有する

多くの場合、複数のタスクが同じリソース(ピン、通信インターフェースなど)にアクセスする必要があります。Embassy は、embassy-sync クレートで多くの異なる同期プリミティブを提供しています。

以下の例では、2 つのタスクが Raspberry Pi Pico ボード上のオンボード LED を同時に使用するさまざまな方法を示します。

Mutex を使った共有

相互排他を使用するのは、周辺機器を共有する最も単純な方法です。

Tip
この例を実行するために必要な依存関係は、こちらにあります
use defmt::*;
use embassy_executor::Spawner;
use embassy_rp::gpio;
use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex;
use embassy_sync::mutex::Mutex;
use embassy_time::{Duration, Ticker};
use gpio::{AnyPin, Level, Output};
use {defmt_rtt as _, panic_probe as _};

type LedType = Mutex<ThreadModeRawMutex, Option<Output<'static, AnyPin>>>;
static LED: LedType = Mutex::new(None);

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    let p = embassy_rp::init(Default::default());
    // グローバルな LED 参照の内容を実際の LED ピンに設定する
    let led = Output::new(AnyPin::from(p.PIN_25), Level::High);
    // ミューテックスに書き込まれると MutexGuard がドロップされ、それによって
    // Mutex が解放されるように、内側のスコープを使っている
    {
        *(LED.lock().await) = Some(led);
    }
    let dt = 100 * 1_000_000;
    let k = 1.003;

    unwrap!(spawner.spawn(toggle_led(&LED, Duration::from_nanos(dt))));
    unwrap!(spawner.spawn(toggle_led(&LED, Duration::from_nanos((dt as f64 * k) as u64))));
}

// プールサイズが 2 であることは、このタスクのインスタンスを 2 つ spawn できることを意味する。
#[embassy_executor::task(pool_size = 2)]
async fn toggle_led(led: &'static LedType, delay: Duration) {
    let mut ticker = Ticker::every(delay);
    loop {
        {
            let mut led_unlocked = led.lock().await;
            if let Some(pin_ref) = led_unlocked.as_mut() {
                pin_ref.toggle();
            }
        }
        ticker.next().await;
    }
}

リソースへのアクセスを実現するのが、定義された LedType です。

なぜこれほど複雑なのか

層を 1 つずつ見ていくと、それぞれが必要な理由がわかります。

Mutex<RawMutexType, T>

ミューテックスがあるのは、あるタスクが先にリソースを取得して変更を始めた場合、書き込みを行いたいほかのすべてのタスクは待たなければならないためです(どのタスクもミューテックスをロックしていなければ led.lock().await は直ちに返り、ほかの場所でアクセスされていればブロックします)。

Option<T>

LED 変数は、タスクが受け取る参照が 'static である必要があるため、main タスクの外で定義する必要があります。しかし、main タスクの外にある場合、ピン自体がまだ初期化されていないため、どのピンを指すようにも初期化できません。そのため、None に設定されています。

Output<AnyPin>

ピンが Output に設定されることを示すためです。AnyPinembassy_rp::peripherals::PIN_25 にすることもできましたが、この方法により toggle_led 関数をより汎用的にできます。

Channel を使った共有

チャネルは、リソースへの排他的アクセスを確保するもう 1 つの方法です。チャネルの使用は、アクセスを後の時点で行えるケースに適しており、操作をキューに入れてほかのことを行えます。

Tip
この例を実行するために必要な依存関係は、こちらにあります
use defmt::*;
use embassy_executor::Spawner;
use embassy_rp::gpio;
use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex;
use embassy_sync::channel::{Channel, Sender};
use embassy_time::{Duration, Ticker};
use gpio::{AnyPin, Level, Output};
use {defmt_rtt as _, panic_probe as _};

enum LedState {
     Toggle,
}
static CHANNEL: Channel<ThreadModeRawMutex, LedState, 64> = Channel::new();

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

    let dt = 100 * 1_000_000;
    let k = 1.003;

    unwrap!(spawner.spawn(toggle_led(CHANNEL.sender(), Duration::from_nanos(dt))));
    unwrap!(spawner.spawn(toggle_led(CHANNEL.sender(), Duration::from_nanos((dt as f64 * k) as u64))));

    loop {
        match CHANNEL.receive().await {
            LedState::Toggle => led.toggle(),
        }
    }
}

// プールサイズが 2 であることは、このタスクのインスタンスを 2 つ spawn できることを意味する。
#[embassy_executor::task(pool_size = 2)]
async fn toggle_led(control: Sender<'static, ThreadModeRawMutex, LedState, 64>, delay: Duration) {
    let mut ticker = Ticker::every(delay);
    loop {
        control.send(LedState::Toggle).await;
        ticker.next().await;
    }
}

この例では、Mutex を Channel に置き換え、別のタスク(メインループ)を使って LED を制御しています。このアプローチの利点は、周辺機器を参照するのが 1 つのタスクだけになり、関心事を分離できることです。しかし、Mutex を使う方がオーバーヘッドは低く、タスク内でほかの作業を続ける前に、操作が完了したことを確実にする必要がある 場合には、こちらが必要になることがあります。

共有のさらに多くの方法を紹介する例は、こちらにあります

複数のデバイス間で I2C または SPI バスを共有する

共通の I2C または SPI バスを共有する複数のデバイスをどのように扱うかの例は、こちらにあります

よくある質問

これは、よくある質問と回答を順不同でまとめたものです。

特にチャットで誰かがあなたの質問に答えてくれた場合は、ぜひ このページ に項目を追加してください!

デバッグプローブなしで RP2040 または RP235x にデプロイする方法

バイナリをアップロードするために Picotool をインストールしてください。

このツールを使用するように runner を設定するには、.cargo/config.toml に次を追加してください:

[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "picotool load --update --verify --execute -t elf"

Picotool はデバイスを検出してバイナリをアップロードし、同一のフラッシュセクタをスキップし(コマンドラインフラグ --update)、バイナリが正しく書き込まれたことを検証し(--verify)、その後新しいコードを実行します(--execute)。詳細は picotool help load を実行してください。

main マクロが見つからない

次のようなエラーが表示される場合:

#[embassy_executor::main]
|                   ^^^^ could not find `main` in `embassy_executor`

おそらく embassy-executor クレートの一部機能が不足しています。

Cortex-M ターゲットでは、embassy-executor クレートについて Cargo.toml で次の機能がすべて有効になっているか確認してください:

  • arch-cortex-m

  • executor-thread

ESP32 では、esp-hal が提供する executor と #[main] マクロの使用を検討してください。

なぜバイナリがこんなに大きいのですか?

バイナリサイズを管理する第一歩は、プロファイル を設定することです。

[profile.release]
lto = true
opt-level = "s"
incremental = false
codegen-units = 1
# 注: debug = true でも問題ありません - デバッグ情報はデバイスに書き込まれません!
debug = true

これらのフラグについては、上でリンクした Rust Book のページで詳しく説明されています。

バイナリがまだ大きいです…​ std::fmt 関連のもので埋め尽くされています!

これは、panic-haltpanic-reset を使用していても、コードが十分に複雑であるため、panic! 呼び出しに必要なフォーマット処理を最適化で取り除けなかったことを意味します。

これに対処するには、.cargo/config.toml に次を追加してください:

[unstable]
build-std = ["core"]
build-std-features = ["panic_immediate_abort"]

これにより、すべての panic が UDF(未定義)命令に置き換えられます。

チップセットによって、この挙動は異なります。

使用しているチップセットの仕様を参照してください。ただし thumbv6m では、これはハードフォールトになります。これは次のように設定できます:

#[exception]
unsafe fn HardFault(_frame: &ExceptionFrame) -> ! {
    SCB::sys_reset() // <- reset 以外のこともできます
}

詳細は cortex-m の 例外処理 を参照してください。

embassy-time でリンカーエラーが発生する

次のようなリンカーエラーが表示される場合:

  = note: rust-lld: error: undefined symbol: _embassy_time_now
          >>> referenced by driver.rs:127 (src/driver.rs:127)
          >>>               embassy_time-846f66f1620ad42c.embassy_time.4f6a638abb75dd4c-cgu.0.rcgu.o:(embassy_time::driver::now::hefb1f99d6e069842) in archive Devel/Embedded/pogodyna/target/thumbv7em-none-eabihf/debug/deps/libembassy_time-846f66f1620ad42c.rlib

          rust-lld: error: undefined symbol: _embassy_time_schedule_wake
          >>> referenced by driver.rs:144 (src/driver.rs:144)
          >>>               embassy_time-846f66f1620ad42c.embassy_time.4f6a638abb75dd4c-cgu.0.rcgu.o:(embassy_time::driver::schedule_wake::h530a5b1f444a6d5b) in archive Devel/Embedded/pogodyna/target/thumbv7em-none-eabihf/debug/deps/libembassy_time-846f66f1620ad42c.rlib

おそらく、HAL 用の time driver を有効にする必要があります(embassy-time ではありません!)。たとえば embassy-stm32 では、time-driver-any を有効にする必要があるかもしれません:

[dependencies.embassy-stm32]
version = "0.1.0"
features = [
    # ...
    "time-driver-any", # この行を追加してください!
    # ...
]

プロジェクト初期設定の段階で、まだ HAL の何も使用していない場合は、リンカーによってデッドコードとして削除されないよう、ソースにこの行を追加して HAL を明示的に使用してください:

use embassy_stm32 as _;

よくある別のエラーとして、次のものがあります:

 = note: rust-lld: error: undefined symbol: __pender
          >>> referenced by mod.rs:373 (src/raw/mod.rs:373)
          >>>               embassy_executor-e78174e249bca7f4.embassy_executor.1e9d60fc90940543-cgu.0.rcgu.o:(embassy_executor::raw::Pender::pend::h0f19b6e01762e4cd) in archive [...]libembassy_executor-e78174e249bca7f4.rlib

このエラーには 2 つの原因が考えられます:

  • アーキテクチャ固有の機能のいずれかを有効にしないまま embassy-executor を使用している一方で、独自の executor を提供しない HAL を使っています。たとえば Cortex-M(RP2040 など)では、embassy-executorarch-cortex-m 機能を有効にする必要があります。

  • embassy-executor を使用していません。この場合、embassy-timegeneric-queue-X 機能のいずれか 1 つを有効にする必要があります。

依存ツリー内に同じクレートの複数バージョンがあります。これは、あなたの embassy クレートの一部が crates.io から来ており、一部が git から来ていて、それぞれが異なる 依存関係のセットを引き込んでいることを意味します。

この問題を解決するには、すべての embassy クレートで単一のソースのみを使用するようにしてください! そのためには、[patch.crates.io] を使って依存関係が git ソースを使うようにパッチを当て、 必要に応じて [patch.'https://github.com/embassy-rs/embassy.git'] も使用してください。

例:

[patch.crates-io]
embassy-time-queue-utils = { git = "https://github.com/embassy-rs/embassy.git", rev = "7f8af8a" }
embassy-time-driver = { git = "https://github.com/embassy-rs/embassy.git", rev = "7f8af8a" }
# embassy-time = { git = "https://github.com/embassy-rs/embassy.git", rev = "7f8af8a" }

git の revision は、使用している他の embassy パッチや git 依存関係と一致している必要がある点に注意してください!

embassy-stm32 プログラムの速度を最適化するにはどうすればよいですか?

  • RCC が可能な限り高速に動作するよう設定されていることを確認する

  • フラッシュキャッシュ が有効になっていることを確認する

  • --release でビルドする

  • Cargo.toml の release プロファイルに次のキーを設定する:

    • opt-level = "s"

    • lto = "fat"

  • .cargo/config.toml[unstable] セクションに次のキーを設定する

    • build-std = ["core"]

    • build-std-features = ["panic_immediate_abort"]

  • InterruptExecutor を使用する場合:

    • executor-thread を無効にする

    • main で全てを spawn し、その後 SCB.SLEEPONEXIT を有効にして loop { cortex_m::asm::wfi() } を実行する

    • 注: 2 つの優先度レベルが必要な場合は、1 つの thread executor + 1 つの interrupt executor よりも、2 つの interrupt executor を使う方が適しています。 == 手動 ISR を Embassy と併用できますか?

はい! これは、イベントにできるだけ速く応答する必要があり、通常の「ISR、ウェイク、ISR からの復帰、起こされたタスクへのコンテキストスイッチ」というフローによって生じるレイテンシが、アプリケーションにとって大きすぎる場合に有用です。

単純に、#[interrupt] fn INTERRUPT_NAME() {} ハンドラーを、ほかの組み込み Rust プロジェクトで行うのと同じように定義できます。

あるいは、embassy-[family]::interrupt::typelevel::Handler トレイトを実装する構造体を on_interrupt() メソッド付きで定義し、bind_interrupts! マクロを介してそれを割り込みベクターに束縛することもできます。これにより追加されるのは 1 回の間接参照だけです。これにより、手動 ISR と Embassy のドライバー定義 ISR を混在させられます。ハンドラーは、マクロ内に記述された順に直接呼び出されます。

リソース使用量(CPU、RAM など)はどのように測定できますか?

CPU 使用率について

文書化されている手法はいくつかありますが、一般にはアイドルループまたは低優先度ループでどれだけの時間を費やしているかを測定します。

embassy でこれを具体的にどう行うかについては文書化が必要ですが、この古い記事で一般的な手順が説明されています。

もし実際にこれを行ったなら、このセクションをより具体的な例で更新してください!

静的メモリ使用量について

cargo-binutilscargo sizecargo nm のようなツールを使うと、グローバル変数やその他の静的使用量のサイズが分かります。特に、.data.bss セクションのサイズを確認するとよいでしょう。これらを合わせたものが、グローバル/静的メモリ使用量の合計です。

最大スタック使用量について

最悪ケースのスタック使用量を静的に計算するには、cargo-call-stack を確認してください。これにはいくつかの注意点や不正確さがあり得ますが、大まかな見当をつけるにはよい方法です。詳細については、README を参照してください。

自分の STM チップ用のメモリ定義が間違っているようです。memory.x ファイルはどう定義すればよいですか?

プロジェクトがコンパイルでき、フラッシュもできるのに実行に失敗することがあります。次の状況があなたの環境に当てはまる可能性があります:

memory.x は、Cargo.toml ファイル内で embassy-stm32 クレートの memory-x feature を有効にすると自動的に生成されます。 そしてこれは、stm32-metapac を使って memory.x ファイルを生成します。残念ながら、このメモリ定義は正しくないことが少なくありません。

独自の memory.x ファイルを追加することで、これを上書きできます。そのようなファイルは次のようになります:

MEMORY
{
  FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
  RAM (xrw)  : ORIGIN = 0x20000000, LENGTH = 320K
}

_stack_start = ORIGIN(RAM) + LENGTH(RAM);

ボードと環境に適した具体的な値については、STM32 のドキュメントを参照してください。STM32 Cube のサンプルには、リンカスクリプト .ld ファイルが含まれていることがよくあります。 MEMORY セクションを探し、FLASH と RAM のサイズおよび開始位置を特定してみてください。

memory.x が間違っているケースを見つけたら、ほかのユーザーが不意を突かれないように、この Github issue で報告してください。

自分のボードで USB のサンプルが動作しません。ほかに設定が必要なことはありますか?

USB のサンプルを試していてデバイスが接続されない場合、最も一般的な問題を以下に示します。

RCC 設定が正しくない

ボードと水晶振動子/発振器を確認してください。特に、HSE が正しい値に設定されていることを確認してください。たとえば、ボードが実際に 8 MHz の発振器で動作しているのであれば 8_000_000 ヘルツです。

STM32 プラットフォームでの VBUS 検出

USB 仕様では、すべての USB デバイスが、接続/切断動作を検出するためにバスを監視することが求められています。デバイスは、ホストが VBUS を供給したら直ちに D+ または D- ラインをプルアップしなければなりません。

vbus_detection を有効/無効にする方法については、たとえば usb/struct.Config.html にあるドキュメントを参照してください。

デバイスが、データ接続も兼ねる USB バスからのみ給電される場合、これは任意です。(VBUS に電力がなければ、どのみちデバイスの電源は切れているため、VBUS には常に電力がある、すなわち USB ケーブルは常に接続されていると仮定しても安全です。)デバイスに VBUS センシングを可能にするための必要な配線がない場合(下記参照)、動作させるにはこのオプションを false に設定する必要があります。

デバイスが別の電源から給電されており、そのため USB ケーブルの接続/切断イベントをまたいでも通電状態を保てる場合は、これは実装されていなければならず、vbus_detection は必ず true に設定しなければなりません。

ボードが USB から給電されていて、vbus_detection をサポートしているかどうか不明な場合は、ボードの回路図を確認して、USB Full Speed では VBUS が PA9 に、USB High Speed では PB13 に、場合によっては分圧回路を介して接続されているかを確認してください。独自のハードウェアを設計する場合は、詳細について ST のアプリケーションノート AN4879(特に 2.6 節)と、使用しているチップ固有のリファレンスマニュアルを参照してください。

既知の問題(詳細および/または回避策)

これらはよく報告される問題です。これらの修正や、可能であれば UX の改善に協力してくれる方を募集しています!

STM32H5 および STM32H7 の電源の問題

内蔵電源管理(SMPS および LDO)の設定を持つ STM32 チップでは、設定がボードの設計と一致していないと、しばしばユーザーに問題を引き起こします。

サンプルの設定や、実際に動作している他のボードの設定でさえ、配線が異なるため、あなたのボードでは動作しないことがあります。

さらに、一部の PWR 設定では完全なデバイス再起動(および電源コンデンサを放電するのに十分な時間!)が必要になるため、トラブルシュートが困難になります。また、一部の 「誤った」電源設定はほとんど動作してしまうため、起動によっては動作したり、しばらくの間は動作したりするものの、予期せずクラッシュします。

これはボード/ハードウェア依存であるため、現時点では修正はありません。詳細については、このトラッキング issue を参照してください

STM32 BDMA は一部の RAM 領域でしか動作しない

一部の STM32H7 チップに含まれる STM32 BDMA コントローラーは、特定の RAM 領域のみを使用するように設定する必要があり、 そうしないと転送は失敗します。

次のようなエラーが表示される場合:

DMA: error on BDMA@1234ABCD channel 4

このための特別な領域を定義するようにリンカスクリプトを設定し、BDMA で使用する前にデータをその領域へコピーする必要があります。

一般的な手順:

  1. BDMA がアクセスできるメモリ領域を特定します。この情報は、STM32 データシートのバスマトリクスとメモリマッピング表から得られます。

  2. メモリ領域を memory.x に追加します。生成済みのものは https://github.com/embassy-rs/stm32-data-generated/tree/main/data/chips から取得して変更できます。

  3. 変更した memory.x を cargo が認識するように、build.rs を変更する必要があるかもしれません。

  4. コード内では、#[unsafe(link_section = ".xxx")] を使って定義したメモリ領域にアクセスします。

  5. BDMA を使用する前に、その領域へデータをコピーします。

詳細については、SMT32H7 SPI BDMA サンプル を参照してください。 == main ブランチに切り替えるにはどうすればよいですか?

新しい変更や修正をテストするために、プロジェクトが GitHub 上のバージョンを使うよう切り替えたくなることがあります。

次のようなセクションを Cargo.toml ファイルに追加できます。この場合、すべての embassy クレートを同じリビジョンにパッチする必要があります。

patch を使うと、直接依存関係と間接依存関係の両方がすべて置き換えられます。

この方法の詳細については、新規プロジェクトのドキュメントを参照してください。

[patch.crates-io]
# GitHub から最新の git rev を取得するようにしてください。最新のものはここで確認できます:
# https://github.com/embassy-rs/embassy/commits/main/
embassy-embedded-hal = { git = "https://github.com/embassy-rs/embassy",     rev = "4cade64ebd34bf93458f17cfe85c5f710d0ff13c" }
embassy-executor     = { git = "https://github.com/embassy-rs/embassy",     rev = "4cade64ebd34bf93458f17cfe85c5f710d0ff13c" }
embassy-rp           = { git = "https://github.com/embassy-rs/embassy",     rev = "4cade64ebd34bf93458f17cfe85c5f710d0ff13c" }
embassy-sync         = { git = "https://github.com/embassy-rs/embassy",     rev = "4cade64ebd34bf93458f17cfe85c5f710d0ff13c" }
embassy-time         = { git = "https://github.com/embassy-rs/embassy",     rev = "4cade64ebd34bf93458f17cfe85c5f710d0ff13c" }
embassy-usb          = { git = "https://github.com/embassy-rs/embassy",     rev = "4cade64ebd34bf93458f17cfe85c5f710d0ff13c" }
embassy-usb-driver   = { git = "https://github.com/embassy-rs/embassy",     rev = "4cade64ebd34bf93458f17cfe85c5f710d0ff13c" }

新しいマイクロコントローラのサポートを embassy に追加するにはどうすればよいですか?

これは特に cortex-m と、場合によっては risc-v を対象にしており、これらでは割り込み処理のような基本機能、あるいはアーキテクチャ向けの embassy-executor サポートさえすでに存在していることがあります。

これは、すでにサポートされているチップで Embassy を使うよりも はるかに難しい道 です。初心者であれば、まずは既存の十分にサポートされたチップでしばらく embassy を使ってから、ドライバをゼロから書くことを検討してください。また、サポートされている Embassy HAL の既存ソースを読んで、さまざまなチップ向けにドライバがどのように実装されているか感覚をつかむのも有益です。すでに unsafe コードの読み書きに慣れており、HAL の利用者に対して安全な抽象化を書く責務を理解している必要があります。

これが唯一の方法というわけではありませんが、どこから始めればよいかを探しているなら、これは妥当な進め方です。

  1. まず Matrix ルームに立ち寄るか検索して、誰かがすでに Embassy あるいは Rust の別の場所でドライバを書き始めていないか確認してください。ゼロから始めなくてよいかもしれません。

  2. ターゲットが probe-rs でサポートされていることを確認してください。おそらくサポートされていますし、もしされていなくても、cmsis-pack を使ってサポートを追加できる可能性があります。そうすれば書き込みやデバッグが可能になります。ドライバを書くときには、SWD や JTAG でデバッグできることのありがたみを間違いなく実感するでしょう。

  3. 利用可能な SVD(ファミリの場合は複数の SVD)があるか確認し、あるなら chiptool に通して、低レベルのレジスタアクセス用 PAC を生成してください。ない場合でも、PDF データシートや既存の C ヘッダーファイルをスクレイピングするなど別の方法はありますが、ドライバ作成に必要なペリフェラルのメモリ位置を定義するには、SVD ファイルから始めるより手間がかかります。

  4. embassy repo を fork してそこにターゲットを追加するか、PAC と空の HAL だけを含む repo を作成してください。最初の段階では、必ずしも embassy repo に置く必要はありません。

  5. 最小限の HAL あるいは PAC アクセスだけを使ってでも、そのチップ上で hello world バイナリを動かしてください。delay を使って LED を点滅させたり、何らかのインターフェイスで生データを送ったりして、正しく動作すること、書き込みできること、defmt + RTT でデバッグできること、適切なリンカスクリプトを書けることなどを確認してください。

  6. 基本的なタイマー操作とタイマー割り込みを動かし、点滅アプリケーションをハードウェアタイマーと割り込みを使う形に発展させ、それらが正確であることを確認してください(可能ならロジックアナライザやオシロスコープを使ってください)。

  7. タイマーとタイマー割り込みのコードを使って embassy-time の driver API を実装し、ドライバやアプリケーションで embassy-time の操作を使えるようにしてください。

  8. その後、GPIO、UART、SPI、I2C など、必要なペリフェラルの実装を始めてください。ここが作業の大部分であり、おそらくしばらく続くでしょう。最初からすべてのペリフェラルを 100% カバーする必要があると考えないでください。これは時間をかけて進む継続的なプロセスになるはずです。

  9. 機能が増えてきたら、HAL ドライバの上に embedded-hal、embedded-io、embedded-hal-async のトレイトを実装し始めてください。これにより、利用者は標準的な外部デバイスドライバ(たとえばセンサー、アクチュエータ、ディスプレイなど)をあなたの HAL と組み合わせて使えるようになります。

  10. Embassy サポートとして PAC/HAL を upstream することを議論するか、あるいは people が見つけられるように、ドライバが awesome-embedded-rust list に追加されるようにしてください。

複数の Tasks と、複数の futures を持つ 1 つの task のどちらですか?

いくつかの例では、main の最後が次のようになっています。

// すべてを並行して実行する。
// 上ですべてを `'static` にしていたなら、代わりに別々のタスクを使ってこれを行うこともできた。
join(usb_fut, join(echo_fut, log_fut)).await;

Embassy で並行性を扱う主な方法は 2 つあります。

  1. #[embassy_executor::task] などを使って複数のタスクを spawn する

  2. join()select() を使って、1 つのタスクの中で複数の future を管理する(上の例のように)

一般に、これらの方法はどちらでも機能します。主な違いは次のとおりです。

別々のタスク を使う場合、各タスクには独自の RAM 割り当てが必要になるため、タスクごとに少しオーバーヘッドがあります。そのため、3 つのことを行う 1 つのタスクは、1 つのことを行う 3 つのタスクよりも、おそらくわずかに小さくなります(差は大きくなく、おそらく数十バイト程度です)。一方、1 つのタスク内の複数の future では、複数のタスク割り当ては不要で、一般的には 1 つのタスクの中でデータを共有したり、borrowed resource を使ったりしやすくなります。 タスク間で物を共有するためのいくつかの方法を示す例は、こちらにあります。

しかし、たとえばデータ転送が完了したときやボタンが押されたときのように、タスクを「ウェイク」することに関しては、専用タスクをウェイクする方が高速です。なぜなら、そのタスクはどの future が実際に準備完了しているかを確認する必要がないからです。joinselect は、管理しているすべての future を確認し、どれ(あるいはどれら)がさらに処理を進められる状態にあるのかを見なければなりません。これは、すべての Rust executor(Embassy や Tokio など)が、特定の future ではなくタスクしかウェイクできないためです。つまり、専用タスクを使う方が、future をやりくりするための CPU 時間はわずかに少なくなります。

実際のところ、どちらの方法でも差はそれほど大きくありません。なので、まずは自分とコードにとって扱いやすい方を選べばよいですが、場合によって少しずつ異なる細部はあります。

タスク間でペリフェラルリソースを分割する

タスク間でリソースを分割する方法は 2 つあり、手動で割り当てる方法と、便利なマクロを使う方法があります。この例を参照してください。

コードやドライバが debug mode では動作するのに、release mode では動作しないのはなぜですか(または LTO 有効時)

ドライバーの実装中にこのような問題が起きる場合、たいていは次のような一般的な原因のいずれかに当てはまります。以下は、確認すべきよくあるエラーの一覧として役立ちます:

  1. 何らかのレースコンディション - コードが速くなったことで、割り込みなどを取りこぼしている

  2. 何らかの UB。unsafe code を使っている場合や、fence が欠けている DMA のような問題

  3. 何らかのハードウェアのエラッタ、またはクロック速度が誤っているなどのハードウェア設定ミス

  4. 何らかの割り込みハンドラの問題。必要なときの割り込みの有効化、無効化、または再有効化に関するもの

  5. 何らかの async の問題。たとえば、フラグを確認する前に waker を完全に登録していない、または適切なタイミングで waker を登録またはペンディングにしていない

thread-mode executor がスリープ状態に入らないようにするには?

場合によっては、thread-mode executor がスリープ状態に入るのを防ぎたいことがあります。たとえば、それによってアナログ性能を低下させる電流スパイクが発生する場合です。 回避策として、ループ内で yield するタスクを spawn することで、executor がスリープ状態に入るのを防げます。なお、これにより消費電力が増える可能性があります。

#[embassy_executor::task]
async fn idle() {
    loop { embassy_futures::yield_now().await; }
}

ブートローダーが再起動ループに入るのはなぜですか?

ブートローダーの再起動ループのトラブルシューティング

ブートローダーが再起動ループに入る場合、原因は複数考えられます。以下の点を確認してください:

memory.x ファイルを検証する

ブートローダーは、memory.x で定義されたアドレスを使ってパーティションを作成する際に、重要なチェックを行います。次のアサーションが成り立つことを確認してください:

const {
    core::assert!(Self::PAGE_SIZE % ACTIVE::WRITE_SIZE as u32 == 0);
    core::assert!(Self::PAGE_SIZE % ACTIVE::ERASE_SIZE as u32 == 0);
    core::assert!(Self::PAGE_SIZE % DFU::WRITE_SIZE as u32 == 0);
    core::assert!(Self::PAGE_SIZE % DFU::ERASE_SIZE as u32 == 0);
}

// コピーの進捗を保存するために十分な進捗ページを確保する
assert_eq!(0, Self::PAGE_SIZE % aligned_buf.len() as u32);
assert!(aligned_buf.len() >= STATE::WRITE_SIZE);
assert_eq!(0, aligned_buf.len() % ACTIVE::WRITE_SIZE);
assert_eq!(0, aligned_buf.len() % DFU::WRITE_SIZE);

これらのアサーションのいずれかが失敗すると、ブートローダーは再起動ループに入る可能性が高いです。この失敗ではメッセージが一切ログに出ないことがあります(たとえば defmt を使用している場合)。memory.x ファイルとフラッシュメモリの設定が、これらの要件に合致していることを確認してください。

panic ログの扱い

panic 時にはメッセージがログ出力されることがありますが、一部のマイクロコントローラでは、メッセージが完全に出力される前にリセットされてしまいます。panic メッセージを確実にログ出力するには、リセットの前に no-operation(NOP)命令を使った遅延を追加してください:

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    for _ in 0..10_000_000 {
        cortex_m::asm::nop();
    }
    cortex_m::asm::udf();
}

ウォッチドッグをフィードする

一部の embassy-boot 実装(embassy-boot-nrfembassy-boot-rp など)は、アプリケーションの障害を検出するためにウォッチドッグタイマに依存しています。アプリケーションコードがウォッチドッグタイマを適切にフィードしないと、ブートローダーは再起動します。正しくフィードするようにしてください。