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
The 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 Controller
|
ADAM 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) |
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.