DualShock4 Reverse Engineering - Part 2
In case you missed it, here is Part 1, where you can read about this DualShock 4 totally messed up that I’m trying to repair.
So, let’s go on with this journey!
At this point I have a working linux driver and finally I can start testing by myself what is working and what is not.
- Bluetooth is totally not working
- Battery controller is not working
- Touchpad not working
- IMU has no calibration
- Broken LED + JDS-055 secondary-board
What a nice gift!
Only last point is not interesting, I just ordered a new JDS-055 one from AliExpress which will fix the broken LED. In the meantime I use another secondary-board from a working DS4 I have at home.
Probably every PS4 user knows that pressing the combo
PS + Share
sends the controller into Bluetooth Pairing Mode, the LED starts blinking white
and the DS4 can be connected to a smartphone or a computer.
This is a good way to test what kind of issue with Bluetooth we have. Maybe the antenna is broken and we can pair only with a device extremely close. Or maybe the LED blinks but no signal is emitted.
After connecting the JDS-055 of another DS4 to have a fully working LED,
double-checking that the LED is working by connecting it to my laptop and
hid-sony driver, we can finally try the
PS + Share combo.
What do you expect? Nothing happens on this DS4: not only it does not enable pairing with other devices, it does not even make the LED blink white! Totally dead.
Just to be 100% sure the antenna is OK, I tested it with a super-powa oscilloscope borrowed from a friend to see if there are at least attempts by the DS4 to transmit anything.
Well, zero emissions such a green device. In the JDM-055 board, the chip responsible for Bluetooth is the main microcontroller (MediaTek MT3160N), so we cannot even blame the connection between the main CPU and the Bluetooth transponder.
In the JDM-055 the battery is controlled by the main microcontroller, no external controllers. What is the problem here? It refuses both to charge the battery and to show any kind of statistics about that.
The linux driver is well written and exposes all battery stats to
/sys/class/power_supply/ps-controller-battery-<bt-mac-address>/, this is the result:
$ cat status Unknown $ cat capacity 0 $ cat present 1 # even without the battery
Even worse, the controller seems refusing to boot if you try to power it on
using a battery. If you press the
PS button on a working DS4, the controller
turns on and the LED starts fading in and out. If you press the
with this controller, well… nothing happens .
So you may think that the battery port is broken or disconnected. Not even close, connecting a bench power supply to the battery port shows that the DS4 drains 30/40mA for the first 10-30 seconds (don’t remember precisely) and then goes to sleep.
The hardware seems OK: If I connect the battery or the touchpad into another DualShock4, they work perfectly. If I connect a tested battery or touchpad into this controller, they do not work.
This starts to remind me this joke :
A man goes to the doctor and says, “Doctor, wherever I touch, it hurts.”
The doctor asks, “What do you mean?”
The man says, “When I touch my shoulder, it really hurts. When I touch my knee - OUCH! When I touch my forehead, it really, really hurts.”
The doctor says, “I know what’s wrong with you. You’ve broken your finger!”
Maybe, just maybe, even if there are 100 different issues, the only real problem is in the microcontroller, maybe a bad firmware update?
Well, let’s try to restore the firmware!
Sure. I dare you to find online any information about firmware updates for a DS4. It seems that updates are really common with the new DualSense PS5 Controller but not with previous models, like the DualShock 4.
After googling a bit about how to repair my controller, this interesting page got all my attention.
The repo linked is not available anymore due to DMCA takedown but there is a mirror on GitHub available, even if some files are missing.
The modding community seems to be focused more on the communication between the DS4 and the PS4, to emulate a fake controller in a way that is accepted by the PS4. That part is working 100% on my controller, so I’m going to focus on other aspects of the reverse engineering.
jedi_tool.py from that repository is really interesting.
It tries to connect to the USB device
054c:05c4 and sends a request to
obtain the Bluetooth MAC address and some firmware information.
For the record,
054c is the Vendor ID of Sony,
05c4 is the Device ID of the
old DualShock4 V1, I’d say board JDM-001 or something like the JDM-030.
It’s an old script, I know.
Here’s the main application code:
bt_addrs = get_bt_mac_addrs() print('ds4 bt mac: %s host bt mac: %s' % (binascii.hexlify(bt_addrs), binascii.hexlify(bt_addrs))) print(get_version_info()) exit()
Let’s just change the Device ID with the one of my DS4 V2:
054c:09cc and run it:
$ python3 jedi_tool.py ds4 bt mac: b'000000000000' host bt mac: b'947665e36628' Compiled at: Sep 21 2018 04:50:51 hw_ver:0100.ff08 sw_ver:00000001.a00a sw_series:2010 code size:0002a000
OK, really interesting.
First because this script works with the new version of the DS4, so the protocol is almost the same. Second because the local Bluetooth MAC Address is all zeros. Third because there are some interesting infos about the firmware, like the build date and the version.
This is the output with another DS4 I have:
$ python3 jedi_tool.py ds4 bt mac: b'28516511aea4' host bt mac: b'9081ee0cdaf0' Compiled at: Sep 21 2018 04:50:51 hw_ver:0100.b400 sw_ver:00000001.a00a sw_series:2010 code size:0002a000
The Bluetooth MAC Address is there, same build date, same software version but slightly different hardware.
Perfect, we are making progress! At least in understanding of what is broken.
After this, I spent some time reading both the
hid-sony linux driver, the new
hid-playstation driver and this
jedi_tool.py trying to understand a bit
more about which commands I can send to the DS4 and what information I can get
Here is what I figured out.
The DS4 uses a similar protocol between Bluetooth and USB, it consists in three kinds of packets:
- HID Input Report: DS4 -> Host
- HID Output Report: Host -> DS4
- HID Feature Report
Input reports are used by the DS4 to inform the host about the status of the buttons, IMUs & co.
Output reports are sent by the Host to the DS4 to change LEDs, make the motors rumble, etc..
Both of them are well documented online, either in the linux driver or in other projects like DS4Windows.
The Feature Reports are way more interesting, these are the ones used to obtain information or change something about the device.
The DS4 differentiate these reports into two categories.
I use the names as they are reported in the
jedi_tool.py: HID Get Report
(Type ID 0x01) and HID Set Report (Type ID 0x09).
- The Hid Get Report is a request to read some information.
- The Hid Set Report is a request to change something.
Obviously, the fact that the GetReport should not change anything depends on the firmware implementation!
After the Type ID byte there is a byte identifying the Request ID. From both the hid-sony and hid-playstation linux kernel drivers I see these feature requests:
|GET||0x02||36||Get Calibration Data|
|GET||0x05||40||Get Calibration Data via Bluetooth (same + 2 bytes of CRC)|
|GET||0x12||15||Get Pairing Info|
|GET||0x81||6||Get MAC Address <- hey that’s the one that clones do not implement!|
|GET||0xA3||48||Get Version Info|
Well, interesting. These instead are the feature requests written in the
|SET||0x08||Set Flash Read Pos||3 bytes: 0xff + 2 bytes for the offset|
|GET||0x11||Read from Flash||2 bytes for the payload|
|SET||0x13||Set BT Link Info||
|SET||0xA1||Enable Bluetooth||1 byte: 1 or 0|
|SET||0xA2||Enable DFU Mode||1 byte: 1 or 0|
|GET||0xA3||Get Version Info||0x30 bytes for the payload|
Woah, this is starting to be a really nice playground!
Playing with HID Requests
Okk now we have a lot of things to play with!
Let’s start with something easy:
GET 0xA3 which explains how
is able to obtain the firmware version.
It’s easy to add to
jedi_tool a function to dump the output of a GET request
as an hex-string:
def dump_req(rid, size): try: buf = hid_get_report(dev, rid, size) strs = ["%02x" % (int(i),) for i in buf] print("RID:0x%02x data: " % (rid, ) + ("".join(strs))) except: pass
And now we can try to read the raw data of the Version Info with
$ python3 jedi_tool.py RID:0xa3 data: 5365702032312032303138000000000030343a35303a35310000000000000000000100b4010000000aa0102000a00200 $ python3 -c 'print(bytes.fromhex("5365702032312032303138000000000030343a35303a35310000000000000000000100b4010000000aa0102000a00200"))' b'Sep 21 2018\x00\x00\x00\x00\x0004:50:51\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\xb4\x01\x00\x00\x00\n\xa0\x10 \x00\xa0\x02\x00'
Good, our code to dump GET requests seems working.
Get Calibration Data
Now we can try something more interesting, let’s see what happens if we call
GET 0x02 (Calibration Data) on two DS4, a working one and the broken one.
Understanding which DS4 is which is left as an exercise to the reader .
DualShock4 number one:
RID:0x02 data: ffff0000050090226bdd812285dd982277dd1c021c02ab1f55e04320bddffb1f06e00500
DualShock4 number two:
RID:0x02 data: 000000000000000000000000000000000000000000000000000000000000000000000000
Pretty clear I would say . For some reason, the broken one does not have any calibration data for us, as expected by the crash shown in the previous blog post.
jedi_tool.py there is a function called
bt_enable() which calls
That’s it folks, I think we solved one issue!
I call that function connecting my broken DS4, the call is executed correctly aand..
Nope. Nothing seems to be changed on the broken DS4.
I’m coward and I don’t want to try it on a working DS4, maybe in future.
Dump the Flash
jedi_tool.py there is a very interesting function called
dump_flash_mirror() that combines
SET 0x08 with
GET 0x11 to dump the
Here’s a simplified version of the code in
def flash_mirror_read(offset): # Read a single word assert offset < 0x800, 'flash mirror offset out of bounds' hid_set_report(dev, 0x08, struct.pack('>BH', 0xff, offset)) return hid_get_report(dev, 0x11, 2) def dump_flash_mirror(path): # Read the whole flash print('dumping flash mirror to %s...' % (path)) with open(path, 'wb') as f: for i in range(0, 0x800, 2): word = flash_mirror_read(i) f.write(word) print('done')
It seems to dump.. “something ” into a file. Something that is big 0x800 bytes, too few for a ROM. Probably a secondary memory?
Anyway, let’s try to understand how it works. It uses
SET 0x08 (with the
first payload byte to
0xff) to set the read offset and then
GET 0x11 to
read these two bytes of flash.
Actually this code works amazingly and dumps 0x800 bytes from the controller! To understand what is this memory, it’s better to have two memory dumps.
One from the broken DS4, one from the working one and diff them using
BTW let me know if you use better tools to do this kind of diffs!
In this picture you can see above the memory of the working DS4 and below the broken one. The highlighted bytes are the ones that differ between them:
The broken one is basically 99% of zeros.
For those who already reverse-engineered the DS4 memory, you can see that there
is a strange 0x03 at line
1E6, well… the memory dump reported in this
picture is not the original broken memory but a close reconstruction.
Analyze the memory dump
So at this point, we can start to search for known patterns in the memory dump of the good DS4. For example the Bluetooth MAC Address or the IMU calibration data.
And.. Yes! Can you spot them?
The calibration data is located at line
1E6, starting with
The MAC Address can be found at line
Both of them are all zeros in the broken DS4.
Probably the main issue with this DualShock 4 is that the flash has been wiped out for some reason.
At this point I’m genuinely curious about the story of this controller. What kind of behaviour can wipe out the configuration memory?
So I asked the person who gave me this gift to contact the previous owner and ask him the story of this controller and how it got broken in this way.
Sad to say that the only answer we got is: “A friend gave it to me because it was not working”, without any further explaination. Also, few minutes later he blocked us .
This journey will continue in Part 3, where we will see how the DFU mode works and play a bit more with these requests.
Thank you for reading!
EDIT: Part 3 here
If you want to get in touch, drop me an email at
email@example.com or ping me on IRC
the_al@freenode|libera|hackint) or Discord (