「PS1のDDRマット用 USBキーボード変換器」の製作 (XIAO, Python)
概要
PlayStation向けのDanceDanceRevolutionのフットコントローラ(初代)を、USBキーボードに変換する変換器を自作しました。
Windows向けDDRではまだ設定を詰めているところですが、Stepmania(DDRライクなパソコンゲーム)ではそれなりにプレイ可能なようなので、ここにログを残しておきます。
背景事情としては、2021年9月1日現在、Windows向けのDanceDanceRevolutionのオープンアルファテストが実施されています。キーボードや普通のゲームパッドでも操作できますが、やっぱり足でプレイしたいので、なんとか準備したいと思い、トライした次第です。
※オープンアルファテストと同時に受付している公式のフットコントローラ(17000円)が買えないだけです。普通にプレイしたいなら、20年物のコントローラと格闘するのではなく、新品を買ったほうが良いかと。
なぜ自作するのか?
初代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" の値で判定タイミングが安定したので、おそらく遅延は実用的な範囲でわずかで、遅延時間幅は一定であるものと思われます。ゲームもプレイ可能でした(まだ低難易度しかやっていないけど)。
投稿者の人気記事
-
verylowfreq
さんが
2021/09/01
に
編集
をしました。
(メッセージ: 初版)
-
verylowfreq
さんが
2021/09/01
に
編集
をしました。
-
verylowfreq
さんが
2021/09/01
に
編集
をしました。
-
verylowfreq
さんが
2021/09/01
に
編集
をしました。
-
verylowfreq
さんが
2021/12/15
に
編集
をしました。
(メッセージ: 記事の種類を設定した。)
ログインしてコメントを投稿する