M5StackをRustで動かす

目的
研究用ロボットのため、M5Stack Core2のIMU(MPU6886)、LCD(ILI9342C)、CANを使用したい。
またRustでコードを書きたいので、そのための環境も用意する。
参考
- https://docs.m5stack.com/ja/core/core2
- https://qiita.com/wararyo/items/fc3b90f72a18b24cf456
- https://community.platformio.org/t/esp32-dev-difference-between-framework-espidf-framework-arduino-and-framework-arduino-espidf/10560
- https://zenn.dev/tremendous1192/articles/92dd86b991f59f
- https://github.com/teruyamato0731/gray_hello_world
- https://qiita.com/lutecia16v/items/1c560bdd7eac7ebeaff7
- https://docs.m5stack.com/en/arduino/m5core/program
- https://labo.agrifeel.net/entry/2022/02/27/133549
- https://monomonotech.jp/kurage/memo/m211117_m5stack_helloworld.html
- https://speakerdeck.com/lovyan03/m5unifiednoshao-jie
- https://lang-ship.com/blog/work/m5unified-1/
- https://github.com/meganetaaan/m5stack-avatar
- https://zenn.dev/koyashiro/scraps/b48ff23accb4f3
- https://github.com/ivmarkov/rust-esp32-std-demo
- https://elchika.com/article/bb1df791-4160-4a6c-a557-aead5ba21d47/
- https://blog.morifuji-is.ninja/post/2023-10-14/
- https://tomo-wait-for-it-yuki.hatenablog.com/entry/embedded-std-rust-on-m5stamp-c3-mate
- https://github.com/ivmarkov/rust-esp32-std-demo
- https://github.com/esp-rs/esp-idf-svc
- https://github.com/esp-rs/esp-idf-hal/tree/master
- https://github.com/esp-rs/esp-idf-sys/tree/master
- https://github.com/esp-rs/espflash/tree/main/espflash#usage
- https://github.com/verylowfreq/rust_on_m5stack_example
- https://docs.rs/embedded-graphics/latest/embedded_graphics/
- https://www.slideshare.net/ciniml/m5stackrust
- https://speakerdeck.com/ciniml/rusty-stack-channosusume
- https://github.com/almindor/mipidsi
- https://github.com/georgik/esp32-rust-multi-target-template/blob/main/m5stack-core2/src/main.rs

通常の環境構築
Rust環境を構築する前にVS CodeのPlatform IO拡張機能でM5Stackが通常通り使用できることを確認する。
事前条件
- VS Codeインストール済み
- Platform IOインストール済み
PIOプロジェクトの初期化
ボードをm5stack-grey、フレームワークをarduinoと指定してプロジェクトを初期化。
すると下記のようなplatform.ini
とmain.cpp
が作成される。
[env:m5stack-grey]
platform = espressif32
board = m5stack-grey
framework = arduino
#include <Arduino.h>
void setup() {
// put your setup code here, to run once:
}
void loop() {
// put your main code here, to run repeatedly:
}
依存にM5Stackを追加
しかしこのままだとArduinoの機能しか使えないため、依存関係にM5Stackライブラリを追加する。これにより#include <M5Stack.h>ができるようになる。[1][2]
M5Stackだと何故か動作しなかったのでM5Unifiedを使用した。M5UnifiedもM5Stack公式であり、Stack以外の環境でも動作するようAPIを統一した版らしいのでM5StackよりM5Unifiedを使用したほうがいいと思う。platform.ini
のlib_deps
にm5stack/M5Unified
と記述すれば#include <M5Unified.h>
で使用できる。
[env:m5stack-grey]
platform = espressif32
board = m5stack-grey
framework = arduino
+monitor_speed = 115200
+lib_deps = m5stack/M5Unified @ ^0.1.14
+#include <M5Unified.h>
-#include <Arduino.h>
void setup() {
// put your setup code here, to run once:
}
void loop() {
// put your main code here, to run repeatedly:
}
コードを記述
m5stackのAPIを参考にしつつ、Arduinoを使用するような感じでコードを書く。[3]
#include <M5Unified.h>
void setup() {
// put your setup code here, to run once:
M5.begin();
}
void loop() {
// put your main code here, to run repeatedly:
Serial.println("Hello, World!");
}


M5Stack on Rustの環境構築
- Rustをインストール。
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
- 依存をインストール。
sudo apt-get update
sudo apt-get install pkg-config libudev-dev flex bison gperf python3 python3-pip python3-venv cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0
- espのビルドツールをインストール。
espup install
は短時間での実行回数に制限があるので注意。
cargo install espup
espup install
# STDならldproxyも必要
cargo install ldproxy
- プロジェクト生成。
cargo-generate
をインストールし、テンプレートからプロジェクトを生成。
cargo install cargo-generate
# STD Project
cargo generate esp-rs/esp-idf-template cargo
# NO-STD (Bare-metal) Project
# cargo generate esp-rs/esp-template
- ビルド。引数を渡さなければ
release
がデフォルトで使用される。
./scripts/build.sh [debug | release]
- 書き込み。
cargo-espflash
を使う方法とespflash
を使う方法がある。
cargo install cargo-espflash
cargo espflash flash --list-all-ports
cargo install espflash
espflash flash target/xtensa-esp32-espidf/release/<PROJECT_NAME> --list-all-ports
- モニター。
cargo-espmonitor
とespmonitor
がある。使い方はどちらも同じ。espflash monitor
でもモニタが見れるっぽい。
cargo install espmonitor
espmonitor <SERIAL_DEVICE>
cargo install espflash
espflash monitor
参考

Rustでコードを書く
cargo generate esp-rs/esp-idf-template cargo
でプロジェクトを生成すると下記のようなmain.rs
が生成される。ここにコードを書いていく。
fn main() {
// It is necessary to call this function once. Otherwise some patches to the runtime
// implemented by esp-idf-sys might not link properly. See https://github.com/esp-rs/esp-idf-template/issues/71
esp_idf_svc::sys::link_patches();
// Bind the log crate to the ESP Logging facilities
esp_idf_svc::log::EspLogger::initialize_default();
loop {
log::info!("Hello, world!");
}
}
2秒スリープの例。
use std::thread;
use std::time::Duration;
fn main() {
// It is necessary to call this function once. Otherwise some patches to the runtime
// implemented by esp-idf-sys might not link properly. See https://github.com/esp-rs/esp-idf-template/issues/71
esp_idf_svc::sys::link_patches();
// Bind the log crate to the ESP Logging facilities
esp_idf_svc::log::EspLogger::initialize_default();
loop {
println!("Sleeping for 2 seconds...");
thread::sleep(Duration::from_secs(2));
}
}

RustでMPU6886を読む
RustにMPU6886のcrateがいくつかあった。esp-idf-halがembedded-halを実装しているのでepsでも使用できそう。
M5Core2とMPU6886の接続は以下の通り。I2Cでつながっている。
ESP32 Chip | GPIO21 | GPIO22 |
---|---|---|
MPU6886 | SDA | SCL |
MPU6886のI2C INTERFACE
slave addressは7bit長の0b110100Xで示される。ただしXはSA0ピンの論理レベルによって決まり、SA01がLOWのとき0b1101000(X=0)、SA0がHIGHのとき0b1101001(X=1)となる。
Writeのとき、送信するデータの中身はレジスタアドレス+書き込むデータ。複数バイトの書き込みも可能で、複数バイトを送信すると自動でレジスタアドレスがインクリメントされる。
Readのとき、レジスタアドレスを送信したのちRead要求をするとデータが送られてくる。読み出しも複数バイト可能。
例えばMPU6886のWHOAMIレジスタの値を読みたい場合
- slave addressを0b1101000と仮定。
- WHOAMIレジスタのアドレスは
0x75
。 - 0b1101000に
0x75
をwrite。 - 1byteのreadを要求待つ。
- WHOAMIのレスポンスとして
0x19
が返ってくる。
SPIの場合
MPU6886のSPI INTERFACE
- MSB first and LSB lastのビッグエンディアン
- 10MHz
- 1byte目にRead/WriteとAddressでコマンドを送信する
1bit目にRead(1)/Write(0)
それ以降の7bitにRegister Address - Writeの場合、2byte目以降に書き込むデータ
Readの場合、レスポンスが返ってくる。
SPI Address format
MSB | LSB | ||||||
---|---|---|---|---|---|---|---|
R/W | A6 | A5 | A4 | A3 | A2 | A1 | A0 |
SPI Data format
MSB | LSB | ||||||
---|---|---|---|---|---|---|---|
D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
例えばMPU6886のWHOAMIレジスタの値を読みたい場合
- WHOAMIレジスタのアドレスは
0x75
- ReadはMSBを1にする。(
0x80 | addr
として送信先のアドレスにビットを立てる。) - 送信するデータは
0x80 | 0x75 = 0xF5
- WHOAMIのレスポンスとして
0x19
が返ってくる。
MPU6886のセットアップ
-
PWR_MGMT_1
(0x6B)に0x80を送信してリセット。 -
PWR_MGMT_1
(0x6B)に0x01を送信してジャイロと加速度センサを有効化。 -
CONFIG
(0x1A)に0x01を送信してDLPFを設定。 -
GYRO_CONFIG
(0x1B)に0x08を送信してジャイロセンサの範囲をから に変更。dps(degrees per second)は1秒あたりの度数、すなわち角速度を示す。 -
ACCEL_CONFIG
(0x1C)に0x08を送信して加速度計の範囲をから に変更。 - (上記3手順を一度に行いたい場合、I2Cなら0x1Aに3byteのバイト列
0x01 0x10 0x10
を送信する。)
MPU6886からセンサの値を取り出す。
-
ACCEL_XOUT_H
(0x3B),ACCEL_XOUT_H
(0x3C)から続いてACCEL_ZOUT_L
(0x40)まで6byte加速度センサのデータが入っている。 -
TEMP_OUT_H
(0x41),TEMP_OUT_L
(0x42)に2byte温度センサのデータが入っている。 -
GYRO_XOUT_H
(0x43),GYRO_XOUT_L
(0x44)から続いてGYRO_ZOUT_L
(0x48)まで6byteジャイロセンサのデータが入っている。 - 上記全データを取り出したい場合、MPU6886のslave addressに0x3Bを書き込んだのち、14byteの読み出し要求を行う。
WHOAMI読み出しの実験コード
use esp_idf_hal::delay::BLOCK;
use esp_idf_hal::gpio::AnyIOPin;
use esp_idf_hal::i2c::{I2c, I2cConfig, I2cDriver};
use esp_idf_hal::peripheral::Peripheral;
use esp_idf_hal::peripherals::Peripherals;
use esp_idf_hal::prelude::*;
use esp_idf_hal::units::Hertz;
const SLAVE_ADDR: u8 = 0x68;
const WHOAMI: u8 = 0x75;
fn i2c_master_init<'d>(
i2c: impl Peripheral<P = impl I2c> + 'd,
sda: AnyIOPin,
scl: AnyIOPin,
baudrate: Hertz,
) -> anyhow::Result<I2cDriver<'d>> {
let config = I2cConfig::new().baudrate(baudrate);
let driver = I2cDriver::new(i2c, sda, scl, &config).unwrap();
Ok(driver)
}
fn main() -> anyhow::Result<()> {
// It is necessary to call this function once. Otherwise some patches to the runtime
// implemented by esp-idf-sys might not link properly. See https://github.com/esp-rs/esp-idf-template/issues/71
esp_idf_svc::sys::link_patches();
// Bind the log crate to the ESP Logging facilities
esp_idf_svc::log::EspLogger::initialize_default();
let peripherals = Peripherals::take().unwrap();
let mut i2c_master = i2c_master_init(
peripherals.i2c0,
peripherals.pins.gpio21.into(),
peripherals.pins.gpio22.into(),
400.kHz().into(),
)?;
loop {
i2c_master.write(SLAVE_ADDR, &[WHOAMI], BLOCK).unwrap();
let mut buffer = [0; 1];
i2c_master.read(SLAVE_ADDR, &mut buffer, BLOCK).unwrap();
println!("WHOAMI: {:#X}", buffer[0]);
}
}
use esp_idf_hal::{
delay::{FreeRtos, BLOCK},
gpio::AnyIOPin,
i2c::{I2c, I2cConfig, I2cDriver},
peripheral::Peripheral,
prelude::*,
sys::EspError,
units::Hertz,
};
const SLAVE_ADDR: u8 = 0x68;
const CONFIG: u8 = 0x1A;
// const GYRO_CONFIG: u8 = 0x1B;
// const ACCEL_CONFIG: u8 = 0x1C;
const WHOAMI: u8 = 0x75;
const PWR_MGMT_1: u8 = 0x6B;
const ACCEL_XOUT_H: u8 = 0x3B;
const GYRO_XOUT_H: u8 = 0x43;
fn main() {
esp_idf_svc::sys::link_patches();
esp_idf_svc::log::EspLogger::initialize_default();
run().unwrap();
}
fn run() -> Result<(), EspError> {
let peripherals = Peripherals::take()?;
let mut i2c_master = i2c_master_init(
peripherals.i2c0,
peripherals.pins.gpio21.into(),
peripherals.pins.gpio22.into(),
400.kHz().into(),
)?;
imu_init(&mut i2c_master)?;
loop {
let acc = imu_read_accel(&mut i2c_master);
let gyro = imu_read_gyro(&mut i2c_master);
print!("acc x:{:6.2}, y:{:6.2}, z:{:6.2} ", acc.0, acc.1, acc.2);
print!("gyro x:{:4.0}, y:{:4.0}, z:{:4.0} ", gyro.0, gyro.1, gyro.2);
println!();
FreeRtos::delay_ms(30);
}
}
fn i2c_master_init<'d>(
i2c: impl Peripheral<P = impl I2c> + 'd,
sda: AnyIOPin,
scl: AnyIOPin,
baudrate: Hertz,
) -> Result<I2cDriver<'d>, EspError> {
let config = I2cConfig::new().baudrate(baudrate);
let driver = I2cDriver::new(i2c, sda, scl, &config)?;
Ok(driver)
}
fn imu_init(i2c_master: &mut I2cDriver) -> Result<(), EspError> {
i2c_master.write(SLAVE_ADDR, &[WHOAMI], BLOCK)?;
let mut tmp = [0];
i2c_master.read(SLAVE_ADDR, &mut tmp, BLOCK)?;
println!("WHOAMI: {:#X}", tmp[0]);
assert_eq!(tmp[0], 0x19);
// reset
i2c_master.write(SLAVE_ADDR, &[PWR_MGMT_1, 0x80], BLOCK)?;
// wait for reset
FreeRtos::delay_ms(15);
// activate gyro and accel
i2c_master.write(SLAVE_ADDR, &[PWR_MGMT_1, 0x01], BLOCK)?;
// set gyro range to +-500 deg/s and accel range to +-4g
i2c_master.write(SLAVE_ADDR, &[CONFIG, 0x1A, 0x08, 0x08], BLOCK)?;
Ok(())
}
fn imu_read_accel(i2c_master: &mut I2cDriver) -> (f32, f32, f32) {
i2c_master
.write(SLAVE_ADDR, &[ACCEL_XOUT_H], BLOCK)
.unwrap();
let mut buffer = [0; 6];
i2c_master.read(SLAVE_ADDR, &mut buffer, BLOCK).unwrap();
return (
conv((buffer[0] as i16) << 8 | buffer[1] as i16, 4.0 * 9.8),
conv((buffer[2] as i16) << 8 | buffer[3] as i16, 4.0 * 9.8),
conv((buffer[4] as i16) << 8 | buffer[5] as i16, 4.0 * 9.8),
);
}
fn imu_read_gyro(i2c_master: &mut I2cDriver) -> (f32, f32, f32) {
i2c_master.write(SLAVE_ADDR, &[GYRO_XOUT_H], BLOCK).unwrap();
let mut buffer = [0; 6];
i2c_master.read(SLAVE_ADDR, &mut buffer, BLOCK).unwrap();
return (
conv((buffer[0] as i16) << 8 | buffer[1] as i16, 500.0),
conv((buffer[2] as i16) << 8 | buffer[3] as i16, 500.0),
conv((buffer[4] as i16) << 8 | buffer[5] as i16, 500.0),
);
}
fn conv(representation: i16, scale: f32) -> f32 {
scale / i16::MAX as f32 * representation as f32
}

RustでLCDを使う
M5stack core 2のLCDはILI9342C。
いい感じのcrateと使用者を見つけた。使用者のコードはバージョンが古いので書き方だけ新しくすればよさそう。mipidsiのexampleもある。
mipidsi::Builderを使用すればLCDが使用できる。
mipidsiはdisplay_interfaceのv0.4.1に依存している。
描画内容の処理はembedded_graphicsが行ってくれる。
M5stack core 2とLCD(ILI9342C)は以下の通りSPIで繋がっている。
ESP32 Chip | GPIO38 | GPIO23 | GPIO18 | GPIO5 | GPIO15 | |||
---|---|---|---|---|---|---|---|---|
AXP192 Chip | AXP_IO4 | AXP_DC3 | AXP_LDO2 | |||||
ILI9342C | MISO | MOSI | SCK | CS | DC | RST | BL | PWR |
注意点としてESP32のGPIOを接続するだけではLCDへの電源供給が行われない。
AXP192を介してLCD PWR, LCD BL, LCD RSTなどに電源供給する必要がある。
axp192のcrateもあるが非常にバージョンが古いため自分で手直しする必要がある。
axp192の初期化
電源供給のためにaxp192を初期化する。またcrateのexampleにあったm5sc2_initを使用して電源を投入する。
use axp192::Axp192;
let mut i2c_master = i2c_master_init(
peripherals.i2c0,
peripherals.pins.gpio21.into(),
peripherals.pins.gpio22.into(),
400.kHz().into(),
)?;
let mut axp = Axp192::new(i2c_master);
m5sc2_init(&mut axp, &mut FreeRtos).unwrap();
m5sc2_initの実装
fn m5sc2_init<I2C>(
axp: &mut axp192::Axp192<I2C>,
delay: &mut impl embedded_hal::delay::DelayNs,
) -> Result<(), I2C::Error>
where
I2C: embedded_hal::i2c::ErrorType,
I2C: embedded_hal::i2c::I2c,
{
// Default setup for M5Stack Core 2
axp.set_dcdc1_voltage(3350)?; // Voltage to provide to the microcontroller (this one!)
axp.set_ldo2_voltage(3300)?; // Peripherals (LCD, ...)
axp.set_ldo2_on(true)?;
axp.set_ldo3_voltage(2000)?; // Vibration motor
axp.set_ldo3_on(false)?;
axp.set_dcdc3_voltage(2800)?; // LCD backlight
axp.set_dcdc3_on(true)?;
axp.set_gpio1_mode(axp192::GpioMode12::NmosOpenDrainOutput)?; // Power LED
axp.set_gpio1_output(false)?; // In open drain modes, state is opposite to what you might
// expect
axp.set_gpio2_mode(axp192::GpioMode12::NmosOpenDrainOutput)?; // Speaker
axp.set_gpio2_output(true)?;
axp.set_key_mode(
// Configure how the power button press will work
axp192::ShutdownDuration::Sd4s,
axp192::PowerOkDelay::Delay64ms,
true,
axp192::LongPress::Lp1000ms,
axp192::BootTime::Boot512ms,
)?;
axp.set_gpio4_mode(axp192::GpioMode34::NmosOpenDrainOutput)?; // LCD reset control
axp.set_battery_voltage_adc_enable(true)?;
axp.set_battery_current_adc_enable(true)?;
axp.set_acin_current_adc_enable(true)?;
axp.set_acin_voltage_adc_enable(true)?;
// Actually reset the LCD
axp.set_gpio4_output(false)?;
axp.set_ldo3_on(true)?; // Buzz the vibration motor while intializing ¯\_(ツ)_/¯
delay.delay_ms(100u32);
axp.set_gpio4_output(true)?;
axp.set_ldo3_on(false)?;
delay.delay_ms(100u32);
Ok(())
}
SpiDeviceDriverの初期化
ピン配置に合わせてSPIを初期化する。
baudrateを高くすると初期化できなかったので20MHzにしている。高くする方法を知りたい。
use esp_idf_hal::{
spi::{SpiConfig, SpiDeviceDriver, SpiDriver, SpiDriverConfig},
};
let spi = peripherals.spi2;
let sclk = peripherals.pins.gpio18;
let serial_in = peripherals.pins.gpio38; // SDI
let serial_out = peripherals.pins.gpio23; // SDO
let cs_1 = peripherals.pins.gpio5;
let driver = SpiDriver::new(
spi,
sclk,
serial_out,
Some(serial_in),
&SpiDriverConfig::new(),
)?;
let config = SpiConfig::new().baudrate(20.MHz().into());
let lcd_spi_master = SpiDeviceDriver::new(&driver, Some(cs_1), &config)?;
display interfaceの初期化
mipidsi::Builderの依存であるdisplay interfaceを初期化する。mipidsi::Builderはv0.4.1のSPIInterfaceNoCSに依存しているので、v0.5.0のSPIInterfaceにしないよう注意。
use display_interface_spi::SPIInterfaceNoCS;
let dc = PinDriver::output(peripherals.pins.gpio15)?;
let spi_iface = SPIInterfaceNoCS::new(lcd_spi_master, dc);
mipidsi::Builderでdisplayを初期化
mipidsi::Builderを使用してili9342cを初期化する。crateのドキュメントにはThe Rgb565 color mode is not supported for displays with SPI connection.
と書いてあるが何故か動く。また私の場合はwith_color_orderとwith_invert_colorsを使用しないと色の表示がおかしかった。
let mut display = mipidsi::Builder::ili9342c_rgb565(spi_iface)
.with_display_size(320, 240)
.with_color_order(mipidsi::ColorOrder::Bgr)
.with_invert_colors(mipidsi::ColorInversion::Inverted)
.init(
&mut esp_idf_hal::delay::FreeRtos,
None::<PinDriver<esp_idf_hal::gpio::AnyOutputPin, esp_idf_hal::gpio::Output>>,
)
.unwrap();
描画する
// Make the display all green
display.clear(Rgb565::RED).unwrap();
// Draw with embedded_graphics
Text::with_alignment(
"hinge",
Point::new(160, 120),
MonoTextStyle::new(&ascii::FONT_9X18_BOLD, RgbColor::BLACK),
embedded_graphics::text::Alignment::Center,
)
.draw(&mut display)
.unwrap();

LCDとIMUを同時に使う
電源管理を行うAXP192とIMUのMPU6886はどちらもi2c0に接続されている。そのため同時に&mut
(可変参照)を生成してしまうとborrow checkerに怒られてしまう。
embedded_hal_busを使えばいい感じにembedded-halのバスを共有できる。
use core::cell::RefCell;
use embedded_hal_bus::i2c;
// RefCellを生成
let i2c_ref_cell = RefCell::new(i2c_master);
// IMU
imu_init(&mut i2c::RefCellDevice::new(&i2c_ref_cell)).unwrap();
// AXP192
let mut axp = Axp192::new(i2c::RefCellDevice::new(&i2c_ref_cell));

Rust用にVS Codeを設定する
拡張機能をいくつか入れる。
- "rust-lang.rust-analyzer",
- "tamasfe.even-better-toml",
- "serayuzgur.crates"
フォーマッタとリンターを設定。インデントをスペース4つに設定。
{
"rust-analyzer.check.command": "clippy",
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer",
"editor.tabSize": 4
},
}
build & runタスクを作成。
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"type": "shell",
"command": "cargo build",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [
"$rustc"
]
},
{
"label": "run",
"type": "shell",
"command": "cargo run",
"group": {
"kind": "test",
"isDefault": true
},
"problemMatcher": [
"$rustc"
]
}
]
}
runタスクのキーボード・ショートカットを作成。
{
{
"key": "ctrl+u",
"command": "workbench.action.tasks.runTask",
"args": "run",
"when": "editorTextFocus && editorLangId == 'rust'"
}
}

RustでCANを使う
example通りで動きそう。と思ったら動かない。embedded_canとesp_idf_hal::canの関係がおかしそう。
use embedded_can::nb::Can;
use embedded_can::Frame;
use embedded_can::StandardId;
use esp_idf_hal::prelude::*;
use esp_idf_hal::can;
let peripherals = Peripherals::take().unwrap();
let pins = peripherals.pins;
// filter to accept only CAN ID 881
let filter = can::config::Filter::Standard {filter: 881, mask: 0x7FF };
// filter that accepts all CAN IDs
// let filter = can::config::Filter::standard_allow_all();
let timing = can::config::Timing::B500K;
let config = can::config::Config::new().filter(filter).timing(timing);
let mut can = can::CanDriver::new(peripherals.can, pins.gpio5, pins.gpio4, &config).unwrap();
let tx_frame = can::Frame::new(StandardId::new(0x042).unwrap(), &[0, 1, 2, 3, 4, 5, 6, 7]).unwrap();
nb::block!(can.transmit(&tx_frame)).unwrap();
if let Ok(rx_frame) = nb::block!(can.receive()) {
info!("rx {:}:", rx_frame);
}

これから忙しくなるのでCANは後回し。
夏頃に再開する。

CANのexampleがおかしいのは認識していそう。誰もPRを出していないだけ。ドキュメントも不十分なのでAPIから使用方法を読み取るしかない。
embedded-canがそもそもv1になっていないので仕方ないのか。

RustでCOMMUを使う
COMMUモジュールにはMCP2515が付いており、CANがしゃべれる。COMMUモジュールのMCP2515はSPIでM5Stackとつながっている。M5Stack Core2はピン配置が変わっているので注意。
MCP2515 | ESP32 Chip |
---|---|
CAN_CS |
|
CAN_INT | GPIO15 |
CAN_SCK | GPIO18 |
CAN_MISO |
|
CAN_MOSI | GPIO23 |
- https://docs.m5stack.com/ja/module/commu
- https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/docs/datasheet/module/MCP2515_en.pdf
- https://github.com/m5stack/M5Stack/blob/master/examples/Modules/COMMU/CAN/commu_can_transmitter/commu_can_transmitter.ino
- https://docs.rs/mcp2515/latest/mcp2515/
- https://github.com/coryjfowler/MCP_CAN_lib/blob/master/mcp_can.h
- https://www.marutsu.co.jp/contents/shop/marutsu/datasheet/385414.pdf

いい感じのライブラリがなかったのでデータシートと向き合いながらライブラリを自作する。
C++のライブラリやバージョンが古いRust向けのライブラリを参考にする。
M5commuのMCP2515についている発振器は8MHz。
// 1Mbps
cfg1 = MCP_8MHz_1000kBPS_CFG1;
cfg2 = MCP_8MHz_1000kBPS_CFG2;
cfg3 = MCP_8MHz_1000kBPS_CFG3;
/*
* Speed 8M
*/
#define MCP_8MHz_1000kBPS_CFG1 (0x00)
#define MCP_8MHz_1000kBPS_CFG2 (0xC0) /* Enabled SAM bit */
#define MCP_8MHz_1000kBPS_CFG3 (0x80) /* Sample point at 75% */

MCP2515に5つの動作モードがある。
起動時はConfigurationモード。初期化処理をこのモードで行う。(bitrateの設定等)
送受信したい場合はNormalモードに設定する必要がある。
また送信バッファが3つと受信バッファが2つある。
送信手順
- TXBnCTRL を読んで TXREQ が立っていないものを探す
- Load TX Buffer Instructionでバッファにデータを投げる
- RTS (Request to Send) Commandで送信要求
受信手順
- READ STATUS INSTRUCTIONで受信バッファを走査
- READ RX BUFFER INSTRUCTIONでデータ読み取り