verylowfreq が 2021年09月01日22時32分55秒 に編集
コメント無し
メイン画像の変更
本文の変更
![製作した変換器の外観](https://camo.elchika.com/7e9dd45e7ca9cd077e21b79cebcc4863e794020b/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f32376633303665652d656131352d343334662d626333342d6162343139336638333839392f30343665306665342d626366652d346335332d383234312d613665386461353537376434/)
## 概要 PlayStation向けのDanceDanceRevolutionのフットコントローラ(初代)を、USBキーボードに変換する変換器を自作しました。 Windows向けDDRではまだ設定を詰めているところですが、Stepmania(DDRライクなパソコンゲーム)ではそれなりにプレイ可能なようなので、ここにログを残しておきます。 背景事情としては、2021年9月1日現在、Windows向けのDanceDanceRevolutionのオープンアルファテストが実施されています。キーボードや普通のゲームパッドでも操作できますが、やっぱり足でプレイしたいので、なんとか準備したいと思い、トライした次第です。 ※オープンアルファテストと同時に受付している公式のフットコントローラ(17000円)が買えないだけです。普通にプレイしたいなら、20年物のコントローラと格闘するのではなく、新品を買ったほうが良いかと。 ## なぜ自作するのか? ![PS DDRコントローラ](https://camo.elchika.com/04dc2452cdd4aaff400d4e386f36ef684fb2ffd4/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f32376633303665652d656131352d343334662d626333342d6162343139336638333839392f35383565643966622d656230652d343264642d623430652d613539383230383262386564/) 初代PlayStation向けに製造されたDDRコントローラ(初代)は、すでに登場から20年くらい経っていますが、探せば見つかるくらいには流通しています。なにより安い。パソコン向けのフットコントローラもありますが、まぁ普通に動きそうなもの(作りが安っぽいものもあるようですが)を買ってもつまらないですし。 PSコントローラの変換器はまだ普通に流通していますが、DDRマットに関しては、一筋縄ではいかない事情があります。 DDRの操作は基本的に上下左右の4方向入力です。またゲーム中には **2つまでの同時押し** があり、上と右、上と左、下と右、下と左に並んで、**上と下** 、**右と左**もあります。 よくある変換器は、PSコントローラをUSBゲームパッドに変換するわけですが、USBゲームパッドで上下、左右の同時押しは正常に処理できません。一方向のみの入力になったり、方向入力なしになったりします。 ※なお特定の変換器では、上下や左右の同時押しを必要とするコントローラのためにボタンのリマップ機能が備わっているらしいです。現在入手可能なのかは不明。 入手可能な市販品ではたぶんなんともならないので、作りましょう。 ## (失敗実装)パソコン側で頑張る PS DDRコン + 変換器をパソコンにつないで上下や左右の同時押しすると、Windowsのゲームコントローラーの入力としては同時押しをとれません。しかしUSBの通信内容を覗いてみると、同時に2方向を押下された方向入力はエラーとしてパソコンに通知されています。 このエラー状態を取得できれば、「上下方向の方向入力のエラー = 上下同時押し」のように見なして処理できるので、まずはこれを利用しようとしました。 構成としては次の通りです。 ``` PS DDRコン ↓ 市販の変換器 ↓ パソコン - Python usbhidライブラリ(HIDレポート取得用)、pyserialライブラリ(シリアル通信用) ↓ (シリアル通信) ↓ Seeduino XIAO ↓ (USB) ↓ パソコン ↓ ゲーム ``` XIAOを経由していますが、これは、ソフトウェア的にゲームへキーボード入力を流し込むのが(私には)難しいので、確実な方法をとったというだけです。 USBゲームパッドにしてしまうと、方向入力の取り扱いに困ってしまうので、キーボードにしています。 動作はしましたが、全体的な安定性に欠ける(たまにUSB HIDレポート取得が詰まる)、遅延量が一定でない気配がある、構成がスマートでない、という理由で、断念しました。 ## (成功実装)PSコントローラと直接通信する 変な寄り道をしてしまいましたが、どう考えても、普通にPSコントローラと通信して押下状況を見たほうが良いので、そうします。 PSコントローラのプロトコルははるか昔に解析されていて、どのドキュメントはもちろん今も有効です。 参考資料: "ps_jpn.txt" at https://applause.elfmimi.jp/dualshock.shtml ※テキストファイルをWebブラウザで開くとおそらく文字化けするので、ダウンロードしてテキストエディタで開いたほうが良いかも。ShiftJISです。 今回はPS DDRコン(初代)の決め打ちで作るので、汎用性はないです。もしアナログスティックを備えたコントローラを接続しても、デジタルなボタン入力のみしか受け付けません。 PSコントローラの通信仕様で、今回必要な範囲だけを簡単にまとめると、 - SPI通信。Mode 3, LSB First - クロックは250KHzくらい(下げても良い。手元では250Hzでも動いた) - [ 0x01, 0x42, 0x00, 0x00, 0x00 ] を送ると、応答は [ (不定), 'A', 'Z', (押下状況1), (押下状況2) ] が得られる。 250KHzなので、ソフトウェアSPIでも間に合います。コントローラの応答の長さや内容は接続されているコントローラによって変わるのですが、コントローラのタイプはPS DDRコンに決め打ちしているので、固定長で解釈します。 PSコントローラのコネクタの物理的な配線を上記資料より引用します。(アスキーアート部のみ修正。) ``` PS PADコネクタ ============================= ∥ * * *| * * * |* * * ∥ (本体正面より見た図) \_______|_________|_______/ ピン 9 8 7 6 5 4 3 2 1 Pin 信号名 方向 論理 機能 -------------------------------------------------------------------- 1. DAT IN 正 PAD/メモリからのデータ 2. CMD OUT 正 PAD/メモリへのコマンド 3. +7V -- -- +7.6V CD-ROMドライブ電源 4. GND -- -- 電源のグランド 5. +3V -- -- +3.6Vシステム電源 6. SEL OUT 負 PAD/メモリのセレクト 7. CLK OUT -- コマンド/データの取り込みクロック 8. -- -- -- 未定 9. ACK IN 負 PAD/メモリからの応答信号 -------------------------------------------------------------------- ``` ピンの注釈の独自解釈は次の通りです。 | # | 信号名 | メモ | | --|--|--|--| | 9 | ACK | 使わない。配線不要 | | 8 | --- | 不明。配線不要 | | 7 | CLK | SPI CLOCK | | 6 | SEL | SPI Slave select | | 5 | +3V | 3.6Vらしいけど、3.3Vでも動いた | | 4 | GND | GND | | 3 | +7V | 配線せず | | 2 | CMD | SPI MOSI | | 1 | DAT | SPI MISO | コネクタの部品をどこで確保するかが課題ですが、今回は安く売っていたマルチタップ(1ポートでコントローラを4つ接続できる公式アクセサリ)を分解して流用しました。 ### 回路 今回、マイコンはSeeduino XIAOを利用しました。使用するピン数が少なく、コントローラとの通信の負荷も小さいので、XIAOでも十分に間に合います。 以下、コントローラのコネクタとXIAOの配線です。 | PSコントローラ | Seeduino XIAO | | -- | -- | | +3V | 3.3V | | SEL | D7 | | CLK | D8 | | DAT | D9 | | CMD | D10 | 今回は動いたのでとくに処置していませんが、直結するならSPI信号線にプルアップ抵抗が必要でしょう。(今回利用したマルチタップ基板にはあらかじめ実装されている。) ### コード 方向入力の同時押しがあるので、複数のキーコードが送出できるように組みます。 ```Python import time import board from digitalio import DigitalInOut, Direction import microcontroller import usb_hid TIME_CLOCK_Q = 1000000 // 250000 // 4 TIME_CLOCK_H = 1000000 // 250000 // 2 TIME_CLOCK = 1000000 // 250000 class USBHIDKeyboard: KEY_ARROW_UP = 0x52 KEY_ARROW_DOWN = 0x51 KEY_ARROW_RIGHT = 0x4F KEY_ARROW_LEFT = 0x50 KEY_1 = 0x1E KEY_2 = 0x1F KEY_3 = 0x20 KEY_4 = 0x21 KEY_ESC = 0x29 KEY_ENTER = 0x58 KEY_O = 0x12 def __init__(self, usbhidkeyboard) -> None: self.hid = usbhidkeyboard self.report = bytearray(8) self.clear_report() def clear_report(self) -> None: for i in range(len(self.report)): self.report[i] = 0 def push_keycode(self, keycode:int) -> bool: for i in range(2, len(self.report)): if self.report[i] == 0x00: self.report[i] = keycode & 0xFF return True return False def send(self) -> None: self.hid.send_report(self.report) class PSController: def __init__(self, pin_ss:DigitalInOut, pin_clk:DigitalInOut, pin_mosi:DigitalInOut, pin_miso:DigitalInOut) -> None: self.pin_ss = pin_ss self.pin_clk = pin_clk self.pin_mosi = pin_mosi self.pin_miso = pin_miso self.pin_ss.value = True self.pin_clk.value = True def deselect(self) -> None: self.pin_ss.value = True def select(self) -> None: self.pin_ss.value = False def transfer_byte(self, b:int) -> int: ret = 0 for i in range(8): # LSB First databit = (b & (1 << i)) != 0 self.pin_clk.value = False self.pin_mosi.value = databit microcontroller.delay_us(TIME_CLOCK_H) self.pin_clk.value = True microcontroller.delay_us(TIME_CLOCK_Q) if self.pin_miso.value: ret |= (1 << i) microcontroller.delay_us(TIME_CLOCK_Q) return ret def transfer_block(self, buf:bytes) -> bytes: ret = bytearray() for b in buf: ret.append(self.transfer_byte(b)) return ret def main(): print("Initialize PSController object...") pin_ss = DigitalInOut(board.D7) pin_ss.direction = Direction.OUTPUT pin_clk = DigitalInOut(board.D8) pin_clk.direction = Direction.OUTPUT pin_miso = DigitalInOut(board.D9) pin_miso.direction = Direction.INPUT pin_mosi = DigitalInOut(board.D10) pin_mosi.direction = Direction.OUTPUT pscon = PSController(pin_ss, pin_clk, pin_mosi, pin_miso) cmdbuf = bytes([0x01, 0x42, 0x00, 0x00, 0x00]) usbkbd = USBHIDKeyboard(usb_hid.devices[2]) print("Ready.") cnt = 0 prev = bytes(1 + 2 + 2) print("Running...") while True: pscon.select() ret = pscon.transfer_block(cmdbuf) pscon.deselect() if prev != ret: print("{:03d}: {}".format(cnt, ret)) prev = ret usbkbd.clear_report() btn = bytes([~ret[3] & 0xFF, ~ret[4] & 0xFF]) if btn[0] & 0x80: # DPad Left usbkbd.push_keycode(USBHIDKeyboard.KEY_ARROW_LEFT) if btn[0] & 0x40: # DPad Down usbkbd.push_keycode(USBHIDKeyboard.KEY_ARROW_DOWN) if btn[0] & 0x20: # DPad Right usbkbd.push_keycode(USBHIDKeyboard.KEY_ARROW_RIGHT) if btn[0] & 0x10: # DPad Up usbkbd.push_keycode(USBHIDKeyboard.KEY_ARROW_UP) if btn[0] & 0x08: # START usbkbd.push_keycode(USBHIDKeyboard.KEY_3) if btn[0] & 0x01: # SELECT usbkbd.push_keycode(USBHIDKeyboard.KEY_4) if btn[1] & 0x40: # cross usbkbd.push_keycode(USBHIDKeyboard.KEY_2) if btn[1] & 0x20: # circle usbkbd.push_keycode(USBHIDKeyboard.KEY_1) usbkbd.send() cnt += 1 time.sleep(0.01) if __name__ == '__main__': main() ``` ## 使用感 (私がマット式のコントローラにまったく慣れていないのと、そもそもタイミング感覚がアーケードでもボロボロなので、記述は正確ではないかも。) オープンアルファ段階のDDRはタイミング調整機能が数値調整しかできないので、頑張って設定値を追い込もうとしましたが、いまだにぴったりくる設定に出会っていません。判定のずれが安定しないような感覚も受けますが、私の踏み方が安定していないだけのような気もします。 StepmaniaというDDRライクなパソコンゲームには調整ガイドがあるので試したところ、"-0.038" の値で判定タイミングが安定したので、おそらく遅延は実用的な範囲でわずかで、遅延時間幅は一定であるものと思われます。ゲームもプレイ可能でした(まだ低難易度しかやっていないけど)。