SDL icon indicating copy to clipboard operation
SDL copied to clipboard

Inaccurate Switch controller gyro sensor data

Open ceski-1 opened this issue 6 months ago • 15 comments

Tested with an official Switch Pro Controller and SDL2 2.32.6. I didn't test SDL3 or the Switch accelerometer sensor data. This also happens with third-party controllers that behave like a Switch controller.

Minimal example:

int main(void)
{
    //SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
    //SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1");
    SDL_Init(SDL_INIT_GAMECONTROLLER);
    SDL_GameController *gamepad = SDL_GameControllerOpen(0);
    SDL_GameControllerSetSensorEnabled(gamepad, SDL_SENSOR_GYRO, SDL_TRUE);
    printf("%s\n", SDL_GameControllerName(gamepad));
    printf("Rotate controller by 360 degrees then press a button...\n");

    float angle = 0.0f;
    int done = 0;
    InitTimer(); // First call to a performance counter

    while (!done)
    {
        SDL_Delay(1);
        SDL_PumpEvents();

        float data[3];
        SDL_GameControllerGetSensorData(gamepad, SDL_SENSOR_GYRO, data, 3);
        angle += data[1] * GetDeltaSeconds() * 180.0f / 3.1415927f;

        SDL_Event event;
        while (SDL_PeepEvents(&event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT) == 1)
        {
            switch (event.type)
            {
                case SDL_CONTROLLERBUTTONDOWN:
                    angle = SDL_fabsf(angle);
                    printf("Rotated %.1f deg (%.1f deg error, %.3f ratio)\n",
                           angle, angle - 360.0f, angle / 360.0f);
                    angle = 0.0f;
                    break;

                case SDL_QUIT:
                    done = 1;
                    break;
            }
        }
    }

    SDL_GameControllerClose(gamepad);
    SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
    SDL_Quit();
    return 0;
}

Output:

Nintendo Switch Pro Controller
Rotate controller by 360 degrees then press a button...
Rotated 402.5 deg (42.5 deg error, 1.118 ratio)
Rotated 404.7 deg (44.7 deg error, 1.124 ratio)
Rotated 406.5 deg (46.5 deg error, 1.129 ratio)
Rotated 407.1 deg (47.1 deg error, 1.131 ratio)

ceski-1 avatar Jun 09 '25 19:06 ceski-1

You need to handle events and use the sensor timestamp in the event to get precise gyro values.

slouken avatar Jun 09 '25 22:06 slouken

You need to handle events and use the sensor timestamp in the event to get precise gyro values.

That appears to have the same issue:

Above code with some changes:

case SDL_CONTROLLERSENSORUPDATE:
    if (event.csensor.sensor == SDL_SENSOR_GYRO)
    {
        SDL_ControllerSensorEvent *c = &event.csensor;
        static uint64_t last_time;
        if (!last_time)
        {
            last_time = c->timestamp_us;
        }
        float dt = (c->timestamp_us - last_time) * 1.0e-6f;
        last_time = c->timestamp_us;
        angle += c->data[1] * dt * 180.0f / 3.1415927f;
    }
    break;

Output:

Nintendo Switch Pro Controller
Rotate controller by 360 degrees then press a button...
Rotated 406.3 deg (46.3 deg error, 1.129 ratio)
Rotated 404.9 deg (44.9 deg error, 1.125 ratio)
Rotated 412.4 deg (52.4 deg error, 1.146 ratio)
Rotated 412.3 deg (52.3 deg error, 1.145 ratio)

ceski-1 avatar Jun 09 '25 22:06 ceski-1

@HilariousCow, can you check this out?

slouken avatar Jun 09 '25 23:06 slouken

Is the wrong scale being used here? https://github.com/libsdl-org/SDL/blob/main/src/joystick/hidapi/SDL_hidapi_switch.c#L62 https://github.com/libsdl-org/SDL/blob/SDL2/src/joystick/hidapi/SDL_hidapi_switch.c#L66

Source: https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/blob/master/imu_sensor_notes.md#default-saturation-free-lsm6ds3-2000-dps--70-mdpsdigit

float corrected_data = c->data[1] * 816.0f / 936.0f;
angle += corrected_data * dt * 180.0f / 3.1415927f;

Output:

Nintendo Switch Pro Controller
Rotate controller by 360 degrees then press a button...
Rotated 362.0 deg (2.0 deg error, 1.006 ratio)
Rotated 355.4 deg (-4.6 deg error, 0.987 ratio)
Rotated 361.7 deg (1.7 deg error, 1.005 ratio)
Rotated 358.9 deg (-1.1 deg error, 0.997 ratio)

ceski-1 avatar Jun 09 '25 23:06 ceski-1

Hmm, that seems very likely based on your results.

slouken avatar Jun 09 '25 23:06 slouken

I'm compelled to go with the dekuNukem, since I'm certain they've more expertise than I do.

While I believe that this has been off for a while, I have seen different controllers have different degrees per second ratings, with no real difference between controllers except for serial number. It's possible some of these controllers are calibrated differently at the factory. It's possible that some are simply subtly broken. It's hard to know what the true standard is.

Basically, if you test this across multiple pro controllers from different manufacturers, do you get the same result?

I'll note that we load up calibration data which makes adjustments to the DPS rate, but I don't fully understand if that is actually any good, since it was probably based on an older reverse engineer. (I'll ask the relevant people who were there at the time.)

Incidentally, I am working on extra instruments in testcontroller to display the integrated rotation over time, so that we can catch this kind of thing earlier. I think that will allow us to gather more data about which controllers are lining up and which are either differently calibrated, or if it's just generally an inconsistent scale between controllers.

HilariousCow avatar Jun 10 '25 00:06 HilariousCow

I'll also note that they believe each (individual) imu packet has a 5ms interval, whereas the current SDL3 code seems to base this off "time since last packet received". That tends to be fine for a wired connection, but is subject to delay and racing over bluetooth.

Using a fixed time interval strategy might result in more stability over bluetooth, but it all depends on whether or not packets are dropping.

I.e. instead of getting SDL_GetTicksNS(), just increment the time stamp by 5ms:


            // We got three IMU samples, calculate the IMU update rate and timestamps
            ctx->m_unIMUSamples += 3;
            if (ctx->m_unIMUSamples >= IMU_UPDATE_RATE_SAMPLE_FREQUENCY) {
                Uint64 now = SDL_GetTicksNS();
                Uint64 elapsed = (now - ctx->m_ulIMUSampleTimestampNS);

                if (elapsed > 0) {
                    ctx->m_ulIMUUpdateIntervalNS = elapsed / ctx->m_unIMUSamples;
                }
                ctx->m_unIMUSamples = 0;
                ctx->m_ulIMUSampleTimestampNS = now;
            }

            ctx->m_ulTimestampNS += ctx->m_ulIMUUpdateIntervalNS;
            sensor_timestamp[0] = ctx->m_ulTimestampNS;
            ctx->m_ulTimestampNS += ctx->m_ulIMUUpdateIntervalNS;
            sensor_timestamp[1] = ctx->m_ulTimestampNS;
            ctx->m_ulTimestampNS += ctx->m_ulIMUUpdateIntervalNS;
            sensor_timestamp[2] = ctx->m_ulTimestampNS;

HilariousCow avatar Jun 10 '25 00:06 HilariousCow

When your tool is done, we can try it across a variety of Switch Pro and 8BitDo controllers to get more data.

slouken avatar Jun 10 '25 00:06 slouken

Sorry for the delay. Work on the tool was quite involved.

Focusing on Switch Pro controllers for now. Joycons later. I have 3 switch controllers infront of me. One is mine, two are Slouken's. One of Slouken's has old firmware intentionally.

Changing 936->816 initially fixed my switch controller, but both of Slouken's controllers prefer the original 936. So there's a real problem there.

The testing tool has come in handy. For a start, the above code was causing the polling rate to be incorrect for periods of time, causing inconsistency. I removed that to make evaluation easier, and found that bluetooth packets come at a rate of 200hz, and wired packets come at a rate of 255hz. This seems pretty consistent across all controllers I tested so far.

In order to differentiate between controllers, and help identify which controllers have the 936 rate vs the 816 rate, I looked to the SDL connection logs. There are not many noticeable differences between controller when plugged in.

GUID number is different between the two 936 controllers. One of them shares a GUID with the 816, so no joy there.

There's a chance we can identify batches from the serial number range? Just thinking out loud.

I'll gather more switch pro controllers tomorrow to widen the tests. Maybe someone in the community sees this and has a bright idea before I go packet sniffing for more clues.

HilariousCow avatar Jun 13 '25 02:06 HilariousCow

@HilariousCow I reviewed the SPI flash notes here and had a suspicion that these values weren't necessarily fixed:

  • Factory accel XYZ sensitivity special coeff: 0x4000 (16384) (aka SWITCH_ACCEL_SCALE_OFFSET in SDL)
  • Factory gyro XYZ sensitivity special coeff: 0x343B (13371) (aka SWITCH_GYRO_SCALE_OFFSET in SDL)

I went through the commit history for that repo and found this, which seems to imply the same thing.

So I read the actual values from the controller using the changes below, and it looks like that fixed the issue. Maybe you can try it out with your collection of controllers too?

Switch controller fix
diff --git a/src/joystick/hidapi/SDL_hidapi_switch.c b/src/joystick/hidapi/SDL_hidapi_switch.c
index 253c4f959..9a93907bb 100644
--- a/src/joystick/hidapi/SDL_hidapi_switch.c
+++ b/src/joystick/hidapi/SDL_hidapi_switch.c
@@ -62,9 +62,7 @@
 #define SWITCH_GYRO_SCALE  14.2842f
 #define SWITCH_ACCEL_SCALE 4096.f
 
-#define SWITCH_GYRO_SCALE_OFFSET  13371.0f
 #define SWITCH_GYRO_SCALE_MULT    936.0f
-#define SWITCH_ACCEL_SCALE_OFFSET 16384.0f
 #define SWITCH_ACCEL_SCALE_MULT   4.0f
 
 typedef enum
@@ -925,6 +923,8 @@ static SDL_bool LoadIMUCalibration(SDL_DriverSwitch_Context *ctx)
     if (WriteSubcommand(ctx, k_eSwitchSubcommandIDs_SPIFlashRead, (uint8_t *)&readParams, sizeof(readParams), &reply)) {
         Uint8 *pIMUScale;
         Sint16 sAccelRawX, sAccelRawY, sAccelRawZ, sGyroRawX, sGyroRawY, sGyroRawZ;
+        Sint16 sAccelSensCoeffX, sAccelSensCoeffY, sAccelSensCoeffZ;
+        Sint16 sGyroSensCoeffX, sGyroSensCoeffY, sGyroSensCoeffZ;
 
         /* IMU scale gives us multipliers for converting raw values to real world values */
         pIMUScale = reply->spiReadData.rgucReadData;
@@ -933,10 +933,18 @@ static SDL_bool LoadIMUCalibration(SDL_DriverSwitch_Context *ctx)
         sAccelRawY = (pIMUScale[3] << 8) | pIMUScale[2];
         sAccelRawZ = (pIMUScale[5] << 8) | pIMUScale[4];
 
+        sAccelSensCoeffX = (pIMUScale[7] << 8) | pIMUScale[6];
+        sAccelSensCoeffY = (pIMUScale[9] << 8) | pIMUScale[8];
+        sAccelSensCoeffZ = (pIMUScale[11] << 8) | pIMUScale[10];
+
         sGyroRawX = (pIMUScale[13] << 8) | pIMUScale[12];
         sGyroRawY = (pIMUScale[15] << 8) | pIMUScale[14];
         sGyroRawZ = (pIMUScale[17] << 8) | pIMUScale[16];
 
+        sGyroSensCoeffX = (pIMUScale[19] << 8) | pIMUScale[18];
+        sGyroSensCoeffY = (pIMUScale[21] << 8) | pIMUScale[20];
+        sGyroSensCoeffZ = (pIMUScale[23] << 8) | pIMUScale[22];
+
         /* Check for user calibration data. If it's present and set, it'll override the factory settings */
         readParams.unAddress = k_unSPIIMUUserScaleStartOffset;
         readParams.ucLength = k_unSPIIMUUserScaleLength;
@@ -953,14 +961,14 @@ static SDL_bool LoadIMUCalibration(SDL_DriverSwitch_Context *ctx)
         }
 
         /* Accelerometer scale */
-        ctx->m_IMUScaleData.fAccelScaleX = SWITCH_ACCEL_SCALE_MULT / (SWITCH_ACCEL_SCALE_OFFSET - (float)sAccelRawX) * SDL_STANDARD_GRAVITY;
-        ctx->m_IMUScaleData.fAccelScaleY = SWITCH_ACCEL_SCALE_MULT / (SWITCH_ACCEL_SCALE_OFFSET - (float)sAccelRawY) * SDL_STANDARD_GRAVITY;
-        ctx->m_IMUScaleData.fAccelScaleZ = SWITCH_ACCEL_SCALE_MULT / (SWITCH_ACCEL_SCALE_OFFSET - (float)sAccelRawZ) * SDL_STANDARD_GRAVITY;
+        ctx->m_IMUScaleData.fAccelScaleX = SWITCH_ACCEL_SCALE_MULT / ((float)sAccelSensCoeffX - (float)sAccelRawX) * SDL_STANDARD_GRAVITY;
+        ctx->m_IMUScaleData.fAccelScaleY = SWITCH_ACCEL_SCALE_MULT / ((float)sAccelSensCoeffY - (float)sAccelRawY) * SDL_STANDARD_GRAVITY;
+        ctx->m_IMUScaleData.fAccelScaleZ = SWITCH_ACCEL_SCALE_MULT / ((float)sAccelSensCoeffZ - (float)sAccelRawZ) * SDL_STANDARD_GRAVITY;
 
         /* Gyro scale */
-        ctx->m_IMUScaleData.fGyroScaleX = SWITCH_GYRO_SCALE_MULT / (SWITCH_GYRO_SCALE_OFFSET - (float)sGyroRawX) * (float)M_PI / 180.0f;
-        ctx->m_IMUScaleData.fGyroScaleY = SWITCH_GYRO_SCALE_MULT / (SWITCH_GYRO_SCALE_OFFSET - (float)sGyroRawY) * (float)M_PI / 180.0f;
-        ctx->m_IMUScaleData.fGyroScaleZ = SWITCH_GYRO_SCALE_MULT / (SWITCH_GYRO_SCALE_OFFSET - (float)sGyroRawZ) * (float)M_PI / 180.0f;
+        ctx->m_IMUScaleData.fGyroScaleX = SWITCH_GYRO_SCALE_MULT / ((float)sGyroSensCoeffX - (float)sGyroRawX) * (float)M_PI / 180.0f;
+        ctx->m_IMUScaleData.fGyroScaleY = SWITCH_GYRO_SCALE_MULT / ((float)sGyroSensCoeffY - (float)sGyroRawY) * (float)M_PI / 180.0f;
+        ctx->m_IMUScaleData.fGyroScaleZ = SWITCH_GYRO_SCALE_MULT / ((float)sGyroSensCoeffZ - (float)sGyroRawZ) * (float)M_PI / 180.0f;
 
     } else {
         /* Use default values */

ceski-1 avatar Jun 13 '25 07:06 ceski-1

This is great! I'll look into it tomorrow!

On Fri, Jun 13, 2025, 00:19 ceski @.***> wrote:

ceski-1 left a comment (libsdl-org/SDL#13197) https://github.com/libsdl-org/SDL/issues/13197#issuecomment-2969356682

@HilariousCow https://github.com/HilariousCow I reviewed the SPI flash notes here https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/blob/master/spi_flash_notes.md#6-axis-sensor-factory-and-user-calibration and had a suspicion that these values weren't necessarily fixed:

  • Factory accel XYZ sensitivity special coeff: 0x4000 (16384) (aka SWITCH_ACCEL_SCALE_OFFSET in SDL)
  • Factory gyro XYZ sensitivity special coeff: 0x343B (13371) (aka SWITCH_GYRO_SCALE_OFFSET in SDL)

I went through the commit history for that repo and found this https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/commit/88bc0767243d874971ea0ae3b7c5c148d757ed0c, which seems to imply the same thing.

So I read the actual values from the controller using the changes below, and it looks like that fixed the issue. Maybe you can try it out with your collection of controllers too? Switch controller fix

diff --git a/src/joystick/hidapi/SDL_hidapi_switch.c b/src/joystick/hidapi/SDL_hidapi_switch.c index 253c4f959..9a93907bb 100644--- a/src/joystick/hidapi/SDL_hidapi_switch.c+++ b/src/joystick/hidapi/SDL_hidapi_switch.c@@ -62,9 +62,7 @@ #define SWITCH_GYRO_SCALE 14.2842f #define SWITCH_ACCEL_SCALE 4096.f -#define SWITCH_GYRO_SCALE_OFFSET 13371.0f #define SWITCH_GYRO_SCALE_MULT 936.0f-#define SWITCH_ACCEL_SCALE_OFFSET 16384.0f #define SWITCH_ACCEL_SCALE_MULT 4.0f

typedef enum@@ -925,6 +923,8 @@ static SDL_bool LoadIMUCalibration(SDL_DriverSwitch_Context *ctx) if (WriteSubcommand(ctx, k_eSwitchSubcommandIDs_SPIFlashRead, (uint8_t *)&readParams, sizeof(readParams), &reply)) { Uint8 *pIMUScale; Sint16 sAccelRawX, sAccelRawY, sAccelRawZ, sGyroRawX, sGyroRawY, sGyroRawZ;+ Sint16 sAccelSensCoeffX, sAccelSensCoeffY, sAccelSensCoeffZ;+ Sint16 sGyroSensCoeffX, sGyroSensCoeffY, sGyroSensCoeffZ;

     /* IMU scale gives us multipliers for converting raw values to real world values */
     pIMUScale = reply->spiReadData.rgucReadData;@@ -933,10 +933,18 @@ static SDL_bool LoadIMUCalibration(SDL_DriverSwitch_Context *ctx)
     sAccelRawY = (pIMUScale[3] << 8) | pIMUScale[2];
     sAccelRawZ = (pIMUScale[5] << 8) | pIMUScale[4];
  •    sAccelSensCoeffX = (pIMUScale[7] << 8) | pIMUScale[6];+        sAccelSensCoeffY = (pIMUScale[9] << 8) | pIMUScale[8];+        sAccelSensCoeffZ = (pIMUScale[11] << 8) | pIMUScale[10];+
      sGyroRawX = (pIMUScale[13] << 8) | pIMUScale[12];
      sGyroRawY = (pIMUScale[15] << 8) | pIMUScale[14];
      sGyroRawZ = (pIMUScale[17] << 8) | pIMUScale[16];
    
  •    sGyroSensCoeffX = (pIMUScale[19] << 8) | pIMUScale[18];+        sGyroSensCoeffY = (pIMUScale[21] << 8) | pIMUScale[20];+        sGyroSensCoeffZ = (pIMUScale[23] << 8) | pIMUScale[22];+
      /* Check for user calibration data. If it's present and set, it'll override the factory settings */
      readParams.unAddress = k_unSPIIMUUserScaleStartOffset;
      readParams.ucLength = k_unSPIIMUUserScaleLength;@@ -953,14 +961,14 @@ static SDL_bool LoadIMUCalibration(SDL_DriverSwitch_Context *ctx)
      }
    
      /* Accelerometer scale */-        ctx->m_IMUScaleData.fAccelScaleX = SWITCH_ACCEL_SCALE_MULT / (SWITCH_ACCEL_SCALE_OFFSET - (float)sAccelRawX) * SDL_STANDARD_GRAVITY;-        ctx->m_IMUScaleData.fAccelScaleY = SWITCH_ACCEL_SCALE_MULT / (SWITCH_ACCEL_SCALE_OFFSET - (float)sAccelRawY) * SDL_STANDARD_GRAVITY;-        ctx->m_IMUScaleData.fAccelScaleZ = SWITCH_ACCEL_SCALE_MULT / (SWITCH_ACCEL_SCALE_OFFSET - (float)sAccelRawZ) * SDL_STANDARD_GRAVITY;+        ctx->m_IMUScaleData.fAccelScaleX = SWITCH_ACCEL_SCALE_MULT / ((float)sAccelSensCoeffX - (float)sAccelRawX) * SDL_STANDARD_GRAVITY;+        ctx->m_IMUScaleData.fAccelScaleY = SWITCH_ACCEL_SCALE_MULT / ((float)sAccelSensCoeffY - (float)sAccelRawY) * SDL_STANDARD_GRAVITY;+        ctx->m_IMUScaleData.fAccelScaleZ = SWITCH_ACCEL_SCALE_MULT / ((float)sAccelSensCoeffZ - (float)sAccelRawZ) * SDL_STANDARD_GRAVITY;
    
      /* Gyro scale */-        ctx->m_IMUScaleData.fGyroScaleX = SWITCH_GYRO_SCALE_MULT / (SWITCH_GYRO_SCALE_OFFSET - (float)sGyroRawX) * (float)M_PI / 180.0f;-        ctx->m_IMUScaleData.fGyroScaleY = SWITCH_GYRO_SCALE_MULT / (SWITCH_GYRO_SCALE_OFFSET - (float)sGyroRawY) * (float)M_PI / 180.0f;-        ctx->m_IMUScaleData.fGyroScaleZ = SWITCH_GYRO_SCALE_MULT / (SWITCH_GYRO_SCALE_OFFSET - (float)sGyroRawZ) * (float)M_PI / 180.0f;+        ctx->m_IMUScaleData.fGyroScaleX = SWITCH_GYRO_SCALE_MULT / ((float)sGyroSensCoeffX - (float)sGyroRawX) * (float)M_PI / 180.0f;+        ctx->m_IMUScaleData.fGyroScaleY = SWITCH_GYRO_SCALE_MULT / ((float)sGyroSensCoeffY - (float)sGyroRawY) * (float)M_PI / 180.0f;+        ctx->m_IMUScaleData.fGyroScaleZ = SWITCH_GYRO_SCALE_MULT / ((float)sGyroSensCoeffZ - (float)sGyroRawZ) * (float)M_PI / 180.0f;
    

    } else { /* Use default values */

— Reply to this email directly, view it on GitHub https://github.com/libsdl-org/SDL/issues/13197#issuecomment-2969356682, or unsubscribe https://github.com/notifications/unsubscribe-auth/AANBE4DIBRC7GVT2Z6RDKOT3DJ3OZAVCNFSM6AAAAAB65XIHICVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDSNRZGM2TMNRYGI . You are receiving this because you were mentioned.Message ID: @.***>

HilariousCow avatar Jun 13 '25 07:06 HilariousCow

I've tested against 5 separate switch controllers now, which previously seemed to have different gyro rates, and with the adjustments above, they all passed tests with my gyro packet integration tool. Thank you so much!

HilariousCow avatar Jun 13 '25 18:06 HilariousCow

Excellent, I can make a PR for these changes.

ceski-1 avatar Jun 13 '25 18:06 ceski-1

Thanks! I may follow up with some timing changes, but this alone is a good fix.

HilariousCow avatar Jun 13 '25 18:06 HilariousCow

Excellent, I can make a PR for these changes.

Yes please, thank you!

slouken avatar Jun 13 '25 18:06 slouken