I2C コントローラー

前回の取り組み で、”黄色い基板”(Cheap Yellow Display、以下 CYD)の I2C 接続方法が分かったので、色々と試したくなりました。そこで本記事では、ESP32-Cheap-Yellow-Display に紹介されている Tetris with NunchuckGalagino を参考に、各種コントローラーの動作検証を報告したいと思います。

ちなみに、実際の Galagino の動作例は以下をご参照ください。

ハードウェア編

CYD

2種類のESP32-2432S028R
2種類のESP32-2432S028R

2.8 インチ版の ESP32-2432S028R には2種類あるようで要注意です。旧版と思われるのは micro-USB が1つで ILI9341 を搭載し、新版と思しきは USB が micro と Type-C、パネルに ST7789 を搭載しています。最初に購入したのが Sunton Store の旧版 で、技適なしのハズレでした。同じく AliExpress で次に購入したのが 新版 で、これはアタリでした。

なんちゃって技適マーク?
なんちゃって技適マーク?

ただ必ずしも新版がアタリとは限らず、肉眼では判別できないほど小さく刻印されたブツもあるようで、とてもスリリングな技適ガチャになってます。

スピーカー

回路図を見ると、”SC8002B” というオーディオアンプ IC が載っていて、「SPEAK」と書かれた JST 1.25mm オス2ピンの端子につながっています。

以下は中国語で書かれた SC8002B データシートの日本語訳です。

SC8002Bはシャットダウンモードを備えたオーディオアンプ IC です。 5V 入力電圧で動作する場合、 負荷 (4Ω) の最大出力電力は 2.5W です。ポータブルデバイスの場合、VDD がシャットダウン端子に 作用すると、SC8002B はシャットダウン モードに入ります。このとき、消費電力は非常に低く、IQ は わずか 0.6uA です。 SC8002B は、高出力、高忠実度のアプリケーション向けに設計されたオーディオ アンプ IC です。 周辺部品が少なく、入力電圧 2.0V~5.0V で動作可能です。

スピーカー端子と回路図
スピーカー端子と回路図
CYDにスピーカーを接続
CYDにスピーカーを接続

ということで、外付けのアンプなどは不要で「SPEAK」にスピーカーを直接接続できますが、結構な出力なので、8Ω 1W の ブレッドボード用ダイナミックスピーカー を接続する際に 200Ω の半固定抵抗で出力を絞れるようにしています。

コントローラー

I2C 接続が可能なコントローラーは、任天堂や Adafruit 製品、micro:bit 用や自作版など、情報源には事欠きません。今回は Arduino との接続が容易でライブラリが揃っている任天堂系と Adafruit 製品を接続し、動作を検証してみました。

Wii および NES 用コントローラー

Nintendo コントローラー

Wii コントローラーのページNES Classic Mini 互換コントローラーのページ に紹介されているモノのうち、自宅にあったヌンチャクとクラシックコントローラー、そして Amazon で購入した並行輸入品の NES Classic Mini(¥1,880) について動作を確認しました(動作未確認ですが、納期がちょっと遅いけど安い¥749 もあります)。

任天堂コントローラ用ライブラリ
任天堂コントローラ用ライブラリ

これらは冒頭に紹介した TetrisGalagino で使われている NintendoExtensionCtrl で簡単に動かすことができます。

また CYD との接続は、AliExpress で購入できる1個 30円 程度の ヌンチャクアダプター や、スイッチサイエンスで買える Adafruit STEMMA QT/Qwiic互換 Wiiヌンチャクアダプタボード(税込 ¥735) が簡単です。

ヌンチャクアダプター
ヌンチャクアダプター
Adafruit Wii Nunchuck Breakout Adapter
Wii Nunchuck Breakout Adapter
アダプターとCYDの接続
アダプターとCYDの接続

また「Wiiリモコンにつなげるゲームコントローラーを作る」で、接続時の暗号を解読したライブラリが紹介されてますが、我が家の Wii リモコンが行方不明で残念ながら試せてません。

I2C アドレスは、ヌンチャクと NES Classic Mini が 0x52 です。一方クラシックコントローラーを I2C Scanner にかけてみると、次のように多くの I2C スレーブがぶら下がっていることが分かります。ボタンの数が多いので分散させているのだと思います。

Scanning...
I2C device found at address 0x01  !
I2C device found at address 0x02  !
I2C device found at address 0x03  !
I2C device found at address 0x04  !
I2C device found at address 0x05  !
I2C device found at address 0x06  !
I2C device found at address 0x07  !
I2C device found at address 0x52  !
I2C device found at address 0x78  !
I2C device found at address 0x79  !
I2C device found at address 0x7A  !
I2C device found at address 0x7B  !
I2C device found at address 0x7C  !
I2C device found at address 0x7D  !
I2C device found at address 0x7E  !
done

Adafruit Mini I2C Gamepad with seesaw - STEMMA QT / Qwiic

Adafruit Mini I2C Gamepad
Adafruit Mini I2C Gamepad

コレ、手の大きいガイジンに操作できるの?ってぐらいに小さいです。スイッチサイエンス(¥1,602)(僕で最後だったようで、売り切れ中)か DigiKey(¥1,217) で入手可能です。また JST SH 1.0mm ピッチの Qwiic コネクタは、秋月電子の コネクター付コード 4P 黒赤青黄(¥100) が便利です。

サムスティックが滑りやすいという方には、Switch 用 がピッタリ嵌ります。

I2C 接続するには Adafruit_Seesaw が必要ですが、色々な周辺機器に対応するためか、実行速度が他に比べて少し遅いです。そのためこのゲームパッドに必須なパーツだけを残してスリム化した Gamepad_Seesaw を作成してみました(後述)。

自作編

エミュレータ用に作ったI2Cゲームコントローラー

PIC マイコンを使った例が「エミュレータ用に作ったI2Cゲームコントローラー」に紹介されています。こーいうのをベアメタルから作れちゃう人って尊敬します。

ソースコードが公開されているので、I2C プロトコル と付き合わせれば勉強になると思います。

番外編

その他のコントローラー達
その他のコントローラー達

メカ工作が苦手な僕は、どうしても既製品に目が行きます 😅

赤い基板は Amazon で2個で¥749 でした。MINIMA に被せると微妙に SCL/SDA が引き出しにくくなるので、CYD との接続は R4 WiFi の Qwiic を使うのが良さげです。またドイツ語ですが、Funduino のページ に使い方が載ってます。

残りの2つは micro:bit 用で、秋月電子(¥2,780)DegiKey(¥3,230) で買えますが、別途 micro:bit が載ったボードが必要です。Arduino をマスターに、micro:bit をスレーブにする例を I2C Communication between micro:bit and Arduino に見つけましたが、いつの日か BLE で無線化するかもと思い、ピックアップしてみました(でも多分…やらない 😏

ソフトウェア編

本章では、ヌンチャクコントローラーからの入力を捌く Galagino の Nunchuck.h を参考に、2タイプのコントローラを検証するサンプルコードを紹介します。テトリスの場合は、初期化部分入力部分 を置き換えれば容易に移植可能と思います。

実行環境は以下を想定しています。

  • ESP32 Espressif ボードパッケージが 3.x の場合
    • ボードタイプに ESP32-2432S028R CYD を選択
  • ESP32 Espressif ボードパッケージが 2.x の場合
    • ボードタイプに ESP32 Dev Module を選択

Wii クラシックコントローラー

Wii クラシックコントローラー
Wii クラシックコントローラー

NintendoExtensionCtrl を Arduino IDE ライブラリマネージャからインストールしてください。

次に新規スケッチを開き、下記のサンプルコードを貼り付けてからコンパイル&アップロードし、動作を確認してください。シリアルモニターにコントローラーのデバッグ情報が出力されれば成功です。

サンプルコードではA、B、スタート、十字の各ボタンと、左ジョイスティックを割り当てていますが、ヌンチャクコントローラーと同じ出力の範囲内でお好きにカスタマイズ可能です。

サンプルコード
#include <Arduino.h>
#include <Wire.h>
#include <NintendoExtensionCtrl.h>

static ClassicController classic;

//---------------------------------------------------------------------------
// Galagino 用の定義
//---------------------------------------------------------------------------
// https://github.com/harbaum/galagino/blob/main/galagino/config.h
#ifndef NUNCHUCK_MOVE_THRESHOLD
#define NUNCHUCK_MOVE_THRESHOLD 30
#endif

#ifndef NUNCHUCK_SDA
#define NUNCHUCK_SDA D27 // or 22 (SCL)
#define NUNCHUCK_SCL SCL // or 27 (D27)
#endif

// https://github.com/harbaum/galagino/blob/main/galagino/emulation.h
#ifndef BUTTON_LEFT
#define BUTTON_LEFT  0x01
#define BUTTON_RIGHT 0x02
#define BUTTON_UP    0x04
#define BUTTON_DOWN  0x08
#define BUTTON_FIRE  0x10
#define BUTTON_START 0x20
#define BUTTON_COIN  0x40
#define BUTTON_EXTRA 0x80
#endif

//---------------------------------------------------------------------------
// Wii クラシックコントローラー用の定義
//---------------------------------------------------------------------------
static int nunchuck_move_threshold = NUNCHUCK_MOVE_THRESHOLD;

void nunchuckSetup() {

  Wire.begin(NUNCHUCK_SDA, NUNCHUCK_SCL);

  classic.begin();

  while (!classic.connect()) {
    Serial.println("Classic Controller not detected!");
    delay(1000);
  }
}

unsigned char getNunchuckInput() {
  boolean success = classic.update();  // Get new data from the controller

  if (!success) {  // Ruh roh
    Serial.println("Controller disconnected!");
    return 0;
  }
  else {
    int joyX = classic.leftJoyX();
    int joyY = classic.leftJoyY();

    return ((joyX < 127 - nunchuck_move_threshold) ? BUTTON_LEFT  : 0) |
           ((joyX > 127 + nunchuck_move_threshold) ? BUTTON_RIGHT : 0) |
           ((joyY > 127 + nunchuck_move_threshold) ? BUTTON_UP    : 0) |
           ((joyY < 127 - nunchuck_move_threshold) ? BUTTON_DOWN  : 0) |
           (classic.dpadLeft()   ? BUTTON_LEFT  : 0) |
           (classic.dpadRight()  ? BUTTON_RIGHT : 0) |
           (classic.dpadUp()     ? BUTTON_UP    : 0) |
           (classic.dpadDown()   ? BUTTON_DOWN  : 0) |
           (classic.buttonB()    ? BUTTON_FIRE  : 0) |
           (classic.buttonA()    ? BUTTON_EXTRA : 0) |
           (classic.buttonPlus() ? BUTTON_EXTRA : 0) ;
  }
}

//---------------------------------------------------------------------------
// setup and loop
//---------------------------------------------------------------------------
void setup() {
  Serial.begin(115200);
  nunchuckSetup();
}

void loop() {
  uint8_t input = getNunchuckInput();
  classic.printDebug();
}

Adafruit Mini I2C Gamepad with seesaw

Adafruit Mini I2C Gamepad
Adafruit Mini I2C Gamepad

ライブラリマネージャで Adafruit_Seesaw をインストールするか、Gamepad_Seesaw から Gamepad_seesaw.hpp をスケッチの実行フォルダにコピーしてください。

両ライブラリとも Adafruit_BusIO が必要なので、合わせてインストールしてください。

サンプルコード
#include <Arduino.h>
#include <Wire.h>

#if   true
//---------------------------------------------------------------------------
// Adafruit_BusIO and Adafruit_Seesaw are required.
// https://github.com/adafruit/Adafruit_BusIO
// https://github.com/adafruit/Adafruit_Seesaw
//---------------------------------------------------------------------------
#include <Adafruit_seesaw.h>
static Adafruit_seesaw seesaw;
#define I2C_CONFIG  0x50

#else

//---------------------------------------------------------------------------
// Adafruit_BusIO and Gamepad_Seesaw are required.
// https://github.com/adafruit/Adafruit_BusIO
// https://github.com/adafruit/Adafruit_Seesaw
//---------------------------------------------------------------------------
#include "Gamepad_seesaw.hpp"
static Gamepad_seesaw seesaw;
#define I2C_CONFIG  0x50, NUNCHUCK_SDA, NUNCHUCK_SCL, 400000

#endif

// Adafruit Mini I2C Gamepad 用の定義
#define GAMEPAD_BUTTON_X      6
#define GAMEPAD_BUTTON_Y      2
#define GAMEPAD_BUTTON_A      5
#define GAMEPAD_BUTTON_B      1
#define GAMEPAD_BUTTON_SELECT 0
#define GAMEPAD_BUTTON_START  16
#define IS_PRESSED(in, bit)   (!((in) & (1UL << (bit))))

static uint32_t button_mask =
  (1UL << GAMEPAD_BUTTON_X     ) |
  (1UL << GAMEPAD_BUTTON_Y     ) |
  (1UL << GAMEPAD_BUTTON_A     ) |
  (1UL << GAMEPAD_BUTTON_B     ) |
  (1UL << GAMEPAD_BUTTON_START ) |
  (1UL << GAMEPAD_BUTTON_SELECT);

// https://qiita.com/ryohji/items/5a7aa375c134528fc7c4
#define BCD(c) (5 * (5 * (5 * (5 * (5 * (5 * (5 * (c & 128) + (c & 64)) + (c & 32)) + (c & 16)) + (c & 8)) + (c & 4)) + (c & 2)) + (c & 1))

//---------------------------------------------------------------------------
// Galagino 用の定義
//---------------------------------------------------------------------------
// https://github.com/harbaum/galagino/blob/main/galagino/config.h
#ifndef NUNCHUCK_MOVE_THRESHOLD
#define NUNCHUCK_MOVE_THRESHOLD 30
#endif

#ifndef NUNCHUCK_SDA
#define NUNCHUCK_SDA D27 // or 22 (SCL)
#define NUNCHUCK_SCL SCL // or 27 (D27)
#endif

// https://github.com/harbaum/galagino/blob/main/galagino/emulation.h
#ifndef BUTTON_LEFT
#define BUTTON_LEFT  0x01
#define BUTTON_RIGHT 0x02
#define BUTTON_UP    0x04
#define BUTTON_DOWN  0x08
#define BUTTON_FIRE  0x10
#define BUTTON_START 0x20
#define BUTTON_COIN  0x40
#define BUTTON_EXTRA 0x80
#endif

//---------------------------------------------------------------------------
// Adafruit Mini I2C Gamepad 用の定義
//---------------------------------------------------------------------------
static int nunchuck_move_threshold = NUNCHUCK_MOVE_THRESHOLD;

void nunchuckSetup() {

  Wire.begin(NUNCHUCK_SDA, NUNCHUCK_SCL);

  while (!seesaw.begin(I2C_CONFIG)) {
    Serial.println("ERROR: Gamepad connection failed.");
    delay(1000);
  }

  uint32_t version = ((seesaw.getVersion() >> 16) & 0xFFFF);
  if (version != 5743) {
    Serial.println("Wrong firmware loaded? " + String(version));
    return;
  }

  seesaw.pinModeBulk(button_mask, INPUT_PULLUP);
  seesaw.setGPIOInterrupts(button_mask, 1);
#ifdef  LIB_SEESAW_H
  Wire.setClock(400000);
#endif
}

unsigned char getNunchuckInput() {
  int joyX = 1023 - seesaw.analogRead(14);
  int joyY = 1023 - seesaw.analogRead(15);

  uint32_t buttons = seesaw.digitalReadBulk(button_mask);

  return ((joyX < 511 - nunchuck_move_threshold) ? BUTTON_LEFT  : 0) |
         ((joyX > 511 + nunchuck_move_threshold) ? BUTTON_RIGHT : 0) |
         ((joyY > 511 + nunchuck_move_threshold) ? BUTTON_UP    : 0) |
         ((joyY < 511 - nunchuck_move_threshold) ? BUTTON_DOWN  : 0) |
         (IS_PRESSED(buttons, GAMEPAD_BUTTON_B ) ? BUTTON_FIRE  : 0) |
         (IS_PRESSED(buttons, GAMEPAD_BUTTON_A ) ? BUTTON_EXTRA : 0) |
         (IS_PRESSED(buttons, GAMEPAD_BUTTON_SELECT) ? BUTTON_EXTRA : 0) |
         (IS_PRESSED(buttons, GAMEPAD_BUTTON_START ) ? BUTTON_EXTRA : 0) ;
}

//---------------------------------------------------------------------------
// setup and loop
//---------------------------------------------------------------------------
void setup() {
  Serial.begin(115200);
  nunchuckSetup();
}

void loop() {
  uint8_t input = getNunchuckInput();
  Serial.printf("%08d\n", BCD(input));
}

このコード、サムスティックを色々動かしてみると分かりますが、「横」や「縦」に入れたつもりでも「斜め」が出力されることが度々起こります。「斜め」が必要なシューティングゲームの場合には反応が良くてイイのですが、「縦横」の動きしかないゲームの場合に「斜め」が出力されると、移動方向が変わらずストレスになっちゃいます。

一番手っ取り早い対策は、ゲームに応じて閾値 nunchuck_move_threshold を変えることです。例えば Galagino の場合、nunchuckSetup() に以下を追加すれば OK でしょう。

#ifndef SINGLE_MACHINE
  extern signed char machine;                         // `machine` は galagino.ino に定義されています
  if (machine == MCH_GALAGA || machine == MCH_1942) { // `MCH_*`   は emulation.h  に定義されています
    nunchuck_move_threshold = 30;
  } else {
    nunchuck_move_threshold = 255;
  }
#endif

応答時間

ライブラリとコントローラーの組み合わせで、1回のスキャンにかかり処理時間を比べてみました(1000回分の平均値)。

ライブラリ コントローラー 応答時間
NintendoExtensionCtrl Nunchuk 1233 μsec
^ NES Classic Mini 1349 μsec
^ Classic controller 1491 μsec
Adafruit_seesaw Adafruit Mini I2C Gamepad 2482 μsec
Gamepad_seesaw ^ 1198 μsec

注目して欲しいのは Wii クラシックコントローラーと Adafruit のゲームパッドで、僅か 1msec ほどの差ですが、エミュレーターにとってはこれが致命的とも言える遅延になり得ます。これが Gamepad_seesaw を作成した理由です 🤟

番外編

ところで、Galagino の README.md には次のような注意喚起が書かれています。

重要なお知らせ2: I2S / DAC のサポートは、ESP32 ボード パッケージ バージョン 2.0.10 以上では機能していないようです。 きれいなサウンドを得るには、ESP32 ボード パッケージ 2.0.9 以前を使用してください。

一方、作者が指摘した 2.0.10 の問題(Issue #8467)は 2.0.12 で解決 されているので、2.0.12 以上なら OK と思います。また I2S ドライバーは 2.X から 3.X へのバージョンアップ時に 完全に再設計されリファクタリングされました が、本記事執筆時点の最新版(3.1.3)ではまだ後方互換性が残っていて、(音質は別として)音は出ると思います(無責任 😜)。

さて、ソースコードを眺めていて CYD で動かすには幾つかの注意点があることに気付いたので書き留めておきたいと思います。

【config.h】

まず 32行目 のコメントアウトを外します。

#define CHEAP_YELLOW_DISPLAY_CONF

ST7789 を搭載した新版の CYD では 68行目 は 80MHz の設定で OK でしょう。

#define TFT_SPICLK 80000000 // 40 Mhz. Some displays cope with 80 Mhz

同時に 78行目 をコメントアウトします。

// #define TFT_ILI9341 // define for ili9341, otherwise st7789

また本記事の接続図では CYD のピン配を逆にしているので、96〜97行目 を変更します。

#define NUNCHUCK_SDA 27 // 22
#define NUNCHUCK_SCL 22 // 27

【video.cpp】

ST7789 で色の反転が不要な場合は、104行目0x210x20 に変更します。

  0x20, 0,                          // INV OFF

またはコメントアウトしても良いでしょう。

//0x21, 0,                          // INV ON

懐かしのアーケードゲームで遊ぶには?

さて、勢いで幾つか安めのコントローラーや部品を買って散財しちゃいましたが、著作権法に触れる恐れなくゲームを楽しむなら、すでに Switch を持ってる僕としては My Nintendo Store アーケードアーカイブス がベストというのが今回のオチです。

今回がキッカケで、ゼビウスドラスピ をダウンロードして楽しんでます 🤗