Wire.hを用いたI2C通信のやり方・注意点 - nearfactory/2024-TOINIOT2 GitHub Wiki

概要

Wire.hを用いたI2C通信の方法・注意点をまとめました

動作確認済み環境

・マスター : Teensy4.1
・スレーブ : XIAO ESP32C3

全体的な注意点

・同じSDA・SCLに繋ぐマスターは1台のみにする(回路)
 → 複数のマスターが同じSDA・SCLに繋ぐと信号が狂って正常に通信できなくなる
・一度の送受信のデータは260バイト以下にする
 → 送信・受信バッファのサイズが260バイトで、それより大きいサイズのデータを一度にやりとりすることはできない


主な関数


A : Wire.begin();
B : Wire.begin(uint8_t address);

A : I2C通信を開始する (主にマスターで使用)
B : 引数で指定したアドレスとしてI2C通信を開始する (マスター・スレーブで使用)

使い方

・AB : setup()内で、I2Cのすべての通信の前に書く (必須)
・B : 引数にアドレスを指定する

注意点

・B : I2Cの仕様上、アドレスには7ビットしか使えないため 0x00 ~ 0x80 の範囲にする
・B : アドレスは他のスレーブと被らない値に指定する

サンプル

void setup(){
  Wire.begin();      // A
  Wire.begin(0x32);  // B
}


Wire.beginTransmission(uint8_t address);

指定したアドレスとの通信準備をする (主にマスターで使用)

使い方

・addressに通信したいスレーブのアドレスを指定する

A : Wire.write(uint8_t data);
B : Wire.write(const uint8_t *data, size_t quantity);

引数に指定したデータを送信バッファの末尾に追加する (マスター・スレーブで使用)
他にも様々な関数がオーバーライドされているが、最終的にすべてこれに行き着く

使い方

・A : 引数に送信するuint8_t型のデータを指定する
・B : 第一引数に送信するuint8_t配列の先頭アドレス、第二引数に要素数を指定する

注意点

・送信バッファに追加するデータは合計260バイト以下にする


Wire.endTransmission();

送信バッファに溜まったデータを送信する (主にマスターで使用)

使い方

・下のように書くことで指定したアドレスのスレーブにデータを送信できる

サンプル

void setup(){
  Wire.begin();

  Wire.beginTransmission(0x32);
  Wire.write("write");
  Wire.endTransmission();
}


Wire.available();

受信バッファのデータサイズを調べる (マスター・スレーブで使用)

使い方

・受信時・リクエスト時に使用する


Wire.read();

受信バッファから1バイト読み出す (マスター・スレーブで使用)

使い方

・Wire.available()と組み合わせて下のように書くことで、受信バッファの中身を全て読み出すことができる

サンプル

void setup(){
  Wire.begin(0x32);
}

void loop(){
  while(Wire.available()){
    Serial.print(Wire.read());
  }
}


Wire.onReceive();

マスターからデータを受信したときに実行する関数を指定する (主にスレーブで使用)

使い方

・受信時に実行したい関数を引数に指定する

注意点

・引数に指定する関数はvoid {関数名}(int {受信したデータサイズを格納する変数名});の形で宣言・定義する
・引数に記述するのは実行したい関数名だけでいい (便利)

サンプル

void receiveEvent(int size){
  while(Wire.available()){
    char c = Wire.read();
    Serial.print(c);
  }
  Serial.println();

  return;
}

void setup(){
  Wire.begin(0x32);
  Wire.onReceive(receiveEvent);
}


Wire.onRequest();

マスターからのリクエスト時に実行する関数を指定する (主にスレーブで使用)

使い方

・リクエスト時に実行したい関数を引数に指定する

注意点

・引数に指定する関数はvoid {関数名}(void);の形で宣言・定義する
・引数に記述するのは実行したい関数名だけで(ry

サンプル

void requestEvent(void){
  Wire.write("request");
  return;
}

void setup(){
  Wire.begin(0x32);
  Wire.onRequest(requestEvent);
}


Teensy4.1 - ESP32C3間でのI2Cサンプル

Teensy4.1 - ESP32C3間で、マスターからの送信・送信リクエストのスレーブ側での処理を行うサンプルコード

Teensy4.1(マスター)側コード

//Teensy4.1(マスター)側コード

#include<string>

#include<Wire.h>

using uint8_t = unsigned char;
constexpr uint8_t ESP32C3_ADDR = 0x32;

void setup() {
  Serial.begin(9600);
  Serial.println("Teensy4.1");

  Wire.begin();
}

void loop() {
  static int count=0;
  count++;
  std::string send_str = "Teensy4.1 : " + std::to_string(count);  //送信データを準備

  // スレーブへデータを送信
  int previous_send_ms = millis();                  // I2Cの送信開始時刻を取得
  Wire.beginTransmission(ESP32C3_ADDR);             // ESP32C3との通信準備を開始
  Wire.write(send_str.c_str());                     // データを送信バッファに追加
  Wire.endTransmission();                           // 送信
  int send_ms = millis() - previous_send_ms;        // 開始時刻との差分を取って通信時間を計測

  // スレーブへデータをリクエスト
  int previous_receive_ms = millis();               // I2Cのリクエスト開始時刻を取得
  std::string receive_str="";                       // 受信データ格納用文字列を宣言
  Wire.requestFrom(ESP32C3_ADDR,128);               // ESP32C3への送信リクエスト
  uint8_t received_size = Wire.read();              // 文字列の長さを示すために1文字目に格納しておいたデータを読み出す
  for(int i=0;i<received_size;i++){                 // 上で読み出した長さの回数分繰り返す
    char c = Wire.read();                           // 受信バッファから文字として1つ読み出す
    receive_str += c;                               // 文字列の末尾に格納する
  }
  int receive_ms = millis() - previous_receive_ms;  // 開始時刻との差分を取ってリクエストの処理にかかった時間を計測

  // 送信・受信したデータを出力
  Serial.printf("Send : \"%s\"(%dms),  Receive : \"%s\"(%dms) \n", send_str.c_str(), send_ms, receive_str.c_str(), receive_ms);

  delay(100);
}

ESP32C3(スレーブ)側コード

//ESP32C3(スレーブ)側コード

#include<Wire.h>

using uint8_t = unsigned char;
constexpr uint8_t ESP32C3_ADDR = 0x32;

int send_ms=0;
int receive_ms=0;

std::string send_str="";
std::string receive_str="";

void setup() {
  Serial.begin(9600);
  Serial.println("ESP32C3");

  Wire.begin(ESP32C3_ADDR);                             // ESP32C3_ADDR(0x32)としてI2Cを開始
  Wire.onReceive(receiveEvent);                         // マスターからの受信時のコールバック関数を設定
  Wire.onRequest(requestEvent);                         // マスターからの送信リクエスト時のコールバック関数を設定
}

void loop() {
  // 送受信結果を出力
  Serial.printf("Send : \"%s\"(%dms),  Receive : \"%s\"(%dms) \n", send_str.c_str(), send_ms, receive_str.c_str(), receive_ms);
  delay(100);
}

// マスターからの送信に対するコールバック関数
void receiveEvent(int size){
  int previous_receive_ms = millis();                   // I2Cの受信開始時刻を取得

  receive_str = "";                                     // 受信文字列をクリア
  while(Wire.available()){                              // 受信バッファが空になるまで繰り返す
    char c = Wire.read();                               // 受信バッファから1文字取り出す
    receive_str += c;                                   // 受信文字列の末尾に追加
  }

  receive_ms = millis() - previous_receive_ms;          // 開始時刻との差分を取って受信にかかった時間を計測

  return;
}

// マスターからのリクエストに対するコールバック関数
void requestEvent(){
  int previous_send_ms = millis();                      // リクエストの開始時刻を取得
  static int count=0;                                   // カウンタを宣言
  
  send_str = "ESP32C3 : " + std::to_string(count/2);    // 送信文字列を準備
  auto length = static_cast<char>(send_str.length());   // 文字列の長さをchar型(文字)として取得
  send_str =  length + send_str;                        // 送信文字列の先頭に、上の文字数を示すデータを追加
  count++;                                              // カウンタを1増やす
  Wire.write(send_str.c_str());                         // 送信バッファに追加

  send_ms = millis() - previous_send_ms;                // 開始時刻との差分を取ってリクエストの処理にかかった時間を計測

  return;
}


番外編

double等の数値データを無理やり*uint8_tに変換する

割と危険な方法ではあるが、ポインタの型を変換することで無理やり変換してデータを詰めることができる

サンプル

#include<iostream>

using uint8_t = unsigned char;

using namespace std;

int main(){
    double val[] = {0.8, 1.6, 3.2, 6.4, 12.8, 25.6};

    // double配列の先頭アドレスをuint8_t*に変換してptrへ代入
    auto ptr = reinterpret_cast<uint8_t*>(val);
    // 後から復元するために元配列のサイズ(バイト)を保存
    auto element = sizeof(val) / sizeof(double);
    auto size = (sizeof(double) / sizeof(uint8_t)) * element;

    // ポインタを再度変換して復元する
    auto *reverse = reinterpret_cast<double*>(ptr);

    // 元の値を出力
    cout << "val:";
    for(auto x : val) cout << x << " ";
    cout << endl;

    // 元配列のサイズを出力
    cout << "size:" << size << endl;

    // 変換後の配列を文字列として表示
    cout << "str:";
    for(int i=0;i<size;i++) cout << ptr[i];
    cout << endl;
  
    // 元配列を復元して出力 
    cout << "reverse:"; 
    for(int i=0;i<element;i++) cout << reverse[i] << " ";

    return 0;
}

参考

・I2C通信(Wireクラス) : https://www.renesas.com/jp/ja/products/gadget-renesas/reference/gr-kurumi/library-wire

2024.05.26 maguronoosushi0807

⚠️ **GitHub.com Fallback** ⚠️