背景
Arduinoでi2cのマスターやスレーブを作れます。しかしながら、Arduinoの解説ページに紹介されるマスターとスレーブの例(MasterWriterとMasterReader)では、レジスタアドレスを利用した情報のやりとり方法は解説されていません。
やりたいと思う度に検索して試行錯誤する流れを何回か経験したので、その流れを短くするために自分の言葉とコードで、大まかな解説を記事として残します。
2020.10.18 追記
この記事で紹介している関数をライブラリにまとめたので、マスター(セントラル)とスレーブ(ペリフェラル)を作る際は、こちらを利用すると楽になると思います。
ArduinoでI2Cのペリフェラル(スレーブ)を作る時に便利なクラスを作ってみた
wire_asukiaaa
使ったもの
- Arduino IDEかPlatformIOをインストールしたPC
この記事はPlatformIOで動作確認しましたが、Arduino IDEでも同じように動くと思います。 - ProMicro(Arduino互換機)2台
マスター用とスレーブ用です。 - ブレッドボード 1枚
はんだ付けせず、これの上で配線します。 - ジャンパワイヤ 1セット
配線に利用します。 - 10k〜4.7kの抵抗 2本
i2cは信号線をプルアップ(高い電圧に抵抗で接続)する必要があるため、SDAとSCLの線と3V3か5Vを接続します。
Arduinoに使われるマイコンは2.5V以上をHIGHとみなすマイコンが多いため、大体3V3でも5Vでもi2c通信できます。
配線
先ほど紹介した材料をこのように接続しました。- ProMicroの、RAW、SDA(D2)、SCL(D3)、GNDを接続
- SDAとSCLの信号線をどちらかのProMicroのVINに抵抗で接続してプルアップ
例として作るもの
16ビットのデータをやり取りするスレーブとマスターを作ります。作成するスレーブのレジスタマップはこちらです。
スレーブのデバイスアドレス: 0x08
| レジスタアドレス | 役割 | 読み書き |
|---|---|---|
| 0x00 | uint16_tの上位バイト | 読み書き可能 |
| 0x01 | uint16_tの下位バイト | 読み書き可能 |
| 0x02 | 0x00と0x01に書き込まれた値に 2を加算したuint16_tの上位バイト |
読み専用 |
| 0x03 | 0x00と0x01に書き込まれた値に 2を加算したuint16_tの上位バイト |
読み専用 |
通信の流れはこちらです。
書き込み時
- マスターからスレーブへ、書き込みの起点となるレジスタアドレスとデータを送信
- スレーブで1バイト目をレジスタアドレス、それ以後をデータとして処理
- 書き込み用のレジスタに値が書き込まれたら、それに2を加算した値を読み込み用のレジスタに書き込み
読み取り時
- マスターからスレーブへ、読み取りの起点となるレジスタアドレスを送信
- マスターからスレーブへ、読み取りを要求
- スレーブは、起点となるレジスタアドレス以後のデータをマスターに送信
- マスターで情報を受け取る
マスターのプログラム
コードを共有して、要所を解説します。#include <Arduino.h>
#include <Wire.h>
#define SLAVE_ADDRESS 0x08
#define SLAVE_REGISTER_WRITE_DATA_UPPER 0x00
#define SLAVE_REGISTER_WRITE_DATA_LOWER 0x01
#define SLAVE_REGISTER_READ_DATA_UPPER 0x02
#define SLAVE_REGISTER_READ_DATA_LOWER 0x03
uint8_t writeToDevice(TwoWire &wire, uint8_t deviceAddress, uint8_t *data, uint8_t length, bool stop=true) {
wire.beginTransmission(deviceAddress);
wire.write(data, length);
return wire.endTransmission(stop);
}
uint8_t writeToRegister(TwoWire &wire, uint8_t deviceAddress, uint8_t registerAddress, uint8_t *data, uint8_t length, bool stop=true) {
wire.beginTransmission(deviceAddress);
wire.write(registerAddress);
wire.write(data, length);
return wire.endTransmission(stop);
}
uint8_t readFromDevice(TwoWire &wire, uint8_t deviceAddress, uint8_t *data, uint8_t length, bool stop=true) {
const uint8_t resultLen = wire.requestFrom(deviceAddress, length, stop);
uint8_t dataIndex = 0;
while (wire.available()) {
char d = wire.read();
if (dataIndex < length) {
data[dataIndex] = d;
++dataIndex;
}
}
return resultLen;
}
uint8_t readFromRegister(TwoWire &wire, uint8_t deviceAddress, uint8_t registerAddress, uint8_t *data, uint8_t length, bool stop=true) {
uint8_t result = writeToDevice(wire, deviceAddress, ®isterAddress, 1, false);
if (result != 0) {
return 0;
}
return readFromDevice(wire, deviceAddress, data, length, stop);
}
void writeAndPrintResult(uint16_t value) {
uint8_t data[2];
data[0] = value >> 8;
data[1] = value & 0xff;
if (writeToRegister(Wire, SLAVE_ADDRESS, SLAVE_REGISTER_WRITE_DATA_UPPER, data, 2) == 0) {
Serial.print("Wrote data: ");
Serial.print(value);
Serial.print(" ( ");
Serial.print(data[0], HEX);
Serial.print(" ");
Serial.print(data[1], HEX);
Serial.println(" )");
} else {
Serial.println("Failed to write data");
}
}
void readAndPrintResult() {
uint8_t data[2];
uint8_t readLen = readFromRegister(Wire, SLAVE_ADDRESS, SLAVE_REGISTER_READ_DATA_UPPER, data, 2);
if (readLen != 0) {
uint16_t value = 0;
if (readLen >= 2) {
value = (uint16_t)data[0] << 8 | data[1];
}
Serial.print("Read data: " + String(value) + " (");
for (uint8_t i=0; i<readLen; ++i) {
Serial.print(" ");
Serial.print(data[i], HEX);
}
Serial.println(" )");
} else {
Serial.println("Failed to read data");
}
}
void setup() {
Wire.begin();
Serial.begin(115200);
}
void loop() {
writeAndPrintResult(1000);
readAndPrintResult(); // expects 1002
writeAndPrintResult(0xff);
readAndPrintResult(); // expects 257 (0x0101)
Serial.println("at " + String(millis()));
delay(1000);
}
要所を解説します。
writeToDevice, writeToRegister, readFromDevice, readFromRegisterは、レジスタを操作するi2cマスターのプログラムを書く時に必要になると思う関数です。
記事のコードはMITとしますので、必要とあればコピーしてご自由にお使いください。
レジスタに値を書き込む場合は、1バイト目に起点となるレジスタアドレスを書き込み、それ以後にデータを書き込みます。
uint8_t writeToRegister(TwoWire &wire, uint8_t deviceAddress, uint8_t registerAddress, uint8_t *data, uint8_t length, bool stop=true) {
wire.beginTransmission(deviceAddress);
wire.write(registerAddress);
wire.write(data, length);
return wire.endTransmission(stop);
}
レジスタから値を読み込む場合は、読み込みたいレジスタアドレスを書き込んでから読み込みを行います。
uint8_t readFromDevice(TwoWire &wire, uint8_t deviceAddress, uint8_t *data, uint8_t length, bool stop=true) {
const uint8_t resultLen = wire.requestFrom(deviceAddress, length, stop);
uint8_t dataIndex = 0;
while (wire.available()) {
char d = wire.read();
if (dataIndex < length) {
data[dataIndex] = d;
++dataIndex;
}
}
return resultLen;
}
uint8_t readFromRegister(TwoWire &wire, uint8_t deviceAddress, uint8_t registerAddress, uint8_t *data, uint8_t length, bool stop=true) {
uint8_t result = writeToDevice(wire, deviceAddress, ®isterAddress, 1);
if (result != 0) {
return 0;
}
return readFromDevice(wire, deviceAddress, data, length, stop);
}
通信相手が居ないなど、i2c通信できない状態だとendTransmissionで0以外の値が帰ってきます。
書き込み時のエラー処理は、その値を参考に行うのが良いと思います。
1000を送ったら1002が読み取れて、255(0xff)を送ったら257(0x0101)が読み取れるることを想定しています。
void loop() {
writeAndPrintResult(1000);
readAndPrintResult(); // expects 1002
writeAndPrintResult(0xff);
readAndPrintResult(); // expects 257 (0x0101)
Serial.println("at " + String(millis()));
delay(1000);
}
スレーブのプログラム
コードを共有して、要所を解説します。#include <Arduino.h>
#include <Wire.h>
#define SLAVE_ADDRESS 0x08
#define SLAVE_REGISTER_WRITE_DATA_UPPER 0x00
#define SLAVE_REGISTER_WRITE_DATA_LOWER 0x01
#define SLAVE_REGISTER_READ_DATA_UPPER 0x02
#define SLAVE_REGISTER_READ_DATA_LOWER 0x03
#define REGISTER_LEN 4
uint8_t registers[REGISTER_LEN];
uint8_t registerIndex = 0;
void updateReadData() {
uint16_t wroteData = (uint16_t) registers[SLAVE_REGISTER_WRITE_DATA_UPPER] << 8 | registers[SLAVE_REGISTER_WRITE_DATA_LOWER];
wroteData += 2;
registers[SLAVE_REGISTER_READ_DATA_UPPER] = wroteData >> 8;
registers[SLAVE_REGISTER_READ_DATA_LOWER] = wroteData & 0xff;
}
void receiveEvent(int _length) {
uint8_t readCount = 0;
bool wroteData = false;
Serial.print("Receive:");
while (Wire.available() > 0) {
uint8_t d = Wire.read();
Serial.print(" ");
Serial.print(d, HEX);
if (readCount == 0) {
registerIndex = d;
} else {
if (registerIndex == SLAVE_REGISTER_WRITE_DATA_LOWER ||
registerIndex == SLAVE_REGISTER_WRITE_DATA_UPPER) {
wroteData = true;
registers[registerIndex] = d;
}
++registerIndex;
}
++readCount;
}
Serial.println("");
if (wroteData) {
updateReadData();
}
}
void requestEvent() {
if (registerIndex < REGISTER_LEN) {
Wire.write(®isters[registerIndex], REGISTER_LEN - registerIndex);
}
}
void setup() {
Wire.begin(SLAVE_ADDRESS);
Wire.onReceive(receiveEvent);
Wire.onRequest(requestEvent);
Serial.begin(115200);
}
void loop() {
delay(100);
}
要所を解説します。
書き込みの1バイト目はレジスタアドレスの操作に利用します。
void receiveEvent(int _length) {
while (Wire.available() > 0) {
if (readCount == 0) {
registerIndex = d;
}
}
}
意図しない書き込みを防ぐため、書き込み可能なレジスタアドレスへの操作のときだけ値を更新します。
void receiveEvent(int _length) {
while (Wire.available() > 0) {
if (readCount == 0) {
} else {
if (registerIndex == SLAVE_REGISTER_WRITE_DATA_LOWER ||
registerIndex == SLAVE_REGISTER_WRITE_DATA_UPPER) {
registers[registerIndex] = d;
}
}
}
}
書き込み可能なレジスタへの書き込みがあったら、読み込み用レジスタを更新します。
void receiveEvent(int _length) {
bool wroteData = false;
while (Wire.available() > 0) {
if (readCount == 0) {
} else {
if (registerIndex == SLAVE_REGISTER_WRITE_DATA_LOWER ||
registerIndex == SLAVE_REGISTER_WRITE_DATA_UPPER) {
wroteData = true;
}
}
}
if (wroteData) {
updateReadData();
}
}
マスターがどの長さのrequestを要求しているかスレーブでは分かりませんが、起点となるレジスタアドレスから送れる有効な情報を全てwriteしておけば、必要な分だけマスターに送信してくれます。
(requestEventでマスターが要求するより短い長さしかwriteしていない、もしくは全くwriteしていない場合、writeが足りないバイトは0や0xffなどの値になるようでした。)
void requestEvent() {
if (registerIndex < REGISTER_LEN) {
Wire.write(®isters[registerIndex], REGISTER_LEN - registerIndex);
}
}
動作確認
上記のマスターとスレーブを動かし、シリアル通信でログを確認します。マスターのログを見ると、期待通りに1000を書き込んだら1002を読み込めて、255を書き込んだら277が読み込めていることが分かります。
Wrote data: 1000 ( 3 E8 ) Read data: 1002 ( 3 EA ) Wrote data: 255 ( 0 FF ) Read data: 257 ( 1 1 ) at 2010 Wrote data: 1000 ( 3 E8 ) Read data: 1002 ( 3 EA ) Wrote data: 255 ( 0 FF ) Read data: 257 ( 1 1 ) at 3014
スレーブのログを見ると、アドレス0に値が書き込まれ、読み込み前にアドレス2が指定されていることが分かります。
Receive: 0 3 E8 Receive: 2 Receive: 0 0 FF Receive: 2 Receive: 0 3 E8 Receive: 2 Receive: 0 0 FF Receive: 2
まとめ
レジスタアドレスを扱うi2cのマスターとスレーブをArduinoで作れました。この例が、未来の自分も含めて、同じようなことをしたいと思っている方の参考になれば嬉しいです。
参考
今回の記事作成に利用したプログラムをgithubでも共有します。https://github.com/asukiaaa/arduino_i2c_practice
背景でも共有した、Arduino本家のi2cのマスターとスレーブの解説ページです。
MasterWriter
MasterReader
変更履歴
2019.07.03readFromRegisterの中でendTransmissionにfalseを渡して処理を継続する記述をしていましたが、endTransmissionの戻り値が0で無ければそこで通信を停止する可能性があるため、falseを渡さず一旦やりとりを停止するようにしました。
2020.10.18
wireに関する処理をライブラリにまとめたので、それの紹介記事のリンクを背景に追記しました。
2021.03.21
TeensyでreadFromRegisterの中のレジスタ設定処理のendTransmissionを引数なし(true)で終えるとteensyで読み取れない現象に遭遇したため、endTransmissionをfalseとして処理を継続する記述に変更しました。


0 件のコメント :
コメントを投稿