Help for Compass readings that are not evenly spaced in 360 degrees

Does this happen to you with your compass module? You get it all working and calculate a heading in degrees and it seems fine except that it’s not consistent as you turn the compass around in 360 degrees? There is a section of the compass where the numbers are consistently way out of kilter with the rest. In addition, you aren’t getting 360 degrees and things like North to West are not 90 degrees apart?

I was totally frustrated by this and after days of research and elusive and vague suggestions and answers (scale, round, limit, adjust, etc., etc.) I was absolutely frustrated. I had just purchased a new OSEPP Compass sensor and although it was providing consistent numbers, they were not sufficient for determining if my project had turned 90 degrees from a specified direction. I tried every library and code example I could find for the HMC5883L. I learned a lot in the process, about declination, about I2C communications, etc.

I also learned how people can give solutions that work for them but don’t really apply to you or don’t provide enough info to really grasp how they came up with seemingly random numbers that adjusted their x-y values. None of the talk about scaling and adjusting was making any sense to me until I came across this blog: Calibrating the Compass - Wayne's Tinkering Page. This guy didn’t even tell you specifically how he calculated his adjustments but what he did show was how he used the x-y coordinates in a spreadsheet to generate an x-y scatter chart. When I saw this, it all made sense!

The problem is that something in the environment where my compass sensor is installed is affecting the compasses perception of the x and y magnetic forces, causing numbers that don’t work for heading angles! After throwing my own x-y numbers into a spreadsheet and charting them, it became even more clear: your x and y min and max values need to average 0! Or in other words, the midpoint between the max and min values in each axis needs to be 0. If this isn’t the case, the origin doesn’t revolve around 0,0 and calculating heading degrees results in the lack of fidelity that has frustrated the living crud out of me.

I’ve stuck with the original example code from the OSEPP site and added all the cool tricks (declination, gauss scale, etc.) I’ve gleaned from other available code. After adding an adjustment for x and y based on my spreadsheet, like magic, I was getting near perfect 360 degree readings all the way around the compass. Needless to say I was quite pleased.

I have seen numerous forum posts where someone described my problem but the provided solutions and/or suggestions had nothing to do with the real problem–the compass output needs adjustment in a calculated way. Some folks touched on this but no one ever laid it out so clearly that I knew what to do.

It is as simple as this:
Collect a sample of x-y readings from all directions (at a minimum collect N, NE, E, SE, S, SW, W, NW).
Calculate the min and max values of each axis (for x and for y).
Calculate the average of the min and max values for each axis (for x and for y).
Add a constant in your code for each value (xAdj and yAdj, for example) and assign the value you calculated for each.
After you retrieve x and y from your compass, subtract the appropriate adjustment value BEFORE you calculate the heading with ATAN2.

Be careful if you’re using the Love Electronics library that provides RAW and Scaled x-y values. If you’re going to use the Scaled values for calculating headings, make sure you use the Scaled values to calculate your adjustment values.

Here is the OSEPP example code I hacked to make this thing finally work reliably and accurately.

// OSEPP Compass Sensor Example Sketch
// by OSEPP <>
// Modifications by Chris W. to accommodate declination, scaling and origin adjustment 2013-02-13
// This sketch demonstrates interactions with the Compass Sensor

#include <Wire.h>

const uint8_t sensorAddr = 0x1E;   // Sensor address (non-configurable)
const float xOffset = 103.0;      // Offset required to adjust x coordinate to zero origin
const float yOffset = -165.0;       // Offset required to adjust y coordinate to zero origin
const float declination = 70.1;    // Enter magnetic declination mrads here (local to your geo area) 

// One-time setup
void setup()
   // Start the serial port for output

   // Join the I2C bus as master

   // Configure the compass to default values (see datasheet for details)
   WriteByte(sensorAddr, 0x0, 0x70);
   WriteByte(sensorAddr, 0x1, 0x20); // +1.3Ga

   // Set compass to continuous-measurement mode (default is single shot)
   WriteByte(sensorAddr, 0x2, 0x0);

// Main program loop
void loop()
   uint8_t x_msb;   // X-axis most significant byte
   uint8_t x_lsb;   // X-axis least significant byte
   uint8_t y_msb;   // Y-axis most significant byte
   uint8_t y_lsb;   // Y-axis least significant byte
   uint8_t z_msb;   // Z-axis most significant byte
   uint8_t z_lsb;   // Z-axis least significant byte

   int x;
   int y;
   int z;
   // Get the value from the sensor
    if ((ReadByte(sensorAddr, 0x3, &x_msb) == 0) &&
       (ReadByte(sensorAddr, 0x4, &x_lsb) == 0) &&
       (ReadByte(sensorAddr, 0x5, &z_msb) == 0) &&
       (ReadByte(sensorAddr, 0x6, &z_lsb) == 0) &&
       (ReadByte(sensorAddr, 0x7, &y_msb) == 0) &&
       (ReadByte(sensorAddr, 0x8, &y_lsb) == 0))
    x = x_msb << 8 | x_lsb;
    y = y_msb << 8 | y_lsb;
    z = z_msb << 8 | z_lsb;
    int xs;
    int ys;
    int zs;

    float gScale = .92;  // Scale factor for +1.3Ga setting
    float adjx = x - xOffset;
    float adjy = y - yOffset;

    xs = adjx * gScale; 
    ys = adjy * gScale;
    zs = z * gScale;    
    float heading = atan2(ys, xs);
    heading += declination / 1000; // Declination for geo area
      if (heading < 0);
        heading += 2*PI;
      if (heading > 2*PI)
        heading -= 2*PI;

      float angle = heading * 180/M_PI;
      Serial.print("X: ");
      Serial.print("   Y: ");
      Serial.print("   Z: ");
      Serial.print("   Xs: ");
      Serial.print("   Ys: ");
      Serial.print("   Zs: ");
      Serial.print("   H: ");
      Serial.print("   A: ");
      Serial.println("Failed to read from sensor");

   // Run again in 1 s (1000 ms)

// Read a byte on the i2c interface
int ReadByte(uint8_t addr, uint8_t reg, uint8_t *data)
   // Do an i2c write to set the register that we want to read from

   // Read a byte from the device
   Wire.requestFrom(addr, (uint8_t)1);
   if (Wire.available())
      *data =;
      // Read nothing back
      return -1;

   return 0;

// Write a byte on the i2c interface
void WriteByte(uint8_t addr, uint8_t reg, byte data)
   // Begin the write sequence

   // First byte is to set the register pointer

   // Write the data byte

   // End the write sequence; bytes are actually transmitted now

Can you be a little more specific about how to calculate your scaling factors?

  1. Collect a sample of x-y readings from all directions (at a minimum collect N, NE, E, SE, S, SW, W, NW).


  1. Calculate the min and max values of each axis (for x and for y).

-Seems straightforward, but what are we calculating? Won’t this just be the largest and smallest values recorded above?

  1. Calculate the average of the min and max values for each axis (for x and for y).


  1. Add a constant in your code for each value (xAdj and yAdj, for example) and assign the value you calculated for each.

-Is the constant just the average, so that when subtracted, the average value becomes zero?


Nevermind, I figured it out. As stated, I made sure the sensor had minimal influence from its surroundings and was flat, then just found the min/max values for x and y by rotating it about the z axis. You then calculate an average (which is basically the median) of the min and max values, and this is your factor to be subtracted out.

Works great, thanks.

You really helpt me out with this. Thanks!

You need to adjust the scale and offset factor for each of the 3 axes of the magnetometer