Download the source code and project files (Visual C# 2013 Desktop/Arduino)
Click here to view a new and improved version of this project!
The end result of this project is that I can now play SNES games on my actual SNES by using my computer keyboard! I originally thought to do this because my brother and I used to play single player games together by taking turns playing. Since we are now living a 16-hour drive apart, I wondered if it wouldn't be possible to somehow get him to play a game at my house in Vancouver from his place in Saskatoon. We were working on Earthbound before he left, so I figured I'd see if I couldn't emulate a SNES controller using an Arduino Mega.
There are a few things required for this project:
The first thing you have to do is open up the controller and expose the board inside. Scratch away at the lines in the board to expose the copper, and solder some new wires to them. The board is clearly labelled with the buttons, so it's pretty easy to figure out where you should solder! Next, I attached these ends onto various pins on my Mega (see the table below), one pin per button (there are 12 buttons total). In the picture below not all the buttons are connected to the Arduino because I only had so many wires handy! But it should illustrate what I'm talking about.
Next, we need to look at the code. Let's start with the Arduino code, shall we?
//ArduinoSNESController.ino #define aPin 53 #define bPin 51 #define xPin 49 #define yPin 47 #define lPin 45 #define rPin 43 #define leftPin 41 #define rightPin 39 #define upPin 37 #define downPin 35 #define startPin 33 #define selectPin 31 void setup() { // put your setup code here pinMode(aPin, OUTPUT); //A button 'a' pinMode(bPin, OUTPUT); //B button 'b' pinMode(xPin, OUTPUT); //X button 'x' pinMode(yPin, OUTPUT); //Y button 'y' pinMode(lPin, OUTPUT); //Left shoulder button; 'q' pinMode(rPin, OUTPUT); //Right shoulder button 'w' pinMode(leftPin, OUTPUT); //Left DPad button 'l' pinMode(rightPin, OUTPUT); //Right DPad button 'r' pinMode(upPin, OUTPUT); //Up DPad button 'u' pinMode(downPin, OUTPUT); //Down DPad button 'd' pinMode(startPin, OUTPUT); //Start button 's' pinMode(selectPin, OUTPUT); //Select button 'z' Serial.begin(9600); } void loop() { int cmd; cmd = Serial.read(); switch(cmd) { case 'A': { pressButton(aPin); break; } case '1': { releaseButton(aPin); break; } case 'B': { pressButton(bPin); break; } case '2': {releaseButton(bPin); break; } case 'X': { pressButton(xPin); break; } case '4': { releaseButton(xPin); break; } case 'Y': { pressButton(yPin); break; } case '3': { releaseButton(yPin); break; } case 'L': { pressButton(leftPin); break; } case '5': { releaseButton(leftPin); break; } case 'R': { pressButton(rightPin); break; } case '6': { releaseButton(rightPin); break; } case 'U': { pressButton(upPin); break; } case '7': { releaseButton(upPin); break; } case 'D': { pressButton(downPin); break; } case '8': { releaseButton(downPin); break; } case 'Q': { pressButton(lPin); break; } case 'O': { releaseButton(lPin); break; } case 'W': { pressButton(rPin); break; } case 'I': { releaseButton(rPin); break; } case 'S': { pressButton(startPin); break; } case '9': { releaseButton(startPin); break; } case 'Z': { pressButton(selectPin); break; } case 'P': { releaseButton(selectPin); break; } } } void pressButton(int button) { digitalWrite(button, LOW); delay(10); } void releaseButton(int button) { digitalWrite(button, HIGH); delay(10); }So what's going on here? First we define the pin numbers for each button, that part is easy enough. Then we set 'em up using pinMode();, again nothing too exotic going on. In the main loop we look for serial input. Each button press AND release has a value with it (again, look at the table below for more information). For example, when the A button is pressed (keyDown) it has a value of A, and when it is released (keyRelease) it has a value of 1. The C# program will send these values to the Arduino via the serial port and the Arduino figures out what the input is, and then calls pressButton(Button Pin) or releaseButton(Button Pin) depending on the input it receives.
The SNES controller works by having all the buttons sitting at HIGH. When a button is pressed, it becomes LOW. So the pressButton(int button) function makes the appropriate button's pin LOW and then adds a bit of a delay (for debouncing). Similarly, releaseButton(in button) makes the button's pin HIGH and has that handy debouncing delay as well. That's really all there is to it!
So what about the C# code?
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using System.IO.Ports; using System.Windows.Input; //using System.Threading; namespace WindowsFormsApplication2 { public partial class Form1 : Form { //Variables to hold the controls Keys aButton; Keys bButton; Keys xButton; Keys yButton; Keys lButton; Keys rButton; Keys upButton; Keys downButton; Keys leftButton; Keys rightButton; Keys selectButton; Keys startButton; //Array to hold contents of controls.txt string[] controlInput = new string[12]; public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { //Open the serial port try { //open serial port serialPort.Open(); serialPort.Write(""); lblcomStatus.Text = "Connected!"; } catch (Exception ex) { MessageBox.Show(ex.Message, "ERROR", MessageBoxButtons.OK, MessageBoxIcon.Error); lblcomStatus.Text = "NOT CONNECTED"; } //Set the controls try { //Load the controls from controls.txt string[] lines = System.IO.File.ReadAllLines(@"controls.txt"); int x = 0; foreach (string line in lines) { controlInput[x] = line; x++; } //Update controls from controls.txt - convert strings to Keys TypeConverter converter = TypeDescriptor.GetConverter(typeof(Keys)); aButton = (Keys)converter.ConvertFromString(controlInput[0]); bButton = (Keys)converter.ConvertFromString(controlInput[1]); yButton = (Keys)converter.ConvertFromString(controlInput[2]); xButton = (Keys)converter.ConvertFromString(controlInput[3]); leftButton = (Keys)converter.ConvertFromString(controlInput[4]); rightButton = (Keys)converter.ConvertFromString(controlInput[5]); upButton = (Keys)converter.ConvertFromString(controlInput[6]); downButton = (Keys)converter.ConvertFromString(controlInput[7]); startButton = (Keys)converter.ConvertFromString(controlInput[8]); selectButton = (Keys)converter.ConvertFromString(controlInput[9]); lButton = (Keys)converter.ConvertFromString(controlInput[10]); rButton = (Keys)converter.ConvertFromString(controlInput[11]); //Update the labels on the form lblA.Text = controlInput[0]; lblB.Text = controlInput[1]; lblY.Text = controlInput[2]; lblX.Text = controlInput[3]; lblLeft.Text = controlInput[4]; lblRight.Text = controlInput[5]; lblUp.Text = controlInput[6]; lblDown.Text = controlInput[7]; lblStart.Text = controlInput[8]; lblSelect.Text = controlInput[9]; lblL.Text = controlInput[10]; lblR.Text = controlInput[11]; } catch (Exception ex) { MessageBox.Show(ex.Message, "Error with controls.txt: ", MessageBoxButtons.OK, MessageBoxIcon.Error); } } protected override void OnKeyDown(System.Windows.Forms.KeyEventArgs e) { keyDownHandler(e); } protected override void OnKeyUp(System.Windows.Forms.KeyEventArgs e) { keyReleaseHandler(e); } void keyDownHandler(System.Windows.Forms.KeyEventArgs e) { if (e.KeyCode == aButton) keyAction("A"); else if (e.KeyCode == bButton) keyAction("B"); else if (e.KeyCode == yButton) keyAction("Y"); else if (e.KeyCode == xButton) keyAction("X"); else if (e.KeyCode == leftButton) keyAction("L"); else if (e.KeyCode == rightButton) keyAction("R"); else if (e.KeyCode == upButton) keyAction("U"); else if (e.KeyCode == downButton) keyAction("D"); else if (e.KeyCode == startButton) keyAction("S"); else if (e.KeyCode == selectButton) keyAction("Z"); else if (e.KeyCode == lButton) keyAction("Q"); else if (e.KeyCode == rButton) keyAction("W"); } void keyReleaseHandler(System.Windows.Forms.KeyEventArgs e) { if (e.KeyCode == aButton) keyAction("1"); else if (e.KeyCode == bButton) keyAction("2"); else if (e.KeyCode == yButton) keyAction("3"); else if (e.KeyCode == xButton) keyAction("4"); else if (e.KeyCode == leftButton) keyAction("5"); else if (e.KeyCode == rightButton) keyAction("6"); else if (e.KeyCode == upButton) keyAction("7"); else if (e.KeyCode == downButton) keyAction("8"); else if (e.KeyCode == startButton) keyAction("9"); else if (e.KeyCode == selectButton) keyAction("P"); else if (e.KeyCode == lButton) keyAction("O"); else if (e.KeyCode == rButton) keyAction("I"); } void keyAction(string button) { serialPort.Write(button); statusLabelHandler(button); } void statusLabelHandler(string button) { switch (button) { case "A": { lblButtonStatus.Text = "A PRESS"; break; } case "1": { lblButtonStatus.Text = "A RELEASE"; break; } case "B": { lblButtonStatus.Text = "B PRESS"; break; } case "2": { lblButtonStatus.Text = "B RELEASE"; break; } case "Y": { lblButtonStatus.Text = "Y PRESS"; break; } case "3": { lblButtonStatus.Text = "Y RELEASE"; break; } case "X": { lblButtonStatus.Text = "X PRESS"; break; } case "4": { lblButtonStatus.Text = "X RELEASE"; break; } case "L": { lblButtonStatus.Text = "LEFT PRESS"; break; } case "5": { lblButtonStatus.Text = "LEFT RELEASE"; break; } case "R": { lblButtonStatus.Text = "RIGHT PRESS"; break; } case "6": { lblButtonStatus.Text = "RIGHT RELEASE"; break; } case "U": { lblButtonStatus.Text = "UP PRESS"; break; } case "7": { lblButtonStatus.Text = "UP RELEASE"; break; } case "D": { lblButtonStatus.Text = "DOWN PRESS"; break; } case "8": { lblButtonStatus.Text = "DOWN RELEASE"; break; } case "S": { lblButtonStatus.Text = "START PRESS"; break; } case "9": { lblButtonStatus.Text = "START RELEASE"; break; } case "Z": { lblButtonStatus.Text = "SELECT PRESS"; break; } case "P": { lblButtonStatus.Text = "SELECT RELEASE";break; } case "Q": { lblButtonStatus.Text = "L PRESS"; break; } case "O": { lblButtonStatus.Text = "L RELEASE"; break; } case "W": { lblButtonStatus.Text = "R PRESS"; break; } case "I": { lblButtonStatus.Text = "R RELEASE"; break; } } } } }The first thing we do is define a bunch of Keys variables, one for each button. Then we make an array which we use later to temporarily hold the keyboard's controls. When the form loads we open the serial port and fill the aforementioned array with the values from controls.txt. controls.txt is located in the same directory as the .exe, and the order of the buttons is the same as the order of the controlInput[] array. This allows the user to edit the controls (shocking, I know) which I thought was a pretty major feature to have. Working with Keys is a bit tricky, however, so we have to convert each string in the array to a Keys value. Happily, after a lot of Googling I found the answer! So we convert each string in the array to Keys and update the labels on the form so the user has a handy reference in front of them while they play.
Next we have two functions - OnKeyDown and OnKeyUp which, as you might've guessed, are called every time a key is pressed and a key is released. These then call, in turn, their respective handlers - keyDownHandler and keyReleaseHandler. All these handers do is look at what button the user pressed and send the appropriate keyDown/keyRelease value (as I talked about above) to the serial port so the Arduino knows what to do. The statusLabelHandler just looks at the user input and displays what button has been pressed. This was added so I could make sure the program was reading my key presses/releases!
If you set this up, with this code, what is the end result? You can play any SNES game on your SNES with your computer keyboard! And it runs fluidly, with negligible lag! Multiple keypresses (for example, holding Y as you run in Super Mario World to run faster) work just fine! I doubt this code is the best way to handle this situation, but it works, and it works as well as I could hope! I would like to put this whole thing in a project box at some point to clean it up, but I have another idea as well...
During my research I found a really cool video on YouTube by a guy who makes a SNES controller using a breadboard with some buttons, wires, and a couple of shift registers. I really like this idea, and it would make the entire thing a lot smaller and cleaner, not having to use the board from within the controller. I plan on ordering some shift registers and seeing if I can't get my C# program to run this setup as well (instead of physical buttons on a breadboard). Check out the video belooooooow:
Finally, here's that handy little table I keep talking about. It shows the keyDown (Arduino), keyRelease (Arduino), the Arduino Mega pin, and the controlInput[] (C#) value for each SNES button equivalent. The buttons that you have defined in controls.txt can be almost any value you like - but these values are in the code and cannot be changed. ...well, unless you change the code!
RILEY'S ARDUINO SNES CONTROLLER MAP SNES | keyDown | keyRelease | Arduino Mega Pin | controlInput[] ----------------------------------------|-------------------|----------------- A | A | 1 | 53 | 0 B | B | 2 | 51 | 1 Y | Y | 3 | 47 | 2 X | X | 4 | 49 | 3 LEFT | L | 5 | 41 | 4 RIGHT | R | 6 | 39 | 5 UP | U | 7 | 37 | 6 DOWN | D | 8 | 35 | 7 START | S | 9 | 33 | 8 SELECT | Z | P | 31 | 9 L | Q | O | 45 | 10 R | W | I | 43 | 11