This year I got a very special gift for Christmas: a broken DualShock 4. :smile: You may say it’s a relatively poor gift, but the person who gave me this knows me very well. This gift kept me interested in it for more than one week, in every moment I had spare. :joy:

With respect to other DS4 I repaired, this one has been so far the most difficult one. Others had just an analog control drifting, a broken button or something else extremely easy to fix. Usually it takes one hour more or less to find the issue and fix it. But not for this one.

The DualShock 4

The DualShock 4 is the official controller of the PlayStation 4.

There are three circuit boards inside a DS4: a main-board, a secondary-board and the touchpad.

  • The main-board is responsible for 99% of the tasks, it is connected to the buttons, touchpad, motors, battery and the secondary-board. It handles audio, battery stats, sensors, motors, etc. It has a JDM-xxx label written on it.
  • The secondary-board is a small board responsible for USB connection and it has the RGB LED on top of it. It is connected to the main-board via a flat cable and is identified by the label JDS-xxx.
  • The touchpad has its own plastic case and is connected directly to the main-board using a flat cable.

I prefer not to spend too many words in describing the hardware, you can find tons and tons of info online, here is a collection of links to start with:

Speaking of reverse engineering, a lot of work has been already done. At the moment you can pair a DS4 with a computer and it works amazingly. There are open-source drivers that do almost everything for you: I/O, leds, audio.

In these blog posts I’m going to reverse engineer other parts of the DS4 that are not of general interest but maybe interesting for those of you interested in some hidden functionalities of it.

Anyway, let’s go back on my christmas gift! :smile:

The gift

The controller I got is an official DualShock4 V2 CUH-ZCT2E, with a JDM-055 motherboard, latest version AFAIK. The gift was bought from FB Marketplace, the description said it was working only via USB and not Bluetooth, and the issues reported by the previous owner (I didn’t talk to him personally) were:

  • Broken led, literally removed from the secondary-board :scream:
  • Drifting analog controls
  • Battery not working
  • Bluetooth not working

Some part of the plastic cover is broken, so I suspect it fell down so hard that the led popped off from the JDS-055 board. After few days of investigation I discovered other issues, but we are going to discuss them in Part 2 :smile:.

Anyway, here are some pictures of the motherboard:

JDM-055 Front JDM-055 Back
JDM-055 Front JDM-055 Back
   

Original or clone?

In the past I had to deal with some DS4 clones. It’s easy to recognize if a DS4 is original or a clone after having it disassembled: the clones usually have a completely different motherboard than the original, at least the ones I got so far.

So, if you have a DS4-V2 and you read JDM-055 on the motherboard, JDS-055 on the secondary-board, usually it’s an original one. If you want to go deeper, you can google which microcontroller should be there and check if everything matches. For example, in this case the microcontroller is a Mediatek MT3610N, which is present.

Anyway, I discovered an easier way to find out if the DS4 is original or not, which does not require a disassembly, and of course, can give you false positives:

Connect it via USB to a Linux computer, look into dmesg (kernel log messages) and see if there is an error regarding HID Report 0x81 right after connecting the DS4 via USB. If the error is there, the controller is a clone.

Pefect then, let’s connect the USB and…

[...] usb 1-2: new full-speed USB device number 3 using xhci_hcd
[...] usb 1-2: New USB device found, idVendor=054c, idProduct=09cc, bcdDevice= 1.00
[...] usb 1-2: New USB device strings: Mfr=1, Product=2, SerialNumber=0
[...] usb 1-2: Product: Wireless Controller
[...] usb 1-2: Manufacturer: Sony Interactive Entertainment
[...] input: Sony Interactive Entertainment Wireless Controller Touchpad as [...]
[...] input: Sony Interactive Entertainment Wireless Controller Motion Sensors as [...]

Seems working and does not report any error about HID Report 0x81, probably it’s not a clone! :smile:

And then, suddenly.. Caps Lock is blinking and the system is totally frozen :anguished:.

[...] divide error: 0000 [#1] PREEMPT SMP PTI
[...] CPU: 3 PID: 0 Comm: swapper/3 Tainted: G[....]
[...] RIP: 0010:dualshock4_parse_report.constprop.0+0xe8/0x5f0 [hid_sony]

Wait.. what? :astonished:

Even if the dmesg does not explicitly say that, the Caps Lock blinking and the system frozen means only one thing: Kernel Panic.

I try again few times and always get the same result.

For some reason, this DS4 causes a kernel panic reliably caused by the hid-sony driver.

What’s happening?

After a bit of investigation, I discovered what is the cause of the division by zero.

A DualShock4 has an IMU chip, which contains an Accelerometer and a Gyroscope. This IMU has a calibration, which tells to the DS4 user what is the range and bias of each sensor.

The hid-sony driver requests to the DS4 the calibration data of this IMU, so that it can show very well calibrated sensors to every user-space app.

The DS4 sends back these calibration data to the driver:

  • Gyroscope Pitch/Raw/Roll bias: 3 values, one per axis.
  • Gyroscope Pitch/Raw/Roll range (minimum and maximum): 6 values, 2 per axis.
  • Accelerometer X/Y/Z range (minimum and maximum): 6 values, 2 per axis.

For the record, all of these calibration values are unsigned int16.

The hid-sony driver processes and stores them in the array ds4_calib_data, where each element type is struct ds4_calibration_data.

/* Used for calibration of DS4 accelerometer and gyro. */
struct ds4_calibration_data {
	int abs_code;
	short bias;
	int sens_numer;
	int sens_denom; /* Here be dragons */
};

struct sony_sc { /* The context */
    /* [...] */
	struct ds4_calibration_data ds4_calib_data[6];
    /* [...] */
};

ds4_calib_data is later used in the dualshock4_parse_report() function, which is called every few milliseconds, reads the live sensor values, processes them and sends them to the user-space apps.

The interesting code is reported here:

static void dualshock4_parse_report(struct sony_sc *sc, u8 *rd, int size) {
    /* [...] */
    for (n = 0; n < 6; n++) {
        /* Store data in int for more precision during mult_frac. */
        int raw_data = (short)((rd[offset+1] << 8) | rd[offset]);
        struct ds4_calibration_data *calib = &sc->ds4_calib_data[n];
        
        /* CRASH HERE */
        int calib_data = mult_frac(calib->sens_numer,
                                   raw_data - calib->bias,
                                   calib->sens_denom);
        
        input_report_abs(sc->sensor_dev, calib->abs_code, calib_data);
        offset += 2;
    }
    input_sync(sc->sensor_dev);
    /* [...] */
}

As reported in the comment in the code above, the division by zero happens in the mult_frac() call.

Ok, let’s dig into the mult_frac() macro, this is how it’s implemented:

#define mult_frac(x, numer, denom)(  \
{                                    \
    typeof(x) quot = (x) / (denom);  \
    typeof(x) rem  = (x) % (denom);  \
    (quot * (numer)) + ((rem * (numer)) / (denom)); \
} \
)

Ok, I see a division there. The denominator is calib->sens_denom, which is provided by the DualShock4 and is not sanitized anywhere.

To sum-up: for each axis, the driver takes the raw value of the sensor and applies the calibration by computing a formula like this one: (not 100% correct but gives you the idea)

$$ y_{out} = k \cdot \frac{y_{raw} - y_{bias}}{y_{max} - y_{min}} $$

Where $ y_{raw} $ is the only value read from the sensor. All others are taken from the calibration when the DS4 is connected.

Now, if $ y_{max} = y_{min} $, the driver divides the raw value by zero. This explains the crash.

Debug and fix

Let’s change the driver so that it prints out the calibration data. In this way we can see what are the real values provided by the broken DS4:

[...] gyro_pitch_plus=0 gyro_pitch_minus=0 gyro_yaw_plus=0   gyro_yaw_minus=0
[...] gyro_roll_plus=0  gyro_roll_minus=0  gyro_speed_plus=0 gyro_speed_minus=0 
[...] acc_x_plus=0      acc_x_minus=0      acc_y_plus=0
[...] acc_y_minus=0     acc_z_plus=0       acc_z_minus=0

Nice, a very well calibrated controller! :chart_with_upwards_trend: This means that, with 99% of confidence, also the IMU is broken in this DS4.

To confirm this hypothesis, we can patch the driver with a workaround: put the denominator to 1 if it’s zero (to prevent the crash) and add a print in the dualshock4_parse_report() to see what are raw values ($y_{raw}$) received from the sensors.

I suspect they will be all zeros, this is what I expect from a broken IMU.

What happens, instead, is this:

[...] rd0=4 rd1=1  rd2=17 rd3=707 rd4=-8063 rd5=-1536 
[...] rd0=4 rd1=0  rd2=17 rd3=698 rd4=-8061 rd5=-1546 
[...] rd0=4 rd1=-1 rd2=19 rd3=699 rd4=-8062 rd5=-1544 
[...] rd0=3 rd1=0  rd2=19 rd3=695 rd4=-8052 rd5=-1554 
[...] rd0=4 rd1=-1 rd2=19 rd3=691 rd4=-8052 rd5=-1547 
[...] rd0=4 rd1=0  rd2=18 rd3=709 rd4=-8057 rd5=-1551 

Ok I’m confused :confused:, the IMU is working really well! The numbers that you see above are the raw values of gyroscope and accelerometer. Rotating the DS4 also causes all values to change along with my rotation.

Conclusion

Here’s the end of Part 1. The DS4 hardware seems to be an original one, but it does have something strange. The IMU calibration is totally broken, even if the IMU is working.

In the JDM-055, the IMU is an external chip. I still don’t know what is going on but the fact that we can receive raw IMU values but not the calibration makes me think that the latter is external to the IMU and has been zeroed for some reason.

I’ll publish soon Part 2 where the investigation of this DS4 will go on. As a preview I can say that also the Bluetooth MAC Address is all zeros, and this led me to investigate if I can restore them. :sunglasses:

Anyway, I wrote a fix for this bug in the linux driver and sent to the linux-kernel mailing list, so that a DS4 with a broken calibration does not cause a kernel panic.

Thanks for reading! :heart:

EDIT: Part 2 here

~~~

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).