verylowfreqのアイコン画像
verylowfreq 2021年09月01日作成 (2021年12月15日更新)
製作品 製作品 閲覧数 3292
verylowfreq 2021年09月01日作成 (2021年12月15日更新) 製作品 製作品 閲覧数 3292

「PS1のDDRマット用 USBキーボード変換器」の製作 (XIAO, Python)

「PS1のDDRマット用 USBキーボード変換器」の製作 (XIAO, Python)

概要

PlayStation向けのDanceDanceRevolutionのフットコントローラ(初代)を、USBキーボードに変換する変換器を自作しました。
Windows向けDDRではまだ設定を詰めているところですが、Stepmania(DDRライクなパソコンゲーム)ではそれなりにプレイ可能なようなので、ここにログを残しておきます。

背景事情としては、2021年9月1日現在、Windows向けのDanceDanceRevolutionのオープンアルファテストが実施されています。キーボードや普通のゲームパッドでも操作できますが、やっぱり足でプレイしたいので、なんとか準備したいと思い、トライした次第です。
※オープンアルファテストと同時に受付している公式のフットコントローラ(17000円)が買えないだけです。普通にプレイしたいなら、20年物のコントローラと格闘するのではなく、新品を買ったほうが良いかと。

なぜ自作するのか?

PS DDRコントローラ

初代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信号線にプルアップ抵抗が必要でしょう。(今回利用したマルチタップ基板にはあらかじめ実装されている。)

コード

方向入力の同時押しがあるので、複数のキーコードが送出できるように組みます。

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" の値で判定タイミングが安定したので、おそらく遅延は実用的な範囲でわずかで、遅延時間幅は一定であるものと思われます。ゲームもプレイ可能でした(まだ低難易度しかやっていないけど)。

1
verylowfreqのアイコン画像
"verylowfreq" あるいは 「三峰スズ」(VTuber 2023年2月より) です。趣味で電子工作や3Dプリンターを楽しんでいます!
ログインしてコメントを投稿する