Open11

M5StackをRustで動かす

1111

目的

研究用ロボットのため、M5Stack Core2のIMU(MPU6886)、LCD(ILI9342C)、CANを使用したい。
またRustでコードを書きたいので、そのための環境も用意する。

参考

1111

通常の環境構築

Rust環境を構築する前にVS CodeのPlatform IO拡張機能でM5Stackが通常通り使用できることを確認する。

事前条件

  • VS Codeインストール済み
  • Platform IOインストール済み

PIOプロジェクトの初期化

ボードをm5stack-grey、フレームワークをarduinoと指定してプロジェクトを初期化。
すると下記のようなplatform.inimain.cppが作成される。

platform.ini
[env:m5stack-grey]
platform = espressif32
board = m5stack-grey
framework = arduino
main.cpp
#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.inilib_depsm5stack/M5Unifiedと記述すれば#include <M5Unified.h>で使用できる。

platform.ini
 [env:m5stack-grey]
 platform = espressif32
 board = m5stack-grey
 framework = arduino
+monitor_speed = 115200
+lib_deps = m5stack/M5Unified @ ^0.1.14
main.cpp
+#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]

main.cpp
#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!");
}
脚注
  1. https://qiita.com/wararyo/items/fc3b90f72a18b24cf456 ↩︎

  2. https://qiita.com/lutecia16v/items/1c560bdd7eac7ebeaff7 ↩︎

  3. https://docs.m5stack.com/en/arduino/m5core/program ↩︎

1111

M5Stack on Rustの環境構築

  1. Rustをインストール。
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
  1. 依存をインストール。
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
  1. espのビルドツールをインストール。espup installは短時間での実行回数に制限があるので注意。
cargo install espup
espup install
# STDならldproxyも必要
cargo install ldproxy
  1. プロジェクト生成。
    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
  1. ビルド。引数を渡さなければreleaseがデフォルトで使用される。
./scripts/build.sh [debug | release]
  1. 書き込み。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
  1. モニター。cargo-espmonitorespmonitorがある。使い方はどちらも同じ。espflash monitorでもモニタが見れるっぽい。
cargo install espmonitor
espmonitor <SERIAL_DEVICE>
cargo install espflash
espflash monitor

参考

1111

Rustでコードを書く

cargo generate esp-rs/esp-idf-template cargoでプロジェクトを生成すると下記のようなmain.rsが生成される。ここにコードを書いていく。

src/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秒スリープの例。

src/main.rs
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));
    }
}
1111

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レジスタの値を読みたい場合

  1. slave addressを0b1101000と仮定。
  2. WHOAMIレジスタのアドレスは0x75
  3. 0b1101000に0x75をwrite。
  4. 1byteのreadを要求待つ。
  5. WHOAMIのレスポンスとして0x19が返ってくる。
SPIの場合

MPU6886のSPI INTERFACE

  1. MSB first and LSB lastのビッグエンディアン
  2. 10MHz
  3. 1byte目にRead/WriteとAddressでコマンドを送信する
    1bit目にRead(1)/Write(0)
    それ以降の7bitにRegister Address
  4. 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レジスタの値を読みたい場合

  1. WHOAMIレジスタのアドレスは0x75
  2. ReadはMSBを1にする。(0x80 | addrとして送信先のアドレスにビットを立てる。)
  3. 送信するデータは0x80 | 0x75 = 0xF5
  4. WHOAMIのレスポンスとして0x19が返ってくる。

MPU6886のセットアップ

  1. PWR_MGMT_1(0x6B)に0x80を送信してリセット。
  2. PWR_MGMT_1(0x6B)に0x01を送信してジャイロと加速度センサを有効化。
  3. CONFIG(0x1A)に0x01を送信してDLPFを設定。
  4. GYRO_CONFIG(0x1B)に0x08を送信してジャイロセンサの範囲を\pm250\,\mathrm{dps}から\pm500\,\mathrm{dps}に変更。dps(degrees per second)は1秒あたりの度数、すなわち角速度を示す。
  5. ACCEL_CONFIG(0x1C)に0x08を送信して加速度計の範囲を\pm2\,\mathrm{g}から\pm4\,\mathrm{g}に変更。
  6. (上記3手順を一度に行いたい場合、I2Cなら0x1Aに3byteのバイト列0x01 0x10 0x10を送信する。)

MPU6886からセンサの値を取り出す。

  1. ACCEL_XOUT_H(0x3B), ACCEL_XOUT_H(0x3C)から続いてACCEL_ZOUT_L(0x40)まで6byte加速度センサのデータが入っている。
  2. TEMP_OUT_H(0x41), TEMP_OUT_L(0x42)に2byte温度センサのデータが入っている。
  3. GYRO_XOUT_H(0x43), GYRO_XOUT_L(0x44)から続いてGYRO_ZOUT_L(0x48)まで6byteジャイロセンサのデータが入っている。
  4. 上記全データを取り出したい場合、MPU6886のslave addressに0x3Bを書き込んだのち、14byteの読み出し要求を行う。
WHOAMI読み出しの実験コード
src/main.rs
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]);
    }
}
src/main.rs
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
}

https://docs.rs/mpu6886/latest/mpu6886/
https://github.com/tana/mpu6050
https://zenn.dev/tana_ash/articles/5a458538cd9204
https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/docs/datasheet/core/MPU-6886-000193%2Bv1.1_GHIC_en.pdf

1111

RustでLCDを使う

M5stack core 2のLCDはILI9342C。
https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/docs/datasheet/core/ILI9342C-ILITEK.pdf

いい感じの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

https://lang-ship.com/blog/work/m5stack-core2-axp192/
https://se.hatenablog.jp/entry/2021/03/31/075715

注意点として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();
1111

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));
1111

Rust用にVS Codeを設定する

拡張機能をいくつか入れる。

  • "rust-lang.rust-analyzer",
  • "tamasfe.even-better-toml",
  • "serayuzgur.crates"

フォーマッタとリンターを設定。インデントをスペース4つに設定。

setting.json
{
    "rust-analyzer.check.command": "clippy",
    "[rust]": {
        "editor.defaultFormatter": "rust-lang.rust-analyzer",
        "editor.tabSize": 4
    },
}

build & runタスクを作成。

tasks.json
{
  "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タスクのキーボード・ショートカットを作成。

keybindings.json
{
    {
        "key": "ctrl+u",
        "command": "workbench.action.tasks.runTask",
        "args": "run",
        "when": "editorTextFocus && editorLangId == 'rust'"
    }
}
1111

RustでCANを使う

https://docs.esp-rs.org/esp-idf-hal/esp_idf_hal/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);
}