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: https://sites.google.com/site/wayneholder/self-driving-rc-car/calibrating-the-compass
. 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 <http://www.osepp.com>
// Modifications by Chris W. to accommodate declination, scaling and origin adjustment 2013-02-13
// This sketch demonstrates interactions with the Compass Sensor
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
// 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
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
// 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;
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(" 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
*data = Wire.read();
// Read nothing back
// 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