PIC18F4550 based arcade game controller for Raspberry PI running C64 emulator "VICE"
In my previous blog "Digital SM Part III: I2C PIC18F4550 Slave and Raspberry Pi master" I wrote about setting up I2C communication between the PIC18F4550 and the Raspberry Pi. In this blog I will describe how I created an arcade controller based on this concept.

The physical layout


First I had to figure out what is a good size for the controller board taking into account that most of the standard boards have a width that is a multiplicity of 5. The result is a board size of 60 cm x 30 cm.
Arcade controller
Arcade controller

For the buttons and joysticks, I used two separate layouts (left and right) so I could position them separately in order to determine the right distance between player 1's section and player 2's section. After that I had to determine the right distance betweeen the players' sections and the "one player" and "two players" buttons.
Button layout left side (player 1)
Button layout left side (player 1)
Button layout right side (player 2)
Button layout right side (player 2)

The controller requirements


For the arcade controller I wanted to use:
  • a Raspberry Pi model B
  • PIC18F4550
  • 2 arcade joysticks
  • 2 x red, 2 x green, 2 x blue, 2 x yellow, 1 x "one player" and 1 x "two player" arcade

I wanted to use every available digital input of the PIC18F4550 except for:
  • RA6: used by xtal
  • RB0 and RB1 used for I2C communication
This means there are 31 digital input ports left. 26 inputs are used by the two arcade joysticks and the arcade buttons. So that means there are 5 unused digital inputs left. No problem, I added 5 mini push button switches to the right side of the controller (blue/red/yellow/white/green). The mini buttons are used for special functions
  • blue: shutdown (halt)
  • red: reboot
  • yellow/white/green: key layout selection bits b2/b1/b0
Up to 8 key layout sets can be defined. I currently use only 5. (At the moment these sets are hard-coded)
Arcade controller side mini push button switches
Arcade controller side mini push button switches

Key layouts


Key layout set 0 is the default. It has some duplicates with other layouts, but this layout contains the keys I use a lot with playing games in VICE.
  • [F1] and [F3] are often used for selecting single player or two players, but there are also games that use [1] or [2].
  • Many games have 'high-score' ([h]) and 'trainer' ([t]) mode.
  • Also a lot of games ask "yes/no" or "ja/nee" questions for 'unlimited ammo/lives/etc'. [y]/[j] and [n]
  • Some games start by pressing space [ ] or return [enter]
  • [F12] is used by VICE to show the menu.

key layout set 0 (default, no key layout selector buttons pressed)
key layout set 0 (default, no key layout selector buttons pressed)

Key layout set 1 is used for lower case [a-z]. Mostly results in upper case in C64.
key layout set 1 (keep green button pressed while pressing buttons or moving joysticks)
key layout set 1 (keep green button pressed while pressing buttons or moving joysticks)

Key layout set 2 is used for upper case [A-Z].
key layout set 2 (keep white button pressed while pressing buttons or moving joysticks)
key layout set 2 (keep white button pressed while pressing buttons or moving joysticks)

Key layout set 3 is used for special characters. Like < and >
key layout set 3 (keep green and white button pressed while pressing buttons or moving joysticks)
key layout set 3 (keep green and white button pressed while pressing buttons or moving joysticks)

Key layout set 4 is used for [0-9] and the <shift> + [0-9] and some other special characters like the plus sign.
key layout set 4 (keep yellow button pressed while pressing buttons or moving joysticks)
key layout set 4 (keep yellow button pressed while pressing buttons or moving joysticks)

The circuit schema and PCB


Below you can see the schema of the PIC18F4550 arcade controller board. It's quite simple: I use a 26-pin 2.54 mm (100 mil) header and I compatible ribbon cable to connect the Rpi with the board. The Raspberry Pi powers the board and the communication between the RPi and the PIC is via I2C. As a result only 5 pins of the header are connected to the circuit. In order to deal with the voltage difference on the I/O ports of the Rpi and the PIC, I use two level shifters for the 2 I2C lines. Besides that, there's the clock circuit for the PIC (crystal) and all other inputs are connected to pull up resistors and to a header pin which is connected to a pull down switch.
Schema for PIC18F4550 controller board
Schema for PIC18F4550 controller board

The PCB for the arcade controller
PCB for arcade controller
PCB for arcade controller

Software running on the Raspberry Pi


On the Raspberry Pi I run two programs at start-up:

The major changes of the i2c program are: the I2C master interprets the I2C data from the PIC and acts as the virtual keyboard. It also handles the key set layouts selection and the shutdown (halt) and reboot commands.

Below you can see the key set definition. The key set exists of self explanatory members for each button.

#ifndef _FSAYS_ARCADE_KEYSET_H #define _FSAYS_ARCADE_KEYSET_H #include <fsaysremotekey.h> class FSaysArcadeKeySet { public: // constructors FSaysArcadeKeySet(); ~FSaysArcadeKeySet(); FSaysRemoteKey* getRemoteKeyForPlayerOneGreenTopButton(); FSaysRemoteKey* getRemoteKeyForPlayerOneGreenBottomButton(); FSaysRemoteKey* getRemoteKeyForPlayerOneBlueTopButton(); FSaysRemoteKey* getRemoteKeyForPlayerOneBlueBottomButton(); FSaysRemoteKey* getRemoteKeyForPlayerOneYellowTopButton(); FSaysRemoteKey* getRemoteKeyForPlayerOneYellowBottomButton(); FSaysRemoteKey* getRemoteKeyForPlayerOneRedTopButton(); FSaysRemoteKey* getRemoteKeyForPlayerOneRedBottomButton(); FSaysRemoteKey* getRemoteKeyForPlayerOneJoyStickWest(); FSaysRemoteKey* getRemoteKeyForPlayerOneJoyStickNorth(); FSaysRemoteKey* getRemoteKeyForPlayerOneJoyStickEast(); FSaysRemoteKey* getRemoteKeyForPlayerOneJoyStickSouth(); FSaysRemoteKey* getRemoteKeyForOnePlayerButton(); FSaysRemoteKey* getRemoteKeyForTwoPlayersButton(); FSaysRemoteKey* getRemoteKeyForPlayerTwoJoyStickWest(); FSaysRemoteKey* getRemoteKeyForPlayerTwoJoyStickNorth(); FSaysRemoteKey* getRemoteKeyForPlayerTwoJoyStickEast(); FSaysRemoteKey* getRemoteKeyForPlayerTwoJoyStickSouth(); FSaysRemoteKey* getRemoteKeyForPlayerTwoRedTopButton(); FSaysRemoteKey* getRemoteKeyForPlayerTwoRedBottomButton(); FSaysRemoteKey* getRemoteKeyForPlayerTwoYellowTopButton(); FSaysRemoteKey* getRemoteKeyForPlayerTwoYellowBottomButton(); FSaysRemoteKey* getRemoteKeyForPlayerTwoBlueTopButton(); FSaysRemoteKey* getRemoteKeyForPlayerTwoBlueBottomButton(); FSaysRemoteKey* getRemoteKeyForPlayerTwoGreenTopButton(); FSaysRemoteKey* getRemoteKeyForPlayerTwoGreenBottomButton(); void setRemoteKeyForPlayerOneGreenTopButton(FSaysRemoteKey* remoteKey); void setRemoteKeyForPlayerOneGreenBottomButton(FSaysRemoteKey* remoteKey); void setRemoteKeyForPlayerOneBlueTopButton(FSaysRemoteKey* remoteKey); void setRemoteKeyForPlayerOneBlueBottomButton(FSaysRemoteKey* remoteKey); void setRemoteKeyForPlayerOneYellowTopButton(FSaysRemoteKey* remoteKey); void setRemoteKeyForPlayerOneYellowBottomButton(FSaysRemoteKey* remoteKey); void setRemoteKeyForPlayerOneRedTopButton(FSaysRemoteKey* remoteKey); void setRemoteKeyForPlayerOneRedBottomButton(FSaysRemoteKey* remoteKey); void setRemoteKeyForPlayerOneJoyStickWest(FSaysRemoteKey* remoteKey); void setRemoteKeyForPlayerOneJoyStickNorth(FSaysRemoteKey* remoteKey); void setRemoteKeyForPlayerOneJoyStickEast(FSaysRemoteKey* remoteKey); void setRemoteKeyForPlayerOneJoyStickSouth(FSaysRemoteKey* remoteKey); void setRemoteKeyForOnePlayerButton(FSaysRemoteKey* remoteKey); void setRemoteKeyForTwoPlayersButton(FSaysRemoteKey* remoteKey); void setRemoteKeyForPlayerTwoJoyStickWest(FSaysRemoteKey* remoteKey); void setRemoteKeyForPlayerTwoJoyStickNorth(FSaysRemoteKey* remoteKey); void setRemoteKeyForPlayerTwoJoyStickEast(FSaysRemoteKey* remoteKey); void setRemoteKeyForPlayerTwoJoyStickSouth(FSaysRemoteKey* remoteKey); void setRemoteKeyForPlayerTwoRedTopButton(FSaysRemoteKey* remoteKey); void setRemoteKeyForPlayerTwoRedBottomButton(FSaysRemoteKey* remoteKey); void setRemoteKeyForPlayerTwoYellowTopButton(FSaysRemoteKey* remoteKey); void setRemoteKeyForPlayerTwoYellowBottomButton(FSaysRemoteKey* remoteKey); void setRemoteKeyForPlayerTwoBlueTopButton(FSaysRemoteKey* remoteKey); void setRemoteKeyForPlayerTwoBlueBottomButton(FSaysRemoteKey* remoteKey); void setRemoteKeyForPlayerTwoGreenTopButton(FSaysRemoteKey* remoteKey); void setRemoteKeyForPlayerTwoGreenBottomButton(FSaysRemoteKey* remoteKey); private: FSaysRemoteKey* _remoteKeyForPlayerOneGreenTopButton; FSaysRemoteKey* _remoteKeyForPlayerOneGreenBottomButton; FSaysRemoteKey* _remoteKeyForPlayerOneBlueTopButton; FSaysRemoteKey* _remoteKeyForPlayerOneBlueBottomButton; FSaysRemoteKey* _remoteKeyForPlayerOneYellowTopButton; FSaysRemoteKey* _remoteKeyForPlayerOneYellowBottomButton; FSaysRemoteKey* _remoteKeyForPlayerOneRedTopButton; FSaysRemoteKey* _remoteKeyForPlayerOneRedBottomButton; FSaysRemoteKey* _remoteKeyForPlayerOneJoyStickWest; FSaysRemoteKey* _remoteKeyForPlayerOneJoyStickNorth; FSaysRemoteKey* _remoteKeyForPlayerOneJoyStickEast; FSaysRemoteKey* _remoteKeyForPlayerOneJoyStickSouth; FSaysRemoteKey* _remoteKeyForOnePlayerButton; FSaysRemoteKey* _remoteKeyForTwoPlayersButton; FSaysRemoteKey* _remoteKeyForPlayerTwoJoyStickWest; FSaysRemoteKey* _remoteKeyForPlayerTwoJoyStickNorth; FSaysRemoteKey* _remoteKeyForPlayerTwoJoyStickEast; FSaysRemoteKey* _remoteKeyForPlayerTwoJoyStickSouth; FSaysRemoteKey* _remoteKeyForPlayerTwoRedTopButton; FSaysRemoteKey* _remoteKeyForPlayerTwoRedBottomButton; FSaysRemoteKey* _remoteKeyForPlayerTwoYellowTopButton; FSaysRemoteKey* _remoteKeyForPlayerTwoYellowBottomButton; FSaysRemoteKey* _remoteKeyForPlayerTwoBlueTopButton; FSaysRemoteKey* _remoteKeyForPlayerTwoBlueBottomButton; FSaysRemoteKey* _remoteKeyForPlayerTwoGreenTopButton; FSaysRemoteKey* _remoteKeyForPlayerTwoGreenBottomButton; }; #endif

The arcadecontroller program should be split up, it has too many responsibilities at the moment. It has counters for determining how long the halt/reboot buttons have been pressed. After about 5 seconds the actual halt/reboot is executed. If you release the halt/reboot button before that, the halt/reboot counters will be reset and the action will not be executed.

#ifndef _FSAYS_ARCADE_CONTROLLER_H #define _FSAYS_ARCADE_CONTROLLER_H #include <fsaysarcadekeyset.h> #include <linux/input.h> #define LOCAL_ESCAPE 27 #define MY_SEC 1000000 #define MY_TSEC 100000 #define MY_FHSEC 50000 #define MY_HSEC 10000 #define MY_FMSEC 5000 #define MY_MSEC 1000 #define MY_USEC 1 const int NumberOfArcadeKeySets = 8; class FSaysArcadeController { public: // constructors FSaysArcadeController(); ~FSaysArcadeController(); int run(); private: void startKey(FSaysRemoteKey* remoteKey); void endKey(FSaysRemoteKey* remoteKey); void processPortA(int port, int value); void processPortB(int port, int value); void processPortC(int port, int value); void processPortD(int port, int value); void processPortE(int port, int value); void halt(); void reboot(); FSaysArcadeKeySet* _arcadeKeySets[NumberOfArcadeKeySets]; FSaysArcadeKeySet* _p_currentArcadeKeySet; struct input_event _event; struct input_event _event_end; int _fd; int _haltCounter; int _rebootCounter; bool _doHalt; bool _doReboot; int _portA; int _portB; int _portC; int _portD; int _portE; }; #endif

The halt and reboot commands:

void FSaysArcadeController::halt() { _keepRunning = false; printf("halting system...\n"); setreuid(geteuid(), getuid()); system("sudo /sbin/halt"); setreuid(geteuid(), getuid()); } void FSaysArcadeController::reboot() { _keepRunning = false; printf("rebooting system...\n"); setreuid(geteuid(), getuid()); system("sudo /sbin/reboot"); setreuid(geteuid(), getuid()); } void FSaysArcadeController::processPortD(int port, int value) { if(port != value && _p_currentArcadeKeySet) { // bit 7: blue side button if((port ^ value) & 128) { if(value & 128) { _doHalt = 0; printf("Cancelling halt: %d\n", _haltCounter); _haltCounter = 0; } else { _doHalt = 1; } } // bit 6: red side button if((port ^ value) & 64) { if(value & 64) { _doReboot = 0; printf("Cancelling reboot: %d\n", _rebootCounter); _rebootCounter = 0; } else { _doReboot = 1; } } ...

Start up script


On the RPi I added a start up script for starting both the uinput and the i2c program: /etc/init.d/fsaysarcadecontroller
I also added symbolic links to this script for run levels 2-5.

#! /bin/sh ### BEGIN INIT INFO # Provides: fsaysuinput and fsaysI2C # Required-Start: $sudo # Required-Stop: # X-Start-Before: not applicable # Default-Start: 2 3 4 5 # Default-Stop: # Short-Description: Provides a uinput device for pi's without a keyboard # Description: Provide a uinput device for receiving keyboard input without having a physical keyboard connected to the pi ### END INIT INFO N=/etc/init.d/fsaysuidev set -e case "$1" in start) # make sure privileges don't persist across reboots echo "FSays uinput is starting in background..." /home/pi/fsaysuinput& /bin/sleep 2 echo "FSays I2C is starting in background..." su pi -c /home/pi/fsaysI2C& ;; stop|reload|restart|force-reload|status) ;; *) echo "Usage: $N {start|stop|restart|force-reload|status}" >&2 exit 1 ;; esac exit 0

ln -s /etc/init.d/fsaysarcadecontroller /etc/rc2.d/S02fsaysarcadecontroller ln -s /etc/init.d/fsaysarcadecontroller /etc/rc3.d/S02fsaysarcadecontroller ln -s /etc/init.d/fsaysarcadecontroller /etc/rc4.d/S02fsaysarcadecontroller ln -s /etc/init.d/fsaysarcadecontroller /etc/rc5.d/S02fsaysarcadecontroller update-rc.d fsaysarcadecontroller enable 2 3 4 5

The I2C program on the Rpi must have the 'set root flag' to true and the owner must be root as well, otherwise the halt and shutdown commands won't work. It will try to get root access before executing these commands. On my machine the programs are located in the /home/pi/fsaysPI folder.


cd /home/pi/fsaysPI sudo chmod 4755 ./fsaysI2C chown root fsaysI2C sudo chown root fsaysI2C

The resulting 'ls -ltr fsays*' should show you the following flags:

-rwxr-xr-x 1 root pi 34186 Jul 28 22:19 /home/pi/fsaysPI/fsaysI2C -rwxr-xr-x 1 pi pi 8685 Jul 25 11:15 /home/pi/fsaysPI/fsaysinput -rwxr-xr-x 1 pi pi 22040 Jul 27 21:19 /home/pi/fsaysPI/fsayskeyboard -rwxr-xr-x 1 pi pi 9319 Jul 25 11:15 /home/pi/fsaysPI/fsaysuinput

Back to List

All form fields are required.
A confirmation mail for the comments will be send to you.