Putting it to work · 09 of 11

Tilt from gravity

A board held still feels nothing but gravity, so how the 1 g is shared between X, Y and Z is its angle. Two lines of trigonometry turn three readings into pitch and roll. Two things spoil it: a zero that is not quite zero, and any movement at all.

Two angles from three numbers

Pitch is how far the header end has tipped down or up. Roll is how far the board has tipped onto its SDA edge or the other one. With X running down the board to the header and Y across it, the sketch works them out as:

float pitch = atan2(-x, sqrt(y * y + z * z)) * 180.0 / PI;
float roll  = atan2(y, z) * 180.0 / PI;

Tip the header end down 30° and X reads −0.50, Z 0.87, Y 0: pitch comes out at 30. Tip the SDA edge up and Y goes positive, and so does roll. The minus sign in front of X is there so that header-down reads as positive, and it is right only if X points the way which way is X says. If your board disagrees, that sign is the one to change.

What spoils it

The arithmetic is exact. The readings are not, and the figure shows the two ways they go wrong:

Tilt from gravity
The board is
True pitch30°
True
30°
Reported
30.0°
Total
1.00 g
Held still, the sketch gets 30.0° for a true 30°: exact, because the only thing the chip feels is the 1 g of the table's push, and its direction is the tilt. The total is 1.00 g, which is how the sketch knows it can trust the answer.

Still is the ideal: the computed angle follows the true one exactly.

Still, with an offset adds 90 mg to X, the typical zero-g error of a mounted chip. Flat, the board now reports about 5° of pitch that is not there, and the error follows it across the whole range. It does not average away, because it is not noise: it is the same every time. Subtract it once, measured level, and it is gone.

Moving adds a push along X, as a hand tipping the board would. The chip cannot tell that push from gravity, so the angle swings to one that is not there, and the total drifts away from 1 g. That is the flag the sketch prints: when the total is more than 0.1 g from 1, it adds (moving) to the line, and the angle on that line should not be trusted.

What you should see

Flat on the desk, then tipped by hand and held:

pitch   1.7  roll  -0.6
pitch  24.9  roll  -0.4  (moving)
pitch  31.2  roll  -0.8

Illustrative lines rather than a recording: yours will differ by your own board's offset, which is the first thing to measure.

The code

The first reading's three helpers, then two lines of trigonometry: pitch from X against the other two, roll from Y against Z. No library, and the same wiring as the first reading.

accelerometer_tilt.ino
/*
  3-Axis Accelerometer - tilt angles                    TK115 / /p/tk115

  Wiring. Count from the square pad, which is GND. Chip side up,
  header at the bottom, left to right:

    GND -> GND
    3V3 -> 3V3      (3.3 V only. The chip's limit is 3.6 V, and the
                     board's pull-ups put this pin on SDA and SCL.)
    SCL -> GPIO 9 on an ESP32-S3, GPIO 22 on an ESP32, GP5 on a Pico
    SDA -> GPIO 8 on an ESP32-S3, GPIO 21 on an ESP32, GP4 on a Pico

  Each board's default I2C pins, so nothing in the sketch names them.
  A 5 V Arduino Uno needs a level converter (TK97) in between.

  Arduino IDE
    Tools > Board                 your board, e.g. ESP32S3 Dev Module
    Tools > Port                  the one that appears when you plug in
    Tools > USB CDC On Boot       Enabled   (ESP32-S3 only)
    Library Manager               nothing to install, only Wire
    Serial Monitor                115200
*/

#include <Wire.h>

// 0x19, because the board leaves the chip's SDO pin open and the chip
// pulls it high itself. Tied to GND it would answer at 0x18.
const uint8_t ACCEL_ADDR = 0x19;

bool writeReg(uint8_t reg, uint8_t value) {
  Wire.beginTransmission(ACCEL_ADDR);
  Wire.write(reg);
  Wire.write(value);
  return Wire.endTransmission() == 0;
}

// Read n registers from reg on. Bit 7 set on the register number makes
// the chip step to the next register after every byte.
bool readRegs(uint8_t reg, uint8_t *buf, uint8_t n) {
  Wire.beginTransmission(ACCEL_ADDR);
  Wire.write(reg | 0x80);
  if (Wire.endTransmission(false) != 0) return false;
  if (Wire.requestFrom(ACCEL_ADDR, n) != n) return false;
  for (uint8_t i = 0; i < n; i++) buf[i] = Wire.read();
  return true;
}

// X, Y and Z in g. Low byte first; the 12 bits sit at the top of the
// 16, so shift them down, and at +-2 g every count is then 1 mg.
bool readG(float &x, float &y, float &z) {
  uint8_t b[6];
  if (!readRegs(0x28, b, 6)) return false;       // OUT_X_L .. OUT_Z_H
  x = ((int16_t)(b[1] << 8 | b[0]) >> 4) / 1000.0;
  y = ((int16_t)(b[3] << 8 | b[2]) >> 4) / 1000.0;
  z = ((int16_t)(b[5] << 8 | b[4]) >> 4) / 1000.0;
  return true;
}

void setup() {
  Serial.begin(115200);
  while (!Serial) delay(10);        // native USB: wait for the monitor
  Wire.begin();

  uint8_t id = 0;                    // WHO_AM_I: 0x11 on this chip
  if (!readRegs(0x0F, &id, 1) || id != 0x11) {
    Serial.println("no SC7A20 at 0x19: check GND, then SDA and SCL");
    while (true) delay(100);
  }

  // It powers up asleep. CTRL_REG1: 100 readings a second, X Y Z on.
  writeReg(0x20, 0x57);
  // CTRL_REG4: +-2 g, and never half of one reading and half the next.
  writeReg(0x23, 0x80);
}

void loop() {
  float x, y, z;
  if (!readG(x, y, z)) return;

  // Gravity is the only thing a still board feels, so the way 1 g is
  // shared out between the axes is the angle. X runs towards the
  // header, Y towards the SDA end, Z out of the chip side.
  float pitch = atan2(-x, sqrt(y * y + z * z)) * 180.0 / PI;
  float roll  = atan2(y, z) * 180.0 / PI;

  // Moving, it feels more than gravity and the angles mean nothing.
  float total = sqrt(x * x + y * y + z * z);

  Serial.print("pitch ");  Serial.print(pitch, 1);
  Serial.print("  roll "); Serial.print(roll, 1);
  Serial.println(fabs(total - 1.0) > 0.1 ? "  (moving)" : "");
  delay(200);
}

atan2 takes the two numbers separately rather than their ratio, so it keeps the sign and never divides by zero when the board stands on end. The total is printed as a flag and not as a number: near 1 g the angle means something, and anywhere else it does not.

View on GitHub · blocks/tk115-3-axis-accelerometer/arduino/accelerometer_tilt/accelerometer_tilt.ino @ v1.12

When it does not work

Flat on the table it says pitch 4.8, not 0.

That is the chip's zero-g offset, up to 120 mg on a mounted part, turned into degrees: 90 mg is about 5°. Lay the board level, note the pitch and roll it prints, and subtract them from every later reading. It is the same one-off calibration for every board.

The angle jumps about while I tilt it by hand.

While it moves, the chip feels your hand's push as well as gravity, and the sketch cannot tell them apart. That is what the (moving) flag is for. Read the angle when the flag is off, or average several readings, and it settles.

Roll flips from +179 to −179 upside down.

That is where the arithmetic wraps round: atan2 gives angles from −180 to +180, and upside down the board is exactly at the join. Pitch has the opposite limit and only runs from −90 to +90. For a board that tips less than about 60° either way, neither edge is reached.

Can it give me the compass heading as well?

No. Turning a level board round never moves the 1 g off Z, so nothing changes to measure. Heading needs a magnetometer, a separate sensor.

Where this goes next

The same 1 g, used the other way round: anything that is not 1 g is motion.

Counting shakes →

Edit this page — content/books/3-axis-accelerometer/tilt-from-gravity.mdx

Community

Questions about this product

See what other owners have asked, and read their solutions.

Ask a question ↗

3-Axis Accelerometer

Loading discussions…

Discuss this article

Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.

Browse Modules and blocks on the forum →