M1MacのRustでESP32をビルドして、M5Stackのジャイロセンサを読み取ってwebサーバーで配信してみた

組み込みがこんな大変やと思わんかった

こんにちは、M5StackでいよいよRustで書いたプログラムを動かせたので記録として残しておきます

環境

  • M1MacMini(Ventura)
  • rustc 1.73.0
  • M5Stack FIRE V2.7

ビルドできるように準備

rustup toolchain install nightly --component rust-src

cargo install cargo-generate

cargo generate esp-rs/esp-idf-template cargo
# プロンプトには以下のように回答
# ✔ 🤷   Which MCU to target? · esp32
# ✔ 🤷   Configure advanced template options? · false

# プログラムをM5Stackに送るためのコマンド。
# espflashではなくcargo-espflashをインストールするとうまく動かなかった
cargo install espflash

cargo install espup
cargo install ldproxy

. ~/export-esp.sh
cargo build

これで動いた!その後、似たような記事を見つけたので、std環境でしかできない実装をしてノリを理解した

Embedded Rust on Espressif

その後、Espressifが公開してたRustでESP32のコードをstd環境で書いて動作させるサイトを見つけたのでこれをひたすらやってた(途中でやめたけど)

Embedded Rust on Espressif

途中でESP32の内部温度を取得してそれをESP32がHTTPサーバーとしてHTMLを公開する、という課題があったが、M5StackのESP32は内部温度を取得できない?みたいで、代わりにジャイロセンサーの値を読み取ることにした
今思うとこれがこんなにしんどいと思わなかった🤢

ArdinoIDEだったら、公式サイトなどで紹介されているように簡単にジャイロの値が取れるらしい。こんな感じでめちゃくちゃ楽

float gyroX, gyroY, gyroZ;
M5.IMU.getGyroData(&gyroX,&gyroY,&gyroZ);

同じようなAPIが都合よくesp_idf_sysに映えてないか探したけどなかった…
よく考えれば当たり前で、ジャイロセンサはESP32とは全く別の部品なのでEspressif社が用意してるわけない🙄

どうやらM5Stackに入っているジャイロセンサはMPU6886というらしい。
MPU6886の公式サイトがあってそこに載ってる資料見ればわかるか?と思って探したらデータシートなるものが公開されてた。(データシートという言葉を聞いてラズパイとLCDディスプレイ(AQM0802)で遊んでみた時の記憶が若干蘇った)

https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/docs/datasheet/core/MPU-6886-000193%2Bv1.1_GHIC_en.pdf

正直全く意味わからん..
ということで結局、M5StackがArdinio用に提供するライブラリの中身を見て実装することにした…
コードジャンプをビュンビュンんしたかったのでgithub.devがおおいに役だった

https://github.dev/m5stack/M5Stack/blob/master/src/utility/MPU6886.cpp

このコードのMPU6886::InitMPU6886::getGyroAdcで行ってるI2C通信をRustで再現したらついに値を取得できた!!😂

この作業で合計4hほど溶けてしまって辛かったのと、何かセンサーを読み取りたいとか操作したいときに毎回こんな苦悩して実装しないといけないのにつらみを感じてここでやめた

書いたコード

誰かのためになればええんですけどね🥹

/*
Copyright 2023 morimorikochan

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


Copyright 2022 Ferrous Systems GmbH

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
https://github.com/esp-rs/std-training/blob/main/LICENSE-MIT.txt
*/
use anyhow::Result;
use core::str;
use esp_idf_hal::{
    i2c::{I2cConfig, I2cDriver},
    prelude::*, delay::BLOCK,
};
use esp_idf_svc::{
    eventloop::EspSystemEventLoop, http::server::{EspHttpServer, Configuration},
};
use std::{
    sync::{Arc, Mutex},
    thread::sleep,
    time::Duration,
};
use embedded_svc::http::Method;
use wifi::wifi;

use esp_idf_sys as _;


#[toml_cfg::toml_config]
pub struct Config {
    #[default("")]
    wifi_ssid: &'static str,
    #[default("")]
    wifi_psk: &'static str,
}

fn main() -> Result<()> {
    esp_idf_sys::link_patches();
    esp_idf_svc::log::EspLogger::initialize_default();

    let peripherals = Peripherals::take().unwrap();
    let sysloop = EspSystemEventLoop::take()?;

    // Initialize temperature sensor
    let sda = peripherals.pins.gpio21;
    let scl = peripherals.pins.gpio22;
    let i2c = peripherals.i2c0;
    let config = I2cConfig::new().baudrate(100.kHz().into());
    let mut i2c = I2cDriver::new(i2c, sda, scl, &config)?;
    // The constant `CONFIG` is auto-generated by `toml_config`.
    let app_config = CONFIG;

    // Connect to the Wi-Fi network
    let _wifi = wifi(
        app_config.wifi_ssid,
        app_config.wifi_psk,
        peripherals.modem,
        sysloop,
    )?;
  
    let MPU6886_ADDRESS = 0x68;

    i2c.write(MPU6886_ADDRESS, &[0x6B, 0x00], BLOCK)?;
    sleep(Duration::from_millis(10));

    i2c.write(MPU6886_ADDRESS, &[0x6B, (0x01 << 7)], BLOCK)?;
    sleep(Duration::from_millis(10));

    i2c.write(MPU6886_ADDRESS, &[0x6B, 0x01], BLOCK)?;
    sleep(Duration::from_millis(10));

    // +- 8g
    i2c.write(MPU6886_ADDRESS, &[0x1C, 0x10], BLOCK)?;
    sleep(Duration::from_millis(1));

    // +- 2000 dps
    i2c.write(MPU6886_ADDRESS, &[0x1B, 0x18], BLOCK)?;
    sleep(Duration::from_millis(1));

    // 1khz output
    i2c.write(MPU6886_ADDRESS, &[0x1A, 0x01], BLOCK)?;
    sleep(Duration::from_millis(1));

    // 2 div, FIFO 500hz out
    i2c.write(MPU6886_ADDRESS, &[0x19, 0x01], BLOCK)?;
    sleep(Duration::from_millis(1));

    i2c.write(MPU6886_ADDRESS, &[0x38, 0x00], BLOCK)?;
    sleep(Duration::from_millis(1));

    i2c.write(MPU6886_ADDRESS, &[0x1D, 0x00], BLOCK)?;
    sleep(Duration::from_millis(1));

    i2c.write(MPU6886_ADDRESS, &[0x6A, 0x00], BLOCK)?;
    sleep(Duration::from_millis(1));

    i2c.write(MPU6886_ADDRESS, &[0x23, 0x00], BLOCK)?;
    sleep(Duration::from_millis(1));

    i2c.write(MPU6886_ADDRESS, &[0x37, 0x22], BLOCK)?;
    sleep(Duration::from_millis(1));

    i2c.write(MPU6886_ADDRESS, &[0x38, 0x01], BLOCK)?;
    sleep(Duration::from_millis(10));

    let convert = |a: u8, b: u8| {
        let mut result = (a as u16) << 8;
        result += b as u16;
        result as i16
    };
   
    let mut _buffer = Arc::new(Mutex::new([0u8; 6]));

    println!("Server preparing..");

    let mut server: EspHttpServer= EspHttpServer::new(&Configuration::default())?;

    let _buffer_cloned = Arc::clone(&_buffer);
    let _convert_cloned = convert.clone();
    server.fn_handler("/", Method::Get, move |request| {
        let mut response = request.into_ok_response()?;
        let buffer = _buffer_cloned.lock().expect("cannot get lock...");
        response.write(templated(format!("<li><ul>x: {}</ul><ul>y: {}</ul><ul>z: {}</ul></li>", _convert_cloned(buffer[0], buffer[1]),_convert_cloned(buffer[2], buffer[3]), _convert_cloned(buffer[4], buffer[5]))).as_bytes())?;
        response.flush()?;
        Ok(())
    })?;

    println!("Server awaiting connection");

    loop {
        i2c.write(0x68, &[0x3B], BLOCK).unwrap();
        sleep(Duration::from_millis(10));

        let mut buffer = _buffer.lock().expect("cannot get lock on write data...");
        i2c.read(MPU6886_ADDRESS, &mut *buffer, BLOCK)?;
        // for n in 0..6 {
        //     print!("[{:x}]",buffer[n]);
        // }
        println!("{},{},{}", convert(buffer[0], buffer[1]),convert(buffer[2], buffer[3]), convert(buffer[4], buffer[5]));
    }
}

fn templated(content: impl AsRef<str>) -> String {
    format!(
        r#"
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>esp-rs web server</title>
    </head>
    <body>
        {}
    </body>
</html>
"#,
        content.as_ref()
    )
}

fn index_html() -> String {
    templated("Hello from mcu!")
}

fn temperature(val: f32) -> String {
    templated(format!("chip temperature: {:.2}°C", val))
}

遭遇したトラブル

M5StackからWifiに繋がらない

ESP32は5GHzのWifiには繋がらないらしい。
2.4HGzのWifiにすると繋がった

M5Stack起動時にエラー bad load address range

E (137) esp_image: Segment 0 0x3c050020-0x3c05fff8 invalid: bad load address range

これは、テンプレートプロジェクトを作成する際に、esp32c3にしていたからだった
esp32を選ぶと問題なくコンパイルが通るようになった

VSCodeでrust-analyzerがエラーを吐く

~/export-esp.shで展開されている環境変数がVSCodeでは読み取られないためだった
.zshrcなどにコピーした後再起動するとうまく動いた

ビルドできない

自分の環境だと.cargo/config.tomlの設定値が以下の通りじゃないと動かなかった。
調べた感じはターゲットがxtensa-esp32-espidfriscv32imc-esp-espidfの2種類あるらしいけど何が違うのか正直よくわからん。全てノリでやっている

[build]
target = "xtensa-esp32-espidf"

[target.xtensa-esp32-espidf]
linker = "ldproxy"
runner = "espflash flash --monitor" # Select this runner for espflash v2.x.x

[unstable]
build-std = ["std", "panic_abort"]

[env]
# Note: these variables are not used when using pio builder (`cargo build --features pio`)
ESP_IDF_VERSION = "v4.4.6"

先に書いた、Embedded Rust on Espressifの初期値が全く違ったけどこれに書き換えたらうまく行った。

所感

tech  Rust  M5Stack 

See also