ESP32 2432S028R (CYD)+任天堂コントローラー=懐かしのアーケードゲームマシン
前回の取り組み で、”黄色い基板”(Cheap Yellow Display、以下 CYD)の I2C 接続方法が分かったので、色々と試したくなりました。そこで本記事では、ESP32-Cheap-Yellow-Display に紹介されている Tetris with Nunchuck と Galagino を参考に、各種コントローラーの動作検証を報告したいと思います。
ちなみに、実際の Galagino の動作例は以下をご参照ください。
ハードウェア編
CYD

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 で動作可能です。


ということで、外付けのアンプなどは不要で「SPEAK」にスピーカーを直接接続できますが、結構な出力なので、8Ω 1W の ブレッドボード用ダイナミックスピーカー を接続する際に 200Ω の半固定抵抗で出力を絞れるようにしています。
コントローラー
I2C 接続が可能なコントローラーは、任天堂や Adafruit 製品、micro:bit 用や自作版など、情報源には事欠きません。今回は Arduino との接続が容易でライブラリが揃っている任天堂系と Adafruit 製品を接続し、動作を検証してみました。
Wii および NES 用コントローラー

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

これらは冒頭に紹介した Tetris と Galagino で使われている NintendoExtensionCtrl で簡単に動かすことができます。
また CYD との接続は、AliExpress で購入できる1個 30円 程度の ヌンチャクアダプター や、スイッチサイエンスで買える Adafruit STEMMA QT/Qwiic互換 Wiiヌンチャクアダプタボード(税込 ¥735) が簡単です。



また「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

コレ、手の大きいガイジンに操作できるの?ってぐらいに小さいです。スイッチサイエンス(¥1,602)(僕で最後だったようで、売り切れ中)か DigiKey(¥1,217) で入手可能です。また JST SH 1.0mm ピッチの Qwiic コネクタは、秋月電子の コネクター付コード 4P 黒赤青黄(¥100) が便利です。
サムスティックが滑りやすいという方には、Switch 用 がピッタリ嵌ります。
I2C 接続するには Adafruit_Seesaw が必要ですが、色々な周辺機器に対応するためか、実行速度が他に比べて少し遅いです。そのためこのゲームパッドに必須なパーツだけを残してスリム化した Gamepad_Seesaw を作成してみました(後述)。
自作編

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 クラシックコントローラー

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_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行目 の 0x21
を 0x20
に変更します。
0x20, 0, // INV OFF
またはコメントアウトしても良いでしょう。
//0x21, 0, // INV ON
懐かしのアーケードゲームで遊ぶには?
さて、勢いで幾つか安めのコントローラーや部品を買って散財しちゃいましたが、著作権法に触れる恐れなくゲームを楽しむなら、すでに Switch を持ってる僕としては My Nintendo Store アーケードアーカイブス がベストというのが今回のオチです。