Switch Dual Shock adapter part 7: Iteration
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.
It’s been a couple of weeks, and I’ve still be working on the Dual Shock to Switch adapter in between using it to play Zelda. I’m glad to report it is very reliable!
I want to write about the changes, but I won’t go into as much detail as I did in previous posts. Earlier, I wanted to document the use of V-USB, get into details of the Pro Controller, and just generally document as I explored how to go about a project like this. But the project has now got to the stage where in-depth exploration of the changes is getting less and less interesting.
So, instead, I’ll just write a little about the latest changes, and you can take a look at the source (or get in touch!) if you’re interested.
In non-chronological order, since the last post:
Small changes
I made the deadzone circular instead of square (d’oh!)
I record where stick state will be stored in the report and wait until just before we are going to send it to the switch to sample the Dual Shock, to decrease latency. I also wait an extra millisecond before preparing the report to V-USB. It’s not necessary to prepare it immediately when V-USB is ready to receive it, it just needs to be done before it’s about to start sending it, so I can shave a millisecond of latency off here.
There are plenty of pins on the ATMega still available, so I used V-USB’s “USB_CFG_PULLUP_IOPORTNAME” and “USB_CFG_PULLUP_BIT” feature and attached the 1.5k USB DATA- pull-up to an ATmega pin rather than +V. This allow V-USB to perform USB connection and disconnection ‘properly’, by pulling it up and down in software.
Arduino Expungement
Using the debug serial output would make USB communication unreliable. I figured that this was because MiniCore (the Arduino framework) uses the serial transmission interrupt (USART_TX_vect
) to be informed about when it’s time to send a new character - and that its interrupt routine was sometimes delaying V-USB’s interrupt handler long enough that it failed to read packets correctly. This is just like what was happening with the Timer0 interrupt before. To solve this, I made a very simple polling-driven serial transmitter and am now using that. This enables much more reliable - and therefore allows more robust - debug output.
After that, having been bitten twice by it using interrupts I didn’t know about, I decided to remove dependency on the Arduino framework entirely! I was still using was some time related routines, so I switched to the AVR Libc’s built in delay routines, and implemented my own very minimal millisecond timer using the AVR’s Timer0. I also had to implement my own main()
to replace the one that framework provides.
All of this had the added advantage of saved a bunch of program space too!
ANALOG Home
My favourite additions are about the ANALOG button.
First, I automatically ensure analog mode is on (so the analog sticks always work).
Then (this is the good bit!) I made the ‘ANALOG’ button work as the ‘home’ button! This required watching for if the Dual Shock switches out of analog mode (because the user pressed the button), and then automatically switching it back on, and treating it as a short home button press.
Unfortunately it’s not possible to tell when the user releases the button, so I can’t make a button hold bring up the “Quick Settings” sidebar. You can, however, now press ANALOG to go to the home screen and sleep the Switch without getting up from the couch.
Sending commands to the Dual Shock is surprisingly unreliable. It improved a little when I implemented the use of the Dual Shock ‘acknowledge’ line to check it had processed each byte, but it still fails sometimes. I made a re-send mechanism.
This is all driven by code in prepareInputSubReportInBuffer(...)
and dualShockCommand_P()
.
Overall, the communication scheme here got kind of convoluted, and the resulting prepareInputSubReportInBuffer(...)
is some of the code I’m least happy with. But it’s working. I may need to rework it to something more flexible if I implement vibration support, because that will necessitate more complex communication with the Dual Shock.
Suspension
I removed the sendReportBlocking()
function, and moved all interfacing with V-USB’s data buffering into the main loop - this made all the rest of the things in this section easier.
Previously some of the communication code would get confused and usually call halt()
when the switch powered down. After implementing a couple more UART commands this no longer happens, and the code happily just continues - and the controller still works - after the switch wakes up again.
I now switch the ATmega into power-down mode when USB is suspended, and rely on the firing of interrupt 0 when there’s USB traffic to start the ATmega back up. Interrupt 0 is already the interrupt that the USB DATA- line is connected to - V-USB is already using it. Only level-driven interrupts will wake the ATmega8A from power down mode, so I had to change usbconfig.h
to cause V-USB to set it up as a low-level interrupt instead of a falling edge interrupt. Happily this works fine and none of V-USB’s code had to change.
To set up power down mode, I set up the desired sleep mode at the start of setup()
, and track the state of USB in the main loop. Search for the sUsbSuspended
variable to see what’s going on.
I was hoping to wake the Switch on a button press on the Dual Shock - but it doesn’t work. ‘Remote Wake’ is a standard USB feature, but I tried implementing it and apparently the Switch doesn’t support it. Reportedly, no commercial wired controllers can wake the Switch either. I’ll just need to hit the physical power button before I sit down on the couch :-(
After doing all this suspend-related work, I took some power measurements.
The system uses very little power (as does the Dual Shock, which surprised me for some reason). I saw an average of 34mA with both LEDs (power and debug) on, and 19mA with no LEDs on. Well within the 100mA we claim in usbconfig.h
.
When USB is suspended, and the ATmega enters power-down, it uses only 4.2mA. This isn’t spec compliant - the USB spec calls for 2.5mA max sustained current draw during suspend. But it’s good enough for real life. If I were to switch off the Dual Shock during suspend, that would probably bring us within range. That could be done by connecting the Dual Shock’s GND pin to a transistor controlled by the ATmega - or potentially just connecting its GND pin to a ATmega pin that I pull low to switch on the Dual Shock, and switch to ‘input’ (high impedance) to switch it off. But I’m worried that would be a bad idea if I ever implement vibration, because the current draw could become too large. I’ve left it always-on for now.
Big change with little results
I had a reasonably elaborate mechanism in usbFunctionWriteOutOrAbandon(...)
to know when the Switch was done sending us a multi-packet command. This turns out to be unnecessary.
It turns out that I can use the usual USB mechanism to know when packets for a report are over. Now, I accumulate data until a ‘short’ (less than eight bytes) packet is received, or a maximum length report is received.
Previously, especially for ‘UART’ commands, I was starting to process them too early. This turned out fine because their later portions were mostly zeros - and, anyway, I never needed any of the data in them. After doing this, I was then ignoring subsequent packets that were actually part of earlier commands without realizing it: this is where most of the mysterious packets I thought had report ID 0 were coming from! They were actually ‘empty’ later parts of real commands!
I had actually tried using the usual USB mechanism to know when reports are done before - but it didn’t work. I think this was because I had the interrupt frequency for the OUT endpoint set too low, and it was causing the Switch to abandon packets early because it didn’t have enough time to complete them before it sent the next packet. I’ve turned the interrupt period for the OUT endpoint down to 3 (from 8) and things seem to be working well.
This hasn’t actually changed reliability overall - I was able to play Zelda for hours with no glitches before, with my old wrong-but-works code - but it does means that the communication is now less special-cased and more ‘normal USB’ than before, which is pleasing.
Still to come
Real hardware! I’ve designed a PCB and will make an enclosure - I’ll post more on that later. Although I guess I really should get a schematic into the project - I’ve just posted breadboard photos so far…
One thing I might explore is allowing calibration with the Switch’s built in calibration interface - which I think intelligently sets up the dead zone and axis scaling and stores it on the controller? I’ve never felt the need to use it so far…
The only real big controller feature missing is vibration. Switch controllers have a very rich vibration mechanism, allowing both frequency and amplitude to be controlled independently. The Dual Shock just has 0-255 motor speed for left and right. Maybe I could measure the frequency of each speed on my physical controller and hard-code some sort of map?
You can follow the code for this series on GitHub. The main
branch there will grow as this series does. The blog_post_7
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.