はじめに
前回記事「Z80+C言語で16ビット機級の本格的なゲームが創れるゲーム機(VGS-Zero)を作ってみた」で想定以上に多くの反響を頂き、有り難いことに多くの方に VGS-Zero を触っていただくことができました。
上記記事を公開前は 100+ 程度あったRaspberryPi Zero 2W のスイッチサイエンスの在庫が、記事を公開後に一気に無くなってしまいました。多分偶然だと思いますが、万が一私の記事の影響だったら何かスミマセン...(2024.02.14本日時点では在庫が復活しているようです)
今回の記事はそんなに伸びないと思うので大丈夫な筈。
触っていただいた方から、「接続しても反応しない USB ジョイパッドがあった」というご報告があったので早速同じ機種の USB ジョイパッドを購入してテストしてみたのですが、報告の通り全く反応がありませんでした。
なお、ジョイパッドにはジョイカード、ジョイスティック、ゲームパッド、ゲームコントローラー、コントローラーなどの様々な呼び方がありますが、本書では表記ゆれ回避のためすべて「ジョイパッド」と表記します。
その USB ジョイパッドは XInput と DirectInput の両モードに対応していて、本体背面のスイッチで切り替えるタイプのものですが、それぞれのモードについて VGS-Zero のリポジトリで提供している USB ジョイパッドの反応テストツール でテストしてみたところ、何となく動かない原因が分かりました。
(DirectInput モードの場合)
usbdev0-1: Device ven46d-c216 found (FS)
usbdev0-1: Product: <<<ややネガティブ情報かもしれないのでメーカー名は伏せておきます>>>
usbdev0-1: Interface int3-0-0 found
usbdev0-1: Using device/interface int3-0-0
dwhci: Transaction failed (status 0x402)
kernel: USB joypad connected
デバイスの接続自体は認識できています。
しかし、ボタンやカーソルを押しても何も反応しません。
VGS-Zero(Circle)で使用可能なジョイパッドの場合、ボタンやカーソルを押すと下図のように Push BUTTON_n detected
や Changed AXIS_n = nnn
といったレコード(config.sys への設定値)を出力するのですが、問題のジョイパッドではどのボタンを押しても何も出力されません。
そこで、問題のジョイパッドのマニュアルを熟読してみたところ、DirectInput モードの場合は専用のデバイスドライバをインストールする必要がある との記述がありました。つまり、問題のジョイパッドの USB 通信プロトコルは標準 HID に準拠しておらず、独自プロトコルで実装されているものと考えられます。
USB 通信プロトコルは大きなオーバーヘッドを要するものなので、各メーカーで工夫した独自プロトコルを使うことで少しでも遅延を少なくする努力をしているのかもしれません。しかし、デバイスドライバが無ければカーネル側が入力を認識できないため、動作させるには専用のデバイスドライバが必要になります。
当然ながら VGS-Zero 用のデバイスドライバ提供されていないので、このゲームパッドを VGS-Zero (RaspberryPi Zero 2W) で動かすことはできません。
そこで、標準 HID 準拠のジョイパッド以外は使用できない旨を VGS-Zero のドキュメントに記載しておきました。
(XInput モードの場合)
usbdev0-1: Device ven46d-c21d found (FS)
usbdev0-1: Product: <<<ややネガティブ情報かもしれないのでメーカー名は伏せておきます>>>
usbdev0-1: Function is not supported
usbdev0-1: Device has no supported function
usbdev0-1: Device ven46d-c21d removed
dwhci: Cannot initialize root port
DirectInput モードとは異なり、デバイスが Circle で非サポートの機能を使っているため認識できないようです。
XInput については、Windows と XBOX であればほぼ問題なく(デバイスドライバ無しで)動作できます。しかし、XInput のプロトコル仕様を調べてみたのですが、ザックリと調べてみた限りでは見つからなかったため、完全にオープンなプロトコル仕様ではないようです。そのため、Windows と XBOX 以外での動作が不完全になりがちなのかもしれません。
そのような事情から、VGS-Zero では XInput のジョイパッドの使用は 基本的に非推奨 となります。VGS-Zero では、先述の通り 標準 HID 準拠のジョイパッドのみ推奨 というのが基本スタンスになります。
ただし、標準 HID 準拠のジョイパッドは EOL の製品が多いという難点があります。個人的に一番オススメできるのは Elecom の JC-3312S シリーズですが、もちろん EOL で後継製品も存在しないようです。そういった製品は Aliexpress で模造品が出回ることを期待するしかありません。
ジョイパッドはゲームにとってかなり重要な構成要素だと私は考えています。
そのため、ジョイパッドの入力問題はクリティカルです。
私が実際に購入してテストしたジョイパッドの動作可否情報を機種毎にリストアップして公開(ドキュメント対処)をしておきましたが、もっとシステム的&確実なアプローチでの解決策も欲しいところです。
そこで USB に頼らない確実に動作可能なジョイパッド への対応を検討することにしました。
ジョイパッド概論
まず、「そもそもジョイパッドとは何なのか?」を私なりに考えてみたところ、「ボタンとインタフェースである」という解にいきついたので、ボタンとインタフェースについての概論を記します。(ガワも重要かもしれませんが)
ボタンの電気的な仕組み
ジョイパッドで使われるボタンの電気的な仕組みはものすごく単純です。
以下、セイミツ工業 PS-14-PCDN ネジ式パールキャップクリア押しボタン24φ を例に説明します。
2本のファストン端子があり、ボタンを押すと内部的にそれらが結線され、ボタンを離すと断線されるメカニカル構造になっています。
つまり、以下のような形にすることでボタンの押下状態をプログラムで検出できます。
- 片方のファストン端子を GPIO に接続
- もう片方のファストン端子を GND に接続
- プログラムで GPIO を PULLUP の入力設定にする
- プログラムで GPIO を読み取る:
- ボタンを離している時:
HIGH (1)
- GPIO が導電状態
- ボタンを押している時:
LOW (0)
- GPIO と GND で接続されることで絶縁状態になる
- ボタンを離している時:
以下のツイートは RaspberryPi Zero 2W (VGS-Zero) ではなく RaspberryPi Pico (東方VGS実機版) ですが、上記の判定によりボタンの ON/OFF を画面上部に表示しています。
十字キーやジョイスティックも仕組み的には全く同じで、上下左右に対応する 4 つのボタンの押下状態をプログラムで検出することができます。
MSX でジョイパッドの入力処理を実装する場合、AY-3-8910 のレジスタ 0x0E の各ビットで上下左右とトリガの押下状態を判定しますが、各ビットは入力されている状態ならリセット (0)、未入力状態ならセット (1) になります。「普通逆では?」と思った方も多いと思われますが、これは「ボタンの電気的な仕組みによるもの」と考えれば腑に落ちます。(ですがやはり不自然なので、初めてプログラムを組んだ時に逆判定で実装してバグりがち)
ジョイパッドのインタフェース
ボタンの電気的な仕組みは、約半世紀前のレトロゲーム機でも最新のプレイステーション5でもほぼ同じだと思われます。アナログスティックは比較的新しい(30年ぐらい前の)ものですが。
しかし、インタフェースは多様にあります。
家庭用ゲーム機で最初に普及したインタフェースは D-SUB9 ピン(DB9)のアタリ規格だと思われますが、DB9 は同じ形状でも各ピンの用途が機種により微妙に異なります。
具体的には、上下左右、トリガ1(Aボタン)、GNDについては概ね各社ジョイパッドで共通のピンアサインになっていますが、ATARI VCS に存在しなかったトリガ2(Bボタン)については機種によりピンの割当がかなり異なり、ある機種ではトリガ2なのに別機種ではVCC(電源)というものが存在します。
これはとても危険です。
例えば、同じ DB9 ジョイパッドだからと別のゲーム機に繋いだ時、Bボタンを押したら機器が故障して動かなくなる可能性があるようです。(参考: この Wikipedia の「歴史」の箇所を参照)
DB9 より後の世代は色々ゴチャゴチャしていたので省略します
混乱期(ゴチャゴチャ世代)の後、ジョイパッドのインタフェースは概ね USB に統一されました。
USB の通信方式(シリアルバス通信)は、1本のピンを特定の間隔で高速に ON/OFF してデータの送受信を行います。その間隔は USB の規格により異なりますが、USB2.0 であれば最大 480Mbps (1秒間に約5億回のON/OFFで 60MB のデータを伝送)です。
1秒間に5億回もの read/write を要するということは、USB の通信処理には物凄く多大なオーバーヘッドが掛かる ことは想像に難くないと思われます。
オーバーヘッドの問題は、「力こそパワー」(マシンを強くすること)で解決できますが、限界まで早く伝送しても1ビットの送信に USB2.0 なら最速でも0.5ns程度の時間が掛かるので、1バイトの通信であれば4ns程度の伝送遅延が発生することは避けられません。さらに、最大速度で伝送すると消費電力やマシン負荷が大きくなるため、実際にはもっと遅い速度(1.5Mbps 程度?)で通信していると思われ、おそらく数マイクロ秒~数ミリ秒程度の入力遅延が発生するものと思われます。
そのため、USB は高度なレスポンス性能が求められるジョイパッドに最適なインタフェースとは言い難いかもしれません。もちろん、体感的(実用的)には気にならない遅延(マイクロ秒オーダー以下)に抑えられているものと思われますが、「遅延ナシ!」みたいなことを堂々と謳っているジョイパッドのインタフェースが USB や I2C などのシリアル通信だと、若干モヤっとした気持ちになってしまいます...それでも Bluetooth よりは幾分マシではありますが。(これだからマニアは面倒くさい...)
DB9 と USB 以外には、JAMMA 規格あたりが有名かもしれません。
完全に私見ですが JAMMA 規格が一番好きです。(シンプル&ゲーム専用なので)
JAMMA 規格は主に業務用ゲーム機で使われていたもので、以下のサイトに掲載されているメーカー資料によると、端子番号18〜25が1Pのジョイパッドのピンアサインになっているようです。
DB9 や JAMMA はボタンの電気的な信号が SoC にそのまま伝わるため、伝送遅延が発生しません。厳密にはボタンからGPIOまでの電子の移動速度や導線のノイズ等による遅延が発生すると思われるので「遅延ゼロ」とまでは言いませんが、極めて遅延ゼロに近い と思われます。
VGS-Zero 独自ジョイパッドの設計
RaspberryPi Zero 2W は GPIO が露出しているので、インタフェースを介さずダイレクトに GPIO とボタンを接続することが可能です。
これにより、確実に動作することは勿論、DB9 や JAMMA と仕組み的に等価な(極めて遅延ゼロに近い)完璧なジョイパッドを実現できます。
RaspberryPi Zero 2W のピン配置
RaspberryPi Zero 2W には 40 本のピン(を半田づけするスルーホール)があり、VGS-Zero に必要なボタンは 8 個(上下左右, A, B, Select, Start)しかないので、1ボタン = 1GPIO という形で余裕だろう...と思ってピン配置の仕様をチェックしたところ、使用できる GPIO ポート(汎用 GPIO ポート)は意外と少ないことが分かりました。
上図の緑色で塗りつぶしている箇所が今回の用途(ジョイパッド接続)で使用できる汎用 GPIO で、G4〜G6 の 3 本 + G22〜G27 の 6 本の 9 本 しかありません。
8本あれば足りるのでギリギリセーフ...
VGS-Zero ジョイパッドのピンアサイン
下表のようなピンアサインにしました。
Button | GPIO |
---|---|
Up | 22 |
Down | 5 |
Left | 26 |
Right | 6 |
A | 24 |
B | 25 |
Start | 4 |
Select | 23 |
上記の表を先程の図面とマージすると下図のような形になります。
このピンアサインに対応した VGS-Zero のカーネル(バージョン 1.3.0)は既にリリース済みです。
カーネルの対応実装は極めてシンプルで こちらの commit で確認できます。
GPIO の割り当てが気に入らない場合、CKernel のコンストラクタで初期化している gpioXXX の第一引数を変更すれば OK です。
CKernel::CKernel(void) : screen(480, 384),
timer(&interrupt),
logger(LogPanic, nullptr),
usb(&interrupt, &timer, TRUE),
vchiq(CMemorySystem::Get(), &interrupt),
sound(&vchiq, (TVCHIQSoundDestination)options.GetSoundOption()),
emmc(&interrupt, &timer, &led),
mcm(CMemorySystem::Get()),
gpioUp(22, GPIOModeInputPullUp), // ココ
gpioDown(5, GPIOModeInputPullUp), // ココ
gpioLeft(26, GPIOModeInputPullUp), // ココ
gpioRight(6, GPIOModeInputPullUp), // ココ
gpioA(24, GPIOModeInputPullUp), // ココ
gpioB(25, GPIOModeInputPullUp), // ココ
gpioStart(4, GPIOModeInputPullUp), // ココ
gpioSelect(23, GPIOModeInputPullUp), // ココ
gamePad(nullptr)
上記のツイートでも言及してますが、折角 RaspberryPi を使っているので自作ジョイパッドや自作ケースなどの同人ハードがどんどん出てくると良いなと妄想しています。VGS-Zero はゲームを創る人だけでなく、同人ハードを創る人にとっても遊べる土台になれたら良いなと。
とかいいつつ、汎用 GPIO を 8 つも潰すのは中々の暴挙かもしれませんが...
I2C、UART、SPI などの用途が決まっている GPIO を潰すのは得策ではないので使用を避けていて、汎用 GPIO はあまり使いどころが無いので潰してしまってもヘーキヘーキ...などと思っていたりするのですが、如何せん私は電子工作についてほぼ初心者なので、色々なことに想像が及んでいない可能性は否定できません😅
MSX-BBS でそちら方面に詳しいちくわ帝国さんから、「エンコーダICを使えば8入力を3ビットにできるよ」とアドバイスを頂いたので、将来的に汎用 GPIO が必要になったらエンコーダ IC を導入するかもしれません。
テスト
どこの家庭にもある余り物のアケコンスティック(三和製)とボタン(セイミツ製)と、Amazonで購入した中国製のアケコンケース(1,000円)を使って組み立てた自作アケコンがあるので、それをそのまま流用しました。
天板の内側は下図のような形です。
写真の下の方にユニバーサル基板がありますが、これは各ボタンの GND を 1 本に集約する用途で使っています。
給電が必要な電子機器は一切無いので、RaspberryPi Zero 2W の GPIO 8 本 + GND 1 本接続にかかる消費電力(16mA x 9 = 144mA)が増加します。USB ジョイパッドを接続すると 500mA 程度の消費電力が掛かるので、GPIO ジョイパッド接続にすれば消費電力を 300mA 程度減らすことができエコロジーな筈です。(測定機器を持っていないので測定はしていませんが)
もちろん、正常に動作しました
このアケコンで Battle Marine をプレイしてみたところ、USB ジョイパッドと比べて格段に遅延が無いことを体感できました。
Windows 版の Battle Marine(開発中)を USB ジョイパッドで動かしていますが、それと同等か、もしかすると Windows 版以上に快適かもしれません。
これについては「自分で作った料理が美味い」のと同じ一種のプラシーボ効果みたいなものかもしれませんが。Windows PC(Surface Laptop GO2を使用)ならマシンパワーが強いので、USBでもほぼ遅延が発生しない筈です。
しかし、RaspberryPi Zero 2W では高オーバーヘッドな USB 通信を処理するにはやはり力不足だったようで、明確な入力レスポンスの向上を体感できます。
かなり難度が高いと思いますが、是非とも電子工作にチャレンジして GPIO ジョイパッドの「本物のゼロ遅延」を体感して頂きたいところです。
はんだ無しで RaspberryPi Zero 2W に取り付け可能な同人ハード作って家電のケンちゃんあたりで販売すれば結構売れるんじゃないかな?(同人ハードは専門外なのでよく分かりませんが)