ADXL345 inclinometer for sheet metal folding

Hei,

I'm in the process of making a little workshop tool, also to learn more about interfacing with sensors. To keep the code small, I am not using an ADXL345 library. Later, the inclinometer shall display x or y (pitch or roll) angles and I intend to incorporate a zeroing-button, in case one begins on a fold that is not co-planar to the workbench or press brake's support.

I have done a 2-point calibration with the 6-point-tumble method, with a leveling block on a leveling table, and should later use the values so obtained to correct for this particular IC. The IC is not soldered to the breakout board to allow assuming it is co-planar to it or parallel to the breakout board edges.

The problem I have is that pitch works alright, but roll only provides around -43° to + 43° degrees, when the breakout board is flipped upside down. In all other cases the serial monitor displays "nan". After reading to start learning about DOF and the +90° to -90° problem, I still do not know why roll does not work the way I thought it would; obviously, I'm not understanding something here.

Any hints much appreciated!

#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 byte 0
char DATAX1 = 0x33; // x axis byte 1
char DATAY0 = 0x34; // y axis byte 0
char DATAY1 = 0x35; // y axis byte 1
char DATAZ0 = 0x36; // z axis byte 0
char DATAZ1 = 0x37; // z axis byte 1

float xMin = -267; // Readings from 2-point calibration with 6-point-tumble method (are particular to each IC)
float xMax =  247;
float yMin = -261;
float yMax =  252;
float zMin = -257;
float zMax =  234;

void setup()

{

  Wire.begin();

  Serial.begin(57600);

  registerWrite(DATA_FORMAT, 0x00); // Set to 2g mode (outputs values -256 +256 per axis as per Analog Devices data-sheet)
  registerWrite(POWER_CTL, 0x08); // Set to measuring mode

} // End setup

void loop()

{

  readSensor(); // Read from the sensor
  delay(1000); // Only for readability

} // End loop

void readSensor() {

  uint8_t bytesToRead = 6;
  registerRead( DATAX0, bytesToRead, _buff); // Read from the 6 registers

  int x = (((int)_buff[1]) << 8) | _buff[0]; // 10 bit (2 bytes), LSB first; convert into an int (Analog Devices data-sheet)
  int y = (((int)_buff[3]) << 8) | _buff[2];
  int z = (((int)_buff[5]) << 8) | _buff[4];

  Serial.print( x ); // Only for calibration process
  Serial.print(", "); // Excel delimiter (use CoolTerm at 57600)
  Serial.print( y );
  Serial.print(", ");
  Serial.println( z );

  // See p. 9, 11 and 12 of https://www.nxp.com/files-static/sensors/doc/app_note/AN3461.pdf as to DOF and singularity problem
  float xRoll  = (atan2(-y, z) * 180.0) / M_PI; // Rotation around the x axis
  float yPitch = (atan2( x, sqrt(y * y + z * z)) * 180.0) / M_PI; // Rotation around the y axis
  Serial.print(xRoll);
  Serial.print(", ");
  Serial.println(yPitch);

} // End readSensor

void registerWrite(byte address, byte val) {

  Wire.beginTransmission(DEVICE);
  Wire.write(address);
  Wire.write(val);
  Wire.endTransmission();

} // End registerWrite

void registerRead(byte address, int num, byte _buff[]) { // Reads num bytes starting from address register on device in to _buff array

  Wire.beginTransmission(DEVICE);
  Wire.write(address);
  Wire.endTransmission();
  Wire.beginTransmission(DEVICE);
  Wire.requestFrom(DEVICE, num);

  int i = 0;

  while (Wire.available())
  {

    _buff[i] = Wire.read(); // Read 1 byte
    i++;
  }

  Wire.endTransmission();

} // End registerRead
  1. x, y, z must be floats. The following is likely to fail if they are ints:
sqrt(y * y + z * z)
  1. You never apply the calibration parameters to correct the accelerometer measurements. You need to calculate an offset to subtract, and scale factors to apply to each axis, and apply them.

Example:

float yMin = -261;
float yMax =  252;
float yOffset = (yMax+yMin)/2.0;
float yScale = (yMax-Ymin)/2.0;
...
float y = (((int)_buff[3]) << 8) | _buff[2];
y = (y-yOffset)/yScale;

Ok, you're right; no ints for that.

I have incorporated the minimum and maximum values from the calibration process now. I checked the leveling block and leveling table and did it all again, just to be sure.

float xMin = -269; // Readings from 2-point calibration with 6-point-tumble method (are particular to each IC) for adjustment
float xMax =  245;
float yMin = -261;
float yMax =  252;
float zMin = -256;
float zMax =  235;

Then, I incorporate these values as suggested by Adafruit

float xAdj = map(x, xMin, xMax + 1, -256, 257); // Add 1 to upper bounds, see http://forum.arduino.cc/index.php?topic=72153.msg540476#msg540476
float yAdj = map(y, yMin, yMax + 1, -256, 257);
float zAdj = map(z, zMin, zMax + 1, -256, 257);

But stragely enough, the adjusted values are slightly worse over 40 sample measurements taken at 1 second intervals. I am sure the temperature is well controlled.

So, yes, but no cigar.

I do what you suggest instead of using the method recommended by Adafruit.

What I also find odd is that the error, evidenced by the 2-point 6-point-tumble calibration, is so very asymmetric, all up values well below the theoretical ideal of 256 and all the down values below -256. As I'm doing this for the first time, is this to be expected?

Where does the "theoretical value" of 256 come from? Closer reading of the ADXL345 data sheet shows that this value is typical for 1 g acceleration, with actual observed values ranging from 230 to 280 for different chips.

All that matters for calculating tilt values is that all axes have zero offset and are on the same scale, which using the offset/scaling procedure outlined in reply #1 is 1 g == 1.0.

The map() function is a poor choice for scaling, because it works only with integers.

Ok, I see; I misunderstood "typical" in that case, disregarding the magnitude of fluctuations that may occur.

I changed the code as you suggested, see below, but am negatively surprised why in several orientations, the calibrated output is either even worse than the non-calibrated one, or marginally better but still way off, even though I made sure to use the same leveling block, leveling table and ambient room temperature, wiping the surfaces clean from dust, etc.

From what I've read on various forums, including Analog Devices data sheets 1, 2 and 3 over the last couple of days, I had the impression one can obtain good results via careful calibration - or that at least the calibrated output should be better than the non-calibrated output.

Do you (or anybody else) have any suggestions how things could be improved from here, if at all?


#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 byte 0
char DATAX1 = 0x33; // x axis byte 1
char DATAY0 = 0x34;
char DATAY1 = 0x35;
char DATAZ0 = 0x36;
char DATAZ1 = 0x37;

const int xMin = -268; // Readings from 2-point calibration with 6-point-tumble method (are particular to each IC) for adjustment
const int xMax =  246;
const int yMin = -261;
const int yMax =  253;
const int zMin = -258;
const int zMax =  234;

const float xOffset = (xMax + xMin) / 2.0;
const float xScale = (xMax - xMin) / 2.0;
const float yOffset = (yMax + yMin) / 2.0;
const float yScale = (yMax - yMin) / 2.0;
const float zOffset = (zMax + zMin) / 2.0;
const float zScale = (zMax - zMin) / 2.0;

void setup()

{

  Wire.begin();

  Serial.begin(57600);

  registerWrite(DATA_FORMAT, 0x00); // Set to 2g mode (outputs values -256 +256 per axis as per Analog Devices data-sheet)
  registerWrite(POWER_CTL, 0x08); // Set to measuring mode

} // End setup

void loop()

{

  readSensor(); // Read from the sensor
  delay(500); // Only for readability

} // End loop

void readSensor() {

  uint8_t bytesToRead = 6;
  registerRead( DATAX0, bytesToRead, _buff); // Read from the 6 registers

  float x = (((int)_buff[1]) << 8) | _buff[0]; // 10 bit (2 bytes), LSB first; convert into an int (Analog Devices data-sheet)
  float y = (((int)_buff[3]) << 8) | _buff[2];
  float z = (((int)_buff[5]) << 8) | _buff[4];
  
  x = (x - yOffset) / xScale;
  y = (y - yOffset) / yScale;
  z = (z - yOffset) / zScale;

  // See p. 9, 11 and 12 of https://www.nxp.com/files-static/sensors/doc/app_note/AN3461.pdf as to DOF and singularity problem
  float xRoll  = (atan2(-y, z) * 180.0) / M_PI; // Rotation around the x axis
  float yPitch = (atan2( x, sqrt(y * y + z * z)) * 180.0) / M_PI; // Rotation around the y axis

  Serial.print(xRoll);
  Serial.print(", ");
  Serial.println(yPitch);

} // End readSensor

void registerWrite(byte address, byte val) {

  Wire.beginTransmission(DEVICE);
  Wire.write(address);
  Wire.write(val);
  Wire.endTransmission();

} // End registerWrite

void registerRead(byte address, int num, byte _buff[]) { // Reads num bytes starting from address register on device in to _buff array

  Wire.beginTransmission(DEVICE);
  Wire.write(address);
  Wire.endTransmission();
  Wire.beginTransmission(DEVICE);
  Wire.requestFrom(DEVICE, num);

  int i = 0;

  while (Wire.available())
  {

    _buff[i] = Wire.read(); // Read 1 byte
    i++;
  }

  Wire.endTransmission();

} // End registerRead

You should be averaging several readings together to reduce noise.

The IC is not soldered to the breakout board to allow assuming it is co-planar to it or parallel to the breakout board edges.

What does this mean? How can you ever be sure that the chip is actually "level"?

jremington:
What does this mean? How can you ever be sure that the chip is actually "level"?

Well, you never can be. Hence I wrote what I wrote.

I already tried an EMA, but that only results in the error showing up with a delay, depending on alpha. Something else is wrong in my case.

So, now with an EMA to smooth things out, does not yield that much better results, see before/after charts and code below.

I have no idea how to troubleshoot this. When in measuring the same conditions are used as in the calibration stage, where I obtained the x, y and z minimum and maximum readings, and I calculate the offset and scale as suggested, how can the values still be not at least a bit better as without using the calibration values?

#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 byte 0
char DATAX1 = 0x33; // x axis byte 1
char DATAY0 = 0x34;
char DATAY1 = 0x35;
char DATAZ0 = 0x36;
char DATAZ1 = 0x37;

const int xMin = -268; // Readings from 2-point calibration with 6-point-tumble method (are particular to each IC) for adjustment
const int xMax =  246;
const int yMin = -261;
const int yMax =  253;
const int zMin = -258;
const int zMax =  234;

const float xOffset = (xMax + xMin) / 2.0;
const float xScale = (xMax - xMin) / 2.0;
const float yOffset = (yMax + yMin) / 2.0;
const float yScale = (yMax - yMin) / 2.0;
const float zOffset = (zMax + zMin) / 2.0;
const float zScale = (zMax - zMin) / 2.0;

const float alphaEMA = 0.2; // Smoothing factor 0 < α < 1 (smaller = slower, less responsive to changes)
float xEMA = 0;  // Initialise with a first reading (x and y in the horizontal plane)
float yEMA = 0;

void setup()

{

  Wire.begin();

  Serial.begin(57600);

  registerWrite(DATA_FORMAT, 0x00); // Set to 2g mode (typical output -256 +256 per axis as per Analog Devices)
  registerWrite(POWER_CTL, 0x08); // Start measuring

} // End setup

void loop()

{

  readSensor(); // Read from the sensor
  delay(500); // Only for readability

} // End loop

void readSensor() {

  uint8_t bytesToRead = 6;
  registerRead( DATAX0, bytesToRead, _buff); // Read from the 6 registers

  float x = (((int)_buff[1]) << 8) | _buff[0]; // 10 bit (2 bytes), LSB first; convert into an int (Analog Devices)
  float y = (((int)_buff[3]) << 8) | _buff[2];
  float z = (((int)_buff[5]) << 8) | _buff[4];

  x = (x - yOffset) / xScale;
  y = (y - yOffset) / yScale;
  z = (z - yOffset) / zScale;

  // See p. 9, 11 and 12 of https://www.nxp.com/files-static/sensors/doc/app_note/AN3461.pdf as to DOF and singularity problem
  float xRoll = (atan2(x, sqrt(y * y + z * z)) * 180.0) / PI; // Rotation around the x axis
  float yPitch = (atan2(y, (sqrt(x * x + z * z))) * 180.0) / PI; // Rotation around the y axis

  xEMA = (alphaEMA * xRoll) + ((1 - alphaEMA) * xEMA); // See https://en.wikipedia.org/wiki/Exponential_smoothing and https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
  yEMA = (alphaEMA * yPitch) + ((1 - alphaEMA) * yEMA);

  Serial.print(xEMA, 1);
  Serial.print(", ");
  Serial.println(yEMA, 1);

} // End readSensor

void registerWrite(byte address, byte val) {

  Wire.beginTransmission(DEVICE);
  Wire.write(address);
  Wire.write(val);
  Wire.endTransmission();

} // End registerWrite

void registerRead(byte address, int num, byte _buff[]) { // Reads num bytes starting from address register on device in to _buff array

  Wire.beginTransmission(DEVICE);
  Wire.write(address);
  Wire.endTransmission();
  Wire.beginTransmission(DEVICE);
  Wire.requestFrom(DEVICE, num);

  int i = 0;

  while (Wire.available())
  {

    _buff[i] = Wire.read(); // Read 1 byte
    i++;
  }

  Wire.endTransmission();

} // End registerRead
  x = (x - yOffset) / xScale;
  y = (y - yOffset) / yScale;
  z = (z - yOffset) / zScale;

Oops!

Also, one of these two equations is wrong:

  float xRoll = (atan2(x, sqrt(y * y + z * z)) * 180.0) / PI; // Rotation around the x axis
  float yPitch = (atan2(y, (sqrt(x * x + z * z))) * 180.0) / PI; // Rotation around the y axis

They are not symmetrical, because the order in which you apply the rotations matters, and you have to make a choice to make sense of the result.

Most people use the Rxyz convention, for which the correct equations are:

  float xRoll = (atan2(y, z) * 180.0) / PI; // Rotation around the x axis
  float yPitch = (atan2(-x, (sqrt(y * y + z * z))) * 180.0) / PI; // Rotation around the y axis

Thanks... dear me, I really overlooked that one; I'm too old or it's too late, probably both. I also changed the equation for roll and pitch back to what they were before.

I checked the leveling gear once again. I use quality AMF and Mitutoyo equipment to standard DIN EN ISO 3650, and I keep it in good shape as I won't be able to ever afford to buy it again. Still, the results are not any better, as the charts show. If the code is now what it should be, then what?

Are the xMin ... zMax values used in the offset and scale calculations off? But how is that possible after taking so much care in measuring thrice? But that's the only possibility, or is there anything else?

#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 byte 0
char DATAX1 = 0x33; // x axis byte 1
char DATAY0 = 0x34;
char DATAY1 = 0x35;
char DATAZ0 = 0x36;
char DATAZ1 = 0x37;

const int xMin = -268; // Readings from 2-point calibration with 6-point-tumble method (are particular to each IC) for adjustment
const int xMax =  246;
const int yMin = -261;
const int yMax =  253;
const int zMin = -258;
const int zMax =  234;

const float xOffset = (xMax + xMin) / 2.0;
const float xScale = (xMax - xMin) / 2.0;
const float yOffset = (yMax + yMin) / 2.0;
const float yScale = (yMax - yMin) / 2.0;
const float zOffset = (zMax + zMin) / 2.0;
const float zScale = (zMax - zMin) / 2.0;

const float alphaEMA = 0.2; // Smoothing factor 0 < α < 1 (smaller = slower, less responsive to changes)
float xEMA = 0;  // Initialise with a first reading (x and y in the horizontal plane)
float yEMA = 0;

void setup()

{

  Wire.begin();

  Serial.begin(57600);

  registerWrite(DATA_FORMAT, 0x00); // Set to 2g mode (typical output -256 +256 per axis as per Analog Devices)
  registerWrite(POWER_CTL, 0x08); // Start measuring

} // End setup

void loop()

{

  readSensor(); // Read from the sensor
  delay(500); // Only for readability

} // End loop

void readSensor() {

  uint8_t bytesToRead = 6;
  registerRead( DATAX0, bytesToRead, _buff); // Read from the 6 registers

  float x = (((int)_buff[1]) << 8) | _buff[0]; // 10 bit (2 bytes), LSB first; convert into an int (Analog Devices)
  float y = (((int)_buff[3]) << 8) | _buff[2];
  float z = (((int)_buff[5]) << 8) | _buff[4];

  x = (x - xOffset) / xScale;
  y = (y - yOffset) / yScale;
  z = (z - zOffset) / zScale;

  // See p. 9, 11 and 12 of https://www.nxp.com/files-static/sensors/doc/app_note/AN3461.pdf as to DOF and singularity problem
  float xRoll = (atan2(y, z) * 180.0) / PI; // Rotation around the x axis
  float yPitch = (atan2(-x, (sqrt(y * y + z * z))) * 180.0) / PI; // Rotation around the y axis

  xEMA = (alphaEMA * xRoll) + ((1 - alphaEMA) * xEMA); // See https://en.wikipedia.org/wiki/Exponential_smoothing and https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
  yEMA = (alphaEMA * yPitch) + ((1 - alphaEMA) * yEMA);

  Serial.print(xEMA, 1);
  Serial.print(", ");
  Serial.println(yEMA, 1);

} // End readSensor

void registerWrite(byte address, byte val) {

  Wire.beginTransmission(DEVICE);
  Wire.write(address);
  Wire.write(val);
  Wire.endTransmission();

} // End registerWrite

void registerRead(byte address, int num, byte _buff[]) { // Reads num bytes starting from address register on device in to _buff array

  Wire.beginTransmission(DEVICE);
  Wire.write(address);
  Wire.endTransmission();
  Wire.beginTransmission(DEVICE);
  Wire.requestFrom(DEVICE, num);

  int i = 0;

  while (Wire.available())
  {

    _buff[i] = Wire.read(); // Read 1 byte
    i++;
  }

  Wire.endTransmission();

} // End registerRead

I'm not sure what the charts are supposed to show.

However, the scaled, offset corrected X and Y acceleration values should both be zero if the calibration is correct and the sensor is level, with Z pointing up OR down.

Is that the case? If not, the calibration is wrong or the sensor is not level.

Note: to avoid the problem of leveling the sensor, you can calculate a single tilt angle between measurements taken at two arbitrary sensor orientations (A and B) as follows. This is just the dot product of (unit down vector A) and (unit down vector B):

cosab = (xA*xB + yA*yB + zA*zB)/sqrt(xA*xA + yA*yA + zA*zA)/sqrt(xB*xB + yB*yB + zB*zB);
tilt = 180.0*acos(cosab)/M_PI; //in degrees

jremington:

  1. x, y, z must be floats. The following is likely to fail if they are ints:
sqrt(y * y + z * z)

Surely that just needs fixing to use long ints internally:

sqrt((long)y * y + (long)z * z)

Pointless suggestion.

jremington:
I'm not sure what the charts are supposed to show.

The charts show in visual form that the calibration does not at all yield the desired results.

jremington:
However, the scaled, offset corrected X and Y acceleration values should both be zero if the calibration is correct and the sensor is level, with Z pointing up OR down.

Is that the case? If not, the calibration is wrong or the sensor is not level.

The sensor was put back in the exact same location. So the calibration is wrong. I did it again, even more slowly and carefully.

From the 3 x 6 x 60 readings, I selected the highest and lowest for x, y and z; see charts and code. With that, I get, in degrees...

zUp xRoll 0.8°
zUp yPitch -1.8°
yUp xRoll 90.3°
yUp yPitch -1.4°
xUp xRoll (wildly fluctuating)
xUp yPitch -88.8°

zDown xRoll -179.9°
zDown yPitch -0.3°
yDown xRoll -90.6°
yDown yPitch -1.7°
xDown xRoll (wildly fluctuating)
xDown yPitch 88.1°

...so zUp, flat on the block, is very much off, whereas zDown is close to 0.0°. These intermediate readings are fairly close...

zUP
80° rotation around y axis 79.7°
60° rotation around y axis 60.2°
30° rotation around y axis 30.1°
10° rotation around y axis 9.7°

zDown
10° rotation around y axis 10.6°
30° rotation around y axis 29.9°
60° rotation around y axis 59.8°
80° rotation around y axis 79.5°

zUP
80° rotation around x axis 80.4°
60° rotation around x axis 60.1°
30° rotation around x axis 29.8°
10° rotation around x axis 9.8°

zDown
10° rotation around x axis 171.2°
30° rotation around x axis 149.8°
60° rotation around x axis 120.7°
80° rotation around x axis 98.8°

#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 byte 0
char DATAX1 = 0x33; // x axis byte 1
char DATAY0 = 0x34;
char DATAY1 = 0x35;
char DATAZ0 = 0x36;
char DATAZ1 = 0x37;


const int xMax =  249; // Readings from 2-point calibration with 6-point-tumble method (are particular to each IC) for adjustment
const int xMin = -268;
const int yMax =  255;
const int yMin = -262;
const int zMax =  232;
const int zMin = -264;

const float xOffset = 0.5 * (xMax + xMin); // p. 8 http://www.analog.com/media/en/technical-documentation/application-notes/AN-1057.pdf
const float xGain = 0.5 * (xMax - xMin);
const float yOffset = 0.5 * (yMax + yMin);
const float yGain = 0.5 * (yMax - yMin);
const float zOffset = 0.5 * (zMax + zMin);
const float zGain = 0.5 * (zMax - zMin);

const float alphaEMA = 0.2; // Smoothing factor 0 < α < 1 (smaller = slower, less responsive to changes)
float xEMA = 0;  // Initialise with a first reading (x and y in the horizontal plane)
float yEMA = 0;

void setup()

{

  Wire.begin();

  Serial.begin(57600);

  registerWrite(DATA_FORMAT, 0x00); // Set to 2g mode (typical output -256 +256 per axis as per Analog Devices)
  registerWrite(POWER_CTL, 0x08); // Start measuring

} // End setup

void loop()

{

  readSensor(); // Read from the sensor
  delay(1000); // Only for readability

} // End loop

void readSensor() {

  uint8_t bytesToRead = 6; // Burst read (preferential as per Analog Devices)
  registerRead( DATAX0, bytesToRead, _buff); // Read from the 6 registers

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

  /*Serial.print(xRaw);
    Serial.print(", ");
    Serial.print(yRaw);
    Serial.print(", ");
    Serial.println(zRaw);*/

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

  // See p. 9, 11 and 12 of https://www.nxp.com/files-static/sensors/doc/app_note/AN3461.pdf as to DOF and singularity problem
  float xRoll = (atan2(y, z) * 180.0) / PI; // Rotation around the x axis
  float yPitch = (atan2(-x, (sqrt(y * y + z * z))) * 180.0) / PI; // Rotation around the y axis

  //xEMA = (alphaEMA * xRoll) + ((1 - alphaEMA) * xEMA); // See https://en.wikipedia.org/wiki/Exponential_smoothing and https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
  //yEMA = (alphaEMA * yPitch) + ((1 - alphaEMA) * yEMA);

  Serial.print(xRoll, 1);
  Serial.print(", ");
  Serial.println(yPitch, 1);

} // End readSensor

void registerWrite(byte address, byte val) {

  Wire.beginTransmission(DEVICE);
  Wire.write(address);
  Wire.write(val);
  Wire.endTransmission();

} // End registerWrite

void registerRead(byte address, int num, byte _buff[]) { // Reads num bytes into _buff array, starting at device address register

  Wire.beginTransmission(DEVICE);
  Wire.write(address);
  Wire.endTransmission();
  Wire.beginTransmission(DEVICE);
  Wire.requestFrom(DEVICE, num);

  int i = 0;

  while (Wire.available())
  {

    _buff[i] = Wire.read(); // Read 1 byte
    i++;
  }

  Wire.endTransmission();

} // End registerRead

The calibrated x/y/z output fluctuates by 0.01 units, but the roll and pitch fluctuates more, besides not resulting in 0°/0° still. With only the roll/pitch formulas "in between" the two outputs shown below, this is where the discrepancy occurs, or is a variable type the culprit? Would rounding/truncating the x/y/z output prior to roll and pitch calculation be advisable? Or running an EMA with x/y/z before roll/pitch calculations?

Thanks!

After talking with an Analog Devices person, I then used the intercept/slope method to no avail (same off xUp angles) and after that the average instead of maximum and minimum values, also to no avail. The values suggest that the certified leveling block is a wedge, which it is not.

I ordered a new ADXL345 breakout board and will see how it goes next week.

In the meantime, I'll relax with an Adafruit Pixie that shall change colour depending on roll and pitch, something to maybe excite my nephews with.

So, I got the new ADXL345 breakout board and did the same calibration routine. Now everything is as it should be. I can measure angles within 0.5° deviation, compared with a physical precision Mitutoyo angle meter.

It appears that the first one was faulty.

In any case, a good exercise in casual hobby metrology.