Hello guys,

Since I wrote Part 5, which was about calibrating the analog sticks of a DualShock4, I got lots of messages about porting the calibration script to the DualSense, the controller of the PlayStation 5.

With this post I want to show you this script and how I managed to do it (spoiler: with a lot of luck :joy:).

DualSense

The DualShock4 calibration tool

Few months ago I wrote this Python script that triggers two kind of recalibration on a DualShock 4:

  • Save the current position of the analog sticks as the new center (0,0)
  • Record the range of the sticks and store it

This is useful mainly when you replace a broken stick with a new one: after the replacement, the stick is usually not centered. To complete the repair, you should carefully add resistors or do some electronics hacks to match the center and range of the old potentiometer.

With the calibration script, you just solder the new stick, follow the instructions on the screen and the DS4 is ready to go in few seconds.

As you can imagine, this got some popularity in the repair community and I got lots of requests to create something similar for the DualSense. GsmHack4You also shipped one to play with! :astonished:

I’m glad to announce that I just released a script to calibrate it, which seems to be working. :tada:

The new DualSense calibration tool

You can find the script here.

It’s quite simple to use: no more ds4-util to change the flash mirror status, all functionalities are available in just in one script.

To calibrate and lose changes after a reset (useful to test):

  • Calibrate center: python ./ds5-calibration-tool.py analog-center
  • Calibrate range: python ./ds5-calibration-tool.py analog-range

To calibrate and make changes permanent (the old set-flash-mirror-status permanent):

  • Calibrate center: python ./ds5-calibration-tool.py -p analog-center
  • Calibrate range: python ./ds5-calibration-tool.py -p analog-range

On the first run I suggest not to provide -p and see if everything works well, this is a script I wrote last night, tested only on my DualSense with an old firmware. There are chances that it may brick your controller!

Ok good, but how does it work under the hood?

DualSense HID commands

The DualSense uses the same protocol as the DS4: HID over USB (or bluetooth).

There are few gotchas to know if you want to play with it:

  • The battery should be connected, otherwise good part of the commands are disabled (wtf..)
  • The maximum payload for the GET requests is 63 bytes and not 256 as the DS4. If you provide a number greater than 63, you get nothing.

There is already a lot of documentation here about the feature requests, I started playing with them.

For example, GET 0x20 (Firmware version) replies these bytes on my controller:

446563203136203230323230323a34343a333103000400100[...]

which decoded is this text:

Dec 16 202202:44:31[...]

Porting the calibration to the DualSense

The calibration works in two steps:

  1. Calibrate
  2. Find a way to store the calibration permanently (“set-flash-mirror-status” in the DS4).

The first point is the easiest part, we are lucky that the authors of the firmware kept the same calibration code and just changed the Feature ID.

I had a look at this wiki and found that there are both commands used in the DS4: Set Calibration and Get Calibration, but their id changed to 0x82 and 0x83.

A search & replace in the ds4-calibration-tool (with minor fixes on top of it) worked out-of-the-box!

Yep, and that was easy.

Store the calibration permanently

The really difficult part is to store the calibration permanently, so that it persists after a reset.

On the DS4 there is a concept of “flash-mirror”: the configuration flash is copied in RAM during startup. Any change happens in RAM and is propagated to the flash only if a flag is set. This was described in Part 4.

Thanks to a leaked firmware, we found out that the command SET 0xa0 0x0a 2 [4-bytes password] (yes, there is a password) configures the flash-mirror to store permanently the changes. The command that sets it to “read-only” has no password: SET 0xa0 0x0a 1.

Before trying to port it to the DualSense, let’s decompose it:

  • SET 0xa0 is the Report ID, this was called “SET test command” in some leaked source, it does multiple things depending on the sub-commands
  • 0x0a is the command id, we can call it “change flash-mirror status”
  • {1, 2} is the action, which means 1=temporary, 2=permanent
  • if the SubAction is 2, there is a 4-byte password as payload

The DS4 firmware also has a corresponding “Get Test Result” command (0xa1) which tells you if the command has been executed successfully or not (e.g. if the password is not correct). This can be used to guess the password!

Ok, how do we port this to the DualSense?

My attempts

As first attempt I tried to execute the exact same command on the DualSense.

The Controllers wiki shows that there are both Set test command (0x80) and the corresponding Get test result (0x81).

God only knows what I changed in my DualSense with these commands:

SET 0x80 0x0A 2 0:
GET 0x81: 0a020201000000000000

SET 0x80 0x0A 5 0
GET 0x81: 0a050239000000000000

SET 0x80 0x0A 6 0
GET 0x81: 0a060100000000000000 which changes after a while to 0a060239000000000

For sure, the flash-mirror never changed. :cry:

After spending some nights playing with all the sub-commands under SET 0xa0 without any good result, I found an interesting (but unrelated) command:

SET 0x80 0x0d 0x03 GG RR BB WW XX YY ZZ

which seems to test all the LEDs available on the controller: every byte in the payload (GG, RR, ..) is the brightness of each available LED.

Last hope

After giving up with the reverse engineering, I found this wiki page which contains a list of some valid commands for the DualSense. NVS Unlock immediately caught my attention.

The structure is the same as the “set flash mirror status” command for the DS4:

SET 0x80 3 2 [4-bytes password]

And yes, this is working perfectly on my DualSense. :astonished: :tada:

Thanks to the similarities with the DS4, it was easy to guess the corresponding “lock” command:

SET 0x80 3 1

Something changed with respect to the DS4: this command does not show anymore the output, which was my only hope to find the password. This is why during my reverse-engineering I skipped Command 3 in the first place. The authors of the firmware designed one single trap and I fell on it :sweat_smile:

With these two parts (Calibration + NVSUnlock), I managed to successfully calibrate the sticks of my DualSense and released the tool.

Acknowledgments

A project like this doesn’t come together without the generous contributions and support of several individuals. I am immensely grateful to the following individuals for their invaluable assistance and contributions:

  • nielk1: I extend my sincerest gratitude for their pioneering work on DualSense and their invaluable assistance with DS4/DS5.
  • zecoxao and Abkarino: Thanks for sharing their discoveries in the wiki, which significantly aided in advancing this project.
  • GsmHack4You: Special appreciation for generously providing me both a DualSense and an Xbox Controller (and even a pack of gummy candies shaped like joysticks!).
  • All the PS Unlocked & Game Controller Collective Discord users: A collective shoutout to these communities whose insights and discussions enriched this project immensely.

Without the collaborative efforts and support of these individuals and communities, this endeavor wouldn’t have been possible. Thank you all for your invaluable contributions and encouragement.

Conclusion

Thank you for your time! :heart:

All these discoveries have been integrated into my ds4-tools repo, which now contains a new script that allows you to calibrate the DualSense.

On a side note, the firmware of the DualSense is updated regularly, so this may not work with your controller or may stop working in the future.

EDIT: I created a web-ui tool to calibrate DS4/DS5 without installing any software on your PC, still in beta but you can give it a try :)

~~~

If you want to get in touch, drop me an email at ds4@the.al or ping me on IRC (the_al@freenode|libera|hackint) or Discord (the_al).