LSM303D tilt compensation problem

Jim, your testing corrected data scenario, reminded me that that was what my problem about the implementation of the correction was.
I didn’t get the results l should have and when l asked about this to Merlin he never answered and l was unsure if the correction implementation was correct. Which l believe it was but couldn’t get the 1+0’s, so l may be
being premature about possible results and are hoping it still maybe able to correct the tilt inaccuracy.
Regards Kevin

Jim, is the above code correct implementation and if not, would you post yours?

Run the test I suggested and see.

UPDATE: working with the useful guide previously referred to (at LSM303DLHC - Calibration, Pitch, Roll and Tilt Compensated Heading - Sensors - Arduino Forum) and using the MagCal software because i had it already (i.e. not Magneto) my raw data AFTER scaling dividing all by 1,000,000 then ‘calculating’ gets these values:

which i input into the second script, HOWEVER i need guidance on factoring in the scaling of the raw input into this script. my logic is that this would ALSO have to be divided by the same scale, so i have:

Xm_off = compass.m.x*(1/11000.0) - 0.3360;
Ym_off = compass.m.y*(1/11000.0) + 0.1941;
Zm_off = compass.m.z*(1/9800.0 ) + 0.0757;

EXPLANATION: i get the 1/11000.0 from the (100000.0/1100.0 from the supplied script) and divide this, THEIR scale by MY scale which was 1000000 Does that make sense? Is this right?

ANYWAY so then i upload the new sketch with these values (and scaling) and grab another bucket of values pointing the sensor ‘painting the inside of a sphere’ a second time. The above website says running it a second time (as i check, i guess) you should “see the values are all closer to almost zero” and that’s not wot i get! Heres mine:

so thinking that graphing this ‘calibrated’ data might reveal some insight???

Clearly not circular, clearly not centred on zero. What are your thoughts?

EXTRA REGARDING SCALING THE RAW STUFF: from the @edwardmallon wordpress site he states If your sensors have SPI or I2C outputs, they will usually directly produce the required format. For example, the MicroMag3 magnetometer directly produces counts from -3411 to +3411.

OK. but my LSM303D outputs values from approx: +280,000.0 to -280,000. So instead of diving by 1M which is what is in the above example, maybe 100 would have been fine? bottom line is MagCal values are VERY different from his example…

MagCal outputs the INVERSE of the matrix that you need to use for the correction.

That is why I don’t use it, and use Magneto instead.

For the norm of the magnetic field, again, I recommend to use a value that is roughly the average absolute value of all the measurements.

ok i dont understand wot you mean by inverse of the matrix @Jim_Remington (note: i thought sailboatinstruments blog said both app.s were pretty much exactly the same but just used different equations to get their output?) but ANYWAY i will download and use Magneto instead to avoid complication + irritation.

oh, and do the same SCALING issues apply to Magneto regarding the scaling of the raw output too?

i will do this tomorrow. standby…

You could look up “matrix inverse”, but here it is:

very kind @Jim_Remington, thanks

Here is the re-run with scales of
x: 1000/1100
y: 1000/1100
z: 1000/980

AND magneto used instead of MagCal (no inversion needed:) Based on the raw data:

re-run with new new data:

values (underlined in red) ARE much clsoer to zero! HURRAH.

Now to calibrate the accelerometer too… standby

Ok so to the accelerometer.

here is the magneto output AFTER calibration:

Compared to the values [20.959833, -31.234477, - 106.387816], it is still not what i would call ‘close to zero’ for the combined bias, but let’s press-on…

Legend: red=heading, Blue=tilt, Yell=Roll

I kept the heading absolutely CONSTANT (in my wooden gimbal) . then
a. moved ONLY tilt (blue) up20deg, down 15deg. then back to neutral
b moved ONLY roll(yellow) right20deg, roll left 20deg, then back to neutral.

i never touched the heading, so the red should stay perfectly the same but as you can see it is LESS stable than before i ‘calibrated’ it!!! WTF. it ran better outta the box without calibration. I’m getting pretty frustrated with the LSM303D right now.

any thoughts?

The accelerometer is clearly not calibrated correctly.

You have up to 10% errors in the axial gain values (the matrix diagonals) and the offsets are too large. It looks to me that you have run the raw accelerometer data through Magneto, not the corrected data.

OK. thank you for the feedback. i have re-run the accelerometer calibration

a note: i think when i did the calibration the first time i ROTATED the device in all directions as you do with the compass calibration, but i did not JOLT the compass in all directions. This time, however i JOLTED the device :slight_smile: instead. (Its an accelerometer test, after all)


  1. here is the raw data and calc.#1 thru Magneto:

2 here is the calibrated data run thru magneto the second time (as a check):

the arduino forum notes say about this step to “… check if bias decreased.” . hmmmm well they have changed substantially, but i wouldn’t say they were close to zero. Anyway lets see if the tilt compensation is any better.

As before i am keeping the heading (red) constant. and then

  • moving the pitch (blue) up a bit then down a bit then back to middle, then
  • moving the roll left a bit, right a bit then back to middle:

As before the red should stay exactly stationary for a tilt compensated compass:

This is ANOTHER very poor result from the LSM303D. Because the values are consistent, my hunch is that it is still a calibration issue. BUT sheesh! What am i doing wrong???

for info: here is the Heading, Pitch + Roll code:

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

LSM303 compass;

const float alpha = 0.5;
float fXa = 0;
float fYa = 0;
float fZa = 0;
float fXm = 0;
float fYm = 0;
float fZm = 0;

void setup() {

void loop()
float pitch, pitch_print, roll, roll_print, Heading, Xa_off, Ya_off, Za_off, Xa_cal, Ya_cal, Za_cal, Xm_off, Ym_off, Zm_off, Xm_cal, Ym_cal, Zm_cal, fXm_comp, fYm_comp;

// Accelerometer calibration
Xa_off = compass.a.x/16.0 -  194.504657; //X-axis combined bias (Non calibrated data - bias)
Ya_off = compass.a.y/16.0 +   48.423808; //Y-axis combined bias (Default: substracting bias)
Za_off = compass.a.z/16.0 -  203.277823; //Z-axis combined bias
Xa_cal =  0.660704 *Xa_off - 0.031031 *Ya_off + 0.021107 *Za_off; //X-axis correction for combined scale factors (Default: positive factors)
Ya_cal = -0.031031 *Xa_off + 0.629166 *Ya_off - 0.027477 *Za_off; //Y-axis correction for combined scale factors
Za_cal =  0.021107 *Xa_off - 0.027477 *Ya_off + 0.615976 *Za_off; //Z-axis correction for combined scale factors

// Magnetometer calibration
Xm_off = compass.m.x*(1000/1100.0) - 398.775359; //all taken from _jan28_invertedM
Ym_off = compass.m.y*(1000/1100.0) + 158.889838; 
Zm_off = compass.m.z*(1000/980.0) + 100.543177;
Xm_cal =  0.360977 *Xm_off  - 0.033216 *Ym_off  + 0.003660 *Zm_off; 
Ym_cal = -0.033216 *Xm_off  + 0.396507 *Ym_off  + 0.004881 *Zm_off; 
Zm_cal =  0.003660 *Xm_off  + 0.004881 *Ym_off  + 0.351985 *Zm_off; 

// Low-Pass filter accelerometer
fXa = Xa_cal * alpha + (fXa * (1.0 - alpha));
fYa = Ya_cal * alpha + (fYa * (1.0 - alpha));
fZa = Za_cal * alpha + (fZa * (1.0 - alpha));

// Low-Pass filter magnetometer
fXm = Xm_cal * alpha + (fXm * (1.0 - alpha));
fYm = Ym_cal * alpha + (fYm * (1.0 - alpha));
fZm = Zm_cal * alpha + (fZm * (1.0 - alpha));

// Pitch and roll
roll  = atan2(fYa, sqrt(fXa*fXa + fZa*fZa));
pitch = atan2(fXa, sqrt(fYa*fYa + fZa*fZa));
roll_print = roll*180.0/M_PI;
pitch_print = pitch*180.0/M_PI;

// Tilt compensated magnetic sensor measurements
fXm_comp = fXm*cos(pitch)+fZm*sin(pitch);
fYm_comp = fXm*sin(roll)*sin(pitch)+fYm*cos(roll)-fZm*sin(roll)*cos(pitch);

// Arctangent of y/x
Heading = (atan2(fYm_comp,fXm_comp)*180.0)/M_PI;
if (Heading < 0)
Heading += 360;

Serial.print("Pitch (X): "); Serial.print(pitch_print); Serial.print("  ");
Serial.print("Roll (Y): "); Serial.print(roll_print); Serial.print("  ");
Serial.print("Heading: "); Serial.println(Heading);

Jolting is not the way to calibrate the accelerometer. Calibration relies on the presence of a constant acceleration, which in this case is supplied by the force of gravity. In the case of the magnetometer, it is the local value of the Earth’s magnetic field.

For accurate calibration, you need to be very careful to hold the accelerometer still while making each measurement, so it is quite a bit more finicky than the magnetometer.

That said, I don’t understand your plot. It seems possible that at some point, you are crossing a boundary of 90 or 180 degrees, and the math is failing because of sign ambiguity in a sin() or cosine() term.

OK thank you @Jim_Remington for the feedback - my mistake on the jolting, (I assumed i needed maximum readings) but no worries.

Before i get to the calibration re run:
Your comment “I don’t understand your plot. It seems possible that at some point, you are crossing a boundary of 90 or 180 degrees, and the math is failing because of sign ambiguity in a sin() or cosine() term.” I’m not sure WHERE in the plot you see this happening. I guess it is possible that i have sign ambiguity , though i have copied and pasted code, so it seems unlikely) but anythings possible, right?

So while we are discussing the +/- signs, and to confirm:

  • for the first three boxes in magneto I have been SUBTRACTING from the equations, and
  • the next nine boxes i have ADDED.
  • Wherever there are two negative, it becomes positive

This time I have held it still and just ROTATED it in all directions (as with compass calibration) and re run accelerometer calibration once more. The results are below:

1. AccelRAW

the coloured data output i put in the script like this:

Then re-run magneto

2. AccelCALIB

3. re-run of Heading Pitch and Roll

the re-run with the eqtns copied across. As before read is heading which i hold constant (via my wooden gimbal) then i only move pitch and then Roll
-i move Pitch (blue) forward, then back then return it to neutral

  • then Roll (yellow) to left a bit, right a bit and neutral
    see below:

The heading of the device stayed the SAME and should not change at all. but the output (red) changes markedly. :frowning:

arggg. What to do next>???

I’m hapy to try testing, changing the +/- signs, but there are multiudes of combinations to try. Where to start? There MUST be a logical solution to this!!!

Question: why is the dividing factor of 16.0 used to scale the accelerometer data? It is not needed. Has that factor always been there, before and after calibration?

Assuming that you have always included the factor of 16, it looks like you are applying the correction matrix and offset correctly.

However, the pitch correction is simply wrong. There must be a fundamental definition or sign error somewhere else – in the definition of the positive direction of the X, Y, Z axes and their association with the pitch roll and yaw axes, the definition of the positive direction of the pitch angle, etc.

Review the definitions of these terms at

Try changing the sign of the pitch angle before it goes into the correction.

sixD, a couple of things.
The Accel and Mag testing to be done correctly should have the IMU fixed to a non magnetic axle (horizontal to the floor, with some means of turning and indicating a equal distance set amount ) and rotated say 24 time (equal steps) around 360 degrees(also away from mag influences). As an example the X axis should point in the direction of the axle. 24 measurements for Mag and Accel are done seperatly but both done while X axis is in this position. Next the Y axis along axle same measurements again, two lots, 1 mag, 1 accel.
Last axis Z, is done by putting the imu on its side, the the z axis now parallel with the axle. Mag, accel , 2 lots of separate data. The 3 lots of separate data for each axis are then all put together as 1 lot of data, 1 for mag, 1 for accel. By using the axle, is how you get the accel without any other forces acting on it.
Photo attached showing cal device and IMU in Z axis test position
Jim says to yous the average for the norm, but Merlin says to use the highest positive reading in your un calibrated data as the norm.
This all takes a bit of time and you need some code to be able to take a averaged sample at each of the equal steps around the 360 degrees.
In the above code l supplied l didn’t un comment that divide by 16 (>>4,(bit move 4) same thing) as the IMU has a 16 bit Accel not 12 which is what that dividing was there for on the older IMU.
Also remove the (1000/1100.0) from the mag, this is also not required (also not un commented) as this is only required if wanting to get a mag field strength back as some data.
I have confirmed that this code works correctly to Merlins Magneto correction data, centreing and making the ellipsoid a circle (also retested with Magneto and with not so good cal data (done by waving around) within <1.2 %
You should have also found that the heading is increasing counter clockwise instead of CW, is this the case?
BUT going CCW tilted to 45 degrees my worst results are left tilt -4, R +5 using min,max cal my IMU was -13,+12 ( CW left +7, R +5) spreed-sheet attached.
Here is the code for CCW (standard code) with Accel scaling (also accel sign for nose up +, right wing down +) and CW heading line 79,80
Jim, could you have a look and see why CW has totally different results.
I have tried changing the signs all over on the mag, only were they are has a reasonable result been able
magneto corrected CCW in shed.xlsx (11.7 KB)
jim.ino (5.9 KB)


sixD, why did you change the alpha constant so much, you will be getting very little sample average?
Correction the latest code is Nose up -, if sign changed to nose up positive Line 44, the pitch becomes unstable like sixD has.

Hi team, thanks for the replies

yes. the scaling factors are there both before and after. they were from the code in :// i know now they are not needed unless you need actual absolute measures (which i dont, but they are there both before and after, so no harm done)

Thanks, i did that. i dont see any issues with that.

I am unsure which sign to change. Do you men within the equation for Xa_cal, fxa or the equation for Fxm_comp?

thanks also to @kevin1961 for the information and picture. i dont have it on ‘an axle’, but instead in a wooden gimbal. I’m thinking that this should be ok. but what i have NOT tried is 24 separate static compass, then accel measurements. INSTEAD i am taking 100s of measurements (1000?) i have been carefully rotating the device as @kevin described i.e. the imagine you are painting the inside of a sphere (and described at sailboatinstruments and the underwater flow project - all linked above) and recording data every 150ms. … It’s how i got the ‘oval’ at the start of this thread and how i understood the calibration data was to be collected.

simply to ‘smooth’ the results. it just affects the filter. not the calibration.

this is my line 44:
fYa = Ya_cal * alpha + (fYa * (1.0 - alpha));

do you mean the ‘-’ infront of the alpha??? this is just part of the filter and i dont think will affect the tilt-compensation.

Thanks for the input so far. Next step is to adding to the code an uncalibrated output alongside calibrated ouptut i.e. running concurrently, so i can try to isolate the problem. and hopefully which of these +/- should be swapped.


I think if you look at it, increasing the constant decrease sampling and makes for a faster result, as per comments in my code.
Also sign change is re my code.

Is your heading increase changing positive CW or CCW

How can you paint the inside of a sphere and have the IMU stationary to be able to get the Accel data?