I2Cモジュールの作成(コントローラ)

コンピュータ、組み込み

RP2040ではI2Cスレーブとして通信する機能があります。
タクトボタンの押下状態を読み取りI2Cスレーブとしてマスターに返答するモジュールを作成します。

完成物

ボタンの押下状態を監視して、I2Cスレーブとして通知するモジュールを作ります。

今回できるだけ気軽にモジュールを使うために、電源もマスター側から供給するようにQwiicコネクタから供給することにしました。

ゲームやコントローラ付きの制御基板などを作成する際に入力装置は欠かせません。
その際にボタンが多く必要でGPIOのやりくりに苦労したり、入力部分のプログラムを作ったりと大変です。
コントローラをモジュール化するのはいろいろメリットがあります。

GPIOピンの節約
ボタンがたくさん必要な場合GPIOを複数消費してしまいますが、専用モジュールがあればマスター側のGPIOの消費を抑えることができます。

・モジュールの再利用ができる
今回作成したモジュールは別のプロジェクトのコントローラとして使用することができます。
プログラムではクラスや関数のモジュールを作成して動的参照するイメージです。

・マスター側の処理負荷軽減
マスター側の処理からコントローラのボタン入力処理が除外できるので、プログラムのコードを減らすことができます。
処理の負荷も軽減できますし、マスターがボタンの状態を確認する周期が間に合わない場合にもスレーブ側で記憶しておくこともできます。

単純な機能を持たせたプログラムが複数動作するので、一つずつのプログラムは小規模になり不具合も少なくなります。

デメリットとしては
・管理するプログラムやマイコンが増える
プロジェクト自体が複数になるので、ソースファイルやマイコンが増えます。実装面積が増えたり消費電力が大きくなったりします。

・小規模な修正でも面倒
一つのプロジェクトで入力処理も行っていればすぐに修正できますが、修正箇所がスレーブ側だとスレーブのプロジェクトを開いて修正することになります。

・影響範囲
修正したらこのモジュールを使用しているすべてのプロジェクトの動作に影響があります。
不具合を修正したらすべてのプロジェクトで修正できたことになりますが、機能を追加や削除をした場合、他のプロジェクトで不具合になることがあるので慎重に。

I2Cのモジュールを作成するためにこちらのマイコン同士のI2C通信を使用しています。

電源モジュールはこちら

工作

用意するもの

アイテム説明
RP2040 ZEROI2Cスレーブになってもらいます。
なるべく小さくしたかったのでRP2040 ZEROにしました
ユニバーサル基板5 x 7 cm サイズ
タクトスイッチ8個
作りたい個数でかまいませんが、将来マウスになってもらうので
上下左右、ホイール上下、クリック左右 として8個です。
ピンソケット2.57ピッチ 9ピンのシングルを2本
RP2040 ZEROと合体するために使用します。
DC-DCコンバータ
0.9V ~ 5V を 5V出力にしてくれます。
RP2040 ZEROをQwiicコネクタで接続します。
マスター側から3.3Vが供給されるので、5V電源を作るために
使用します。
Qwiicコネクタ
JST SH 1mm 4ピン
Qwiicモジュールにする予定です。
MINTIA(大)のケースコントローラの筐体にします。
サイズ感の良いものや加工しやすいものを選びます。
ポリガン
(グルーガン)
100円ショップで入手できます。
Qwiicコネクタを固定します。
配線各色配線作業
ここでのピン規格が同じ基板とは信号ピン、GNDピン、電源ピンが同じ位置にある基板。
信号ピンのピン番号は違ってもかまいません。スケッチで修正ができます。

手順

ピンソケット切断

ピンソケット9ピンがない場合ルータなどで切断します。
スリットがあり、手で折れるタイプもあります。

別のページ使用したで作業動画がありますので参考にしてください。
切断による怪我や、破片の飛散対策をして作業してください。

部品の実装(はんだ付け)

部品実装

①ボタンの押しやすい位置を決めて実装します。
今回マウス相当の操作ができるコントローラを作るので、上下左右の4個とホイールの上下と左右クリックの4個で合計8個のボタンを実装します。

②次にDC-DC昇圧モジュールを実装します。
こちらはピンヘッダが取り付けられていますが、ピンヘッダを直接ユニバーサル基板にはんだします。

③最後にピンソケット(RP2040-ZERO用)を実装します。

配線

基本的に作業しやすいところからワイヤをはんだします。
直線的に引っ張るより多少余裕のある状態で配線します。
ボタンの電源になる3.3Vは合計8個分必要なのでスズメッキ線を使用しています。
今回の配線図は以下の表です。

RP2040-
ZERO
配線ボタン配線DC-DC
5V昇圧
配線JST SH1mm
Qwiic
5VVOUT
VINVCC
GNDGNDGND
GPIO0(SDA)SDA
GPIO1(SCL)SCL
3.3Vボタンの電源
GPIO27①上
GPIO26②下
GPIO15③左
GPIO14④右
GPIO8⑤ホイール上
GPIO7⑥ホイール下
GPIO6⑦クリック左
GPIO5⑧クリック右

筐体

今回MINTIA(大粒)の箱を加工します。
ユニバーサル基板は箱のはめ込み箇所とかみ合うように切り込みを入れています。
MINTIAの蓋側はボタンのとピンソケットの位置に穴空け加工をします。
ハンドルータのドリルと棒やすりで根気よく作業します。

ユニバーサル基板の納まりを確認したら、Qwiic配線を適当な穴から表面に通してJST SH 1mm 4ピンコネクタにはんだ付け配線をします。
作業前にQwiic配線をよく確認してから配線します。
配線後はポリガン(グルーガン)で接着します。
半田付け箇所はストレスがかかりやすいのではんだ付け箇所にも多めに盛り付けします。

スケッチ

RP2040-ZEROスレーブ側

スケッチの説明

I2Cマスターから問い合わせのあったタイミングで押されているボタンの情報を返答します。
返答は8bit で、各ビットとボタンの状態が連動します。
1が押下状態、0が解放状態。

I2Cマスターから”LATCH” + CR(0x0d)を受信するとその返答は、前回’L’により返答したボタンの状態の論理和を返答します。
これはマスターがボタンの状態を細かい周期で確認できない場合に、ボタンの押下状態を読みこぼすことがないようにするための機能です。

I2Cマスターから”CLEAR” + CR(0x0d)を受信すると論理和の情報をクリアします。

関数 ExecuteCommand()が、受信したコマンドの解析と実行を行っています。

スケッチ
/**********************************************************************
【ライセンスについて】
Copyright(c) 2022 by tamanegi
Released under the MIT license
'http://tamanegi.digick.jp/about-licence/

【マイコン基板】
RP2040-Zero

【スケッチの説明】
タクトボタンの入力状態を読み取ります。

ステップ1.
押下されたボタンの状態により、対応するビットをHIGHにする。
複数ボタンが押下されている場合は対応ビットのORとなる。

デバッグとして、押下状態はCOMに出力される。

ステップ2.(I2Cスレーブ側)
I2Cスレーブとして起動させる。(アドレスは 0x10固定)
マスターからの要求に対して、現在押下されているボタン情報を返答する。

ステップ3.(I2Cスレーブ側)

【ライブラリ】
Raspberry Pi Pico/RP2040 > Generic RP2040

【準備】
RP2040-Zero  <-> 接続先
GPIO27(INPUT)      <-> ボタン上
GPIO26(INPUT)      <-> ボタン下
GPIO15(INPUT)      <-> ボタン左
GPIO14(INPUT)      <-> ボタン右
GPIO8(INPUT)       <-> ボタン奥側
GPIO7(INPUT)       <-> ボタン手前側
GPIO6(INPUT)       <-> ボタン左側
GPIO5(INPUT)       <-> ボタン右側

5V出力モジュールは、マスター側のI2Cと接続するときにQwiicやGroveコネクタを使用できるようにするため。
コネクタへの入力電圧の3.3Vを5Vに変換してRP2040 Zeroを起動できる。

RP2040-Zero   <-> DCDC 5V出力モジュール
VIN(5V)           <-> VOUT
GND               <-> GND

RP2040-Zero   <-> JST SH 1mm 4pin
GPIO0(SDA)    <-> SDA
GPIO1(SCL)    <-> SCL

【バージョン情報】
2023/1/2 : 新規
**********************************************************************/

#include "Wire.h"

#define I2C_ADDR  0x10            //スレーブ側に指定するI2Cアドレス
#define I2C_SDA   0               //SDAにはGP0を使用する
#define I2C_SCL   1               //SCLにはGP1を使用する

//#define DEBUG

//I2Cマスターからのコマンドの要求による動作モード
enum eMode
{
  EM_Actual,                    //問い合わせがあったタイミングで押されている情報を返答する
  EM_Latch,                     //問い合わせがあるまでに押されているボタンのORを返答する
};

static short sGPIOTableSize = 0;        //ボタン配列数

static eMode lMode = EM_Actual;         //返答モード
static short sActual = 0x0000;          //問い合わせがあったタイミングで押されている情報を返答する
static short sLatch = 0x0000;           //問い合わせがあるまでに押されているボタンのORを返答する

//上、下、左、右、奥、手前、左ボタン、右ボタン
static const char cGPIOTable[] = {
  27, 26, 15, 14, 8, 7, 6, 5
};

//ボタン状態の確認順番配列と対応させることで、押下しているビットをHIGHにする。
//上、下、左、右、奥、手前、左ボタン、右ボタン
static const short sBitTable[] = {
  0x0010, 0x0020, 0x0040, 0x0080, 0x0001, 0x0002, 0x0004, 0x0008
};

static short Get_ButtonStat(void);        //ボタンの押下状態を読み取る。
static void onRequest();
static void onReceive(int len);
static void ExecuteCommand(char *Cmd);
static void Debug_DIN(void);


void setup()
{
#ifdef DEBUG
  Serial.begin(115200);
#endif

  sGPIOTableSize = sizeof(cGPIOTable);

  //GPIOの入力設定
  for(int i = 0; i < sGPIOTableSize; i ++)
  {
    pinMode(cGPIOTable[i], INPUT_PULLDOWN);
  }

  Wire.setSDA(I2C_SDA);             //通信に使用するI2Cアドレスピンを指定する
  Wire.setSCL(I2C_SCL);
  Wire.onReceive(onReceive);        //マスターからデータを受信したときに呼び出される関数の登録
  Wire.onRequest(onRequest);        //マスターからデータを要求されたときに呼び出される関数の登録
  Wire.begin(I2C_ADDR);             //通信開始

}

void loop()
{
  sActual = Get_ButtonStat();
  sLatch |= sActual;                //Latchの場合は、最新の情報のORを続ける。

//debugモード時はCOMにボタンの押下状態を出力
#ifdef DEBUG
  Serial.printf("Actual = %04x, Latch = %04x\r\n", sActual, sLatch);
  delay(2);
#endif

}

//現在のボタン押下状態を読み取る。
//戻り値 :
// ボタンの押下状態の組み合わせ
// bit下位から、上、下、左、右、奥、手前、左ボタン、右ボタン
static short Get_ButtonStat(void)
{
  short Ret = 0x0000;
  char cVal = 0;

  for(int i = 0; i < sGPIOTableSize; i ++)
  {
    cVal = digitalRead(cGPIOTable[i]);

    //読み取ったGPIO(n)の信号がHIGHなら、GPIO(n)に対するビットをHIGHにする。
    if(cVal == true){
      Ret |= sBitTable[i];
    } 
  }

  return Ret;

}

//マスターから要求があればモードの状態に応じて返答する。
static void onRequest()
{
  switch (lMode)
  {
    case EM_Actual:               //通常は現在の押下状態を返答する
      Wire.write(sActual);
      break;

    case EM_Latch:                //Latchモードの場合、一度返答したらActualモードに戻す
      Wire.write(sLatch);
      lMode = EM_Actual;
      sLatch = sActual;           //一度返答するとsLatchに現在の押下状態を保存する。
      break;
  }

}

//マスターからデータ送られてくると読み取りをする。
static void onReceive(int len)
{
  char cCommand[128];     //コマンドを一時的に保存するバッファ

  char cBuf = 0;
  long lCommandIndex = 0;

  while(Wire.available()){

    //マスターからの送信データがあれば読み取りを行う。
    //デリミタ(0x0d)を検出したらコマンドの実行
    //それ以外はコマンドバッファに積み上げる。
    cBuf = Wire.read();
    if(cBuf == 0x0d)
    {
      cCommand[lCommandIndex] = 0x00;
      ExecuteCommand(cCommand);         //マスターから読み取ったコマンドを実行する
    }
    else
    {
      cCommand[lCommandIndex] = cBuf;
      lCommandIndex ++;
    }
  }
}

static void ExecuteCommand(char *Cmd)
{
  String cBuf = Cmd;

#ifdef DEBUG
  Serial.printf("Command = %s\r\n", Cmd);
#endif

  //I2Cマスターからのコマンドを解析する
  //"LATCH"  前回のLATCHの返答から押下されたボタンのすべてのOR情報を次の返答にする
  if(cBuf.compareTo("LATCH") == 0)
  {
    lMode = EM_Latch;
  }
  //"LATCH"問い合わせ用の返答をクリア(現在の押下状態)にする
  else if(cBuf.compareTo("CLEAR") == 0)
  {
    sLatch = sActual;
  }

}

//デバッグ用の出力
//GPIO配列の入力状態をCOM出力する
static void Debug_DIN(void)
{
  long val;

  for(int i = 0; i < sGPIOTableSize; i ++)
  {
    val = digitalRead(cGPIOTable[i]);

    Serial.printf("[%2d] %4ld, ", cGPIOTable[i], val);
  }
  Serial.println("");

  delay(2);

}

PRO MICRO RP2040マスター側(動作確認用)

スケッチの説明

1秒ごとにI2Cスレーブにボタンの押下状態を問い合わせます。
結果はシリアルモニタに表示されます。

シリアルモニタから’L’を送信すると、I2Cからスレーブに”LATCH” + CR(0x0d)を送信します。
I2Cスレーブからは、前回”LATCH”コマンドを受け取ってからのボタン押下状態の論理和が返答されます。

シリアルモニタから’C’を送信すると、これまでのボタンの押下状態の論理和をクリアします。

スケッチ
/**********************************************************************
【ライセンスについて】
Copyright(c) 2022 by tamanegi
Released under the MIT license
'http://tamanegi.digick.jp/about-licence/

【マイコン基板】
Sparkfun Pro Micro RP2040

【スケッチの説明】
I2C接続したI2C Controlerの状態を読み取ります。
1秒ごとに読み取りを行い、結果をCOMに出力します。

ステップ2.(I2Cマスター側)
I2Cスレーブとして起動させる。(アドレスは 0x10固定)
マスターからの要求に対して、現在押下されているボタン情報を返答する。

【ライブラリ】
Raspberry Pi Pico/RP2040 > SparkFun ProMicro RP2040

【準備】
Raspberry Pi Pico BOOT(マスター) <-> RP2040 Zero(スレーブ)
Qwiic <-> Qwiic(JST SH 1mm 4pin)

【バージョン情報】
2023/1/2 : 新規
**********************************************************************/

#include "Wire.h"

#define I2C_ADDR  0x10            //スレーブ側に指定するI2Cアドレス
#define I2C_SDA   16               //SDAにはGP0を使用する
#define I2C_SCL   17               //SCLにはGP1を使用する

void Debug_CommandTest(void);


uint32_t i = 0;

void setup() 
{

  Serial.begin(115200);           //パソコン側からの読み取りと書き込みに使用します。

  //I2Cマスター通信開始
  Wire.setSDA(I2C_SDA);           //I2Cアドレスピンを指定して通信開始
  Wire.setSCL(I2C_SCL);
  Wire.begin();
}

void loop() 
{
  char buf_com = 0;
  char buf_i2c = 0;

  Debug_CommandTest();

  //スレーブから現在のボタンの押下情報を読み取る
  if(Wire.requestFrom(I2C_ADDR, 2) != 0)
  {
    buf_i2c = Wire.read();
  }

  //スレーブから読み取ったデータをCOMへ送信する。
  if(Serial.availableForWrite())
  {
    Serial.printf("%04x \r\n", buf_i2c);
  }
}

//COMから入力した文字れにより、I2Cスレーブにコマンドを送信するテストコード
void Debug_CommandTest(void)
{
  delay(1000);            //ゆっくり動いてもらうためにウエイト
  char x;

  if(Serial.available() != 0)
  {
    x = Serial.read();
    if(x == 'L')
    {
      Wire.beginTransmission(I2C_ADDR);
      Wire.printf("LATCH\x0d");
      if(Wire.endTransmission(true) != 0)
      {
        Serial.println("送信エラー");
      }
    }
    else if(x == 'C')
    {    
      Wire.beginTransmission(I2C_ADDR);
      Wire.printf("CLEAR\x0d");
      if(Wire.endTransmission(true) != 0)
      {
        Serial.println("送信エラー");
      }
    }
  }

}

PRO MICRO RP2040マスター側(HIDMouse)

スケッチの説明

HID Mouseとして動作します。

I2C接続したI2C Controlerの状態を読み取り、ボタンの状態に応じマウスカーソルの操作をします。

スケッチ
/**********************************************************************
【ライセンスについて】
Copyright(c) 2022 by tamanegi
Released under the MIT license
'http://tamanegi.digick.jp/about-licence/

【マイコン基板】
Sparkfun Pro Micro RP2040

【スケッチの説明】
HID Mouseとして動作します。

I2C接続したI2C Controlerの状態を読み取り、ボタンの状態に応じマウスカーソルの操作をします。

【ライブラリ】
Raspberry Pi Pico/RP2040 > SparkFun ProMicro RP2040

【準備】
Raspberry Pi Pico BOOT(マスター) <-> RP2040 Zero(スレーブ)
Qwiic <-> Qwiic(JST SH 1mm 4pin)

【バージョン情報】
2023/1/24 : 新規
***********************************************************************/

#include <Mouse.h>
#include "Wire.h"

#define I2C_ADDR  0x10            //スレーブ側に指定するI2Cアドレス
#define I2C_SDA   16               //SDAにはGP0を使用する
#define I2C_SCL   17               //SCLにはGP1を使用する

//ボタン押下状態のビット情報
#define BUTTON_UP         0x10        //上
#define BUTTON_DOWN       0x20        //下
#define BUTTON_LEFT       0x40        //左
#define BUTTON_RIGHT      0x80        //右
#define BUTTON_WHEELUP    0x01        //ホイール上
#define BUTTON_WHEELDOWN  0x02        //ホイール下
#define BUTTON_CLICKLEFT  0x04        //クリック左
#define BUTTON_CLICKRIGHT 0x08        //クリック右


bool gbStatClickLeft = false;          //false = 解放状態, true = 押下状態
bool gbStatClickRight = false;         //false = 解放状態, true = 押下状態
long glWheelCount = 0;                  //ホイールの動作カウント

void setup()
{

  //I2Cマスター通信開始
  Wire.setSDA(I2C_SDA);           //I2Cアドレスピンを指定して通信開始
  Wire.setSCL(I2C_SCL);
  Wire.begin();
  
  Mouse.begin();

  Serial.begin(115200);
}

void loop() 
{
  const long lMoveVolume = 3;       //移動量

  long lVCount = 0;       //上下移動
  long lHCount = 0;       //左右移動
  long lWheelCount = 0;   //ホイール

  char cStat = 0;
  //I2C コントローラから現在のボタン押下状態を読み取る
  if(Wire.requestFrom(I2C_ADDR, 2) != 0)
  {
    cStat = Wire.read();
  }
  Serial.printf("%02x\r\n",cStat);

  //<<カーソル移動箇所>>
  //カーソルの座標は画面の左上が原点(0,0)、上や左に移動するときはマイナス方向、下や右に移動するときはプラス方向
  //通常上下の同時押しや左右の同時押しはありえないので、それぞれに加減することで相殺する。
  if((cStat & BUTTON_UP) != 0) lVCount -= lMoveVolume;            //カーソル上移動(カーソル座標はマイナス方向)
  if((cStat & BUTTON_DOWN) != 0) lVCount += lMoveVolume;          //カーソルした移動 (カーソル座標はプラス方向)
  
  if((cStat & BUTTON_LEFT) != 0) lHCount -= lMoveVolume;          //カーソル左移動 (カーソル座標はマイナス方向)
  if((cStat & BUTTON_RIGHT) != 0) lHCount += lMoveVolume;         //カーソル右移動 (カーソル座標はプラス方向)
  Mouse.move(lHCount, lVCount, 0);


  //<<ホイール箇所>>
  if((cStat & BUTTON_WHEELUP) != 0) lWheelCount ++;                //ホイールプラス方向
  if((cStat & BUTTON_WHEELDOWN) != 0) lWheelCount --;              //ホイールマイナス方向
  if(lWheelCount != 0) Mouse.move(0,0, lWheelCount);

  //クリック箇所>>
  //左クリックボタンの押下/解放状態が変化したらマウスイベントを発生させる
  if((cStat & BUTTON_CLICKLEFT) != 0)
  {
      //false = 解放状態, true = 押下状態
      if(gbStatClickLeft == false)
      {
        //現在解放状態から押下状態に変化したら、クリックイベントの発生と状態の保存を行う
        Mouse.press(MOUSE_LEFT);
        gbStatClickLeft = true;
      }
  }
  else
  {
    //false = 解放状態, true = 押下状態
    if(gbStatClickLeft == true)
    {
      //現在解放状態から解放状態に変化したら、ボタンの解放イベントの発生と状態の保存を行う
      Mouse.release(MOUSE_LEFT);
      gbStatClickLeft = false;
    }
  }


  //左クリックボタンの押下/解放状態が変化したらマウスイベントを発生させる
  if((cStat & BUTTON_CLICKRIGHT) != 0)
  {
      //false = 解放状態, true = 押下状態
      if(gbStatClickRight == false)
      {
        //現在解放状態から押下状態に変化したら、クリックイベントの発生と状態の保存を行う
        Mouse.press(MOUSE_RIGHT);
        gbStatClickRight = true;
      }
  }
  else
  {
    //false = 解放状態, true = 押下状態
    if(gbStatClickRight == true)
    {
      //現在解放状態から解放状態に変化したら、ボタンの解放イベントの発生と状態の保存を行う
      Mouse.release(MOUSE_RIGHT);
      gbStatClickRight = false;
    }
  }
}

I2Cマスターに使用したQwiic搭載マイコン PRO MICRO RP2040

コメント

タイトルとURLをコピーしました