From 5ba9e5328888bd4eeadb4f113740a353fd9c3117 Mon Sep 17 00:00:00 2001 From: sorgelig Date: Sun, 1 Mar 2020 05:49:13 +0800 Subject: [PATCH] Add PSX JogCon USB adapter. --- JogConUSB/Gamepad.cpp | 174 +++++++++++++++++++++++ JogConUSB/Gamepad.h | 88 ++++++++++++ JogConUSB/JogConUSB.ino | 308 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 570 insertions(+) create mode 100644 JogConUSB/Gamepad.cpp create mode 100644 JogConUSB/Gamepad.h create mode 100644 JogConUSB/JogConUSB.ino diff --git a/JogConUSB/Gamepad.cpp b/JogConUSB/Gamepad.cpp new file mode 100644 index 0000000..790b230 --- /dev/null +++ b/JogConUSB/Gamepad.cpp @@ -0,0 +1,174 @@ +/* Gamepad.cpp + * + * Based on the advanced HID library for Arduino: + * https://github.com/NicoHood/HID + * Copyright (c) 2014-2015 NicoHood + * + * Copyright (c) 2020 Mikael Norrgård + * + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "Gamepad.h" + +static const uint8_t _hidReportDescriptor[] PROGMEM = { + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x04, // USAGE (Joystick) (Maybe change to gamepad? I don't think so but...) + 0xa1, 0x01, // COLLECTION (Application) + 0xa1, 0x00, // COLLECTION (Physical) + + 0x05, 0x09, // USAGE_PAGE (Button) + 0x19, 0x01, // USAGE_MINIMUM (Button 1) + 0x29, 0x0C, // USAGE_MAXIMUM (Button 12) + 0x15, 0x00, // LOGICAL_MINIMUM (0) + 0x25, 0x01, // LOGICAL_MAXIMUM (1) + 0x95, 0x10, // REPORT_COUNT (16) + 0x75, 0x01, // REPORT_SIZE (1) + 0x81, 0x02, // INPUT (Data,Var,Abs) + + 0x05, 0x01, // USAGE_PAGE (Generic Desktop) + 0x09, 0x01, // USAGE (pointer) + 0xa1, 0x00, // COLLECTION (Physical) + 0x09, 0x30, // USAGE (X) + 0x09, 0x31, // USAGE (Y) + 0x15, 0x80, // LOGICAL_MINIMUM (-128) + 0x25, 0x7F, // LOGICAL_MAXIMUM (127) + 0x95, 0x02, // REPORT_COUNT (2) + 0x75, 0x08, // REPORT_SIZE (8) + 0x81, 0x02, // INPUT (Data,Var,Abs) + 0xc0, // END_COLLECTION + 0xa1, 0x00, // COLLECTION (Physical) + 0x09, 0x37, // USAGE (Dial) + 0x15, 0x00, // LOGICAL_MINIMUM (0) + 0x26, 0xFF, 0x00, // LOGICAL_MAXIMUM (255) + 0x95, 0x01, // REPORT_COUNT (1) + 0x75, 0x08, // REPORT_SIZE (8) + 0x81, 0x02, // INPUT (Data,Var,Abs) + 0xc0, // END_COLLECTION + + 0x15, 0x00, // Logical Minimum (0) + 0x25, 0x07, // Logical Maximum (7) + 0x35, 0x00, // Physical Minimum (0) + 0x46, 0x3B, 0x01, // Physical Maximum (315) + 0x75, 0x08, // Report Size (8) + 0x95, 0x01, // Report Count (1) + 0x65, 0x14, // Unit (20) + 0x05, 0x01, // Usage Page (Generic Desktop) + 0x09, 0x39, // Usage (Hat switch) + 0x81, 0x42, // Input (variable,absolute,null_state) + + 0xc0, // END_COLLECTION + 0xc0, // END_COLLECTION +}; + +Gamepad_::Gamepad_(void) : PluggableUSBModule(1, 1, epType), protocol(HID_REPORT_PROTOCOL), idle(1) +{ + epType[0] = EP_TYPE_INTERRUPT_IN; + PluggableUSB().plug(this); +} + +int Gamepad_::getInterface(uint8_t* interfaceCount) +{ + *interfaceCount += 1; // uses 1 + HIDDescriptor hidInterface = { + D_INTERFACE(pluggedInterface, 1, USB_DEVICE_CLASS_HUMAN_INTERFACE, HID_SUBCLASS_NONE, HID_PROTOCOL_NONE), + D_HIDREPORT(sizeof(_hidReportDescriptor)), + D_ENDPOINT(USB_ENDPOINT_IN(pluggedEndpoint), USB_ENDPOINT_TYPE_INTERRUPT, USB_EP_SIZE, 0x01) + }; + return USB_SendControl(0, &hidInterface, sizeof(hidInterface)); +} + +int Gamepad_::getDescriptor(USBSetup& setup) +{ + // Check if this is a HID Class Descriptor request + if (setup.bmRequestType != REQUEST_DEVICETOHOST_STANDARD_INTERFACE) { return 0; } + if (setup.wValueH != HID_REPORT_DESCRIPTOR_TYPE) { return 0; } + + // In a HID Class Descriptor wIndex cointains the interface number + if (setup.wIndex != pluggedInterface) { return 0; } + + // Reset the protocol on reenumeration. Normally the host should not assume the state of the protocol + // due to the USB specs, but Windows and Linux just assumes its in report mode. + protocol = HID_REPORT_PROTOCOL; + + return USB_SendControl(TRANSFER_PGM, _hidReportDescriptor, sizeof(_hidReportDescriptor)); +} + +bool Gamepad_::setup(USBSetup& setup) +{ + if (pluggedInterface != setup.wIndex) { + return false; + } + + uint8_t request = setup.bRequest; + uint8_t requestType = setup.bmRequestType; + + if (requestType == REQUEST_DEVICETOHOST_CLASS_INTERFACE) + { + if (request == HID_GET_REPORT) { + // TODO: HID_GetReport(); + return true; + } + if (request == HID_GET_PROTOCOL) { + // TODO: Send8(protocol); + return true; + } + } + + if (requestType == REQUEST_HOSTTODEVICE_CLASS_INTERFACE) + { + if (request == HID_SET_PROTOCOL) { + protocol = setup.wValueL; + return true; + } + if (request == HID_SET_IDLE) { + idle = setup.wValueL; + return true; + } + if (request == HID_SET_REPORT) + { + } + } + + return false; +} + +void Gamepad_::reset() +{ + _GamepadReport.X = 0; + _GamepadReport.Y = 0; + _GamepadReport.dial = 0; + _GamepadReport.hat = 15; + _GamepadReport.buttons = 0; + this->send(); +} + +void Gamepad_::send() +{ + USB_Send(pluggedEndpoint | TRANSFER_RELEASE, &_GamepadReport, sizeof(GamepadReport)); +} + +uint8_t Gamepad_::getShortName(char *name) +{ + if(!next) + { + strcpy(name, gp_serial); + return strlen(name); + } + return 0; +} diff --git a/JogConUSB/Gamepad.h b/JogConUSB/Gamepad.h new file mode 100644 index 0000000..021044b --- /dev/null +++ b/JogConUSB/Gamepad.h @@ -0,0 +1,88 @@ +/* Gamepad.h + * + * Based on the advanced HID library for Arduino: + * https://github.com/NicoHood/HID + * Copyright (c) 2014-2015 NicoHood + * + * Copyright (c) 2020 Mikael Norrgård + * + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#pragma once + +#include +#include "HID.h" + +extern const char* gp_serial; + +// The numbers after colon are bit fields, meaning how many bits the field uses. +// Remove those if there are problems +typedef struct { + union + { + struct { + bool b0: 1; + bool b1: 1; + bool b2: 1; + bool b3: 1; + bool b4: 1; + bool b5: 1; + bool b6: 1; + bool b7: 1; + bool b8: 1; + bool b9: 1; + bool b10: 1; + bool b11: 1; + bool b12: 1; + bool b13: 1; + bool b14: 1; + bool b15: 1; + }; + uint16_t buttons; + }; + + int8_t X; + int8_t Y; + int8_t dial; + uint8_t hat; + +} GamepadReport; + + +class Gamepad_ : public PluggableUSBModule +{ + private: + uint8_t reportId; + + protected: + int getInterface(uint8_t* interfaceCount); + int getDescriptor(USBSetup& setup); + bool setup(USBSetup& setup); + uint8_t getShortName(char *name); + + uint8_t epType[1]; + uint8_t protocol; + uint8_t idle; + + public: + GamepadReport _GamepadReport; + Gamepad_(void); + void reset(void); + void send(); +}; diff --git a/JogConUSB/JogConUSB.ino b/JogConUSB/JogConUSB.ino new file mode 100644 index 0000000..d3f4b42 --- /dev/null +++ b/JogConUSB/JogConUSB.ino @@ -0,0 +1,308 @@ +/* + * PSX JogCon based Arcade USB controller + * (C) Alexey Melnikov + * + * Based on project by Mikael Norrgård + * + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +// LOOKING AT THE PLUG +// |-----------------------------| +// PIN 1-> | o o o | o o o | o o o | +// \_________|_________|_________/ +// +// +// Arduino Plug PCB connector +// ----------------------------------- +// RXI(D0) 1 2 DATA +// GND 4 3 GND +// 3.3V 5 4 3.3V (main) +// D4 6 5 ATT +// TXO(D1) 2 6 COMMAND +// D3 7 7 CLOCK +// 5V 3 8 5V (motor) +// + +//////////////////////////////////////////////////////// + +#define CMD 1 +#define DAT 0 +#define CLK 3 +#define ATT 4 + +#define DELAY 4 +#define SP_MAX 160 + +//////////////////////////////////////////////////////// + +// ID for special support in MiSTer +// ATT: 20 chars max (including NULL at the end) according to Arduino source code. +// Additionally serial number is used to differentiate arduino projects to have different button maps! +const char *gp_serial = "MiSTer-A1 JogCon"; + +#include +#include "Gamepad.h" + +Gamepad_ Gamepad; + +byte ff = 0; +byte mode = 0; +byte force = 15; +int16_t sp_step = 4; +byte data[8]; + +const byte cmd_read[] = {0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +const byte cmd_cfg_enter[] = {0x43, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00}; +const byte cmd_cfg_exit[] = {0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +const byte cmd_set_mode_a[] = {0x44, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00}; +const byte cmd_set_mode_d[] = {0x44, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00}; +const byte cmd_get_mode[] = {0x45, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00}; +const byte cmd_unlock_ff[] = {0x4D, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFF}; + +byte psx_io(byte out) +{ + byte in = 0; + + for (byte i = 0; i < 8; i++) + { + digitalWrite(CLK, LOW); + digitalWrite(CMD, (out & (1 << i)) ? HIGH : LOW); + + delayMicroseconds(DELAY); + in = in | (digitalRead(DAT) << i); + digitalWrite(CLK, HIGH); + + delayMicroseconds(DELAY); + } + + delayMicroseconds(DELAY*2); + return in; +} + +void send_cmd(const byte *cmd) +{ + digitalWrite(ATT, LOW); + + psx_io(0x01); + for(int i=0; i<7; i++) data[i] = psx_io((i==2 && cmd[0] == 0x42 && ff) ? (force | 0x30) : cmd[i]); + data[7] = psx_io(0xFF); + + data[2] = ~data[2]; + data[3] = ~data[3]; + + digitalWrite(ATT, HIGH); + delayMicroseconds(DELAY*10); +} + +void init_jogcon() +{ + send_cmd(cmd_cfg_enter); + send_cmd(cmd_set_mode_a); + send_cmd(cmd_unlock_ff); + send_cmd(cmd_read); + send_cmd(cmd_read); +} + +#define UP 0x1 +#define RIGHT 0x2 +#define DOWN 0x4 +#define LEFT 0x8 + +uint8_t dpad2hat(uint8_t dpad) +{ + switch(dpad & (UP|DOWN|LEFT|RIGHT)) + { + case UP: return 0; + case UP|RIGHT: return 1; + case RIGHT: return 2; + case DOWN|RIGHT: return 3; + case DOWN: return 4; + case DOWN|LEFT: return 5; + case LEFT: return 6; + case UP|LEFT: return 7; + } + return 15; +} + +uint8_t sp_div; +int16_t sp_max; +int16_t sp_half; + +void setup() +{ + Gamepad.reset(); + + pinMode(CLK, OUTPUT); digitalWrite(CLK, HIGH); + pinMode(CMD, OUTPUT); digitalWrite(CMD, HIGH); + pinMode(ATT, OUTPUT); digitalWrite(ATT, HIGH); + pinMode(DAT, INPUT_PULLUP); + + mode = EEPROM.read(0) & 0x3; + if(mode == 3) mode = 0; + + force = EEPROM.read(1) & 0xF; + if(!force) force = 15; + + sp_step = EEPROM.read(2); + if(sp_step > 8) sp_step = 8; + if(sp_step < 1) sp_step = 4; + + sp_div = EEPROM.read(3) ? 1 : 2; + sp_max = SP_MAX/sp_div; + sp_half = sp_max/2; + + init_jogcon(); +} + +void loop() +{ + static uint16_t counter = 0, newcnt = 0, cleancnt = 0; + static uint16_t newbtn = 0, oldbtn = 0; + static int32_t pdlpos = sp_half; + static uint16_t prevcnt = 0; + + send_cmd(cmd_read); + if(data[0] == 0x41 || !data[1]) + { + if(data[0] == 0xF3 && !data[1]) + { + // Mode switch by pressing "mode" button while holding: + // L2 - paddle mode (with FF stoppers) + // R2 - steering mode (FF always enabled) + // L2+R2 - spinner mode (no FF) + if(data[3]&3) + { + mode = data[3] & 3; + if(mode == 3) mode = 0; + EEPROM.write(0, mode); + } + + // Force Feedback adjust + // by pressing "mode" button while holding /\,O,X,[] + if(data[3] & 0xF0) + { + if(data[3] & 0x10) force = 1; + if(data[3] & 0x20) force = 3; + if(data[3] & 0x40) force = 7; + if(data[3] & 0x80) force = 15; + EEPROM.write(1, force); + } + + // Spinner pulses per step adjust + // by pressing "mode" button while holding up,right,down,left + if(data[2] & 0xF0) + { + if(data[2] & 0x10) sp_step = 1; + if(data[2] & 0x20) sp_step = 2; + if(data[2] & 0x40) sp_step = 4; + if(data[2] & 0x80) sp_step = 8; + EEPROM.write(2, sp_step); + } + } + + // Paddle range switch by pressing "mode" button while holding: + // L1 - 270 degree + // R1 - 135 degree + if(data[3]&0xC) + { + sp_div = (data[3] & 4) ? 2 : 1; + sp_max = SP_MAX/sp_div; + sp_half = sp_max/2; + EEPROM.write(3, !(sp_div>>1)); + } + + // some time for visual confirmation + delay(200); + + // reset zero position + init_jogcon(); + + prevcnt = 0; + cleancnt = 0; + counter = (data[5] << 8) | data[4]; + pdlpos = sp_half; + } + + newcnt = (data[5] << 8) | data[4]; + newbtn = (data[3] << 8) | data[2]; + newbtn = (newbtn & ~3) | ((newbtn&1)<<2); + + if(data[0] == 0xF3) + { + if(data[6]&3) + { + cleancnt += newcnt - counter; + if(!mode) + { + ff = 0; + pdlpos += (int16_t)(newcnt - counter); + if(pdlpos<0) pdlpos = 0; + if(pdlpos>sp_max) pdlpos = sp_max; + } + } + + if(mode) + { + if(((int16_t)newcnt) < -sp_half) + { + pdlpos = 0; + if(mode == 1) ff = 1; + } + else if(((int16_t)newcnt) > sp_half) + { + pdlpos = sp_max; + if(mode == 1) ff = 1; + } + else + { + if(mode == 1) ff = 0; + pdlpos = (uint16_t)(newcnt + sp_half); + } + } + + if(mode == 2) ff = 1; + + if(!(Gamepad._GamepadReport.buttons&3)) + { + int16_t diff = cleancnt - prevcnt; + if(diff >= sp_step) + { + newbtn |= 2; + prevcnt += sp_step; + } + else if(diff <= -sp_step) + { + newbtn |= 1; + prevcnt -= sp_step; + } + } + + uint8_t dial = ((pdlpos*255)/sp_max); + if(oldbtn != newbtn || Gamepad._GamepadReport.dial != dial) + { + oldbtn = newbtn; + Gamepad._GamepadReport.buttons = (newbtn & 0xF) | ((newbtn>>4) & ~0xF); + Gamepad._GamepadReport.dial = dial; + Gamepad._GamepadReport.hat = dpad2hat(newbtn>>4); + Gamepad.send(); + } + } + + counter = newcnt; +}