MPU9250 Yaw Not Zeroed & Not Correct

I am using an ESP-32 S3 to calculate Yaw/Pitch/Roll from a Haitronic MPU-9250.
Managed to get the Pitch and Roll angles accurately but I am unable to get a proper Yaw angle. I am afraid it is due to the Magnetometer not being calibrated correctly.
The library I am using -> GitHub - hideakitai/MPU9250: Arduino library for MPU9250 Nine-Axis (Gyro + Accelerometer + Compass) MEMS MotionTracking™ Device
I have used the calibration code in the library to calibrate my IMU hence the values for the bias. The declination was calculated from the global magnetic field using an online calculator.

Kindly, I need help solving the yaw, depending on where I look it, when starting the device the initial yaw angle is never 0. for example if I look east its at 40 degrees. If I look west its at 100 degrees. Never does it go from 0 to 90. Nor have the same value.

If possible, a simple yet effective guide on calibrating the magnetometer or a better library as I've tried everything I could think of so far. Thank you in advance.

The Wiring:
MPU-9250 -> ESP-32 S3 Dev module
VCC -> 3.3V
GND & AD0* -> GND
SDA** -> GPIO 4
SCL**-> GPIO 5
**The GPIOs on the ESP-32 S3 are fully programmable and can select any for SDA/SCL.
*AD0 is shorted to GND to enable the use of the Magnetometer on the MPU9250.

The Code:

#include "MPU9250.h"
MPU9250Setting setting;
MPU9250 mpu;
float cRoll = 0, cPitch = 0, cYaw = 0;
unsigned long previousTime = 0;  // Stores the previous time
unsigned long currentTime = 0;   // Stores the current time
unsigned long elapsedTime = 0;
//
const float Pi = 3.14159;
//
void setup() {
  Serial.begin(115200);
  Wire.begin(4, 5);
  mpu.verbose(true);
  delay(2000);
  setMPU();
  if (!mpu.setup(0x68, setting, Wire)) {  // change to your own address
    while (1) {
      Serial.println("MPU connection failed. Please check your connection with `connection_check` example.");
      delay(5000);
    }
  }
  mpu.setMagneticDeclination(4.35);
  mpu.setAccBias(10.84, -1.02, 28.18);
  mpu.setGyroBias(-3.30, 3.78, 1.61);
  mpu.setMagBias(176.21, 830.72, -701.03);
  mpu.setMagScale(1.54, 0.78, 0.93);
  mpu.verbose(false);
}

void setMPU() {
  setting.accel_fs_sel = ACCEL_FS_SEL::A16G;
  setting.gyro_fs_sel = GYRO_FS_SEL::G2000DPS;
  setting.mag_output_bits = MAG_OUTPUT_BITS::M16BITS;
  setting.fifo_sample_rate = FIFO_SAMPLE_RATE::SMPL_200HZ;
  setting.gyro_fchoice = 0x03;
  setting.gyro_dlpf_cfg = GYRO_DLPF_CFG::DLPF_41HZ;
  setting.accel_fchoice = 0x01;
  setting.accel_dlpf_cfg = ACCEL_DLPF_CFG::DLPF_45HZ;
  return;
}

void loop() {
  if (mpu.update()) {
    //
    float aX = mpu.getAccX();
    float aY = mpu.getAccY();
    float aZ = mpu.getAccZ();
    float gX = mpu.getGyroX();
    float gY = mpu.getGyroY();
    float gZ = mpu.getGyroZ();
    float mX = mpu.getMagX();
    float mY = mpu.getMagY();
    float mZ = mpu.getMagZ();

    //roll calc
    float roll = atan2(aY, sqrt(aX * aX + aZ * aZ)) * (180 / Pi);
    //pitch calc
    float pitch = atan2(-aX, sqrt(aY * aY + aZ * aZ)) * (180 / Pi);

    // Magnetic-X and Magnetic-Y calc
    float Mx = (mX * cos(pitch)) + (mY * sin(roll) * sin(pitch)) - (mZ * cos(roll) * sin(pitch));
    float My = (mY * cos(roll)) - (mZ * sin(roll));

    // yaw calc
    float yaw = atan2(My, Mx) * (180 / Pi);
    //time 
    currentTime = millis();
    elapsedTime = (currentTime-previousTime)/1000;
    previousTime = currentTime;
    //complimentary
    cRoll = ((0.98 * (cRoll + (gX * elapsedTime))) + (0.02 * roll));
    cPitch = ((0.98 * (cPitch + (gY * elapsedTime))) + (0.02 * pitch));
    cYaw = ((0.98 * (cYaw + (gZ * elapsedTime))) + (0.02 * yaw));

    //output
    Serial.print(" ");
    Serial.print(roll);
    Serial.print(", ");
    Serial.print(cRoll);
    Serial.print(", ");
    Serial.print(pitch);
    Serial.print(", ");
    Serial.print(cPitch);
    Serial.print(", ");
    Serial.print(yaw);
    Serial.print(", ");
    Serial.print(cYaw);
    Serial.print(", ");
    Serial.print(mpu.getYaw());
    Serial.println(", ");
    delay(5);
  }
}

Output sample: (Roll/cRoll/Pitch/cPitch/Yaw/cYaw)

If the magnetometer is correctly calibrated, the corrected readings for the Earth's magnetic field will map approximately to the surface of a sphere, centered on the origin. If they don't, you will not be able to accurately measure the yaw angle.

See this excellent tutorial on magnetometer calibration.

Hi there,
Indeed I do need a magnetometer calibration tool, the link you sent me has most of the links in it to the tools as unavailable, for example the freeIMU link leads to a "page not found".

For reference, I need it user friendly and as simple as it can get to calibrate a magnetometer to get a yaw angle that can start from 0 to 90 regardless of where its initialized. Either a pre-built software or a simple solution is what I require. Definitely not advanced mathematics as I am incapable of solving that. I'm at best capable to use a complimentary filter to get the final angle out. Kindly provide me with alternative guides Thanks you in advance.

I don't know what this means. Yaw is measured starting from zero at magnetic North.

Okay let me re-iterate, if I rotate it 90 degrees physically, it doesn't actually rotate 90 degrees on the output. I am 100% certain its due to the magnetometer of the mpu9250 and I have 0 clue how to calibrate it properly.

That is pretty clearly described in the tutorial I linked, and it is not true that the links to tools are unavailable.

The two tools I use for magnetometer calibration can be found on the Sailboat Instruments site, several of my Github AHRS links, and this forum post: https://forum.pololu.com/t/correcting-the-balboa-magnetometer/14315

I recommend to use the Python program calibrate3.py in the Github link.

None of these tools are intended for a beginner who is unwilling to put some work into understanding the basics.

I am thankful for your assistance, and sorry if I sound a bit demotivated but I tried over and over. I am using the Magneto 1.2 which is found on the Sailboat Instruments site Part 2, Here's my result:


I would also like to ask if this is the correct equation for the norm of magnetic/gravitational field. Since I'm using a magnetometer, the heading is calculated through this Eq:
image

Another question is, the code as shown in the initial post,
The 'combined bias' found is for the mpu.setMagBias(magX,magY,magZ)bias I'd assume.
But to use the information gathered from the Magneto 1.2 Scale values. Do I just try one row at a time for the command mpu.setMagScale(magXscale,magYscale,magZscale) to see which is correct?

There is nothing obviously wrong with the results of Magneto 1.2, but it sounds like you are not sure how to apply the corrections.

Here is the code I use to calculate the corrected magnetometer readings (excerpted from ICM_20948-AHRS/ICM_20948_tiltcomp.ino at main · jremington/ICM_20948-AHRS · GitHub)

//Mag scale divide by 369.4 to normalize. These are significant corrections, especially the large offsets.
float M_B[3]
{ -156.70,  -52.79, -141.07};

float M_Ainv[3][3]
{ {  1.12823, -0.01142,  0.00980},
  { -0.01142,  1.09539,  0.00927},
  {  0.00980,  0.00927,  1.10625}
};

// ...
// raw magnetometer data in, corrected data out.
void get_scaled_IMU(float Axyz[3], float Mxyz[3]) {
  byte i;
  float temp[3];


  //apply offsets (bias) and scale factors from Magneto
  for (int i = 0; i < 3; i++) temp[i] = (Mxyz[i] - M_B[i]);
  Mxyz[0] = M_Ainv[0][0] * temp[0] + M_Ainv[0][1] * temp[1] + M_Ainv[0][2] * temp[2];
  Mxyz[1] = M_Ainv[1][0] * temp[0] + M_Ainv[1][1] * temp[1] + M_Ainv[1][2] * temp[2];
  Mxyz[2] = M_Ainv[2][0] * temp[0] + M_Ainv[2][1] * temp[1] + M_Ainv[2][2] * temp[2];
  vector_normalize(Mxyz);
}

void vector_normalize(float a[3])
{
  float mag = sqrt(vector_dot(a, a));
  a[0] /= mag;
  a[1] /= mag;
  a[2] /= mag;
}

Below is a plot, produced by the Python program calibrate3.py on the Github site linked above, showing that a set of corrected, 3D magnetometer readings map to a sphere centered on the origin. They work very well to make a tilt compensated compass, accurate to +/-2 degrees or better.