I wanted to design my own Raspberry Pi handheld gaming system. I logged most of my gaming hours in the early '90s. So I have a soft spot for games from that era, and I particularly like the Wii Classic controller for its SNES-like layout.
I knew from a previous project that Wii controllers can communicate over an I2C bus such as the one available on a Raspberry Pi. I chose a 5" LCD screen based on being a similar width as a Wii Classic Controller.
When designing the case I realized the dimensions were roughly square. So I decided to make it square and the name came naturally as an homage to the GameCube.
The case is 160 mm x 160 mm x 37 mm. The face has D-pad, select (-), home, start (+), A, B, X, and Y. The back has the L and R buttons. The bottom has a headphone jack and micro B USB jack for charging. The right side has access to two of the Raspberry Pi USB ports which is good for connecting a keyboard. The top has heat vents, a power switch for the external amp/speaker, a volume knob, a halt button, a power switch, and an LED that indicates battery high or low. In initial tests I am getting about two hours of play time running on only the battery while using the external amp/speaker and with the Wi-Fi turned on.
- Wii Classic Controller (only need the main PCB and buttons on face)
- Raspberry Pi 3 with 32GB micro SD card
- Chuanganzhuo 5" LCD screen
- Powerboost 1000C
- PAM8302 mono 2.5W ampliflier
- 8 ohm 0.5W speaker
- 2500 mAh LiPo battery
- 2 green 16mm momentary pushbutton
- 1 red 16mm momentary pushbutton
- panel mount 10 K pot and knob
- 2 panel mount SPDT switches
- panel mount stereo audio extension cable
- panel mount micro B USB extension cable
- ribbon cable for connecting parts
- 4 Aluminum Female Threaded Hex Standoff 6 mm Hex Size, 31 mm Length, M3 Thread Size
- 4 Aluminum Female Threaded Hex Standoff 6 mm Hex Size, 25 mm Length, M3 Thread Size
- 1 pack (only 16 needed) Black-Oxide 18-8 Stainless Steel Phillips Rounded Head Screw M3 x 0.5 mm Thread, 10 mm Long, packs of 50
- 5 mm red LED
- 220 ohm resistor
Step 1: Hardware Assembly
The case slots together from the laser cut parts, and the screws and standoffs hold it together. The four longer (31 mm) standoffs go in the four corners of the case, and the four shorter standoffs (25 mm) go in the back of the case to hold the controller PCB in place. The screws fit snugly in the laser cut holes, so take care to ease them in the first time.
The two switches, three buttons, one potentiometer, headphone jack, and micro USB jack panel mount on the case. The LCD is hot-glued on the face plate. The controller buttons just sit on the PCB and are carefully wiggled into place when the face plate is fastened. Velcro holds the battery, the speaker, and the Raspberry Pi in place. I also used some Lego to hold the Raspberry Pi in place when plugging in a USB device. Hot glue and electrical tape were used as needed to fasten parts and to prevent accidental short circuits.
The plastic outer case can be pried open. The third photo above shows where the composite video (yellow), ground (black), and 5V (blue) wires are to be soldered to the control board for the LCD. (I used different colored wires in the final assembly shown in the first and second photos above.)
Wii Classic Controller:
Open the controller case with a tri-wing screwdriver, and separate the PCB with a phillips screwdriver. Desolder all the connections at J1, J2, J3, J4, ZL, and ZR. Save the main PCB and the buttons and membranes on face of the controller. I snipped off an empty corner of the PCB under J4 to make it fit in the case.
In initial tests the 2500 mAh LiPo battery provides about 2 hours of play time. When the battery goes low, the LED next to the power switch turns off. The next thing that happens is the I2C bus stops working. This is problematic since system halt (without an attached keyboard) relies on buttons connected to the I2C bus. (If I can stop playing video games) I may add functionality to monitor the Powerboost 1000C LBO pin with a Raspberry Pi pin to force a system halt when it drops to ground (indicating low battery).
PAM8302 audio amplifier:
The wiring schematic shows all the relevant parts except the Raspberry Pi 3. The Raspberry Pi pins are simply labeled. This is a good reference for the forty-pin connector. The PP# pins are labeled on the underside of the PCB near the analog audio/video TRRS connector. I made all the Raspberry Pi connections by soldering to pins on the underside.
Step 2: Software
The Raspberry Pi 3 is running the current standard Raspbian image. Here are the relevant contents of my /boot/config.txt file.
I wrote two custom programs: "WiiClassicPi" allows the Wii Classic Controller plus volume knob and halt button to control the Raspberry Pi, and "emu.py" is a bare-bones file browser for launching games.
WiiClassicPi code and configuration:
Set up the uinput device by adding a line for "uinput" in /etc/modules. Make it accessible for all users by adding the following line to /etc/udev/rules.d/99-com.rules:
After a reboot, confirm that it shows up with read/write privileges for all.
pi@raspberrypi:~ $ ls -l /dev/uinput
crw-rw-rw- 1 root root 10, 223 Feb 2 13:36 /dev/uinput
The delay times after I2C transactions can be customized at the top of my code. The delay times provide enough time for I2C transactions to occur. They probably can be less, but the values I've chosen seem to work okay. According to wikipedia, input lag becomes distracting around 200 ms, so keep the delay times much less than that.
The valid key codes to be listed in the keys array can be found in /usr/include/linux/input.h. The key codes should be listed in the same order as the commented list of Wii Classic buttons.
Notice that the ZL button is mapped along with the select (-) button to perform a system halt, and the LX analog joystick is mapped to the 10 K potentiometer and used to adjust the volume.
Also, line 147 of the code contains a workaround for a glitch. When the Raspberry Pi is running something CPU intensive (like an emulator) the I2C read sometimes results in garbage (0xFF)) in the 2nd and 3rd of the 6 bytes. So the code throws away those reads and moves on. This might cause some noticeable controller latency if too many reads in a row produce garbage, but I haven't noticed any problems.
Compile using: gcc -lwiringPi -lsuinput WiiClassicPi.c -oWiiClassicPi
Run as "./WiiClassicPi verbose" to see controller data.
emu.py code and configuration:
I initially considered using RetroPie, however it would not recognize the virtual keyboard presses from /dev/uinput. I also find it way too complex for such a simple thing as launching an emulator, especially on a small screen.
So I wrote emu.py, a Python 3 barebone curses-based frontend for launching emulators. All the customization happens at the top of the code. The keys are mapped to work with WiiClassicPi (except for the exit key (F1) to be used with an attached keyboard), and the extensions, directories, and launch commands have corresponding entries for each system.
# GUI keys
exit_key = curses.KEY_F1
up_key = curses.KEY_UP
down_key = curses.KEY_DOWN
left_key = curses.KEY_LEFT # back 1 page
right_key = curses.KEY_RIGHT # forward 1 page
launch_key = ord("j")
a_key = ord("x") # back 5 pages
b_key = ord("z") # forward 5 pages
# rom file extensions
file_ext = [".gb", ".gbc", ".nes", ".smc"]
# rom directories
romdir = ["/home/pi/roms/gb", "/home/pi/roms/gbc", "/home/pi/roms/nes", "/home/pi/roms/snes"]
# emulator launch commands
emulator = ["/home/pi/emu/./gambatte_sdl -i j g x z up down left right -s 8",
"/home/pi/emu/./gambatte_sdl -i j g x z up down left right -s 8",