Wire.hを用いたI2C通信のやり方・注意点 - nearfactory/2024-TOINIOT2 GitHub Wiki
Wire.hを用いたI2C通信の方法・注意点をまとめました
・マスター : Teensy4.1
・スレーブ : XIAO ESP32C3
・同じSDA・SCLに繋ぐマスターは1台のみにする(回路)
→ 複数のマスターが同じSDA・SCLに繋ぐと信号が狂って正常に通信できなくなる
・一度の送受信のデータは260バイト以下にする
→ 送信・受信バッファのサイズが260バイトで、それより大きいサイズのデータを一度にやりとりすることはできない
A : I2C通信を開始する (主にマスターで使用)
B : 引数で指定したアドレスとしてI2C通信を開始する (マスター・スレーブで使用)
・AB : setup()内で、I2Cのすべての通信の前に書く (必須)
・B : 引数にアドレスを指定する
・B : I2Cの仕様上、アドレスには7ビットしか使えないため 0x00 ~ 0x80 の範囲にする
・B : アドレスは他のスレーブと被らない値に指定する
void setup(){
Wire.begin(); // A
Wire.begin(0x32); // B
}
指定したアドレスとの通信準備をする (主にマスターで使用)
・addressに通信したいスレーブのアドレスを指定する
引数に指定したデータを送信バッファの末尾に追加する (マスター・スレーブで使用)
他にも様々な関数がオーバーライドされているが、最終的にすべてこれに行き着く
・A : 引数に送信するuint8_t型のデータを指定する
・B : 第一引数に送信するuint8_t配列の先頭アドレス、第二引数に要素数を指定する
・送信バッファに追加するデータは合計260バイト以下にする
送信バッファに溜まったデータを送信する (主にマスターで使用)
・下のように書くことで指定したアドレスのスレーブにデータを送信できる
void setup(){
Wire.begin();
Wire.beginTransmission(0x32);
Wire.write("write");
Wire.endTransmission();
}
受信バッファのデータサイズを調べる (マスター・スレーブで使用)
・受信時・リクエスト時に使用する
受信バッファから1バイト読み出す (マスター・スレーブで使用)
・Wire.available()と組み合わせて下のように書くことで、受信バッファの中身を全て読み出すことができる
void setup(){
Wire.begin(0x32);
}
void loop(){
while(Wire.available()){
Serial.print(Wire.read());
}
}
マスターからデータを受信したときに実行する関数を指定する (主にスレーブで使用)
・受信時に実行したい関数を引数に指定する
・引数に指定する関数は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);
}
マスターからのリクエスト時に実行する関数を指定する (主にスレーブで使用)
・リクエスト時に実行したい関数を引数に指定する
・引数に指定する関数はvoid {関数名}(void);
の形で宣言・定義する
・引数に記述するのは実行したい関数名だけで(ry
void requestEvent(void){
Wire.write("request");
return;
}
void setup(){
Wire.begin(0x32);
Wire.onRequest(requestEvent);
}
Teensy4.1 - ESP32C3間で、マスターからの送信・送信リクエストのスレーブ側での処理を行うサンプルコード
//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(スレーブ)側コード
#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;
}
割と危険な方法ではあるが、ポインタの型を変換することで無理やり変換してデータを詰めることができる
サンプル
#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