Arduino USB Mouse to FMT converter - supports real USB protocol (not PS/2)

Started by Zuofu, December 28, 2021, 04:30:49 AM

Previous topic - Next topic

Zuofu

Hi All,

I know there are lots of PS2 adapters floating around, but while I was waiting for an eBay purchase to come in for my new to me FM Towns, I decided to make my own which uses the real USB protocol (not the PS/2 protocol adapted from USB). This way even modern gaming mice (many of which don't support the PS/2 protocol anymore) will work. It should also work on an MSX, but I don't have a real MSX computer to test on (I do have a 1-chip MSX from back in the day, maybe I'll check it out).

Pretty self explanatory, I tested mine with the Sparkfun version of the USB Host shield: https://www.sparkfun.com/products/9947, but the ones on eBay/Amazon should work as well. I used a STEMTera board, but any old Arduino Uno style thing should be fine. Works best with gaming mice, USB mice which have a
very slow reporting rate (e.g. 15ms) will be laggy.

I'm not in any MSX communities, but feel free to link this post from other communities which may benefit.



// USB Mouse to FM Towns/MSX '9-pin' mouse converter
//
// Requires USB Host Shield (Sparkfun or clone ok) and USB Host Shield Library 2.0
// (install from Arduino library manager).
//
// Note that this is only tested with a 5V Arduino, so the host shield will need to have level
// shifters (MAX3421E is only 3.3V).
//
// Pinout: (DE9 male looking at computer)
// _________________________
//  \(A0)(A1)(A2)(A3)(+5V)/
//   \ (A4)(A5)(D2)(GND) /
//     -----------------
// Ax = Arduino "Analog" ports (used as digital)
// D2 = Digital port 2
// +5V and GND used to power board and mouse
//
// Note that this is designed to be powered by the computer, FM Towns (at least my OG Model)
// has no current limiting, so any USB mouse can be used, but some other machines might limit current.
// External power for Arduino is recommended for mice with lighting, etc, in which case wire a
// Schottky diode between +5V pin on DE9 to Arduino to prevent current backflow.
//
// some variants of Sparkfun USB shield require wire from D7 pin to reset on shield.
//
// Supports true USB protocol (even through hub), so all types of USB mice may be used.
// A gaming mouse with fast reporting rate is recommended otherwise mouse motion may be laggy.
//
// Provided free of charge and without warranty by Zuofu, you may use this code
// for either non-commercial or commercial purposes (e.g. sell on eBay), but please
// give credit.

#include <hidboot.h>
#include <usbhub.h>

// Satisfy IDE, which only needs to see the include statment in the ino.
#ifdef dobogusinclude
#include <spi4teensy3.h>
#endif
#include <SPI.h>

volatile int             cur_dx, cur_dy;
int                      last_dx, last_dy;
volatile byte            state, sleep;
volatile int             last_strobe;
volatile unsigned long   last_strobe_time, last_task_time, last_completed_time;

class MouseRptParser : public MouseReportParser
{
  protected:
    void OnMouseMove(MOUSEINFO *mi);
    void OnLeftButtonUp(MOUSEINFO *mi);
    void OnLeftButtonDown(MOUSEINFO *mi);
    void OnRightButtonUp(MOUSEINFO *mi);
    void OnRightButtonDown(MOUSEINFO *mi);
    void OnMiddleButtonUp(MOUSEINFO *mi);
    void OnMiddleButtonDown(MOUSEINFO *mi);
};
void MouseRptParser::OnMouseMove(MOUSEINFO *mi)
{
  cur_dx += (-(mi->dX)) >> 1;
  cur_dy += (-(mi->dY)) >> 1;
};
void MouseRptParser::OnLeftButtonUp (MOUSEINFO *mi)
{
  pinMode(A4, INPUT); //emulate switch OPEN
};
void MouseRptParser::OnLeftButtonDown (MOUSEINFO *mi)
{
  digitalWrite(A4, LOW); //emulate switch CLOSED
  pinMode(A4, OUTPUT);
};
void MouseRptParser::OnRightButtonUp (MOUSEINFO *mi)
{
  pinMode(A5, INPUT); //emulate switch OPEN
};
void MouseRptParser::OnRightButtonDown (MOUSEINFO *mi)
{
  digitalWrite(A5, LOW); //emulate switch CLOSED
  pinMode(A5, OUTPUT);
};
void MouseRptParser::OnMiddleButtonUp (MOUSEINFO *mi)
{
  Serial.println("M Butt Up"); //unused
};
void MouseRptParser::OnMiddleButtonDown (MOUSEINFO *mi)
{
  Serial.println("M Butt Dn"); //unused
};

USB        Usb;
USBHub     Hub(&Usb);

HIDBoot<USB_HID_PROTOCOL_KEYBOARD | USB_HID_PROTOCOL_MOUSE> HidComposite(&Usb);
HIDBoot<USB_HID_PROTOCOL_KEYBOARD>                          HidKeyboard(&Usb);
HIDBoot<USB_HID_PROTOCOL_MOUSE>                             HidMouse(&Usb);

MouseRptParser MousePrs;

void setup()
{
  Serial.begin( 115200 );
#if !defined(__MIPSEL__)
  while (!Serial); // Wait for serial port to connect - used on Leonardo, Teensy and other boards with built-in USB CDC serial connection
#endif
  Serial.println("Start");

  if (Usb.Init() == -1)
    Serial.println("OSC did not start.");

  delay( 200 );

  HidComposite.SetReportParser(1, &MousePrs);
  HidMouse.SetReportParser(0, &MousePrs);

  //PORTD
  pinMode(2, INPUT);

  //ANALOG (PORTC)
  pinMode(A0, OUTPUT);
  pinMode(A1, OUTPUT);
  pinMode(A2, OUTPUT);
  pinMode(A3, OUTPUT);

  //buttons default to high-Z
  pinMode(A4, INPUT);
  pinMode(A5, INPUT);

 
  cur_dx = 0;
  cur_dy = 0;
  last_strobe = digitalRead(2);
  state = 0;
  sleep = 0;

  Serial.println("Initializing USB");
  for (int init = 0; init < 50; init ++)
  {
    Usb.Task();
    Serial.println(".");
  }
  Serial.println("Running");
}

void loop()
{
  volatile int new_strobe = digitalRead(2);
  if (last_strobe != new_strobe)     //detected STROBE/COM signal, start FSM
  {       
    last_strobe = new_strobe;
    last_strobe_time = millis();
    PORTC &= 0xF0; //clear lower data port
    switch (state)
    {
       case 0: //T1
        PORTC |= ((unsigned)(0xF0 & cur_dx) >> 4); //output upper nibble of dx
        state ++;
        break;
       case 1: //T2
        PORTC |= ((unsigned)(0x0F & cur_dx)); //output lower nibble of dx
        cur_dx = 0;
        state ++;
        last_completed_time = millis(); //sent out all 4 phases, so update timeout
        if (sleep) //strobe started back up, so leave sleep
        { 
          pinMode(A0, OUTPUT);
          pinMode(A1, OUTPUT);
          pinMode(A2, OUTPUT);
          pinMode(A3, OUTPUT);
          sleep = 0;
          Serial.println("Leaving sleep");
        }
        break;
       case 2: //T3
        PORTC |= ((unsigned)(0xF0 & cur_dy) >> 4); //output upper nibble of dy
        state ++;
        break;
       case 3: //T4
        PORTC |= ((unsigned)(0x0F & cur_dy)); //output lower nibble of dy
        cur_dy = 0;
        state ++;
        break;
       case 4: //used to read from USB
        break;
       default:
       break;
    }
  }
 
  if (state == 4)
  {
    //Serial.println(millis()-last_task_time, DEC);
    last_task_time = millis();
    Usb.Task();//dead time, use to read from USB 
  }
 
  if (millis()-last_strobe_time > 1)//resync FSM
  {
    state = 0;
    last_strobe = 0;
  }

  if (millis()-last_completed_time > 1000)//haven't sent out full report in a while
  {
    if (!sleep)
    {
      Serial.println("Going to sleep");
      pinMode(A0, INPUT);
      pinMode(A1, INPUT);
      pinMode(A2, INPUT);
      pinMode(A3, INPUT);
      sleep = 1;
    }
  }
}

Cyothevile

Very cool.

I assume it could be ported to something like the pro micro?

Zuofu

Quote from: Cyothevile on December 28, 2021, 02:24:03 PMVery cool.

I assume it could be ported to something like the pro micro?

Most likely, that's what I'm going to try next actually. I'll keep this forum posted.

Zuofu

So it actually works quite well with the 3.3V Arduino Pro Mini. I used the Mini USB Host Shield (readily available off of eBay or Amazon in the US: https://smile.amazon.com/HiLetgo-Development-Compatible-Interface-Arduino/dp/B01EWW9R1E). Note that the Mini Host Shield requires a couple mods itself, !RST should be wired to VCC, the trace between VCC and VBus should be cut, and Vbus should be wired to RAW (to get 5V for the USB device for better compatibility). These are well documented in the Amazon reviews of the  Mini Host Shield.

Once the mods on the Mini Host Shield are done, it can be soldered directly to the Pro Mini assuming you are using the 3.3V of the Arduino. You can then wire the D-sub connector as in the comments of the code above. Keep in mind A4=SDA and A5=SCL and +5V should connect to RAW. I just cut a Sega controller extension cable but you can use a Dsub connector directly too. The only change is that an LED needs to go between D-sub pin 8 (COM) and digital input D2 (forward bias). This LED is merely to drop the voltage from 5 to ~3V as the 3.3V Pro Mini's I/O is not 5V tolerant. The LED will actually not turn on during operation (at least visibly, in the case of the LED I used).