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:
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.8Illustrative 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.
/*
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.12The same two angles in MicroPython, with math.atan2 and math.degrees doing what the Arduino version does by hand.
"""
3-Axis Accelerometer - tilt angles, MicroPython 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.
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
Change SDA_PIN and SCL_PIN below for an ESP32 or a Pico.
Thonny
Run > Configure interpreter MicroPython (ESP32) or
MicroPython (Raspberry Pi Pico)
No library needed: the registers are all in this file.
"""
from machine import I2C, Pin
import time
import math
# 0x19: the board leaves the chip's SDO pin open and the chip pulls it
# high itself.
ADDR = 0x19
# GPIO numbers. ESP32-S3: 8 and 9. ESP32: 21 and 22. Pico: 4 and 5.
SDA_PIN = 8
SCL_PIN = 9
i2c = I2C(0, sda=Pin(SDA_PIN), scl=Pin(SCL_PIN), freq=100_000)
try:
found = i2c.readfrom_mem(ADDR, 0x0F, 1)[0] == 0x11 # WHO_AM_I
except OSError:
found = False
if not found:
print("no SC7A20 at 0x19: check GND, then SDA and SCL")
raise SystemExit
# It powers up asleep. CTRL_REG1: 100 readings a second, X Y Z on.
i2c.writeto_mem(ADDR, 0x20, bytes([0x57]))
# CTRL_REG4: +-2 g, and never half of one reading and half the next.
i2c.writeto_mem(ADDR, 0x23, bytes([0x80]))
def axis(lo, hi):
v = (hi << 8) | lo
if v & 0x8000:
v -= 0x10000 # two's complement
return (v >> 4) / 1000 # 12 bits at the top; 1 mg a count
def read_g():
# 0x28 is OUT_X_L; bit 7 set steps through all six registers.
b = i2c.readfrom_mem(ADDR, 0x28 | 0x80, 6)
return axis(b[0], b[1]), axis(b[2], b[3]), axis(b[4], b[5])
# 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.
while True:
x, y, z = read_g()
pitch = math.degrees(math.atan2(-x, math.sqrt(y * y + z * z)))
roll = math.degrees(math.atan2(y, z))
total = math.sqrt(x * x + y * y + z * z)
moving = " (moving)" if abs(total - 1) > 0.1 else ""
print("pitch %5.1f roll %5.1f%s" % (pitch, roll, moving))
time.sleep_ms(200)SDA_PIN and SCL_PIN are GPIO numbers: 8 and 9 on an ESP32-S3, 21 and 22 on an ESP32, 4 and 5 on a Pico. It prints five times a second; stop it with Ctrl-C.
View on GitHub · blocks/tk115-3-axis-accelerometer/micropython/accelerometer_tilt.py @ v1.12When it does not work
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.
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.
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.
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.
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
Questions about this product
See what other owners have asked, and read their solutions.
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.