半導体エンジニアとして、ハードウェアをいじくる機会が多く、お家でもお勉強がてらデバイスを触ってみるのだが、I2Cマスターとして使えるデバイスって意外に入手性や使い勝手が悪い。この個人的課題を解決するために、巨人の肩に乗りつつ、Arduinoを使ってI2C通信してみた。まだまだ改善の余地ありそうだが、関数も工夫して作ったので、それなりに便利に使えるものになったと思う。
想定読者
- いつかの自分。
- オープンソースハードウェアでI2C通信してセンサーなどから情報を得たい/制御したい人。
背景
ArduinoでI2C通信ができることを知った
Arduinoを30分でI2C通信するや[Arduinoでi2c通信でセンサから値をとってみる]
(https://qiita.com/hurusu1006/items/f493ee4eb9998d5bd740)などの記事を参考にすれば、いとも簡単にArduinoでI2C通信ができることを知った。
もう少し深掘りしたかった。
デバイスから1byteを取得する、といった簡易的な接続以外に、デバイスの任意のレジスタアドレスにレジスタデータを書き込む、任意のレジスタアドレスからレジスタデータを読み込む、といったことがしたかった。自分がやりたいことが網羅的に書かれた記事はなかったため執筆した。
この記事の目的
- 想定読者の役に立つこと。
- この記事の内容を昇華するような助言に出会うこと。
この記事のポリシー
- 過去の偉人たちを尊重して情報を参照しまくる。
- 丸パクリすれば動く、ではなく原理や考え方を説明する。
- デバイスアドレス、レジスタアドレス、レジスタデータのどれについて言及しているか明示する。
- 以降は明記しないが、Arduinoは常にI2Cマスター(命令を出す側)である。
- コードは全てArduino IDE(C++ベース)の文法に則す。
詳細
I2Cとは
Wikipedia参照。
フィリップスさんが作ったバスの規格で、たくさんの機器で使われています。
SPIやUARTなどとやりたいことは近くてできることがちょっとずつ違う感じ。
デバイスアドレス、レジスタアドレス、レジスタデータの指定の仕方
早速本題。
I2C通信ではデバイスアドレス、レジスタアドレス、レジスタデータをそれぞれ指定する必要がありまして、下記のように宣言する。
int DeviceAddress = 10; // 0x0Aと書いても同じ意味
int Register_Address = 20; // 0x14と書いても同じ意味
int Register_Data = 30; // 0x1Eと書いても同じ意味
3つとも変数として置くことができ、整数である必要がある。
(後述するが、配列でももちろん大丈夫。)
これらの変数は、一般的に、デバイスのデータシートなどで16進数で書かれていることが多いが、10進数で書くこともできる。
まずI2C通信ができるかどうか確認してみる
基本的には下記の記事を参考すれば動作確認できるので、記事に従って手を動かす。
Arduinoを30分でI2C通信する
[Arduinoでi2c通信でセンサから値をとってみる]
(https://qiita.com/hurusu1006/items/f493ee4eb9998d5bd740)
Arduino2つでI2C通信
wire構文についてもう少しブレイクダウン(書き込み編)
I2Cで書き込むときは下記のようにコードを書く。
int DeviceAddress = 0x0A; // デバイスアドレス
int Register_Address = 0x14; // レジスタアドレス
int Register_Data = 0x1E; // レジスタドレスに書き込みたい値
Wire.beginTransmission(Device_Address);
Wire.write(Register_Address);
Wire.write(Register_Data);
Wire.endTransmission(false);
ちなみに、もう少し綺麗な書き方として
Wire.write(Register_Address);
Wire.write(Register_Data);
の部分は
Wire.write(Register_Address, Register_Data);
って書いても多分大丈夫。
(自分で確かめたことはない。)
wire構文についてもう少しブレイクダウン(読み出し編)
Wire構文でレジスタデータを読み出すコードはちょっとわかりにくくて、読み出し(read)したい時も、レジスタアドレスの指定の時にはWire.writeを使う。
int DeviceAddress = 0x0A; // デバイスアドレス
int Register_Address = 0x14; // レジスタアドレス
int Register_Data = 0; // レジスタデータを格納する変数
Wire.beginTransmission(DeviceAddress);
Wire.write(Register_Address);
Wire.endTransmission(false);
Wire.requestFrom(DeviceAddress, 1,true);
Register_Data = Wire.read();
wire構文と配列を使ってレジスタにアクセスする。
レジスタアドレスやレジスタデータを配列で指定してあげると、連続して書くのが楽になる。例えばこんな感じ。レジスタアドレスnに対してレジスタデータ10*nを書き込む時の宣言とfor文でのそれらの読み出し。
int Register_Address[3]={
1, // レジスタアドレス1
2, // レジスタアドレス2
3 // レジスタアドレス3
}
int Register_Data[3]={
10, // レジスタデータ1
20, // レジスタデータ2
30 // レジスタデータ3
}
// レジスタアドレスnにレジスタデータ10*nを書き込む関数
void WriteRegister(){
for (i=0; i<4; i++) {
Wire.beginTransmission(Device_Address);
Wire.write(Register_Address[i]);
Wire.write(Register_Data[i]);
Wire.endTransmission(false);
}
}
どのようなデータが書き込まれたかシリアルモニター上で確認する
私は、Arduinoが何をしているか把握するために、シリアルモニターを活用する。
Serial.print(Register_Address[i],HEX);
と書けば、シリアルモニターに配列の中身が表示される。しかし、このままだと、見た目に少々の問題あり。Register_Address[i]の値が16未満の場合は1桁(0からFまでの16進数)で表されるのに対して、Register_Address[i]の値が16以上の場合は2桁(10からFFまでの16進数)で表される。桁があっていないと気持ち悪いので、どんな場合でも2桁(0FやFFなど)で表したいと思い、桁数を調整する関数を用意した。
void Insert0x0(int num){
Serial.print("0x");
if (num<16){ Serial.print("0"); }
else Serial.print("");
}
この関数は、0xをシリアルプリントして、
引数が16未満の場合(15以下の場合)には0を追記、
引数が16以上の場合には何も追記しない、という関数になっている。
(つまり0x0を挿入したり0xだけ挿入したりする関数)
この関数を使うと、シリアルモニターで確認した時に表示が綺麗になる。
シリアルモニター上で書き込み(write)と読み出し(read)のどちらかを選択
ユーザーとしては、I2C通信に置いて、書き込みしたいか読み出ししたいかのどちらかなので、シリアルモニター上から指定できるようにする。シリアルモニターで読み込んだ値は
int x=0;
x = Serial.read();
で変数xに取り込むことができる。(ASCIコードで定義される10進数として取り込まれる。)wは119、rは114らしいので、これらの値と実際に取り込まれた値を比較して、ユーザーがどちらの動作をしたいのか、判別する。
int w=119;
int r=114;
void loop() {
delay(100);
Serial.println("Please send command either w or r");
delay(1000);
Serial.print("Serial.available()=");
Serial.println(Serial.available());
if(Serial.available()>0){
Serial.println("Serial available!");
int x=0;
x = Serial.read();
Serial.print("x=");
Serial.println(x,DEC);
if(x==w){
Serial.println("Write");
WriteReg();
}
if(x==r){
Serial.println("Read");
ReadReg();
}
}
}
関数を合体
※先にごめんなさいしておくと、2019年7月20日現在では、このコードそのものはうまく動くかどうか自分の手で確かめていません。基本原理の確認は自分の手で行いましたが、動作確認したコードからわかりやすくするためにいくつか変更を加えています。間違いを見つけましたら、どうかご一報くださいませ。
#include <Wire.h>
void setup() {
Wire.begin();
Serial.begin(9600);
}
int i=0; //initialize
int w=119;
int r=114;
int Device_Address = 10;
int Register_Address[3]={
1, // Register_Address 1
2, // Register_Address 2
3 // Register_Address 3
};
int Register_Data[3]={
10, // Register_Address 1
20, // Register_Address 2
30, // Register_Address 3
};
void loop() {
delay(100);
Serial.println("Please send command either w or r");
delay(1000);
Serial.print("Serial.available()=");
Serial.println(Serial.available());
if(Serial.available()>0){
Serial.println("Serial available!");
int x=0;
x = Serial.read();
Serial.print("x=");
Serial.println(x,DEC);
if(x==w){
Serial.println("Write");
WriteReg();
}
if(x==r){
Serial.println("Read");
ReadReg();
}
}
}
void WriteReg(){
for (i=0; i<4; i++) {
Wire.beginTransmission(Device_Address);
Wire.write(Register_Address[i]);
Wire.write(Register_Data[i]);
Wire.endTransmission(false);
Insert0x0(Register_Address[i]);
Serial.print(Register_Address[i],HEX);
Serial.print("=");
Insert0x0(Register_Data[i]);
Serial.println(Register_Data[i], HEX);
delay(0);
} while(1){}//finalize
}
void ReadReg(){
for (i=0; i<4; i++) {
Wire.beginTransmission(Device_Address);
Wire.write(Register_Address[i]);
Wire.endTransmission(false);
Wire.requestFrom(Device_Address, 1,true);
Register_Data[i] = Wire.read();
Insert0x0(Register_Address[i]);
Serial.print(Register_Address[i],HEX);
Serial.print("=");
Insert0x0(Register_Data[i]);
Serial.println(Register_Data[i],HEX);
delay(0);
} while(1){}//finalize
}
void Insert0x0(int num){
Serial.print("0x");
if (num<16){ Serial.print("0"); }
else Serial.print("");
}
※実はこのコードのif(Serial.available()>0){}は、2019年7月20日現在で、上手く機能しない場合があることが確認できています。具体的には、I2C通信ができていようがいまいが、このif文のループの中に入ってしまいます。原因は全くの不明です。ご助言いただけると嬉しいです。いちおう、I2C通信での読み書きは問題なく行えるようです。
結果
- ArduinoのI2C通信でデバイスのレジスタにアクセスできるソースコードを書くことができた。
まとめ
- ArduinoでI2C通信できる。
- I2C通信でデバイスのレジスタにアクセスできる。
- デバイスのレジスタにアクセスできると、レジスタデータを読み書きできる。
編集履歴
2019/08/18
void Insert0x0(int num){...}の内容を修正。
修正前) if (num<17){ Serial.print("0"); }
修正後) if (num<16){ Serial.print("0"); }
0x00~0x0Fは二進数に直すと0~15なので、引数が「16未満」の場合に0を追記する、が正しいため。また、この修正に付随する説明文を修正。