Get stable values from magnetometer (MPU9250)

Hey,

ive bought myself a mpu 9250. im most interested in the magnetometer values.
i can read those values very easily, but theyre very unstable. ive already calculated
the average value out of three but its not enough.
Is there a way to stabilize these values? I read about some filters but i dont
know which one i should use for this problem.

Here is my code:

#include "MPU9250.h"

MPU9250 IMU(Wire,0x68);
int status;

void setup() {
  // serial to display data
  Serial.begin(115200);

  Serial.println(IMU.calibrateMag());
  delay(1000);
  while(!Serial) {}

  status = IMU.begin();
  if (status < 0) {
    ...
  }
}

void loop() {
 
  //IMU.readSensor();
  int count = 3;

  float vals[count];
  float sum = 0.0f;
  for(int i = 0; i < count;i++)
  {
    IMU.readSensor();
    vals[i] = IMU.getMagY_uT();
    sum += vals[i];
  }

  sum /= count;
  


  Serial.print(sum,6);
  Serial.print("\t");

  Serial.println("");
  delay(50);
}

  float vals[count];Why the array?

okay the array is redundant. i can sum up the sum variable instead. Do you know how to stabilize these values ?

Post examples of "unstable" values.

If the sensor is being used correctly and is not moving, then any variations in the output are due to
(1) measurement noise, as defined in the sensor data sheet
(2) time varying magnetic fields.

-1.792239	
-0.358448	
-0.358448	
-2.867582	
-2.867582	
-0.896119	
-0.896119	
-0.896119	
-0.896119	
-0.896119	
-1.613015	
-1.613015	
-1.433791	
-1.433791	
-1.433791	
-1.433791	
-1.433791	
-1.792239	
-1.792239	
-1.971463	
-1.971463	
-1.971463	
-2.150687	
-2.150687	
-1.254567	
-1.254567	
-1.792239	
-1.792239	
-1.792239	
-2.867582	
-2.867582	
-1.971463	
-1.971463	
-2.688358	
-2.688358	
-2.688358	
-1.971463	
-1.971463	
-1.792239	
-1.792239

The sensor is not moving.

I dont think that its due to a noisy environment. I put the arduino and the mpu on the ground away from my desk. There was no difference. Even when i hold the arduino close to the mpu there is no difference.
There is a significant difference when i hold my smartphone near to the mpu but else not.
with

What do you mean by time varying magnetic fields? You mean the latitude and longitude?

ps: im using this library from github:

There is a significant difference when i hold my smartphone near to the mpu

Does your smartphone have speakers?

What are the measurement units of the output you posted?

Please explain why those variations matter to you.

What do you mean by time varying magnetic fields

Magnetic fields that vary with time, such as those generated by electric currents in computers, monitors and other electrical appliances, motors, relays, generators, scooters and automobiles, moving magnets or iron objects, audio speakers, household AC wiring.

AWOL:
Does your smartphone have speakers?

Yes of course.

jremington:
What are the measurement units of the output you posted?

Please explain why those variations matter to you.

Magnetic fields that vary with time, such as those generated by electric currents in computers, monitors and other electrical appliances, motors, relays, generators, scooters and automobiles, moving magnets or iron objects, audio speakers, household AC wiring.

What do you mean with measurement units? Im sry.
These variations matters to me because i want to build a compass for a robot. The robot gets some kind of orientation. My idea was to map those mpu magnetometer values to degrees. (1-360) then the robot can adjust the motor power by reading the degrees. For example to drive a "perfect" straight line. And of course other cool stuff i can make it do then. Even integers of the mpu are varying very much as you can see so there is still space for improvement.

Even integers of the mpu are varying very much as you can see

You did not post any integers. You posted some small floating point numbers, that were not varying by much.

To make a compass, it is essential to calibrate the magnetometer, following this tutorial.

Then use

float heading = atan2(magy, magx)*180.0/PI;  //in degrees
if (heading < 0.0) heading = heading+360.0// map to 0-359

In this case you don't need to worry about the units of the measurement.

Instead of taking an average of three measurements very close together you might be better served with a longer term moving average. I would sample at half the local AC frequency to try to avoid any AC hum.

const unsigned ACLineFrequency = 60;
const unsigned SamplesPerACCycle = 2; // We take two samples, 180° out of phase, during each AC cycle

// Let's make the averaging time one second
const unsigned SampleCount = ACLineFrequency * SamplesPerACCycle;
const unsigned long SampleIntervalMicroseconds = 1000000UL / SampleCount;

// You need a set of these for each value to be averaged
float Samples[SampleCount] = {0.0}; 
float SampleTotal = 0;
float SampleAverage = 0;


// You only need one set of these for all values
unsigned SampleIndex = 0;

unsigned long LastSampleTime;

float GetNewValue()
{
  return 1.0;  // Put your sensor reading code here
}


void AddSample()
{
  // You need these three line for each value being sampled
  SampleTotal -= Samples[SampleIndex];  // Remove the value falling out of the buffer
  Samples[SampleIndex] = GetNewValue();
  SampleTotal += Samples[SampleIndex];


  // This is only done once per sample interval
  SampleIndex = (SampleIndex + 1) % SampleCount;
}


void setup()
{
  // Pre-fill the sample buffer
  for (unsigned i = 0; i < SampleCount; i++)
  {
    AddSample();
    delayMicroseconds(SampleIntervalMicroseconds);  // Crude, but close enough for the first second.
  }
  LastSampleTime = micros();


  // Do this line for each value:
  SampleAverage = SampleTotal / SampleCount;
}


void loop()
{
  unsigned long currentMicros = micros();
  if (currentMicros - LastSampleTime >= SampleIntervalMicroseconds)
  {
    LastSampleTime += SampleIntervalMicroseconds;
    AddSample();


    // Do this line for each value:

    SampleAverage = SampleTotal / SampleCount;
  }
}

jremington:
You did not post any integers. You posted some small floating point numbers, that were not varying by much.

To make a compass, it is essential to calibrate the magnetometer, following this tutorial.

Then use

float heading = atan2(magy, magx)*180.0/PI;  //in degrees

if (heading < 0.0) heading = heading+360.0// map to 0-359




In this case you don't need to worry about the units of the measurement.

Is the code you posted to calibrate the magnetometer?

Following your tutorial we need to find a bias that is affecting the measurement
of the magnetometer by calibrating it. Then we just need to substract the bias from the measurements
and we have calibrated values right?
If im right this script shows how to do it.

So your code is just for mapping the values to 0-360 degrees?

johnwasser:
Instead of taking an average of three measurements very close together you might be better served with a longer term moving average. I would sample at half the local AC frequency to try to avoid any AC hum.

const unsigned ACLineFrequency = 60;

const unsigned SamplesPerACCycle = 2; // We take two samples, 180° out of phase, during each AC cycle

// Let's make the averaging time one second
const unsigned SampleCount = ACLineFrequency * SamplesPerACCycle;
const unsigned long SampleIntervalMicroseconds = 1000000UL / SampleCount;

// You need a set of these for each value to be averaged
float Samples[SampleCount] = {0.0};
float SampleTotal = 0;
float SampleAverage = 0;

// You only need one set of these for all values
unsigned SampleIndex = 0;

unsigned long LastSampleTime;

float GetNewValue()
{
  return 1.0;  // Put your sensor reading code here
}

void AddSample()
{
  // You need these three line for each value being sampled
  SampleTotal -= Samples[SampleIndex];  // Remove the value falling out of the buffer
  Samples[SampleIndex] = GetNewValue();
  SampleTotal += Samples[SampleIndex];

// This is only done once per sample interval
  SampleIndex = (SampleIndex + 1) % SampleCount;
}

void setup()
{
  // Pre-fill the sample buffer
  for (unsigned i = 0; i < SampleCount; i++)
  {
    AddSample();
    delayMicroseconds(SampleIntervalMicroseconds);  // Crude, but close enough for the first second.
  }
  LastSampleTime = micros();

// Do this line for each value:
  SampleAverage = SampleTotal / SampleCount;
}

void loop()
{
  unsigned long currentMicros = micros();
  if (currentMicros - LastSampleTime >= SampleIntervalMicroseconds)
  {
    LastSampleTime += SampleIntervalMicroseconds;
    AddSample();

// Do this line for each value:

SampleAverage = SampleTotal / SampleCount;
  }
}

Ah this is much better now. Where do you get the values from ACLineFrequency and SamplesPerCycle?
Why 60 and 2 cycles?
The code works only for one dimension right?(x,y,z) When i use the code of jremingto to map the values
to 360 degrees i have to repeat the average calculation for another dimension ?

And im very thankful for all your responses!

Then we just need to substract the bias from the measurements
and we have calibrated values right?

No, but that is a start.

Study the material in this link.

eniddelemaj:
Where do you get the values from ACLineFrequency and SamplesPerCycle? Why 60 and 2 cycles

I used the value 60 for 'ACLineFrequency' because that is the frequency of line current here in the United States of America. The comment explains why I am doing two samples per AC cycle.

eniddelemaj:
The code works only for one dimension right? (x,y,z) When i use the code of jremingto to map the values to 360 degrees i have to repeat the average calculation for another dimension ?

Yes. THAT IS EXACTLY WHY THE COMMENTS POINT OUT EXACTLY WHAT LINES TO REPEAT FOR EACH ADDITIONAL VALUE.

Thank you very much for your help.

Everything is working now...almost. I get right values from 1 to 360 degrees, well sometimes.
Sometimes instead of degrees i get a non arithmetical number(nan) or just a wrong number which doesnt change.
I searched for the origin of the failure and it took me to this:

float getNewValue(char dimension)
{
  IMU->readSensor();

  float x, y, z;

  x = IMU->getMagX_uT() * (100000.0 / 1100.0) + xBias;
  ...

  //Normal values as supposed
  delay(100);
  Serial.println(x)

  float calX, calY, calZ;

  calX = x * xScaleX + y * xScaleY + z * xScaleZ;
  ...

  delay(100);
  Serial.println(calX); // -> Here i can see the failure. Sometimes nan or ovf or (not often) inf


  if (dimension == 'x')
    return calX;  // Put your sensor reading code here
  else if...
}

The weird thing is that sometimes it works as it should be
and sometimes i get ovf,nan or inf. I couldnt point out when
or under what conditions it is doing it. It seems to be very
unspecific.

ovf means overflow so i changed the floats to doubles but nothing changed.
The x variable is always alright but after the calX calculation the value
get messed up like this. As already said somentimes it is like this
and sometimes it works after multiple restarts or after i reconnected the power supply.
I already connected the arduino to a 9v power supply from plug instead of just
using the usb cable but the same result appears.

Here is the full code:

#include "MPU9250.h"

MPU9250 *IMU;
int status;

const  double xBias = 1624.766973;
const  double yBias = -977.863214;
const  double zBias = -674.986618;

const  double xScaleX = 0.012480;
const  double xScaleY = 0.000097;
const  double xScaleZ = 0.000032;

const  double yScaleX = 0.000097;
const  double yScaleY = 0.012680;
const  double yScaleZ = -0.000110;


const unsigned ACLineFrequency = 50;
const unsigned samplesPerACCycle = 2;
const unsigned sampleCount = ACLineFrequency * samplesPerACCycle;
const unsigned  sampleIntervalMicroseconds = 1000000UL / sampleCount;

 double *samplesX;
 double *samplesY;

 double sampleTotalX = 0;
 double sampleTotalY = 0;

 double sampleAverageX = 0;
 double sampleAverageY = 0;

unsigned sampleIndexX = 0;
unsigned sampleIndexY = 0;

unsigned  LastsampleTime;

 double getNewValue(char dimension)
{
  IMU->readSensor();

   double x, y, z;

  x = IMU->getMagX_uT() * (100000.0 / 1100.0) + xBias;
  y = IMU->getMagY_uT() * (100000.0 / 1100.0) + yBias;
  z = IMU->getMagZ_uT() * (100000.0 / 1100.0) + zBias;

   double calX, calY, calZ;

  calX = x * xScaleX + y * xScaleY + z * xScaleZ;
  calY = x * yScaleX + y * yScaleY + z * yScaleZ;

  if (dimension == 'x')
    return calX;  
 else if (dimension == 'y')
    return calY;
  else
    return calZ;
}


void Addsample()
{
  sampleTotalX -= samplesX[sampleIndexX];
  samplesX[sampleIndexX] = getNewValue('x');
  sampleTotalX += samplesX[sampleIndexX];

  sampleTotalY -= samplesY[sampleIndexY];
  samplesY[sampleIndexY] = getNewValue('y');
  sampleTotalY += samplesY[sampleIndexY];

  sampleIndexX = (sampleIndexX + 1) % sampleCount;
  sampleIndexY = (sampleIndexY + 1) % sampleCount;
}



void setup() {
  IMU = new MPU9250(Wire, 0x68);
  Serial.begin(115200);

  while (!Serial) {}
  status = IMU->begin();
  if (status < 0) {
     //...
  }

  samplesX = new  double[sampleCount]();
  samplesY = new  double[sampleCount]();

  for (unsigned i = 0; i < sampleCount; i++)
  {
    Addsample();
    delayMicroseconds(sampleIntervalMicroseconds); 
  }
  LastsampleTime = micros();

  sampleAverageX = sampleTotalX / sampleCount;
}

void loop() {

  unsigned  currentMicros = micros();
  if (currentMicros - LastsampleTime >= sampleIntervalMicroseconds)
  {
    LastsampleTime += sampleIntervalMicroseconds;
    Addsample();

    sampleAverageX = sampleTotalX / sampleCount;
    sampleAverageY = sampleTotalY / sampleCount;


  }

   double heading = atan2(sampleAverageY, sampleAverageX) * 180.0 / PI; 
  if (heading < 0.0) heading = heading + 360.0; 


  Serial.println(heading, 6);


}

i changed the floats to doubles but nothing changed.

Floats and doubles are the same on AVR-based Arduinos; 32 bits.

Your best option is to put in lots of serial.print() statements and determine what circumstances lead to NAN or OVF situations.

Your program is much more complicated and obscurely written than it should be, just to average a few values. That creates plenty of opportunities for silly mistakes. You don't need ANY arrays, for example.

To average 10 values can be as simple as this:

int number=10;
float avg=0.0;
for (int i=0; i<number; i++) {
 avg += get_value();
}
avg = avg/number;

Since you are rescaling the data, the constant in () is pointless:

x = IMU->getMagX_uT() * (100000.0 / 1100.0) + xBias;

Okay finally i did it. Now everything is working.There was something wrong with the library
i used the library on github from bolderflight. I dont know why but the function which
returns the raw value sometimes returns an ovf. I used the library from sparkfun. Then everything
worked all the time.

jremington:
Floats and doubles are the same on AVR-based Arduinos; 32 bits.

Your best option is to put in lots of serial.print() statements and determine what circumstances lead to NAN or OVF situations.

Your program is much more complicated and obscurely written than it should be, just to average a few values. That creates plenty of opportunities for silly mistakes. You don't need ANY arrays, for example.

To average 10 values can be as simple as this:

int number=10;

float avg=0.0;
for (int i=0; i<number; i++) {
avg += get_value();
}
avg = avg/number;




Since you are rescaling the data, the constant in () is pointless: 


x = IMU->getMagX_uT() * (100000.0 / 1100.0) + xBias;

I think the arrays are important because of this line.

sampleIndexX = (sampleIndexX + 1) % sampleCount;

Then i can reference the array element with sampleIndexX at the specific index.
Honestly i dont really understand why this should be but its not working
without arrays. (Maybe i did another mistake but as far as i went thats the result).

jremington:
Your program is much more complicated and obscurely written than it should be, just to average a few values. That creates plenty of opportunities for silly mistakes.

The OP's original sketch averaged three samples over a very short timespan. It was not producing acceptable results. I would not expect that expanding it to ten samples would do much better. I suspected 60 Hz AC hum was causing at least part of the noise. Taking 120 samples at 120 Hz (in the USA and other places with 60 Hz current) should do a much better job of filtering out AC hum.
If you have a much simpler way of calculating a 120 sample moving average at 120 Hz then I'd love to see it. :slight_smile:

I do not believe that AC hum is the problem. In fact, the OP has not given us evidence that there IS a problem other than measurement noise.

In any case I would advise the OP to try the original code again (minus the silly use of an array), and increase count to somewhere in the range 20 to 100.

I know this topic is old but i have another question.
The magnetometer data is as far as calibrated as i get 0 - 359 degree values.
But when the mpu is heading to 180 degrees and i turn it another 180 degrees it shows
deviation of about 20 degrees.
I already calibrated it multiple times but the best result i got was 5 degrees deviaton.
I saw examples of people that combined the magneto,accel and gyro data to get perfect
orientations. At least it looked like in those videos because they used a graphical representation.

Is sensor fusion the right way i need to go or is it simply a bad calibration?