はしのした


2025-09-23 無いなら無いで作る

USB HID キーボードの入力を取り込んで、ゲームキューブのコントローラで使われている JoyBus というプロトコルに変換するシステムを作ったよ、というお話になります。 ソースコードや基板の回路図・レイアウト一式も公開しています。こちらから ダウンロードしてください。ライセンスは New BSD です。

  名前は LTek2GC

先日、近く引越しを予定している新居におうち DDR 環境を整えたいと思い、海外製のメタルパッド を2つ購入。 世の中にはゲーセン筐体と本当に同じスペックのパネル(あるいはそのもの)を置く人もいますが、あれは片側で重量 100 kg。1人で持ち運べるものではありません。 一方こちらは片側で重量 18 kg と、持ち運びも十分可能なサイズ。せっかくなら別の活用法もしてみたいなと考えました。

そうするとまずは Switch との接続できないかなと考えるわけですが、残念ながらそのままでは認識せず。 Switch の場合、一般的なゲームパッドは本体からは認識されず、Switch からの呼びかけに「自分はプロコンだよ」と応答する必要があります。 もっとも、仮に今回のメタルパッドがそうしていたとしても、コントローラの割当てを変えるために L+R ボタンの同時押しが必要なので、どのみち詰みです。 というわけで、メタルパッドからの入力を、何とかして Switch が認識できる形に変換してあげるシステムが必要になりました。

技術選定

ここで目をつけたのは、Wii U 版や Switch 版のスマブラとともに発売された、ゲームキューブコントローラ接続タップです。 ゲームキューブ(あるいは NINTENDO 64)で使われていた JoyBus というプロトコルは、USB と比べてもかなり単純なものであることを知っていました。 これを使って、ゲームキューブのコントローラが2個接続されたと、Switch に認識させられれば良いわけです。

そうすると、今回やるべきことは以下の要素に分割できることになります。

  • USB 接続されたメタルパッド2つ(と、必要なら USB ハブ)の入力を読み取る
  • JoyBus 信号をプレイヤーごとに生成する
  • ゲームキューブコントローラの信号線を差し替える

最初の要素にふさわしいハードウェアとして、黒井宏一氏 が製作された同人ハードウェアである、かんたん USB ホスト(HID キーボード用) が見つかりました。 USB ハブを通じた複数台のキーボード接続も可能、動作モードをコマンドモードに切り替えればキーを離したことも認識可能など、欲しい機能がしっかり揃っていました。 また、今回購入したメタルパッドは、自身を USB HID キーボードとして認識させるモードを持っていました(キー割当ては WASD か IJKL の2通り)。 ですので、USB ハブを通じて2つのメタルパッドをこれに接続するだけで、パネルの踏む/離すがコマンドとして取得できることになります。 極めつけは スイッチサイエンスで普通に買えます。 ありがたく使わせていただくことにしました。

2つ目の要素としてよく見かけるのは,Raspberry Pi Pico でしたが、今回は採用を見送りました。 単体のゲームパッドとして動かすのであれば、間違いなく第1選択となります。 例えば、自分好みのレバーレスコントローラを作りたいといった場合ですね。 一般の USB ゲームパッド、ゲームキューブコントローラなど、様々なオープン実装がすぐに見つかります。 しかし今回は JoyBus 信号を2本生成しなければいけません。 JoyBus では、本体側からリクエストを受け取ったら、おおよそ 100 μs 以内に応答を始めなければいけません。 そして、(後述しますが)1回の応答には典型的には 256 μs かかります。 この要求を Raspberry Pi Pico で満たそうとすると、1つのプロセッサコアをこのリクエスト待ちと応答のためだけに使わなくてはなりません。 デュアルコアの Raspberry Pi Pico で2本の JoyBus 信号を生成しようとすると、他の処理ができなくなって詰みます。 割込みなどを使って頑張れば何とかなったのかもしれませんが、かなりの困難が予想された(また、ちゃんと動かせる自信が持てなかった)ため、この方法は諦めました。

かわりに採用したのは、FPGA です。ぷよスロの画面キャプチャ のとき以来の登場です。 円安やら半導体不足やらを経て、AMD(当時は Xilinx)の FPGA も随分と高くなってしまったので、今回は中国 Gowin の FPGA を使いました。 Sipeed 社の Tang Nano 9K です。秋月で 3,000 円です。安いですね。 回路規模がもっと小さいものだと 1,500 円から手に入るのですが、これだとギリギリになりそうだったので、安定を取りました。

最後の要素ですが、中国系の通販(あるいは、そこから仕入れているネットショップ)で売っているゲームキューブコントローラの延長ケーブルを買うか、中古のゲームキューブコントローラや GBA ケーブルを買ってきて、ケーブルをチョキンとしてしまいましょう。 私の場合は、中古の GBA ケーブルがたまたま近所に売ってたので、それを使いました。現時点では 1,000 円弱が相場のようです。 GBA ケーブルにはコントローラ振動のための 5 V の電源線がありませんが、今回はコントローラ側から電源を取らないので、特に問題ありません。 引き出すのは信号線と GND 線だけです。

基板設計

必要な部品が揃ったら、ブレッドボード上にがしがしと回路を組んでいきます。 ひと通り動くことが確認できたところで、KiCad を使って回路図を清書します。 最終的にはこんな感じの回路図になりました。

LTek2GC 回路図

5 V の電源は、Tang Nano 9K に搭載された USB Type-C ポート(回路図には記載なし)から、31番の電源出力ピンを通じて、かんたん USB ホストや、そこに接続された USB ハブ・メタルパッドに供給します。 かんたん USB ホストは、9, 10, 11, 16, 20 番ピンを GND に接続し、通信速度 115,200 baud/s、イベントモード、リピート無効に設定しておきます。

基板上にはタクトスイッチが2個あり、各プレイヤーの L+R ボタンに対応します。 ついクセでプルダウン抵抗(R1, R2)を外付けしてしまいましたが、Gowin の FPGA には内部プルアップ/プルダウンがあるので、なくても問題なさそうです。 実際、7番ピンの SWAP という入力はそうしています。これは、↑パネルの挙動を切り替えるための入力です。 J3 のジャンパが接続されている場合は、GND に接続されることにより信号が Low になります。 このとき、↑パネルを上ボタンに、メタルパネル右上にあるスタートボタンは A ボタンに、それぞれ割り当てます。 接続されていない場合は、FPGA の内部プルアップにより信号が High になります。 この場合は↑パネルとスタートボタンの割当てが入れ替わります。つまり↑パネルが A ボタン、スタートが上ボタンです。

一方、JoyBus の信号にあるプルアップ抵抗(R3 と R4)は必要です。 数百 Ω 程度の抵抗(回路図では手元にあった 470 Ω を使いました)で、少し強めに High に引っ張る必要があるためです。 JoyBus の信号はオープンドレイン、つまり Low を出力したいときだけトランジスタで信号を Low に引っ張り、High を出力したいときは何もしない、という仕様になっています。 双方向の通信を1本の信号で行う場合、意図せぬショートを防ぐため、オープンドレインは一般的です。 ただ、JoyBus は 200~250 kbaud/s というそこそこの速度で通信を行います。 プルアップの抵抗値が小さすぎると、High に引っ張り上げるのが遅れてしまい、信号が正しく読み取れなくなる可能性があります。 内部プルアップは一般に kΩ 単位の大きな抵抗値をもちますので、これでは力不足なわけです。

JoyBus プロトコル概説

※ この部分については、Jeff Longo 氏による解析結果 を大いに参考にしました。御礼申し上げます。

NINTENDO 64 やゲームキューブで使われている JoyBus は、ざっくり言えば1線式の非同期シリアル通信です。 本体側(ゲームキューブコントローラ接続タップを含む。以下同じ)からのリクエストが先にあって、それにコントローラが応答するという主従関係をもちます。 JoyBus で送受信するすべてのシンボルは、所定の時間の Low → 所定の時間の High からなります。 本体とコントローラが '0' や '1'、ストップビットを送信する際の所定の時間は、下表の通りです。単位はすべて μs です。

方向 ビット Low High
本体 → コントローラ'0' 3.751.25
'1' 1.253.75
ストップ2.5 2.5
コントローラ → 本体'0' 3 1
'1' 1 3
ストップ2 2

つまり、本体からコントローラへの通信速度は 200 kbaud/s、コントローラから本体へは 250 kbaud/s です。 スタートビットはありません。

本体から送られてくるコマンドは、1バイト単位で、最大3バイトです。 各バイトのデータは、上位ビットから順に送られます。 最初の1バイトがコマンドの種類になっているので、ここを見て適切な応答を返すことになります。

コマンドにはいくつか種類があるようですが、ここではゲームキューブコントローラとして認識してもらうための、最低限のものだけを列挙します。 NINTENDO 64 で使われていたものを含む、その他のコマンドの情報は、他のサイトを参照してください。

認識(0x00) は、本体側からコントローラが認識されていないときに、定期的に送信されます。 リセット(0xFF) は、コントローラの状態をリセットするときに送信されるらしいです。 いずれのコマンドのリクエストに対しても、返すべき応答は同じで、3バイトからなります。 第1~2バイトは、ゲームキューブコントローラを表す 0x090x00 を返します。 第3バイトは、すでにアナログスティックの初期値に関する情報を送っているなら 0x03、そうでないなら 0x23 を返します。

状態(0x40) は、本体側からコントローラが認識されているときに、定期的に送信されます。 応答は8バイトからなり、最初の2バイトがスイッチのオン/オフの情報、残りの6バイトがアナログスティックや L/R トリガの情報になります。 最初の2バイトは以下の情報から構成されます。いずれもスイッチが押されているときに '1' となります。

バイトMSB6 5 4 3 2 1 LSB
第1 '0' '0' StartY X B A
第2 '1' L R Z

※ アナログスティックの初期値に関する情報を送っているなら '1'

第3バイトから先は、左スティックのX座標・Y座標、CスティックのX座標・Y座標、L・R トリガの値を、それぞれ8ビットで表します。 今回はアナログ入力は使いませんので、座標は適当にすべて 128、L・R トリガは押されているときのみ 64 に設定しています。

また、アナログスティックの初期値に関する情報は、初期状態(0x41) のコマンドのリクエストに応答する形で送るようです。 応答は10バイトからなり、第8バイトまでは状態コマンドへの応答と同じ、第9~10バイトで A・B ボタンのアナログ値を送ることになっているようです。 ゲームキューブの開発段階では、A・B ボタンもアナログ入力で押し込み具合を測れるようにするつもりだった、なんて言われてますね。 長い状態(0x42) というコマンドもあるようですが、これは初期状態と同様の10バイトのフォーマットで応答すればいいようです。

FPGA 論理設計

これを踏まえて FPGA に実装する回路のロジックを実装していきます。 今回は、(仕事で今後使うことになるという事情もあって)VHDL の比較的新しい規格である VHDL-2008 で記述しています。 component 周りのうざったさは残っているものの、それ以外は無駄な制約が取り払われていて、だいぶ書きやすくなってますね……ってのはともかく。

今回は、UART と JoyBus の送信回路・受信回路を、それぞれ本体の回路とは分けて記述しています。 また、UART については1文字単位での送受信回路をまず用意し、それを使って複数文字の送受信を行う回路を作成しています。 さらに、複数文字の送信では、FIFO を使って送信待ちの文字列を管理します。 このため、回路の階層関係は以下の通りとなります。

  • 回路本体
    • JoyBus のコマンド受信回路 (x 2)
    • JoyBus のコマンド送信回路 (x 2)
    • UART の文字列受信回路
      • UART の文字受信回路
    • UART の文字列送信回路
      • UART の文字送信回路
      • 送信待ち文字列用の FIFO

UART については、特筆すべきところはありません。 山ほど既存実装があるかと思いますので、説明は省略します。

JoyBus の送信回路についても、それほど難しいところはありません。 UART の送信回路をほとんど流用した上で、スタートビットを省略し、シンボルの最初の 1/4 は必ず '0' を、最後の 1/4 は必ず '1' を出力すれば OK です。 また、送信していないときに出力する信号値はハイインピーダンス('Z')にしておきます。

JoyBus の受信回路は、ストップビット(ないし、本体側からの送信終了)を検出しないといけないので、少し工夫が必要です。 シミュレーション波形ベースで説明します。

JoyBus 受信回路のシミュレーション波形

まず、受信待ち状態(IDLE)では、送信回路が動作しておらず、かつ '0' を 1/8 シンボル受信したタイミング(図中の赤線)で、読み取り状態(READ)に入ります。 そこから 1/2 シンボル後、つまりシンボル全体の 5/8 のタイミング(図中の水色線)で信号値を読み取り、そのビットの値とします。 ただし、各バイトの最初のビットは、ストップビットである可能性があります。 ストップビットだったかどうかを検出するため、各バイトの最初のビットを読み取ったらチェック状態(CHECK)に入り、その 1/2 シンボル後のタイミング(図中の黄色線)の信号値を読み取ります。 これが '0' なら、まだ本体側からの送信が続いているので、読み取り状態を継続します。 これが '1' なら送信は終了しているので、それまでに受信したビット列をコマンドとして出力し、受信待ち状態に戻ります。

ここまで部品を作り切ってしまえば、本体の回路でやるべきことは比較的シンプルです。 まず、どのパネルが踏まれているかを管理するためのレジスタを用意しておきます。 パネルが踏まれた/離されたことを UART 経由で検出したら、それに応じてレジスタの値を '1'/'0' に切り替えます。 JoyBus からリクエストを受信したら、その時点でのレジスタの値に応じて返答を用意して、送信します。

1点だけややこしいのは、パネルとキーの割当てが WASD か IJKL かにかかわらず、スタートボタンは Enter キーに割り当てられているということです。 そのため、IJKL のいずれかのキーが押されたときのデバイスの ID を覚えておいて、Enter キーが押されたときは ID をもとに 1P と 2P を振り分けるようにしています。 パネルを一切踏まない状態でスタートボタンを押すと誤検出してしまう可能性がありますが、致し方ないでしょう。

ひととおりシミュレーションで問題なさそうなことを確認したら、実機での動作確認です。 Gowin の FPGA 開発環境を立ち上げ、ピン番号や I/O 電圧、プルアップ/プルダウンなど、適切な入出力制約を設定してから、論理合成・配置配線を行い、出来上がった回路を FPGA に書き込みます。 その後、若干のデバッグを経て……。

動いた!

無事、GC コントローラとして認識されるようになりました。これでぷよ橋で「足ぷよ」ができますね。 (持っているのは購入特典としてついてきたミニコントローラです。わざわざパネルを持ち運びしなくて済んだので、助かりました。)

Fediverse 上での反応(全1件)