Arduino Leonardo Custom Joystick

As hinted at in the previous post: I made a joystick. While you can buy dedicated PCBs for custom joysticks (and I would potentially recommend this for most people), I felt like doing mine a bit more manually with an Arduino instead. And here's how it looks!

Joystick Front

To be fair, I didn't exactly start from scratch. The initial USB stack patch came from a post on Imaginary Industries, but I modified it quite a bit since then to have just the USB descriptor I wanted, and to be a bit more intelligent with the struct handling and USB report data packing. I also used this reference for the SNES connection scheme.

The Hardware

The joystick hardware is not particularly complicated. Here's a picture of the inside of the stick:
Joystick Inside
The main parts I used are:

Each button and joystick input is tied to one end of each screw terminal, then connected to the Arduino via a shorter solid-core wire that is less prone to wear. The button wires are stranded 22 guage wire, and the connects are quite secure once attached to the screw terminals. As per standard convention, all buttons inputs are configured with pull-ups (using Arduino internal pull up mode), with the other side of the button connected to ground. When the button is not pressed, the Arduino will read high; when it is pressed, it will be pulled down to ground. Since ground is common among all buttons, the wire is simply daisy-chained to each from a single screw terminal.

The Joystick has a built-in PCB with a five-wire connector. It already connects the common ground for each switch on the joystick to the black wire, then provides a signal wire for each direction. These are connected to the Arduino in the same manner as the buttons. Unfortunately, the joystick I picked came with a strange offset mounting plate, which wasn't ideal for my application. I inserted two small pieces of wood to offset the joystick slightly so I could fully tighten the screws without bowing the plastic.

The rear of the joystick has two ports: USB and DA15:
Joystick Rear

My original plan was to support both SNES and Genesis through the DA15 connector, but I ran into problems with Genesis support (e.g. powering the Arduino stopped the genesis from pulsing the select line), so it just supports the 5 pins for the SNES. If anyone wants to just make a USB/SNES controller, I recommend getting a 5-pin round connector instead, which would be much easier to drill a hole for.

The breadboard is a remnant of attempting to support the Genesis and is really just providing a relay for the pins that don't fit directly into the Arduino. And the pins are only needed because I used stranded wire for my DA15 pins. For simple SNES support, I recommend instead just using solid core wire and running it direct to the Arduino.

Software

Here is the download for my Arduino project (99 kB). It also includes some Oscilloscope traces when testing the design, as well as two modified USB stack files for the Arduino libraries. The two files are HID.cpp and USBAPI.h, and need to be merged into arduino-[version]/hardware/arduino/avr/cores/. I do not recommend blindly copying the files into this folder; instead you should use a comparison utility to merge in the appropriate changes. This is especially important if these files are modified in future Arduino library versions. You should, of course, back up the existing files as well.

Note that this approach will also work with the Arduino Due, but you won't be able to support the SNES without a line-level converter. The Due's USB stack can be found in arduino-[version]/hardware/arduino/sam/cores/arduino/USB/. If you are making a joystick with the Due, take extra care doing the comparison, as there are other differences between the Leonardo and Due libraries that you don't want to change. The joystick code is fairly isolated, but let me know if you are trying this and have problems.

As far as what the code does, it's somewhat braindead. It checks the discrete state of every button and input, then changes the appropriate bit or X/Y value in the USB data. Then it sends the USB report to the PC and repeats.

The SNES code is only slightly more complicated. It stores the button states in the main loop, then uses an interrupt on the SNES latch line. When the SNES triggers that signal, it stores the button/direction values into a fake 'shift register' and shifts the first bit. On each subsequent clock pulse (also an interrupt), it shifts the next bit. This is more or less the same approach as mentioned by Kalshagar on his SNES cabinet post.

It may also be possible to use either SPI or I2C to have the microprocessor handle the clocking automatically. However, you will still require an ISR for the initial value, and you will also need to handle the case where the buffer empties, since the Leonardo only has an 8-bit communication buffer.

Finally, this should also mostly support NES games if you wire up an SNES to NES adaptor. However, NES support isn't perfect; see below.

Limitations

As per my previous post, the Super Gameboy won't work. Also, there are some minor problems with a few NES games. Instead of using the Y and B buttons as per the typical data sequence, it actually responds to the Y and A buttons. After scoping this, the problem is that some NES games appear to start clocking the data very soon after the latch command. This means the Arduino doesn't have enough time to get the first data value ready (SNES B button/NES A button) until it is already too late! However, we are partially saved in this case, because the previous output of the SNES A button is actually the 9th bit in the previous message, and ends up being the value on the data line between data sequences. If the NES samples the data line before the Arduino is ready, this is the value it reads.

Here's how it looks on a scope. First, a first-party Nintendo game without this problem. Latch and Clock lines:
first party nintendo game trace with latch and clock lines

B button:
scope trace with B button pressed

Next is a a problem game. Here is the latch/clock sequence (note the scale is different to show all pulses; the clock pulses are a bit further apart)
scope trace of latch/clock sequence for problem game

And here's the the B button pressed. Note it misses all the clock pulses.
scope trace with B button pressed for problem game

And the A button for reference:
scope trace with A button pressed

There's no real easy solution to this, however. Maybe there's a way to edit the Arduino libraries to reduce the ISR preamble. However, this is a rather dangerous approach, as it could result in data loss for the background task, and unexpected behaviours.

NOTE: The Teensy-based XInput stick I made much later has a faster CPU, so it does not have this problem. In fact, it does not even need the hack of shifting on the wrong clock pulse.

Comments

Add a Comment
Comment Atom Feed