Calibrating Magnetometer inside main sketch - LSM303

Good day all,

I'm currently trying to build a device which turns to a given magnetic heading and have come across a hurdle.

I am able to interface the stepper motor with no issues, and am able to calibrate the magnetometer in one sketch to use in another sketch, but I am unable to calibrate the compass and use that calibration in the same sketch. Ideally I would like the system to turn 360 degrees performing a calibration run (with appropriate delays to prevent weird fields put out by the stepper motor), followed by being able to use that calibration data to know where North is, etc.

The calibration and heading sketches are examples in the LSM303.h library.

Calibration sketch: (works well)

#include <Wire.h>
#include <LSM303.h>

LSM303 compass;
LSM303::vector<int16_t> running_min = {32767, 32767, 32767}, running_max = {-32768, -32768, -32768};

char report[80];

void setup() {
  Serial.begin(9600);
  Wire.begin();
  compass.init();
  compass.enableDefault();
}

void loop() {  
  compass.read();
  
  running_min.x = min(running_min.x, compass.m.x);
  running_min.y = min(running_min.y, compass.m.y);
  running_min.z = min(running_min.z, compass.m.z);

  running_max.x = max(running_max.x, compass.m.x);
  running_max.y = max(running_max.y, compass.m.y);
  running_max.z = max(running_max.z, compass.m.z);
  
  snprintf(report, sizeof(report), "min: {%+6d, %+6d, %+6d}    max: {%+6d, %+6d, %+6d}",
    running_min.x, running_min.y, running_min.z,
    running_max.x, running_max.y, running_max.z);
  Serial.println(report);
  
  delay(100);
}

Heading determining sketch: (following separate calibration sketch) (works well)

#include <Wire.h>
#include <LSM303.h>

LSM303 compass;

void setup() {
  Serial.begin(9600);
  Wire.begin();
  compass.init();
  compass.enableDefault();
  
  /*
  Calibration values; the default values of +/-32767 for each axis
  lead to an assumed magnetometer bias of 0. Use the Calibrate example
  program to determine appropriate values for your particular unit.
  */
  compass.m_min = (LSM303::vector<int16_t>){-32767, -32767, -32767};
  compass.m_max = (LSM303::vector<int16_t>){+32767, +32767, +32767};
}

void loop() {
  compass.read();
  
  /*
  When given no arguments, the heading() function returns the angular
  difference in the horizontal plane between a default vector and
  north, in degrees.
  
  The default vector is chosen by the library to point along the
  surface of the PCB, in the direction of the top of the text on the
  silkscreen. This is the +X axis on the Pololu LSM303D carrier and
  the -Y axis on the Pololu LSM303DLHC, LSM303DLM, and LSM303DLH
  carriers.
  
  To use a different vector as a reference, use the version of heading()
  that takes a vector argument; for example, use
  
    compass.heading((LSM303::vector<int>){0, 0, 1});
  
  to use the +Z axis as a reference.
  */
  float heading = compass.heading();
  
  Serial.println(heading);
  delay(100);
}

My current test sketch: (my coding is not the strongest, though I have persisted thus far and got quite a lot done! (insert proud face))

#include <AccelStepper.h>
#include <Wire.h>
#include <LSM303.h>

// Define a stepper and the pins it will use
AccelStepper stepper(AccelStepper::DRIVER, 2, 3);
#define MS1 3
#define MS2 4
#define MS3 5

//Setting up Compass Variables
LSM303 compass;
LSM303::vector<int16_t> running_min = {32767, 32767, 32767}, running_max = { -32768, -32768, -32768};
float heading;
long azimuth;
long where;

void setup()
{
  stepper.setMaxSpeed(800.0);
  stepper.setAcceleration(8000.0);
  pinMode(MS1, OUTPUT);
  pinMode(MS2, OUTPUT);
  pinMode(MS3, OUTPUT);
  digitalWrite(MS1, LOW);
  digitalWrite(MS2, LOW);
  digitalWrite(MS3, LOW);
  stepper.setCurrentPosition(0);
  Wire.begin();
  compass.init();
  compass.enableDefault();
}

void loop()
{
  for (int i = 0; i < 72; i++)
  {
    stepper.runToNewPosition(876);
    delay(100);
    stepper.setCurrentPosition(0);
    compass.read();
    running_min.x = min(running_min.x, compass.m.x);    //Local Magnetic Field x axis Minimum value in MicroTeslas
    running_min.y = min(running_min.y, compass.m.y);    //Local Magnetic Field y axis Minimum value in MicroTeslas
    running_min.z = min(running_min.z, compass.m.z);    //Local Magnetic Field z axis Minimum value in MicroTeslas

    running_max.x = max(running_max.x, compass.m.x);    //Local Magnetic Field x axis Maximum value in MicroTeslas
    running_max.y = max(running_max.y, compass.m.y);    //Local Magnetic Field y axis Maximum value in MicroTeslas
    running_max.z = max(running_max.z, compass.m.z);    //Local Magnetic Field z axis Maximum value in MicroTeslas
    delay(100);
  }
  delay(500);
  compass.read();
  where = compass.heading();
  azimuth = 0 - ((where / 360) * 63000);
  delay(500);
  stepper.runToNewPosition(azimuth);
  delay(10000);
}

The system does turn as expected, though doesn't then make the final turn after the 'for loop' where I would expect about a 90 degree turn from where I have set this up on the table (facing East before and at end of 360 degree calibration spin).

From what I gather, the vectors used are a special type which have been created for this library, where an array might traditionally be used?

Just wondering if anyone has any suggestions or if anyone knows what might be going wrong with I've typed up? If all else fails I'll just run a calibration whenever I want to run this system, though I would of course love this to be integrated into the main code for a better user experience!

p.s. the calculation at the end for azimuth is because my stepper setup requires 63000 steps to complete a single rotation of the system. Hope that helps for clarity!

LSM303 github page:

AccelStepper library link:
http://www.airspayce.com/mikem/arduino/AccelStepper/classAccelStepper.html

Thanks all!
Chris

In order to calibrate the magnetometer properly, you need to rotate it through a decent sampling of all possible 3D orientations, so that the minimum and maximum values recorded for each axis are reasonable estimates of the correct values.

See this exhaustive tutorial for the details.

I don't see how rotating the sensor through 360 degrees using a stepper can accomplish that task.

Thank you for the reply.

I have run it a few times with the stepper motor method, and can assure you it works. Since I only need to know the calibration in what is essentially 2D space I needn’t know the third axis beyond what is recorded during its full x-y rotation.

I’d be happy to post a video of the system in action if you want an to see it running? I, too, did have your concern at first, though it seems to be exceptionally stable and reliable, and is currently several hours into testing without fault.

Selfishly, I don’t suppose you have any experience working with the sensors and might be able to help with the original question? This morning I moved location and thus had to recalibrate before uploading the full system sketch again. A bit of a pain, but a fine interim solution.

You might be interested in a self calibrating magnetometer?

Take 2 magnetometers, mount them so that each axis is 180 degrees out of phase; x to x y to y and z to z. By summing the 2 readings together the interfering magnetic fields will cancel each other out. This scheme works quite well for when the magnetometers need to be calibrated and moved.

I use the LSM303 sensor, but that has nothing to do with your actual problem, which is not very clearly explained.

From what I gather, the motor does not execute this move:

  where = compass.heading();
  azimuth = 0 - ((where / 360) * 63000);
  delay(500);
  stepper.runToNewPosition(azimuth);

Of course, azimuth will be zero if "where" is between 0 and 360, because you are doing an integer divide.

Use floats or rearrange the expression to avoid that divide.

  azimuth = 0 - (where*63000/360);

When in doubt, it is almost always useful to put in some extra Serial.prints().

Thank you Idahowalker - I’ll look into that! I haven’t got a second magnetometer at the moment but I’ll try do some reading around the topic. Much appreciated!

Jremington - the “ azimuth = 0 - ((where / 360) * 63000);” part of the code actually works. I have it in another sketch - hence, I write, ‘I am able to interface the stepper motor with no issues’.

This was a quick burner sketch (with all the relevant detail) as my whole code is quite large, and wholly unnecessary to copy to here. My main code does have a disproportionate number of prints, fear not!

The problem seems pretty clear to me, but I’ll try again for you. Should be easy since you use the sensor. You have to calibrate the sensor in one sketch, right? From that you gather minimum and maximum values on the magnetic field in microteslas. You then enter these values as constants for your minimum and maximum range values for your local magnetic field in your main sketch, given that the magnetic field changes all over the earth and you have to recalibrate it every time you move any significant distance.

I would like to homogenise these parts into a single setup part of a sketch, making the calibration automatic at the beginning of the system’s operation, meaning there is no manual calibration and data transfer ever needed.

It’s no worries if you don’t know how to do it. The system works, like I have said. I just manually calibrate the magnetometer for the sketch before I run it. I’m just looking to optimise. An engineer’s got to engineer, y’know.

What I said about " azimuth = 0 - ((where / 360) * 63000);" is correct. "azimuth" will always be zero if the magnitude of "where" is less than 360, since "where" is declared as long.

Magnetometer calibration is required to correct for manufacturing offsets and errors in relative axial scale factors, plus hard and soft iron influences from the surroundings.

I recommend this exhaustive tutorial on magnetometer calibration: Tutorial: How to calibrate a compass (and accelerometer) with Arduino | Underwater Arduino Data Loggers

You do not need absolute units for the magnetometer, since the heading is calculated from the relative values of the X and Y components of the Earth's magnetic field, in the horizontal plane. It is irrelevant where you are on the Earth's surface, except for the need to correct for magnetic declination to get true North.

Good luck with your project.

And what I’m correcting for is the hard iron biases, therefore, there is a need to calibrate. I have read that whole tutorial before. After finding it meaningless to my project I decided to do more research, and then ended up on here asking if anyone had actually tried to achieve what I was aiming for. With what I’m doing there is no need to calibrate to the extent of that software, and even if I did - I would still need to correct for the biases.

It’s fine if you don’t know the answer to the actual question here, like I said. There’s no shame in that. I was merely inquisitive to know if anyone had combined the two sketches with any level of success.

The two operations are easily combined.

May I ask how?

I and others have placed the calibration routine as an option in setup(), which saves the 6 or 9 correction parameters in EEPROM.

The corrections are applied in the function that reads the magnetometer, and the resulting measurements are used in loop().

If you would like someone to write code for you, post in the Gigs and Collaborations forum section. You may be asked to pay for the help.

I had tried that solution, or at least that type of layout where a function is called in the setup before anything else major happens, though as I have previously mentioned, my coding base isn’t that strong. I probably made an error somewhere in my data storage and calling.

Thanks for the reply anyway.

I’m not looking for a handout, so much as advice. Like I said, engineer’s got to engineer, and I love working things out, but you’ve got to know when to ask for help. The system is working, and that’s really what matters.