Classic Joystick to USB Adaptor
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:
- blueMSX (ColecoVision, MSX, and others) - http://www.bluemsx.com/
- Stella (Atari 2600) - http://stella.sourceforge.net/
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
- Arduino Leonardo - http://arduino.cc/en/Main/ArduinoBoardLeonardo or Arduino Micro - http://arduino.cc/en/Main/ArduinoBoardMicro
- 9-Position HD Male D-Sub Connector - http://www.radioshack.com/product/index.jsp?productId=2102497 or equivalent
- Wire
- Classic Console Joystick
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
MêgånaP 5 years ago
MikeGI 7 years ago
MatthewH 7 years ago
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).
ShaunM57 7 years ago
A Teensy 2.0 should work since it has an ATmega32U4 right?
MatthewH 7 years ago
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.
LakePHall 7 years ago
Could an Arduino Pro Trinket be used for this?
MatthewH 7 years ago
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.).
ibenkos 9 years ago
mikko.suihko 9 years ago
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!
MatthewH 9 years ago
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.
MatthewH 9 years ago
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.
mikko.suihko 9 years ago
Can you make this adapter using an Arduino Nano instead of the Micro?
MatthewH 9 years ago
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.
gismgism 9 years ago
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!
gismgism 9 years ago
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.
blodefood 9 years ago
ZOMG! I had an ADAM with one of those controllers. It took up an entire desk. Nice reuse 'ible!
MatthewH 9 years ago
thebear1 9 years ago
thanks for sharing will this work for a microsoft sidewinder game pad
good work