Switch Dual Shock adapter part 6: Dual Shock

Posted on May 5, 2023

In this series of posts, I’m attempting to make a Dual Shock to Switch controller adapter. It will plug into the Switch Dock’s USB port.

So far, I’ve got a nice, efficient, fake Switch Pro Controller running on my breadboard. But it doesn’t do anything useful - it just fakes out left and right dpad presses. In this installment, I’ll connect a Dual Shock to it!

Two hands holding a Dual Shock in the usual position

…and play Zelda!

In this post, I’m indebted to Curious Inventor’s consolidated guide to interfacing with a Dual Shock, and Jacques Gagon’s Hackaday.io page about the PlayStation & PlayStation 2 SPI interface, both of which link to a long list of predecessors. Thanks to all those involved in reverse-engineering it over the last quarter century!

Interfacing with a Dual Shock is, thankfully, much more straightforward than interfacing with a USB host.

There are actually a few open source libraries out there for interfacing a Dual Shock with an Arduino, but I decided to code this myself. It’s an interesting project, I’ll be in full control of the timing, and it’s actually not as hard as it might seem - in some ways, the libraries add an extra layer of obfuscation over what’s going on underneath.

SPI

The ‘PlayStation’ (in this case, my circuit) pings the controller every so often, and the controller replies with its state.

This is done over a serial interface - almost an SPI interface, to be exact.

SPI (‘Serial Peripheral Interface’) is a two-way serial communication protocol - but it’s not symmetrical: one side is the “Controller”, the other side is the “Peripheral”. The controller provides a clock signal, and the two data lines are used to send and receive one bit in both directions on every clock transition, eight bits in a row. Often, communication is only useful in one direction at a time. In that case, zeros, some padding pattern, or even just garbage gets sent in the other direction and ignored.

SPI terminology has been in a bit of turmoil in recent years1. The industry seems to be drifting2 towards de-facto agreement on “Controller/Peripheral” - so that’s what I’ll use here. I’ve listed all the terms I’ll use in the table below. Unfortunately, the ATmega8A datasheet still uses “MOSI” in the places I’ll refer to as “PICO”, and “MISO” where I will use “POCI”.

Abbreviation Expansion
PICO [MOSI] Peripheral In Controller Out
POCI [MISO] Peripheral Out Controller In
SDO Serial Data Out
SDI Serial Data In
SCK Serial Clock
CS [SS] Chip Select

The ATMega8A has hardware support for SPI communication. It can essentially send and receive whole bytes at a time without the programmer (me!) needing to control the details.

To use it, you need to use certain prescribed pins. Here’s the pin-out again (copied from the data sheet, so still using the old terminology).

ATMega8A pin out

Notice pins 17, 18 and 19.

Unfortunately, some of these pins are the ones I chose for my oscilloscope-friendly debugging signals in part 5. I just removed the debug signals. I should’ve thought ahead more carefully when choosing the pins! If I need them again, PORTC is still pretty open.

The flashing LED also had to move - it’s connected to the SCK pin. I moved it to pin 14, on the ‘bottom’ of the ATmega on the breadboard.

Connection

Here’s a pin-out of the Dual Shock:

Dual Shock pin out, looking ‘into’ the plug, out of the Playstation. From left to right, POCI, PICO, +MOTORV, GND, +3.3V, CS, SCK, N/A, and ACK
Dual Shock pin out, looking 'into' the plug, out of the Playstation.

I connected PICO, POCI and SCK up to the appropriate pins of the ATmega.

I also connected a 3.3k resistor between 3.3V and POCI. This is necessary to ensure that POCI reads high when the controller is not driving it low. Pull-up resistors are not usually required for SPI, which is why I said the Dual Shock almost used SPI above.

‘~CS’ is a ‘Chip Select’ pin in SPI parlance - but I guess here it’s perhaps more appropriate to think if it as ‘Controller Select’?

It must be driven low3 to tell the controller to listen and respond to SPI traffic. In a real Playstation, two controllers share the same SPI lines, and the console uses the two ~CS lines, one connected to each controller, to alternately switch between reading each one.

I chose to connect ~CS to PB2 - pin 16. It’s not strictly necessary to choose this pin for this - I’m going to switch its state manually in code when ‘selecting’ the controller, so any would do. It is, however, mandatory to have pin 16 configured as an output when using the ATmega8A as an SPI controller. If it’s configured as an input, the ATmega will interpret any low signal on it as it’s own ‘chip select’ being pulled down, and switch out of SPI controller mode into SPI peripheral mode - which means it won’t generate the SPI clock. Using it it as an output now saves any potential confusion later - I won’t be tempted to configure it as an input and be confused as to why things are breaking.

The +3.3V and GND lines go to the sane power supply lines as the ATmega is connected to.

That just leaves ‘+MOTORV’ and ‘ACK’. +MOTORV provides voltage to the rumble/vibration motors. I won’t be using it for now - though I might try to get them working later. ACK can be used to tell if the controller is receiving communication - but it’s not necessary (what would I even do if if the controller didn’t acknowledge besides try again?)

To use SPI on the ATMega8A, you need to set up a few things. The pins need to be appropriately set up as outputs or inputs, and the SPCR (SPI Control Register), and SPSR (SPI Status Register) both need to be set up to use the right ‘flavor’ of SPI.

As always, the data sheet describes what each bit of these registers controls - and that’s what I used to decide how to set things up. In setup(), I replaced the debug pin code form part 5 with this:

    // Set port 2 outputs. Most of these pind are prescribed by the ATmega's
    // built in SPI communication hardware.
    DDRB |=
        1 << 3 | // PICO (PB3) - Serial data from ATmega to controller.
        1 << 5 | // SCK (PB5) - Serial data clock.
        1 << 2 | // Use PB2 for the controller's ^CS line
        1 << 0   // PB0 for our blinking LED.
    ;

    // Set up the SPI Control Register. Form right to left:
    // SPIE = 0 (SPI interrupt disabled - we'll just poll)
    // SPE  = 1 (SPI enabled)
    // DORD = 1 (Data order: LSB of the data word is transmitted first)
    // MSTR = 1 (Controller/Peripheral Select: Controller mode)
    // CPOL = 1 (Clock Polarity: Leading edge = falling)
    // CPHA = 1 (Clock Phase: Leading edge = setup, trailing ecdge = sample)
    // SPR1 SPR0 = 10 (Serial clock rate: 10 means (F_CPU / 64) - 
    //                 so: (12.8MHz / 64) = 200kHz - but we'll double that below).
    SPCR = 0b01111110;

    // Double the SPI rate defined above (so 200kHz * 2 = 400kHz)
    SPSR |= 1 << SPI2X;

    // Need to set the controller's CS, which is active-low, to high so we can 
    // pull it low for each transaction - and PICO and SCK should rest at high 
    // too.
    PORTB |= 1 << 5 | 1 << 3 | 1 << 2;

Communication

Now, it’s a matter of using the SPI interface to communicate with the Dual Shock.

The way this works is that we send a command to the controller, and it both processes the command and replies with its state in the same SPI transaction.

static uint8_t *sampleDualShock_P(const uint8_t *toTransmit, const uint8_t toTransmitLength)
{
    // This is static because we return it to the caller.
    static uint8_t toReceive[21] = {0};
    uint8_t toReceiveLen = sizeof(toReceive);

    // Pull-down the ~CS ('Attention') line.
    PORTB &= ~(1 << 2);

    // Give the controller time to notice.
    delayMicroseconds(10);

    // Loop using SPI to send/receive one byte at a time.
    uint8_t byteIndex = 0;
    do {
        // Put what we want to send into the SPI Data Register.
        if(byteIndex == 0) {
            // All transactions start with a 1 byte.
            SPDR = 0x01;
        } else if(byteIndex <= toTransmitLength) {
            // If we still have [part of] a command to transmit.
            SPDR = pgm_read_byte(toTransmit + (byteIndex - 1));
        } else {
            // Otherwise, pad with 0s like a PS1 would.
            SPDR = 0x00;
        }

        // Wait for the SPI hardware do its thing:
        // Loop until SPIF, the SPI Interrupt Flag in the SPI State Register,
        // is set, signifying the SPI transaction is complete.
        while(!(SPSR & (1<<SPIF)));

        // Grab the received byte from the SPI Date register.
        // This has the side-effect of clearing the SPIF flag.
        const uint8_t received = SPDR;

        // Process what we've received.
        if(byteIndex == 1) {
            // The byte in position 1 contains the length to expect after
            // the header.
            toReceiveLen = min(toReceiveLen, ((received & 0xf) * 2) + 3);
        }
        if(byteIndex == 2) {
            // This should always be 0x5a.
            if(received != 0x5a) {
                byteIndex = 0;
                memset(toReceive, 0, sizeof(toReceive));
                break;
            }
        }

        toReceive[byteIndex] = received;
        ++byteIndex;

        if(byteIndex < toReceiveLen) {
            // The Dual Shock requires us to wait a bit between packets.
            delayMicroseconds(10);
        }
    } while(byteIndex < toReceiveLen);

    // ~CS line ('Attention') needs to be raised to its inactive state between
    // each transaction.
    PORTB |= 1 << 2;

    DualShockReport *receivedReport = (DualShockReport *)toReceive;
    if(byteIndex <= offsetof(DualShockReport, rightStickX)) {
        // If we didn't get any analog reports (the controller is in digital
        // mode), set the analog sticks to centered.
        receivedReport->rightStickX = 0x80;
        receivedReport->rightStickY = 0x80;
        receivedReport->leftStickX = 0x80;
        receivedReport->leftStickY = 0x80;
    }

    return toReceive;
}

As for what commands to send, the single byte 0x42 means ‘poll’, and it causes the Dual Shock to do nothing but send back its current state. Just what we need! There are other commands4 that control things like whether the controller is in analog mode5, control rumble etc. - but none of that is important to me right now. It would be nice to auto-switch-on analog mode and maybe make rumble work - but that’s for another day.

Before, when we prepared a report of controller state, we just faked out a dpad press. Now, we can actually query the DualShock:

static uint8_t prepareInputSubReportInBuffer(uint8_t *buffer) 
{    
    static const PROGMEM uint8_t toTransmit[] = { 0x42 };
    const uint8_t *received = sampleDualShock_P(toTransmit, sizeof(toTransmit));

    convertDualShockToSwitch((const DualShockReport *)received, (SwitchReport *)buffer);

    return sizeof(SwitchReport);
}

Done?

Conversion

Well, that code won’t run - the convertDualShockToSwitch(...) function does not yet exist. And neither do the DualShockReport or SwitchReport data structures.

So far, I’ve been dealing with the reports mostly as byte arrays. It’s time to switch to something more semantic.

I added this to the existing descriptors.h:

typedef struct DualShockReport {
    uint8_t effEff;
    uint8_t reportId;
    uint8_t fiveAy;

    int selectButton:1;
    int l3Button:1;
    int r3Button:1;
    int startButton:1;
    int upButton:1;
    int rightButton:1;
    int downButton:1;
    int leftButton:1;

    int l2Button:1;
    int r2Button:1;
    int l1Button:1;
    int r1Button:1;
    int triangleButton:1;
    int circleButton:1;
    int crossButton:1;
    int squareButton:1;

    uint8_t rightStickX;
    uint8_t rightStickY;
    uint8_t leftStickX;
    uint8_t leftStickY;
} DualShockReport;

typedef struct SwitchReport {
    int connectionInfo:4;
    int batteryLevel:4;

    int yButton:1;
    int xButton:1;
    int bButton:1;
    int aButton:1;
    int shoulderRightRightButton:1;
    int shoulderRightLeftButton:1;
    int rShoulderButton:1;
    int zRShoulderButton:1;

    int minusButton:1;
    int plusButton:1;
    int rStickButton:1;
    int lStickButton:1;
    int homeButton:1;
    int captureButton:1;
    int unusedButton:1;
    int chargingGrip:1;

    int downButton:1;
    int upButton:1;
    int rightButton:1;
    int leftButton:1;
    int shoulderLeftRightButton:1;
    int shoulderLeftLeftButton:1;
    int lShoulderButton:1;
    int zLShoulderButton:1;
    
    uint8_t leftStick[3];
    uint8_t rightStick[3];
    uint8_t vibrationReport;
} SwitchReport;

These straight translations into C6 of the descriptions in Curious Inventor protocol, and Jacques Gagon’s Dual Shock documents, and Yuki Mizuno’s “Controlling Nintendo Switch with a Smartphone” blog post, with some help from the Bluetooth HID Notes in dekuNukem’s Nintendo_Switch_Reverse_Engineering GiuHub repo.

Now, the convertDualShockToSwitch(...) function:

static void convertDualShockToSwitch(const DualShockReport *dualShockReport, SwitchReport *switchReport)
{
    memset(switchReport, 0, sizeof(SwitchReport));

    // Fake values.
    switchReport->connectionInfo = 0x1;
    switchReport->batteryLevel = 0x8;

    // Dual Shock buttons are 'active low' (0 = on, 1 = off), so we need to
    // invert their value before assigning to the Switch report.

    switchReport->yButton = !dualShockReport->squareButton;
    switchReport->xButton = !dualShockReport->triangleButton;
    switchReport->bButton = !dualShockReport->crossButton;
    switchReport->aButton = !dualShockReport->circleButton;
    switchReport->rShoulderButton = !dualShockReport->r1Button;
    switchReport->zRShoulderButton = !dualShockReport->r2Button;

    switchReport->minusButton = !dualShockReport->selectButton;
    switchReport->plusButton = !dualShockReport->startButton;
    switchReport->rStickButton = !dualShockReport->r3Button;
    switchReport->lStickButton = !dualShockReport->l3Button;

    switchReport->downButton = !dualShockReport->downButton;
    switchReport->upButton = !dualShockReport->upButton;
    switchReport->rightButton = !dualShockReport->rightButton;
    switchReport->leftButton = !dualShockReport->leftButton;
    switchReport->lShoulderButton = !dualShockReport->l1Button;
    switchReport->zLShoulderButton = !dualShockReport->l2Button;


    // The Switch has 12-bit analog sticks. The Dual Shock has 8-bit.
    // We replicate the high 4 bits into the bottom 4 bits of the
    // Switch report, so that e.g. 0xFF maps to 0XFFF and 0x00 maps to 0x000
    // The mid-point of 0x80 maps to 0x808, which is a bit off - but
    // 0x80 is in fact off too: the real midpoint of [0x00 - 0xff] is 0x7f.8
    // (using a hexadecimal point there, like a decimal point).

    const uint8_t leftStickX = dualShockReport->leftStickX;
    const uint8_t leftStickY = 0xff - dualShockReport->leftStickY;
    switchReport->leftStick[0] = (leftStickX << 4) | (leftStickX >> 4);
    switchReport->leftStick[1] = (leftStickX >> 4) | (leftStickY & 0xf0);
    switchReport->leftStick[2] = leftStickY;

    const uint8_t rightStickX = dualShockReport->rightStickX;
    const uint8_t rightStickY = 0xff - dualShockReport->rightStickY;
    switchReport->rightStick[0] = (rightStickX << 4) | (rightStickX >> 4);
    switchReport->rightStick[1] = (rightStickX >> 4) | (rightStickY & 0xf0);
    switchReport->rightStick[2] = rightStickY;
}

This could probably be more efficient with some fancy bit-level hacking - but this is straightforward and seems to be fast enough. There’s no point in making it faster because we’re waiting on the USB communication anyway.

It’s mostly a direct mapping. The only real interesting part is the 8-bit to 12-bit conversion of the analog stick positions (comments in the code). Both systems use unsigned integers, so at first, it seems like just shifting the values up by four bits will work - but something needs to be done to fill in the remaining four bits, otherwise you get a slight drift towards 0 because the 0xFF would translate to 0xFF0 instead of 0xFFF

Breath of the Wild

Moment of truth - let’s plug this into the Switch…

It works!

One slightly annoying thing that you can’t really tell from the video is that there’s a bit of stick drift on the left stick when I’m not holding it. I haven’t seen this when using the same controller in other places, so I think it must be usually ‘dead-zoned’ out. Most controllers - either in the controller or when their input is processed by the console/computer, have a ‘dead zone’ around the middle where any movement is zeroed-out.

After a little more research, I came up with this:

static uint8_t deadZonedStickPosition(uint8_t rawStickPosition)
{
    // Switch pro controller docs suggest the pro controller has a 10% radial 
    // dead zone:
    //   https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/blob/master/spi_flash_notes.md
    // This does seem to feel okay? It's square though...
    static const uint8_t deadZoneRadius = floor(0xff / 10);
    static const uint8_t midPosition = 0x80;
    if(rawStickPosition >= midPosition - deadZoneRadius && rawStickPosition <= midPosition + deadZoneRadius) {
        return midPosition;
    }
    return rawStickPosition;
}

I use it when I’m converting from Dual Shock to Switch format.

The Switch contains some interesting controller calibration settings - and when they’re used it sends the calibration bits back to the controller. I wonder if it would be possible to interface with that?

…but that’s enough for today. The project is working! This is quite a milestone!

Future Enhancements

I have plenty of ideas for enhancements to the project - and some of them are definitely possible!

  • Switch on (and lock?) analog mode automatically.
  • Detect presses of the ‘Analog’ button; force analog back on and translate as the home button?
  • Detect controller connection?
  • Allow dead zone configuration using the Switch’s built in UI? What is the configuration data the Switch sends?
  • Better middle placement of 0x80?
  • Correctly report the color of controller to the Switch? Can the Dual Shock report this?
  • Make a fake individual MAC address for the controller from Dual Shock properties?
  • Remove all the switch code and make a ‘blank V-USB’ PlatformIO project for easy use.
  • Maybe I could hold off from polling the Dual Shock until the very last minute before V-USB gets an interrupt request, and reduce latency by a millisecond or so?

and, of course:

  • Make an enclosure to store this permanently, and move the components onto a PCB.

I suspect I will post about this project at least once more - covering the implementation of some of the enhancements, and maybe summarizing the entire project - the whole thing in less detail (these posts have become quite long!)

In the meantime, I’ll be using this to finish Breath of the Wild just in time for Tears of the Kingdom 😃.

Thanks for following along! I’d love to hear more ideas about where to go next.

You can follow the code for this series on GitHub. The main branch there will grow as this series does. The blog_post_6 tag corresponds to the code as it is (was) at the end of this post.

All the posts in this series will be listed here as they appear.


  1. Historically, the ‘controller’ was known as the ‘master’, and the ‘peripheral’ as the ‘slave’. No-one controls SPI protocol - it’s an ad-hoc standard - but in 2022, the Open Source Hardware Association proposed the new controller/peripheral terminology. The industry seems to be slowly accepting this terminology.

    Texas Instruments has switched in new documentation and in revisions of old documentation, for example.

    Very confusingly, Microchip (the manufacturers of AVR chips like the ATmega series) seems to have somehow landed on a third, worse, way to rename things. Recent data sheets (for example, here’s the data sheet for the newish ATtiny13A) still use ‘MISO’ and ‘MOSI’ as abbreviations, but in full text use ‘Host’ and ‘Client’. So MISO stands for “Host In Client Out”. 🙄. ↩︎

  2. Drifting far too slowly. I really do not understand why some resist changes like this - but even if I did, wow, it would be better if we all just switched and everyone was using the same names. The direction is clear, so not switching is just prolonging confusion. ↩︎

  3. The ‘~’ signifies an ‘active low’ signal. ↩︎

  4. Jacques Gagnon’s ‘PlayStation & PlayStation 2 SPI interface’ hackday.io page has a great rundown of all the commands. ↩︎

  5. Remember, back in the old days you had to press the ‘ANALOG’ button to make the analog sticks work! ↩︎

  6. If you’re not familiar with the suffixes like e.g. :4 and :1, the tell the compiler that the struct components being defined are explicitly e.g. 4 and 1 bits long. ↩︎