Tuesday, September 09, 2014

Arduino: 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 article is for you. This article describes how to use an Arduino Leonardo (or similar) card to make your classic console joystick look like a USB keyboard.

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:

Default Keyboard Mappings

By default this classic console joystick to USB keyboard adapter uses the following mappings (these are the default mappings used by the ADAMEm emulator):

Joystick Action Keyboard Mapping
Up Up Arrow Key
Down Down Arrow Key
Left Left Arrow Key
Right Right Arrow Key
Left Fire Button Left Alt Key
Right Fire Button Left Ctrl Key
Purple Fire Button Left Shift Key
Blue Fire Button z
Keypad 0 0
Keypad 1 1
Keypad 2 2
Keypad 3 3
Keypad 4 4
Keypad 5 5
Keypad 6 6
Keypad 7 7
Keypad 8 8
Keypad 9 9
Keypad * -
Keypad # =

These mappings can be changed by altering the Arduino sketch file provided (see the Keyboard Keys section).

Atari 2600 Joysticks

Atari 2600 JoystickThe Atari 2600 joysticks are the simplest of the controllers supporter by this adaptor. They have a D-Subminiature Female 9 Position connector. The following table describes the function of each pin:

Pin Number Description
1 Up
2 Down
3 Left
4 Right
6 Fire
8 Ground

ColecoVision Controllers

ColecoVision Joystick
ADAM Joystick
ColecoVision Controller
ADAM Controller
ColecoVision Super Action Controller
ColecoVision Super Action Controller

The ColecoVision and ADAM joysticks are much more complicated. In addition to the four direction control stick, they have two fire buttons (or 4 in the case of the Super Action Controller) and a 12 button keypad. Like the Atari 2600 joystick, they have a D-Subminiature Female 9 Position connector, but the pin outs have two different modes. When pin 8 is grounded, pins 1-4 are the four directions and pin 6 is the left fire button. When pin 5 is grounded, pins 1-4 are a keypad scan code and pin 6 is the right fire button. The table below lists the pin outs for the ColecoVision and ADAM controllers.

Pin Number Pin 8 Grounded Pin 5 Grounded
1 Up Bit 0
2 Down Bit 2
3 Left Bit 3
4 Right Bit 1
5 Keypad Mode Keypad Mode
6 Left Fire Right Fire
7 Spinner Spinner
8 Control Stick Mode Control Stick Mode
9 Spinner Spinner

The keypad scan codes are listed below:

Keypad Value
Bit 3 / Pin 3
Bit 2 / Pin 2
Bit 1 / Pin 4
Bit 0 / Pin 1
Decimal Value
Hex Value
1
0
0
1
0
2
2
2
1
0
0
0
8
8
3
0
0
1
1
3
3
4
1
1
0
1
13
D
5
1
1
0
0
12
C
6
0
0
0
1
1
1
7
1
0
1
0
10
A
8
1
1
1
0
14
E
9
0
1
0
0
4
4
0
0
1
0
1
5
5
*
0
1
1
0
6
6
#
1
0
0
1
9
9
Purple Fire Button
0
1
1
1
7
7
Blue Fire Button
1
0
1
1
11
B

The solution I have developed will support Atari 2600 joysticks, ColecoVision and ADAM controllers, and most of the ColecoVision Super Action Controllers. This solution does not currently support the Spinner located below the keypad on the Super Action Controllers.

What You Need

Hardware

The pins of the D-Sub Male 9 Position connector should be connected to the pins of the Arduino Leonardo or Arduino Micro as shown in the table below:

Arduino Pin Male D-Sub Pin Description
2 6 Fire Button
3 1 Up
4 2 Down
5 3 Left
6 4 Right
7 5 Keypad Mode
8 8 Joystick Mode
9 7 Spinner (not used)
10 9 Spinner (not used)

Classic Joystick to USB Adaptor

Software

The Arduino sketch file listed below should be compiled and uploaded into the Arduino Leonardo or Arduino Micro.


// ColecoVision / ADAM Joystick to PC Keyboard Converter
// for the Arduino Leonardo
// 2014-08-24
//----------------------------------------------------------------------------------

// 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;

// 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 = '=';

// 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;

// 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;

// 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;

// Frame Variables
const int gcFrameLength = 10;
unsigned int gLoopsPerFrame = 0;
unsigned long gLastFrameStart = 0;

// Shows the status of the joystick.
void ShowJoystickStatus()
{
  // Debug Information
//  Serial.println(gLoopsPerFrame);

  digitalWrite(gcFireMonitorPin, gLeftFireButton | gRightFireButton);
}

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);
}

void SendLineStateToPc(byte lastState, byte currentState, char keyUsedForLine)
{
  if ((lastState == 1) && (currentState == 0))
  {
    Keyboard.release(keyUsedForLine);
  }
  if ((lastState == 0) && (currentState == 1))
  {
    Keyboard.press(keyUsedForLine);
  }
}

unsigned int CheckJoystickLine(byte pin)
{
  return (digitalRead(pin) == LOW);
}

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);

  // 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);
}

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;
}

byte DetermineJoystickLineValue(unsigned int pressCount)
{
    return (pressCount >= gcThreashold);
}

void DetermineJoystickValues()
{
  const char cKeypadValueLookup[16] = {
    ' ', '6', '1', '3', '9', '0', gcAsteriskKey, ' ', 
    '2', gcPoundKey, '7', ' ', '5', '4', '8', ' '};
    
  gLeftFireButton = DetermineJoystickLineValue(gLeftFireCount);
  gRightFireButton = DetermineJoystickLineValue(gRightFireCount);

  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;
  }
}

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);
}

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();
  }
}


Setup Function

The setup function is used to initialize the Arduino pins. Pins 2-6 are setup as input pins with pull-up resistors. Pins 7 and 8 are setup as output pins to toggle between Keypad Mode and Controller Mode. Pin 13 is setup as an output pin. On most Arduino boards pin 13 is connected to an LED. This LED is used to monitor the left and right fire buttons.

Loop Function

The main loop function is setup to report the value of the various joystick elements as either keydown or keyup events at a rate defined by the gcFrameLength constant. The main loop will read the state of all input pins in both the Keypad and Controller modes as many times as possible within a frame. If the number of times a pin was low exceeds the gcThreashold value, the pin is considered low for that frame. This is how each input is debounced.

Debugging

The Arduino COM port can be used for debugging by uncommenting out the call to Serial.begin in the setup function and adding debug output to the ShowJoystickStatus function.

Possible Future Enhancements

  • Add support for the ColecoVision Super Action Controller’s Spinner. This would map to the X-axis of the mouse.
  • Add support for ColecoVision Expansion Module #2 (the driving module).
  • Add support for the ColecoVision Roller Controller.
  • Add support for the Atari 2600 Paddle Controllers.
  • Create a version that presents the classic console controller as USB Game Controller instead of a USB Keyboard.

9 comments:

zabagar said...

So if I just have my old classic Atari joystick, and my Atari emulator I can have the stick emulate the keyboard, I see.
What connections from the db9 to the arduino can I leave off since I'm just doing the Atari stick? I think you cover more than I need.
I guess I could cut out parts of the script too then? I'm brand new to arduino but can program and am a tech-head! I just got an arduino uno kit.....walked thru a few samples from the book.....thanks for any help. -Bob

Matthew Heironimus said...

zabagar - You asked:
> What connections from the db9 to the arduino can I leave off since I'm just doing the Atari stick?
A: For the Atari 2600 controllers, you only need to connect the following pins: 1 (Up), 2 (Down), 3 (Left), 4 (Right), 6 (Fire), and 8 (Ground).
> I guess I could cut out parts of the script too then?
A: Yes, there is some stuff you can cut out of the sketch file if you are just using Atari 2600 controllers, but it will not hurt to leave the code as it is. Let me know if you would like specifics on what can be taken out.

zabagar said...

Thanks so much! I'll post after I set it up! I'll probably have the chance over the weekend to test it!!
-Bob

zabagar said...

Oh now I see that the Arduino UNO does not understand the keyboard calls - that's an Arduino Leonardo improvement....oh well...maybe I'll but a Leonardo. I'm new to Arduino so I didn't (and still don't) really know the difference between all of the different models...there are so many variations.

zabagar said...

Wow, just trying to find the "right" Arduino Leonardo is difficult.
In your opinion does this look like it's what I should get?

http://www.amazon.com/Arduino-A000057-Leonardo-with-Headers/dp/B008A36R2Y/ref=sr_1_1?s=electronics&ie=UTF8&qid=1441469476&sr=1-1&keywords=arduino+leonardo&pebp=1441469490834&perid=00VDY5C4G6VNANK7RA12

Matthew Heironimus said...

Yes, the link you posted is for an Arduino Leonardo, which is one of the ones that will work for this project. The other one that will work with this project is the Arduino Micro (http://www.amazon.com/Arduino-A000053-Micro/dp/B00AFY2S56).

zabagar said...

Cool, thanks. Being able to use keyboard functions will be great for other little projects as well. I will get such a kick out of playing my old atari 800 and atari 2600 games on an emulator, on my vista laptop, with an actual atari joystick. Being the computer geek I still actually have a working atari 800xl and 2600 too!! I'm so glad I found your post. I picked up an arduino knowing I was going to have some fun with it, then it hit me that I should be able to use it the way you've already done. So I found your page & code doing a google search. Glad somebody figured it out already, so I didn't have to fumble through it! Thanks a million - - - Bob Z.

zabagar said...

I'm playing River Raid at the moment with my original Atari stick via Leonardo. Thanks again. If Karma exists you have a bunch on its way. Thank you!

Matthew Heironimus said...

That's cool. I am glad to hear it worked for you.