While I am very fond of the good old joysticks like the TAC-2 and Competition Pro, they do have one drawback - a cable. Sometimes I’d just want to sit down on my couch and play some Amiga games on actual oldskool hardware without having to untangle some extra-long cables and replug everything just to get the computer to sit on my coffee table. Trip up on those cables on your way to the fridge and the Amiga goes flying on the floor.

Modern game consoles these days use wireless gamepads with a fantastically long battery life and range, but usually require either Bluetooth or some proprietary hardware to use them on a computer. Adding a full Bluetooth stack into AmigaOS 3.x doesn’t sound like a very appetising idea for a fun project, nor would it be much use on most games anyway. However - what if we used some additional hardware that already has all that, is fairly cheap and hacker friendly - the ubiquitous Raspberry Pi!

(If you’re in a hurry to see the source code, check the GitHub project page)

I’ll admit - it does feel a bit backwards to use a tiny 1GHz/512MB computer just to enable the use of wireless gamepads on a 30-year old 8MHz/1MB computer. However, the flexibility of the RPi is just unbeatable when you can have a full Linux system with a development environment right on that tiny little circuit board.

My basic idea was to use the built-in Bluetooth hardware of a Raspberry Pi Zero W along with the Bluetooth stack on the Linux kernel to read input from my gamepads and mice, then use the GPIO pins to emulate two Atari-style DB9 joysticks or an Amiga-style mouse with rotary encoders. The nice thing about this is that it works with any computer that has DB9 joystick ports: C64, Atari ST, MSX, ZX Spectrum - the list goes on.

Pull-ups on an A500 (thanks to Amigawiki.de)

While doing some research prior to starting the project, a slight show-stopper was brought to my attention. The GPIO pins on the RPi use 3.3V logic levels and are not safe to use with +5V logic levels of the DB9 joystick port. Because the pins on the port are active-high, at least Amiga A500 and A1200 models use 4.7KΩ resistors as pull-ups to +5V. Without any level conversion, connecting the DB9 pins directly to a RPi would probably kill the GPIO pins or worse.

Rather than slap a level converting buffer on the GPIO pins, I opted to go for a separate I/O extender board that I can neatly sandwich on top of the RPi. To emulate two joysticks with 1 or 2 fire buttons, a minimum of 12 I/O pins are required. The IO Pi Zero from AB Electronics in the UK seemed like a perfect solution - inexpensive, simple I2C protocol, has 16 I/O pins and stacks perfectly on top of a RPi Zero W.

Raspberry Pi Zero W and 16-channel IO Pi Zero

Getting Bluetooth LE and PS3 controllers to work on Raspbian

Before writing any code, I first needed to test that I can actually pair and connect the Raspberry Pi with the two PS3 Sixaxis controllers and a Microsoft Bluetooth Mouse 3600 I have. This was actually a fairly necessary step, because absolutely nothing seemed to work out-of-the-box. The bluez  package bundled with the latest stable Raspbian is hopelessly out of date and has very buggy support for both Bluetooth LE and simplified pairing protocols. So the first order of business was to download the latest version (5.46 at the time), compile it and replace the old one.

Rather than install the new version of bluez, I chose to just overwrite all the files of the older version. This way I can have the required firmware and driver packages for the Raspberry Pi installed (bluez-firmware and pi-bluetooth), along with all the udev configuration and startup scripts from the main bluez package.

I started by downloading the latest source package from the Linux kernel archive:

https://www.kernel.org/pub/linux/bluetooth/

To build the binaries from the source, Debian packages libglib2.0-dev, libdbus-1-dev, libudev1-dev, libreadline6-dev and libical-dev are required. The Makefiles for building can be generated using autoconf with the following command line:

$ ./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var --enable-library --disable-systemd --enable-testing --enable-experimental --enable-deprecated --enable-sixaxis

Once the configuration has completed successfully, run make followed by make install.

Note that bluetoothd seems to be installed into /usr/libexec/bluetooth/ rather than /usr/lib/bluetooth which the deb-package uses. Stop the bluetooth service and overwrite the old binary with the newly compiled one:

$ sudo service bluetooth stop
$ sudo cp /usr/libexec/bluetooth/bluetoothd /usr/lib/bluetooth
$ sudo service bluetooth start

Additionally, to fix some issues I ran into later on, I had to edit the file /lib/udev/50-bluetooth-hci-auto-poweron.rules and change the action line to the following:

ACTION=="add", SUBSYSTEM=="bluetooth", KERNEL=="hci[0-9]*", ENV{DEVTYPE}=="host", RUN+="/bin/hciconfig %k up"

At this point, it’s easiest to just reboot Linux on the Raspberry Pi and then see if the Bluetooth on the RPi is more receptive to the devices I have.

While pairing with the Microsoft mouse just failed every time on the older version, the latest one works without any trouble. All that’s needed is to do is to start bluetoothctl and pair up with the mouse:

[bluetooth] scan on
[NEW] Device DF:1C:76:50:8A:A4 BluetoothMouse3600
[bluetooth] trust DF:1C:76:4F:8A:A4
Changing DF:1C:76:4F:8A:A4 trust succeeded
[bluetooth]# connect DF:1C:76:4F:8A:A4
Attempting to connect to DF:1C:76:4F:8A:A4
[CHG] Device DF:1C:76:4F:8A:A4 Connected: yes
Connection successful
[CHG] Device DF:1C:76:4F:8A:A4 ServicesResolved: yes
[BluetoothMouse3600]# pair DF:1C:76:50:8A:A4
Attempting to pair with DF:1C:76:50:8A:A4
[CHG] Device DF:1C:76:50:8A:A4 Paired: yes
Pairing successful

On a few occasions I did have bluetoothd crashing while attempting to pair, requiring a sudo service bluetooth restart to get it back up. This also seemed to sometimes help when the kernel was throwing a stream of Bluetooth: SMP security requested but not available errors.

The PS3 Sixaxis controllers are a bit more difficult because they don’t support wireless pairing at all. Instead, they must be paired over USB before they even acknowledge the RPi. The procedure that works for me is as follows:

  • If the controller is on, power off the Sixaxis controller by holding the PS button down for 10 seconds.
  • Connect the controller to the USB port on the Raspberry Pi with a suitable cable and adapter combo.
  • Make note that the four LEDs on the controller will do the “slow blink” to indicate charging. Then press the PS button once. The LEDs should now change to indicate controller number - usually ‘1’.
  • Disconnect the controller from the USB cable. The LEDs will now change to the “fast blink” indicating that it is searching for the host.
  • Start bluetoothctl on the Raspberry Pi, type scan on, wait for the address of the controller to be discovered, then scan off.
  • Trust and connect with the controller (note that bluetoothctl will show “Paired: no” for the controller even though it is connected and the HID devices are available). Eg.
    [bluetooth] trust 00:19:C1:63:0E:16
    Changing 00:19:C1:63:0E:16 trust succeeded
    [bluetooth] connect 00:19:C1:63:0E:16
    Attempting to connect to 00:19:C1:63:0E:16
    [CHG] Device 00:19:C1:63:0E:16 Connected: yes
    [PLAYSTATION(R)3 Controller]#
  • Check the LEDs on the controller to see if it now displays its number. If it is still doing the “fast blink” and the HID device isn’t registered (runcat /proc/bus/input/devices to see if the controller is shown), it may help to run again. The PS3 controller will probably power off after the interface goes down, so press the PS button again after the interface is back up. After this, you should both see the LEDs show correct number and the HID device appear.
    $ sudo hciconfig hci0 down
    $ sudo hciconfig hci0 up
  • And we’re done - hooray!

I wouldn’t call that a particularly user-friendly way of doing it, really. Also note that if you even briefly recharge the controller by plugging it to a PC, Mac or a Playstation, it will immediately pair with that host and you’ll have to pair it again with the Raspberry Pi over USB. Fortunately, it is usually sufficient to just plug it back in once to the RPi and cycle the hci0 interface down and back up again to reconnect.

Before getting into writing some actual code, it’s a good idea to install the packages libevdev2, libevdev-tools and libevdev-dev at this point. Among other stuff, these packages will install the utility evtest, which can be used to list the connected input devices and list of supported event types and codes from any of them:

$ evtest
No device specified, trying to scan all of /dev/input/event*
Not running as root, no devices may be available.
Available devices:
/dev/input/event0: BluetoothMouse3600
/dev/input/event1: PLAYSTATION(R)3 Controller
/dev/input/event2: PLAYSTATION(R)3 Controller
Select the device event number [0-2]: 1
Input driver version is 1.0.1
Input device ID: bus 0x5 vendor 0x54c product 0x268 version 0x100
Input device name: "PLAYSTATION(R)3 Controller"
Supported events:
  Event type 0 (EV_SYN)
  Event type 1 (EV_KEY)
    Event code 288 (BTN_TRIGGER)
    Event code 289 (BTN_THUMB)
    Event code 290 (BTN_THUMB2)
    Event code 291 (BTN_TOP)
    Event code 292 (BTN_TOP2)
    Event code 293 (BTN_PINKIE)
    Event code 294 (BTN_BASE)
    Event code 295 (BTN_BASE2)
    Event code 296 (BTN_BASE3)
    Event code 297 (BTN_BASE4)
    Event code 298 (BTN_BASE5)
    Event code 299 (BTN_BASE6)
    Event code 300 (?)
    Event code 301 (?)
    Event code 302 (?)
    Event code 303 (BTN_DEAD)
    Event code 704 (BTN_TRIGGER_HAPPY1)
    Event code 705 (BTN_TRIGGER_HAPPY2)
    Event code 706 (BTN_TRIGGER_HAPPY3)
...

Note how the names for the button event codes on the Sixaxis  controller make absolutely no sense - more on that next.

Querying and polling Linux evdev input devices

With the Bluetooth stuff taken care of, the next step is to start actually start writing some code and reading input from the event devices. Note that I’ll be writing everything in plain C to keep things simple.

As is customary in Linux, the input devices are presented as files named /dev/input/event[0-9]+. Both wired USB and wireless HID devices present the same interface, so we actually get support for wired devices as a bonus! Instead of doing ioctls directly to the devices, I’m making things a bit easier for me and using libevdev to access the devices. Including the headers libevdev/libevdev.h and libevdev/libevdev-uinput.h allows the use the required library functions.

To both query the capabilities and poll events from a device, it’s simply sufficient to get a read-only file descriptor to it and :

struct libevdev *dev = NULL;

int fd = open("/dev/input/event0", O_RDONLY|O_NONBLOCK);
int rc = libevdev_new_from_fd(fd, &dev);

To query a device if it supports a particular event type and code, the following functions returning boolean values can be used:

int i = libevdev_has_event_type(dev, EV_KEY);
int j = libevdev_has_event_code(dev, EV_KEY, BTN_DPAD_UP);

The above example checks if the device is able to send button press events and then if it is able to send a dpad up button press event. Seems pretty logical, right? Hah - you’d wish!

Although it may seem that the event devices have a nice and standardised set of event codes, at least the gamepads I tested with have their own absolutely non-standard sets of event codes. Here’s a table with the dpad and face buttons for PS3 and XBOX360 controllers:

Button PS3 XBOX360
Dpad up EV_KEY / 292 EV_ABS / ABS_HAT0X / -1
Dpad right EV_KEY / 293 EV_ABS / ABS_HAT0Y / 1
Dpad down EV_KEY / 294 EV_ABS / ABS_HAT0X / 0
Dpad left EV_KEY / 295 EV_ABS / ABS_HAT0Y / -1
Button top (△) EV_KEY / 300 (Y) EV_KEY / BTN_NORTH
Button right (◯) EV_KEY / 301 (B) EV_KEY / BTN_EAST
Button bottom (X) EV_KEY / 302 (A) EV_KEY / BTN_SOUTH
Button left (□) EV_KEY / 303 (X) EV_KEY / BTN_WEST

Mice tend to stick to a more coherent set of events, with the movement being sent as EV_REL events and buttons as EV_KEY events with codes BTN_LEFT and BTN_RIGHT. The value returned with the event is negative if the movement is left or up and positive otherwise.

The event polling loop runs the following (non-blocking) call repeatedly to get new events from a device:

struct input_event ev;
int rc = libevdev_next_event(dev, LIBEVDEV_READ_FLAG_NORMAL, &ev);

rc will contain 0 if no event was available, 1 if a new event was read into ev. The struct input_event is defined in the Linux kernel header file linux/input.h.  Pretty simple once the various quirks of the actual hardware are known and taken into account when parsing the event.

Interfacing with the DB9 joystick ports

Now that we know what the gamepads and mouse are doing, we can finally get to the hardware side of things and start to actually control the pins. To keep things tidy, I’m emulating the actual 9-pin port states in two variables and updating them according to the events read from HID devices.

uint16_t port1_pins=0x016f, port2_pins=0x016f;

I won’t go into detail over how the pin states are changed to gamepad mouse input as it’s just some rather boring bit clearing and setting. If you’re interested in looking at the code a bit further, read the source for ports.c on the GitHub page for the project. Note that the mouse encoder and quadrature pulse generation is more or less written so that the mouse I’m using will generate roughly similar DPI to an actual Amiga mouse.

The library provided for the IO Pi Zero board is written in Python, so I needed to write my own code for controlling the MCP23017 chip over I2C. Looking at the datasheet, it appeared to be fairly simple to hack up some code to just run all GPIO pins as outputs and control their state.

Writing I2C code becomes a bit easier by installing the package libi2c-dev, which contains a modifed header file linux/i2c-dev.h, allowing the kernel I2C functions to be called also from userland. Using these calls saves you from manually writing register numbers before reading or writing data.

On the Pi Zero W, the I2C bus on the 40-pin header has bus number 1, so the device can be opened with:

int fd=open("/dev/i2c-1", O_RDWR);

By default, the IO Pi Zero board is configured to address 0x20, so after successfully opening the device, slave access needs to be acquired to that address:

int rc=ioctl(fd, I2C_SLAVE, 0x20);

If both calls were successful, it is now possible to write bytes to registers on the MCP23017 using the aforementioned kernel I2C functions:

int rc=i2c_smbus_write_byte_data(fd, 0x01, 0x00);

To initialize the chip, it is sufficient to write zero to IOCON, IODIRA and IODIRB to assign all 16 pins as outputs. After that, you can just update the pin states by writing into GPIOA (0x12) and GPIOB (0x13). I’ve set up the registers so that each GPIO register controls one joystick port and the pin assignments are the same for each:

Pin GPIOA/GPIOB bit DB9 port 1/2
Up 0 1
Down 1 2
Left 2 3
Right 3 4
Button 1 4 6
Button 2 5 9

A thread running alongside the event processing thread looks constantly at the emulated port variables and upon detecting a change, copies the relevant bits into an 8-bit variable and writes to the corresponding GPIO register:

uint8_t p=(port2_pins&0x00f) | ((port2_pins&0x020)>>1) | ((port2_pins&0x100)>>3);
i2c_smbus_write_byte_data(fd, 0x13, 0x00);

With all the required code in place, all that’s needed is to build a cable to connect the 2x8pin header from the IO Pi Zero into two female DB9 connectors that can be plugged into the joystick ports of Amiga. Getting single wires from two regular round data cables into a ribbon cable connector was a bit fiddly but eventually it turned out fine. I double checked all the pins with a multimeter just in case, though.

Mouse/joystick ports on an Amiga A1200

One thing to remember is that the ground plane on the joystick ports (pin 8) must be connected to the Raspberry Pi’s ground. You could do this through the ground pins on the 40-pin header, but I used the solder points for external power on the IO Pi Zero. The +5V input pin is not being used, although it would be cool to power the Raspberry Pi from the joystick ports. Unfortunately it’s not possible as they only provide 50mA of current each - the Pi Zero W with the IO Pi Zero can draw up to ~350mA so we need an external power supply.

Connected to an A1200 for testing

With both the hardware and software taken care of, it’s time for some testing!

Using joyemu

The finished “product” is being called joyemu, and it offers some configurability instead of hardcoding everything and relying on defaults:

Usage: ./joyemu [-vqh] [-i bus] [-a addr] [-d (j1|j2|m):evdev] [-m port] [-j port] [-e type]

  -v		add verbosity
  -q		add quietness
  -i n		set I2C bus number for I/O expander (default: 1)
  -a 0xnn	set I2C address for I/O expander as a hexadecimal byte (default: 0x20)
  -d j1:n	set event device number for joystick 1
  -d j2:n	set event device number for joystick 2
  -d m:n	set event device number for mouse
  -m n		set mouse port: 1 (default) or 2
  -j n		set first joystick port: 1 or 2 (default)
  -e n		set mouse emulation type: 0=Amiga (default), 1=Atari ST
  -h		display this help

Upon startup, joyemu detects which input devices are capable of behaving like a mouse and gamepads, then assigns them to joystick ports in the order of detection. As the above usage help shows, the device numbers and mapping can be forced on the command line although in this example, we’re going with the defaults:

$ ./joyemu -vv
[1502621551.0000361211] ++ Checking device /dev/input/event0, number 0
[1502621551.0000361972] ++ Input device name: "Sony PLAYSTATION(R)3 Controller"
[1502621551.0000362041] -- Input device ID: bus 0x3 vendor 0x54c product 0x268
[1502621551.0000362086] -- Doesn't look like a mouse
[1502621551.0000362122] -- Device has a dpad, event type 3
[1502621551.0000362159] ++ Device has capabilities to function as a joystick, assigning it to port 2
[1502621551.0000362227] ** Using "Sony PLAYSTATION(R)3 Controller" to emulate a joystick in port 2
[1502621551.0000362457] -- I2C: opened bus device /dev/i2c-1 and acquired access to slave at 0x020
[1502621551.0000366991] -- Started port I/O thread
[1502621551.0000368686] -- Port 1 pins [ 1 0 1 1 0 1 1 1 1 ]
[1502621551.0000372461] -- Port 2 pins [ 1 0 1 1 0 1 1 1 1 ]
[1502621552.0000373611] -- Started event poll thread

As you can see, one PS3 controller was detected and assigned to emulate a joystick on port 2. Pressing the buttons on the joypad will toggle the bits on the joystick port:

[1502621553.0000113653] ++ Joystick 2 Y axis state up
[1502621553.0000123654] -- Port 2 pins [ 1 0 1 1 0 1 1 1 0 ]
[1502621553.0000233648] ++ Joystick 2 Y axis state center
[1502621553.0000243647] -- Port 2 pins [ 1 0 1 1 0 1 1 1 1 ]
[1502621553.0000793636] ++ Joystick 2 Y axis state down
[1502621553.0000803652] -- Port 2 pins [ 1 0 1 1 0 1 1 0 1 ]
[1502621553.0000913601] ++ Joystick 2 Y axis state center
[1502621553.0000919653] -- Port 2 pins [ 1 0 1 1 0 1 1 1 1 ]
[1502621554.0000433628] ++ Joystick 2 X axis state left
[1502621554.0000436646] -- Port 2 pins [ 1 0 1 1 0 1 0 1 1 ]
[1502621554.0000553591] ++ Joystick 2 X axis state center
[1502621554.0000573581] -- Port 2 pins [ 1 0 1 1 0 1 1 1 1 ]
[1502621555.0000063639] ++ Joystick 2 X axis state right
[1502621555.0000073625] -- Port 2 pins [ 1 0 1 1 0 0 1 1 1 ]
[1502621555.0000183650] ++ Joystick 2 X axis state center
[1502621555.0000193626] -- Port 2 pins [ 1 0 1 1 0 1 1 1 1 ]
[1502621555.0000803582] ++ Joystick 2 fire button down
[1502621555.0000813630] -- Port 2 pins [ 1 0 1 0 0 1 1 1 1 ]
[1502621555.0000903568] ++ Joystick 2 fire button up
[1502621555.0000913618] -- Port 2 pins [ 1 0 1 1 0 1 1 1 1 ]

If we connect a mouse and attach it to port 1 (which is the default), we can see how the rotary encoders are being emulated as the mouse is being moved:

[1502622105.0000249606] -- Mouse moved vertically 4 units
[1502622105.0000249792] -- Port 1 pins [ 1 0 1 1 0 1 0 0 0 ]
[1502622105.0000250176] -- Port 1 pins [ 1 0 1 1 0 0 0 0 1 ]
[1502622105.0000252941] -- Port 1 pins [ 1 0 1 1 0 0 1 1 1 ]
[1502622105.0000255866] -- Port 1 pins [ 1 0 1 1 0 1 1 1 0 ]
[1502622105.0000257344] -- Mouse moved horizontally 10 units
[1502622105.0000258944] -- Port 1 pins [ 1 0 1 1 0 1 0 0 0 ]
[1502622105.0000259450] -- Port 1 pins [ 1 0 1 1 0 0 0 0 1 ]
[1502622105.0000259837] -- Port 1 pins [ 1 0 1 1 0 0 1 1 1 ]
[1502622105.0000261842] -- Port 1 pins [ 1 0 1 1 0 1 1 1 0 ]

By default the mouse pulses are sent on the pins assigned by Commodore for the Amiga series of computers but a command line switch is available to use Atari ST pin assignments instead. Note that Atari support is untested as I don’t have the hardware to try it on. The mouse emulation also does not work with a Commodore C64 due to the 1351 mouse’s signalling protocol being very different to Amiga’s.

Here’s a rather noisy video of running joyemu on the Pi Zero W connected to an Amiga A1200 and using the Bluetooth mouse to start a game and briefly play it using a PS3 gamepad. The inlay image shows the console output with some added verbosity.

joyemu is open source and BSD licensed, so you’re welcome to fork it and hack it in any way you want. If you add something cool, please do submit a pull request with your changes! The source code can be found on GitHub. Thanks for reading!