Hacking a $1 Wireless PS1 Controller to game with on my PC

Felcjo Ringo
7 min readOct 25, 2020

There is a GameStop store in my area that is going out of business and selling off its inventory at 70–90% off. Being a video game enthusiast, I had to go check it out. I bought about 15 games for ~$0.75 each, mostly for my XBOX360 (yes I have a lot of catching up to do). What an unbelievable steal, even if I never play through most of them. When I got to the checkout, I saw a PS1 controller with $3 price tag. It’s still listed on the GameStop website for that price, so you can buy it and follow this guide to get a cheap PC controller. Now, this controller was made specifically for a PS1 Classic System, and I was warned about this at checkout; this did not deter me from buying it, however, as it was essentially a wireless controller with a USB dongle, and I was sure I could figure out something to do with it (as I had with an N64 controller). At $0.89, you really can’t go wrong.

First, it should be noted that, out of the box, this controller will not work with your PC. There is no plug-n-play functionality for PCs, and there are likely no off the shelf drivers to get this working. Thus we need to do some reverse engineering as well as write our own code to do just that.

Reverse Engineering the Controller Data Stream (sounds complicated, isn’t too bad)

When I got home, I immediately took the controller out of the package, and put the USB dongle into my linux laptop. On linux, all devices that you plug in to the machine are registered as device drivers; you can find them under /dev/. The first matter of business is to see what name it was registered under. In the terminal, run ls /dev/ once with the USB dongle unplugged, and then again with the dongle plugged in. Here, there should be a difference of one, meaning you have to search for the discrepancy between the two outputs. I found the device to be registered under /dev/hidraw2.

Terminal output showcasing hidraw2 in /dev/

hidraw? As in high power draw? Uh-oh, that’s not good. Fortunately it turns out that hidraw actually means Human Interface Device (hid) raw binary stream. Phew .. Another common option for USB devices is to be registered under /dev/ttyUSBX, where X is a number.

Now that we know the name, let’s turn on the controller and do a quick cat to stream the contents of the device file.

Output of running `sudo cat /dev/hidraw2`

Hmmm … gibberish. Just as I expected! All we were trying to do here was access the stream though, and we did it — yay!

The next step is to try and decipher what data is being sent across the connection. cat is useful for just dumping data from files in string format, but our data is likely binary. For that, let’s run hexdump -C /dev/hidraw2 and see what we get. The -C flag tells hexdump to output in a hexadecimal format, with bytes separated by spaces.

Ok. Here we come across some more structured-looking output. As we are looking at controller output, we should expect there to be a neutral state that the controller is in when nothing is pressed. When one or more of the buttons are pressed, the hex values should change accordingly. The bit position that it changes on will correspond to the button that is being pressed. So, the way to go about which buttons correspond to which bit positions is as follows: run the above hexdump command, press a single button, and monitor the output to see if anything changes.

In the screenshot below, I pressed on the triangle button on the controller, then unpressed it. When I pressed down, the first set of two lines appeared, and when I unpressed the button a second later, the second set appeared. The left-most column shows the number of 16-byte lines (in hex), and so this acts as a sort of clock, as the controller is likely sending out its data at a given frequency. There are two transitions that occur in the output, the first being from 00 14 to 01 14. We can tell they are transitions because each 2-byte block in between them contains 01 14 and those outside contain 00 14. This tells us two things — first, that the data payload is 2 bytes, or 16 bits long, and second, that the triangle button is mapped to the first bit of the first byte of the payload (assuming little endian format).

Following the same process of mapping out individual buttons by looking at the individual bit changes between frames, we get the following picture.

The top line is the value in hexadecimal from hexdump. The second line is the binary value conversion (info on hex2binary conversion). The third line shows the mapping of the buttons to the binary values in the data stream. Note that 2 bits are not used. When mapping the D-Pad buttons, however, I noticed that the buttons do not map one-to-one. This is because there are in fact nine different combinations that the D-Pad can be in: centered, up, down, left, right, and up-left, up-right, down-left, down-right. The way this is implemented in firmware is pretty cool, as the high two bits represent the up-down value (00 is up, 01 is center, 10 is down), and the low two bits represent the left-right value in the same way. The values are mapped to the controller below.

Cool! So now we are able to see when buttons or combinations of buttons are being pressed. We have no way of doing anything with this yet, so we’ll have to do some data wrangling using code.

PS1 Controller Interface

Now that we understand what data the controller is sending over, the code should be pretty easy to write. First, we should define a global array to contain the information we want to keep track of, specifically for the 10 buttons in addition to the 9 D-Pad configurations.

Next we need to open the device file as readable, and have a loop to read in the data. os.read(file_descriptor, num_bytes) is a blocking call that waits for the appropriate number of bytes to then return. Here, we’ll wait for two bytes as that is the size of the payload. Once we have read the two bytes, we will simply check if a bit is a 1 or a 0 by &ing it with a 1 in the position we are querying, and assign it to its place in the btns array (l. 21–31). The D-Pad calculation is done similarly, but now we are equating the second byte with the hex term (l. 33–42)

With the code written, let’s run it and see what we get! Run sudo python ps1_interface.py to see a stream of data pour out. Try pressing a combination of the buttons and see if the output reports the presses correctly.

Terminal output of controller stream. 0 = not pressed, 1 = pressed.

Wrap-up

It’s always fun to find uses for electronics that you may not have even known it was possible to use. I had a lot of fun sleuthing around to get this controller to work, and once you’ve done a good amount of hacks like this, the process becomes a bit of a game, and an addictive one at that.

From here, there are many things you can use the controller for. Many video game emulators are built to handle input from wireless controllers, and so you could map your buttons to buttons in the emulator and play your favorite PS1 games. If you wanted to extend the code, you could create a device driver that would load into the kernel such that the controller would work as soon as you plug it in! You could also write a higher-level script to process transition events (such as button down-presses, release) — game engines use these functions as triggers for various events (i.e. start shooting the gun when Right Trigger is pressed, stop when RT is released). I did something similar to control a tank robot with a robotic arm, and also to control a drone to follow me using an XBOX360 controller (which won an award!).

I hope you enjoyed this article, and, if you did, feel free to check out my other work!

--

--