🙄

Pro MicroでI2Cを動かす (3) - ピン状態の判定

2024/05/25に公開

シリーズ3つ目のこの記事では、繋いだデバイスの状態を受け取ります。

前回はI2Cデバイスを発見したので、次に対象のアドレスでボタンが押されたかを判定してみます。今回繋がっている機器はアドレス 0x20 なので、これに対して初期化処理などを行い、ピンのhigh/lowを確認します。

用意するパーツ

スイッチを接続するだけです。

  • 動作確認用スイッチx1
    • セット用スイッチと同じものでOK

ここまでに使ったもの

  • ブレッドボード(BB-801 など)x1

  • Pro Micro + ピンヘッダ x1

  • リセット用スイッチx1

  • ジャンパーワイヤ x沢山

  • Pro MicroとPCを繋ぐケーブル

  • MCP23017 x1

  • 1kΩの抵抗x2

  • ブレッドボード(BB-801 など)x1

    • ブレッドボードを分けたい場合。1つで収まる場合はそれでも良い

配線

データシートを読み解く(ピン関連)

MCP23017 がどのようなピンを持っているかをデータシートから確認しましょう。なお、データシートを探す際は名前でググることもできますし、秋月などパーツサイトにあるならそこから辿ることができます。

データシートは論文と同様、最初にサマリーが書かれています。タイトルとサマリーを見て雰囲気を把握し、必要に応じて詳細を見るのが無難です。親切なものには参考実装も書かれているので、一通り見てみると手間が省けます。

ここからは、MCP23017 のデータシートを使って読み方を学んでいきます。

引用: https://ww1.microchip.com/downloads/aemDocuments/documents/APID/ProductDocuments/DataSheets/MCP23017-Data-Sheet-DS20001952.pdf
引用: https://ww1.microchip.com/downloads/aemDocuments/documents/APID/ProductDocuments/DataSheets/MCP23017-Data-Sheet-DS20001952.pdf

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以外の入力ピン

GPIO以外の入力ピンを見てみると、Must be externally biased. と書かれています。ちゃんとGNDやVCCに繋いであげないといけない、ということですね。ここがオープンだとアドレスがフラフラしたり、勝手にリセットされて酷い目に会います。実際会いました。

なお、使わない入力ピンはVCCかGNDに繋ぎます。さらにプルアップまたはプルダウンするのが推奨です。理想的には直接VCC/GNDに繋げば問題ないはずですが、ノイズの影響や不慮のショートなどで良くないことが起き得るので、ちゃんと組む時はプルアップしておくと決めるのが良いとされています(参考1参考2)。

なお、NCピンは入力ですが、IC内部で繋がっていないようなので放置しても大丈夫そうです。一応GNDに繋いでおくと安心ですが、今回は放置します。

ブレッドボードに実装

前回ふんわりと繋いだ配線を理解した気がしたところで、スイッチを追加してみましょう。ここでは GPB0 に接続します。

実績: I2Cデバイスにスイッチを追加
実績: I2Cデバイスにスイッチを追加

先述したとおり内部のプルアップを使うため、GPB0ピン → スイッチ → GNDというだけのシンプルな配線になります。

状態読み取りの基礎

ここからは、接続後のプログラム作成に重要な部分です。

データシートを読み解く(レジスタ関連)

データシートからピンの状態を読むための処理を確認します。ただ、全てを読むのは大変辛いので、要点だけ抽出して紹介してゆきます。

まず、設定可能なレジスタの一覧を見てみましょう。

https://ww1.microchip.com/downloads/aemDocuments/documents/APID/ProductDocuments/DataSheets/MCP23017-Data-Sheet-DS20001952.pdf
https://ww1.microchip.com/downloads/aemDocuments/documents/APID/ProductDocuments/DataSheets/MCP23017-Data-Sheet-DS20001952.pdf

IOCON.BANKの値によって、設定のアドレスも変わるというややこしさです。デフォルトはBANK=0なので、これは変更しないで進めます。

沢山ありますが、大事なのは IODIRxGPPUxGPIOx の3つです。また、IPOLx も便利です。それぞれAピン列、Bピン列があります。これらの詳細を見てみましょう。

IODIR

IODIR

各ピンの入出力設定を行います。1で入力、0で出力、デフォルト値は全て0(出力)です。今回は入力したいので、全ピンを1にすることで達成できそうです。

ただし、Noteにあるとおり、最上位ビットは0である必要があります。

GPPU

GPPU

こちらはプルアップ設定です。IC内部にプルアップ抵抗が入っていると先述しましたが、ここの設定で有効無効を切り替えられます。1で有効、0で無効、デフォルト値は0です。

100kΩの抵抗が入っていると書かれていますね。プルアップ抵抗としては弱いので、必要に応じて外部に用意したものを使うのも検討されます。

IPOL

IPOL

入力の極性(Low/High)を反転して教えるかどうかの設定です。デフォルトはそのままです。
一見何の役に立つのか分かりにくい設定ですが、実は便利なシロモノです。後々使い方を見てゆきます。

GPIOx

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ステップ必要です。まずbeginTransmissionwriteendTransmission を書き込みと同様に行います。ここで、書き込むアドレスには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のライブラリを利用。便利。
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ケーブルを使って、離れたところにあるブレッドボード同士を繋いでみます。

Discussion