Classic Joystick to USB Adaptor

19K10619

Intro: Classic Joystick to USB Adaptor

If you grew up in the early 1980’s and were into video games, you probably had an Atari 2600, ColecoVision, or similar game console. The controllers or joysticks for each of these systems had a distinct feel that is different from today’s game consoles or PC game controllers. If you find yourself longing to plug your old ColecoVision or Atari 2600 joystick into your modern PC, but are not sure how to go about it, this project is for you.

I designed this Classic Joystick to USB Keyboard Adapter with the ADAMEm ColecoVision and Coleco ADAM emulator in mind (http://www.komkon.org/~dekogel/adamem.html, which can be run in Microsoft Windows using Virtual ADAM http://www.sacnews.net/adamcomputer/downloads/). However it will work with most emulators and any game or program that can use the keyboard as input. I have also tested it with the following emulators:

The following classic game console controllers are support:

  • Atari 2600
  • ColecoVision
  • Coleco ADAM
  • ColecoVision Super Action Controller (Spinner is not currently supported)

STEP 1: What You Need

STEP 2: Hardware

Connect the pins of the D-Sub Male 9 Position connector to the pins of the Arduino Leonardo or Arduino Micro as shown in the table.

STEP 3: Software

The following Arduino Sketch file should be compiled and uploaded into the Arduino Leonardo or Arduino Micro: JoystickToKeyboard.ino

<p>// ColecoVision / ADAM Joystick to PC Keyboard Converter<br>// for the Arduino Leonardo
// 2014-08-24
//-----------------------------------------------------------------------------</p><p>// Joystick Pins
const byte gcFirePin = 2;
const byte gcUpPin = 3;
const byte gcDownPin = 4;
const byte gcLeftPin = 5;
const byte gcRightPin = 6;
const byte gcModeAPin = 7;
const byte gcModeBPin = 8;
const byte gcFireMonitorPin = 13;
const byte gcBit0Pin = 3;
const byte gcBit2Pin = 4;
const byte gcBit3Pin = 5;
const byte gcBit1Pin = 6;</p><p>// Keyboard Keys
const char gcUpKey = KEY_UP_ARROW;
const char gcDownKey = KEY_DOWN_ARROW;
const char gcLeftKey = KEY_LEFT_ARROW;
const char gcRightKey = KEY_RIGHT_ARROW;
const char gcLeftFireKey = KEY_LEFT_ALT;
const char gcRightFireKey = KEY_LEFT_CTRL;
const char gcPurpleButtonKey = KEY_LEFT_SHIFT;
const char gcBlueButtonKey = 'z';
const char gcAsteriskKey = '-';
const char gcPoundKey = '=';</p><p>// Current Frame Joystick Status
byte gLeftFireButton = 0;
byte gRightFireButton = 0;
byte gUp = 0;
byte gDown = 0;
byte gLeft = 0;
byte gRight = 0;
char gNumPadValue = ' ';
byte gPurpleButton = 0;
byte gBlueButton = 0;
byte gBit0 = 0;
byte gBit1 = 0;
byte gBit2 = 0;
byte gBit3 = 0;</p><p>// Last Frame Joystick Status
byte gLastLeftFireButton = 0;
byte gLastRightFireButton = 0;
byte gLastUp = 0;
byte gLastDown = 0;
byte gLastLeft = 0;
byte gLastRight = 0;
char gLastNumPadValue = ' ';
byte gLastPurpleButton = 0;
byte gLastBlueButton = 0;</p><p>// Line Read Variables
const unsigned int gcThreashold = 4;
unsigned int gLeftFireCount = 0;
unsigned int gRightFireCount = 0;
unsigned int gUpCount = 0;
unsigned int gDownCount = 0;
unsigned int gLeftCount = 0;
unsigned int gRightCount = 0;
unsigned int gBit0Count = 0;
unsigned int gBit1Count = 0;
unsigned int gBit2Count = 0;
unsigned int gBit3Count = 0;</p><p>// Frame Variables
const int gcFrameLength = 10;
unsigned int gLoopsPerFrame = 0;
unsigned long gLastFrameStart = 0;</p><p>// Shows the status of the joystick.
void ShowJoystickStatus()
{
  // Debug Information
//  Serial.println(gLoopsPerFrame);</p><p>  digitalWrite(gcFireMonitorPin, gLeftFireButton | gRightFireButton);
}</p><p>void SendKeyboardStateToPc()
{
  if ((gLastNumPadValue != ' ') && (gLastNumPadValue != gNumPadValue))
  {
    Keyboard.release(gLastNumPadValue);
  }
  if ((gNumPadValue != ' ') && (gLastNumPadValue != gNumPadValue))
  {
    Keyboard.press(gNumPadValue);
  }
  
  SendLineStateToPc(gLastLeftFireButton, gLeftFireButton, gcLeftFireKey);
  SendLineStateToPc(gLastRightFireButton, gRightFireButton, gcRightFireKey);
  SendLineStateToPc(gLastUp, gUp, gcUpKey);
  SendLineStateToPc(gLastDown, gDown, gcDownKey);
  SendLineStateToPc(gLastLeft, gLeft, gcLeftKey);
  SendLineStateToPc(gLastRight, gRight, gcRightKey);
  SendLineStateToPc(gLastPurpleButton, gPurpleButton, gcPurpleButtonKey);
  SendLineStateToPc(gLastBlueButton, gBlueButton, gcBlueButtonKey);
}</p><p>void SendLineStateToPc(byte lastState, byte currentState, char keyUsedForLine)
{
  if ((lastState == 1) && (currentState == 0))
  {
    Keyboard.release(keyUsedForLine);
  }
  if ((lastState == 0) && (currentState == 1))
  {
    Keyboard.press(keyUsedForLine);
  }
}</p><p>unsigned int CheckJoystickLine(byte pin)
{
  return (digitalRead(pin) == LOW);
}</p><p>void CheckJoystickLines()
{
  const int cLineDelay = 50;
  
  // Put Joystick in Direction Mode
  digitalWrite(gcModeAPin, HIGH);
  digitalWrite(gcModeBPin, LOW);
  delayMicroseconds(cLineDelay);
  gLeftFireCount += CheckJoystickLine(gcFirePin);
  gUpCount += CheckJoystickLine(gcUpPin);
  gDownCount += CheckJoystickLine(gcDownPin);
  gLeftCount += CheckJoystickLine(gcLeftPin);
  gRightCount += CheckJoystickLine(gcRightPin);</p><p>  // Put Joystick in Keypad Mode
  digitalWrite(gcModeAPin, LOW);
  digitalWrite(gcModeBPin, HIGH);
  delayMicroseconds(cLineDelay);
  gRightFireCount += CheckJoystickLine(gcFirePin);
  gBit0Count += CheckJoystickLine(gcBit0Pin);
  gBit1Count += CheckJoystickLine(gcBit1Pin);
  gBit2Count += CheckJoystickLine(gcBit2Pin);
  gBit3Count += CheckJoystickLine(gcBit3Pin);
}</p><p>void ResetFrameVariables()
{
  // Copy Current Frame to Last Frame
  gLastNumPadValue = gNumPadValue;
  gLastLeftFireButton = gLeftFireButton;
  gLastRightFireButton = gRightFireButton;
  gLastUp = gUp;
  gLastDown = gDown;
  gLastLeft = gLeft;
  gLastRight = gRight;
  gLastPurpleButton = gPurpleButton;
  gLastBlueButton = gBlueButton;
  
  // Reset Frame Loop Counter
  gLoopsPerFrame = 0;
  
  // Reset Joysick State
  gLeftFireCount = 0;
  gLeftFireButton = 0;
  gRightFireCount = 0;
  gRightFireButton = 0;
  gUpCount = 0;
  gUp = 0;
  gDownCount = 0;
  gDown = 0;
  gLeftCount = 0;
  gLeft = 0;
  gRightCount = 0;
  gRight = 0;
  gBit0Count = 0;
  gBit0 = 0;
  gBit1Count = 0;
  gBit1 = 0;
  gBit2Count = 0;
  gBit2 = 0;
  gBit3Count = 0;
  gBit3 = 0;
  gNumPadValue = ' ';
  gPurpleButton = 0;
  gBlueButton = 0;
}</p><p>byte DetermineJoystickLineValue(unsigned int pressCount)
{
    return (pressCount >= gcThreashold);
}</p><p>void DetermineJoystickValues()
{
  const char cKeypadValueLookup[16] = {
    ' ', '6', '1', '3', '9', '0', gcAsteriskKey, ' ', 
    '2', gcPoundKey, '7', ' ', '5', '4', '8', ' '};
    
  gLeftFireButton = DetermineJoystickLineValue(gLeftFireCount);
  gRightFireButton = DetermineJoystickLineValue(gRightFireCount);</p><p>  gUp = DetermineJoystickLineValue(gUpCount);
  gDown = DetermineJoystickLineValue(gDownCount);
  gLeft = DetermineJoystickLineValue(gLeftCount);
  gRight = DetermineJoystickLineValue(gRightCount);
  
  gBit0 = DetermineJoystickLineValue(gBit0Count);
  gBit1 = DetermineJoystickLineValue(gBit1Count);
  gBit2 = DetermineJoystickLineValue(gBit2Count);
  gBit3 = DetermineJoystickLineValue(gBit3Count);
  
  int keypadCode = (gBit3 << 3) + (gBit2 << 2) + (gBit1 << 1) + gBit0;
  
  gNumPadValue = cKeypadValueLookup[keypadCode];
  
  // Check for SuperAction Controller Buttons.
  if (keypadCode == 7)
  {
    gPurpleButton = 1;
  }
  if (keypadCode == 11)
  {
    gBlueButton = 1;
  }
}</p><p>void setup()
{
  // Setup Serial Monitor
//  Serial.begin(19200);
  
  // Setup Joystick Pins
  pinMode(gcFirePin, INPUT_PULLUP);
  pinMode(gcUpPin, INPUT_PULLUP);
  pinMode(gcDownPin, INPUT_PULLUP);
  pinMode(gcLeftPin, INPUT_PULLUP);
  pinMode(gcRightPin, INPUT_PULLUP);
  pinMode(gcModeAPin, OUTPUT);
  pinMode(gcModeBPin, OUTPUT);
  pinMode(gcFireMonitorPin, OUTPUT);
}</p><p>void loop()
{
  unsigned long currentTime;
  
  currentTime = millis();
  if (currentTime >= (gLastFrameStart + gcFrameLength))
  {
    // Do Joystick Value Commit Logic
    DetermineJoystickValues();
    
    // Send Values to Monitor
    ShowJoystickStatus();
    
    // Send Keyboad State to the PC
    SendKeyboardStateToPc();
    
    // Reset Frame Variables
    ResetFrameVariables();
  
    // Time to start next frame
    gLastFrameStart = currentTime;
  }
  else
  {
    // Check the value of the input lines and make note of them.
    gLoopsPerFrame++;
    CheckJoystickLines();
  }
}</p>

STEP 4: Default Keyboard Mappings

By default the classic console joystick to USB keyboard adapter uses the mappings shown in the table (these are the default mappings used by the ADAMEm emulator). These mappings can be changed by altering the Arduino sketch file provided (see the Keyboard Keys section).

STEP 5: Additional Details

Additional details on the nuances of reading values from Atari 2600 and ColecoVision controllers can be found on my blog entry at http://mheironimus.blogspot.com/2014/09/arduino-classic-joystick-to-usb-adaptor.html.

18 Comments

Does it supports android games
Think this would work with a Teensy ++ 2.0? It is a different processor - seems like it has more memory, and more outputs.

Unfortunately, I do not have one to test with and I have not heard of anyone attempting to try this with a Teensy++ 2.0, so I cannot really say. If you do end up giving it a try, please let me know if it works or not. I am trying to keep a list of boards that work and boards that do not work on my GitHub site (https://github.com/MHeironimus/ArduinoJoystickLibrary).

A Teensy 2.0 should work since it has an ATmega32U4 right?

It should, but I do not have one to test with. Let me know if it works. I am trying to keep a list of boards that people have used. Thanks, Matt.

Could an Arduino Pro Trinket be used for this?

I do not think so. The Pro Trinket uses the Atmega328P, not the ATmega32U4. This project requires an Arduino with a ATmega32U4 (i.e. the Arduino Leonardo, Arduino Micro, etc.).

Smart idea! I really like this project. Thanks for shearig :)

Would it be possible to handle two joysticks at once? It would be awesome for some classic 2-player action! ps. Parts arrived yesterday and I'm building it next weekend!

Glad to hear you are going to make one. Let me know if you have any problems.

Yes, you can read two joysticks using one Arduino Leonardo (or Arduino Micro). I would build the single version first (just to make sure everything is working correctly) then add the second.

There are a couple of ways you could do this, but this is the approach I would take:

Hardware

1. Get a second 9-Position HD Male D-Sub Connector (this will be for the second joystick).

2. Wire pins 5 and 8 on the second D-Sub Connector to the same Arduino pins as Joystick one (this will save you two pins on your Arduino).

3. Wire pins 1-6 on the second D-Sub Connector to open pins on the Arduino (do not use pin 13, however). If it were me, I would use pins A0-A5.

I would not bother wiring up pins 7 or 9 on the second D-Sub Connector (since I do not use them at this time).

Software

1. In the “Joystick Pins” section, change all of the pin byte variables to be 2 element arrays (e.g. const byte gcFirePin[] = {2, A0}; const byte gcUpPin = {3, A1}; etc.). The gcModeAPin or gcModeBPin constants do not need to be arrays if both joystick 1 and 2 are using the same “mode” pins on the Arduino.

2. In the “Keyboard Keys” section, change all of the key mapping constants to be 2 element arrays. Decide which keyboard keys will be used to represent the second joystick’s up, down, left, right, etc. Your decision on how to map these keys will probably depend on which emulator or game you are planning to use.

3. The variables in the “Current Frame Joystick Status”, “Last Frame Joystick Status”, and “Line Read Variables” will need to be converted into two element arrays as well.

4. The gcThreashold constant and “Frame Variables” section will not need to be converted to arrays.

5. You can update the ShowJoystickStatus however you want. I just use that for debugging.

6. You will want to add a new joystickIndex argument to the SendKeyboardStateToPc, ResetFrameVariables, and DetermineJoystickValues functions and use that for all of the variables and constants you converted to arrays {e.g. SendLineStateToPc(gLastLeftFireButton, gLeftFireButton, gcLeftFireKey) will become SendLineStateToPc(gLastLeftFireButton[joystickIndex], gLeftFireButton[joystickIndex], gcLeftFireKey[joystickIndex])}.

7. In CheckJoystickLines start a [for(int joystickIndex = 0; joystickIndex < 2; joystickIndex++)] loop right after the first delayMicroseconds(cLineDelay) call and end it right before the “Put Joystick in Keypad Mode” comment. Do the same thing after the second delayMicroseconds(cLineDelay) call.

8. If you want both keypads to send the same keys, you can leave the DetermineJoystickValues function alone (this will work for most games). If you have a game where both players are active at the same time and the game uses the keypad buttons (e.g. Fortune Builder), you will need to convert cKeypadValueLookup into a two dimensional array and create a set of mappings for the second joystick’s keypad.

9. Put the 0-1 “for” loop in the setup function to initialize the Arduino pins.

The Arduino is fast enough to read both joysticks without messing with the timing variables (there are a lot of spare cycles, even running at 100 Hertz).

Let me know if you have any problems with the software changes I outlined above.

In the "Hardware" section above, item #3 should have read:
3. Wire pins 1-4 and pin 6 on the second D-Sub Connector to open pins on the Arduino (do not use pin 13, however). If it were me, I would use pins A1-A5.

Can you make this adapter using an Arduino Nano instead of the Micro?

Not the way it is currently written. The microcontroller on the board must be an ATmega32u4, but the Arduino Nano uses either an ATmega168 or ATmega328. Currently only the Arduino Leonardo and the Arduino Micro use the ATmega32u4.

That said, it would be theoretically possible to use some of the spare pins on the Arduino Nano, along with some additional hardware, to create another USB port that would be for the virtual keyboard. See the following example for more details: https://github.com/practicalarduino/VirtualUsbKeyboard/blob/master/VirtualUsbKeyboard.pde

Another possibility would be to reprogram the FT232 chip on the board to be a keyboard. See https://learn.adafruit.com/arduino-tips-tricks-and-techniques for more details.

Hello, can you explain more in detail how does it work?

I see you are using Keyboard object. Where is it implemented? do we need to import some library?

Does the arduino simulate a keyboard HID or it is sending the information thrugh virtual serial port?

Thanks for sharing!

Auto answer:
I seen that arduino leonard and micro have ATmega32u4 that implements build-in USB communication. And keyboard is just a class already implemented in arduino IDE.

Nice boards, I didn't knew it.

ZOMG! I had an ADAM with one of those controllers. It took up an entire desk. Nice reuse 'ible!

Me too. I actually still have one that takes up an entire table. My kids actually like playing the old games on it.

thanks for sharing will this work for a microsoft sidewinder game pad

good work