verylowfreqのアイコン画像

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

verylowfreq 2021年09月01日に作成  (2021年09月01日に更新)

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

ログインしてコメントを投稿する