かつかれーのメモ帳

実験ノートか勉強記録

PS2タイトルを自作USBコントローラーで遊ぶ

IIDX自作コントローラー記事のソフト編(その1)です。
CS弐寺*1を初期型PS3で遊ぶために、PS2エミュレータ*2上で自作コントローラーを動作させました。

自作コントローラーをPS3に認識させるには

PS3本体のファームウエアバージョンなんかにも依存していて難しいのですが、比較的新しいファームウエアでは適当に作ったHIDゲームパッドも認識されます。4軸+13ボタンで作れば、軸が左右のスティック(X, Y方向の動き*2つで4軸分です)に、ボタンが各ボタン(方向キーを除く)に対応します。
ただPSボタンだけは特殊で、対応する13番ボタン押下の信号を送っていても無視されてしまいます。どうやらエンドポイント0(=コントロール転送)におけるHID_GET_REPORTリクエストに特定のバイト列で応答するか本体側でチェックしているようです。簡単な認証が為されているという感じですね。
この事実は結構有名で、例えば
ps3-teensy-hid - USB hid device for the PlayStation3 using the Teensy platform
におけるmagic_init_bytesのような実装例が結構見つかります。逆に、これさえ実装していればPSボタンも正しく動作するようになります。めでたしめでたし…?

PS2タイトルの謎仕様

さっそく基板を試作し、ホーム画面・PS1タイトル・PS3タイトルの操作に成功したのですが、PS2タイトルだけは全く操作できないんですよね…なんで?(全ギレ)
PS2のソフトをやる時だけ「何故かコントローラーの接続が解除されてしまい、PSボタンを押してコントローラーを再接続する必要がある」という仕様、実機を触ったことがある方ならご存じだと思います。後でわかったのですが、実はこれってPS2の時だけコントローラーの認証が非常に厳しくなっているためで、非純正コンはここですべて振り落とされるようになっています。
オフィシャルに動作が保証されているのは標準の純正コントローラー、つまりDUALSHOCK 3とSIXAXISだけ。こいつらは機能がてんこ盛りなせいで動作が複雑で、認証をすり抜けられるほど高い再現度は達成されていません*3。さすがにDUALSHOCKの解析は大変そうなので、もっと単純なコントローラーで使えるものはないか調べてみました。
www.jp.playstation.com
…ありました。HORI製のRAPV3という格ゲーコンです。
格ゲー界隈はやはりコントローラーへのこだわりが強いようで、PS2タイトルでの動作報告が活発に上がっていたので助かりました。SIEの公式サイトで「オフィシャルライセンス商品」とされているコントローラーは新しめのファームウエアでサポートしているっぽいです。

RAPV3の解析

中古でコントローラーを手に入れ、パケットキャプチャによる解析を試みました。今回はコントローラーの接続先がPCではなくPS3なので、Wiresharkなどのアプリを立ち上げておくことができません。こういう時はUSBスニファを使います。
USBスニファとは、通ったUSBのパケットをすべて記録しつつ中継する装置です。beagleboard xMというシングルボードコンピュータ(Linuxが動くボード。ラズパイみたいなやつ。)を使いました。とりあえず写真を見てもらうのが早いと思います。
f:id:DSKK:20200529111819p:plain
PS3-スニファ-コントローラーという接続になっています。LANケーブルはパケットキャプチャそのものには関係ありません。
このボードはUSBのホスト側とデバイス側両方の口を持っているので、本来USBケーブルで直結されるPS3とコントローラーの間に挿入することができます。
f:id:DSKK:20200529112038j:plainf:id:DSKK:20200529112048j:plain
USB mini-Bがデバイスとして振舞える口。標準Aコネクタにはデバイス(今回の場合コントローラー)をぶら下げられます。
パケットは速やかに(かつ、変更を受けずに)スニファをパスするので、PS3やコントローラーは直結時と変わらない通信ができます。また、beagleboard xM自体の制御は他PCからのSSH接続で行えます(LANケーブルはこのためにつないでいます)。
github.com
今回焼くファームウエアとしては、これをそのままありがたく使わせてもらいます。作者はGIMXの管理人をしているMatlo氏*4カーネルレベルでUSBパケットの中継を実装してくれているらしく、漏れや遅延が起こりにくそうです。./sniffコマンドでパケットをすべてdumpしてくれるのすごい。
さて、どんな認証が行われているのかとワクワクでdumpファイルを見たのですが…

特別なパケットは何もやり取りされていない

f:id:DSKK:20200529115408p:plain
PS2タイトル起動後にコントローラーを接続したときの挙動。デバイスディスクリプタコンフィギュレーションディスクリプタの要求のあと、セットコンフィギュレーションが行われデバイスが使用可能になっている。あとはひたすらインタラプト転送のpollとresponseが続く。
はい。いたって普通に通信しているようにしか見えませんでした。え、なにこれは…

うーん…では逆に、PS2で使えないコントローラーはどうなっているんでしょう?
今度はELECOM製のJC-U4013Sという、普通のゲームパッド(ホーム画面やPS3タイトルの操作は可能)をPS2タイトル起動後に接続したときの挙動を見てみました。

f:id:DSKK:20200529120412p:plain
セットコンフィギュレーションが来ていない!
バイスディスクリプタコンフィギュレーションディスクリプタを見た時点で通信をやめています。オフィシャルライセンス商品についてはこれらのディスクリプタホワイトリスト形式で持っていて、合致しなければガン無視を決め込む仕様っぽいです*5
これはなるほどという感じ。オリジナル要素のあるコントローラーが動かないわけだ。

自作コントローラーの仕様確定

要するに、

なら、PS2タイトルでもホーム画面でもフルで機能するってことですね。ディスクリプタ類は以下の通りです。

/* Device Descriptor */
 18,                // bLength
 1,                 // bDescriptorType
 0x00, 0x02,        // bcdUSB
 0,                 // bDeviceClass
 0,                 // bDeviceSubClass
 0,                 // bDeviceProtocol
 64,                // bMaxPacketSize0
 0x0d,              // idVendor[0]
 0x0f,              // idVendor[1]
 0x22,              // idProduct[0]
 0x00,              // idProduct[1]
 0x00, 0x10,        // bcdDevice (=version number)
 1,                 // iManufacturer
 2,                 // iProduct
 0,                 // iSerialNumber
 1                  // bNumConfigurations

/* Configuration Descriptor */
 9,                              // bLength;
 2,                              // bDescriptorType;
 0x29,
 0x00,                           // wTotalLength
 1,                              // bNumInterfaces
 1,                              // bConfigurationValue
 0,                              // iConfiguration
 0x80,                           // bmAttributes
 50,                             // bMaxPower
 // interface descriptor
 9,                              // bLength
 4,                              // bDescriptorType
 0,                              // bInterfaceNumber
 0,                              // bAlternateSetting
 2,                              // bNumEndpoints
 0x03,                           // bInterfaceClass (0x03 = HID)
 0x00,                           // bInterfaceSubClass (0x00 = No Boot)
 0x00,                           // bInterfaceProtocol (0x00 = No Protocol)
 0,                              // iInterface
 // HID interface descriptor
 9,                              // bLength
 0x21,                           // bDescriptorType
 0x11, 0x01,                     // bcdHID
 0,                              // bCountryCode
 1,                              // bNumDescriptors
 0x22,                           // bDescriptorType
 0x89,
 0x00,                           // wDescriptorLength
 // endpoint descriptor
 7,                              // bLength
 5,                              // bDescriptorType
 2,                              // bEndpointAddress (ep2,out)
 0x03,                           // bmAttributes (0x03=intr)
 64, 0,                          // wMaxPacketSize
 10,                              // bInterval
 // endpoint descriptor
 7,                              // bLength
 5,                              // bDescriptorType
 0x81,                           // bEndpointAddress (ep1,in)
 0x03,                           // bmAttributes (0x03=intr)
 64, 0,                          // wMaxPacketSize
 10                              // bInterval

/* Report Descriptor */
 0x05, 0x01,        // Usage Page (Generic Desktop Ctrls)
 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 (0x01)
 0x29, 0x0D,        //   Usage Maximum (0x0D)
 0x81, 0x02,        //   Input (Data,Var,Abs)
 0x95, 0x03,        //   Report Count (3)
 0x81, 0x01,        //   Input (Const,Array,Abs)
 0x05, 0x01,        //   Usage Page (Generic Desktop Ctrls)
 0x25, 0x07,        //   Logical Maximum (7)
 0x46, 0x3B, 0x01,  //   Physical Maximum (315)
 0x75, 0x04,        //   Report Size (4)
 0x95, 0x01,        //   Report Count (1)
 0x65, 0x14,        //   Unit (System: English Rotation, Length: Centimeter)
 0x09, 0x39,        //   Usage (Hat switch)
 0x81, 0x42,        //   Input (Data,Var,Abs)
 0x65, 0x00,        //   Unit (None)
 0x95, 0x01,        //   Report Count (1)
 0x81, 0x01,        //   Input (Const,Array,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)
 0x06, 0x00, 0xFF,  //   Usage Page (Vendor Defined 0xFF00)
 0x09, 0x20,        //   Usage (0x20)
 0x09, 0x21,        //   Usage (0x21)
 0x09, 0x22,        //   Usage (0x22)
 0x09, 0x23,        //   Usage (0x23)
 0x09, 0x24,        //   Usage (0x24)
 0x09, 0x25,        //   Usage (0x25)
 0x09, 0x26,        //   Usage (0x26)
 0x09, 0x27,        //   Usage (0x27)
 0x09, 0x28,        //   Usage (0x28)
 0x09, 0x29,        //   Usage (0x29)
 0x09, 0x2A,        //   Usage (0x2A)
 0x09, 0x2B,        //   Usage (0x2B)
 0x95, 0x0C,        //   Report Count (12)
 0x81, 0x02,        //   Input (Data,Var,Abs)
 0x0A, 0x21, 0x26,  //   Usage (0x2621)
 0x95, 0x08,        //   Report Count (8)
 0xB1, 0x02,        //   Feature (Data,Var,Abs)
 0x0A, 0x21, 0x26,  //   Usage (0x2621)
 0x91, 0x02,        //   Output (Data,Var,Abs)
 0x26, 0xFF, 0x03,  //   Logical Maximum (1023)
 0x46, 0xFF, 0x03,  //   Physical Maximum (1023)
 0x09, 0x2C,        //   Usage (0x2C)
 0x09, 0x2D,        //   Usage (0x2D)
 0x09, 0x2E,        //   Usage (0x2E)
 0x09, 0x2F,        //   Usage (0x2F)
 0x75, 0x10,        //   Report Size (16)
 0x95, 0x04,        //   Report Count (4)
 0x81, 0x02,        //   Input (Data,Var,Abs)
 0xC0               // End Collection

HIDレポートは27バイトありますが、初めの3バイトだけが変化します。ボタン等との対応は以下の通りです。後ろの24バイトはおそらくPS3の要求する仕様を満たすためにダミーを送っているのでしょう*6

1バイト目:
  R2, L2, R1, L1, △, 〇, ×, □

2バイト目:
   -,  -,  -, PS, R3, L3, START, SELECT

3バイト目:
  ハットスイッチ(POV)。対応は以下の通り。
  0x00 = Up
  0x01 = Up+Right
  0x02 = Right
  0x03 = Down+Right
  0x04 = Down
  0x05 = Down+Left
  0x06 = Left
  0x07 = Up+Left
  0x0f = None

1バイト目、2バイト目は左のものほど上の位です。例えば△と□が押されていると1バイト目は0x09になります。
後ろの24バイトは以下の通り固定です。
0x80, 0x80, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02

また、弐寺の鍵盤とプレステのボタンの対応は

1: □
2: L1
3: ×
4: R1
5: 〇
6: L2
7: ←

のようになっています。あとはこれを実装すればOKです。

まとめ

内容が盛りだくさんになってしまいましたが、とにかくマネしたいコントローラーを見つけてその動作を丁寧に解析・実装するということに尽きます。次回はINFINITASに対応したファームウエアの仕様について同様に解説し、次々回でそれら2つのファームウエアを共存させる実装方法について述べていくので、具体的なソースコードの解説はその時に。
下のリンクからシェアやツイートなどしてもらえると大変執筆モチベが上がりありがたいです。良かったらぜひ。何か気になる点があったらコメントか
しありす (@cialis438) | Twitter
へのリプをお気軽にどうぞ。感想やコントローラー自作に関する雑談・質問なども歓迎です。

*1:beatmania IIDXPS2移植版。いわゆる家庭用。これとか。

*2:PS3PS2後方互換機能で、最初に発売されたモデル(CECHAxxとCECHBxx。分厚くてつやつやしたやつ)にだけ搭載されている。

*3:世界最大の自作コントローラーコミュニティ、GIMXでも未解決でした。 2015年の投稿 で少しだけ話題に上がりましたが、作者が実機を持っておらず進展がないまま放置。ちなみに2020年の投稿は私によるものです。本記事のような知見をシェアして貢献できると良いなあ。

*4:コントローラー解析と実装のガチプロでGIMXと関連プロジェクトを素晴らしくメンテしているので尊敬している。いろいろなコメントに対する返答も丁寧なナイスガイ。

*5:余談ですが、HIDレポートディスクリプタはRAPV3と全く一緒でした。サードパーティーが別々にコントローラー開発したとして、1バイトも違わず偶然一致するものではないと思うのだけどいいのか…?

*6:レポートディスクリプタによれば軸が1バイト×4、vendor_definedが1バイト×12+2バイト×4。vendor_definedの前者は感圧センサ12ボタン分、後者は加速度センサ4軸分らしい。