[SOLVED] ADXL345 accelerometer 6-point calibration but only 3 offset registers

I machined a cube, put it on a leveling table, screwed the breakout board on, and averaged 50 readings:

X up 252 delta 4
X down -262 delta 6
Y up 257 delta 1
Y down -256 delta 0
Z up 255 delta 0
Z down -242 delta 14

I want to use the device in the ±2 g mode, so I set bits D0 and D1 in register 0x31 accordingly.

On page 30 of Analog Devices' application note, I see there are 3 internal offset register addresses:

0x1E OFSX X-axis offset
0x1F OFSY Y-axis offset
0x20 OFSZ Z-axis offset

On page 30, the offset calibration only refers to a no-turn or 1-point calibration scheme.

But what must I do now with the 6 deltas I obtained from the 6 orientations, when there are only 3 offset registers and no 6-point calibration scheme mentioned?

Are the offset registers only good for a more or less useless 1-point calibration, which would mean I would have to take care of the delta in code instead?

There is no reason to use the internal offset registers. Simply apply the six correction factors (3 offset, 3 scale factors, expressed as floats) to the raw readings in the Arduino code.

Example for one axis:

float ax_corr = ax_scale*(ax_raw - ax_offset);

To check that the corrections have been applied properly, rerun the calibration step on the corrected values. You should end up with three new offsets = 0.0 and three new scale factors = 1.0.

Ok, thanks, I thought using the IC’s own registers is “better” and/or “faster”.

Where did you obtain the scale and offset factors? I meanwhile found another application note with different formulas, mentioning an “offset” and “gain” factor, allowing to use the 6 deltas, the way I understand it.

  1. Xoffset = 0.5 × (Xup + Xdown)
  2. Xgain= 0.5 × ((Xup - Xdown)/256)
  3. Xcorrected = Xraw - Xoffset/Xgain

I read it so that my raw ~256 readings Xup, Xdown, etc. must be first converted to g (a value between 0-1 I suppose, or 0-9.81) first of all.

Where did you obtain the scale and offset factors?

One way to obtain them is to use the procedure described in this link: Calibration and Programming | Adafruit Analog Accelerometer Breakouts | Adafruit Learning System

That tutorial uses the map() function to apply the scale and offsets, which use the same equation as I posted above, but is less clear.

See this post and this tutorial for more practical and more accurate methods.

I read Analog Devices' ADXL specification sheet and used the offset and gain compensation as described in AN-1057 page 8 in the code below. Despite the values calculated to compensate, when tumbling around the y-axis, the x and z values reach 255 - 256 all right, but x never goes to 0 but lingers around +2 and z never goes to 0 but lingers around -3, please see y-axis tumble graph below.

Is there something wrong with my code implementation from Analog Devices? Is this simply a limit of accuracy that can be achieved?

#include <Wire.h>

#define DEVICE (0x53) // ADXL345 I2C address (fixed)

byte _buff[6];

char POWER_CTL = 0x2D;

char DATA_FORMAT = 0x31;

char DATAX0 = 0x32; // x-axis data register byte 0
char DATAX1 = 0x33; // x-axis data register byte 1
char DATAY0 = 0x34; // y-axis
char DATAY1 = 0x35; //
char DATAZ0 = 0x36;
char DATAZ1 = 0x37;

// Averaged readings from 2-point calibration with 6-point-tumble method (values particular to each IC)
const int xUp =  254; // Delta -2 to 256
const int xDown = -262; // Delta +6
const int yUp =  257; // Delta +1
const int yDown = -255; // Delta -1
const int zUp =  255; // Delta -1
const int zDown = -242; // Delta -14

float xOffset, xGain, yOffset, yGain, zOffset, zGain;

const float alphaEMA = 0.2; // Smoothing factor 0 < α < 1 (smaller = smoother = less responsive)
float xEMA = 0;  // Seed with an arbitrary reading (0 = PCB oriented z-up at start)
float yEMA = 0;
float zEMA = 256;

void setup()

{

  Wire.begin();

  Serial.begin(57600);

  registerWrite(DATA_FORMAT, 0x00); // Set to 2g mode, typical output -256 +256 per axis, see p. 4 http://www.analog.com/media/en/technical-documentation/data-sheets/ADXL345.pdf
  registerWrite(POWER_CTL, 0x08); // Set to measuring mode

  // Cast ints to float at execution time, as per Analog Devices and Sparkfun tutorial
  xOffset = 0.5 * ((float)xUp + (float)xDown); // -4
  xGain = 0.5 * (((float)xUp - (float)xDown) / 256); // 1.008
  yOffset = 0.5 * ((float)yUp + (float)yDown); // +1
  yGain = 0.5 * (((float)yUp - (float)yDown) / 256); // 1
  zOffset = 0.5 * ((float)zUp + (float)zDown); // +6.5
  zGain = 0.5 * (((float)zUp - (float)zDown) / 256); // 0.971

}

void loop()
{
  readSensor();

  //float xRot = atan(xEMA / sqrt((pow(yEMA, 2)) + pow(zEMA, 2))) * 57.2957 + 90; // See p. 7 http://www.analog.com/media/en/technical-documentation/application-notes/AN-1057.pdf
  //float yRot = atan(yEMA / sqrt((pow(xEMA, 2)) + pow(zEMA, 2))) * 57.2957 + 90;
  //float zRot = atan(sqrt((pow(xEMA, 2)) + pow(yEMA, 2)) / zEMA) * 57.2957;

  delay(100); // For CoolTerm/Excel use only
}

void readSensor() { // Read from the sensor, calibrate readings, smooth output
  uint8_t bytesToRead = 6; // Burst read (preferential as per Analog Devices)
  registerRead( DATAX0, bytesToRead, _buff); // Read from the 6 registers

  int xRaw = (((int)_buff[1]) << 8) | _buff[0]; // 10 bit (2 bytes), LSB first, convert into integer
  int yRaw = (((int)_buff[3]) << 8) | _buff[2];
  int zRaw = (((int)_buff[5]) << 8) | _buff[4];

  float xCal = ((float)xRaw - xOffset) / xGain; // See p. 8 http://www.analog.com/media/en/technical-documentation/application-notes/AN-1057.pdf
  float yCal = ((float)yRaw - yOffset) / yGain;
  float zCal = ((float)zRaw - zOffset) / zGain;

  xEMA = (alphaEMA * xCal) + ((1 - alphaEMA) * xEMA); // See https://en.wikipedia.org/wiki/Exponential_smoothing
  yEMA = (alphaEMA * yCal) + ((1 - alphaEMA) * yEMA);
  zEMA = (alphaEMA * zCal) + ((1 - alphaEMA) * zEMA);

  Serial.print(xEMA,0);
  Serial.print(", ");
  Serial.print(yEMA,0);
  Serial.print(", ");
  Serial.println(zEMA,0);
}

void registerWrite(byte address, byte val)
{
  Wire.beginTransmission(DEVICE);
  Wire.write(address);
  Wire.write(val);
  Wire.endTransmission();
}

void registerRead(byte address, byte num, byte _buff[]) // Reads num bytes into _buff array
{
  Wire.beginTransmission(DEVICE);
  Wire.write(address);
  Wire.endTransmission();
  Wire.beginTransmission(DEVICE);
  Wire.requestFrom(DEVICE, num);
  
  byte i = 0;
  
  while (Wire.available())
  {
    _buff[i] = Wire.read(); // Read 1 byte
    i++;
  }
  
  Wire.endTransmission();
}

x never goes to 0 but lingers around +2 and z never goes to 0 but lingers around -3

The offsets were not accurately enough determined.

It is tricky to calibrate an accelerometer, because you need to make sure that the accelerometer is perfectly still when each calibration data point is collected.

I'm using a levelling table and a precision machined block of steel, the sensor PCB was glued on in a rectilinear jig, no motion, temperature 20°C.

The PCBs were purchased from Adafruit/Sparkfun. The whole lot shows the same issue. Sometimes x is a bit off. Sometimes y. Sometimes z.

At least the li'l error is always repeatable, lol.

I could fudge the figures calculated as per Analog Devices, but that would feel... wrong. But maybe that's the only way to do it.

It is much more accurate to do a full 3D calibration, as described in this tutorial.

The min/max 6 point method introduces statistical errors.

Thanks, the link does not work, but I figured it out with help from Analog Devices.

Each sensor/PCB is a bit different; stick each to a machined rectangular block and set on a leveling table. Take raw readings in all six orientations and average them. In other words, use the two-point six-point-tumble method:

// 2g mode sensor resolution 3.9 mg/LSB => factor = 0.0039 g
const float rawToG = 0.0039;
float xUp1g, xDown1g, yUp1g, yDown1g, zUp1g, zDown1g;
float xOffset, xGain, yOffset, yGain, zOffset, zGain;

// Values for one of the sensors/PCBs
const int xUp =  252;
const int xDown = -262;
const int yUp =  257;
const int yDown = -255;
const int zUp =  255;
const int zDown = -241;

Then, in setup(); for the 2g mode, convert the above to g and calculate offset and gain:

xUp1g = xUp * rawToG;
xDown1g = xDown * rawToG;
yUp1g = yUp * rawToG;
yDown1g = yDown * rawToG;
zUp1g = zUp * rawToG;
zDown1g = zDown * rawToG;

xOffset = 0.5 * (xUp1g + xDown1g);
xGain = 0.5 * (xUp1g - xDown1g);
yOffset = 0.5 * (yUp1g + yDown1g);
yGain = 0.5 * (yUp1g - yDown1g);
zOffset = 0.5 * (zUp1g + zDown1g);
zGain = 0.5 * (zUp1g - zDown1g);

Finally, in loop(); or a function, obtain the raw sensor output as before, and calculate the adjusted x, y and z values:

float xCal = (((float)xRaw * 0.0039 - xOffset) / xGain) * 256;
float yCal = (((float)yRaw * 0.0039 - yOffset) / yGain) * 256;
float zCal = (((float)zRaw * 0.0039 - zOffset) / zGain) * 256;

Now each sensor/PCB yields repeatable adusted readings (x = 0, y = 0, z = 256) flat on the table, and one can convert into pitch and roll. Using a simple EMA with an alpha of 0.2 makes the pitch and roll output smooth and still sufficiently reactive (for my use case).

Sorry, fixed the link above. Here it is again: http://thecavepearlproject.org/2015/05/22/calibrating-any-compass-or-accelerometer-for-arduino/

All right, thanks!