Projects / Navigation

Digital Compass & Inclinometer

Tilt-compensated heading and an angle gauge on a bright LED display.

Intermediate⏱ 2 hours5 ElectroTrical boards

Overview

A magnetometer alone only gives a correct heading when it's held perfectly flat. Combining the ET-S64 QMC5883L compass with the ET-S63 MPU-6500 accelerometer corrects for tilt, so the heading stays right when you hold it at an angle.

The same accelerometer turns it into an inclinometer for setting up shelves, solar panels or antennas. The ET-A20 button pad switches modes and runs calibration, and the ET-D01 display shows the result.

  • Tilt-compensated compass heading (0–359°)
  • Pitch and roll angle gauge
  • Hard-iron calibration saved in flash
  • Large 4-digit LED readout

What you need

ElectroTrical boards

ET-M02ESP32-S3 Dev BoardController
Coming soon
ET-S64QMC5883L Compass3-axis magnetometer
Coming soon
ET-S63MPU-6500 6-DoF IMUAccelerometer for tilt
Coming soon
ET-A204-Button PadMode and calibration buttons
Coming soon
ET-D01HT16K33 4-Digit 7-Segment DisplayReadout
Coming soon

You'll also need

  • 3 Qwiic cables
  • Jumper wires
  • USB power bank or USB-C supply
  • A rigid base to mount the sensors flat and aligned

Wiring

Addresses: QMC5883L 0x0D, MPU-6500 0x69, HT16K33 0x70. Mount the ET-S64 and ET-S63 flat, side by side, with their printed X arrows pointing the same way.

Qwiic chain

FromToNote
ET-M02 QwiicET-S64 Qwiic
ET-S64 QwiicET-S63 Qwiic
ET-S63 QwiicET-D01 Qwiic

ET-A20 → ET-M02

FromToNote
3V3VIN
GNDGND
IO4B1Mode: heading / pitch / roll
IO5B2Hold 2 s: calibrate
IO6B3Zero the angle

Build it

  1. 01

    Mount the sensors

    Fix both sensor boards flat on the same base with their X arrows aligned and forward. Keep them at least 10 cm from the USB power bank, magnets, speakers and steel screws.

  2. 02

    Set up the Arduino IDE

    Install the Espressif ESP32 package, select “ESP32S3 Dev Module” and set USB CDC On Boot to Enabled. Install the Adafruit LED Backpack library.

  3. 03

    Calibrate

    Hold B2 for 2 seconds; the display shows “CAL”. Slowly rotate the device through every direction — a figure-eight, then roll it over — for 20 seconds. The calibration is saved.

  4. 04

    Use the compass

    In heading mode the display shows degrees from magnetic north. Add your local declination (about +2° in the UAE) for true north.

  5. 05

    Use the inclinometer

    Press B1 for pitch, again for roll. Place it on a reference surface and press B3 to zero, then measure relative angles.

Arduino code

Written for the ET-M02 ESP32-S3 Dev Board. Install these libraries from the Arduino Library Manager:

  • Adafruit LED Backpack Library
  • Adafruit GFX Library
compass_inclinometer.ino
#include <Wire.h>
#include <Preferences.h>
#include <Adafruit_LEDBackpack.h>

const uint8_t QMC = 0x0D, MPU = 0x69;
const int B1_PIN = 4, B2_PIN = 5, B3_PIN = 6;
const float DECLINATION = 2.0;           // degrees, UAE ≈ +2

Adafruit_7segment disp;
Preferences prefs;
float offX, offY, offZ, zeroPitch = 0, zeroRoll = 0;
int mode = 0;                            // 0 heading, 1 pitch, 2 roll

void w8(uint8_t a, uint8_t r, uint8_t v) { Wire.beginTransmission(a); Wire.write(r); Wire.write(v); Wire.endTransmission(); }
void rd(uint8_t a, uint8_t r, uint8_t* d, int n) {
  Wire.beginTransmission(a); Wire.write(r); Wire.endTransmission(false);
  Wire.requestFrom(a, (uint8_t)n); for (int i = 0; i < n; i++) d[i] = Wire.read();
}
void readMag(float& x, float& y, float& z) {
  uint8_t d[6]; rd(QMC, 0x00, d, 6);    // little-endian X, Y, Z
  x = (int16_t)(d[0] | d[1] << 8); y = (int16_t)(d[2] | d[3] << 8); z = (int16_t)(d[4] | d[5] << 8);
}
void readAcc(float& x, float& y, float& z) {
  uint8_t d[6]; rd(MPU, 0x3B, d, 6);    // big-endian X, Y, Z
  x = (int16_t)(d[0] << 8 | d[1]); y = (int16_t)(d[2] << 8 | d[3]); z = (int16_t)(d[4] << 8 | d[5]);
}

void calibrate() {
  disp.println("CAL"); disp.writeDisplay();
  float mn[3] = {1e6, 1e6, 1e6}, mx[3] = {-1e6, -1e6, -1e6}, v[3];
  unsigned long t0 = millis();
  while (millis() - t0 < 20000) {
    readMag(v[0], v[1], v[2]);
    for (int i = 0; i < 3; i++) { mn[i] = min(mn[i], v[i]); mx[i] = max(mx[i], v[i]); }
    delay(20);
  }
  offX = (mn[0] + mx[0]) / 2; offY = (mn[1] + mx[1]) / 2; offZ = (mn[2] + mx[2]) / 2;
  prefs.putFloat("x", offX); prefs.putFloat("y", offY); prefs.putFloat("z", offZ);
}

void setup() {
  Serial.begin(115200);
  Wire.begin(8, 9);
  pinMode(B1_PIN, INPUT_PULLUP); pinMode(B2_PIN, INPUT_PULLUP); pinMode(B3_PIN, INPUT_PULLUP);

  w8(QMC, 0x0B, 0x01);                   // set/reset period
  w8(QMC, 0x09, 0x1D);                   // continuous, 200 Hz, 8 G, OSR 512
  w8(MPU, 0x6B, 0x00);                   // wake MPU-6500
  disp.begin(0x70);

  prefs.begin("compass");
  offX = prefs.getFloat("x", 0); offY = prefs.getFloat("y", 0); offZ = prefs.getFloat("z", 0);
}

void loop() {
  // buttons
  static bool last1 = HIGH; bool b1 = digitalRead(B1_PIN);
  if (b1 == LOW && last1 == HIGH) mode = (mode + 1) % 3;
  last1 = b1;
  static unsigned long b2Down = 0;
  if (digitalRead(B2_PIN) == LOW) { if (!b2Down) b2Down = millis(); else if (millis() - b2Down > 2000) { calibrate(); b2Down = 0; } }
  else b2Down = 0;

  float ax, ay, az, mx, my, mz;
  readAcc(ax, ay, az);
  readMag(mx, my, mz);
  mx -= offX; my -= offY; mz -= offZ;

  float roll  = atan2(ay, az);
  float pitch = atan2(-ax, sqrt(ay * ay + az * az));
  if (digitalRead(B3_PIN) == LOW) { zeroPitch = pitch; zeroRoll = roll; }

  // tilt-compensated heading
  float xh = mx * cos(pitch) + mz * sin(pitch);
  float yh = mx * sin(roll) * sin(pitch) + my * cos(roll) - mz * sin(roll) * cos(pitch);
  float heading = atan2(-yh, xh) * RAD_TO_DEG + DECLINATION;
  if (heading < 0) heading += 360;
  if (heading >= 360) heading -= 360;

  float value = mode == 0 ? heading
              : mode == 1 ? (pitch - zeroPitch) * RAD_TO_DEG
                          : (roll - zeroRoll) * RAD_TO_DEG;
  if (mode == 0) disp.print((int)heading, DEC);
  else disp.printFloat(value, 1);
  disp.writeDisplay();

  Serial.printf("heading %.0f  pitch %.1f  roll %.1f\n", heading,
                (pitch - zeroPitch) * RAD_TO_DEG, (roll - zeroRoll) * RAD_TO_DEG);
  delay(100);
}

Troubleshooting

Heading turns the wrong way

The two boards aren't aligned. Make sure both X arrows point forward; if one board is rotated, change the sign of the matching axis in readMag().

Heading changes when you tilt it

Recalibrate away from metal, and include rolling the device over during calibration, not just turning it flat.

Reading jumps near the power bank

Batteries and USB cables carry current that disturbs the field. Keep them at least 10 cm from the ET-S64.

Take it further

  • Add the ET-S41 GNSS receiver for course and position
  • Show a level bubble on an ET-D08 8×8 LED matrix
  • Log heading and tilt for vehicle or boat orientation

Turning this into a product?

We design custom boards that combine these sensors on one PCB, with firmware and production.