Let’s just say that I like fightsticks. I also like the PlayStation 2, if the amount of articles tangentially related to it aren’t revealing enough. Besides fightsticks and the Playstation 2, I also like Playstation 2 fighting games, specially SoulCalibur 2.
So, creating a custom fightstick, powered by the extremely powerful RP2040 microcontroller in the spirit of other projects like GP-2040CE, the PhobGCC and its distant cousin, the pico rectangle seemed so enticing to me.
This idea came about more or less 2 years ago, in the same time frame I began building my own fight sticks. I searched for similar projects or, at least, a library that could allow me to interface with the Playstation 2 as a controller. Basically, I wanted to know if someone more intelligent than me had already taken the mantle of building the ultimate Playstation 2 controller.
Let’s just say, I couldn’t find such projects. But there are some similar ones, like pico memcard, which is a PSX memory card powered by an RP2040 microcontroller (both memory cards and controllers in the first Playstation consoles spoke the same protocol, and the Playstation 2 protocol is, let’s say, a superset of the Playstation 1’s) and the ps1 mouse, a raspberry pi pico powered USB to PSX adapter that allows one to use a USB mouse as a Playstation mouse, which, yes, it did exist.
My main objective, and the project I’ll be making (and hope to finish) is a dualshock protocol implementation library: reasonably high level and in C, allowing for projects both in C and C++. MicroPython compatibility could be in the roadmap, too.
It will be under the MIT license because I ultimately want to make the GP2040-CE firmware Playstation 2 (and maybe 1) compatible, and GP2040-CE is licensed under these terms.
This license also, sadly, doesn’t let me derive my work off of pico memcard, as it is licensed under the GPL, so I’ve been studying instead the ps1 mouse’s PIO (more on that later) source code. But… it’s esoteric.
Let this series of posts bare witness to my success (or failure) on making this project. If I don’t achieve this, well, let them be a mark of shame forever sitting on my blog. But hell, I really would like to have a universal fightstick which I could use on my favorite console, with zero latency. Because the RP2040 is just that powerful (refer to the GP2040-CE’s latency tests)
Yesterday I formally began my journey. I’ve been researching the Pico’s Programmable Input/Output (PIO for short) feature for a while now, and made an (unfinished) mock project to familiarize myself with the debugging and build process of pico projects.
I started with a simple program, that reads the command pin (the Playstation -> controller communication bus) and outputs to the screen what it has read:
#include <stdint.h>
#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/pio.h"
#include "dualshock.pio.h"
#define COMMAND_PIN 27
int main() {
stdio_init_all();
PIO pio = pio0;
uint offset = pio_add_program(pio, &dualshock_program);
uint sm = pio_claim_unused_sm(pio, true);
dualshock_program_init(pio, sm, offset, COMMAND_PIN);
while (true) {
uint32_t data = pio_sm_get(pio, sm);
printf("%x\n", data);
sleep_ms(1000);
}
}
As you can see, I reference some mystical functions like pio_add_program
and pio_claim_unused_sm
. These are SDK functions that
work in conjunction with the PIO feature I talked about.
In the world of microcontrollers there are usually two ways of talking to a device: bitbang whatever data it transmits to us or use specific hardware usually programmed by a third party vendor that allows us to communicate in a specific protocol, like I2C or SPI.
The first one has been the most used in the world of hobbyist electronics using arduino. Bitbanging is, basically, using the CPU to try and time reads and writes to the device. If the device we want to communicate with sends us bytes on a, let’s say, 1 MHz clock cycle, then we do our best to make our CPU receive that byte in the time the device expects us to, same for writes.
This is inefficient and hard, but, in the world of the PS2, has been done: Sukko Pera’s PSX Newlib and Bill Porter’s arduino library come to mind.
If we read the source code of these projects, we can see the use of sleep
directives and the like, to try and time every read
and write.
PIO allows us to not do that. Instead, it provides us a piece of hardware specialized for input and output, consisting of state machines, two FIFOs for reading and writing data and several shift registers to, well, shift data around. We program this hardware under a pretty basic, but powerful, assembly language.
For what I’ve learned about PIO (remember, I’m not an expert) it handles timing by running on a fraction of the frequency of the RP2040 main CPU clock, which, if I remember correctly, runs at 125 MHz. This fraction can be 1, by the way, meaning it can run at the full speed of the CPU. But in our case, and, quoting the source I’ll probably base all of my work on, the dualshock 2 runs at a frequency of 500 KHz.
So now that we know PIO, what have I written in it? Well, let’s remember that I want to read the data the playstation has sent me and output it on the screen. In PIO terms, I want to read a byte off of my input pin, shift this byte to the ISR, and then push whatever is in the ISR to my TX (read) FIFO, for it to be consumed by the pico’s main CPU.
.program dualshock
.wrap_target
in pins, 8 ; Read 8 bits from the input pin, and
; shift them to the Input Shift Register
push ; Push the data present in the Input Shift Register
; to the RX FIFO.
.wrap
% c-sdk {
void dualshock_program_init(PIO pio, uint sm, uint offset, uint command_pin) {
pio_sm_config cfg = dualshock_program_get_default_config(offset);
sm_config_set_in_pins(&cfg, command_pin);
sm_config_set_in_shift(&cfg, false, false, 8);
sm_config_set_clkdiv_int_frac(&cfg, 250, 0x00);
}
%}
Here it is, my masterpiece. Pretty basic, right? But I think it was a pretty good exercise in getting to know the PIO feature and actually using it (later we’ll see the Cmake file, to show you how you can build your project taking a PIO program in mind).
Everything below the c-sdk is code that will be injected to the header file the assembler will generate when assembling this program. In it, we configure our PIO state machine: the pin we’ll use as the command pin (I.E., the one we’ll be reading), the direction we’ll shift bits on (in this case, the playstation 2 speaks a Least significant bit protocol, in layman terms, we’ll read what it has to say from left to right) and its clock frequency: 125/250 = 0.5 MHz, or, 500 KHz. I think. I broke my head trying to come up with this, let’s just say the documentation on clock dividers was hard to parse, for me, at least.
The cmake file is as follows:
cmake_minimum_required(VERSION 4.0)
include($ENV{PICO_SDK_PATH}/external/pico_sdk_import.cmake)
project(pico_shock C CXX ASM)
pico_sdk_init()
add_executable(main main.c)
pico_generate_pio_header(main ${CMAKE_SOURCE_DIR}/dualshock.pio)
target_link_libraries(main pico_stdlib hardware_pio)
pico_enable_stdio_uart(main 1)
pico_add_extra_outputs(main)
We only care about the pico_generate_pio_header(main ${CMAKE_SOURCE_DIR}/dualshock.pio)
line. This tells make to
use the pio assembler to, well, assemble our pio program and generate the dualshock.pio.h
file we include in our C program.
Well, the PS2 is speaking to us in chinese:
None of this means anything per the Curious Inventor guide, and, it’s expected. I was hoping to at least get a 0x00 byte, which seems to be the first byte the console sends in every packet. But no.
It’s expected because the PS2 speaks in a (albeit heavily modified) duplex SPI protocol: we receive and read data at the same time, regulated by a clock pulse, and also using a chip select (attention, in DualShock terms) and acknowledge signals.
For now, I want to receive actual, useful data. I hope I can achieve this by only implementing the clock signal for now, but of course, I’ll have to implement the other ones (data, attention and acknowledge) sooner or later.
The pico-examples repository provides a really relevant example: an SPI implementation in PIO. I’ve been basing most of my current work on it, and the C code seems specially interesting: particularly the way it reads (and writes) data passed by the PIO state machine.
It seems that using pio_sm_get
and friends is the sucker’s way of doing it. Well, I only hope the debugger plays nice, because
I’m gonna need it to poke at what I’ll be actually receiving from the PIO state machine.