最近のArduino IDEはXInputゲームコントローラーを作るのが面倒
Windows PCやPlaystation Classicなどでレトロゲームを遊ぶとき、自作のXInput方式のゲームコントローラー(Xbox360互換)を使っています。RetroArchベースのシステムでは設定せずに使えて便利ですし、現時点でWindows標準のコントローラーですから。
この自作コントローラーはSparkfun Pro Microを使ったもので、プレイステーションのノーマルのパッドだけでなくDUALSHOCKやネジコンなど複数のコントローラーに対応しています。DUALSHOCKはACアダプタをつなげばランブル機能(バイブレータ)も動作します。


その他にWiiのヌンチャク(振動モーター付き)やN64コントローラー(振動パック付き)などもXInputのコントローラーにしてきたのですが、これらは全てArduinoのXInputライブラリを使っています。
このXInputライブラリは結構古くからあり、コントローラーもかなり前に作っていました。今回、Playstation Classicに入れているRetroarchのcoreを手動でアップデートした時にアナログスティックの感度調整もしたくなり、コントローラープログラムを再コンパイルすることを思い立ちました。
作業はArduino IDEで行うのですが、最近はPlatformIOでマイコン開発を行っていてArduino IDEはずっと使っていませんでした。そこで古いバージョンのものはライブラリも含めて消してしまい、まっさらな状態からインストールし直しました。ボードパッケージやライブラリなど必要なものはすべて新しくして上記コントローラのスケッチの再コンパイルを行ったのですが、ここで問題が発生しました。
普通Sparkfun Pro MicroなどのUSBインターフェースのあるマイコンはブートローダーでプログラムをPCからUSB経由でダウンロードしてメモリに書き込みます。ところがXInputライブラリは特殊で、ブートローダを書き換えてXboxコントローラーに見せかけています。そのため一度スケッチを書き込むとUSBからのプログラム書き込みはできなくなります。
このPro Microにはリセットスイッチを付けたのでスイッチを素早く2回押すとわずかな時間(8秒とのこと)comポートが見えるのですが、書き込みするにはタイミングを計ってうまくスイッチを押す必要があります。しかし大抵は失敗します。なので確実に書き込むには「書き込み装置」が必要です。
書き込み装置を使うときはメニューから使用する装置を選択する必要があります。

XInputライブラリはマイコンの機種別に専用のボードを選択する必要があります。ところがXInputのボードに切り替えると書き込み装置の選択メニューが消えてしまうのです。

このままではXInputコントローラーにしたマイコンに、スケッチを書き込むことが出来ません。以前は書き込めていたはずなので、最近のArduino IDEの問題でしょう。
せっかく最新の環境にしたのにバージョンダウンするのは嫌なので、なんとかメニューを出せないかと設定ファイルなどをいじってみましたが、どうにもなりませんでした。
他になにかやり方がないか色々試した結果、あまりスマートな方法ではありませんがなんとか書き込むことが出来ました。その手順ですが、せっかくの機会なので新たにコントローラーを作りながら紹介してみたいと思います。
今回、Xbox360に仕立てるコントローラーはWiiのクラシックコントローラーにしました。ずいぶん前に電子工作のためにハードオフで購入したものですが、今は使っていないので再利用します。

マイコンはLinino OneというArduino Yun MINI互換のマイコンボードにしました。2024年の秋月電子福箱に入っていたものです。XInputライブラリでサポートされているというのもありますが、最近使うこともなくなったのでこれも再利用することにしました。


それではXInputライブラリから。
導入手順についてはネットの情報を参照してください。こちらのサイトがとても分かりやすいです。「手順」の項目の「ライブラリインストール」までの作業を行ってください。
配線ですが、WiiのクラシックコントローラーはI2C接続なので簡単です。接続コネクタはジャンクパーツで自作してみました。


スケッチはこちら。
#include <Wire.h>
#include <XInput.h>
#define PADADDRESS 0x52
uint8_t LeftX;
uint8_t LeftY;
uint8_t RightX;
uint8_t RightY;
uint8_t LeftT;
uint8_t RightT;
bool ButtonUp;
bool ButtonDown;
bool ButtonLeft;
bool ButtonRight;
bool ButtonA;
bool ButtonB;
bool ButtonX;
bool ButtonY;
bool ButtonL;
bool ButtonR;
bool ButtonZL;
bool ButtonZR;
bool ButtonSelect;
bool ButtonHome;
bool ButtonStart;
uint8_t padDat[6];
void setup()
{
Wire.begin();
sendTwoByte(0xf0, 0x55);
sendTwoByte(0xfb, 0x00);
XInput.setJoystickRange(0, 64);
XInput.setTriggerRange(0, 32);
XInput.setAutoSend(false);
XInput.begin();
}
void loop()
{
int count = 0;
sendByte(0);
delay(20);
Wire.requestFrom(PADADDRESS, 6);
while(Wire.available())
padDat[count++] = Wire.read();
if(count >= 6)
{
LeftX = padDat[0] & 0b00111111;
LeftY = padDat[1] & 0b00111111;
RightX = ((padDat[0] & 0b11000000) >> 3) + ((padDat[1] & 0b11000000) >> 5)
+ ((padDat[2] & 0b10000000) >> 7);
RightY = padDat[2] & 0b00011111;
LeftT = ((padDat[2] & 0b01100000) >> 2) + ((padDat[3] & 0b11100000) >> 5);
RightT = padDat[3] & 0b00011111;
ButtonRight = (padDat[4] & 0b10000000) == 0;
ButtonDown = (padDat[4] & 0b01000000) == 0;
ButtonL = (padDat[4] & 0b00100000) == 0;
ButtonSelect = (padDat[4] & 0b00010000) == 0;
ButtonHome = (padDat[4] & 0b00001000) == 0;
ButtonStart = (padDat[4] & 0b00000100) == 0;
ButtonR = (padDat[4] & 0b00000010) == 0;
ButtonZL = (padDat[5] & 0b10000000) == 0;
ButtonB = (padDat[5] & 0b01000000) == 0;
ButtonY = (padDat[5] & 0b00100000) == 0;
ButtonA = (padDat[5] & 0b00010000) == 0;
ButtonX = (padDat[5] & 0b00001000) == 0;
ButtonZR = (padDat[5] & 0b00000100) == 0;
ButtonLeft = (padDat[5] & 0b00000010) == 0;
ButtonUp = (padDat[5] & 0b00000001) == 0;
XInput.setButton(BUTTON_LOGO, ButtonHome);
XInput.setButton(BUTTON_A, ButtonB);
XInput.setButton(BUTTON_B, ButtonA);
XInput.setButton(BUTTON_X, ButtonY);
XInput.setButton(BUTTON_Y, ButtonX);
XInput.setButton(BUTTON_LB, ButtonZL);
XInput.setButton(BUTTON_RB, ButtonZR);
XInput.setButton(BUTTON_BACK, ButtonSelect);
XInput.setButton(BUTTON_START, ButtonStart);
XInput.setButton(BUTTON_L3, ButtonL);
XInput.setButton(BUTTON_R3, ButtonR);
XInput.setButton(DPAD_UP, ButtonUp);
XInput.setButton(DPAD_DOWN, ButtonDown);
XInput.setButton(DPAD_LEFT, ButtonLeft);
XInput.setButton(DPAD_RIGHT, ButtonRight);
XInput.setTrigger(TRIGGER_LEFT, LeftT);
XInput.setTrigger(TRIGGER_RIGHT, RightT);
XInput.setJoystick(JOY_LEFT, LeftX, LeftY);
XInput.setJoystick(JOY_RIGHT, RightX * 2, RightY * 2);
XInput.send();
}
}
void sendByte(uint8_t bt)
{
Wire.beginTransmission(PADADDRESS);
Wire.write(bt);
Wire.endTransmission();
}
void sendTwoByte(uint8_t bt1, uint8_t bt2)
{
Wire.beginTransmission(PADADDRESS);
Wire.write(bt1);
Wire.write(bt2);
Wire.endTransmission();
}もっと簡潔に書けますが、XInputライブラリの使い方がわかりやすいように書いてみました。アナログスティックやアナログスイッチは生のデータをそのまま使っているので、本格的に使いたいときはコントローラーの入力値を補正するほうがいいでしょう。
各ボタンの割り当てはXboxとWiiコントローラーのボタン位置が一致するようにしてあります。
それではスケッチの書き込みです。初回はマイコンのブートローダを使ってUSBシリアルで書き込めます。使用するマイコンに対応したXInputボードを選びcomポートも選択して「書き込み」を行います。

これでXbox360コントローラーが作れました。

驚いたことにLinino OneをPlaystation ClassicのUSBポートにつないでも動きませんでした。ArduinoのATmega32U4とLinux用のAtheros AR9331という2つのCPUを積んでいるのでPlaystation ClassicのUSBポートの能力では電力不足になるのでしょう。USBハブを使って電力を供給してやるとちゃんと動くようになりました。
問題はこの後、スケッチを再書き込みする場合です。上記で説明したようにマイコンのUSBをPCにつないでもcomポートは現れません。
この後の作業には書き込み装置が必要です。こちらのサイトなどを参考にして書き込み装置を作りましょう。書き込み装置があればブートローダがらみのトラブルの際に役に立つので作っておいて損はありません。個人的にはブートローダは使わずにArduino ISPによる書き込みしか行っていません。(PlatformIOでも)
それでは書き込み装置とマイコンをつないで、再度スケッチ書き込みが行えるようにします。

まず、ボードをXInputでなくマイコン標準のボードに切り替えます。comポートは書き込み装置のシリアルポートに変更します。

書き込み装置を選択します。Arduinoで作った場合は「Arduino as ISP」や「Arduino as ISP(ATmega32U4)」を選びます。
続いて「ブートローダを書き込む」を選択します。書き込みが終わるとXboxの情報が消えcomポートが見えるようになります。
書き込み装置をはずします。ボード選択を再びXInputのものに戻します。スケッチを修正したら、マイコンのUSBをPCと接続しcomポートも選択し直して「書き込み」を行います。

以後スケッチを書き込むときは上記作業の繰り返しとなります。
他にもっとましな方法があるかもしれませんが、コントローラーが完成した後は書き込み作業を行うことはなくなるのでこれで良しとしました。
