ESP32でstdなRust開発入門 その2 Lチカ

概要

前回は環境構築だけで終わってしまったので、今回はRust環境の説明とLチカを行っていきたいと思います。

Rustと組み込みRust環境の違い(toolchains)

$ rustup show
Default host: x86_64-unknown-linux-gnu
rustup home:  /home/mt/.rustup

installed toolchains
--------------------

stable-x86_64-unknown-linux-gnu (default)
nightly-x86_64-unknown-linux-gnu
esp

installed targets for active toolchain
--------------------------------------

x86_64-pc-windows-gnu
x86_64-unknown-linux-gnu

active toolchain
----------------

stable-x86_64-unknown-linux-gnu (default)
rustc 1.66.0 (69f9c33d7 2022-12-12)

rustupで導入済みのRustを表示してみました。3つの環境が導入されています。

stable

通常のRust環境です。一般的な開発はこちらを使いますが、一般的な環境でしか利用することができません。3週間間隔でリリースされるはずで、たまに更新してあげましょう。

組み込み開発の場合にはツールまわりではこちらの環境を利用しています。

nightly

かなり頻繁に更新されている開発中のRust環境です。名前的には毎晩ですが、実際のリリースはそこまで毎晩ではないはずです。こちらは環境設定を外部から指定することができるので組み込みなどのRISC-V環境などはこちらのRust環境を利用します。

ESP32で利用しているXtensaはこちらのツールにマージする作業中ですが、現状はまだ利用することができません。RISC-VのESP32-C3はこちらでビルドが可能はなずです。

esp

ESP32、ESP32-S2、ESP32-S3で利用しているXtensaに対応したRust環境です。ESP32の開発元であるEspressif Systemsが保守しています。どこかのタイミングで廃止され、nightlyでビルドできるようになるかもしれません。

ビルド環境の指定について(rust-toolchain.toml)

ビルド時にどの環境を利用するかを指定することが可能です。

コマンドラインでの指定

cargo +esp build

例えば、上記でesp環境でビルドされますが、この指定方法はあまり使われません。そして無指定のときのデフォルト環境を変更することも可能ですが、stable以外をデフォルトにするのはあまりおすすめしません。

プロジェクトでの指定

rust-toolchain.tomlにどの環境を利用するのかを指定することが可能です。このファイルはcargo generateでプロジェクトを作成した場合には自動的に追加されているはずです。

[toolchain]
channel = "esp"

上記の指定でツールチェインがespに指定されています。このように複数のRust環境がある場合にもこのように適切なツールチェインでビルドすることが可能になっています。

ビルドの仕組み(Cargo.toml)

Cargo.tomlにてプロジェクトのビルドについての重要な設定がされています。

[package]
name = "test-esp32"
version = "0.1.0"
authors = ["mt"]
edition = "2021"
resolver = "2"

[profile.release]
opt-level = "s"

[profile.dev]
debug = true # Symbols are nice and they don't increase the size on Flash
opt-level = "z"

[features]
pio = ["esp-idf-sys/pio"]

[dependencies]
esp-idf-sys = { version = "0.31.11", features = ["binstart"] }


[build-dependencies]
embuild = "0.30.4"
anyhow = "1"

package

プロジェクトの名前やバージョンなどです。ここはみたままの感じで最初は変更する必要がありません。

profile.release

リリースビルドをしたときの設定です。最適化レベルをあげています。通常は触りません。

profile.dev

デバッグビルドをしたときの設定です。実際にJTAGを利用したデバッグを実行しない場合でもdebugをtrueにしていると、ソースコードの行数などがわかるので便利なはずです。

features

上記に説明がありますが、pioはPlatformIOを利用してESP-IDFをビルドします。

dependencies

プロジェクトが依存するクレートを指定します。クレートはRustのライブラリのことです。利用したいライブラリを追加する場合にはここに追記していきます。

上記はVSCodeのcrate拡張機能を利用した場合ですが、バージョン番号が重要でなるべく最新を細かく指定したほうがよいと思います。テンプレートでは0.31.11を指定されていましたが、現在の最新は0.32.1でした。

binstartは上のfeaturesと同じところにありますが、ESP-IDFを利用したstdなRustの場合にはbinstartを指定するようです。

Cargo.toml を壊れたままにしない
Rust で Cargo.toml により依存パッケージのバージョン指定をしますが、これが壊れている場合が見受けられます。気付いて直しましょう。

バージョンの指定方法については上記のサイトがわかりやすいと思います。

build-dependencies

こちらはわかりにくのですが、ビルドで利用する設定になります。ビルド自体がRustのプログラムになっていますので、そこで依存するcrateを指定しています。

embuildは少し古いバージョン。anyhowは1とざっくりしたバージョン指定となっています。ビルド実行で利用されるcrateであり、最終作成物には影響を与えないのでざっくりしたバージョンみたいです。

ビルド用プログラム(build.rs)

build.rsがビルドで利用しているプログラムとなります。基本的に編集はしません。

// Necessary because of this issue: https://github.com/rust-lang/cargo/issues/9641
fn main() -> anyhow::Result<()> {
    embuild::build::CfgArgs::output_propagated("ESP_IDF")?;
    embuild::build::LinkArgs::output_propagated("ESP_IDF")
}

ここでanyhow::Resultとembuild::buildを利用しているため、build-dependenciesに依存を追加していたようです。

ビルド設定ファイル(.cargo/config.toml)

細かいビルド設定は.cargo/config.tomlで指定します。

build

[build]
# Uncomment the relevant target for your chip here (ESP32, ESP32-S2, ESP32-S3 or ESP32-C3)
target = "xtensa-esp32-espidf"
#target = "xtensa-esp32s2-espidf"
#target = "xtensa-esp32s3-espidf"
#target = "riscv32imc-esp-espidf"

ターゲットとなるMCUを指定します。通常はテンプレートからプロジェクトを作成する際に指定しているはずですので変更する必要はありません。

target.xtensa-esp32*-espidf

[target.xtensa-esp32-espidf]
linker = "ldproxy"
#runner = "espflash --monitor"
runner = "./wsltool.sh esp32 COM4 1500000"
#rustflags = ["--cfg", "espidf_time64"] # Extending time_t for ESP IDF 5: https://github.com/esp-rs/rust/issues/110

MCU別に設定ファイルがあります。上記はcargo runのときに実行されるrunnerを変更している例となります。

unstable

[unstable]

build-std = ["std", "panic_abort"]
#build-std-features = ["panic_immediate_abort"] # Required for older ESP-IDF versions without a realpath implementation

標準ライブラリであるstdを利用し、パニック時にはpanic_abortを利用する設定です。

パニック - The Embedded Rust Book

上記にパニック時の指定方法が書いてあります。開発中とリリース後ではパニック時に表示する内容などは変更する必要があるはずです。個人開発であればつねに多めの表示に変更したほうがよいかもしれません。

env

[env]
# Note: these variables are not used when using pio builder (`cargo build --features pio`)
# Builds against ESP-IDF stable (v4.4)
ESP_IDF_VERSION = "release/v4.4"
# Builds against ESP-IDF master (mainline)
#ESP_IDF_VERSION = "master"

ESP-IDFのバージョンを指定します。基本的にはテンプレートから指定されるので変更しないほうがよいと思います。現在5系が最新リリースですがRustでは4.4系までしか利用できません。

今後テンプレートが更新されて、5が選択可能になっている場合には5を指定しても大丈夫になります。ここはなるべく手で書き換えずに、既存プロジェクトを保存してテンプレートからプロジェクトを新規作成しなおしたほうが安全だと思います。

Hello, world!

やっとHello, world!の説明まできました!

use esp_idf_sys as _; // If using the `binstart` feature of `esp-idf-sys`, always keep this module imported

fn main() {
    // Temporary. Will disappear once ESP-IDF 4.4 is released, but for now it is necessary to call this function once,
    // or else some patches to the runtime implemented by esp-idf-sys might not link properly.
    esp_idf_sys::link_patches();

    println!("Hello, world!");
}

上記がテンプレートから作成された初期プログラムです。

ESP-IDF利用宣言

use esp_idf_sys as _;

コメントはけしていますが、ESP-IDFを利用するためのcrateであるesp_idf_sysの利用宣言をしています。このへんはPythonなどと同じ感じですね。

main関数

fn main() {
}

main関数を定義しています。戻り値と引数は未指定ですね。

パッチ

esp_idf_sys::link_patches();

そのうち必要なくなるはずですが、パッチを最初に一度実行します。こちらも4.4がリリースされたら消されると書いてありますが、消えていません。テンプレートから新規プロジェクト作成をして、この関数が残っていた場合には残してあげてください。

Hello, world!

println!("Hello, world!");

改行付きのテキスト表示命令ですね。最後の!はマクロであることを表しています。内部的にはもっと複雑な関数呼び出しになっているようです。

マクロ - The Rust Programming Language 日本語版

マクロは上記に目を通してみてください。このドキュメントは最終的にかるく目を通して、あとで確認できるようにしておく必要があります。

まとめ

こちらが最低限の処理でした。とくに特別なことはなかったので、普通のRustとそれほど変わらないと思います。

ループ版Hello, world!

Arduinoっぽくループ処理とdelay()を追加してみたいと思います。

コード

use esp_idf_sys as _;
use std::thread;
use std::time::Duration;

fn main() {
    esp_idf_sys::link_patches();

    println!("Hello, world!");

    loop {
        thread::sleep(Duration::from_millis(999));
        println!("ESP32 Rust std");
    }
}

ほぼ同じなのですが、std::threadのsleepを使ってdelayしています。時間指定はstd::timeになります。loopは条件無しで囲えばループしてくれるようです。

Lチカ

先程のプログラムはほぼESP32の機能を利用していませんでした。今回はLチカをしてみたいと思いますが、GPIO操作をする際にはデバイス特有の処理が必要となります。

基礎から学ぶ 組込みRust
シーアンドアール研究所
¥4,202(2024/12/06 17:45時点)

上記にこの辺の仕組みが書いてありますので、気になる人は目を通してみてください。

Rustではマイコンなどの組み込み系デバイス向けにembedded向けの基本クレートがあり、同じような操作でプログラムができるように整備が進められています。もちろんデバイスごとに差はあるので全く同じにはならないのですが、基本的な考えは共通化されています。

ESP32の場合にはESP-IDFを利用してstd向けのesp-idf-halクレートと、no-std向けのesp-halクレートがあります。今回はstd環境を利用するのでesp-idf-halクレートを利用します。

サンプル

https://github.com/esp-rs/esp-idf-hal/blob/master/examples/blinky.rs

上記にLチカの例があるのでこちらをベースに解説をしていきます。

依存関係の追加

Cargo.tomlに利用するクレートを追加します。

[dependencies]
esp-idf-sys = { version = "0.32.1", features = ["binstart"] }
esp-idf-hal = "0.40.1"
anyhow = "1.0.68"

上記のようにesp-idf-halとanyhowを追加しました。

コード

use esp_idf_hal::delay::FreeRtos;
use esp_idf_hal::gpio::PinDriver;
use esp_idf_hal::peripherals::Peripherals;

fn main() -> anyhow::Result<()> {
    esp_idf_sys::link_patches();

    let peripherals = Peripherals::take().unwrap();
    let mut led = PinDriver::output(peripherals.pins.gpio2)?;

    loop {
        led.set_high()?;
        println!("Output High");
        FreeRtos::delay_ms(1000);

        led.set_low()?;
        println!("Output Low");
        FreeRtos::delay_ms(1000);
    }
}

ちょっとだけ変更しています。

delay

use esp_idf_hal::delay::FreeRtos;

FreeRTOS版のdelayを利用宣言しています。

esp_idf_hal::delay - Rust
Delay providers.

上記に解説がありますが、標準のESP-IDFではFreeRTOSは1ティックが10msです。FreeRTOSのdelayは10以下を指定しても、10ms以上必ず遅延します。Arduinoは1ティックが1msに変更されているのでこの差は注意してください。

10ms以下のdelayを利用したい場合にはesp_idf_hal::delay::Etsを利用するかstd::threadを利用してみてください。

        FreeRtos::delay_ms(1000);

実際のdelayは上記のように指定します。ms以外にusもありますが、上記のようにFreeRTOSは10ms単位で動きますのでusを使うことは無いと思います。us単位で動かす場合にはesp_idf_hal::delay::Ets::delay_us()関数を利用します。

pinMode

    let peripherals = Peripherals::take().unwrap();
    let mut led = PinDriver::output(peripherals.pins.gpio2)?;

Peripherals::take()でペリフェラルを取得しています。

Option と unwrap - Rust By Example 日本語版
Rust by Example (RBE) is a collection of runnable examples that illustrate various Rust concepts and standard libraries.

unwrap()は上記を確認してください。Nullチェックを省略するような処理になります。呼び出して失敗することがない処理や、失敗してもリカバリができない場合にはunwrap()でエラー処理を省略します。

PinDriverでGPIOの設定を変更しています。ここではGPIO2を出力モードで初期化しています。Rustの場合には状態に応じて呼び出せる関数が違いますので入力状態のときに出力系の関数を呼び出せなくしており、単純なミスを減らすことができます。

入出力系については次回以降で詳しく解説していきたいと思います。

出力

        led.set_high()?;
        led.set_low()?;

上記が出力している関数です。

?の導入 - Rust By Example 日本語版
Rust by Example (RBE) is a collection of runnable examples that illustrate various Rust concepts and standard libraries.

最後の?は上記で解説しています。こちらもunwrap()と同じようにエラー処理を単純化するための処理となります。

複数のプログラムを動かす方法

通常はsrc/main.rsを編集して動かしますが、複数のサンプルを実行しようとすると毎回ESP-IDFをダウンロードしてきて1G以上の容量が必要となっていきます。ビルドもかなり重いので辛いです。

そこでCargoでは複数のプログラムを切り替えながら実行する方法が準備されています。

examplesフォルダに複数のプログラムを設置

上記のようにexamplesフォルダを作成し、その中に複数のプログラムを入れることができます。

指定して実行

cargo run --example 01-blink

上記のように–exampleオプションをついけてcargoコマンドを呼び出すことで、main.rsの変わりに指定したファイルでビルドすることができます。ファイルが1つしかないような小さなプロジェクトの場合にはexamplesフォルダを活用して、複数のプログラムを同居させるのがおすすめです。

VSCodeからの実行(失敗)

main関数の上にRunボタンがあり、これを押すと「cargo run –package esp32-test –example 01-blink」が実行されて、いい感じなのですがWSL環境の場合には「. ~/export-esp.sh」を呼び出していない環境で実行されるのでビルド失敗します。

自動読み込みにするか適切にVSCodeを実行すれば大丈夫なはずですが、私は別窓のWSLから実行しています。

まとめ

なんとなくESP32でのRust環境を解説してみました。次回以降はもう少しESP32のRustに特化した内容ですすめていきたいと思っています。

コメント