今回はスーパーファミコン(SFC)のコントローラーをUSB Gamepad化する基板を作りました。
こちらの記事では、SFCコントローラー USBゲームパッド化基板の制作過程を順を追って紹介します。
設計データはgithubに公開していますので、自作にもぜひ挑戦してみてください。
- お知らせ
- こんな感じの仕様で作ります。
- 回路図を描いてみます
- アートワークを描いてみます
- 基板を製造します
- 部品を実装します
- プログラムを書いてみます
- SFCコントローラーを改造してみます
- 動かしてみます
- 最後に
お知らせ
こちらの記事で紹介している基板は製品化して、以下で頒布しています。
geekyfab.booth.pm
また、自作キットはスイッチサイエンスで頒布しています。
興味を持っていただいた方はご覧ください。
では本編です。
こんな感じの仕様で作ります。
SFCコントローラーの筐体にピッタリ収まるUSBゲームパッドの基板を作ります。
SFCコントローラーはボタン数が少ないので、今どきのゲームも遊べるように、Start, Selectボタンを長押しすることでボタンの割り当てを変更できるようにします。
回路図を描いてみます
全体回路図はこういう感じにしました。
基本的にはメインマイコンであるPIC16F1459のIOポートにボタン用のパッドを直つなぎしている回路になっています。
PIC16F1459はIOポートによっては内蔵プルアップ抵抗がついてたりついてなかったりのようなので、念のためすべてのボタン用のパッドにはプルアップ抵抗用のランドを設けてあります。
メインマイコン PIC16F1459
メインマイコンにはPICマイコンのPIC16F1459を使用しています。
このPICマイコンは俗にいうUSBマイコンで、USBデバイスとしてふるまうことができます。
IOポート数も多いところもよいところです。
キーマトリックスなどの回路を組まなくても、SFCコントローラーのボタン数をカバーすることができるので、回路が単純になります。
アートワークを描いてみます
今回のプロジェクトの肝です。
SFCコントローラーの筐体と基板から寸法を実測し、筐体にぴったりとはまり、ボタン位置も合うようにアートワークを設計します。
とはいえ、一発でSFCの筐体にピッタリはまる基板を作るのは難しくて、基板を製造して微調整してまた作って…みたいなことをしました。
そして、最終的に出来上がったアートワークがこちらになります。
左下にちょろっとついてるのはL,Rボタン用の基板になります。
基板を製造します
基板製造はAllpcbに注文をしました。
基本的にはデフォルトのオプションになりますが、表面処理については金メッキ処理を選択する必要があります。
これは、ボタン用のパッドの酸化を避けるためです。
半田レベラーや銅箔むき出しではボタン用のパッドが酸化して反応しなくなってしまうので、金メッキ処理をして酸化を防ぎます。
そして、届いた基板がこちらです。
金メッキでピカピカのきれいな基板です。
ロゴをレジスト抜きで作ってみましたが、なかなかかっこいいです。
部品を実装します
早速部品を実装していきます。
そして、実装後の基板がこちらです。
配布するならもっときれいに作りますが、自分用なのでこんなもんでしょう。
プログラムを書いてみます
USB Gamepad化基板用のソースコード全文はGithubに公開していますので、この記事ではいくつかのポイントだけ紹介します。
このプログラムはMicrochip社が提供するMicrochip Libraries for Applications (MLA) をベースに書いています。
MLA内にはPICマイコンごとにプロジェクトファイルのサンプルが用意されています。
例えば、今回使用するPIC16F1459のUSB Joystickのサンプルプロジェクトは下記階層にあるはずです。
mla\v2018_11_26\apps\usb\device\hid_joystick\firmware\low_pin_count_usb_development_kit_pic16f1459.x
PICのポート設定とタイマ初期化
PICのポート設定とタイマ初期化はmain.cのmainの初めのほうで行っています。
/* set all ports input*/ TRISA = 0x30; TRISB = 0xf0; TRISC = 0xff; /* enabling internal pull up*/ OPTION_REGbits.nWPUEN = 0; WPUA = 0x30; WPUB = 0xf0; /* all ports used as degital ports.*/ ANSELA = 0x00; ANSELB = 0x00; ANSELC = 0x00; /* initializing timer0 and interruption*/ OPTION_REGbits.PS = 0b111; // clock divided by 256 OPTION_REGbits.PSA = 0; // enabling prescaler OPTION_REGbits.TMR0SE = 0; // edge select (rise) OPTION_REGbits.TMR0CS = 0; // clock source select (internal) OPTION_REGbits.INTEDG = 1; // interrupt edge select (rise) // INTCONbits.TMR0IF = 0; // reset timer0 interrupt flag // INTCONbits.TMR0IE = 1; // enabling peripheral interrupts INTCONbits.GIE = 1; // enabling interrupts // setting initial value of timer 0. // 1 clock = 256/16MHz = 16us // the timer interrupt happens every 16us x 0xff(255) = 4080us . // // set the timer0 value so that timer interruption happens every 4000us. // 80us / 16us = 5 clocks TMR0bits.TMR0 = (uint8_t)5;
コメントの通りです。
IOポートをすべて入力にして、内部プルアップがついてるポートはすべて有効にして、デフォルトのポート設定はアナログなのですべてデジタルに設定しています。
次に、タイマー0のパラメーターを設定しています。
USB通信に使うデータ型
USB通信で送るpacketのデータの型はapp_device_joystick.h内で次のように宣言しています。
typedef union _INTPUT_CONTROLS_TYPEDEF { struct { struct { uint8_t y:1; uint8_t b:1; uint8_t a:1; uint8_t x:1; uint8_t L1:1; uint8_t R1:1; uint8_t L2:1; uint8_t R2:1;// uint8_t select:1; uint8_t start:1; uint8_t left_stick:1; uint8_t right_stick:1; uint8_t home:1; uint8_t :3; //filler } buttons; struct { uint8_t hat_switch:4; uint8_t :4;//filler } hat_switch; struct { uint8_t X; uint8_t Y; uint8_t Z; uint8_t Rz; } analog_stick; } members; uint8_t val[7]; } INPUT_CONTROLS;
ボタンの押下に合わせて、これらの変数に値を格納することでUSB Gamepadを動作させることができます。
例えば、"Y"ボタンが押されたら、変数yの値を1にしてUSBでPCに送信します。
これにより、USB Gamepadで"Y"ボタンが押されたんだなということがわかります。
ボタン処理部
ボタン処理部はmy_app_device_gamepad.cに書いています。
ここのコードでは、
- ボタンの状態の監視
- start/select長押し時の処理とステートの管理
を行っています。
ボタン状態の監視は例えば下記のような簡単なコードをボタン分用意しているだけです。
gamepad_input->members.buttons.select = BUTTON_IsPressed(BUTTON_SELECT);
start/select長押し時の処理にはPICマイコンの初期化したタイマー0を使っています。
以下はstartの長押し処理のための関数です。
void ChangeSWMode_Button_Start(void){ uint16_t cnt_timer =0; if (BUTTON_IsPressed(BUTTON_START)){ INTCONbits.TMR0IF = 0; // reset timer0 interrupt flag TMR0bits.TMR0 = (uint8_t)5; cnt_timer = 0; while(BUTTON_IsPressed(BUTTON_START)){ if(INTCONbits.TMR0IF){ // INTCONbits.TMR0IF happens every 4ms cnt_timer++; if(cnt_timer >=500){ // 2s flags.sw_flag = ~(flags.sw_flag); cnt_timer =0; while(BUTTON_IsPressed(BUTTON_START)); } INTCONbits.TMR0IF = 0; TMR0bits.TMR0 = (uint8_t)5; } } } return; }
USB HID report descriptors
USB HID report descriptorsは資料が公式のドキュメント以外あまりなく、仕様もわかりにくく、かなり厄介でした。
内容的にはusb_descriptors.cに書かれている下記部分になります。
const struct{uint8_t report[HID_RPT01_SIZE];}hid_rpt01={{ 0x05,0x01, //USAGE_PAGE (Generic Desktop) 0x09,0x05, //USAGE (Game Pad) 0xA1,0x01, //COLLECTION (Application) 0x15,0x00, // LOGICAL_MINIMUM(0) 0x25,0x01, // LOGICAL_MAXIMUM(1) 0x35,0x00, // PHYSICAL_MINIMUM(0) 0x45,0x01, // PHYSICAL_MAXIMUM(1) 0x75,0x01, // REPORT_SIZE(1) 0x95,0x0D, // REPORT_COUNT(13) 0x05,0x09, // USAGE_PAGE(Button) 0x19,0x01, // USAGE_MINIMUM(Button 1) 0x29,0x0D, // USAGE_MAXIMUM(Button 13) 0x81,0x02, // INPUT(Data,Var,Abs) 0x95,0x03, // REPORT_COUNT(3) 0x81,0x01, // INPUT(Cnst,Ary,Abs) 0x05,0x01, // USAGE_PAGE(Generic Desktop) 0x25,0x07, // LOGICAL_MAXIMUM(7) 0x46,0x3B,0x01, // PHYSICAL_MAXIMUM(315) 0x75,0x04, // REPORT_SIZE(4) 0x95,0x01, // REPORT_COUNT(1) 0x65,0x14, // UNIT(Eng Rot:Angular Pos) 0x09,0x39, // USAGE(Hat Switch) 0x81,0x42, // INPUT(Data,Var,Abs,Null) 0x65,0x00, // UNIT(None) 0x95,0x01, // REPORT_COUNT(1) 0x81,0x01, // INPUT(Cnst,Ary,Abs) 0x26,0xFF,0x00, // LOGICAL_MAXIMUM(255) 0x46,0xFF,0x00, // PHYSICAL_MAXIMUM(255) 0x09,0x30, // USAGE(X) 0x09,0x31, // USAGE(Y) 0x09,0x32, // USAGE(Z) 0x09,0x35, // USAGE(Rz) 0x75,0x08, // REPORT_SIZE(8) 0x95,0x04, // REPORT_COUNT(4) 0x81,0x02, // INPUT(Data,Var,Abs) 0xC0 //END_COLLECTION } };
ここの書き方は需要があれば別の記事で書きたいと思います。
プログラムのライセンスについて
Microchipが提供するMLAのソースコードのほとんどはApache License 2.0で提供されています。
私が追加したソースコードについてもApache License 2.0で提供することにしましたので、個人利用・商用利用かかわらずご自由にお使いください。
ただし、USB プロダクトID(PID)についてはMicrochip社とのサブライセンス契約により提供されているものですので、商用利用不可となります。
ファイルとしてはmy_usb_pid.hが商用利用不可です。
SFCコントローラーを改造してみます
出来上がったプログラムをpickit3で基板に書き込めば、基板は完成です。
では早速、出来上がった基板を使ってSFCコントローラーをUSBゲームパッド化してみましょう。
これを、
こうして、
こうして、
こうじゃ。
あとはふたを戻して出来上がりです。
早速動かしてみましょう。
動かしてみます
Windowsで動作確認
まずは動作確認です。
PCに挿してみると、コントロールパネルにちゃんと"SFC Gamepad"が認識されました。
では、上のアイコンを右クリック→ "ゲームコントローラーの設定"→"プロパティ"と移動し、ゲームパッドの動作確認画面を表示させます。
で、動かすとこんな感じです。
しっかり動いてくれてますね。
初めて動いたときはかなりうれしかったです。
Raspberry PIで動作確認
Raspberry PIでの動作確認もしておきました。
ラズパイでGamepadを使うには"Joystick"パッケージを使用します。
また、動作確認には"jstest-gtk"が使いやすいです。
まずはterminalから次のコマンドを実行してこれらのパッケージをインストールします。
sudo apt install joystick jstest-gtk
続いて、terminalから次のコマンドを実行してjstest-gtkを立ち上げます。
jstest-gtk
あとは、windowsと同じ感じです。
最終的に動作確認したOSは次の通りです。
- Raspberry PI 3
- Raspberry Pi OS
- Recalbox for RASPBERRY PI 3
- RetroPie 4.7.1 for RASPBERRY PI 2/3
- Raspberry PI 4
- Raspberry Pi OS
- Recalbox for RASPBERRY PI 4/400
- RetroPie 4.7.1 for RASPBERRY PI 4/400
その他は確認してませんが、特殊なドライバーは使っておらず、標準ドライバーで動いてるのでおそらく行けるでしょう。
Steamで遊んでみる
USBゲームパッド化したSFCコントローラーで、Steamのゲームを遊んでみました。
まずは子供の頃スーファミでよく遊んでたボンバーマンのバトロワゲーム、"スーパーボンバーマン R オンライン"。
最新のボンバーマンがSFCコントローラーで遊べるなんて、感動です…
大好きなゲーム"UNDERTALE"を手掛けたToby Fox氏が贈る新作、"DELTARUNE"。
最近のゲームなのにSFCコントローラーでプレイすることに全く違和感がない。
今時な3Dパーティーゲーム"Fall Guys" さすがに無理かと思いましたが、全然遊べました!
テイストが1930年代のカートゥーンっぽい"Cuphead"
SFCコントローラーにめちゃくちゃあう。
EPIC Gamesで遊んでみる…?
EPIC Gamesでは対応コントローラーが限られているようで、遊べませんでした…
FORTNITEも遊びたかったなぁ…
<後日談>
x360ceを使用することでできました。
詳しくは↓
geekyfab.com
最後に
今回は、スーパーファミコンのコントローラーをUSBゲームパッド化する基板を作り、実際にスーファミのコントローラーを改造してゲームを遊んでみました。
スーファミのコントローラーってすごく好きなんで、最近のゲームもこれで遊びたいなぁと思ったのが制作のきっかけです。
結果、大満足な仕上がりになりました。
作った基板のガーバーデータとファームウェアのソースコードはGithub上で公開してるので、よろしければぜひ作ってみてください。
今回はこれで終わります。