Myself and many others agree that Nintendo's GameCube controller is one of the best controllers made. Its asymmetric stick layout and major/minor face buttons make it perfect for playing platform fighters and retro games. In fact, Nintendo seems to agree and just restarted production of Smash 4 branded GameCube controllers.
Unfortunately, using a GameCube controller on your PC requires a fairly large adapter. Official and unofficial ones exist for around $15, but Nintendo's adapter requires some driver hacking before it works on a desktop. You can also buy GameCube "style" USB controllers from third party manufacturers, but those tend to be pretty bad quality.
Since I play a lot of PC games with the GCN controller, I decided to convert one of my controllers to USB. Instead of its normal connector, it just has a USB cord that registers itself as a HID joystick. This means that you can plug it into any modern computer as it just works.
This mod is also a significant milestone towards my final vision of the Shinewave controller mod. In fact, you can think of it as the second Shinewave prototype(where this is the first). My goal was to demonstrate the following features:
- Act as a native USB joystick
- Can be programmed without through a USB bootloader without needing to disassemble the controller
- Is built on a PCB with surface-mount components
This is a completely internal mod for the Nintendo GameCube controller that replaces the stock GameCube connection cord with a USB cord. The rumble motor has been removed to make room for a custom designed circuit board and microcontroller.
The ATtiny84 microcontroller is the brain of the mod. The board that houses it has three connection ports: For talking to the host computer over USB, for impersonating a GameCube console and talking to the controller to get its state, and for a strand of individually addressable full-color LEDs.
All of the components used have been selected to keep the final cost as low as possible. At single-quantity units, you could expect to spend about $6 total building this mod, assuming you already have a controller and the tools needed.
The ATtiny84 used has only 8kB of program memory and .5kB of RAM. Within this space, I had to fit the following:
- A custom build of the micronucleus bootloader
- The V-USB library for USB communication
- A custom-written script to talk to the GameCube controller
- A lightweight Neopixel library written by bigjosh2
All of the code is hosted on GitHub in C and C++ and compiled with the avr-gcc toolchain on a Linux machine.
V-USB is a software-only implementation of a low-speed USB device for Atmel’s AVR® microcontrollers, making it possible to build USB hardware with almost any AVR® microcontroller, not requiring any additional chip.
In order to implement the USB protocol, most circuits use either external hardware(such as an FTDI) or rely on hardware features. With both of these solutions, USB communication gets off-loaded asynchronously to some purpose-built silicon which allows the main processor to continue uninterrupted.
There are also "bit-banged" solutions where the USB protocol is implemented entirely in software. With this kind of setup, the microcontroller needs to be available during USB data requests. Your entire program needs to be designed to be interrupted several times a second in order to handle the USB connection.
V-USB is one of these bit-banged solutions, built for many of Atmel's AVR-series microcontrollers, including the ATTiny84 that I use. It takes up a bit over 1KB of the available program space and due to limitations of the chip, only targets support for USB 1.1(low speed). However, it is very flexible and can be used to implement both HID and non-native devices.
In order to act as an HID joystick, I needed to write a USB Device Descriptor. This piece of information tells the host computer what kind of device is being connected, what its capabilities are and how to talk to it.
Once I got the library working, communicating over USB is as simple as filling a data buffer and waiting for the USB request to complete.
The goal of a bootloader is to allow the device to be reprogrammed through a higher level protocol besides the normal ISP pins. For example micronucleus, the bootloader I'm using, allows for reprogramming over USB. It's a small(<2kb) piece of code that runs on boot and checks for a certain command. If a specially written program isn't waiting on the host computer, it boots normally after a few seconds.
It still takes a special piece of hardware to install the bootloader on each chip, but afterwards all you need is a USB cord!
In order to make the bootloader compatible with my circuit design, I had to modify the default configuration to take into account the pins I used, my clock speed, and lower the waiting period before booting into the user program. In the future, I may modify it further to boot into the bootloader only if USB is connected, or if a key combo is held down on the controller.
There are really two different ways to find out what buttons are pressed: Either solder onto each of the button contacts and potentiometer pins and poll them, or just ask the controller! Although the first option is possible it'd require using a larger microprocessor with more pins or adding a shift register to the design, and would take a lot more work to assemble. Meanwhile, the latter only takes one pin, but is much harder to implement in software. I ended up going with the second option.
The GameCube and its controllers have a unique one-wire interface that they use to communicate. While idle, the line is pulled high to 3.3V by both the console and the controller. Every frame(or whenever the console feels like it), the console sends a message to the controller asking for its current status. The controller then responds immediately with an eight byte message containing the values of all the digital buttons in two bytes, and the six analog values in each of the remaining bytes. Each bit is sent out consecutively in a four microsecond wide window. A digital '1' is specified by 3µs high followed by 1µs low, while a digital '0' is 1us high and 3µs low.
After studying the format of this protocol, I had to write a program to impersonate the GCN console and talk to the controller. Since the request message never changes, it's pretty easy to hard-code the message with the proper timings and bit-bang it out to the controller. However, reading back the response is a bit more difficult!
In order to reduce costs, I'm eliminating the need for a 5V<->3.3V level-shifter by running the ATtiny84 at 3.3V instead of 5V. This makes the hardware for talking over USB and to the controller a bit simpler. The chip has to run at a lower clockspeed of 12MHz in order to compensate.
Since new bits come in from the controller every 4µs I get 48 clock cycles to store each incoming bit. Because only the middle 2µs of each bit actually contains the bit's state, I need to wait for half the signal before reading. As a result, I only actually get about 24 clock cycles per bit. This could be pretty easily solved by writing the code in Assembly, but the code would be less portable and I'd need to count clock cycles per instruction. I was also pretty stubborn at finding a C only solution.
When I was running at 16MHz and had plenty of clock cycles, the following plan worked just fine:
- Wait for the line to go high, signaling the start of a bit
- Stall the microprocessor for 2µs, in order to hit the critical part of the signal
- Read the value of the line and store it on the end of a byte-wide buffer
- Check if the buffer was full and if so, push it on top of the stack
The only way I could ever get it to work at 3/4ths the speed was by cutting out step 2 and finding another method of waiting asynchronously. After a lot of research and datasheet reading, I found that I could combine two on-board features, the Universal Serial Interface(ch14) and one of the Timer modules(ch11) to accomplish this. My new plan turned into the following:
- Wait for the line to go high, signaling the start of a bit
- Kick off Timer0. After 2µs have passed, it will trigger a compare-match, causing a bit to be stored in the USI buffer
- Immediately check if the USI buffer is full and if so, push it on top of the stack
There's a lot more to talk about here, but I'll cut it short. If there's enough interest, I'll be glad to write another post explaining exactly how this approach works!
My earlier designs were simple enough that I could get away with simple point-to-point wiring. Unfortunately, this meant that I had to take a few shortcuts that made the circuit a little less stable. I didn't have any current limiting resistors on either of the data lines, or a smoothing capacitor on the power supply. I also used the chip's Analog Comparator so that I could run at 5V without needing another chip to level shift. However, I couldn't ever transmit 3.3V to the controller and could only read from it.
My new design includes a power and data transistor, external clock, USB communications and proper safety resistors. There's no way that I could get all of those to fit inside of a controller with point-to-point wiring! So, I made the decision to formalize my circuit and build it on a printed circuit board(PCB).
I built a prototype circuit on a breadboard while getting the software and design ironed out. This working period ended up being really helpful.
After I had a good design ironed out, I built it digitally using EAGLE and laid it out in 2D space. I actually really enjoyed this last step! Moving components around and minimizing routes is a really great optimization problem that I wish I could do more of.
My boards ended up being 1.06x0.81 inch (27.00x20.65 mm) big, or just under a square inch! The smallest components are the resistors and capacitors, at 1.6mm by 0.8mm, or 0.6in by 0.3in, so I'd hesitate to recommend this project to anyone who's never soldered before.
After I got the design finalized, it was time to have it printed. I chose to send it off to OSH Park, although there are a lot of other similar services. The turnaround was about a week and a half, and the boards worked perfectly!
Actually assembling the boards also went smoothly! I cut the end off of a USB cable and used a continuity tester to identify each wire before soldering it directly onto the board. After removing the cord connector from the board, I soldered four wires for GND, 3.3V, 5V, and SIG to the exposed vias. Likewise, I needed to connect my LED strand to the vias on my own board.
The board rests in the controller where the rumble motor usually sits. Because I prefer my controller to have the motor removed, it's a convenient location for my own electronics. If I wanted to keep rumble, I could place the electronics in one of the handles instead.
Issues and Improvements
I've been working towards this result for the last few months and ran into more issues than I'd like to admit. I spent a week working with different V-USB configurations before I thought to plug my circuit into a different computer and discovered that I had faulty USB ports. I accidentally ordered the ATtiny841 instead of the ATtiny84 and got all the way through assembly and didn't realize until my programmer complained. Thankfully I had a stack of the right part laying around from a previous order!
I also got micro and nano-seconds mixed up in my math and ended up waiting for 0 clock cycles instead of around 20. The interactions between the Timer0 and USI model weren't well-defined in the datasheet, so I needed to do some experimentation to determine the actual behavior. Some lazy coding ended up causing an infinite loop and USB disconnects weeks after the fact.
During the prototyping phase, I realized that the chip was failing to read from the controller every other transmission. Something in the chip didn't like that the transmission had an odd number of bits and was causing it to fail. I fixed it by forcing another read at the end of each transmission to reset the chip into an even state.
I also need to change how my board handles the USB, GCN, and LED connections. Permanent soldering into vias is not that great of a solution, so I plan on adding polarized connectors and a microUSB port. I also need to add a second GCN signal line for my next project. ;D
Finally, it may be possible for me to get this device to emulate the official Wii U GameCube controller adapter well enough that this mod would be usable in Smash 4.
Do it yourself!
I've done my best to document as much of this project as possible so that it'll be possible for others to follow in my footsteps. The firmware, schematics, and bootloader(through micronucleus) are all open source and I'd be glad to answer any questions in the comments below or by email.
The entire bill of materials is about $6, plus about $3 for three PCBs minimum, and ~$20 for a tool to do the initial programming. If there's enough interest(comment below!), I may consider putting together and selling preprogrammed kits or even assembled boards. I may also write another post or video just of the assembly process.