Overview
We'll start by turning the buttons of the game pad into keyboard buttons, so that pressing 'up' is converted into the 'U' key, for example. The firmware is easily adaptable, so you can adjust it for whatever software it will be used with.
Then we'll make the project more interesting by adding an accelerometer. This will allow the game pad to be used as a mouse by tilting it!
This tutorial including the original code and Portal video is by Devlin Thyne! Rock!
What you'll need:
You'll need the following in order to build the project:
- Game Pad Controller - We'll be using an SNES Controller
- Teensy - This is a very small microcontroller board that can act as a keyboard/mouse
- Triple-axis accelerometer - We'll be using the nice ADXL335 on a breakout board. You can skip this if you're not planning to add in the mouse capability
- USB cable with mini-b connector - to attach to the Teensy for plugging into a computer!
- Ribbon cable - for all the soldering connections. Rainbow cable is the easiest to work with as its color coded
If you want to build the entire project, we have a project pack in the shop with all the parts listed above!
You'll also need some basic hand tools such as screwdrivers, wire strippers, soldering iron, solder, diagonal cutters, vise or third hand tool, etc.
All the code is on GitHub, including some extra sketches we've written so be sure to look there!
Page last edited March 08, 2024
Text editor powered by tinymce.
Disassemble the SNES Controller
Each button is made of 3 parts - theres the plastic part that you press, beneath that is the elastomer which is a rubber molded piece with a conductive bit that goes underneath the plastic part, and finally on the PCB there are two interdigitated and exposed traces. When the user presses the plastic button, it pushes down on the elastomer which then pushes the conductive rubber onto both traces, shorting them.
There is also a black blob in the middle. This blob is a chip that takes all the button inputs and then converts it into the way that the SNES wants to hear. Thats all fine, but we dont want to use the blob because we are going to make our own custom chip software. (Note that it would be pretty easy to make the Teensy 'talk' right to the blob using the SNES protocol but then you wouldn't be able to adapt this tutorial to other controllers, for that reason we're going to do it the 'hard way')
The question is now how can we listen to all the buttons?
Note that this is really just a symbolic schematic, the ground wire doesn't necessarily connect on the side thats indicated, we're just showing how all the buttons have a common ground pin!
OK now this is straight forward, if you are not sure how to read buttons with a microcontroller, we have a nice tutorial you might want to check out (in fact, we really suggest it as we'll be referring to concepts in that tutorial) Basically each button connects to an input of the microcontroller. We'll need a pull-up resistor, but luckily we can set the microcontroller's internal pullups (so we dont have to solder in 12 10K resistors!) Then the microcontroller can listen on each pin for a button press and when it is received, generate a keypress event.
Page last edited March 08, 2024
Text editor powered by tinymce.
Introducing the Teensy with HID
The Arduino is a USB serial port - it appears under Ports here, not under Keyboards!
For that, we need a different kind of chip, a chip that is USB native! USB native chips can act as USB serial ports, but they can also act as MIDI devices, keyboards, mice, audio devices, joysticks, etc. Nearly anything! A nice chip that does all this is the ATmega32U4 (the U is for usb!) .
The Teensy 2.0 is basically this chip, a USB connector, button and some other necessary things. It's very tiny (thus the name) and has a fantastic programming interface that is basically the Arduino + a helper, it runs under Mac, Linux or Windows.
Since this tutorial was written, a number of other 32U4 microcontroller boards have been developed including the large Arduino Leonardo and the smaller Adafruit ItsyBitsy 32u4 - 5V 16MHz, Adafruit ItsyBitsy 32u4 - 3V 8MHz, and Adafruit Feather 32u4 Basic Proto, which can do similar things.
Page last edited March 08, 2024
Text editor powered by tinymce.
Assemble the Gamepad
OK we're basically ready to go. The plan is to solder a single Ground wire to the common ground for all the buttons, then solder a seperate wire to each button (the not-ground side). The ground connects to the Teensy ground, the button wires connect to all the solder pads down the side. Then we'll write the code that listens to the button presses and converts them.
I didnt end up using the Red or Brown wires so I tore those off the ribbon. Now cut another piece the same size but with only the white, gray, purple, blue and green wires.
Connect white to B, gray to A, purple to X, blue to Y and green to R1.
OK! Now all the buttons are wired up, its time to attach them to the Teensy. Place the Teensy in a vise or carefully use a 'third hand' to hold it (grab by the USB connector).
First, solder the black wire to the ground pin.
Page last edited March 08, 2024
Text editor powered by tinymce.
Programming the Teensy
The Teensy uses the USB connection for programing, so we don't need a seperate AVR programmer. We will use the Teensyduino IDE, which is a patch to the Arduino IDE.
If you don't have it yet, download & install the Arduino IDE software
Next, download the Teensyduino installer for your OS and run it, patching the Arduino IDE
Finally, be sure to also grab Teensyloader which is a helper that talks to the Teensy for you.
One Button Test
We'll start with the 'one button test' sketch, which will only listen for the 'Up' D-Pad button and output the letter 'u'
Understanding this code now will make it a lot easier to understand the later sketches that are much more complex!
You can also grab this code (which may be updated!) at GitHub
// SPDX-FileCopyrightText: 2019 Limor Fried/ladyada for Adafruit Industries
//
// SPDX-License-Identifier: MIT
const int pinBtnUp = 0;
const int pinLEDOutput = 11;
//Variables for the states of the SNES buttons
boolean boolBtnUp;
void setup()
{
//Setup the pin modes.
pinMode( pinLEDOutput, OUTPUT );
//Special for the Teensy is the INPUT_PULLUP
//It enables a pullup resitor on the pin.
pinMode( pinBtnUp, INPUT_PULLUP );
//Zero the SNES controller button keys:
boolBtnUp = false;
}
void loop()
{
// //debugging the start button...
digitalWrite ( pinLEDOutput, digitalRead(pinBtnUp));
//Progess the SNES controller buttons to send keystrokes.
fcnProcessButtons();
}
//Function to process the buttons from the SNES controller
void fcnProcessButtons()
{
//Assign temporary values for the buttons.
//Remember, the SNES buttons are read as active LOW.
//Capture their status here:
boolean boolBtnUp = !digitalRead(pinBtnUp);
if ( boolBtnUp )
{
//Set key1 to the U key
Keyboard.set_key1( KEY_U );
} else {
Keyboard.set_key1( 0 );
}
//Send all of the set keys.
Keyboard.send_now();
}
All Button Test
Next we can upload the sketch that uses all the buttons so you can test each connection. It is much longer. Download it from GitHub.
// SPDX-FileCopyrightText: 2019 Limor Fried/ladyada for Adafruit Industries
//
// SPDX-License-Identifier: MIT
#define REPEATRATE 100 // milliseconds
const int pinBtnUp = 0;
const int pinBtnRight = 1;
const int pinBtnDown = 2;
const int pinBtnLeft = 3;
const int pinBtnSelect = 4;
const int pinBtnStart = 5;
const int pinBtnB = 7;
const int pinBtnA = 8;
const int pinBtnY = 10;
const int pinBtnX = 9;
const int pinBtnTrigLeft = 6;
const int pinBtnTrigRight = 23;
const int pinLEDOutput = 11;
//Variables for the states of the SNES buttons
byte buttons[] = { pinBtnUp, pinBtnRight, pinBtnDown, pinBtnLeft, pinBtnSelect, pinBtnStart,
pinBtnB, pinBtnA, pinBtnY, pinBtnX, pinBtnTrigLeft, pinBtnTrigRight
};
uint16_t keys[] = {KEY_U, KEY_R, KEY_D, KEY_L, KEY_ENTER, KEY_TAB, KEY_B, KEY_A, KEY_Y, KEY_X, KEY_P, KEY_Q};
void myset_key1(uint16_t c);
void myset_key2(uint16_t c);
void myset_key3(uint16_t c);
void myset_key4(uint16_t c);
void myset_key5(uint16_t c);
void myset_key6(uint16_t c);
#define NUMBUTTONS sizeof(buttons)
typedef void KeyFunction_t(uint16_t c);
KeyFunction_t* buttonActive[NUMBUTTONS];
KeyFunction_t* keyList[] = {myset_key6, myset_key5, myset_key4, myset_key3, myset_key2, myset_key1};
int keySlot = sizeof(keyList) / sizeof(KeyFunction_t*);
void setup()
{
//Setup the pin modes.
pinMode( pinLEDOutput, OUTPUT );
//Special for the Teensy is the INPUT_PULLUP
//It enables a pullup resitor on the pin.
for (byte i=0; i< NUMBUTTONS; i++) {
pinMode(buttons[i], INPUT_PULLUP);
}
//Uncomment this line to debug the acceleromter values:
// Serial.begin();
for (int i=0; i < NUMBUTTONS; i++) {
buttonActive[i] = 0;
}
}
void loop()
{
// //debugging the start button...
digitalWrite ( pinLEDOutput, digitalRead(pinBtnStart));
//Progess the SNES controller buttons to send keystrokes.
fcnProcessButtons();
}
//Function to process the buttons from the SNES controller
void fcnProcessButtons()
{
bool keysPressed = false;
bool keysReleased = false;
// run through all the buttons
for (byte i = 0; i < NUMBUTTONS; i++) {
// are any of them pressed?
if (! digitalRead(buttons[i]))
{ //this button is pressed
keysPressed = true;
if (!buttonActive[i]) //was it pressed before?
activateButton(i); //no - activate the keypress
}
else
{ //this button is not pressed
if (buttonActive[i]) { //was it pressed before?
releaseButton(i); //yes - release the keypress
keysReleased = true;
}
}
}
if (keysPressed || keysReleased)
Keyboard.send_now(); //update all the keypresses
}
void activateButton(byte index)
{
if (keySlot) //any key slots left?
{
keySlot--; //Push the keySlot stack
buttonActive[index] = keyList[keySlot]; //Associate the keySlot function pointer with the button
(*keyList[keySlot])(keys[index]); //Call the key slot function to set the key value
}
}
void releaseButton(byte index)
{
keyList[keySlot] = buttonActive[index]; //retrieve the keySlot function pointer
buttonActive[index] = 0; //mark the button as no longer pressed
(*keyList[keySlot])(0); //release the key slot
keySlot++; //pop the keySlot stack
}
void myset_key1(uint16_t c)
{
Keyboard.set_key1(c);
}
void myset_key2(uint16_t c)
{
Keyboard.set_key2(c);
}
void myset_key3(uint16_t c)
{
Keyboard.set_key3(c);
}
void myset_key4(uint16_t c)
{
Keyboard.set_key4(c);
}
void myset_key5(uint16_t c)
{
Keyboard.set_key5(c);
}
void myset_key6(uint16_t c)
{
Keyboard.set_key6(c);
}
You should test all the buttons, to make sure they all output characters.
This code is more involved since it has to listen to 12 buttons. You can see at the top where we define an array of all the buttons, and then the keys that correspond to the presses. In this case, we're using a simple one-to-one correspondence for keypresses, such as Up being 'u'. To adapt this code to allow for things like "Alt-F3" would be a little more complex.
The code supports up to 6 simultaneous keypresses.
Page last edited March 08, 2024
Text editor powered by tinymce.
Adding the Accelerometer
You should now try out the next sketch, teensySNES_test2.ino which will move the mouse as you tilt the controller.
// SPDX-FileCopyrightText: 2019 Anne Barela for Adafruit Industries
//
// SPDX-License-Identifier: MIT
const int pinAnalogXInput = 3;
const int pinAnalogYInput = 1;
const int pinAnalogZInput = 2;
const int pinAnalogDummyInput = 0;
#define KEYREPEAT 100 // milliseconds
#define KEYDELAY 200 // delay from first to second character
const int pinBtnUp = 0;
const int pinBtnRight = 1;
const int pinBtnDown = 2;
const int pinBtnLeft = 3;
const int pinBtnSelect = 4;
const int pinBtnStart = 5;
const int pinBtnB = 7;
const int pinBtnA = 8;
const int pinBtnY = 10;
const int pinBtnX = 9;
const int pinBtnTrigLeft = 6;
const int pinBtnTrigRight = 23;
const int pinLEDOutput = 11;
//Variables for the states of the SNES buttons
byte buttons[] = { pinBtnUp, pinBtnRight, pinBtnDown, pinBtnLeft, pinBtnSelect, pinBtnStart,
pinBtnB, pinBtnA, pinBtnY, pinBtnX, pinBtnTrigLeft, pinBtnTrigRight
};
short keys[] = {KEY_U, KEY_R, KEY_D, KEY_L, KEY_ENTER, KEY_TAB, KEY_B, KEY_A, KEY_Y, KEY_X, KEY_P, KEY_Q};
#define NUMBUTTONS sizeof(buttons)
typedef void KeyFunction_t(uint8_t c);
KeyFunction_t* buttonActive[NUMBUTTONS];
KeyFunction_t* keyList[] = {myset_key6, myset_key5, myset_key4, myset_key3, myset_key2, myset_key1};
int keySlot = sizeof(keyList) / sizeof(KeyFunction_t*);
//Change these values if accelerometer reading are different:
//How far the accerometer is tilted before
//the Teensy starts moving the mouse:
const int cintMovementThreshold = 18;
//The average zero acceleration values read
//from the accelerometer for each axis:
const int cintZeroXValue = 328;
const int cintZeroYValue = 328;
const int cintZeroZValue = 328;
//The maximum (positive) acceleration values read
//from the accelerometer for each axis:
const int cintMaxXValue = 396;
const int cintMaxYValue = 396;
const int cintMaxZValue = 396;
//The minimum (negative) acceleration values read
//from the accelerometer for each axis:
const int cintMinXValue = 256;
const int cintMinYValue = 256;
const int cintMinZValue = 256;
//The sign of the mouse movement relative to the acceleration.
//If your cursor is going in the opposite direction you think it
//should go, change the sign for the appropriate axis.
const int cintXSign = 1;
const int cintYSign = -1;
const int cintZSign = 1;
//const float cfloatMovementMultiplier = 1;
//The maximum speed in each axis (x and y)
//that the cursor should move. Set this to a higher or lower
//number if the cursor does not move fast enough or is too fast.
const int cintMaxMouseMovement = 10;
//This reduces the 'twitchiness' of the cursor by calling
//a delay function at the end of the main loop.
//There is a better way to do this without delaying the whole
//microcontroller, but that is left for another time or person.
const int cintMouseDelay = 8;
void setup()
{
//This is not needed and set to default but can be useful if you
//want to get the full range out of the analog channels when
//reading from the 3.3V ADXL335.
//If the analog reference is used, the thresholds, zeroes,
//maxima and minima will need to be re-evaluated.
analogReference( DEFAULT );
//Setup the pin modes.
pinMode( pinLEDOutput, OUTPUT );
//Special for the Teensy is the INPUT_PULLUP
//It enables a pullup resitor on the pin.
for (byte i=0; i< NUMBUTTONS; i++) {
pinMode(buttons[i], INPUT_PULLUP);
}
//Uncomment this line to debug the acceleromter values:
// Serial.begin();
}
void loop()
{
// //debugging the start button...
digitalWrite ( pinLEDOutput, digitalRead(pinBtnStart));
//Process the accelerometer to make the cursor move.
//Comment this line to debug the accelerometer values:
fcnProcessAccelerometer();
//Progess the SNES controller buttons to send keystrokes.
fcnProcessButtons();
//Delay to avoid 'twitchiness' and bouncing inputs
//due to too fast of sampling.
//As said above, there is a better way to do this
//than delay the whole MCU.
delay(cintMouseDelay);
}
//Function to process the acclerometer data
//and send mouse movement information to the host computer.
void fcnProcessAccelerometer()
{
//Initialize values for the mouse cursor movement.
int intMouseXMovement = 0;
int intMouseYMovement = 0;
//Read the dummy analog channel
//This must be done first because the X analog channel was first
//and was unstable, it dropped or pegged periodically regardless
//of pin or source.
analogRead( pinAnalogDummyInput );
//Read accelerometer readings
int intAnalogXReading = analogRead(pinAnalogXInput);
int intAnalogYReading = analogRead(pinAnalogYInput);
int intAnalogZReading = analogRead(pinAnalogZInput);
//Calculate mouse movement
//If the analog X reading is ouside of the zero threshold...
if( cintMovementThreshold < abs( intAnalogXReading - cintZeroXValue ) )
{
//...calculate X mouse movement based on how far the X acceleration is from its zero value.
intMouseXMovement = cintXSign * ( ( ( (float)( 2 * cintMaxMouseMovement ) / ( cintMaxXValue - cintMinXValue ) ) * ( intAnalogXReading - cintMinXValue ) ) - cintMaxMouseMovement );
//it could use some improvement, like making it trigonometric.
}
else
{
//Within the zero threshold, the cursor does not move in the X.
intMouseXMovement = 0;
}
//If the analog Y reading is ouside of the zero threshold...
if( cintMovementThreshold < abs( intAnalogYReading - cintZeroYValue ) )
{
//...calculate Y mouse movement based on how far the Y acceleration is from its zero value.
intMouseYMovement = cintYSign * ( ( ( (float)( 2 * cintMaxMouseMovement ) / ( cintMaxYValue - cintMinYValue ) ) * ( intAnalogYReading - cintMinYValue ) ) - cintMaxMouseMovement );
//it could use some improvement, like making it trigonometric.
}
else
{
//Within the zero threshold, the cursor does not move in the Y.
intMouseYMovement = 0;
}
Mouse.move(intMouseXMovement, intMouseYMovement);
}
//Function to process the buttons from the SNES controller
void fcnProcessButtons()
{
bool keysPressed = false;
bool keysReleased = false;
// run through all the buttons
for (byte i = 0; i < NUMBUTTONS; i++) {
// are any of them pressed?
if (! digitalRead(buttons[i]))
{ //this button is pressed
keysPressed = true;
if (!buttonActive[i]) //was it pressed before?
activateButton(i); //no - activate the keypress
}
else
{ //this button is not pressed
if (buttonActive[i]) { //was it pressed before?
releaseButton(i); //yes - release the keypress
keysReleased = true;
}
}
}
if (keysPressed || keysReleased)
Keyboard.send_now(); //update all the keypresses
}
void activateButton(byte index)
{
if (keySlot) //any key slots left?
{
keySlot--; //Push the keySlot stack
buttonActive[index] = keyList[keySlot]; //Associate the keySlot function pointer with the button
(*keyList[keySlot])(keys[index]); //Call the key slot function to set the key value
}
}
void releaseButton(byte index)
{
keyList[keySlot] = buttonActive[index]; //retrieve the keySlot function pointer
buttonActive[index] = 0; //mark the button as no longer pressed
(*keyList[keySlot])(0); //release the key slot
keySlot++; //pop the keySlot stack
}
void myset_key1(uint8_t c)
{
Keyboard.set_key1(c);
}
void myset_key2(uint8_t c)
{
Keyboard.set_key2(c);
}
void myset_key3(uint8_t c)
{
Keyboard.set_key3(c);
}
void myset_key4(uint8_t c)
{
Keyboard.set_key4(c);
}
void myset_key5(uint8_t c)
{
Keyboard.set_key5(c);
}
void myset_key6(uint8_t c)
{
Keyboard.set_key6(c);
}
Page last edited March 08, 2024
Text editor powered by tinymce.
Closing it Up
As you close the case, use tweezers to poke the wires around inside so that they do not interfere with the standoffs.
We wanted to make sure we could update the code without going through the disassembly process, so we drilled a hole in the back right over where the button is, then used a paper clip to push the tiny button. You can also just solder two wires to GND and RST and bring these out of the case, when shorted it will start the bootloader.
You're done! Enjoy your new toy, and modify the sketch if you need to change the key commands or mouse movements.
Page last edited March 08, 2024
Text editor powered by tinymce.