Yaw changes when I roll/pitch my IMU board

Debugging notes from a real robotics build — the kind of problem that looks “impossible” until you model the frames correctly.

Problem

When I roll or pitch my sensor board, roll and pitch readings are clean and stable (no drift). But at the same time, my yaw value changes, even though I did not yaw the board.

Observation

  • Roll/pitch look accurate and not drifting.
  • Yaw changes as roll/pitch changes (coupled).
  • The coupling is asymmetric: some directions (e.g. positive roll/pitch) behave “more correct” than others.

Images / plots

I’m keeping the original screenshots referenced in my notes. When I publish the final version, I’ll export them as PNGs and put them under /assets/images/.

  • Placeholder: Pasted image 20250228204519.png
  • Placeholder: Pasted image 20250228204307.png
  • Placeholder: Pasted image 20250228204314.png

What we tried (and what we learned)

  1. Mag recalibration (outside, minimal interference)
    We suspected raw data issues. In microtesla vector form, the direction to magnetic north looks reasonable, but when each axis is pointed to magnetic north, the axes don’t return the same magnitude.
  2. Tilt compensation using accel + magnetometer projection
    Yaw was computed by correcting tilt of the mag heading using accel. But accel is noisy, so the gravity unit vector (a-hat) becomes unreliable. We explored using Madgwick’s getGrav() as an alternative gravity vector; gravX/gravY looked reliable, but gravZ never reached 1.00g, so we weren’t sure if this was due to interference or modeling.
  3. 3×3 rotation matrix to decouple magnetometer under roll/pitch
    Applying a rotation matrix helped for positive pitch and positive roll coupling, but other directions still produced the yaw problem.
  4. Switching away from Madgwick for yaw (Kalman filter)
    Madgwick was hard to debug, so we tried a Kalman approach for yaw. Inputs: corrected yaw (from offset mag + rotation correction) and gyroZ. But the filter barely had any effect.
  5. Suspected overdamping / tuning issues
    The yaw response didn’t correspond to the real-world tilt angle magnitude — it felt overdamped. I attempted to tune filter parameters (e.g. Q/R values depending on the filter), but it didn’t improve.

Working hypothesis (what finally made this make sense)

Yaw is typically computed from atan2(magX, magY). But roll/pitch changes can change magX and magY even if the board isn’t yawed. So “yaw changes when I roll/pitch” can be a frame/coupling issue rather than a yaw rotation.

Solution direction

To solve the coupling effect, the core idea is: offset/correct the roll/pitch-induced change in the horizontal components used for yaw so yaw doesn’t move when it shouldn’t.

  • Model how roll affects the XZ plane (rotation about Y) and pitch affects the YZ plane (rotation about X).
  • Recognize that yaw uses the XY projection, so if roll/pitch changes X/Y, yaw will change.
  • Use a quaternion or rotation-based compensation so the magnetometer is expressed in a consistent reference frame before heading is computed.
  • For the “90 degrees test”: fit a regression line (yaw vs pitch/roll) to learn a corrective function.

Snippet (tilt-compensated magnetometer yaw)

This is the core structure I used while experimenting (cleaned up formatting, same idea):

// Compute yaw using tilt-compensated magnetometer
// roll = phi (rad), pitch = theta (rad)

float cosPhi = cos(roll);
float sinPhi = sin(roll);
float cosTheta = cos(pitch);
float sinTheta = sin(pitch);

float mX_rot = magX * cosTheta + magZ * sinTheta;
float mY_rot = magX * sinPhi * sinTheta + magY * cosPhi - magZ * sinPhi * cosTheta;

float yaw = atan2(mY_rot, mX_rot) * 180.0f / M_PI;

Next steps

  • Export the referenced images and add them under /assets/images/.
  • Add a short “final takeaway” once the regression/quaternion approach is fully validated.
  • Write a follow-up: how I validate calibration quality (hard/soft iron, misalignment).