沚消沙の物置

日々のメモ書き

ネジコンのUSB化

tl; dr

PCでネジコンが使いたい

ネジコンを手に入れたのでPCで使おうと手元のコンバーターを使ったが、Lトリガー(アナログ入力)を認識してくれない。 他のコンバーターを調べてみても、ネジコンに対応してないどころか終売になっているものだらけ。

Arduino互換機(Seeeduino XIAO)を利用してコンバーターを作成する。

開発環境

名称 バージョン
Seeed SAMD Boards 1.8.2
PsxNewLib 0.4.0
Adfruit TinyUSB Library 0.10.5

ネジコン読み取り

とりあえずコネクターが欲しいので、Amazonで適当なPS2/3→USBコンバーターを購入。(約400円)
(もしかしてネジコン対応しているかなと試してみたが、やっぱり駄目だった)

ノーブランドのコンバーター

はんだごてで取り外して、適当な基板に接続。使いやすいようにピンも付けた。

スパゲッティ配線図

Seeeduino XIAOと接続して入力を読み取る。ライブラリーはPsxNewLibを利用。(v0.3.0以上) サンプルコードを参考に読み取り処理を実装。

読み取りコード

//#include <PsxControllerHwSpi.h>
#include <PsxControllerBitBang.h>

const byte PIN_PS2_DAT = 10;
const byte PIN_PS2_CMD = 9;
const byte PIN_PS2_CLK = 8;
const byte PIN_PS2_ATT = 7;

PsxControllerBitBang<PIN_PS2_ATT, PIN_PS2_CMD, PIN_PS2_DAT, PIN_PS2_CLK> psx;
//PsxControllerHwSpi<PIN_PS2_ATT> psx;

bool haveController = false;

void printInput(PsxController *psx)
{
  String str;

  auto protocol = psx->getProtocol();
  switch(protocol)
  {
    //case PSPROTO_DIGITAL:
    //case PSPROTO_DUALSHOCK:
    //case PSPROTO_DUALSHOCK2:
    //case PSPROTO_FLIGHTSTICK:
    case PSPROTO_NEGCON:
      str = "NEGCON: ";
      str += (psx->buttonPressed(PSB_PAD_LEFT))  ? "←" : " ";
      str += (psx->buttonPressed(PSB_PAD_DOWN))  ? "↓" : " ";
      str += (psx->buttonPressed(PSB_PAD_UP))    ? "↑" : " ";
      str += (psx->buttonPressed(PSB_PAD_RIGHT)) ? "→" : " ";
      
      str += (psx->buttonPressed(PSB_TRIANGLE))  ? "△" : " ";
      str += (psx->buttonPressed(PSB_CIRCLE))    ? "○" : " ";
      
      str += (psx->buttonPressed(PSB_R1))        ? "R1" : "  ";
      str += (psx->buttonPressed(PSB_START))     ? "ST" : "  ";
      
      str += " L1:";
      str += psx->getAnalogButton(PSAB_L1);
      str += " ×:";
      str += psx->getAnalogButton(PSAB_CROSS);
      str += " □:";
      str += psx->getAnalogButton(PSAB_SQUARE);
      
      byte lx, ly;
      psx->getLeftAnalog(lx, ly);
      str += " LX:";
      str += lx;
      str += " LY:";
      str += ly;
      
      break;
    //case PSPROTO_JOGCON:
    
    default:
      break;
  }
  Serial.println(str);
}

void setup()
{
  Serial.begin(9600);
  Serial.println("setup");
}

void loop()
{
  if (!haveController)
  {
    if (psx.begin())
    {
      Serial.println("Controller found!");
      if (!psx.enterConfigMode ())
      {
        Serial.println("Cannot enter config mode");
      }
      else
      {
        // Try to enable analog sticks
        if (!psx.enableAnalogSticks ()) {
          Serial.println("Cannot enable analog sticks");
        }

        if (!psx.exitConfigMode ()) {
          Serial.println("Cannot exit config mode");
        }
      }
      
      haveController = true;
    }
  }
  else
  {
    if (!psx.read())
    {
      Serial.println("controller lost.");
      haveController = false;
    }
    else
    {
      printInput(&psx);
    }
  }
}

ネジコンの入力が読み取れた。(音ゲーではない)

USB変換

XIAOをUSBデバイスとして扱うためにはTinyUSBを利用するらしい。

設定から「USB Stack」を「TinyUSB」に変更。

しかし、公式のサンプルを試してもビルドが通らない。

cannot declare variable 'usb_hid' to be of abstract type 'Adfruit_USBD_HID'

調べると、どうもXIAOの場合はTinyUSBライブラリーが0.10.5までしか対応していないらしい。(2021年7月現在)
ただ、0.10.5を使用すると、ゲームパッド用のディスクリプターが色々と足りていないので、手動で追加してあげる必要がある。

こちらのページを参考に、最新版のコードからパーツを拝借。

GamePadの定義を追加

/* tinyusb.h */

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2019 Ha Thach (tinyusb.org)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * This file is part of the TinyUSB stack.
 */




// https://github.com/adafruit/Adafruit_TinyUSB_Arduino/blob/e02de0e0e9e1bd6d46a9ed032048d22deb032ea4/src/class/hid/hid.h

//--------------------------------------------------------------------+
// GAMEPAD
//--------------------------------------------------------------------+

/// HID Gamepad Protocol Report.
typedef struct TU_ATTR_PACKED
{
  int8_t  x;         ///< Delta x  movement of left analog-stick
  int8_t  y;         ///< Delta y  movement of left analog-stick
  int8_t  z;         ///< Delta z  movement of right analog-joystick
  int8_t  rz;        ///< Delta Rz movement of right analog-joystick
  int8_t  rx;        ///< Delta Rx movement of analog left trigger
  int8_t  ry;        ///< Delta Ry movement of analog right trigger
  uint8_t hat;       ///< Buttons mask for currently pressed buttons in the DPad/hat
  uint32_t buttons;  ///< Buttons mask for currently pressed buttons
}hid_gamepad_report_t;

/// Standard Gamepad Buttons Bitmap
typedef enum
{
  GAMEPAD_BUTTON_0  = TU_BIT(0),
  GAMEPAD_BUTTON_1  = TU_BIT(1),
  GAMEPAD_BUTTON_2  = TU_BIT(2),
  GAMEPAD_BUTTON_3  = TU_BIT(3),
  GAMEPAD_BUTTON_4  = TU_BIT(4),
  GAMEPAD_BUTTON_5  = TU_BIT(5),
  GAMEPAD_BUTTON_6  = TU_BIT(6),
  GAMEPAD_BUTTON_7  = TU_BIT(7),
  GAMEPAD_BUTTON_8  = TU_BIT(8),
  GAMEPAD_BUTTON_9  = TU_BIT(9),
  GAMEPAD_BUTTON_10 = TU_BIT(10),
  GAMEPAD_BUTTON_11 = TU_BIT(11),
  GAMEPAD_BUTTON_12 = TU_BIT(12),
  GAMEPAD_BUTTON_13 = TU_BIT(13),
  GAMEPAD_BUTTON_14 = TU_BIT(14),
  GAMEPAD_BUTTON_15 = TU_BIT(15),
  GAMEPAD_BUTTON_16 = TU_BIT(16),
  GAMEPAD_BUTTON_17 = TU_BIT(17),
  GAMEPAD_BUTTON_18 = TU_BIT(18),
  GAMEPAD_BUTTON_19 = TU_BIT(19),
  GAMEPAD_BUTTON_20 = TU_BIT(20),
  GAMEPAD_BUTTON_21 = TU_BIT(21),
  GAMEPAD_BUTTON_22 = TU_BIT(22),
  GAMEPAD_BUTTON_23 = TU_BIT(23),
  GAMEPAD_BUTTON_24 = TU_BIT(24),
  GAMEPAD_BUTTON_25 = TU_BIT(25),
  GAMEPAD_BUTTON_26 = TU_BIT(26),
  GAMEPAD_BUTTON_27 = TU_BIT(27),
  GAMEPAD_BUTTON_28 = TU_BIT(28),
  GAMEPAD_BUTTON_29 = TU_BIT(29),
  GAMEPAD_BUTTON_30 = TU_BIT(30),
  GAMEPAD_BUTTON_31 = TU_BIT(31),
}hid_gamepad_button_bm_t;

/// Standard Gamepad Buttons Naming from Linux input event codes
/// https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h
#define GAMEPAD_BUTTON_A       GAMEPAD_BUTTON_0
#define GAMEPAD_BUTTON_SOUTH   GAMEPAD_BUTTON_0

#define GAMEPAD_BUTTON_B       GAMEPAD_BUTTON_1
#define GAMEPAD_BUTTON_EAST    GAMEPAD_BUTTON_1

#define GAMEPAD_BUTTON_C       GAMEPAD_BUTTON_2

#define GAMEPAD_BUTTON_X       GAMEPAD_BUTTON_3
#define GAMEPAD_BUTTON_NORTH   GAMEPAD_BUTTON_3

#define GAMEPAD_BUTTON_Y       GAMEPAD_BUTTON_4
#define GAMEPAD_BUTTON_WEST    GAMEPAD_BUTTON_4

#define GAMEPAD_BUTTON_Z       GAMEPAD_BUTTON_5
#define GAMEPAD_BUTTON_TL      GAMEPAD_BUTTON_6
#define GAMEPAD_BUTTON_TR      GAMEPAD_BUTTON_7
#define GAMEPAD_BUTTON_TL2     GAMEPAD_BUTTON_8
#define GAMEPAD_BUTTON_TR2     GAMEPAD_BUTTON_9
#define GAMEPAD_BUTTON_SELECT  GAMEPAD_BUTTON_10
#define GAMEPAD_BUTTON_START   GAMEPAD_BUTTON_11
#define GAMEPAD_BUTTON_MODE    GAMEPAD_BUTTON_12
#define GAMEPAD_BUTTON_THUMBL  GAMEPAD_BUTTON_13
#define GAMEPAD_BUTTON_THUMBR  GAMEPAD_BUTTON_14

/// Standard Gamepad HAT/DPAD Buttons (from Linux input event codes)
typedef enum
{
  GAMEPAD_HAT_CENTERED   = 0,  ///< DPAD_CENTERED
  GAMEPAD_HAT_UP         = 1,  ///< DPAD_UP
  GAMEPAD_HAT_UP_RIGHT   = 2,  ///< DPAD_UP_RIGHT
  GAMEPAD_HAT_RIGHT      = 3,  ///< DPAD_RIGHT
  GAMEPAD_HAT_DOWN_RIGHT = 4,  ///< DPAD_DOWN_RIGHT
  GAMEPAD_HAT_DOWN       = 5,  ///< DPAD_DOWN
  GAMEPAD_HAT_DOWN_LEFT  = 6,  ///< DPAD_DOWN_LEFT
  GAMEPAD_HAT_LEFT       = 7,  ///< DPAD_LEFT
  GAMEPAD_HAT_UP_LEFT    = 8,  ///< DPAD_UP_LEFT
}hid_gamepad_hat_t;





// https://github.com/adafruit/Adafruit_TinyUSB_Arduino/blob/3b4d46b717f554c66fa95638abd4c28c0e6f22d2/src/class/hid/hid_device.h

// Gamepad Report Descriptor Template
// with 16 buttons, 2 joysticks and 1 hat/dpad with following layout
// | X | Y | Z | Rz | Rx | Ry (1 byte each) | hat/DPAD (1 byte) | Button Map (2 bytes) |
#define TUD_HID_REPORT_DESC_GAMEPAD(...) \
  HID_USAGE_PAGE ( HID_USAGE_PAGE_DESKTOP     )                 ,\
  HID_USAGE      ( HID_USAGE_DESKTOP_GAMEPAD  )                 ,\
  HID_COLLECTION ( HID_COLLECTION_APPLICATION )                 ,\
    /* Report ID if any */\
    __VA_ARGS__ \
    /* 8 bit X, Y, Z, Rz, Rx, Ry (min -127, max 127 ) */ \
    HID_USAGE_PAGE   ( HID_USAGE_PAGE_DESKTOP                 ) ,\
    HID_USAGE        ( HID_USAGE_DESKTOP_X                    ) ,\
    HID_USAGE        ( HID_USAGE_DESKTOP_Y                    ) ,\
    HID_USAGE        ( HID_USAGE_DESKTOP_Z                    ) ,\
    HID_USAGE        ( HID_USAGE_DESKTOP_RZ                   ) ,\
    HID_USAGE        ( HID_USAGE_DESKTOP_RX                   ) ,\
    HID_USAGE        ( HID_USAGE_DESKTOP_RY                   ) ,\
    HID_LOGICAL_MIN  ( 0x81                                   ) ,\
    HID_LOGICAL_MAX  ( 0x7f                                   ) ,\
    HID_REPORT_COUNT ( 6                                      ) ,\
    HID_REPORT_SIZE  ( 8                                      ) ,\
    HID_INPUT        ( HID_DATA | HID_VARIABLE | HID_ABSOLUTE ) ,\
    /* 8 bit DPad/Hat Button Map  */ \
    HID_USAGE_PAGE   ( HID_USAGE_PAGE_DESKTOP                 ) ,\
    HID_USAGE        ( HID_USAGE_DESKTOP_HAT_SWITCH           ) ,\
    HID_LOGICAL_MIN  ( 1                                      ) ,\
    HID_LOGICAL_MAX  ( 8                                      ) ,\
    HID_PHYSICAL_MIN ( 0                                      ) ,\
    HID_PHYSICAL_MAX_N ( 315, 2                               ) ,\
    HID_REPORT_COUNT ( 1                                      ) ,\
    HID_REPORT_SIZE  ( 8                                      ) ,\
    HID_INPUT        ( HID_DATA | HID_VARIABLE | HID_ABSOLUTE ) ,\
    /* 16 bit Button Map */ \
    HID_USAGE_PAGE   ( HID_USAGE_PAGE_BUTTON                  ) ,\
    HID_USAGE_MIN    ( 1                                      ) ,\
    HID_USAGE_MAX    ( 32                                     ) ,\
    HID_LOGICAL_MIN  ( 0                                      ) ,\
    HID_LOGICAL_MAX  ( 1                                      ) ,\
    HID_REPORT_COUNT ( 32                                     ) ,\
    HID_REPORT_SIZE  ( 1                                      ) ,\
    HID_INPUT        ( HID_DATA | HID_VARIABLE | HID_ABSOLUTE ) ,\
  HID_COLLECTION_END \

あとは実行コードを書いてXIAOに書き込む。

スケッチファイル

//#include <PsxControllerHwSpi.h>
#include <PsxControllerBitBang.h>
#include <Adafruit_TinyUSB.h> // v0.10.x
#include "tinyusb.h"

// 十字キー(HATスイッチ)をボタン化したい場合はコメントアウト
#define USE_GAMEPAD_HAT

const byte PIN_PS2_DAT = 10;
const byte PIN_PS2_CMD = 9;
const byte PIN_PS2_CLK = 8;
const byte PIN_PS2_ATT = 7;

PsxControllerBitBang<PIN_PS2_ATT, PIN_PS2_CMD, PIN_PS2_DAT, PIN_PS2_CLK> psx;
//PsxControllerHwSpi<PIN_PS2_ATT> psx;

uint8_t const desc_hid_report[] =
{
  TUD_HID_REPORT_DESC_GAMEPAD()
};

Adafruit_USBD_HID usb_hid;
hid_gamepad_report_t gp;

bool haveController = false;

void sendGamePadStatus(PsxController *psx)
{
  auto protocol = psx->getProtocol();
  switch(protocol)
  {
    //case PSPROTO_DIGITAL:
    //case PSPROTO_DUALSHOCK:
    //case PSPROTO_DUALSHOCK2:
    //case PSPROTO_FLIGHTSTICK:
    case PSPROTO_NEGCON:
      // set digital button
      if (psx->buttonPressed(PSB_CIRCLE))
      {
        gp.buttons += GAMEPAD_BUTTON_0;
      }
      if (psx->buttonPressed(PSB_TRIANGLE))
      {
        gp.buttons += GAMEPAD_BUTTON_1;
      }
      if (psx->buttonPressed(PSB_R1))
      {
        gp.buttons += GAMEPAD_BUTTON_2;
      }
      if (psx->buttonPressed(PSB_START))
      {
        gp.buttons += GAMEPAD_BUTTON_3;
      }

      // set hat
#ifdef USE_GAMEPAD_HAT
      if (psx->buttonPressed(PSB_PAD_UP))
      {
        gp.hat = GAMEPAD_HAT_UP;
      }
      if (psx->buttonPressed(PSB_PAD_RIGHT))
      {
        gp.hat = GAMEPAD_HAT_RIGHT;
      }
      if (psx->buttonPressed(PSB_PAD_DOWN))
      {
        gp.hat = GAMEPAD_HAT_DOWN;
      }
      if (psx->buttonPressed(PSB_PAD_LEFT))
      {
        gp.hat = GAMEPAD_HAT_LEFT;
      }
#else
      if (psx->buttonPressed(PSB_PAD_UP))
      {
        gp.buttons += GAMEPAD_BUTTON_4;
      }
      if (psx->buttonPressed(PSB_PAD_RIGHT))
      {
        gp.buttons += GAMEPAD_BUTTON_5;
      }
      if (psx->buttonPressed(PSB_PAD_DOWN))
      {
        gp.buttons += GAMEPAD_BUTTON_6;
      }
      if (psx->buttonPressed(PSB_PAD_LEFT))
      {
        gp.buttons += GAMEPAD_BUTTON_7;
      }
#endif

      // set analog button
      byte lx, ly;
      psx->getLeftAnalog(lx, ly);
      gp.x = lx - 128;
      gp.y = ly - 128;
      gp.z = psx->getAnalogButton(PSAB_L1) - 128;
      gp.rx = psx->getAnalogButton(PSAB_CROSS) - 128;
      gp.ry = psx->getAnalogButton(PSAB_SQUARE) - 128;
  }
  usb_hid.sendReport(0, &gp, sizeof(gp));
}

void resetGamePad()
{
  gp.x = 0;
  gp.y = 0;
  gp.z = 0;
  gp.rx = 0;
  gp.ry = 0;
  gp.rz = 0;
  gp.hat = 0;
  gp.buttons = 0;
}

void setup()
{
  Serial.begin(9600);
  Serial.println("setup");

  usb_hid.setPollInterval(2);
  usb_hid.setReportDescriptor(desc_hid_report, sizeof(desc_hid_report));
  usb_hid.begin();

  while( !USBDevice.mounted() )
  {
    delay(1);
  }
}

void loop()
{
  if (!usb_hid.ready())
  {
    return;
  }

  resetGamePad();

  if (!haveController)
  {
    if (psx.begin())
    {
      Serial.println("Controller found!");
      if (!psx.enterConfigMode ())
      {
        Serial.println("Cannot enter config mode");
      }
      else
      {
        // Try to enable analog sticks
        if (!psx.enableAnalogSticks ()) {
          Serial.println("Cannot enable analog sticks");
        }

        if (!psx.exitConfigMode ()) {
          Serial.println("Cannot exit config mode");
        }
      }
      
      haveController = true;
    }
  }
  else
  {
    if (!psx.read())
    {
      Serial.println("controller lost.");
      haveController = false;
    }
    else
    {
      sendGamePadStatus(&psx);
    }
  }
}

動作確認

Win + RからJOY.CPLを起動して入力を確認。 (十字キーはボタンとして認識させている)

とりあえず完成

github.com

あとは基板作って、この中(↓)に収めたいが、それは気が向いたときにでも・・・

続き

sikisya.hateblo.jp