4

I'm trying to learn how to read the angles measured from an ADXL345 accelerometer (Adafruit) for a beginner project where the sensor will be attached to a short stick and I want to read the angles I'm holding/moving the stick at. I have a couple questions about some code I'm trying to understand (from: https://howtomechatronics.com/tutorials/arduino/how-to-track-orientation-with-arduino-and-adxl345-accelerometer/):

#include <Wire.h>  // Wire library - used for I2C communication

int ADXL345 = 0x53; // The ADXL345 sensor I2C address

float X_out, Y_out, Z_out;  // Outputs

void setup() {
  Serial.begin(9600); // Initiate serial communication for printing the results on the Serial monitor
  Wire.begin(); // Initiate the Wire library
  // Set ADXL345 in measuring mode
  Wire.beginTransmission(ADXL345); // Start communicating with the device 
  Wire.write(0x2D); // Access/ talk to POWER_CTL Register - 0x2D
  // Enable measurement
  Wire.write(8); // (8dec -> 0000 1000 binary) Bit D3 High for measuring enable 
  Wire.endTransmission();
  delay(10);
}

void loop() {
  // === Read acceleromter data === //
  Wire.beginTransmission(ADXL345);
  Wire.write(0x32); // Start with register 0x32 (ACCEL_XOUT_H)
  Wire.endTransmission(false);
  Wire.requestFrom(ADXL345, 6, true); // Read 6 registers total, each axis value is stored in 2 registers
  X_out = ( Wire.read()| Wire.read() << 8); // X-axis value
  X_out = X_out/256; //For a range of +-2g, we need to divide the raw values by 256, according to the datasheet
  Y_out = ( Wire.read()| Wire.read() << 8); // Y-axis value
  Y_out = Y_out/256;
  Z_out = ( Wire.read()| Wire.read() << 8); // Z-axis value
  Z_out = Z_out/256;

  Serial.print("Xa= ");
  Serial.print(X_out);
  Serial.print("   Ya= ");
  Serial.print(Y_out);
  Serial.print("   Za= ");
  Serial.println(Z_out);

My questions are regarding the section that merges the two values for each axis:

X_out = ( Wire.read()| Wire.read() << 8); // X-axis value
      X_out = X_out/256; //For a range of +-2g, we need to divide the raw values by 256, according to the datasheet
  1. I understand that there are two values for each axis, but how does this expression merge them correctly? How would you know how to merge them as I can't seem to find an explanation anywhere as to why there are two values and what each represents?
  2. I've also seen other articles (e.g: https://morf.lv/mems-part-1-guide-to-using-accelerometer-adxl345) that talk about how you need to choose a resolution and range for the sensor, and those will correlate to a number in mG/LSB? So what does all this mean and how would I go about choosing them?
toolic
  • 5,637
  • 5
  • 20
  • 33
  • 1
    Datas values (X,Y,Z) are 8 bits in 6 registers. They can be read as "left justified" datas or "right justified". So "X_out = ( Wire.read() | Wire.read() << 8); // X-axis value" are ok for 16 bits data then formatted to floating. But dividing by 256 is not correct. 10 bits (+/- 2g scale) are used so must divide by 32, if left justified? Only for upper rates ! – Antonio51 Apr 07 '22 at 09:32
  • 1
    See datasheet page 32. – Antonio51 Apr 07 '22 at 09:37
  • 1
    `(Wire.read()|Wire.read()<<8)` is not good practice because the compiler is allowed to put the two calls any way it wants (it doesn't have to be left-to-right) – user253751 Apr 07 '22 at 19:09
  • @J... Either `Wire.read()` or `Wire.read()<<8` could be evaluated first. – user253751 Apr 07 '22 at 21:20

3 Answers3

3
  1. The 16-bit values are transferred as 8-bit bytes so one of the bytes contains the high 8 bits and the other contains the low 8 bits.

Just like us humans understand the number 42 as containing a digit of 4 that represents tens and a digit of 2 that represents ones, we get the value 42 by taking 10×4 + 1×2, or just by concatenating the 4 with 2.

This concatenation is exactly the same with binary digits and bytes, the byte that contains the high 8 bits is moved 8 bits left so the low 8 bits can be added to the final value. Shifting left by 8 bits is same as multiplying with 256.

Since floating point values are used and the value of 256 read from the chip equals 1.0, it needs to be divided by 256.

  1. You have 16 bits to represent a value. If you want it to represent a large value between say +/- 16, you need more bits for the integer part, so less bits are left for the fractional part, so there is less resolution as the steps are larger. If you only want to represent a small value between +/- 2, you need less bits for the integer part, and have more bits left for the fractional part.

Basically again same than having say 4 digits to represent a number, and you can select where your decimal point will be. So you can have a large number range between 000.0 and 999.9 with steps of 0.1, or you can have small number range between 0.000 and 9.999 with steps of 0.001 which means more resolution.

Justme
  • 127,425
  • 3
  • 97
  • 261
  • > Since floating-point values are used and the value of 256 read from the chip equals 1.0, it needs to be divided by 256. Don't understand, please. p27: "The output data is twos complement, with DATAx0 as the least significant byte and DATAx1 as the most significant byte, where x represents X, Y, or Z." So data read are "integer"? – Antonio51 Apr 07 '22 at 10:41
  • The data is just 16 bits that represent a value of some range and precision with some coding, and it is read from the chip into two separate bytes. If you want you can think of it as "integer" of 16 bits. If so, then the coding is such that the "decimal point" is between the 8 integer and 8 fractional bits which you need to "decode" if you want a floating point number. – Justme Apr 07 '22 at 11:16
  • ok ... I found this ... > "full resolution mode with a sensitivity of typically 256 LSB/g." – Antonio51 Apr 07 '22 at 11:33
  • @Justme thank you so much for taking the time to answer this in such detail! I understand it a lot better now. Could you just explain why shifting left is the same as multiplying by 256 (not really clear on what shifting means)? Also for this project would you recommend I use a smaller resolution and range as the sensor probably wouldn't feel high accelerations here? –  Apr 07 '22 at 11:35
  • 1
    Shifting the decimal point by one digot right or left in decimal base 10 numbers will multiply or divide by 10. Similarly, with binary base 2 numbers, shifting the binary equivalent of "decimal point" by right or left by one binary digit will multiply or divide by 2. This information should be well covered by beginner tutorials on programming and binary numbers or Wikipedia article on bit shifts as bitwise operation. – Justme Apr 07 '22 at 11:41
  • Ahh I get it, thank u! –  Apr 07 '22 at 17:52
  • 1
    @Display_n4me `shifting left is the same as multiplying by 256` ... not quite accurate ... it is `shifting left eight times is the same as multiplying by 256` – jsotola Apr 07 '22 at 17:54
  • So do you know why they've used the '|' in Wire.read()| Wire.read() << 8 ? I can't find any other code for the sensor that uses this –  Apr 07 '22 at 17:59
  • @Display_n4me I don't know why the programmer chose to do a bitwise OR, as any method that ends up in the same result like simple addition can be used. If you are asking what '|' means, it's just bitwise OR. – Justme Apr 07 '22 at 18:05
  • Na, I know it means or, i'm just wondering how it works? Surely an or statement would only count one of the values? And why would the or statement or simple addition concatenate the two values once its been shifted left –  Apr 07 '22 at 18:11
  • Same way you can get 42 by taking a 4, shifting it left by one digit so it's 40, and then summing 2 to it. I don't get how you think it works so I can't say what exactly is incorrect with it. – Justme Apr 07 '22 at 18:22
  • I get that, i just dont see how an or statement is doing that –  Apr 07 '22 at 20:13
  • 2
    @Display_n4me The first byte of 8 bits has the 8 bits moved left 8 times to have them in the 8 high bits of a 16-bit variable. Thus low 8 bits in the variable are now all zeroes. The second byte of 8 bits can be just ORed to the low 8 bits of the 16-bit variable. – Justme Apr 07 '22 at 21:15
  • @Justme Honestly, thank you so much for all your time today. I get it now, and that last comment led me towards a video that explained concatenation in more depth. –  Apr 08 '22 at 01:10
3

I've redrawn Figures 49 and 50 from the ADXL345 datasheet to make it clearer.

Right-justified data format

Figure 49a – Right-justified data format in full-resolution mode.

Figure 49b – Right-justified data format in 10-bit mode.

C++ code examples to merge right-justified registers and convert to floating point. Sign-extension is provided by the ADXL345 hardware:

int16_t right_justified_value_2g = DATAx1 << 8 | DATAx0;

int16_t right_justified_value_4g = DATAx1 << 8 | DATAx0;

int16_t right_justified_value_8g = DATAx1 << 8 | DATAx0;

int16_t right_justified_value_16g = DATAx1 << 8 | DATAx0;

float f2g = right_justified_value_2g / 256.0f;
float f4g = right_justified_value_4g / 256.0f;
float f8g = right_justified_value_8g / 256.0f;
float f16g = right_justified_value_16g / 256.0f;

Left-justified data format

Figure 50a – Left-justified data format in full-resolution mode.

Figure 50a – Left-justified data format in 10-bit mode.

C++ code examples to merge left-justified registers using sign extension and convert to floating point:

int16_t left_justified_value_2g = int16_t(DATAx1 << 8 | DATAx0) >> 6;

int16_t left_justified_value_4g = int16_t(DATAx1 << 8 | DATAx0) >> 5;

int16_t left_justified_value_8g = int16_t(DATAx1 << 8 | DATAx0) >> 4;

int16_t left_justified_value_16g = int16_t(DATAx1 << 8 | DATAx0) >> 3;

float f2g = left_justified_value_2g / 256.0f;
float f4g = left_justified_value_4g / 256.0f;
float f8g = left_justified_value_8g / 256.0f;
float f16g = left_justified_value_16g / 256.0f;

Converting from fixed-point binary to floating point

surely nothing is happening to the lowest significant bits

why do we divide the entire number rather than just the most significant byte?

Step 1: Consider the ADXL345 measuring a value of 1.5g. This is 0000 0001 1000 0000 binary in the register pair of DATAx1 and DATAx0. This binary format is called fixed-point binary with 8 binary places.

Step 2: The ADXL sends the value one byte at a time: 000 0001 (1 decimal) and 1000 0000 (128 decimal). Notice how the least significant byte has a value of 256 x 0.5, so surely something is happening to the lowest significant bits.

Step 3: The MCU shifts the most significant byte to the left by 8 bits before ORing it with the least significant byte. The composite 16-bit integer value now contains 0000 0001 1000 0000 as before. We know where the binary point is but the MCU doesn't. It regards it as an integer with no binary point and a value of 384 (1.5 x 256).

Step 4: We now have a situation where both bytes have been multiplied by 256 (shifted left by 8 bits), so when converting to floating point the entire value needs to be scaled down by 256. Note that the divisor depends on the bit mode of the ADXL345.

tim
  • 850
  • 6
  • 13
  • Wow, thank you so much! Can I just ask why we're dividing the concatenated version by 256? I understand that the most significant bits are multiplied by256 during the concatenation, but surely nothing is happening to the lowest significant bits, so why do we divide the entire number rather than just the msb? –  Apr 08 '22 at 01:41
  • 1
    Hi @Display_n4me, I've added a section to my answer which shows that something does happen to the least significant byte, see Step 2, which effectively multiplies it by 256. Also beware that the various bit modes of the ADXL345 will affect the value of the divisor, see updated Figures 49 and 50. – tim Apr 08 '22 at 13:50
  • Man, I really can't thank you enough. This was all so helpful –  Apr 09 '22 at 19:47
1

Here is "how" are the data read ... in this library

void ADXL345::readXYZ(int *x, int *y, int *z)
{ readFrom(ADXL345_DATAX0, ADXL345_TO_READ, _buff);
//read the acceleration data from the ADXL345
*x = (short)((((unsigned short)_buff1) << 8) | _buff[0]);
*y = (short)((((unsigned short)_buff[3]) << 8) | _buff[2]);
*z = (short)((((unsigned short)_buff[5]) << 8) | _buff[4]);
}

"full resolution mode with a sensitivity of typically 256 LSB/g."

If data "right adjusted", dividing by 256 give "g" in real value (1 g = 0x100)

Antonio51
  • 11,004
  • 1
  • 7
  • 20