Pro MicroでI2Cを動かす (3) - ピン状態の判定
お知らせ: この記事の内容を大幅に加筆修正してまとめた書籍ができました。まとまった情報が欲しい場合はぜひ確認してみてください。
シリーズ3つ目のこの記事では、繋いだデバイスの状態を受け取ります。
前回はI2Cデバイスを発見したので、次に対象のアドレスでボタンが押されたかを判定してみます。今回繋がっている機器はアドレス 0x20
なので、これに対して初期化処理などを行い、ピンのhigh/lowを確認します。
用意するパーツ
スイッチを接続するだけです。
- 動作確認用スイッチx1
- リセット用スイッチと同じものでOK
ここまでに使ったもの
-
ブレッドボード(BB-801 など)x1
-
リセット用スイッチx1
-
ジャンパーワイヤ x沢山
-
Pro MicroとPCを繋ぐケーブル
-
MCP23017 x1
-
1kΩの抵抗x2
-
ブレッドボード(BB-801 など)x1
- ブレッドボードを分けたい場合。1つで収まる場合はそれでも良い
配線
データシートを読み解く(ピン関連)
MCP23017
がどのようなピンを持っているかをデータシートから確認しましょう。なお、データシートを探す際は名前でググることもできますし、秋月などパーツサイトにあるならそこから辿ることができます。
データシートは論文と同様、最初にサマリーが書かれています。タイトルとサマリーを見て雰囲気を把握し、必要に応じて詳細を見るのが無難です。親切なものには参考実装も書かれているので、一通り見てみると手間が省けます。
ここからは、MCP23017
のデータシートを使って読み方を学んでいきます。
1ページ目のサマリーを読むだけでも、色々な情報がありますね。
-
これ1つで16bitの入出力ができる
-
MCP23S17
という同族のICもあり、こちらはSPI通信をするらしい。間違えないよう注意! -
400kHzのI2C通信(Fast-mode)に対応
-
状態リセットピンもある
-
5Vでも3.3Vでも動く
- USBの電圧は5V、Pro Microは3.3Vに変換されて出てくることが多い
-
GPA7、GPB7のピンは注意が必要
他にも割り込み関連の機能も書かれていますが、使わないので無視します。ピンアサインも掲載されています。今後何度も見ることになるやつですね。
何度も見ることになるやつ
Pro Microで見なかったピンがいくつかありますが、基本的に同じです。
VDD、VSSはそれぞれVCCとGNDだと思えばOKです。DrainとSourceの略ですが、利用者側として気にすることがあるのかは謎です。詳しい人から見ると違う模様。詳細はこちら: 【VCC、VEE、VDD、VSSとは?】『違い』と『使い分け』について!
SCKはSCLと同じです。大変むずかしい。ただしSCKと書かれていてもI2Cじゃない場合もあるので、ちゃんと確認して使いましょう。
途中の難しい波形ページを無視してピンの詳細を見てみると、汎用入出力品(GPIO)について書かれています。ここを見ると、GPxxを使うことで入力を受け取れそうです。
ここで要確認なのは、 internal weak pull-up resistorと書かれているところです。このICのGPIOは内部にプルアップレジスタを持っており、自分で用意する必要がありません。楽ちんですね。また、 Can be
と書かれていることから分かるとおり、有効無効を変更することが出来ます。
GPIO以外の入力ピンを見てみると、Must be externally biased.
と書かれています。ちゃんとGNDやVCCに繋いであげないといけない、ということですね。ここがオープンだとアドレスがフラフラしたり、勝手にリセットされて酷い目に会います。実際会いました。
なお、使わない入力ピンはVCCかGNDに繋ぎます。さらにプルアップまたはプルダウンするのが推奨です。理想的には直接VCC/GNDに繋げば問題ないはずですが、ノイズの影響や不慮のショートなどで良くないことが起き得るので、ちゃんと組む時はプルアップしておくと決めるのが良いとされています(参考1、参考2)。
なお、NCピンは入力ですが、IC内部で繋がっていないようなので放置しても大丈夫そうです。一応GNDに繋いでおくと安心ですが、今回は放置します。
ブレッドボードに実装
前回ふんわりと繋いだ配線を理解した気がしたところで、スイッチを追加してみましょう。ここでは GPB0
に接続します。
実績: I2Cデバイスにスイッチを追加
先述したとおり内部のプルアップを使うため、GPB0ピン → スイッチ → GNDというだけのシンプルな配線になります。
状態読み取りの基礎
ここからは、接続後のプログラム作成に重要な部分です。
データシートを読み解く(レジスタ関連)
データシートからピンの状態を読むための処理を確認します。ただ、全てを読むのは大変辛いので、要点だけ抽出して紹介してゆきます。
まず、設定可能なレジスタの一覧を見てみましょう。
IOCON.BANKの値によって、設定のアドレスも変わるというややこしさです。デフォルトはBANK=0なので、これは変更しないで進めます。
沢山ありますが、大事なのは IODIRx
、 GPPUx
、 GPIOx
の3つです。また、IPOLx
も便利です。それぞれAピン列、Bピン列があります。これらの詳細を見てみましょう。
IODIR
各ピンの入出力設定を行います。1で入力、0で出力、デフォルト値は全て0(出力)です。今回は入力したいので、全ピンを1にすることで達成できそうです。
ただし、Noteにあるとおり、最上位ビットは0である必要があります。
GPPU
こちらはプルアップ設定です。IC内部にプルアップ抵抗が入っていると先述しましたが、ここの設定で有効無効を切り替えられます。1で有効、0で無効、デフォルト値は0です。
100kΩの抵抗が入っていると書かれていますね。プルアップ抵抗としては弱いので、必要に応じて外部に用意したものを使うのも検討されます。
IPOL
入力の極性(Low/High)を反転して教えるかどうかの設定です。デフォルトはそのままです。
一見何の役に立つのか分かりにくい設定ですが、実は便利なシロモノです。後々使い方を見てゆきます。
GPIOx
これは上記の設定レジスタと異なり、値の読み取りに利用します。このレジスタアドレスを指定して読み取りを行うと、それぞれのピンの列の値が返ってきます。
読み取り方法
MCP23017からピン状態を読み取るためには、読み取り指令をPro Microから出します。するとbyte単位で返ってくるので、Pro Micro側でいい感じに処理をします。MCP23017は16bitあるので、2回この処理をすると全てのピン情報を取り出せます。
挙動を変更するには、レジスタに設定値を書き込みます。ピンから読み取る前のセットアップ段階で書き込むことで、最初の読み取りからその設定を反映することが出来ます。
レジスタの設定値は、電源が入った時にデフォルト値で指定されます。要確認です。今回はMCP23017側のリセットを変更できないので、強制的にデフォルト値にしたい場合はPro Microの電源を抜き挿ししましょう。
プログラム作成(Arduino標準ライブラリ編)
では実際に入力を確認してみましょう。
MCP23017は広く使われている模様で、便利なライブラリもあります。まずは標準ライブラリで記述し、その後に便利なライブラリを使ってみましょう。
レジスタへの書き込み
レジスタへの書き込みは、 beginTransmission
を使い接続を確立した後、write
を使ってアドレスと値を書き込み、送信終了したらendTransmission
を叩きます。
void writeRegister(byte i2c_addr, byte addr, byte v) {
Wire.beginTransmission(i2c_addr);
Wire.write(addr);
Wire.write(v);
Wire.endTransmission();
}
GPIOからの読み取り
ピン状態の読み取りには2ステップ必要です。まずbeginTransmission
、write
、endTransmission
を書き込みと同様に行います。ここで、書き込むアドレスにはGPIOxを指定します。
int readGPIO(byte i2c_addr) {
Wire.beginTransmission(i2c_addr);
Wire.write(GPIOA);
Wire.endTransmission();
byte received_bytes = Wire.requestFrom(i2c_addr, static_cast<byte>(2));
if (received_bytes != 2) {
Serial.println("cannot read 2 bytes");
}
uint8_t gpioA = Wire.read();
uint8_t gpioB = Wire.read();
Serial.print("gpioA: ");
Serial.println(gpioA);
Serial.print("gpioB: ");
Serial.println(gpioB);
return gpioA | (gpioB << 8);
}
ここで、GPIOAのみを指定し、2byte読み込むだけでGPIOA、GPIOBの値を両方取得できています。これは、上手くGPIOA、GPIOBのアドレスが順番になっているため実現できます。レジスタの設定値書き込みにも同じ方法が使えますが、分かりやすさのため1つずつ行っています。
全プログラム
これらの処理をまとめると、次のような処理になります。
#include <Wire.h>
const byte I2C_ADDR = 0x20;
const byte IODIRA = 0x00;
const byte IODIRB = 0x01;
const byte IPOLA = 0x02;
const byte IPOLB = 0x03;
const byte GPPUA = 0x0C;
const byte GPPUB = 0x0D;
const byte GPIOA = 0x12;
const byte GPIOB = 0x13;
void writeRegister(byte i2c_addr, byte addr, byte v) {
Wire.beginTransmission(i2c_addr);
Wire.write(addr);
Wire.write(v);
Wire.endTransmission();
}
int readGPIO(byte i2c_addr) {
Wire.beginTransmission(i2c_addr);
Wire.write(GPIOA);
Wire.endTransmission();
byte received_bytes = Wire.requestFrom(i2c_addr, static_cast<byte>(2));
if (received_bytes != 2) {
Serial.println("cannot read 2 bytes");
}
uint8_t gpioA = Wire.read();
uint8_t gpioB = Wire.read();
Serial.print("gpioA: ");
Serial.println(gpioA);
Serial.print("gpioB: ");
Serial.println(gpioB);
return gpioA | (gpioB << 8);
}
void I2CSetup(byte address) {
writeRegister(address, IODIRA, 0xFF >> 1);
writeRegister(address, IODIRB, 0xFF >> 1);
writeRegister(address, GPPUA, 0xFF);
writeRegister(address, GPPUB, 0xFF);
// writeRegister(address, IPOLA, 0x00);
// writeRegister(address, IPOLB, 0x00);
// writeRegister(address, IPOLA, 0xFF);
// writeRegister(address, IPOLB, 0xFF);
}
void setup() {
Wire.begin();
Serial.begin(9600);
while (!Serial)
; // Leonardo: wait for serial monitor
I2CSetup(I2C_ADDR);
}
void loop() {
int gpio = readGPIO(I2C_ADDR);
Serial.print("gpio: ");
Serial.println(gpio);
delay(1000);
}
これを書き込むと、一秒ごとにボタンの入力を判定し、結果を出力してくれます。入力しないと127、ボタンを押しているとgpioBが126になります。
入力が無い状態
極性変更
出力値を見ると、GPIOA,Bが両方127
になっています。これは何故でしょうか?
内部プルアップしているので、入力が無ければhighになります。また、GPIOA/Bの7はoutputで必ず0になるため、結果 0b01111111
= 127が表示されているわけです。
とはいえプログラムで扱う場合、何もしなければ0、ボタンを押すと1が欲しいですね。そんな時に便利なのが、途中に出てきた IPOLx
です。これは、入力ピンの極性を変更して教えてくれます。
コード中にある次の記述をコメントアウトすると、極性が変更されます。
writeRegister(address, IPOLA, 0xFF);
writeRegister(address, IPOLB, 0xFF);
実行してみると、とても人間に優しい表示となりました。
極性を変更した状態
ライブラリの利用
上手く行ったところで、最後にライブラリを使って書き換えてみましょう。Arduino IDEにはライブラリマネージャーが標準で用意されているので、これを使います。
MCP23017で調べてみるといくつか出てきますが、Adafruitのものを使います。依存関係も引っ張ってインストールでき、大変便利です。
Adafruitのライブラリを利用。便利。
コードもGithubで公開されています。
https://github.com/adafruit/Adafruit-MCP23017-Arduino-Library
サンプルコードを参考にしつつ、こんな感じのコードを書くと近い挙動になります。極性を反転させる関数が見つからなかったので、ここでは受け取ってから反転します。
#include <Adafruit_MCP23X17.h>
const byte I2C_ADDR = 0x20;
const byte button_pin = 8;
Adafruit_MCP23X17 mcp;
void I2CSetup(byte address) {
if (!mcp.begin_I2C()) {
Serial.println("Error.");
while (1)
;
}
for (byte i = 0; i < 16; i++)
mcp.pinMode(i, INPUT_PULLUP);
}
void setup() {
Wire.begin();
Serial.begin(9600);
while (!Serial)
; // Leonardo: wait for serial monitor
I2CSetup(I2C_ADDR);
}
void loop() {
uint16_t gpio = mcp.readGPIOAB();
Serial.print("gpio: ");
Serial.println(~gpio);
delay(1000);
}
ライブラリの利用
スッキリしました。巨人の肩に乗っていきましょう。
まとめ
入力を受け取り、ようやくI2Cデバイスを活用できました。今回出てきた手順は、今後登場する各種デバイスでも同じことを行います。
次回はTRRSケーブルを使って、離れたところにあるブレッドボード同士を繋いでみます。