chakokuのブログ(rev4)

テック・コミック・DTM・・・ごくまれにチャリ

ESP32-C3/Rustの環境でSoft I2C ドライバを試作する (QMP6988用)

背景:ESP32-C3を搭載するM5 StampでI2C接続のCO2センサを接続して日々空気質を計測している。このセンサには気圧計がないので、気圧センサを追加したい(2つのセンサをI2Cで接続)
課題:I2Cのバスに並列で接続したらいいのだけど、I2C HUBが必要となり部品点が増えて見た目にすっきりしない
対策:I2Cは低速で動作させることも可能でソフトウエアでドライバを実装することもできる。Rustを使ってSoft版のI2C ドライバを作る
詳細:

すでにMicroPythonではSoft I2Cドライバを試作しており、それをRustにポーティングする。特にややこしそうなのは、、双方向バスを実装するところ。一度設定したgpio を一旦資産解放して、IOの方向を切り替える必要がある。Rustで資産解放ってどうやったらできるのやら。

GPIOのpinを引数で渡してサブルーチンでGPIOを操作する方法も仮引数の型定義でエラー出まくって自力では到底正しい記法にたどり着けそうになかったが、サンプルコードを公開してくれている人がいてそのコードをベースに実装するとエラーが解消された(mitoneko様に感謝)。Generic型で表現されているのだろうか。。このレベルになるとどう書くのが正しいのか理解できず。

use anyhow::Result;
use std::{thread::sleep, time::Duration};

use esp_idf_svc::hal::gpio::PinDriver;
use esp_idf_svc::hal::gpio::Pin;
use esp_idf_svc::hal::gpio::Output;
use esp_idf_svc::hal::peripherals::Peripherals;

fn main() -> Result<()> {

    esp_idf_svc::sys::link_patches();
    esp_idf_svc::log::EspLogger::initialize_default();

    log::info!("Hello, world!");

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

    log::info!("Hello, world!");
    loop{ 
        print!("-----ok??----------\n");
        //p0.set_high()?;
        send_clk(&mut scl);
        sleep(Duration::from_millis(100));
        print!("-----zzz----------\n");
        //p0.set_low()?;
        send_clk(&mut scl);
        sleep(Duration::from_millis(100));
    }
}

/*---------------------
      send clock
 ---------------------*/
fn send_clk<T: Pin>(pin: &mut PinDriver<T,Output>){
    pin.set_high().unwrap();         // SCL:H
    sleep(Duration::from_millis(1));
    pin.set_low().unwrap();          //SCL:L
    sleep(Duration::from_millis(1));
}

M5 製のENV.III SENSOR (QMP6988)とI2Cで接続するところまではできた*1
I2Cバスをソフトで実装している(クロックの同期とかは適当)。I2Cのバスは双方向なのだが、GPIOの入出力方向(Input/Output)を切り替えるのに、6msecぐらいかかる。デバイスからのACK/NACKの受け取りのためにOutputからInputに切り替える必要がありこのステップで必ず6msec遅延が発生する。GPIOのレジスタを直接操作するともう少し早くなるかもですが。。

//
//
//  Test Prog for I2C  Driver
//
use anyhow::Result;
use esp_idf_svc::hal::gpio::{PinDriver, Pin, Output};
use esp_idf_svc::hal::peripherals::Peripherals;
use esp_idf_svc::sys::link_patches;

use std::{thread::sleep, time::Duration};

fn main() -> Result<()> {

  link_patches();

  // Bind the log crate to the ESP Logging facilities
  esp_idf_svc::log::EspLogger::initialize_default();

  let peripherals = Peripherals::take()?;
  let mut scl = PinDriver::output(peripherals.pins.gpio2)?;
  let mut sda = PinDriver::output(peripherals.pins.gpio3)?;

  loop {

      // send start condition
      send_start_cond(&mut scl, &mut sda);

      // send device address
      let read_write:u8 = 0;      // 0: write
      send_device_address(0x70, read_write, &mut scl, &mut sda); 

      // receive ACK / NACK
      let pin_in = sda.into_input()?;
      scl.set_high()?;
      sleep(Duration::from_millis(1));
      if pin_in.is_low() {
          print!("L\n device found\n");
      }else{
          print!("H\n device is not found\n");
      }
      scl.set_low()?;
      sda = pin_in.into_output()?;
      sda.set_low()?;
      sleep(Duration::from_millis(1));

      // send stop condition
      send_stop_cond(&mut scl, &mut sda);

  }
}
/* 
    send Address  To Target Device
*/
fn send_device_address<T1: Pin,T2: Pin>(addr: u8, read_write :u8, scl: &mut PinDriver<T1,Output>, sda: &mut PinDriver<T2,Output>){

      let data = (addr << 1) + (read_write & 0x1);
      let mut mask = 0x80;

      for _ in 0..8 {
          if data & mask > 0 {
              sda.set_high().unwrap();
          }else{
              sda.set_low().unwrap();
          }
          mask = mask >> 1;
          sleep(Duration::from_millis(1));
          scl.set_high().unwrap();
          sleep(Duration::from_millis(1));
          scl.set_low().unwrap();
          sleep(Duration::from_millis(1));
      }
}

/*---------------------
    send start cond
---------------------*/
fn send_start_cond<T1: Pin,T2: Pin>(scl: &mut PinDriver<T1,Output>, sda: &mut PinDriver<T2,Output>){

    // ready....
    sda.set_high().unwrap();         // SDA:H
    scl.set_high().unwrap();         // SCL:H
    sleep(Duration::from_millis(1));

    // Start Condition
    sda.set_low().unwrap();           //  SCL:H / SDA:H -> L
    sleep(Duration::from_millis(1));
    scl.set_low().unwrap();           //  SCL:L
    sleep(Duration::from_millis(1));

}


/*---------------------
   send stop cond
 ---------------------*/
fn send_stop_cond<T1: Pin, T2: Pin>(scl: &mut PinDriver<T1,Output>, sda: &mut PinDriver<T2,Output>){

    // send stop cond
    scl.set_high().unwrap();          // SCL:H
    sleep(Duration::from_millis(1));
    sda.set_high().unwrap();         //  SCL:L / SDA: L -> H
    sleep(Duration::from_millis(1));

}

オシロで確認した波形は以下、デバイスアドレス0x70を送信して、Read/WriteのビットはWrite(0)を設定、センサからはACK(0)が返却されている

カーソル(B)の部分がStart Condition、カーソル(A)の部分がStop Condition (I2Cの仕様書ではデータが上で、クロックが下になっており、各信号が上下逆なのでちょっと見づらいですが・・)

■追記
WatchDogTimerのお世話をしていないせいか、一定周期でリセットが入る。

■追記
GPIOの操作をサブルーチンに移したところ、途端に所有権の問題が発生した。細かい所では型も違っているようだ。型も難しいが所有権のところが難しい。

error[E0507]: cannot move out of `*sda` which is behind a mutable reference
   --> examples/QMP6988.rs:46:17
    |
46  |    let pin_in = sda.into_input().unwrap();
    |                 ^^^ ------------ `*sda` moved due to this method call
    |                 |
    |                 move occurs because `*sda` has type `PinDriver<'_, T2, esp_idf_svc::hal::gpio::Output>`, which does not implement the `Copy` trait
    |
note: `PinDriver::<'d, T, MODE>::into_input` takes ownership of the receiver `self`, which moves `*sda`
   --> /home/<uid>/.cargo/registry/src/index.crates.io-6f17d22bba15001f/esp-idf-hal-0.43.1/src/gpio.rs:638:23
    |
638 |     pub fn into_input(self) -> Result<PinDriver<'d, T, Input>, EspError>
    |                       ^^^^
For more information about this error, try `rustc --explain E0507`.

&mutでサブルーチンに値を渡しているので、所有権は呼び出し側に残っていると思うのだけど、なぜ呼ばれたサブルーチン側で、ownershipがどうしたこうしたと怒られるのか。into_input関数により、データ型を変換させたから (?) 新しいデータが作り出された??
GPIOを直接サブルーチンに渡すのではなく、使いたいGPIOをまとめる構造体を作り、構造体への参照型としてサブルーチンに渡す方が良いのかも。
■追記
呼ばれた側(サブルーチン側)でGPIOの向きを変えるのがそもそもいけないのでは?と思うに至り、サブルーチンを2つに分けて、呼ぶ側でGPIOのIn/Outを切り替えてからサブルーチンを呼ぶべきなのでは?と思った(outputするだけのサブルーチン、inputするだけのサブルーチンに分けて、GPIOを切り替えてから呼び分ける)。確かに、初期設定ではoutputで宣言したのに、サブルーチン側で勝手にinputに切り替えられても困るとは言える。
とはいえ、I2Cメイン処理部を作ったとしても、オブジェクト指向で作らない限り、データと手続きを一体化できないので、GPIO関連のデータはグローバルに置かれている構造体の要素として蓄えておくしかないような気がする。
■追記
プログラミングを続け、なんとかQMP6988のレジスタの読み書きまではできた。できたものの、GPIOの操作(特にI2CのデータバスのInput/Outputの方向を変える実装)をRustで怒られずに書くのが非常に難しく、メインでGPIOをインスタンス化?して、それをサブルーチンまで実体渡しで処理してもらい、また実体をメインまで戻すような書き方しか自分にはできなかった。Struct型にGPIOのインスタンス?を閉じ込める実装は自分のスキルでは到底できないのであった。ソフト版I2Cドライバを書いたらQMP6988はさくっと繋がるだろうと考えていたがこれは甘い考えで、4日以上プログラミングしてボロボロのプログラムでなんとかレジスタが読み書きできるところまで到達したのであった。
■参考URL
rust-esp32-std-demo/src/main.rs at main · ivmarkov/rust-esp32-std-demo · GitHub
ESP32マイコンのRust開発環境(Lチカまで) #embedded - Qiita
https://www.nxp.com/docs/ja/user-guide/UM10204.pdf
ENV III Unit with Temperature Humidity Air Pressure Sensor (SHT30+QMP6988) | m5stack-store
esp-idf-hal/src/gpio.rs at master · esp-rs/esp-idf-hal · GitHub
PinDriver in esp_idf_hal::gpio - Rust
gpio - Rust
https://blog.theembeddedrustacean.com/esp32-standard-library-embedded-rust-gpio-control
rust embedded change GPIO pin from output to input - Stack Overflow
How to borrow Peripherals struct? - Embedded - The Rust Programming Language Forum
STM32 Rust halを使ってGPIOを抽象化してみる - moon's STM32づくし
Rustの借用の話をする|TechRacho by BPS株式会社
m5-docs

*1:このコードを書くのに一日かかった